Vaults are standardized smart contracts used to manage tokenized assets in Ethereum, providing a consistent interface for deposit, withdrawal, and balance management. ERC-4626 and its extension, ERC-7540, defines a set of functions and events that allow developers to create yield-bearing vaults that can hold ERC20 tokens, enabling interoperability between different vault implementations.

What is a Vault?

It is a smart contract that accepts deposits of an underlying asset (usually an ERC20 token) and generates yield on those deposits through various strategies, such as lending, staking, or liquidity provision.

Whenever a user deposits the underlying asset, they receive a tokenized representation of their deposit, often referred to as a “vault share” or “vault token”. This token represents the user’s share of the vault’s total assets and can be used to track their ownership and claim rewards.

Overview of ERC-4626

Before ERC-4626, different vault implementations had their own unique interfaces, making it difficult for developers to interact with multiple vaults.

ERC-4626 standardizes the interface for vaults, making it easier for developers to create and interact with vaults. The key methods of ERC-4626 include:

  • asset(): Returns the address of the underlying asset token.
  • totalAssets(): Returns the total amount of the underlying asset held by the vault.
  • convertToShares(uint256 assets): Returns the equivalent amount of vault shares for a given amount of the underlying asset if deposited.
  • convertToAssets(uint256 shares): Returns the equivalent amount of the underlying asset for a given amount of vault shares if withdrawn.
  • maxDeposit(address receiver): Returns the maximum amount of the underlying asset that can be deposited for a given receiver.
  • previewDeposit(uint256 assets): Simulate the effects of a deposit and returns the number of shares that would be minted.
  • deposit(uint256 assets, address receiver): Deposits a specified amount of the underlying asset and mints vault shares to the receiver.
  • maxMint(address receiver): Returns the maximum amount of vault shares that can be minted for a given receiver.
  • previewMint(uint256 shares): Simulate the effects of minting shares and returns the amount of the underlying asset that would be required.
  • mint(uint256 shares, address receiver): Mints a specified amount of vault shares by depositing the required amount of the underlying asset.
  • maxWithdraw(address owner): Returns the maximum amount of the underlying asset that can be withdrawn by a given owner.
  • previewWithdraw(uint256 assets): Simulate the effects of a withdrawal and returns the amount of vault shares that would be burned.
  • withdraw(uint256 assets, address receiver, address owner): Withdraws a specified amount of the underlying asset by burning the corresponding amount of vault shares from the owner.
  • maxRedeem(address owner): Returns the maximum amount of vault shares that can be redeemed by a given owner.
  • previewRedeem(uint256 shares): Simulate the effects of redeeming shares and returns the amount of the underlying asset that would be received.
  • redeem(uint256 shares, address receiver, address owner): Redeems a specified amount of vault shares and transfers the corresponding amount of the underlying asset to the receiver.
  • totalSupply(): Returns the total supply of unredeemed vault shares.
  • balanceOf(address owner): Returns the balance of vault shares held by a given owner.

ERC-4626 is great for synchronous vaults, where deposits and withdrawals are processed immediately. That means when a user deposits assets, they receive their vault shares right away, and when they withdraw, they get their assets back immediately.

This only works with protocols where the underlying assets are always liquid and can be accessed without delay. As soon as there is a delay, for example in lending protocols where assets are locked for a certain period or bridging is required, ERC4626 falls short.

Additionally, ERC4626 supports only a single underlying asset per vault, which limits its applicability in more complex scenarios.

To address these limitations, ERC-7540 and ERC-7575 were introduced as an extension to ERC-4626.

Overview of ERC-7540

ERC 7540 is an extension of ERC 4626 designed to support asynchronous vaults: vaults where deposits and withdrawals cannot be processed immediately. This situation commonly arises in protocols where assets need to be transferred across chains, staked into external systems, or locked into lending or yield strategies that take time to execute.

Why ERC 7540?

Under ERC 4626, when a user deposits tokens, they receive vault shares in the same transaction. When they withdraw, they immediately get the underlying tokens back. However, in many real-world scenarios, this synchronous assumption breaks.

For example:

  • In staking protocols, yield-bearing tokens may not be instantly mintable because the network validators must first allocate stake.
  • In bridged or Layer‑2 vaults, assets must first be confirmed on the target network before new shares can be issued.
  • In lending markets, withdrawal requests might need to wait until there’s enough liquidity available.

ERC 7540 introduces a robust framework for these delayed operations by replacing immediate actions with a request/claim model.

Key Concepts

Deposit Request:

Instead of calling deposit() and receiving shares instantly, users initiate a deposit request with details such as amount and receiver.

The vault then issues a deposit receipt or request ID that can later be claimed once the operation finalizes.

Withdraw Request:

Similarly, instead of instant redemptions, users can request to withdraw assets. After underlying liquidity becomes available, they can finalize the operation by invoking a claim function.

Lifecycle Events:

ERC 7540 defines new events (e.g., DepositRequested, DepositFinalized, RedeemRequested, RedeemFinalized) to track asynchronous vault activity transparently.

Benefits

  • Enables delayed execution flows.
  • Supports cross-chain or external staking operations.
  • Maintains ERC 4626 compatibility so integrators can still use common vault interfaces.

Overview of ERC-7575

ERC 7575 builds on ERC 7540 and extends ERC 4626 to support multi-asset vaults. While ERC 4626 is limited to a single underlying asset per vault, ERC 7575 introduces the ability to manage multiple underlying tokens through a shared interface.

Motivation

Many yield strategies today use diversified portfolios. For example, stablecoin vaults that accept USDT, USDC, and DAI, or structured vaults that hold multiple DeFi positions simultaneously.

Instead of deploying separate vaults for each token, ERC 7575 enables one composite vault contract to manage multiple underlying assets, each potentially associated with its own strategy.

Core Additions

  • tokenList(): Returns the list of all supported underlying asset addresses.
  • convertToShares(address asset, uint256 assets): Allows conversions per asset type.
  • convertToAssets(address asset, uint256 shares): Computes how many units of a specific underlying asset correspond to the given shares.
  • deposit(address asset, uint256 assets, address receiver): Deposit a specific supported asset type.
  • withdraw(address asset, uint256 assets, address receiver, address owner): Withdraw from a specific asset type.

Each vault maintains internal accounting for the total assets managed per token while ensuring that vault shares represent a proportional claim on the total basket value.

Use Cases

  • Diversified yield funds combining multiple tokens in one vault.
  • Index vaults representing portfolio backing across different DeFi protocols.
  • Cross-collateral strategies where user deposits can span multiple tokens but share a single accounting model.

Summary

Standard Purpose Key Upgrade
ERC 4626 Base vault interface for single-asset, synchronous operations Standardized deposit/withdraw flows
ERC 7540 Asynchronous vaults Adds request/claim lifecycle for delayed operations
ERC 7575 Multi-asset vaults Extends vault logic to multiple underlying tokens

Together, these standards form a progressive stack — ERC 4626 for simple yield vaults, ERC 7540 for asynchronous protocols, and ERC 7575 for multi-asset management — enabling consistent, composable, and extensible vault design across the Ethereum ecosystem.

Example Implementations

We are going to have a look at an example implementation for each of the standards in the next posts.

ERC-4626 Vault

this example, we’ll explore a minimal implementation of an ERC‑4626 vault built with OpenZeppelin’s library.

The vault accepts a single deposit token and automatically provides liquidity to a Uniswap V2 pool by pairing it with a second token, allowing the vault to earn trading fees from the pool.

DISCLAIMER: This code is provided for educational purposes in a blog example. It has NOT been audited or thoroughly tested and is NOT ready for production or mainnet deployment. Use it at your own risk. Bugs, incorrect accounting, and economic exploits may exist.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "openzeppelin-contracts/token/ERC20/extensions/ERC4626.sol";

interface IUniswapV2Router {
    function swapExactTokensForTokens(
        uint amountIn, uint amountOutMin,
        address[] calldata path, address to, uint deadline
    ) external returns (uint[] memory amounts);

    function addLiquidity(
        address tokenA, address tokenB,
        uint amountADesired, uint amountBDesired,
        uint amountAMin, uint amountBMin,
        address to, uint deadline
    ) external returns (uint amountA, uint amountB, uint liquidity);

    function removeLiquidity(
        address tokenA,
        address tokenB,
        uint liquidity,
        uint amountAMin,
        uint amountBMin,
        address to,
        uint deadline
    ) external returns (uint amountA, uint amountB);
}

contract OneSidedLPVault is ERC4626 {
    IERC20 public immutable tokenA;
    IERC20 public immutable tokenB;
    IUniswapV2Router public immutable router;
    IERC20 public immutable lpToken;
    address[] private swapPath;

    constructor(
        IERC20 _tokenA,
        IERC20 _tokenB,
        IERC20 _lpToken,
        IUniswapV2Router _router
    )
        ERC20("One‑Sided LP Vault", "vA")
        ERC4626(_tokenA)
    {
        tokenA = _tokenA;
        tokenB = _tokenB;
        lpToken = _lpToken;
        router = _router;
        swapPath = new address[](2);
        swapPath[0] = address(_tokenA);
        swapPath[1] = address(_tokenB);
    }

    function deposit(uint256 assets, address receiver)
        public override returns (uint256 shares)
    {
        tokenA.transferFrom(msg.sender, address(this), assets);
        tokenA.approve(address(router), assets);

        uint256 half = assets / 2;

        // swap half of A for B
        router.swapExactTokensForTokens(
            half, 0, swapPath, address(this), block.timestamp
        );

        uint256 balA = tokenA.balanceOf(address(this));
        uint256 balB = tokenB.balanceOf(address(this));

        tokenB.approve(address(router), balB);

        (, , uint256 liquidity) = router.addLiquidity(
            address(tokenA), address(tokenB),
            balA, balB, 0, 0, address(this), block.timestamp
        );

        shares = liquidity;
        _mint(receiver, shares);
    }

    function redeem(
        uint256 shares,
        address receiver,
        address owner
    ) public override returns (uint256 assets) {
        if (msg.sender != owner) _spendAllowance(owner, msg.sender, shares);

        // Remove the user’s portion of LP liquidity
        uint256 lpBalance = lpToken.balanceOf(address(this));
        uint256 totalShares = totalSupply();

        // share of LP tokens backing these shares
        uint256 lpAmount = (lpBalance * shares) / totalShares;

        // Burn shares from owner
        _burn(owner, shares);

        lpToken.approve(address(router), lpAmount);

        (uint256 amtA, uint256 amtB) = router.removeLiquidity(
            address(tokenA),
            address(tokenB),
            lpAmount,
            0,
            0,
            address(this),
            block.timestamp
        );

        // Swap all B -> A
        tokenB.approve(address(router), amtB);

        address[] memory path = new address[](2);
        path[0] = address(tokenB);
        path[1] = address(tokenA);

        uint256[] memory amounts = router.swapExactTokensForTokens(
            amtB,
            0, // accept any for demo, but should set slippage
            path,
            address(this),
            block.timestamp
        );

        uint256 swappedA = amounts[amounts.length - 1];

        assets = amtA + swappedA;

        tokenA.transfer(receiver, assets);
    }
}

ERC-7540 Async Vault

Below is a toy contract that extends an ERC‑4626‑like vault but introduces the extra bookkeeping needed for asynchronous deposits and withdrawals. At the time of writing, OpenZeppelin does not provide an official ERC‑7540 implementation, so this is a simplified example to illustrate the concept.

No real asset management or yield logic here, it’s purely to show the moving parts.

DISCLAIMER: This code is provided for educational purposes in a blog example. It has NOT been audited or thoroughly tested and is NOT ready for production or mainnet deployment. Use it at your own risk. Bugs, incorrect accounting, and economic exploits may exist.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract AsyncVault7540 {
    IERC20 public immutable asset;
    uint256 public totalShares;
    mapping(address => uint256) public balanceOf;

    // New async request tracking
    struct PendingRequest {
        uint256 assets;
        bool completed;
    }

    // pending deposits and withdrawals keyed by user
    mapping(address => PendingRequest) public pendingDeposits;
    mapping(address => PendingRequest) public pendingWithdrawals;

    event DepositRequested(address indexed user, uint256 assets);
    event DepositFinalized(address indexed user, uint256 assets, uint256 shares);
    event WithdrawRequested(address indexed user, uint256 shares);
    event WithdrawFinalized(address indexed user, uint256 shares, uint256 assets);

    constructor(IERC20 _asset) {
        asset = _asset;
    }

    // --- async deposit ---

    /// @notice User signals intent to deposit;
    /// funds move now but shares arrive later.
    function requestDeposit(uint256 assets) external {
        asset.transferFrom(msg.sender, address(this), assets);
        pendingDeposits[msg.sender] = PendingRequest(assets, false);
        emit DepositRequested(msg.sender, assets);
    }

    /// @notice Later, when settlement logic (off‑chain or by strategy) says ready.
    function finalizeDeposit(address user) external {
        PendingRequest storage r = pendingDeposits[user];
        require(!r.completed, "already finalized");

        uint256 shares = assetsToShares(r.assets);
        balanceOf[user] += shares;
        totalShares += shares;

        r.completed = true;
        emit DepositFinalized(user, r.assets, shares);
    }

    function requestWithdrawal(uint256 shares) external {
        require(balanceOf[msg.sender] >= shares, "not enough shares");
        balanceOf[msg.sender] -= shares;
        totalShares -= shares;

        pendingWithdrawals[msg.sender] = PendingRequest(shares, false);
        emit WithdrawRequested(msg.sender, shares);
    }

    /// @notice Later once underlying liquidity becomes available.
    function finalizeWithdrawal(address user) external {
        PendingRequest storage r = pendingWithdrawals[user];
        require(!r.completed, "already finalized");

        uint256 assets = sharesToAssets(r.assets);
        asset.transfer(user, assets);

        r.completed = true;
        emit WithdrawFinalized(user, r.assets, assets);
    }

    // simplistic conversions for demo purposes
    function assetsToShares(uint256 a) public pure returns (uint256) { return a; }
    function sharesToAssets(uint256 s) public pure returns (uint256) { return s; }
}

interface IERC20 {
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
    function transfer(address to, uint256 amount) external returns (bool);
}

ERC-7575 Multi‑Asset Vault

Below you will find a multi‑asset vault implementation system extends the behavior of ERC-4626 to support multiple vaults under a single shared share token, following the principles of ERC‑7575 (“Multi‑Vault Standard”).

Each vault handles deposits and withdrawals for one ERC‑20 asset, but instead of minting a dedicated vault token, all vaults mint and burn the same mSHARE token, giving users unified ownership across the system.

The architecture makes it possible for wallets, aggregators, or protocols:

  • View all vaults and their assets through a single interface.
  • Aggregate user balances across multiple assets.
  • Transfer or trade multi‑vault shares seamlessly.

Contracts Overview

  1. SharedVaultShare An ERC‑20‑like share token (mSHARE) used across all vaults. Also acts as an ERC‑7575 registry, mapping each asset to its vault. Key functions:
    • vault(asset): returns the vault address linked to the asset.
    • shares(asset): returns the shared share token address (mSHARE).
  2. AssetVault A simplified ERC-4626‑compatible vault managing deposits and withdrawals for one ERC‑20 asset. Mints and burns mSHARE to represent ownership. Can be queried via:
    • asset(): underlying ERC‑20 token address.
    • totalAssets(): total amount of asset held.
    • deposit() / withdraw(): user entry and exit points.
  3. TwoAssetVaultSystem Example deployment contract showing how to initialize two vaults (e.g., USDC and DAI) under one unified SharedVaultShare. Automatically registers each vault within the shared registry.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/* ============================================================
                        INTERFACES
   ============================================================ */

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address to, uint256 value) external returns (bool);
    function transferFrom(address from, address to, uint256 value) external returns (bool);
    function approve(address spender, uint256 value) external returns (bool);
    function allowance(address owner, address spender) external view returns (uint256);
}

/**
 * @dev Minimal ERC‑7575 registry interface.
 * Lets external integrators discover vaults and shares for assets.
 */
interface IERC7575Registry {
    function vault(address asset) external view returns (address);
    function shares(address asset) external view returns (address);
}

/* ============================================================
                        SHARED SHARE TOKEN
   ============================================================ */

contract SharedVaultShare is IERC7575Registry {
    string public constant name = "MultiVault Share";
    string public constant symbol = "mSHARE";
    uint8 public constant decimals = 18;

    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;

    // Asset → Vault mapping
    mapping(address => address) private vaultForAsset;

    event Transfer(address indexed from, address indexed to, uint256 value);
    event VaultRegistered(address indexed asset, address vault);

    /* ---------------- ERC20‑like internal mint/burn ---------------- */
    function mint(address to, uint256 amount) external {
        balanceOf[to] += amount;
        totalSupply += amount;
        emit Transfer(address(0), to, amount);
    }

    function burn(address from, uint256 amount) external {
        uint256 bal = balanceOf[from];
        require(bal >= amount, "insufficient");
        balanceOf[from] = bal - amount;
        totalSupply -= amount;
        emit Transfer(from, address(0), amount);
    }

    /* ---------------- ERC7575 registry views ---------------- */
    function vault(address asset) public view override returns (address) {
        return vaultForAsset[asset];
    }

    function shares(address asset) public view override returns (address) {
        require(vaultForAsset[asset] != address(0), "unknown asset");
        return address(this); // same share token for all vaults
    }

    /* ---------------- Registry management ---------------- */
    function registerVault(address asset, address vaultAddr) external {
        vaultForAsset[asset] = vaultAddr;
        emit VaultRegistered(asset, vaultAddr);
    }
}

/* ============================================================
                        ERC4626‑STYLE VAULT
   ============================================================ */

contract AssetVault {
    IERC20   public immutable underlying;
    SharedVaultShare public immutable shareToken;

    uint256 public totalManaged;

    event Deposit(address indexed caller, address indexed receiver, uint256 assets, uint256 shares);
    event Withdraw(address indexed caller, address indexed receiver, address indexed owner, uint256 assets, uint256 shares);

    constructor(IERC20 _asset, SharedVaultShare _share) {
        underlying = _asset;
        shareToken = _share;
    }

    /* ---------- ERC4626 views ---------- */

    function asset() external view returns (address) {
        return address(underlying);
    }

    function totalAssets() public view returns (uint256) {
        return totalManaged;
    }

    function convertToShares(uint256 assets) public view returns (uint256 shares) {
        uint256 supply = shareToken.totalSupply();
        return supply == 0 ? assets : assets * supply / totalManaged;
    }

    function convertToAssets(uint256 shares) public view returns (uint256 assets) {
        uint256 supply = shareToken.totalSupply();
        return supply == 0 ? shares : shares * totalManaged / supply;
    }

    /* ---------- ERC4626 mutators ---------- */

    function deposit(uint256 assets, address receiver) external returns (uint256 shares) {
        require(assets > 0, "zero");
        underlying.transferFrom(msg.sender, address(this), assets);
        totalManaged += assets;
        shares = convertToShares(assets);
        shareToken.mint(receiver, shares);
        emit Deposit(msg.sender, receiver, assets, shares);
    }

    function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares) {
        require(assets > 0, "zero");
        shares = convertToShares(assets);
        shareToken.burn(owner, shares);
        totalManaged -= assets;
        underlying.transfer(receiver, assets);
        emit Withdraw(msg.sender, receiver, owner, assets, shares);
    }

    /* ---------- ERC7575 alignment ---------- */

    /// @notice Return the common share token used by this vault group
    function shares() external view returns (address) {
        return address(shareToken);
    }
}

/* ============================================================
                        TWO‑ASSET DEPLOYMENT
   ============================================================ */

contract TwoAssetVaultSystem {
    SharedVaultShare public share;
    AssetVault public usdcVault;
    AssetVault public daiVault;

    constructor(IERC20 usdc, IERC20 dai) {
        share = new SharedVaultShare();

        usdcVault = new AssetVault(usdc, share);
        daiVault  = new AssetVault(dai,  share);

        share.registerVault(address(usdc), address(usdcVault));
        share.registerVault(address(dai),  address(daiVault));
    }
}

How to deploy

Deploy the TwoAssetVaultSystem contract by specifying the addresses of two ERC‑20 tokens (e.g., USDC and DAI):

new TwoAssetVaultSystem(usdcAddress, daiAddress);

Deployment automatically:

  • Creates a new SharedVaultShare (mSHARE) token.
  • Deploys two AssetVaults (one for each token).
  • Registers them with the shared registry.
  • Retrieve contract addresses:
const shareToken  = await system.share();        // => address of mSHARE
const usdcVault   = await system.usdcVault();    // => vault for USDC
const daiVault    = await system.daiVault();     // => vault for DAI

How to interact

  • Deposit into vault:
// Approve vault to spend user's USDC
await usdc.approve(usdcVault, 1_000e6);

// Deposit 1000 USDC and mint mSHARE
await usdcVault.deposit(1_000e6, userAddress);
  • Withdraw from a vault
// Redeem equivalent USDC by burning mSHARE
await usdcVault.withdraw(500e6, userAddress, userAddress);
  • Query registry
const vaultForDai = await share.vault(daiAddress);
const shareAddr = await share.shares(daiAddress);

Both return addresses that prove DAI is linked to its vault and governed by the same mSHARE.

Conclusion

Vaults have become one of the foundational building blocks in DeFi, enabling users to generate yield, automate portfolio management, and interact with complex financial strategies through a simple standardized interface.

The progression from ERC 4626 to ERC 7540 and ERC 7575 represents the natural evolution of the vault ecosystem:

  • ERC 4626 brought uniformity to single‑asset, synchronous vaults.
  • ERC 7540 expanded this model to asynchronous environments, accommodating delayed or cross‑chain operations.
  • ERC 7575 unified entire vault families under shared accounting, making multi‑asset strategies native to Ethereum standards.

Together, these standards create a composable architecture for on‑chain asset management, one that is modular, interoperable, and future‑proof.

From single‑token yield vaults to multi‑token portfolio managers, developers now have a coherent set of tools to build the next generation of decentralized investment products.

As the ecosystem continues to mature, these vault standards will serve as the foundation for more sophisticated, interoperable, and user‑friendly DeFi protocols.