-
Notifications
You must be signed in to change notification settings - Fork 36
/
Cellar.sol
1491 lines (1277 loc) · 57.9 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.16;
import { ERC4626, SafeTransferLib, Math, ERC20 } from "./ERC4626.sol";
import { Registry } from "src/Registry.sol";
import { PriceRouter } from "src/modules/price-router/PriceRouter.sol";
import { IGravity } from "src/interfaces/external/IGravity.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 { Owned } from "@solmate/auth/Owned.sol";
/**
* @title Sommelier Cellar
* @notice A composable ERC4626 that can use arbitrary DeFi assets/positions using adaptors.
* @author crispymangoes
*/
contract Cellar is ERC4626, Owned, ERC721Holder {
using Uint32Array for uint32[];
using SafeTransferLib for ERC20;
using Math for uint256;
using Address for address;
// ========================================= REENTRANCY GUARD =========================================
/**
* @notice `locked` is public, so that the state can be checked even during view function calls.
*/
uint256 public locked = 1;
modifier nonReentrant() {
require(locked == 1, "REENTRANCY");
locked = 2;
_;
locked = 1;
}
// ========================================= 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 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 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 Array of uint32s made up of cellars credit positions Ids.
*/
uint32[] public creditPositions;
/**
* @notice Array of uint32s made up of cellars debt positions Ids.
*/
uint32[] public 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) public 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 public constant MAX_POSITIONS = 16;
/**
* @notice Stores the index of the holding position in the creditPositions array.
*/
uint32 public holdingPosition;
/**
* @notice Allows owner to change the holding position.
*/
function setHoldingPosition(uint32 positionId) external onlyOwner {
_setHoldingPosition(positionId);
}
function _setHoldingPosition(uint32 positionId) internal {
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 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
*/
function addPosition(
uint32 index,
uint32 positionId,
bytes memory configurationData,
bool inDebtArray
) external onlyOwner whenNotShutdown {
_addPosition(index, positionId, configurationData, inDebtArray);
}
/**
* @notice Internal function ise used by `addPosition` and initialize function.
*/
function _addPosition(
uint32 index,
uint32 positionId,
bytes memory configurationData,
bool inDebtArray
) internal {
// Check if position is already being used.
if (isPositionUsed[positionId]) revert Cellar__PositionAlreadyUsed(positionId);
// Grab position data from registry.
(address adaptor, bool isDebt, bytes memory adaptorData) = registry.cellarAddPosition(
positionId,
assetRiskTolerance,
protocolRiskTolerance
);
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.
* @param index index at which to remove the position
*/
function removePosition(uint32 index, bool inDebtArray) external onlyOwner {
// Get position being removed.
uint32 positionId = inDebtArray ? debtPositions[index] : creditPositions[index];
if (positionId == holdingPosition) revert Cellar__RemovingHoldingPosition();
// 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);
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.
*/
function swapPositions(
uint32 index1,
uint32 index2,
bool inDebtArray
) external onlyOwner {
// 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 platform fees is changed.
* @param oldPlatformFee value platform fee was changed from
* @param newPlatformFee value platform fee was changed to
*/
event PlatformFeeChanged(uint64 oldPlatformFee, uint64 newPlatformFee);
/**
* @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 public constant MAX_PLATFORM_FEE = 0.2e18;
/**
* @notice Sets the max possible fee cut for this cellar.
*/
uint64 public constant MAX_FEE_CUT = 1e18;
/**
* @notice Set the percentage of platform fees accrued over a year.
* @param newPlatformFee value out of 1e18 that represents new platform fee percentage
*/
function setPlatformFee(uint64 newPlatformFee) external onlyOwner {
if (newPlatformFee > MAX_PLATFORM_FEE) revert Cellar__InvalidFee();
emit PlatformFeeChanged(feeData.platformFee, newPlatformFee);
feeData.platformFee = newPlatformFee;
}
/**
* @notice Sets the Strategists cut of platform fees
* @param cut the platform cut for the strategist
*/
function setStrategistPlatformCut(uint64 cut) external onlyOwner {
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
*/
function setStrategistPayoutAddress(address payout) external onlyOwner {
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 Whether or not the contract is shutdown in case of an emergency.
*/
bool public isShutdown;
/**
* @notice Prevent a function from being called during a shutdown.
*/
modifier whenNotShutdown() {
if (isShutdown) revert Cellar__ContractShutdown();
_;
}
/**
* @notice Shutdown the cellar. Used in an emergency or if the cellar has been deprecated.
* @dev In the case where
*/
function initiateShutdown() external whenNotShutdown onlyOwner {
isShutdown = true;
emit ShutdownChanged(true);
}
/**
* @notice Restart the cellar.
*/
function liftShutdown() external onlyOwner {
if (!isShutdown) revert Cellar__ContractNotShutdown();
isShutdown = false;
emit ShutdownChanged(false);
}
// =========================================== CONSTRUCTOR ===========================================
/**
* @notice Addresses of the positions currently used by the cellar.
*/
uint256 public constant PRICE_ROUTER_REGISTRY_SLOT = 2;
/**
* @notice Address of the platform's registry contract. Used to get the latest address of modules.
*/
Registry public registry;
/**
* @notice Determines this cellars risk tolerance in regards to assets it is exposed to.
* @dev 0: safest
* type(uint128).max: no restrictions
*/
uint128 public assetRiskTolerance;
/**
* @notice Determines this cellars risk tolerance in regards to protocols it uses.
* @dev 0: safest
* type(uint128).max: no restrictions
*/
uint128 public protocolRiskTolerance;
/**
* @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 params abi encode values.
* - _creditPositions ids of the credit positions to initialize the cellar with
* - _debtPositions ids of the credit positions to initialize the cellar with
* - _creditConfigurationData configuration data for each position
* - _debtConfigurationData configuration data for each position
* - _holdingIndex the index in _creditPositions to use as the holding position.
* - _strategistPayout the address to send the strategists fee shares.
* - _assetRiskTolerance this cellars risk tolerance for assets it is exposed to
* - _protocolRiskTolerance this cellars risk tolerance for protocols it will use
*/
// TODO add tests related to setting new holding position
// Fix initializer function
// Fix tests.
constructor(
Registry _registry,
ERC20 _asset,
string memory _name,
string memory _symbol,
bytes memory params
) ERC4626(_asset, _name, _symbol, 18) Owned(_registry.getAddress(0)) {
registry = _registry;
{
(
uint32[] memory _creditPositions,
uint32[] memory _debtPositions,
bytes[] memory _creditConfigurationData,
bytes[] memory _debtConfigurationData,
uint32 _holdingPosition
) = abi.decode(params, (uint32[], uint32[], bytes[], bytes[], uint8));
// Initialize positions.
for (uint32 i; i < _creditPositions.length; ++i) {
_addPosition(i, _creditPositions[i], _creditConfigurationData[i], false);
}
for (uint32 i; i < _debtPositions.length; ++i) {
_addPosition(i, _debtPositions[i], _debtConfigurationData[i], true);
}
// This check allows us to deploy an implementation contract.
/// @dev No cellars will be deployed with a zero length credit positions array.
if (_creditPositions.length > 0) _setHoldingPosition(_holdingPosition);
}
// Initialize last accrual timestamp to time that cellar was created, otherwise the first
// `accrue` will take platform fees from 1970 to the time it is called.
feeData.lastAccrual = uint64(block.timestamp);
(, , , , , address _strategistPayout, uint128 _assetRiskTolerance, uint128 _protocolRiskTolerance) = abi.decode(
params,
(uint32[], uint32[], bytes[], bytes[], uint8, address, uint128, uint128)
);
feeData.strategistPayoutAddress = _strategistPayout;
assetRiskTolerance = _assetRiskTolerance;
protocolRiskTolerance = _protocolRiskTolerance;
}
// =========================================== CORE LOGIC ===========================================
/**
* @notice Emitted when share locking period is changed.
* @param oldPeriod the old locking period
* @param newPeriod the new locking period
*/
event ShareLockingPeriodChanged(uint256 oldPeriod, uint256 newPeriod);
/**
* @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 Attempted to set `shareLockPeriod` to an invalid number.
*/
error Cellar__InvalidShareLockPeriod();
/**
* @notice Attempted to burn shares when they are locked.
* @param timeSharesAreUnlocked time when caller can transfer/redeem shares
* @param currentBlock the current block number.
*/
error Cellar__SharesAreLocked(uint256 timeSharesAreUnlocked, uint256 currentBlock);
/**
* @notice Attempted deposit on behalf of a user without being approved.
*/
error Cellar__NotApprovedToDepositOnBehalf(address depositor);
/**
* @notice Shares must be locked for at least 5 minutes after minting.
*/
uint256 public constant MINIMUM_SHARE_LOCK_PERIOD = 5 * 60;
/**
* @notice Shares can be locked for at most 2 days after minting.
*/
uint256 public constant MAXIMUM_SHARE_LOCK_PERIOD = 2 days;
/**
* @notice After deposits users must wait `shareLockPeriod` time before being able to transfer or withdraw their shares.
*/
uint256 public shareLockPeriod = MAXIMUM_SHARE_LOCK_PERIOD;
/**
* @notice mapping that stores every users last time stamp they minted shares.
*/
mapping(address => uint256) public userShareLockStartTime;
/**
* @notice Allows share lock period to be updated.
* @param newLock the new lock period
*/
function setShareLockPeriod(uint256 newLock) external onlyOwner {
if (newLock < MINIMUM_SHARE_LOCK_PERIOD || newLock > MAXIMUM_SHARE_LOCK_PERIOD)
revert Cellar__InvalidShareLockPeriod();
uint256 oldLockingPeriod = shareLockPeriod;
shareLockPeriod = newLock;
emit ShareLockingPeriodChanged(oldLockingPeriod, newLock);
}
/**
* @notice helper function that checks enough time has passed to unlock shares.
* @param owner the address of the user to check
*/
function _checkIfSharesLocked(address owner) internal view {
uint256 lockTime = userShareLockStartTime[owner];
if (lockTime != 0) {
uint256 timeSharesAreUnlocked = lockTime + shareLockPeriod;
if (timeSharesAreUnlocked > block.timestamp)
revert Cellar__SharesAreLocked(timeSharesAreUnlocked, block.timestamp);
}
}
/**
* @notice Override `transfer` to add share lock check.
*/
function transfer(address to, uint256 amount) public override returns (bool) {
_checkIfSharesLocked(msg.sender);
return super.transfer(to, amount);
}
/**
* @notice Override `transferFrom` to add share lock check.
*/
function transferFrom(
address from,
address to,
uint256 amount
) public override returns (bool) {
_checkIfSharesLocked(from);
return super.transferFrom(from, to, amount);
}
/**
* @notice Attempted deposit more than the max deposit.
* @param assets the assets user attempted to deposit
* @param maxDeposit the max assets that can be deposited
*/
error Cellar__DepositRestricted(uint256 assets, uint256 maxDeposit);
/**
* @notice called at the beginning of deposit.
* @param assets amount of assets deposited by user.
* @param receiver address receiving the shares.
*/
function beforeDeposit(
uint256 assets,
uint256,
address receiver
) internal view override whenNotShutdown {
if (msg.sender != receiver) {
if (!registry.approvedForDepositOnBehalf(msg.sender))
revert Cellar__NotApprovedToDepositOnBehalf(msg.sender);
}
uint256 maxAssets = maxDeposit(receiver);
if (assets > maxAssets) revert Cellar__DepositRestricted(assets, maxAssets);
}
/**
* @notice called at the end of deposit.
* @param assets amount of assets deposited by user.
*/
function afterDeposit(
uint256 assets,
uint256,
address receiver
) internal override {
_depositTo(holdingPosition, assets);
userShareLockStartTime[receiver] = block.timestamp;
}
/**
* @notice called at the beginning of withdraw.
*/
function beforeWithdraw(
uint256,
uint256,
address,
address owner
) internal view override {
// Make sure users shares are not locked.
_checkIfSharesLocked(owner);
}
function _enter(
uint256 assets,
uint256 shares,
address receiver
) internal {
beforeDeposit(assets, shares, receiver);
// Need to transfer before minting or ERC777s could reenter.
asset.safeTransferFrom(msg.sender, address(this), assets);
_mint(receiver, shares);
emit Deposit(msg.sender, receiver, assets, shares);
afterDeposit(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 override nonReentrant returns (uint256 shares) {
// Use `_accounting` instead of totalAssets bc re-entrancy is already checked in this function.
uint256 _totalAssets = _accounting(false);
// Check for rounding error since we round down in previewDeposit.
if ((shares = _convertToShares(assets, _totalAssets)) == 0) revert Cellar__ZeroShares();
_enter(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) {
// Use `_accounting` instead of totalAssets bc re-entrancy is already checked in this function.
uint256 _totalAssets = _accounting(false);
// previewMint rounds up, but initial mint could return zero assets, so check for rounding error.
if ((assets = _previewMint(shares, _totalAssets)) == 0) revert Cellar__ZeroAssets();
_enter(assets, shares, receiver);
}
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) {
// Use `_accounting` instead of totalAssets bc re-entrancy is already checked in this function.
uint256 _totalAssets = _accounting(false);
// No need to check for rounding error, `previewWithdraw` rounds up.
shares = _previewWithdraw(assets, _totalAssets);
_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) {
// Use `_accounting` instead of totalAssets bc re-entrancy is already checked in this function.
uint256 _totalAssets = _accounting(false);
// Check for rounding error since we round down in previewRedeem.
if ((assets = _convertToAssets(shares, _totalAssets)) == 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 {
// Get the price router.
PriceRouter priceRouter = PriceRouter(registry.getAddress(PRICE_ROUTER_REGISTRY_SLOT));
// Save asset price in USD, and decimals to reduce external calls.
WithdrawPricing memory pricingInfo;
pricingInfo.priceQuoteUSD = priceRouter.getPriceInUSD(asset);
pricingInfo.oneQuote = 10**asset.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 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 _accounting(bool reportWithdrawable) internal view returns (uint256 assets) {
uint256 numOfCreditPositions = creditPositions.length;
ERC20[] memory creditAssets = new ERC20[](numOfCreditPositions);
uint256[] memory creditBalances = new uint256[](numOfCreditPositions);
PriceRouter priceRouter = PriceRouter(registry.getAddress(PRICE_ROUTER_REGISTRY_SLOT));
// 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 {
uint256 numOfDebtPositions = debtPositions.length;
ERC20[] memory debtAssets = new ERC20[](numOfDebtPositions);
uint256[] memory debtBalances = new uint256[](numOfDebtPositions);
for (uint256 i; i < numOfCreditPositions; ++i) {
uint32 position = creditPositions[i];
// If the balance is zero there is no point to query the asset since a zero balance has zero value.
if ((creditBalances[i] = _balanceOf(position)) == 0) continue;
creditAssets[i] = _assetOf(position);
}
for (uint256 i; i < numOfDebtPositions; ++i) {
uint32 position = debtPositions[i];
// If the balance is zero there is no point to query the asset since a zero balance has zero value.
if ((debtBalances[i] = _balanceOf(position)) == 0) continue;
debtAssets[i] = _assetOf(position);
}
assets = priceRouter.getValuesDelta(creditAssets, creditBalances, debtAssets, debtBalances, asset);
}
}
/**
* @notice The total amount of assets in the cellar.
* @dev EIP4626 states totalAssets needs to be inclusive of fees.
* Since performance fees mint shares, total assets remains unchanged,
* so this implementation is inclusive of fees even though it does not explicitly show it.
* @dev EIP4626 states totalAssets must not revert, but it is possible for `totalAssets` to revert
* so it does NOT conform to ERC4626 standards.
* @dev Run a re-entrancy check because totalAssets can be wrong if re-entering from deposit/withdraws.
*/
function totalAssets() public view override returns (uint256 assets) {
require(locked == 1, "REENTRANCY");
assets = _accounting(false);
}
/**
* @notice The total amount of withdrawable assets in the cellar.
* @dev Run a re-entrancy check because totalAssetsWithdrawable can be wrong if re-entering from deposit/withdraws.
*/
function totalAssetsWithdrawable() public view returns (uint256 assets) {
require(locked == 1, "REENTRANCY");
assets = _accounting(true);
}
/**
* @notice The amount of assets that the cellar would exchange for the amount of shares provided.
* @param shares amount of shares to convert
* @return assets the shares can be exchanged for
*/
function convertToAssets(uint256 shares) public view override returns (uint256 assets) {
assets = _convertToAssets(shares, totalAssets());
}
/**
* @notice The amount of shares that the cellar would exchange for the amount of assets provided.
* @param assets amount of assets to convert
* @return shares the assets can be exchanged for
*/
function convertToShares(uint256 assets) public view override returns (uint256 shares) {
shares = _convertToShares(assets, totalAssets());
}
/**
* @notice Simulate the effects of minting shares at the current block, given current on-chain conditions.
* @param shares amount of shares to mint
* @return assets that will be deposited
*/
function previewMint(uint256 shares) public view override returns (uint256 assets) {
uint256 _totalAssets = totalAssets();
assets = _previewMint(shares, _totalAssets);
}
/**
* @notice Simulate the effects of withdrawing assets at the current block, given current on-chain conditions.
* @param assets amount of assets to withdraw
* @return shares that will be redeemed
*/
function previewWithdraw(uint256 assets) public view override returns (uint256 shares) {
uint256 _totalAssets = totalAssets();