Fluid Protocol
Fluid is a DeFi protocol by Instadapp offering lending and vault products. All data reads use on-chain resolver contracts — no API dependency, no keys required.
On-chain first: Every read operation goes through resolver contracts deployed at the same address on all chains.
Skill Files
| File | URL | Status |
|---|---|---|
| SKILL.md (this file) | https://fluid.io/skill.md |
✅ Live |
| Deployments | deployments.md on GitHub | ✅ Live |
| Integration Docs | https://fluid-integration-docs.vercel.app/ |
✅ Live |
Fluid: Lending Protocol
How fTokens Work (ERC-4626)
Fluid lending tokens (fUSDC, fWETH, etc.) follow the ERC-4626 tokenized vault standard. This is important to understand:
Shares are NOT 1:1 with underlying tokens. When you deposit USDC, you receive fUSDC shares. The exchange rate between shares and assets increases over time as interest accrues.
Day 0: deposit 1000 USDC → receive 1000 fUSDC shares (rate: 1.000)
Day 90: your 1000 fUSDC shares → now worth 1010 USDC (rate: 1.010)
| Concept | Description |
|---|---|
| Assets | The underlying token (USDC, WETH, etc.) |
| Shares | The fToken balance you hold (fUSDC, fWETH, etc.) |
| Exchange Rate | Increases over time. convertToAssets(shares) gives current value. |
deposit(assets, receiver) |
Deposit underlying → receive shares. Amount is in assets (e.g., USDC). |
withdraw(assets, receiver, owner) |
Specify underlying amount to withdraw. Burns the required shares. |
redeem(shares, receiver, owner) |
Specify shares to burn. Receive the equivalent underlying. |
mint(shares, receiver) |
Specify exact shares you want. Deposit the required assets. |
Common mistake: Don't assume
shares == assets. Always useconvertToAssets()orconvertToShares()to convert between them. The exchange rate at genesis is approximately 1:1 but diverges over time.
// Check how much your shares are worth
const shares = await fUsdc.read.balanceOf([userAddress]);
const currentValue = await fUsdc.read.convertToAssets([shares]);
console.log(`${shares} fUSDC shares = ${formatUnits(currentValue, 6)} USDC`);
Quick Start
No registration or API keys needed. Just call the on-chain resolver.
Step 1: Pick your chain and RPC
| Network | Chain ID | LendingResolver Address | Public RPC |
|---|---|---|---|
| Ethereum | 1 | 0x48D32f49aFeAEC7AE66ad7B9264f446fc11a1569 |
https://eth.llamarpc.com |
| Arbitrum | 42161 | 0x48D32f49aFeAEC7AE66ad7B9264f446fc11a1569 |
https://arb1.arbitrum.io/rpc |
| Base | 8453 | 0x48D32f49aFeAEC7AE66ad7B9264f446fc11a1569 |
https://mainnet.base.org |
| Polygon | 137 | 0x48D32f49aFeAEC7AE66ad7B9264f446fc11a1569 |
https://polygon-rpc.com |
| Plasma | 9745 | 0x48D32f49aFeAEC7AE66ad7B9264f446fc11a1569 |
https://rpc.plasma.to |
Same resolver address on all chains (CREATE2 deployment). Any RPC provider works (Alchemy, Infura, QuickNode, etc.).
Step 2: Query fToken data
# Get all fToken addresses on the chain
cast call 0x48D32f49aFeAEC7AE66ad7B9264f446fc11a1569 \
"getAllFTokens()(address[])" \
--rpc-url https://eth.llamarpc.com
# Get complete data for all fTokens (APY, TVL, rates)
cast call 0x48D32f49aFeAEC7AE66ad7B9264f446fc11a1569 \
"getFTokensEntireData()" \
--rpc-url https://eth.llamarpc.com
Step 3: Check a user position
# Get user position for fUSDC
cast call 0x48D32f49aFeAEC7AE66ad7B9264f446fc11a1569 \
"getUserPosition(address,address)" \
0x9Fb7b4477576Fe5B32be4C1843aFB1e55F251B33 \
0xYOUR_ADDRESS \
--rpc-url https://eth.llamarpc.com
# Get all user positions across every fToken
cast call 0x48D32f49aFeAEC7AE66ad7B9264f446fc11a1569 \
"getUserPositions(address)" \
0xYOUR_ADDRESS \
--rpc-url https://eth.llamarpc.com
Step 4: Deposit or withdraw
# Deposit 1000 USDC into fUSDC (requires prior ERC-20 approval)
cast send 0x9Fb7b4477576Fe5B32be4C1843aFB1e55F251B33 \
"deposit(uint256,address)" \
1000000000 0xYOUR_ADDRESS \
--rpc-url https://eth.llamarpc.com \
--private-key $PRIVATE_KEY
# Withdraw 500 USDC from fUSDC
cast send 0x9Fb7b4477576Fe5B32be4C1843aFB1e55F251B33 \
"withdraw(uint256,address,address)" \
500000000 0xYOUR_ADDRESS 0xYOUR_ADDRESS \
--rpc-url https://eth.llamarpc.com \
--private-key $PRIVATE_KEY
# Deposit native ETH into fWETH (payable)
cast send 0x90551c1795392094FE6D29B758EcCD233cFAa260 \
"depositNative(address)" \
0xYOUR_ADDRESS \
--value 1ether \
--rpc-url https://eth.llamarpc.com \
--private-key $PRIVATE_KEY
Everything You Can Do
| Action | Method | Contract |
|---|---|---|
| Get all fToken addresses | getAllFTokens() |
LendingResolver |
| Get all fToken data (APY, TVL) | getFTokensEntireData() |
LendingResolver |
| Get single fToken details | getFTokenDetails(fToken) |
LendingResolver |
| Get user position (one fToken) | getUserPosition(fToken, user) |
LendingResolver |
| Get user positions (all fTokens) | getUserPositions(user) |
LendingResolver |
| Preview deposit/mint/withdraw/redeem | getPreviews(fToken, assets, shares) |
LendingResolver |
| Deposit ERC-20 assets | deposit(assets, receiver) |
fToken |
| Mint exact shares | mint(shares, receiver) |
fToken |
| Withdraw assets | withdraw(assets, receiver, owner) |
fToken |
| Redeem shares | redeem(shares, receiver, owner) |
fToken |
| Deposit native ETH | depositNative(receiver) |
fToken (payable) |
| Withdraw native ETH | withdrawNative(assets, receiver, owner) |
fToken |
| Check max deposit/withdraw | maxDeposit(receiver) / maxWithdraw(owner) |
fToken |
| Check exchange rate | convertToAssets(shares) / convertToShares(assets) |
fToken |
Contract Addresses (All Verified)
All contracts are verified on their respective block explorers. Click any link to read the source code directly.
LendingResolver (read positions, APY, TVL)
Address: 0x48D32f49aFeAEC7AE66ad7B9264f446fc11a1569 — same on all chains (CREATE2). Verified.
| Network | Explorer (verified source) |
|---|---|
| Ethereum | Etherscan |
| Arbitrum | Arbiscan |
| Base | Basescan |
| Polygon | Polygonscan |
| Plasma | Plasmascan |
LendingFactory
Address: 0x54B91A0D94cb471F37f949c60F7Fa7935b551D03 — same on all chains (CREATE2). Verified.
| Network | Explorer (verified source) |
|---|---|
| Ethereum | Etherscan |
| Arbitrum | Arbiscan |
| Base | Basescan |
| Polygon | Polygonscan |
| Plasma | Plasmascan |
Source: deployments.md
fTokens (Ethereum Mainnet)
| Token | fToken Address | Underlying |
|---|---|---|
| fUSDC | 0x9Fb7b4477576Fe5B32be4C1843aFB1e55F251B33 |
USDC |
| fUSDT | 0x5C20B550819128074FD538Edf79791733ccEdd18 |
USDT |
| fWETH | 0x90551c1795392094FE6D29B758EcCD233cFAa260 |
WETH (native) |
| fGHO | Check resolver | GHO |
| fwstETH | Check resolver | wstETH |
Use
getAllFTokens()on LendingResolver to get the complete, up-to-date list for any network.
Understanding Rates
The resolver returns rates in basis points (1 bp = 0.01%, so 10000 = 100%):
supplyRate— interest from borrowers at the Liquidity Layer (e.g.,390= 3.90%)rewardsRate— additional native rewards, if active (e.g.,149= 1.49%)- Total APR =
supplyRate+rewardsRate
const supplyApr = Number(result.supplyRate) / 100; // 390 → 3.90%
const rewardsApr = Number(result.rewardsRate) / 100; // 149 → 1.49%
const totalApr = supplyApr + rewardsApr; // 5.39%
Code Examples (viem/Node.js)
Deposit USDC
const usdcAddress = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
const fUsdcAddress = '0x9Fb7b4477576Fe5B32be4C1843aFB1e55F251B33';
const amount = 1000n * 10n ** 6n; // 1000 USDC
// 1. Approve fUSDC to spend USDC
await usdc.write.approve([fUsdcAddress, amount]);
// 2. Deposit USDC → receive fUSDC shares
const shares = await fUsdc.write.deposit([amount, userAddress]);
Deposit Native ETH
const fWethAddress = '0x90551c1795392094FE6D29B758EcCD233cFAa260';
const amount = parseEther('1'); // 1 ETH
// Deposit ETH directly (payable)
const shares = await fWeth.write.depositNative([userAddress], { value: amount });
Withdraw Assets
// Withdraw specific amount of underlying
const assetsToWithdraw = 500n * 10n ** 6n; // 500 USDC
await fUsdc.write.withdraw([assetsToWithdraw, userAddress, userAddress]);
// OR redeem all fToken shares
const shares = await fUsdc.read.balanceOf([userAddress]);
const maxRedeemable = await fUsdc.read.maxRedeem([userAddress]);
await fUsdc.write.redeem([Math.min(shares, maxRedeemable), userAddress, userAddress]);
Full Example: Check APY + User Position
import { createPublicClient, http, formatUnits } from 'viem';
import { mainnet } from 'viem/chains';
const LENDING_RESOLVER = '0x48D32f49aFeAEC7AE66ad7B9264f446fc11a1569';
const F_USDC = '0x9Fb7b4477576Fe5B32be4C1843aFB1e55F251B33';
const resolverAbi = [
{
name: 'getFTokenDetails',
type: 'function',
stateMutability: 'view',
inputs: [{ name: 'fToken_', type: 'address' }],
outputs: [
{
type: 'tuple',
components: [
{ name: 'tokenAddress', type: 'address' },
{ name: 'eip2612Deposits', type: 'bool' },
{ name: 'isNativeUnderlying', type: 'bool' },
{ name: 'name', type: 'string' },
{ name: 'symbol', type: 'string' },
{ name: 'decimals', type: 'uint256' },
{ name: 'asset', type: 'address' },
{ name: 'totalAssets', type: 'uint256' },
{ name: 'totalSupply', type: 'uint256' },
{ name: 'convertToShares', type: 'uint256' },
{ name: 'convertToAssets', type: 'uint256' },
{ name: 'rewardsRate', type: 'uint256' },
{ name: 'supplyRate', type: 'uint256' },
{ name: 'rebalanceDifference', type: 'int256' },
{ name: 'liquidityUserSupplyData', type: 'tuple', components: [] },
],
},
],
},
{
name: 'getUserPosition',
type: 'function',
stateMutability: 'view',
inputs: [
{ name: 'fToken_', type: 'address' },
{ name: 'user_', type: 'address' },
],
outputs: [
{
type: 'tuple',
components: [
{ name: 'fTokenShares', type: 'uint256' },
{ name: 'underlyingAssets', type: 'uint256' },
{ name: 'underlyingBalance', type: 'uint256' },
{ name: 'allowance', type: 'uint256' },
],
},
],
},
];
async function main() {
const client = createPublicClient({
chain: mainnet,
transport: http(process.env.ETH_RPC_URL),
});
// 1. Get fUSDC details
const details = await client.readContract({
address: LENDING_RESOLVER,
abi: resolverAbi,
functionName: 'getFTokenDetails',
args: [F_USDC],
});
const supplyApr = Number(details.supplyRate) / 100;
const rewardsApr = Number(details.rewardsRate) / 100;
const tvl = formatUnits(details.totalAssets, 6);
console.log(
`fUSDC — Supply APR: ${supplyApr.toFixed(2)}%, Rewards: ${rewardsApr.toFixed(2)}%, TVL: ${Number(tvl).toLocaleString()} USDC`,
);
// 2. Check user position
const userAddress = '0xYourAddress';
const position = await client.readContract({
address: LENDING_RESOLVER,
abi: resolverAbi,
functionName: 'getUserPosition',
args: [F_USDC, userAddress],
});
console.log(
`Position — Shares: ${formatUnits(position.fTokenShares, 6)}, Underlying: ${formatUnits(position.underlyingAssets, 6)} USDC`,
);
}
main().catch(console.error);
Common Patterns
Check if deposit will succeed
const minDeposit = await fToken.minDeposit();
const maxDeposit = await fToken.maxDeposit(receiver);
if (amount < minDeposit) throw new Error('Amount below minimum');
if (amount > maxDeposit) throw new Error('Amount exceeds maximum');
const expectedShares = await fToken.previewDeposit(amount);
Withdraw max
const maxWithdrawable = await fToken.maxWithdraw(userAddress);
await fToken.withdraw(maxWithdrawable, userAddress, userAddress);
Monitor yield earned (using blockTag)
Compare exchange rates at deposit time vs now to calculate exact yield:
const depositBlockNumber = 19500000n; // from tx receipt
const userShares = await fToken.read.balanceOf([userAddress]);
// Value at deposit time
const valueAtDeposit = await client.readContract({
address: F_USDC,
abi: fTokenAbi,
functionName: 'convertToAssets',
args: [userShares],
blockNumber: depositBlockNumber,
});
// Value now
const valueNow = await client.readContract({
address: F_USDC,
abi: fTokenAbi,
functionName: 'convertToAssets',
args: [userShares],
blockTag: 'latest',
});
const earned = valueNow - valueAtDeposit;
console.log(`Yield earned: ${formatUnits(earned, 6)} USDC`);
With cast (CLI):
# Value at deposit block
cast call 0x9Fb7b4477576Fe5B32be4C1843aFB1e55F251B33 \
"convertToAssets(uint256)(uint256)" $USER_SHARES \
--rpc-url $ETH_RPC_URL --block 19500000
# Value now
cast call 0x9Fb7b4477576Fe5B32be4C1843aFB1e55F251B33 \
"convertToAssets(uint256)(uint256)" $USER_SHARES \
--rpc-url $ETH_RPC_URL
Tip: Store the deposit block number from
receipt.blockNumberfor later yield calculations.
Permit2 Deposits
For tokens that don't support EIP-2612, use Permit2 for gasless approvals:
const details = await resolver.getFTokenDetails(fToken);
if (details.eip2612Deposits) {
// Use standard EIP-2612 permit
} else {
// Use Permit2 signature
await fToken.depositWithSignature(assets, receiver, minAmountOut, permit, signature);
}
Key Concepts
fToken Exchange Rate
fTokens appreciate over time as interest accrues. The exchange rate between shares and underlying assets increases continuously. Use convertToAssets(shares) and convertToShares(assets) to convert between them.
Withdrawal Limits
Fluid has withdrawal limits that expand over time to protect against bank runs. Always check maxWithdraw(owner) before withdrawing large amounts. The liquidityUserSupplyData in FTokenDetails exposes the limit parameters: withdrawalLimit, expandPercent, expandDuration, and baseWithdrawalLimit.
ABIs
LendingResolver ABI
[
{
"name": "getAllFTokens",
"type": "function",
"stateMutability": "view",
"inputs": [],
"outputs": [{ "type": "address[]" }]
},
{
"name": "getFTokensEntireData",
"type": "function",
"stateMutability": "view",
"inputs": [],
"outputs": [
{
"type": "tuple[]",
"components": [
{ "name": "tokenAddress", "type": "address" },
{ "name": "eip2612Deposits", "type": "bool" },
{ "name": "isNativeUnderlying", "type": "bool" },
{ "name": "name", "type": "string" },
{ "name": "symbol", "type": "string" },
{ "name": "decimals", "type": "uint256" },
{ "name": "asset", "type": "address" },
{ "name": "totalAssets", "type": "uint256" },
{ "name": "totalSupply", "type": "uint256" },
{ "name": "convertToShares", "type": "uint256" },
{ "name": "convertToAssets", "type": "uint256" },
{ "name": "rewardsRate", "type": "uint256" },
{ "name": "supplyRate", "type": "uint256" },
{ "name": "rebalanceDifference", "type": "int256" },
{
"name": "liquidityUserSupplyData",
"type": "tuple",
"components": [
{ "name": "isAllowed", "type": "bool" },
{ "name": "supply", "type": "uint256" },
{ "name": "withdrawalLimit", "type": "uint256" },
{ "name": "lastUpdateTimestamp", "type": "uint256" },
{ "name": "expandPercent", "type": "uint256" },
{ "name": "expandDuration", "type": "uint256" },
{ "name": "baseWithdrawalLimit", "type": "uint256" }
]
}
]
}
]
},
{
"name": "getFTokenDetails",
"type": "function",
"stateMutability": "view",
"inputs": [{ "name": "fToken_", "type": "address" }],
"outputs": [
{
"type": "tuple",
"components": [
{ "name": "tokenAddress", "type": "address" },
{ "name": "eip2612Deposits", "type": "bool" },
{ "name": "isNativeUnderlying", "type": "bool" },
{ "name": "name", "type": "string" },
{ "name": "symbol", "type": "string" },
{ "name": "decimals", "type": "uint256" },
{ "name": "asset", "type": "address" },
{ "name": "totalAssets", "type": "uint256" },
{ "name": "totalSupply", "type": "uint256" },
{ "name": "convertToShares", "type": "uint256" },
{ "name": "convertToAssets", "type": "uint256" },
{ "name": "rewardsRate", "type": "uint256" },
{ "name": "supplyRate", "type": "uint256" },
{ "name": "rebalanceDifference", "type": "int256" },
{
"name": "liquidityUserSupplyData",
"type": "tuple",
"components": [
{ "name": "isAllowed", "type": "bool" },
{ "name": "supply", "type": "uint256" },
{ "name": "withdrawalLimit", "type": "uint256" },
{ "name": "lastUpdateTimestamp", "type": "uint256" },
{ "name": "expandPercent", "type": "uint256" },
{ "name": "expandDuration", "type": "uint256" },
{ "name": "baseWithdrawalLimit", "type": "uint256" }
]
}
]
}
]
},
{
"name": "getUserPosition",
"type": "function",
"stateMutability": "view",
"inputs": [
{ "name": "fToken_", "type": "address" },
{ "name": "user_", "type": "address" }
],
"outputs": [
{
"type": "tuple",
"components": [
{ "name": "fTokenShares", "type": "uint256" },
{ "name": "underlyingAssets", "type": "uint256" },
{ "name": "underlyingBalance", "type": "uint256" },
{ "name": "allowance", "type": "uint256" }
]
}
]
},
{
"name": "getUserPositions",
"type": "function",
"stateMutability": "view",
"inputs": [{ "name": "user_", "type": "address" }],
"outputs": [
{
"type": "tuple[]",
"components": [
{
"name": "fTokenDetails",
"type": "tuple",
"components": [
{ "name": "tokenAddress", "type": "address" },
{ "name": "eip2612Deposits", "type": "bool" },
{ "name": "isNativeUnderlying", "type": "bool" },
{ "name": "name", "type": "string" },
{ "name": "symbol", "type": "string" },
{ "name": "decimals", "type": "uint256" },
{ "name": "asset", "type": "address" },
{ "name": "totalAssets", "type": "uint256" },
{ "name": "totalSupply", "type": "uint256" },
{ "name": "convertToShares", "type": "uint256" },
{ "name": "convertToAssets", "type": "uint256" },
{ "name": "rewardsRate", "type": "uint256" },
{ "name": "supplyRate", "type": "uint256" },
{ "name": "rebalanceDifference", "type": "int256" },
{
"name": "liquidityUserSupplyData",
"type": "tuple",
"components": [
{ "name": "isAllowed", "type": "bool" },
{ "name": "supply", "type": "uint256" },
{ "name": "withdrawalLimit", "type": "uint256" },
{ "name": "lastUpdateTimestamp", "type": "uint256" },
{ "name": "expandPercent", "type": "uint256" },
{ "name": "expandDuration", "type": "uint256" },
{ "name": "baseWithdrawalLimit", "type": "uint256" }
]
}
]
},
{
"name": "userPosition",
"type": "tuple",
"components": [
{ "name": "fTokenShares", "type": "uint256" },
{ "name": "underlyingAssets", "type": "uint256" },
{ "name": "underlyingBalance", "type": "uint256" },
{ "name": "allowance", "type": "uint256" }
]
}
]
}
]
},
{
"name": "getPreviews",
"type": "function",
"stateMutability": "view",
"inputs": [
{ "name": "fToken_", "type": "address" },
{ "name": "assets_", "type": "uint256" },
{ "name": "shares_", "type": "uint256" }
],
"outputs": [
{ "name": "previewDeposit_", "type": "uint256" },
{ "name": "previewMint_", "type": "uint256" },
{ "name": "previewWithdraw_", "type": "uint256" },
{ "name": "previewRedeem_", "type": "uint256" }
]
}
]
fToken ABI (ERC4626)
[
{
"name": "deposit",
"type": "function",
"stateMutability": "nonpayable",
"inputs": [
{ "name": "assets", "type": "uint256" },
{ "name": "receiver", "type": "address" }
],
"outputs": [{ "name": "shares", "type": "uint256" }]
},
{
"name": "mint",
"type": "function",
"stateMutability": "nonpayable",
"inputs": [
{ "name": "shares", "type": "uint256" },
{ "name": "receiver", "type": "address" }
],
"outputs": [{ "name": "assets", "type": "uint256" }]
},
{
"name": "withdraw",
"type": "function",
"stateMutability": "nonpayable",
"inputs": [
{ "name": "assets", "type": "uint256" },
{ "name": "receiver", "type": "address" },
{ "name": "owner", "type": "address" }
],
"outputs": [{ "name": "shares", "type": "uint256" }]
},
{
"name": "redeem",
"type": "function",
"stateMutability": "nonpayable",
"inputs": [
{ "name": "shares", "type": "uint256" },
{ "name": "receiver", "type": "address" },
{ "name": "owner", "type": "address" }
],
"outputs": [{ "name": "assets", "type": "uint256" }]
},
{
"name": "asset",
"type": "function",
"stateMutability": "view",
"inputs": [],
"outputs": [{ "type": "address" }]
},
{
"name": "totalAssets",
"type": "function",
"stateMutability": "view",
"inputs": [],
"outputs": [{ "type": "uint256" }]
},
{
"name": "convertToShares",
"type": "function",
"stateMutability": "view",
"inputs": [{ "name": "assets", "type": "uint256" }],
"outputs": [{ "type": "uint256" }]
},
{
"name": "convertToAssets",
"type": "function",
"stateMutability": "view",
"inputs": [{ "name": "shares", "type": "uint256" }],
"outputs": [{ "type": "uint256" }]
},
{
"name": "maxDeposit",
"type": "function",
"stateMutability": "view",
"inputs": [{ "name": "receiver", "type": "address" }],
"outputs": [{ "type": "uint256" }]
},
{
"name": "maxWithdraw",
"type": "function",
"stateMutability": "view",
"inputs": [{ "name": "owner", "type": "address" }],
"outputs": [{ "type": "uint256" }]
},
{
"name": "previewDeposit",
"type": "function",
"stateMutability": "view",
"inputs": [{ "name": "assets", "type": "uint256" }],
"outputs": [{ "type": "uint256" }]
},
{
"name": "previewWithdraw",
"type": "function",
"stateMutability": "view",
"inputs": [{ "name": "assets", "type": "uint256" }],
"outputs": [{ "type": "uint256" }]
},
{
"name": "minDeposit",
"type": "function",
"stateMutability": "view",
"inputs": [],
"outputs": [{ "type": "uint256" }]
}
]
fToken Native (fWETH) ABI
[
{
"name": "depositNative",
"type": "function",
"stateMutability": "payable",
"inputs": [{ "name": "receiver_", "type": "address" }],
"outputs": [{ "name": "shares_", "type": "uint256" }]
},
{
"name": "depositNative",
"type": "function",
"stateMutability": "payable",
"inputs": [
{ "name": "receiver_", "type": "address" },
{ "name": "minAmountOut_", "type": "uint256" }
],
"outputs": [{ "name": "shares_", "type": "uint256" }]
},
{
"name": "withdrawNative",
"type": "function",
"stateMutability": "nonpayable",
"inputs": [
{ "name": "assets_", "type": "uint256" },
{ "name": "receiver_", "type": "address" },
{ "name": "owner_", "type": "address" }
],
"outputs": [{ "name": "shares_", "type": "uint256" }]
},
{
"name": "redeemNative",
"type": "function",
"stateMutability": "nonpayable",
"inputs": [
{ "name": "shares_", "type": "uint256" },
{ "name": "receiver_", "type": "address" },
{ "name": "owner_", "type": "address" }
],
"outputs": [{ "name": "assets_", "type": "uint256" }]
}
]
Fluid: Vault Protocol
Fluid Vaults enable leveraged borrowing positions — deposit collateral, borrow assets, and manage positions with automated risk management. All positions are represented as NFTs (ERC-721). All data reads use on-chain resolver contracts — no API dependency, no keys required.
On-chain first: Every read operation goes through resolver contracts deployed at the same address on all chains.
Borrow Flow (Step by Step)
Borrowing on Fluid always follows this pattern: deposit collateral → borrow assets. Both happen in a single operate() call.
┌─────────────────────────────────────────────────────────────┐
│ 1. Pick a vault (e.g., ETH/USDC = deposit ETH, borrow USDC) │
│ 2. Approve collateral token (skip for native ETH) │
│ 3. Call operate() with: │
│ - nftId = 0 (create new position) │
│ - newCol = positive (deposit collateral) │
│ - newDebt = positive (borrow amount) │
│ - to = your address (receive borrowed tokens) │
│ 4. You get back: NFT ID + borrowed tokens │
└─────────────────────────────────────────────────────────────┘
Minimum viable borrow (T1 vault, cast CLI):
# Deposit 1 ETH as collateral, borrow 2000 USDC, in a single transaction
cast send 0x348aD11DB2c90e7FdF8e57420C569F76dBe38a59 \
"operate(uint256,int256,int256,address)" \
0 1000000000000000000 2000000000 0xYOUR_ADDRESS \
--value 1ether \
--rpc-url https://mainnet.base.org \
--private-key $PRIVATE_KEY
Repay + withdraw (close position):
# Step 1: Approve debt token to vault (USDC in this case)
cast send 0xUSDC_ADDRESS "approve(address,uint256)" \
0x348aD11DB2c90e7FdF8e57420C569F76dBe38a59 \
2000000000 \
--rpc-url https://mainnet.base.org \
--private-key $PRIVATE_KEY
# Step 2: Repay all debt + withdraw all collateral
# newDebt = type(int256).min means repay everything
# newCol = type(int256).min means withdraw everything
cast send 0x348aD11DB2c90e7FdF8e57420C569F76dBe38a59 \
"operate(uint256,int256,int256,address)" \
YOUR_NFT_ID \
-57896044618658097711785492504343953926634992332820282019728792003956564819968 \
-57896044618658097711785492504343953926634992332820282019728792003956564819968 \
0xYOUR_ADDRESS \
--rpc-url https://mainnet.base.org \
--private-key $PRIVATE_KEY
Key insight:
operate()is the only function you need. One call can deposit + borrow, or repay + withdraw, or just adjust one side. The sign ofnewColandnewDebtdetermines the direction (positive = deposit/borrow, negative = withdraw/repay).
Vault Types
Fluid supports 4 vault types based on collateral and debt composition:
| Type | Name | Collateral | Debt | Description |
|---|---|---|---|---|
| T1 | Basic | Normal (single token) | Normal (single token) | Most gas-efficient. Standard token collateral, standard token debt. |
| T2 | Smart Col | Smart (DEX LP shares) | Normal (single token) | Collateral is a Fluid DEX LP position (two tokens), debt is standard. |
| T3 | Smart Debt | Normal (single token) | Smart (DEX LP shares) | Collateral is standard, debt is a Fluid DEX LP position (two tokens). |
| T4 | Smart Both | Smart (DEX LP shares) | Smart (DEX LP shares) | Both collateral and debt are Fluid DEX LP positions (most flexible). |
Smart Collateral / Smart Debt means the asset is a Fluid DEX liquidity provider position, exposing the user to two underlying tokens instead of one. This enables capital-efficient strategies like looping correlated pairs.
Discovering Vaults Programmatically
Use the VaultResolver (0xA5C3E16523eeeDDcC34706b0E6bE88b4c6EA95cC, same on all chains) to find vaults on any chain without hardcoding addresses.
import { createPublicClient, http } from 'viem';
import { base } from 'viem/chains';
const VAULT_RESOLVER = '0xA5C3E16523eeeDDcC34706b0E6bE88b4c6EA95cC';
const resolverAbi = [
{
name: 'getAllVaultsAddresses',
type: 'function',
stateMutability: 'view',
inputs: [],
outputs: [{ type: 'address[]' }],
},
{
name: 'getVaultType',
type: 'function',
stateMutability: 'view',
inputs: [{ name: 'vault_', type: 'address' }],
outputs: [{ type: 'uint256' }],
},
{
name: 'getVaultEntireData',
type: 'function',
stateMutability: 'view',
inputs: [{ name: 'vault_', type: 'address' }],
outputs: [{ type: 'tuple', components: [
{ name: 'vault', type: 'address' },
{ name: 'isSmartCol', type: 'bool' },
{ name: 'isSmartDebt', type: 'bool' },
{ name: 'constantVariables', type: 'tuple', components: [
{ name: 'liquidity', type: 'address' },
{ name: 'factory', type: 'address' },
{ name: 'operateImplementation', type: 'address' },
{ name: 'adminImplementation', type: 'address' },
{ name: 'secondaryImplementation', type: 'address' },
{ name: 'deployer', type: 'address' },
{ name: 'supply', type: 'address' },
{ name: 'borrow', type: 'address' },
{ name: 'supplyToken', type: 'tuple', components: [
{ name: 'token0', type: 'address' },
{ name: 'token1', type: 'address' },
]},
{ name: 'borrowToken', type: 'tuple', components: [
{ name: 'token0', type: 'address' },
{ name: 'token1', type: 'address' },
]},
]},
{ name: 'configs', type: 'tuple', components: [
{ name: 'supplyRateMagnifier', type: 'uint256' },
{ name: 'borrowRateMagnifier', type: 'uint256' },
{ name: 'collateralFactor', type: 'uint256' },
{ name: 'liquidationThreshold', type: 'uint256' },
{ name: 'liquidationMaxLimit', type: 'uint256' },
{ name: 'withdrawalGap', type: 'uint256' },
{ name: 'liquidationPenalty', type: 'uint256' },
{ name: 'borrowFee', type: 'uint256' },
{ name: 'oracle', type: 'address' },
{ name: 'oraclePriceOperate', type: 'uint256' },
{ name: 'oraclePriceLiquidate', type: 'uint256' },
{ name: 'rebalancer', type: 'address' },
]},
]}],
},
];
async function discoverVaults() {
const client = createPublicClient({
chain: base,
transport: http('https://mainnet.base.org'),
});
// 1. Get all vault addresses on Base
const vaults = await client.readContract({
address: VAULT_RESOLVER,
abi: resolverAbi,
functionName: 'getAllVaultsAddresses',
});
console.log(`Found ${vaults.length} vaults on Base`);
// 2. For each vault, get type and key data
for (const vault of vaults) {
const vaultType = await client.readContract({
address: VAULT_RESOLVER,
abi: resolverAbi,
functionName: 'getVaultType',
args: [vault],
});
const typeLabel = { 10000n: 'T1', 20000n: 'T2', 30000n: 'T3', 40000n: 'T4' }[vaultType] || `Unknown(${vaultType})`;
const data = await client.readContract({
address: VAULT_RESOLVER,
abi: resolverAbi,
functionName: 'getVaultEntireData',
args: [vault],
});
const maxLtv = Number(data.configs.collateralFactor) / 100;
const borrowRate = Number(data.configs.borrowRateMagnifier) / 100;
console.log(`${vault} | ${typeLabel} | Max LTV: ${maxLtv}% | Smart Col: ${data.isSmartCol} | Smart Debt: ${data.isSmartDebt}`);
}
}
discoverVaults().catch(console.error);
With cast (CLI):
# List all vaults on Base
cast call 0xA5C3E16523eeeDDcC34706b0E6bE88b4c6EA95cC \
"getAllVaultsAddresses()(address[])" \
--rpc-url https://mainnet.base.org
# Check vault type (10000=T1, 20000=T2, 30000=T3, 40000=T4)
cast call 0xA5C3E16523eeeDDcC34706b0E6bE88b4c6EA95cC \
"getVaultType(address)(uint256)" \
0xVAULT_ADDRESS \
--rpc-url https://mainnet.base.org
# Get full vault data (collateral tokens, debt tokens, rates, limits)
cast call 0xA5C3E16523eeeDDcC34706b0E6bE88b4c6EA95cC \
"getVaultEntireData(address)" \
0xVAULT_ADDRESS \
--rpc-url https://mainnet.base.org
No hardcoded vault addresses needed. The resolver always returns the current, up-to-date list of all active vaults on any chain. Use
getVaultEntireData()to inspect collateral/debt tokens, rates, and limits before interacting.
Quick Start
No registration or API keys needed. Just call the on-chain resolver.
Step 1: Pick your chain and RPC
| Network | Chain ID | VaultResolver Address | Public RPC |
|---|---|---|---|
| Ethereum | 1 | 0xA5C3E16523eeeDDcC34706b0E6bE88b4c6EA95cC |
https://eth.llamarpc.com |
| Arbitrum | 42161 | 0xA5C3E16523eeeDDcC34706b0E6bE88b4c6EA95cC |
https://arb1.arbitrum.io/rpc |
| Base | 8453 | 0xA5C3E16523eeeDDcC34706b0E6bE88b4c6EA95cC |
https://mainnet.base.org |
| Polygon | 137 | 0xA5C3E16523eeeDDcC34706b0E6bE88b4c6EA95cC |
https://polygon-rpc.com |
| Plasma | 9745 | 0xA5C3E16523eeeDDcC34706b0E6bE88b4c6EA95cC |
https://rpc.plasma.to |
Same resolver address on all chains (CREATE2 deployment). Any RPC provider works (Alchemy, Infura, QuickNode, etc.).
Step 2: Query vault data
# Get all vault addresses on the chain
cast call 0xA5C3E16523eeeDDcC34706b0E6bE88b4c6EA95cC \
"getAllVaultsAddresses()(address[])" \
--rpc-url https://eth.llamarpc.com
# Get total number of vaults
cast call 0xA5C3E16523eeeDDcC34706b0E6bE88b4c6EA95cC \
"getTotalVaults()(uint256)" \
--rpc-url https://eth.llamarpc.com
# Get complete data for a specific vault (T1: ETH/GHO)
cast call 0xA5C3E16523eeeDDcC34706b0E6bE88b4c6EA95cC \
"getVaultEntireData(address)" \
0xD9A7Dcdc57C6e44f00740dC73664fA456B983669 \
--rpc-url https://eth.llamarpc.com
# Get vault type (10000=T1, 20000=T2, 30000=T3, 40000=T4)
cast call 0xA5C3E16523eeeDDcC34706b0E6bE88b4c6EA95cC \
"getVaultType(address)(uint256)" \
0xD9A7Dcdc57C6e44f00740dC73664fA456B983669 \
--rpc-url https://eth.llamarpc.com
Step 3: Check a user position
# Get all NFT position IDs owned by a user
cast call 0xA5C3E16523eeeDDcC34706b0E6bE88b4c6EA95cC \
"positionsNftIdOfUser(address)(uint256[])" \
0xYOUR_ADDRESS \
--rpc-url https://eth.llamarpc.com
# Get position data + vault data by NFT ID
cast call 0xA5C3E16523eeeDDcC34706b0E6bE88b4c6EA95cC \
"positionByNftId(uint256)" \
42 \
--rpc-url https://eth.llamarpc.com
# Get all positions + vault data for a user
cast call 0xA5C3E16523eeeDDcC34706b0E6bE88b4c6EA95cC \
"positionsByUser(address)" \
0xYOUR_ADDRESS \
--rpc-url https://eth.llamarpc.com
# Find which vault a position NFT belongs to
cast call 0xA5C3E16523eeeDDcC34706b0E6bE88b4c6EA95cC \
"vaultByNftId(uint256)(address)" \
42 \
--rpc-url https://eth.llamarpc.com
Step 4: Open a position (T1 Vault — deposit collateral & borrow)
# Open a new T1 vault position: deposit 1 ETH as collateral, borrow 2000 GHO
# nftId=0 means create new position
# newCol > 0 means deposit collateral
# newDebt > 0 means borrow
cast send 0xD9A7Dcdc57C6e44f00740dC73664fA456B983669 \
"operate(uint256,int256,int256,address)" \
0 \
1000000000000000000 \
2000000000000000000000 \
0xYOUR_ADDRESS \
--value 1ether \
--rpc-url https://eth.llamarpc.com \
--private-key $PRIVATE_KEY
Step 5: Manage an existing position
# Add more collateral to an existing position (nftId=42)
cast send 0xD9A7Dcdc57C6e44f00740dC73664fA456B983669 \
"operate(uint256,int256,int256,address)" \
42 \
500000000000000000 \
0 \
0xYOUR_ADDRESS \
--value 0.5ether \
--rpc-url https://eth.llamarpc.com \
--private-key $PRIVATE_KEY
# Repay debt (negative newDebt = payback)
# Requires prior ERC-20 approval of the debt token to the vault
cast send 0xD9A7Dcdc57C6e44f00740dC73664fA456B983669 \
"operate(uint256,int256,int256,address)" \
42 \
0 \
-1000000000000000000000 \
0xYOUR_ADDRESS \
--rpc-url https://eth.llamarpc.com \
--private-key $PRIVATE_KEY
# Withdraw collateral (negative newCol = withdraw)
cast send 0xD9A7Dcdc57C6e44f00740dC73664fA456B983669 \
"operate(uint256,int256,int256,address)" \
42 \
-250000000000000000 \
0 \
0xYOUR_ADDRESS \
--rpc-url https://eth.llamarpc.com \
--private-key $PRIVATE_KEY
Everything You Can Do
VaultResolver (read data)
| Action | Method | Contract |
|---|---|---|
| Get all vault addresses | getAllVaultsAddresses() |
VaultResolver |
| Get total number of vaults | getTotalVaults() |
VaultResolver |
| Get vault type | getVaultType(vault) |
VaultResolver |
| Get complete vault data | getVaultEntireData(vault) |
VaultResolver |
| Get data for multiple vaults | getVaultsEntireData(vaults[]) / getVaultsEntireData() |
VaultResolver |
| Get vault state | getVaultState(vault) |
VaultResolver |
| Get position by NFT ID | positionByNftId(nftId) |
VaultResolver |
| Get all positions for a user | positionsByUser(user) |
VaultResolver |
| Get NFT IDs owned by a user | positionsNftIdOfUser(user) |
VaultResolver |
| Find vault by NFT ID | vaultByNftId(nftId) |
VaultResolver |
| Get liquidation data |
…(truncated)