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

  • Michael.W
  • 更新于 2024-03-26 18:32
  • 阅读 1185

ERC4626库本身是一种有底层ERC20资产质押的shares且本身同样满足ERC20标准。用户可以通过deposit或mint方法来质押底层资产并增发shares,也可使用burn或redeem方法来销毁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的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神。 以下是我个人的公众号,如果有技术问题可以关注我的公众号来跟我交流。 同时我也会在这个公众号上每周更新我的原创文章,喜欢的小伙伴或者老伙计可以支持一下! 如果需要转发,麻烦注明作者。十分感谢!

1.jpeg

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

点赞 0
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Michael.W
Michael.W
0x93E7...0000
狂热的区块链爱好者