> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getpara.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Mellow Core Vaults

> Integrate deposits, cancellations, claims, redemptions, and fee handling for Mellow Core Vaults with Para-powered viem wallets.

This guide covers the full lifecycle for integrating deposits and redemptions with **Mellow Core Vaults**.

## 1. Architecture Overview

A **Mellow Core Vault** is a programmable, modular asset management contract. It serves as the central hub for capital management, risk control, and composable logic. **Depositors** provide capital; **Curators** manage that capital within guardrails set by the vault configuration.

All deposit and redemption flows are time-buffered through an off-chain oracle - protecting depositors against flash-loan attacks and front-running by design.

### Core Components

| Component        | Role                                                                                                                                                      |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Vault**        | Central contract. Orchestrates ACLModule (access control), ShareModule (queues & shares), VaultModule (subvault management), and BaseModule (reentrancy). |
| **DepositQueue** | Accepts token deposits, stores them as timestamped checkpoints, and mints vault shares after oracle pricing.                                              |
| **RedeemQueue**  | Accepts share redemptions, locks shares immediately, and releases assets after oracle pricing and liquidity settlement.                                   |
| **ShareManager** | ERC20-compatible contract managing share supply, whitelisting, global lockups, and compliance controls.                                                   |
| **Oracle**       | Trusted off-chain price reporter. Submits `handleReport()` with a price and timestamp.                                                                    |
| **Curator**      | Manages capital allocation across subvaults; calls `handleBatches()` to settle redemption liquidity.                                                      |

### Deposit Lifecycle

#### Async queue

Time-buffered. Oracle prices the batch; Curator settles liquidity. Claim is a separate transaction after processing.

```
Step 1 - User calls deposit(assets, referral, merkleProof)
         +--> Request stored as a timestamped checkpoint in DepositQueue

Step 2 - Handle Report submitted handleReport(priceD18, depositTimestamp)
         +--> Processes all requests older than the configured depositInterval
         +--> Shares are allocated lazily using a Fenwick tree (computed at claim time)

Step 3 - User calls DepositQueue.claim(account)
         +--> Share amount computed, deposit fee deducted, shares transferred to user
```

#### Sync queue

Shares issued or assets returned in the same transaction. No separate claim step.

```
Step 1 - User calls deposit(assets, referral, merkleProof)
         +--> Share amount computed, deposit fee deducted, shares transferred to user
```

### Redemption Lifecycle

```
Step 1 - User calls RedeemQueue.redeem(shares)
         +--> Shares locked immediately from the user's wallet

Step 2 - Handle Report submitted handleReport(priceD18, redeemTimestamp)
         +--> Prices all requests older than the configured redeemInterval

Step 3 - Curator calls RedeemQueue.handleBatches(n)
         +--> Pulls required liquidity from vault/subvaults into RedeemQueue
         +--> RedeemRequestsHandled event emitted; isClaimable becomes true

Step 4 - User calls RedeemQueue.claim(receiver, timestamps[])
         +--> Underlying assets transferred to receiver
```

***

## 2. Supported Networks

| Chain ID | Network           |
| -------- | ----------------- |
| 1        | Ethereum          |
| 8453     | Base              |
| 42161    | Arbitrum          |
| 17000    | Holesky (testnet) |
| 560048   | Hoodi (testnet)   |
| 143      | Monad (testnet)   |
| 9745     | Plasma            |
| 999      | HyperEVM          |
| 31612    | Mezo              |

***

## 3. Vault Discovery

Fetch the list of all vaults from the Mellow REST API. No authentication is required.

```
GET https://api.mellow.finance/v1/vaults
-> VaultData[]
```

```ts theme={null}
async function fetchVaults(): Promise<VaultData[]> {
  const response = await fetch('https://api.mellow.finance/v1/vaults');
  if (!response.ok) {
    throw new Error(`Failed to fetch vaults: ${response.status} ${response.statusText}`)
  }
  return response.json() as Promise<VaultData[]>
}
```

***

## 4. TypeScript Interfaces & Constants

```ts theme={null}
import type { Address } from 'viem'

// -- Token ---------------------------------------------------------------------
interface Token {
  address: Address
  symbol: string
  decimals: number
}

// -- Queue ---------------------------------------------------------------------
interface Queue {
  /** The queue contract address */
  queue: Address
  /** The token this queue accepts (for deposits) or pays out (for redemptions) */
  asset: Address
  /** When true, new submissions are rejected */
  is_paused: boolean
  /** Async queues require a separate claim step after oracle processing */
  type: 'async' | 'sync'
}

// -- VaultData -----------------------------------------------------------------
interface VaultData {
  id: string
  chain_id: number
  address: Address
  symbol: string
  /** Decimals used for vault shares - use this when parsing redeem amounts */
  decimals: number
  name: string
  base_token: Token
  deposit_tokens: Token[]
  withdraw_tokens: Token[]
  collector: Address
  deposit_queues: Queue[]
  redeem_queues: Queue[]
}

// -- RedeemRequest -------------------------------------------------------------
interface RedeemRequest {
  /** uint32 unix timestamp identifying this request */
  timestamp: bigint
  /** Shares submitted for this request */
  shares: bigint
  /** True when the oracle has processed this batch and assets can be claimed */
  isClaimable: boolean
  /** Assets available to claim (0 until isClaimable is true) */
  assets: bigint
}

// -- Constants -----------------------------------------------------------------

/** Sentinel address representing native ETH in the Mellow protocol */
const NATIVE_ETH_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE' as const

/** Maximum value for Solidity uint224 - the deposit() assets parameter type */
const UINT224_MAX = (1n << 224n) - 1n

function isNativeEth(address: string): boolean {
  return address.toLowerCase() === NATIVE_ETH_ADDRESS.toLowerCase()
}
```

***

## 5. ABIs

Only the functions and events relevant to integrations are shown here.

### 5.1 Deposit Queue ABI

```ts theme={null}
const DEPOSIT_QUEUE_ABI = [
  // -- View functions ----------------------------------------------------------
  {
    type: 'function',
    name: 'asset',
    inputs: [],
    outputs: [{ name: '', type: 'address' }],
    stateMutability: 'view',
  },
  {
    type: 'function',
    name: 'requestOf',
    inputs: [{ name: 'account', type: 'address' }],
    outputs: [
      { name: 'timestamp', type: 'uint256' },
      { name: 'assets',    type: 'uint256' },
    ],
    stateMutability: 'view',
  },
  {
    type: 'function',
    name: 'claimableOf',
    inputs: [{ name: 'account', type: 'address' }],
    outputs: [{ name: 'shares', type: 'uint256' }],  // returns claimable shares, not assets
    stateMutability: 'view',
  },
  // -- Write functions ---------------------------------------------------------
  {
    type: 'function',
    name: 'deposit',
    inputs: [
      { name: 'assets',      type: 'uint224'   },
      { name: 'referral',    type: 'address'   },
      { name: 'merkleProof', type: 'bytes32[]' },
    ],
    outputs: [],
    stateMutability: 'payable',
  },
  {
    type: 'function',
    name: 'claim',
    inputs: [{ name: 'account', type: 'address' }],
    outputs: [{ name: '', type: 'bool' }],
    stateMutability: 'nonpayable',
  },
  {
    type: 'function',
    name: 'cancelDepositRequest',
    inputs: [],
    outputs: [],
    stateMutability: 'nonpayable',
  },
  // -- Events ------------------------------------------------------------------
  {
    type: 'event',
    name: 'DepositRequested',
    inputs: [
      { name: 'account',   type: 'address', indexed: true  },
      { name: 'referral',  type: 'address', indexed: true  },
      { name: 'assets',    type: 'uint224', indexed: false },
      { name: 'timestamp', type: 'uint32',  indexed: false },
    ],
  },
  {
    type: 'event',
    name: 'DepositRequestClaimed',
    inputs: [
      { name: 'account',   type: 'address', indexed: true  },
      { name: 'shares',    type: 'uint256', indexed: false },
      { name: 'timestamp', type: 'uint32',  indexed: false },
    ],
  },
  {
    type: 'event',
    name: 'DepositRequestCanceled',
    inputs: [
      { name: 'account',   type: 'address', indexed: true  },
      { name: 'assets',    type: 'uint256', indexed: false },
      { name: 'timestamp', type: 'uint32',  indexed: false },
    ],
  },
  // -- Errors ------------------------------------------------------------------
  { type: 'error', name: 'PendingRequestExists',   inputs: [] },
  { type: 'error', name: 'ClaimableRequestExists', inputs: [] },
  { type: 'error', name: 'NoPendingRequest',        inputs: [] },
  { type: 'error', name: 'QueuePaused',             inputs: [] },
  { type: 'error', name: 'DepositNotAllowed',       inputs: [] },
  { type: 'error', name: 'ZeroValue',               inputs: [] },
  {
    type: 'error',
    name: 'InsufficientBalance',
    inputs: [
      { name: 'balance', type: 'uint256' },
      { name: 'needed',  type: 'uint256' },
    ],
  },
] as const
```

### 5.2 Redeem Queue ABI

```ts theme={null}
const REDEEM_QUEUE_ABI = [
  // -- View functions ----------------------------------------------------------
  {
    type: 'function',
    name: 'asset',
    inputs: [],
    outputs: [{ name: '', type: 'address' }],
    stateMutability: 'view',
  },
  {
    type: 'function',
    name: 'requestsOf',
    inputs: [
      { name: 'account', type: 'address' },
      { name: 'offset',  type: 'uint256' },
      { name: 'limit',   type: 'uint256' },
    ],
    outputs: [
      {
        name: 'requests',
        type: 'tuple[]',
        components: [
          { name: 'timestamp',   type: 'uint256' },
          { name: 'shares',      type: 'uint256' },
          { name: 'isClaimable', type: 'bool'    },
          { name: 'assets',      type: 'uint256' },
        ],
      },
    ],
    stateMutability: 'view',
  },
  // -- Write functions ---------------------------------------------------------
  {
    type: 'function',
    name: 'redeem',
    inputs: [{ name: 'shares', type: 'uint256' }],
    outputs: [],
    stateMutability: 'nonpayable',
  },
  {
    type: 'function',
    name: 'claim',
    inputs: [
      { name: 'receiver',   type: 'address'  },
      { name: 'timestamps', type: 'uint32[]' },
    ],
    outputs: [{ name: 'assets', type: 'uint256' }],
    stateMutability: 'nonpayable',
  },
  // -- Events ------------------------------------------------------------------
  {
    type: 'event',
    name: 'RedeemRequested',
    inputs: [
      { name: 'account',   type: 'address', indexed: true  },
      { name: 'shares',    type: 'uint256', indexed: false },
      { name: 'timestamp', type: 'uint256', indexed: false },
    ],
  },
  {
    type: 'event',
    name: 'RedeemRequestClaimed',
    inputs: [
      { name: 'account',   type: 'address', indexed: true  },
      { name: 'receiver',  type: 'address', indexed: true  },
      { name: 'assets',    type: 'uint256', indexed: false },
      { name: 'timestamp', type: 'uint32',  indexed: false },
    ],
  },
  // -- Errors ------------------------------------------------------------------
  { type: 'error', name: 'QueuePaused', inputs: [] },
  { type: 'error', name: 'ZeroValue',   inputs: [] },
  {
    type: 'error',
    name: 'InsufficientBalance',
    inputs: [
      { name: 'balance', type: 'uint256' },
      { name: 'needed',  type: 'uint256' },
    ],
  },
] as const
```

### 5.3 ERC20 ABI (subset - for approval)

```ts theme={null}
const ERC20_ABI = [
  {
    type: 'function',
    name: 'allowance',
    inputs: [
      { name: 'owner',   type: 'address' },
      { name: 'spender', type: 'address' },
    ],
    outputs: [{ name: '', type: 'uint256' }],
    stateMutability: 'view',
  },
  {
    type: 'function',
    name: 'approve',
    inputs: [
      { name: 'spender', type: 'address' },
      { name: 'amount',  type: 'uint256' },
    ],
    outputs: [{ name: '', type: 'bool' }],
    stateMutability: 'nonpayable',
  },
  {
    type: 'function',
    name: 'balanceOf',
    inputs: [{ name: 'account', type: 'address' }],
    outputs: [{ name: '', type: 'uint256' }],
    stateMutability: 'view',
  },
] as const
```

### 5.4 Vault ABI (subset - for share balance lookup)

```ts theme={null}
const VAULT_ABI = [
  {
    type: 'function',
    name: 'shareManager',
    inputs: [],
    outputs: [{ name: '', type: 'address' }],
    stateMutability: 'view',
  },
] as const
```

The `shareManager` address is itself an ERC20-compatible contract. Use `ERC20_ABI` with `balanceOf`, or use the richer `SHARE_MANAGER_ABI` below for more precise balance reads.

### 5.5 ShareManager ABI (subset)

```ts theme={null}
const SHARE_MANAGER_ABI = [
  // -- Balance reads (prefer these over raw balanceOf) -------------------------
  {
    type: 'function',
    name: 'sharesOf',
    inputs: [{ name: 'account', type: 'address' }],
    outputs: [{ name: '', type: 'uint256' }],
    stateMutability: 'view',
  },
  {
    type: 'function',
    name: 'activeSharesOf',
    inputs: [{ name: 'account', type: 'address' }],
    outputs: [{ name: '', type: 'uint256' }],
    stateMutability: 'view',
    // Returns only the shares that are not locked in a redeem queue.
    // Use this to check the redeemable balance.
  },
  {
    type: 'function',
    name: 'claimableSharesOf',
    inputs: [{ name: 'account', type: 'address' }],
    outputs: [{ name: '', type: 'uint256' }],
    stateMutability: 'view',
    // Shares processed by the oracle and awaiting DepositQueue.claim().
  },
  // -- Whitelist / permissions --------------------------------------------------
  {
    type: 'function',
    name: 'flags',
    inputs: [],
    outputs: [
      {
        name: '',
        type: 'tuple',
        components: [
          { name: 'hasMintPause',          type: 'bool'   },
          { name: 'hasBurnPause',          type: 'bool'   },
          { name: 'hasTransferPause',      type: 'bool'   },
          { name: 'hasWhitelist',          type: 'bool'   }, // deposit whitelist active
          { name: 'hasTransferWhitelist',  type: 'bool'   },
          { name: 'globalLockup',          type: 'uint32' }, // seconds all shares are locked after mint
        ],
      },
    ],
    stateMutability: 'view',
  },
  {
    type: 'function',
    name: 'isDepositorWhitelisted',
    inputs: [
      { name: 'account',     type: 'address'   },
      { name: 'merkleProof', type: 'bytes32[]' },
    ],
    outputs: [{ name: '', type: 'bool' }],
    stateMutability: 'view',
  },
] as const

interface ShareManagerFlags {
  hasMintPause:         boolean
  hasBurnPause:         boolean
  hasTransferPause:     boolean
  hasWhitelist:         boolean  // when true, deposits require a valid Merkle proof
  hasTransferWhitelist: boolean
  globalLockup:         number   // seconds before newly minted shares become transferable
}
```

***

## 6. Deposit

### Overview

Depositing submits tokens to a `DepositQueue` contract. For **async** queues the shares are not immediately available - an oracle processes the batch and sets a price, after which the user calls `claim` to receive their shares.

```
Step 1 - approve token spend (ERC20 only)
Step 2 - call deposit()
                   v
          [oracle processes batch]
                   v
Step 3 - call claim() to receive vault shares
```

### Steps

1. Pick a deposit queue from `vault.deposit_queues`. You can match by `queue.asset` address and `queue.type` deposit type ("sync" | "async").
2. Check `queue.is_paused === false`. Throw early if paused.
3. **Whitelist check (permissioned vaults only):** Read `shareManager.flags()`. If `flags.hasWhitelist === true`, call `shareManager.isDepositorWhitelisted(userAddress, merkleProof)`. If it returns `false`, the deposit will revert with `DepositNotAllowed`. For public vaults (`hasWhitelist === false`), pass `[]` as the proof.
4. Find the matching `Token` in `vault.deposit_tokens` for `queue.asset`.
5. Parse the human-readable amount: `parseUnits(amount, token.decimals)`.
6. Validate `parsedAmount > 0n` and `parsedAmount <= UINT224_MAX`.
7. Only one pending deposit request per user is allowed per queue. For async queues, call `requestOf(userAddress)` and check `timestamp === 0n` before depositing.
8. **If the asset is native ETH** (`queue.asset === NATIVE_ETH_ADDRESS`): check the user's ETH balance, then call `deposit()` with `value = parsedAmount`.
9. **If the asset is an ERC20**:
   * Read `allowance(userAddress, queueAddress)`.
   * If `currentAllowance > 0n`, send `approve(queueAddress, 0n)` first. This is required for tokens like USDT that revert if you set a non-zero allowance on top of an existing one.
   * Send `approve(queueAddress, parsedAmount)`.
   * Then call `deposit(parsedAmount, zeroAddress, merkleProof)` with `value = 0n`.
10. For **async** queues: do not expect shares immediately. Poll `claimableOf` or listen for `DepositRequestClaimed` events, then call `claim`.

### Code Example

```ts theme={null}
import { createParaViemAccount, createParaViemClient } from "@getpara/viem-v2-integration";
import {
  createPublicClient,
  http,
  parseUnits,
  zeroAddress,
} from 'viem';
import { mainnet } from 'viem/chains';
import Para from "@getpara/web-sdk";

const para = new Para("YOUR_API_KEY");

// Authenticate first...

const account = createParaViemAccount({ para });
const publicClient = createPublicClient({ chain: mainnet, transport: http() });
const walletClient = createParaViemClient({ para, walletClientConfig: {
  account,
  chain: mainnet,
  transport: http(),
}});

async function deposit(
  vault: VaultData,
  queueAddress: string,
  humanAmount: string,
  // Pass [] for public vaults. For whitelisted vaults, obtain proof from the Mellow API.
  merkleProof: `0x${string}`[] = [],
) {
  const userAddress = account.address;

  // 1. Find the queue and validate it is open
  const queue = vault.deposit_queues.find(q => q.queue.toLowerCase() === queueAddress.toLowerCase());
  if (!queue) throw new Error('Queue not found');
  if (queue.is_paused) throw new Error('Queue is paused');

  // 2. Whitelist check - read shareManager.flags() to see if this vault requires a proof
  const shareManagerAddress = await publicClient.readContract({
    address: vault.address,
    abi: VAULT_ABI,
    functionName: 'shareManager',
  });
  const flags = await publicClient.readContract({
    address: shareManagerAddress,
    abi: SHARE_MANAGER_ABI,
    functionName: 'flags',
  }) as ShareManagerFlags;

  if (flags.hasWhitelist) {
    const allowed = await publicClient.readContract({
      address: shareManagerAddress,
      abi: SHARE_MANAGER_ABI,
      functionName: 'isDepositorWhitelisted',
      args: [userAddress, merkleProof],
    });
    if (!allowed) throw new Error('Address is not whitelisted for this vault');
  }

  // 3. Resolve token metadata
  const token = vault.deposit_tokens.find(t => t.address.toLowerCase() === queue.asset.toLowerCase());
  if (!token) throw new Error('Token not found');

  // 4. Parse and validate amount
  const parsedAmount = parseUnits(humanAmount, token.decimals);
  if (parsedAmount <= 0n) throw new Error('Amount must be greater than zero');
  if (parsedAmount > UINT224_MAX) throw new Error('Amount exceeds maximum (uint224)');

  const native = isNativeEth(queue.asset);

  if (native) {
    // -- Native ETH path --------------------------------------------------------
    const balance = await publicClient.getBalance({ address: userAddress });
    if (parsedAmount > balance) throw new Error('Insufficient ETH balance');

    const hash = await walletClient.writeContract({
      address: queue.queue as `0x${string}`,
      abi: DEPOSIT_QUEUE_ABI,
      functionName: 'deposit',
      args: [parsedAmount, zeroAddress, merkleProof],
      value: parsedAmount,
    });
    return publicClient.waitForTransactionReceipt({ hash });
  }
  else {
    // -- ERC20 path -------------------------------------------------------------
    // Check balance
    const balance = await publicClient.readContract({
      address: queue.asset as `0x${string}`,
      abi: ERC20_ABI,
      functionName: 'balanceOf',
      args: [userAddress],
    });
    if (parsedAmount > balance) throw new Error('Insufficient token balance');

    // Check for pending request - only one pending request per user per queue is allowed
    if (queue.type === 'async') {
      const [timestamp] = await publicClient.readContract({
        address: queue.queue as `0x${string}`,
        abi: DEPOSIT_QUEUE_ABI,
        functionName: 'requestOf',
        args: [userAddress],
      });
      if (timestamp > 0n) {
        throw new Error('Pending deposit request already exists - cancel or claim it first');
      }
    }

    // Read allowance BEFORE sending any transactions
    const currentAllowance = await publicClient.readContract({
      address: queue.asset as `0x${string}`,
      abi: ERC20_ABI,
      functionName: 'allowance',
      args: [userAddress, queue.queue as `0x${string}`],
    })

    // Reset allowance if needed (required for tokens like USDT)
    if (currentAllowance > 0n) {
      const resetHash = await walletClient.writeContract({
        address: queue.asset as `0x${string}`,
        abi: ERC20_ABI,
        functionName: 'approve',
        args: [queue.queue as `0x${string}`, 0n],
      });
      await publicClient.waitForTransactionReceipt({ hash: resetHash });
    }

    // Approve exact amount
    const approveHash = await walletClient.writeContract({
      address: queue.asset as `0x${string}`,
      abi: ERC20_ABI,
      functionName: 'approve',
      args: [queue.queue as `0x${string}`, parsedAmount],
    });
    await publicClient.waitForTransactionReceipt({ hash: approveHash });

    // Deposit
    const depositHash = await walletClient.writeContract({
      address: queue.queue as `0x${string}`,
      abi: DEPOSIT_QUEUE_ABI,
      functionName: 'deposit',
      args: [parsedAmount, zeroAddress, merkleProof],
      value: 0n,
    });
    return publicClient.waitForTransactionReceipt({ hash: depositHash });
  }
}
```

<Note>
  **Referral address:** Pass `zeroAddress` (`0x0000000000000000000000000000000000000000`) unless you have been issued a referral address by the Mellow team.
</Note>

<Warning>
  **Merkle proof and whitelisting:** Pass `[]` for public vaults. When `shareManager.flags().hasWhitelist === true`, the vault is permissioned. `deposit()` will revert with `DepositNotAllowed` if the proof is invalid or empty. Contact the Mellow team or query the Mellow API to obtain a valid proof for whitelisted addresses.
</Warning>

***

## 7. Cancel Deposit

### Overview

A pending async deposit request can be cancelled before the oracle processes it. Cancelling returns the deposited tokens to the user.

You **cannot** cancel once the request is claimable - call `claim` instead.

### Steps

1. Call `requestOf(userAddress)` on the deposit queue. Check `timestamp > 0n` - if zero, there is no pending request.
2. Call `claimableOf(userAddress)`. If `> 0n`, the oracle has already processed the request - you must claim it, not cancel it.
3. Call `cancelDepositRequest()`. This function takes no arguments - it cancels the caller's own request.

### Code Example

```ts theme={null}
async function cancelDeposit(queueAddress: string) {
  const userAddress = account.address;

  // 1. Check for a pending request
  const [timestamp] = await publicClient.readContract({
    address: queueAddress as `0x${string}`,
    abi: DEPOSIT_QUEUE_ABI,
    functionName: 'requestOf',
    args: [userAddress],
  });
  if (timestamp === 0n) throw new Error('No pending deposit request to cancel');

  // 2. Check whether it is already claimable
  const claimable = await publicClient.readContract({
    address: queueAddress as `0x${string}`,
    abi: DEPOSIT_QUEUE_ABI,
    functionName: 'claimableOf',
    args: [userAddress],
  });
  if (claimable > 0n) {
    throw new Error('Request already processed - call claim() instead of cancel');
  }

  // 3. Cancel
  const hash = await walletClient.writeContract({
    address: queueAddress as `0x${string}`,
    abi: DEPOSIT_QUEUE_ABI,
    functionName: 'cancelDepositRequest',
    args: [],
  });
  return publicClient.waitForTransactionReceipt({ hash });
}
```

***

## 8. Claim Deposit (async)

### Overview

After the oracle processes an async deposit request, vault shares are held in the queue contract ready for collection. Call `claim` to transfer them to the user.

### Steps

1. Call `claimableOf(userAddress)`. If `> 0n`, shares are ready to claim.
2. If `claimableOf` returns `0n`, call `requestOf(userAddress)`. If `timestamp > 0n`, the oracle has not yet processed the request - wait and retry later.
3. Call `claim(userAddress)`. Returns `true` when shares are successfully transferred.

### Code Example

```ts theme={null}
async function claimDeposit(queueAddress: string) {
  const userAddress = account.address;

  // 1. Check if there is anything to claim
  const claimable = await publicClient.readContract({
    address: queueAddress as `0x${string}`,
    abi: DEPOSIT_QUEUE_ABI,
    functionName: 'claimableOf',
    args: [userAddress],
  });

  if (claimable === 0n) {
    // Check if still pending
    const [timestamp] = await publicClient.readContract({
      address: queueAddress as `0x${string}`,
      abi: DEPOSIT_QUEUE_ABI,
      functionName: 'requestOf',
      args: [userAddress],
    });
    if (timestamp > 0n) {
      throw new Error('Deposit is pending oracle processing - try again later');
    }
    throw new Error('No claimable deposit found');
  }

  // 2. Claim shares
  const hash = await walletClient.writeContract({
    address: queueAddress as `0x${string}`,
    abi: DEPOSIT_QUEUE_ABI,
    functionName: 'claim',
    args: [userAddress],
  });
  return publicClient.waitForTransactionReceipt({ hash });
}
```

***

## 9. Redeem

### Overview

Redemption burns vault shares and, after oracle processing, returns the underlying asset to the user. Redeem queues are **always async** - there is always a separate claim step.

```
Step 1 - call redeem(shares)
                   v
          [oracle processes batch]
                   v
Step 2 - call claim(receiver, timestamps)
```

### Steps

1. Pick a redeem queue from `vault.redeem_queues`.
2. Check `queue.is_paused === false`.
3. Fetch the user's redeemable share balance:
   * Call `vault.shareManager()` to get the share manager address.
   * Call `shareManager.activeSharesOf(userAddress)` - this returns only shares that are **not** currently locked in a pending redeem request. Use `sharesOf` if you want the total including locked shares.
4. Parse the share amount using **vault decimals** (`vault.decimals`), **not** the token's decimals. This is a common mistake - the share token uses the vault's decimal precision.
5. Validate `parsedShares > 0n` and `parsedShares <= activeShareBalance`.
6. Call `redeem(parsedShares)`. No ETH value, no ERC20 approval - the vault contract locks shares directly from the caller.
7. Wait for oracle processing and `handleBatches()`, then call `claimRedeem` (see Section 10).

<Note>
  **Multiple requests are allowed.** Unlike deposits, a user can have many concurrent redemption requests. Each `redeem()` call creates a new request with its own timestamp.
</Note>

<Warning>
  **Requests cannot be cancelled.** Once submitted, a redemption request is permanent. This is intentional. Cancellable redemptions would allow yield-griefing by requesting redemption to force liquidity pulls from external protocols, then withdrawing the request. The locked shares remain locked until claimed.
</Warning>

<Info>
  **Settlement flow:** After `redeem()`, two off-chain steps must happen before you can claim: (1) the oracle calls `handleReport()` to price the batch, then (2) the Curator calls `handleBatches()` on the RedeemQueue to pull the required liquidity from subvaults. Listen for the `RedeemRequestsHandled` event. It fires when `handleBatches()` settles one or more batches and requests become claimable. The timing depends on vault configuration (`redeemInterval`) and curator activity, typically ranging from minutes to hours.
</Info>

### Code Example

```ts theme={null}
async function redeem(vault: VaultData, queueAddress: string, humanShares: string) {
  const userAddress = account.address;

  // 1. Find the queue
  const queue = vault.redeem_queues.find(q => q.queue.toLowerCase() === queueAddress.toLowerCase());
  if (!queue) throw new Error('Redeem queue not found');
  if (queue.is_paused) throw new Error('Redeem queue is paused');

  // 2. Get redeemable share balance via vault's shareManager
  //    activeSharesOf excludes shares already locked in pending redeem requests
  const shareManagerAddress = await publicClient.readContract({
    address: vault.address,
    abi: VAULT_ABI,
    functionName: 'shareManager',
  });
  const activeShareBalance = await publicClient.readContract({
    address: shareManagerAddress,
    abi: SHARE_MANAGER_ABI,
    functionName: 'activeSharesOf',
    args: [userAddress],
  });

  // 3. Parse share amount using vault decimals (NOT the output token's decimals)
  const parsedShares = parseUnits(humanShares, vault.decimals);
  if (parsedShares <= 0n) throw new Error('Redeem amount must be greater than zero');
  if (parsedShares > activeShareBalance) {
    throw new Error(`Insufficient redeemable share balance: have ${activeShareBalance}, need ${parsedShares}`);
  }

  // 4. Submit redeem - no approval required, vault locks shares from caller
  const hash = await walletClient.writeContract({
    address: queue.queue as `0x${string}`,
    abi: REDEEM_QUEUE_ABI,
    functionName: 'redeem',
    args: [parsedShares],
  });
  return publicClient.waitForTransactionReceipt({ hash });
}
```

<Note>
  **No approval needed:** Unlike deposits, redemptions do not require an ERC20 `approve`. The vault contract has the authority to lock and burn shares on behalf of the caller.
</Note>

***

## 10. Claim Redeem

### Overview

A user may accumulate multiple redemption requests over time. The `requestsOf` function returns all requests paginated. Once the oracle marks a request `isClaimable`, the user can batch-claim them by passing the corresponding timestamps to `claim`.

### Steps

1. Paginate `requestsOf(userAddress, offset, 100)`:
   * Start with `offset = 0`.
   * Increment by `100` each iteration.
   * Stop when a page returns fewer than `100` items.
2. Filter to requests where `isClaimable === true`.
3. Extract the `timestamp` field from each claimable request. Cast to `number` - timestamps are `uint32` values, safely representable as JavaScript numbers until year 2106.
4. Call `claim(userAddress, timestamps)`. Returns the total `assets` transferred.

<Note>
  **`claim()` is idempotent.** Non-claimable or already-claimed timestamps are silently skipped. The contract does not revert. You may safely pass all known timestamps and let the contract filter them.
</Note>

### Code Example

```ts theme={null}
async function claimRedeem(vault: VaultData, queueAddress: string) {
  const userAddress = account.address;
  const PAGE_SIZE = 100;

  // 1. Collect all requests via pagination
  const allRequests: RedeemRequest[] = [];
  let offset = 0;

  while (true) {
    const page = await publicClient.readContract({
      address: queueAddress as `0x${string}`,
      abi: REDEEM_QUEUE_ABI,
      functionName: 'requestsOf',
      args: [userAddress, BigInt(offset), BigInt(PAGE_SIZE)],
    }) as RedeemRequest[];

    allRequests.push(...page);
    if (page.length < PAGE_SIZE) break;
    offset += PAGE_SIZE;
  }

  if (allRequests.length === 0) {
    throw new Error('No redemption requests found');
  }

  // 2. Filter to claimable requests
  const claimable = allRequests.filter(r => r.isClaimable);
  if (claimable.length === 0) {
    throw new Error(
      `${allRequests.length} redemption request(s) are pending oracle processing - try again later`,
    );
  }

  // 3. Extract timestamps as number[] (uint32 - safe as JS number)
  const timestamps = claimable.map(r => Number(r.timestamp));

  console.log(`Claiming ${claimable.length} of ${allRequests.length} redemption request(s)...`);

  // 4. Batch claim
  const hash = await walletClient.writeContract({
    address: queueAddress as `0x${string}`,
    abi: REDEEM_QUEUE_ABI,
    functionName: 'claim',
    args: [userAddress, timestamps],
  });
  const receipt = await publicClient.waitForTransactionReceipt({ hash });

  const pending = allRequests.length - claimable.length;
  if (pending > 0) {
    console.log(`${pending} request(s) are still pending and will need to be claimed later.`);
  }

  return receipt;
}
```

***

## 11. Fees

Fees in Mellow Core Vaults are paid in **vault shares**, not in underlying assets. The `FeeManager` contract calculates and deducts fees automatically during oracle report handling - integrators do not call fee functions directly.

| Fee Type            | When Applied                   | Effect on Integrator                                                                                   |
| ------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------ |
| **Deposit fee**     | At `DepositQueue.claim()` time | User receives fewer shares than the raw price implies. Calculated as `shares * depositFeeD6 / 1e6`.    |
| **Redeem fee**      | At `RedeemQueue.redeem()` time | A portion of shares is deducted before the redemption amount is finalized.                             |
| **Performance fee** | Oracle report trigger          | Accrued to the vault as yield is generated; does not directly affect per-request calculations.         |
| **Protocol fee**    | Continuous accrual             | Time-based, deducted from share supply; transparent to depositors but reduces NAV per share over time. |

<Note>
  Fees are vault-specific and set by Curators. Check the vault configuration or the Mellow API for exact fee parameters before displaying estimated returns to users.
</Note>

***

## 12. Error Reference

| Error                    | Contract     | When it occurs                                                                        | What to do                                                                          |
| ------------------------ | ------------ | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `PendingRequestExists`   | DepositQueue | `deposit()` called when a pending request already exists                              | Call `cancelDepositRequest()` first, or wait for oracle and then `claim()`          |
| `ClaimableRequestExists` | DepositQueue | `cancelDepositRequest()` called when request is already claimable                     | The oracle processed the request - call `claim()` instead                           |
| `NoPendingRequest`       | DepositQueue | `cancelDepositRequest()` called with no pending request                               | Nothing to cancel                                                                   |
| `QueuePaused`            | Both         | Queue is temporarily suspended                                                        | Check `queue.is_paused` before submitting; wait for it to re-open                   |
| `DepositNotAllowed`      | DepositQueue | Deposit rejected - queue paused, or vault has a whitelist and address is not included | Check `flags.hasWhitelist`; if true, obtain a valid Merkle proof via the Mellow API |
| `ZeroValue`              | RedeemQueue  | `redeem()` called with `shares = 0`                                                   | Validate amount > 0 before calling                                                  |
| `InsufficientBalance`    | Both         | Token or share balance too low                                                        | Validate balance on-chain before submitting                                         |
| `Forbidden`              | Both         | Caller does not have the required role for the called function                        | This is a contract-operator error; user-facing code should not hit this             |

***

## 13. Events Reference

Listen for these events to drive UI state or index on-chain activity.

| Event                                                        | Contract     | Emitted when                                                                                                                |
| ------------------------------------------------------------ | ------------ | --------------------------------------------------------------------------------------------------------------------------- |
| `DepositRequested(account, referral, assets, timestamp)`     | DepositQueue | `deposit()` succeeds                                                                                                        |
| `DepositRequestClaimed(account, shares, timestamp)`          | DepositQueue | `claim()` succeeds - user received vault shares                                                                             |
| `DepositRequestCanceled(account, assets, timestamp)`         | DepositQueue | `cancelDepositRequest()` succeeds - tokens returned                                                                         |
| `RedeemRequested(account, shares, timestamp)`                | RedeemQueue  | `redeem()` succeeds                                                                                                         |
| `RedeemRequestsHandled(counter, demand)`                     | RedeemQueue  | Curator called `handleBatches()` and settled one or more batches - **listen for this to know when claims become available** |
| `RedeemRequestClaimed(account, receiver, assets, timestamp)` | RedeemQueue  | `claim()` succeeds - user received underlying assets                                                                        |

### Example: watching for claim events with viem

```ts theme={null}
const unwatch = publicClient.watchContractEvent({
  address: depositQueueAddress as `0x${string}`,
  abi: DEPOSIT_QUEUE_ABI,
  eventName: 'DepositRequestClaimed',
  args: { account: userAddress },
  onLogs: (logs) => {
    for (const log of logs) {
      console.log(`Shares received: ${log.args.shares}, timestamp: ${log.args.timestamp}`)
    }
  },
});

// Stop watching when done
unwatch();
```
