Michael.W基于Foundry精读Openzeppelin第52期——ERC4626.sol
- 0. 版本
- 1. 目标合约
- 2. 代码精读
- 2.1 constructor()
- 2.2 maxDeposit(address) && previewDeposit(uint256 assets) && deposit(uint256 assets, address receiver)
- 2.3 maxMint(address) && previewMint(uint256 shares) && mint(uint256 shares, address receiver)
- 2.4 maxWithdraw(address owner) && previewWithdraw(uint256 assets) && withdraw( uint256 assets, address receiver, address owner)
- 2.5 maxRedeem(address owner) && previewRedeem(uint256 shares) && redeem(uint256 shares, address receiver, address owner)
- 2.6 asset() && totalAssets() && convertToShares(uint256 assets) && convertToAssets(uint256 shares)
0. 版本
[openzeppelin]:v4.8.3,[forge-std]:v1.5.6
0.1 ERC4626.sol
ERC4626库本身是一种有底层ERC20资产质押的shares且本身同样满足ERC20标准。用户可以通过deposit或mint方法来质押底层资产并增发shares,也可使用burn或redeem方法来销毁shares并赎回底层资产。需要注意的是:当底层资产接近或等于0时,可以通过事先向本合约转入少许底层资产来急速拉升shares的价格。这本质上是一种基于滑点问题的攻击手段,合约部署者可以向合约内提供一笔初始底层资产来抵御以上攻击。在赎回底层资产的过程中同样也会面临滑点问题,较好的解决方式是为ERC4626的各业务方法套一层带结果校验的wrapper。具体模板可参见:https://github.com/ERC4626-Alliance/ERC4626-Contracts/blob/main/src/ERC4626RouterBase.sol
注:ERC4626标准细节参见:https://eips.ethereum.org/EIPS/eip-4626
1. 目标合约
继承ERC4626合约:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC4626.sol";
contract MockERC4626 is ERC4626 {
constructor(
string memory name,
string memory symbol,
IERC20 asset
)
ERC4626(asset)
ERC20(name, symbol)
{}
function burn(address account, uint amount) external {
_burn(account, amount);
}
function transferAsset(address account, uint amount) external {
IERC20(asset()).transfer(account, amount);
}
}
全部foundry测试合约:
测试使用的物料合约:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
contract MockERC20WithDecimals is ERC20 {
uint8 private _decimals;
constructor(
string memory name,
string memory symbol,
uint8 dec
)
ERC20(name, symbol){
_decimals = dec;
}
function decimals() public view override returns (uint8){
return _decimals;
}
function mint(address account, uint amount) external {
_mint(account, amount);
}
}
contract MockERC20WithLargeDecimals {
function decimals() public pure returns (uint){
return type(uint8).max + 1;
}
}
contract MockERC20WithoutDecimals {}
2. 代码精读
2.1 constructor()
using Math for uint256;
// 底层的ERC20资产合约地址
IERC20 private immutable _asset;
// shares的decimals
uint8 private immutable _decimals;
// 初始化函数
constructor(IERC20 asset_) {
// 获取底层资产的decimals
// 注:success表示asset_的decimals符合预期
(bool success, uint8 assetDecimals) = _tryGetAssetDecimals(asset_);
// 如果asset_的decimals符合预期,则设置本合约的decimals与之保持一致;
// 如果asset_的decimals不符合预期,则设置本合约的decimals与ERC20.decimals()一致,即18
_decimals = success ? assetDecimals : super.decimals();
// 存储asset_地址
_asset = asset_;
}
// 获取ERC20合约asset_的decimals
function _tryGetAssetDecimals(IERC20 asset_) private view returns (bool, uint8) {
// 通过staticcall调用assert_.decimals()
(bool success, bytes memory encodedDecimals) = address(asset_).staticcall(
abi.encodeWithSelector(IERC20Metadata.decimals.selector)
);
if (success && encodedDecimals.length >= 32) {
// 如果上述调用成功且返回值大于等于1个字节,表示合约assert_中存在decimals()方法且有返回值
// 将返回值转成uint256类型
uint256 returnedDecimals = abi.decode(encodedDecimals, (uint256));
if (returnedDecimals <= type(uint8).max) {
// 如果asset_的decimals介于[0,255],则认为是一个符合预期的decimals
// 返回true和uint8类型的decimals
return (true, uint8(returnedDecimals));
}
}
// 如果上述调用不成功 或 调用无返回值 或 返回值转成uint256后大于255,则认为asset_的decimals不符合预期。返回false和0
return (false, 0);
}
// 获取本shares的decimals
// 注:此处是对IERC20标准中decimals方法的重写
function decimals() public view virtual override(IERC20Metadata, ERC20) returns (uint8) {
return _decimals;
}
foundry代码验证:
contract ERC4626Test is Test {
MockERC20WithDecimals private _asset = new MockERC20WithDecimals("test name", "test symbol", 6);
MockERC4626 private _testing = new MockERC4626("test name", "test symbol", _asset);
function test_Constructor() external {
// case 1: asset with uint8 decimal
assertEq(_testing.decimals(), 6);
assertEq(_testing.asset(), address(_asset));
// case 2: asset with decimal that > type(uint8).max
MockERC20WithLargeDecimals _assetWithLargeDecimals = new MockERC20WithLargeDecimals();
_testing = new MockERC4626("test name", "test symbol", IERC20(address(_assetWithLargeDecimals)));
// default decimals 18 of shares with a large decimal on asset
assertEq(_testing.decimals(), 18);
assertEq(_testing.asset(), address(_assetWithLargeDecimals));
// case 3: asset without {decimals}
MockERC20WithoutDecimals _assetWithoutDecimals = new MockERC20WithoutDecimals();
_testing = new MockERC4626("test name", "test symbol", IERC20(address(_assetWithoutDecimals)));
// default decimals 18 of shares without decimals() in asset
assertEq(_testing.decimals(), 18);
assertEq(_testing.asset(), address(_assetWithoutDecimals));
}
}
2.2 maxDeposit(address) && previewDeposit(uint256 assets) && deposit(uint256 assets, address receiver)
maxDeposit(address)
:返回可以抵押进本合约的底层资产的最大数量。在{deposit}中会使用该函数进行检查。注:唯一的address参数表示本次抵押会增发shares给的目标地址;previewDeposit(uint256 assets)
:计算此时此刻调用deposit方法去抵押数量为assets的底层资产可以铸造出shares的数量。链上和链下的用户可以使用该方法来预估在当前区块调用{deposit}会增发shares的数量。注:- 在deposit函数里,抵押底层资产兑换shares的过程也是使用该方法来计算shares值的;
- 如果{convertToShares}和{previewDeposit}的结果存在差异,可以认为是shares价格的滑点;
deposit(uint256 assets, address receiver)
:质押assets数量的底层资产并为receiver地址增发相应比例的shares。
function maxDeposit(address) public view virtual override returns (uint256) {
// 如果本合约名下底层资产数量大于0 或 目前还没有shares在流通,返回2^256-1,否则返回0
return _isVaultCollateralized() ? type(uint256).max : 0;
}
function previewDeposit(uint256 assets) public view virtual override returns (uint256) {
// 通过底层资产数量计算增发shares的过程要向下取整,即将铸造出的shares数量会略小于真实值
// 注:保证系统安全,即在shares与底层资产的预期比例下,shares的真实流通量会略小于理论值
// 这样就不会造成还有shares但没有底层资产的情况发生
return _convertToShares(assets, Math.Rounding.Down);
}
function deposit(uint256 assets, address receiver) public virtual override returns (uint256) {
// 检查本次抵押的底层资产数量assets不可大于可抵押给receiver的底层资产的最大值
require(assets <= maxDeposit(receiver), "ERC4626: deposit more than max");
// 计算当前抵押assets数量的底层资产可以获得的shares数量
uint256 shares = previewDeposit(assets);
// 从_msgSender()名下向本合约转入数量为assets的底层资产并为receiver增发数量为shares的shares
_deposit(_msgSender(), receiver, assets, shares);
// 返回增发的shares数量
return shares;
}
// caller向本合约转入数量为assets的底层资产,本合约为receiver增发数量为shares的shares
function _deposit(
address caller,
address receiver,
uint256 assets,
uint256 shares
) internal virtual {
// 使用SafeERC20库的safeTransferFrom的方法,从caller名下转移数量为assets的底层资产到本合约
SafeERC20.safeTransferFrom(_asset, caller, address(this), assets);
// 为receiver增发数量为shares的shares
_mint(receiver, shares);
// 抛出事件
emit Deposit(caller, receiver, assets, shares);
}
// 检查函数,用于校验本合约目前底层资产是否符合预期
// 往细的说就是验证本合约是否还有底层资产来支持业务上的shares流通
function _isVaultCollateralized() private view returns (bool) {
// 本合约名下底层资产数量大于0 或 目前还没有shares在流通 都认为是符合预期,返回true。否则返回false
return totalAssets() > 0 || totalSupply() == 0;
}
// 从底层资产到shares的转换函数
// - assets: 底层资产的输入数量
// - rounding: 取整方式
function _convertToShares(uint256 assets, Math.Rounding rounding) internal view virtual returns (uint256 shares) {
// 获取当前shares总量
uint256 supply = totalSupply();
// 如果输入assets为0 或 当前shares总量为0,那么直接返回_initialConvertToShares(assets, rounding)的结果;
// 否则返回 assets/本合约名下全部底层资产数量*当前shares总量
return
(assets == 0 || supply == 0)
? _initialConvertToShares(assets, rounding)
: assets.mulDiv(supply, totalAssets(), rounding);
}
// 当本合约中无底层资产时,从底层资产到shares的转换函数
// 注:如果要重写本函数,需要保证函数{_initialConvertToAssets}与本函数的转换过程可逆
// - assets: 底层资产的输入数量
// - rounding: 取整方式
function _initialConvertToShares(
uint256 assets,
Math.Rounding
) internal view virtual returns (uint256 shares) {
// 直接返回assets数量,即默认1:1
return assets;
}
foundry代码验证:
contract ERC4626Test is Test {
MockERC20WithDecimals private _asset = new MockERC20WithDecimals("test name", "test symbol", 6);
MockERC4626 private _testing = new MockERC4626("test name", "test symbol", _asset);
address private receiver = address(1);
function setUp() external {
_asset.mint(address(this), 100);
}
function test_MaxDeposit() external {
// case 1: asset && shares total supply == 0
assertEq(_testing.totalAssets(), 0);
assertEq(_testing.totalSupply(), 0);
assertEq(_testing.maxDeposit(receiver), type(uint256).max);
// case 2: asset > 0 && total supply > 0
_asset.approve(address(_testing), 10);
_testing.deposit(10, receiver);
assertEq(_testing.totalAssets(), 10);
assertEq(_testing.totalSupply(), 10);
assertEq(_testing.maxDeposit(receiver), type(uint256).max);
// case 3: asset == 0 && total supply > 0
_testing.transferAsset(receiver, 10);
assertEq(_testing.totalAssets(), 0);
assertEq(_testing.totalSupply(), 10);
assertEq(_testing.maxDeposit(receiver), 0);
// case 4: asset > 0 && total supply == 0
_testing.burn(receiver, 10);
_asset.transfer(address(_testing), 10);
assertEq(_testing.totalAssets(), 10);
assertEq(_testing.totalSupply(), 0);
assertEq(_testing.maxDeposit(receiver), type(uint256).max);
}
function test_DepositAndAndPreviewDeposit() external {
// case 1: asset && shares total supply == 0
assertEq(_testing.totalAssets(), 0);
assertEq(_testing.totalSupply(), 0);
// deposit 0
uint assetToDeposit = 0;
uint sharesToMint = assetToDeposit;
assertEq(_testing.previewDeposit(assetToDeposit), sharesToMint);
assertEq(_testing.deposit(assetToDeposit, receiver), sharesToMint);
assertEq(_testing.totalAssets(), assetToDeposit);
assertEq(_testing.totalSupply(), sharesToMint);
assertEq(_testing.balanceOf(receiver), sharesToMint);
// deposit some
assetToDeposit = 20;
sharesToMint = assetToDeposit;
assertEq(_testing.previewDeposit(assetToDeposit), sharesToMint);
_asset.approve(address(_testing), assetToDeposit);
assertEq(_testing.deposit(assetToDeposit, receiver), sharesToMint);
assertEq(_testing.totalAssets(), assetToDeposit);
assertEq(_testing.totalSupply(), sharesToMint);
assertEq(_testing.balanceOf(receiver), sharesToMint);
// case 2: asset > 0 && total supply > 0
// deposit 0
assetToDeposit = 0;
sharesToMint = assetToDeposit;
assertEq(_testing.previewDeposit(assetToDeposit), sharesToMint);
assertEq(_testing.deposit(assetToDeposit, receiver), sharesToMint);
assertEq(_testing.totalAssets(), 20 + assetToDeposit);
assertEq(_testing.totalSupply(), 20 + sharesToMint);
assertEq(_testing.balanceOf(receiver), 20 + sharesToMint);
// deposit some
assetToDeposit = 22;
sharesToMint = assetToDeposit * _testing.totalSupply() / _testing.totalAssets();
assertEq(_testing.previewDeposit(assetToDeposit), sharesToMint);
_asset.approve(address(_testing), assetToDeposit);
assertEq(_testing.deposit(assetToDeposit, receiver), sharesToMint);
assertEq(_testing.totalAssets(), 20 + assetToDeposit);
assertEq(_testing.totalSupply(), 20 + sharesToMint);
assertEq(_testing.balanceOf(receiver), 20 + sharesToMint);
// case 3: asset == 0 && total supply > 0
_testing.transferAsset(receiver, 42);
assertEq(_testing.totalAssets(), 0);
assertEq(_testing.totalSupply(), 42);
// deposit 0
assetToDeposit = 0;
sharesToMint = assetToDeposit;
assertEq(_testing.previewDeposit(assetToDeposit), sharesToMint);
assertEq(_testing.deposit(assetToDeposit, receiver), sharesToMint);
assertEq(_testing.totalAssets(), 0 + assetToDeposit);
assertEq(_testing.totalSupply(), 42 + sharesToMint);
assertEq(_testing.balanceOf(receiver), 42 + sharesToMint);
// deposit some
// revert for division by 0
assetToDeposit = 21;
vm.expectRevert();
_testing.previewDeposit(assetToDeposit);
vm.expectRevert("ERC4626: deposit more than max");
_testing.deposit(assetToDeposit, receiver);
// case 4: asset > 0 && total supply == 0
_asset.transfer(address(_testing), 20);
_testing.burn(receiver, 42);
assertEq(_testing.totalAssets(), 20);
assertEq(_testing.totalSupply(), 0);
// deposit 0
assetToDeposit = 0;
sharesToMint = assetToDeposit;
assertEq(_testing.previewDeposit(assetToDeposit), sharesToMint);
assertEq(_testing.deposit(assetToDeposit, receiver), sharesToMint);
assertEq(_testing.totalAssets(), 20 + assetToDeposit);
assertEq(_testing.totalSupply(), 0 + sharesToMint);
assertEq(_testing.balanceOf(receiver), 0 + sharesToMint);
// deposit some
assetToDeposit = 15;
sharesToMint = assetToDeposit;
assertEq(_testing.previewDeposit(assetToDeposit), sharesToMint);
_asset.approve(address(_testing), assetToDeposit);
assertEq(_testing.deposit(assetToDeposit, receiver), sharesToMint);
assertEq(_testing.totalAssets(), 20 + assetToDeposit);
assertEq(_testing.totalSupply(), 0 + sharesToMint);
assertEq(_testing.balanceOf(receiver), 0 + sharesToMint);
}
}
2.3 maxMint(address) && previewMint(uint256 shares) && mint(uint256 shares, address receiver)
maxMint(address)
:返回可以给receiver增发的shares的最大值。在{mint}中会使用该函数进行检查。注:唯一的address参数表示本次抵押会增发shares给的目标地址;previewMint(uint256 shares)
:计算此时此刻调用mint方法去铸造数量为shares的shares需要底层资产的数量。 链上和链下的用户可以使用该方法来预估在当前区块调用{mint}会质押底层资产的数量。注:- 在mint函数里,增发指定数量shares所需质押的底层资产数量也是使用该方法来计算的;
- 如果{convertToAssets}和{previewMint}的结果存在差异,可以认为是shares价格的滑点;
mint(uint256 shares, address receiver)
:{deposit}的另一种变体,指定要mint出的shares数量(合约会在内部帮你计算需要质押多少底层资产)和receiver。
function maxMint(address) public view virtual override returns (uint256) {
// 返回uint256的最大值
return type(uint256).max;
}
function previewMint(uint256 shares) public view virtual override returns (uint256) {
// 通过shares计算存入合约的底层资产数量的过程要向上取整,即将抵押进合约的底层资产数量会略大于真实值
// 注:保证系统安全,即在shares与底层资产的预期比例下,底层资产的真实在押值会略大于理论值
// 这样就不会造成还有shares但没有底层资产的情况发生
return _convertToAssets(shares, Math.Rounding.Up);
}
function mint(uint256 shares, address receiver) public virtual override returns (uint256) {
// 检查本次增发的shares数量不可大于可抵押给receiver的shares数量的最大值
require(shares <= maxMint(receiver), "ERC4626: mint more than max");
// 计算当前增发shares数量的shares需要底层资产的数量
uint256 assets = previewMint(shares);
// 从_msgSender()名下向本合约转入数量为assets的底层资产并为receiver增发数量为shares的shares
_deposit(_msgSender(), receiver, assets, shares);
// 返回抵押进合约的底层资产数量
return assets;
}
// 从shares到底层资产的转换函数
// - shares: shares的输入数量
// - rounding: 取整方式
function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view virtual returns (uint256 assets) {
// 获取当前shares总量
uint256 supply = totalSupply();
// 如果当前shares总量为0,那么直接返回_initialConvertToAssets(shares,rounding)的结果;
// 否则返回 shares/shares总量*本合约名下全部底层资产数量
return
(supply == 0) ? _initialConvertToAssets(shares, rounding) : shares.mulDiv(totalAssets(), supply, rounding);
}
// 当本合约中无底层资产时,从shares到底层资产的转换函数
// 注:如果要重写本函数时,需要保证函数{_initialConvertToShares}与本函数的转换过程可逆
// - shares: shares的输入数量
// - rounding: 取整方式
function _initialConvertToAssets(
uint256 shares,
Math.Rounding
) internal view virtual returns (uint256 assets) {
// 直接返回shares数量,即默认1:1
return shares;
}
foundry代码验证:
contract ERC4626Test is Test {
MockERC20WithDecimals private _asset = new MockERC20WithDecimals("test name", "test symbol", 6);
MockERC4626 private _testing = new MockERC4626("test name", "test symbol", _asset);
address private receiver = address(1);
function setUp() external {
_asset.mint(address(this), 100);
}
function test_MaxMintAndMintAndPreviewMint() external {
// case 1: total supply == 0
assertEq(_testing.totalSupply(), 0);
assertEq(_testing.maxMint(receiver), type(uint).max);
// 1 asset 1 share
uint sharesToMint = 15;
uint assetToDeposit = sharesToMint;
assertEq(_testing.previewMint(sharesToMint), assetToDeposit);
_asset.approve(address(_testing), assetToDeposit);
assertEq(_testing.mint(sharesToMint, receiver), assetToDeposit);
assertEq(_testing.totalAssets(), 0 + 15);
assertEq(_testing.totalSupply(), 0 + sharesToMint);
assertEq(_testing.balanceOf(receiver), sharesToMint);
// case 2: total supply != 0
assertEq(_testing.maxMint(receiver), type(uint).max);
sharesToMint = 10;
assetToDeposit = sharesToMint * _testing.totalAssets() / _testing.totalSupply();
assertEq(_testing.previewMint(sharesToMint), assetToDeposit);
_asset.approve(address(_testing), 10);
assertEq(_testing.mint(sharesToMint, receiver), assetToDeposit);
assertEq(_testing.totalAssets(), 15 + assetToDeposit);
assertEq(_testing.totalSupply(), 15 + sharesToMint);
assertEq(_testing.balanceOf(receiver), 15 + sharesToMint);
}
}
2.4 maxWithdraw(address owner) && previewWithdraw(uint256 assets) && withdraw( uint256 assets, address receiver, address owner)
maxWithdraw(address owner)
:返回可以通过销毁owner名下的一定shares来取走底层资产的最大值。在{withdraw}中会使用该函数进行检查;previewWithdraw(uint256 assets)
:计算此时此刻调用withdraw方法赎回assets数量的底层资产所需要销毁的shares数量。链上和链下的用户可以使用该方法来预估在当前区块调用{withdraw}会销毁掉shares的数量。注:- 在withdraw函数里,赎回指定数量底层资产所需销毁的shares数量也是使用该方法来计算的;
- 如果{_convertToShares}和{previewWithdraw}的结果存在差异,可以认为是shares价格的滑点;
withdraw(uint256 assets, address receiver, address owner)
:销毁owner名下的一定数量shares并将assets数量的底层资产转给receiver。
function maxWithdraw(address owner) public view virtual override returns (uint256) {
// 计算owner当前名下的全部shares可以兑换出底层资产的数量
return _convertToAssets(balanceOf(owner), Math.Rounding.Down);
}
function previewWithdraw(uint256 assets) public view virtual override returns (uint256) {
// 通过要赎回的底层资产数量计算要销毁shares的过程要向上取整,即将销毁的shares数量会略大于真实值
// 注:保证系统安全,即在shares与底层资产的预期比例下,shares的真实流通量会略小于理论值
// 这样就不会造成还有shares但没有底层资产的情况发生
return _convertToShares(assets, Math.Rounding.Up);
}
function withdraw(
uint256 assets,
address receiver,
address owner
) public virtual override returns (uint256) {
// 检查本次要取出的底层资产数量不可大于owner名下全部shares可兑换的底层资产的最大值
require(assets <= maxWithdraw(owner), "ERC4626: withdraw more than max");
// 计算当前取走assets数量的底层资产可以所需shares的数量
uint256 shares = previewWithdraw(assets);
// 销毁owner名下数量为shares的shares并从本合约提取assets数量的底层资产给receiver
_withdraw(_msgSender(), receiver, owner, assets, shares);
// 返回销毁的shares数量
return shares;
}
// 经caller调用,销毁owner名下数量为shares的shares,并从本合约转移数量为assets的底层资产给receiver
// 注:如果caller不是owner,那么该过程会消耗owner给caller在shares上的授权额度
function _withdraw(
address caller,
address receiver,
address owner,
uint256 assets,
uint256 shares
) internal virtual {
if (caller != owner) {
// 如果caller并不是owner
// 需要消耗掉owner给caller的数量为shares的授权额度
_spendAllowance(owner, caller, shares);
}
// 销毁owner名下数量为shares的shares
_burn(owner, shares);
// 使用SafeERC20库的safeTransfer的方法,从本合约转移数量为assets的底层资产给receiver
SafeERC20.safeTransfer(_asset, receiver, assets);
// 抛出事件
emit Withdraw(caller, receiver, owner, assets, shares);
}
foundry代码验证:
contract ERC4626Test is Test {
MockERC20WithDecimals private _asset = new MockERC20WithDecimals("test name", "test symbol", 6);
MockERC4626 private _testing = new MockERC4626("test name", "test symbol", _asset);
address private receiver = address(1);
function setUp() external {
_asset.mint(address(this), 100);
}
function test_MaxWithdraw() external {
// case 1: total supply == 0
assertEq(_testing.totalSupply(), 0);
assertEq(_testing.maxWithdraw(receiver), 0);
// case 2: total supply != 0
_asset.approve(address(_testing), 10);
_testing.deposit(10, receiver);
assertEq(_testing.totalSupply(), 10);
assertEq(
_testing.maxWithdraw(receiver),
_testing.balanceOf(receiver) * _testing.totalAssets() / _testing.totalSupply()
);
}
function test_WithdrawAndPreviewWithdraw() external {
// case 1: asset && shares total supply == 0
// withdraw 0 asset
uint assetsToWithdraw = 0;
uint sharesToBurn = assetsToWithdraw;
assertEq(_testing.previewWithdraw(assetsToWithdraw), 0);
assertEq(_testing.withdraw(assetsToWithdraw, receiver, address(this)), sharesToBurn);
assertEq(_testing.totalSupply(), 0);
assertEq(_testing.totalAssets(), 0);
assertEq(_testing.balanceOf(address(this)), 0);
assertEq(_asset.balanceOf(receiver), 0);
// withdraw some asset
assetsToWithdraw = 10;
assertEq(_testing.previewWithdraw(assetsToWithdraw), 10);
vm.expectRevert("ERC4626: withdraw more than max");
_testing.withdraw(assetsToWithdraw, receiver, address(this));
// case 2: asset > 0 && total supply > 0
_asset.approve(address(_testing), 20);
_testing.deposit(20, receiver);
assertEq(_testing.totalSupply(), 20);
assertEq(_testing.totalAssets(), 20);
assertEq(_testing.balanceOf(receiver), 20);
assertEq(_asset.balanceOf(receiver), 0);
assetsToWithdraw = 10;
sharesToBurn = assetsToWithdraw * _testing.totalSupply() / _testing.totalAssets();
assertEq(_testing.previewWithdraw(assetsToWithdraw), sharesToBurn);
vm.prank(receiver);
assertEq(_testing.withdraw(assetsToWithdraw, receiver, receiver), sharesToBurn);
assertEq(_testing.totalSupply(), 20 - assetsToWithdraw);
assertEq(_testing.totalAssets(), 20 - assetsToWithdraw);
assertEq(_testing.balanceOf(receiver), 20 - sharesToBurn);
assertEq(_asset.balanceOf(receiver), 0 + assetsToWithdraw);
// msg.sender is not the owner
assetsToWithdraw = 2;
sharesToBurn = assetsToWithdraw * _testing.totalSupply() / _testing.totalAssets();
assertEq(_testing.previewWithdraw(assetsToWithdraw), sharesToBurn);
vm.prank(receiver);
_testing.approve(address(this), assetsToWithdraw);
assertEq(_testing.withdraw(assetsToWithdraw, receiver, receiver), sharesToBurn);
assertEq(_testing.totalSupply(), 20 - 10 - assetsToWithdraw);
assertEq(_testing.totalAssets(), 20 - 10 - assetsToWithdraw);
assertEq(_testing.balanceOf(receiver), 20 - 10 - sharesToBurn);
assertEq(_asset.balanceOf(receiver), 0 + 10 + assetsToWithdraw);
// revert if withdraw more asset
assetsToWithdraw = _testing.maxWithdraw(receiver) + 1;
vm.expectRevert("ERC4626: withdraw more than max");
vm.prank(receiver);
_testing.withdraw(assetsToWithdraw, receiver, receiver);
// case 3: asset == 0 && total supply > 0
_testing.transferAsset(address(this), _testing.totalAssets());
assertEq(_testing.totalAssets(), 0);
assertEq(_testing.totalSupply(), 8);
assertEq(_testing.balanceOf(receiver), 8);
assertEq(_asset.balanceOf(receiver), 12);
// revert if without any
assetsToWithdraw = 1;
vm.expectRevert();
_testing.previewWithdraw(assetsToWithdraw);
vm.expectRevert();
_testing.withdraw(assetsToWithdraw, receiver, receiver);
// case 4: asset > 0 && total supply == 0
_asset.mint(address(_testing), 20);
_testing.burn(receiver, 8);
assertEq(_testing.totalAssets(), 20);
assertEq(_testing.totalSupply(), 0);
assertEq(_testing.balanceOf(receiver), 0);
assertEq(_asset.balanceOf(receiver), 12);
assetsToWithdraw = 3;
sharesToBurn = assetsToWithdraw;
assertEq(_testing.previewWithdraw(assetsToWithdraw), sharesToBurn);
// revert if withdraw any
vm.expectRevert("ERC4626: withdraw more than max");
_testing.withdraw(assetsToWithdraw, receiver, receiver);
}
}
2.5 maxRedeem(address owner) && previewRedeem(uint256 shares) && redeem(uint256 shares, address receiver, address owner)
maxRedeem(address owner)
:获取owner地址名下可销毁shares的最大值。在{redeem}中会使用该函数进行检查;previewRedeem(uint256 shares)
:计算此时此刻调用redeem方法销毁shares数量的shares可换出的底层资产数量;redeem(uint256 shares, address receiver, address owner)
:销毁owner名下的一定数量shares并将对应比例的底层资产转给receiver。
function maxRedeem(address owner) public view virtual override returns (uint256) {
// 返回owner地址的shares余额
return balanceOf(owner);
}
function previewRedeem(uint256 shares) public view virtual override returns (uint256) {
// 通过要销毁的shares数量计算要赎回的底层资产数量的过程要向下取整,即将赎回的底层资产数量会略小于真实值
// 注:保证系统安全,即在shares与底层资产的预期比例下,底层资产的真实在押值会略大于理论值
// 这样就不会造成还有shares但没有底层资产的情况发生
return _convertToAssets(shares, Math.Rounding.Down);
}
function redeem(
uint256 shares,
address receiver,
address owner
) public virtual override returns (uint256) {
// 检查本次要销毁的shares数量不可大于owner名下可销毁shares的最大值
require(shares <= maxRedeem(owner), "ERC4626: redeem more than max");
// 计算当前销毁shares数量的shares可以赎回底层资产的数量
uint256 assets = previewRedeem(shares);
// 销毁owner名下数量为shares的shares并从本合约提取assets数量的底层资产给receiver
_withdraw(_msgSender(), receiver, owner, assets, shares);
// 返回最终赎回底层资产的数量
return assets;
}
foundry代码验证:
contract ERC4626Test is Test {
MockERC20WithDecimals private _asset = new MockERC20WithDecimals("test name", "test symbol", 6);
MockERC4626 private _testing = new MockERC4626("test name", "test symbol", _asset);
address private receiver = address(1);
function setUp() external {
_asset.mint(address(this), 100);
}
function test_MaxRedeemAndRedeemAndPreviewRedeem() external {
// case 1: total supply == 0
assertEq(_testing.totalSupply(), 0);
assertEq(_testing.maxRedeem(receiver), _testing.balanceOf(receiver));
// 1 asset 1 share
uint sharesToBurn = 1;
uint assetToRedeem = sharesToBurn;
assertEq(_testing.previewRedeem(sharesToBurn), assetToRedeem);
// revert if redeem any
vm.expectRevert("ERC4626: redeem more than max");
vm.prank(receiver);
_testing.redeem(sharesToBurn, receiver, receiver);
// case 2: total supply != 0
_asset.approve(address(_testing), 50);
_testing.deposit(50, receiver);
assertEq(_testing.totalAssets(), 50);
assertEq(_testing.totalSupply(), 50);
assertEq(_testing.balanceOf(receiver), 50);
assertEq(_asset.balanceOf(receiver), 0);
assertEq(_testing.maxRedeem(receiver), _testing.balanceOf(receiver));
sharesToBurn = 20;
assetToRedeem = sharesToBurn * _testing.totalAssets() / _testing.totalSupply();
assertEq(_testing.previewRedeem(sharesToBurn), assetToRedeem);
vm.prank(receiver);
assertEq(_testing.redeem(sharesToBurn, receiver, receiver), assetToRedeem);
assertEq(_testing.totalAssets(), 50 - assetToRedeem);
assertEq(_testing.totalSupply(), 50 - sharesToBurn);
assertEq(_testing.balanceOf(receiver), 50 - sharesToBurn);
assertEq(_asset.balanceOf(receiver), assetToRedeem);
// revert if redeem more
sharesToBurn = _testing.maxRedeem(receiver) + 1;
vm.expectRevert("ERC4626: redeem more than max");
_testing.redeem(sharesToBurn, receiver, receiver);
}
}
2.6 asset() && totalAssets() && convertToShares(uint256 assets) && convertToAssets(uint256 shares)
asset()
:获取底层ERC20资产地址;totalAssets()
:获取本合约中锁存的底层ERC20资产数量;convertToShares(uint256 assets)
:计算此时此刻,数量为assets的底层资产可以转换成shares的数量。注:- 该函数只是用于展示数据估计,不要使用该函数来计算具体底层资产兑换shares的数量;
- IERC4626中规定:该方法的计算不能表示某个用户的share的单价,而是反映全部用户的share单价;
convertToAssets(uint256 shares)
:计算此时此刻,数量为shares的shares可以转换成底层资产的数量。注:- 该函数只是用于展示数据估计,不要使用该函数来计算具体shares数量可赎回底层资产数;
- IERC4626中规定:该方法的计算不能表示某个用户的share的单价,而是反映全部用户的share单价。
function asset() public view virtual override returns (address) {
// 返回底层资产的合约地址
return address(_asset);
}
function totalAssets() public view virtual override returns (uint256) {
// 返回本合约名下的底层资产数量
return _asset.balanceOf(address(this));
}
function convertToShares(uint256 assets) public view virtual override returns (uint256 shares) {
// 返回调用{_convertToShares}的返回值
// 注:这里的取整方式为向下取整
return _convertToShares(assets, Math.Rounding.Down);
}
function convertToAssets(uint256 shares) public view virtual override returns (uint256 assets) {
// 返回调用{_convertToAssets}的返回值
// 注:这里的取整方式为向下取整
return _convertToAssets(shares, Math.Rounding.Down);
}
foundry代码验证:
contract ERC4626Test is Test {
MockERC20WithDecimals private _asset = new MockERC20WithDecimals("test name", "test symbol", 6);
MockERC4626 private _testing = new MockERC4626("test name", "test symbol", _asset);
address private receiver = address(1);
function setUp() external {
_asset.mint(address(this), 100);
}
function test_AssetAndTotalAssetsAndConvertToSharesAndConvertToAssets() external {
// test {asset}
assertEq(_testing.asset(), address(_asset));
// total supply == 0
// test {convertToShares}
assertEq(_testing.totalSupply(), 0);
for (uint assets = 0; assets < 100; ++assets) {
assertEq(_testing.convertToShares(assets), assets);
}
// test {convertToAssets}
for (uint shares = 0; shares < 100; ++shares) {
assertEq(_testing.convertToAssets(shares), shares);
}
// total supply != 0
_asset.approve(address(_testing), 50);
_testing.deposit(50, receiver);
assertEq(_testing.totalSupply(), 50);
// test {totalAssets}
assertEq(_testing.totalAssets(), 50);
// test {convertToShares}
for (uint assets = 1; assets < 100; ++assets) {
assertEq(_testing.convertToShares(assets), assets * _testing.totalSupply() / _testing.totalAssets());
}
// test {convertToAssets}
for (uint shares = 1; shares < 100; ++shares) {
assertEq(_testing.convertToAssets(shares), shares * _testing.totalAssets() / _testing.totalSupply());
}
}
}
ps:
本人热爱图灵,热爱中本聪,热爱V神。
以下是我个人的公众号,如果有技术问题可以关注我的公众号来跟我交流。
同时我也会在这个公众号上每周更新我的原创文章,喜欢的小伙伴或者老伙计可以支持一下!
如果需要转发,麻烦注明作者。十分感谢!
公众号名称:后现代泼痞浪漫主义奠基人