PaymentSplitter库可以在一组领取地址无感知的情况下,将定量eth或某ERC20 token按照shares占比释放给该组中的某地址。当eth或ERC20 token被转入该合约后,在册的领取地址就可以来领取属于自己占比的那部分。各领取人的shares数量只能在该合约部署时被设置。
[openzeppelin]:v4.8.3,[forge-std]:v1.5.6
PaymentSplitter库可以在一组领取地址无感知的情况下,将定量eth或某ERC20 token按照shares占比释放给该组中的某地址。当eth或ERC20 token被转入该合约后,在册的领取地址就可以来领取属于自己占比的那部分。各领取人的shares数量只能在该合约部署时被设置。领取eth和ERC20 token需要通过触发release函数完成。
PaymentSplitter合约可直接部署。
全部foundry测试合约:
测试使用的物料合约:
// 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);
}
}
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);
}
}
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);
}
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);
}
}
}
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];
}
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神。 以下是我个人的公众号,如果有技术问题可以关注我的公众号来跟我交流。 同时我也会在这个公众号上每周更新我的原创文章,喜欢的小伙伴或者老伙计可以支持一下! 如果需要转发,麻烦注明作者。十分感谢!
公众号名称:后现代泼痞浪漫主义奠基人
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!