xyyme.eth

Posted on Mar 21, 2022Read on Mirror.xyz

PaymentSplitter合约详解

PaymentSplitter 是 OpenZeppelin 合约库中的一个合约,简单来说目的就是为了分赃 :)

概述

认真来说,就是在团队协作中,将利润分配这一步骤写在合约中,给每个团队成员提前定好分成,然后将其写在合约中,这样就会避免某一个成员贪污其他成员的币的情况发生。

画个图简单描述下:

利润分配

代码

数据结构:

// 所有成员的份额总和
uint256 private _totalShares;
// 已经分发的 ETH 总量
uint256 private _totalReleased;

// 每个成员对应其份额
mapping(address => uint256) private _shares;
// 每个成员对应其已经领取的数量
mapping(address => uint256) private _released;
// 成员列表
address[] private _payees;

// 已经分发的 ERC20 token 总量
mapping(IERC20 => uint256) private _erc20TotalReleased;
// ERC20 token -> 成员地址 -> 已经分发的数量
mapping(IERC20 => mapping(address => uint256)) private _erc20Released;

从数据结构可以看出,该合约同时支持 ETH 和 ERC20 币种的分发。

构造方法:

// 在构造方法中初始化成员地址与份额,这两个信息只能在构造方法中添加,不能后面再次添加

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");
    // 不能重复
    require(_shares[account] == 0, "PaymentSplitter: account already has shares");

    _payees.push(account);
    _shares[account] = shares_;
    _totalShares = _totalShares + shares_;
    emit PayeeAdded(account, shares_);
}

添加成员时,并没有要求所有成员的份额总和是 100,但是我个人还是推荐使用总和 100 的数据比较好,这样计算方便一点。

合约接收 ETH:

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

当合约接收到 ETH 的时候,会发送 PaymentReceived 事件。注意,这里的事件发送并不是可靠的,因为有一些情况,比如合约 selfdestruct 的时候,如果要给合约发送 ETH,也是不会触发这个事件的。

不过这里并不影响分配的逻辑,只是说不要过度依赖于事件。

ETH 分配逻辑:

// 参数是成员的地址
function release(address payable account) public virtual {
    // 校验成员地址有效
    require(_shares[account] > 0, "PaymentSplitter: account has no shares");

    // 合约一共接收的 ETH 的数量
    uint256 totalReceived = address(this).balance + totalReleased();
    // 计算该给成员地址打多少币
    uint256 payment = _pendingPayment(account, totalReceived, released(account));

    require(payment != 0, "PaymentSplitter: account is not due payment");

    // 更新该地址的分成数量
    _released[account] += payment;
    // 更新总分发数量
    _totalReleased += payment;

    // 打币
    Address.sendValue(account, payment);
    emit PaymentReleased(account, payment);
}


function _pendingPayment(
    address account,
    uint256 totalReceived,
    uint256 alreadyReleased
) private view returns (uint256) {
    return (totalReceived * _shares[account]) / _totalShares - alreadyReleased;
}

_pendingPayment 中的计算逻辑是,用合约接收到的总量以及该地址的份额,计算出该地址应该接收的数量,然后再减去该地址已经接收到的数量,就是这次应该接收的数量。

这里需要减去 alreadyReleased 的原因是,合约可能会不止一次接收到外部打入的币种,如果该地址上次已经领取过币,那么需要去除上次的数量,才是这次的增量。

ERC20 的 release 方法与上面 ETH 的大同小异,这里就不再赘述了。

总结

PaymentSplitter 合约本身逻辑比较简单,是一个公平的分配方法。如果团队的利润分配地址是由一个 EOA 管理,那么就无法避免管理该地址的成员作恶。

该合约本身试用场景也挺多,比如在最近很火的 NFT 方向。项目方要将售卖的 ETH 从合约中取出来,那么就可以在售卖合约中将接收人地址设置为 PaymentSplitter 合约地址,然后由团队成员自行领取利润。

参考:

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/finance/PaymentSplitter.sol