Skip to main content

Core contractsVault capabilities

Vault capabilities

How to detect what a 4626 vault supports with ERC-165, and how the optional ERC-7540 async request module wraps a sync vault.

Integrators can ask a vault what it supports instead of guessing. This page covers the ERC-165 capability IDs, what share() means on these vaults, and the optional async request module.

At a glance​

RoleCapability discovery for Creator and Agent vaults, plus an optional async (ERC-7540) front end
DeployedCapability IDs are a shared library; OVaultAsyncRequestModule is deployed in front of a vault when needed
Standard(s)ERC-165, ERC-7575, ERC-7540, ERC-7887 (Draft), ERC-8161
Key relationshipsCreatorOVault, CreatorOVaultWrapper, CreatorShareOFT, Registry4626

How it works​

Call supportsInterface(bytes4) with the official ERC-165 IDs, which are also collected in OVaultCapabilityIds. Shared capability interfaces live under contracts/shared/interfaces/vault/. Creator and Agent vaults keep their own lane identities.

IOVault4626 is a lean wiring ABI. Don't look there for share externalization or async requests.

Live vaults don't advertise ERC-7575 yet

Live CreatorOVault and AgentOVault do not implement supportsInterface or share() today. Adding them to the shared facade would push AgentOVault past the EIP-170 size limit. The intended ABI is covered by tests in test/shared/Test7575OVault.sol.

Capability matrix​

CapabilityInterface IDIntended sync vaultLive sync vaultAsync request module
ERC-1650x01ffc9a7yesnoyes
ERC-7575 vault0x2f0a18c5yesnoyes
ERC-7575 share0xf815c03dyesnono (share is the underlying vault)
ERC-7540 operator0xe3bc4e65nonoyes
ERC-7540 deposit0xce3bbe50nonoyes
ERC-7540 redeem0x620ee8e4nonoyes
ERC-7887 cancel deposit (Draft)0x8bf840e3nonoyes
ERC-7887 cancel redeem (Draft)0xe76cffc7nonoyes
ERC-8161 transfer deposit0x53b3bb0anonoyes
ERC-8161 transfer redeem0x7846f5bdnonoyes

The advertised ERC-7575 vault ID is IERC-4626 (excluding ERC-20) XOR share(), which is 0x2f0a18c5. It is not type(IOVault7575).interfaceId, which is only the share() selector (0xa8d5fd65).

What share() means here​

Intended sync vaults expose:

function share() external view returns (address); // address(this)
function vault(address asset_) external view returns (address);

share() returns the vault itself: the vault claim (, or ◇ on agent lanes). That is the ERC-4626 accounting share, using the EIP-allowed "share() MAY return address(this)" path.

ShareOFT (, or ◆ on agent lanes) is not share(). It is a LayerZero token minted by the wrapper at a fixed 1000:1 normalization, with trade fees and lottery behavior. Find it through IOVaultWrapper4626.shareOFT() or Registry4626.

A true external-share ERC-7575 vault (share != address(this)) is not fully ERC-4626 compatible: the vault could not be the ERC-20, and entry functions would have to mint the external share 1:1. That shape is reserved for a future implementation.

Async request module (ERC-7540)​

OVaultAsyncRequestModule sits in front of a compatible sync vault, including a live CreatorOVault that does not advertise ERC-7575. If the vault has no supportsInterface / share(), the module treats the vault itself as the ERC-4626 share token. It does not change CreatorOVault or AgentOVault.

The request flow:

  1. The controller calls requestDeposit or requestRedeem. The request is Pending.
  2. fulfillDeposit / fulfillRedeem moves it to Claimable.
  3. The controller claims with deposit / redeem, which settles against the underlying vault.

Key functions​

FunctionWhat it doesWho can call
requestDeposit(assets, controller, owner) / requestRedeem(shares, controller, owner)Open a pending request; controller must equal ownerOwner or the owner's operator
fulfillDeposit(controller) / fulfillRedeem(controller)Mark pending as claimable (bookkeeping only)The controller or its operator — not the module owner, not a stranger
deposit / redeem (and mint / withdraw)Claim against the underlying vaultController or its operator
cancelDepositRequest(requestId, controller) / cancelRedeemRequest(requestId, controller)ERC-7887 cancel; skips straight to cancel-claimableController or its operator

Behavior details​

  • requestId is always 0.
  • previewDeposit, previewMint, previewRedeem, and previewWithdraw revert.
  • share() is the underlying vault ( / ◇), not the module and not ShareOFT.
  • Owner pause is instant (emergency) and does not lock claims.
  • A new request or pending transfer is blocked while the destination controller still has a cancel-claimable balance.
  • Claims emit Deposit / Withdraw on the module (controller, receiver), so indexers don't need to watch the sync vault.
  • Cancel recovers pending or claimable assets or shares, so fulfill cannot trap funds. ERC-8161 transfer still moves pending balances only.
  • asset and share are cached at construction. totalAssets is escrowed request value (not donation-inclusive AUM) and never reverts.
  • Claims measure the token or share delta and revert if the vault spends more than reserved or mints zero shares. Cancel-claims revert if the module cannot cover the booked amount. request* credits the received delta and reverts on a surplus (rebase-up or reflection tokens).

Max functions​

FunctionReturns
maxDeposit(controller)min(claimable, vault.maxDeposit(controller)). Pass the controller (the claim-to-self receiver), not the module.
maxMint(controller)Inverts previewMint against the deposit cap and vault.maxMint(controller), so a mint-disabled vault advertises 0
maxWithdraw(controller)Inverts previewWithdraw against maxRedeem and vault.maxWithdraw(address(this)) (the module holds the escrowed shares)

mint(maxMint()) and withdraw(maxWithdraw()) succeed.

Lessons from other ERC-7540 stacks​

These are production bugs elsewhere whose fixes we locked in with tests. They are not new product scope.

SourceFailureLocked by
Centrifuge / OZ ERC7540Public or owner fulfill griefs ERC-8161 pending transferstestLesson_centrifuge_strangerFulfillCannotGriefPendingTransfer, testLesson_oz_operatorCanFulfillOwnController, testOwnerCannotFulfill
OpenZeppelin ERC7540 natspecDead controller locks the requesttestRejectsModuleOrVaultAsController, testCancelAfterFulfillDeposit
Superform v2 (SUP-14312)controller != owner on redeem desyncs share bucketstestLesson_superform_redeemControllerMustEqualOwner
Lagoon V1–V5Hybrid sync and async on one vaulttestAsyncSelectorsAreAbsentOnSyncVaults, testSyncVaultPreviewsStillWork
ERC-7887New request while cancel is outstandingtestRequestBlockedWhileCancelClaimable, testLesson_eip7887_redeemBlockedWhileCancelClaimable
EIP-7540 / Centrifuge claim eventsIndexers miss claims if only the inner vault emitstestLesson_eip7540_claimEmitsDepositWithController, testLesson_eip7540_claimEmitsWithdrawWithController
ERC-8161Transfer must not move claimabletestLesson_eip8161_transferDoesNotMoveClaimable

Run them with:

forge test --match-test testLesson_ --no-match-path 'test/vault/strategies/CreatorOVaultStrategies.Rebalance.*'

If any fail, a known production bug has come back.

Gotchas​

  • To find the accounting share, require supportsInterface(0x2f0a18c5) and read share(). Never infer it from shareOFT(), IShareOFT4626.vault(), or type(IOVault7575).interfaceId.
  • Do not map share() to ShareOFT on current vaults.
  • The large-withdrawal queue on the sync vault is an MEV and flash-loan gate, not ERC-7540.
  • share(), vault(address), and supportsInterface are additive ABI. Live vaults keep their current bytecode until a size-safe reseal; this change does not cut a new greenfield shell or reseal DEPLOY_BYTECODE. They won't be added to the shared facade until AgentOVault runtime is back under 24,576 bytes.

Source​