Michael.W基于Foundry精读Openzeppelin第55期——PaymentSplitter.sol

  • Michael.W
  • 更新于 2024-05-31 12:22
  • 阅读 999

PaymentSplitter库可以在一组领取地址无感知的情况下,将定量eth或某ERC20 token按照shares占比释放给该组中的某地址。当eth或ERC20 token被转入该合约后,在册的领取地址就可以来领取属于自己占比的那部分。各领取人的shares数量只能在该合约部署时被设置。

0. 版本

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

0.1 PaymentSplitter.sol

Github: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.8.3/contracts/finance/PaymentSplitter.sol

PaymentSplitter库可以在一组领取地址无感知的情况下,将定量eth或某ERC20 token按照shares占比释放给该组中的某地址。当eth或ERC20 token被转入该合约后,在册的领取地址就可以来领取属于自己占比的那部分。各领取人的shares数量只能在该合约部署时被设置。领取eth和ERC20 token需要通过触发release函数完成。

1. 目标合约

PaymentSplitter合约可直接部署。

全部foundry测试合约:

Github: https://github.com/RevelationOfTuring/foundry-openzeppelin-contracts/blob/master/test/finance/PaymentSplitter/PaymentSplitter.t.sol

测试使用的物料合约:

Github: https://github.com/RevelationOfTuring/foundry-openzeppelin-contracts/blob/master/test/finance/PaymentSplitter/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);
    }
}

2. 代码精读

2.1 constructor() payable

    event PayeeAdded(address account, uint256 shares);
    event PaymentReleased(address to, uint256 amount);
    event ERC20PaymentReleased(IERC20 indexed token, address to, uint256 amount);
    event PaymentReceived(address from, uint256 amount);

    // 全部领款人的股份总和
    uint256 private _totalShares;
    // 已领取的eth总量
    uint256 private _totalReleased;

    // 记录领款人的领款股份(key为领款人地址)
    mapping(address => uint256) private _shares;
    // 记录领款人已领取eth的数量(key为领款人地址)
    mapping(address => uint256) private _released;
    // 记录领款人地址的不定长数组
    address[] private _payees;

    // 记录已领取的某ERC20总量(key为ERC20合约地址)
    mapping(IERC20 => uint256) private _erc20TotalReleased;
    // 记录某领款人已领取某ERC20的数量(key1为ERC20合约地址)
    mapping(IERC20 => mapping(address => uint256)) private _erc20Released;

    constructor(address[] memory payees, uint256[] memory shares_) payable {
        // 要求每个领款人都有对应的股份
        require(payees.length == shares_.length, "PaymentSplitter: payees and shares length mismatch");
        require(payees.length > 0, "PaymentSplitter: no payees");
        // 循环增添设置领款人与其对应股份
        for (uint256 i = 0; i < payees.length; i++) {
            _addPayee(payees[i], shares_[i]);
        }
    }

    // 设置领款人及其对应的股份
    function _addPayee(address account, uint256 shares_) private {
        // 验证领款人地址不能为0地址
        require(account != address(0), "PaymentSplitter: account is the zero address");
        // 验证领款股份不为0
        require(shares_ > 0, "PaymentSplitter: shares are 0");
        // 验证领款人account尚未被添加过
        require(_shares[account] == 0, "PaymentSplitter: account already has shares");

        // 将领款人地址追加进数组_payees
        _payees.push(account);
        // 设置领款人与其对应股份
        _shares[account] = shares_;
        // 累计股份自增shares_
        _totalShares = _totalShares + shares_;
        // 抛出事件
        emit PayeeAdded(account, shares_);
    }

foundry代码验证:

contract PaymentSplitterTest is Test {
    PaymentSplitter private _testing;
    address[] private _payees = [address(1), address(2), address(3)];
    uint[] private _shares = [20, 30, 50];

    function setUp() external {
        _testing = new PaymentSplitter{value: 10000}(_payees, _shares);
    }

    event PayeeAdded(address account, uint shares);

    function test_Constructor() external {
        // check events
        for (uint i; i < _payees.length; ++i) {
            vm.expectEmit();
            emit PayeeAdded(_payees[i], _shares[i]);
        }
        _testing = new PaymentSplitter(_payees, _shares);

        // revert without the same length of payees and shares
        _payees.push(address(4));
        vm.expectRevert("PaymentSplitter: payees and shares length mismatch");
        new PaymentSplitter(_payees, _shares);

        // revert with 0 length of payees and shares
        vm.expectRevert("PaymentSplitter: no payees");
        new PaymentSplitter(new address[](0), new uint[](0));

        // revert with zero address in payees
        _payees = [address(0)];
        _shares = [10];
        vm.expectRevert("PaymentSplitter: account is the zero address");
        new PaymentSplitter(_payees, _shares);

        // revert with 0 in shares
        _payees = [address(1)];
        _shares = [0];
        vm.expectRevert("PaymentSplitter: shares are 0");
        new PaymentSplitter(_payees, _shares);

        // revert with repetitive addresses in payees
        _payees = [address(1), address(1)];
        _shares = [10, 20];
        vm.expectRevert("PaymentSplitter: account already has shares");
        new PaymentSplitter(_payees, _shares);
    }
}

2.2 shares(address account) && totalShares() && payee(uint256 index) && totalReleased() && released(address account) && receive() payable

  • shares(address account):返回地址account的股份数量;
  • totalShares():返回当前合约内全部领款人的股份总和;
  • payee(uint256 index):返回索引为index的领款人地址;
  • totalReleased():返回已释放的eth总量;
  • released(address account):返回地址account已领取eth数量;
  • receive() payable:当本合约被转入eth时,抛出事件PaymentReceived。
    function shares(address account) public view returns (uint256) {
        return _shares[account];
    }

    function totalShares() public view returns (uint256) {
        return _totalShares;
    }

    function payee(uint256 index) public view returns (address) {
        return _payees[index];
    }

    function totalReleased() public view returns (uint256) {
        return _totalReleased;
    }

    function released(address account) public view returns (uint256) {
        return _released[account];
    }

    receive() external payable virtual {
        emit PaymentReceived(_msgSender(), msg.value);
    }

2.3 release(address payable account) && releasable(address account)

  • release(address payable account):给领款人account发放eth;
  • releasable(address account):返回领款人account可领取eth的数量。
    function release(address payable account) public virtual {
        // 要求领款人account已在册
        require(_shares[account] > 0, "PaymentSplitter: account has no shares");
        // 计算领款人account可领取eth的数量
        uint256 payment = releasable(account);
        // 要求领取数量不为0
        require(payment != 0, "PaymentSplitter: account is not due payment");

        // 已领取的eth总量自增payment
        _totalReleased += payment;
        // 关闭solidity 0.8的整数运算溢出检查
        unchecked {
            // account已领取eth总量自增payment
            // 注:前面已领取eth总量自增payment未溢出可以确保个人已领取eth总量自增payment不会溢出
            _released[account] += payment;
        }

        // 调用Address库的sendValue方法,从本合约转payment数量的eth给account
        Address.sendValue(account, payment);
        // 抛出事件
        emit PaymentReleased(account, payment);
    }

    function releasable(address account) public view returns (uint256) {
        // 计算全部应释放的eth数量,即当前合约名下eth余额 + 到目前为止已释放的eth总量
        uint256 totalReceived = address(this).balance + totalReleased();
        // 计算account尚未领取的eth数量
        // 注:released(account)是account目前已领取的eth数量
        return _pendingPayment(account, totalReceived, released(account));
    }

    // 计算领款人account在其已领取alreadyReleased数量token且该token全局有totalReceived数量可以被领取的情况下还能领取的token数量
    // - account: 领款人地址
    // - totalReceived: 理论上某token一共可以被领取的数量
    // - alreadyReleased: account目前已领取某token的数量
    function _pendingPayment(
        address account,
        uint256 totalReceived,
        uint256 alreadyReleased
    ) private view returns (uint256) {
        // 某token全部可被领取熟练 * account的股票于全部股票总数的占比 - account已领取某token数量
        return (totalReceived * _shares[account]) / _totalShares - alreadyReleased;
    } 

foundry代码验证:

contract PaymentSplitterTest is Test {
    PaymentSplitter private _testing;
    address[] private _payees = [address(1), address(2), address(3)];
    uint[] private _shares = [20, 30, 50];

    function setUp() external {
        _testing = new PaymentSplitter{value: 10000}(_payees, _shares);
    }

    event PaymentReleased(address to, uint amount);
    event PaymentReceived(address from, uint amount);

    function test_releaseEth() external {
        assertEq(address(_testing).balance, 10000);
        // test for totalShares()
        assertEq(_testing.totalShares(), 20 + 30 + 50);
        // test for totalReleased()
        assertEq(_testing.totalReleased(), 0);
        // test for shares(address) && released(address) && payee(uint)
        for (uint i; i < _payees.length; ++i) {
            assertEq(_testing.shares(_payees[i]), _shares[i]);
            assertEq(_testing.released(_payees[i]), 0);
            assertEq(_testing.payee(i), _payees[i]);
        }

        // test for releasable(address)
        assertEq(_testing.releasable(_payees[0]), 10000 * 20 / (20 + 30 + 50));
        assertEq(_testing.releasable(_payees[1]), 10000 * 30 / (20 + 30 + 50));
        assertEq(_testing.releasable(_payees[2]), 10000 * 50 / (20 + 30 + 50));

        // test for release(address payable)
        address account = _payees[0];
        uint amountReleased = 20 / (20 + 30 + 50) * 10000;
        assertEq(account.balance, 0);

        vm.expectEmit(address(_testing));
        emit PaymentReleased(account, amountReleased);
        _testing.release(payable(account));
        assertEq(account.balance, 0 + amountReleased);
        assertEq(address(_testing).balance, 10000 - amountReleased);

        // check getter
        assertEq(_testing.totalReleased(), 0 + amountReleased);
        assertEq(_testing.released(account), 0 + amountReleased);
        assertEq(_testing.releasable(account), 0);

        // transfer 1000 wei into contract
        uint additional = 1000;
        vm.expectEmit(address(_testing));
        emit PaymentReceived(address(this), additional);
        (bool ok,) = address(_testing).call{value: additional}("");
        assertTrue(ok);
        assertEq(address(_testing).balance, 10000 - amountReleased + additional);

        // check releasable(address)
        // full payment for _payees[1] && _payees[2]
        assertEq(_testing.releasable(_payees[1]), (10000 + additional) * 30 / (20 + 30 + 50));
        assertEq(_testing.releasable(_payees[2]), (10000 + additional) * 50 / (20 + 30 + 50));
        // payment only for additional eth
        assertEq(_testing.releasable(_payees[0]), additional * 20 / (20 + 30 + 50));

        // release all
        for (uint i; i < _payees.length; ++i) {
            _testing.release(payable(_payees[i]));
        }

        // check eth balances
        assertEq(address(_testing).balance, 0);
        assertEq(_testing.totalReleased(), 10000 + additional);

        for (uint i; i < _payees.length; ++i) {
            uint ethBalance = (10000 + additional) * _testing.shares(_payees[i]) / _testing.totalShares();
            assertEq(_payees[i].balance, ethBalance);
            assertEq(_testing.released(_payees[i]), ethBalance);
        }
    }
}

2.4 totalReleased(IERC20 token) && released(IERC20 token, address account)

  • totalReleased(IERC20 token):返回已释放的某ERC20总量;
  • released(IERC20 token, address account):返回地址account已领取某ERC20的数量。
    function totalReleased(IERC20 token) public view returns (uint256) {
        return _erc20TotalReleased[token];
    }

    function released(IERC20 token, address account) public view returns (uint256) {
        return _erc20Released[token][account];
    }

2.5 release(IERC20 token, address account) && releasable(IERC20 token, address account)

  • release(IERC20 token, address account):给领款人account发放某ERC20 token;
  • releasable(IERC20 token, address account):返回领款人account可领取ERC20 token的数量。
    function release(IERC20 token, address account) public virtual {
        // 要求领款人account已在册
        require(_shares[account] > 0, "PaymentSplitter: account has no shares");
        // 计算领款人account可领取该ERC20 token的数量
        uint256 payment = releasable(token, account);
        // 要求领取数量不为0
        require(payment != 0, "PaymentSplitter: account is not due payment");

        // 已领取的该token总量自增payment
        _erc20TotalReleased[token] += payment;
        // 关闭solidity 0.8的整数运算溢出检查
        unchecked {
            // account已领取该token总量自增payment
            // 注:前面已领取的该token总量自增payment未溢出可以确保个人已领取该token总量自增payment不会溢出
            _erc20Released[token][account] += payment;
        }

        // 调用SafeERC20库的safeTransfer方法,从本合约转payment数量的该token给account
        SafeERC20.safeTransfer(token, account, payment);
        // 抛出事件
        emit ERC20PaymentReleased(token, account, payment);
    }

    function releasable(IERC20 token, address account) public view returns (uint256) {
        // 计算全部应释放的ERC20 token数量,即当前合约名下该token的余额 + 到目前为止已释放的该token总量
        uint256 totalReceived = token.balanceOf(address(this)) + totalReleased(token);
        // 计算account尚未领取的该token的数量
        // 注:released(token, account)是account目前已领取的该token数量
        return _pendingPayment(account, totalReceived, released(token, account));
    }

foundry代码验证:

contract PaymentSplitterTest is Test {
    PaymentSplitter private _testing;
    MockERC20 private _erc20 = new MockERC20("test name", "test symbol");
    address[] private _payees = [address(1), address(2), address(3)];
    uint[] private _shares = [20, 30, 50];

    function setUp() external {
        _testing = new PaymentSplitter{value: 10000}(_payees, _shares);
    }

    event ERC20PaymentReleased(IERC20 indexed token, address to, uint amount);

    function test_releaseERC20() external {
        _erc20.mint(address(_testing), 10000);
        assertEq(_erc20.balanceOf(address(_testing)), 10000);
        // test for totalShares()
        assertEq(_testing.totalShares(), 20 + 30 + 50);
        // test for totalReleased(IERC20)
        assertEq(_testing.totalReleased(_erc20), 0);
        // test for shares() && released(IERC20,address) && payee() && releasable(IERC20,address)
        for (uint i; i < _payees.length; ++i) {
            assertEq(_testing.shares(_payees[i]), _shares[i]);
            assertEq(_testing.released(_erc20, _payees[i]), 0);
            assertEq(_testing.payee(i), _payees[i]);
        }

        // test for releasable(IERC20,address)
        assertEq(_testing.releasable(_erc20, _payees[0]), 10000 * 20 / (20 + 30 + 50));
        assertEq(_testing.releasable(_erc20, _payees[1]), 10000 * 30 / (20 + 30 + 50));
        assertEq(_testing.releasable(_erc20, _payees[2]), 10000 * 50 / (20 + 30 + 50));

        // test for release(IERC20,address)
        address account = _payees[0];
        uint amountReleased = 20 / (20 + 30 + 50) * 10000;
        assertEq(_erc20.balanceOf(account), 0);

        vm.expectEmit(address(_testing));
        emit ERC20PaymentReleased(_erc20, account, amountReleased);
        _testing.release(_erc20, account);
        assertEq(_erc20.balanceOf(account), 0 + amountReleased);
        assertEq(_erc20.balanceOf(address(_testing)), 10000 - amountReleased);

        // check getter
        assertEq(_testing.totalReleased(_erc20), 0 + amountReleased);
        assertEq(_testing.released(_erc20, account), 0 + amountReleased);
        assertEq(_testing.releasable(_erc20, account), 0);

        // transfer 1000 erc20 token into contract
        uint additional = 1000;
        _erc20.mint(address(_testing), additional);
        assertEq(_erc20.balanceOf(address(_testing)), 10000 - amountReleased + additional);

        // check releasable(IERC20,address)
        // full payment for _payees[1] && _payees[2]
        assertEq(_testing.releasable(_erc20, _payees[1]), (10000 + additional) * 30 / (20 + 30 + 50));
        assertEq(_testing.releasable(_erc20, _payees[2]), (10000 + additional) * 50 / (20 + 30 + 50));
        // payment only for additional erc20 token
        assertEq(_testing.releasable(_erc20, _payees[0]), additional * 20 / (20 + 30 + 50));

        // release all
        for (uint i; i < _payees.length; ++i) {
            _testing.release(_erc20, _payees[i]);
        }

        // check erc20 token balances
        assertEq(_erc20.balanceOf(address(_testing)), 0);
        assertEq(_testing.totalReleased(_erc20), 10000 + additional);

        for (uint i; i < _payees.length; ++i) {
            uint erc20TokenBalance = (10000 + additional) * _testing.shares(_payees[i]) / _testing.totalShares();
            assertEq(_erc20.balanceOf(_payees[i]), erc20TokenBalance);
            assertEq(_testing.released(_erc20, _payees[i]), erc20TokenBalance);
        }
    }
}

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

1.jpeg

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

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

0 条评论

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