Use Case

ERC4626 is a standard for creating yield-bearing tokens that are compatible with ERC20. This assertion enforces a critical security invariant in ERC4626 vaults: the total shares must never exceed the total assets converted to shares. This is essential because:

  • Prevents share price manipulation that could lead to loss of user funds
  • Ensures the vault maintains proper accounting of user deposits
  • Protects against potential overflow attacks in share calculations
  • Maintains the integrity of the yield-bearing token’s value proposition

For example, in a vault with 1000 USDC total assets and 1000 total shares:

  • Normal case: 1 share = 1 USDC
  • If somehow total shares become 2000: 1 share = 0.5 USDC
  • Users who deposited expecting 1:1 ratio would lose 50% of their value

This assertion is particularly important for:

  • Preventing unauthorized share minting that could dilute existing holders
  • Ensuring accurate share price calculations for deposits and withdrawals
  • Maintaining proper accounting of user positions in the vault
  • Protecting against potential arithmetic overflow in share calculations
  • Detecting direct storage manipulation attempts
  • Ensuring fair distribution of yield to all holders

Applicable Protocols

  • Yield aggregators that use ERC4626 for their vaults
  • Lending protocols implementing ERC4626 for their deposit tokens
  • Liquidity pools using ERC4626 for LP token representation
  • Staking protocols that wrap rewards in ERC4626 vaults

Each of these protocol types relies on accurate share-to-asset conversion for:

  • Proper distribution of yield to depositors
  • Accurate calculation of user positions
  • Fair withdrawal amounts for users
  • Correct accounting of protocol fees

Explanation

The assertion implements a basic approach to verify the ERC4626 share-to-asset relationship. This relationship is fundamental to ERC4626 vaults, where shares represent proportional ownership of the vault’s assets. The total assets must always be sufficient to back all outstanding shares to maintain the integrity of the vault’s accounting.

The assertion uses the following cheatcodes and functions:

  • registerStorageChangeTrigger(): Triggers the assertion when the total supply storage slot changes

The implementation performs a fundamental check:

  • It verifies that the total assets are sufficient to back all outstanding shares
  • This ensures that share value is preserved and users can withdraw their assets

This approach ensures that:

  1. There are always enough assets to back all shares
  2. Users’ share values remain accurate and fair
  3. No value can be extracted improperly from the vault

For more information about cheatcodes, see the Cheatcodes Documentation.

Code Example

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

import {Assertion} from "credible-std/Assertion.sol";
import {PhEvm} from "credible-std/PhEvm.sol";

interface IERC4626 {
    function totalAssets() external view returns (uint256);
    function totalSupply() external view returns (uint256);
    function convertToShares(uint256 assets) external view returns (uint256);
    function convertToAssets(uint256 shares) external view returns (uint256);
}

contract ERC4626AssetsSharesAssertion is Assertion {
    IERC4626 public vault;

    constructor(address _vault) {
        vault = IERC4626(_vault);
    }

    function triggers() external view override {
        // Register trigger specifically for changes to the total supply storage slot
        registerStorageChangeTrigger(
            this.assertionAssetsShares.selector,
            bytes32(uint256(1)) // Total supply storage slot
        );
    }

    // Assert that the total assets are sufficient to back all shares
    function assertionAssetsShares() external {
        uint256 totalAssets = vault.totalAssets();
        uint256 totalSupply = vault.totalSupply();

        // Calculate how many assets are needed to back all shares
        uint256 requiredAssets = vault.convertToAssets(totalSupply);

        // The total assets should be at least what's needed to back all shares
        require(totalAssets >= requiredAssets, "Not enough assets to back all shares");
    }
}

Note: This code example is maintained in the Phylax Assertion Examples Repository. For a full examples with mock protocol code and tests please refer to the repository.

Testing

To test this assertion effectively:

  1. Basic Functionality:

    • Deploy an ERC4626 vault with test tokens
    • Verify the assertion passes for normal deposits and withdrawals
    • Test edge cases with zero assets and maximum values
  2. Manipulation Attempts:

    • Try to manipulate share calculations through direct storage access
    • Attempt to create more shares than assets through complex transactions
    • Test potential overflow scenarios in share calculations

Assertion Best Practices

  • Combine this assertion with assertions covering other ERC4626 invariants
  • Monitor both total assets and total shares changes for complete coverage