-
Notifications
You must be signed in to change notification settings - Fork 75
/
Vault.sol
828 lines (652 loc) · 38.8 KB
/
Vault.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
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity 0.8.10;
import {Auth} from "solmate/auth/Auth.sol";
import {ERC4626} from "solmate/mixins/ERC4626.sol";
import {SafeCastLib} from "solmate/utils/SafeCastLib.sol";
import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol";
import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol";
import {WETH} from "solmate/tokens/WETH.sol";
import {ERC20} from "solmate/tokens/ERC20.sol";
import {Strategy, ERC20Strategy, ETHStrategy} from "./interfaces/Strategy.sol";
/// @title Rari Vault (rvToken)
/// @author Transmissions11 and JetJadeja
/// @notice Flexible, minimalist, and gas-optimized yield
/// aggregator for earning interest on any ERC20 token.
contract Vault is ERC4626, Auth {
using SafeCastLib for uint256;
using SafeTransferLib for ERC20;
using FixedPointMathLib for uint256;
/*///////////////////////////////////////////////////////////////
CONSTANTS
//////////////////////////////////////////////////////////////*/
/// @notice The maximum number of elements allowed on the withdrawal stack.
/// @dev Needed to prevent denial of service attacks by queue operators.
uint256 internal constant MAX_WITHDRAWAL_STACK_SIZE = 32;
/*///////////////////////////////////////////////////////////////
IMMUTABLES
//////////////////////////////////////////////////////////////*/
/// @notice The underlying token the Vault accepts.
ERC20 public immutable UNDERLYING;
/// @notice The base unit of the underlying token and hence rvToken.
/// @dev Equal to 10 ** decimals. Used for fixed point arithmetic.
uint256 internal immutable BASE_UNIT;
/// @notice Creates a new Vault that accepts a specific underlying token.
/// @param _UNDERLYING The ERC20 compliant token the Vault should accept.
constructor(ERC20 _UNDERLYING)
ERC4626(
// Underlying token
_UNDERLYING,
// ex: Rari Dai Stablecoin Vault
string(abi.encodePacked("Rari ", _UNDERLYING.name(), " Vault")),
// ex: rvDAI
string(abi.encodePacked("rv", _UNDERLYING.symbol()))
)
Auth(Auth(msg.sender).owner(), Auth(msg.sender).authority())
{
UNDERLYING = _UNDERLYING;
BASE_UNIT = 10**decimals;
// Prevent minting of rvTokens until
// the initialize function is called.
totalSupply = type(uint256).max;
}
/*///////////////////////////////////////////////////////////////
FEE CONFIGURATION
//////////////////////////////////////////////////////////////*/
/// @notice The percentage of profit recognized each harvest to reserve as fees.
/// @dev A fixed point number where 1e18 represents 100% and 0 represents 0%.
uint256 public feePercent;
/// @notice Emitted when the fee percentage is updated.
/// @param user The authorized user who triggered the update.
/// @param newFeePercent The new fee percentage.
event FeePercentUpdated(address indexed user, uint256 newFeePercent);
/// @notice Sets a new fee percentage.
/// @param newFeePercent The new fee percentage.
function setFeePercent(uint256 newFeePercent) external requiresAuth {
// A fee percentage over 100% doesn't make sense.
require(newFeePercent <= 1e18, "FEE_TOO_HIGH");
// Update the fee percentage.
feePercent = newFeePercent;
emit FeePercentUpdated(msg.sender, newFeePercent);
}
/*///////////////////////////////////////////////////////////////
HARVEST CONFIGURATION
//////////////////////////////////////////////////////////////*/
/// @notice Emitted when the harvest window is updated.
/// @param user The authorized user who triggered the update.
/// @param newHarvestWindow The new harvest window.
event HarvestWindowUpdated(address indexed user, uint128 newHarvestWindow);
/// @notice Emitted when the harvest delay is updated.
/// @param user The authorized user who triggered the update.
/// @param newHarvestDelay The new harvest delay.
event HarvestDelayUpdated(address indexed user, uint64 newHarvestDelay);
/// @notice Emitted when the harvest delay is scheduled to be updated next harvest.
/// @param user The authorized user who triggered the update.
/// @param newHarvestDelay The scheduled updated harvest delay.
event HarvestDelayUpdateScheduled(address indexed user, uint64 newHarvestDelay);
/// @notice The period in seconds during which multiple harvests can occur
/// regardless if they are taking place before the harvest delay has elapsed.
/// @dev Long harvest windows open the Vault up to profit distribution slowdown attacks.
uint128 public harvestWindow;
/// @notice The period in seconds over which locked profit is unlocked.
/// @dev Cannot be 0 as it opens harvests up to sandwich attacks.
uint64 public harvestDelay;
/// @notice The value that will replace harvestDelay next harvest.
/// @dev In the case that the next delay is 0, no update will be applied.
uint64 public nextHarvestDelay;
/// @notice Sets a new harvest window.
/// @param newHarvestWindow The new harvest window.
/// @dev The Vault's harvestDelay must already be set before calling.
function setHarvestWindow(uint128 newHarvestWindow) external requiresAuth {
// A harvest window longer than the harvest delay doesn't make sense.
require(newHarvestWindow <= harvestDelay, "WINDOW_TOO_LONG");
// Update the harvest window.
harvestWindow = newHarvestWindow;
emit HarvestWindowUpdated(msg.sender, newHarvestWindow);
}
/// @notice Sets a new harvest delay.
/// @param newHarvestDelay The new harvest delay to set.
/// @dev If the current harvest delay is 0, meaning it has not
/// been set before, it will be updated immediately, otherwise
/// it will be scheduled to take effect after the next harvest.
function setHarvestDelay(uint64 newHarvestDelay) external requiresAuth {
// A harvest delay of 0 makes harvests vulnerable to sandwich attacks.
require(newHarvestDelay != 0, "DELAY_CANNOT_BE_ZERO");
// A harvest delay longer than 1 year doesn't make sense.
require(newHarvestDelay <= 365 days, "DELAY_TOO_LONG");
// If the harvest delay is 0, meaning it has not been set before:
if (harvestDelay == 0) {
// We'll apply the update immediately.
harvestDelay = newHarvestDelay;
emit HarvestDelayUpdated(msg.sender, newHarvestDelay);
} else {
// We'll apply the update next harvest.
nextHarvestDelay = newHarvestDelay;
emit HarvestDelayUpdateScheduled(msg.sender, newHarvestDelay);
}
}
/*///////////////////////////////////////////////////////////////
TARGET FLOAT CONFIGURATION
//////////////////////////////////////////////////////////////*/
/// @notice The desired percentage of the Vault's holdings to keep as float.
/// @dev A fixed point number where 1e18 represents 100% and 0 represents 0%.
uint256 public targetFloatPercent;
/// @notice Emitted when the target float percentage is updated.
/// @param user The authorized user who triggered the update.
/// @param newTargetFloatPercent The new target float percentage.
event TargetFloatPercentUpdated(address indexed user, uint256 newTargetFloatPercent);
/// @notice Set a new target float percentage.
/// @param newTargetFloatPercent The new target float percentage.
function setTargetFloatPercent(uint256 newTargetFloatPercent) external requiresAuth {
// A target float percentage over 100% doesn't make sense.
require(newTargetFloatPercent <= 1e18, "TARGET_TOO_HIGH");
// Update the target float percentage.
targetFloatPercent = newTargetFloatPercent;
emit TargetFloatPercentUpdated(msg.sender, newTargetFloatPercent);
}
/*///////////////////////////////////////////////////////////////
UNDERLYING IS WETH CONFIGURATION
//////////////////////////////////////////////////////////////*/
/// @notice Whether the Vault should treat the underlying token as WETH compatible.
/// @dev If enabled the Vault will allow trusting strategies that accept Ether.
bool public underlyingIsWETH;
/// @notice Emitted when whether the Vault should treat the underlying as WETH is updated.
/// @param user The authorized user who triggered the update.
/// @param newUnderlyingIsWETH Whether the Vault nows treats the underlying as WETH.
event UnderlyingIsWETHUpdated(address indexed user, bool newUnderlyingIsWETH);
/// @notice Sets whether the Vault treats the underlying as WETH.
/// @param newUnderlyingIsWETH Whether the Vault should treat the underlying as WETH.
/// @dev The underlying token must have 18 decimals, to match Ether's decimal scheme.
function setUnderlyingIsWETH(bool newUnderlyingIsWETH) external requiresAuth {
// Ensure the underlying token's decimals match ETH if is WETH being set to true.
require(!newUnderlyingIsWETH || UNDERLYING.decimals() == 18, "WRONG_DECIMALS");
// Update whether the Vault treats the underlying as WETH.
underlyingIsWETH = newUnderlyingIsWETH;
emit UnderlyingIsWETHUpdated(msg.sender, newUnderlyingIsWETH);
}
/*///////////////////////////////////////////////////////////////
STRATEGY STORAGE
//////////////////////////////////////////////////////////////*/
/// @notice The total amount of underlying tokens held in strategies at the time of the last harvest.
/// @dev Includes maxLockedProfit, must be correctly subtracted to compute available/free holdings.
uint256 public totalStrategyHoldings;
/// @dev Packed struct of strategy data.
/// @param trusted Whether the strategy is trusted.
/// @param balance The amount of underlying tokens held in the strategy.
struct StrategyData {
// Used to determine if the Vault will operate on a strategy.
bool trusted;
// Used to determine profit and loss during harvests of the strategy.
uint248 balance;
}
/// @notice Maps strategies to data the Vault holds on them.
mapping(Strategy => StrategyData) public getStrategyData;
/*///////////////////////////////////////////////////////////////
HARVEST STORAGE
//////////////////////////////////////////////////////////////*/
/// @notice A timestamp representing when the first harvest in the most recent harvest window occurred.
/// @dev May be equal to lastHarvest if there was/has only been one harvest in the most last/current window.
uint64 public lastHarvestWindowStart;
/// @notice A timestamp representing when the most recent harvest occurred.
uint64 public lastHarvest;
/// @notice The amount of locked profit at the end of the last harvest.
uint128 public maxLockedProfit;
/*///////////////////////////////////////////////////////////////
WITHDRAWAL STACK STORAGE
//////////////////////////////////////////////////////////////*/
/// @notice An ordered array of strategies representing the withdrawal stack.
/// @dev The stack is processed in descending order, meaning the last index will be withdrawn from first.
/// @dev Strategies that are untrusted, duplicated, or have no balance are filtered out when encountered at
/// withdrawal time, not validated upfront, meaning the stack may not reflect the "true" set used for withdrawals.
Strategy[] public withdrawalStack;
/// @notice Gets the full withdrawal stack.
/// @return An ordered array of strategies representing the withdrawal stack.
/// @dev This is provided because Solidity converts public arrays into index getters,
/// but we need a way to allow external contracts and users to access the whole array.
function getWithdrawalStack() external view returns (Strategy[] memory) {
return withdrawalStack;
}
/*///////////////////////////////////////////////////////////////
DEPOSIT/WITHDRAWAL LOGIC
//////////////////////////////////////////////////////////////*/
function afterDeposit(uint256, uint256) internal override {}
function beforeWithdraw(uint256 assets, uint256) internal override {
// Retrieve underlying tokens from strategies/float.
retrieveUnderlying(assets);
}
/// @dev Retrieves a specific amount of underlying tokens held in strategies and/or float.
/// @dev Only withdraws from strategies if needed and maintains the target float percentage if possible.
/// @param underlyingAmount The amount of underlying tokens to retrieve.
function retrieveUnderlying(uint256 underlyingAmount) internal {
// Get the Vault's floating balance.
uint256 float = totalFloat();
// If the amount is greater than the float, withdraw from strategies.
if (underlyingAmount > float) {
// Compute the amount needed to reach our target float percentage.
uint256 floatMissingForTarget = (totalAssets() - underlyingAmount).mulWadDown(targetFloatPercent);
// Compute the bare minimum amount we need for this withdrawal.
uint256 floatMissingForWithdrawal = underlyingAmount - float;
// Pull enough to cover the withdrawal and reach our target float percentage.
pullFromWithdrawalStack(floatMissingForWithdrawal + floatMissingForTarget);
}
}
/*///////////////////////////////////////////////////////////////
VAULT ACCOUNTING LOGIC
//////////////////////////////////////////////////////////////*/
/// @notice Calculates the total amount of underlying tokens the Vault holds.
/// @return totalUnderlyingHeld The total amount of underlying tokens the Vault holds.
function totalAssets() public view override returns (uint256 totalUnderlyingHeld) {
unchecked {
// Cannot underflow as locked profit can't exceed total strategy holdings.
totalUnderlyingHeld = totalStrategyHoldings - lockedProfit();
}
// Include our floating balance in the total.
totalUnderlyingHeld += totalFloat();
}
/// @notice Calculates the current amount of locked profit.
/// @return The current amount of locked profit.
function lockedProfit() public view returns (uint256) {
// Get the last harvest and harvest delay.
uint256 previousHarvest = lastHarvest;
uint256 harvestInterval = harvestDelay;
unchecked {
// If the harvest delay has passed, there is no locked profit.
// Cannot overflow on human timescales since harvestInterval is capped.
if (block.timestamp >= previousHarvest + harvestInterval) return 0;
// Get the maximum amount we could return.
uint256 maximumLockedProfit = maxLockedProfit;
// Compute how much profit remains locked based on the last harvest and harvest delay.
// It's impossible for the previous harvest to be in the future, so this will never underflow.
return maximumLockedProfit - (maximumLockedProfit * (block.timestamp - previousHarvest)) / harvestInterval;
}
}
/// @notice Returns the amount of underlying tokens that idly sit in the Vault.
/// @return The amount of underlying tokens that sit idly in the Vault.
function totalFloat() public view returns (uint256) {
return UNDERLYING.balanceOf(address(this));
}
/*///////////////////////////////////////////////////////////////
HARVEST LOGIC
//////////////////////////////////////////////////////////////*/
/// @notice Emitted after a successful harvest.
/// @param user The authorized user who triggered the harvest.
/// @param strategies The trusted strategies that were harvested.
event Harvest(address indexed user, Strategy[] strategies);
/// @notice Harvest a set of trusted strategies.
/// @param strategies The trusted strategies to harvest.
/// @dev Will always revert if called outside of an active
/// harvest window or before the harvest delay has passed.
function harvest(Strategy[] calldata strategies) external requiresAuth {
// If this is the first harvest after the last window:
if (block.timestamp >= lastHarvest + harvestDelay) {
// Set the harvest window's start timestamp.
// Cannot overflow 64 bits on human timescales.
lastHarvestWindowStart = uint64(block.timestamp);
} else {
// We know this harvest is not the first in the window so we need to ensure it's within it.
require(block.timestamp <= lastHarvestWindowStart + harvestWindow, "BAD_HARVEST_TIME");
}
// Get the Vault's current total strategy holdings.
uint256 oldTotalStrategyHoldings = totalStrategyHoldings;
// Used to store the total profit accrued by the strategies.
uint256 totalProfitAccrued;
// Used to store the new total strategy holdings after harvesting.
uint256 newTotalStrategyHoldings = oldTotalStrategyHoldings;
// Will revert if any of the specified strategies are untrusted.
for (uint256 i = 0; i < strategies.length; i++) {
// Get the strategy at the current index.
Strategy strategy = strategies[i];
// If an untrusted strategy could be harvested a malicious user could use
// a fake strategy that over-reports holdings to manipulate the exchange rate.
require(getStrategyData[strategy].trusted, "UNTRUSTED_STRATEGY");
// Get the strategy's previous and current balance.
uint256 balanceLastHarvest = getStrategyData[strategy].balance;
uint256 balanceThisHarvest = strategy.balanceOfUnderlying(address(this));
// Update the strategy's stored balance. Cast overflow is unrealistic.
getStrategyData[strategy].balance = balanceThisHarvest.safeCastTo248();
// Increase/decrease newTotalStrategyHoldings based on the profit/loss registered.
// We cannot wrap the subtraction in parenthesis as it would underflow if the strategy had a loss.
newTotalStrategyHoldings = newTotalStrategyHoldings + balanceThisHarvest - balanceLastHarvest;
unchecked {
// Update the total profit accrued while counting losses as zero profit.
// Cannot overflow as we already increased total holdings without reverting.
totalProfitAccrued += balanceThisHarvest > balanceLastHarvest
? balanceThisHarvest - balanceLastHarvest // Profits since last harvest.
: 0; // If the strategy registered a net loss we don't have any new profit.
}
}
// Compute fees as the fee percent multiplied by the profit.
uint256 feesAccrued = totalProfitAccrued.mulDivDown(feePercent, 1e18);
// If we accrued any fees, mint an equivalent amount of rvTokens.
// Authorized users can claim the newly minted rvTokens via claimFees.
_mint(address(this), feesAccrued.mulDivDown(BASE_UNIT, convertToAssets(BASE_UNIT)));
// Update max unlocked profit based on any remaining locked profit plus new profit.
maxLockedProfit = (lockedProfit() + totalProfitAccrued - feesAccrued).safeCastTo128();
// Set strategy holdings to our new total.
totalStrategyHoldings = newTotalStrategyHoldings;
// Update the last harvest timestamp.
// Cannot overflow on human timescales.
lastHarvest = uint64(block.timestamp);
emit Harvest(msg.sender, strategies);
// Get the next harvest delay.
uint64 newHarvestDelay = nextHarvestDelay;
// If the next harvest delay is not 0:
if (newHarvestDelay != 0) {
// Update the harvest delay.
harvestDelay = newHarvestDelay;
// Reset the next harvest delay.
nextHarvestDelay = 0;
emit HarvestDelayUpdated(msg.sender, newHarvestDelay);
}
}
/*///////////////////////////////////////////////////////////////
STRATEGY DEPOSIT/WITHDRAWAL LOGIC
//////////////////////////////////////////////////////////////*/
/// @notice Emitted after the Vault deposits into a strategy contract.
/// @param user The authorized user who triggered the deposit.
/// @param strategy The strategy that was deposited into.
/// @param underlyingAmount The amount of underlying tokens that were deposited.
event StrategyDeposit(address indexed user, Strategy indexed strategy, uint256 underlyingAmount);
/// @notice Emitted after the Vault withdraws funds from a strategy contract.
/// @param user The authorized user who triggered the withdrawal.
/// @param strategy The strategy that was withdrawn from.
/// @param underlyingAmount The amount of underlying tokens that were withdrawn.
event StrategyWithdrawal(address indexed user, Strategy indexed strategy, uint256 underlyingAmount);
/// @notice Deposit a specific amount of float into a trusted strategy.
/// @param strategy The trusted strategy to deposit into.
/// @param underlyingAmount The amount of underlying tokens in float to deposit.
function depositIntoStrategy(Strategy strategy, uint256 underlyingAmount) external requiresAuth {
// A strategy must be trusted before it can be deposited into.
require(getStrategyData[strategy].trusted, "UNTRUSTED_STRATEGY");
// Increase totalStrategyHoldings to account for the deposit.
totalStrategyHoldings += underlyingAmount;
unchecked {
// Without this the next harvest would count the deposit as profit.
// Cannot overflow as the balance of one strategy can't exceed the sum of all.
getStrategyData[strategy].balance += underlyingAmount.safeCastTo248();
}
emit StrategyDeposit(msg.sender, strategy, underlyingAmount);
// We need to deposit differently if the strategy takes ETH.
if (strategy.isCEther()) {
// Unwrap the right amount of WETH.
WETH(payable(address(UNDERLYING))).withdraw(underlyingAmount);
// Deposit into the strategy and assume it will revert on error.
ETHStrategy(address(strategy)).mint{value: underlyingAmount}();
} else {
// Approve underlyingAmount to the strategy so we can deposit.
UNDERLYING.safeApprove(address(strategy), underlyingAmount);
// Deposit into the strategy and revert if it returns an error code.
require(ERC20Strategy(address(strategy)).mint(underlyingAmount) == 0, "MINT_FAILED");
}
}
/// @notice Withdraw a specific amount of underlying tokens from a strategy.
/// @param strategy The strategy to withdraw from.
/// @param underlyingAmount The amount of underlying tokens to withdraw.
/// @dev Withdrawing from a strategy will not remove it from the withdrawal stack.
function withdrawFromStrategy(Strategy strategy, uint256 underlyingAmount) external requiresAuth {
// A strategy must be trusted before it can be withdrawn from.
require(getStrategyData[strategy].trusted, "UNTRUSTED_STRATEGY");
// Without this the next harvest would count the withdrawal as a loss.
getStrategyData[strategy].balance -= underlyingAmount.safeCastTo248();
unchecked {
// Decrease totalStrategyHoldings to account for the withdrawal.
// Cannot underflow as the balance of one strategy will never exceed the sum of all.
totalStrategyHoldings -= underlyingAmount;
}
emit StrategyWithdrawal(msg.sender, strategy, underlyingAmount);
// Withdraw from the strategy and revert if it returns an error code.
require(strategy.redeemUnderlying(underlyingAmount) == 0, "REDEEM_FAILED");
// Wrap the withdrawn Ether into WETH if necessary.
if (strategy.isCEther()) WETH(payable(address(UNDERLYING))).deposit{value: underlyingAmount}();
}
/*///////////////////////////////////////////////////////////////
STRATEGY TRUST/DISTRUST LOGIC
//////////////////////////////////////////////////////////////*/
/// @notice Emitted when a strategy is set to trusted.
/// @param user The authorized user who trusted the strategy.
/// @param strategy The strategy that became trusted.
event StrategyTrusted(address indexed user, Strategy indexed strategy);
/// @notice Emitted when a strategy is set to untrusted.
/// @param user The authorized user who untrusted the strategy.
/// @param strategy The strategy that became untrusted.
event StrategyDistrusted(address indexed user, Strategy indexed strategy);
/// @notice Stores a strategy as trusted, enabling it to be harvested.
/// @param strategy The strategy to make trusted.
function trustStrategy(Strategy strategy) external requiresAuth {
// Ensure the strategy accepts the correct underlying token.
// If the strategy accepts ETH the Vault should accept WETH, it'll handle wrapping when necessary.
require(
strategy.isCEther() ? underlyingIsWETH : ERC20Strategy(address(strategy)).underlying() == UNDERLYING,
"WRONG_UNDERLYING"
);
// Store the strategy as trusted.
getStrategyData[strategy].trusted = true;
emit StrategyTrusted(msg.sender, strategy);
}
/// @notice Stores a strategy as untrusted, disabling it from being harvested.
/// @param strategy The strategy to make untrusted.
function distrustStrategy(Strategy strategy) external requiresAuth {
// Store the strategy as untrusted.
getStrategyData[strategy].trusted = false;
emit StrategyDistrusted(msg.sender, strategy);
}
/*///////////////////////////////////////////////////////////////
WITHDRAWAL STACK LOGIC
//////////////////////////////////////////////////////////////*/
/// @notice Emitted when a strategy is pushed to the withdrawal stack.
/// @param user The authorized user who triggered the push.
/// @param pushedStrategy The strategy pushed to the withdrawal stack.
event WithdrawalStackPushed(address indexed user, Strategy indexed pushedStrategy);
/// @notice Emitted when a strategy is popped from the withdrawal stack.
/// @param user The authorized user who triggered the pop.
/// @param poppedStrategy The strategy popped from the withdrawal stack.
event WithdrawalStackPopped(address indexed user, Strategy indexed poppedStrategy);
/// @notice Emitted when the withdrawal stack is updated.
/// @param user The authorized user who triggered the set.
/// @param replacedWithdrawalStack The new withdrawal stack.
event WithdrawalStackSet(address indexed user, Strategy[] replacedWithdrawalStack);
/// @notice Emitted when an index in the withdrawal stack is replaced.
/// @param user The authorized user who triggered the replacement.
/// @param index The index of the replaced strategy in the withdrawal stack.
/// @param replacedStrategy The strategy in the withdrawal stack that was replaced.
/// @param replacementStrategy The strategy that overrode the replaced strategy at the index.
event WithdrawalStackIndexReplaced(
address indexed user,
uint256 index,
Strategy indexed replacedStrategy,
Strategy indexed replacementStrategy
);
/// @notice Emitted when an index in the withdrawal stack is replaced with the tip.
/// @param user The authorized user who triggered the replacement.
/// @param index The index of the replaced strategy in the withdrawal stack.
/// @param replacedStrategy The strategy in the withdrawal stack replaced by the tip.
/// @param previousTipStrategy The previous tip of the stack that replaced the strategy.
event WithdrawalStackIndexReplacedWithTip(
address indexed user,
uint256 index,
Strategy indexed replacedStrategy,
Strategy indexed previousTipStrategy
);
/// @notice Emitted when the strategies at two indexes are swapped.
/// @param user The authorized user who triggered the swap.
/// @param index1 One index involved in the swap
/// @param index2 The other index involved in the swap.
/// @param newStrategy1 The strategy (previously at index2) that replaced index1.
/// @param newStrategy2 The strategy (previously at index1) that replaced index2.
event WithdrawalStackIndexesSwapped(
address indexed user,
uint256 index1,
uint256 index2,
Strategy indexed newStrategy1,
Strategy indexed newStrategy2
);
/// @dev Withdraw a specific amount of underlying tokens from strategies in the withdrawal stack.
/// @param underlyingAmount The amount of underlying tokens to pull into float.
/// @dev Automatically removes depleted strategies from the withdrawal stack.
function pullFromWithdrawalStack(uint256 underlyingAmount) internal {
// We will update this variable as we pull from strategies.
uint256 amountLeftToPull = underlyingAmount;
// We'll start at the tip of the stack and traverse backwards.
uint256 currentIndex = withdrawalStack.length - 1;
// Iterate in reverse so we pull from the stack in a "last in, first out" manner.
// Will revert due to underflow if we empty the stack before pulling the desired amount.
for (; ; currentIndex--) {
// Get the strategy at the current stack index.
Strategy strategy = withdrawalStack[currentIndex];
// Get the balance of the strategy before we withdraw from it.
uint256 strategyBalance = getStrategyData[strategy].balance;
// If the strategy is currently untrusted or was already depleted:
if (!getStrategyData[strategy].trusted || strategyBalance == 0) {
// Remove it from the stack.
withdrawalStack.pop();
emit WithdrawalStackPopped(msg.sender, strategy);
// Move onto the next strategy.
continue;
}
// We want to pull as much as we can from the strategy, but no more than we need.
uint256 amountToPull = strategyBalance > amountLeftToPull ? amountLeftToPull : strategyBalance;
unchecked {
// Compute the balance of the strategy that will remain after we withdraw.
// Cannot underflow as we cap the amount to pull at the strategy's balance.
uint256 strategyBalanceAfterWithdrawal = strategyBalance - amountToPull;
// Without this the next harvest would count the withdrawal as a loss.
getStrategyData[strategy].balance = strategyBalanceAfterWithdrawal.safeCastTo248();
// Adjust our goal based on how much we can pull from the strategy.
// Cannot underflow as we cap the amount to pull at the amount left to pull.
amountLeftToPull -= amountToPull;
emit StrategyWithdrawal(msg.sender, strategy, amountToPull);
// Withdraw from the strategy and revert if returns an error code.
require(strategy.redeemUnderlying(amountToPull) == 0, "REDEEM_FAILED");
// If we fully depleted the strategy:
if (strategyBalanceAfterWithdrawal == 0) {
// Remove it from the stack.
withdrawalStack.pop();
emit WithdrawalStackPopped(msg.sender, strategy);
}
}
// If we've pulled all we need, exit the loop.
if (amountLeftToPull == 0) break;
}
unchecked {
// Account for the withdrawals done in the loop above.
// Cannot underflow as the balances of some strategies cannot exceed the sum of all.
totalStrategyHoldings -= underlyingAmount;
}
// Cache the Vault's balance of ETH.
uint256 ethBalance = address(this).balance;
// If the Vault's underlying token is WETH compatible and we have some ETH, wrap it into WETH.
if (ethBalance != 0 && underlyingIsWETH) WETH(payable(address(UNDERLYING))).deposit{value: ethBalance}();
}
/// @notice Pushes a single strategy to front of the withdrawal stack.
/// @param strategy The strategy to be inserted at the front of the withdrawal stack.
/// @dev Strategies that are untrusted, duplicated, or have no balance are
/// filtered out when encountered at withdrawal time, not validated upfront.
function pushToWithdrawalStack(Strategy strategy) external requiresAuth {
// Ensure pushing the strategy will not cause the stack exceed its limit.
require(withdrawalStack.length < MAX_WITHDRAWAL_STACK_SIZE, "STACK_FULL");
// Push the strategy to the front of the stack.
withdrawalStack.push(strategy);
emit WithdrawalStackPushed(msg.sender, strategy);
}
/// @notice Removes the strategy at the tip of the withdrawal stack.
/// @dev Be careful, another authorized user could push a different strategy
/// than expected to the stack while a popFromWithdrawalStack transaction is pending.
function popFromWithdrawalStack() external requiresAuth {
// Get the (soon to be) popped strategy.
Strategy poppedStrategy = withdrawalStack[withdrawalStack.length - 1];
// Pop the first strategy in the stack.
withdrawalStack.pop();
emit WithdrawalStackPopped(msg.sender, poppedStrategy);
}
/// @notice Sets a new withdrawal stack.
/// @param newStack The new withdrawal stack.
/// @dev Strategies that are untrusted, duplicated, or have no balance are
/// filtered out when encountered at withdrawal time, not validated upfront.
function setWithdrawalStack(Strategy[] calldata newStack) external requiresAuth {
// Ensure the new stack is not larger than the maximum stack size.
require(newStack.length <= MAX_WITHDRAWAL_STACK_SIZE, "STACK_TOO_BIG");
// Replace the withdrawal stack.
withdrawalStack = newStack;
emit WithdrawalStackSet(msg.sender, newStack);
}
/// @notice Replaces an index in the withdrawal stack with another strategy.
/// @param index The index in the stack to replace.
/// @param replacementStrategy The strategy to override the index with.
/// @dev Strategies that are untrusted, duplicated, or have no balance are
/// filtered out when encountered at withdrawal time, not validated upfront.
function replaceWithdrawalStackIndex(uint256 index, Strategy replacementStrategy) external requiresAuth {
// Get the (soon to be) replaced strategy.
Strategy replacedStrategy = withdrawalStack[index];
// Update the index with the replacement strategy.
withdrawalStack[index] = replacementStrategy;
emit WithdrawalStackIndexReplaced(msg.sender, index, replacedStrategy, replacementStrategy);
}
/// @notice Moves the strategy at the tip of the stack to the specified index and pop the tip off the stack.
/// @param index The index of the strategy in the withdrawal stack to replace with the tip.
function replaceWithdrawalStackIndexWithTip(uint256 index) external requiresAuth {
// Get the (soon to be) previous tip and strategy we will replace at the index.
Strategy previousTipStrategy = withdrawalStack[withdrawalStack.length - 1];
Strategy replacedStrategy = withdrawalStack[index];
// Replace the index specified with the tip of the stack.
withdrawalStack[index] = previousTipStrategy;
// Remove the now duplicated tip from the array.
withdrawalStack.pop();
emit WithdrawalStackIndexReplacedWithTip(msg.sender, index, replacedStrategy, previousTipStrategy);
}
/// @notice Swaps two indexes in the withdrawal stack.
/// @param index1 One index involved in the swap
/// @param index2 The other index involved in the swap.
function swapWithdrawalStackIndexes(uint256 index1, uint256 index2) external requiresAuth {
// Get the (soon to be) new strategies at each index.
Strategy newStrategy2 = withdrawalStack[index1];
Strategy newStrategy1 = withdrawalStack[index2];
// Swap the strategies at both indexes.
withdrawalStack[index1] = newStrategy1;
withdrawalStack[index2] = newStrategy2;
emit WithdrawalStackIndexesSwapped(msg.sender, index1, index2, newStrategy1, newStrategy2);
}
/*///////////////////////////////////////////////////////////////
FEE CLAIM LOGIC
//////////////////////////////////////////////////////////////*/
/// @notice Emitted after fees are claimed.
/// @param user The authorized user who claimed the fees.
/// @param rvTokenAmount The amount of rvTokens that were claimed.
event FeesClaimed(address indexed user, uint256 rvTokenAmount);
/// @notice Claims fees accrued from harvests.
/// @param rvTokenAmount The amount of rvTokens to claim.
/// @dev Accrued fees are measured as rvTokens held by the Vault.
function claimFees(uint256 rvTokenAmount) external requiresAuth {
emit FeesClaimed(msg.sender, rvTokenAmount);
// Transfer the provided amount of rvTokens to the caller.
ERC20(this).safeTransfer(msg.sender, rvTokenAmount);
}
/*///////////////////////////////////////////////////////////////
INITIALIZATION AND DESTRUCTION LOGIC
//////////////////////////////////////////////////////////////*/
/// @notice Emitted when the Vault is initialized.
/// @param user The authorized user who triggered the initialization.
event Initialized(address indexed user);
/// @notice Whether the Vault has been initialized yet.
/// @dev Can go from false to true, never from true to false.
bool public isInitialized;
/// @notice Initializes the Vault, enabling it to receive deposits.
/// @dev All critical parameters must already be set before calling.
function initialize() external requiresAuth {
// Ensure the Vault has not already been initialized.
require(!isInitialized, "ALREADY_INITIALIZED");
// Mark the Vault as initialized.
isInitialized = true;
// Open for deposits.
totalSupply = 0;
emit Initialized(msg.sender);
}
/// @notice Self destructs a Vault, enabling it to be redeployed.
/// @dev Caller will receive any ETH held as float in the Vault.
function destroy() external requiresAuth {
selfdestruct(payable(msg.sender));
}
/*///////////////////////////////////////////////////////////////
RECIEVE ETHER LOGIC
//////////////////////////////////////////////////////////////*/
/// @dev Required for the Vault to receive unwrapped ETH.
receive() external payable {}
}