Skip to main content
  1. Blog/

Understanding Vault Standards: EIP-4626 and Beyond

·1543 words·8 mins
Antonio Morrone
Author
Antonio Morrone
Staff/Tech Lead Software Engineer. Building software since 2013, in blockchain protocols & Web3 infrastructure since 2021. Security-minded. Remote from Italy.

While researching vault standards to inform the design of a planned vault product on Rootstock, it became clear pretty quickly that “use ERC-4626” wasn’t the whole answer. The base standard covers a lot, but it also assumes a specific shape (one share token, one asset, everything settling synchronously) that doesn’t fit every vault a real product might need. I put together an internal walkthrough for the team, covering ERC-4626 and the standards that extend it, for exactly the cases where the base spec runs out. This post is that walkthrough.

Definitions, first
#

Three terms are worth pinning down before anything else, since the rest of this only makes sense once they’re precise:

  • Asset: the underlying token a user deposits (e.g. an ERC-20 stablecoin or the vault’s target token).
  • Share: the vault’s own token, minted to a depositor in exchange for assets, representing their proportional claim on everything the vault holds.
  • Fee: value the vault or its operator takes on entry, exit, or over time; the standards below don’t mandate a specific fee mechanism, just leave room for one.

ERC-4626: the base standard
#

ERC-4626 standardizes the tokenized vault pattern as an extension of ERC-20: asset()/totalAssets() to inspect the vault, deposit()/mint() to go in, withdraw()/redeem() to go out, a max*/preview* variant of each so a caller can check limits and expected outcomes before committing, and convertToShares()/convertToAssets() for the underlying exchange rate. That exchange rate is the core of the whole standard, and it comes down to one ratio: a vault holding totalAssets backing totalSupply shares prices one share at totalAssets / totalSupply assets. Going from assets to shares, which is what deposit() does, divides by that price instead of multiplying it: depositing a assets mints a * totalSupply / totalAssets shares. Going the other way, which is what redeem() does, applies the price directly: redeeming s shares returns s * totalAssets / totalSupply assets. Every deposit/mint/redeem/withdraw call is one of these two directions.

One consequence of this formula is worth understanding early, because it’s the standard’s best-known failure mode: the inflation attack. Since the exchange rate is a ratio over the vault’s actual balances, an attacker can deposit a trivial amount to mint the first share, then donate assets directly to the vault (bypassing deposit() entirely) to distort totalAssets before any real user shows up. The next honest depositor’s shares get computed against that distorted ratio, and rounding in the vault’s favor (or the attacker’s) can be enough to siphon value from them. This is why the spec is explicit that preview functions must reflect what execution will actually do as closely as possible: an approximate preview is exactly the gap this attack lives in. The standard mitigation you’ll see in production implementations is a decimal offset (effectively minting the vault some unclaimable “virtual” shares up front), which makes the same manipulation prohibitively expensive rather than free.

Why one flavor of vault isn’t enough
#

ERC-4626 assumes two things by default: one share token backed by exactly one asset, and a deposit/withdraw that settles fully within a single transaction. Two situations break each of those assumptions:

  • A vault design where several entry points, each in a different asset, should all mint the same share (for example, a vault that accepts multiple stablecoins and treats them as fungible for accounting purposes).
  • A vault whose underlying strategy can’t settle atomically: it needs to wait on something external (an oracle price, a batch auction, unwinding a real-world position) before it knows what a deposit or redemption is actually worth.

Neither is solved by ERC-4626 alone. Both got their own standard.

ERC-7575: multi-asset vaults
#

ERC-7575 solves the multi-asset case with an idea it calls share externalization. In plain ERC-4626, the vault contract is the ERC-20 share token; they’re the same address. ERC-7575 breaks that coupling: a vault exposes a share() method that can point to an external token contract instead of itself, and that external share token can optionally point back with a vault() lookup.

Once share and vault are separate, nothing stops multiple vaults (each denominated in a different asset) from pointing at the same share token. A user can deposit asset A into vault A or asset B into vault B and receive the same fungible share either way. Converting between a given asset and the shared token happens through what the standard calls a pipe, which can be one-directional (deposit/mint only) or bidirectional. Because a vault is no longer guaranteed to be ERC-20 itself, the standard requires ERC-165 support so callers can safely detect what they’re actually talking to.

ERC-7540: async vaults
#

ERC-7540 solves the second case (a vault that can’t settle a deposit or redemption within one transaction) by replacing the instant conversion with a three-state request lifecycle:

stateDiagram-v2
    [*] --> Pending: requestDeposit / requestRedeem
    Pending --> Claimable: vault processes the request internally
    Claimable --> Claimed: deposit / redeem (claim)
    Claimed --> [*]

A user calls requestDeposit() or requestRedeem() to move into Pending. Once the vault has done whatever it needs to do off the critical path (settle a batch, get a price, unwind a position), the request becomes Claimable, and the user then calls the ordinary ERC-4626 deposit()/mint() or redeem()/withdraw() as a separate transaction to actually claim the result and move to Claimed. Two roles come with this: a controller owns the request and is entitled to claim it, and an operator is an account the controller can approve to act on their behalf, which is useful for automation that shouldn’t need the controller’s private key on every claim.

Worth flagging an asymmetry the deck itself flags as an open question: there’s no requestMint() or requestWithdraw(). Only requestDeposit() (by asset amount) and requestRedeem() (by share amount) exist on the request side. The spec’s own reasoning is that an async vault can only act with certainty on the quantity that’s actually known at request time: how many assets you’re putting in, or how many shares you’re giving up. Requesting the opposite quantity (“give me exactly N shares” before the vault has processed anything) would require guessing an exchange rate that hasn’t been fixed yet. The claim side doesn’t have this restriction: once a request is Claimable, both deposit() and mint() can claim a deposit request (by asset amount or share amount respectively), and both redeem() and withdraw() can claim a redemption request. The uncertainty is gone by then, so either quantity works.

A few details worth knowing if you’re actually implementing this: a vault can be fully or only partially async. Synchronous deposits paired with async redemptions, for example, is a common shape for anything backed by a real-world or illiquid position, where getting money in is easy but getting it out requires unwinding something first. And the preview* functions the base standard leans on for a reliable quote don’t have a sane answer for a request that hasn’t been processed yet, so the standard has them revert for the async legs rather than return a number that isn’t real.

The three side by side
#

ERC-4626ERC-7575ERC-7540
Share is itself ERC-20Yes (the vault contract is the token)No (externalized to its own contract)Not specified by this standard
Share-to-asset shapeOne share, one assetOne share, potentially many assets/vaults via pipesn/a (orthogonal to this; a 7540 vault can also be 7575)
SettlementSynchronousSynchronousAsynchronous (request then claim)
Request siden/an/aOnly requestDeposit/requestRedeem (no requestMint/requestWithdraw)
New rolesn/an/aController, operator

Two more worth knowing, briefly
#

Two adjacent standards came up in the same research pass, without needing a full section each:

  • EIP-2612 (permit): a plain approve() is its own transaction before the action a user actually wants to take, which for a vault means “approve, then deposit” as two separate round trips. Permit lets a user sign an off-chain message the vault (or the token) can consume as an approval inside the same transaction as the deposit, cutting that extra step out of the flow entirely.
  • EIP-7579 (modular smart accounts, still Draft status) and EIP-5115 (the SY/Standardized Yield token, also still Draft status) both surfaced as adjacent standards worth tracking rather than immediately relevant. 7579 standardizes how a smart-contract wallet installs and manages modules (validators, executors, fallback handlers, hooks) at runtime, and 5115 does for yield-bearing tokens roughly what ERC-4626 does for vaults, wrapping deposit/redeem behind one common interface. Neither changes anything about the vault standards above; they’re worth knowing exist for adjacent design decisions.

Takeaway
#

None of this is about picking a winner. ERC-4626, ERC-7575, and ERC-7540 solve different problems, and a real vault product might only need the base standard, or might need exactly one of the two extensions, depending on whether it has to support multiple assets or can’t settle atomically. Going through all three before writing any vault code was about knowing which shape the product actually needed before picking a specific implementation to build it with. That implementation choice (comparing how OpenZeppelin, Solmate, and Solady actually build out the base ERC-4626 standard, and what each costs in gas) was part of the same evaluation, and is written up as its own post rather than folded into this one.

Resources
#