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 合约地址,然后由团队成员自行领取利润。