forked from allo-protocol/allo-v2
-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathDonationVotingMerkleDistribution.sol
More file actions
201 lines (164 loc) · 12.1 KB
/
Copy pathDonationVotingMerkleDistribution.sol
File metadata and controls
201 lines (164 loc) · 12.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.19;
// External Imports
// External Libraries
import {MerkleProof} from "openzeppelin-contracts/contracts/utils/cryptography/MerkleProof.sol";
// Internal Imports
// Interfaces
import {IAllo} from "contracts/core/interfaces/IAllo.sol";
// Core Contracts
import {DonationVotingOffchain} from "strategies/examples/donation-voting/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 DonationVotingMerkleDistribution_DistributionAlreadyStarted();
/// @notice Thrown when distribution is invoked but the merkle root has not been set yet
error DonationVotingMerkleDistribution_MerkleRootNotSet();
/// @notice Thrown when distribution is attempted twice for the same 'index'
/// @param _index The index for which a repeated distribution was attempted
error DonationVotingMerkleDistribution_AlreadyDistributed(uint256 _index);
/// ================================
/// ========== Struct ==============
/// ================================
/// @notice Stores the details of the distribution.
/// @param index The index in the merkle tree
/// @param recipientId The id of the recipient
/// @param amount The amount the should be distributed to the recipient
/// @param merkleProof The merkle proof
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, "DonationVotingMerkleDistribution", _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 DonationVotingMerkleDistribution_DistributionAlreadyStarted();
(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 DonationVotingMerkleDistribution_MerkleRootNotSet();
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; i++) {
_distributeSingle(distributions[i], pool.token, _sender);
}
}
/// @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 RecipientsExtension_RecipientNotAccepted();
// 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 DonationVotingMerkleDistribution_AlreadyDistributed(_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));
}
}
}