From 183fe805d47807ff1c6628b94b9f9b6c457a9700 Mon Sep 17 00:00:00 2001 From: Kurt Date: Tue, 1 Nov 2022 18:37:04 +0100 Subject: [PATCH 1/8] added donate many --- src/DonationHandler.sol | 200 +++++++++++++++++++++------- test/DonationHandler.t.sol | 47 ++++--- test/DonationHandlerMulticall.t.sol | 4 +- 3 files changed, 184 insertions(+), 67 deletions(-) diff --git a/src/DonationHandler.sol b/src/DonationHandler.sol index 919e83e..0a575ed 100644 --- a/src/DonationHandler.sol +++ b/src/DonationHandler.sol @@ -2,10 +2,8 @@ pragma solidity 0.8.17; import {IERC20Upgradeable as IERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; -import {SafeERC20Upgradeable as SafeERC20} from - "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; -import {ReentrancyGuardUpgradeable as ReentrancyGuard} from - "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import {SafeERC20Upgradeable as SafeERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; +import {ReentrancyGuardUpgradeable as ReentrancyGuard} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; import {MulticallUpgradeable as Multicall} from "@openzeppelin/contracts-upgradeable/utils/MulticallUpgradeable.sol"; import "./DonationHandlerRoles.sol"; @@ -15,8 +13,8 @@ import "./DonationHandlerRoles.sol"; /// This contract is build to use with proxies. /// /// The user can donate whitelisted token to whitelisted recipients by calling the donate function. -/// A donation fee can be set by the user. The fee is taken from the donation amount. -/// The donation fee is a percentage of the donation amount where 1e18 is 100%, 1e17 10%, etc.. +/// A donation fee can be set by the user. The fee is paid in addition to the donation amount. +/// The donation fee is the amount the donor pays to the fee receiver (protocol) /// The donation fee can be set by the user and is limited by the minFee and maxFee. /// The min fee is set by default to 0 and can be changed by the protocol admins. /// The max fee is set by default to 1e18 and can't be changed. @@ -44,6 +42,17 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall { /// @notice mapping: user => token => amount mapping(address => mapping(address => uint256)) public balances; + struct RecipientInfo { + address recipient; + uint256 amount; + } + + struct Donation { + address token; + uint256 fee; + RecipientInfo[] recipients; + } + /// @notice Initialize the contract. /// @param _acceptedToken Array of accepted tokens /// @param _donationReceiver Array of donation receivers @@ -55,39 +64,97 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall { address[] calldata _feeReceiver, address[] calldata _admins ) public initializer { - __DonationHandlerRoles_init(_acceptedToken, _donationReceiver, _feeReceiver, _admins); + __DonationHandlerRoles_init( + _acceptedToken, + _donationReceiver, + _feeReceiver, + _admins + ); __ReentrancyGuard_init(); __Multicall_init(); } - /// @notice Donate tokens to a recipient. The fee is deducted from the donation amount. + /// @notice Donate tokens to a recipient. The fee added to the donation amount. /// @param _token Address of the token to donate /// @param _recipient Address of the recipient /// @param _amount Amount of tokens to donate /// @param _fee Fee to be paid to the fee receiver (protocol) - function donate(address _token, address _recipient, uint256 _amount, uint256 _fee) external payable nonReentrant { - if (_fee > HUNDRED) revert FeeTooHigh(); - if (_fee < minFee) revert FeeTooLow(); + function donate( + address _token, + address _recipient, + uint256 _amount, + uint256 _fee + ) external payable nonReentrant { if (_amount == 0) revert InvalidAmount(); - _validateDonation(_token, _recipient); + uint256 totalDonationAmount = _amount + _fee; - if (_token != NATIVE) { - _transfer(_token, _amount); - } else { - if (msg.value != _amount) revert InvalidAmount(); + _checkToken(_token); + _checkDonationRecipient(_recipient); + + _registerDonation(_token, _recipient, _amount); + _handleFee(_token, totalDonationAmount, _fee); + + _transfer(_token, totalDonationAmount); + } + + /// @notice Donate a list of donations. + /// @param _donations Array of donations. Each donation contains a token, a fee and a list of recipients. Each recipient contains an address and an amount. + function donateMany(Donation[] memory _donations) + external + payable + nonReentrant + { + uint256 donationLength = _donations.length; + + for (uint256 i; i < donationLength; ) { + Donation memory donation = _donations[i]; + + _checkToken(donation.token); + + uint256 totalDonationAmount = donation.fee; + uint256 recipientLength = donation.recipients.length; + + for (uint256 j; j < recipientLength; ) { + RecipientInfo memory recipientInfo = donation.recipients[j]; + + _checkDonationRecipient(recipientInfo.recipient); + + if (recipientInfo.amount == 0) revert InvalidAmount(); + totalDonationAmount += recipientInfo.amount; + + _registerDonation( + donation.token, + recipientInfo.recipient, + recipientInfo.amount + ); + + unchecked { + j++; + } + } + + _handleFee(donation.token, totalDonationAmount, donation.fee); + _transfer(donation.token, totalDonationAmount); + + unchecked { + i++; + } } + } - if (_fee == 0) { - _registerDonation(_token, _recipient, _amount); - } else if (_fee == HUNDRED) { - _registerFee(_token, _amount); - } else { - uint256 feeAmount = (_amount * _fee) / HUNDRED; - uint256 donationAmount = _amount - feeAmount; + /// @notice registers the fee (if fee > 0) and checks if the fee amount is valid (only if the minFee is > 0) + /// @param _token Address of the token + /// @param _totalDonationAmount Total donation amount + /// @param _fee Fee to be paid to the fee receiver (protocol) + function _handleFee(address _token, uint256 _totalDonationAmount, uint256 _fee) internal { + if (_fee > 0) { + _registerFee(_token, _fee); + } - _registerDonation(_token, _recipient, donationAmount); - _registerFee(_token, feeAmount); + if (minFee > 0) { + if ((_fee * HUNDRED) / _totalDonationAmount < minFee) + revert FeeTooLow(); } } @@ -103,24 +170,24 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall { /// @param _token Address of the token /// @param _recipient Address of the recipient /// @param _amount Amount of tokens - function _registerDonation(address _token, address _recipient, uint256 _amount) internal { + function _registerDonation( + address _token, + address _recipient, + uint256 _amount + ) internal { balances[_recipient][_token] += _amount; emit DonationRegistered(_token, msg.sender, _recipient, _amount); } - /// @notice Internal function. Validates a donation by checking if token and donation recipient are whitelisted. - /// @param _token Address of the token - /// @param _recipient Address of the recipient - function _validateDonation(address _token, address _recipient) internal view { - _checkToken(_token); - _checkDonationRecipient(_recipient); - } - /// @notice Internal function. Transfers tokens from the sender to the contract. /// @param _token Address of the token /// @param _amount Amount of tokens function _transfer(address _token, uint256 _amount) internal { - IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount); + if (_token != NATIVE) { + IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount); + } else { + if (msg.value != _amount) revert InvalidAmount(); + } } /// @notice Withdraw tokens from the contract to msg.sender. @@ -140,7 +207,10 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall { /// @notice Distributes full amount of token arrays token from the contract to a recipient. /// @param _token Address array of the token /// @param _to Address of the recipient - function distribute(address[] calldata _token, address _to) external nonReentrant { + function distribute(address[] calldata _token, address _to) + external + nonReentrant + { // TODO: maybe restrict to admins _withdrawAll(_token, _to, _to); } @@ -148,10 +218,13 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall { /// @notice Distributes full amount of token arrays token from the contract to an array of recipients. /// @param _token Address array of the token /// @param _to Address array of the recipients - function distributeMany(address[] calldata _token, address[] calldata _to) external nonReentrant { + function distributeMany(address[] calldata _token, address[] calldata _to) + external + nonReentrant + { // TODO: maybe restrict to admins uint256 length = _to.length; - for (uint256 i = 0; i < length;) { + for (uint256 i = 0; i < length; ) { _withdrawAll(_token, _to[i], _to[i]); unchecked { i++; @@ -179,10 +252,14 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall { /// @param _token Address array of the token to withdraw /// @param _from Address of the spender /// @param _to Address of the recipient - function _withdrawAll(address[] memory _token, address _from, address _to) internal { + function _withdrawAll( + address[] memory _token, + address _from, + address _to + ) internal { uint256 length = _token.length; - for (uint256 i = 0; i < length;) { + for (uint256 i = 0; i < length; ) { uint256 amount = balances[_from][_token[i]]; if (amount > 0) { @@ -200,12 +277,17 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall { /// @param _from Address of the spender /// @param _to Address of the recipient /// @param _amount Amount of tokens to withdraw - function _withdraw(address _token, address _from, address _to, uint256 _amount) internal { + function _withdraw( + address _token, + address _from, + address _to, + uint256 _amount + ) internal { if (_amount > balances[_from][_token]) revert InsufficientBalance(); balances[_from][_token] -= _amount; if (_token == NATIVE) { - (bool success,) = payable(_to).call{value: _amount}(""); + (bool success, ) = payable(_to).call{value: _amount}(""); if (!success) revert TransferFailed(); } else { IERC20(_token).safeTransfer(_to, _amount); @@ -218,7 +300,11 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall { /// @param _token Address of the token /// @param _user Address of the user /// @return Token balance of the user - function balanceOf(address _token, address _user) external view returns (uint256) { + function balanceOf(address _token, address _user) + external + view + returns (uint256) + { return balances[_user][_token]; } @@ -226,11 +312,15 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall { /// @param _token Address array of the token /// @param _user Address of the user /// @return Uint256 array. Token balances of the user - function balancesOf(address[] calldata _token, address _user) external view returns (uint256[] memory) { + function balancesOf(address[] calldata _token, address _user) + external + view + returns (uint256[] memory) + { uint256 length = _token.length; uint256[] memory result = new uint256[](length); - for (uint256 i = 0; i < length;) { + for (uint256 i = 0; i < length; ) { result[i] = balances[_user][_token[i]]; unchecked { i++; @@ -267,21 +357,35 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall { /// @param token The token address /// @param from The address of the sender /// @param amount The amount of tokens - event FeeRegistered(address indexed token, address indexed from, uint256 amount); + event FeeRegistered( + address indexed token, + address indexed from, + uint256 amount + ); /// @notice Emitted when a donation is registered /// @param token The token address /// @param from The address of the sender /// @param recipient The address of the recipient /// @param amount The amount of tokens - event DonationRegistered(address indexed token, address indexed from, address indexed recipient, uint256 amount); + event DonationRegistered( + address indexed token, + address indexed from, + address indexed recipient, + uint256 amount + ); /// @notice Emitted when a withdrawal is made /// @param token The token address /// @param from The address of the sender /// @param to The address of the recipient /// @param amount The amount of tokens - event Withdraw(address indexed token, address indexed from, address indexed to, uint256 amount); + event Withdraw( + address indexed token, + address indexed from, + address indexed to, + uint256 amount + ); /// @notice Emitted when the minimum fee is set /// @param minFee The minimum fee diff --git a/test/DonationHandler.t.sol b/test/DonationHandler.t.sol index 244e91c..8ee532a 100644 --- a/test/DonationHandler.t.sol +++ b/test/DonationHandler.t.sol @@ -24,10 +24,10 @@ contract DonationHandlerTest is SharedInitialization { function _donate() internal { allowedToken.approve(address(donationHandler), 100); - donationHandler.donate(address(allowedToken), address(1), 100, 1e17); // 10% fee + donationHandler.donate(address(allowedToken), address(1), 90, 10); allowedToken2.approve(address(donationHandler), 100); - donationHandler.donate(address(allowedToken2), address(1), 100, 1e17); // 10% fee + donationHandler.donate(address(allowedToken2), address(1), 90, 10); } function test_donate() public { @@ -41,22 +41,10 @@ contract DonationHandlerTest is SharedInitialization { assertEq(balances[1], 90); } - function test_donateHundred() public { - allowedToken.approve(address(donationHandler), 100); - donationHandler.donate(address(allowedToken), address(1), 100, 1e18); - assertEq(donationHandler.balanceOf(address(allowedToken), address(1)), 0); - assertEq(donationHandler.balanceOf(address(allowedToken), address(donationHandler)), 100); - } - - function testFail_donateTooHigh() public { - allowedToken.approve(address(donationHandler), 100); - donationHandler.donate(address(allowedToken), address(1), 100, 1.1e18); // 110% fee - } - function testFail_donateTooLow() public { donationHandler.setMinFee(1e17); // min fee: 10% allowedToken.approve(address(donationHandler), 100); - donationHandler.donate(address(allowedToken), address(1), 100, 1e16); // 1% fee + donationHandler.donate(address(allowedToken), address(1), 100, 1); // 1% fee } function testFail_donateToWrongRecipient() public { @@ -70,11 +58,36 @@ contract DonationHandlerTest is SharedInitialization { } function test_donateEth() public { - donationHandler.donate{value: 100}(NATIVE, address(1), 100, 1e17); // 10% fee + donationHandler.donate{value: 100}(NATIVE, address(1), 90, 10); assertEq(donationHandler.balanceOf(NATIVE, address(1)), 90); assertEq(donationHandler.balanceOf(NATIVE, address(donationHandler)), 10); } + function test_donateMany() public { + DonationHandler.RecipientInfo[] memory receiptsToken1 = new DonationHandler.RecipientInfo[](2); + receiptsToken1[0] = DonationHandler.RecipientInfo(address(1), 90); + receiptsToken1[1] = DonationHandler.RecipientInfo(address(1), 90); + + DonationHandler.RecipientInfo[] memory receiptsToken2 = new DonationHandler.RecipientInfo[](2); + receiptsToken2[0] = DonationHandler.RecipientInfo(address(1), 90); + receiptsToken2[1] = DonationHandler.RecipientInfo(address(1), 90); + + DonationHandler.Donation[] memory donations = new DonationHandler.Donation[](2); + donations[0] = DonationHandler.Donation(address(allowedToken), 20, receiptsToken1); + donations[1] = DonationHandler.Donation(address(allowedToken2), 20, receiptsToken2); + + allowedToken.approve(address(donationHandler), 200); + allowedToken2.approve(address(donationHandler), 200); + + donationHandler.donateMany(donations); + + assertEq(donationHandler.balanceOf(address(allowedToken), address(1)), 180); + assertEq(donationHandler.balanceOf(address(allowedToken), address(donationHandler)), 20); + + assertEq(donationHandler.balanceOf(address(allowedToken2), address(1)), 180); + assertEq(donationHandler.balanceOf(address(allowedToken2), address(donationHandler)), 20); + } + // withdraw function test_withdraw() public { @@ -135,7 +148,7 @@ contract DonationHandlerTest is SharedInitialization { } function test_WithdrawEth() public { - donationHandler.donate{value: 100}(NATIVE, address(1), 100, 1e17); // 10% fee + donationHandler.donate{value: 100}(NATIVE, address(1), 90, 10); assertEq(donationHandler.balanceOf(NATIVE, address(1)), 90); assertEq(donationHandler.balanceOf(NATIVE, address(donationHandler)), 10); diff --git a/test/DonationHandlerMulticall.t.sol b/test/DonationHandlerMulticall.t.sol index d66d9a0..815025b 100644 --- a/test/DonationHandlerMulticall.t.sol +++ b/test/DonationHandlerMulticall.t.sol @@ -35,8 +35,8 @@ contract DonationHandlerMulticallTest is SharedInitialization { bytes[] memory data = new bytes[](2); - data[0] = abi.encodeWithSelector(donationHandler.donate.selector, address(allowedToken), address(1), 100, 1e17); - data[1] = abi.encodeWithSelector(donationHandler.donate.selector, address(allowedToken2), address(1), 200, 9e17); + data[0] = abi.encodeWithSelector(donationHandler.donate.selector, address(allowedToken), address(1), 90, 10); + data[1] = abi.encodeWithSelector(donationHandler.donate.selector, address(allowedToken2), address(1), 20, 180); vm.expectEmit(true, true, true, true, address(donationHandler)); emit DonationRegistered(address(allowedToken), address(this), address(1), 90); From bfe00c5d1fb74a8487d2f5a38689c67b177c0e6a Mon Sep 17 00:00:00 2001 From: Kurt Date: Tue, 1 Nov 2022 19:07:48 +0100 Subject: [PATCH 2/8] description fix --- src/DonationHandler.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DonationHandler.sol b/src/DonationHandler.sol index 0a575ed..e5ce49e 100644 --- a/src/DonationHandler.sol +++ b/src/DonationHandler.sol @@ -15,7 +15,7 @@ import "./DonationHandlerRoles.sol"; /// The user can donate whitelisted token to whitelisted recipients by calling the donate function. /// A donation fee can be set by the user. The fee is paid in addition to the donation amount. /// The donation fee is the amount the donor pays to the fee receiver (protocol) -/// The donation fee can be set by the user and is limited by the minFee and maxFee. +/// The donation fee can be set by the user and is limited by the minFee. /// The min fee is set by default to 0 and can be changed by the protocol admins. /// The max fee is set by default to 1e18 and can't be changed. /// From 73c1e2ab2ba25fe7dbff250d547049f0f7285bf7 Mon Sep 17 00:00:00 2001 From: Kurt Date: Tue, 1 Nov 2022 19:41:43 +0100 Subject: [PATCH 3/8] added struct comments --- src/DonationHandler.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/DonationHandler.sol b/src/DonationHandler.sol index e5ce49e..fc1f1d8 100644 --- a/src/DonationHandler.sol +++ b/src/DonationHandler.sol @@ -42,11 +42,13 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall { /// @notice mapping: user => token => amount mapping(address => mapping(address => uint256)) public balances; + /// @notice struct stores recipient and amount of a donation struct RecipientInfo { address recipient; uint256 amount; } + /// @notice struct stores the informations of a donation with multiple receipients struct Donation { address token; uint256 fee; From f5a8380b2bab064b4b0ada8b4ef24b74eb5a24b0 Mon Sep 17 00:00:00 2001 From: Kurt Date: Tue, 1 Nov 2022 20:01:31 +0100 Subject: [PATCH 4/8] forge fmt --- src/DonationHandler.sol | 108 +++++++++---------------------------- test/DonationHandler.t.sol | 2 +- 2 files changed, 27 insertions(+), 83 deletions(-) diff --git a/src/DonationHandler.sol b/src/DonationHandler.sol index fc1f1d8..96594a7 100644 --- a/src/DonationHandler.sol +++ b/src/DonationHandler.sol @@ -2,8 +2,10 @@ pragma solidity 0.8.17; import {IERC20Upgradeable as IERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; -import {SafeERC20Upgradeable as SafeERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; -import {ReentrancyGuardUpgradeable as ReentrancyGuard} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import {SafeERC20Upgradeable as SafeERC20} from + "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; +import {ReentrancyGuardUpgradeable as ReentrancyGuard} from + "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; import {MulticallUpgradeable as Multicall} from "@openzeppelin/contracts-upgradeable/utils/MulticallUpgradeable.sol"; import "./DonationHandlerRoles.sol"; @@ -66,12 +68,7 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall { address[] calldata _feeReceiver, address[] calldata _admins ) public initializer { - __DonationHandlerRoles_init( - _acceptedToken, - _donationReceiver, - _feeReceiver, - _admins - ); + __DonationHandlerRoles_init(_acceptedToken, _donationReceiver, _feeReceiver, _admins); __ReentrancyGuard_init(); __Multicall_init(); } @@ -81,12 +78,7 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall { /// @param _recipient Address of the recipient /// @param _amount Amount of tokens to donate /// @param _fee Fee to be paid to the fee receiver (protocol) - function donate( - address _token, - address _recipient, - uint256 _amount, - uint256 _fee - ) external payable nonReentrant { + function donate(address _token, address _recipient, uint256 _amount, uint256 _fee) external payable nonReentrant { if (_amount == 0) revert InvalidAmount(); uint256 totalDonationAmount = _amount + _fee; @@ -102,14 +94,10 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall { /// @notice Donate a list of donations. /// @param _donations Array of donations. Each donation contains a token, a fee and a list of recipients. Each recipient contains an address and an amount. - function donateMany(Donation[] memory _donations) - external - payable - nonReentrant - { + function donateMany(Donation[] memory _donations) external payable nonReentrant { uint256 donationLength = _donations.length; - for (uint256 i; i < donationLength; ) { + for (uint256 i; i < donationLength;) { Donation memory donation = _donations[i]; _checkToken(donation.token); @@ -117,7 +105,7 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall { uint256 totalDonationAmount = donation.fee; uint256 recipientLength = donation.recipients.length; - for (uint256 j; j < recipientLength; ) { + for (uint256 j; j < recipientLength;) { RecipientInfo memory recipientInfo = donation.recipients[j]; _checkDonationRecipient(recipientInfo.recipient); @@ -125,11 +113,7 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall { if (recipientInfo.amount == 0) revert InvalidAmount(); totalDonationAmount += recipientInfo.amount; - _registerDonation( - donation.token, - recipientInfo.recipient, - recipientInfo.amount - ); + _registerDonation(donation.token, recipientInfo.recipient, recipientInfo.amount); unchecked { j++; @@ -155,8 +139,9 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall { } if (minFee > 0) { - if ((_fee * HUNDRED) / _totalDonationAmount < minFee) + if ((_fee * HUNDRED) / _totalDonationAmount < minFee) { revert FeeTooLow(); + } } } @@ -172,11 +157,7 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall { /// @param _token Address of the token /// @param _recipient Address of the recipient /// @param _amount Amount of tokens - function _registerDonation( - address _token, - address _recipient, - uint256 _amount - ) internal { + function _registerDonation(address _token, address _recipient, uint256 _amount) internal { balances[_recipient][_token] += _amount; emit DonationRegistered(_token, msg.sender, _recipient, _amount); } @@ -209,10 +190,7 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall { /// @notice Distributes full amount of token arrays token from the contract to a recipient. /// @param _token Address array of the token /// @param _to Address of the recipient - function distribute(address[] calldata _token, address _to) - external - nonReentrant - { + function distribute(address[] calldata _token, address _to) external nonReentrant { // TODO: maybe restrict to admins _withdrawAll(_token, _to, _to); } @@ -220,13 +198,10 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall { /// @notice Distributes full amount of token arrays token from the contract to an array of recipients. /// @param _token Address array of the token /// @param _to Address array of the recipients - function distributeMany(address[] calldata _token, address[] calldata _to) - external - nonReentrant - { + function distributeMany(address[] calldata _token, address[] calldata _to) external nonReentrant { // TODO: maybe restrict to admins uint256 length = _to.length; - for (uint256 i = 0; i < length; ) { + for (uint256 i = 0; i < length;) { _withdrawAll(_token, _to[i], _to[i]); unchecked { i++; @@ -254,14 +229,10 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall { /// @param _token Address array of the token to withdraw /// @param _from Address of the spender /// @param _to Address of the recipient - function _withdrawAll( - address[] memory _token, - address _from, - address _to - ) internal { + function _withdrawAll(address[] memory _token, address _from, address _to) internal { uint256 length = _token.length; - for (uint256 i = 0; i < length; ) { + for (uint256 i = 0; i < length;) { uint256 amount = balances[_from][_token[i]]; if (amount > 0) { @@ -279,17 +250,12 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall { /// @param _from Address of the spender /// @param _to Address of the recipient /// @param _amount Amount of tokens to withdraw - function _withdraw( - address _token, - address _from, - address _to, - uint256 _amount - ) internal { + function _withdraw(address _token, address _from, address _to, uint256 _amount) internal { if (_amount > balances[_from][_token]) revert InsufficientBalance(); balances[_from][_token] -= _amount; if (_token == NATIVE) { - (bool success, ) = payable(_to).call{value: _amount}(""); + (bool success,) = payable(_to).call{value: _amount}(""); if (!success) revert TransferFailed(); } else { IERC20(_token).safeTransfer(_to, _amount); @@ -302,11 +268,7 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall { /// @param _token Address of the token /// @param _user Address of the user /// @return Token balance of the user - function balanceOf(address _token, address _user) - external - view - returns (uint256) - { + function balanceOf(address _token, address _user) external view returns (uint256) { return balances[_user][_token]; } @@ -314,15 +276,11 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall { /// @param _token Address array of the token /// @param _user Address of the user /// @return Uint256 array. Token balances of the user - function balancesOf(address[] calldata _token, address _user) - external - view - returns (uint256[] memory) - { + function balancesOf(address[] calldata _token, address _user) external view returns (uint256[] memory) { uint256 length = _token.length; uint256[] memory result = new uint256[](length); - for (uint256 i = 0; i < length; ) { + for (uint256 i = 0; i < length;) { result[i] = balances[_user][_token[i]]; unchecked { i++; @@ -359,35 +317,21 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall { /// @param token The token address /// @param from The address of the sender /// @param amount The amount of tokens - event FeeRegistered( - address indexed token, - address indexed from, - uint256 amount - ); + event FeeRegistered(address indexed token, address indexed from, uint256 amount); /// @notice Emitted when a donation is registered /// @param token The token address /// @param from The address of the sender /// @param recipient The address of the recipient /// @param amount The amount of tokens - event DonationRegistered( - address indexed token, - address indexed from, - address indexed recipient, - uint256 amount - ); + event DonationRegistered(address indexed token, address indexed from, address indexed recipient, uint256 amount); /// @notice Emitted when a withdrawal is made /// @param token The token address /// @param from The address of the sender /// @param to The address of the recipient /// @param amount The amount of tokens - event Withdraw( - address indexed token, - address indexed from, - address indexed to, - uint256 amount - ); + event Withdraw(address indexed token, address indexed from, address indexed to, uint256 amount); /// @notice Emitted when the minimum fee is set /// @param minFee The minimum fee diff --git a/test/DonationHandler.t.sol b/test/DonationHandler.t.sol index 8ee532a..e22a68a 100644 --- a/test/DonationHandler.t.sol +++ b/test/DonationHandler.t.sol @@ -24,7 +24,7 @@ contract DonationHandlerTest is SharedInitialization { function _donate() internal { allowedToken.approve(address(donationHandler), 100); - donationHandler.donate(address(allowedToken), address(1), 90, 10); + donationHandler.donate(address(allowedToken), address(1), 90, 10); allowedToken2.approve(address(donationHandler), 100); donationHandler.donate(address(allowedToken2), address(1), 90, 10); From 8e2ec8e8256c5609eb5b571fd89b7daf018ef056 Mon Sep 17 00:00:00 2001 From: Kurt Date: Sun, 4 Dec 2022 17:23:11 +0100 Subject: [PATCH 5/8] gitcoin compatible donationshandler --- script/Config.sol | 17 +- script/DonationHandler.s.sol | 6 +- src/DonationHandler.sol | 240 +++++++++++------- src/DonationHandlerRoles.sol | 5 +- src/gitcoin/IVotingStrategy.sol | 52 ++++ test/DonationHandler.t.sol | 140 ++++++---- ....sol => DonationHandlerMulticall.t.sol.rm} | 0 test/shared/SharedInitialization.sol | 5 +- 8 files changed, 309 insertions(+), 156 deletions(-) create mode 100644 src/gitcoin/IVotingStrategy.sol rename test/{DonationHandlerMulticall.t.sol => DonationHandlerMulticall.t.sol.rm} (100%) diff --git a/script/Config.sol b/script/Config.sol index 72916b4..18a1f07 100644 --- a/script/Config.sol +++ b/script/Config.sol @@ -7,6 +7,7 @@ contract Config { address[] donationRecipient; address[] feeReceiver; address[] admins; + uint256 minFee; } NetworkConfig private activeNetworkConfig; @@ -34,7 +35,7 @@ contract Config { feeReceivers[0] = address(1); admins[0] = address(1); - return getConfig(acceptedTokens, donationRecipients, feeReceivers, admins); + return getConfig(acceptedTokens, donationRecipients, feeReceivers, admins, 5e16); } // ================== Gnosis Config ================== @@ -50,7 +51,7 @@ contract Config { feeReceivers[0] = address(1); admins[0] = address(1); - return getConfig(acceptedTokens, donationRecipients, feeReceivers, admins); + return getConfig(acceptedTokens, donationRecipients, feeReceivers, admins, 5e16); } // ================== Goerli Config ================== @@ -66,7 +67,7 @@ contract Config { feeReceivers[0] = address(1); admins[0] = address(1); - return getConfig(acceptedTokens, donationRecipients, feeReceivers, admins); + return getConfig(acceptedTokens, donationRecipients, feeReceivers, admins, 5e16); } // ================== Helper ================== @@ -79,7 +80,8 @@ contract Config { address[] memory _acceptedToken, address[] memory _donationRecipient, address[] memory _feeReceiver, - address[] memory _admins + address[] memory _admins, + uint256 _minFee ) internal pure returns (NetworkConfig memory) { uint256 tLength = _acceptedToken.length; uint256 dLength = _donationRecipient.length; @@ -90,7 +92,8 @@ contract Config { acceptedToken: new address[](tLength), donationRecipient: new address[](dLength), feeReceiver: new address[](fLength), - admins: new address[](aLength) + admins: new address[](aLength), + minFee: _minFee }); for (uint256 i = 0; i < tLength; i++) { @@ -127,4 +130,8 @@ contract Config { function getAdmins() public view returns (address[] memory) { return activeNetworkConfig.admins; } + + function getMinFee() public view returns (uint256) { + return activeNetworkConfig.minFee; + } } diff --git a/script/DonationHandler.s.sol b/script/DonationHandler.s.sol index 39c1afe..cddda73 100644 --- a/script/DonationHandler.s.sol +++ b/script/DonationHandler.s.sol @@ -23,7 +23,11 @@ contract DeployDonationHandler is Script, Config { ); DonationHandler(address(proxy)).initialize( - config.getAcceptedTokens(), config.getDonationRecipients(), config.getFeeReceivers(), config.getAdmins() + config.getAcceptedTokens(), + config.getDonationRecipients(), + config.getFeeReceivers(), + config.getAdmins(), + config.getMinFee() ); vm.stopBroadcast(); diff --git a/src/DonationHandler.sol b/src/DonationHandler.sol index 96594a7..bde44af 100644 --- a/src/DonationHandler.sol +++ b/src/DonationHandler.sol @@ -2,12 +2,11 @@ pragma solidity 0.8.17; import {IERC20Upgradeable as IERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; -import {SafeERC20Upgradeable as SafeERC20} from - "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; -import {ReentrancyGuardUpgradeable as ReentrancyGuard} from - "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import {SafeERC20Upgradeable as SafeERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; +import {ReentrancyGuardUpgradeable as ReentrancyGuard} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; import {MulticallUpgradeable as Multicall} from "@openzeppelin/contracts-upgradeable/utils/MulticallUpgradeable.sol"; import "./DonationHandlerRoles.sol"; +import "./gitcoin/IVotingStrategy.sol"; /// @title DonationHandler /// @author @Kurt for Giveth @@ -32,7 +31,12 @@ import "./DonationHandlerRoles.sol"; /// /// The donation balance of one token can be checked by calling the balanceOf function. /// The donation balance of multiple token can be checked by calling the balancesOf function. -contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall { +contract DonationHandler is + IVotingStrategy, + DonationHandlerRoles, + ReentrancyGuard, + Multicall +{ using SafeERC20 for IERC20; /// @notice 1e18 represents 100%, 1e16 represents 1% @@ -41,22 +45,12 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall { /// @notice Minimum donation fee. 0 by default uint256 public minFee; + bool public isTokenWhitelistActive; + bool public isRecipientWhitelistActive; + /// @notice mapping: user => token => amount mapping(address => mapping(address => uint256)) public balances; - /// @notice struct stores recipient and amount of a donation - struct RecipientInfo { - address recipient; - uint256 amount; - } - - /// @notice struct stores the informations of a donation with multiple receipients - struct Donation { - address token; - uint256 fee; - RecipientInfo[] recipients; - } - /// @notice Initialize the contract. /// @param _acceptedToken Array of accepted tokens /// @param _donationReceiver Array of donation receivers @@ -66,82 +60,69 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall { address[] calldata _acceptedToken, address[] calldata _donationReceiver, address[] calldata _feeReceiver, - address[] calldata _admins + address[] calldata _admins, + uint256 _minFee ) public initializer { - __DonationHandlerRoles_init(_acceptedToken, _donationReceiver, _feeReceiver, _admins); + __DonationHandlerRoles_init( + _acceptedToken, + _donationReceiver, + _feeReceiver, + _admins + ); __ReentrancyGuard_init(); __Multicall_init(); - } - - /// @notice Donate tokens to a recipient. The fee added to the donation amount. - /// @param _token Address of the token to donate - /// @param _recipient Address of the recipient - /// @param _amount Amount of tokens to donate - /// @param _fee Fee to be paid to the fee receiver (protocol) - function donate(address _token, address _recipient, uint256 _amount, uint256 _fee) external payable nonReentrant { - if (_amount == 0) revert InvalidAmount(); - - uint256 totalDonationAmount = _amount + _fee; - _checkToken(_token); - _checkDonationRecipient(_recipient); - - _registerDonation(_token, _recipient, _amount); - _handleFee(_token, totalDonationAmount, _fee); + if (_acceptedToken.length > 0) { + isTokenWhitelistActive = true; + } + if (_donationReceiver.length > 0) { + isRecipientWhitelistActive = true; + } - _transfer(_token, totalDonationAmount); + if(_minFee > 0) { + _setMinFee(_minFee); + } } - /// @notice Donate a list of donations. - /// @param _donations Array of donations. Each donation contains a token, a fee and a list of recipients. Each recipient contains an address and an amount. - function donateMany(Donation[] memory _donations) external payable nonReentrant { - uint256 donationLength = _donations.length; - - for (uint256 i; i < donationLength;) { - Donation memory donation = _donations[i]; - - _checkToken(donation.token); - - uint256 totalDonationAmount = donation.fee; - uint256 recipientLength = donation.recipients.length; - - for (uint256 j; j < recipientLength;) { - RecipientInfo memory recipientInfo = donation.recipients[j]; - - _checkDonationRecipient(recipientInfo.recipient); + function vote( + bytes[] calldata encodedVotes, + address voterAddress + ) external payable override nonReentrant isRoundContract { + /// @dev iterate over multiple donations and transfer funds + uint256 length = encodedVotes.length; + uint256 msgValue = 0; + + for (uint256 i = 0; i < length; ) { + (address _token, uint256 _amount, address _grantAddress) = abi + .decode(encodedVotes[i], (address, uint256, address)); + + if (isTokenWhitelistActive) _checkToken(_token); + if (isRecipientWhitelistActive) { + _checkDonationRecipient(_grantAddress); + } - if (recipientInfo.amount == 0) revert InvalidAmount(); - totalDonationAmount += recipientInfo.amount; + if (_token != NATIVE) { + _transfer(voterAddress, _token, _amount); + } else { + msgValue += _amount; + } - _registerDonation(donation.token, recipientInfo.recipient, recipientInfo.amount); + if (minFee > 0) { + uint256 fee = (_amount * minFee) / HUNDRED; - unchecked { - j++; - } + _registerDonation(_token, _grantAddress, _amount - fee); + _registerFee(_token, fee); + } else { + _registerDonation(_token, _grantAddress, _amount); } - _handleFee(donation.token, totalDonationAmount, donation.fee); - _transfer(donation.token, totalDonationAmount); - unchecked { i++; } } - } - - /// @notice registers the fee (if fee > 0) and checks if the fee amount is valid (only if the minFee is > 0) - /// @param _token Address of the token - /// @param _totalDonationAmount Total donation amount - /// @param _fee Fee to be paid to the fee receiver (protocol) - function _handleFee(address _token, uint256 _totalDonationAmount, uint256 _fee) internal { - if (_fee > 0) { - _registerFee(_token, _fee); - } - if (minFee > 0) { - if ((_fee * HUNDRED) / _totalDonationAmount < minFee) { - revert FeeTooLow(); - } + if (msgValue > msg.value) { + revert InvalidAmount(); } } @@ -157,7 +138,11 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall { /// @param _token Address of the token /// @param _recipient Address of the recipient /// @param _amount Amount of tokens - function _registerDonation(address _token, address _recipient, uint256 _amount) internal { + function _registerDonation( + address _token, + address _recipient, + uint256 _amount + ) internal { balances[_recipient][_token] += _amount; emit DonationRegistered(_token, msg.sender, _recipient, _amount); } @@ -165,12 +150,12 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall { /// @notice Internal function. Transfers tokens from the sender to the contract. /// @param _token Address of the token /// @param _amount Amount of tokens - function _transfer(address _token, uint256 _amount) internal { - if (_token != NATIVE) { - IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount); - } else { - if (msg.value != _amount) revert InvalidAmount(); - } + function _transfer( + address _from, + address _token, + uint256 _amount + ) internal { + IERC20(_token).safeTransferFrom(_from, address(this), _amount); } /// @notice Withdraw tokens from the contract to msg.sender. @@ -190,7 +175,10 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall { /// @notice Distributes full amount of token arrays token from the contract to a recipient. /// @param _token Address array of the token /// @param _to Address of the recipient - function distribute(address[] calldata _token, address _to) external nonReentrant { + function distribute( + address[] calldata _token, + address _to + ) external nonReentrant { // TODO: maybe restrict to admins _withdrawAll(_token, _to, _to); } @@ -198,10 +186,13 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall { /// @notice Distributes full amount of token arrays token from the contract to an array of recipients. /// @param _token Address array of the token /// @param _to Address array of the recipients - function distributeMany(address[] calldata _token, address[] calldata _to) external nonReentrant { + function distributeMany( + address[] calldata _token, + address[] calldata _to + ) external nonReentrant { // TODO: maybe restrict to admins uint256 length = _to.length; - for (uint256 i = 0; i < length;) { + for (uint256 i = 0; i < length; ) { _withdrawAll(_token, _to[i], _to[i]); unchecked { i++; @@ -229,10 +220,14 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall { /// @param _token Address array of the token to withdraw /// @param _from Address of the spender /// @param _to Address of the recipient - function _withdrawAll(address[] memory _token, address _from, address _to) internal { + function _withdrawAll( + address[] memory _token, + address _from, + address _to + ) internal { uint256 length = _token.length; - for (uint256 i = 0; i < length;) { + for (uint256 i = 0; i < length; ) { uint256 amount = balances[_from][_token[i]]; if (amount > 0) { @@ -250,12 +245,17 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall { /// @param _from Address of the spender /// @param _to Address of the recipient /// @param _amount Amount of tokens to withdraw - function _withdraw(address _token, address _from, address _to, uint256 _amount) internal { + function _withdraw( + address _token, + address _from, + address _to, + uint256 _amount + ) internal { if (_amount > balances[_from][_token]) revert InsufficientBalance(); balances[_from][_token] -= _amount; if (_token == NATIVE) { - (bool success,) = payable(_to).call{value: _amount}(""); + (bool success, ) = payable(_to).call{value: _amount}(""); if (!success) revert TransferFailed(); } else { IERC20(_token).safeTransfer(_to, _amount); @@ -268,7 +268,10 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall { /// @param _token Address of the token /// @param _user Address of the user /// @return Token balance of the user - function balanceOf(address _token, address _user) external view returns (uint256) { + function balanceOf( + address _token, + address _user + ) external view returns (uint256) { return balances[_user][_token]; } @@ -276,11 +279,14 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall { /// @param _token Address array of the token /// @param _user Address of the user /// @return Uint256 array. Token balances of the user - function balancesOf(address[] calldata _token, address _user) external view returns (uint256[] memory) { + function balancesOf( + address[] calldata _token, + address _user + ) external view returns (uint256[] memory) { uint256 length = _token.length; uint256[] memory result = new uint256[](length); - for (uint256 i = 0; i < length;) { + for (uint256 i = 0; i < length; ) { result[i] = balances[_user][_token[i]]; unchecked { i++; @@ -293,11 +299,29 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall { /// @param _minFee Minimum donation fee function setMinFee(uint256 _minFee) external { _checkAdmin(msg.sender); + _setMinFee(_minFee); + } + + function _setMinFee(uint256 _minFee) internal { if (_minFee > HUNDRED) revert FeeTooHigh(); minFee = _minFee; emit MinFeeSet(_minFee); } + function setIsTokenWhitelistActive(bool _isTokenWhitelistActive) external { + _checkAdmin(msg.sender); + isTokenWhitelistActive = _isTokenWhitelistActive; + emit IsTokenWhitelistActiveSet(_isTokenWhitelistActive); + } + + function setIsRecipientWhitelistActive( + bool _isRecipientWhitelistActive + ) external { + _checkAdmin(msg.sender); + isRecipientWhitelistActive = _isRecipientWhitelistActive; + emit IsRecipientWhitelistActiveSet(_isRecipientWhitelistActive); + } + /// @notice Throws if passed fee is above 100%. error FeeTooHigh(); @@ -317,23 +341,45 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall { /// @param token The token address /// @param from The address of the sender /// @param amount The amount of tokens - event FeeRegistered(address indexed token, address indexed from, uint256 amount); + event FeeRegistered( + address indexed token, + address indexed from, + uint256 amount + ); /// @notice Emitted when a donation is registered /// @param token The token address /// @param from The address of the sender /// @param recipient The address of the recipient /// @param amount The amount of tokens - event DonationRegistered(address indexed token, address indexed from, address indexed recipient, uint256 amount); + event DonationRegistered( + address indexed token, + address indexed from, + address indexed recipient, + uint256 amount + ); /// @notice Emitted when a withdrawal is made /// @param token The token address /// @param from The address of the sender /// @param to The address of the recipient /// @param amount The amount of tokens - event Withdraw(address indexed token, address indexed from, address indexed to, uint256 amount); + event Withdraw( + address indexed token, + address indexed from, + address indexed to, + uint256 amount + ); /// @notice Emitted when the minimum fee is set /// @param minFee The minimum fee event MinFeeSet(uint256 minFee); + + /// @notice Emitted when the token whitelist is set to active or inactive + /// @param isTokenWhitelistActive The token whitelist status + event IsTokenWhitelistActiveSet(bool isTokenWhitelistActive); + + /// @notice Emitted when the recipient whitelist is set to active or inactive + /// @param isRecipientWhitelistActive The recipient whitelist status + event IsRecipientWhitelistActiveSet(bool isRecipientWhitelistActive); } diff --git a/src/DonationHandlerRoles.sol b/src/DonationHandlerRoles.sol index a0d26c5..58712af 100644 --- a/src/DonationHandlerRoles.sol +++ b/src/DonationHandlerRoles.sol @@ -13,8 +13,8 @@ contract DonationHandlerRoles is AccessControl { bytes32 public constant FEE_RECEIVER = keccak256("FEE_RECEIVER"); bytes32 public constant ADMIN = keccak256("ADMIN"); - /// @notice special address which represents the native network currency - address public constant NATIVE = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + /// @notice special address which represents the native network currency. address(0) is used by gitcoin. + address public constant NATIVE = address(0); //0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; /// @notice Initializes the contract settings by adding all addresses to their roles. /// @param _acceptedToken The list of accepted tokens. @@ -29,6 +29,7 @@ contract DonationHandlerRoles is AccessControl { ) internal onlyInitializing { __AccessControl_init(); _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); + _setupRole(DONATION_RECIPIENT, address(this)); // this contract is a donation recipient (for fees) by default _setRoleAdmin(ACCEPTED_TOKEN, ADMIN); _setRoleAdmin(DONATION_RECIPIENT, ADMIN); diff --git a/src/gitcoin/IVotingStrategy.sol b/src/gitcoin/IVotingStrategy.sol new file mode 100644 index 0000000..e40a0e2 --- /dev/null +++ b/src/gitcoin/IVotingStrategy.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.17; + +/** + * @notice Defines the abstract contract for voting algorithms on grants + * within a round. Any new voting algorithm would be expected to + * extend this abstract contract. + * Every IVotingStrategy contract would be unique to RoundImplementation + * and would be deployed before creating a round + */ +abstract contract IVotingStrategy { + // --- Data --- + + /// @notice Round address + address public roundAddress; + + // --- Modifier --- + + /// @notice modifier to check if sender is round contract. + modifier isRoundContract() { + require(roundAddress != address(0), "error: voting contract not linked to a round"); + require(msg.sender == roundAddress, "error: can be invoked only by round contract"); + _; + } + + // --- Core methods --- + + /** + * @notice Invoked by RoundImplementation on creation to + * set the round for which the voting contracts is to be used + * + */ + function init() external { + require(roundAddress == address(0), "init: roundAddress already set"); + roundAddress = msg.sender; + } + + /** + * @notice Invoked by RoundImplementation to allow voter to case + * vote for grants during a round. + * + * @dev + * - allows contributor to do cast multiple votes which could be weighted. + * - should be invoked by RoundImplementation contract + * - ideally IVotingStrategy implementation should emit events after a vote is cast + * - this would be triggered when a voter casts their vote via grant explorer + * + * @param _encodedVotes encoded votes + * @param _voterAddress voter address + */ + function vote(bytes[] calldata _encodedVotes, address _voterAddress) external payable virtual; +} diff --git a/test/DonationHandler.t.sol b/test/DonationHandler.t.sol index e22a68a..e9b61b9 100644 --- a/test/DonationHandler.t.sol +++ b/test/DonationHandler.t.sol @@ -12,80 +12,103 @@ contract DonationHandlerTest is SharedInitialization { // Donate - function testFail_donateWithoutApproval() public { - donationHandler.donate(address(allowedToken), address(1), 100, 0); + function _encode( + address token, + uint256 amount, + address recipient + ) internal pure returns (bytes memory) { + return abi.encode(token, amount, recipient); } - function test_donateWithoutFee() public { - allowedToken.approve(address(donationHandler), 100); - donationHandler.donate(address(allowedToken), address(1), 100, 0); - assertEq(donationHandler.balanceOf(address(allowedToken), address(1)), 100); + function testFail_donateWithoutApproval() public { + bytes[] memory donation = new bytes[](1); + donation[0] = _encode(address(allowedToken), 100, address(1)); + donationHandler.vote(donation, deployer); } function _donate() internal { - allowedToken.approve(address(donationHandler), 100); - donationHandler.donate(address(allowedToken), address(1), 90, 10); + bytes[] memory donation = new bytes[](2); + + donation[0] = _encode(address(allowedToken), 100, address(1)); + donation[1] = _encode(address(allowedToken2), 100, address(1)); + allowedToken.approve(address(donationHandler), 100); allowedToken2.approve(address(donationHandler), 100); - donationHandler.donate(address(allowedToken2), address(1), 90, 10); + + donationHandler.vote(donation, deployer); } function test_donate() public { _donate(); - assertEq(donationHandler.balanceOf(address(allowedToken), address(1)), 90); - assertEq(donationHandler.balanceOf(address(allowedToken), address(donationHandler)), 10); - - uint256[] memory balances = donationHandler.balancesOf(acceptedToken, address(1)); + assertEq( + donationHandler.balanceOf(address(allowedToken), address(1)), + 90 + ); + + uint256[] memory balances = donationHandler.balancesOf( + acceptedToken, + address(1) + ); assertEq(balances.length, 2); assertEq(balances[0], 90); assertEq(balances[1], 90); } - function testFail_donateTooLow() public { - donationHandler.setMinFee(1e17); // min fee: 10% - allowedToken.approve(address(donationHandler), 100); - donationHandler.donate(address(allowedToken), address(1), 100, 1); // 1% fee - } - function testFail_donateToWrongRecipient() public { allowedToken.approve(address(donationHandler), 100); - donationHandler.donate(address(allowedToken), address(2), 100, 0); + bytes[] memory donation = new bytes[](1); + donation[0] = _encode(address(allowedToken), 100, address(2)); + donationHandler.vote(donation, deployer); } function testFail_donateWithWrongToken() public { notAllowedToken.approve(address(donationHandler), 100); - donationHandler.donate(address(notAllowedToken), address(1), 100, 0); + bytes[] memory donation = new bytes[](1); + donation[0] = _encode(address(notAllowedToken), 100, address(1)); + donationHandler.vote(donation, deployer); } function test_donateEth() public { - donationHandler.donate{value: 100}(NATIVE, address(1), 90, 10); + bytes[] memory donation = new bytes[](1); + donation[0] = _encode(NATIVE, 100, address(1)); + donationHandler.vote{value: 100}(donation, deployer); assertEq(donationHandler.balanceOf(NATIVE, address(1)), 90); - assertEq(donationHandler.balanceOf(NATIVE, address(donationHandler)), 10); } function test_donateMany() public { - DonationHandler.RecipientInfo[] memory receiptsToken1 = new DonationHandler.RecipientInfo[](2); - receiptsToken1[0] = DonationHandler.RecipientInfo(address(1), 90); - receiptsToken1[1] = DonationHandler.RecipientInfo(address(1), 90); + bytes[] memory donation = new bytes[](2); - DonationHandler.RecipientInfo[] memory receiptsToken2 = new DonationHandler.RecipientInfo[](2); - receiptsToken2[0] = DonationHandler.RecipientInfo(address(1), 90); - receiptsToken2[1] = DonationHandler.RecipientInfo(address(1), 90); - - DonationHandler.Donation[] memory donations = new DonationHandler.Donation[](2); - donations[0] = DonationHandler.Donation(address(allowedToken), 20, receiptsToken1); - donations[1] = DonationHandler.Donation(address(allowedToken2), 20, receiptsToken2); + donation[0] = _encode(address(allowedToken), 200, address(1)); + donation[1] = _encode(address(allowedToken2), 200, address(1)); allowedToken.approve(address(donationHandler), 200); allowedToken2.approve(address(donationHandler), 200); - donationHandler.donateMany(donations); - - assertEq(donationHandler.balanceOf(address(allowedToken), address(1)), 180); - assertEq(donationHandler.balanceOf(address(allowedToken), address(donationHandler)), 20); - - assertEq(donationHandler.balanceOf(address(allowedToken2), address(1)), 180); - assertEq(donationHandler.balanceOf(address(allowedToken2), address(donationHandler)), 20); + donationHandler.vote(donation, deployer); + + assertEq( + donationHandler.balanceOf(address(allowedToken), address(1)), + 180 + ); + assertEq( + donationHandler.balanceOf( + address(allowedToken), + address(donationHandler) + ), + 20 + ); + + assertEq( + donationHandler.balanceOf(address(allowedToken2), address(1)), + 180 + ); + assertEq( + donationHandler.balanceOf( + address(allowedToken2), + address(donationHandler) + ), + 20 + ); } // withdraw @@ -93,16 +116,22 @@ contract DonationHandlerTest is SharedInitialization { function test_withdraw() public { _donate(); vm.prank(address(1)); - donationHandler.withdraw(address(allowedToken), 80); - assertEq(donationHandler.balanceOf(address(allowedToken), address(1)), 10); - assertEq(allowedToken.balanceOf(address(1)), 80); + donationHandler.withdraw(address(allowedToken), 90); + assertEq( + donationHandler.balanceOf(address(allowedToken), address(1)), + 0 + ); + assertEq(allowedToken.balanceOf(address(1)), 90); } function test_withdrawMany() public { _donate(); vm.prank(address(1)); donationHandler.withdrawMany(acceptedToken); - assertEq(donationHandler.balanceOf(address(allowedToken), address(1)), 0); + assertEq( + donationHandler.balanceOf(address(allowedToken), address(1)), + 0 + ); assertEq(allowedToken.balanceOf(address(1)), 90); assertEq(allowedToken2.balanceOf(address(1)), 90); } @@ -116,13 +145,13 @@ contract DonationHandlerTest is SharedInitialization { function testFail_withdrawWrongToken() public { _donate(); vm.prank(address(1)); - donationHandler.withdraw(address(notAllowedToken), 80); + donationHandler.withdraw(address(notAllowedToken), 90); } function testFail_withdrawWrongRecipient() public { _donate(); vm.prank(address(2)); - donationHandler.withdraw(address(allowedToken), 80); + donationHandler.withdraw(address(allowedToken), 90); } function testFail_withdrawFeeNotAdmin() public { @@ -135,7 +164,13 @@ contract DonationHandlerTest is SharedInitialization { _donate(); vm.prank(address(2)); donationHandler.withdrawFee(address(allowedToken)); - assertEq(donationHandler.balanceOf(address(allowedToken), address(donationHandler)), 0); + assertEq( + donationHandler.balanceOf( + address(allowedToken), + address(donationHandler) + ), + 0 + ); assertEq(allowedToken.balanceOf(address(2)), 10); } @@ -148,9 +183,16 @@ contract DonationHandlerTest is SharedInitialization { } function test_WithdrawEth() public { - donationHandler.donate{value: 100}(NATIVE, address(1), 90, 10); + bytes[] memory donation = new bytes[](1); + donation[0] = _encode(NATIVE, 100, address(1)); + + donationHandler.vote{value: 100}(donation, deployer); + assertEq(donationHandler.balanceOf(NATIVE, address(1)), 90); - assertEq(donationHandler.balanceOf(NATIVE, address(donationHandler)), 10); + assertEq( + donationHandler.balanceOf(NATIVE, address(donationHandler)), + 10 + ); vm.prank(address(1)); uint256 balanceBefore = address(1).balance; diff --git a/test/DonationHandlerMulticall.t.sol b/test/DonationHandlerMulticall.t.sol.rm similarity index 100% rename from test/DonationHandlerMulticall.t.sol rename to test/DonationHandlerMulticall.t.sol.rm diff --git a/test/shared/SharedInitialization.sol b/test/shared/SharedInitialization.sol index 0726c46..89cb633 100644 --- a/test/shared/SharedInitialization.sol +++ b/test/shared/SharedInitialization.sol @@ -7,7 +7,7 @@ import "../mocks/MockERC20.sol"; contract SharedInitialization is Test { address deployer = 0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84; - address public constant NATIVE = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + address public constant NATIVE = address(0); DonationHandler public donationHandler; MockERC20 public allowedToken; @@ -39,7 +39,8 @@ contract SharedInitialization is Test { } function _initializeDonationHandler() internal { - donationHandler.initialize(acceptedToken, donationRecipient, feeReceiver, admins); + donationHandler.init(); + donationHandler.initialize(acceptedToken, donationRecipient, feeReceiver, admins, 1e17); } // Events From da31483890c122f27615dd6dfc5e2caa452716b0 Mon Sep 17 00:00:00 2001 From: Kurt Date: Sun, 4 Dec 2022 17:24:48 +0100 Subject: [PATCH 6/8] removed multi call --- src/DonationHandler.sol | 5 +- test/DonationHandlerMulticall.t.sol.rm | 83 -------------------------- 2 files changed, 1 insertion(+), 87 deletions(-) delete mode 100644 test/DonationHandlerMulticall.t.sol.rm diff --git a/src/DonationHandler.sol b/src/DonationHandler.sol index bde44af..d7e70f0 100644 --- a/src/DonationHandler.sol +++ b/src/DonationHandler.sol @@ -4,7 +4,6 @@ pragma solidity 0.8.17; import {IERC20Upgradeable as IERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; import {SafeERC20Upgradeable as SafeERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; import {ReentrancyGuardUpgradeable as ReentrancyGuard} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; -import {MulticallUpgradeable as Multicall} from "@openzeppelin/contracts-upgradeable/utils/MulticallUpgradeable.sol"; import "./DonationHandlerRoles.sol"; import "./gitcoin/IVotingStrategy.sol"; @@ -34,8 +33,7 @@ import "./gitcoin/IVotingStrategy.sol"; contract DonationHandler is IVotingStrategy, DonationHandlerRoles, - ReentrancyGuard, - Multicall + ReentrancyGuard { using SafeERC20 for IERC20; @@ -70,7 +68,6 @@ contract DonationHandler is _admins ); __ReentrancyGuard_init(); - __Multicall_init(); if (_acceptedToken.length > 0) { isTokenWhitelistActive = true; diff --git a/test/DonationHandlerMulticall.t.sol.rm b/test/DonationHandlerMulticall.t.sol.rm deleted file mode 100644 index 815025b..0000000 --- a/test/DonationHandlerMulticall.t.sol.rm +++ /dev/null @@ -1,83 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import "./shared/SharedInitialization.sol"; - -contract DonationHandlerMulticallTest is SharedInitialization { - // acceptedToken[0] = address(allowedToken); - // acceptedToken[1] = address(allowedToken2); - // donationRecipient[0] = address(1); - // feeReceiver[0] = address(2); - // admins[0] = address(3); - - // Donate - - function testFail_donateWithoutApproval() public { - bytes[] memory data = new bytes[](1); - data[0] = abi.encodeWithSelector(donationHandler.donate.selector, address(allowedToken), address(1), 100, 0); - donationHandler.multicall(data); - } - - function testFail_donateWithoutApproval2() public { - allowedToken.approve(address(donationHandler), 100); - - bytes[] memory data = new bytes[](2); - - data[0] = abi.encodeWithSelector(donationHandler.donate.selector, address(allowedToken), address(1), 100, 1e17); - data[1] = abi.encodeWithSelector(donationHandler.donate.selector, address(allowedToken2), address(1), 100, 1e17); - - donationHandler.multicall(data); - } - - function test_donate() public { - allowedToken.approve(address(donationHandler), 100); - allowedToken2.approve(address(donationHandler), 200); - - bytes[] memory data = new bytes[](2); - - data[0] = abi.encodeWithSelector(donationHandler.donate.selector, address(allowedToken), address(1), 90, 10); - data[1] = abi.encodeWithSelector(donationHandler.donate.selector, address(allowedToken2), address(1), 20, 180); - - vm.expectEmit(true, true, true, true, address(donationHandler)); - emit DonationRegistered(address(allowedToken), address(this), address(1), 90); - - vm.expectEmit(true, true, true, true, address(donationHandler)); - emit FeeRegistered(address(allowedToken), address(this), 10); - - vm.expectEmit(true, true, true, true, address(donationHandler)); - emit DonationRegistered(address(allowedToken2), address(this), address(1), 20); - - vm.expectEmit(true, true, true, true, address(donationHandler)); - emit FeeRegistered(address(allowedToken2), address(this), 180); - - donationHandler.multicall(data); - assertEq(donationHandler.balanceOf(address(allowedToken), address(1)), 90); - assertEq(donationHandler.balanceOf(address(allowedToken), address(donationHandler)), 10); - - assertEq(donationHandler.balanceOf(address(allowedToken2), address(1)), 20); - assertEq(donationHandler.balanceOf(address(allowedToken2), address(donationHandler)), 180); - - uint256[] memory balances = donationHandler.balancesOf(acceptedToken, address(1)); - assertEq(balances.length, 2); - assertEq(balances[0], 90); - assertEq(balances[1], 20); - } - - function test_donateWithoutFee() public { - allowedToken.approve(address(donationHandler), 100); - allowedToken2.approve(address(donationHandler), 100); - - bytes[] memory data = new bytes[](2); - - data[0] = abi.encodeWithSelector(donationHandler.donate.selector, address(allowedToken), address(1), 100, 0); - data[1] = abi.encodeWithSelector(donationHandler.donate.selector, address(allowedToken2), address(1), 100, 0); - - vm.expectEmit(true, true, true, true, address(donationHandler)); - emit DonationRegistered(address(allowedToken), address(this), address(1), 100); - - vm.expectEmit(true, true, true, true, address(donationHandler)); - emit DonationRegistered(address(allowedToken2), address(this), address(1), 100); - - donationHandler.multicall(data); - } -} From db7074f2ebe244272df7766c2dde249667cbf0fc Mon Sep 17 00:00:00 2001 From: Kurt Date: Sun, 4 Dec 2022 17:31:03 +0100 Subject: [PATCH 7/8] changed/fixed comments --- src/DonationHandler.sol | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/DonationHandler.sol b/src/DonationHandler.sol index d7e70f0..322d454 100644 --- a/src/DonationHandler.sol +++ b/src/DonationHandler.sol @@ -12,11 +12,10 @@ import "./gitcoin/IVotingStrategy.sol"; /// @notice This contract is used to handle donations /// This contract is build to use with proxies. /// -/// The user can donate whitelisted token to whitelisted recipients by calling the donate function. -/// A donation fee can be set by the user. The fee is paid in addition to the donation amount. -/// The donation fee is the amount the donor pays to the fee receiver (protocol) -/// The donation fee can be set by the user and is limited by the minFee. -/// The min fee is set by default to 0 and can be changed by the protocol admins. +/// The user can donate whitelisted token to whitelisted recipients by calling the vote function. +/// +/// The fee is deducted from the users donation, assigned to the contracts address and can be withdrawn by a fee receiver. +/// The min fee is set during initialization and can be changed by the protocol admins. /// The max fee is set by default to 1e18 and can't be changed. /// /// The user can withdraw the donation of a single token by calling the withdraw function. @@ -54,6 +53,7 @@ contract DonationHandler is /// @param _donationReceiver Array of donation receivers /// @param _feeReceiver Array of fee receivers /// @param _admins Array of admins + /// @param _minFee Minimum donation fee function initialize( address[] calldata _acceptedToken, address[] calldata _donationReceiver, @@ -81,6 +81,9 @@ contract DonationHandler is } } + /// @notice Donate(vote) to a whitelisted recipient. + /// @param encodedVotes Array of donations + /// @param voterAddress Address of the voter function vote( bytes[] calldata encodedVotes, address voterAddress @@ -299,18 +302,24 @@ contract DonationHandler is _setMinFee(_minFee); } + /// @notice Internal function. Set minimum donation fee. Emits MinFeeSet event. + /// @param _minFee Minimum donation fee function _setMinFee(uint256 _minFee) internal { if (_minFee > HUNDRED) revert FeeTooHigh(); minFee = _minFee; emit MinFeeSet(_minFee); } + /// @notice Enable/Disable token whitelist. Can only be called by an Admin. Emits IsTokenWhitelistActiveSet event. + /// @param _isTokenWhitelistActive Enable/Disable token whitelist function setIsTokenWhitelistActive(bool _isTokenWhitelistActive) external { _checkAdmin(msg.sender); isTokenWhitelistActive = _isTokenWhitelistActive; emit IsTokenWhitelistActiveSet(_isTokenWhitelistActive); } + /// @notice Enable/Disable recipient whitelist. Can only be called by an Admin. Emits IsRecipientWhitelistActiveSet event. + /// @param _isRecipientWhitelistActive Enable/Disable recipient whitelist function setIsRecipientWhitelistActive( bool _isRecipientWhitelistActive ) external { From e12ce3200f2cd9b38f926efe40481838455bef1c Mon Sep 17 00:00:00 2001 From: Kurt Date: Sun, 4 Dec 2022 17:39:13 +0100 Subject: [PATCH 8/8] forge fmt --- Makefile | 2 +- src/DonationHandler.sol | 115 +++++++++++-------------------------- test/DonationHandler.t.sol | 69 +++++----------------- 3 files changed, 47 insertions(+), 139 deletions(-) diff --git a/Makefile b/Makefile index f92d7bb..9e61395 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ update:; forge update build:; forge fmt && forge build -test :; forge test +test :; forge fmt && forge build && forge test snapshot :; forge snapshot diff --git a/src/DonationHandler.sol b/src/DonationHandler.sol index 322d454..90bba59 100644 --- a/src/DonationHandler.sol +++ b/src/DonationHandler.sol @@ -2,8 +2,10 @@ pragma solidity 0.8.17; import {IERC20Upgradeable as IERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; -import {SafeERC20Upgradeable as SafeERC20} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; -import {ReentrancyGuardUpgradeable as ReentrancyGuard} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import {SafeERC20Upgradeable as SafeERC20} from + "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; +import {ReentrancyGuardUpgradeable as ReentrancyGuard} from + "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; import "./DonationHandlerRoles.sol"; import "./gitcoin/IVotingStrategy.sol"; @@ -29,11 +31,7 @@ import "./gitcoin/IVotingStrategy.sol"; /// /// The donation balance of one token can be checked by calling the balanceOf function. /// The donation balance of multiple token can be checked by calling the balancesOf function. -contract DonationHandler is - IVotingStrategy, - DonationHandlerRoles, - ReentrancyGuard -{ +contract DonationHandler is IVotingStrategy, DonationHandlerRoles, ReentrancyGuard { using SafeERC20 for IERC20; /// @notice 1e18 represents 100%, 1e16 represents 1% @@ -61,12 +59,7 @@ contract DonationHandler is address[] calldata _admins, uint256 _minFee ) public initializer { - __DonationHandlerRoles_init( - _acceptedToken, - _donationReceiver, - _feeReceiver, - _admins - ); + __DonationHandlerRoles_init(_acceptedToken, _donationReceiver, _feeReceiver, _admins); __ReentrancyGuard_init(); if (_acceptedToken.length > 0) { @@ -76,7 +69,7 @@ contract DonationHandler is isRecipientWhitelistActive = true; } - if(_minFee > 0) { + if (_minFee > 0) { _setMinFee(_minFee); } } @@ -84,17 +77,20 @@ contract DonationHandler is /// @notice Donate(vote) to a whitelisted recipient. /// @param encodedVotes Array of donations /// @param voterAddress Address of the voter - function vote( - bytes[] calldata encodedVotes, - address voterAddress - ) external payable override nonReentrant isRoundContract { + function vote(bytes[] calldata encodedVotes, address voterAddress) + external + payable + override + nonReentrant + isRoundContract + { /// @dev iterate over multiple donations and transfer funds uint256 length = encodedVotes.length; uint256 msgValue = 0; - for (uint256 i = 0; i < length; ) { - (address _token, uint256 _amount, address _grantAddress) = abi - .decode(encodedVotes[i], (address, uint256, address)); + for (uint256 i = 0; i < length;) { + (address _token, uint256 _amount, address _grantAddress) = + abi.decode(encodedVotes[i], (address, uint256, address)); if (isTokenWhitelistActive) _checkToken(_token); if (isRecipientWhitelistActive) { @@ -138,11 +134,7 @@ contract DonationHandler is /// @param _token Address of the token /// @param _recipient Address of the recipient /// @param _amount Amount of tokens - function _registerDonation( - address _token, - address _recipient, - uint256 _amount - ) internal { + function _registerDonation(address _token, address _recipient, uint256 _amount) internal { balances[_recipient][_token] += _amount; emit DonationRegistered(_token, msg.sender, _recipient, _amount); } @@ -150,11 +142,7 @@ contract DonationHandler is /// @notice Internal function. Transfers tokens from the sender to the contract. /// @param _token Address of the token /// @param _amount Amount of tokens - function _transfer( - address _from, - address _token, - uint256 _amount - ) internal { + function _transfer(address _from, address _token, uint256 _amount) internal { IERC20(_token).safeTransferFrom(_from, address(this), _amount); } @@ -175,10 +163,7 @@ contract DonationHandler is /// @notice Distributes full amount of token arrays token from the contract to a recipient. /// @param _token Address array of the token /// @param _to Address of the recipient - function distribute( - address[] calldata _token, - address _to - ) external nonReentrant { + function distribute(address[] calldata _token, address _to) external nonReentrant { // TODO: maybe restrict to admins _withdrawAll(_token, _to, _to); } @@ -186,13 +171,10 @@ contract DonationHandler is /// @notice Distributes full amount of token arrays token from the contract to an array of recipients. /// @param _token Address array of the token /// @param _to Address array of the recipients - function distributeMany( - address[] calldata _token, - address[] calldata _to - ) external nonReentrant { + function distributeMany(address[] calldata _token, address[] calldata _to) external nonReentrant { // TODO: maybe restrict to admins uint256 length = _to.length; - for (uint256 i = 0; i < length; ) { + for (uint256 i = 0; i < length;) { _withdrawAll(_token, _to[i], _to[i]); unchecked { i++; @@ -220,14 +202,10 @@ contract DonationHandler is /// @param _token Address array of the token to withdraw /// @param _from Address of the spender /// @param _to Address of the recipient - function _withdrawAll( - address[] memory _token, - address _from, - address _to - ) internal { + function _withdrawAll(address[] memory _token, address _from, address _to) internal { uint256 length = _token.length; - for (uint256 i = 0; i < length; ) { + for (uint256 i = 0; i < length;) { uint256 amount = balances[_from][_token[i]]; if (amount > 0) { @@ -245,17 +223,12 @@ contract DonationHandler is /// @param _from Address of the spender /// @param _to Address of the recipient /// @param _amount Amount of tokens to withdraw - function _withdraw( - address _token, - address _from, - address _to, - uint256 _amount - ) internal { + function _withdraw(address _token, address _from, address _to, uint256 _amount) internal { if (_amount > balances[_from][_token]) revert InsufficientBalance(); balances[_from][_token] -= _amount; if (_token == NATIVE) { - (bool success, ) = payable(_to).call{value: _amount}(""); + (bool success,) = payable(_to).call{value: _amount}(""); if (!success) revert TransferFailed(); } else { IERC20(_token).safeTransfer(_to, _amount); @@ -268,10 +241,7 @@ contract DonationHandler is /// @param _token Address of the token /// @param _user Address of the user /// @return Token balance of the user - function balanceOf( - address _token, - address _user - ) external view returns (uint256) { + function balanceOf(address _token, address _user) external view returns (uint256) { return balances[_user][_token]; } @@ -279,14 +249,11 @@ contract DonationHandler is /// @param _token Address array of the token /// @param _user Address of the user /// @return Uint256 array. Token balances of the user - function balancesOf( - address[] calldata _token, - address _user - ) external view returns (uint256[] memory) { + function balancesOf(address[] calldata _token, address _user) external view returns (uint256[] memory) { uint256 length = _token.length; uint256[] memory result = new uint256[](length); - for (uint256 i = 0; i < length; ) { + for (uint256 i = 0; i < length;) { result[i] = balances[_user][_token[i]]; unchecked { i++; @@ -320,9 +287,7 @@ contract DonationHandler is /// @notice Enable/Disable recipient whitelist. Can only be called by an Admin. Emits IsRecipientWhitelistActiveSet event. /// @param _isRecipientWhitelistActive Enable/Disable recipient whitelist - function setIsRecipientWhitelistActive( - bool _isRecipientWhitelistActive - ) external { + function setIsRecipientWhitelistActive(bool _isRecipientWhitelistActive) external { _checkAdmin(msg.sender); isRecipientWhitelistActive = _isRecipientWhitelistActive; emit IsRecipientWhitelistActiveSet(_isRecipientWhitelistActive); @@ -347,35 +312,21 @@ contract DonationHandler is /// @param token The token address /// @param from The address of the sender /// @param amount The amount of tokens - event FeeRegistered( - address indexed token, - address indexed from, - uint256 amount - ); + event FeeRegistered(address indexed token, address indexed from, uint256 amount); /// @notice Emitted when a donation is registered /// @param token The token address /// @param from The address of the sender /// @param recipient The address of the recipient /// @param amount The amount of tokens - event DonationRegistered( - address indexed token, - address indexed from, - address indexed recipient, - uint256 amount - ); + event DonationRegistered(address indexed token, address indexed from, address indexed recipient, uint256 amount); /// @notice Emitted when a withdrawal is made /// @param token The token address /// @param from The address of the sender /// @param to The address of the recipient /// @param amount The amount of tokens - event Withdraw( - address indexed token, - address indexed from, - address indexed to, - uint256 amount - ); + event Withdraw(address indexed token, address indexed from, address indexed to, uint256 amount); /// @notice Emitted when the minimum fee is set /// @param minFee The minimum fee diff --git a/test/DonationHandler.t.sol b/test/DonationHandler.t.sol index e9b61b9..ce7b0cf 100644 --- a/test/DonationHandler.t.sol +++ b/test/DonationHandler.t.sol @@ -12,11 +12,7 @@ contract DonationHandlerTest is SharedInitialization { // Donate - function _encode( - address token, - uint256 amount, - address recipient - ) internal pure returns (bytes memory) { + function _encode(address token, uint256 amount, address recipient) internal pure returns (bytes memory) { return abi.encode(token, amount, recipient); } @@ -40,15 +36,9 @@ contract DonationHandlerTest is SharedInitialization { function test_donate() public { _donate(); - assertEq( - donationHandler.balanceOf(address(allowedToken), address(1)), - 90 - ); - - uint256[] memory balances = donationHandler.balancesOf( - acceptedToken, - address(1) - ); + assertEq(donationHandler.balanceOf(address(allowedToken), address(1)), 90); + + uint256[] memory balances = donationHandler.balancesOf(acceptedToken, address(1)); assertEq(balances.length, 2); assertEq(balances[0], 90); assertEq(balances[1], 90); @@ -86,29 +76,11 @@ contract DonationHandlerTest is SharedInitialization { donationHandler.vote(donation, deployer); - assertEq( - donationHandler.balanceOf(address(allowedToken), address(1)), - 180 - ); - assertEq( - donationHandler.balanceOf( - address(allowedToken), - address(donationHandler) - ), - 20 - ); - - assertEq( - donationHandler.balanceOf(address(allowedToken2), address(1)), - 180 - ); - assertEq( - donationHandler.balanceOf( - address(allowedToken2), - address(donationHandler) - ), - 20 - ); + assertEq(donationHandler.balanceOf(address(allowedToken), address(1)), 180); + assertEq(donationHandler.balanceOf(address(allowedToken), address(donationHandler)), 20); + + assertEq(donationHandler.balanceOf(address(allowedToken2), address(1)), 180); + assertEq(donationHandler.balanceOf(address(allowedToken2), address(donationHandler)), 20); } // withdraw @@ -117,10 +89,7 @@ contract DonationHandlerTest is SharedInitialization { _donate(); vm.prank(address(1)); donationHandler.withdraw(address(allowedToken), 90); - assertEq( - donationHandler.balanceOf(address(allowedToken), address(1)), - 0 - ); + assertEq(donationHandler.balanceOf(address(allowedToken), address(1)), 0); assertEq(allowedToken.balanceOf(address(1)), 90); } @@ -128,10 +97,7 @@ contract DonationHandlerTest is SharedInitialization { _donate(); vm.prank(address(1)); donationHandler.withdrawMany(acceptedToken); - assertEq( - donationHandler.balanceOf(address(allowedToken), address(1)), - 0 - ); + assertEq(donationHandler.balanceOf(address(allowedToken), address(1)), 0); assertEq(allowedToken.balanceOf(address(1)), 90); assertEq(allowedToken2.balanceOf(address(1)), 90); } @@ -164,13 +130,7 @@ contract DonationHandlerTest is SharedInitialization { _donate(); vm.prank(address(2)); donationHandler.withdrawFee(address(allowedToken)); - assertEq( - donationHandler.balanceOf( - address(allowedToken), - address(donationHandler) - ), - 0 - ); + assertEq(donationHandler.balanceOf(address(allowedToken), address(donationHandler)), 0); assertEq(allowedToken.balanceOf(address(2)), 10); } @@ -189,10 +149,7 @@ contract DonationHandlerTest is SharedInitialization { donationHandler.vote{value: 100}(donation, deployer); assertEq(donationHandler.balanceOf(NATIVE, address(1)), 90); - assertEq( - donationHandler.balanceOf(NATIVE, address(donationHandler)), - 10 - ); + assertEq(donationHandler.balanceOf(NATIVE, address(donationHandler)), 10); vm.prank(address(1)); uint256 balanceBefore = address(1).balance;