diff --git a/contracts/core/interfaces/IDAI.sol b/contracts/core/interfaces/IDAI.sol new file mode 100644 index 000000000..8eab34db6 --- /dev/null +++ b/contracts/core/interfaces/IDAI.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.19; + +interface IDAI { + function permit( + address holder, + address spender, + uint256 nonce, + uint256 expiry, + bool allowed, + uint8 v, + bytes32 r, + bytes32 s + ) external; +} diff --git a/contracts/core/libraries/Transfer.sol b/contracts/core/libraries/Transfer.sol index c04f9b47b..d1056425c 100644 --- a/contracts/core/libraries/Transfer.sol +++ b/contracts/core/libraries/Transfer.sol @@ -3,6 +3,11 @@ pragma solidity 0.8.19; // External Libraries import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; +// Interfaces +import {ISignatureTransfer} from "permit2/ISignatureTransfer.sol"; +import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IDAI} from "contracts/core/interfaces/IDAI.sol"; // ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ // ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⣿⣿⣿⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ @@ -28,6 +33,25 @@ library Transfer { /// @notice Address of the native token address public constant NATIVE = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + /// @notice Thrown if a signature of a length different than 64 or 65 bytes is passed + error INVALID_SIGNATURE_LENGTH(); + + /// @notice Supported permit formats + enum PermitFormat { + None, + Permit, + PermitDAI, + Permit2 + } + + /// @notice Stores the permit2 data for the allocation + struct PermitData { + ISignatureTransfer permit2; + uint256 nonce; + uint256 deadline; + bytes signature; + } + /// @notice Transfer an amount of a token to an address /// @dev When this function is used, it must be checked that balances or msg.value is correct for native tokens /// @param _token The token to transfer @@ -73,4 +97,63 @@ library Transfer { return _token.balanceOf(_account); } } + + /// @notice Uses a permit if given. Three permit formats are accepted: permit2, ERC20Permit and DAI-like permits + /// @dev permit data is ignored if empty + /// @param _token The token address + /// @param _from The address signing the permit + /// @param _to The address to give allowance to + /// @param _amount The amount to allow + /// @param _permitData The PermitData containing the signature and relevant permit data + function usePermit(address _token, address _from, address _to, uint256 _amount, bytes memory _permitData) + internal + { + // Only try to use permit if needed + if (_permitData.length == 0) return; + if (IERC20(_token).allowance(_from, _to) >= _amount) return; + + (PermitFormat permitFormat, PermitData memory permit) = abi.decode(_permitData, (PermitFormat, PermitData)); + if (permitFormat == PermitFormat.Permit2) { + permit.permit2.permitTransferFrom( + ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: _token, amount: _amount}), + nonce: permit.nonce, + deadline: permit.deadline + }), + ISignatureTransfer.SignatureTransferDetails({to: _to, requestedAmount: _amount}), + _from, + permit.signature + ); + } else if (permitFormat == PermitFormat.Permit) { + (bytes32 r, bytes32 s, uint8 v) = _splitSignature(permit.signature); + IERC20Permit(_token).permit(_from, _to, _amount, permit.deadline, v, r, s); + } else if (permitFormat == PermitFormat.PermitDAI) { + (bytes32 r, bytes32 s, uint8 v) = _splitSignature(permit.signature); + IDAI(_token).permit(_from, _to, permit.nonce, permit.deadline, true, v, r, s); + } + } + + /// @notice Splits a signature into its r, s, v components + /// @dev compact EIP-2098 signatures are accepted as well + /// @param _signature The signature + function _splitSignature(bytes memory _signature) internal pure returns (bytes32 r, bytes32 s, uint8 v) { + if (_signature.length == 65) { + assembly { + r := mload(add(_signature, 0x20)) + s := mload(add(_signature, 0x40)) + v := byte(0, mload(add(_signature, 0x60))) + } + } else if (_signature.length == 64) { + // EIP-2098 + bytes32 vs; + assembly { + r := mload(add(_signature, 0x20)) + vs := mload(add(_signature, 0x40)) + } + s = vs & (0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff); + v = uint8(uint256(vs >> 255)) + 27; + } else { + revert INVALID_SIGNATURE_LENGTH(); + } + } } diff --git a/contracts/strategies/DonationVotingMerkleDistribution.sol b/contracts/strategies/DonationVotingMerkleDistribution.sol new file mode 100644 index 000000000..6fae05756 --- /dev/null +++ b/contracts/strategies/DonationVotingMerkleDistribution.sol @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.19; + +// External Libraries +import {MerkleProof} from "openzeppelin-contracts/contracts/utils/cryptography/MerkleProof.sol"; +import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; +// Interfaces +import {IAllo} from "contracts/core/interfaces/IAllo.sol"; +// Core Contracts +import {DonationVotingOffchain} from "contracts/strategies/DonationVotingOffchain.sol"; +// Internal Libraries +import {Metadata} from "contracts/core/libraries/Metadata.sol"; +import {Transfer} from "contracts/core/libraries/Transfer.sol"; + +// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⣿⣿⣿⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣿⣿⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⣿⣿⣿⢿⣿⣿⣿⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⣿⣿⣿⣿⣿⣿⣿⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣿⣿⣿⡟⠘⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +// ⠀⠀⠀⠀⠀⠀⠀⠀⣀⣴⣾⣿⣿⣿⣿⣾⠻⣿⣿⣿⣿⣿⣿⣿⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⣿⡿⠀⠀⠸⣿⣿⣿⣧⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠀⠀⠀⠀⢀⣠⣴⣴⣶⣶⣶⣦⣦⣀⡀⠀⠀⠀⠀⠀⠀ +// ⠀⠀⠀⠀⠀⠀⠀⣴⣿⣿⣿⣿⣿⣿⡿⠃⠀⠙⣿⣿⣿⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⣿⣿⠁⠀⠀⠀⢻⣿⣿⣿⣧⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠀⠀⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⡀⠀⠀⠀⠀ +// ⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⣿⣿⡿⠁⠀⠀⠀⠘⣿⣿⣿⣿⣿⡿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⠃⠀⠀⠀⠀⠈⢿⣿⣿⣿⣆⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠀⣰⣿⣿⣿⡿⠋⠁⠀⠀⠈⠘⠹⣿⣿⣿⣿⣆⠀⠀⠀ +// ⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⣿⣿⡿⠀⠀⠀⠀⠀⠀⠈⢿⣿⣿⣿⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⣿⣿⣿⠏⠀⠀⠀⠀⠀⠀⠘⣿⣿⣿⣿⡄⠀⠀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⢰⣿⣿⣿⣿⠁⠀⠀⠀⠀⠀⠀⠀⠘⣿⣿⣿⣿⡀⠀⠀ +// ⠀⠀⠀⢠⣿⣿⣿⣿⣿⣿⣿⣟⠀⡀⢀⠀⡀⢀⠀⡀⢈⢿⡟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡄⠀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⡇⠀⠀ +// ⠀⠀⣠⣿⣿⣿⣿⣿⣿⡿⠋⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣶⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⣿⣿⣿⡿⢿⠿⠿⠿⠿⠿⠿⠿⠿⠿⢿⣿⣿⣿⣷⡀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠸⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⣿⣿⠂⠀⠀ +// ⠀⠀⠙⠛⠿⠻⠻⠛⠉⠀⠀⠈⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣿⣿⣿⣿⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢿⣿⣿⣿⣧⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠀⢻⣿⣿⣿⣷⣀⢀⠀⠀⠀⡀⣰⣾⣿⣿⣿⠏⠀⠀⠀ +// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠛⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⣿⣿⣿⣿⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⣿⣿⣿⣿⣧⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠀⠀⠹⢿⣿⣿⣿⣿⣾⣾⣷⣿⣿⣿⣿⡿⠋⠀⠀⠀⠀ +// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⠙⠋⠛⠙⠋⠛⠙⠋⠛⠙⠋⠃⠀⠀⠀⠀⠀⠀⠀⠀⠠⠿⠻⠟⠿⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⠟⠿⠟⠿⠆⠀⠸⠿⠿⠟⠯⠀⠀⠀⠸⠿⠿⠿⠏⠀⠀⠀⠀⠀⠈⠉⠻⠻⡿⣿⢿⡿⡿⠿⠛⠁⠀⠀⠀⠀⠀⠀ +// allo.gitcoin.co + +/// @title Donation Voting Strategy with off-chain setup +/// @notice Strategy that allows allocations in multiple tokens to accepted recipient. The actual payouts are set +/// by the pool manager. +contract DonationVotingMerkleDistribution is DonationVotingOffchain { + using Transfer for address; + + /// =============================== + /// ========== Events ============= + /// =============================== + + /// @notice Emitted when the distribution has been updated with a new merkle root or metadata + /// @param merkleRoot The merkle root of the distribution + /// @param metadata The metadata of the distribution + event DistributionUpdated(bytes32 merkleRoot, Metadata metadata); + + /// ================================ + /// ========== Errors ============== + /// ================================ + + /// @notice Thrown when the merkle root is attempted to be updated but the distribution is ongoing + error DISTRIBUTION_ALREADY_STARTED(); + + /// @notice Thrown when distribution is invoked but the merkle root has not been set yet + error MERKLE_ROOT_NOT_SET(); + + /// @notice Thrown when distribution is attempted twice for the same 'index' + error ALREADY_DISTRIBUTED(uint256 _index); + + /// ================================ + /// ========== Struct ============== + /// ================================ + + /// @notice Stores the details of the distribution. + struct Distribution { + uint256 index; + address recipientId; + uint256 amount; + bytes32[] merkleProof; + } + + /// ================================ + /// ========== Storage ============= + /// ================================ + + /// @notice Metadata containing the distribution data. + Metadata public distributionMetadata; + + /// @notice Flag to indicate whether the distribution has started or not. + bool public distributionStarted; + + /// @notice The merkle root of the distribution will be set by the pool manager. + bytes32 public merkleRoot; + + /// @notice This is a packed array of booleans to keep track of claims distributed. + /// @dev _distributedBitMap[0] is the first row of the bitmap and allows to store 256 bits to describe + /// the status of 256 claims + mapping(uint256 => uint256) internal _distributedBitMap; + + /// =============================== + /// ======== Constructor ========== + /// =============================== + + /// @notice Constructor for the Donation Voting Offchain strategy + /// @param _allo The 'Allo' contract + /// @param _directTransfer false if allocations must be manually claimed, true if they are sent during allocation. + constructor(address _allo, bool _directTransfer) DonationVotingOffchain(_allo, _directTransfer) {} + + /// =============================== + /// ======= External/Custom ======= + /// =============================== + + /// @notice Invoked by round operator to update the merkle root and distribution Metadata. + /// @param _data The data to be decoded + /// @custom:data (bytes32 _merkleRoot, Metadata _distributionMetadata) + function setPayout(bytes memory _data) external virtual override onlyPoolManager(msg.sender) onlyAfterAllocation { + // The merkleRoot can only be updated before the distribution has started + if (distributionStarted) revert DISTRIBUTION_ALREADY_STARTED(); + + (bytes32 _merkleRoot, Metadata memory _distributionMetadata) = abi.decode(_data, (bytes32, Metadata)); + + merkleRoot = _merkleRoot; + distributionMetadata = _distributionMetadata; + + emit DistributionUpdated(_merkleRoot, _distributionMetadata); + } + + /// @notice Utility function to check if distribution is done. + /// @dev This function doesn't change the state even if it is not marked as 'view' + /// @param _index index of the distribution + /// @return 'true' if distribution is completed, otherwise 'false' + function hasBeenDistributed(uint256 _index) external returns (bool) { + return _distributed(_index, false); + } + + /// ==================================== + /// ============ Internal ============== + /// ==================================== + + /// @notice Distributes funds (tokens) to recipients. + /// @param _data Data to be decoded + /// @custom:data (Distribution[] _distributions) + /// @param _sender The address of the sender + function _distribute(address[] memory, bytes memory _data, address _sender) + internal + virtual + override + onlyAfterAllocation + { + if (merkleRoot == bytes32(0)) revert MERKLE_ROOT_NOT_SET(); + + if (!distributionStarted) distributionStarted = true; + + // Loop through the distributions and distribute the funds + Distribution[] memory distributions = abi.decode(_data, (Distribution[])); + IAllo.Pool memory pool = allo.getPool(poolId); + for (uint256 i; i < distributions.length;) { + _distributeSingle(distributions[i], pool.token, _sender); + unchecked { + i++; + } + } + } + + /// @notice Check if the distribution has been distributed. + /// @param _index index of the distribution + /// @param _set if 'true' sets the '_distributedBitMap' index to 'true', otherwise it is left unmodified + /// @return 'true' if the distribution has been distributed, otherwise 'false' + function _distributed(uint256 _index, bool _set) internal returns (bool) { + uint256 wordIndex = _index / 256; + uint256 distributedWord = _distributedBitMap[wordIndex]; + + uint256 bitIndex = _index % 256; + // Get the mask by shifting 1 to the left of the 'bitIndex' + uint256 mask = (1 << bitIndex); + + // Set the 'bitIndex' of 'distributedWord' to 1 + if (_set) _distributedBitMap[wordIndex] = distributedWord | (1 << bitIndex); + + // Return 'true' if the 'distributedWord' was 1 at 'bitIndex' + return distributedWord & mask == mask; + } + + /// @notice Distribute funds to recipient. + /// @dev Emits a 'Distributed()' event per distributed recipient + /// @param _distribution Distribution to be distributed + /// @param _poolToken Token address of the strategy + /// @param _sender The address of the sender + function _distributeSingle(Distribution memory _distribution, address _poolToken, address _sender) internal { + if (!_isAcceptedRecipient(_distribution.recipientId)) revert RECIPIENT_NOT_ACCEPTED(); + + // Generate the node that will be verified in the 'merkleRoot' + bytes32 node = keccak256(abi.encode(_distribution.index, _distribution.recipientId, _distribution.amount)); + + // Validate the distribution and transfer the funds to the recipient, otherwise skip + if (MerkleProof.verify(_distribution.merkleProof, merkleRoot, node)) { + if (_distributed(_distribution.index, true)) revert ALREADY_DISTRIBUTED(_distribution.index); + poolAmount -= _distribution.amount; + + address recipientAddress = _recipients[_distribution.recipientId].recipientAddress; + _poolToken.transferAmount(recipientAddress, _distribution.amount); + + emit Distributed(_distribution.recipientId, abi.encode(recipientAddress, _distribution.amount, _sender)); + } + } +} diff --git a/contracts/strategies/DonationVotingOffchain.sol b/contracts/strategies/DonationVotingOffchain.sol index 021fb39e9..91185bfa2 100644 --- a/contracts/strategies/DonationVotingOffchain.sol +++ b/contracts/strategies/DonationVotingOffchain.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.19; // Interfaces -import {IAllo} from "../core/interfaces/IAllo.sol"; +import {IAllo} from "contracts/core/interfaces/IAllo.sol"; // Core Contracts import {CoreBaseStrategy} from "./CoreBaseStrategy.sol"; import {RecipientsExtension} from "../extensions/contracts/RecipientsExtension.sol"; @@ -91,6 +91,9 @@ contract DonationVotingOffchain is CoreBaseStrategy, RecipientsExtension, Native /// ========== Storage ============= /// ================================ + /// @notice If true, allocations are directly sent to recipients. Otherwise, they they must be claimed later. + bool public immutable DIRECT_TRANSFER; + /// @notice The start and end times for allocations uint64 public allocationStartTime; uint64 public allocationEndTime; @@ -131,7 +134,10 @@ contract DonationVotingOffchain is CoreBaseStrategy, RecipientsExtension, Native /// @notice Constructor for the Donation Voting Offchain strategy /// @param _allo The 'Allo' contract - constructor(address _allo) RecipientsExtension(_allo, false) {} + /// @param _directTransfer false if allocations must be manually claimed, true if they are sent during allocation. + constructor(address _allo, bool _directTransfer) RecipientsExtension(_allo, false) { + DIRECT_TRANSFER = _directTransfer; + } /// =============================== /// ========= Initialize ========== @@ -201,12 +207,19 @@ contract DonationVotingOffchain is CoreBaseStrategy, RecipientsExtension, Native _updatePoolTimestamps(_registrationStartTime, _registrationEndTime); } - /// @notice Claim allocated tokens - /// @param _claims Claims to be claimed - function claimAllocation(Claim[] calldata _claims) external onlyAfterAllocation { + /// @notice Transfers the allocated tokens to recipients. + /// @dev This function is ignored if DIRECT_TRANSFER is enabled, in which case allocated tokens are not stored + /// in the contract for later claim but directly sent to recipients in `_allocate()`. + /// @param _data The data to be decoded + /// @custom:data (Claim[] _claims) + function claimAllocation(bytes memory _data) external virtual onlyAfterAllocation { + if (DIRECT_TRANSFER) revert NOT_IMPLEMENTED(); + + (Claim[] memory _claims) = abi.decode(_data, (Claim[])); + uint256 claimsLength = _claims.length; for (uint256 i; i < claimsLength; i++) { - Claim calldata claim = _claims[i]; + Claim memory claim = _claims[i]; uint256 amount = amountAllocated[claim.recipientId][claim.token]; address recipientAddress = _recipients[claim.recipientId].recipientAddress; @@ -218,14 +231,12 @@ contract DonationVotingOffchain is CoreBaseStrategy, RecipientsExtension, Native } } - /// @notice Set payout for the recipients - /// @param _recipientIds Ids of the recipients - /// @param _amounts Amounts to be paid out - function setPayout(address[] memory _recipientIds, uint256[] memory _amounts) - external - onlyPoolManager(msg.sender) - onlyAfterAllocation - { + /// @notice Sets the payout amounts to be distributed to. + /// @param _data The data to be decoded + /// @custom:data (address[] _recipientIds, uint256[] _amounts) + function setPayout(bytes memory _data) external virtual onlyPoolManager(msg.sender) onlyAfterAllocation { + (address[] memory _recipientIds, uint256[] memory _amounts) = abi.decode(_data, (address[], uint256[])); + uint256 totalAmount; for (uint256 i; i < _recipientIds.length; i++) { address recipientId = _recipientIds[i]; @@ -252,37 +263,43 @@ contract DonationVotingOffchain is CoreBaseStrategy, RecipientsExtension, Native /// ==================================== /// @notice This will allocate to recipients. - /// @dev The encoded '_data' is an array of token addresses corresponding to the _amounts array. - /// @param _recipients The addresses of the recipients to allocate to + /// @dev The encoded '_data' is a tuple containing an array of token addresses corresponding to '_amounts' and + /// an array of permits data + /// @param __recipients The addresses of the recipients to allocate to /// @param _amounts The amounts to allocate to the recipients /// @param _data The data to use to allocate to the recipient + /// @custom:data ( + /// address[] tokens, + /// bytes[] permits + /// ) /// @param _sender The address of the sender - function _allocate(address[] memory _recipients, uint256[] memory _amounts, bytes memory _data, address _sender) + function _allocate(address[] memory __recipients, uint256[] memory _amounts, bytes memory _data, address _sender) internal virtual override onlyActiveAllocation { - (address[] memory tokens) = abi.decode(_data, (address[])); + (address[] memory tokens, bytes[] memory permits) = abi.decode(_data, (address[], bytes[])); uint256 totalNativeAmount; - for (uint256 i = 0; i < _recipients.length; i++) { - if (!_isAcceptedRecipient(_recipients[i])) revert RECIPIENT_NOT_ACCEPTED(); + for (uint256 i = 0; i < __recipients.length; i++) { + if (!_isAcceptedRecipient(__recipients[i])) revert RECIPIENT_NOT_ACCEPTED(); - if (!allowedTokens[tokens[i]] && !allowedTokens[address(0)]) { - revert TOKEN_NOT_ALLOWED(); - } + if (!allowedTokens[tokens[i]] && !allowedTokens[address(0)]) revert TOKEN_NOT_ALLOWED(); + + if (!DIRECT_TRANSFER) amountAllocated[__recipients[i]][tokens[i]] += _amounts[i]; - // Update the total payout amount for the claim and the total claimable amount - amountAllocated[_recipients[i]][tokens[i]] += _amounts[i]; + address recipientAddress = DIRECT_TRANSFER ? _recipients[__recipients[i]].recipientAddress : address(this); if (tokens[i] == NATIVE) { totalNativeAmount += _amounts[i]; } else { - tokens[i].transferAmountFrom(_sender, address(this), _amounts[i]); + tokens[i].usePermit(_sender, recipientAddress, _amounts[i], permits[i]); } - emit Allocated(_recipients[i], _sender, _amounts[i], abi.encode(tokens[i])); + tokens[i].transferAmountFrom(_sender, recipientAddress, _amounts[i]); + + emit Allocated(__recipients[i], _sender, _amounts[i], abi.encode(tokens[i])); } if (msg.value != totalNativeAmount) revert ETH_MISMATCH(); diff --git a/contracts/strategies/DonationVotingOnchain.sol b/contracts/strategies/DonationVotingOnchain.sol index fb41dc2aa..a626daa8a 100644 --- a/contracts/strategies/DonationVotingOnchain.sol +++ b/contracts/strategies/DonationVotingOnchain.sol @@ -2,12 +2,12 @@ pragma solidity 0.8.19; // Interfaces -import {IAllo} from "../core/interfaces/IAllo.sol"; +import {IAllo} from "contracts/core/interfaces/IAllo.sol"; // Core Contracts -import {CoreBaseStrategy} from "./CoreBaseStrategy.sol"; -import {RecipientsExtension} from "../extensions/contracts/RecipientsExtension.sol"; +import {CoreBaseStrategy} from "contracts/strategies/CoreBaseStrategy.sol"; +import {RecipientsExtension} from "contracts/extensions/contracts/RecipientsExtension.sol"; // Internal Libraries -import {QFHelper} from "../core/libraries/QFHelper.sol"; +import {QFHelper} from "contracts/core/libraries/QFHelper.sol"; import {Native} from "contracts/core/libraries/Native.sol"; import {Transfer} from "contracts/core/libraries/Transfer.sol"; @@ -167,8 +167,9 @@ contract DonationVotingOnchain is CoreBaseStrategy, RecipientsExtension, Native /// @notice This will allocate to recipients. /// @param _recipients The addresses of the recipients to allocate to /// @param _amounts The amounts to allocate to the recipients + /// @param _data The data containing permit data for the sum of '_amounts' if needed (ignored if empty) /// @param _sender The address of the sender - function _allocate(address[] memory _recipients, uint256[] memory _amounts, bytes memory, address _sender) + function _allocate(address[] memory _recipients, uint256[] memory _amounts, bytes memory _data, address _sender) internal virtual override @@ -188,6 +189,7 @@ contract DonationVotingOnchain is CoreBaseStrategy, RecipientsExtension, Native if (allocationToken == NATIVE) { if (msg.value != totalAmount) revert ETH_MISMATCH(); } else { + allocationToken.usePermit(_sender, address(this), totalAmount, _data); allocationToken.transferAmountFrom(_sender, address(this), totalAmount); } diff --git a/test/foundry/integration/Allo.t.sol b/test/foundry/integration/Allo.t.sol index 301f0fab0..a418286a6 100644 --- a/test/foundry/integration/Allo.t.sol +++ b/test/foundry/integration/Allo.t.sol @@ -18,7 +18,7 @@ contract IntegrationAllo is IntegrationBase { allo = IAllo(ALLO_PROXY); - strategy = new DonationVotingOffchain(ALLO_PROXY); + strategy = new DonationVotingOffchain(ALLO_PROXY, false); // Deal 130k DAI to the user deal(DAI, userAddr, 130_000 ether); @@ -139,6 +139,7 @@ contract IntegrationAllo is IntegrationBase { address[] memory _recipients = new address[](1); uint256[] memory _amounts = new uint256[](1); address[] memory _tokens = new address[](1); + bytes[] memory _permits = new bytes[](1); _tokens[0] = DAI; _amounts[0] = 10_000 ether; @@ -146,7 +147,7 @@ contract IntegrationAllo is IntegrationBase { _sendWithRelayer( userAddr, address(allo), - abi.encodeWithSelector(allo.allocate.selector, poolId, _recipients, _amounts, abi.encode(_tokens)), + abi.encodeWithSelector(allo.allocate.selector, poolId, _recipients, _amounts, abi.encode(_tokens, _permits)), userPk ); @@ -154,7 +155,7 @@ contract IntegrationAllo is IntegrationBase { _sendWithRelayer( userAddr, address(allo), - abi.encodeWithSelector(allo.allocate.selector, poolId, _recipients, _amounts, abi.encode(_tokens)), + abi.encodeWithSelector(allo.allocate.selector, poolId, _recipients, _amounts, abi.encode(_tokens, _permits)), userPk ); // Strategy still has 120k DAI, userAddr has 10k DAI @@ -180,7 +181,7 @@ contract IntegrationAllo is IntegrationBase { // Set payout (it's needed to distribute) vm.prank(userAddr); - deployedStrategy.setPayout(_recipientsToDistribute, _amountsToDistribute); + deployedStrategy.setPayout(abi.encode(_recipientsToDistribute, _amountsToDistribute)); // Distribute _sendWithRelayer( @@ -291,14 +292,15 @@ contract IntegrationAllo is IntegrationBase { address[] memory _recipients = new address[](1); uint256[] memory _amounts = new uint256[](1); address[] memory _tokens = new address[](1); + bytes[] memory _permits = new bytes[](1); _tokens[0] = DAI; _amounts[0] = 10_000 ether; _recipients[0] = recipient0Addr; - allo.allocate(poolId, _recipients, _amounts, abi.encode(_tokens)); + allo.allocate(poolId, _recipients, _amounts, abi.encode(_tokens, _permits)); _recipients[0] = recipient1Addr; - allo.allocate(poolId, _recipients, _amounts, abi.encode(_tokens)); + allo.allocate(poolId, _recipients, _amounts, abi.encode(_tokens, _permits)); // Strategy still has 120k DAI, userAddr has 10k DAI assertTrue(IERC20(DAI).balanceOf(address(deployedStrategy)) == 120_000 ether); assertTrue(IERC20(DAI).balanceOf(userAddr) == 10_000 ether); @@ -321,7 +323,7 @@ contract IntegrationAllo is IntegrationBase { _amountsToDistribute[2] = 35_000 ether; // Set payout (it's needed to distribute) - deployedStrategy.setPayout(_recipientsToDistribute, _amountsToDistribute); + deployedStrategy.setPayout(abi.encode(_recipientsToDistribute, _amountsToDistribute)); // Distribute allo.distribute(poolId, _recipientsToDistribute, bytes("")); diff --git a/test/foundry/integration/DonationVotingMerkleDistribution.t.sol b/test/foundry/integration/DonationVotingMerkleDistribution.t.sol new file mode 100644 index 000000000..9576077e4 --- /dev/null +++ b/test/foundry/integration/DonationVotingMerkleDistribution.t.sol @@ -0,0 +1,709 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.19; + +import {IAllo} from "contracts/core/interfaces/IAllo.sol"; +import {Metadata} from "contracts/core/Registry.sol"; +import { + DonationVotingMerkleDistribution, + DonationVotingOffchain +} from "contracts/strategies/DonationVotingMerkleDistribution.sol"; +import {Errors} from "contracts/core/libraries/Errors.sol"; +import {IRecipientsExtension} from "contracts/extensions/interfaces/IRecipientsExtension.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IntegrationBase} from "./IntegrationBase.sol"; + +contract IntegrationDonationVotingMerkleDistributionBase is IntegrationBase { + uint256 internal constant POOL_AMOUNT = 1000; + + IAllo internal allo; + DonationVotingMerkleDistribution internal strategy; + DonationVotingMerkleDistribution internal strategyWithDirectTransfers; + + address internal allocationToken = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; // USDC + + address internal allocator0; + address internal allocator1; + + uint256 internal poolId; + + uint64 internal registrationStartTime; + uint64 internal registrationEndTime; + uint64 internal allocationStartTime; + uint64 internal allocationEndTime; + uint64 internal withdrawalCooldown = 1 days; + + function setUp() public virtual override { + super.setUp(); + + allo = IAllo(ALLO_PROXY); + + allocator0 = makeAddr("allocator0"); + allocator1 = makeAddr("allocator1"); + + strategy = new DonationVotingMerkleDistribution(address(allo), false); + strategyWithDirectTransfers = new DonationVotingMerkleDistribution(address(allo), true); + + // Deal + deal(DAI, userAddr, POOL_AMOUNT * 2); + vm.prank(userAddr); + IERC20(DAI).approve(address(allo), POOL_AMOUNT * 2); + + // Creating pool (and deploying strategy) + address[] memory managers = new address[](1); + managers[0] = userAddr; + vm.startPrank(userAddr); + + registrationStartTime = uint64(block.timestamp); + registrationEndTime = uint64(block.timestamp + 7 days); + allocationStartTime = uint64(block.timestamp + 7 days + 1); + allocationEndTime = uint64(block.timestamp + 10 days); + address[] memory allowedTokens = new address[](0); + + // Deploy strategy with direct transfers disabled + poolId = allo.createPoolWithCustomStrategy( + profileId, + address(strategy), + abi.encode( + IRecipientsExtension.RecipientInitializeData({ + metadataRequired: false, + registrationStartTime: registrationStartTime, + registrationEndTime: registrationEndTime + }), + allocationStartTime, + allocationEndTime, + withdrawalCooldown, + allowedTokens + ), + DAI, + POOL_AMOUNT, + Metadata({protocol: 0, pointer: ""}), + managers + ); + + // Deploy strategy with direct transfers enabled + allo.createPoolWithCustomStrategy( + profileId, + address(strategyWithDirectTransfers), + abi.encode( + IRecipientsExtension.RecipientInitializeData({ + metadataRequired: false, + registrationStartTime: registrationStartTime, + registrationEndTime: registrationEndTime + }), + allocationStartTime, + allocationEndTime, + withdrawalCooldown, + allowedTokens + ), + DAI, + POOL_AMOUNT, + Metadata({protocol: 0, pointer: ""}), + managers + ); + + // Adding recipients + vm.startPrank(address(allo)); + + address[] memory recipients = new address[](1); + bytes[] memory data = new bytes[](1); + + recipients[0] = recipient0Addr; + uint256 proposalBid = 10; + data[0] = abi.encode(address(0), Metadata({protocol: 0, pointer: ""}), abi.encode(uint256(proposalBid))); + strategy.register(recipients, abi.encode(data), recipient0Addr); + strategyWithDirectTransfers.register(recipients, abi.encode(data), recipient0Addr); + + recipients[0] = recipient1Addr; + proposalBid = 20; + data[0] = abi.encode(address(0), Metadata({protocol: 0, pointer: ""}), abi.encode(uint256(proposalBid))); + strategy.register(recipients, abi.encode(data), recipient1Addr); + strategyWithDirectTransfers.register(recipients, abi.encode(data), recipient1Addr); + + recipients[0] = recipient2Addr; + proposalBid = 30; + data[0] = abi.encode(address(0), Metadata({protocol: 0, pointer: ""}), abi.encode(uint256(proposalBid))); + strategy.register(recipients, abi.encode(data), recipient2Addr); + strategyWithDirectTransfers.register(recipients, abi.encode(data), recipient2Addr); + + vm.stopPrank(); + + // NOTE: removing all the ETH from the strategy before testing + vm.prank(address(strategy)); + address(0).call{value: address(strategy).balance}(""); + } +} + +contract IntegrationDonationVotingMerkleDistributionReviewRecipients is + IntegrationDonationVotingMerkleDistributionBase +{ + function test_reviewRecipients() public { + // Review recipients + vm.startPrank(userAddr); + + IRecipientsExtension.ApplicationStatus[] memory statuses = new IRecipientsExtension.ApplicationStatus[](1); + uint256 recipientsCounter = strategy.recipientsCounter(); + + address[] memory _recipientIds = new address[](2); + uint256[] memory _newStatuses = new uint256[](2); + + // Set accepted recipients + _recipientIds[0] = recipient0Addr; + _recipientIds[1] = recipient1Addr; + _newStatuses[0] = uint256(IRecipientsExtension.Status.Accepted); + _newStatuses[1] = uint256(IRecipientsExtension.Status.Accepted); + statuses[0] = _getApplicationStatus(_recipientIds, _newStatuses, address(strategy)); + strategy.reviewRecipients(statuses, recipientsCounter); + + // Revert if the registration period has finished + vm.warp(registrationEndTime + 1); + _newStatuses[1] = uint256(IRecipientsExtension.Status.Rejected); + statuses[0] = _getApplicationStatus(_recipientIds, _newStatuses, address(strategy)); + vm.expectRevert(Errors.REGISTRATION_NOT_ACTIVE.selector); + strategy.reviewRecipients(statuses, recipientsCounter); + + vm.stopPrank(); + } +} + +contract IntegrationDonationVotingMerkleDistributionTimestamps is IntegrationDonationVotingMerkleDistributionBase { + function test_updateTimestamps() public { + vm.warp(registrationStartTime - 1 days); + + // Review recipients + vm.startPrank(userAddr); + + vm.expectRevert(DonationVotingOffchain.INVALID_TIMESTAMPS.selector); + // allocationStartTime > allocationEndTime + strategy.updatePoolTimestamps( + registrationStartTime, registrationEndTime, allocationEndTime, allocationStartTime + ); + + vm.expectRevert(DonationVotingOffchain.INVALID_TIMESTAMPS.selector); + // _registrationStartTime > _registrationEndTime + strategy.updatePoolTimestamps( + registrationEndTime, registrationStartTime, allocationStartTime, allocationEndTime + ); + + vm.expectRevert(DonationVotingOffchain.INVALID_TIMESTAMPS.selector); + // _registrationStartTime > allocationStartTime + strategy.updatePoolTimestamps( + allocationStartTime + 1, allocationEndTime, allocationStartTime, allocationEndTime + ); + + vm.expectRevert(DonationVotingOffchain.INVALID_TIMESTAMPS.selector); + // _registrationEndTime > allocationEndTime + strategy.updatePoolTimestamps( + registrationStartTime, allocationEndTime + 1, allocationStartTime, allocationEndTime + ); + + vm.warp(registrationStartTime + 1); + vm.expectRevert(DonationVotingOffchain.INVALID_TIMESTAMPS.selector); + // block.timestamp > _registrationStartTime + strategy.updatePoolTimestamps( + registrationStartTime, registrationEndTime, allocationStartTime, allocationEndTime + ); + + vm.stopPrank(); + } +} + +contract IntegrationDonationVotingMerkleDistributionAllocateERC20 is IntegrationDonationVotingMerkleDistributionBase { + function setUp() public override { + super.setUp(); + + // Review recipients + vm.startPrank(userAddr); + + IRecipientsExtension.ApplicationStatus[] memory statuses = new IRecipientsExtension.ApplicationStatus[](1); + address[] memory _recipientIds = new address[](2); + uint256[] memory _newStatuses = new uint256[](2); + + // Set accepted recipients + _recipientIds[0] = recipient0Addr; + _recipientIds[1] = recipient1Addr; + _newStatuses[0] = uint256(IRecipientsExtension.Status.Accepted); + _newStatuses[1] = uint256(IRecipientsExtension.Status.Accepted); + statuses[0] = _getApplicationStatus(_recipientIds, _newStatuses, address(strategy)); + + uint256 recipientsCounter = strategy.recipientsCounter(); + strategy.reviewRecipients(statuses, recipientsCounter); + + vm.stopPrank(); + } + + function test_allocate() public { + vm.warp(allocationStartTime); + deal(allocationToken, allocator0, 4 + 25); + vm.startPrank(allocator0); + IERC20(allocationToken).approve(address(strategy), 4 + 25); + + address[] memory recipients = new address[](2); + recipients[0] = recipient0Addr; + recipients[1] = recipient1Addr; + + uint256[] memory amounts = new uint256[](2); + amounts[0] = 4; + amounts[1] = 25; + + address[] memory tokens = new address[](2); + tokens[0] = allocationToken; + tokens[1] = allocationToken; + + bytes[] memory permits = new bytes[](2); + + bytes memory data = abi.encode(tokens, permits); + + vm.startPrank(address(allo)); + + strategy.allocate(recipients, amounts, data, allocator0); + + assertEq(IERC20(allocationToken).balanceOf(allocator0), 0); + assertEq(IERC20(allocationToken).balanceOf(address(strategy)), 4 + 25); + + recipients[0] = recipient2Addr; + vm.expectRevert(Errors.RECIPIENT_NOT_ACCEPTED.selector); + strategy.allocate(recipients, amounts, data, allocator0); + + vm.stopPrank(); + } +} + +contract IntegrationDonationVotingMerkleDistributionAllocateETH is IntegrationDonationVotingMerkleDistributionBase { + function setUp() public override { + super.setUp(); + + // Review recipients + vm.startPrank(userAddr); + + IRecipientsExtension.ApplicationStatus[] memory statuses = new IRecipientsExtension.ApplicationStatus[](1); + address[] memory _recipientIds = new address[](2); + uint256[] memory _newStatuses = new uint256[](2); + + // Set accepted recipients + _recipientIds[0] = recipient0Addr; + _recipientIds[1] = recipient1Addr; + _newStatuses[0] = uint256(IRecipientsExtension.Status.Accepted); + _newStatuses[1] = uint256(IRecipientsExtension.Status.Accepted); + statuses[0] = _getApplicationStatus(_recipientIds, _newStatuses, address(strategy)); + + uint256 recipientsCounter = strategy.recipientsCounter(); + strategy.reviewRecipients(statuses, recipientsCounter); + + vm.stopPrank(); + } + + function test_allocate() public { + vm.warp(allocationStartTime); + + deal(allocationToken, allocator0, 4); + vm.startPrank(allocator0); + IERC20(allocationToken).approve(address(strategy), 4); + + vm.deal(address(allo), 25); + + address[] memory recipients = new address[](2); + recipients[0] = recipient0Addr; + recipients[1] = recipient1Addr; + + uint256[] memory amounts = new uint256[](2); + amounts[0] = 4; + amounts[1] = 25; + + vm.startPrank(address(allo)); + + address[] memory tokens = new address[](2); + tokens[0] = allocationToken; + tokens[1] = NATIVE; + + bytes[] memory permits = new bytes[](2); + + bytes memory data = abi.encode(tokens, permits); + + strategy.allocate{value: 25}(recipients, amounts, data, allocator0); + + assertEq(allocator0.balance, 0); + assertEq(address(strategy).balance, 25); + assertEq(IERC20(allocationToken).balanceOf(allocator0), 0); + assertEq(IERC20(allocationToken).balanceOf(address(strategy)), 4); + + recipients[0] = recipient2Addr; + vm.expectRevert(Errors.RECIPIENT_NOT_ACCEPTED.selector); + strategy.allocate(recipients, amounts, data, allocator0); + + vm.stopPrank(); + } +} + +contract IntegrationDonationVotingMerkleDistributionDirectAllocateERC20 is + IntegrationDonationVotingMerkleDistributionBase +{ + function setUp() public override { + super.setUp(); + + // Review recipients + vm.startPrank(userAddr); + + IRecipientsExtension.ApplicationStatus[] memory statuses = new IRecipientsExtension.ApplicationStatus[](1); + address[] memory _recipientIds = new address[](2); + uint256[] memory _newStatuses = new uint256[](2); + + // Set accepted recipients + _recipientIds[0] = recipient0Addr; + _recipientIds[1] = recipient1Addr; + _newStatuses[0] = uint256(IRecipientsExtension.Status.Accepted); + _newStatuses[1] = uint256(IRecipientsExtension.Status.Accepted); + statuses[0] = _getApplicationStatus(_recipientIds, _newStatuses, address(strategyWithDirectTransfers)); + + uint256 recipientsCounter = strategyWithDirectTransfers.recipientsCounter(); + strategyWithDirectTransfers.reviewRecipients(statuses, recipientsCounter); + + vm.stopPrank(); + } + + function test_allocate() public { + vm.warp(allocationStartTime); + deal(allocationToken, allocator0, 4 + 25); + vm.startPrank(allocator0); + IERC20(allocationToken).approve(address(strategyWithDirectTransfers), 4 + 25); + + address[] memory recipients = new address[](2); + recipients[0] = recipient0Addr; + recipients[1] = recipient1Addr; + + uint256[] memory amounts = new uint256[](2); + amounts[0] = 4; + amounts[1] = 25; + + address[] memory tokens = new address[](2); + tokens[0] = allocationToken; + tokens[1] = allocationToken; + + bytes[] memory permits = new bytes[](2); + + bytes memory data = abi.encode(tokens, permits); + + vm.startPrank(address(allo)); + + strategyWithDirectTransfers.allocate(recipients, amounts, data, allocator0); + + assertEq(IERC20(allocationToken).balanceOf(allocator0), 0); + assertEq(IERC20(allocationToken).balanceOf(address(strategyWithDirectTransfers)), 0); + assertEq(IERC20(allocationToken).balanceOf(recipient0Addr), 4); + assertEq(IERC20(allocationToken).balanceOf(recipient1Addr), 25); + + uint256 amountAllocated0 = strategyWithDirectTransfers.amountAllocated(recipient0Addr, allocationToken); + uint256 amountAllocated1 = strategyWithDirectTransfers.amountAllocated(recipient0Addr, allocationToken); + assertEq(amountAllocated0, 0); + assertEq(amountAllocated1, 0); + + recipients[0] = recipient2Addr; + vm.expectRevert(Errors.RECIPIENT_NOT_ACCEPTED.selector); + strategyWithDirectTransfers.allocate(recipients, amounts, data, allocator0); + + vm.stopPrank(); + } +} + +contract IntegrationDonationVotingMerkleDistributionDirectAllocateETH is + IntegrationDonationVotingMerkleDistributionBase +{ + function setUp() public override { + super.setUp(); + + // Review recipients + vm.startPrank(userAddr); + + IRecipientsExtension.ApplicationStatus[] memory statuses = new IRecipientsExtension.ApplicationStatus[](1); + address[] memory _recipientIds = new address[](2); + uint256[] memory _newStatuses = new uint256[](2); + + // Set accepted recipients + _recipientIds[0] = recipient0Addr; + _recipientIds[1] = recipient1Addr; + _newStatuses[0] = uint256(IRecipientsExtension.Status.Accepted); + _newStatuses[1] = uint256(IRecipientsExtension.Status.Accepted); + statuses[0] = _getApplicationStatus(_recipientIds, _newStatuses, address(strategyWithDirectTransfers)); + + uint256 recipientsCounter = strategyWithDirectTransfers.recipientsCounter(); + strategyWithDirectTransfers.reviewRecipients(statuses, recipientsCounter); + + vm.stopPrank(); + } + + function test_allocate() public { + vm.warp(allocationStartTime); + + deal(allocationToken, allocator0, 4); + vm.startPrank(allocator0); + IERC20(allocationToken).approve(address(strategyWithDirectTransfers), 4); + + vm.deal(address(allo), 25); + + address[] memory recipients = new address[](2); + recipients[0] = recipient0Addr; + recipients[1] = recipient1Addr; + + uint256[] memory amounts = new uint256[](2); + amounts[0] = 4; + amounts[1] = 25; + + vm.startPrank(address(allo)); + + address[] memory tokens = new address[](2); + tokens[0] = allocationToken; + tokens[1] = NATIVE; + + bytes[] memory permits = new bytes[](2); + + bytes memory data = abi.encode(tokens, permits); + + strategyWithDirectTransfers.allocate{value: 25}(recipients, amounts, data, allocator0); + + assertEq(allocator0.balance, 0); + assertEq(address(strategyWithDirectTransfers).balance, 0); + assertEq(recipient1Addr.balance, 25); + + assertEq(IERC20(allocationToken).balanceOf(allocator0), 0); + assertEq(IERC20(allocationToken).balanceOf(address(strategyWithDirectTransfers)), 0); + assertEq(IERC20(allocationToken).balanceOf(recipient0Addr), 4); + + recipients[0] = recipient2Addr; + vm.expectRevert(Errors.RECIPIENT_NOT_ACCEPTED.selector); + strategyWithDirectTransfers.allocate(recipients, amounts, data, allocator0); + + vm.stopPrank(); + } +} + +contract IntegrationDonationVotingMerkleDistributionClaim is IntegrationDonationVotingMerkleDistributionBase { + function setUp() public override { + super.setUp(); + + // Review recipients + vm.startPrank(userAddr); + + IRecipientsExtension.ApplicationStatus[] memory statuses = new IRecipientsExtension.ApplicationStatus[](1); + address[] memory _recipientIds = new address[](2); + uint256[] memory _newStatuses = new uint256[](2); + + // Set accepted recipients + _recipientIds[0] = recipient0Addr; + _recipientIds[1] = recipient1Addr; + _newStatuses[0] = uint256(IRecipientsExtension.Status.Accepted); + _newStatuses[1] = uint256(IRecipientsExtension.Status.Accepted); + statuses[0] = _getApplicationStatus(_recipientIds, _newStatuses, address(strategy)); + + uint256 recipientsCounter = strategy.recipientsCounter(); + strategy.reviewRecipients(statuses, recipientsCounter); + + vm.stopPrank(); + + vm.warp(allocationStartTime); + deal(allocationToken, allocator0, 4 + 25); + vm.startPrank(allocator0); + IERC20(allocationToken).approve(address(strategy), 4 + 25); + + address[] memory recipients = new address[](2); + recipients[0] = recipient0Addr; + recipients[1] = recipient1Addr; + + uint256[] memory amounts = new uint256[](2); + amounts[0] = 4; + amounts[1] = 25; + + address[] memory tokens = new address[](2); + tokens[0] = allocationToken; + tokens[1] = allocationToken; + + bytes[] memory permits = new bytes[](2); + + bytes memory data = abi.encode(tokens, permits); + + vm.startPrank(address(allo)); + strategy.allocate(recipients, amounts, data, allocator0); + vm.stopPrank(); + } + + function test_claim() public { + // Claim allocation funds + vm.warp(allocationEndTime + 1); + + vm.startPrank(recipient0Addr); + + DonationVotingMerkleDistribution.Claim[] memory claims = new DonationVotingMerkleDistribution.Claim[](2); + claims[0].recipientId = recipient0Addr; + claims[0].token = allocationToken; + claims[1].recipientId = recipient1Addr; + claims[1].token = allocationToken; + + strategy.claimAllocation(abi.encode(claims)); + + assertEq(IERC20(allocationToken).balanceOf(recipient0Addr), 4); + assertEq(IERC20(allocationToken).balanceOf(recipient1Addr), 25); + assertEq(IERC20(allocationToken).balanceOf(address(strategy)), 0); + + vm.stopPrank(); + } +} + +contract IntegrationDonationVotingMerkleDistributionDisabledClaim is IntegrationDonationVotingMerkleDistributionBase { + function test_claim() public { + // Claim allocation funds + vm.warp(allocationEndTime + 1); + + vm.startPrank(recipient0Addr); + + DonationVotingMerkleDistribution.Claim[] memory claims = new DonationVotingMerkleDistribution.Claim[](2); + claims[0].recipientId = recipient0Addr; + claims[0].token = allocationToken; + claims[1].recipientId = recipient1Addr; + claims[1].token = allocationToken; + + vm.expectRevert(Errors.NOT_IMPLEMENTED.selector); + strategyWithDirectTransfers.claimAllocation(abi.encode(claims)); + + vm.stopPrank(); + } +} + +contract IntegrationDonationVotingMerkleDistributionSetPayout is IntegrationDonationVotingMerkleDistributionBase { + function setUp() public override { + super.setUp(); + + // Review recipients + vm.startPrank(userAddr); + + IRecipientsExtension.ApplicationStatus[] memory statuses = new IRecipientsExtension.ApplicationStatus[](1); + address[] memory _recipientIds = new address[](2); + uint256[] memory _newStatuses = new uint256[](2); + + // Set accepted recipients + _recipientIds[0] = recipient0Addr; + _recipientIds[1] = recipient1Addr; + _newStatuses[0] = uint256(IRecipientsExtension.Status.Accepted); + _newStatuses[1] = uint256(IRecipientsExtension.Status.Accepted); + statuses[0] = _getApplicationStatus(_recipientIds, _newStatuses, address(strategy)); + + uint256 recipientsCounter = strategy.recipientsCounter(); + strategy.reviewRecipients(statuses, recipientsCounter); + + vm.stopPrank(); + } + + function test_setPayout() public { + vm.warp(allocationEndTime + 1); + + vm.startPrank(userAddr); + + bytes32 merkleRoot = keccak256(abi.encode("merkleRoot")); + Metadata memory distributionMetadata = Metadata({protocol: 1, pointer: "A"}); + strategy.setPayout(abi.encode(merkleRoot, distributionMetadata)); + + (uint256 protocol, string memory pointer) = strategy.distributionMetadata(); + assertEq(strategy.merkleRoot(), merkleRoot); + assertEq(protocol, 1); + assertEq(pointer, "A"); + + vm.stopPrank(); + } +} + +contract IntegrationDonationVotingMerkleDistributionDistribute is IntegrationDonationVotingMerkleDistributionBase { + DonationVotingMerkleDistribution.Distribution[] internal _distributions; + bytes32 internal _merkleRoot; + + function setUp() public override { + super.setUp(); + + // Review recipients + vm.startPrank(userAddr); + + IRecipientsExtension.ApplicationStatus[] memory statuses = new IRecipientsExtension.ApplicationStatus[](1); + address[] memory _recipientIds = new address[](2); + uint256[] memory _newStatuses = new uint256[](2); + + // Set accepted recipients + _recipientIds[0] = recipient0Addr; + _recipientIds[1] = recipient1Addr; + _newStatuses[0] = uint256(IRecipientsExtension.Status.Accepted); + _newStatuses[1] = uint256(IRecipientsExtension.Status.Accepted); + statuses[0] = _getApplicationStatus(_recipientIds, _newStatuses, address(strategy)); + + uint256 recipientsCounter = strategy.recipientsCounter(); + strategy.reviewRecipients(statuses, recipientsCounter); + + // Set payouts + vm.warp(allocationEndTime + 1); + vm.startPrank(userAddr); + + address[] memory recipients = new address[](2); + recipients[0] = recipient0Addr; + recipients[1] = recipient1Addr; + + uint256[] memory amounts = new uint256[](2); + amounts[0] = POOL_AMOUNT * 1 / 4; + amounts[1] = POOL_AMOUNT - POOL_AMOUNT * 1 / 4; + + (bytes32 merkleRoot, DonationVotingMerkleDistribution.Distribution[] memory distributions) = + _getMerkleRootAndDistributions(recipients, amounts); + _distributions.push(distributions[0]); + _distributions.push(distributions[1]); + Metadata memory distributionMetadata = Metadata({protocol: 1, pointer: "A"}); + strategy.setPayout(abi.encode(merkleRoot, distributionMetadata)); + vm.stopPrank(); + } + + function test_distribute() public { + address[] memory recipients = new address[](0); + + vm.startPrank(address(allo)); + + bytes memory data = abi.encode(_distributions); + strategy.distribute(recipients, data, recipient2Addr); + + assertEq(IERC20(DAI).balanceOf(recipient0Addr), POOL_AMOUNT * 1 / 4); + assertEq(IERC20(DAI).balanceOf(recipient1Addr), POOL_AMOUNT - POOL_AMOUNT * 1 / 4); + assertEq(IERC20(DAI).balanceOf(address(strategy)), 0); + assertEq(strategy.getPoolAmount(), 0); + + vm.expectRevert(abi.encodeWithSelector(DonationVotingMerkleDistribution.ALREADY_DISTRIBUTED.selector, 0)); + strategy.distribute(recipients, data, recipient2Addr); + + vm.startPrank(address(userAddr)); + vm.expectRevert(DonationVotingMerkleDistribution.DISTRIBUTION_ALREADY_STARTED.selector); + bytes32 merkleRoot = keccak256(abi.encode("merkleRoot")); + Metadata memory distributionMetadata = Metadata({protocol: 1, pointer: "A"}); + strategy.setPayout(abi.encode(merkleRoot, distributionMetadata)); + } + + function _getMerkleRootAndDistributions(address[] memory _recipientIds, uint256[] memory _amounts) + internal + pure + returns (bytes32, DonationVotingMerkleDistribution.Distribution[] memory) + { + DonationVotingMerkleDistribution.Distribution[] memory distributions = + new DonationVotingMerkleDistribution.Distribution[](2); + + DonationVotingMerkleDistribution.Distribution memory distribution0 = DonationVotingMerkleDistribution + .Distribution({index: 0, recipientId: _recipientIds[0], amount: _amounts[0], merkleProof: new bytes32[](1)}); + bytes32 node0 = keccak256(abi.encode(distribution0.index, distribution0.recipientId, distribution0.amount)); + + DonationVotingMerkleDistribution.Distribution memory distribution1 = DonationVotingMerkleDistribution + .Distribution({index: 1, recipientId: _recipientIds[1], amount: _amounts[1], merkleProof: new bytes32[](1)}); + bytes32 node1 = keccak256(abi.encode(distribution1.index, distribution1.recipientId, distribution1.amount)); + + distribution0.merkleProof[0] = node1; + distribution1.merkleProof[0] = node0; + + distributions[0] = distribution0; + distributions[1] = distribution1; + + bytes32 merkleRoot = _hashPair(node1, node0); + + return (merkleRoot, distributions); + } + + function _hashPair(bytes32 a, bytes32 b) internal pure returns (bytes32) { + return a < b ? keccak256(abi.encode(a, b)) : keccak256(abi.encode(b, a)); + } +} diff --git a/test/foundry/integration/DonationVotingOffchain.t.sol b/test/foundry/integration/DonationVotingOffchain.t.sol index b0c935bbd..c4f91c218 100644 --- a/test/foundry/integration/DonationVotingOffchain.t.sol +++ b/test/foundry/integration/DonationVotingOffchain.t.sol @@ -10,11 +10,11 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IntegrationBase} from "./IntegrationBase.sol"; contract IntegrationDonationVotingOffchainBase is IntegrationBase { - address internal constant NATIVE = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; uint256 internal constant POOL_AMOUNT = 1000; IAllo internal allo; DonationVotingOffchain internal strategy; + DonationVotingOffchain internal strategyWithDirectTransfers; address internal allocationToken = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; // USDC @@ -37,23 +37,26 @@ contract IntegrationDonationVotingOffchainBase is IntegrationBase { allocator0 = makeAddr("allocator0"); allocator1 = makeAddr("allocator1"); - strategy = new DonationVotingOffchain(address(allo)); + strategy = new DonationVotingOffchain(address(allo), false); + strategyWithDirectTransfers = new DonationVotingOffchain(address(allo), true); // Deal - deal(DAI, userAddr, POOL_AMOUNT); + deal(DAI, userAddr, POOL_AMOUNT * 2); vm.prank(userAddr); - IERC20(DAI).approve(address(allo), POOL_AMOUNT); + IERC20(DAI).approve(address(allo), POOL_AMOUNT * 2); // Creating pool (and deploying strategy) address[] memory managers = new address[](1); managers[0] = userAddr; - vm.prank(userAddr); + vm.startPrank(userAddr); registrationStartTime = uint64(block.timestamp); registrationEndTime = uint64(block.timestamp + 7 days); allocationStartTime = uint64(block.timestamp + 7 days + 1); allocationEndTime = uint64(block.timestamp + 10 days); address[] memory allowedTokens = new address[](0); + + // Deploy strategy with direct transfers disabled poolId = allo.createPoolWithCustomStrategy( profileId, address(strategy), @@ -74,6 +77,27 @@ contract IntegrationDonationVotingOffchainBase is IntegrationBase { managers ); + // Deploy strategy with direct transfers enabled + allo.createPoolWithCustomStrategy( + profileId, + address(strategyWithDirectTransfers), + abi.encode( + IRecipientsExtension.RecipientInitializeData({ + metadataRequired: false, + registrationStartTime: registrationStartTime, + registrationEndTime: registrationEndTime + }), + allocationStartTime, + allocationEndTime, + withdrawalCooldown, + allowedTokens + ), + DAI, + POOL_AMOUNT, + Metadata({protocol: 0, pointer: ""}), + managers + ); + // Adding recipients vm.startPrank(address(allo)); @@ -84,16 +108,19 @@ contract IntegrationDonationVotingOffchainBase is IntegrationBase { uint256 proposalBid = 10; data[0] = abi.encode(address(0), Metadata({protocol: 0, pointer: ""}), abi.encode(uint256(proposalBid))); strategy.register(recipients, abi.encode(data), recipient0Addr); + strategyWithDirectTransfers.register(recipients, abi.encode(data), recipient0Addr); recipients[0] = recipient1Addr; proposalBid = 20; data[0] = abi.encode(address(0), Metadata({protocol: 0, pointer: ""}), abi.encode(uint256(proposalBid))); strategy.register(recipients, abi.encode(data), recipient1Addr); + strategyWithDirectTransfers.register(recipients, abi.encode(data), recipient1Addr); recipients[0] = recipient2Addr; proposalBid = 30; data[0] = abi.encode(address(0), Metadata({protocol: 0, pointer: ""}), abi.encode(uint256(proposalBid))); strategy.register(recipients, abi.encode(data), recipient2Addr); + strategyWithDirectTransfers.register(recipients, abi.encode(data), recipient2Addr); vm.stopPrank(); @@ -216,7 +243,10 @@ contract IntegrationDonationVotingOffchainAllocateERC20 is IntegrationDonationVo address[] memory tokens = new address[](2); tokens[0] = allocationToken; tokens[1] = allocationToken; - bytes memory data = abi.encode(tokens); + + bytes[] memory permits = new bytes[](2); + + bytes memory data = abi.encode(tokens, permits); vm.startPrank(address(allo)); @@ -279,7 +309,10 @@ contract IntegrationDonationVotingOffchainAllocateETH is IntegrationDonationVoti address[] memory tokens = new address[](2); tokens[0] = allocationToken; tokens[1] = NATIVE; - bytes memory data = abi.encode(tokens); + + bytes[] memory permits = new bytes[](2); + + bytes memory data = abi.encode(tokens, permits); strategy.allocate{value: 25}(recipients, amounts, data, allocator0); @@ -296,6 +329,143 @@ contract IntegrationDonationVotingOffchainAllocateETH is IntegrationDonationVoti } } +contract IntegrationDonationVotingOffchainDirectAllocateERC20 is IntegrationDonationVotingOffchainBase { + function setUp() public override { + super.setUp(); + + // Review recipients + vm.startPrank(userAddr); + + IRecipientsExtension.ApplicationStatus[] memory statuses = new IRecipientsExtension.ApplicationStatus[](1); + address[] memory _recipientIds = new address[](2); + uint256[] memory _newStatuses = new uint256[](2); + + // Set accepted recipients + _recipientIds[0] = recipient0Addr; + _recipientIds[1] = recipient1Addr; + _newStatuses[0] = uint256(IRecipientsExtension.Status.Accepted); + _newStatuses[1] = uint256(IRecipientsExtension.Status.Accepted); + statuses[0] = _getApplicationStatus(_recipientIds, _newStatuses, address(strategyWithDirectTransfers)); + + uint256 recipientsCounter = strategyWithDirectTransfers.recipientsCounter(); + strategyWithDirectTransfers.reviewRecipients(statuses, recipientsCounter); + + vm.stopPrank(); + } + + function test_allocate() public { + vm.warp(allocationStartTime); + deal(allocationToken, allocator0, 4 + 25); + vm.startPrank(allocator0); + IERC20(allocationToken).approve(address(strategyWithDirectTransfers), 4 + 25); + + address[] memory recipients = new address[](2); + recipients[0] = recipient0Addr; + recipients[1] = recipient1Addr; + + uint256[] memory amounts = new uint256[](2); + amounts[0] = 4; + amounts[1] = 25; + + address[] memory tokens = new address[](2); + tokens[0] = allocationToken; + tokens[1] = allocationToken; + + bytes[] memory permits = new bytes[](2); + + bytes memory data = abi.encode(tokens, permits); + + vm.startPrank(address(allo)); + + strategyWithDirectTransfers.allocate(recipients, amounts, data, allocator0); + + assertEq(IERC20(allocationToken).balanceOf(allocator0), 0); + assertEq(IERC20(allocationToken).balanceOf(address(strategyWithDirectTransfers)), 0); + assertEq(IERC20(allocationToken).balanceOf(recipient0Addr), 4); + assertEq(IERC20(allocationToken).balanceOf(recipient1Addr), 25); + + uint256 amountAllocated0 = strategyWithDirectTransfers.amountAllocated(recipient0Addr, allocationToken); + uint256 amountAllocated1 = strategyWithDirectTransfers.amountAllocated(recipient1Addr, allocationToken); + assertEq(amountAllocated0, 0); + assertEq(amountAllocated1, 0); + + recipients[0] = recipient2Addr; + vm.expectRevert(Errors.RECIPIENT_NOT_ACCEPTED.selector); + strategyWithDirectTransfers.allocate(recipients, amounts, data, allocator0); + + vm.stopPrank(); + } +} + +contract IntegrationDonationVotingOffchainDirectAllocateETH is IntegrationDonationVotingOffchainBase { + function setUp() public override { + super.setUp(); + + // Review recipients + vm.startPrank(userAddr); + + IRecipientsExtension.ApplicationStatus[] memory statuses = new IRecipientsExtension.ApplicationStatus[](1); + address[] memory _recipientIds = new address[](2); + uint256[] memory _newStatuses = new uint256[](2); + + // Set accepted recipients + _recipientIds[0] = recipient0Addr; + _recipientIds[1] = recipient1Addr; + _newStatuses[0] = uint256(IRecipientsExtension.Status.Accepted); + _newStatuses[1] = uint256(IRecipientsExtension.Status.Accepted); + statuses[0] = _getApplicationStatus(_recipientIds, _newStatuses, address(strategyWithDirectTransfers)); + + uint256 recipientsCounter = strategyWithDirectTransfers.recipientsCounter(); + strategyWithDirectTransfers.reviewRecipients(statuses, recipientsCounter); + + vm.stopPrank(); + } + + function test_allocate() public { + vm.warp(allocationStartTime); + + deal(allocationToken, allocator0, 4); + vm.startPrank(allocator0); + IERC20(allocationToken).approve(address(strategyWithDirectTransfers), 4); + + vm.deal(address(allo), 25); + + address[] memory recipients = new address[](2); + recipients[0] = recipient0Addr; + recipients[1] = recipient1Addr; + + uint256[] memory amounts = new uint256[](2); + amounts[0] = 4; + amounts[1] = 25; + + vm.startPrank(address(allo)); + + address[] memory tokens = new address[](2); + tokens[0] = allocationToken; + tokens[1] = NATIVE; + + bytes[] memory permits = new bytes[](2); + + bytes memory data = abi.encode(tokens, permits); + + strategyWithDirectTransfers.allocate{value: 25}(recipients, amounts, data, allocator0); + + assertEq(allocator0.balance, 0); + assertEq(address(strategyWithDirectTransfers).balance, 0); + assertEq(recipient1Addr.balance, 25); + + assertEq(IERC20(allocationToken).balanceOf(allocator0), 0); + assertEq(IERC20(allocationToken).balanceOf(address(strategyWithDirectTransfers)), 0); + assertEq(IERC20(allocationToken).balanceOf(recipient0Addr), 4); + + recipients[0] = recipient2Addr; + vm.expectRevert(Errors.RECIPIENT_NOT_ACCEPTED.selector); + strategyWithDirectTransfers.allocate(recipients, amounts, data, allocator0); + + vm.stopPrank(); + } +} + contract IntegrationDonationVotingOffchainClaim is IntegrationDonationVotingOffchainBase { function setUp() public override { super.setUp(); @@ -335,7 +505,10 @@ contract IntegrationDonationVotingOffchainClaim is IntegrationDonationVotingOffc address[] memory tokens = new address[](2); tokens[0] = allocationToken; tokens[1] = allocationToken; - bytes memory data = abi.encode(tokens); + + bytes[] memory permits = new bytes[](2); + + bytes memory data = abi.encode(tokens, permits); vm.startPrank(address(allo)); strategy.allocate(recipients, amounts, data, allocator0); @@ -354,7 +527,7 @@ contract IntegrationDonationVotingOffchainClaim is IntegrationDonationVotingOffc claims[1].recipientId = recipient1Addr; claims[1].token = allocationToken; - strategy.claimAllocation(claims); + strategy.claimAllocation(abi.encode(claims)); assertEq(IERC20(allocationToken).balanceOf(recipient0Addr), 4); assertEq(IERC20(allocationToken).balanceOf(recipient1Addr), 25); @@ -364,6 +537,26 @@ contract IntegrationDonationVotingOffchainClaim is IntegrationDonationVotingOffc } } +contract IntegrationDonationVotingOffchainDisabledClaim is IntegrationDonationVotingOffchainBase { + function test_claim() public { + // Claim allocation funds + vm.warp(allocationEndTime + 1); + + vm.startPrank(recipient0Addr); + + DonationVotingOffchain.Claim[] memory claims = new DonationVotingOffchain.Claim[](2); + claims[0].recipientId = recipient0Addr; + claims[0].token = allocationToken; + claims[1].recipientId = recipient1Addr; + claims[1].token = allocationToken; + + vm.expectRevert(Errors.NOT_IMPLEMENTED.selector); + strategyWithDirectTransfers.claimAllocation(abi.encode(claims)); + + vm.stopPrank(); + } +} + contract IntegrationDonationVotingOffchainSetPayout is IntegrationDonationVotingOffchainBase { function setUp() public override { super.setUp(); @@ -401,7 +594,7 @@ contract IntegrationDonationVotingOffchainSetPayout is IntegrationDonationVoting amounts[0] = POOL_AMOUNT * 1 / 4; amounts[1] = POOL_AMOUNT * 3 / 4; - strategy.setPayout(recipients, amounts); + strategy.setPayout(abi.encode(recipients, amounts)); (address recipientAddress, uint256 amount) = strategy.payoutSummaries(recipient0Addr); assertEq(amount, amounts[0]); @@ -413,11 +606,11 @@ contract IntegrationDonationVotingOffchainSetPayout is IntegrationDonationVoting // Reverts vm.expectRevert(abi.encodeWithSelector(DonationVotingOffchain.PAYOUT_ALREADY_SET.selector, recipient0Addr)); - strategy.setPayout(recipients, amounts); + strategy.setPayout(abi.encode(recipients, amounts)); recipients[0] = recipient2Addr; vm.expectRevert(Errors.RECIPIENT_NOT_ACCEPTED.selector); - strategy.setPayout(recipients, amounts); + strategy.setPayout(abi.encode(recipients, amounts)); vm.stopPrank(); } @@ -456,7 +649,7 @@ contract IntegrationDonationVotingOffchainDistribute is IntegrationDonationVotin amounts[0] = POOL_AMOUNT * 1 / 4; amounts[1] = POOL_AMOUNT - POOL_AMOUNT * 1 / 4; - strategy.setPayout(recipients, amounts); + strategy.setPayout(abi.encode(recipients, amounts)); vm.stopPrank(); } diff --git a/test/foundry/integration/DonationVotingOnchain.t.sol b/test/foundry/integration/DonationVotingOnchain.t.sol index 57270233d..f30c4f983 100644 --- a/test/foundry/integration/DonationVotingOnchain.t.sol +++ b/test/foundry/integration/DonationVotingOnchain.t.sol @@ -10,7 +10,6 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IntegrationBase} from "./IntegrationBase.sol"; contract IntegrationDonationVotingOnchainBase is IntegrationBase { - address internal constant NATIVE = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; uint256 internal constant POOL_AMOUNT = 1000; IAllo internal allo; diff --git a/test/foundry/integration/IntegrationBase.sol b/test/foundry/integration/IntegrationBase.sol index 8476718a7..992cbda5b 100644 --- a/test/foundry/integration/IntegrationBase.sol +++ b/test/foundry/integration/IntegrationBase.sol @@ -18,6 +18,7 @@ abstract contract IntegrationBase is Test { address public constant ALLO_PROXY = 0x1133eA7Af70876e64665ecD07C0A0476d09465a1; address public constant BICONOMY_FORWARDER = 0x84a0856b038eaAd1cC7E297cF34A7e72685A8693; address public constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + address public constant NATIVE = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; bytes32 public constant DOMAIN_SEPARATOR = 0x4fde6db5140ab711910b567033f2d5e64dc4f7123d722004dd748edf6ed07abb;