Skip to main content
Multi-depositor vaults extend the base guardian workflow with shared liquidity across multiple depositors, tokenized vault units (ERC-20), and an async order-solving model for deposits and redemptions. For background on how the guardian role works at the protocol level, see the Guardian Model. For base guardian operations like strategy execution and vault reads, see the Single-Depositor Vaults guide — this page focuses on the operations unique to multi-depositor vaults.
Before starting, make sure you have:

Setup

Configure your environment for interacting with the vault, Provisioner, and PriceAndFeeCalculator contracts. Multi-depositor vaults require more contract addresses than single-depositor vaults because pricing and order fulfillment involve additional periphery contracts.
import {
  createPublicClient,
  createWalletClient,
  http,
} from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { mainnet } from 'viem/chains'

const account = privateKeyToAccount('0xYOUR_PRIVATE_KEY')

const publicClient = createPublicClient({
  chain: mainnet,
  transport: http(process.env.RPC_URL_ETHEREUM!),
})

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

// Contract addresses -- see Contract Reference for deployed addresses
// https://docs.gauntlet.xyz/contract-reference/addresses
const VAULT_ADDRESS = '0x...' as const
const PROVISIONER_ADDRESS = '0x...' as const
const PRICE_FEE_CALCULATOR_ADDRESS = '0x...' as const
const FEE_VAULT_ADDRESS = '0x...' as const

Strategy Execution

Strategy execution in multi-depositor vaults uses the same submit(Operation[]) pattern as single-depositor vaults — the guardian submits batched operations that are validated against a Merkle tree. See the Single-Depositor Vaults guide for full details on operation submission, vault state reads, and rebalancing.
// Minimal ABI fragment for submit -- see BaseVault Contract Reference for full ABI
const baseVaultAbi = [
  {
    name: 'submit',
    type: 'function',
    stateMutability: 'nonpayable',
    inputs: [{
      name: 'operations',
      type: 'tuple[]',
      components: [
        { name: 'target', type: 'address' },
        { name: 'data', type: 'bytes' },
        { name: 'value', type: 'uint256' },
        { name: 'proof', type: 'bytes32[]' },
        { name: 'preHook', type: 'address' },
        { name: 'preHookData', type: 'bytes' },
        { name: 'postHook', type: 'address' },
        { name: 'postHookData', type: 'bytes' },
      ],
    }],
    outputs: [],
  },
] as const

// Example: single operation in an MDV context
const operations = [
  {
    target: '0x...' as `0x${string}`,   // Target protocol contract
    data: '0x...' as `0x${string}`,     // Encoded function call
    value: 0n,
    proof: [] as `0x${string}`[],       // Merkle proof for this operation
    preHook: '0x0000000000000000000000000000000000000000' as `0x${string}`,
    preHookData: '0x' as `0x${string}`,
    postHook: '0x0000000000000000000000000000000000000000' as `0x${string}`,
    postHookData: '0x' as `0x${string}`,
  },
]

const { request } = await publicClient.simulateContract({
  address: VAULT_ADDRESS,
  abi: baseVaultAbi,
  functionName: 'submit',
  args: [operations],
  account,
})

const txHash = await walletClient.writeContract(request)
await publicClient.waitForTransactionReceipt({ hash: txHash, confirmations: 2 })

Order Solving

Order solving is the primary difference between multi-depositor and single-depositor vault operations. Depositors interact with the vault through the Provisioner contract using an asynchronous request/fulfill lifecycle. The guardian (acting as solver) monitors pending requests and fulfills them through the vault’s submit flow.

How Orders Work

The async order lifecycle has three stages:
  1. Request — A depositor calls requestDeposit or requestRedeem on the Provisioner, locking their assets (for deposits) or vault units (for redemptions). Each request receives a unique requestId.
  2. Fulfill — The guardian processes the request through the vault’s submit flow. For deposit requests, the guardian ensures assets are accounted for and vault units are allocated. For redemption requests, the guardian ensures exit positions are unwound and assets are available.
  3. Claim — The depositor (or receiver) calls claimDeposit or claimRedeem on the Provisioner to receive their vault units or underlying assets.
This async model is essential for cross-chain operations where CCTP bridging introduces latency between the request and fulfillment. See Cross-Chain for the conceptual overview of cross-chain vault flows.

Monitoring Pending Orders

Monitor the Provisioner for incoming deposit and redemption requests by watching for DepositRequested and RedeemRequested events. This lets the guardian detect new orders that need fulfillment.
const provisionerEventAbi = [
  {
    name: 'DepositRequested',
    type: 'event',
    inputs: [
      { name: 'requestId', type: 'uint256', indexed: true },
      { name: 'owner', type: 'address', indexed: true },
      { name: 'assets', type: 'uint256', indexed: false },
    ],
  },
  {
    name: 'RedeemRequested',
    type: 'event',
    inputs: [
      { name: 'requestId', type: 'uint256', indexed: true },
      { name: 'owner', type: 'address', indexed: true },
      { name: 'shares', type: 'uint256', indexed: false },
    ],
  },
] as const

// Watch for new deposit requests
const depositLogs = await publicClient.getLogs({
  address: PROVISIONER_ADDRESS,
  event: provisionerEventAbi[0],
  fromBlock: 'latest',
})

for (const log of depositLogs) {
  console.log('Pending deposit:', {
    requestId: log.args.requestId,
    owner: log.args.owner,
    assets: log.args.assets,
  })
}

// Watch for new redemption requests
const redeemLogs = await publicClient.getLogs({
  address: PROVISIONER_ADDRESS,
  event: provisionerEventAbi[1],
  fromBlock: 'latest',
})

for (const log of redeemLogs) {
  console.log('Pending redemption:', {
    requestId: log.args.requestId,
    owner: log.args.owner,
    shares: log.args.shares,
  })
}

Fulfilling Deposit Orders

When a depositor places a deposit request via requestDeposit, the guardian fulfills it by processing the request through the vault’s submit flow. Once fulfilled, the depositor can claim their vault units by calling claimDeposit on the Provisioner.
const provisionerAbi = [
  {
    name: 'claimDeposit',
    type: 'function',
    stateMutability: 'nonpayable',
    inputs: [
      { name: 'requestId', type: 'uint256' },
      { name: 'receiver', type: 'address' },
    ],
    outputs: [{ type: 'uint256' }],
  },
] as const

// After the guardian fulfills the request via submit, the depositor claims:
const requestId = 1n // The request ID from DepositRequested event
const receiver = '0x...' as `0x${string}` // Depositor's address

const { request: claimReq } = await publicClient.simulateContract({
  address: PROVISIONER_ADDRESS,
  abi: provisionerAbi,
  functionName: 'claimDeposit',
  args: [requestId, receiver],
  account,
})

const claimTx = await walletClient.writeContract(claimReq)
const receipt = await publicClient.waitForTransactionReceipt({ hash: claimTx, confirmations: 2 })

console.log('Deposit claimed, vault units received:', receipt.transactionHash)

Fulfilling Redemption Orders

Redemption requests follow the same async pattern. The depositor calls requestRedeem on the Provisioner, and after the guardian ensures the underlying assets are available (by unwinding positions if needed via submit), the depositor claims the assets with claimRedeem.
const redeemClaimAbi = [
  {
    name: 'claimRedeem',
    type: 'function',
    stateMutability: 'nonpayable',
    inputs: [
      { name: 'requestId', type: 'uint256' },
      { name: 'receiver', type: 'address' },
    ],
    outputs: [{ type: 'uint256' }],
  },
] as const

// After the guardian fulfills the redemption via submit, the depositor claims:
const redeemRequestId = 2n // The request ID from RedeemRequested event
const assetReceiver = '0x...' as `0x${string}`

const { request: redeemReq } = await publicClient.simulateContract({
  address: PROVISIONER_ADDRESS,
  abi: redeemClaimAbi,
  functionName: 'claimRedeem',
  args: [redeemRequestId, assetReceiver],
  account,
})

const redeemTx = await walletClient.writeContract(redeemReq)
const redeemReceipt = await publicClient.waitForTransactionReceipt({ hash: redeemTx, confirmations: 2 })

console.log('Redemption claimed, assets received:', redeemReceipt.transactionHash)
Order fulfillment is executed through the guardian’s submit flow on the vault. The exact fulfillment mechanics depend on whether the deposit/redemption is same-chain or cross-chain. See the Provisioner Contract Reference for full function signatures and the Cross-Chain page for how cross-chain bridging integrates with the order lifecycle.

Price Reporting

Price reporting is unique to multi-depositor vaults. Because multiple depositors share the same pool, accurate unit pricing is essential for fair minting and redemption of vault shares. The PriceAndFeeCalculator contract manages price snapshots and uses them for both share pricing and fee computation.

Why Prices Matter

In a multi-depositor vault, each depositor’s ownership is represented by ERC-20 vault units. The unit price determines how many shares a depositor receives on deposit and how many assets they receive on redemption. Accurate, timely price reporting ensures:
  • Fair entry and exit prices for all depositors
  • Correct NAV (Net Asset Value) calculation for the vault
  • Accurate fee accrual based on vault performance
The PriceAndFeeCalculator uses managed accountant snapshots rather than real-time pricing to prevent fee manipulation through short-term vault value changes. See the Periphery Contract Reference for full function signatures.

Taking Snapshots

The guardian takes price snapshots by calling snapshot on the PriceAndFeeCalculator. This records the current unit price and total supply for fee calculation. Snapshots are typically taken as part of the submit flow via an afterSubmit hook, but can also be called directly.
const priceCalcAbi = [
  {
    name: 'snapshot',
    type: 'function',
    stateMutability: 'nonpayable',
    inputs: [
      { name: 'unitPrice', type: 'uint256' },
      { name: 'totalSupply', type: 'uint256' },
    ],
    outputs: [],
  },
] as const

// Current unit price and total supply -- derived from vault state
const unitPrice = 1050000000000000000n  // 1.05 in 18 decimals (vault has appreciated 5%)
const totalSupply = 10000000000000000000000n // 10,000 vault units

const { request: snapshotReq } = await publicClient.simulateContract({
  address: PRICE_FEE_CALCULATOR_ADDRESS,
  abi: priceCalcAbi,
  functionName: 'snapshot',
  args: [unitPrice, totalSupply],
  account,
})

const snapshotTx = await walletClient.writeContract(snapshotReq)
await publicClient.waitForTransactionReceipt({ hash: snapshotTx, confirmations: 2 })

console.log('Price snapshot taken:', snapshotTx)

Monitoring Prices

Read the current vault state to determine the unit price and total supply before taking a snapshot or verifying pricing accuracy.
const vaultPriceAbi = [
  {
    name: 'totalAssets',
    type: 'function',
    stateMutability: 'view',
    inputs: [],
    outputs: [{ type: 'uint256' }],
  },
  {
    name: 'totalSupply',
    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' }],
  },
] as const

const [totalAssets, totalSupply] = await Promise.all([
  publicClient.readContract({
    address: VAULT_ADDRESS,
    abi: vaultPriceAbi,
    functionName: 'totalAssets',
  }),
  publicClient.readContract({
    address: VAULT_ADDRESS,
    abi: vaultPriceAbi,
    functionName: 'totalSupply',
  }),
])

// Derive unit price: totalAssets / totalSupply
const unitPrice = totalSupply > 0n
  ? (totalAssets * 10n ** 18n) / totalSupply
  : 10n ** 18n // Default 1:1 if no supply

console.log('Total assets:', totalAssets.toString())
console.log('Total supply:', totalSupply.toString())
console.log('Unit price (18 decimals):', unitPrice.toString())

// Convert between shares and assets at current rate
const sharesFor1000 = await publicClient.readContract({
  address: VAULT_ADDRESS,
  abi: vaultPriceAbi,
  functionName: 'convertToShares',
  args: [1000n * 10n ** 18n],
})

console.log('Shares for 1000 assets:', sharesFor1000.toString())

Fee Reporting

Multi-depositor vaults use the PriceAndFeeCalculator for fee computation instead of the DelayedFeeCalculator used by single-depositor vaults. The same two fee types apply — management fees (percentage of AUM over time) and performance fees (percentage of gains above a high-water mark). See the Single-Depositor Vaults guide for the conceptual explanation of how these fee types work.

Reporting Fees

Report fees by providing the current vault value to the fee vault. The PriceAndFeeCalculator uses its most recent snapshot to compute accrued fees.
const feeVaultAbi = [
  {
    name: 'reportFees',
    type: 'function',
    stateMutability: 'nonpayable',
    inputs: [{ name: 'vaultValue', type: 'uint256' }],
    outputs: [],
  },
] as const

// Report current vault value for fee calculation
const currentVaultValue = 10500000n * 10n ** 18n // Example: 10.5M in 18 decimals

const { request: reportReq } = await publicClient.simulateContract({
  address: FEE_VAULT_ADDRESS,
  abi: feeVaultAbi,
  functionName: 'reportFees',
  args: [currentVaultValue],
  account,
})

const reportTx = await walletClient.writeContract(reportReq)
await publicClient.waitForTransactionReceipt({ hash: reportTx, confirmations: 2 })

console.log('Fees reported for vault value:', currentVaultValue.toString())

Claiming Fees

The fee recipient claims accrued fees from the fee vault. Specify the token, amount, and recipient address. See the FeeVault Contract Reference for full function details.
const claimFeesAbi = [
  {
    name: 'claimFees',
    type: 'function',
    stateMutability: 'nonpayable',
    inputs: [
      { name: 'token', type: 'address' },
      { name: 'amount', type: 'uint256' },
      { name: 'recipient', type: 'address' },
    ],
    outputs: [],
  },
  {
    name: 'accruedFees',
    type: 'function',
    stateMutability: 'view',
    inputs: [],
    outputs: [{ type: 'uint256' }],
  },
] as const

// Check accrued fees first
const accrued = await publicClient.readContract({
  address: FEE_VAULT_ADDRESS,
  abi: claimFeesAbi,
  functionName: 'accruedFees',
})

console.log('Accrued fees:', accrued.toString())

// Claim fees
const FEE_TOKEN = '0x...' as const       // Token to claim fees in
const FEE_RECIPIENT = '0x...' as const   // Address to receive fees

const { request: claimReq } = await publicClient.simulateContract({
  address: FEE_VAULT_ADDRESS,
  abi: claimFeesAbi,
  functionName: 'claimFees',
  args: [FEE_TOKEN, accrued, FEE_RECIPIENT],
  account,
})

const claimTx = await walletClient.writeContract(claimReq)
await publicClient.waitForTransactionReceipt({ hash: claimTx, confirmations: 2 })

console.log('Fees claimed:', claimTx)

Transfer Hooks

Multi-depositor vault units are ERC-20 tokens that can be transferred between addresses. Transfer hooks allow the vault owner to enforce restrictions on these transfers — for example, compliance blocklists, lock-up periods, or allowlists for approved recipients.

Setting Transfer Hooks

Setting the transfer hook is an owner operation (not a guardian operation), but understanding the mechanism is important for guardians because transfer hooks affect how vault units flow between depositors.
// Owner-only: set the transfer hook contract on the vault
const setHookAbi = [
  {
    name: 'setTransferHook',
    type: 'function',
    stateMutability: 'nonpayable',
    inputs: [{ name: 'hook', type: 'address' }],
    outputs: [],
  },
] as const

const HOOK_ADDRESS = '0x...' as const // Contract implementing IBeforeTransferHook

const { request: hookReq } = await publicClient.simulateContract({
  address: VAULT_ADDRESS,
  abi: setHookAbi,
  functionName: 'setTransferHook',
  args: [HOOK_ADDRESS],
  account, // Must be vault owner
})

const hookTx = await walletClient.writeContract(hookReq)
await publicClient.waitForTransactionReceipt({ hash: hookTx, confirmations: 2 })

console.log('Transfer hook set:', hookTx)
When a transfer hook is configured, every vault unit transfer calls beforeTransfer(address from, address to, uint256 amount) on the hook contract before executing. If the hook reverts, the transfer fails with MultiDepositorVault__TransferHookFailed. See Custom Hooks for building custom transfer hook contracts and the Hook Interfaces Contract Reference for the IBeforeTransferHook interface.

Emergency Procedures

Emergency procedures for multi-depositor vaults follow the same pattern as single-depositor vaults. See the Single-Depositor Vaults guide for full details on when and why to use these. The key operations are summarized below.

Pausing the Vault

Calling pause immediately halts all guardian operations. Both the guardian and the vault owner can pause.
const pauseAbi = [
  {
    name: 'pause',
    type: 'function',
    stateMutability: 'nonpayable',
    inputs: [],
    outputs: [],
  },
] as const

const { request: pauseReq } = await publicClient.simulateContract({
  address: VAULT_ADDRESS,
  abi: pauseAbi,
  functionName: 'pause',
  account,
})

const pauseTx = await walletClient.writeContract(pauseReq)
await publicClient.waitForTransactionReceipt({ hash: pauseTx, confirmations: 2 })

console.log('Vault paused:', pauseTx)
Only the vault owner can call unpause to resume operations. As a guardian, once you pause the vault, you cannot unpause it yourself. Coordinate with the vault owner before pausing in non-emergency situations.

Checking Pause Status

Verify whether the vault is currently paused before attempting operations.
const pausedAbi = [
  {
    name: 'paused',
    type: 'function',
    stateMutability: 'view',
    inputs: [],
    outputs: [{ type: 'bool' }],
  },
] as const

const isPaused = await publicClient.readContract({
  address: VAULT_ADDRESS,
  abi: pausedAbi,
  functionName: 'paused',
})

console.log('Vault paused:', isPaused)

Guardian Whitelist Check

Verify that the guardian is still authorized on the vault’s whitelist.
const whitelistAbi = [
  {
    name: 'checkGuardianWhitelist',
    type: 'function',
    stateMutability: 'nonpayable',
    inputs: [],
    outputs: [],
  },
] as const

const { request: whitelistReq } = await publicClient.simulateContract({
  address: VAULT_ADDRESS,
  abi: whitelistAbi,
  functionName: 'checkGuardianWhitelist',
  account,
})

const whitelistTx = await walletClient.writeContract(whitelistReq)
await publicClient.waitForTransactionReceipt({ hash: whitelistTx, confirmations: 2 })

console.log('Guardian whitelist check passed:', whitelistTx)