Skip to main content

CoreVault capabilities

Vault capabilities

Inspect one vault with ERC-165 supportsInterface to read the full capability matrix. Shared capability interfaces live under contracts/shared/interfaces/vault/. Concrete Creator and Agent vaults keep their lane identities.

IOVault4626 stays a lean wiring ABI. Do not look there for share externalization or async requests.

How to inspect

Call supportsInterface(bytes4) with the official ERC-165 ids (also in OVaultCapabilityIds). Live CreatorOVault / AgentOVault do not advertise 7575 yet — AgentOVault runtime exceeds EIP-170 if those methods are added to the shared facade. OVaultAsyncRequestModule still attaches: if the vault has no supportsInterface / share(), it treats the vault itself as the ERC-4626 share token. Tests also cover the intended 7575 ABI via Test7575OVault.

CapabilityInterface IDIntended sync OVaultLive sync OVaultAsync request module
ERC-1650x01ffc9a7yesnoyes
ERC-7575 vault0x2f0a18c5yesnoyes
ERC-7575 share0xf815c03dyesnono (share is the underlying vault)
ERC-7540 operator0xe3bc4e65noyes
ERC-7540 deposit0xce3bbe50noyes
ERC-7540 redeem0x620ee8e4noyes
ERC-7887 cancel deposit (Draft)0x8bf840e3noyes
ERC-7887 cancel redeem (Draft)0xe76cffc7noyes
ERC-8161 transfer deposit0x53b3bb0anoyes
ERC-8161 transfer redeem0x7846f5bdnoyes

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

Integrator rule: require supportsInterface(0x2f0a18c5) and read share(). Never infer the accounting share from shareOFT(), IShareOFT4626.vault(), or type(IOVault7575).interfaceId. Advertised IDs are centralized in OVaultCapabilityIds / OVaultCapabilitiesLib.

ERC-7575 share token

Intended synchronous vaults implement (test harness today; live facades deferred for EIP-170):

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

share() returns the vault itself ( / ◇). That is the ERC-4626 accounting share and the EIP-allowed share() MAY return address(this) path.

ShareOFT ( / ◆) is not share(). It is a LayerZero wrapper minted by the OVault wrapper at a fixed 1000:1 normalization, with trade fees and lottery behavior. Discover it via IOVaultWrapper4626.shareOFT() or Registry4626, not ERC-7575.

A true external-share ERC-7575 vault (share != address(this)) is not fully ERC-4626 compatible: the vault itself must not be the ERC-20, and entry functions must mint the external share 1:1. That shape is reserved for a future implementation. Do not map share() to ShareOFT on the current vaults.

Async request module (ERC-7540)

OVaultAsyncRequestModule sits in front of a compatible sync OVault, including a live CreatorOVault that does not advertise ERC-7575. It does not change CreatorOVault / AgentOVault.

Flow: requestDeposit / requestRedeemfulfill* (Pending → Claimable) → deposit / redeem claim against the underlying vault. requestId is always 0. previewDeposit, previewMint, previewRedeem, and previewWithdraw revert. share() is the underlying vault ( / ◇), not the module and not ShareOFT.

fulfill* is the controller or that controller's operator — not the module owner and not a random caller. Owner pause is instant (emergency) and does not lock claims. controller must equal owner on request. A new request or pending transfer is blocked while the destination controller still has cancel-claimable outstanding. Claims emit Deposit / Withdraw on this module (controller, receiver) so indexers are not forced to watch the sync vault.

Cancel (ERC-7887, Draft) skips straight to cancel-claimable so users can recover pending or claimable assets or shares. Fulfill is only a bookkeeping flip — it cannot trap funds. Transfer (ERC-8161) still moves pending balances only.

asset and share are cached at construct. totalAssets is escrowed request value (not donation-inclusive AUM) and must not revert. Claims measure the token/share delta and revert if the vault spends more than reserved or mints zero shares. Cancel-claims measure the paid delta and revert if the module cannot cover the booked amount. request* credits the received delta and reverts if a surplus arrives (rebase-up / reflection). maxDeposit is min(claimable, vault.maxDeposit(controller)) — the argument is the share receiver (EIP-4626), so controller is the claim-to-self proxy; do not pass the module. maxMint inverts previewMint against that deposit cap and vault.maxMint(controller) so a mint-disabled vault (AgentOVault) advertises 0. maxWithdraw inverts previewWithdraw against maxRedeem and vault.maxWithdraw(address(this)) (the module owns the escrowed shares). mint(maxMint()) and withdraw(maxWithdraw()) succeed.

The existing large-withdrawal queue on the sync vault is an MEV / flash-loan gate, not ERC-7540.

Learnings from other ERC-7540 stacks

These are production mistakes we copied the fix for, 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+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 the lock: forge test --match-test testLesson_ --no-match-path 'test/vault/strategies/CreatorOVaultStrategies.Rebalance.*'. If any of those fail, we unlearned the corresponding production bug.

Epoch note

share(), vault(address), and supportsInterface are additive ABI. Live vaults keep prior bytecode until a size-safe reseal. This source change does not cut a new greenfield shell or reseal DEPLOY_BYTECODE. Do not add these methods to the shared CreatorOVault facade until AgentOVault runtime is back under 24,576 bytes.

Prev: CreatorOVault · Next: CreatorOVaultWrapper