forked from allo-protocol/allo-v2
-
Notifications
You must be signed in to change notification settings - Fork 3
/
Allo.sol
702 lines (596 loc) · 33.3 KB
/
Allo.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
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity 0.8.19;
// External Libraries
import "solady/auth/Ownable.sol";
import "openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol";
import "openzeppelin-contracts-upgradeable/contracts/token/ERC20/IERC20Upgradeable.sol";
import "openzeppelin-contracts-upgradeable/contracts/access/AccessControlUpgradeable.sol";
import "openzeppelin-contracts-upgradeable/contracts/security/ReentrancyGuardUpgradeable.sol";
// Interfaces
import "./interfaces/IAllo.sol";
// Internal Libraries
import {Clone} from "./libraries/Clone.sol";
import {Errors} from "./libraries/Errors.sol";
import "./libraries/Native.sol";
import {Transfer} from "./libraries/Transfer.sol";
// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⣿⣿⣿⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣿⣿⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⣿⣿⣿⢿⣿⣿⣿⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⣿⣿⣿⣿⣿⣿⣿⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣿⣿⣿⡟⠘⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
// ⠀⠀⠀⠀⠀⠀⠀⠀⣀⣴⣾⣿⣿⣿⣿⣾⠻⣿⣿⣿⣿⣿⣿⣿⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⣿⡿⠀⠀⠸⣿⣿⣿⣧⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠀⠀⠀⠀⢀⣠⣴⣴⣶⣶⣶⣦⣦⣀⡀⠀⠀⠀⠀⠀⠀
// ⠀⠀⠀⠀⠀⠀⠀⣴⣿⣿⣿⣿⣿⣿⡿⠃⠀⠙⣿⣿⣿⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⣿⣿⠁⠀⠀⠀⢻⣿⣿⣿⣧⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠀⠀⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⡀⠀⠀⠀⠀
// ⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⣿⣿⡿⠁⠀⠀⠀⠘⣿⣿⣿⣿⣿⡿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⠃⠀⠀⠀⠀⠈⢿⣿⣿⣿⣆⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠀⣰⣿⣿⣿⡿⠋⠁⠀⠀⠈⠘⠹⣿⣿⣿⣿⣆⠀⠀⠀
// ⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⣿⣿⡿⠀⠀⠀⠀⠀⠀⠈⢿⣿⣿⣿⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⣿⣿⣿⠏⠀⠀⠀⠀⠀⠀⠘⣿⣿⣿⣿⡄⠀⠀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⢰⣿⣿⣿⣿⠁⠀⠀⠀⠀⠀⠀⠀⠘⣿⣿⣿⣿⡀⠀⠀
// ⠀⠀⠀⢠⣿⣿⣿⣿⣿⣿⣿⣟⠀⡀⢀⠀⡀⢀⠀⡀⢈⢿⡟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡄⠀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⡇⠀⠀
// ⠀⠀⣠⣿⣿⣿⣿⣿⣿⡿⠋⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣶⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⣿⣿⣿⡿⢿⠿⠿⠿⠿⠿⠿⠿⠿⠿⢿⣿⣿⣿⣷⡀⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠸⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⣿⣿⠂⠀⠀
// ⠀⠀⠙⠛⠿⠻⠻⠛⠉⠀⠀⠈⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣿⣿⣿⣿⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢿⣿⣿⣿⣧⠀⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠀⢻⣿⣿⣿⣷⣀⢀⠀⠀⠀⡀⣰⣾⣿⣿⣿⠏⠀⠀⠀
// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠛⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⣿⣿⣿⣿⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⣿⣿⣿⣿⣧⠀⠀⢸⣿⣿⣿⣗⠀⠀⠀⢸⣿⣿⣿⡯⠀⠀⠀⠀⠹⢿⣿⣿⣿⣿⣾⣾⣷⣿⣿⣿⣿⡿⠋⠀⠀⠀⠀
// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⠙⠋⠛⠙⠋⠛⠙⠋⠛⠙⠋⠃⠀⠀⠀⠀⠀⠀⠀⠀⠠⠿⠻⠟⠿⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⠟⠿⠟⠿⠆⠀⠸⠿⠿⠟⠯⠀⠀⠀⠸⠿⠿⠿⠏⠀⠀⠀⠀⠀⠈⠉⠻⠻⡿⣿⢿⡿⡿⠿⠛⠁⠀⠀⠀⠀⠀⠀
// allo.gitcoin.co
/// @title Allo
/// @author @thelostone-mc <[email protected]>, @0xKurt <[email protected]>, @codenamejason <[email protected]>, @0xZakk <[email protected]>, @nfrgosselin <[email protected]>
/// @notice This contract is used to create & manage pools as well as manage the protocol.
/// @dev The contract must be initialized with the 'initialize()' function.
contract Allo is
IAllo,
Native,
Transfer,
Initializable,
Ownable,
AccessControlUpgradeable,
ReentrancyGuardUpgradeable,
Errors
{
// ==========================
// === Storage Variables ====
// ==========================
/// @notice Percentage that is used to calculate the fee Allo takes from each pool when funded
/// and is deducted when a pool is funded. So if you want to fund a round with 1000 DAI and the fee
/// percentage is 1e17 (10%), then 100 DAI will be deducted from the 1000 DAI and the pool will be
/// funded with 900 DAI. The fee is then sent to the treasury address.
/// @dev How the percentage is represented in our contracts: 1e18 = 100%, 1e17 = 10%, 1e16 = 1%, 1e15 = 0.1%
uint256 private percentFee;
/// @notice Fee Allo charges for all pools on creation
/// @dev This is different from the 'percentFee' in that this is a flat fee and not a percentage. So if you want to create a pool
/// with a base fee of 100 DAI, then you would pass 100 DAI to the 'createPool()' function and the pool would be created
/// with 100 DAI less than the amount you passed to the function. The base fee is sent to the treasury address.
uint256 internal baseFee;
/// @notice Incremental index to track the pools created
uint256 private _poolIndex;
/// @notice Allo treasury
address payable private treasury;
/// @notice Registry contract
IRegistry private registry;
/// @notice Maps the `msg.sender` to a `nonce` to prevent duplicates
/// @dev 'msg.sender' -> 'nonce' for cloning strategies
mapping(address => uint256) private _nonces;
/// @notice Maps the pool ID to the pool details
/// @dev 'Pool.id' -> 'Pool'
mapping(uint256 => Pool) private pools;
/// @notice Returns a bool for whether a strategy is cloneable or not using the strategy address as the key
/// @dev Strategy.address -> bool
mapping(address => bool) private cloneableStrategies;
// ====================================
// =========== Initializer =============
// ====================================
/// @notice Initializes the contract after an upgrade
/// @dev During upgrade -> a higher version should be passed to reinitializer
/// @param _owner The owner of allo
/// @param _registry The address of the registry
/// @param _treasury The address of the treasury
/// @param _percentFee The percentage fee
/// @param _baseFee The base fee
function initialize(
address _owner,
address _registry,
address payable _treasury,
uint256 _percentFee,
uint256 _baseFee
) external reinitializer(1) {
// Initialize the owner using Solady ownable library
_initializeOwner(_owner);
// Set the address of the registry
_updateRegistry(_registry);
// Set the address of the treasury
_updateTreasury(_treasury);
// Set the fee percentage
_updatePercentFee(_percentFee);
// Set the base fee
_updateBaseFee(_baseFee);
}
// ====================================
// =========== Modifier ===============
// ====================================
// Both modifiers below are using OpenZeppelin's AccessControl.sol with custom roles under the hood
/// @notice Reverts UNAUTHORIZED() if the caller is not a pool manager
/// @param _poolId The pool id
modifier onlyPoolManager(uint256 _poolId) {
_checkOnlyPoolManager(_poolId);
_;
}
/// @notice Reverts UNAUTHORIZED() if the caller is not a pool admin
/// @param _poolId The pool id
modifier onlyPoolAdmin(uint256 _poolId) {
_checkOnlyPoolAdmin(_poolId);
_;
}
// ====================================
// ==== External/Public Functions =====
// ====================================
/// @notice Creates a new pool (with a custom strategy)
/// @dev 'msg.sender' must be a member or owner of a profile to create a pool with or without a custom strategy, The encoded data
/// will be specific to a given strategy requirements, reference the strategy implementation of 'initialize()'. The strategy
/// address passed must not be a cloneable strategy. The strategy address passed must not be the zero address. 'msg.sender' must
/// be a member or owner of the profile id passed as '_profileId'.
/// @param _profileId The 'profileId' of the registry profile, used to check if 'msg.sender' is a member or owner of the profile
/// @param _strategy The address of the deployed custom strategy
/// @param _initStrategyData The data to initialize the strategy
/// @param _token The address of the token you want to use in your pool
/// @param _amount The amount of the token you want to deposit into the pool on initialization
/// @param _metadata The 'Metadata' of the pool, this uses our 'Meatdata.sol' struct (consistent throughout the protocol)
/// @param _managers The managers of the pool, and can be added/removed later by the pool admin
/// @return poolId The ID of the pool
function createPoolWithCustomStrategy(
bytes32 _profileId,
address _strategy,
bytes memory _initStrategyData,
address _token,
uint256 _amount,
Metadata memory _metadata,
address[] memory _managers
) external payable returns (uint256 poolId) {
// Revert if the strategy address passed is the zero address with 'ZERO_ADDRESS()'
if (_strategy == address(0)) revert ZERO_ADDRESS();
// Revert if we already have this strategy in our cloneable mapping with 'IS_APPROVED_STRATEGY()' (only non-cloneable strategies can be used)
if (_isCloneableStrategy(_strategy)) revert IS_APPROVED_STRATEGY();
// Call the internal '_createPool()' function and return the pool ID
return _createPool(_profileId, IStrategy(_strategy), _initStrategyData, _token, _amount, _metadata, _managers);
}
/// @notice Creates a new pool (by cloning a cloneable strategies).
/// @dev 'msg.sender' must be owner or member of the profile id passed as '_profileId'.
/// @param _profileId The ID of the registry profile, used to check if 'msg.sender' is a member or owner of the profile
/// @param _strategy The address of the strategy contract the pool will use.
/// @param _initStrategyData The data to initialize the strategy
/// @param _token The address of the token
/// @param _amount The amount of the token
/// @param _metadata The metadata of the pool
/// @param _managers The managers of the pool
/// @custom:initstrategydata The encoded data will be specific to a given strategy requirements,
/// reference the strategy implementation of 'initialize()'
function createPool(
bytes32 _profileId,
address _strategy,
bytes memory _initStrategyData,
address _token,
uint256 _amount,
Metadata memory _metadata,
address[] memory _managers
) external payable nonReentrant returns (uint256 poolId) {
if (!_isCloneableStrategy(_strategy)) {
revert NOT_APPROVED_STRATEGY();
}
// Returns the created pool ID
return _createPool(
_profileId,
IStrategy(Clone.createClone(_strategy, _nonces[msg.sender]++)),
_initStrategyData,
_token,
_amount,
_metadata,
_managers
);
}
/// @notice Update pool metadata
/// @dev 'msg.sender' must be a pool manager. Emits 'PoolMetadataUpdated()' event.
/// @param _poolId ID of the pool
/// @param _metadata The new metadata of the pool
function updatePoolMetadata(uint256 _poolId, Metadata memory _metadata) external onlyPoolManager(_poolId) {
Pool storage pool = pools[_poolId];
pool.metadata = _metadata;
emit PoolMetadataUpdated(_poolId, _metadata);
}
/// @notice Updates the registry address.
/// @dev Use this to update the registry address. 'msg.sender' must be Allo owner.
/// @param _registry The new registry address
function updateRegistry(address _registry) external onlyOwner {
_updateRegistry(_registry);
}
/// @notice Updates the treasury address.
/// @dev Use this to update the treasury address. 'msg.sender' must be Allo owner.
/// @param _treasury The new treasury address
function updateTreasury(address payable _treasury) external onlyOwner {
_updateTreasury(_treasury);
}
/// @notice Updates the fee percentage.
/// @dev Use this to update the fee percentage. 'msg.sender' must be Allo owner.
/// @param _percentFee The new fee
function updatePercentFee(uint256 _percentFee) external onlyOwner {
_updatePercentFee(_percentFee);
}
/// @notice Updates the base fee.
/// @dev Use this to update the base fee. 'msg.sender' must be Allo owner.
/// @param _baseFee The new base fee
function updateBaseFee(uint256 _baseFee) external onlyOwner {
_updateBaseFee(_baseFee);
}
/// @notice Add a strategy to the allowlist.
/// @dev Emits the 'StrategyApproved()' event. 'msg.sender' must be Allo owner.
/// @param _strategy The address of the strategy
function addToCloneableStrategies(address _strategy) external onlyOwner {
if (_strategy == address(0)) revert ZERO_ADDRESS();
cloneableStrategies[_strategy] = true;
emit StrategyApproved(_strategy);
}
/// @notice Remove a strategy from the allowlist
/// @dev Emits 'StrategyRemoved()' event. 'msg.sender must be Allo owner.
/// @param _strategy The address of the strategy
function removeFromCloneableStrategies(address _strategy) external onlyOwner {
// Set the strategy to false in the cloneableStrategies mapping
cloneableStrategies[_strategy] = false;
// Emit the StrategyRemoved event
emit StrategyRemoved(_strategy);
}
/// @notice Add a pool manager
/// @dev Emits 'RoleGranted()' event. 'msg.sender' must be a pool admin.
/// @param _poolId ID of the pool
/// @param _manager The address to add
function addPoolManager(uint256 _poolId, address _manager) external onlyPoolAdmin(_poolId) {
// Reverts if the address is the zero address with 'ZERO_ADDRESS()'
if (_manager == address(0)) revert ZERO_ADDRESS();
// Grants the pool manager role to the '_manager' address
_grantRole(pools[_poolId].managerRole, _manager);
}
/// @notice Remove a pool manager
/// @dev Emits 'RoleRevoked()' event. 'msg.sender' must be a pool admin.
/// @param _poolId ID of the pool
/// @param _manager The address to remove
function removePoolManager(uint256 _poolId, address _manager) external onlyPoolAdmin(_poolId) {
_revokeRole(pools[_poolId].managerRole, _manager);
}
/// @notice Transfer the funds recovered to the recipient
/// @dev 'msg.sender' must be Allo owner
/// @param _token The token to transfer
/// @param _recipient The recipient
function recoverFunds(address _token, address _recipient) external onlyOwner {
// Get the amount of the token to transfer, which is always the entire balance of the contract address
uint256 amount = _token == NATIVE ? address(this).balance : IERC20Upgradeable(_token).balanceOf(address(this));
// Transfer the amount to the recipient (pool owner)
_transferAmount(_token, _recipient, amount);
}
// ====================================
// ======= Strategy Functions =========
// ====================================
/// @notice Passes _data through to the strategy for that pool.
/// @dev The encoded data will be specific to a given strategy requirements, reference the strategy
/// implementation of registerRecipient().
/// @param _poolId ID of the pool
/// @param _data Encoded data unique to a strategy that registerRecipient() requires
/// @return recipientId The recipient ID that has been registered
function registerRecipient(uint256 _poolId, bytes memory _data) external payable nonReentrant returns (address) {
// Return the recipientId (address) from the strategy
return pools[_poolId].strategy.registerRecipient{value: msg.value}(_data, msg.sender);
}
/// @notice Register multiple recipients to multiple pools.
/// @dev Returns the 'recipientIds' from the strategy that have been registered from calling this function.
/// Encoded data unique to a strategy that registerRecipient() requires. Encoded '_data' length must match
/// '_poolIds' length or this will revert with MISMATCH(). Other requirements will be determined by the strategy.
/// @param _poolIds ID's of the pools
/// @param _data An array of encoded data unique to a strategy that registerRecipient() requires.
/// @return recipientIds The recipient IDs that have been registered
function batchRegisterRecipient(uint256[] memory _poolIds, bytes[] memory _data)
external
nonReentrant
returns (address[] memory recipientIds)
{
uint256 poolIdLength = _poolIds.length;
recipientIds = new address[](poolIdLength);
if (poolIdLength != _data.length) revert MISMATCH();
// Loop through the '_poolIds' & '_data' and call the 'strategy.registerRecipient()' function
for (uint256 i; i < poolIdLength;) {
recipientIds[i] = pools[_poolIds[i]].strategy.registerRecipient(_data[i], msg.sender);
unchecked {
++i;
}
}
// Return the recipientIds that have been registered
return recipientIds;
}
/// @notice Fund a pool.
/// @dev Anyone can fund a pool and call this function.
/// @param _poolId ID of the pool
/// @param _amount The amount to be deposited into the pool
function fundPool(uint256 _poolId, uint256 _amount) external payable nonReentrant {
// if amount is 0, revert with 'NOT_ENOUGH_FUNDS()' error
if (_amount == 0) revert NOT_ENOUGH_FUNDS();
Pool memory pool = pools[_poolId];
if (pool.token == NATIVE && _amount != msg.value) revert NOT_ENOUGH_FUNDS();
// Call the internal fundPool() function
_fundPool(_amount, _poolId, pool.strategy);
}
/// @notice Allocate to a recipient or multiple recipients.
/// @dev The encoded data will be specific to a given strategy requirements, reference the strategy
/// implementation of allocate().
/// @param _poolId ID of the pool
/// @param _data Encoded data unique to the strategy for that pool
function allocate(uint256 _poolId, bytes memory _data) external payable nonReentrant {
_allocate(_poolId, _data);
}
/// @notice Allocate to multiple pools
/// @dev The encoded data will be specific to a given strategy requirements, reference the strategy
/// implementation of allocate(). Please note that this is not a 'payable' function, so if you
/// want to send funds to the strategy, you must send the funds using 'fundPool()'.
/// @param _poolIds IDs of the pools
/// @param _datas encoded data unique to the strategy for that pool
function batchAllocate(uint256[] calldata _poolIds, bytes[] memory _datas) external nonReentrant {
uint256 numPools = _poolIds.length;
// Reverts if the length of _poolIds does not match the length of _datas with 'MISMATCH()' error
if (numPools != _datas.length) revert MISMATCH();
// Loop through the _poolIds & _datas and call the internal _allocate() function
for (uint256 i; i < numPools;) {
_allocate(_poolIds[i], _datas[i]);
unchecked {
++i;
}
}
}
/// @notice Distribute to a recipient or multiple recipients.
/// @dev The encoded data will be specific to a given strategy requirements, reference the strategy
/// implementation of 'strategy.distribute()'.
/// @param _poolId ID of the pool
/// @param _recipientIds Ids of the recipients of the distribution
/// @param _data Encoded data unique to the strategy
function distribute(uint256 _poolId, address[] memory _recipientIds, bytes memory _data) external nonReentrant {
pools[_poolId].strategy.distribute(_recipientIds, _data, msg.sender);
}
/// ====================================
/// ======= Internal Functions =========
/// ====================================
/// @notice Internal function to check is caller is pool manager
/// @param _poolId The pool id
function _checkOnlyPoolManager(uint256 _poolId) internal view {
if (!_isPoolManager(_poolId, msg.sender)) revert UNAUTHORIZED();
}
/// @notice Internal function to check is caller is pool admin
/// @param _poolId The pool id
function _checkOnlyPoolAdmin(uint256 _poolId) internal view {
if (!_isPoolAdmin(_poolId, msg.sender)) revert UNAUTHORIZED();
}
/// @notice Creates a new pool.
/// @dev This is an internal function that is called by the 'createPool()' & 'createPoolWithCustomStrategy()' functions
/// It is used to create a new pool and is called by both functions. The 'msg.sender' must be a member or owner of
/// a profile to create a pool.
/// @param _profileId The ID of the profile of for pool creator in the registry
/// @param _strategy The address of strategy
/// @param _initStrategyData The data to initialize the strategy
/// @param _token The address of the token that the pool is denominated in
/// @param _amount The amount of the token to be deposited into the pool
/// @param _metadata The 'Metadata' of the pool
/// @param _managers The managers of the pool
/// @return poolId The ID of the pool
function _createPool(
bytes32 _profileId,
IStrategy _strategy,
bytes memory _initStrategyData,
address _token,
uint256 _amount,
Metadata memory _metadata,
address[] memory _managers
) internal returns (uint256 poolId) {
if (!registry.isOwnerOrMemberOfProfile(_profileId, msg.sender)) revert UNAUTHORIZED();
poolId = ++_poolIndex;
// Generate the manager & admin roles for the pool (this is the way we do this throughout the protocol for consistency)
bytes32 POOL_MANAGER_ROLE = bytes32(poolId);
bytes32 POOL_ADMIN_ROLE = keccak256(abi.encodePacked(poolId, "admin"));
// Create the Pool instance
Pool memory pool = Pool({
profileId: _profileId,
strategy: _strategy,
metadata: _metadata,
token: _token,
managerRole: POOL_MANAGER_ROLE,
adminRole: POOL_ADMIN_ROLE
});
// Add the pool to the mapping of created pools
pools[poolId] = pool;
// Grant admin roles to the pool creator
_grantRole(POOL_ADMIN_ROLE, msg.sender);
// Set admin role for POOL_MANAGER_ROLE
_setRoleAdmin(POOL_MANAGER_ROLE, POOL_ADMIN_ROLE);
// initialize strategies
// Initialization is expected to revert when invoked more than once with 'ALREADY_INITIALIZED()' error
_strategy.initialize(poolId, _initStrategyData);
if (_strategy.getPoolId() != poolId || address(_strategy.getAllo()) != address(this)) revert MISMATCH();
// grant pool managers roles
uint256 managersLength = _managers.length;
for (uint256 i; i < managersLength;) {
address manager = _managers[i];
if (manager == address(0)) revert ZERO_ADDRESS();
_grantRole(POOL_MANAGER_ROLE, manager);
unchecked {
++i;
}
}
if (baseFee > 0) {
// To prevent paying the baseFee from the Allo contract's balance
// If _token is NATIVE, then baseFee + _amount should be > than msg.value.
// If _token is not NATIVE, then baseFee should be > than msg.value.
if ((_token == NATIVE && (baseFee + _amount != msg.value)) || (_token != NATIVE && baseFee != msg.value)) {
revert NOT_ENOUGH_FUNDS();
}
_transferAmount(NATIVE, treasury, baseFee);
emit BaseFeePaid(poolId, baseFee);
}
if (_amount > 0) {
_fundPool(_amount, poolId, _strategy);
}
emit PoolCreated(poolId, _profileId, _strategy, _token, _amount, _metadata);
}
/// @notice Allocate to recipient(s).
/// @dev Passes '_data' & 'msg.sender' through to the strategy for that pool.
/// This is an internal function that is called by the 'allocate()' & 'batchAllocate()' functions.
/// @param _poolId ID of the pool
/// @param _data Encoded data unique to the strategy for that pool
function _allocate(uint256 _poolId, bytes memory _data) internal {
pools[_poolId].strategy.allocate{value: msg.value}(_data, msg.sender);
}
/// @notice Fund a pool.
/// @dev Deducts the fee and transfers the amount to the distribution strategy.
/// Emits a 'PoolFunded' event.
/// @param _amount The amount to transfer
/// @param _poolId The 'poolId' for the pool you are funding
/// @param _strategy The address of the strategy
function _fundPool(uint256 _amount, uint256 _poolId, IStrategy _strategy) internal {
uint256 feeAmount;
uint256 amountAfterFee = _amount;
Pool storage pool = pools[_poolId];
address _token = pool.token;
if (percentFee > 0) {
feeAmount = (_amount * percentFee) / getFeeDenominator();
amountAfterFee -= feeAmount;
if (feeAmount + amountAfterFee != _amount) revert INVALID();
if (_token == NATIVE) {
_transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));
} else {
uint256 balanceBeforeFee = _getBalance(_token, treasury);
_transferAmountFrom(_token, TransferData({from: msg.sender, to: treasury, amount: feeAmount}));
uint256 balanceAfterFee = _getBalance(_token, treasury);
// Track actual fee paid to account for fee on ERC20 token transfers
feeAmount = balanceAfterFee - balanceBeforeFee;
}
}
if (_token == NATIVE) {
_transferAmountFrom(
_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee})
);
} else {
uint256 balanceBeforeFundingPool = _getBalance(_token, address(_strategy));
_transferAmountFrom(
_token, TransferData({from: msg.sender, to: address(_strategy), amount: amountAfterFee})
);
uint256 balanceAfterFundingPool = _getBalance(_token, address(_strategy));
// Track actual fee paid to account for fee on ERC20 token transfers
amountAfterFee = balanceAfterFundingPool - balanceBeforeFundingPool;
}
_strategy.increasePoolAmount(amountAfterFee);
emit PoolFunded(_poolId, amountAfterFee, feeAmount);
}
/// @notice Checks if the strategy is an approved cloneable strategy.
/// @dev Internal function used by createPoolwithCustomStrategy and createPool to
/// determine if a strategy is in the cloneable strategy allow list.
/// @param _strategy The address of the strategy
/// @return This will return 'true' if the strategy is cloneable, otherwise 'false'
function _isCloneableStrategy(address _strategy) internal view returns (bool) {
return cloneableStrategies[_strategy];
}
/// @notice Checks if the address is a pool admin
/// @dev Internal function used to determine if an address is a pool admin
/// @param _poolId The ID of the pool
/// @param _address The address to check
/// @return This will return 'true' if the address is a pool admin, otherwise 'false'
function _isPoolAdmin(uint256 _poolId, address _address) internal view returns (bool) {
return hasRole(pools[_poolId].adminRole, _address);
}
/// @notice Checks if the address is a pool manager
/// @dev Internal function used to determine if an address is a pool manager
/// @param _poolId The ID of the pool
/// @param _address The address to check
/// @return This will return 'true' if the address is a pool manager, otherwise 'false'
function _isPoolManager(uint256 _poolId, address _address) internal view returns (bool) {
return hasRole(pools[_poolId].managerRole, _address) || _isPoolAdmin(_poolId, _address);
}
/// @notice Updates the registry address
/// @dev Internal function used to update the registry address.
/// Emits a RegistryUpdated event.
/// @param _registry The new registry address
function _updateRegistry(address _registry) internal {
if (_registry == address(0)) revert ZERO_ADDRESS();
registry = IRegistry(_registry);
emit RegistryUpdated(_registry);
}
/// @notice Updates the treasury address
/// @dev Internal function used to update the treasury address.
/// Emits a TreasuryUpdated event.
/// @param _treasury The new treasury address
function _updateTreasury(address payable _treasury) internal {
if (_treasury == address(0)) revert ZERO_ADDRESS();
treasury = _treasury;
emit TreasuryUpdated(treasury);
}
/// @notice Updates the fee percentage
/// @dev Internal function used to update the percentage fee.
/// Emits a PercentFeeUpdated event.
/// @param _percentFee The new fee
function _updatePercentFee(uint256 _percentFee) internal {
if (_percentFee > 1e18) revert INVALID_FEE();
percentFee = _percentFee;
emit PercentFeeUpdated(percentFee);
}
/// @notice Updates the base fee
/// @dev Internal function used to update the base fee.
/// Emits a BaseFeeUpdated event.
/// @param _baseFee The new base fee
function _updateBaseFee(uint256 _baseFee) internal {
baseFee = _baseFee;
emit BaseFeeUpdated(baseFee);
}
// =========================
// ==== View Functions =====
// =========================
/// @notice Getter for the fee denominator
/// @return FEE_DENOMINATOR The fee denominator is (1e18) which represents 100%
function getFeeDenominator() public pure returns (uint256 FEE_DENOMINATOR) {
return 1e18;
}
/// @notice Checks if the address is a pool admin.
/// @param _poolId The ID of the pool
/// @param _address The address to check
/// @return 'true' if the address is a pool admin, otherwise 'false'
function isPoolAdmin(uint256 _poolId, address _address) external view returns (bool) {
return _isPoolAdmin(_poolId, _address);
}
/// @notice Checks if the address is a pool manager
/// @param _poolId The ID of the pool
/// @param _address The address to check
/// @return 'true' if the address is a pool manager, otherwise 'false'
function isPoolManager(uint256 _poolId, address _address) external view returns (bool) {
return _isPoolManager(_poolId, _address);
}
/// @notice Getter for the strategy.
/// @param _poolId The ID of the pool
/// @return The address of the strategy
function getStrategy(uint256 _poolId) external view returns (address) {
return address(pools[_poolId].strategy);
}
/// @notice Getter for fee percentage.
/// @return The fee percentage (1e18 = 100%)
function getPercentFee() external view returns (uint256) {
return percentFee;
}
/// @notice Getter for base fee.
/// @return The base fee
function getBaseFee() external view returns (uint256) {
return baseFee;
}
/// @notice Getter for treasury address.
/// @return The treasury address
function getTreasury() external view returns (address payable) {
return treasury;
}
/// @notice Getter for registry.
/// @return The registry address
function getRegistry() external view returns (IRegistry) {
return registry;
}
/// @notice Getter for if strategy is cloneable.
/// @param _strategy The address of the strategy
/// @return 'true' if the strategy is cloneable, otherwise 'false'
function isCloneableStrategy(address _strategy) external view returns (bool) {
return _isCloneableStrategy(_strategy);
}
/// @notice Getter for the 'Pool'.
/// @param _poolId The ID of the pool
/// @return The 'Pool' struct
function getPool(uint256 _poolId) external view returns (Pool memory) {
return pools[_poolId];
}
}