Heurist Logo
Back to Skill Marketplace

fluid

VerifiedCryptoaccess level:high

Interact with Fluid Protocol - lending (ERC-4626 fTokens) and vaults (T1/T2/T3/T4). Lending: deposit, withdraw, check positions, query APY rates. Vaults: deposit collateral, borrow, repay, manage leveraged positions. Discover contracts programmatically via on-chain resolvers. No API keys needed.

Install

npx @heurist-network/skills add fluid

Installs

5

Timeline

Updated Mar 9, 2026

Created Mar 7, 2026

Verification

Reviewed and verified

SHA256: 743fa61f72163d30...

Approved Mar 7, 2026 by admin

Access Level

high

Required Permissions

Requires Private KeysSigns TransactionsUses Leverage

Files (1)

SKILL.md

Summary

version
1.0.1
homepage
https://fluid.io
metadata
chains: ["ethereum","arbitrum","base","polygon","plasma"]category: defiprotocol: fluid

SKILL.md

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

FileURLStatus
SKILL.md (this file)https://fluid.io/skill.md✅ Live
Deploymentsdeployments.md on GitHub✅ Live
Integration Docshttps://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)
ConceptDescription
AssetsThe underlying token (USDC, WETH, etc.)
SharesThe fToken balance you hold (fUSDC, fWETH, etc.)
Exchange RateIncreases 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 use convertToAssets() or convertToShares() 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

NetworkChain IDLendingResolver AddressPublic RPC
Ethereum10x48D32f49aFeAEC7AE66ad7B9264f446fc11a1569https://eth.llamarpc.com
Arbitrum421610x48D32f49aFeAEC7AE66ad7B9264f446fc11a1569https://arb1.arbitrum.io/rpc
Base84530x48D32f49aFeAEC7AE66ad7B9264f446fc11a1569https://mainnet.base.org
Polygon1370x48D32f49aFeAEC7AE66ad7B9264f446fc11a1569https://polygon-rpc.com
Plasma97450x48D32f49aFeAEC7AE66ad7B9264f446fc11a1569https://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

ActionMethodContract
Get all fToken addressesgetAllFTokens()LendingResolver
Get all fToken data (APY, TVL)getFTokensEntireData()LendingResolver
Get single fToken detailsgetFTokenDetails(fToken)LendingResolver
Get user position (one fToken)getUserPosition(fToken, user)LendingResolver
Get user positions (all fTokens)getUserPositions(user)LendingResolver
Preview deposit/mint/withdraw/redeemgetPreviews(fToken, assets, shares)LendingResolver
Deposit ERC-20 assetsdeposit(assets, receiver)fToken
Mint exact sharesmint(shares, receiver)fToken
Withdraw assetswithdraw(assets, receiver, owner)fToken
Redeem sharesredeem(shares, receiver, owner)fToken
Deposit native ETHdepositNative(receiver)fToken (payable)
Withdraw native ETHwithdrawNative(assets, receiver, owner)fToken
Check max deposit/withdrawmaxDeposit(receiver) / maxWithdraw(owner)fToken
Check exchange rateconvertToAssets(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.

NetworkExplorer (verified source)
EthereumEtherscan
ArbitrumArbiscan
BaseBasescan
PolygonPolygonscan
PlasmaPlasmascan

LendingFactory

Address: 0x54B91A0D94cb471F37f949c60F7Fa7935b551D03 — same on all chains (CREATE2). Verified.

NetworkExplorer (verified source)
EthereumEtherscan
ArbitrumArbiscan
BaseBasescan
PolygonPolygonscan
PlasmaPlasmascan

Source: deployments.md

fTokens (Ethereum Mainnet)

TokenfToken AddressUnderlying
fUSDC0x9Fb7b4477576Fe5B32be4C1843aFB1e55F251B33USDC
fUSDT0x5C20B550819128074FD538Edf79791733ccEdd18USDT
fWETH0x90551c1795392094FE6D29B758EcCD233cFAa260WETH (native)
fGHOCheck resolverGHO
fwstETHCheck resolverwstETH

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.blockNumber for 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 of newCol and newDebt determines the direction (positive = deposit/borrow, negative = withdraw/repay).

Vault Types

Fluid supports 4 vault types based on collateral and debt composition:

TypeNameCollateralDebtDescription
T1BasicNormal (single token)Normal (single token)Most gas-efficient. Standard token collateral, standard token debt.
T2Smart ColSmart (DEX LP shares)Normal (single token)Collateral is a Fluid DEX LP position (two tokens), debt is standard.
T3Smart DebtNormal (single token)Smart (DEX LP shares)Collateral is standard, debt is a Fluid DEX LP position (two tokens).
T4Smart BothSmart (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

NetworkChain IDVaultResolver AddressPublic RPC
Ethereum10xA5C3E16523eeeDDcC34706b0E6bE88b4c6EA95cChttps://eth.llamarpc.com
Arbitrum421610xA5C3E16523eeeDDcC34706b0E6bE88b4c6EA95cChttps://arb1.arbitrum.io/rpc
Base84530xA5C3E16523eeeDDcC34706b0E6bE88b4c6EA95cChttps://mainnet.base.org
Polygon1370xA5C3E16523eeeDDcC34706b0E6bE88b4c6EA95cChttps://polygon-rpc.com
Plasma97450xA5C3E16523eeeDDcC34706b0E6bE88b4c6EA95cChttps://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)

ActionMethodContract
Get all vault addressesgetAllVaultsAddresses()VaultResolver
Get total number of vaultsgetTotalVaults()VaultResolver
Get vault typegetVaultType(vault)VaultResolver
Get complete vault datagetVaultEntireData(vault)VaultResolver
Get data for multiple vaultsgetVaultsEntireData(vaults[]) / getVaultsEntireData()VaultResolver
Get vault stategetVaultState(vault)VaultResolver
Get position by NFT IDpositionByNftId(nftId)VaultResolver
Get all positions for a userpositionsByUser(user)VaultResolver
Get NFT IDs owned by a userpositionsNftIdOfUser(user)VaultResolver
Find vault by NFT IDvaultByNftId(nftId)VaultResolver
Get liquidation datagetVaultLiquidation(vault, tokenInAmt)VaultResolver
Get all liquidation opportunitiesgetAllVaultsLiquidation()VaultResolver
Get total positions counttotalPositions()VaultResolver

Vault Operations (write — send transactions)

ActionMethodVault Type
Open/manage position (T1)operate(nftId, newCol, newDebt, to)T1
Open/manage position (T2)operate(nftId, newColToken0, newColToken1, colSharesMinMax, newDebt, to)T2
Open/manage position (T2 perfect)operatePerfect(nftId, perfectColShares, colToken0MinMax, colToken1MinMax, newDebt, to)T2
Open/manage position (T3)operate(nftId, newCol, newDebtToken0, newDebtToken1, debtSharesMinMax, to)T3
Open/manage position (T3 perfect)operatePerfect(nftId, newCol, perfectDebtShares, debtToken0MinMax, debtToken1MinMax, to)T3
Open/manage position (T4)operate(nftId, newColToken0, newColToken1, colSharesMinMax, newDebtToken0, newDebtToken1, debtSharesMinMax, to)T4
Open/manage position (T4 perfect)operatePerfect(nftId, perfectColShares, colToken0MinMax, colToken1MinMax, perfectDebtShares, debtToken0MinMax, debtToken1MinMax, to)T4
Liquidate position (T1)liquidate(debtAmt, colPerUnitDebt, to, absorb)T1

operate() Parameter Guide

The operate() function is the primary way to interact with vaults. It handles opening new positions, depositing/withdrawing collateral, and borrowing/repaying debt — all in a single transaction.

T1: operate(nftId, newCol, newDebt, to)

ParameterTypeDescription
nftIduint256Position NFT ID. Pass 0 to create a new position.
newColint256Collateral change. Positive = deposit, negative = withdraw.
newDebtint256Debt change. Positive = borrow, negative = repay.
toaddressRecipient for borrowed/withdrawn tokens. address(0) = msg.sender.

Returns: (nftId, finalSupplyAmt, finalBorrowAmt) — the NFT ID (useful when creating new positions) and final supply/borrow amounts (negative = withdraw/payback).

T2: operate(nftId, newColToken0, newColToken1, colSharesMinMax, newDebt, to)

ParameterTypeDescription
nftIduint256Position NFT ID. Pass 0 to create a new position.
newColToken0int256Token0 collateral change. Positive = deposit, negative = withdraw.
newColToken1int256Token1 collateral change. Positive = deposit, negative = withdraw.
colSharesMinMaxint256Slippage protection. Min shares when depositing (positive), max shares when withdrawing (negative).
newDebtint256Debt change. Positive = borrow, negative = repay.
toaddressRecipient for borrowed/withdrawn tokens.

T2 also supports operatePerfect() for share-denominated operations with exact share amounts.

T3: operate(nftId, newCol, newDebtToken0, newDebtToken1, debtSharesMinMax, to)

ParameterTypeDescription
nftIduint256Position NFT ID. Pass 0 to create a new position.
newColint256Collateral change. Positive = deposit, negative = withdraw.
newDebtToken0int256Token0 debt change. Positive = borrow, negative = repay.
newDebtToken1int256Token1 debt change. Positive = borrow, negative = repay.
debtSharesMinMaxint256Slippage protection. Max shares when borrowing (positive), min shares when repaying (negative).
toaddressRecipient for borrowed/withdrawn tokens.

T3 also supports operatePerfect() for share-denominated operations with exact share amounts.

T4: operate(nftId, newColToken0, newColToken1, colSharesMinMax, newDebtToken0, newDebtToken1, debtSharesMinMax, to)

ParameterTypeDescription
nftIduint256Position NFT ID. Pass 0 to create a new position.
newColToken0int256Token0 collateral change.
newColToken1int256Token1 collateral change.
colSharesMinMaxint256Slippage protection for collateral shares.
newDebtToken0int256Token0 debt change.
newDebtToken1int256Token1 debt change.
debtSharesMinMaxint256Slippage protection for debt shares.
toaddressRecipient for borrowed/withdrawn tokens.

T4 also supports operatePerfect() for share-denominated operations on both collateral and debt.

operatePerfect() (T2, T3, T4)

The operatePerfect() variant lets you specify exact share amounts for DEX LP positions, with the protocol computing the optimal token amounts. Use this when you want precise control over share quantities rather than token amounts.

Contract Addresses (All Verified)

All contracts are verified on their respective block explorers. Click any link to read the source code directly.

VaultResolver (read vault data, positions, liquidations)

Address: 0xA5C3E16523eeeDDcC34706b0E6bE88b4c6EA95cC — same on all chains (CREATE2). Verified.

NetworkExplorer (verified source)
EthereumEtherscan
ArbitrumArbiscan
BaseBasescan
PolygonPolygonscan
PlasmaPlasmascan

VaultFactory (NFT minting, vault creation)

Address: 0x324c5Dc1fC42c7a4D43d92df1eBA58a54d13Bf2d — same on all chains (CREATE2). Verified.

NetworkExplorer (verified source)
EthereumEtherscan
ArbitrumArbiscan
BaseBasescan
PolygonPolygonscan
PlasmaPlasmascan

VaultPositionsResolver

Address: 0xaA21a86030EAa16546A759d2d10fd3bF9D053Bc7 (Ethereum mainnet). Verified.

VaultLiquidationResolver

Address: 0xd8d1a39b1Fe519113b6D8e1E82Dc92aedaD40948 (Ethereum mainnet). Verified.

Source: deployments.md

Example Vaults (Ethereum Mainnet)

T1 Vaults (Normal Collateral / Normal Debt)

VaultAddressCollateralDebt
ETH / GHO0xD9A7Dcdc57C6e44f00740dC73664fA456B983669ETHGHO
wstETH / GHO0xB0F2B58af3F17F1A3377c37F6E85eA41c369D1C7wstETHGHO
USDC / ETH0x348aD11DB2c90e7FdF8e57420C569F76dBe38a59USDCETH
weETH / GHO0xaF40e84C6AD7C214E4d89f63B3c07B3eb99632dFweETHGHO

T2 Vaults (Smart Collateral / Normal Debt)

VaultAddressCollateral (DEX LP)Debt
DEX-weETH-ETH / wstETH0xb4a15526d427f4d20b0dAdaF3baB4177C85A699AweETH-ETH LPwstETH
DEX-WBTC-cbBTC / USDCCheck resolverWBTC-cbBTC LPUSDC

T3 Vaults (Normal Collateral / Smart Debt)

VaultAddressCollateralDebt (DEX LP)
wstETH / DEX-USDC-USDT0x221E35b5655A1eEB3C42c4DeFc39648531f6C9CFwstETHUSDC-USDT LP
ETH / DEX-USDC-USDT0x3E11B9aEb9C7dBbda4DD41477223Cc2f3f24b9d7ETHUSDC-USDT LP

T4 Vaults (Smart Collateral / Smart Debt)

VaultAddressCollateral (DEX LP)Debt (DEX LP)
DEX-wstETH-ETH / DEX-wstETH-ETH0x528CF7DBBff878e02e48E83De5097F8071af768DwstETH-ETH LPwstETH-ETH LP
DEX-WBTC-cbBTC / DEX-WBTC-cbBTC0xDCe03288F9A109150f314ED0Ca9b59a690300d9dWBTC-cbBTC LPWBTC-cbBTC LP

Use getAllVaultsAddresses() on VaultResolver to get the complete, up-to-date list for any network.

Understanding Vault Data

VaultEntireData Structure

When you call getVaultEntireData(), you receive a comprehensive struct containing:

FieldDescription
vaultVault contract address
isSmartColtrue if collateral is a Fluid DEX LP position (T2 or T4)
isSmartDebttrue if debt is a Fluid DEX LP position (T3 or T4)
constantVariablesImmutable vault settings (tokens, factory, liquidity layer, slots)
configsVault configuration (collateral factor, liquidation threshold, oracle, rates)
exchangePricesAndRatesCurrent and stored exchange prices, supply/borrow rates at vault and liquidity level
totalSupplyAndBorrowAggregate supply/borrow across the vault
limitsAndAvailabilityWithdrawal limits, borrow limits, and currently available amounts
vaultStateTotal positions, top tick, branch state
liquidityUserSupplyDataSupply data at the Liquidity layer (or DEX for smart col)
liquidityUserBorrowDataBorrow data at the Liquidity layer (or DEX for smart debt)

Configs

FieldDescription
supplyRateMagnifierMultiplier for supply rate (or absolute rate for smart col vaults)
borrowRateMagnifierMultiplier for borrow rate (or absolute rate for smart debt vaults)
collateralFactorMax LTV ratio in basis points (e.g., 8000 = 80%)
liquidationThresholdLTV at which liquidation begins, in basis points (e.g., 8500 = 85%)
liquidationMaxLimitMaximum LTV during liquidation, in basis points (e.g., 9000 = 90%)
withdrawalGapSafety gap for withdrawals, in basis points
liquidationPenaltyPenalty applied during liquidation, in basis points
borrowFeeOne-time fee on new borrows, in basis points
oracleOracle contract address (returns debt-per-collateral price)
oraclePriceOperateCurrent oracle price for operations (debt amount per 1 unit of collateral)
oraclePriceLiquidateCurrent oracle price for liquidations

Exchange Prices and Rates

FieldDescription
liquiditySupplyExchangePriceCurrent supply exchange price at liquidity layer (1e12 for smart col)
liquidityBorrowExchangePriceCurrent borrow exchange price at liquidity layer (1e12 for smart debt)
vaultSupplyExchangePriceCurrent supply exchange price at the vault level
vaultBorrowExchangePriceCurrent borrow exchange price at the vault level
supplyRateLiquiditySupply interest rate at liquidity layer (0 for smart col)
borrowRateLiquidityBorrow interest rate at liquidity layer (0 for smart debt)
supplyRateVaultEffective supply rate at the vault (can be negative for smart col)
borrowRateVaultEffective borrow rate at the vault (can be negative for smart debt)
rewardsOrFeeRateSupplyRewards (positive) or fee (negative) rate for supply, in 1e2 precision
rewardsOrFeeRateBorrowRewards (negative) or fee (positive) rate for borrow, in 1e2 precision

Understanding Vault Rates

For normal collateral/debt vaults (T1):

  • supplyRateVault = supplyRateLiquidity * supplyRateMagnifier / 10000
  • borrowRateVault = borrowRateLiquidity * borrowRateMagnifier / 10000
  • rewardsOrFeeRate is the relative % difference from the liquidity rate

For smart collateral/debt vaults (T2, T3, T4):

  • The magnifier bits store the absolute interest rate
  • supplyRateVault == rewardsOrFeeRateSupply (for smart col)
  • borrowRateVault == rewardsOrFeeRateBorrow (for smart debt)
  • Full rates require combining with DEX resolver data

Rates are in basis points (1 bp = 0.01%, 10000 = 100%):

const borrowRateVault = Number(result.exchangePricesAndRates.borrowRateVault);
const borrowApr = borrowRateVault / 100; // e.g. 450 → 4.50%

const collateralFactor = Number(result.configs.collateralFactor);
const maxLtv = collateralFactor / 100; // e.g. 8000 → 80%

UserPosition Structure

FieldDescription
nftIdPosition NFT ID
ownerCurrent owner address
isLiquidatedWhether the position has been (partially) liquidated
isSupplyPositiontrue if supply-only (no borrow)
tickCurrent tick of the position (debt/collateral ratio)
tickIdID within the tick
supplyCurrent collateral amount (in token units or shares for smart col)
borrowCurrent debt amount (in token units or shares for smart debt)
dustBorrowDust borrow amount (very small residual)
beforeSupplyCollateral before any liquidation adjustments
beforeBorrowDebt before any liquidation adjustments
beforeDustBorrowDust borrow before adjustments

Code Examples (viem/Node.js)

Open a T1 Vault Position (Deposit ETH + Borrow GHO)

import { createWalletClient, createPublicClient, http, parseEther, parseUnits } from 'viem';
import { mainnet } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';

const VAULT_T1_ETH_GHO = '0xD9A7Dcdc57C6e44f00740dC73664fA456B983669';

const vaultT1Abi = [
  {
    name: 'operate',
    type: 'function',
    stateMutability: 'payable',
    inputs: [
      { name: 'nftId_', type: 'uint256' },
      { name: 'newCol_', type: 'int256' },
      { name: 'newDebt_', type: 'int256' },
      { name: 'to_', type: 'address' },
    ],
    outputs: [
      { type: 'uint256' },
      { type: 'int256' },
      { type: 'int256' },
    ],
  },
];

async function openPosition() {
  const account = privateKeyToAccount(process.env.PRIVATE_KEY);

  const walletClient = createWalletClient({
    account,
    chain: mainnet,
    transport: http(process.env.ETH_RPC_URL),
  });

  const collateralAmount = parseEther('1'); // 1 ETH
  const borrowAmount = parseUnits('2000', 18); // 2000 GHO (18 decimals)

  // nftId = 0 → create new position
  // newCol = positive → deposit
  // newDebt = positive → borrow
  const hash = await walletClient.writeContract({
    address: VAULT_T1_ETH_GHO,
    abi: vaultT1Abi,
    functionName: 'operate',
    args: [0n, collateralAmount, borrowAmount, account.address],
    value: collateralAmount, // send ETH as value for native collateral
  });

  console.log(`Transaction: ${hash}`);
}

openPosition().catch(console.error);

Query User Vault Positions

import { createPublicClient, http } from 'viem';
import { mainnet } from 'viem/chains';

const VAULT_RESOLVER = '0xA5C3E16523eeeDDcC34706b0E6bE88b4c6EA95cC';

const vaultResolverAbi = [
  {
    name: 'positionsNftIdOfUser',
    type: 'function',
    stateMutability: 'view',
    inputs: [{ name: 'user_', type: 'address' }],
    outputs: [{ type: 'uint256[]' }],
  },
  {
    name: 'getAllVaultsAddresses',
    type: 'function',
    stateMutability: 'view',
    inputs: [],
    outputs: [{ type: 'address[]' }],
  },
  {
    name: 'getVaultType',
    type: 'function',
    stateMutability: 'view',
    inputs: [{ name: 'vault_', type: 'address' }],
    outputs: [{ type: 'uint256' }],
  },
];

async function main() {
  const client = createPublicClient({
    chain: mainnet,
    transport: http(process.env.ETH_RPC_URL),
  });

  // Get all position NFT IDs
  const nftIds = await client.readContract({
    address: VAULT_RESOLVER,
    abi: vaultResolverAbi,
    functionName: 'positionsNftIdOfUser',
    args: ['0xYourAddress'],
  });

  console.log(`User has ${nftIds.length} vault positions: [${nftIds.join(', ')}]`);
}

main().catch(console.error);

Common Patterns

Check vault health before operating

const vaultData = await client.readContract({
  address: VAULT_RESOLVER,
  abi: vaultResolverAbi,
  functionName: 'getVaultEntireData',
  args: [vaultAddress],
});

const collateralFactor = Number(vaultData.configs.collateralFactor) / 100; // e.g. 80%
const liquidationThreshold = Number(vaultData.configs.liquidationThreshold) / 100; // e.g. 85%
const borrowRate = Number(vaultData.exchangePricesAndRates.borrowRateVault) / 100; // e.g. 4.5%

console.log(`Max LTV: ${collateralFactor}%`);
console.log(`Liquidation at: ${liquidationThreshold}%`);
console.log(`Borrow APR: ${borrowRate}%`);

Check borrowing limits

const { limitsAndAvailability } = vaultData;

const borrowable = limitsAndAvailability.borrowable;
const borrowLimit = limitsAndAvailability.borrowLimit;
const withdrawable = limitsAndAvailability.withdrawable;

console.log(`Currently borrowable: ${borrowable}`);
console.log(`Borrow limit: ${borrowLimit}`);
console.log(`Currently withdrawable: ${withdrawable}`);

// Check minimum borrow amount
const minimumBorrowing = limitsAndAvailability.minimumBorrowing;
if (borrowAmount < minimumBorrowing) {
  throw new Error(`Amount below minimum borrow: ${minimumBorrowing}`);
}

Fetch liquidation opportunities

// Get liquidation data for a specific vault
const liquidationData = await client.readContract({
  address: VAULT_RESOLVER,
  abi: vaultResolverAbi,
  functionName: 'getVaultLiquidation',
  args: [vaultAddress, 0n], // 0 = get max available
});

if (liquidationData.inAmt > 0n) {
  console.log(`Liquidation available!`);
  console.log(`  Debt to pay: ${liquidationData.inAmt}`);
  console.log(`  Collateral received: ${liquidationData.outAmt}`);
  console.log(`  Absorb available: ${liquidationData.absorbAvailable}`);
}

Transfer a position NFT

Vault positions are ERC-721 NFTs on the VaultFactory. Transfer them like any NFT:

const VAULT_FACTORY = '0x324c5Dc1fC42c7a4D43d92df1eBA58a54d13Bf2d';

await walletClient.writeContract({
  address: VAULT_FACTORY,
  abi: [
    {
      name: 'transferFrom',
      type: 'function',
      inputs: [
        { name: 'from', type: 'address' },
        { name: 'to', type: 'address' },
        { name: 'tokenId', type: 'uint256' },
      ],
      outputs: [],
    },
  ],
  functionName: 'transferFrom',
  args: [fromAddress, toAddress, nftId],
});

Key Concepts

NFT Positions

Every vault position is an ERC-721 NFT minted by the VaultFactory (0x324c5Dc1fC42c7a4D43d92df1eBA58a54d13Bf2d). The NFT ID uniquely identifies a position across all vaults and chains. Positions can be transferred by transferring the NFT.

Collateral Factor & Liquidation

  • Collateral Factor (e.g., 80%): Maximum loan-to-value ratio for operating (borrowing, withdrawing).
  • Liquidation Threshold (e.g., 85%): LTV at which the position becomes liquidatable.
  • Liquidation Max Limit (e.g., 90%): Hard upper bound — positions cannot exceed this LTV.
  • Liquidation Penalty (e.g., 5%): Discount given to liquidators as incentive.

Oracle Pricing

The oracle returns the price as debt per unit of collateral (i.e., how much debt 1 unit of collateral is worth). For smart collateral/debt vaults, this price is expressed in shares rather than underlying tokens:

  • T1: debt token per 1 collateral token
  • T2: debt token per 1 collateral share
  • T3: debt shares per 1 collateral token
  • T4: debt shares per 1 collateral share

Exchange Prices

Vault exchange prices convert between raw (stored) amounts and actual token amounts. They increase over time as interest accrues. Precision is 1e12.

Withdrawal & Borrow Limits

Fluid implements expanding limits to protect against sudden large withdrawals or borrows:

  • Withdrawal Limit: Starts at baseWithdrawalLimit and expands by expandPercent over expandDuration.
  • Borrow Limit: Similarly expands over time. Check limitsAndAvailability.borrowable for the current available amount.
  • Always check limits before large operations using the resolver.

Smart Collateral & Smart Debt (T2, T3, T4)

When collateral or debt is "smart", it is a Fluid DEX LP position rather than a single token. This means:

  • Amounts are in shares, not token amounts
  • Token0 and token1 addresses are available in constantVariables.supplyToken / constantVariables.borrowToken
  • For full rate calculations, combine vault resolver data with DexResolver data
  • operatePerfect() lets you specify exact share amounts

Dust Borrow

A small "dust" borrow amount may exist in positions. This is a protocol mechanism to handle rounding. The dustBorrow field in UserPosition shows this amount.

ABIs

VaultResolver ABI (key methods)

[
  {
    "name": "getAllVaultsAddresses",
    "type": "function",
    "stateMutability": "view",
    "inputs": [],
    "outputs": [{ "type": "address[]" }]
  },
  {
    "name": "getTotalVaults",
    "type": "function",
    "stateMutability": "view",
    "inputs": [],
    "outputs": [{ "type": "uint256" }]
  },
  {
    "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": "vaultId", "type": "uint256" },
              { "name": "vaultType", "type": "uint256" },
              { "name": "supplyExchangePriceSlot", "type": "bytes32" },
              { "name": "borrowExchangePriceSlot", "type": "bytes32" },
              { "name": "userSupplySlot", "type": "bytes32" },
              { "name": "userBorrowSlot", "type": "bytes32" }
            ]
          },
          {
            "name": "configs",
            "type": "tuple",
            "components": [
              { "name": "supplyRateMagnifier", "type": "uint16" },
              { "name": "borrowRateMagnifier", "type": "uint16" },
              { "name": "collateralFactor", "type": "uint16" },
              { "name": "liquidationThreshold", "type": "uint16" },
              { "name": "liquidationMaxLimit", "type": "uint16" },
              { "name": "withdrawalGap", "type": "uint16" },
              { "name": "liquidationPenalty", "type": "uint16" },
              { "name": "borrowFee", "type": "uint16" },
              { "name": "oracle", "type": "address" },
              { "name": "oraclePriceOperate", "type": "uint256" },
              { "name": "oraclePriceLiquidate", "type": "uint256" },
              { "name": "rebalancer", "type": "address" },
              { "name": "lastUpdateTimestamp", "type": "uint256" }
            ]
          },
          {
            "name": "exchangePricesAndRates",
            "type": "tuple",
            "components": [
              { "name": "lastStoredLiquiditySupplyExchangePrice", "type": "uint256" },
              { "name": "lastStoredLiquidityBorrowExchangePrice", "type": "uint256" },
              { "name": "lastStoredVaultSupplyExchangePrice", "type": "uint256" },
              { "name": "lastStoredVaultBorrowExchangePrice", "type": "uint256" },
              { "name": "liquiditySupplyExchangePrice", "type": "uint256" },
              { "name": "liquidityBorrowExchangePrice", "type": "uint256" },
              { "name": "vaultSupplyExchangePrice", "type": "uint256" },
              { "name": "vaultBorrowExchangePrice", "type": "uint256" },
              { "name": "supplyRateLiquidity", "type": "uint256" },
              { "name": "borrowRateLiquidity", "type": "uint256" },
              { "name": "supplyRateVault", "type": "int256" },
              { "name": "borrowRateVault", "type": "int256" },
              { "name": "rewardsOrFeeRateSupply", "type": "int256" },
              { "name": "rewardsOrFeeRateBorrow", "type": "int256" }
            ]
          },
          {
            "name": "totalSupplyAndBorrow",
            "type": "tuple",
            "components": [
              { "name": "totalSupplyVault", "type": "uint256" },
              { "name": "totalBorrowVault", "type": "uint256" },
              { "name": "totalSupplyLiquidityOrDex", "type": "uint256" },
              { "name": "totalBorrowLiquidityOrDex", "type": "uint256" },
              { "name": "absorbedSupply", "type": "uint256" },
              { "name": "absorbedBorrow", "type": "uint256" }
            ]
          },
          {
            "name": "limitsAndAvailability",
            "type": "tuple",
            "components": [
              { "name": "withdrawLimit", "type": "uint256" },
              { "name": "withdrawableUntilLimit", "type": "uint256" },
              { "name": "withdrawable", "type": "uint256" },
              { "name": "borrowLimit", "type": "uint256" },
              { "name": "borrowableUntilLimit", "type": "uint256" },
              { "name": "borrowable", "type": "uint256" },
              { "name": "borrowLimitUtilization", "type": "uint256" },
              { "name": "minimumBorrowing", "type": "uint256" }
            ]
          },
          {
            "name": "vaultState",
            "type": "tuple",
            "components": [
              { "name": "totalPositions", "type": "uint256" },
              { "name": "topTick", "type": "int256" },
              { "name": "currentBranch", "type": "uint256" },
              { "name": "totalBranch", "type": "uint256" },
              { "name": "totalBorrow", "type": "uint256" },
              { "name": "totalSupply", "type": "uint256" },
              {
                "name": "currentBranchState",
                "type": "tuple",
                "components": [
                  { "name": "status", "type": "uint256" },
                  { "name": "minimaTick", "type": "int256" },
                  { "name": "debtFactor", "type": "uint256" },
                  { "name": "partials", "type": "uint256" },
                  { "name": "debtLiquidity", "type": "uint256" },
                  { "name": "baseBranchId", "type": "uint256" },
                  { "name": "baseBranchMinima", "type": "int256" }
                ]
              }
            ]
          },
          {
            "name": "liquidityUserSupplyData",
            "type": "tuple",
            "components": [
              { "name": "modeWithInterest", "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": "withdrawableUntilLimit", "type": "uint256" },
              { "name": "withdrawable", "type": "uint256" }
            ]
          },
          {
            "name": "liquidityUserBorrowData",
            "type": "tuple",
            "components": [
              { "name": "modeWithInterest", "type": "bool" },
              { "name": "borrow", "type": "uint256" },
              { "name": "borrowLimit", "type": "uint256" },
              { "name": "lastUpdateTimestamp", "type": "uint256" },
              { "name": "expandPercent", "type": "uint256" },
              { "name": "expandDuration", "type": "uint256" },
              { "name": "baseBorrowLimit", "type": "uint256" },
              { "name": "maxBorrowLimit", "type": "uint256" },
              { "name": "borrowableUntilLimit", "type": "uint256" },
              { "name": "borrowable", "type": "uint256" },
              { "name": "borrowLimitUtilization", "type": "uint256" }
            ]
          }
        ]
      }
    ]
  },
  {
    "name": "positionByNftId",
    "type": "function",
    "stateMutability": "view",
    "inputs": [{ "name": "nftId_", "type": "uint256" }],
    "outputs": [
      {
        "name": "userPosition_",
        "type": "tuple",
        "components": [
          { "name": "nftId", "type": "uint256" },
          { "name": "owner", "type": "address" },
          { "name": "isLiquidated", "type": "bool" },
          { "name": "isSupplyPosition", "type": "bool" },
          { "name": "tick", "type": "int256" },
          { "name": "tickId", "type": "uint256" },
          { "name": "beforeSupply", "type": "uint256" },
          { "name": "beforeBorrow", "type": "uint256" },
          { "name": "beforeDustBorrow", "type": "uint256" },
          { "name": "supply", "type": "uint256" },
          { "name": "borrow", "type": "uint256" },
          { "name": "dustBorrow", "type": "uint256" }
        ]
      },
      {
        "name": "vaultData_",
        "type": "tuple",
        "components": "...same as getVaultEntireData output..."
      }
    ]
  },
  {
    "name": "positionsByUser",
    "type": "function",
    "stateMutability": "view",
    "inputs": [{ "name": "user_", "type": "address" }],
    "outputs": [
      { "name": "userPositions_", "type": "tuple[]", "components": "...same as UserPosition..." },
      { "name": "vaultsData_", "type": "tuple[]", "components": "...same as VaultEntireData..." }
    ]
  },
  {
    "name": "positionsNftIdOfUser",
    "type": "function",
    "stateMutability": "view",
    "inputs": [{ "name": "user_", "type": "address" }],
    "outputs": [{ "type": "uint256[]" }]
  },
  {
    "name": "vaultByNftId",
    "type": "function",
    "stateMutability": "view",
    "inputs": [{ "name": "nftId_", "type": "uint256" }],
    "outputs": [{ "type": "address" }]
  },
  {
    "name": "getVaultLiquidation",
    "type": "function",
    "stateMutability": "nonpayable",
    "inputs": [
      { "name": "vault_", "type": "address" },
      { "name": "tokenInAmt_", "type": "uint256" }
    ],
    "outputs": [
      {
        "type": "tuple",
        "components": [
          { "name": "vault", "type": "address" },
          { "name": "token0In", "type": "address" },
          { "name": "token0Out", "type": "address" },
          { "name": "token1In", "type": "address" },
          { "name": "token1Out", "type": "address" },
          { "name": "inAmt", "type": "uint256" },
          { "name": "outAmt", "type": "uint256" },
          { "name": "inAmtWithAbsorb", "type": "uint256" },
          { "name": "outAmtWithAbsorb", "type": "uint256" },
          { "name": "absorbAvailable", "type": "bool" }
        ]
      }
    ]
  },
  {
    "name": "totalPositions",
    "type": "function",
    "stateMutability": "view",
    "inputs": [],
    "outputs": [{ "type": "uint256" }]
  }
]

VaultT1 ABI

[
  {
    "name": "operate",
    "type": "function",
    "stateMutability": "payable",
    "inputs": [
      { "name": "nftId_", "type": "uint256" },
      { "name": "newCol_", "type": "int256" },
      { "name": "newDebt_", "type": "int256" },
      { "name": "to_", "type": "address" }
    ],
    "outputs": [
      { "name": "nftId", "type": "uint256" },
      { "name": "supplyAmt", "type": "int256" },
      { "name": "borrowAmt", "type": "int256" }
    ]
  },
  {
    "name": "liquidate",
    "type": "function",
    "stateMutability": "payable",
    "inputs": [
      { "name": "debtAmt_", "type": "uint256" },
      { "name": "colPerUnitDebt_", "type": "uint256" },
      { "name": "to_", "type": "address" },
      { "name": "absorb_", "type": "bool" }
    ],
    "outputs": [
      { "name": "actualDebtAmt_", "type": "uint256" },
      { "name": "actualColAmt_", "type": "uint256" }
    ]
  },
  {
    "name": "constantsView",
    "type": "function",
    "stateMutability": "view",
    "inputs": [],
    "outputs": [
      {
        "type": "tuple",
        "components": [
          { "name": "liquidity", "type": "address" },
          { "name": "factory", "type": "address" },
          { "name": "adminImplementation", "type": "address" },
          { "name": "secondaryImplementation", "type": "address" },
          { "name": "supplyToken", "type": "address" },
          { "name": "borrowToken", "type": "address" },
          { "name": "supplyDecimals", "type": "uint8" },
          { "name": "borrowDecimals", "type": "uint8" },
          { "name": "vaultId", "type": "uint256" },
          { "name": "liquiditySupplyExchangePriceSlot", "type": "bytes32" },
          { "name": "liquidityBorrowExchangePriceSlot", "type": "bytes32" },
          { "name": "liquidityUserSupplySlot", "type": "bytes32" },
          { "name": "liquidityUserBorrowSlot", "type": "bytes32" }
        ]
      }
    ]
  },
  {
    "name": "VAULT_ID",
    "type": "function",
    "stateMutability": "view",
    "inputs": [],
    "outputs": [{ "type": "uint256" }]
  }
]

VaultT2 ABI (Smart Collateral)

[
  {
    "name": "operate",
    "type": "function",
    "stateMutability": "payable",
    "inputs": [
      { "name": "nftId_", "type": "uint256" },
      { "name": "newColToken0_", "type": "int256" },
      { "name": "newColToken1_", "type": "int256" },
      { "name": "colSharesMinMax_", "type": "int256" },
      { "name": "newDebt_", "type": "int256" },
      { "name": "to_", "type": "address" }
    ],
    "outputs": [
      { "name": "nftId", "type": "uint256" },
      { "name": "supplyAmt", "type": "int256" },
      { "name": "borrowAmt", "type": "int256" }
    ]
  },
  {
    "name": "operatePerfect",
    "type": "function",
    "stateMutability": "payable",
    "inputs": [
      { "name": "nftId_", "type": "uint256" },
      { "name": "perfectColShares_", "type": "int256" },
      { "name": "colToken0MinMax_", "type": "int256" },
      { "name": "colToken1MinMax_", "type": "int256" },
      { "name": "newDebt_", "type": "int256" },
      { "name": "to_", "type": "address" }
    ],
    "outputs": [
      { "name": "nftId", "type": "uint256" },
      { "name": "r", "type": "int256[]" }
    ]
  }
]

VaultT3 ABI (Smart Debt)

[
  {
    "name": "operate",
    "type": "function",
    "stateMutability": "payable",
    "inputs": [
      { "name": "nftId_", "type": "uint256" },
      { "name": "newCol_", "type": "int256" },
      { "name": "newDebtToken0_", "type": "int256" },
      { "name": "newDebtToken1_", "type": "int256" },
      { "name": "debtSharesMinMax_", "type": "int256" },
      { "name": "to_", "type": "address" }
    ],
    "outputs": [
      { "name": "nftId", "type": "uint256" },
      { "name": "supplyAmt", "type": "int256" },
      { "name": "borrowAmt", "type": "int256" }
    ]
  },
  {
    "name": "operatePerfect",
    "type": "function",
    "stateMutability": "payable",
    "inputs": [
      { "name": "nftId_", "type": "uint256" },
      { "name": "newCol_", "type": "int256" },
      { "name": "perfectDebtShares_", "type": "int256" },
      { "name": "debtToken0MinMax_", "type": "int256" },
      { "name": "debtToken1MinMax_", "type": "int256" },
      { "name": "to_", "type": "address" }
    ],
    "outputs": [
      { "name": "nftId", "type": "uint256" },
      { "name": "r", "type": "int256[]" }
    ]
  }
]

VaultT4 ABI (Smart Collateral + Smart Debt)

[
  {
    "name": "operate",
    "type": "function",
    "stateMutability": "payable",
    "inputs": [
      { "name": "nftId_", "type": "uint256" },
      { "name": "newColToken0_", "type": "int256" },
      { "name": "newColToken1_", "type": "int256" },
      { "name": "colSharesMinMax_", "type": "int256" },
      { "name": "newDebtToken0_", "type": "int256" },
      { "name": "newDebtToken1_", "type": "int256" },
      { "name": "debtSharesMinMax_", "type": "int256" },
      { "name": "to_", "type": "address" }
    ],
    "outputs": [
      { "name": "nftId", "type": "uint256" },
      { "name": "supplyAmt", "type": "int256" },
      { "name": "borrowAmt", "type": "int256" }
    ]
  },
  {
    "name": "operatePerfect",
    "type": "function",
    "stateMutability": "payable",
    "inputs": [
      { "name": "nftId_", "type": "uint256" },
      { "name": "perfectColShares_", "type": "int256" },
      { "name": "colToken0MinMax_", "type": "int256" },
      { "name": "colToken1MinMax_", "type": "int256" },
      { "name": "perfectDebtShares_", "type": "int256" },
      { "name": "debtToken0MinMax_", "type": "int256" },
      { "name": "debtToken1MinMax_", "type": "int256" },
      { "name": "to_", "type": "address" }
    ],
    "outputs": [
      { "name": "nftId", "type": "uint256" },
      { "name": "r", "type": "int256[]" }
    ]
  }
]

VaultFactory ABI (ERC-721)

[
  {
    "name": "ownerOf",
    "type": "function",
    "stateMutability": "view",
    "inputs": [{ "name": "tokenId", "type": "uint256" }],
    "outputs": [{ "type": "address" }]
  },
  {
    "name": "balanceOf",
    "type": "function",
    "stateMutability": "view",
    "inputs": [{ "name": "owner", "type": "address" }],
    "outputs": [{ "type": "uint256" }]
  },
  {
    "name": "tokenOfOwnerByIndex",
    "type": "function",
    "stateMutability": "view",
    "inputs": [
      { "name": "owner", "type": "address" },
      { "name": "index", "type": "uint256" }
    ],
    "outputs": [{ "type": "uint256" }]
  },
  {
    "name": "totalSupply",
    "type": "function",
    "stateMutability": "view",
    "inputs": [],
    "outputs": [{ "type": "uint256" }]
  },
  {
    "name": "totalVaults",
    "type": "function",
    "stateMutability": "view",
    "inputs": [],
    "outputs": [{ "type": "uint256" }]
  },
  {
    "name": "getVaultAddress",
    "type": "function",
    "stateMutability": "view",
    "inputs": [{ "name": "vaultId", "type": "uint256" }],
    "outputs": [{ "type": "address" }]
  },
  {
    "name": "transferFrom",
    "type": "function",
    "stateMutability": "nonpayable",
    "inputs": [
      { "name": "from", "type": "address" },
      { "name": "to", "type": "address" },
      { "name": "tokenId", "type": "uint256" }
    ],
    "outputs": []
  }
]

Complete Working Examples

These are copy-paste-ready scripts. Install dependencies: npm install viem

Example 1: Lend USDC on Base (deposit + check position)

import { createPublicClient, createWalletClient, http, parseUnits, formatUnits } from 'viem';
import { base } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';

// --- Config ---
const RPC_URL = 'https://mainnet.base.org';
const LENDING_RESOLVER = '0x48D32f49aFeAEC7AE66ad7B9264f446fc11a1569';
const account = privateKeyToAccount(process.env.PRIVATE_KEY);

const client = createPublicClient({ chain: base, transport: http(RPC_URL) });
const wallet = createWalletClient({ account, chain: base, transport: http(RPC_URL) });

// Step 1: Find fUSDC address on Base via resolver
const resolverAbi = [
  {
    name: 'getAllFTokens', type: 'function', stateMutability: 'view',
    inputs: [], outputs: [{ type: 'address[]' }],
  },
  {
    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' },
    ]}],
  },
];

const erc20Abi = [
  {
    name: 'approve', type: 'function', stateMutability: 'nonpayable',
    inputs: [{ name: 'spender', type: 'address' }, { name: 'amount', type: 'uint256' }],
    outputs: [{ type: 'bool' }],
  },
];

const fTokenAbi = [
  {
    name: 'deposit', type: 'function', stateMutability: 'nonpayable',
    inputs: [{ name: 'assets', type: 'uint256' }, { name: 'receiver', type: 'address' }],
    outputs: [{ name: 'shares', type: 'uint256' }],
  },
];

async function lendUSDC() {
  // 1. Discover fTokens
  const fTokens = await client.readContract({
    address: LENDING_RESOLVER, abi: resolverAbi, functionName: 'getAllFTokens',
  });
  console.log(`Found ${fTokens.length} fTokens on Base`);

  // 2. Find fUSDC by checking each fToken's symbol
  let fUsdcAddress;
  for (const ft of fTokens) {
    const details = await client.readContract({
      address: LENDING_RESOLVER, abi: resolverAbi, functionName: 'getFTokenDetails', args: [ft],
    });
    if (details.symbol === 'fUSDC') {
      fUsdcAddress = ft;
      const apr = Number(details.supplyRate) / 100;
      const rewards = Number(details.rewardsRate) / 100;
      console.log(`Found fUSDC at ${ft} | APR: ${apr}% + ${rewards}% rewards = ${apr + rewards}%`);
      console.log(`TVL: ${formatUnits(details.totalAssets, 6)} USDC`);
      break;
    }
  }
  if (!fUsdcAddress) throw new Error('fUSDC not found on this chain');

  // 3. Approve + Deposit 100 USDC
  const amount = parseUnits('100', 6); // 100 USDC
  const usdcAddress = (await client.readContract({
    address: LENDING_RESOLVER, abi: resolverAbi, functionName: 'getFTokenDetails', args: [fUsdcAddress],
  })).asset;

  console.log(`Approving ${formatUnits(amount, 6)} USDC...`);
  const approveTx = await wallet.writeContract({
    address: usdcAddress, abi: erc20Abi, functionName: 'approve',
    args: [fUsdcAddress, amount],
  });
  await client.waitForTransactionReceipt({ hash: approveTx });

  console.log(`Depositing ${formatUnits(amount, 6)} USDC into fUSDC...`);
  const depositTx = await wallet.writeContract({
    address: fUsdcAddress, abi: fTokenAbi, functionName: 'deposit',
    args: [amount, account.address],
  });
  const receipt = await client.waitForTransactionReceipt({ hash: depositTx });
  console.log(`Deposited! Tx: ${receipt.transactionHash} | Block: ${receipt.blockNumber}`);

  // 4. Check position
  const position = await client.readContract({
    address: LENDING_RESOLVER, abi: resolverAbi, functionName: 'getUserPosition',
    args: [fUsdcAddress, account.address],
  });
  console.log(`Position — Shares: ${position.fTokenShares}, Worth: ${formatUnits(position.underlyingAssets, 6)} USDC`);
}

lendUSDC().catch(console.error);

Example 2: Borrow from a Vault on Base (deposit collateral + borrow)

import { createPublicClient, createWalletClient, http, parseEther, parseUnits, formatUnits, formatEther } from 'viem';
import { base } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';

// --- Config ---
const RPC_URL = 'https://mainnet.base.org';
const VAULT_RESOLVER = '0xA5C3E16523eeeDDcC34706b0E6bE88b4c6EA95cC';
const account = privateKeyToAccount(process.env.PRIVATE_KEY);

const client = createPublicClient({ chain: base, transport: http(RPC_URL) });
const wallet = createWalletClient({ account, chain: base, transport: http(RPC_URL) });

const vaultResolverAbi = [
  {
    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: 'positionsNftIdOfUser', type: 'function', stateMutability: 'view',
    inputs: [{ name: 'user_', type: 'address' }],
    outputs: [{ type: 'uint256[]' }],
  },
];

const vaultT1Abi = [
  {
    name: 'operate', type: 'function', stateMutability: 'payable',
    inputs: [
      { name: 'nftId_', type: 'uint256' },
      { name: 'newCol_', type: 'int256' },
      { name: 'newDebt_', type: 'int256' },
      { name: 'to_', type: 'address' },
    ],
    outputs: [
      { name: 'nftId', type: 'uint256' },
      { name: 'supplyAmt', type: 'int256' },
      { name: 'borrowAmt', type: 'int256' },
    ],
  },
];

async function borrowFromVault() {
  // 1. Discover vaults on Base
  const vaults = await client.readContract({
    address: VAULT_RESOLVER, abi: vaultResolverAbi, functionName: 'getAllVaultsAddresses',
  });
  console.log(`Found ${vaults.length} vaults on Base`);

  // 2. Find a T1 vault (simplest)
  let t1Vault;
  for (const v of vaults) {
    const vType = await client.readContract({
      address: VAULT_RESOLVER, abi: vaultResolverAbi, functionName: 'getVaultType', args: [v],
    });
    if (vType === 10000n) {
      t1Vault = v;
      console.log(`Found T1 vault: ${v}`);
      break;
    }
  }
  if (!t1Vault) throw new Error('No T1 vault found on this chain');

  // 3. Open position: deposit 0.1 ETH collateral, borrow 100 USDC
  const collateral = parseEther('0.1');
  const borrow = parseUnits('100', 6); // USDC has 6 decimals

  console.log(`Opening vault position...`);
  console.log(`  Collateral: ${formatEther(collateral)} ETH`);
  console.log(`  Borrow: ${formatUnits(borrow, 6)} USDC`);

  const tx = await wallet.writeContract({
    address: t1Vault,
    abi: vaultT1Abi,
    functionName: 'operate',
    args: [
      0n,           // nftId = 0 → new position
      collateral,   // deposit ETH
      BigInt(borrow), // borrow USDC
      account.address,
    ],
    value: collateral, // send ETH with the transaction
  });

  const receipt = await client.waitForTransactionReceipt({ hash: tx });
  console.log(`Position opened! Tx: ${receipt.transactionHash}`);

  // 4. Check our positions
  const nftIds = await client.readContract({
    address: VAULT_RESOLVER, abi: vaultResolverAbi,
    functionName: 'positionsNftIdOfUser', args: [account.address],
  });
  console.log(`You now have ${nftIds.length} vault position(s): [${nftIds.join(', ')}]`);
}

borrowFromVault().catch(console.error);

Both examples work on Base. Set PRIVATE_KEY env var and run with node --experimental-modules script.mjs.


Resources

Lending

Vaults

General