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
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;
}
}
6 changes: 5 additions & 1 deletion script/DonationHandler.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
139 changes: 96 additions & 43 deletions src/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,74 @@ 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 {
__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;
}

if (minFee > 0) {
uint256 fee = (_amount * minFee) / HUNDRED;

_registerDonation(_token, _recipient, donationAmount);
_registerFee(_token, feeAmount);
_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 +139,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 @@ -243,11 +266,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 +331,12 @@ 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);
}
5 changes: 3 additions & 2 deletions src/DonationHandlerRoles.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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);
Expand Down
52 changes: 52 additions & 0 deletions src/gitcoin/IVotingStrategy.sol
Original file line number Diff line number Diff line change
@@ -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;
}
Loading