Many DeFi projects issue an ERC-20 token representing a lending position. Compound v2 cTokens accrue interest through a changing exchange rate against the underlying asset. Aave aTokens work differently: their balances grow as interest accrues. These are useful motivations for a common vault interface, but neither naming convention by itself establishes ERC-4626 compliance.

Racoon Meme

Or you have an asset like xSushi which we explained here, that will generate income from the trading fees on SushiSwap. The xSushi will represent ownership over all the Sushi tokens currently in the contract. This is very similar to the previous lending platform.

And then you also have aggregators like Yearn or Rari Vaults. Those are services that you can put your funds in and that have some mechanism to put those funds into various other projects that generate yield. And the ownership over the funds of such so-called vaults are also handled by an ERC-20 token. And since the vault funds will increase over time, you again have a similar situation in as on a lending platform.

So now that we have all these services implementing something similar, it only makes sense to standardize it. Rather than adhering to dozens of different interfaces, an aggregator has to implement only one. Or let's say you want to implement additional features for such tokens, you also benefit from having a single standardized interface. This is ERC-4626. So if you want to implement your own interest generating token, an aggregator or just want to learn more about Defi, this post is for you.

Will Smith - Chris Rock Money Market Meme

Overview of ERC-4626

The standard describes tokenized shares in a vault with one underlying ERC-20 asset. It defines an interface, not a guarantee of yield or protection from losses. The vault itself is an ERC-20 standard extension. In theory vaults may generate interest over various tokens. But the first and most important standard is just for a single ERC-20 token. In the future one could imagine more advanced standards for multiple tokens and possibly not only ERC-20. For now it's kept as simple as possible.

On a high-level there's functionality to deposit and redeem the vault ownership token for the underlying asset. There are two functions each, one for using the amounts of the underlying as input and one for using the amounts of the ownership token (shares) as input. And then you have some additional helper functions for conversions, receiving maximum amounts and previews. Let's get into the details!

Below infographic was provided by MidasCapital, thank you!

ERC-4626

1. The Execution Functions

You can call deposit to pass an amount of the underlying asset, e.g. DAI. The asset will be transferred and in return you will receive shares in that ERC-4626 vault, in the normal case determined by the current conversion rate.

Rounding matters: convertToShares and convertToAssets estimate an ideal, fee-free conversion and round down. Previews model the corresponding operation, including its fees: deposit and redeem round the output down; mint and withdraw round the required input up. Preview functions do not enforce the limits returned by the separate max* methods. Integrations should also enforce suitable slippage bounds.

solidity
function deposit(uint256 assets, address receiver)
    external
    returns (uint256 shares);

Similarly to deposit, you can also use mint. Here instead of passing an amount of the underlying asset, you will pass the amount of shares. And the amount of the underlying being used and transferred will be determined when executing the call.

solidity
function mint(uint256 shares, address receiver)
    external
    returns (uint256 assets);

You can then call redeem to convert shares back into the underlying asset. In redeem you will pass the amount of shares to be burnt and the amount of the underlying asset will be determined when executing the call.

solidity
function redeem(
    uint256 shares,
    address receiver,
    address owner
  ) external returns (uint256 assets);

Like for the deposit/mint, you also have a second function here where instead you can pass the amount of assets you'd like to withdraw. And the amount of shares that have to be burnt will be determined when executing the call.

solidity
function withdraw(
    uint256 assets,
    address receiver,
    address owner
  ) external returns (uint256 shares);

2. Max Functions

Then you will have a bunch of view functions to read the maximally allowed inputs for each previous function.

Know Your Limits Meme
solidity
function maxDeposit(address receiver) external view returns (uint256);

Returns the maximally allowed amount of the underlying asset that can be deposited and passed as input to the deposit function.

solidity
function maxMint(address receiver) external view returns (uint256);

Returns the maximally allowed amount of the shares that can be minted and passed as input to the mint function.

solidity
function maxWithdraw(address owner) external view returns (uint256);

Returns the maximally allowed amount of the underlying asset that can be withdrawn for the owner. It implies the owner may call the withdraw function with a value up to that amount.

solidity
function maxRedeem(address owner) external view returns (uint256);

Returns the maximally allowed amount of shares that can be burnt for the owner. It implies the owner may call the redeem function with a value up to that amount.

3. Assets

This view function will simply return the address of the ERC-20 token contract used as the underlying asset, e.g. the DAI token address.

solidity
function asset() external view returns (address);

This view function will return the total amount of the underlying asset that is managed by the current vault.

solidity
function totalAssets() external view returns (uint256);

4. Converters

There are also two conversion view functions. Rather than having an exchange rate to return those two functions can directly be used to tell you how much current assets would be in shares and vice versa.

To convert an amount of the underlying asset into shares, you can use:

solidity
function convertToShares(uint256 assets) external view returns (uint256);

To convert an amount of shares into the underlying asset, you can use:

solidity
function convertToAssets(uint256 shares) external view returns (uint256);

5. Previews

Lastly, to allow for  simulating the effects of execution functions at the current block, given current on-chain conditions, there are (pre-)view functions available:

solidity
function previewDeposit(uint256 assets) external view returns (uint256);
function previewMint(uint256 shares) external view returns (uint256);
function previewWithdraw(uint256 assets) external view returns (uint256);
function previewRedeem(uint256 shares) external view returns (uint256);

Implementation Example

You can find an opinionated example implementation here.

And here's a version that's slightly easier to understand for educational purposes:

soliditySolidity ^0.8.28
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ERC4626} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";

// OpenZeppelin Contracts 5.x. This vault has no yield strategy.
// Review asset behavior, fees, limits and integration slippage before use.
contract ExampleVault is ERC4626 {
    constructor(IERC20 asset_)
        ERC20("Vault Share", "VSH")
        ERC4626(asset_)
    {}
}