Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
src = 'src'
out = 'out'
libs = ['lib']
remappings = ["@openzeppelin/contracts-upgradeable=lib/openzeppelin-contracts-upgradeable/contracts", "@openzeppelin/contracts=lib/openzeppelin-contracts/contracts"]
remappings = ["@openzeppelin/contracts-upgradeable=lib/openzeppelin-contracts-upgradeable/contracts", "@openzeppelin/contracts=lib/openzeppelin-contracts/contracts", "solmate=lib/solmate/src"]

# See more config options https://github.com/foundry-rs/foundry/tree/master/config
24 changes: 24 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"dependencies": {
"solmate": "^6.6.1"
}
}
17 changes: 12 additions & 5 deletions script/Config.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ contract Config {
address[] donationRecipient;
address[] feeReceiver;
address[] admins;
uint256 minFee;
}

NetworkConfig private activeNetworkConfig;
Expand Down Expand Up @@ -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 ==================
Expand All @@ -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 ==================
Expand All @@ -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 ==================
Expand All @@ -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;
Expand All @@ -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++) {
Expand Down Expand Up @@ -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;
}
}
8 changes: 6 additions & 2 deletions script/DonationHandler.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import "forge-std/Script.sol";
import "./Config.sol";
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
import "../src/DonationHandler.sol";
import "../src/DonationHandler/DonationHandler.sol";

contract DeployDonationHandler is Script, Config {
function run() external {
Expand All @@ -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();
Expand Down
162 changes: 118 additions & 44 deletions src/DonationHandler.sol → src/DonationHandler/DonationHandler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,18 @@ 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
/// @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 taken from the donation amount.
/// The donation fee is a percentage of the donation amount where 1e18 is 100%, 1e17 10%, etc..
/// 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 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.
Expand All @@ -32,7 +31,7 @@ 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 {
using SafeERC20 for IERC20;

/// @notice 1e18 represents 100%, 1e16 represents 1%
Expand All @@ -41,6 +40,9 @@ 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;

Expand All @@ -49,45 +51,87 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall {
/// @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,
address[] calldata _feeReceiver,
address[] calldata _admins
address[] calldata _admins,
uint256 _minFee
) public initializer {
__DonationHandler_init(_acceptedToken, _donationReceiver, _feeReceiver, _admins, _minFee);
}

function __DonationHandler_init(
address[] calldata _acceptedToken,
address[] calldata _donationReceiver,
address[] calldata _feeReceiver,
address[] calldata _admins,
uint256 _minFee
) internal onlyInitializing {
__DonationHandlerRoles_init(_acceptedToken, _donationReceiver, _feeReceiver, _admins);
__ReentrancyGuard_init();
__Multicall_init();

if (_acceptedToken.length > 0) {
isTokenWhitelistActive = true;
}
if (_donationReceiver.length > 0) {
isRecipientWhitelistActive = true;
}

if (_minFee > 0) {
_setMinFee(_minFee);
}
}

/// @notice Donate tokens to a recipient. The fee is deducted from 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();
if (_amount == 0) revert InvalidAmount();
/// @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
{
/// @dev iterate over multiple donations and transfer funds
uint256 length = encodedVotes.length;
uint256 msgValue = 0;

_validateDonation(_token, _recipient);
for (uint256 i = 0; i < length;) {
(address _token, uint256 _amount, address _grantAddress) =
abi.decode(encodedVotes[i], (address, uint256, address));

if (_token != NATIVE) {
_transfer(_token, _amount);
} else {
if (msg.value != _amount) revert InvalidAmount();
}
if (isTokenWhitelistActive) _checkToken(_token);
if (isRecipientWhitelistActive) {
_checkDonationRecipient(_grantAddress);
}

if (_fee == 0) {
_registerDonation(_token, _recipient, _amount);
} else if (_fee == HUNDRED) {
_registerFee(_token, _amount);
} else {
uint256 feeAmount = (_amount * _fee) / HUNDRED;
uint256 donationAmount = _amount - feeAmount;
if (_token != NATIVE) {
_transfer(voterAddress, _token, _amount);
} else {
msgValue += _amount;
}

/// @dev emit event for transfer
emit Voted(_token, _amount, voterAddress, _grantAddress, msg.sender);

_registerDonation(_token, _recipient, donationAmount);
_registerFee(_token, feeAmount);
if (minFee > 0) {
uint256 fee = (_amount * minFee) / HUNDRED;

_registerDonation(_token, _grantAddress, _amount - fee);
_registerFee(_token, fee);
} else {
_registerDonation(_token, _grantAddress, _amount);
}

unchecked {
i++;
}
}

if (msgValue > msg.value) {
revert InvalidAmount();
}
}

Expand All @@ -108,19 +152,11 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall {
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);
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.
Expand Down Expand Up @@ -200,7 +236,7 @@ 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 virtual {
if (_amount > balances[_from][_token]) revert InsufficientBalance();

balances[_from][_token] -= _amount;
Expand Down Expand Up @@ -243,11 +279,33 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall {
/// @param _minFee Minimum donation fee
function setMinFee(uint256 _minFee) external {
_checkAdmin(msg.sender);
_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 {
_checkAdmin(msg.sender);
isRecipientWhitelistActive = _isRecipientWhitelistActive;
emit IsRecipientWhitelistActiveSet(_isRecipientWhitelistActive);
}

/// @notice Throws if passed fee is above 100%.
error FeeTooHigh();

Expand Down Expand Up @@ -286,4 +344,20 @@ contract DonationHandler is DonationHandlerRoles, ReentrancyGuard, Multicall {
/// @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);

/// @notice Emitted when a new vote is sent
event Voted( // voting token
// voting amount
// voter address
// grant address
// round address
address token, uint256 amount, address indexed voter, address indexed grantAddress, address indexed roundAddress);
}
Loading