/
RecurringGrantDrop.sol
336 lines (270 loc) · 13.3 KB
/
RecurringGrantDrop.sol
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
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol";
import {Ownable2Step} from "openzeppelin-contracts/contracts/access/Ownable2Step.sol";
import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
import {IGrant} from "./IGrant.sol";
import {IWorldIDGroups} from "world-id-contracts/interfaces/IWorldIDGroups.sol";
import {ECDSA} from "openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol";
/// @title RecurringGrantDrop
/// @author Worldcoin
contract RecurringGrantDrop is Ownable2Step {
///////////////////////////////////////////////////////////////////////////////
/// CONFIG STORAGE ///
//////////////////////////////////////////////////////////////////////////////
/// @dev The WorldID router instance that will be used for managing groups and verifying proofs
IWorldIDGroups public worldIdRouter;
/// @dev The World ID group whose participants can claim this airdrop
uint256 public groupId;
/// @notice The ERC20 token airdropped
ERC20 public token;
/// @notice The address that holds the tokens that are being airdropped
/// @dev Make sure the holder has approved spending for this contract!
address public holder;
/// @notice The grant instance used
IGrant public grant;
/// @dev Whether a nullifier hash has been used already. Used to prevent double-signaling
mapping(uint256 => bool) public nullifierHashes;
/// @dev Allowed addresses to sign a reservation
mapping(address => bool) internal allowedSigners;
///////////////////////////////////////////////////////////////////////////////
/// ERRORS ///
//////////////////////////////////////////////////////////////////////////////
/// @notice Error in case the configuration is invalid.
error InvalidConfiguration();
/// @notice Error in case the receiver is zero address.
error InvalidReceiver();
/// @notice Error in case the receiver is zero address.
error InvalidTimestamp();
/// @notice Thrown when passed an invalid caller address
error InvalidReservationSigner();
/// @notice Thrown when passed an invalid caller address
error UnauthorizedSigner();
/// @notice Thrown when attempting to reuse a nullifier
error InvalidNullifier();
/// @notice Emmitted in revert if the owner attempts to resign ownership.
error CannotRenounceOwnership();
///////////////////////////////////////////////////////////////////////////////
/// EVENTS ///
//////////////////////////////////////////////////////////////////////////////
/// @notice Emitted when a grant is successfully claimed
/// @param _worldIdRouter The WorldID router that will manage groups and verify proofs
/// @param _groupId The group ID of the World ID
/// @param _token The ERC20 token that will be airdropped
/// @param _holder The address holding the tokens that will be airdropped
/// @param _grant The grant that contains the amounts and validity
event RecurringGrantDropInitialized(
IWorldIDGroups _worldIdRouter,
uint256 _groupId,
ERC20 _token,
address _holder,
IGrant _grant
);
/// @notice Emitted when a grant is successfully claimed
/// @param receiver The address that received the tokens
event GrantClaimed(uint256 grantId, address receiver);
/// @notice Emitted when the worldIdRouter is changed
/// @param worldIdRouter The new worldIdRouter instance
event WorldIdRouterUpdated(IWorldIDGroups worldIdRouter);
/// @notice Emitted when the groupId is changed
/// @param groupId The new groupId
event GroupIdUpdated(uint256 groupId);
/// @notice Emitted when the token is changed
/// @param token The new token
event TokenUpdated(ERC20 token);
/// @notice Emitted when the holder is changed
/// @param holder The new holder
event HolderUpdated(address holder);
/// @notice Emitted when the grant is changed
/// @param grant The new grant instance
event GrantUpdated(IGrant grant);
/// @notice Emitted when an allowed reservation signer is added
/// @param signer The new signer
event AllowedReservationSignerAdded(address signer);
/// @notice Emitted when an allowed reservation signer is removed
/// @param signer The new signer
event AllowedReservationSignerRemoved(address signer);
///////////////////////////////////////////////////////////////////////////////
/// CONSTRUCTOR ///
//////////////////////////////////////////////////////////////////////////////
/// @notice Deploys a WorldIDAirdrop instance
/// @param _worldIdRouter The WorldID router that will manage groups and verify proofs
/// @param _groupId The group ID of the World ID
/// @param _token The ERC20 token that will be airdropped
/// @param _holder The address holding the tokens that will be airdropped
/// @param _grant The grant that contains the amounts and validity
constructor(
IWorldIDGroups _worldIdRouter,
uint256 _groupId,
ERC20 _token,
address _holder,
IGrant _grant
) Ownable(msg.sender) {
if (address(_worldIdRouter) == address(0)) revert InvalidConfiguration();
if (address(_token) == address(0)) revert InvalidConfiguration();
if (address(_holder) == address(0)) revert InvalidConfiguration();
if (address(_grant) == address(0)) revert InvalidConfiguration();
worldIdRouter = _worldIdRouter;
groupId = _groupId;
token = _token;
holder = _holder;
grant = _grant;
emit RecurringGrantDropInitialized(worldIdRouter, groupId, token, holder, grant);
}
///////////////////////////////////////////////////////////////////////////////
/// CLAIM LOGIC ///
//////////////////////////////////////////////////////////////////////////////
/// @notice Claim the airdrop
/// @param grantId The grant ID to claim
/// @param receiver The address that will receive the tokens (this is also the signal of the ZKP)
/// @param root The root of the Merkle tree (signup-sequencer or world-id-contracts provides this)
/// @param nullifierHash The nullifier for this proof, preventing double signaling
/// @param proof The zero knowledge proof that demonstrates the claimer has a verified World ID
/// @dev hashToField function docs are in lib/world-id-contracts/src/libraries/ByteHasher.sol
function claim(
uint256 grantId,
address receiver,
uint256 root,
uint256 nullifierHash,
uint256[8] calldata proof
) external {
checkClaim(grantId, receiver, root, nullifierHash, proof);
nullifierHashes[nullifierHash] = true;
SafeERC20.safeTransferFrom(token, holder, receiver, grant.getAmount(grantId));
emit GrantClaimed(grantId, receiver);
}
/// @notice Check whether a claim is valid
/// @param grantId The grant ID to claim
/// @param receiver The address that will receive the tokens (this is also the signal of the ZKP)
/// @param root The root of the Merkle tree (signup-sequencer or world-id-contracts provides this)
/// @param nullifierHash The nullifier for this proof, preventing double signaling
/// @param proof The zero knowledge proof that demonstrates the claimer has a verified World ID
function checkClaim(
uint256 grantId,
address receiver,
uint256 root,
uint256 nullifierHash,
uint256[8] calldata proof
) public {
if (receiver == address(0)) revert InvalidReceiver();
if (nullifierHashes[nullifierHash]) revert InvalidNullifier();
grant.checkValidity(grantId);
worldIdRouter.verifyProof(
root,
groupId,
uint256(keccak256(abi.encodePacked(receiver))) >> 8,
nullifierHash,
grantId,
proof
);
}
/// @notice Claim a reserved grant from the past
/// @param timestamp The timestamp of the reservation
/// @param receiver The address that will receive the tokens (this is also the signal of the ZKP)
/// @param root The root of the Merkle tree (signup-sequencer or world-id-contracts provides this)
/// @param nullifierHash The nullifier for this proof, preventing double signaling
/// @param proof The zero knowledge proof that demonstrates the claimer has a verified World ID
/// @param signature The signature of the reservation
function claimReserved(
uint256 timestamp,
address receiver,
uint256 root,
uint256 nullifierHash,
uint256[8] calldata proof,
bytes calldata signature
) external {
uint256 grantId = grant.calculateId(timestamp);
checkClaimReserved(timestamp, receiver, root, nullifierHash, proof, signature);
nullifierHashes[nullifierHash] = true;
SafeERC20.safeTransferFrom(token, holder, receiver, grant.getAmount(grantId));
emit GrantClaimed(grantId, receiver);
}
/// @notice Check whether a reservation is valid
/// @param timestamp The timestamp of the reservation
/// @param receiver The address that will receive the tokens (this is also the signal of the ZKP)
/// @param root The root of the Merkle tree (signup-sequencer or world-id-contracts provides this)
/// @param nullifierHash The nullifier for this proof, preventing double signaling
/// @param proof The zero knowledge proof, array of 8 uint256 elements, demonstrating that the claimer has a verified World ID
/// @param signature The off-chain signature of the reservation.
function checkClaimReserved(
uint256 timestamp,
address receiver,
uint256 root,
uint256 nullifierHash,
uint256[8] calldata proof,
bytes calldata signature
) public {
uint256 grantId = grant.calculateId(timestamp);
if (receiver == address(0)) revert InvalidReceiver();
if (timestamp > block.timestamp) revert InvalidTimestamp();
if (nullifierHashes[nullifierHash]) revert InvalidNullifier();
grant.checkReservationValidity(timestamp);
address signer = ECDSA.recover(keccak256(abi.encode(timestamp, nullifierHash)), signature);
if (!allowedSigners[signer]) revert UnauthorizedSigner();
worldIdRouter.verifyProof(
root,
groupId,
uint256(keccak256(abi.encodePacked(receiver))) >> 8,
nullifierHash,
grantId,
proof
);
}
///////////////////////////////////////////////////////////////////////////////
/// CONFIG LOGIC ///
//////////////////////////////////////////////////////////////////////////////
/// @notice Add a caller to the list of allowed callers
/// @param _signer The address to add
function addAllowedReservationSigner(address _signer) external onlyOwner {
if (_signer == address(0)) revert InvalidReservationSigner();
allowedSigners[_signer] = true;
emit AllowedReservationSignerAdded(_signer);
}
/// @notice Remove a signer to the list of allowed signers
/// @param _signer The address to remove
function removeAllowedReservationSigner(address _signer) external onlyOwner {
if (_signer == address(0)) revert InvalidReservationSigner();
allowedSigners[_signer] = false;
emit AllowedReservationSignerRemoved(_signer);
}
/// @notice Update the worldIdRouter
/// @param _worldIdRouter The new worldIdRouter
function setWorldIdRouter(IWorldIDGroups _worldIdRouter) external onlyOwner {
if (address(_worldIdRouter) == address(0)) revert InvalidConfiguration();
worldIdRouter = _worldIdRouter;
emit WorldIdRouterUpdated(_worldIdRouter);
}
/// @notice Update the groupId
/// @param _groupId The new worldIdRouter
function setGroupId(uint256 _groupId) external onlyOwner {
groupId = _groupId;
emit GroupIdUpdated(_groupId);
}
/// @notice Update the token
/// @param _token The new token
function setToken(ERC20 _token) external onlyOwner {
if (address(_token) == address(0)) revert InvalidConfiguration();
token = _token;
emit TokenUpdated(_token);
}
/// @notice Update the holder
/// @param _holder The new holder
function setHolder(address _holder) external onlyOwner {
if (address(_holder) == address(0)) revert InvalidConfiguration();
holder = _holder;
emit HolderUpdated(holder);
}
/// @notice Update the grant
/// @param _grant The new grant
function setGrant(IGrant _grant) external onlyOwner {
if (address(_grant) == address(0)) revert InvalidConfiguration();
grant = _grant;
emit GrantUpdated(_grant);
}
/// @notice Prevents the owner from renouncing ownership
/// @dev onlyOwner
function renounceOwnership() public view override onlyOwner {
revert CannotRenounceOwnership();
}
}