Michael.W基于Foundry精读Openzeppelin第52期——ERC4626.sol
- 0. 版本
- 0.1 ERC4626.sol
- 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
Github: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.8.3/contracts/token/ERC20/extensions/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合约:
Github: https://github.com/RevelationOfTuring/foundry-openzeppelin-contracts/blob/master/src/token/ERC20/extensions/MockERC4626.sol
// 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测试合约:
Github: https://github.com/RevelationOfTuring/foundry-openzeppelin-contracts/blob/master/test/token/ERC20/extensions/ERC4626/ERC4626.t.sol
测试使用的物料合约:
Github: https://github.com/RevelationOfTuring/foundry-openzeppelin-contracts/blob/master/test/token/ERC20/extensions/ERC4626/MockERC20.sol
// 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的decimalsuint8 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_的decimalsfunction _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类型的decimalsreturn (true, uint8(returnedDecimals));}}// 如果上述调用不成功 或 调用无返回值 或 返回值转成uint256后大于255,则认为asset_的decimals不符合预期。返回false和0return (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 decimalassertEq(_testing.decimals(), 6);assertEq(_testing.asset(), address(_asset));// case 2: asset with decimal that > type(uint8).maxMockERC20WithLargeDecimals _assetWithLargeDecimals = new MockERC20WithLargeDecimals();_testing = new MockERC4626("test name", "test symbol", IERC20(address(_assetWithLargeDecimals)));// default decimals 18 of shares with a large decimal on assetassertEq(_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 assetassertEq(_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,否则返回0return _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的sharesfunction _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。否则返回falsereturn 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:1return 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 == 0assertEq(_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 == 0assertEq(_testing.totalAssets(), 0);assertEq(_testing.totalSupply(), 0);// deposit 0uint 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 someassetToDeposit = 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 0assetToDeposit = 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 someassetToDeposit = 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 0assetToDeposit = 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 0assetToDeposit = 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 0assetToDeposit = 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 someassetToDeposit = 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:1return 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 == 0assertEq(_testing.totalSupply(), 0);assertEq(_testing.maxMint(receiver), type(uint).max);// 1 asset 1 shareuint 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 != 0assertEq(_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的底层资产给receiverSafeERC20.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 == 0assertEq(_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 assetuint 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 assetassetsToWithdraw = 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 ownerassetsToWithdraw = 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 assetassetsToWithdraw = _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 anyassetsToWithdraw = 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 anyvm.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 == 0assertEq(_testing.totalSupply(), 0);assertEq(_testing.maxRedeem(receiver), _testing.balanceOf(receiver));// 1 asset 1 shareuint sharesToBurn = 1;uint assetToRedeem = sharesToBurn;assertEq(_testing.previewRedeem(sharesToBurn), assetToRedeem);// revert if redeem anyvm.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 moresharesToBurn = _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神。
以下是我个人的公众号,如果有技术问题可以关注我的公众号来跟我交流。
同时我也会在这个公众号上每周更新我的原创文章,喜欢的小伙伴或者老伙计可以支持一下!
如果需要转发,麻烦注明作者。十分感谢!
公众号名称:后现代泼痞浪漫主义奠基人