Extend PaymentSplitter to support ERC20 tokens (#2858)
* Add MultiPaymentSplitter with ERC20 support on top of the existing PaymentSplitter * consistency and linting * Add MultiPaymentSplitter tests * fix lint * add changelog entry * add MultiPaymentSplitter to documentation * rework PaymentSplitter to include ERC20 support by default * remove test file for MultiPaymentSplitter * fix lint * completelly split erc20 and token tracking * address some PR comments * add notice about rebasing tokens * fix minor error in tests Co-authored-by: Francisco Giordano <frangio.1@gmail.com>
This commit is contained in:
@ -2,6 +2,7 @@
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import "../token/ERC20/utils/SafeERC20.sol";
|
||||
import "../utils/Address.sol";
|
||||
import "../utils/Context.sol";
|
||||
|
||||
@ -17,10 +18,15 @@ import "../utils/Context.sol";
|
||||
* `PaymentSplitter` follows a _pull payment_ model. This means that payments are not automatically forwarded to the
|
||||
* accounts but kept in this contract, and the actual transfer is triggered as a separate step by calling the {release}
|
||||
* function.
|
||||
*
|
||||
* NOTE: This contract assumes that ERC20 tokens will behave similarly to native tokens (Ether). Rebasing tokens, and
|
||||
* tokens that apply fees during transfers, are likely to not be supported as expected. If in doubt, we encourage you
|
||||
* to run tests before sending real value to this contract.
|
||||
*/
|
||||
contract PaymentSplitter is Context {
|
||||
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;
|
||||
@ -30,6 +36,9 @@ contract PaymentSplitter is Context {
|
||||
mapping(address => uint256) private _released;
|
||||
address[] private _payees;
|
||||
|
||||
mapping(IERC20 => uint256) private _erc20TotalReleased;
|
||||
mapping(IERC20 => mapping(address => uint256)) private _erc20Released;
|
||||
|
||||
/**
|
||||
* @dev Creates an instance of `PaymentSplitter` where each account in `payees` is assigned the number of shares at
|
||||
* the matching position in the `shares` array.
|
||||
@ -73,6 +82,14 @@ contract PaymentSplitter is Context {
|
||||
return _totalReleased;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Getter for the total amount of `token` already released. `token` should be the address of an IERC20
|
||||
* contract.
|
||||
*/
|
||||
function totalReleased(IERC20 token) public view returns (uint256) {
|
||||
return _erc20TotalReleased[token];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Getter for the amount of shares held by an account.
|
||||
*/
|
||||
@ -87,6 +104,14 @@ contract PaymentSplitter is Context {
|
||||
return _released[account];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Getter for the amount of `token` tokens already released to a payee. `token` should be the address of an
|
||||
* IERC20 contract.
|
||||
*/
|
||||
function released(IERC20 token, address account) public view returns (uint256) {
|
||||
return _erc20Released[token][account];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Getter for the address of the payee number `index`.
|
||||
*/
|
||||
@ -101,18 +126,50 @@ contract PaymentSplitter is Context {
|
||||
function release(address payable account) public virtual {
|
||||
require(_shares[account] > 0, "PaymentSplitter: account has no shares");
|
||||
|
||||
uint256 totalReceived = address(this).balance + _totalReleased;
|
||||
uint256 payment = (totalReceived * _shares[account]) / _totalShares - _released[account];
|
||||
uint256 totalReceived = address(this).balance + totalReleased();
|
||||
uint256 payment = _pendingPayment(account, totalReceived, released(account));
|
||||
|
||||
require(payment != 0, "PaymentSplitter: account is not due payment");
|
||||
|
||||
_released[account] = _released[account] + payment;
|
||||
_totalReleased = _totalReleased + payment;
|
||||
_released[account] += payment;
|
||||
_totalReleased += payment;
|
||||
|
||||
Address.sendValue(account, payment);
|
||||
emit PaymentReleased(account, payment);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Triggers a transfer to `account` of the amount of `token` tokens they are owed, according to their
|
||||
* percentage of the total shares and their previous withdrawals. `token` must be the address of an IERC20
|
||||
* contract.
|
||||
*/
|
||||
function release(IERC20 token, address account) public virtual {
|
||||
require(_shares[account] > 0, "PaymentSplitter: account has no shares");
|
||||
|
||||
uint256 totalReceived = token.balanceOf(address(this)) + totalReleased(token);
|
||||
uint256 payment = _pendingPayment(account, totalReceived, released(token, account));
|
||||
|
||||
require(payment != 0, "PaymentSplitter: account is not due payment");
|
||||
|
||||
_erc20Released[token][account] += payment;
|
||||
_erc20TotalReleased[token] += payment;
|
||||
|
||||
SafeERC20.safeTransfer(token, account, payment);
|
||||
emit ERC20PaymentReleased(token, account, payment);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev internal logic for computing the pending payment of an `account` given the token historical balances and
|
||||
* already released amounts.
|
||||
*/
|
||||
function _pendingPayment(
|
||||
address account,
|
||||
uint256 totalReceived,
|
||||
uint256 alreadyReleased
|
||||
) private view returns (uint256) {
|
||||
return (totalReceived * _shares[account]) / _totalShares - alreadyReleased;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Add a new payee to the contract.
|
||||
* @param account The address of the payee to add.
|
||||
|
||||
Reference in New Issue
Block a user