# Fluid

> Fluid Protocol

- Skill: `basezh/fluid` (Agent Skill)
- Install (CLI): `npx skillmds@latest add basezh/fluid`
- Raw SKILL.md: https://api.skillmd.com/api/skills/basezh/fluid/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: basezh (https://skillmd.com/u/basezh)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/basezh/fluid

---


# 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](https://github.com/Instadapp/fluid-contracts-public/blob/main/deployments/deployments.md) | ✅ 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 use `convertToAssets()` or `convertToShares()` to convert between them. The exchange rate at genesis is approximately 1:1 but diverges over time.

```javascript
// 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

```bash
# 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

```bash
# 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

```bash
# 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](https://etherscan.io/address/0x48D32f49aFeAEC7AE66ad7B9264f446fc11a1569#code) |
| Arbitrum | [Arbiscan](https://arbiscan.io/address/0x48D32f49aFeAEC7AE66ad7B9264f446fc11a1569#code) |
| Base | [Basescan](https://basescan.org/address/0x48D32f49aFeAEC7AE66ad7B9264f446fc11a1569#code) |
| Polygon | [Polygonscan](https://polygonscan.com/address/0x48D32f49aFeAEC7AE66ad7B9264f446fc11a1569#code) |
| Plasma | [Plasmascan](https://plasmascan.to/address/0x48D32f49aFeAEC7AE66ad7B9264f446fc11a1569#code) |

### LendingFactory

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

| Network | Explorer (verified source) |
| -------- | ---------------------------------------------------------------------------------------------- |
| Ethereum | [Etherscan](https://etherscan.io/address/0x54B91A0D94cb471F37f949c60F7Fa7935b551D03#code) |
| Arbitrum | [Arbiscan](https://arbiscan.io/address/0x54B91A0D94cb471F37f949c60F7Fa7935b551D03#code) |
| Base | [Basescan](https://basescan.org/address/0x54B91A0D94cb471F37f949c60F7Fa7935b551D03#code) |
| Polygon | [Polygonscan](https://polygonscan.com/address/0x54B91A0D94cb471F37f949c60F7Fa7935b551D03#code) |
| Plasma | [Plasmascan](https://plasmascan.to/address/0x54B91A0D94cb471F37f949c60F7Fa7935b551D03#code) |

**Source:** [deployments.md](https://github.com/Instadapp/fluid-contracts-public/blob/main/deployments/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`

```javascript
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

```javascript
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

```javascript
const fWethAddress = '0x90551c1795392094FE6D29B758EcCD233cFAa260';
const amount = parseEther('1'); // 1 ETH

// Deposit ETH directly (payable)
const shares = await fWeth.write.depositNative([userAddress], { value: amount });
```

### Withdraw Assets

```javascript
// 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

```javascript
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

```javascript
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

```javascript
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:

```javascript
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):**

```bash
# 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:

```javascript
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

```json
[
  {
    "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)

```json
[
  {
    "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

```json
[
  {
    "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):**

```bash
# 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):**

```bash
# 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:

| 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.

```javascript
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):**

```bash
# 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

```bash
# 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

```bash
# 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)

```bash
# 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

```bash
# 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)
