I have recently been revising Solidity to consolidate the details, and am writing a "WTF Simplified Introduction to Solidity" for beginners to use (programming experts can find other tutorials), with weekly updates of 1-3 lectures.
Twitter: @0xAA_Science
Community: Discord|WeChat group|Official website wtf.academy
All code and tutorials are open source on GitHub: github.com/AmazingAng/WTF-Solidity
We often say that DeFi is like LEGO blocks, where you can create new protocols by combining multiple protocols. However, due to the lack of standards in DeFi, its composability is severely affected. ERC4626 extends the ERC20 token standard and aims to standardize yield vaults. In this talk, we will introduce the new generation DeFi standard - ERC4626 and write a simple vault contract. The teaching code reference comes from the ERC4626 contract in Openzeppelin and Solmate and is for teaching purposes only.
Vault
The vault contract is the foundation of DeFi LEGO blocks. It allows you to stake basic assets (tokens) to the contract in exchange for certain returns, including the following scenarios:
- Yield farming: In Yearn Finance, you can stake USDT to earn interest.
- Borrow/Lend: In AAVE, you can supply ETH to earn deposit interest and get a loan.
- Stake: In Lido, you can stake ETH to participate in ETH 2.0 staking and obtain stETH that can be used to earn interest.
ERC4626

Since the vault contract lacks standards, there are various ways of implementation. A yield aggregator needs to write many interfaces to connect with different DeFi projects. The ERC4626 Tokenized Vault Standard has emerged to enable easy expansion of DeFi with the following advantages:
-
Tokenization: ERC4626 inherits ERC20. When depositing to the vault, you will receive vault shares that are also compliant with the ERC20 standard. For example, when staking ETH, you will automatically get stETH as your share.
-
Better liquidity: Due to tokenization, you can use vault shares to do other things without withdrawing the underlying assets. For example, you can use Lido's stETH to provide liquidity or trade on Uniswap, without withdrawing any ETH.
-
Better composability: With the standard in place, a single set of interfaces can interact with all ERC4626 vaults, making it easier to develop applications, plugins, and tools based on vaults.
In summary, the importance of ERC4626 for DeFi is no less than that of ERC721 for NFTs.
Key Points of ERC4626
The ERC4626 standard mainly implements the following logic:
- ERC20: ERC4626 inherits ERC20, and the vault shares are represented by ERC20 tokens: users deposit specific ERC20 underlying assets (such as WETH) into the vault, and the contract mints a specific number of vault share tokens for them; When users withdraw underlying assets from the vault, the corresponding number of vault share tokens will be destroyed. The
asset()
function returns the token address of the vault's underlying asset. - Deposit logic: allows users to deposit underlying assets and mint the corresponding number of vault shares. Related functions are
deposit()
andmint()
. Thedeposit(uint assets, address receiver)
function allows users to depositassets
units of assets and mint the corresponding number of vault shares to thereceiver
address.mint(uint shares, address receiver)
is similar, except that it takes the minted vault shares as a parameter. - Withdrawal logic: allows users to destroy vault share tokens and withdraw the corresponding number of underlying assets from the vault. Related functions are
withdraw()
andredeem()
, the former taking the amount of underlying assets to be withdrawn as a parameter, and the latter taking the number of destroyed vault share tokens as a parameter. - Accounting and limit logic: other functions in the ERC4626 standard are for asset accounting in the vault, deposit and withdrawal limits and the number of underlying assets and vault shares for deposit and withdrawal.
IERC4626 Interface Contract
The IERC4626 interface contract includes a total of 2
events:
Deposit
event: triggered when depositing.Withdraw
event: triggered when withdrawing.
The IERC4626 interface contract also includes 16
functions, which are classified into 4
categories according to their functionality: metadata functions, deposit/withdrawal logic functions, accounting logic functions, and deposit/withdrawal limit logic functions.
-
Metadata
asset()
: returns the address of the underlying asset token of the vault, which is used for deposit and withdrawal.
-
Deposit/Withdrawal Logic
deposit()
: a function that allows users to depositassets
units of the underlying asset into the vault, and the contract mintsshares
units of the vault's shares to thereceiver
address. It releases aDeposit
event.mint()
: a minting function (also a deposit function) that allows users to depositassets
units of the underlying asset and the contract mints the corresponding amount of the vault's shares to thereceiver
address. It releases aDeposit
event.withdraw()
: a function that allows theowner
address to burnshare
units of the vault's shares, and the contract sends the corresponding amount of the underlying asset to thereceiver
address.redeem()
: a redemption function (also a withdrawal function) that allows theowner
address to burnshare
units of the vault's shares and the contract sends the corresponding amount of the underlying asset to thereceiver
address.
-
Accounting Logic
totalAssets()
: returns the total amount of underlying asset tokens managed in the vault.convertToShares()
: returns the amount of vault shares that can be obtained by using a certain amount of the underlying asset.convertToAssets()
: returns the amount of underlying asset that can be obtained by using a certain amount of vault shares.previewDeposit()
: used by users to simulate the amount of vault shares they can obtain by depositing a certain amount of the underlying asset in the current on-chain environment.previewMint()
: used by users to simulate the amount of underlying asset needed to mint a certain amount of vault shares in the current on-chain environment.previewWithdraw()
: used by users to simulate the amount of vault shares they need to redeem to withdraw a certain amount of the underlying asset in the current on-chain environment.previewRedeem()
: used by on-chain and off-chain users to simulate the amount of underlying asset they can redeem by burning a certain amount of vault shares in the current on-chain environment.
-
Deposit/Withdrawal Limit Logic
maxDeposit()
: returns the maximum amount of the underlying asset that a certain user address can deposit in a single deposit.maxMint()
: returns the maximum amount of vault shares that a certain user address can mint in a single mint.maxWithdraw()
: returns the maximum amount of the underlying asset that a certain user address can withdraw in a single withdrawal.
-
maxRedeem()
: Returns the maximum vault quota that can be destroyed in a single redemption for a given user address.
ERC4626 Contract
Here, we are implementing an extremely simple version of tokenized vault contract:
- The constructor initializes the address of the underlying asset contract, the name, and symbol of the vault shares token. Note that the name and symbol of the vault shares token should be associated with the underlying asset. For example, if the underlying asset is called
WTF
, the vault shares should be calledvWTF
. - When a user deposits
x
units of the underlying asset into the vault,x
units (equivalent) of vault shares will be minted. - When a user withdraws
x
units of vault shares from the vault,x
units (equivalent) of the underlying asset will be withdrawn as well.
Note: In actual use, special care should be taken to consider whether the accounting logic related functions should be rounded up or rounded down. You can refer to the implementations of OpenZeppelin and Solmate. However, this is not considered in the teaching example in this section.
Remix
Demo
Note: the demo show below uses the second remix account, which is 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2
, to deploy and call functions.
-
Deploy an
ERC20
token contract with the token name and symbol both set toWTF
, and mint yourself10000
tokens. -
Deploy an
ERC4626
token contract with the underlying asset contract address set to the address ofWTF
, and set the name and symbol tovWTF
. -
Call the
approve()
function of theERC20
contract to authorize theERC4626
contract. -
Call the
deposit()
function of theERC4626
contract to deposit1000
tokens. Then call thebalanceOf()
function to check that your vault share has increased to1000
. -
Call the
mint()
function of theERC4626
contract to deposit another1000
tokens. Then callbalanceOf()
function to check that your vault share has increased to2000
. -
Call the
withdraw()
function of theERC4626
contract to withdraw1000
tokens. Then call thebalanceOf()
function to check that your vault share has decreased to1000
. -
Call the
redeem()
function of theERC4626
contract to withdraw1000
tokens. Then call thebalanceOf()
function to check that your vault share has decreased to0
.
Summary
In this lesson, we introduced the ERC4626 tokenized vault standard and wrote a simple vault contract that converts underlying assets to 1:1 vault share tokens. The ERC4626 standard improves the liquidity and composability of DeFi and it will gradually become more popular in the future. What applications would you build with ERC4626?