Vault Contracts¶
Vaults are the depositor-facing entry point: ERC-7540/ERC-4626 contracts through which users deposit assets and redeem shares. Behind them sit two request engines — one on the spoke (per-user state and escrow mechanics) and one on the hub (epoch batching) — plus pricing infrastructure. All contracts on this page are unmodified Centrifuge V3 (see the fork framing).
This page covers the contract-level fund flows. For the off-chain orchestration of these flows (who approves, issues, and fulfills in practice), see the backend lifecycle; for the integrator-facing request/claim API, see the SDK concepts.
Vault kinds¶
AsyncVault(ERC-7540): asynchronous deposits and asynchronous redemptions. The ERC-4626preview*functions revert by design — outcomes depend on epoch pricing that is not known synchronously.SyncDepositVault: synchronous ERC-4626 deposits with asynchronous ERC-7540 redemptions.
Both extend a shared BaseVault:
- Shared views:
poolId,scId,asset,share,totalAssets,convertTo*,pricePerShare,priceLastUpdated,isPermissioned. - Operator model:
setOperator, signature-basedauthorizeOperator(EIP-712 / ERC-7741), andsetEndorsedOperatorfor Root-endorsed callers — i.e. the VaultRouter. - Requests are non-fungible:
requestIdis always0.
AsyncRequestManager — spoke-side engine¶
The AsyncRequestManager holds all per-user request state on a spoke chain: for each (vault, depositor) it tracks maxMint, maxWithdraw, weighted-average deposit and redeem prices, pending request amounts, cancellation flags, and claimable-cancel amounts. The vaults themselves are its wards, cross-relied at factory deploy time.
Outbound (user → hub). requestDeposit, requestRedeem, and the cancel functions validate hook restrictions, mutate pending state, then dispatch a Request message via the Spoke. Deposit assets are pulled into the pool escrow and reserved under a deposit reason; redeem shares move to the escrow and are reserved under a redeem reason. Cross-chain message gas can be paid from the pool's ETH subsidy via the SubsidyManager.
Inbound fulfillment (callbacks from the hub, decoded by callback type):
ApprovedDeposits→ unreserve the assets, pin the price,noteDeposit(queuing the hub-side holding increase), reset the price override.IssuedShares→ pin the share price,balanceSheet.issueto the pool escrow, reserve the shares.RevokedShares→ reserve the payout assets, unreserve the shares,noteWithdrawqueued atomically with the burn (avoiding NAV desync),balanceSheet.revoke.FulfilledDepositRequest/FulfilledRedeemRequest→ update the weighted-average deposit/redeem price, bumpmaxMint/maxWithdraw, decrement pending, and notify the vault so claims become available.
Claims consume maxMint/maxWithdraw and withdraw from the escrow with the appropriate withdraw mode — claims use the modes that skip re-queuing, because hub accounting was already queued at fulfillment time.
BatchRequestManager — hub-side epoch engine¶
The hub-side counterpart batches requests into epochs per (pool, shareClass, asset):
- State: four monotone epoch counters (deposit, issue, redeem, revoke); pending deposit/redeem totals; per-depositor orders (
pending,lastUpdate); queued orders for requests arriving mid-epoch (folded in after the user's claims catch up); andallowForceCancelsafeguards — a manager cannot force-cancel a user's request without the user's prior explicit permission. - Flow: inbound
request()(ward = Hub) mutates pending/queued state → the pool manager callsapproveDeposits/approveRedeems(snapshotting amount and price and advancing the epoch), thenissueShares/revokeShares(computing outcomes viaPricingLiband firing the request callback cross-chain to the spoke's AsyncRequestManager) →notifyDeposit/notifyRedeem(permissionless, reentrancy-guarded) aggregate a user's claims across epochs into a single fulfillment callback. - Rounding invariant: pending reductions round up and payouts round down, so the sum of claims never exceeds the sum issued. A user may lose at most 1 wei per claim; the invariant prevents cumulative drift.
SyncManager¶
Backs synchronous deposits:
- An optional per-share-class valuation source; if unset, the Spoke share price is used.
- A
maxReservecap per(pool, shareClass, asset):maxDeposit = maxReserve − escrowAvailable, bounding synchronous inflows. - Sync deposit flow: the vault calls into the SyncManager, which notes the deposit, pins the price, and issues shares directly to the receiver via the BalanceSheet; the vault transfers the assets straight into the pool escrow.
- Configured via the trusted-call channel from the Hub (through the ContractUpdater) or by wards.
VaultRouter¶
The EOA entry point: enable/disable (as an endorsed operator), request/claim/cancel wrappers, ERC-2612 permit, and cross-chain share transfers — all reentrancy-protected, payable, and multicall-batchable so a user can combine steps into one transaction with one gas payment.
Invariant: no funds may remain in the router after a transaction — anything left over would be claimable by others.
Vault factories¶
AsyncVaultFactory and SyncDepositVaultFactory (wards of the VaultRegistry) deploy vaults at deterministic CREATE2 addresses (salt derived from pool, share class, and asset; ERC-20 assets only), then wire atomically: rely Root on the vault, rely the request manager on the vault, rely the vault on its request manager (making the vault a ward of its manager), and deny the factory itself.
Fund flow: async deposit lifecycle¶
SPOKE CHAIN HUB CHAIN
─────────── ─────────
depositor
│ requestDeposit(assets)
▼
AsyncVault → AsyncRequestManager
│ pull assets → PoolEscrow (reserved)
│ Request ────────────────────────────► HubHandler → BatchRequestManager
│ pending += assets
│ pool manager:
│ approveDeposits (epoch, price)
│ ApprovedDeposits ◄──────────────────── │
│ unreserve, pin price, noteDeposit │
│ issueShares (PricingLib)
│ IssuedShares ◄───────────────────────── │
│ issue shares → PoolEscrow (reserved) │
│ notifyDeposit (permissionless)
│ FulfilledDepositRequest ◄───────────── aggregates epochs
│ maxMint ↑, pending ↓
▼
depositor claims: mint/deposit
│ shares released from escrow to depositor
▼
BalanceSheet.submitQueuedAssets/Shares ────► Holdings + Accounting update
(isSnapshot when queues drain)
Redemptions mirror this flow: shares are escrowed and reserved on request, revoked at fulfillment (with the payout assets reserved), and the depositor's claim withdraws assets from the escrow.
Valuations and pricing¶
D18 and PricingLib¶
All protocol prices are D18 — 18-decimal fixed point. PricingLib provides the conversion primitives: convertWithPrice(amount, baseDecimals, quoteDecimals, priceQuotePerBase) at the core, with asset↔share helpers (raw-price overloads reserved for AsyncRequestManager fulfillments; pool-price variants elsewhere), priceAssetPerShare, and reciprocal-price conversion.
Valuation contracts¶
Both implement a common IValuation interface (getPrice, getQuote) and convert via PricingLib with registry decimals:
OracleValuation— a trusted-feeder price feed, prices in pool currency, quorum of one. Feeder management (updateFeeder) is hub-manager-only;setPriceis feeder-only and also triggers a hub holding re-mark so the books track the new price immediately.getPricerevertsPriceNotSetuntil a price has been fed.IdentityValuation— a constant 1.0 price with decimal conversion only, for stablecoins and pegged assets.