Michael.W基于Foundry精读Openzeppelin第52期——ERC4626.sol

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神。
以下是我个人的公众号,如果有技术问题可以关注我的公众号来跟我交流。
同时我也会在这个公众号上每周更新我的原创文章,喜欢的小伙伴或者老伙计可以支持一下!
如果需要转发,麻烦注明作者。十分感谢!

在这里插入图片描述

公众号名称:后现代泼痞浪漫主义奠基人

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/771984.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

君正X2100 RTOS JPEG硬件编码

一、配置 进入SDK的tools/iconfigtool/IConfigToolApp目录&#xff0c;执行./IConfigTool指令&#xff0c;进入配置界面&#xff1a; Config.in 是生成配置界面的文件&#xff0c;Config是需要修改的配置文件&#xff0c;选择之后点击Open。 选择 xburst2系列CPU->X2000系列…

Harmony OS 网络编程 实验指南

netcat简介 netcat 是什么&#xff1f; netcat是一个非常强大的网络实用工具&#xff0c;可以用它来调试TCP/UDP应用程序&#xff1b; netcat 如何安装&#xff1f; Linux上可以使用发行版的包管理器安装&#xff0c;例如Debian/Ubuntu上&#xff1a; sudo apt-get instal…

别想宰我,怎么查看云厂商是否超卖?详解 cpu steal time

据说有些云厂商会超卖&#xff0c;宿主有 96 个核心&#xff0c;结果卖出去 100 多个 vCPU&#xff0c;如果这些虚机负载都不高&#xff0c;大家相安无事&#xff0c;如果这些虚机同时运行一些高负载的任务&#xff0c;相互之间就会抢占 CPU&#xff0c;对应用程序有较大影响&a…

缺省和重载。引用——初识c++

. 个人主页&#xff1a;晓风飞 专栏&#xff1a;数据结构|Linux|C语言 路漫漫其修远兮&#xff0c;吾将上下而求索 文章目录 C输入&输出cout 和cin<<>> 缺省参数全缺省半缺省应用场景声明和定义分离的情况 函数重载1.参数的类型不同2.参数的个数不同3.参数的顺…

生成可读取配置文件的独立运行jar程序

前言: 周五刚躺下,前线打来语音要个下载文件的小程序,下载路径和下载码需要根据配置获取,程序需要在服务器执行。当然配置的设计是个人设计的,不然每次更新下载码都要重新出具jar包,太麻烦。多年没写独立运行的jar包了,翻阅了相关资料,最终还是功夫不负有心人。想着这种…

Redis锁,乐观锁与悲观锁

锁 悲观锁 认为什么时候都会出问题&#xff0c;无论做什么都会加锁 乐观锁 很乐观&#xff0c;认为什么时候都不会出问题&#xff0c;所以不会上锁。 更新数据时去判断一下&#xff0c;在此期间&#xff0c;是否有人修改过这个数据 应用于&#xff1a;秒杀场景 **watch*…

小折叠手机如何经久耐用?收下这份日常养护指南

不同于普通手机的玻璃屏幕&#xff0c;折叠机出于折叠的特性&#xff0c;使用了柔性屏幕。因此撕除原厂保护膜时&#xff0c;由于贴膜较强的粘合力&#xff0c;很容易就会导致屏幕产生不可修复的损伤。 这也是为什么各大手机厂商都不允许折叠机私自贴膜的原因&#xff0c;并且…

从产品组装和维护/维修的角度来看,基于增强现实的指导:关于挑战和机遇的最新综述

作者&#xff1a; 1. M. Eswaran 2. Anil Kumar Gulivindala 3. M.V.A.Raju Bahubalendruni 关于本文 •分析了增强现实在装配和维护/维修中的作用。 •讨论了AR辅助制造系统的软件和硬件元素。 •讨论了AR跟踪和配准技术面临的挑战。 •讨论了AR辅助制造系统的未来…

视频素材app有哪些?视频素材网址推荐

在这个视觉传达愈发重要的时代&#xff0c;拥有一款好的无水印短视频素材网站就如同握有一把打开创意之门的钥匙&#xff0c;选择合适的短视频素材平台至关重要&#xff0c;这会让你的视频制作更加轻松而高效。 1&#xff0c;蛙学府 以其广泛的优质视频素材库而闻名&#xff0…

Maven发布开源框架到远程仓库

1.背景 当你写了一个自我感觉良好的开源工具希望给他人分享&#xff0c;如果只是在github等网站进行公布之外&#xff0c;用户使用起来还不是很方便&#xff0c;特别是当你提供是特定领域的基础工具。你还可以把它部署到中央仓库&#xff0c;这样别人使用就会方便很多。接下来…

后端常见面经之MySQL

MySQL字段类型 数值类型 整型经常被用到&#xff0c;比如 tinyint、int、bigint 。默认是有符号的&#xff0c;若只需存储无符号值&#xff0c;可增加 unsigned 属性。 int(M)中的 M 代表最大显示宽度&#xff0c;并不是说 int(1) 就不能存储数值10了&#xff0c;不管设定了显…

【@changesets/cli】变更集实战教程

一、背景概述 前端目前基于Monorepo架构的npm包开发很普遍&#xff0c;在开发完毕后&#xff0c;我们需要对包进行版本号升级&#xff0c;并且部署&#xff0c;这些操作如果是手动来操作的话&#xff0c;很麻烦&#xff0c;而且容易出错。 例如有这样的场景&#xff1a; -ap…

1.Git快速入门

文章目录 Git快速入门1.Git概述2.SCM概述3.Git安装3.1 软件下载3.2 软件安装3.3 软件测试 Git快速入门 1.Git概述 Git是一个免费的&#xff0c;开源的分布式版本控制系统&#xff0c;可以快速高效地处理从小型到大型的各种项目&#xff0c;Git易于学习&#xff0c;占用空间小&…

供应链 | 顶刊OR论文精读:在线最小峰值作业调度的竞争算法

Competitive Algorithms for the Online Minimum Peak Job Scheduling 本文为OR期刊论文&#xff0c;原文信息&#xff1a; Clia Escribe, Michael Hu, Retsef Levi (2023) Competitive Algorithms for the Online Minimum Peak Job Scheduling. Operations Research. Article…

ZYNQ学习之PetaLinux开发环境搭建

基本都是摘抄正点原子的文章&#xff1a;<领航者 ZYNQ 之嵌入式Linux 开发指南 V3.2.pdf&#xff0c;因初次学习&#xff0c;仅作学习摘录之用&#xff0c;有不懂之处后续会继续更新~ FTP&#xff1a;File Transfer Protocol 一、Ubuntu 和 Windows 文件互传 1.1、开启 Ubu…

【git分支管理策略】如何高效的管理好代码版本

目录 1.分支管理策略 2.我用的分支管理策略 3.一些常见问题 1.分支管理策略 分支管理策略就是一些经过实践后总结出来的可靠的分支管理的办法&#xff0c;让分支之间能科学合理、高效的进行协作&#xff0c;帮助我们在整个开发流程中合理的管理好代码版本。 目前有两套Git…

【线段树】第十三届蓝桥杯省赛C++ A组 Java C组 Python A组/B组《最长不下降子序列》(C++)

【题目描述】 给定一个长度为 N 的整数序列&#xff1a;,,⋅⋅⋅,。 现在你有一次机会&#xff0c;将其中连续的 K 个数修改成任意一个相同值。 请你计算如何修改可以使修改后的数列的最长不下降子序列最长&#xff0c;请输出这个最长的长度。 最长不下降子序列是指序列中的…

python每日分析练习:产品季度销售的比较分析

这次我们将关注一家零售公司的季度销售分析。 假设场景 一家零售公司希望分析其过去一年内各季度的销售表现&#xff0c;以便更好地理解其业务趋势&#xff0c;评估不同产品类别的表现&#xff0c;并优化未来的销售策略。 分析目的 理解季度销售趋势&#xff1a;分析公司整体…

Mac 装 虚拟机 vmware、centos7等

vmware&#xff1a; https://www.vmware.com/products/fusion.html centos7 清华镜像&#xff1a; 暂时没有官方的 m1 arm架构镜像 centos7 链接: https://pan.baidu.com/s/1oZw1cLyl6Uo3lAD2_FqfEw?pwdzjt4 提取码: zjt4 复制这段内容后打开百度网盘手机App&#xff0c;操…

C语言例4-7:格式字符f的使用例子

%f&#xff0c;实型&#xff0c;小数部分为6位 代码如下&#xff1a; //格式字符f的使用例子 #include<stdio.h> int main(void) {float f 123.456;double d1, d2;d11111111111111.111111111;d22222222222222.222222222;printf("%f,%12f,%12.2f,%-12.2f,%.2f\n&qu…