🎟️The Dangers of Not Properly Implementing ERC-4626 in Yield Vaults

ERC-4626 is a tokenized vault standard designed to optimize and standardize yield-bearing vaults. It provides a common framework for managing assets deposited into a vault and specifies how shares and assets are handled. However, improper implementation of ERC-4626 can lead to significant vulnerabilities, such as incorrect accounting of assets, over-withdrawing of funds, and violations of the protocol, which can cause integration issues with other systems.

In this tutorial, we will discuss the importance of following ERC-4626 correctly, highlight the vulnerabilities that arise when this standard is not properly implemented, and provide guidance on how to mitigate these issues.


Understanding ERC-4626: The Tokenized Vault Standard

The ERC-4626 standard defines the behavior for tokenized vaults. Tokenized vaults allow users to deposit assets (e.g., ETH, USDC) into a vault in exchange for shares that represent their claim on the underlying assets. These shares are redeemable for a proportional amount of assets held by the vault.

Key functions defined by ERC-4626 include:

  • deposit(uint256 assets, address receiver): Allows users to deposit assets and receive shares.

  • mint(uint256 shares, address receiver): Allows users to mint a specific number of shares by depositing the corresponding amount of assets.

  • withdraw(uint256 assets, address receiver, address owner): Allows users to redeem their shares for a specific amount of assets.

  • maxWithdraw(address owner): Returns the maximum amount of assets that can be withdrawn.

  • convertToShares(uint256 assets): Converts assets into their equivalent number of shares.

  • convertToAssets(uint256 shares): Converts shares into the equivalent number of assets.

One of the main advantages of ERC-4626 is that it standardizes how vaults interact with assets and shares, making it easier to integrate with various protocols.


Vulnerability: Incorrect Implementation of ERC-4626

Improper implementation of the maxWithdraw and maxRedeem functions in the ERC-4626 vault standard can lead to serious vulnerabilities. For example, if the vault's convertToAssets() function is used incorrectly, it may return too many assets, leading to a violation of the ERC-4626 specification and the potential for users to withdraw more than they should.

In the example provided, the vault's _maxYieldVaultWithdraw() function incorrectly uses convertToAssets() to calculate the maximum amount that can be withdrawn. Since convertToAssets() is only an approximation, it may overestimate the assets, causing functions like maxWithdraw() and maxRedeem() to return too much, leading to excessive withdrawals.

Impact

  1. Over-withdrawals: If users are allowed to withdraw more than they should, this can deplete the vault's assets, leaving it under-collateralized or even insolvent.

  2. Violation of ERC-4626 Specification: Failing to implement functions like maxWithdraw() and maxRedeem() properly leads to a violation of the ERC-4626 standard, which can create compatibility issues with protocols that rely on ERC-4626 compliance.

  3. Inaccurate Accounting: Incorrect calculations of the assets and shares lead to inaccurate accounting, which can break the vault's internal logic and affect its interaction with other yield-bearing strategies or protocols.


Proof of Concept: Vulnerable maxWithdraw Implementation

Consider the following example, where _maxYieldVaultWithdraw() uses convertToAssets() to calculate the maximum amount of assets that can be withdrawn from the vault:

function _maxYieldVaultWithdraw() internal view returns (uint256) {
    return yieldVault.convertToAssets(yieldVault.maxRedeem(address(this)));
}

In this case, convertToAssets() is used to calculate the asset amount, but since convertToAssets() is an approximation, this can result in returning too many assets. Consequently, the maxWithdraw() function might allow users to withdraw more than they should:

function maxWithdraw(address owner) public view returns (uint256) {
    return _maxYieldVaultWithdraw();
}

This flawed implementation violates the ERC-4626 standard, which specifies that these functions must accurately reflect the maximum allowable withdrawal based on the vault's actual holdings and the user's share balance.


Mitigating the Vulnerability: Correctly Implementing ERC-4626

To ensure compliance with ERC-4626 and avoid over-withdrawal vulnerabilities, you need to adjust the logic used in functions like maxWithdraw() and maxRedeem(). Instead of using convertToAssets(), which can lead to inaccuracies, it is recommended to use previewRedeem() or similar functions that provide a more accurate calculation of the maximum redeemable assets.

Step 1: Replace convertToAssets() with previewRedeem()

The ERC-4626 standard includes the previewRedeem() function, which gives a more accurate estimate of the assets that can be withdrawn based on the number of shares:

function _maxYieldVaultWithdraw() internal view returns (uint256) {
    return yieldVault.previewRedeem(yieldVault.maxRedeem(address(this)));
}

This change ensures that the vault returns the correct amount of assets when calculating the maximum allowable withdrawal.

Step 2: Ensure Proper Validation in deposit and mint

In addition to fixing the maxWithdraw() and maxRedeem() functions, make sure that deposit() and mint() functions correctly validate inputs to prevent over-deposits or over-minting of shares. For example, ensure that users cannot exceed the vault's deposit or mint limits.

function deposit(uint256 _assets, address _receiver) public override returns (uint256) {
    uint256 shares = convertToShares(_assets);
    if (shares > maxDeposit(_receiver)) {
        revert DepositMoreThanMax(_receiver, _assets, maxDeposit(_receiver));
    }

    _deposit(_receiver, _assets, shares);
    return shares;
}

By properly validating the inputs and ensuring compliance with the ERC-4626 specification, you reduce the risk of over-deposits and maintain accurate vault accounting.

Step 3: Test the Implementation with Edge Cases

To ensure robustness, test the vault implementation with various edge cases, such as withdrawing the maximum possible amount, attempting to mint more shares than allowed, and exceeding deposit limits. This helps identify potential rounding errors or other issues that could break compliance with ERC-4626.


Conclusion

Properly implementing ERC-4626 is crucial for the security and functionality of tokenized vaults. Failing to correctly implement key functions like maxWithdraw(), maxRedeem(), deposit(), and mint() can lead to over-withdrawals, inaccurate accounting, and violations of the standard. These issues can have serious consequences, including protocol insolvency or broken integrations with other systems.

By adhering to the ERC-4626 specification and using more accurate functions like previewRedeem() for calculations, you can ensure that your vaults remain compliant and secure. Proper testing and validation are essential to maintain accuracy and prevent users from exploiting vulnerabilities caused by improper implementation.

Last updated