ERC4626 & its quirks

·

9 min read

ERC4626 is a new Ethereum standard for tokenized vaults. It provides a standard interface for developers to build yield-bearing vaults that represent shares of a single underlying ERC-20 token. It outlines an optional extension for tokenized vaults utilizing ERC-20, offering basic functionality for depositing, withdrawing tokens, and reading balances.

So, what exactly is an ERC4626?

ERC4626 is a standard that introduces tokenized vaults. These vaults are essentially an extension of the ERC-20 token standard and come with a built-in vault feature. When you interact with an ERC4626 vault, you're essentially getting a share represented as vault tokens. These tokens are minted by the vaults to represent the assets you've provided to the vault.

One of the key features of ERC4626 vaults is their composability. This means you can use them alongside other DeFi applications and protocols. For example, you can use an ERC4626 vault to provide collateral to a lending protocol.

What is a tokenized vault, you ask?

Imagine a tokenized vault as a smart contract that securely stores assets while creating tokens that signify ownership shares in those assets. For instance, consider a tokenized vault holding Ethereum (ETH) and issuing tokens that mirror these ETH shares. Users have the option to deposit their ETH into this vault and, in exchange, receive tokens that serve as proof of their share. Later on, these tokens can be used to redeem their portion of ETH from the vault when needed.

Structure:


interface IERC4626 is IERC20, IERC20Metadata {
    event Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares);

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

    function asset() external view returns (address assetTokenAddress);
    function totalAssets() external view returns (uint256 totalManagedAssets);
    function convertToShares(uint256 assets) external view returns (uint256 shares);
    function convertToAssets(uint256 shares) external view returns (uint256 assets);
    function maxDeposit(address receiver) external view returns (uint256 maxAssets);
    function previewDeposit(uint256 assets) external view returns (uint256 shares);
    function deposit(uint256 assets, address receiver) external returns (uint256 shares);
    function maxMint(address receiver) external view returns (uint256 maxShares);
    function previewMint(uint256 shares) external view returns (uint256 assets);
    function mint(uint256 shares, address receiver) external returns (uint256 assets);
    function maxWithdraw(address owner) external view returns (uint256 maxAssets);
    function previewWithdraw(uint256 assets) external view returns (uint256 shares);
    function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares);
    function maxRedeem(address owner) external view returns (uint256 maxShares);
    function previewRedeem(uint256 shares) external view returns (uint256 assets);
    function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets);
}

Use of Functions:

  1. asset() - When a vault is created, it has an underlying token. The vault token issued in respect to the ERC20. This function returns the address of that underlying ERC20 token. (MUST be an EIP-20 token contract)

  2. totalAssets() - The function returns the total amount of an underlying asset that is managed by the vault. So if a vault manages say - 1000 DAI tokens in total, then this function will return 1000 value.

  3. convertToShares(uint256 assets) - The function returns the amount of equivalent shares that a vault will return for the amount of (underlying) asset that is provided by the user. This calculation does not return the price of a share that a user bought for, but the price of a share for a buyer at the time (live price) of the function call.

  4. convertToAssets(uint256 shares) - The function returns the amount of equivalent assets that a vault will exchange for the amount of shares provided.

  5. maxDeposit(address receiver) - The function returns the maximum amount of (underlying) tokens that can be deposited in a single deposit call by the receiver (assuming user has infinite assets). The receiver in the maxDeposit() function refers to the address of the user who is depositing assets into the vault. As there can be global or user-specific limits based on the protocol's functioning, these limits are imposed accordingly.

  6. previewDeposit(uint256 assets) - The function allows users to simulate the effects of their deposit at the current block. It takes the amount of assets to be deposited as input and returns the number of vault shares that the user would receive in return. For example, a developer could build a front-end application that allows users to preview their deposit before making it. This would allow users to see how many vault shares they would receive and to make sure that they are happy with the amount before making a deposit.

  7. deposit(uint256 assets, address receiver) - The deposit() function allows users to deposit assets into a vault and receive vault tokens in return. The function takes two arguments: the amount of assets to be deposited, and the address of the receiver who will receive the vault tokens. The function first checks to make sure that the user is depositing a valid amount of assets. If the amount is valid, the function then mints vault tokens to the user's address. The number of vault tokens minted is equal to the amount of assets deposited divided by the current exchange rate of the vault.

  8. maxMint(address receiver) - The maxMint() function returns the maximum amount of vault shares that a user can mint in a single mint call without causing a revert. This function is useful for users who want to know how many vault shares they can mint before actually making a mint call.

  9. previewMint(uint256 shares) - The previewMint(uint256 shares) function allows users to simulate the effects of their mint at the current block. It takes the number of shares to be minted as input and returns the amount of assets that the user would need to deposit to mint those shares.

  10. mint(uint256 shares, address receiver) - The mint(uint256 shares, address receiver) function allows users to mint vault shares and receive them at a specified address. It takes two arguments:
    1. The number of vault shares to mint.
    2. The address of the user who will receive the vault shares.

  11. maxWithdraw(address owner) - The maxWithdraw(address owner) function returns the maximum amount of underlying assets that the owner can withdraw from the vault in a single withdraw call. This function is useful for users who want to know how much assets they can withdraw before actually making a withdraw call.

  12. previewWithdraw(uint256 assets) - The previewWithdraw(uint256 assets) function allows users to simulate the effects of their withdrawal at the current block. It takes the amount of assets to be withdrawn as input and returns the number of vault shares that the user would need to burn in order to withdraw those assets.

  13. withdraw(uint256 assets, address receiver, address owner) - The withdraw(uint256 assets, address receiver, address owner) function allows the owner to withdraw assets from the vault and send them to a specified receiver address. It takes three arguments:
    1. The amount of assets to withdraw.
    2. The address of the user who will receive the withdrawn assets.
    3. The address of the owner of the vault shares that will be burned to pay for the withdrawal.

  14. maxRedeem(address owner) - The maxRedeem(address owner) function returns the maximum amount of vault shares that the owner can redeem from the vault in a single redeem call. This function is useful for users who want to know how many vault shares they can redeem before actually making a redeem call.

  15. previewRedeem(uint256 shares) - The previewRedeem(uint256 shares) function allows users to simulate the effects of their redemption at the current block. It takes the number of vault shares to be redeemed as input and returns the amount of underlying assets that the user would receive in return.

  16. redeem(uint256 shares, address receiver, address owner) - The redeem(uint256 shares, address receiver, address owner) function allows the owner to redeem vault shares for the underlying asset and send the assets to a specified receiver address. It takes three arguments:
    1. The number of vault shares to redeem.
    2. The address of the user who will receive the redeemed assets.
    3. The address of the owner of the vault shares that will be burned to pay for the redemption.

Quirks:

ERC4626 is a promising new standard for tokenized vaults. It allows developers to build yield-bearing vaults that represent shares of a single underlying ERC-20 token. However, since there are no specific guidelines on how to implement the logic of the functionalities in the standard, there can be cases where this leads to vulnerablities:

  • Vault Price Reset: The idea refers to a specific scenario where an ERC4626 vault tokens are admitted to register as collateral for a lending protocol. In such a case, with the absence of defensive measures, where the protocol has ingested an inflated price, it may be followed by an attacker withdrawing all the assets from the vault. Since the lending protocol will take its time to reflect the price drop, in the meantime, the attacker can borrow assets (steal) from the lending protocol and hence make a good profit from the protocol as well. (More here)

    A good defense in such a situation would be to issue virtual shares to enforce an initial exchange rate.

  • Precision Loss (Round Up / Round Down) : The issue concerns the instance where the assets/shares calculation does not take into account - the possibility of precision loss. A key measure includes performing a multiplication operation before a division since division operations tend to round down decimals thus by multiplying first we minimize any rounding errors.

    In the case of ERC4626, the simple rule is when we have incoming assets, we round up and when we are sending assets out to the user, we round down. The calculations should be kept in favor of the protocol.

  • First Deposit Issue: When the exchange rate for an asset is calculated using a formula such as, shares = ( assets * totalShares ) / totalAssets, special care needs to be taken. Early users can manipulate the price per share by minting a very small amount in the beginning and inflating the price by depositing a high amount of supporting tokens and profiting off of late users' deposits because of the precision loss caused by the rather large value of the price per share.

    Enforcing a minimum deposit that cannot be withdrawn by minting it to 0x00 address on the first deposit is a good way to mitigate it.

  • Mints wrong amount: There have been examples where the mint function used the "amount" value instead of the "share" value while minting the shares to a user. This could work well if the ratio between those 2 values was maintained but that is not the case. This ratio will eventually change and usually, the asset amount is larger than the share amount as vaults also receive asset yield, the user will receive more than the right amount of shares.

  • No support for fee-on-transfer tokens: As the widely known fee-on-transfer tokens deduce a fee for any transfer, the project implementing the use of ERC20 tokens needs to ensure that accounting of the tokens is done by taking note of the fee that the token itself implements.

  • Use TWAP, not previewRedeem: In ERC4626 Oracles, the function can use the totalSupply (or totalAsset) value which represents the total value of the underlying token. The problem with this is that the value can be manipulated in a single transaction by simply depositing/withdrawing the underlying asset. To remediate this issue, we should implement a TWAP so that the price cannot be inflated or deflated within a single block/transaction or in a short period.

Did you find this article valuable?

Support Mrigendra's Blog by becoming a sponsor. Any amount is appreciated!