Michael.W基于Foundry精读Openzeppelin第49期——ERC20Wrapper.sol

  • Michael.W
  • 更新于 2024-03-06 16:27
  • 阅读 1141

ERC20Wrapper库是一种对某ERC20 token作为标的资产进行包装的wrapped ERC20 token。用户可以质押或赎回标的token,同时获得或销毁相同数量的wrapped token。

0. 版本

[openzeppelin]:v4.8.3,[forge-std]:v1.5.6

0.1 ERC20Wrapper.sol

Github: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.8.3/contracts/token/ERC20/extensions/ERC20Wrapper.sol

ERC20Wrapper库是一种对某ERC20 token作为标的资产进行包装的wrapped ERC20 token。用户可以质押或赎回标的token,同时获得或销毁相同数量的wrapped token。该库可与其他ERC20拓展库结合使用,如:与ERC20Votes库结合,可将已有的ERC20 token包装成治理token。

1. 目标合约

继承ERC20Wrapper合约:

Github: https://github.com/RevelationOfTuring/foundry-openzeppelin-contracts/blob/master/src/token/ERC20/extensions/MockERC20Wrapper.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Wrapper.sol";

contract MockERC20Wrapper is ERC20Wrapper {
    constructor(
        string memory name,
        string memory symbol,
        IERC20 underlyingToken
    )
    ERC20Wrapper(underlyingToken)
    ERC20(name, symbol)
    {}

    function recover(address account) external {
        _recover(account);
    }
}

全部foundry测试合约:

Github: https://github.com/RevelationOfTuring/foundry-openzeppelin-contracts/blob/master/test/token/ERC20/extensions/ERC20Wrapper/ERC20Wrapper.t.sol

测试使用的物料合约:

Github: https://github.com/RevelationOfTuring/foundry-openzeppelin-contracts/blob/master/test/token/ERC20/extensions/ERC20Wrapper/MockERC20.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";

contract MockERC20 is ERC20 {
    constructor(string memory name, string memory symbol)
    ERC20(name, symbol) {}

    function mint(address account, uint amount) external {
        _mint(account, amount);
    }
}

contract MockERC20WithDecimals {
    uint8 private _decimals;

    constructor(uint8 dec){
        _decimals = dec;
    }

    function decimals() external view returns (uint8){
        return _decimals;
    }
}

contract MockERC20WithoutDecimals {}

2. 代码精读

2.1 constructor()

    // 底层的标的ERC20合约地址
    IERC20 public immutable underlying;

    // 设置底层的标的ERC20合约地址
    constructor(IERC20 underlyingToken) {
        underlying = underlyingToken;
    }

2.2 decimals()

重写ERC20.decimals(),定义wrapper token的decimals。

    function decimals() public view virtual override returns (uint8) {
        // 如果标的token的decimals()方法可以成功调用,wrapped token的decimals与之一致
        try IERC20Metadata(address(underlying)).decimals() returns (uint8 value) {
            return value;
        } catch {
            // 如果标的token的decimals()方法无法成功调用,wrapped token的decimals为ERC20.decimals(),即18
            return super.decimals();
        }
    }

foundry代码验证:

contract ERC20WrapperTest is Test {
    MockERC20Wrapper private _testing = new MockERC20Wrapper("test name", "test symbol", _underlyingToken);

    function test_Decimals() external {
        // case 1: underlying token with 1 decimals
        MockERC20WithDecimals mockERC20WithDecimals = new MockERC20WithDecimals(1);
        _testing = new MockERC20Wrapper("test name", "test symbol", IERC20(address(mockERC20WithDecimals)));
        assertEq(_testing.decimals(), mockERC20WithDecimals.decimals());

        // case 2: underlying token without decimals
        MockERC20WithoutDecimals mockERC20WithoutDecimals = new MockERC20WithoutDecimals();
        _testing = new MockERC20Wrapper("test name", "test symbol", IERC20(address(mockERC20WithoutDecimals)));
        assertEq(_testing.decimals(), 18);
    }
}

2.3 depositFor(address account, uint256 amount)

用户质押标的token到本合约内并为account地址铸造对应数量的wrapped token。

    function depositFor(address account, uint256 amount) public virtual returns (bool) {
        // 使用SafeERC20库的safeTransferFrom方法,转移用户名下数量为amount的标的token到本合约下
        SafeERC20.safeTransferFrom(underlying, _msgSender(), address(this), amount);
        // 为account地址铸造数量为amount的wrapped token
        _mint(account, amount);
        // 返回true
        return true;
    }

foundry代码验证:

contract ERC20WrapperTest is Test {
    MockERC20 private _underlyingToken = new MockERC20("test name", "test symbol");
    MockERC20Wrapper private _testing = new MockERC20Wrapper("test name", "test symbol", _underlyingToken);
    address private account = address(1);

    function setUp() external {
        _underlyingToken.mint(address(this), 100);
    }

    function test_DepositFor() external {
        assertEq(_testing.balanceOf(account), 0);
        assertEq(_testing.totalSupply(), 0);
        assertEq(_underlyingToken.balanceOf(address(this)), 100);
        assertEq(_underlyingToken.balanceOf(address(_testing)), 0);

        _underlyingToken.approve(address(_testing), 100);
        uint amountToDeposit = 10;
        assertTrue(_testing.depositFor(account, amountToDeposit));
        // check balances
        assertEq(_testing.balanceOf(account), 0 + amountToDeposit);
        assertEq(_testing.totalSupply(), 0 + amountToDeposit);
        assertEq(_underlyingToken.balanceOf(address(this)), 100 - amountToDeposit);
        assertEq(_underlyingToken.balanceOf(address(_testing)), 0 + amountToDeposit);
    }
}

2.4 withdrawTo(address account, uint256 amount)

用户销毁数量为amount的wrapped token并从本合约转移相同数量的标的token到account地址。

    function withdrawTo(address account, uint256 amount) public virtual returns (bool) {
        // 销毁调用者数量为amount的wrapped token
        _burn(_msgSender(), amount);
        // 使用SafeERC20库的safeTransfer方法,从本合约转移数量为amount的标的token到account地址
        SafeERC20.safeTransfer(underlying, account, amount);
        // 返回true
        return true;
    }

foundry代码验证:

contract ERC20WrapperTest is Test {
    MockERC20 private _underlyingToken = new MockERC20("test name", "test symbol");
    MockERC20Wrapper private _testing = new MockERC20Wrapper("test name", "test symbol", _underlyingToken);
    address private account = address(1);

    function setUp() external {
        _underlyingToken.mint(address(this), 100);
    }

    function test_WithdrawTo() external {
        _underlyingToken.approve(address(_testing), 100);
        _testing.depositFor(account, 100);

        assertEq(_underlyingToken.balanceOf(address(_testing)), 100);
        assertEq(_underlyingToken.balanceOf(address(account)), 0);
        assertEq(_underlyingToken.balanceOf(address(this)), 0);
        assertEq(_testing.balanceOf(address(account)), 100);
        assertEq(_testing.totalSupply(), 100);

        uint amountToWithdraw = 10;
        vm.prank(account);
        assertTrue(_testing.withdrawTo(address(this), amountToWithdraw));
        assertEq(_underlyingToken.balanceOf(address(_testing)), 100 - amountToWithdraw);
        assertEq(_underlyingToken.balanceOf(address(account)), 0);
        assertEq(_underlyingToken.balanceOf(address(this)), 0 + amountToWithdraw);
        assertEq(_testing.balanceOf(address(account)), 100 - amountToWithdraw);
        assertEq(_testing.totalSupply(), 100 - amountToWithdraw);
    }
}

2.5 _recover(address account) internal

当本合约名下的标的资产数量与wrapped token总量出现不一致时,为account地址铸造对应差值的wrapped token。

注:本方法为internal方法,可以将其封装成一个带权限管理的external方法。

    function _recover(address account) internal virtual returns (uint256) {
        // 计算本合约名下的标的资产数量与wrapped token总量的差值
        uint256 value = underlying.balanceOf(address(this)) - totalSupply();
        // 为account地址铸造数量为上述差值的wrapped token
        _mint(account, value);
        // 返回差值
        return value;
    }

foundry代码验证:

contract ERC20WrapperTest is Test {
    MockERC20 private _underlyingToken = new MockERC20("test name", "test symbol");
    MockERC20Wrapper private _testing = new MockERC20Wrapper("test name", "test symbol", _underlyingToken);
    address private account = address(1);

    function setUp() external {
        _underlyingToken.mint(address(this), 100);
    }

    function test_Recover() external {
        // transfer underlying token into ERC20Wrapper directly
        _underlyingToken.transfer(address(_testing), 20);
        assertEq(_underlyingToken.balanceOf(address(_testing)), 20);
        assertEq(_testing.totalSupply(), 0);
        assertEq(_testing.balanceOf(account), 0);

        uint difference = _underlyingToken.balanceOf(address(_testing)) - _testing.totalSupply();
        _testing.recover(account);
        assertEq(_underlyingToken.balanceOf(address(_testing)), 20);
        assertEq(_testing.totalSupply(), 0 + difference);
        assertEq(_testing.balanceOf(account), 0 + difference);
    }
}

ps: 本人热爱图灵,热爱中本聪,热爱V神。 以下是我个人的公众号,如果有技术问题可以关注我的公众号来跟我交流。 同时我也会在这个公众号上每周更新我的原创文章,喜欢的小伙伴或者老伙计可以支持一下! 如果需要转发,麻烦注明作者。十分感谢!

1.jpeg

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

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

0 条评论

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