From 9629817e4f1a765bb34df4fb0efca69d90b18355 Mon Sep 17 00:00:00 2001 From: zugdev Date: Wed, 2 Oct 2024 10:42:19 -0300 Subject: [PATCH 1/5] feat: kickstart TreasurySplitter --- .../src/dollar/core/TreasurySplitter.sol | 237 ++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 packages/contracts/src/dollar/core/TreasurySplitter.sol diff --git a/packages/contracts/src/dollar/core/TreasurySplitter.sol b/packages/contracts/src/dollar/core/TreasurySplitter.sol new file mode 100644 index 000000000..66cdbe4b3 --- /dev/null +++ b/packages/contracts/src/dollar/core/TreasurySplitter.sol @@ -0,0 +1,237 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.8.0) (finance/PaymentSplitter.sol) + +pragma solidity ^0.8.0; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {Context} from "@openzeppelin/contracts/utils/Context.sol"; + +/** + * @title PaymentSplitter + * @dev This contract allows to split Ether payments among a group of accounts. The sender does not need to be aware + * that the Ether will be split in this way, since it is handled transparently by the contract. + * + * The split can be in equal parts or in any other arbitrary proportion. The way this is specified is by assigning each + * account to a number of shares. Of all the Ether that this contract receives, each account will then be able to claim + * an amount proportional to the percentage of total shares they were assigned. The distribution of shares is set at the + * time of contract deployment and can't be updated thereafter. + * + * `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; + uint256 private _totalReleased; + + mapping(address => uint256) private _shares; + 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. + * + * All addresses in `payees` must be non-zero. Both arrays must have the same non-zero length, and there must be no + * duplicates in `payees`. + */ + 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]); + } + } + + /** + * @dev The Ether received will be logged with {PaymentReceived} events. Note that these events are not fully + * reliable: it's possible for a contract to receive Ether without triggering this function. This only affects the + * reliability of the events, and not the actual splitting of Ether. + * + * To learn more about this see the Solidity documentation for + * https://solidity.readthedocs.io/en/latest/contracts.html#fallback-function[fallback + * functions]. + */ + receive() external payable virtual { + emit PaymentReceived(_msgSender(), msg.value); + } + + /** + * @dev Getter for the total shares held by payees. + */ + function totalShares() public view returns (uint256) { + return _totalShares; + } + + /** + * @dev Getter for the total amount of Ether already released. + */ + function totalReleased() public view returns (uint256) { + 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. + */ + function shares(address account) public view returns (uint256) { + return _shares[account]; + } + + /** + * @dev Getter for the amount of Ether already released to a payee. + */ + function released(address account) public view returns (uint256) { + 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`. + */ + function payee(uint256 index) public view returns (address) { + return _payees[index]; + } + + /** + * @dev Getter for the amount of payee's releasable Ether. + */ + function releasable(address account) public view returns (uint256) { + uint256 totalReceived = address(this).balance + totalReleased(); + return _pendingPayment(account, totalReceived, released(account)); + } + + /** + * @dev Getter for the amount of payee's releasable `token` tokens. `token` should be the address of an + * IERC20 contract. + */ + function releasable( + IERC20 token, + address account + ) public view returns (uint256) { + uint256 totalReceived = token.balanceOf(address(this)) + + totalReleased(token); + return + _pendingPayment(account, totalReceived, released(token, account)); + } + + /** + * @dev Triggers a transfer to `account` of the amount of Ether they are owed, according to their percentage of the + * total shares and their previous withdrawals. + */ + function release(address payable account) public virtual { + require(_shares[account] > 0, "PaymentSplitter: account has no shares"); + + uint256 payment = releasable(account); + + require(payment != 0, "PaymentSplitter: account is not due payment"); + + // _totalReleased is the sum of all values in _released. + // If "_totalReleased += payment" does not overflow, then "_released[account] += payment" cannot overflow. + _totalReleased += payment; + unchecked { + _released[account] += 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 payment = releasable(token, account); + + require(payment != 0, "PaymentSplitter: account is not due payment"); + + // _erc20TotalReleased[token] is the sum of all values in _erc20Released[token]. + // If "_erc20TotalReleased[token] += payment" does not overflow, then "_erc20Released[token][account] += payment" + // cannot overflow. + _erc20TotalReleased[token] += payment; + unchecked { + _erc20Released[token][account] += 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. + * @param shares_ The number of shares owned by the payee. + */ + function _addPayee(address account, uint256 shares_) private { + require( + account != address(0), + "PaymentSplitter: account is the zero address" + ); + 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_); + } +} From 58e243eeac74bef859640591bd06a8ddeb7eddc3 Mon Sep 17 00:00:00 2001 From: zugdev Date: Wed, 2 Oct 2024 11:06:47 -0300 Subject: [PATCH 2/5] feat: add a GovernanceRewardsSplitter draft --- .../dollar/core/GovernanceRewardsSplitter.sol | 199 +++++++++++++++ .../src/dollar/core/TreasurySplitter.sol | 237 ------------------ 2 files changed, 199 insertions(+), 237 deletions(-) create mode 100644 packages/contracts/src/dollar/core/GovernanceRewardsSplitter.sol delete mode 100644 packages/contracts/src/dollar/core/TreasurySplitter.sol diff --git a/packages/contracts/src/dollar/core/GovernanceRewardsSplitter.sol b/packages/contracts/src/dollar/core/GovernanceRewardsSplitter.sol new file mode 100644 index 000000000..cf16bab93 --- /dev/null +++ b/packages/contracts/src/dollar/core/GovernanceRewardsSplitter.sol @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +/** + * @title GovernanceRewardsSplitter + * @notice A configurable mono ERC20 payment splitter. + * @dev This contract allows to split governance token payments among a group of accounts. The sender does not need to be aware + * that the ERC20 will be split in this way, since it is handled transparently by the contract. + * + * The split can be in equal parts or in any other arbitrary proportion. The way this is specified is by assigning each + * account to a number of shares. Of all tokens that this contract receives, each account will then be able to claim + * an amount proportional to the percentage of total shares they were assigned. + * + * This contract is configurable by owner, which means that at any time it's owner can update the split configuration. + * The configuration is tracked through IDs and all previous configurations can be transparently checked. + * + * `GovernanceRewardsSplitter` 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. + */ +contract GovernanceRewardsSplitter is Ownable { + IERC20 governanceToken = IERC20(address(0x0)); + + event NewSplitConfiguration( + uint256 indexed currentConfig, + address[] payees, + uint256[] shares + ); + event PayeeAdded( + uint256 indexed currentConfig, + address account, + uint256 shares + ); + event GovernanceTokenReleased( + IERC20 governanceToken, + address indexed to, + uint256 amount + ); + + /// @dev Split configuration is ID based, whenever a new config is set currentConfig is incremented. + uint256 public currentConfig; + mapping(uint256 => address[]) public _configToPayees; + mapping(uint256 => mapping(address => uint256)) public _configToShares; + mapping(uint256 => uint256) public _configTotalShares; + + uint256 public governanceTokenTotalReleased; + mapping(address => uint256) public accountToGovernanceTokenReleased; + + /** + * @dev Creates an instance of `GovernanceRewardsSplitter` where each account in `payees` is assigned the number of shares at + * the matching position in the `shares` array. + * + * All addresses in `payees` must be non-zero. Both arrays must have the same non-zero length, and there must be no + * duplicates in `payees`. + */ + constructor(address[] memory payees, uint256[] memory shares_) payable { + require( + payees.length == shares_.length, + "GovernanceRewardsSplitter: payees and shares length mismatch" + ); + require(payees.length > 0, "GovernanceRewardsSplitter: no payees"); + + // Initial configuration ID will be 1 as setNewConfig will increment currentConfig + currentConfig = 0; + + // This will set an initial configuration of payees and shares + setNewConfig(payees, shares_); + } + + /** + * @dev Triggers a transfer to `account` of the amount of `governanceToken` tokens they are owed, according to their + * percentage of the total shares and their previous withdrawals. + */ + function release(address account) public virtual { + require( + _configToShares[currentConfig][account] > 0, + "GovernanceRewardsSplitter: account has no shares" + ); + + uint256 payment = releasable(account); + + require( + payment != 0, + "GovernanceRewardsSplitter: account is not due payment" + ); + + // _erc20TotalReleased[governanceToken] is the sum of all values in _erc20Released[governanceToken]. + // If "_erc20TotalReleased[governanceToken] += payment" does not overflow, then "_erc20Released[governanceToken][account] += payment" + // cannot overflow. + governanceTokenTotalReleased += payment; + unchecked { + accountToGovernanceTokenReleased[account] += payment; + } + + SafeERC20.safeTransfer(governanceToken, account, payment); + emit GovernanceTokenReleased(governanceToken, account, payment); + } + + /** + * @dev Add a new payee to the contract. + * @param account The address of the payee to add. + * @param shares_ The number of shares owned by the payee. + */ + function addPayee(address account, uint256 shares_) private { + require( + account != address(0), + "GovernanceRewardsSplitter: account is the zero address" + ); + require(shares_ > 0, "GovernanceRewardsSplitter: shares are 0"); + require( + _configToShares[currentConfig][account] == 0, + "GovernanceRewardsSplitter: account already has shares" + ); + + _configToPayees[currentConfig].push(account); + _configToShares[currentConfig][account] = shares_; + _configTotalShares[currentConfig] += shares_; + emit PayeeAdded(currentConfig, account, shares_); + } + + /** + * @dev Sets a new payee and share config + * @param payees The addresses of the payees to be set. + * @param shares_ The number of shares owned respectively by each payee. (i.e shares_[0] is the amount owned by payees[0]) + */ + function setNewConfig( + address[] memory payees, + uint256[] memory shares_ + ) public onlyOwner { + currentConfig++; // Start's this new splitter config round + require( + payees.length == shares_.length, + "GovernanceRewardsSplitter: miss match between payees length and shares_ length" + ); + require(payees.length > 0, "GovernanceRewardsSplitter: no payees"); + + for (uint256 i = 0; i < payees.length; i++) { + addPayee(payees[i], shares_[i]); + } + emit NewSplitConfiguration(currentConfig, payees, shares_); + } + + /** + * @dev Getter for current round's payees. + */ + function currentPayees() public view returns (address[] memory) { + return _configToPayees[currentConfig]; + } + + /** + * @dev Getter for the current round's amount of shares held by an account. + */ + function currentShares(address account) public view returns (uint256) { + return _configToShares[currentConfig][account]; + } + + /** + * @dev Getter for the total shares held by payees. + */ + function currentTotalShares() public view returns (uint256) { + return _configTotalShares[currentConfig]; + } + + /** + * @dev Getter for the amount of payee's releasable `governanceToken` tokens. + */ + function releasable(address account) public view returns (uint256) { + uint256 totalReceived = governanceToken.balanceOf(address(this)) + + governanceTokenTotalReleased; + return + _pendingPayment( + account, + totalReceived, + accountToGovernanceTokenReleased[account] + ); + } + + /** + * @dev internal logic for computing the pending payment of an `account` given the governanceToken historical balances and + * already released amounts. + */ + function _pendingPayment( + address account, + uint256 totalReceived, + uint256 alreadyReleased + ) private view returns (uint256) { + return + (totalReceived * _configToShares[currentConfig][account]) / + _configTotalShares[currentConfig] - + alreadyReleased; + } +} diff --git a/packages/contracts/src/dollar/core/TreasurySplitter.sol b/packages/contracts/src/dollar/core/TreasurySplitter.sol deleted file mode 100644 index 66cdbe4b3..000000000 --- a/packages/contracts/src/dollar/core/TreasurySplitter.sol +++ /dev/null @@ -1,237 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (finance/PaymentSplitter.sol) - -pragma solidity ^0.8.0; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {Address} from "@openzeppelin/contracts/utils/Address.sol"; -import {Context} from "@openzeppelin/contracts/utils/Context.sol"; - -/** - * @title PaymentSplitter - * @dev This contract allows to split Ether payments among a group of accounts. The sender does not need to be aware - * that the Ether will be split in this way, since it is handled transparently by the contract. - * - * The split can be in equal parts or in any other arbitrary proportion. The way this is specified is by assigning each - * account to a number of shares. Of all the Ether that this contract receives, each account will then be able to claim - * an amount proportional to the percentage of total shares they were assigned. The distribution of shares is set at the - * time of contract deployment and can't be updated thereafter. - * - * `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; - uint256 private _totalReleased; - - mapping(address => uint256) private _shares; - 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. - * - * All addresses in `payees` must be non-zero. Both arrays must have the same non-zero length, and there must be no - * duplicates in `payees`. - */ - 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]); - } - } - - /** - * @dev The Ether received will be logged with {PaymentReceived} events. Note that these events are not fully - * reliable: it's possible for a contract to receive Ether without triggering this function. This only affects the - * reliability of the events, and not the actual splitting of Ether. - * - * To learn more about this see the Solidity documentation for - * https://solidity.readthedocs.io/en/latest/contracts.html#fallback-function[fallback - * functions]. - */ - receive() external payable virtual { - emit PaymentReceived(_msgSender(), msg.value); - } - - /** - * @dev Getter for the total shares held by payees. - */ - function totalShares() public view returns (uint256) { - return _totalShares; - } - - /** - * @dev Getter for the total amount of Ether already released. - */ - function totalReleased() public view returns (uint256) { - 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. - */ - function shares(address account) public view returns (uint256) { - return _shares[account]; - } - - /** - * @dev Getter for the amount of Ether already released to a payee. - */ - function released(address account) public view returns (uint256) { - 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`. - */ - function payee(uint256 index) public view returns (address) { - return _payees[index]; - } - - /** - * @dev Getter for the amount of payee's releasable Ether. - */ - function releasable(address account) public view returns (uint256) { - uint256 totalReceived = address(this).balance + totalReleased(); - return _pendingPayment(account, totalReceived, released(account)); - } - - /** - * @dev Getter for the amount of payee's releasable `token` tokens. `token` should be the address of an - * IERC20 contract. - */ - function releasable( - IERC20 token, - address account - ) public view returns (uint256) { - uint256 totalReceived = token.balanceOf(address(this)) + - totalReleased(token); - return - _pendingPayment(account, totalReceived, released(token, account)); - } - - /** - * @dev Triggers a transfer to `account` of the amount of Ether they are owed, according to their percentage of the - * total shares and their previous withdrawals. - */ - function release(address payable account) public virtual { - require(_shares[account] > 0, "PaymentSplitter: account has no shares"); - - uint256 payment = releasable(account); - - require(payment != 0, "PaymentSplitter: account is not due payment"); - - // _totalReleased is the sum of all values in _released. - // If "_totalReleased += payment" does not overflow, then "_released[account] += payment" cannot overflow. - _totalReleased += payment; - unchecked { - _released[account] += 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 payment = releasable(token, account); - - require(payment != 0, "PaymentSplitter: account is not due payment"); - - // _erc20TotalReleased[token] is the sum of all values in _erc20Released[token]. - // If "_erc20TotalReleased[token] += payment" does not overflow, then "_erc20Released[token][account] += payment" - // cannot overflow. - _erc20TotalReleased[token] += payment; - unchecked { - _erc20Released[token][account] += 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. - * @param shares_ The number of shares owned by the payee. - */ - function _addPayee(address account, uint256 shares_) private { - require( - account != address(0), - "PaymentSplitter: account is the zero address" - ); - 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_); - } -} From addf9ebb05e8dc29cef7d1e431517dcd6db36fda Mon Sep 17 00:00:00 2001 From: zugdev Date: Wed, 2 Oct 2024 11:08:58 -0300 Subject: [PATCH 3/5] feat: allow owner to add a single payee --- .../contracts/src/dollar/core/GovernanceRewardsSplitter.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/src/dollar/core/GovernanceRewardsSplitter.sol b/packages/contracts/src/dollar/core/GovernanceRewardsSplitter.sol index cf16bab93..2c76a5aaa 100644 --- a/packages/contracts/src/dollar/core/GovernanceRewardsSplitter.sol +++ b/packages/contracts/src/dollar/core/GovernanceRewardsSplitter.sol @@ -108,7 +108,7 @@ contract GovernanceRewardsSplitter is Ownable { * @param account The address of the payee to add. * @param shares_ The number of shares owned by the payee. */ - function addPayee(address account, uint256 shares_) private { + function addPayee(address account, uint256 shares_) public onlyOwner { require( account != address(0), "GovernanceRewardsSplitter: account is the zero address" From 684ab94da269513d26d2654554e07482a14e23ca Mon Sep 17 00:00:00 2001 From: zugdev Date: Wed, 2 Oct 2024 11:23:47 -0300 Subject: [PATCH 4/5] feat: specify governance token as a constant --- .../contracts/src/dollar/core/GovernanceRewardsSplitter.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/src/dollar/core/GovernanceRewardsSplitter.sol b/packages/contracts/src/dollar/core/GovernanceRewardsSplitter.sol index 2c76a5aaa..37621b50f 100644 --- a/packages/contracts/src/dollar/core/GovernanceRewardsSplitter.sol +++ b/packages/contracts/src/dollar/core/GovernanceRewardsSplitter.sol @@ -26,7 +26,7 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol * tokens that apply fees during transfers, are likely to not be supported as expected. */ contract GovernanceRewardsSplitter is Ownable { - IERC20 governanceToken = IERC20(address(0x0)); + IERC20 public constant governanceToken = IERC20(address(0x0)); event NewSplitConfiguration( uint256 indexed currentConfig, From 807b3e40ff09434e51c3d50fa6065bc62af878cd Mon Sep 17 00:00:00 2001 From: zugdev Date: Fri, 4 Oct 2024 12:47:44 -0300 Subject: [PATCH 5/5] feat: change splitter to enumerable map approach --- .../dollar/core/GovernanceRewardsSplitter.sol | 125 +++++++++--------- 1 file changed, 65 insertions(+), 60 deletions(-) diff --git a/packages/contracts/src/dollar/core/GovernanceRewardsSplitter.sol b/packages/contracts/src/dollar/core/GovernanceRewardsSplitter.sol index 37621b50f..9c78b0078 100644 --- a/packages/contracts/src/dollar/core/GovernanceRewardsSplitter.sol +++ b/packages/contracts/src/dollar/core/GovernanceRewardsSplitter.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.19; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {EnumerableMap} from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol"; /** * @title GovernanceRewardsSplitter @@ -15,40 +16,30 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol * account to a number of shares. Of all tokens that this contract receives, each account will then be able to claim * an amount proportional to the percentage of total shares they were assigned. * - * This contract is configurable by owner, which means that at any time it's owner can update the split configuration. - * The configuration is tracked through IDs and all previous configurations can be transparently checked. - * - * `GovernanceRewardsSplitter` 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} + * This contract is configurable by the owner, meaning the owner can update the split configuration at any time. + * `GovernanceRewardsSplitter` follows a _pull payment_ model, where payments are not automatically forwarded to the + * accounts but kept in this contract. 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. */ contract GovernanceRewardsSplitter is Ownable { + using EnumerableMap for EnumerableMap.AddressToUintMap; + IERC20 public constant governanceToken = IERC20(address(0x0)); - event NewSplitConfiguration( - uint256 indexed currentConfig, - address[] payees, - uint256[] shares - ); - event PayeeAdded( - uint256 indexed currentConfig, - address account, - uint256 shares - ); + event PayeeAdded(address account, uint256 shares); + event PayeeEdited(address account, uint256 newShares); + event PayeeDeleted(address account); event GovernanceTokenReleased( IERC20 governanceToken, address indexed to, uint256 amount ); - /// @dev Split configuration is ID based, whenever a new config is set currentConfig is incremented. - uint256 public currentConfig; - mapping(uint256 => address[]) public _configToPayees; - mapping(uint256 => mapping(address => uint256)) public _configToShares; - mapping(uint256 => uint256) public _configTotalShares; + EnumerableMap.AddressToUintMap private _payeesToShares; + uint256 public totalShares; uint256 public governanceTokenTotalReleased; mapping(address => uint256) public accountToGovernanceTokenReleased; @@ -67,11 +58,10 @@ contract GovernanceRewardsSplitter is Ownable { ); require(payees.length > 0, "GovernanceRewardsSplitter: no payees"); - // Initial configuration ID will be 1 as setNewConfig will increment currentConfig - currentConfig = 0; - - // This will set an initial configuration of payees and shares - setNewConfig(payees, shares_); + // Initialize payees and shares + for (uint256 i = 0; i < payees.length; i++) { + addPayee(payees[i], shares_[i]); + } } /** @@ -80,7 +70,7 @@ contract GovernanceRewardsSplitter is Ownable { */ function release(address account) public virtual { require( - _configToShares[currentConfig][account] > 0, + _payeesToShares.contains(account), "GovernanceRewardsSplitter: account has no shares" ); @@ -91,9 +81,6 @@ contract GovernanceRewardsSplitter is Ownable { "GovernanceRewardsSplitter: account is not due payment" ); - // _erc20TotalReleased[governanceToken] is the sum of all values in _erc20Released[governanceToken]. - // If "_erc20TotalReleased[governanceToken] += payment" does not overflow, then "_erc20Released[governanceToken][account] += payment" - // cannot overflow. governanceTokenTotalReleased += payment; unchecked { accountToGovernanceTokenReleased[account] += payment; @@ -115,57 +102,77 @@ contract GovernanceRewardsSplitter is Ownable { ); require(shares_ > 0, "GovernanceRewardsSplitter: shares are 0"); require( - _configToShares[currentConfig][account] == 0, + !_payeesToShares.contains(account), "GovernanceRewardsSplitter: account already has shares" ); - _configToPayees[currentConfig].push(account); - _configToShares[currentConfig][account] = shares_; - _configTotalShares[currentConfig] += shares_; - emit PayeeAdded(currentConfig, account, shares_); + _payeesToShares.set(account, shares_); + totalShares += shares_; + emit PayeeAdded(account, shares_); } /** - * @dev Sets a new payee and share config - * @param payees The addresses of the payees to be set. - * @param shares_ The number of shares owned respectively by each payee. (i.e shares_[0] is the amount owned by payees[0]) + * @dev Edit an existing payee's shares. + * @param account The address of the payee to edit. + * @param newShares The new number of shares owned by the payee. */ - function setNewConfig( - address[] memory payees, - uint256[] memory shares_ - ) public onlyOwner { - currentConfig++; // Start's this new splitter config round + function editPayee(address account, uint256 newShares) public onlyOwner { require( - payees.length == shares_.length, - "GovernanceRewardsSplitter: miss match between payees length and shares_ length" + _payeesToShares.contains(account), + "GovernanceRewardsSplitter: account does not exist" + ); + require( + newShares > 0, + "GovernanceRewardsSplitter: new shares must be greater than 0" ); - require(payees.length > 0, "GovernanceRewardsSplitter: no payees"); - for (uint256 i = 0; i < payees.length; i++) { - addPayee(payees[i], shares_[i]); - } - emit NewSplitConfiguration(currentConfig, payees, shares_); + uint256 oldShares = _payeesToShares.get(account); + totalShares = totalShares - oldShares + newShares; + _payeesToShares.set(account, newShares); + emit PayeeEdited(account, newShares); + } + + /** + * @dev Delete an existing payee from the contract. + * @param account The address of the payee to delete. + */ + function deletePayee(address account) public onlyOwner { + require( + _payeesToShares.contains(account), + "GovernanceRewardsSplitter: account does not exist" + ); + + uint256 shares = _payeesToShares.get(account); + totalShares -= shares; + _payeesToShares.remove(account); + emit PayeeDeleted(account); } /** - * @dev Getter for current round's payees. + * @dev Getter for all current payees. */ function currentPayees() public view returns (address[] memory) { - return _configToPayees[currentConfig]; + address[] memory payees = new address[](_payeesToShares.length()); + for (uint256 i = 0; i < _payeesToShares.length(); i++) { + (address payee, ) = _payeesToShares.at(i); + payees[i] = payee; + } + return payees; } /** - * @dev Getter for the current round's amount of shares held by an account. + * @dev Getter for the current amount of shares held by an account. */ function currentShares(address account) public view returns (uint256) { - return _configToShares[currentConfig][account]; + (, uint256 shares) = _payeesToShares.tryGet(account); + return shares; } /** - * @dev Getter for the total shares held by payees. + * @dev Getter for the total shares held by all payees. */ function currentTotalShares() public view returns (uint256) { - return _configTotalShares[currentConfig]; + return totalShares; } /** @@ -183,7 +190,7 @@ contract GovernanceRewardsSplitter is Ownable { } /** - * @dev internal logic for computing the pending payment of an `account` given the governanceToken historical balances and + * @dev Internal logic for computing the pending payment of an `account` given the governanceToken historical balances and * already released amounts. */ function _pendingPayment( @@ -191,9 +198,7 @@ contract GovernanceRewardsSplitter is Ownable { uint256 totalReceived, uint256 alreadyReleased ) private view returns (uint256) { - return - (totalReceived * _configToShares[currentConfig][account]) / - _configTotalShares[currentConfig] - - alreadyReleased; + uint256 shares = _payeesToShares.get(account); + return (totalReceived * shares) / totalShares - alreadyReleased; } }