// 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 {} }
0.4.18