-
Notifications
You must be signed in to change notification settings - Fork 43
/
Copy pathCellar.sol
1487 lines (1270 loc) · 57.8 KB
/
Cellar.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
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.21;
import {Math} from "src/utils/Math.sol";
import {ERC4626} from "@solmate/mixins/ERC4626.sol";
import {SafeTransferLib} from "@solmate/utils/SafeTransferLib.sol";
import {ERC20} from "@solmate/tokens/ERC20.sol";
// import { ERC4626, SafeTransferLib, Math, ERC20 } from "src/base/ERC4626.sol";
import {Registry} from "src/Registry.sol";
import {PriceRouter} from "src/modules/price-router/PriceRouter.sol";
import {Uint32Array} from "src/utils/Uint32Array.sol";
import {BaseAdaptor} from "src/modules/adaptors/BaseAdaptor.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
import {Auth, Authority} from "@solmate/auth/Auth.sol";
/**
* @title Sommelier Cellar
* @notice A composable ERC4626 that can use arbitrary DeFi assets/positions using adaptors.
* @author crispymangoes
*/
contract Cellar is ERC4626, Auth, ERC721Holder {
using Uint32Array for uint32[];
using SafeTransferLib for ERC20;
using Math for uint256;
using Address for address;
// ========================================= One Slot Values =========================================
// Below values are frequently accessed in the same TXs. By moving them to the top
// they will be stored in the same slot, reducing cold access reads.
/**
* @notice The maximum amount of shares that can be in circulation.
* @dev Can be decreased by the strategist.
* @dev Can be increased by Sommelier Governance.
*/
uint192 public shareSupplyCap;
/**
* @notice `locked` is public, so that the state can be checked even during view function calls.
*/
bool public locked;
/**
* @notice Whether or not the contract is shutdown in case of an emergency.
*/
bool public isShutdown;
/**
* @notice Pauses all user entry/exits, and strategist rebalances.
*/
bool public ignorePause;
/**
* @notice This bool is used to stop strategists from abusing Base Adaptor functions(deposit/withdraw).
*/
bool public blockExternalReceiver;
/**
* @notice Stores the position id of the holding position in the creditPositions array.
*/
uint32 public holdingPosition;
// ========================================= MULTICALL =========================================
/**
* @notice Allows caller to call multiple functions in a single TX.
* @dev Does NOT return the function return values.
*/
function multicall(bytes[] calldata data) external {
for (uint256 i = 0; i < data.length; i++) {
address(this).functionDelegateCall(data[i]);
}
}
// ========================================= REENTRANCY GUARD =========================================
modifier nonReentrant() {
require(!locked, "REENTRANCY");
locked = true;
_;
locked = false;
}
// ========================================= _isAuthorized ========================================
function _isAuthorized() internal requiresAuth {}
// ========================================= PRICE ROUTER CACHE =========================================
/**
* @notice Cached price router contract.
* @dev This way cellar has to "opt in" to price router changes.
*/
PriceRouter public priceRouter;
/**
* @notice Updates the cellar to use the lastest price router in the registry.
* @param checkTotalAssets If true totalAssets is checked before and after updating the price router,
* and is verified to be withing a +- 5% envelope.
* If false totalAssets is only called after updating the price router.]
* @param allowableRange The +- range the total assets may deviate between the old and new price router.
* - 1_000 == 10%
* - 500 == 5%
* @param expectedPriceRouter The registry price router differed from the expected price router.
* @dev `allowableRange` reverts from arithmetic underflow if it is greater than 10_000, this is
* desired behavior.
* @dev Callable by Sommelier Governance.
*/
function cachePriceRouter(bool checkTotalAssets, uint16 allowableRange, address expectedPriceRouter) external {
_isAuthorized();
uint256 minAssets;
uint256 maxAssets;
if (checkTotalAssets) {
uint256 assetsBefore = totalAssets();
minAssets = assetsBefore.mulDivDown(1e4 - allowableRange, 1e4);
maxAssets = assetsBefore.mulDivDown(1e4 + allowableRange, 1e4);
}
// Make sure expected price router is equal to price router grabbed from registry.
_checkRegistryAddressAgainstExpected(PRICE_ROUTER_REGISTRY_SLOT, expectedPriceRouter);
priceRouter = PriceRouter(expectedPriceRouter);
uint256 assetsAfter = totalAssets();
if (checkTotalAssets) {
if (assetsAfter < minAssets || assetsAfter > maxAssets) {
revert Cellar__TotalAssetDeviatedOutsideRange(assetsAfter, minAssets, maxAssets);
}
}
}
// ========================================= POSITIONS CONFIG =========================================
/**
* @notice Emitted when a position is added.
* @param position id of position that was added
* @param index index that position was added at
*/
event PositionAdded(uint32 position, uint256 index);
/**
* @notice Emitted when a position is removed.
* @param position id of position that was removed
* @param index index that position was removed from
*/
event PositionRemoved(uint32 position, uint256 index);
/**
* @notice Emitted when the positions at two indexes are swapped.
* @param newPosition1 id of position (previously at index2) that replaced index1.
* @param newPosition2 id of position (previously at index1) that replaced index2.
* @param index1 index of first position involved in the swap
* @param index2 index of second position involved in the swap.
*/
event PositionSwapped(uint32 newPosition1, uint32 newPosition2, uint256 index1, uint256 index2);
/**
* @notice Emitted when Governance adds/removes a position to/from the cellars catalogue.
*/
event PositionCatalogueAltered(uint32 positionId, bool inCatalogue);
/**
* @notice Emitted when Governance adds/removes an adaptor to/from the cellars catalogue.
*/
event AdaptorCatalogueAltered(address adaptor, bool inCatalogue);
/**
* @notice Attempted to add a position that is already being used.
* @param position id of the position
*/
error Cellar__PositionAlreadyUsed(uint32 position);
/**
* @notice Attempted to make an unused position the holding position.
* @param position id of the position
*/
error Cellar__PositionNotUsed(uint32 position);
/**
* @notice Attempted to add a position that is not in the catalogue.
* @param position id of the position
*/
error Cellar__PositionNotInCatalogue(uint32 position);
/**
* @notice Attempted an action on a position that is required to be empty before the action can be performed.
* @param position address of the non-empty position
* @param sharesRemaining amount of shares remaining in the position
*/
error Cellar__PositionNotEmpty(uint32 position, uint256 sharesRemaining);
/**
* @notice Attempted an operation with an asset that was different then the one expected.
* @param asset address of the asset
* @param expectedAsset address of the expected asset
*/
error Cellar__AssetMismatch(address asset, address expectedAsset);
/**
* @notice Attempted to add a position when the position array is full.
* @param maxPositions maximum number of positions that can be used
*/
error Cellar__PositionArrayFull(uint256 maxPositions);
/**
* @notice Attempted to add a position, with mismatched debt.
* @param position the posiiton id that was mismatched
*/
error Cellar__DebtMismatch(uint32 position);
/**
* @notice Attempted to remove the Cellars holding position.
*/
error Cellar__RemovingHoldingPosition();
/**
* @notice Attempted to add an invalid holding position.
* @param positionId the id of the invalid position.
*/
error Cellar__InvalidHoldingPosition(uint32 positionId);
/**
* @notice Attempted to force out the wrong position.
*/
error Cellar__FailedToForceOutPosition();
/**
* @notice Array of uint32s made up of cellars credit positions Ids.
*/
uint32[] internal creditPositions;
/**
* @notice Array of uint32s made up of cellars debt positions Ids.
*/
uint32[] internal debtPositions;
/**
* @notice Tell whether a position is currently used.
*/
mapping(uint256 => bool) public isPositionUsed;
/**
* @notice Get position data given position id.
*/
mapping(uint32 => Registry.PositionData) internal getPositionData;
/**
* @notice Get the ids of the credit positions currently used by the cellar.
*/
function getCreditPositions() external view returns (uint32[] memory) {
return creditPositions;
}
/**
* @notice Get the ids of the debt positions currently used by the cellar.
*/
function getDebtPositions() external view returns (uint32[] memory) {
return debtPositions;
}
/**
* @notice Maximum amount of positions a cellar can have in it's credit/debt arrays.
*/
uint256 internal constant MAX_POSITIONS = 32;
/**
* @notice Allows owner to change the holding position.
* @dev Callable by Sommelier Strategist.
*/
function setHoldingPosition(uint32 positionId) public {
_isAuthorized();
if (!isPositionUsed[positionId]) revert Cellar__PositionNotUsed(positionId);
if (_assetOf(positionId) != asset) revert Cellar__AssetMismatch(address(asset), address(_assetOf(positionId)));
if (getPositionData[positionId].isDebt) revert Cellar__InvalidHoldingPosition(positionId);
holdingPosition = positionId;
}
/**
* @notice Positions the strategist is approved to use without any governance intervention.
*/
mapping(uint32 => bool) internal positionCatalogue;
/**
* @notice Adaptors the strategist is approved to use without any governance intervention.
*/
mapping(address => bool) internal adaptorCatalogue;
/**
* @notice Allows Governance to add positions to this cellar's catalogue.
* @dev Callable by Sommelier Governance.
*/
function addPositionToCatalogue(uint32 positionId) public {
_isAuthorized();
// Make sure position is not paused and is trusted.
registry.revertIfPositionIsNotTrusted(positionId);
positionCatalogue[positionId] = true;
emit PositionCatalogueAltered(positionId, true);
}
/**
* @notice Allows Governance to remove positions from this cellar's catalogue.
* @dev Callable by Sommelier Strategist.
*/
function removePositionFromCatalogue(uint32 positionId) external {
_isAuthorized();
positionCatalogue[positionId] = false;
emit PositionCatalogueAltered(positionId, false);
}
/**
* @notice Allows Governance to add adaptors to this cellar's catalogue.
* @dev Callable by Sommelier Governance.
*/
function addAdaptorToCatalogue(address adaptor) external {
_isAuthorized();
// Make sure adaptor is trusted.
registry.revertIfAdaptorIsNotTrusted(adaptor);
adaptorCatalogue[adaptor] = true;
emit AdaptorCatalogueAltered(adaptor, true);
}
/**
* @notice Allows Governance to remove adaptors from this cellar's catalogue.
* @dev Callable by Sommelier Strategist.
*/
function removeAdaptorFromCatalogue(address adaptor) external {
_isAuthorized();
adaptorCatalogue[adaptor] = false;
emit AdaptorCatalogueAltered(adaptor, false);
}
/**
* @notice Insert a trusted position to the list of positions used by the cellar at a given index.
* @param index index at which to insert the position
* @param positionId id of position to add
* @param configurationData data used to configure how the position behaves
* @dev Callable by Sommelier Strategist.
*/
function addPosition(uint32 index, uint32 positionId, bytes memory configurationData, bool inDebtArray) public {
_isAuthorized();
_whenNotShutdown();
// Check if position is already being used.
if (isPositionUsed[positionId]) revert Cellar__PositionAlreadyUsed(positionId);
// Check if position is in the position catalogue.
if (!positionCatalogue[positionId]) revert Cellar__PositionNotInCatalogue(positionId);
// Grab position data from registry.
// Also checks if position is not trusted and reverts if so.
(address adaptor, bool isDebt, bytes memory adaptorData) = registry.addPositionToCellar(positionId);
if (isDebt != inDebtArray) revert Cellar__DebtMismatch(positionId);
// Copy position data from registry to here.
getPositionData[positionId] = Registry.PositionData({
adaptor: adaptor,
isDebt: isDebt,
adaptorData: adaptorData,
configurationData: configurationData
});
if (isDebt) {
if (debtPositions.length >= MAX_POSITIONS) revert Cellar__PositionArrayFull(MAX_POSITIONS);
// Add new position at a specified index.
debtPositions.add(index, positionId);
} else {
if (creditPositions.length >= MAX_POSITIONS) revert Cellar__PositionArrayFull(MAX_POSITIONS);
// Add new position at a specified index.
creditPositions.add(index, positionId);
}
isPositionUsed[positionId] = true;
emit PositionAdded(positionId, index);
}
/**
* @notice Remove the position at a given index from the list of positions used by the cellar.
* @dev Called by strategist.
* @param index index at which to remove the position
* @dev Callable by Sommelier Strategist.
*/
function removePosition(uint32 index, bool inDebtArray) external {
_isAuthorized();
// Get position being removed.
uint32 positionId = inDebtArray ? debtPositions[index] : creditPositions[index];
// Only remove position if it is empty, and if it is not the holding position.
uint256 positionBalance = _balanceOf(positionId);
if (positionBalance > 0) revert Cellar__PositionNotEmpty(positionId, positionBalance);
_removePosition(index, positionId, inDebtArray);
}
/**
* @notice Allows Sommelier Governance to forceably remove a position from the Cellar without checking its balance is zero.
* @dev Callable by Sommelier Governance.
*/
function forcePositionOut(uint32 index, uint32 positionId, bool inDebtArray) external {
_isAuthorized();
// Get position being removed.
uint32 _positionId = inDebtArray ? debtPositions[index] : creditPositions[index];
// Make sure position id right, and is distrusted.
if (positionId != _positionId || registry.isPositionTrusted(positionId)) {
revert Cellar__FailedToForceOutPosition();
}
_removePosition(index, positionId, inDebtArray);
}
/**
* @notice Internal helper function to remove positions from cellars tracked arrays.
*/
function _removePosition(uint32 index, uint32 positionId, bool inDebtArray) internal {
if (positionId == holdingPosition) revert Cellar__RemovingHoldingPosition();
if (inDebtArray) {
// Remove position at the given index.
debtPositions.remove(index);
} else {
creditPositions.remove(index);
}
isPositionUsed[positionId] = false;
delete getPositionData[positionId];
emit PositionRemoved(positionId, index);
}
/**
* @notice Swap the positions at two given indexes.
* @param index1 index of first position to swap
* @param index2 index of second position to swap
* @param inDebtArray bool indicating to switch positions in the debt array, or the credit array.
* @dev Callable by Sommelier Strategist.
*/
function swapPositions(uint32 index1, uint32 index2, bool inDebtArray) external {
_isAuthorized();
// Get the new positions that will be at each index.
uint32 newPosition1;
uint32 newPosition2;
if (inDebtArray) {
newPosition1 = debtPositions[index2];
newPosition2 = debtPositions[index1];
// Swap positions.
(debtPositions[index1], debtPositions[index2]) = (newPosition1, newPosition2);
} else {
newPosition1 = creditPositions[index2];
newPosition2 = creditPositions[index1];
// Swap positions.
(creditPositions[index1], creditPositions[index2]) = (newPosition1, newPosition2);
}
emit PositionSwapped(newPosition1, newPosition2, index1, index2);
}
// =============================================== FEES CONFIG ===============================================
/**
* @notice Emitted when strategist platform fee cut is changed.
* @param oldPlatformCut value strategist platform fee cut was changed from
* @param newPlatformCut value strategist platform fee cut was changed to
*/
event StrategistPlatformCutChanged(uint64 oldPlatformCut, uint64 newPlatformCut);
/**
* @notice Emitted when strategists payout address is changed.
* @param oldPayoutAddress value strategists payout address was changed from
* @param newPayoutAddress value strategists payout address was changed to
*/
event StrategistPayoutAddressChanged(address oldPayoutAddress, address newPayoutAddress);
/**
* @notice Attempted to change strategist fee cut with invalid value.
*/
error Cellar__InvalidFeeCut();
/**
* @notice Attempted to change platform fee with invalid value.
*/
error Cellar__InvalidFee();
/**
* @notice Data related to fees.
* @param strategistPlatformCut Determines how much platform fees go to strategist.
* This should be a value out of 1e18 (ie. 1e18 represents 100%, 0 represents 0%).
* @param platformFee The percentage of total assets accrued as platform fees over a year.
* This should be a value out of 1e18 (ie. 1e18 represents 100%, 0 represents 0%).
* @param strategistPayoutAddress Address to send the strategists fee shares.
*/
struct FeeData {
uint64 strategistPlatformCut;
uint64 platformFee;
uint64 lastAccrual;
address strategistPayoutAddress;
}
/**
* @notice Stores all fee data for cellar.
*/
FeeData public feeData = FeeData({
strategistPlatformCut: 0.75e18,
platformFee: 0.01e18,
lastAccrual: 0,
strategistPayoutAddress: address(0)
});
/**
* @notice Sets the max possible performance fee for this cellar.
*/
uint64 internal constant MAX_PLATFORM_FEE = 0.2e18;
/**
* @notice Sets the max possible fee cut for this cellar.
*/
uint64 internal constant MAX_FEE_CUT = 1e18;
/**
* @notice Sets the Strategists cut of platform fees
* @param cut the platform cut for the strategist
* @dev Callable by Sommelier Governance.
*/
function setStrategistPlatformCut(uint64 cut) external {
_isAuthorized();
if (cut > MAX_FEE_CUT) revert Cellar__InvalidFeeCut();
emit StrategistPlatformCutChanged(feeData.strategistPlatformCut, cut);
feeData.strategistPlatformCut = cut;
}
/**
* @notice Sets the Strategists payout address
* @param payout the new strategist payout address
* @dev Callable by Sommelier Strategist.
*/
function setStrategistPayoutAddress(address payout) external {
_isAuthorized();
emit StrategistPayoutAddressChanged(feeData.strategistPayoutAddress, payout);
feeData.strategistPayoutAddress = payout;
}
// =========================================== EMERGENCY LOGIC ===========================================
/**
* @notice Emitted when cellar emergency state is changed.
* @param isShutdown whether the cellar is shutdown
*/
event ShutdownChanged(bool isShutdown);
/**
* @notice Attempted action was prevented due to contract being shutdown.
*/
error Cellar__ContractShutdown();
/**
* @notice Attempted action was prevented due to contract not being shutdown.
*/
error Cellar__ContractNotShutdown();
/**
* @notice Attempted to interact with the cellar when it is paused.
*/
error Cellar__Paused();
/**
* @notice View function external contracts can use to see if the cellar is paused.
*/
function isPaused() external view returns (bool) {
if (!ignorePause) {
return registry.isCallerPaused(address(this));
}
return false;
}
/**
* @notice Pauses all user entry/exits, and strategist rebalances.
*/
function _checkIfPaused() internal view {
if (!ignorePause) {
if (registry.isCallerPaused(address(this))) revert Cellar__Paused();
}
}
/**
* @notice Allows governance to choose whether or not to respect a pause.
* @dev Callable by Sommelier Governance.
*/
function toggleIgnorePause() external {
_isAuthorized();
ignorePause = ignorePause ? false : true;
}
/**
* @notice Prevent a function from being called during a shutdown.
*/
function _whenNotShutdown() internal view {
if (isShutdown) revert Cellar__ContractShutdown();
}
/**
* @notice Shutdown the cellar. Used in an emergency or if the cellar has been deprecated.
* @dev Callable by Sommelier Strategist.
*/
function initiateShutdown() external {
_isAuthorized();
_whenNotShutdown();
isShutdown = true;
emit ShutdownChanged(true);
}
/**
* @notice Restart the cellar.
* @dev Callable by Sommelier Strategist.
*/
function liftShutdown() external {
_isAuthorized();
if (!isShutdown) revert Cellar__ContractNotShutdown();
isShutdown = false;
emit ShutdownChanged(false);
}
// =========================================== CONSTRUCTOR ===========================================
/**
* @notice Id to get the gravity bridge from the registry.
*/
uint256 internal constant GRAVITY_BRIDGE_REGISTRY_SLOT = 0;
/**
* @notice Id to get the price router from the registry.
*/
uint256 internal constant PRICE_ROUTER_REGISTRY_SLOT = 2;
/**
* @notice The minimum amount of shares to be minted in the contructor.
*/
uint256 internal constant MINIMUM_CONSTRUCTOR_MINT = 1e4;
/**
* @notice Attempted to deploy contract without minting enough shares.
*/
error Cellar__MinimumConstructorMintNotMet();
/**
* @notice Address of the platform's registry contract. Used to get the latest address of modules.
*/
Registry public immutable registry;
/**
* @dev Owner should be set to the Gravity Bridge, which relays instructions from the Steward
* module to the cellars.
* https://github.com/PeggyJV/steward
* https://github.com/cosmos/gravity-bridge/blob/main/solidity/contracts/Gravity.sol
* @param _registry address of the platform's registry contract
* @param _asset address of underlying token used for the for accounting, depositing, and withdrawing
* @param _name name of this cellar's share token
* @param _symbol symbol of this cellar's share token
* @param _holdingPosition the holding position of the Cellar
* must use a position that does NOT call back to cellar on use(Like ERC20 positions).
* @param _holdingPositionConfig configuration data for holding position
* @param _initialDeposit initial amount of assets to deposit into the Cellar
* @param _strategistPlatformCut platform cut to use
* @param _shareSupplyCap starting share supply cap
*/
constructor(
address _owner,
Registry _registry,
ERC20 _asset,
string memory _name,
string memory _symbol,
uint32 _holdingPosition,
bytes memory _holdingPositionConfig,
uint256 _initialDeposit,
uint64 _strategistPlatformCut,
uint192 _shareSupplyCap
) ERC4626(_asset, _name, _symbol) Auth(msg.sender, Authority(address(0))) {
registry = _registry;
priceRouter = PriceRouter(_registry.getAddress(PRICE_ROUTER_REGISTRY_SLOT));
// Initialize holding position.
addPositionToCatalogue(_holdingPosition);
addPosition(0, _holdingPosition, _holdingPositionConfig, false);
setHoldingPosition(_holdingPosition);
// Update Share Supply Cap.
shareSupplyCap = _shareSupplyCap;
if (_initialDeposit < MINIMUM_CONSTRUCTOR_MINT) revert Cellar__MinimumConstructorMintNotMet();
// Deposit into Cellar, and mint shares to Deployer address.
_asset.safeTransferFrom(_owner, address(this), _initialDeposit);
// Set the share price as 1:1 with underlying asset.
_mint(msg.sender, _initialDeposit);
// Deposit _initialDeposit into holding position.
_depositTo(_holdingPosition, _initialDeposit);
feeData.strategistPlatformCut = _strategistPlatformCut;
transferOwnership(_owner);
}
// =========================================== CORE LOGIC ===========================================
/**
* @notice Attempted an action with zero shares.
*/
error Cellar__ZeroShares();
/**
* @notice Attempted an action with zero assets.
*/
error Cellar__ZeroAssets();
/**
* @notice Withdraw did not withdraw all assets.
* @param assetsOwed the remaining assets owed that were not withdrawn.
*/
error Cellar__IncompleteWithdraw(uint256 assetsOwed);
/**
* @notice Attempted to withdraw an illiquid position.
* @param illiquidPosition the illiquid position.
*/
error Cellar__IlliquidWithdraw(address illiquidPosition);
/**
* @notice called at the beginning of deposit.
*/
function beforeDeposit(ERC20, uint256, uint256, address) internal view virtual {
_whenNotShutdown();
_checkIfPaused();
}
/**
* @notice called at the end of deposit.
* @param position the position to deposit to.
* @param assets amount of assets deposited by user.
*/
function afterDeposit(uint32 position, uint256 assets, uint256, address) internal virtual {
_depositTo(position, assets);
}
/**
* @notice called at the beginning of withdraw.
*/
function beforeWithdraw(uint256, uint256, address, address) internal view virtual {
_checkIfPaused();
}
/**
* @notice Called when users enter the cellar via deposit or mint.
*/
function _enter(ERC20 depositAsset, uint32 position, uint256 assets, uint256 shares, address receiver)
internal
virtual
{
beforeDeposit(asset, assets, shares, receiver);
// Need to transfer before minting or ERC777s could reenter.
depositAsset.safeTransferFrom(msg.sender, address(this), assets);
_mint(receiver, shares);
emit Deposit(msg.sender, receiver, assets, shares);
afterDeposit(position, assets, shares, receiver);
}
/**
* @notice Deposits assets into the cellar, and returns shares to receiver.
* @param assets amount of assets deposited by user.
* @param receiver address to receive the shares.
* @return shares amount of shares given for deposit.
*/
function deposit(uint256 assets, address receiver) public virtual override nonReentrant returns (uint256 shares) {
// Use `_calculateTotalAssetsOrTotalAssetsWithdrawable` instead of totalAssets bc re-entrancy is already checked in this function.
(uint256 _totalAssets, uint256 _totalSupply) = _getTotalAssetsAndTotalSupply(true);
// Check for rounding error since we round down in previewDeposit.
if ((shares = _convertToShares(assets, _totalAssets, _totalSupply)) == 0) revert Cellar__ZeroShares();
if ((_totalSupply + shares) > shareSupplyCap) revert Cellar__ShareSupplyCapExceeded();
_enter(asset, holdingPosition, assets, shares, receiver);
}
/**
* @notice Mints shares from the cellar, and returns shares to receiver.
* @param shares amount of shares requested by user.
* @param receiver address to receive the shares.
* @return assets amount of assets deposited into the cellar.
*/
function mint(uint256 shares, address receiver) public override nonReentrant returns (uint256 assets) {
(uint256 _totalAssets, uint256 _totalSupply) = _getTotalAssetsAndTotalSupply(true);
// previewMint rounds up, but initial mint could return zero assets, so check for rounding error.
if ((assets = _previewMint(shares, _totalAssets, _totalSupply)) == 0) revert Cellar__ZeroAssets();
if ((_totalSupply + shares) > shareSupplyCap) revert Cellar__ShareSupplyCapExceeded();
_enter(asset, holdingPosition, assets, shares, receiver);
}
/**
* @notice Called when users exit the cellar via withdraw or redeem.
*/
function _exit(uint256 assets, uint256 shares, address receiver, address owner) internal {
beforeWithdraw(assets, shares, receiver, owner);
if (msg.sender != owner) {
uint256 allowed = allowance[owner][msg.sender]; // Saves gas for limited approvals.
if (allowed != type(uint256).max) allowance[owner][msg.sender] = allowed - shares;
}
_burn(owner, shares);
emit Withdraw(msg.sender, receiver, owner, assets, shares);
_withdrawInOrder(assets, receiver);
/// @notice `afterWithdraw` is currently not used.
// afterWithdraw(assets, shares, receiver, owner);
}
/**
* @notice Withdraw assets from the cellar by redeeming shares.
* @dev Unlike conventional ERC4626 contracts, this may not always return one asset to the receiver.
* Since there are no swaps involved in this function, the receiver may receive multiple
* assets. The value of all the assets returned will be equal to the amount defined by
* `assets` denominated in the `asset` of the cellar (eg. if `asset` is USDC and `assets`
* is 1000, then the receiver will receive $1000 worth of assets in either one or many
* tokens).
* @param assets equivalent value of the assets withdrawn, denominated in the cellar's asset
* @param receiver address that will receive withdrawn assets
* @param owner address that owns the shares being redeemed
* @return shares amount of shares redeemed
*/
function withdraw(uint256 assets, address receiver, address owner)
public
override
nonReentrant
returns (uint256 shares)
{
(uint256 _totalAssets, uint256 _totalSupply) = _getTotalAssetsAndTotalSupply(false);
// No need to check for rounding error, `previewWithdraw` rounds up.
shares = _previewWithdraw(assets, _totalAssets, _totalSupply);
_exit(assets, shares, receiver, owner);
}
/**
* @notice Redeem shares to withdraw assets from the cellar.
* @dev Unlike conventional ERC4626 contracts, this may not always return one asset to the receiver.
* Since there are no swaps involved in this function, the receiver may receive multiple
* assets. The value of all the assets returned will be equal to the amount defined by
* `assets` denominated in the `asset` of the cellar (eg. if `asset` is USDC and `assets`
* is 1000, then the receiver will receive $1000 worth of assets in either one or many
* tokens).
* @param shares amount of shares to redeem
* @param receiver address that will receive withdrawn assets
* @param owner address that owns the shares being redeemed
* @return assets equivalent value of the assets withdrawn, denominated in the cellar's asset
*/
function redeem(uint256 shares, address receiver, address owner)
public
override
nonReentrant
returns (uint256 assets)
{
(uint256 _totalAssets, uint256 _totalSupply) = _getTotalAssetsAndTotalSupply(false);
// Check for rounding error since we round down in previewRedeem.
if ((assets = _convertToAssets(shares, _totalAssets, _totalSupply)) == 0) revert Cellar__ZeroAssets();
_exit(assets, shares, receiver, owner);
}
/**
* @notice Struct used in `_withdrawInOrder` in order to hold multiple pricing values in a single variable.
* @dev Prevents stack too deep errors.
*/
struct WithdrawPricing {
uint256 priceBaseUSD;
uint256 oneBase;
uint256 priceQuoteUSD;
uint256 oneQuote;
}
/**
* @notice Multipler used to insure calculations use very high precision.
*/
uint256 private constant PRECISION_MULTIPLIER = 1e18;
/**
* @dev Withdraw from positions in the order defined by `positions`.
* @param assets the amount of assets to withdraw from cellar
* @param receiver the address to sent withdrawn assets to
* @dev Only loop through credit array because debt can not be withdraw by users.
*/
function _withdrawInOrder(uint256 assets, address receiver) internal {
// Save asset price in USD, and decimals to reduce external calls.
WithdrawPricing memory pricingInfo;
pricingInfo.priceQuoteUSD = priceRouter.getPriceInUSD(asset);
pricingInfo.oneQuote = 10 ** decimals;
uint256 creditLength = creditPositions.length;
for (uint256 i; i < creditLength; ++i) {
uint32 position = creditPositions[i];
uint256 withdrawableBalance = _withdrawableFrom(position);
// Move on to next position if this one is empty.
if (withdrawableBalance == 0) continue;
ERC20 positionAsset = _assetOf(position);
pricingInfo.priceBaseUSD = priceRouter.getPriceInUSD(positionAsset);
pricingInfo.oneBase = 10 ** positionAsset.decimals();
uint256 totalWithdrawableBalanceInAssets;
{
uint256 withdrawableBalanceInUSD = (PRECISION_MULTIPLIER * withdrawableBalance).mulDivDown(
pricingInfo.priceBaseUSD, pricingInfo.oneBase
);
totalWithdrawableBalanceInAssets =
withdrawableBalanceInUSD.mulDivDown(pricingInfo.oneQuote, pricingInfo.priceQuoteUSD);
totalWithdrawableBalanceInAssets = totalWithdrawableBalanceInAssets / PRECISION_MULTIPLIER;
}
// We want to pull as much as we can from this position, but no more than needed.
uint256 amount;
if (totalWithdrawableBalanceInAssets > assets) {
// Convert assets into position asset.
uint256 assetsInUSD =
(PRECISION_MULTIPLIER * assets).mulDivDown(pricingInfo.priceQuoteUSD, pricingInfo.oneQuote);
amount = assetsInUSD.mulDivDown(pricingInfo.oneBase, pricingInfo.priceBaseUSD);
amount = amount / PRECISION_MULTIPLIER;
assets = 0;
} else {
amount = withdrawableBalance;
assets = assets - totalWithdrawableBalanceInAssets;
}
// Withdraw from position.
_withdrawFrom(position, amount, receiver);
// Stop if no more assets to withdraw.
if (assets == 0) break;
}
// If withdraw did not remove all assets owed, revert.
if (assets > 0) revert Cellar__IncompleteWithdraw(assets);
}
// ========================================= ACCOUNTING LOGIC =========================================
/**
* @notice Get the Cellars Total Assets, and Total Supply.
* @dev bool input is not used, but if it were used the following is true.
* true: return the largest possible total assets
* false: return the smallest possible total assets
*/
function _getTotalAssetsAndTotalSupply(bool)
internal
view
virtual
returns (uint256 _totalAssets, uint256 _totalSupply)
{
_totalAssets = _calculateTotalAssetsOrTotalAssetsWithdrawable(false);
_totalSupply = totalSupply;
}
/**
* @notice Internal accounting function that can report total assets, or total assets withdrawable.
* @param reportWithdrawable if true, then the withdrawable total assets is reported,
* if false, then the total assets is reported
*/
function _calculateTotalAssetsOrTotalAssetsWithdrawable(bool reportWithdrawable)
internal
view
returns (uint256 assets)
{
uint256 numOfCreditPositions = creditPositions.length;
ERC20[] memory creditAssets = new ERC20[](numOfCreditPositions);
uint256[] memory creditBalances = new uint256[](numOfCreditPositions);
// If we just need the withdrawable, then query credit array value.
if (reportWithdrawable) {
for (uint256 i; i < numOfCreditPositions; ++i) {
uint32 position = creditPositions[i];
// If the withdrawable balance is zero there is no point to query the asset since a zero balance has zero value.
if ((creditBalances[i] = _withdrawableFrom(position)) == 0) continue;
creditAssets[i] = _assetOf(position);
}
assets = priceRouter.getValues(creditAssets, creditBalances, asset);
} else {