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

# Core Vaults Integration Guide

> Reference for integrating deposit and redemption flows with Mellow Core Vaults, including contract ABIs, supported networks, fees, errors, and events, with TypeScript examples.

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[]
```

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

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

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

```typescript 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)

```typescript 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)

```typescript 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)

```typescript 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()
                   ↓
          [oracle processes batch]
                   ↓
Step 3 — call claim() to receive vault shares
```

### Steps

<Steps>
  <Step title="Pick a deposit queue">
    Pick a deposit queue from `vault.deposit_queues`. You can match by `queue.asset` address and `queue.type` deposit type (`"sync"` | `"async"`).
  </Step>

  <Step title="Check whether the queue is paused">
    Check `queue.is_paused === false`. Throw early if paused.
  </Step>

  <Step title="Check whitelist requirements">
    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.
  </Step>

  <Step title="Resolve token metadata">
    Find the matching `Token` in `vault.deposit_tokens` for `queue.asset`.
  </Step>

  <Step title="Parse and validate the amount">
    Parse the human-readable amount with `parseUnits(amount, token.decimals)`.

    Validate that:

    * `parsedAmount > 0n`
    * `parsedAmount <= UINT224_MAX`
  </Step>

  <Step title="Check for an existing pending request">
    Only one pending deposit request per user is allowed per queue. For async queues, call `requestOf(userAddress)` and check `timestamp === 0n` before depositing.
  </Step>

  <Step title="Handle native ETH deposits">
    If the asset is native ETH (`queue.asset === NATIVE_ETH_ADDRESS`), check the user's ETH balance, then call `deposit()` with `value = parsedAmount`.
  </Step>

  <Step title="Handle ERC20 deposits">
    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`.
  </Step>

  <Step title="Wait for async claimability">
    For **async** queues: do not expect shares immediately. Poll `claimableOf` or listen for `DepositRequestClaimed` events, then call `claim`.
  </Step>
</Steps>

### Code Example

```typescript theme={null}
import {
  createPublicClient,
  createWalletClient,
  http,
  parseUnits,
  zeroAddress,
} from 'viem'
import { mainnet } from 'viem/chains'
import { privateKeyToAccount } from 'viem/accounts'

const account  = privateKeyToAccount('0xYOUR_PRIVATE_KEY')
const publicClient = createPublicClient({ chain: mainnet, transport: http() })
const walletClient = createWalletClient({ 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 })
  }
}
```

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

<Warning>
  **Merkle proof & 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

<Steps>
  <Step title="Check for a pending request">
    Call `requestOf(userAddress)` on the deposit queue. Check `timestamp > 0n` — if zero, there is no pending request.
  </Step>

  <Step title="Check whether it is already claimable">
    Call `claimableOf(userAddress)`. If `> 0n`, the oracle has already processed the request — you must claim it, not cancel it.
  </Step>

  <Step title="Cancel the request">
    Call `cancelDepositRequest()`. This function takes no arguments — it cancels the caller's own request.
  </Step>
</Steps>

### Code Example

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

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

<Steps>
  <Step title="Check whether there is anything to claim">
    Call `claimableOf(userAddress)`. If `> 0n`, shares are ready to claim.
  </Step>

  <Step title="Check whether the request is still pending">
    If `claimableOf` returns `0n`, call `requestOf(userAddress)`. If `timestamp > 0n`, the oracle has not yet processed the request — wait and retry later.
  </Step>

  <Step title="Claim the shares">
    Call `claim(userAddress)`. Returns `true` when shares are successfully transferred.
  </Step>
</Steps>

### Code Example

```typescript 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)
                   ↓
          [oracle processes batch]
                   ↓
Step 2 — call claim(receiver, timestamps)
```

### Steps

<Steps>
  <Step title="Pick a redeem queue">
    Pick a redeem queue from `vault.redeem_queues`.
  </Step>

  <Step title="Check whether the queue is paused">
    Check `queue.is_paused === false`.
  </Step>

  <Step title="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.
  </Step>

  <Step title="Parse the share amount with vault decimals">
    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.
  </Step>

  <Step title="Validate the amount">
    Validate `parsedShares > 0n` and `parsedShares <= activeShareBalance`.
  </Step>

  <Step title="Submit the redeem request">
    Call `redeem(parsedShares)`. No ETH value, no ERC20 approval — the vault contract locks shares directly from the caller.
  </Step>

  <Step title="Wait for settlement and claim later">
    Wait for oracle processing and `handleBatches()`, then call `claimRedeem` (see Section 10).
  </Step>
</Steps>

<Info>
  **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.
</Info>

<Warning>
  **Requests cannot be cancelled.** Once submitted, a redemption request is permanent. This is intentional — cancellable redemptions would allow yield-griefing (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

```typescript 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 })
}
```

<Info>
  **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.
</Info>

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

<Steps>
  <Step title="Paginate redemption requests">
    Paginate `requestsOf(userAddress, offset, 100)`:

    * Start with `offset = 0`.
    * Increment by `100` each iteration.
    * Stop when a page returns fewer than `100` items.
  </Step>

  <Step title="Filter claimable requests">
    Filter to requests where `isClaimable === true`.
  </Step>

  <Step title="Extract timestamps">
    Extract the `timestamp` field from each claimable request. Cast to `number` — timestamps are `uint32` values, safely representable as JavaScript numbers until year 2106.
  </Step>

  <Step title="Batch-claim the requests">
    Call `claim(userAddress, timestamps)`. Returns the total `assets` transferred.
  </Step>
</Steps>

<Info>
  **`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.
</Info>

### Code Example

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

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

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

```typescript 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()
```

## 14. Integration with Para

Para is a wallet and authentication suite for crypto and fintech developers. It provides infrastructure for embedded wallets, external wallet connections, authentication, and transaction flows inside third-party applications.

Applications built with Para can integrate Mellow Core Vault interactions directly into their own interface. This allows teams using Para for the wallet layer to add direct Mellow Vaults access without building separate wallet or vault interaction infrastructure from scratch.

Para maintains a dedicated walkthrough for integrating Mellow Core Vaults with Para-powered wallets. The walkthrough covers the Para-side implementation for Core Vault interactions.

Get started with the [Mellow Core Vaults walkthrough in Para Docs](https://docs.getpara.com/v3/walkthroughs/mellow-core-vaults).
