# PaymentSplitter合约详解

By [xyyme.eth](https://paragraph.com/@xyyme) · 2022-03-21

---

[PaymentSplitter](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/finance/PaymentSplitter.sol) 是 OpenZeppelin 合约库中的一个合约，简单来说目的就是为了分赃 :)

### 概述

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

画个图简单描述下：

![利润分配](https://storage.googleapis.com/papyrus_images/55eb7173d8aec9f204711a981d611a76d9d1d7d405c126ba93f5091ebd08313c.png)

利润分配

### 代码

#### 数据结构：

    // 所有成员的份额总和
    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](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/finance/PaymentSplitter.sol)

---

*Originally published on [xyyme.eth](https://paragraph.com/@xyyme/paymentsplitter)*
