diff --git a/contracts/contracts/harvest/AbstractHarvester.sol b/contracts/contracts/harvest/AbstractHarvester.sol index 99471bdad7..36eedb7c27 100644 --- a/contracts/contracts/harvest/AbstractHarvester.sol +++ b/contracts/contracts/harvest/AbstractHarvester.sol @@ -198,7 +198,7 @@ abstract contract AbstractHarvester is Governable { // Revert if feed does not exist // slither-disable-next-line unused-return - IOracle(IVault(vaultAddress).priceProvider()).price(_tokenAddress); + // IOracle(IVault(vaultAddress).priceProvider()).price(_tokenAddress); IERC20 token = IERC20(_tokenAddress); // if changing token swap provider cancel existing allowance @@ -443,10 +443,12 @@ abstract contract AbstractHarvester is Governable { _harvest(_strategyAddr); IStrategy strategy = IStrategy(_strategyAddr); address[] memory rewardTokens = strategy.getRewardTokenAddresses(); - IOracle priceProvider = IOracle(IVault(vaultAddress).priceProvider()); + //IOracle priceProvider = IOracle(IVault(vaultAddress).priceProvider()); uint256 len = rewardTokens.length; for (uint256 i = 0; i < len; ++i) { - _swap(rewardTokens[i], _rewardTo, priceProvider); + // This harvester contract is not used anymore. Keeping the code + // for passing test deployment. Safe to use address(0x1) as oracle. + _swap(rewardTokens[i], _rewardTo, IOracle(address(0x1))); } } diff --git a/contracts/contracts/interfaces/IVault.sol b/contracts/contracts/interfaces/IVault.sol index 0b0bc98e93..eecb4fed4d 100644 --- a/contracts/contracts/interfaces/IVault.sol +++ b/contracts/contracts/interfaces/IVault.sol @@ -6,8 +6,6 @@ import { VaultStorage } from "../vault/VaultStorage.sol"; interface IVault { // slither-disable-start constable-states - event AssetSupported(address _asset); - event AssetDefaultStrategyUpdated(address _asset, address _strategy); event AssetAllocated(address _asset, address _strategy, uint256 _amount); event StrategyApproved(address _addr); event StrategyRemoved(address _addr); @@ -15,11 +13,11 @@ interface IVault { event Redeem(address _addr, uint256 _value); event CapitalPaused(); event CapitalUnpaused(); + event DefaultStrategyUpdated(address _strategy); event RebasePaused(); event RebaseUnpaused(); event VaultBufferUpdated(uint256 _vaultBuffer); event RedeemFeeUpdated(uint256 _redeemFeeBps); - event PriceProviderUpdated(address _priceProvider); event AllocateThresholdUpdated(uint256 _threshold); event RebaseThresholdUpdated(uint256 _threshold); event StrategistUpdated(address _address); @@ -27,18 +25,10 @@ interface IVault { event YieldDistribution(address _to, uint256 _yield, uint256 _fee); event TrusteeFeeBpsChanged(uint256 _basis); event TrusteeAddressChanged(address _address); - event SwapperChanged(address _address); - event SwapAllowedUndervalueChanged(uint256 _basis); - event SwapSlippageChanged(address _asset, uint256 _basis); - event Swapped( - address indexed _fromAsset, - address indexed _toAsset, - uint256 _fromAssetAmount, - uint256 _toAssetAmount - ); event StrategyAddedToMintWhitelist(address indexed strategy); event StrategyRemovedFromMintWhitelist(address indexed strategy); - event DripperChanged(address indexed _dripper); + event RebasePerSecondMaxChanged(uint256 rebaseRatePerSecond); + event DripDurationChanged(uint256 dripDuration); event WithdrawalRequested( address indexed _withdrawer, uint256 indexed _requestId, @@ -51,6 +41,7 @@ interface IVault { uint256 _amount ); event WithdrawalClaimable(uint256 _claimable, uint256 _newClaimable); + event WithdrawalClaimDelayUpdated(uint256 _newDelay); // Governable.sol function transferGovernance(address _newGovernor) external; @@ -62,10 +53,6 @@ interface IVault { function ADMIN_IMPLEMENTATION() external view returns (address); // VaultAdmin.sol - function setPriceProvider(address _priceProvider) external; - - function priceProvider() external view returns (address); - function setRedeemFeeBps(uint256 _redeemFeeBps) external; function redeemFeeBps() external view returns (uint256); @@ -98,28 +85,13 @@ interface IVault { function trusteeFeeBps() external view returns (uint256); - function ousdMetaStrategy() external view returns (address); - - function setSwapper(address _swapperAddr) external; - - function setSwapAllowedUndervalue(uint16 _percentageBps) external; - - function setOracleSlippage(address _asset, uint16 _allowedOracleSlippageBps) - external; - - function supportAsset(address _asset, uint8 _unitConversion) external; - function approveStrategy(address _addr) external; function removeStrategy(address _addr) external; - function setAssetDefaultStrategy(address _asset, address _strategy) - external; + function setDefaultStrategy(address _strategy) external; - function assetDefaultStrategies(address _asset) - external - view - returns (address); + function defaultStrategy() external view returns (address); function pauseRebase() external; @@ -135,10 +107,6 @@ interface IVault { function transferToken(address _asset, uint256 _amount) external; - function priceUnitMint(address asset) external view returns (uint256); - - function priceUnitRedeem(address asset) external view returns (uint256); - function withdrawAllFromStrategy(address _strategyAddr) external; function withdrawAllFromStrategies() external; @@ -172,14 +140,6 @@ interface IVault { function rebase() external; - function swapCollateral( - address fromAsset, - address toAsset, - uint256 fromAssetAmount, - uint256 minToAssetAmount, - bytes calldata data - ) external returns (uint256 toAssetAmount); - function totalValue() external view returns (uint256 value); function checkBalance(address _asset) external view returns (uint256); @@ -191,48 +151,24 @@ interface IVault { function getAssetCount() external view returns (uint256); - function getAssetConfig(address _asset) - external - view - returns (VaultStorage.Asset memory config); - function getAllAssets() external view returns (address[] memory); function getStrategyCount() external view returns (uint256); - function swapper() external view returns (address); - - function allowedSwapUndervalue() external view returns (uint256); - function getAllStrategies() external view returns (address[] memory); function isSupportedAsset(address _asset) external view returns (bool); - function netOusdMintForStrategyThreshold() external view returns (uint256); - - function setOusdMetaStrategy(address _ousdMetaStrategy) external; - - function setNetOusdMintForStrategyThreshold(uint256 _threshold) external; - - function netOusdMintedForStrategy() external view returns (int256); - function setDripper(address _dripper) external; function dripper() external view returns (address); - function weth() external view returns (address); - - function cacheWETHAssetIndex() external; - - function wethAssetIndex() external view returns (uint256); + function backingAsset() external view returns (address); - function initialize(address, address) external; + function initialize(address) external; function setAdminImpl(address) external; - function removeAsset(address _asset) external; - - // These are OETH specific functions function addWithdrawalQueueLiquidity() external; function requestWithdrawal(uint256 _amount) @@ -257,7 +193,6 @@ interface IVault { view returns (VaultStorage.WithdrawalRequest memory); - // OETHb specific functions function addStrategyToMintWhitelist(address strategyAddr) external; function removeStrategyFromMintWhitelist(address strategyAddr) external; @@ -285,5 +220,7 @@ interface IVault { function previewYield() external view returns (uint256 yield); + function weth() external view returns (address); + // slither-disable-end constable-states } diff --git a/contracts/contracts/mocks/MockEvilReentrantContract.sol b/contracts/contracts/mocks/MockEvilReentrantContract.sol index 47fee94c19..fea2c81bb1 100644 --- a/contracts/contracts/mocks/MockEvilReentrantContract.sol +++ b/contracts/contracts/mocks/MockEvilReentrantContract.sol @@ -19,6 +19,7 @@ contract MockEvilReentrantContract { IVault public immutable oethVault; address public immutable poolAddress; bytes32 public immutable balancerPoolId; + address public immutable priceProvider; constructor( address _balancerVault, @@ -37,7 +38,6 @@ contract MockEvilReentrantContract { } function doEvilStuff() public { - address priceProvider = oethVault.priceProvider(); uint256 rethPrice = IOracle(priceProvider).price(address(reth)); // 1. Join pool @@ -99,9 +99,6 @@ contract MockEvilReentrantContract { virtual returns (uint256 bptExpected) { - // Get the oracle from the OETH Vault - address priceProvider = oethVault.priceProvider(); - for (uint256 i = 0; i < _assets.length; ++i) { uint256 strategyAssetMarketPrice = IOracle(priceProvider).price( _assets[i] diff --git a/contracts/contracts/mocks/MockOETHVault.sol b/contracts/contracts/mocks/MockOETHVault.sol index 9e34d8430f..6e2d7ed3dc 100644 --- a/contracts/contracts/mocks/MockOETHVault.sol +++ b/contracts/contracts/mocks/MockOETHVault.sol @@ -13,13 +13,6 @@ contract MockOETHVault is OETHVaultCore { } function supportAsset(address asset) external { - assets[asset] = Asset({ - isSupported: true, - unitConversion: UnitConversion(0), - decimals: 18, - allowedOracleSlippageBps: 0 - }); - - allAssets.push(asset); + require(asset == backingAsset, "Only backingAsset supported"); } } diff --git a/contracts/contracts/mocks/MockOETHVaultAdmin.sol b/contracts/contracts/mocks/MockOETHVaultAdmin.sol index a2664212b5..4fedfede64 100644 --- a/contracts/contracts/mocks/MockOETHVaultAdmin.sol +++ b/contracts/contracts/mocks/MockOETHVaultAdmin.sol @@ -19,6 +19,6 @@ contract MockOETHVaultAdmin is OETHVaultAdmin { } function wethAvailable() external view returns (uint256) { - return _wethAvailable(); + return _backingAssetAvailable(); } } diff --git a/contracts/contracts/mocks/MockRebornMinter.sol b/contracts/contracts/mocks/MockRebornMinter.sol index b0c812ca56..3a8be2ed06 100644 --- a/contracts/contracts/mocks/MockRebornMinter.sol +++ b/contracts/contracts/mocks/MockRebornMinter.sol @@ -97,15 +97,15 @@ contract Reborner { log("We are attempting to mint.."); address asset = sanctum.asset(); address vault = sanctum.vault(); - IERC20(asset).approve(vault, 1e18); - IVault(vault).mint(asset, 1e18, 0); + IERC20(asset).approve(vault, 1e6); + IVault(vault).mint(asset, 1e6, 0); log("We are now minting.."); } function redeem() public { log("We are attempting to redeem.."); address vault = sanctum.vault(); - IVault(vault).redeem(1e18, 1e18); + IVault(vault).redeem(1e18, 0); log("We are now redeeming.."); } diff --git a/contracts/contracts/mocks/MockVault.sol b/contracts/contracts/mocks/MockVault.sol index 717c1f31f3..39baa220df 100644 --- a/contracts/contracts/mocks/MockVault.sol +++ b/contracts/contracts/mocks/MockVault.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.0; import { VaultCore } from "../vault/VaultCore.sol"; import { StableMath } from "../utils/StableMath.sol"; -import { VaultInitializer } from "../vault/VaultInitializer.sol"; import "../utils/Helpers.sol"; contract MockVault is VaultCore { @@ -11,6 +10,8 @@ contract MockVault is VaultCore { uint256 storedTotalValue; + constructor(address _backingAsset) VaultCore(_backingAsset) {} + function setTotalValue(uint256 _value) public { storedTotalValue = _value; } @@ -31,7 +32,7 @@ contract MockVault is VaultCore { { // Avoids rounding errors by returning the total value // in a single currency - if (allAssets[0] == _asset) { + if (backingAsset == _asset) { uint256 decimals = Helpers.getDecimals(_asset); return storedTotalValue.scaleBy(decimals, 18); } else { diff --git a/contracts/contracts/mocks/MockVaultCoreInstantRebase.sol b/contracts/contracts/mocks/MockVaultCoreInstantRebase.sol index 7043497725..93eac66eb6 100644 --- a/contracts/contracts/mocks/MockVaultCoreInstantRebase.sol +++ b/contracts/contracts/mocks/MockVaultCoreInstantRebase.sol @@ -4,6 +4,8 @@ pragma solidity ^0.8.0; import { VaultCore } from "../vault/VaultCore.sol"; contract MockVaultCoreInstantRebase is VaultCore { + constructor(address _backingAsset) VaultCore(_backingAsset) {} + function _nextYield(uint256 supply, uint256 vaultValue) internal view diff --git a/contracts/contracts/strategies/BridgedWOETHStrategy.sol b/contracts/contracts/strategies/BridgedWOETHStrategy.sol index 1f602318d0..a5ea8b9aa7 100644 --- a/contracts/contracts/strategies/BridgedWOETHStrategy.sol +++ b/contracts/contracts/strategies/BridgedWOETHStrategy.sol @@ -21,6 +21,7 @@ contract BridgedWOETHStrategy is InitializableAbstractStrategy { IWETH9 public immutable weth; IERC20 public immutable bridgedWOETH; IERC20 public immutable oethb; + IOracle public immutable oracle; uint256 public constant MAX_PRICE_STALENESS = 2 days; @@ -31,11 +32,13 @@ contract BridgedWOETHStrategy is InitializableAbstractStrategy { BaseStrategyConfig memory _stratConfig, address _weth, address _bridgedWOETH, - address _oethb + address _oethb, + address _oracle ) InitializableAbstractStrategy(_stratConfig) { weth = IWETH9(_weth); bridgedWOETH = IERC20(_bridgedWOETH); oethb = IERC20(_oethb); + oracle = IOracle(_oracle); } function initialize(uint128 _maxPriceDiffBps) @@ -100,8 +103,7 @@ contract BridgedWOETHStrategy is InitializableAbstractStrategy { */ function _updateWOETHOraclePrice() internal returns (uint256) { // WETH price per unit of bridged wOETH - uint256 oraclePrice = IOracle(IVault(vaultAddress).priceProvider()) - .price(address(bridgedWOETH)); + uint256 oraclePrice = oracle.price(address(bridgedWOETH)); // 1 wOETH > 1 WETH, always require(oraclePrice > 1 ether, "Invalid wOETH value"); diff --git a/contracts/contracts/vault/OETHVault.sol b/contracts/contracts/vault/OETHVault.sol deleted file mode 100644 index 0ec1690ad5..0000000000 --- a/contracts/contracts/vault/OETHVault.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.0; - -import { Vault } from "./Vault.sol"; - -/** - * @title OETH Vault Contract - * @author Origin Protocol Inc - */ -contract OETHVault is Vault { - -} diff --git a/contracts/contracts/vault/OETHVaultAdmin.sol b/contracts/contracts/vault/OETHVaultAdmin.sol index 080dec140b..8bf2e417ad 100644 --- a/contracts/contracts/vault/OETHVaultAdmin.sol +++ b/contracts/contracts/vault/OETHVaultAdmin.sol @@ -1,11 +1,6 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.0; -import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import { IStrategy } from "../interfaces/IStrategy.sol"; -import { IVault } from "../interfaces/IVault.sol"; import { VaultAdmin } from "./VaultAdmin.sol"; /** @@ -13,132 +8,5 @@ import { VaultAdmin } from "./VaultAdmin.sol"; * @author Origin Protocol Inc */ contract OETHVaultAdmin is VaultAdmin { - using SafeERC20 for IERC20; - - address public immutable weth; - - constructor(address _weth) { - weth = _weth; - } - - /** - * @notice Adds a strategy to the mint whitelist. - * Reverts if strategy isn't approved on Vault. - * @param strategyAddr Strategy address - */ - function addStrategyToMintWhitelist(address strategyAddr) - external - onlyGovernor - { - require(strategies[strategyAddr].isSupported, "Strategy not approved"); - - require( - !isMintWhitelistedStrategy[strategyAddr], - "Already whitelisted" - ); - - isMintWhitelistedStrategy[strategyAddr] = true; - - emit StrategyAddedToMintWhitelist(strategyAddr); - } - - /** - * @notice Removes a strategy from the mint whitelist. - * @param strategyAddr Strategy address - */ - function removeStrategyFromMintWhitelist(address strategyAddr) - external - onlyGovernor - { - // Intentionally skipping `strategies.isSupported` check since - // we may wanna remove an address even after removing the strategy - - require(isMintWhitelistedStrategy[strategyAddr], "Not whitelisted"); - - isMintWhitelistedStrategy[strategyAddr] = false; - - emit StrategyRemovedFromMintWhitelist(strategyAddr); - } - - /// @dev Simplified version of the deposit function as WETH is the only supported asset. - function _depositToStrategy( - address _strategyToAddress, - address[] calldata _assets, - uint256[] calldata _amounts - ) internal override { - require( - strategies[_strategyToAddress].isSupported, - "Invalid to Strategy" - ); - require( - _assets.length == 1 && _amounts.length == 1 && _assets[0] == weth, - "Only WETH is supported" - ); - - // Check the there is enough WETH to transfer once the WETH reserved for the withdrawal queue is accounted for - require(_amounts[0] <= _wethAvailable(), "Not enough WETH available"); - - // Send required amount of funds to the strategy - IERC20(weth).safeTransfer(_strategyToAddress, _amounts[0]); - - // Deposit all the funds that have been sent to the strategy - IStrategy(_strategyToAddress).depositAll(); - } - - function _withdrawFromStrategy( - address _recipient, - address _strategyFromAddress, - address[] calldata _assets, - uint256[] calldata _amounts - ) internal override { - super._withdrawFromStrategy( - _recipient, - _strategyFromAddress, - _assets, - _amounts - ); - - IVault(address(this)).addWithdrawalQueueLiquidity(); - } - - function _withdrawAllFromStrategy(address _strategyAddr) internal override { - super._withdrawAllFromStrategy(_strategyAddr); - - IVault(address(this)).addWithdrawalQueueLiquidity(); - } - - function _withdrawAllFromStrategies() internal override { - super._withdrawAllFromStrategies(); - - IVault(address(this)).addWithdrawalQueueLiquidity(); - } - - /// @dev Calculate how much WETH in the vault is not reserved for the withdrawal queue. - // That is, it is available to be redeemed or deposited into a strategy. - function _wethAvailable() internal view returns (uint256 wethAvailable) { - WithdrawalQueueMetadata memory queue = withdrawalQueueMetadata; - - // The amount of WETH that is still to be claimed in the withdrawal queue - uint256 outstandingWithdrawals = queue.queued - queue.claimed; - - // The amount of sitting in WETH in the vault - uint256 wethBalance = IERC20(weth).balanceOf(address(this)); - - // If there is not enough WETH in the vault to cover the outstanding withdrawals - if (wethBalance <= outstandingWithdrawals) { - return 0; - } - - return wethBalance - outstandingWithdrawals; - } - - function _swapCollateral( - address, - address, - uint256, - uint256, - bytes calldata - ) internal pure override returns (uint256) { - revert("Collateral swap not supported"); - } + constructor(address _weth) VaultAdmin(_weth) {} } diff --git a/contracts/contracts/vault/OETHVaultCore.sol b/contracts/contracts/vault/OETHVaultCore.sol index cbc59a8eee..821124b8b1 100644 --- a/contracts/contracts/vault/OETHVaultCore.sol +++ b/contracts/contracts/vault/OETHVaultCore.sol @@ -1,524 +1,15 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.0; -import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; - -import { StableMath } from "../utils/StableMath.sol"; import { VaultCore } from "./VaultCore.sol"; -import { IStrategy } from "../interfaces/IStrategy.sol"; /** * @title OETH VaultCore Contract * @author Origin Protocol Inc */ contract OETHVaultCore is VaultCore { - using SafeERC20 for IERC20; - using StableMath for uint256; - - address public immutable weth; - uint256 public wethAssetIndex; - // For future use (because OETHBaseVaultCore inherits from this) uint256[50] private __gap; - constructor(address _weth) { - weth = _weth; - } - - /** - * @dev Caches WETH's index in `allAssets` variable. - * Reduces gas usage by redeem by caching that. - */ - function cacheWETHAssetIndex() external onlyGovernor { - uint256 assetCount = allAssets.length; - for (uint256 i; i < assetCount; ++i) { - if (allAssets[i] == weth) { - wethAssetIndex = i; - break; - } - } - - require(allAssets[wethAssetIndex] == weth, "Invalid WETH Asset Index"); - } - - // @inheritdoc VaultCore - function mintForStrategy(uint256 amount) - external - override - whenNotCapitalPaused - { - require( - strategies[msg.sender].isSupported == true, - "Unsupported strategy" - ); - require( - isMintWhitelistedStrategy[msg.sender] == true, - "Not whitelisted strategy" - ); - - emit Mint(msg.sender, amount); - - // Mint matching amount of OTokens - oUSD.mint(msg.sender, amount); - } - - // @inheritdoc VaultCore - function burnForStrategy(uint256 amount) - external - override - whenNotCapitalPaused - { - require( - strategies[msg.sender].isSupported == true, - "Unsupported strategy" - ); - require( - isMintWhitelistedStrategy[msg.sender] == true, - "Not whitelisted strategy" - ); - - emit Redeem(msg.sender, amount); - - // Burn OTokens - oUSD.burn(msg.sender, amount); - } - - // @inheritdoc VaultCore - // slither-disable-start reentrancy-no-eth - function _mint( - address _asset, - uint256 _amount, - uint256 _minimumOusdAmount - ) internal virtual override { - require(_asset == weth, "Unsupported asset for minting"); - require(_amount > 0, "Amount must be greater than 0"); - require( - _amount >= _minimumOusdAmount, - "Mint amount lower than minimum" - ); - - emit Mint(msg.sender, _amount); - - // Rebase must happen before any transfers occur. - if (!rebasePaused && _amount >= rebaseThreshold) { - _rebase(); - } - - // Mint oTokens - oUSD.mint(msg.sender, _amount); - - // Transfer the deposited coins to the vault - IERC20(_asset).safeTransferFrom(msg.sender, address(this), _amount); - - // Give priority to the withdrawal queue for the new WETH liquidity - _addWithdrawalQueueLiquidity(); - - // Auto-allocate if necessary - if (_amount >= autoAllocateThreshold) { - _allocate(); - } - } - - // slither-disable-end reentrancy-no-eth - - // @inheritdoc VaultCore - function _calculateRedeemOutputs(uint256 _amount) - internal - view - virtual - override - returns (uint256[] memory outputs) - { - // Overrides `VaultCore._calculateRedeemOutputs` to redeem with only - // WETH instead of LST-mix. Doesn't change the function signature - // for backward compatibility - - // Calculate redeem fee - if (redeemFeeBps > 0) { - uint256 redeemFee = _amount.mulTruncateScale(redeemFeeBps, 1e4); - _amount = _amount - redeemFee; - } - - // Ensure that the WETH index is cached - uint256 _wethAssetIndex = wethAssetIndex; - require( - allAssets[_wethAssetIndex] == weth, - "WETH Asset index not cached" - ); - - outputs = new uint256[](allAssets.length); - outputs[_wethAssetIndex] = _amount; - } - - // @inheritdoc VaultCore - function _redeem(uint256 _amount, uint256 _minimumUnitAmount) - internal - virtual - override - { - // Override `VaultCore._redeem` to simplify it. Gets rid of oracle - // usage and looping through all assets for LST-mix redeem. Instead - // does a simple WETH-only redeem. - emit Redeem(msg.sender, _amount); - - if (_amount == 0) { - return; - } - - // Amount excluding fees - // No fee for the strategist or the governor, makes it easier to do operations - uint256 amountMinusFee = (msg.sender == strategistAddr || isGovernor()) - ? _amount - : _calculateRedeemOutputs(_amount)[wethAssetIndex]; - - require( - amountMinusFee >= _minimumUnitAmount, - "Redeem amount lower than minimum" - ); - - // Is there enough WETH in the Vault available after accounting for the withdrawal queue - require(_wethAvailable() >= amountMinusFee, "Liquidity error"); - - // Transfer WETH minus the fee to the redeemer - IERC20(weth).safeTransfer(msg.sender, amountMinusFee); - - // Burn OETH from user (including fees) - oUSD.burn(msg.sender, _amount); - - // Prevent insolvency - _postRedeem(_amount); - } - - /** - * @notice Request an asynchronous withdrawal of WETH in exchange for OETH. - * The OETH is burned on request and the WETH is transferred to the withdrawer on claim. - * This request can be claimed once the withdrawal queue's `claimable` amount - * is greater than or equal this request's `queued` amount. - * There is a minimum of 10 minutes before a request can be claimed. After that, the request just needs - * enough WETH liquidity in the Vault to satisfy all the outstanding requests to that point in the queue. - * OETH is converted to WETH at 1:1. - * @param _amount Amount of OETH to burn. - * @return requestId Unique ID for the withdrawal request - * @return queued Cumulative total of all WETH queued including already claimed requests. - */ - function requestWithdrawal(uint256 _amount) - external - virtual - whenNotCapitalPaused - nonReentrant - returns (uint256 requestId, uint256 queued) - { - require(withdrawalClaimDelay > 0, "Async withdrawals not enabled"); - - // The check that the requester has enough OETH is done in to later burn call - - requestId = withdrawalQueueMetadata.nextWithdrawalIndex; - queued = withdrawalQueueMetadata.queued + _amount; - - // Store the next withdrawal request - withdrawalQueueMetadata.nextWithdrawalIndex = SafeCast.toUint128( - requestId + 1 - ); - // Store the updated queued amount which reserves WETH in the withdrawal queue - // and reduces the vault's total assets - withdrawalQueueMetadata.queued = SafeCast.toUint128(queued); - // Store the user's withdrawal request - withdrawalRequests[requestId] = WithdrawalRequest({ - withdrawer: msg.sender, - claimed: false, - timestamp: uint40(block.timestamp), - amount: SafeCast.toUint128(_amount), - queued: SafeCast.toUint128(queued) - }); - - // Burn the user's OETH - oUSD.burn(msg.sender, _amount); - - // Prevent withdrawal if the vault is solvent by more than the allowed percentage - _postRedeem(_amount); - - emit WithdrawalRequested(msg.sender, requestId, _amount, queued); - } - - // slither-disable-start reentrancy-no-eth - /** - * @notice Claim a previously requested withdrawal once it is claimable. - * This request can be claimed once the withdrawal queue's `claimable` amount - * is greater than or equal this request's `queued` amount and 10 minutes has passed. - * If the requests is not claimable, the transaction will revert with `Queue pending liquidity`. - * If the request is not older than 10 minutes, the transaction will revert with `Claim delay not met`. - * OETH is converted to WETH at 1:1. - * @param _requestId Unique ID for the withdrawal request - * @return amount Amount of WETH transferred to the withdrawer - */ - function claimWithdrawal(uint256 _requestId) - external - virtual - whenNotCapitalPaused - nonReentrant - returns (uint256 amount) - { - // Try and get more liquidity if there is not enough available - if ( - withdrawalRequests[_requestId].queued > - withdrawalQueueMetadata.claimable - ) { - // Add any WETH to the withdrawal queue - // this needs to remain here as: - // - Vault can be funded and `addWithdrawalQueueLiquidity` is not externally called - // - funds can be withdrawn from a strategy - // - // Those funds need to be added to withdrawal queue liquidity - _addWithdrawalQueueLiquidity(); - } - - amount = _claimWithdrawal(_requestId); - - // transfer WETH from the vault to the withdrawer - IERC20(weth).safeTransfer(msg.sender, amount); - - // Prevent insolvency - _postRedeem(amount); - } - - // slither-disable-end reentrancy-no-eth - - /** - * @notice Claim a previously requested withdrawals once they are claimable. - * This requests can be claimed once the withdrawal queue's `claimable` amount - * is greater than or equal each request's `queued` amount and 10 minutes has passed. - * If one of the requests is not claimable, the whole transaction will revert with `Queue pending liquidity`. - * If one of the requests is not older than 10 minutes, - * the whole transaction will revert with `Claim delay not met`. - * @param _requestIds Unique ID of each withdrawal request - * @return amounts Amount of WETH received for each request - * @return totalAmount Total amount of WETH transferred to the withdrawer - */ - function claimWithdrawals(uint256[] calldata _requestIds) - external - virtual - whenNotCapitalPaused - nonReentrant - returns (uint256[] memory amounts, uint256 totalAmount) - { - // Add any WETH to the withdrawal queue - // this needs to remain here as: - // - Vault can be funded and `addWithdrawalQueueLiquidity` is not externally called - // - funds can be withdrawn from a strategy - // - // Those funds need to be added to withdrawal queue liquidity - _addWithdrawalQueueLiquidity(); - - amounts = new uint256[](_requestIds.length); - for (uint256 i; i < _requestIds.length; ++i) { - amounts[i] = _claimWithdrawal(_requestIds[i]); - totalAmount += amounts[i]; - } - - // transfer all the claimed WETH from the vault to the withdrawer - IERC20(weth).safeTransfer(msg.sender, totalAmount); - - // Prevent insolvency - _postRedeem(totalAmount); - } - - function _claimWithdrawal(uint256 requestId) - internal - returns (uint256 amount) - { - require(withdrawalClaimDelay > 0, "Async withdrawals not enabled"); - - // Load the structs from storage into memory - WithdrawalRequest memory request = withdrawalRequests[requestId]; - WithdrawalQueueMetadata memory queue = withdrawalQueueMetadata; - - require( - request.timestamp + withdrawalClaimDelay <= block.timestamp, - "Claim delay not met" - ); - // If there isn't enough reserved liquidity in the queue to claim - require(request.queued <= queue.claimable, "Queue pending liquidity"); - require(request.withdrawer == msg.sender, "Not requester"); - require(request.claimed == false, "Already claimed"); - - // Store the request as claimed - withdrawalRequests[requestId].claimed = true; - // Store the updated claimed amount - withdrawalQueueMetadata.claimed = queue.claimed + request.amount; - - emit WithdrawalClaimed(msg.sender, requestId, request.amount); - - return request.amount; - } - - /// @notice Adds WETH to the withdrawal queue if there is a funding shortfall. - /// @dev is called from the Native Staking strategy when validator withdrawals are processed. - /// It also called before any WETH is allocated to a strategy. - function addWithdrawalQueueLiquidity() external { - _addWithdrawalQueueLiquidity(); - } - - /// @dev Adds WETH to the withdrawal queue if there is a funding shortfall. - /// This assumes 1 WETH equal 1 OETH. - function _addWithdrawalQueueLiquidity() - internal - returns (uint256 addedClaimable) - { - WithdrawalQueueMetadata memory queue = withdrawalQueueMetadata; - - // Check if the claimable WETH is less than the queued amount - uint256 queueShortfall = queue.queued - queue.claimable; - - // No need to do anything is the withdrawal queue is full funded - if (queueShortfall == 0) { - return 0; - } - - uint256 wethBalance = IERC20(weth).balanceOf(address(this)); - - // Of the claimable withdrawal requests, how much is unclaimed? - // That is, the amount of WETH that is currently allocated for the withdrawal queue - uint256 allocatedWeth = queue.claimable - queue.claimed; - - // If there is no unallocated WETH then there is nothing to add to the queue - if (wethBalance <= allocatedWeth) { - return 0; - } - - uint256 unallocatedWeth = wethBalance - allocatedWeth; - - // the new claimable amount is the smaller of the queue shortfall or unallocated weth - addedClaimable = queueShortfall < unallocatedWeth - ? queueShortfall - : unallocatedWeth; - uint256 newClaimable = queue.claimable + addedClaimable; - - // Store the new claimable amount back to storage - withdrawalQueueMetadata.claimable = SafeCast.toUint128(newClaimable); - - // emit a WithdrawalClaimable event - emit WithdrawalClaimable(newClaimable, addedClaimable); - } - - /*************************************** - View Functions - ****************************************/ - - /// @dev Calculate how much WETH in the vault is not reserved for the withdrawal queue. - // That is, it is available to be redeemed or deposited into a strategy. - function _wethAvailable() internal view returns (uint256 wethAvailable) { - WithdrawalQueueMetadata memory queue = withdrawalQueueMetadata; - - // The amount of WETH that is still to be claimed in the withdrawal queue - uint256 outstandingWithdrawals = queue.queued - queue.claimed; - - // The amount of sitting in WETH in the vault - uint256 wethBalance = IERC20(weth).balanceOf(address(this)); - - // If there is not enough WETH in the vault to cover the outstanding withdrawals - if (wethBalance <= outstandingWithdrawals) { - return 0; - } - - return wethBalance - outstandingWithdrawals; - } - - /// @dev Get the balance of an asset held in Vault and all strategies - /// less any WETH that is reserved for the withdrawal queue. - /// WETH is the only asset that can return a non-zero balance. - /// All other assets will return 0 even if there is some dust amounts left in the Vault. - /// For example, there is 1 wei left of stETH in the OETH Vault but will return 0 in this function. - /// - /// If there is not enough WETH in the vault and all strategies to cover all outstanding - /// withdrawal requests then return a WETH balance of 0 - function _checkBalance(address _asset) - internal - view - override - returns (uint256 balance) - { - if (_asset != weth) { - return 0; - } - - // Get the WETH in the vault and the strategies - balance = super._checkBalance(_asset); - - WithdrawalQueueMetadata memory queue = withdrawalQueueMetadata; - - // If the vault becomes insolvent enough that the total value in the vault and all strategies - // is less than the outstanding withdrawals. - // For example, there was a mass slashing event and most users request a withdrawal. - if (balance + queue.claimed < queue.queued) { - return 0; - } - - // Need to remove WETH that is reserved for the withdrawal queue - return balance + queue.claimed - queue.queued; - } - - /** - * @notice Allocate unallocated funds on Vault to strategies. - **/ - function allocate() external override whenNotCapitalPaused nonReentrant { - // Add any unallocated WETH to the withdrawal queue first - _addWithdrawalQueueLiquidity(); - - _allocate(); - } - - /// @dev Allocate WETH to the default WETH strategy if there is excess to the Vault buffer. - /// This is called from either `mint` or `allocate` and assumes `_addWithdrawalQueueLiquidity` - /// has been called before this function. - function _allocate() internal override { - // No need to do anything if no default strategy for WETH - address depositStrategyAddr = assetDefaultStrategies[weth]; - if (depositStrategyAddr == address(0)) return; - - uint256 wethAvailableInVault = _wethAvailable(); - // No need to do anything if there isn't any WETH in the vault to allocate - if (wethAvailableInVault == 0) return; - - // Calculate the target buffer for the vault using the total supply - uint256 totalSupply = oUSD.totalSupply(); - uint256 targetBuffer = totalSupply.mulTruncate(vaultBuffer); - - // If available WETH in the Vault is below or equal the target buffer then there's nothing to allocate - if (wethAvailableInVault <= targetBuffer) return; - - // The amount of assets to allocate to the default strategy - uint256 allocateAmount = wethAvailableInVault - targetBuffer; - - IStrategy strategy = IStrategy(depositStrategyAddr); - // Transfer WETH to the strategy and call the strategy's deposit function - IERC20(weth).safeTransfer(address(strategy), allocateAmount); - strategy.deposit(weth, allocateAmount); - - emit AssetAllocated(weth, depositStrategyAddr, allocateAmount); - } - - /// @dev The total value of all WETH held by the vault and all its strategies - /// less any WETH that is reserved for the withdrawal queue. - /// - // If there is not enough WETH in the vault and all strategies to cover all outstanding - // withdrawal requests then return a total value of 0. - function _totalValue() internal view override returns (uint256 value) { - // As WETH is the only asset, just return the WETH balance - return _checkBalance(weth); - } - - /// @dev Only WETH is supported in the OETH Vault so return the WETH balance only - /// Any ETH balances in the Vault will be ignored. - /// Amounts from previously supported vault assets will also be ignored. - /// For example, there is 1 wei left of stETH in the OETH Vault but is will be ignored. - function _totalValueInVault() - internal - view - override - returns (uint256 value) - { - value = IERC20(weth).balanceOf(address(this)); - } + constructor(address _weth) VaultCore(_weth) {} } diff --git a/contracts/contracts/vault/OSonicVaultAdmin.sol b/contracts/contracts/vault/OSonicVaultAdmin.sol index 445ba03550..595cd5897c 100644 --- a/contracts/contracts/vault/OSonicVaultAdmin.sol +++ b/contracts/contracts/vault/OSonicVaultAdmin.sol @@ -8,36 +8,5 @@ import { OETHVaultAdmin } from "./OETHVaultAdmin.sol"; * @author Origin Protocol Inc */ contract OSonicVaultAdmin is OETHVaultAdmin { - /// @param _wS Sonic's Wrapped S token constructor(address _wS) OETHVaultAdmin(_wS) {} - - /*************************************** - Asset Config - ****************************************/ - - /** - * @notice Add a supported asset to the contract, i.e. one that can be to mint OTokens. - * @dev Overridden to remove price provider integration - * @param _asset Address of asset - * @param _unitConversion 0 decimals, 1 exchange rate - */ - function supportAsset(address _asset, uint8 _unitConversion) - external - override - onlyGovernor - { - require(!assets[_asset].isSupported, "Asset already supported"); - - assets[_asset] = Asset({ - isSupported: true, - unitConversion: UnitConversion(_unitConversion), - decimals: 0, // will be overridden in _cacheDecimals - allowedOracleSlippageBps: 0 // 0% by default - }); - - _cacheDecimals(_asset); - allAssets.push(_asset); - - emit AssetSupported(_asset); - } } diff --git a/contracts/contracts/vault/OUSDVaultAdmin.sol b/contracts/contracts/vault/OUSDVaultAdmin.sol new file mode 100644 index 0000000000..5ec50a406f --- /dev/null +++ b/contracts/contracts/vault/OUSDVaultAdmin.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { VaultAdmin } from "./VaultAdmin.sol"; + +/** + * @title OUSD VaultAdmin Contract + * @author Origin Protocol Inc + */ +contract OUSDVaultAdmin is VaultAdmin { + constructor(address _usdc) VaultAdmin(_usdc) {} +} diff --git a/contracts/contracts/vault/OUSDVaultCore.sol b/contracts/contracts/vault/OUSDVaultCore.sol new file mode 100644 index 0000000000..e4c41e4cbc --- /dev/null +++ b/contracts/contracts/vault/OUSDVaultCore.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { VaultCore } from "./VaultCore.sol"; + +/** + * @title OUSD VaultCore Contract + * @author Origin Protocol Inc + */ +contract OUSDVaultCore is VaultCore { + // For future use (because OETHBaseVaultCore inherits from this) + uint256[50] private __gap; + + constructor(address _usdc) VaultCore(_usdc) {} + + // @inheritdoc VaultCore + function _redeem(uint256 _amount, uint256 _minimumUnitAmount) + internal + virtual + override + { + // Only Strategist or Governor can redeem using the Vault for now. + // We don't have the onlyGovernorOrStrategist modifier on VaultCore. + // Since we won't be using that modifier anywhere in the VaultCore as well, + // the check has been added inline instead of moving it to VaultStorage. + require( + msg.sender == strategistAddr || isGovernor(), + "Caller is not the Strategist or Governor" + ); + + super._redeem(_amount, _minimumUnitAmount); + } +} diff --git a/contracts/contracts/vault/Vault.sol b/contracts/contracts/vault/Vault.sol deleted file mode 100644 index 7753403040..0000000000 --- a/contracts/contracts/vault/Vault.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.0; - -/** - * @title OUSD VaultInitializer Contract - * @notice The VaultInitializer sets up the initial contract. - * @author Origin Protocol Inc - */ -import { VaultInitializer } from "./VaultInitializer.sol"; -import { VaultAdmin } from "./VaultAdmin.sol"; - -contract Vault is VaultInitializer, VaultAdmin {} diff --git a/contracts/contracts/vault/VaultAdmin.sol b/contracts/contracts/vault/VaultAdmin.sol index fc402d97e4..c3dddb14b5 100644 --- a/contracts/contracts/vault/VaultAdmin.sol +++ b/contracts/contracts/vault/VaultAdmin.sol @@ -9,15 +9,13 @@ pragma solidity ^0.8.0; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { IOracle } from "../interfaces/IOracle.sol"; -import { ISwapper } from "../interfaces/ISwapper.sol"; import { IVault } from "../interfaces/IVault.sol"; import { StableMath } from "../utils/StableMath.sol"; import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import "./VaultStorage.sol"; -contract VaultAdmin is VaultStorage { +abstract contract VaultAdmin is VaultStorage { using SafeERC20 for IERC20; using StableMath for uint256; using SafeCast for uint256; @@ -33,19 +31,12 @@ contract VaultAdmin is VaultStorage { _; } + constructor(address _backingAsset) VaultStorage(_backingAsset) {} + /*************************************** Configuration ****************************************/ - /** - * @notice Set address of price provider. - * @param _priceProvider Address of price provider - */ - function setPriceProvider(address _priceProvider) external onlyGovernor { - priceProvider = _priceProvider; - emit PriceProviderUpdated(_priceProvider); - } - /** * @notice Set a fee in basis points to be charged for a redeem. * @param _redeemFeeBps Basis point fee to be charged @@ -57,8 +48,8 @@ contract VaultAdmin is VaultStorage { } /** - * @notice Set a buffer of assets to keep in the Vault to handle most - * redemptions without needing to spend gas unwinding assets from a Strategy. + * @notice Set a buffer of backingAsset to keep in the Vault to handle most + * redemptions without needing to spend gas unwinding backingAsset from a Strategy. * @param _vaultBuffer Percentage using 18 decimals. 100% = 1e18. */ function setVaultBuffer(uint256 _vaultBuffer) @@ -103,57 +94,26 @@ contract VaultAdmin is VaultStorage { } /** - * @notice Set the default Strategy for an asset, i.e. the one which the asset - will be automatically allocated to and withdrawn from - * @param _asset Address of the asset + * @notice Set the default Strategy for backingAsset, i.e. the one which + * the backingAsset will be automatically allocated to and withdrawn from * @param _strategy Address of the Strategy */ - function setAssetDefaultStrategy(address _asset, address _strategy) + function setDefaultStrategy(address _strategy) external onlyGovernorOrStrategist { - emit AssetDefaultStrategyUpdated(_asset, _strategy); + emit DefaultStrategyUpdated(_strategy); // If its a zero address being passed for the strategy we are removing // the default strategy if (_strategy != address(0)) { // Make sure the strategy meets some criteria require(strategies[_strategy].isSupported, "Strategy not approved"); - IStrategy strategy = IStrategy(_strategy); - require(assets[_asset].isSupported, "Asset is not supported"); require( - strategy.supportsAsset(_asset), + IStrategy(_strategy).supportsAsset(backingAsset), "Asset not supported by Strategy" ); } - assetDefaultStrategies[_asset] = _strategy; - } - - /** - * @notice Set maximum amount of OTokens that can at any point be minted and deployed - * to strategy (used only by ConvexOUSDMetaStrategy for now). - * @param _threshold OToken amount with 18 fixed decimals. - */ - function setNetOusdMintForStrategyThreshold(uint256 _threshold) - external - onlyGovernor - { - /** - * Because `netOusdMintedForStrategy` check in vault core works both ways - * (positive and negative) the actual impact of the amount of OToken minted - * could be double the threshold. E.g.: - * - contract has threshold set to 100 - * - state of netOusdMinted is -90 - * - in effect it can mint 190 OToken and still be within limits - * - * We are somewhat mitigating this behaviour by resetting the netOusdMinted - * counter whenever new threshold is set. So it can only move one threshold - * amount in each direction. This also enables us to reduce the threshold - * amount and not have problems with current netOusdMinted being near - * limits on either side. - */ - netOusdMintedForStrategy = 0; - netOusdMintForStrategyThreshold = _threshold; - emit NetOusdMintForStrategyThresholdChanged(_threshold); + defaultStrategy = _strategy; } /** @@ -202,267 +162,6 @@ contract VaultAdmin is VaultStorage { emit DripDurationChanged(_dripDuration); } - /*************************************** - Swaps - ****************************************/ - - /** - * @notice Strategist swaps collateral assets sitting in the vault. - * @param _fromAsset The token address of the asset being sold by the vault. - * @param _toAsset The token address of the asset being purchased by the vault. - * @param _fromAssetAmount The amount of assets being sold by the vault. - * @param _minToAssetAmount The minimum amount of assets to be purchased. - * @param _data implementation specific data. eg 1Inch swap data - * @return toAssetAmount The amount of toAssets that was received from the swap - */ - function swapCollateral( - address _fromAsset, - address _toAsset, - uint256 _fromAssetAmount, - uint256 _minToAssetAmount, - bytes calldata _data - ) - external - nonReentrant - onlyGovernorOrStrategist - returns (uint256 toAssetAmount) - { - toAssetAmount = _swapCollateral( - _fromAsset, - _toAsset, - _fromAssetAmount, - _minToAssetAmount, - _data - ); - } - - function _swapCollateral( - address _fromAsset, - address _toAsset, - uint256 _fromAssetAmount, - uint256 _minToAssetAmount, - bytes calldata _data - ) internal virtual returns (uint256 toAssetAmount) { - // Check fromAsset and toAsset are valid - Asset memory fromAssetConfig = assets[_fromAsset]; - Asset memory toAssetConfig = assets[_toAsset]; - require(fromAssetConfig.isSupported, "From asset is not supported"); - require(toAssetConfig.isSupported, "To asset is not supported"); - - // Load swap config into memory to avoid separate SLOADs - SwapConfig memory config = swapConfig; - - // Scope a new block to remove toAssetBalBefore from the scope of swapCollateral. - // This avoids a stack too deep error. - { - uint256 toAssetBalBefore = IERC20(_toAsset).balanceOf( - address(this) - ); - - // Transfer from assets to the swapper contract - IERC20(_fromAsset).safeTransfer(config.swapper, _fromAssetAmount); - - // Call to the Swapper contract to do the actual swap - // The -1 is required for stETH which sometimes transfers 1 wei less than what was specified. - // slither-disable-next-line unused-return - ISwapper(config.swapper).swap( - _fromAsset, - _toAsset, - _fromAssetAmount - 1, - _minToAssetAmount, - _data - ); - - // Compute the change in asset balance held by the Vault - toAssetAmount = - IERC20(_toAsset).balanceOf(address(this)) - - toAssetBalBefore; - } - - // Check the to assets returned is above slippage amount specified by the strategist - require( - toAssetAmount >= _minToAssetAmount, - "Strategist slippage limit" - ); - - // Scope a new block to remove minOracleToAssetAmount from the scope of swapCollateral. - // This avoids a stack too deep error. - { - // Check the slippage against the Oracle in case the strategist made a mistake or has become malicious. - // to asset amount = from asset amount * from asset price / to asset price - uint256 minOracleToAssetAmount = (_fromAssetAmount * - (1e4 - fromAssetConfig.allowedOracleSlippageBps) * - IOracle(priceProvider).price(_fromAsset)) / - (IOracle(priceProvider).price(_toAsset) * - (1e4 + toAssetConfig.allowedOracleSlippageBps)); - - // Scale both sides up to 18 decimals to compare - require( - toAssetAmount.scaleBy(18, toAssetConfig.decimals) >= - minOracleToAssetAmount.scaleBy( - 18, - fromAssetConfig.decimals - ), - "Oracle slippage limit exceeded" - ); - } - - // Check the vault's total value hasn't gone below the OToken total supply - // by more than the allowed percentage. - require( - IVault(address(this)).totalValue() >= - (oUSD.totalSupply() * ((1e4 - config.allowedUndervalueBps))) / - 1e4, - "Allowed value < supply" - ); - - emit Swapped(_fromAsset, _toAsset, _fromAssetAmount, toAssetAmount); - } - - /*************************************** - Swap Config - ****************************************/ - - /** - * @notice Set the contract the performs swaps of collateral assets. - * @param _swapperAddr Address of the Swapper contract that implements the ISwapper interface. - */ - function setSwapper(address _swapperAddr) external onlyGovernor { - swapConfig.swapper = _swapperAddr; - emit SwapperChanged(_swapperAddr); - } - - /// @notice Contract that swaps the vault's collateral assets - function swapper() external view returns (address swapper_) { - swapper_ = swapConfig.swapper; - } - - /** - * @notice Set max allowed percentage the vault total value can drop below the OToken total supply in basis points - * when executing collateral swaps. - * @param _basis Percentage in basis points. eg 100 == 1% - */ - function setSwapAllowedUndervalue(uint16 _basis) external onlyGovernor { - require(_basis < 10001, "Invalid basis points"); - swapConfig.allowedUndervalueBps = _basis; - emit SwapAllowedUndervalueChanged(_basis); - } - - /** - * @notice Max allowed percentage the vault total value can drop below the OToken total supply in basis points - * when executing a collateral swap. - * For example 100 == 1% - * @return value Percentage in basis points. - */ - function allowedSwapUndervalue() external view returns (uint256 value) { - value = swapConfig.allowedUndervalueBps; - } - - /** - * @notice Set the allowed slippage from the Oracle price for collateral asset swaps. - * @param _asset Address of the asset token. - * @param _allowedOracleSlippageBps allowed slippage from Oracle in basis points. eg 20 = 0.2%. Max 10%. - */ - function setOracleSlippage(address _asset, uint16 _allowedOracleSlippageBps) - external - onlyGovernor - { - require(assets[_asset].isSupported, "Asset not supported"); - require(_allowedOracleSlippageBps < 1000, "Slippage too high"); - - assets[_asset].allowedOracleSlippageBps = _allowedOracleSlippageBps; - - emit SwapSlippageChanged(_asset, _allowedOracleSlippageBps); - } - - /*************************************** - Asset Config - ****************************************/ - - /** - * @notice Add a supported asset to the contract, i.e. one that can be - * to mint OTokens. - * @param _asset Address of asset - */ - function supportAsset(address _asset, uint8 _unitConversion) - external - virtual - onlyGovernor - { - require(!assets[_asset].isSupported, "Asset already supported"); - - assets[_asset] = Asset({ - isSupported: true, - unitConversion: UnitConversion(_unitConversion), - decimals: 0, // will be overridden in _cacheDecimals - allowedOracleSlippageBps: 0 // 0% by default - }); - - _cacheDecimals(_asset); - allAssets.push(_asset); - - // Verify that our oracle supports the asset - // slither-disable-next-line unused-return - IOracle(priceProvider).price(_asset); - - emit AssetSupported(_asset); - } - - /** - * @notice Remove a supported asset from the Vault - * @param _asset Address of asset - */ - function removeAsset(address _asset) external onlyGovernor { - require(assets[_asset].isSupported, "Asset not supported"); - - // 1e13 for 18 decimals. And 10 for 6 decimals - uint256 maxDustBalance = uint256(1e13).scaleBy( - assets[_asset].decimals, - 18 - ); - - require( - IVault(address(this)).checkBalance(_asset) <= maxDustBalance, - "Vault still holds asset" - ); - - uint256 assetsCount = allAssets.length; - uint256 assetIndex = assetsCount; // initialize at invalid index - for (uint256 i = 0; i < assetsCount; ++i) { - if (allAssets[i] == _asset) { - assetIndex = i; - break; - } - } - - // Note: If asset is not found in `allAssets`, the following line - // will revert with an out-of-bound error. However, there's no - // reason why an asset would have `Asset.isSupported = true` but - // not exist in `allAssets`. - - // Update allAssets array - allAssets[assetIndex] = allAssets[assetsCount - 1]; - allAssets.pop(); - - // Reset default strategy - assetDefaultStrategies[_asset] = address(0); - emit AssetDefaultStrategyUpdated(_asset, address(0)); - - // Remove asset from storage - delete assets[_asset]; - - emit AssetRemoved(_asset); - } - - /** - * @notice Cache decimals on OracleRouter for a particular asset. This action - * is required before that asset's price can be accessed. - * @param _asset Address of asset token - */ - function cacheDecimals(address _asset) external onlyGovernor { - _cacheDecimals(_asset); - } - /*************************************** Strategy Config ****************************************/ @@ -485,14 +184,10 @@ contract VaultAdmin is VaultStorage { function removeStrategy(address _addr) external onlyGovernor { require(strategies[_addr].isSupported, "Strategy not approved"); - - uint256 assetCount = allAssets.length; - for (uint256 i = 0; i < assetCount; ++i) { - require( - assetDefaultStrategies[allAssets[i]] != _addr, - "Strategy is default for an asset" - ); - } + require( + defaultStrategy != _addr, + "Strategy is default for backing asset" + ); // Initialize strategyIndex with out of bounds result so function will // revert if no valid index found @@ -512,7 +207,7 @@ contract VaultAdmin is VaultStorage { // Mark the strategy as not supported strategies[_addr].isSupported = false; - // Withdraw all assets + // Withdraw all backingAsset IStrategy strategy = IStrategy(_addr); strategy.withdrawAll(); @@ -520,13 +215,52 @@ contract VaultAdmin is VaultStorage { } } + /** + * @notice Adds a strategy to the mint whitelist. + * Reverts if strategy isn't approved on Vault. + * @param strategyAddr Strategy address + */ + function addStrategyToMintWhitelist(address strategyAddr) + external + onlyGovernor + { + require(strategies[strategyAddr].isSupported, "Strategy not approved"); + + require( + !isMintWhitelistedStrategy[strategyAddr], + "Already whitelisted" + ); + + isMintWhitelistedStrategy[strategyAddr] = true; + + emit StrategyAddedToMintWhitelist(strategyAddr); + } + + /** + * @notice Removes a strategy from the mint whitelist. + * @param strategyAddr Strategy address + */ + function removeStrategyFromMintWhitelist(address strategyAddr) + external + onlyGovernor + { + // Intentionally skipping `strategies.isSupported` check since + // we may wanna remove an address even after removing the strategy + + require(isMintWhitelistedStrategy[strategyAddr], "Not whitelisted"); + + isMintWhitelistedStrategy[strategyAddr] = false; + + emit StrategyRemovedFromMintWhitelist(strategyAddr); + } + /*************************************** Strategies ****************************************/ /** - * @notice Deposit multiple assets from the vault into the strategy. - * @param _strategyToAddress Address of the Strategy to deposit assets into. + * @notice Deposit multiple backingAsset from the vault into the strategy. + * @param _strategyToAddress Address of the Strategy to deposit backingAsset into. * @param _assets Array of asset address that will be deposited into the strategy. * @param _amounts Array of amounts of each corresponding asset to deposit. */ @@ -547,26 +281,30 @@ contract VaultAdmin is VaultStorage { strategies[_strategyToAddress].isSupported, "Invalid to Strategy" ); - require(_assets.length == _amounts.length, "Parameter length mismatch"); + require( + _assets.length == 1 && + _amounts.length == 1 && + _assets[0] == backingAsset, + "Only backing asset is supported" + ); - uint256 assetCount = _assets.length; - for (uint256 i = 0; i < assetCount; ++i) { - address assetAddr = _assets[i]; - require( - IStrategy(_strategyToAddress).supportsAsset(assetAddr), - "Asset unsupported" - ); - // Send required amount of funds to the strategy - IERC20(assetAddr).safeTransfer(_strategyToAddress, _amounts[i]); - } + // Check the there is enough backing asset to transfer once the backing + // asset reserved for the withdrawal queue is accounted for + require( + _amounts[0] <= _backingAssetAvailable(), + "Not enough backing asset available" + ); + + // Send required amount of funds to the strategy + IERC20(backingAsset).safeTransfer(_strategyToAddress, _amounts[0]); // Deposit all the funds that have been sent to the strategy IStrategy(_strategyToAddress).depositAll(); } /** - * @notice Withdraw multiple assets from the strategy to the vault. - * @param _strategyFromAddress Address of the Strategy to withdraw assets from. + * @notice Withdraw multiple backingAsset from the strategy to the vault. + * @param _strategyFromAddress Address of the Strategy to withdraw backingAsset from. * @param _assets Array of asset address that will be withdrawn from the strategy. * @param _amounts Array of amounts of each corresponding asset to withdraw. */ @@ -607,11 +345,13 @@ contract VaultAdmin is VaultStorage { _amounts[i] ); } + + IVault(address(this)).addWithdrawalQueueLiquidity(); } /** * @notice Sets the maximum allowable difference between - * total supply and backing assets' value. + * total supply and backing backingAsset' value. */ function setMaxSupplyDiff(uint256 _maxSupplyDiff) external onlyGovernor { maxSupplyDiff = _maxSupplyDiff; @@ -637,18 +377,6 @@ contract VaultAdmin is VaultStorage { emit TrusteeFeeBpsChanged(_basis); } - /** - * @notice Set OToken Metapool strategy - * @param _ousdMetaStrategy Address of OToken metapool strategy - */ - function setOusdMetaStrategy(address _ousdMetaStrategy) - external - onlyGovernor - { - ousdMetaStrategy = _ousdMetaStrategy; - emit OusdMetaStrategyUpdated(_ousdMetaStrategy); - } - /*************************************** Pause ****************************************/ @@ -699,16 +427,43 @@ contract VaultAdmin is VaultStorage { external onlyGovernor { - require(!assets[_asset].isSupported, "Only unsupported assets"); + require(backingAsset != _asset, "Only unsupported backingAsset"); IERC20(_asset).safeTransfer(governor(), _amount); } + /** + * @dev Calculate how much backingAsset (eg. WETH or USDC) in the vault is not reserved for the withdrawal queue. + * That is, it is available to be redeemed or deposited into a strategy. + */ + function _backingAssetAvailable() + internal + view + returns (uint256 backingAssetAvailable) + { + WithdrawalQueueMetadata memory queue = withdrawalQueueMetadata; + + // The amount of backingAsset that is still to be claimed in the withdrawal queue + uint256 outstandingWithdrawals = queue.queued - queue.claimed; + + // The amount of sitting in backingAsset in the vault + uint256 backingAssetBalance = IERC20(backingAsset).balanceOf( + address(this) + ); + + // If there is not enough backingAsset in the vault to cover the outstanding withdrawals + if (backingAssetBalance <= outstandingWithdrawals) { + return 0; + } + + return backingAssetBalance - outstandingWithdrawals; + } + /*************************************** Strategies Admin ****************************************/ /** - * @notice Withdraws all assets from the strategy and sends assets to the Vault. + * @notice Withdraws all backingAsset from the strategy and sends backingAsset to the Vault. * @param _strategyAddr Strategy address. */ function withdrawAllFromStrategy(address _strategyAddr) @@ -725,10 +480,11 @@ contract VaultAdmin is VaultStorage { ); IStrategy strategy = IStrategy(_strategyAddr); strategy.withdrawAll(); + IVault(address(this)).addWithdrawalQueueLiquidity(); } /** - * @notice Withdraws all assets from all the strategies and sends assets to the Vault. + * @notice Withdraws all backingAsset from all the strategies and sends backingAsset to the Vault. */ function withdrawAllFromStrategies() external onlyGovernorOrStrategist { _withdrawAllFromStrategies(); @@ -739,19 +495,6 @@ contract VaultAdmin is VaultStorage { for (uint256 i = 0; i < stratCount; ++i) { IStrategy(allStrategies[i]).withdrawAll(); } - } - - /*************************************** - Utils - ****************************************/ - - function _cacheDecimals(address token) internal { - Asset storage tokenAsset = assets[token]; - if (tokenAsset.decimals != 0) { - return; - } - uint8 decimals = IBasicToken(token).decimals(); - require(decimals >= 6 && decimals <= 18, "Unexpected precision"); - tokenAsset.decimals = decimals; + IVault(address(this)).addWithdrawalQueueLiquidity(); } } diff --git a/contracts/contracts/vault/VaultCore.sol b/contracts/contracts/vault/VaultCore.sol index ef1abed76a..b676431610 100644 --- a/contracts/contracts/vault/VaultCore.sol +++ b/contracts/contracts/vault/VaultCore.sol @@ -3,23 +3,22 @@ pragma solidity ^0.8.0; /** * @title OToken VaultCore contract - * @notice The Vault contract stores assets. On a deposit, OTokens will be minted + * @notice The Vault contract stores backingAsset. On a deposit, OTokens will be minted and sent to the depositor. On a withdrawal, OTokens will be burned and - assets will be sent to the withdrawer. The Vault accepts deposits of + backingAsset will be sent to the withdrawer. The Vault accepts deposits of interest from yield bearing strategies which will modify the supply of OTokens. * @author Origin Protocol Inc */ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import { StableMath } from "../utils/StableMath.sol"; -import { IOracle } from "../interfaces/IOracle.sol"; -import { IGetExchangeRateToken } from "../interfaces/IGetExchangeRateToken.sol"; import "./VaultInitializer.sol"; -contract VaultCore is VaultInitializer { +abstract contract VaultCore is VaultInitializer { using SafeERC20 for IERC20; using StableMath for uint256; /// @dev max signed int @@ -41,17 +40,11 @@ contract VaultCore is VaultInitializer { _; } - /** - * @dev Verifies that the caller is the AMO strategy. - */ - modifier onlyOusdMetaStrategy() { - require( - msg.sender == ousdMetaStrategy, - "Caller is not the OUSD meta strategy" - ); - _; - } + constructor(address _backingAsset) VaultInitializer(_backingAsset) {} + //////////////////////////////////////////////////// + /// MINT / REDEEM / BURN /// + //////////////////////////////////////////////////// /** * @notice Deposit a supported asset and mint OTokens. * @param _asset Address of the asset being deposited @@ -66,6 +59,7 @@ contract VaultCore is VaultInitializer { _mint(_asset, _amount, _minimumOusdAmount); } + // slither-disable-start reentrancy-no-eth /** * @dev Deposit a supported asset and mint OTokens. * @param _asset Address of the asset being deposited @@ -77,43 +71,44 @@ contract VaultCore is VaultInitializer { uint256 _amount, uint256 _minimumOusdAmount ) internal virtual { - require(assets[_asset].isSupported, "Asset is not supported"); + require(_asset == backingAsset, "Unsupported asset for minting"); require(_amount > 0, "Amount must be greater than 0"); - uint256 units = _toUnits(_amount, _asset); - uint256 unitPrice = _toUnitPrice(_asset, true); - uint256 priceAdjustedDeposit = (units * unitPrice) / 1e18; - - if (_minimumOusdAmount > 0) { - require( - priceAdjustedDeposit >= _minimumOusdAmount, - "Mint amount lower than minimum" - ); - } + // Scale amount to 18 decimals + uint256 scaledAmount = _amount.scaleBy(18, backingAssetDecimals); + require( + scaledAmount >= _minimumOusdAmount, + "Mint amount lower than minimum" + ); - emit Mint(msg.sender, priceAdjustedDeposit); + emit Mint(msg.sender, scaledAmount); // Rebase must happen before any transfers occur. - if (priceAdjustedDeposit >= rebaseThreshold && !rebasePaused) { + if (!rebasePaused && scaledAmount >= rebaseThreshold) { _rebase(); } - // Mint matching amount of OTokens - oUSD.mint(msg.sender, priceAdjustedDeposit); + // Mint oTokens + oUSD.mint(msg.sender, scaledAmount); - // Transfer the deposited coins to the vault - IERC20 asset = IERC20(_asset); - asset.safeTransferFrom(msg.sender, address(this), _amount); + IERC20(_asset).safeTransferFrom(msg.sender, address(this), _amount); + + // Give priority to the withdrawal queue for the new backingAsset liquidity + _addWithdrawalQueueLiquidity(); - if (priceAdjustedDeposit >= autoAllocateThreshold) { + // Auto-allocate if necessary + if (scaledAmount >= autoAllocateThreshold) { _allocate(); } } + // slither-disable-end reentrancy-no-eth + /** - * @notice Mint OTokens for a Metapool Strategy - * @param _amount Amount of the asset being deposited + * @notice Mint OTokens for an allowed Strategy + * @param _amount Amount of OToken to mint * + * Todo: Maybe this is a comment that we can remove now? * Notice: can't use `nonReentrant` modifier since the `mint` function can * call `allocate`, and that can trigger `ConvexOUSDMetaStrategy` to call this function * while the execution of the `mint` has not yet completed -> causing a `nonReentrant` collision. @@ -127,26 +122,21 @@ contract VaultCore is VaultInitializer { external virtual whenNotCapitalPaused - onlyOusdMetaStrategy { - require(_amount < MAX_INT, "Amount too high"); - - emit Mint(msg.sender, _amount); - - // safe to cast because of the require check at the beginning of the function - netOusdMintedForStrategy += int256(_amount); - require( - abs(netOusdMintedForStrategy) < netOusdMintForStrategyThreshold, - "Minted ousd surpassed netOusdMintForStrategyThreshold." + strategies[msg.sender].isSupported == true, + "Unsupported strategy" + ); + require( + isMintWhitelistedStrategy[msg.sender] == true, + "Not whitelisted strategy" ); + emit Mint(msg.sender, _amount); // Mint matching amount of OTokens oUSD.mint(msg.sender, _amount); } - // In memoriam - /** * @notice Withdraw a supported asset and burn OTokens. * @param _amount Amount of OTokens to burn @@ -169,52 +159,36 @@ contract VaultCore is VaultInitializer { internal virtual { - // Calculate redemption outputs - uint256[] memory outputs = _calculateRedeemOutputs(_amount); - emit Redeem(msg.sender, _amount); - // Send outputs - uint256 assetCount = allAssets.length; - for (uint256 i = 0; i < assetCount; ++i) { - if (outputs[i] == 0) continue; - - address assetAddr = allAssets[i]; - - if (IERC20(assetAddr).balanceOf(address(this)) >= outputs[i]) { - // Use Vault funds first if sufficient - IERC20(assetAddr).safeTransfer(msg.sender, outputs[i]); - } else { - address strategyAddr = assetDefaultStrategies[assetAddr]; - if (strategyAddr != address(0)) { - // Nothing in Vault, but something in Strategy, send from there - IStrategy strategy = IStrategy(strategyAddr); - strategy.withdraw(msg.sender, assetAddr, outputs[i]); - } else { - // Cant find funds anywhere - revert("Liquidity error"); - } - } - } + if (_amount == 0) return; - if (_minimumUnitAmount > 0) { - uint256 unitTotal = 0; - for (uint256 i = 0; i < outputs.length; ++i) { - unitTotal += _toUnits(outputs[i], allAssets[i]); - } - require( - unitTotal >= _minimumUnitAmount, - "Redeem amount lower than minimum" - ); - } + // Amount excluding fees + // No fee for the strategist or the governor, makes it easier to do operations + uint256 amountMinusFee = (msg.sender == strategistAddr || isGovernor()) + ? _amount.scaleBy(backingAssetDecimals, 18) + : _calculateRedeemOutputs(_amount)[0]; + require( + amountMinusFee >= _minimumUnitAmount, + "Redeem amount lower than minimum" + ); + + // Is there enough backingAsset in the Vault available after accounting for the withdrawal queue + require(_backingAssetAvailable() >= amountMinusFee, "Liquidity error"); + + // Transfer backingAsset minus the fee to the redeemer + IERC20(backingAsset).safeTransfer(msg.sender, amountMinusFee); + + // Burn OToken from user (including fees) oUSD.burn(msg.sender, _amount); + // Prevent insolvency _postRedeem(_amount); } function _postRedeem(uint256 _amount) internal { - // Until we can prove that we won't affect the prices of our assets + // Until we can prove that we won't affect the prices of our backingAsset // by withdrawing them, this should be here. // It's possible that a strategy was off on its asset total, perhaps // a reward token sold for more or for less than anticipated. @@ -225,15 +199,15 @@ contract VaultCore is VaultInitializer { totalUnits = _totalValue(); } - // Check that the OTokens are backed by enough assets + // Check that the OTokens are backed by enough backingAsset if (maxSupplyDiff > 0) { - // If there are more outstanding withdrawal requests than assets in the vault and strategies - // then the available assets will be negative and totalUnits will be rounded up to zero. + // If there are more outstanding withdrawal requests than backingAsset in the vault and strategies + // then the available backingAsset will be negative and totalUnits will be rounded up to zero. // As we don't know the exact shortfall amount, we will reject all redeem and withdrawals require(totalUnits > 0, "Too many outstanding requests"); // Allow a max difference of maxSupplyDiff% between - // backing assets value and OUSD total supply + // backing backingAsset value and OUSD total supply uint256 diff = oUSD.totalSupply().divPrecisely(totalUnits); require( (diff > 1e18 ? diff - 1e18 : 1e18 - diff) <= maxSupplyDiff, @@ -243,9 +217,10 @@ contract VaultCore is VaultInitializer { } /** - * @notice Burn OTokens for Metapool Strategy - * @param _amount Amount of OUSD to burn + * @notice Burn OTokens for an allowed Strategy + * @param _amount Amount of OToken to burn * + * Todo: Maybe this is a comment that we can remove now? * @dev Notice: can't use `nonReentrant` modifier since the `redeem` function could * require withdrawal on `ConvexOUSDMetaStrategy` and that one can call `burnForStrategy` * while the execution of the `redeem` has not yet completed -> causing a `nonReentrant` collision. @@ -259,101 +234,249 @@ contract VaultCore is VaultInitializer { external virtual whenNotCapitalPaused - onlyOusdMetaStrategy { - require(_amount < MAX_INT, "Amount too high"); + require( + strategies[msg.sender].isSupported == true, + "Unsupported strategy" + ); + require( + isMintWhitelistedStrategy[msg.sender] == true, + "Not whitelisted strategy" + ); emit Redeem(msg.sender, _amount); - // safe to cast because of the require check at the beginning of the function - netOusdMintedForStrategy -= int256(_amount); + // Burn OTokens + oUSD.burn(msg.sender, _amount); + } + + //////////////////////////////////////////////////// + /// ASYNC WITHDRAWALS /// + //////////////////////////////////////////////////// + /** + * @notice Request an asynchronous withdrawal of backingAsset in exchange for OToken. + * The OToken is burned on request and the backingAsset is transferred to the withdrawer on claim. + * This request can be claimed once the withdrawal queue's `claimable` amount + * is greater than or equal this request's `queued` amount. + * There is a minimum of 10 minutes before a request can be claimed. After that, the request just needs + * enough backingAsset liquidity in the Vault to satisfy all the outstanding requests to that point in the queue. + * OToken is converted to backingAsset at 1:1. + * @param _amount Amount of OToken to burn. + * @return requestId Unique ID for the withdrawal request + * @return queued Cumulative total of all backingAsset queued including already claimed requests. + */ + function requestWithdrawal(uint256 _amount) + external + virtual + whenNotCapitalPaused + nonReentrant + returns (uint256 requestId, uint256 queued) + { + require(withdrawalClaimDelay > 0, "Async withdrawals not enabled"); + + // The check that the requester has enough OToken is done in to later burn call + + requestId = withdrawalQueueMetadata.nextWithdrawalIndex; + queued = + withdrawalQueueMetadata.queued + + _amount.scaleBy(backingAssetDecimals, 18); + + // Store the next withdrawal request + withdrawalQueueMetadata.nextWithdrawalIndex = SafeCast.toUint128( + requestId + 1 + ); + // Store the updated queued amount which reserves backingAsset in the withdrawal queue + // and reduces the vault's total backingAsset + withdrawalQueueMetadata.queued = SafeCast.toUint128(queued); + // Store the user's withdrawal request + // `queued` is in backingAsset decimals, while `amount` is in OToken decimals (18) + withdrawalRequests[requestId] = WithdrawalRequest({ + withdrawer: msg.sender, + claimed: false, + timestamp: uint40(block.timestamp), + amount: SafeCast.toUint128(_amount), + queued: SafeCast.toUint128(queued) + }); + + // Burn the user's OToken + oUSD.burn(msg.sender, _amount); + + // Prevent withdrawal if the vault is solvent by more than the allowed percentage + _postRedeem(_amount); + + emit WithdrawalRequested(msg.sender, requestId, _amount, queued); + } + + // slither-disable-start reentrancy-no-eth + /** + * @notice Claim a previously requested withdrawal once it is claimable. + * This request can be claimed once the withdrawal queue's `claimable` amount + * is greater than or equal this request's `queued` amount and 10 minutes has passed. + * If the requests is not claimable, the transaction will revert with `Queue pending liquidity`. + * If the request is not older than 10 minutes, the transaction will revert with `Claim delay not met`. + * OToken is converted to backingAsset at 1:1. + * @param _requestId Unique ID for the withdrawal request + * @return amount Amount of backingAsset transferred to the withdrawer + */ + function claimWithdrawal(uint256 _requestId) + external + virtual + whenNotCapitalPaused + nonReentrant + returns (uint256 amount) + { + // Try and get more liquidity if there is not enough available + if ( + withdrawalRequests[_requestId].queued > + withdrawalQueueMetadata.claimable + ) { + // Add any backingAsset to the withdrawal queue + // this needs to remain here as: + // - Vault can be funded and `addWithdrawalQueueLiquidity` is not externally called + // - funds can be withdrawn from a strategy + // + // Those funds need to be added to withdrawal queue liquidity + _addWithdrawalQueueLiquidity(); + } + + // Scale amount to backingAsset decimals + amount = _claimWithdrawal(_requestId).scaleBy(backingAssetDecimals, 18); + + // transfer backingAsset from the vault to the withdrawer + IERC20(backingAsset).safeTransfer(msg.sender, amount); + + // Prevent insolvency + _postRedeem(amount.scaleBy(18, backingAssetDecimals)); + } + + // slither-disable-end reentrancy-no-eth + /** + * @notice Claim a previously requested withdrawals once they are claimable. + * This requests can be claimed once the withdrawal queue's `claimable` amount + * is greater than or equal each request's `queued` amount and 10 minutes has passed. + * If one of the requests is not claimable, the whole transaction will revert with `Queue pending liquidity`. + * If one of the requests is not older than 10 minutes, + * the whole transaction will revert with `Claim delay not met`. + * @param _requestIds Unique ID of each withdrawal request + * @return amounts Amount of backingAsset received for each request + * @return totalAmount Total amount of backingAsset transferred to the withdrawer + */ + function claimWithdrawals(uint256[] calldata _requestIds) + external + virtual + whenNotCapitalPaused + nonReentrant + returns (uint256[] memory amounts, uint256 totalAmount) + { + // Add any backingAsset to the withdrawal queue + // this needs to remain here as: + // - Vault can be funded and `addWithdrawalQueueLiquidity` is not externally called + // - funds can be withdrawn from a strategy + // + // Those funds need to be added to withdrawal queue liquidity + _addWithdrawalQueueLiquidity(); + + amounts = new uint256[](_requestIds.length); + for (uint256 i; i < _requestIds.length; ++i) { + // Scale all amounts to backingAsset decimals, thus totalAmount is also in backingAsset decimals + amounts[i] = _claimWithdrawal(_requestIds[i]).scaleBy( + backingAssetDecimals, + 18 + ); + totalAmount += amounts[i]; + } + + // transfer all the claimed backingAsset from the vault to the withdrawer + IERC20(backingAsset).safeTransfer(msg.sender, totalAmount); + + // Prevent insolvency + _postRedeem(totalAmount.scaleBy(18, backingAssetDecimals)); + + return (amounts, totalAmount); + } + + function _claimWithdrawal(uint256 requestId) + internal + returns (uint256 amount) + { + require(withdrawalClaimDelay > 0, "Async withdrawals not enabled"); + + // Load the structs from storage into memory + WithdrawalRequest memory request = withdrawalRequests[requestId]; + WithdrawalQueueMetadata memory queue = withdrawalQueueMetadata; require( - abs(netOusdMintedForStrategy) < netOusdMintForStrategyThreshold, - "Attempting to burn too much OUSD." + request.timestamp + withdrawalClaimDelay <= block.timestamp, + "Claim delay not met" ); + // If there isn't enough reserved liquidity in the queue to claim + require(request.queued <= queue.claimable, "Queue pending liquidity"); + require(request.withdrawer == msg.sender, "Not requester"); + require(request.claimed == false, "Already claimed"); + + // Store the request as claimed + withdrawalRequests[requestId].claimed = true; + // Store the updated claimed amount + withdrawalQueueMetadata.claimed = + queue.claimed + + SafeCast.toUint128( + StableMath.scaleBy(request.amount, backingAssetDecimals, 18) + ); - // Burn OTokens - oUSD.burn(msg.sender, _amount); + emit WithdrawalClaimed(msg.sender, requestId, request.amount); + + return request.amount; } /** * @notice Allocate unallocated funds on Vault to strategies. - **/ + */ function allocate() external virtual whenNotCapitalPaused nonReentrant { + // Add any unallocated backingAsset to the withdrawal queue first + _addWithdrawalQueueLiquidity(); + _allocate(); } /** - * @dev Allocate unallocated funds on Vault to strategies. - **/ + * @dev Allocate backingAsset (eg. WETH or USDC) to the default backingAsset strategy + * if there is excess to the Vault buffer. + * This is called from either `mint` or `allocate` and assumes `_addWithdrawalQueueLiquidity` + * has been called before this function. + */ function _allocate() internal virtual { - uint256 vaultValue = _totalValueInVault(); - // Nothing in vault to allocate - if (vaultValue == 0) return; - uint256 strategiesValue = _totalValueInStrategies(); - // We have a method that does the same as this, gas optimisation - uint256 calculatedTotalValue = vaultValue + strategiesValue; - - // We want to maintain a buffer on the Vault so calculate a percentage - // modifier to multiply each amount being allocated by to enforce the - // vault buffer - uint256 vaultBufferModifier; - if (strategiesValue == 0) { - // Nothing in Strategies, allocate 100% minus the vault buffer to - // strategies - vaultBufferModifier = uint256(1e18) - vaultBuffer; - } else { - vaultBufferModifier = - (vaultBuffer * calculatedTotalValue) / - vaultValue; - if (1e18 > vaultBufferModifier) { - // E.g. 1e18 - (1e17 * 10e18)/5e18 = 8e17 - // (5e18 * 8e17) / 1e18 = 4e18 allocated from Vault - vaultBufferModifier = uint256(1e18) - vaultBufferModifier; - } else { - // We need to let the buffer fill - return; - } - } - if (vaultBufferModifier == 0) return; - - // Iterate over all assets in the Vault and allocate to the appropriate - // strategy - uint256 assetCount = allAssets.length; - for (uint256 i = 0; i < assetCount; ++i) { - IERC20 asset = IERC20(allAssets[i]); - uint256 assetBalance = asset.balanceOf(address(this)); - // No balance, nothing to do here - if (assetBalance == 0) continue; - - // Multiply the balance by the vault buffer modifier and truncate - // to the scale of the asset decimals - uint256 allocateAmount = assetBalance.mulTruncate( - vaultBufferModifier - ); + // No need to do anything if no default strategy for backingAsset + address depositStrategyAddr = defaultStrategy; + if (depositStrategyAddr == address(0)) return; + + uint256 backingAssetAvailableInVault = _backingAssetAvailable(); + // No need to do anything if there isn't any backingAsset in the vault to allocate + if (backingAssetAvailableInVault == 0) return; + + // Calculate the target buffer for the vault using the total supply + uint256 totalSupply = oUSD.totalSupply(); + // Scaled to backingAsset decimals + uint256 targetBuffer = totalSupply.mulTruncate(vaultBuffer).scaleBy( + backingAssetDecimals, + 18 + ); - address depositStrategyAddr = assetDefaultStrategies[ - address(asset) - ]; - - if (depositStrategyAddr != address(0) && allocateAmount > 0) { - IStrategy strategy = IStrategy(depositStrategyAddr); - // Transfer asset to Strategy and call deposit method to - // mint or take required action - asset.safeTransfer(address(strategy), allocateAmount); - strategy.deposit(address(asset), allocateAmount); - emit AssetAllocated( - address(asset), - depositStrategyAddr, - allocateAmount - ); - } - } + // If available backingAsset in the Vault is below or equal the target buffer then there's nothing to allocate + if (backingAssetAvailableInVault <= targetBuffer) return; + + // The amount of backingAsset to allocate to the default strategy + uint256 allocateAmount = backingAssetAvailableInVault - targetBuffer; + + IStrategy strategy = IStrategy(depositStrategyAddr); + // Transfer backingAsset to the strategy and call the strategy's deposit function + IERC20(backingAsset).safeTransfer(address(strategy), allocateAmount); + strategy.deposit(backingAsset, allocateAmount); + + emit AssetAllocated(backingAsset, depositStrategyAddr, allocateAmount); } /** - * @notice Calculate the total value of assets held by the Vault and all + * @notice Calculate the total value of backingAsset held by the Vault and all * strategies and update the supply of OTokens. */ function rebase() external virtual nonReentrant { @@ -361,7 +484,7 @@ contract VaultCore is VaultInitializer { } /** - * @dev Calculate the total value of assets held by the Vault and all + * @dev Calculate the total value of backingAsset held by the Vault and all * strategies and update the supply of OTokens, optionally sending a * portion of the yield to the trustee. * @return totalUnits Total balance of Vault in units @@ -461,7 +584,7 @@ contract VaultCore is VaultInitializer { } /** - * @notice Determine the total value of assets held by the vault and its + * @notice Determine the total value of backingAsset held by the vault and its * strategies. * @return value Total value in USD/ETH (1e18) */ @@ -470,16 +593,25 @@ contract VaultCore is VaultInitializer { } /** - * @dev Internal Calculate the total value of the assets held by the - * vault and its strategies. + * @dev Internal Calculate the total value of the backingAsset held by the + * vault and its strategies. + * @dev The total value of all WETH held by the vault and all its strategies + * less any WETH that is reserved for the withdrawal queue. + * If there is not enough WETH in the vault and all strategies to cover + * all outstanding withdrawal requests then return a total value of 0. * @return value Total value in USD/ETH (1e18) */ function _totalValue() internal view virtual returns (uint256 value) { - return _totalValueInVault() + _totalValueInStrategies(); + // As backingAsset is the only asset, just return the backingAsset balance + value = _checkBalance(backingAsset).scaleBy(18, backingAssetDecimals); } /** - * @dev Internal to calculate total value of all assets held in Vault. + * @dev Internal to calculate total value of all backingAsset held in Vault. + * @dev Only backingAsset is supported in the OETH Vault so return the backingAsset balance only + * Any ETH balances in the Vault will be ignored. + * Amounts from previously supported vault backingAsset will also be ignored. + * For example, there is 1 wei left of stETH in the OETH Vault but is will be ignored. * @return value Total value in USD/ETH (1e18) */ function _totalValueInVault() @@ -488,48 +620,7 @@ contract VaultCore is VaultInitializer { virtual returns (uint256 value) { - uint256 assetCount = allAssets.length; - for (uint256 y; y < assetCount; ++y) { - address assetAddr = allAssets[y]; - uint256 balance = IERC20(assetAddr).balanceOf(address(this)); - if (balance > 0) { - value += _toUnits(balance, assetAddr); - } - } - } - - /** - * @dev Internal to calculate total value of all assets held in Strategies. - * @return value Total value in USD/ETH (1e18) - */ - function _totalValueInStrategies() internal view returns (uint256 value) { - uint256 stratCount = allStrategies.length; - for (uint256 i = 0; i < stratCount; ++i) { - value = value + _totalValueInStrategy(allStrategies[i]); - } - } - - /** - * @dev Internal to calculate total value of all assets held by strategy. - * @param _strategyAddr Address of the strategy - * @return value Total value in USD/ETH (1e18) - */ - function _totalValueInStrategy(address _strategyAddr) - internal - view - returns (uint256 value) - { - IStrategy strategy = IStrategy(_strategyAddr); - uint256 assetCount = allAssets.length; - for (uint256 y; y < assetCount; ++y) { - address assetAddr = allAssets[y]; - if (strategy.supportsAsset(assetAddr)) { - uint256 balance = strategy.checkBalance(assetAddr); - if (balance > 0) { - value += _toUnits(balance, assetAddr); - } - } - } + value = IERC20(backingAsset).balanceOf(address(this)); } /** @@ -543,6 +634,15 @@ contract VaultCore is VaultInitializer { /** * @notice Get the balance of an asset held in Vault and all strategies. + * @dev Get the balance of an asset held in Vault and all strategies + * less any backingAsset that is reserved for the withdrawal queue. + * BaseAsset is the only asset that can return a non-zero balance. + * All other backingAsset will return 0 even if there is some dust amounts left in the Vault. + * For example, there is 1 wei left of stETH (or USDC) in the OETH (or OUSD) Vault but + * will return 0 in this function. + * + * If there is not enough backingAsset in the vault and all strategies to cover all outstanding + * withdrawal requests then return a backingAsset balance of 0 * @param _asset Address of asset * @return balance Balance of asset in decimals of asset */ @@ -552,6 +652,9 @@ contract VaultCore is VaultInitializer { virtual returns (uint256 balance) { + if (_asset != backingAsset) return 0; + + // Get the backingAsset in the vault and the strategies IERC20 asset = IERC20(_asset); balance = asset.balanceOf(address(this)); uint256 stratCount = allStrategies.length; @@ -561,6 +664,18 @@ contract VaultCore is VaultInitializer { balance = balance + strategy.checkBalance(_asset); } } + + WithdrawalQueueMetadata memory queue = withdrawalQueueMetadata; + + // If the vault becomes insolvent enough that the total value in the vault and all strategies + // is less than the outstanding withdrawals. + // For example, there was a mass slashing event and most users request a withdrawal. + if (balance + queue.claimed < queue.queued) { + return 0; + } + + // Need to remove backingAsset that is reserved for the withdrawal queue + return balance + queue.claimed - queue.queued; } /** @@ -578,7 +693,7 @@ contract VaultCore is VaultInitializer { /** * @dev Calculate the outputs for a redeem function, i.e. the mix of * coins that will be returned. - * @return outputs Array of amounts respective to the supported assets + * @return outputs Array of amounts respective to the supported backingAsset */ function _calculateRedeemOutputs(uint256 _amount) internal @@ -586,247 +701,113 @@ contract VaultCore is VaultInitializer { virtual returns (uint256[] memory outputs) { - // We always give out coins in proportion to how many we have, - // Now if all coins were the same value, this math would easy, - // just take the percentage of each coin, and multiply by the - // value to be given out. But if coins are worth more than $1, - // then we would end up handing out too many coins. We need to - // adjust by the total value of coins. - // - // To do this, we total up the value of our coins, by their - // percentages. Then divide what we would otherwise give out by - // this number. - // - // Let say we have 100 DAI at $1.06 and 200 USDT at $1.00. - // So for every 1 DAI we give out, we'll be handing out 2 USDT - // Our total output ratio is: 33% * 1.06 + 66% * 1.00 = 1.02 - // - // So when calculating the output, we take the percentage of - // each coin, times the desired output value, divided by the - // totalOutputRatio. - // - // For example, withdrawing: 30 OUSD: - // DAI 33% * 30 / 1.02 = 9.80 DAI - // USDT = 66 % * 30 / 1.02 = 19.60 USDT - // - // Checking these numbers: - // 9.80 DAI * 1.06 = $10.40 - // 19.60 USDT * 1.00 = $19.60 - // - // And so the user gets $10.40 + $19.60 = $30 worth of value. - - uint256 assetCount = allAssets.length; - uint256[] memory assetUnits = new uint256[](assetCount); - uint256[] memory assetBalances = new uint256[](assetCount); - outputs = new uint256[](assetCount); - // Calculate redeem fee if (redeemFeeBps > 0) { uint256 redeemFee = _amount.mulTruncateScale(redeemFeeBps, 1e4); _amount = _amount - redeemFee; } - // Calculate assets balances and decimals once, - // for a large gas savings. - uint256 totalUnits = 0; - for (uint256 i = 0; i < assetCount; ++i) { - address assetAddr = allAssets[i]; - uint256 balance = _checkBalance(assetAddr); - assetBalances[i] = balance; - assetUnits[i] = _toUnits(balance, assetAddr); - totalUnits = totalUnits + assetUnits[i]; - } - // Calculate totalOutputRatio - uint256 totalOutputRatio = 0; - for (uint256 i = 0; i < assetCount; ++i) { - uint256 unitPrice = _toUnitPrice(allAssets[i], false); - uint256 ratio = (assetUnits[i] * unitPrice) / totalUnits; - totalOutputRatio = totalOutputRatio + ratio; - } - // Calculate final outputs - uint256 factor = _amount.divPrecisely(totalOutputRatio); - for (uint256 i = 0; i < assetCount; ++i) { - outputs[i] = (assetBalances[i] * factor) / totalUnits; - } + // Todo: Maybe we can change function signature and return a simple uint256 + outputs = new uint256[](1); + outputs[0] = _amount.scaleBy(backingAssetDecimals, 18); } - /*************************************** - Pricing - ****************************************/ - /** - * @notice Returns the total price in 18 digit units for a given asset. - * Never goes above 1, since that is how we price mints. - * @param asset address of the asset - * @return price uint256: unit (USD / ETH) price for 1 unit of the asset, in 18 decimal fixed + * @notice Adds WETH to the withdrawal queue if there is a funding shortfall. + * @dev is called from the Native Staking strategy when validator withdrawals are processed. + * It also called before any WETH is allocated to a strategy. */ - function priceUnitMint(address asset) - external - view - returns (uint256 price) - { - /* need to supply 1 asset unit in asset's decimals and can not just hard-code - * to 1e18 and ignore calling `_toUnits` since we need to consider assets - * with the exchange rate - */ - uint256 units = _toUnits( - uint256(1e18).scaleBy(_getDecimals(asset), 18), - asset - ); - price = (_toUnitPrice(asset, true) * units) / 1e18; + function addWithdrawalQueueLiquidity() external { + _addWithdrawalQueueLiquidity(); } /** - * @notice Returns the total price in 18 digit unit for a given asset. - * Never goes below 1, since that is how we price redeems - * @param asset Address of the asset - * @return price uint256: unit (USD / ETH) price for 1 unit of the asset, in 18 decimal fixed + * @dev Adds backingAsset (eg. WETH or USDC) to the withdrawal queue if there is a funding shortfall. + * This assumes 1 backingAsset equal 1 corresponding OToken. */ - function priceUnitRedeem(address asset) - external - view - returns (uint256 price) + function _addWithdrawalQueueLiquidity() + internal + returns (uint256 addedClaimable) { - /* need to supply 1 asset unit in asset's decimals and can not just hard-code - * to 1e18 and ignore calling `_toUnits` since we need to consider assets - * with the exchange rate - */ - uint256 units = _toUnits( - uint256(1e18).scaleBy(_getDecimals(asset), 18), - asset - ); - price = (_toUnitPrice(asset, false) * units) / 1e18; - } + WithdrawalQueueMetadata memory queue = withdrawalQueueMetadata; - /*************************************** - Utils - ****************************************/ + // Check if the claimable backingAsset is less than the queued amount + uint256 queueShortfall = queue.queued - queue.claimable; - /** - * @dev Convert a quantity of a token into 1e18 fixed decimal "units" - * in the underlying base (USD/ETH) used by the vault. - * Price is not taken into account, only quantity. - * - * Examples of this conversion: - * - * - 1e18 DAI becomes 1e18 units (same decimals) - * - 1e6 USDC becomes 1e18 units (decimal conversion) - * - 1e18 rETH becomes 1.2e18 units (exchange rate conversion) - * - * @param _raw Quantity of asset - * @param _asset Core Asset address - * @return value 1e18 normalized quantity of units - */ - function _toUnits(uint256 _raw, address _asset) - internal - view - returns (uint256) - { - UnitConversion conversion = assets[_asset].unitConversion; - if (conversion == UnitConversion.DECIMALS) { - return _raw.scaleBy(18, _getDecimals(_asset)); - } else if (conversion == UnitConversion.GETEXCHANGERATE) { - uint256 exchangeRate = IGetExchangeRateToken(_asset) - .getExchangeRate(); - return (_raw * exchangeRate) / 1e18; - } else { - revert("Unsupported conversion type"); + // No need to do anything is the withdrawal queue is full funded + if (queueShortfall == 0) { + return 0; } - } - /** - * @dev Returns asset's unit price accounting for different asset types - * and takes into account the context in which that price exists - - * - mint or redeem. - * - * Note: since we are returning the price of the unit and not the one of the - * asset (see comment above how 1 rETH exchanges for 1.2 units) we need - * to make the Oracle price adjustment as well since we are pricing the - * units and not the assets. - * - * The price also snaps to a "full unit price" in case a mint or redeem - * action would be unfavourable to the protocol. - * - */ - function _toUnitPrice(address _asset, bool isMint) - internal - view - returns (uint256 price) - { - UnitConversion conversion = assets[_asset].unitConversion; - price = IOracle(priceProvider).price(_asset); - - if (conversion == UnitConversion.GETEXCHANGERATE) { - uint256 exchangeRate = IGetExchangeRateToken(_asset) - .getExchangeRate(); - price = (price * 1e18) / exchangeRate; - } else if (conversion != UnitConversion.DECIMALS) { - revert("Unsupported conversion type"); - } + uint256 backingAssetBalance = IERC20(backingAsset).balanceOf( + address(this) + ); - /* At this stage the price is already adjusted to the unit - * so the price checks are agnostic to underlying asset being - * pegged to a USD or to an ETH or having a custom exchange rate. - */ - require(price <= MAX_UNIT_PRICE_DRIFT, "Vault: Price exceeds max"); - require(price >= MIN_UNIT_PRICE_DRIFT, "Vault: Price under min"); - - if (isMint) { - /* Never price a normalized unit price for more than one - * unit of OETH/OUSD when minting. - */ - if (price > 1e18) { - price = 1e18; - } - require(price >= MINT_MINIMUM_UNIT_PRICE, "Asset price below peg"); - } else { - /* Never give out more than 1 normalized unit amount of assets - * for one unit of OETH/OUSD when redeeming. - */ - if (price < 1e18) { - price = 1e18; - } + // Of the claimable withdrawal requests, how much is unclaimed? + // That is, the amount of backingAsset that is currently allocated for the withdrawal queue + uint256 allocatedBaseAsset = queue.claimable - queue.claimed; + + // If there is no unallocated backingAsset then there is nothing to add to the queue + if (backingAssetBalance <= allocatedBaseAsset) { + return 0; } + + uint256 unallocatedBaseAsset = backingAssetBalance - allocatedBaseAsset; + // the new claimable amount is the smaller of the queue shortfall or unallocated backingAsset + addedClaimable = queueShortfall < unallocatedBaseAsset + ? queueShortfall + : unallocatedBaseAsset; + uint256 newClaimable = queue.claimable + addedClaimable; + + // Store the new claimable amount back to storage + withdrawalQueueMetadata.claimable = SafeCast.toUint128(newClaimable); + + // emit a WithdrawalClaimable event + emit WithdrawalClaimable(newClaimable, addedClaimable); } /** - * @dev Get the number of decimals of a token asset - * @param _asset Address of the asset - * @return decimals number of decimals + * @dev Calculate how much backingAsset (eg. WETH or USDC) in the vault is not reserved for the withdrawal queue. + * That is, it is available to be redeemed or deposited into a strategy. */ - function _getDecimals(address _asset) + function _backingAssetAvailable() internal view - returns (uint256 decimals) + returns (uint256 backingAssetAvailable) { - decimals = assets[_asset].decimals; - require(decimals > 0, "Decimals not cached"); - } + WithdrawalQueueMetadata memory queue = withdrawalQueueMetadata; - /** - * @notice Return the number of assets supported by the Vault. - */ - function getAssetCount() public view returns (uint256) { - return allAssets.length; + // The amount of backingAsset that is still to be claimed in the withdrawal queue + uint256 outstandingWithdrawals = queue.queued - queue.claimed; + + // The amount of sitting in backingAsset in the vault + uint256 backingAssetBalance = IERC20(backingAsset).balanceOf( + address(this) + ); + // If there is not enough backingAsset in the vault to cover the outstanding withdrawals + if (backingAssetBalance <= outstandingWithdrawals) return 0; + + return backingAssetBalance - outstandingWithdrawals; } + /*************************************** + Utils + ****************************************/ + /** - * @notice Gets the vault configuration of a supported asset. - * @param _asset Address of the token asset + * @notice Return the number of backingAsset supported by the Vault. */ - function getAssetConfig(address _asset) - public - view - returns (Asset memory config) - { - config = assets[_asset]; + function getAssetCount() public view returns (uint256) { + return 1; } /** * @notice Return all vault asset addresses in order */ function getAllAssets() external view returns (address[] memory) { - return allAssets; + address[] memory a = new address[](1); + a[0] = backingAsset; + return a; } /** @@ -849,7 +830,7 @@ contract VaultCore is VaultInitializer { * @return true if supported */ function isSupportedAsset(address _asset) external view returns (bool) { - return assets[_asset].isSupported; + return backingAsset == _asset; } function ADMIN_IMPLEMENTATION() external view returns (address adminImpl) { diff --git a/contracts/contracts/vault/VaultInitializer.sol b/contracts/contracts/vault/VaultInitializer.sol index 692a3d1e83..b7ee58f0c2 100644 --- a/contracts/contracts/vault/VaultInitializer.sol +++ b/contracts/contracts/vault/VaultInitializer.sol @@ -9,19 +9,14 @@ pragma solidity ^0.8.0; import "./VaultStorage.sol"; -contract VaultInitializer is VaultStorage { - function initialize(address _priceProvider, address _oToken) - external - onlyGovernor - initializer - { - require(_priceProvider != address(0), "PriceProvider address is zero"); +abstract contract VaultInitializer is VaultStorage { + constructor(address _backingAsset) VaultStorage(_backingAsset) {} + + function initialize(address _oToken) external onlyGovernor initializer { require(_oToken != address(0), "oToken address is zero"); oUSD = OUSD(_oToken); - priceProvider = _priceProvider; - rebasePaused = false; capitalPaused = true; diff --git a/contracts/contracts/vault/VaultStorage.sol b/contracts/contracts/vault/VaultStorage.sol index 53ef34a936..b46d2b22b5 100644 --- a/contracts/contracts/vault/VaultStorage.sol +++ b/contracts/contracts/vault/VaultStorage.sol @@ -12,17 +12,15 @@ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.s import { Address } from "@openzeppelin/contracts/utils/Address.sol"; import { IStrategy } from "../interfaces/IStrategy.sol"; +import { IWETH9 } from "../interfaces/IWETH9.sol"; import { Governable } from "../governance/Governable.sol"; import { OUSD } from "../token/OUSD.sol"; import { Initializable } from "../utils/Initializable.sol"; import "../utils/Helpers.sol"; -contract VaultStorage is Initializable, Governable { +abstract contract VaultStorage is Initializable, Governable { using SafeERC20 for IERC20; - event AssetSupported(address _asset); - event AssetRemoved(address _asset); - event AssetDefaultStrategyUpdated(address _asset, address _strategy); event AssetAllocated(address _asset, address _strategy, uint256 _amount); event StrategyApproved(address _addr); event StrategyRemoved(address _addr); @@ -30,12 +28,11 @@ contract VaultStorage is Initializable, Governable { event Redeem(address _addr, uint256 _value); event CapitalPaused(); event CapitalUnpaused(); + event DefaultStrategyUpdated(address _strategy); event RebasePaused(); event RebaseUnpaused(); event VaultBufferUpdated(uint256 _vaultBuffer); - event OusdMetaStrategyUpdated(address _ousdMetaStrategy); event RedeemFeeUpdated(uint256 _redeemFeeBps); - event PriceProviderUpdated(address _priceProvider); event AllocateThresholdUpdated(uint256 _threshold); event RebaseThresholdUpdated(uint256 _threshold); event StrategistUpdated(address _address); @@ -43,16 +40,6 @@ contract VaultStorage is Initializable, Governable { event YieldDistribution(address _to, uint256 _yield, uint256 _fee); event TrusteeFeeBpsChanged(uint256 _basis); event TrusteeAddressChanged(address _address); - event NetOusdMintForStrategyThresholdChanged(uint256 _threshold); - event SwapperChanged(address _address); - event SwapAllowedUndervalueChanged(uint256 _basis); - event SwapSlippageChanged(address _asset, uint256 _basis); - event Swapped( - address indexed _fromAsset, - address indexed _toAsset, - uint256 _fromAssetAmount, - uint256 _toAssetAmount - ); event StrategyAddedToMintWhitelist(address indexed strategy); event StrategyRemovedFromMintWhitelist(address indexed strategy); event RebasePerSecondMaxChanged(uint256 rebaseRatePerSecond); @@ -95,9 +82,9 @@ contract VaultStorage is Initializable, Governable { } /// @dev mapping of supported vault assets to their configuration - mapping(address => Asset) internal assets; + mapping(address => Asset) internal _deprecated_assets; /// @dev list of all assets supported by the vault. - address[] internal allAssets; + address[] internal _deprecated_allAssets; // Strategies approved for use by the Vault struct Strategy { @@ -110,7 +97,7 @@ contract VaultStorage is Initializable, Governable { address[] internal allStrategies; /// @notice Address of the Oracle price provider contract - address public priceProvider; + address internal _deprecated_priceProvider; /// @notice pause rebasing if true bool public rebasePaused; /// @notice pause operations that change the OToken supply. @@ -144,7 +131,7 @@ contract VaultStorage is Initializable, Governable { /// @notice Mapping of asset address to the Strategy that they should automatically // be allocated to - mapping(address => address) public assetDefaultStrategies; + mapping(address => address) public _deprecated_assetDefaultStrategies; /// @notice Max difference between total supply and total value of assets. 18 decimals. uint256 public maxSupplyDiff; @@ -162,13 +149,13 @@ contract VaultStorage is Initializable, Governable { /// @notice Metapool strategy that is allowed to mint/burn OTokens without changing collateral - address public ousdMetaStrategy; + address public _deprecated_ousdMetaStrategy; /// @notice How much OTokens are currently minted by the strategy - int256 public netOusdMintedForStrategy; + int256 public _deprecated_netOusdMintedForStrategy; /// @notice How much net total OTokens are allowed to be minted by all strategies - uint256 public netOusdMintForStrategyThreshold; + uint256 public _deprecated_netOusdMintForStrategyThreshold; uint256 constant MIN_UNIT_PRICE_DRIFT = 0.7e18; uint256 constant MAX_UNIT_PRICE_DRIFT = 1.3e18; @@ -182,7 +169,8 @@ contract VaultStorage is Initializable, Governable { // For example 100 == 1% uint16 allowedUndervalueBps; } - SwapConfig internal swapConfig = SwapConfig(address(0), 0); + + SwapConfig internal _deprecated_swapConfig = SwapConfig(address(0), 0); // List of strategies that can mint oTokens directly // Used in OETHBaseVaultCore @@ -249,12 +237,26 @@ contract VaultStorage is Initializable, Governable { uint256 internal constant MAX_REBASE_PER_SECOND = uint256(0.05 ether) / 1 days; + /// @notice Default strategy for backingAsset + address public defaultStrategy; + // For future use - uint256[43] private __gap; + uint256[42] private __gap; + + /// @dev Address of the backing asset (eg. WETH or USDC) + address public immutable backingAsset; + uint8 internal immutable backingAssetDecimals; // slither-disable-end constable-states // slither-disable-end uninitialized-state + constructor(address _backingAsset) { + uint8 _decimals = IWETH9(_backingAsset).decimals(); + require(_decimals <= 18, "BackingAsset not supported"); + backingAsset = _backingAsset; + backingAssetDecimals = _decimals; + } + /** * @notice set the implementation for the admin, this needs to be in a base class else we cannot set it * @param newImpl address of the implementation diff --git a/contracts/deploy/base/000_mock.js b/contracts/deploy/base/000_mock.js index d06adc7ab5..53a2df1bad 100644 --- a/contracts/deploy/base/000_mock.js +++ b/contracts/deploy/base/000_mock.js @@ -83,7 +83,6 @@ const deployCore = async () => { const dwOETHb = await deployWithConfirmation("WOETHBase", [ cOETHbProxy.address, // Base token ]); - const dOETHbVault = await deployWithConfirmation("OETHVault"); const dOETHbVaultCore = await deployWithConfirmation("OETHBaseVaultCore", [ cWETH.address, ]); @@ -98,7 +97,6 @@ const deployCore = async () => { "IVault", cOETHbVaultProxy.address ); - const cOracleRouter = await ethers.getContract("MockOracleRouter"); // Init OETHb const resolution = ethers.utils.parseUnits("1", 27); @@ -119,16 +117,15 @@ const deployCore = async () => { // Init OETHbVault const initDataOETHbVault = cOETHbVault.interface.encodeFunctionData( - "initialize(address,address)", + "initialize(address)", [ - cOracleRouter.address, // OracleRouter cOETHbProxy.address, // OETHb ] ); // prettier-ignore await cOETHbVaultProxy .connect(sDeployer)["initialize(address,address,bytes)"]( - dOETHbVault.address, + dOETHbVaultCore.address, governorAddr, initDataOETHbVault ); @@ -149,7 +146,6 @@ const deployCore = async () => { await cOETHbVaultProxy.connect(sGovernor).upgradeTo(dOETHbVaultCore.address); await cOETHbVault.connect(sGovernor).setAdminImpl(dOETHbVaultAdmin.address); - await cOETHbVault.connect(sGovernor).supportAsset(cWETH.address, 0); await cOETHbVault.connect(sGovernor).unpauseCapital(); }; @@ -171,11 +167,13 @@ const deployBridgedWOETHStrategy = async () => { await deployWithConfirmation("BridgedWOETHStrategyProxy"); const cStrategyProxy = await ethers.getContract("BridgedWOETHStrategyProxy"); + const cOracleRouter = await ethers.getContract("MockOracleRouter"); const dStrategyImpl = await deployWithConfirmation("BridgedWOETHStrategy", [ [addresses.zero, cOETHbVaultProxy.address], cWETH.address, cWOETHProxy.address, cOETHbProxy.address, + cOracleRouter.address, ]); const cStrategy = await ethers.getContractAt( "BridgedWOETHStrategy", diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 94692f1a98..fc0731f17b 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -283,7 +283,6 @@ const deployConvexOUSDMetaStrategy = async () => { * Configure Vault by adding supported assets and Strategies. */ const configureVault = async () => { - const assetAddresses = await getAssetAddresses(deployments); const { governorAddr, strategistAddr } = await getNamedAccounts(); // Signers const sGovernor = await ethers.provider.getSigner(governorAddr); @@ -294,19 +293,6 @@ const configureVault = async () => { await ethers.getContract("VaultProxy") ).address ); - // Set up supported assets for Vault - await withConfirmation( - cVault.connect(sGovernor).supportAsset(assetAddresses.USDS, 0) - ); - log("Added USDS asset to Vault"); - await withConfirmation( - cVault.connect(sGovernor).supportAsset(assetAddresses.USDT, 0) - ); - log("Added USDT asset to Vault"); - await withConfirmation( - cVault.connect(sGovernor).supportAsset(assetAddresses.USDC, 0) - ); - log("Added USDC asset to Vault"); // Unpause deposits await withConfirmation(cVault.connect(sGovernor).unpauseCapital()); log("Unpaused deposits on Vault"); @@ -319,8 +305,7 @@ const configureVault = async () => { /** * Configure OETH Vault by adding supported assets and Strategies. */ -const configureOETHVault = async (isSimpleOETH) => { - const assetAddresses = await getAssetAddresses(deployments); +const configureOETHVault = async () => { let { governorAddr, deployerAddr, strategistAddr } = await getNamedAccounts(); // Signers let sGovernor = await ethers.provider.getSigner(governorAddr); @@ -338,14 +323,6 @@ const configureOETHVault = async (isSimpleOETH) => { sGovernor = sDeployer; } - // Set up supported assets for Vault - const { WETH, RETH, stETH, frxETH } = assetAddresses; - const assets = isSimpleOETH ? [WETH] : [WETH, RETH, stETH, frxETH]; - for (const asset of assets) { - await withConfirmation(cVault.connect(sGovernor).supportAsset(asset, 0)); - } - log("Added assets to OETH Vault"); - // Unpause deposits await withConfirmation(cVault.connect(sGovernor).unpauseCapital()); log("Unpaused deposits on OETH Vault"); @@ -354,9 +331,6 @@ const configureOETHVault = async (isSimpleOETH) => { cVault.connect(sGovernor).setStrategistAddr(strategistAddr) ); - // Cache WETH asset address - await withConfirmation(cVault.connect(sGovernor).cacheWETHAssetIndex()); - // Redeem fee to 0 await withConfirmation(cVault.connect(sGovernor).setRedeemFeeBps(0)); @@ -508,29 +482,47 @@ const configureStrategies = async (harvesterProxy, oethHarvesterProxy) => { // Signers const sGovernor = await ethers.provider.getSigner(governorAddr); - const compoundProxy = await ethers.getContract("CompoundStrategyProxy"); - const compound = await ethers.getContractAt( - "CompoundStrategy", - compoundProxy.address - ); - await withConfirmation( - compound.connect(sGovernor).setHarvesterAddress(harvesterProxy.address) - ); + // Configure Compound Strategy if deployed + const compoundDeployment = await hre.deployments + .get("CompoundStrategyProxy") + .catch(() => null); + if (compoundDeployment) { + const compound = await ethers.getContractAt( + "CompoundStrategy", + compoundDeployment.address + ); + await withConfirmation( + compound.connect(sGovernor).setHarvesterAddress(harvesterProxy.address) + ); + } - const aaveProxy = await ethers.getContract("AaveStrategyProxy"); - const aave = await ethers.getContractAt("AaveStrategy", aaveProxy.address); - await withConfirmation( - aave.connect(sGovernor).setHarvesterAddress(harvesterProxy.address) - ); + // Configure Aave Strategy if deployed + const aaveDeployment = await hre.deployments + .get("AaveStrategyProxy") + .catch(() => null); + if (aaveDeployment) { + const aave = await ethers.getContractAt( + "AaveStrategy", + aaveDeployment.address + ); + await withConfirmation( + aave.connect(sGovernor).setHarvesterAddress(harvesterProxy.address) + ); + } - const convexProxy = await ethers.getContract("ConvexStrategyProxy"); - const convex = await ethers.getContractAt( - "ConvexStrategy", - convexProxy.address - ); - await withConfirmation( - convex.connect(sGovernor).setHarvesterAddress(harvesterProxy.address) - ); + // Configure Convex Strategy if deployed + const convexDeployment = await hre.deployments + .get("ConvexStrategyProxy") + .catch(() => null); + if (convexDeployment) { + const convex = await ethers.getContractAt( + "ConvexStrategy", + convexDeployment.address + ); + await withConfirmation( + convex.connect(sGovernor).setHarvesterAddress(harvesterProxy.address) + ); + } const nativeStakingSSVStrategyProxy = await ethers.getContract( "NativeStakingSSVStrategyProxy" @@ -1080,7 +1072,6 @@ const deployOETHCore = async () => { // Main contracts const dOETH = await deployWithConfirmation("OETH"); - const dOETHVault = await deployWithConfirmation("OETHVault"); const dOETHVaultCore = await deployWithConfirmation("OETHVaultCore", [ assetAddresses.WETH, ]); @@ -1092,10 +1083,6 @@ const deployOETHCore = async () => { const cOETHProxy = await ethers.getContract("OETHProxy"); const cOETHVaultProxy = await ethers.getContract("OETHVaultProxy"); const cOETH = await ethers.getContractAt("OETH", cOETHProxy.address); - - const oracleRouterContractName = - isMainnet || isHoodiOrFork ? "OETHOracleRouter" : "OracleRouter"; - const cOETHOracleRouter = await ethers.getContract(oracleRouterContractName); const cOETHVault = await ethers.getContractAt( "IVault", cOETHVaultProxy.address @@ -1114,7 +1101,7 @@ const deployOETHCore = async () => { // prettier-ignore await withConfirmation( cOETHVaultProxy.connect(sDeployer)["initialize(address,address,bytes)"]( - dOETHVault.address, + dOETHVaultCore.address, governorAddr, [], await getTxOpts() @@ -1125,11 +1112,7 @@ const deployOETHCore = async () => { await withConfirmation( cOETHVault .connect(sGovernor) - .initialize( - cOETHOracleRouter.address, - cOETHProxy.address, - await getTxOpts() - ) + .initialize(cOETHProxy.address, await getTxOpts()) ); log("Initialized OETHVault"); @@ -1167,16 +1150,19 @@ const deployOETHCore = async () => { }; const deployOUSDCore = async () => { - const { governorAddr } = await hre.getNamedAccounts(); + const { governorAddr, deployerAddr } = await hre.getNamedAccounts(); + const assetAddresses = await getAssetAddresses(deployments); log(`Using asset addresses: ${JSON.stringify(assetAddresses, null, 2)}`); // Signers const sGovernor = await ethers.provider.getSigner(governorAddr); + const sDeployer = await ethers.provider.getSigner(deployerAddr); // Proxies await deployWithConfirmation("OUSDProxy"); await deployWithConfirmation("VaultProxy"); + log("Deployed OUSD Token and OUSD Vault proxies"); // Main contracts let dOUSD; @@ -1185,17 +1171,23 @@ const deployOUSDCore = async () => { } else { dOUSD = await deployWithConfirmation("OUSD"); } - const dVault = await deployWithConfirmation("Vault"); - const dVaultCore = await deployWithConfirmation("VaultCore"); - const dVaultAdmin = await deployWithConfirmation("VaultAdmin"); + + // Deploy Vault implementations + const dVaultCore = await deployWithConfirmation("OUSDVaultCore", [ + assetAddresses.USDC, + ]); + const dVaultAdmin = await deployWithConfirmation("OUSDVaultAdmin", [ + assetAddresses.USDC, + ]); + log("Deployed OUSD Vault implementations (Core, Admin)"); // Get contract instances const cOUSDProxy = await ethers.getContract("OUSDProxy"); const cVaultProxy = await ethers.getContract("VaultProxy"); const cOUSD = await ethers.getContractAt("OUSD", cOUSDProxy.address); - const cOracleRouter = await ethers.getContract("OracleRouter"); const cVault = await ethers.getContractAt("IVault", cVaultProxy.address); + // Initialize OUSD Token Proxy await withConfirmation( cOUSDProxy["initialize(address,address,bytes)"]( dOUSD.address, @@ -1203,34 +1195,31 @@ const deployOUSDCore = async () => { [] ) ); - log("Initialized OUSDProxy"); + log("Initialized OUSD Token Proxy"); - // Need to call the initializer on the Vault then upgraded it to the actual - // VaultCore implementation + // Initialize OUSD Vault Proxy with Vault Core implementation + // prettier-ignore await withConfirmation( - cVaultProxy["initialize(address,address,bytes)"]( - dVault.address, + cVaultProxy.connect(sDeployer)["initialize(address,address,bytes)"]( + dVaultCore.address, governorAddr, - [] + [], + await getTxOpts() ) ); + log("Initialized OUSD Vault Proxy"); + // Initialize OUSD Vault Core await withConfirmation( - cVault - .connect(sGovernor) - .initialize(cOracleRouter.address, cOUSDProxy.address) - ); - log("Initialized Vault"); - - await withConfirmation( - cVaultProxy.connect(sGovernor).upgradeTo(dVaultCore.address) + cVault.connect(sGovernor).initialize(cOUSDProxy.address) ); - log("Upgraded VaultCore implementation"); + log("Initialized OUSD Vault Core"); + // Set Vault implementation await withConfirmation( cVault.connect(sGovernor).setAdminImpl(dVaultAdmin.address) ); - log("Initialized VaultAdmin implementation"); + log("Initialized OUSD VaultAdmin implementation"); // Initialize OUSD /* Set the original resolution to 27 decimals. We used to have it set to 18 @@ -1252,7 +1241,7 @@ const deployOUSDCore = async () => { await withConfirmation( cOUSD.connect(sGovernor).initialize(cVaultProxy.address, resolution) ); - log("Initialized OUSD"); + log("Initialized OUSD Token"); await withConfirmation( cVault @@ -1486,17 +1475,11 @@ const deployWOeth = async () => { }; const deployOETHSwapper = async () => { - const { deployerAddr, governorAddr } = await getNamedAccounts(); + const { deployerAddr } = await getNamedAccounts(); const sDeployer = await ethers.provider.getSigner(deployerAddr); - const sGovernor = await ethers.provider.getSigner(governorAddr); const assetAddresses = await getAssetAddresses(deployments); - const vaultProxy = await ethers.getContract("OETHVaultProxy"); - const vault = await ethers.getContractAt("IVault", vaultProxy.address); - - const mockSwapper = await ethers.getContract("MockSwapper"); - await deployWithConfirmation("Swapper1InchV5"); const cSwapper = await ethers.getContract("Swapper1InchV5"); @@ -1508,27 +1491,13 @@ const deployOETHSwapper = async () => { assetAddresses.WETH, assetAddresses.frxETH, ]); - - await vault.connect(sGovernor).setSwapper(mockSwapper.address); - await vault.connect(sGovernor).setSwapAllowedUndervalue(100); - - await vault.connect(sGovernor).setOracleSlippage(assetAddresses.RETH, 200); - await vault.connect(sGovernor).setOracleSlippage(assetAddresses.stETH, 70); - await vault.connect(sGovernor).setOracleSlippage(assetAddresses.WETH, 20); - await vault.connect(sGovernor).setOracleSlippage(assetAddresses.frxETH, 20); }; const deployOUSDSwapper = async () => { - const { deployerAddr, governorAddr } = await getNamedAccounts(); + const { deployerAddr } = await getNamedAccounts(); const sDeployer = await ethers.provider.getSigner(deployerAddr); - const sGovernor = await ethers.provider.getSigner(governorAddr); const assetAddresses = await getAssetAddresses(deployments); - - const vaultProxy = await ethers.getContract("VaultProxy"); - const vault = await ethers.getContractAt("IVault", vaultProxy.address); - - const mockSwapper = await ethers.getContract("MockSwapper"); // Assumes deployOETHSwapper has already been run const cSwapper = await ethers.getContract("Swapper1InchV5"); @@ -1539,13 +1508,6 @@ const deployOUSDSwapper = async () => { assetAddresses.USDC, assetAddresses.USDT, ]); - - await vault.connect(sGovernor).setSwapper(mockSwapper.address); - await vault.connect(sGovernor).setSwapAllowedUndervalue(100); - - await vault.connect(sGovernor).setOracleSlippage(assetAddresses.USDS, 50); - await vault.connect(sGovernor).setOracleSlippage(assetAddresses.USDC, 50); - await vault.connect(sGovernor).setOracleSlippage(assetAddresses.USDT, 50); }; const deployBaseAerodromeAMOStrategyImplementation = async () => { diff --git a/contracts/deploy/mainnet/000_mock.js b/contracts/deploy/mainnet/000_mock.js index f2a598e705..e1eb11be80 100644 --- a/contracts/deploy/mainnet/000_mock.js +++ b/contracts/deploy/mainnet/000_mock.js @@ -133,6 +133,7 @@ const deployMocks = async ({ getNamedAccounts, deployments }) => { // Deploy a mock Vault with additional functions for tests await deploy("MockVault", { + args: [(await ethers.getContract("MockUSDC")).address], from: governorAddr, }); diff --git a/contracts/deploy/mainnet/001_core.js b/contracts/deploy/mainnet/001_core.js index 024146b8b9..cd2cec69e5 100644 --- a/contracts/deploy/mainnet/001_core.js +++ b/contracts/deploy/mainnet/001_core.js @@ -39,7 +39,7 @@ const main = async () => { oethDripper ); await configureVault(); - await configureOETHVault(false); + await configureOETHVault(); await configureStrategies(harvesterProxy, oethHarvesterProxy); await deployBuyback(); await deployUniswapV3Pool(); diff --git a/contracts/deploy/mainnet/159_ousd_vault_upgrade.js b/contracts/deploy/mainnet/159_ousd_vault_upgrade.js new file mode 100644 index 0000000000..d347cdc877 --- /dev/null +++ b/contracts/deploy/mainnet/159_ousd_vault_upgrade.js @@ -0,0 +1,64 @@ +const addresses = require("../../utils/addresses"); +const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); + +module.exports = deploymentWithGovernanceProposal( + { + deployName: "159_ousd_vault_upgrade", + forceDeploy: false, + //forceSkip: true, + reduceQueueTime: true, + deployerIsProposer: false, + proposalId: "", + }, + async ({ deployWithConfirmation }) => { + // Deployer Actions + // ---------------- + + // 1. Deploy new OUSD Vault Core and Admin implementations + const dVaultCore = await deployWithConfirmation("OUSDVaultCore", [ + addresses.mainnet.USDC, + ]); + const dVaultAdmin = await deployWithConfirmation("OUSDVaultAdmin", [ + addresses.mainnet.USDC, + ]); + + // 2. Connect to the OUSD Vault as its governor via the proxy + const cVaultProxy = await ethers.getContract("VaultProxy"); + const cVault = await ethers.getContractAt("IVault", cVaultProxy.address); + + // 3. Connect to the OUSD/USDC AMO + const cOUSDAMO = await ethers.getContract("OUSDCurveAMOProxy"); + + // Governance Actions + // ---------------- + return { + name: "Upgrade OUSD Vault to new Core and Admin implementations", + actions: [ + // 1. Upgrade the OUSD Vault proxy to the new core vault implementation + { + contract: cVaultProxy, + signature: "upgradeTo(address)", + args: [dVaultCore.address], + }, + // 2. Set OUSD Vault proxy to the new admin vault implementation + { + contract: cVault, + signature: "setAdminImpl(address)", + args: [dVaultAdmin.address], + }, + // 3. Add OUSD/USDC AMO to mint whitelist + { + contract: cVault, + signature: "addStrategyToMintWhitelist(address)", + args: [cOUSDAMO.address], + }, + // 4. Set OUSD/USDC AMO as default strategy + { + contract: cVault, + signature: "setDefaultStrategy(address)", + args: [cOUSDAMO.address], + }, + ], + }; + } +); diff --git a/contracts/deploy/sonic/000_mock.js b/contracts/deploy/sonic/000_mock.js index 72f53dde49..3abd7cb04e 100644 --- a/contracts/deploy/sonic/000_mock.js +++ b/contracts/deploy/sonic/000_mock.js @@ -61,7 +61,6 @@ const deployCore = async () => { "IVault", cOSonicVaultProxy.address ); - const cOracleRouter = await ethers.getContract("MockOracleRouter"); // Init OSonic const resolution = ethers.utils.parseUnits("1", 27); @@ -82,9 +81,8 @@ const deployCore = async () => { // Init OSonicVault const initDataOSonicVault = cOSonicVault.interface.encodeFunctionData( - "initialize(address,address)", + "initialize(address)", [ - cOracleRouter.address, // OracleRouter cOSonicProxy.address, // OSonic ] ); @@ -114,7 +112,6 @@ const deployCore = async () => { .upgradeTo(dOSonicVaultCore.address); await cOSonicVault.connect(sGovernor).setAdminImpl(dOSonicVaultAdmin.address); - await cOSonicVault.connect(sGovernor).supportAsset(cWS.address, 0); await cOSonicVault.connect(sGovernor).unpauseCapital(); // Set withdrawal claim delay to 1 day await cOSonicVault.connect(sGovernor).setWithdrawalClaimDelay(86400); diff --git a/contracts/scripts/governor/propose.js b/contracts/scripts/governor/propose.js index 6be3eb9c38..66228cf454 100644 --- a/contracts/scripts/governor/propose.js +++ b/contracts/scripts/governor/propose.js @@ -366,11 +366,11 @@ async function proposeUpgradeVaultCoreArgs(config) { async function proposeUpgradeVaultCoreAndAdminArgs() { const cVaultProxy = await ethers.getContract("VaultProxy"); const cVaultCoreProxy = await ethers.getContractAt( - "VaultCore", + "OUSDVaultCore", cVaultProxy.address ); - const cVaultCore = await ethers.getContract("VaultCore"); - const cVaultAdmin = await ethers.getContract("VaultAdmin"); + const cVaultCore = await ethers.getContract("OUSDVaultCore"); + const cVaultAdmin = await ethers.getContract("OUSDVaultAdmin"); const args = await proposeArgs([ { @@ -663,12 +663,11 @@ async function proposeProp14Args() { async function proposeProp17Args() { const cVaultProxy = await ethers.getContract("VaultProxy"); const cVaultCoreProxy = await ethers.getContractAt( - "VaultCore", + "OUSDVaultCore", cVaultProxy.address ); - const cVaultCore = await ethers.getContract("VaultCore"); - const cVaultAdmin = await ethers.getContract("VaultAdmin"); - + const cVaultCore = await ethers.getContract("OUSDVaultCore"); + const cVaultAdmin = await ethers.getContract("OUSDVaultAdmin"); const cAaveStrategyProxy = await ethers.getContract("AaveStrategyProxy"); const cAaveStrategy = await ethers.getContractAt( "AaveStrategy", diff --git a/contracts/test/_fixture-sonic.js b/contracts/test/_fixture-sonic.js index 2dfef95c27..0d320dc748 100644 --- a/contracts/test/_fixture-sonic.js +++ b/contracts/test/_fixture-sonic.js @@ -237,8 +237,7 @@ async function swapXAMOFixture( ) { const fixture = await defaultSonicFixture(); - const { oSonic, oSonicVault, rafael, nick, strategist, timelock, wS } = - fixture; + const { oSonic, oSonicVault, rafael, nick, strategist, wS } = fixture; let swapXAMOStrategy, swapXPool, swapXGauge, swpx; @@ -264,10 +263,6 @@ async function swapXAMOFixture( swpx = await resolveAsset("SWPx"); } - await oSonicVault - .connect(timelock) - .setAssetDefaultStrategy(wS.address, addresses.zero); - // mint some OS using wS if configured if (config?.wsMintAmount > 0) { const wsAmount = parseUnits(config.wsMintAmount.toString()); @@ -279,7 +274,7 @@ async function swapXAMOFixture( const wsBalance = await wS.balanceOf(oSonicVault.address); const queue = await oSonicVault.withdrawalQueueMetadata(); const available = wsBalance.add(queue.claimed).sub(queue.queued); - const mintAmount = wsAmount.sub(available); + const mintAmount = wsAmount.sub(available).mul(10); if (mintAmount.gt(0)) { // Approve the Vault to transfer wS diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 86cd1ecc8e..8ed3a966e7 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -20,7 +20,7 @@ const { deployWithConfirmation } = require("../utils/deploy"); const { replaceContractAt } = require("../utils/hardhat"); const { getAssetAddresses, - usdsUnits, + usdcUnits, getOracleAddresses, oethUnits, ousdUnits, @@ -274,9 +274,9 @@ const createAccountTypes = async ({ vault, ousd, ousdUnlocked, deploy }) => { if (!isFork) { await fundAccounts(); - const usds = await ethers.getContract("MockUSDS"); - await usds.connect(matt).approve(vault.address, usdsUnits("1000")); - await vault.connect(matt).mint(usds.address, usdsUnits("1000"), 0); + const usdc = await ethers.getContract("MockUSDC"); + await usdc.connect(matt).approve(vault.address, usdcUnits("1000")); + await vault.connect(matt).mint(usdc.address, usdcUnits("1000"), 0); } const createAccount = async () => { @@ -450,6 +450,8 @@ const createAccountTypes = async ({ vault, ousd, ousdUnlocked, deploy }) => { rebase_delegate_target_1.address ); + // Allow matt to burn OUSD + await vault.connect(governor).setStrategistAddr(matt.address); // matt burn remaining OUSD await vault .connect(matt) @@ -527,12 +529,14 @@ const loadTokenTransferFixture = deployments.createFixture(async () => { let governor = signers[1]; let strategist = signers[0]; + log("Creating account types..."); const accountTypes = await createAccountTypes({ ousd: vaultAndTokenConracts.ousd, ousdUnlocked: vaultAndTokenConracts.ousdUnlocked, vault: vaultAndTokenConracts.vault, deploy: deployments.deploy, }); + log("Account types created."); return { ...vaultAndTokenConracts, @@ -1002,15 +1006,8 @@ const defaultFixture = deployments.createFixture(async () => { } if (!isFork) { - const assetAddresses = await getAssetAddresses(deployments); - const sGovernor = await ethers.provider.getSigner(governorAddr); - // Add TUSD in fixture, it is disabled by default in deployment - await vaultAndTokenConracts.vault - .connect(sGovernor) - .supportAsset(assetAddresses.TUSD, 0); - // Enable capital movement await vaultAndTokenConracts.vault.connect(sGovernor).unpauseCapital(); } @@ -1044,12 +1041,12 @@ const defaultFixture = deployments.createFixture(async () => { // Matt and Josh each have $100 OUSD & 100 OETH for (const user of [matt, josh]) { - await usds + await usdc .connect(user) - .approve(vaultAndTokenConracts.vault.address, usdsUnits("100")); + .approve(vaultAndTokenConracts.vault.address, usdcUnits("100")); await vaultAndTokenConracts.vault .connect(user) - .mint(usds.address, usdsUnits("100"), 0); + .mint(usdc.address, usdcUnits("100"), 0); // Fund WETH contract await hardhatSetBalance(user.address, "50000"); @@ -1163,6 +1160,7 @@ const defaultFixture = deployments.createFixture(async () => { oethMorphoAaveStrategy, convexEthMetaStrategy, oethDripper, + oethFixedRateDripperProxy, oethHarvester, oethZapper, swapper, @@ -1450,29 +1448,10 @@ async function compoundVaultFixture() { await fixture.compoundStrategy .connect(sGovernor) .setPTokenAddress(assetAddresses.USDT, assetAddresses.cUSDT); - await fixture.vault - .connect(sGovernor) - .setAssetDefaultStrategy( - fixture.usdt.address, - fixture.compoundStrategy.address - ); // Add USDC await fixture.compoundStrategy .connect(sGovernor) .setPTokenAddress(assetAddresses.USDC, assetAddresses.cUSDC); - await fixture.vault - .connect(sGovernor) - .setAssetDefaultStrategy( - fixture.usdc.address, - fixture.compoundStrategy.address - ); - // Add allocation mapping for USDS - await fixture.vault - .connect(sGovernor) - .setAssetDefaultStrategy( - fixture.usds.address, - fixture.compoundStrategy.address - ); return fixture; } @@ -1496,16 +1475,7 @@ async function convexVaultFixture() { await fixture.vault .connect(sGovernor) - .setAssetDefaultStrategy( - fixture.usdt.address, - fixture.convexStrategy.address - ); - await fixture.vault - .connect(sGovernor) - .setAssetDefaultStrategy( - fixture.usdc.address, - fixture.convexStrategy.address - ); + .setDefaultStrategy(fixture.convexStrategy.address); return fixture; } @@ -1524,10 +1494,10 @@ async function balancerREthFixture(config = { defaultStrategy: true }) { if (config.defaultStrategy) { await oethVault .connect(timelock) - .setAssetDefaultStrategy(reth.address, balancerREthStrategy.address); + .setDefaultStrategy(reth.address, balancerREthStrategy.address); await oethVault .connect(timelock) - .setAssetDefaultStrategy(weth.address, balancerREthStrategy.address); + .setDefaultStrategy(weth.address, balancerREthStrategy.address); } fixture.rEthBPT = await ethers.getContractAt( @@ -1863,24 +1833,15 @@ async function morphoCompoundFixture() { if (isFork) { await fixture.vault .connect(timelock) - .setAssetDefaultStrategy( - fixture.usdt.address, - fixture.morphoCompoundStrategy.address - ); + .setDefaultStrategy(fixture.morphoCompoundStrategy.address); await fixture.vault .connect(timelock) - .setAssetDefaultStrategy( - fixture.usdc.address, - fixture.morphoCompoundStrategy.address - ); + .setDefaultStrategy(fixture.morphoCompoundStrategy.address); await fixture.vault .connect(timelock) - .setAssetDefaultStrategy( - fixture.dai.address, - fixture.morphoCompoundStrategy.address - ); + .setDefaultStrategy(fixture.morphoCompoundStrategy.address); } else { throw new Error( "Morpho strategy only supported in forked test environment" @@ -1901,10 +1862,7 @@ async function aaveFixture() { if (isFork) { await fixture.vault .connect(timelock) - .setAssetDefaultStrategy( - fixture.usdt.address, - fixture.aaveStrategy.address - ); + .setDefaultStrategy(fixture.aaveStrategy.address); } else { throw new Error( "Aave strategy supported for USDT in forked test environment" @@ -1924,19 +1882,12 @@ async function morphoAaveFixture() { if (isFork) { // The supply of DAI and USDT has been paused for Morpho Aave V2 so no default strategy - await fixture.vault - .connect(timelock) - .setAssetDefaultStrategy(fixture.dai.address, addresses.zero); - await fixture.vault - .connect(timelock) - .setAssetDefaultStrategy(fixture.usdt.address, addresses.zero); + await fixture.vault.connect(timelock).setDefaultStrategy(addresses.zero); + await fixture.vault.connect(timelock).setDefaultStrategy(addresses.zero); await fixture.vault .connect(timelock) - .setAssetDefaultStrategy( - fixture.usdc.address, - fixture.morphoAaveStrategy.address - ); + .setDefaultStrategy(fixture.morphoAaveStrategy.address); } else { throw new Error( "Morpho strategy only supported in forked test environment" @@ -1953,11 +1904,11 @@ async function oethMorphoAaveFixture() { const fixture = await oethDefaultFixture(); if (isFork) { - const { oethVault, timelock, weth, oethMorphoAaveStrategy } = fixture; + const { oethVault, timelock, oethMorphoAaveStrategy } = fixture; await oethVault .connect(timelock) - .setAssetDefaultStrategy(weth.address, oethMorphoAaveStrategy.address); + .setDefaultStrategy(oethMorphoAaveStrategy.address); } else { throw new Error( "Morpho strategy only supported in forked test environment" @@ -1999,7 +1950,7 @@ async function nativeStakingSSVStrategyFixture() { } else { fixture.ssvNetwork = await ethers.getContract("MockSSVNetwork"); const { governorAddr } = await getNamedAccounts(); - const { oethVault, weth, nativeStakingSSVStrategy } = fixture; + const { oethVault, nativeStakingSSVStrategy } = fixture; const sGovernor = await ethers.provider.getSigner(governorAddr); // Approve Strategy @@ -2013,7 +1964,7 @@ async function nativeStakingSSVStrategyFixture() { // Set as default await oethVault .connect(sGovernor) - .setAssetDefaultStrategy(weth.address, nativeStakingSSVStrategy.address); + .setDefaultStrategy(nativeStakingSSVStrategy.address); await nativeStakingSSVStrategy .connect(sGovernor) @@ -2086,7 +2037,7 @@ async function compoundingStakingSSVStrategyFixture() { } else { fixture.ssvNetwork = await ethers.getContract("MockSSVNetwork"); const { governorAddr, registratorAddr } = await getNamedAccounts(); - const { oethVault, weth } = fixture; + const { oethVault } = fixture; const sGovernor = await ethers.provider.getSigner(governorAddr); const sRegistrator = await ethers.provider.getSigner(registratorAddr); @@ -2098,10 +2049,7 @@ async function compoundingStakingSSVStrategyFixture() { // Set as default await oethVault .connect(sGovernor) - .setAssetDefaultStrategy( - weth.address, - compoundingStakingSSVStrategy.address - ); + .setDefaultStrategy(compoundingStakingSSVStrategy.address); await compoundingStakingSSVStrategy .connect(sGovernor) @@ -2209,17 +2157,11 @@ async function convexGeneralizedMetaForkedFixture( await fixture.vault .connect(sGovernor) - .setAssetDefaultStrategy( - fixture.usdt.address, - fixture.metaStrategy.address - ); + .setDefaultStrategy(fixture.metaStrategy.address); await fixture.vault .connect(sGovernor) - .setAssetDefaultStrategy( - fixture.usdc.address, - fixture.metaStrategy.address - ); + .setDefaultStrategy(fixture.metaStrategy.address); return fixture; } @@ -2285,9 +2227,7 @@ async function convexOETHMetaVaultFixture( .connect(timelock) .setNetOusdMintForStrategyThreshold(parseUnits("500", 21)); - await oethVault - .connect(timelock) - .setAssetDefaultStrategy(weth.address, addresses.zero); + await oethVault.connect(timelock).setDefaultStrategy(addresses.zero); // Impersonate the OETH Vault fixture.oethVaultSigner = await impersonateAndFund(oethVault.address); @@ -2504,15 +2444,27 @@ async function hackedVaultFixture() { /** * Instant rebase vault, for testing systems external to the vault */ -async function instantRebaseVaultFixture() { +async function instantRebaseVaultFixture(tokenName) { const fixture = await defaultFixture(); const { deploy } = deployments; const { governorAddr } = await getNamedAccounts(); const sGovernor = await ethers.provider.getSigner(governorAddr); + // Default to "usdc" if tokenName not provided + const name = tokenName ? tokenName.toLowerCase() : "usdc"; + let deployTokenAddress; + if (name === "usdc") { + deployTokenAddress = fixture.usdc.address; + } else if (name === "weth") { + deployTokenAddress = fixture.weth.address; + } else { + throw new Error(`Unsupported token name: ${name}`); + } + await deploy("MockVaultCoreInstantRebase", { from: governorAddr, + args: [deployTokenAddress], }); const instantRebase = await ethers.getContract("MockVaultCoreInstantRebase"); @@ -2538,7 +2490,7 @@ async function rebornFixture() { await deploy("Sanctum", { from: governorAddr, - args: [assetAddresses.USDS, vault.address], + args: [assetAddresses.USDC, vault.address], }); const sanctum = await ethers.getContract("Sanctum"); @@ -2575,7 +2527,7 @@ async function rebornFixture() { async function buybackFixture() { const fixture = await defaultFixture(); - const { ousd, oeth, oethVault, vault, weth, usds, josh, governor, timelock } = + const { ousd, oeth, oethVault, vault, weth, usdc, josh, governor, timelock } = fixture; const ousdBuybackProxy = await ethers.getContract("BuybackProxy"); @@ -2618,15 +2570,15 @@ async function buybackFixture() { // Load with funds to test swaps await setERC20TokenBalance(josh.address, weth, "10000"); - await setERC20TokenBalance(josh.address, usds, "10000"); + await setERC20TokenBalance(josh.address, usdc, "10000"); await weth.connect(josh).approve(oethVault.address, oethUnits("10000")); - await usds.connect(josh).approve(vault.address, ousdUnits("10000")); + await usdc.connect(josh).approve(vault.address, usdcUnits("10000")); // Mint & transfer oToken await oethVault.connect(josh).mint(weth.address, oethUnits("1.23"), "0"); await oeth.connect(josh).transfer(oethBuyback.address, oethUnits("1.1")); - await vault.connect(josh).mint(usds.address, oethUnits("1231"), "0"); + await vault.connect(josh).mint(usdc.address, usdcUnits("1231"), "0"); await ousd.connect(josh).transfer(ousdBuyback.address, oethUnits("1100")); await setERC20TokenBalance(armBuyback.address, weth, "100"); @@ -2639,9 +2591,9 @@ async function buybackFixture() { fixture.cvxLocker = await ethers.getContract("MockCVXLocker"); // Mint some OUSD - await usds.connect(josh).mint(ousdUnits("3000")); - await usds.connect(josh).approve(vault.address, ousdUnits("3000")); - await vault.connect(josh).mint(usds.address, ousdUnits("3000"), "0"); + await usdc.connect(josh).mint(usdcUnits("3000")); + await usdc.connect(josh).approve(vault.address, usdcUnits("3000")); + await vault.connect(josh).mint(usdc.address, usdcUnits("3000"), "0"); // Mint some OETH await weth.connect(josh).mint(oethUnits("3")); @@ -2674,7 +2626,6 @@ async function harvesterFixture() { vault, governor, harvester, - usdc, aaveStrategy, comp, aaveToken, @@ -2690,9 +2641,7 @@ async function harvesterFixture() { .setSupportedStrategy(aaveStrategy.address, true); // Add direct allocation of USDC to Aave - await vault - .connect(governor) - .setAssetDefaultStrategy(usdc.address, aaveStrategy.address); + await vault.connect(governor).setDefaultStrategy(aaveStrategy.address); // Let strategies hold some reward tokens await comp diff --git a/contracts/test/_metastrategies-fixtures.js b/contracts/test/_metastrategies-fixtures.js index 119a0860c1..2f1bf7bb85 100644 --- a/contracts/test/_metastrategies-fixtures.js +++ b/contracts/test/_metastrategies-fixtures.js @@ -21,17 +21,10 @@ async function withDefaultOUSDMetapoolStrategiesSet() { const { vault, timelock, dai, usdt, usdc, OUSDmetaStrategy, daniel } = fixture; - await vault - .connect(timelock) - .setAssetDefaultStrategy(dai.address, OUSDmetaStrategy.address); + await vault.connect(timelock).setDefaultStrategy(OUSDmetaStrategy.address); - await vault - .connect(timelock) - .setAssetDefaultStrategy(usdt.address, OUSDmetaStrategy.address); - - await vault - .connect(timelock) - .setAssetDefaultStrategy(usdc.address, OUSDmetaStrategy.address); + await vault.connect(timelock).setDefaultStrategy(OUSDmetaStrategy.address); + await vault.connect(timelock).setDefaultStrategy(OUSDmetaStrategy.address); fixture.cvxRewardPool = await ethers.getContractAt( "IRewardStaking", diff --git a/contracts/test/behaviour/harvester.js b/contracts/test/behaviour/harvester.js index dee9c505ac..12c080e32d 100644 --- a/contracts/test/behaviour/harvester.js +++ b/contracts/test/behaviour/harvester.js @@ -39,7 +39,7 @@ const { MAX_UINT256 } = require("../../utils/constants"); })); */ const shouldBehaveLikeHarvester = (context) => { - describe("Harvest behaviour", () => { + describe.skip("Harvest behaviour", () => { async function _checkBalancesPostHarvesting(harvestFn, strategies) { const { harvester } = context(); @@ -94,7 +94,7 @@ const shouldBehaveLikeHarvester = (context) => { }); }); - describe("RewardTokenConfig", () => { + describe.skip("RewardTokenConfig", () => { it("Should only allow valid Uniswap V2 path", async () => { const { harvester, crv, usdt, governor, uniswapRouter } = context(); @@ -409,8 +409,8 @@ const shouldBehaveLikeHarvester = (context) => { ).to.be.revertedWith("InvalidHarvestRewardBps"); }); - it("Should revert for unsupported tokens", async () => { - const { harvester, ousd, governor, uniswapRouter } = context(); + it.skip("Should revert for unsupported tokens", async () => { + const { harvester, governor, uniswapRouter, usdc } = context(); const config = { allowedSlippageBps: 133, @@ -424,7 +424,7 @@ const shouldBehaveLikeHarvester = (context) => { await expect( harvester .connect(governor) - .setRewardTokenConfig(ousd.address, config, []) + .setRewardTokenConfig(usdc.address, config, []) ).to.be.revertedWith("Asset not available"); }); @@ -448,7 +448,7 @@ const shouldBehaveLikeHarvester = (context) => { }); }); - describe("Swap", () => { + describe.skip("Swap", () => { async function _swapWithRouter(swapRouterConfig, swapData) { const { harvester, @@ -900,7 +900,7 @@ const shouldBehaveLikeHarvester = (context) => { }); }); - describe("Admin function", () => { + describe.skip("Admin function", () => { it("Should only allow governor to change RewardProceedsAddress", async () => { const { harvester, governor, daniel, strategist } = context(); diff --git a/contracts/test/behaviour/sfcStakingStrategy.js b/contracts/test/behaviour/sfcStakingStrategy.js index 4f2eaaa083..e2cd87e80d 100644 --- a/contracts/test/behaviour/sfcStakingStrategy.js +++ b/contracts/test/behaviour/sfcStakingStrategy.js @@ -31,13 +31,8 @@ const MIN_WITHDRAWAL_EPOCH_ADVANCE = 4; const shouldBehaveLikeASFCStakingStrategy = (context) => { describe("Initial setup", function () { it("Should verify the initial state", async () => { - const { - sonicStakingStrategy, - addresses, - oSonicVault, - testValidatorIds, - wS, - } = await context(); + const { sonicStakingStrategy, addresses, oSonicVault, testValidatorIds } = + await context(); expect(await sonicStakingStrategy.wrappedSonic()).to.equal( addresses.wS, "Incorrect wrapped sonic address set" @@ -77,16 +72,6 @@ const shouldBehaveLikeASFCStakingStrategy = (context) => { expect( (await sonicStakingStrategy.getRewardTokenAddresses()).length ).to.equal(0, "Incorrectly configured Reward Token Addresses"); - - expect(await oSonicVault.priceProvider()).to.not.equal( - AddressZero, - "Price provider address not set" - ); - - expect(await oSonicVault.priceUnitMint(wS.address)).to.equal( - oethUnits("1"), - "not expected PriceUnitMint" - ); }); }); diff --git a/contracts/test/behaviour/ssvStrategy.js b/contracts/test/behaviour/ssvStrategy.js index 534e8e148d..10a8e92dac 100644 --- a/contracts/test/behaviour/ssvStrategy.js +++ b/contracts/test/behaviour/ssvStrategy.js @@ -670,11 +670,13 @@ const shouldBehaveLikeAnSsvStrategy = (context) => { josh, nativeStakingSSVStrategy, nativeStakingFeeAccumulator, - oethVault, + oethFixedRateDripperProxy, weth, validatorRegistrator, } = await context(); - const dripperWethBefore = await weth.balanceOf(oethVault.address); + const dripperWethBefore = await weth.balanceOf( + oethFixedRateDripperProxy.address + ); const strategyBalanceBefore = await nativeStakingSSVStrategy.checkBalance( weth.address ); @@ -710,15 +712,14 @@ const shouldBehaveLikeAnSsvStrategy = (context) => { nativeStakingSSVStrategy.address, weth.address, executionRewards.add(consensusRewards), - oethVault.address + oethFixedRateDripperProxy.address ); - // check balances after expect( await nativeStakingSSVStrategy.checkBalance(weth.address) ).to.equal(strategyBalanceBefore, "checkBalance should not increase"); - expect(await weth.balanceOf(oethVault.address)).to.equal( + expect(await weth.balanceOf(oethFixedRateDripperProxy.address)).to.equal( dripperWethBefore.add(executionRewards).add(consensusRewards), "Vault WETH balance should increase" ); diff --git a/contracts/test/hacks/reborn.js b/contracts/test/hacks/reborn.js index b7a420dee0..07875c93fd 100644 --- a/contracts/test/hacks/reborn.js +++ b/contracts/test/hacks/reborn.js @@ -1,7 +1,7 @@ const { expect } = require("chai"); const { createFixtureLoader, rebornFixture } = require("../_fixture"); -const { isFork, usdsUnits, ousdUnits } = require("../helpers"); +const { isFork, usdcUnits, ousdUnits } = require("../helpers"); describe("Reborn Attack Protection", function () { if (isFork) { @@ -15,9 +15,9 @@ describe("Reborn Attack Protection", function () { fixture = await loadFixture(); }); it("Should correctly do accounting when reborn calls mint as different types of addresses", async function () { - const { usds, ousd, matt, rebornAddress, reborner, deployAndCall } = + const { usdc, ousd, matt, rebornAddress, reborner, deployAndCall } = fixture; - await usds.connect(matt).transfer(rebornAddress, usdsUnits("4")); + await usdc.connect(matt).transfer(rebornAddress, usdcUnits("4")); // call mint and self destruct (since account.code.length = 0) in constructor this is done // as an EOA from OUSD.sol's point of view await deployAndCall({ shouldAttack: true, shouldDestruct: true }); @@ -31,10 +31,11 @@ describe("Reborn Attack Protection", function () { expect(await ousd.nonRebasingSupply()).to.equal(ousdUnits("2")); }); - it("Should correctly do accounting when reborn calls burn as different types of addresses", async function () { - const { usds, ousd, matt, reborner, rebornAddress, deployAndCall } = + // Skipped as instant redeem is no longer supported for ousd + it.skip("Should correctly do accounting when reborn calls burn as different types of addresses", async function () { + const { usdc, ousd, matt, reborner, rebornAddress, deployAndCall } = fixture; - await usds.connect(matt).transfer(reborner.address, usdsUnits("4")); + await usdc.connect(matt).transfer(reborner.address, usdcUnits("4")); // call mint and self destruct (since account.code.length = 0) in constructor this is done // as an EOA from OUSD.sol's point of view await deployAndCall({ shouldAttack: true, shouldDestruct: true }); @@ -50,9 +51,9 @@ describe("Reborn Attack Protection", function () { }); it("Should correctly do accounting when reborn calls transfer as different types of addresses", async function () { - const { usds, ousd, matt, reborner, rebornAddress, deployAndCall } = + const { usdc, ousd, matt, reborner, rebornAddress, deployAndCall } = fixture; - await usds.connect(matt).transfer(reborner.address, usdsUnits("4")); + await usdc.connect(matt).transfer(reborner.address, usdcUnits("4")); // call mint and self destruct (since account.code.length = 0) in constructor this is done // as an EOA from OUSD.sol's point of view await deployAndCall({ shouldAttack: true, shouldDestruct: true }); @@ -74,10 +75,10 @@ describe("Reborn Attack Protection", function () { }); it("Should have correct balance even after recreating", async function () { - const { usds, matt, reborner, deployAndCall, ousd } = fixture; + const { usdc, matt, reborner, deployAndCall, ousd } = fixture; // Mint one OUSD and self-destruct - await usds.connect(matt).transfer(reborner.address, usdsUnits("4")); + await usdc.connect(matt).transfer(reborner.address, usdcUnits("4")); await deployAndCall({ shouldAttack: true, shouldDestruct: true }); await expect(reborner).to.have.a.balanceOf("1", ousd); diff --git a/contracts/test/hacks/reentrant.js b/contracts/test/hacks/reentrant.js deleted file mode 100644 index 6892d52886..0000000000 --- a/contracts/test/hacks/reentrant.js +++ /dev/null @@ -1,22 +0,0 @@ -const { expect } = require("chai"); - -const { createFixtureLoader, hackedVaultFixture } = require("../_fixture"); -const { isFork } = require("../helpers"); - -describe("Reentry Attack Protection", function () { - if (isFork) { - this.timeout(0); - } - - describe("Vault", function () { - const loadFixture = createFixtureLoader(hackedVaultFixture); - it("Should not allow malicious coin to reentrant call vault function", async function () { - const { evilDAI, vault } = await loadFixture(); - - // to see this fail just comment out the require in the nonReentrant() in Governable.sol - await expect(vault.mint(evilDAI.address, 10, 0)).to.be.revertedWith( - "Reentrant call" - ); - }); - }); -}); diff --git a/contracts/test/oracle/oracle.js b/contracts/test/oracle/oracle.js deleted file mode 100644 index 943f5b3723..0000000000 --- a/contracts/test/oracle/oracle.js +++ /dev/null @@ -1,90 +0,0 @@ -const { expect } = require("chai"); - -const { loadDefaultFixture } = require("../_fixture"); -const { ousdUnits, setOracleTokenPriceUsd } = require("../helpers"); - -/* - * Because the oracle code is so tightly intergrated into the vault, - * the actual tests for the core oracle features are just a part of the vault tests. - */ - -describe("Oracle", async () => { - let fixture; - beforeEach(async () => { - fixture = await loadDefaultFixture(); - }); - describe("Oracle read methods for DAPP", () => { - it("should read the mint price", async () => { - const { vault, usdt } = fixture; - const tests = [ - ["0.998", "0.998"], - ["1.00", "1.00"], - ["1.05", "1.00"], - ]; - for (const test of tests) { - const [actual, expectedRead] = test; - await setOracleTokenPriceUsd("USDT", actual); - expect(await vault.priceUnitMint(usdt.address)).to.equal( - ousdUnits(expectedRead) - ); - } - }); - - it("should fail below peg on the mint price", async () => { - const { vault, usdt } = fixture; - const prices = ["0.85", "0.997"]; - for (const price of prices) { - await setOracleTokenPriceUsd("USDT", price); - await expect(vault.priceUnitMint(usdt.address)).to.be.revertedWith( - "Asset price below peg" - ); - } - }); - - it("should read the redeem price", async () => { - const { vault, usdt } = fixture; - const tests = [ - ["0.80", "1.00"], - ["1.00", "1.00"], - ["1.05", "1.05"], - ]; - for (const test of tests) { - const [actual, expectedRead] = test; - await setOracleTokenPriceUsd("USDT", actual); - expect(await vault.priceUnitRedeem(usdt.address)).to.equal( - ousdUnits(expectedRead) - ); - } - }); - }); - - describe("Min/Max Drift", async () => { - const tests = [ - ["0.10", "Oracle: Price under min"], - ["0.699", "Oracle: Price under min"], - ["0.70"], - ["0.98"], - ["1.00"], - ["1.04"], - ["1.30"], - ["1.31", "Oracle: Price exceeds max"], - ["6.00", "Oracle: Price exceeds max"], - ]; - - for (const test of tests) { - const [price, expectedRevert] = test; - const revertLabel = expectedRevert ? "revert" : "not revert"; - const label = `Should ${revertLabel} because of drift at $${price}`; - it(label, async () => { - const { vault, usdt } = fixture; - await setOracleTokenPriceUsd("USDT", price); - if (expectedRevert) { - const tx = vault.priceUnitRedeem(usdt.address); - await expect(tx).to.be.revertedWith(expectedRevert); - } else { - await vault.priceUnitRedeem(usdt.address); - } - }); - } - }); -}); diff --git a/contracts/test/poolBooster/poolBooster.sonic.fork-test.js b/contracts/test/poolBooster/poolBooster.sonic.fork-test.js index 8257d9991f..c10119e858 100644 --- a/contracts/test/poolBooster/poolBooster.sonic.fork-test.js +++ b/contracts/test/poolBooster/poolBooster.sonic.fork-test.js @@ -646,7 +646,7 @@ describe("ForkTest: Pool Booster", function () { ], "PoolBoosterFactorySwapxSingle" ) - ).to.be.revertedWith("Invalid oSonic address"); + ).to.be.revertedWith("Invalid oToken address"); }); it("Can not deploy a factory with zero governor address", async () => { diff --git a/contracts/test/safe-modules/bridge-helper.base.fork-test.js b/contracts/test/safe-modules/bridge-helper.base.fork-test.js index d46b46a609..e6d96bad03 100644 --- a/contracts/test/safe-modules/bridge-helper.base.fork-test.js +++ b/contracts/test/safe-modules/bridge-helper.base.fork-test.js @@ -56,9 +56,9 @@ describe("ForkTest: Bridge Helper Safe Module (Base)", function () { } = fixture; // Make sure Vault has some WETH - _mintWETH(nick, "1"); - await weth.connect(nick).approve(oethbVault.address, oethUnits("1")); - await oethbVault.connect(nick).mint(weth.address, oethUnits("1"), "0"); + _mintWETH(nick, "10000"); + await weth.connect(nick).approve(oethbVault.address, oethUnits("10000")); + await oethbVault.connect(nick).mint(weth.address, oethUnits("10000"), "0"); // Update oracle price await woethStrategy.updateWOETHOraclePrice(); diff --git a/contracts/test/safe-modules/bridge-helper.mainnet.fork-test.js b/contracts/test/safe-modules/bridge-helper.mainnet.fork-test.js index 7c4fb77fe3..dacf7ff60c 100644 --- a/contracts/test/safe-modules/bridge-helper.mainnet.fork-test.js +++ b/contracts/test/safe-modules/bridge-helper.mainnet.fork-test.js @@ -179,26 +179,16 @@ describe("ForkTest: Bridge Helper Safe Module (Ethereum)", function () { }); it("Should unwrap WOETH and redeem it to WETH", async () => { - const { - woeth, - weth, - josh, - timelock, - safeSigner, - bridgeHelperModule, - oethVault, - } = fixture; + const { woeth, weth, josh, safeSigner, bridgeHelperModule, oethVault } = + fixture; await oethVault.connect(josh).rebase(); // Do a huge yield deposit to fund the Vault - await oethVault - .connect(timelock) - .setAssetDefaultStrategy(weth.address, addresses.zero); - await impersonateAndFund(josh.address, "3000"); - await weth.connect(josh).deposit({ value: oethUnits("2500") }); - await weth.connect(josh).approve(oethVault.address, oethUnits("2500")); - await oethVault.connect(josh).mint(weth.address, oethUnits("2000"), "0"); + await impersonateAndFund(josh.address, "10000"); + await weth.connect(josh).deposit({ value: oethUnits("9500") }); + await weth.connect(josh).approve(oethVault.address, oethUnits("9500")); + await oethVault.connect(josh).mint(weth.address, oethUnits("9000"), "0"); const woethAmount = oethUnits("1"); diff --git a/contracts/test/safe-modules/bridge-helper.plume.fork-test.js b/contracts/test/safe-modules/bridge-helper.plume.fork-test.js index 3e1c58122c..41f5c35fd0 100644 --- a/contracts/test/safe-modules/bridge-helper.plume.fork-test.js +++ b/contracts/test/safe-modules/bridge-helper.plume.fork-test.js @@ -230,7 +230,7 @@ describe("ForkTest: Bridge Helper Safe Module (Plume)", function () { ); }); - it("Should mint OETHp with WETH and redeem it for wOETH", async () => { + it.skip("Should mint OETHp with WETH and redeem it for wOETH", async () => { const { _mintWETH, oethpVault, diff --git a/contracts/test/strategies/base/aerodrome-amo.base.fork-test.js b/contracts/test/strategies/base/aerodrome-amo.base.fork-test.js index dcadeb0b2e..7ecc6fab08 100644 --- a/contracts/test/strategies/base/aerodrome-amo.base.fork-test.js +++ b/contracts/test/strategies/base/aerodrome-amo.base.fork-test.js @@ -260,6 +260,7 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { governor, strategist, rafael, + nick, aeroSwapRouter, aeroNftManager, harvester, @@ -275,6 +276,7 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { governor = fixture.governor; strategist = fixture.strategist; rafael = fixture.rafael; + nick = fixture.nick; aeroSwapRouter = fixture.aeroSwapRouter; aeroNftManager = fixture.aeroNftManager; oethbVaultSigner = await impersonateAndFund(oethbVault.address); @@ -1276,6 +1278,7 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { await mintAndDepositToStrategy({ amount: oethUnits("5"), returnTransaction: true, + depositALotBefore: false, }); const { value, direction } = await quoteAmountToSwapBeforeRebalance({ @@ -1418,9 +1421,17 @@ describe("ForkTest: Aerodrome AMO Strategy (Base)", async function () { userOverride, amount, returnTransaction, + depositALotBefore = false, } = {}) => { const user = userOverride || rafael; amount = amount || oethUnits("5"); + // Deposit a lot of WETH into the vault + if (depositALotBefore) { + const _amount = oethUnits("5000"); + await setERC20TokenBalance(nick.address, weth, _amount, hre); + await weth.connect(nick).approve(oethbVault.address, _amount); + await oethbVault.connect(nick).mint(weth.address, _amount, _amount); + } const balance = weth.balanceOf(user.address); if (balance < amount) { diff --git a/contracts/test/strategies/convex.js b/contracts/test/strategies/convex.js index 640d87f10d..599d0556e7 100644 --- a/contracts/test/strategies/convex.js +++ b/contracts/test/strategies/convex.js @@ -2,7 +2,7 @@ const { expect } = require("chai"); const { createFixtureLoader, convexVaultFixture } = require("../_fixture"); const { - usdsUnits, + usdcUnits, ousdUnits, units, expectApproxSupply, @@ -14,32 +14,29 @@ describe("Convex Strategy", function () { this.timeout(0); } - let anna, - ousd, + let ousd, vault, governor, + strategist, crv, threePoolToken, convexStrategy, cvxBooster, - usdt, - usdc, - usds; + usdc; const mint = async (amount, asset) => { - await asset.connect(anna).mint(await units(amount, asset)); + await asset.connect(strategist).mint(await units(amount, asset)); await asset - .connect(anna) + .connect(strategist) .approve(vault.address, await units(amount, asset)); return await vault - .connect(anna) + .connect(strategist) .mint(asset.address, await units(amount, asset), 0); }; const loadFixture = createFixtureLoader(convexVaultFixture); beforeEach(async function () { const fixture = await loadFixture(); - anna = fixture.anna; vault = fixture.vault; ousd = fixture.ousd; governor = fixture.governor; @@ -47,61 +44,39 @@ describe("Convex Strategy", function () { threePoolToken = fixture.threePoolToken; convexStrategy = fixture.convexStrategy; cvxBooster = fixture.cvxBooster; - usdt = fixture.usdt; usdc = fixture.usdc; - usds = fixture.usds; + strategist = fixture.strategist; }); describe("Mint", function () { - it("Should stake USDT in Curve gauge via 3pool", async function () { - await expectApproxSupply(ousd, ousdUnits("200")); - await mint("30000.00", usdt); - await expectApproxSupply(ousd, ousdUnits("30200")); - await expect(anna).to.have.a.balanceOf("30000", ousd); - await expect(cvxBooster).has.an.approxBalanceOf("30000", threePoolToken); - }); - it("Should stake USDC in Curve gauge via 3pool", async function () { await expectApproxSupply(ousd, ousdUnits("200")); await mint("50000.00", usdc); await expectApproxSupply(ousd, ousdUnits("50200")); - await expect(anna).to.have.a.balanceOf("50000", ousd); - await expect(cvxBooster).has.an.approxBalanceOf("50000", threePoolToken); - }); - - it("Should use a minimum LP token amount when depositing USDT into 3pool", async function () { - await expect(mint("29000", usdt)).to.be.revertedWith( - "Slippage ruined your day" - ); - }); - - it("Should use a minimum LP token amount when depositing USDC into 3pool", async function () { - await expect(mint("29000", usdc)).to.be.revertedWith( - "Slippage ruined your day" - ); + await expect(strategist).to.have.a.balanceOf("50000", ousd); + await expect(cvxBooster).has.an.approxBalanceOf("50200", threePoolToken); }); }); describe("Redeem", function () { it("Should be able to unstake from gauge and return USDT", async function () { await expectApproxSupply(ousd, ousdUnits("200")); - await mint("10000.00", usds); await mint("10000.00", usdc); - await mint("10000.00", usdt); - await vault.connect(anna).redeem(ousdUnits("20000"), 0); - await expectApproxSupply(ousd, ousdUnits("10200")); + await vault.connect(governor).set; + await vault.connect(strategist).redeem(ousdUnits("10000"), 0); + await expectApproxSupply(ousd, ousdUnits("200")); }); }); describe("Utilities", function () { it("Should allow transfer of arbitrary token by Governor", async () => { - await usds.connect(anna).approve(vault.address, usdsUnits("8.0")); - await vault.connect(anna).mint(usds.address, usdsUnits("8.0"), 0); - // Anna sends her OUSD directly to Strategy + await usdc.connect(strategist).approve(vault.address, usdcUnits("8.0")); + await vault.connect(strategist).mint(usdc.address, usdcUnits("8.0"), 0); + // Strategist sends her OUSD directly to Strategy await ousd - .connect(anna) + .connect(strategist) .transfer(convexStrategy.address, ousdUnits("8.0")); - // Anna asks Governor for help + // Strategist asks Governor for help await convexStrategy .connect(governor) .transferToken(ousd.address, ousdUnits("8.0")); @@ -109,10 +84,10 @@ describe("Convex Strategy", function () { }); it("Should not allow transfer of arbitrary token by non-Governor", async () => { - // Naughty Anna + // Naughty Strategist await expect( convexStrategy - .connect(anna) + .connect(strategist) .transferToken(ousd.address, ousdUnits("8.0")) ).to.be.revertedWith("Caller is not the Governor"); }); diff --git a/contracts/test/strategies/curve-amo-ousd.mainnet.fork-test.js b/contracts/test/strategies/curve-amo-ousd.mainnet.fork-test.js index 75ca6e8c4b..e8294b615e 100644 --- a/contracts/test/strategies/curve-amo-ousd.mainnet.fork-test.js +++ b/contracts/test/strategies/curve-amo-ousd.mainnet.fork-test.js @@ -228,6 +228,7 @@ describe("Curve AMO OUSD strategy", function () { )} usdc to the pool` ); + await setERC20TokenBalance(nick.address, usdc, attackerusdcAmount, hre); await usdc.connect(nick).approve(curvePool.address, attackerusdcAmount); // Attacker adds a lot of usdc into the pool // prettier-ignore diff --git a/contracts/test/strategies/dripper.js b/contracts/test/strategies/dripper.js index 9584574918..9a6209c713 100644 --- a/contracts/test/strategies/dripper.js +++ b/contracts/test/strategies/dripper.js @@ -7,7 +7,7 @@ const { const { usdtUnits, advanceTime } = require("../helpers"); describe("Dripper", async () => { - let dripper, usdt, vault, ousd, governor, josh; + let dripper, usdt, vault, governor, josh; const loadFixture = createFixtureLoader(instantRebaseVaultFixture); beforeEach(async () => { @@ -15,7 +15,6 @@ describe("Dripper", async () => { dripper = fixture.dripper; usdt = fixture.usdt; vault = fixture.vault; - ousd = fixture.ousd; governor = fixture.governor; josh = fixture.josh; @@ -81,16 +80,6 @@ describe("Dripper", async () => { await expectApproxCollectOf(expected, dripper.collect); }); }); - describe("collectAndRebase()", async () => { - it("transfers funds to the vault and rebases", async () => { - const beforeRct = await ousd.rebasingCreditsPerToken(); - await dripper.connect(governor).setDripDuration("20000"); - await advanceTime(1000); - await expectApproxCollectOf("50", dripper.collectAndRebase); - const afterRct = await ousd.rebasingCreditsPerToken(); - expect(afterRct).to.be.lt(beforeRct); - }); - }); describe("Drip math", async () => { it("gives all funds if collect is after the duration end", async () => { await dripper.connect(governor).setDripDuration("20000"); diff --git a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js index f6bb3b7760..0945b59fe2 100644 --- a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js +++ b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js @@ -112,7 +112,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { describe("with wS in the vault", () => { const loadFixture = createFixtureLoader(swapXAMOFixture, { - wsMintAmount: 5000, + wsMintAmount: 5000000, depositToStrategy: false, balancePool: true, }); @@ -514,7 +514,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { swapXAMOStrategy, wS, } = fixture; - const withdrawAmount = parseUnits("2000"); + const withdrawAmount = parseUnits("200"); const dataBeforeWithdraw = await snapData(); logSnapData( @@ -893,7 +893,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { describe("with an insolvent vault", () => { const loadFixture = createFixtureLoader(swapXAMOFixture, { - wsMintAmount: 50000000, + wsMintAmount: 5000000, depositToStrategy: false, }); beforeEach(async () => { diff --git a/contracts/test/strategies/vault-value-checker.js b/contracts/test/strategies/vault-value-checker.js index 52f6a50531..5b6ecbab18 100644 --- a/contracts/test/strategies/vault-value-checker.js +++ b/contracts/test/strategies/vault-value-checker.js @@ -2,16 +2,17 @@ const { expect } = require("chai"); const { loadDefaultFixture } = require("../_fixture"); const { impersonateAndFund } = require("../../utils/signers"); +const { usdcUnits, ousdUnits } = require("../helpers"); describe("Check vault value", () => { - let vault, ousd, matt, usds, checker, vaultSigner; + let vault, ousd, matt, usdc, checker, vaultSigner; beforeEach(async () => { const fixture = await loadDefaultFixture(); vault = fixture.vault; ousd = fixture.ousd; matt = fixture.matt; - usds = fixture.usds; + usdc = fixture.usdc; checker = await ethers.getContract("VaultValueChecker"); vaultSigner = await ethers.getSigner(vault.address); await impersonateAndFund(vaultSigner.address); @@ -26,10 +27,10 @@ describe("Check vault value", () => { // Alter value if (vaultChange > 0) { - await usds.mintTo(vault.address, vaultChange); + await usdc.mintTo(vault.address, vaultChange); } else if (vaultChange < 0) { // transfer amount out of the vault - await usds + await usdc .connect(vaultSigner) .transfer(matt.address, vaultChange * -1, { gasPrice: 0 }); } @@ -77,58 +78,58 @@ describe("Check vault value", () => { it( "should succeed if vault gain was inside the allowed band", testChange({ - vaultChange: 200, - expectedProfit: 0, - profitVariance: 100, - supplyChange: 200, - expectedVaultChange: 200, - vaultChangeVariance: 100, + vaultChange: usdcUnits("2"), // In USDC, 6 decimals + expectedProfit: ousdUnits("0"), + profitVariance: ousdUnits("100"), + supplyChange: ousdUnits("2"), // In OUSD, 18 decimals + expectedVaultChange: ousdUnits("2"), + vaultChangeVariance: ousdUnits("100"), }) ); it( "should revert if vault gain less than allowed", testChange({ - vaultChange: 50, - expectedProfit: 125, - profitVariance: 25, - supplyChange: 2, - expectedVaultChange: 1, - vaultChangeVariance: 1, + vaultChange: usdcUnits("50"), + expectedProfit: ousdUnits("125"), + profitVariance: ousdUnits("25"), + supplyChange: ousdUnits("2"), + expectedVaultChange: ousdUnits("1"), + vaultChangeVariance: ousdUnits("1"), expectedRevert: "Profit too low", }) ); it( "should revert if vault gain more than allowed", testChange({ - vaultChange: 550, - expectedProfit: 500, - profitVariance: 50, - supplyChange: 2, - expectedVaultChange: 1, - vaultChangeVariance: 1, + vaultChange: usdcUnits("550"), + expectedProfit: ousdUnits("500"), + profitVariance: ousdUnits("50"), + supplyChange: ousdUnits("2"), + expectedVaultChange: ousdUnits("1"), + vaultChangeVariance: ousdUnits("1"), expectedRevert: "Vault value change too high", }) ); it( "should succeed if vault loss was inside the allowed band", testChange({ - vaultChange: -200, - expectedProfit: -200, - profitVariance: 100, - supplyChange: 0, - expectedVaultChange: -200, - vaultChangeVariance: 0, + vaultChange: usdcUnits("200").mul(-1), + expectedProfit: ousdUnits("200").mul(-1), + profitVariance: ousdUnits("100"), + supplyChange: ousdUnits("0"), + expectedVaultChange: ousdUnits("200").mul(-1), + vaultChangeVariance: ousdUnits("0"), }) ); it( "should revert if vault loss under allowed band", testChange({ - vaultChange: -400, - expectedProfit: -400, - profitVariance: 40, - supplyChange: 0, - expectedVaultChange: 0, - vaultChangeVariance: 100, + vaultChange: usdcUnits("40").mul(-1), + expectedProfit: ousdUnits("40").mul(-1), + profitVariance: ousdUnits("4"), + supplyChange: ousdUnits("0"), + expectedVaultChange: ousdUnits("0"), + vaultChangeVariance: ousdUnits("10"), expectedRevert: "Vault value change too low", }) ); @@ -136,12 +137,12 @@ describe("Check vault value", () => { it( "should revert if vault loss over allowed band", testChange({ - vaultChange: 100, - expectedProfit: 100, - profitVariance: 100, - supplyChange: 0, - expectedVaultChange: 0, - vaultChangeVariance: 50, + vaultChange: usdcUnits("100"), + expectedProfit: ousdUnits("100"), + profitVariance: ousdUnits("100"), + supplyChange: ousdUnits("0"), + expectedVaultChange: ousdUnits("0"), + vaultChangeVariance: ousdUnits("50"), expectedRevert: "Vault value change too high", }) ); diff --git a/contracts/test/token/ousd.js b/contracts/test/token/ousd.js index 6edf446b55..8ac1708298 100644 --- a/contracts/test/token/ousd.js +++ b/contracts/test/token/ousd.js @@ -6,7 +6,7 @@ const { } = require("../_fixture"); const { utils, BigNumber } = require("ethers"); -const { usdsUnits, ousdUnits, usdcUnits, isFork } = require("../helpers"); +const { ousdUnits, usdcUnits, isFork } = require("../helpers"); const zeroAddress = "0x0000000000000000000000000000000000000000"; @@ -718,23 +718,23 @@ describe("Token", function () { }); it("Should mint correct amounts on non-rebasing account without previously set creditsPerToken", async () => { - let { ousd, usds, vault, josh, mockNonRebasing } = fixture; + let { ousd, usdc, vault, josh, mockNonRebasing } = fixture; // Give contract 100 USDS from Josh - await usds + await usdc .connect(josh) - .transfer(mockNonRebasing.address, usdsUnits("100")); + .transfer(mockNonRebasing.address, usdcUnits("100")); await expect(mockNonRebasing).has.a.balanceOf("0", ousd); const totalSupplyBefore = await ousd.totalSupply(); await mockNonRebasing.approveFor( - usds.address, + usdc.address, vault.address, - usdsUnits("100") + usdcUnits("100") ); const tx = await mockNonRebasing.mintOusd( vault.address, - usds.address, - usdsUnits("50") + usdc.address, + usdcUnits("50") ); await expect(tx) .to.emit(ousd, "AccountRebasingDisabled") @@ -759,22 +759,22 @@ describe("Token", function () { }); it("Should mint correct amounts on non-rebasing account with previously set creditsPerToken", async () => { - let { ousd, usds, vault, matt, usdc, josh, mockNonRebasing } = fixture; + let { ousd, usdc, vault, matt, josh, mockNonRebasing } = fixture; // Give contract 100 USDS from Josh - await usds + await usdc .connect(josh) - .transfer(mockNonRebasing.address, usdsUnits("100")); + .transfer(mockNonRebasing.address, usdcUnits("100")); await expect(mockNonRebasing).has.a.balanceOf("0", ousd); const totalSupplyBefore = await ousd.totalSupply(); await mockNonRebasing.approveFor( - usds.address, + usdc.address, vault.address, - usdsUnits("100") + usdcUnits("100") ); await mockNonRebasing.mintOusd( vault.address, - usds.address, - usdsUnits("50") + usdc.address, + usdcUnits("50") ); expect(await ousd.totalSupply()).to.equal( totalSupplyBefore.add(ousdUnits("50")) @@ -793,8 +793,8 @@ describe("Token", function () { // Mint again await mockNonRebasing.mintOusd( vault.address, - usds.address, - usdsUnits("50") + usdc.address, + usdcUnits("50") ); expect(await ousd.totalSupply()).to.equal( // Note 200 additional from simulated yield @@ -817,22 +817,22 @@ describe("Token", function () { }); it("Should burn the correct amount for non-rebasing account", async () => { - let { ousd, usds, vault, matt, usdc, josh, mockNonRebasing } = fixture; + let { ousd, usdc, vault, matt, josh, mockNonRebasing } = fixture; // Give contract 100 USDS from Josh - await usds + await usdc .connect(josh) - .transfer(mockNonRebasing.address, usdsUnits("100")); + .transfer(mockNonRebasing.address, usdcUnits("100")); await expect(mockNonRebasing).has.a.balanceOf("0", ousd); const totalSupplyBefore = await ousd.totalSupply(); await mockNonRebasing.approveFor( - usds.address, + usdc.address, vault.address, - usdsUnits("100") + usdcUnits("100") ); await mockNonRebasing.mintOusd( vault.address, - usds.address, - usdsUnits("50") + usdc.address, + usdcUnits("50") ); await expect(await ousd.totalSupply()).to.equal( totalSupplyBefore.add(ousdUnits("50")) diff --git a/contracts/test/token/woeth.js b/contracts/test/token/woeth.js index aa33d78f86..b4447d8726 100644 --- a/contracts/test/token/woeth.js +++ b/contracts/test/token/woeth.js @@ -11,7 +11,7 @@ describe("WOETH", function () { if (isFork) { this.timeout(0); } - const loadFixture = createFixtureLoader(instantRebaseVaultFixture); + const loadFixture = createFixtureLoader(instantRebaseVaultFixture, "weth"); let oeth, weth, woeth, oethVault, usds, matt, josh, governor; beforeEach(async () => { diff --git a/contracts/test/token/wousd.js b/contracts/test/token/wousd.js index c2f7ba0fa1..fab72b4d40 100644 --- a/contracts/test/token/wousd.js +++ b/contracts/test/token/wousd.js @@ -4,13 +4,13 @@ const { createFixtureLoader, instantRebaseVaultFixture, } = require("../_fixture"); -const { ousdUnits, usdsUnits, isFork } = require("../helpers"); +const { ousdUnits, usdcUnits, isFork } = require("../helpers"); describe("WOUSD", function () { if (isFork) { this.timeout(0); } - let ousd, wousd, vault, usds, matt, josh, governor; + let ousd, wousd, vault, usdc, matt, josh, governor; const loadFixture = createFixtureLoader(instantRebaseVaultFixture); beforeEach(async () => { @@ -18,7 +18,7 @@ describe("WOUSD", function () { ousd = fixture.ousd; wousd = fixture.wousd; vault = fixture.vault; - usds = fixture.usds; + usdc = fixture.usdc; matt = fixture.matt; josh = fixture.josh; governor = fixture.governor; @@ -34,13 +34,15 @@ describe("WOUSD", function () { await increaseOUSDSupplyAndRebase(await ousd.totalSupply()); }); - const increaseOUSDSupplyAndRebase = async (usdsAmount) => { - await usds.connect(matt).transfer(vault.address, usdsAmount); + const increaseOUSDSupplyAndRebase = async (usdcAmount) => { + await usdc.mintTo(matt.address, usdcAmount.div(1e12)); + await usdc.connect(matt).transfer(vault.address, usdcAmount.div(1e12)); await vault.rebase(); }; describe("Funds in, Funds out", async () => { it("should deposit at the correct ratio", async () => { + console.log((await wousd.balanceOf(josh.address)).toString()); await wousd.connect(josh).deposit(ousdUnits("50"), josh.address); await expect(josh).to.have.a.balanceOf("75", wousd); await expect(josh).to.have.a.balanceOf("50", ousd); @@ -70,7 +72,7 @@ describe("WOUSD", function () { describe("Collects Rebase", async () => { it("should increase with an OUSD rebase", async () => { await expect(wousd).to.have.approxBalanceOf("100", ousd); - await usds.connect(josh).transfer(vault.address, usdsUnits("200")); + await usdc.connect(josh).transfer(vault.address, usdcUnits("200")); await vault.rebase(); await expect(wousd).to.have.approxBalanceOf("150", ousd); }); @@ -86,12 +88,12 @@ describe("WOUSD", function () { describe("Token recovery", async () => { it("should allow a governor to recover tokens", async () => { - await usds.connect(matt).transfer(wousd.address, usdsUnits("2")); - await expect(wousd).to.have.a.balanceOf("2", usds); - await expect(governor).to.have.a.balanceOf("1000", usds); - await wousd.connect(governor).transferToken(usds.address, usdsUnits("2")); - await expect(wousd).to.have.a.balanceOf("0", usds); - await expect(governor).to.have.a.balanceOf("1002", usds); + await usdc.connect(matt).transfer(wousd.address, usdcUnits("2")); + await expect(wousd).to.have.a.balanceOf("2", usdc); + await expect(governor).to.have.a.balanceOf("1000", usdc); + await wousd.connect(governor).transferToken(usdc.address, usdcUnits("2")); + await expect(wousd).to.have.a.balanceOf("0", usdc); + await expect(governor).to.have.a.balanceOf("1002", usdc); }); it("should not allow a governor to collect OUSD", async () => { await expect( diff --git a/contracts/test/vault/collateral-swaps.mainnet.fork-test.js b/contracts/test/vault/collateral-swaps.mainnet.fork-test.js deleted file mode 100644 index cd9a769814..0000000000 --- a/contracts/test/vault/collateral-swaps.mainnet.fork-test.js +++ /dev/null @@ -1,332 +0,0 @@ -const { expect } = require("chai"); -const { parseUnits, formatUnits } = require("ethers/lib/utils"); - -const { - createFixtureLoader, - defaultFixture, - ousdCollateralSwapFixture, -} = require("../_fixture"); -const { getIInchSwapData, recodeSwapData } = require("../../utils/1Inch"); -const { decimalsFor, isCI } = require("../helpers"); -const { resolveAsset } = require("../../utils/resolvers"); - -const log = require("../../utils/logger")("test:fork:swaps"); - -describe.skip("ForkTest: OUSD Vault", function () { - this.timeout(0); - - // Retry up to 3 times on CI - this.retries(isCI ? 3 : 0); - - let fixture; - - describe("post deployment", () => { - const loadFixture = createFixtureLoader(defaultFixture); - beforeEach(async () => { - fixture = await loadFixture(); - }); - - it("should have swapper set", async () => { - const { vault, swapper } = fixture; - - expect(await vault.swapper()).to.equal(swapper.address); - }); - it("assets should have allowed slippage", async () => { - const { vault, usds, usdc, usdt } = fixture; - - const assets = [usds, usdc, usdt]; - const expectedDecimals = [18, 6, 6]; - const expectedConversions = [0, 0, 0]; - const expectedSlippage = [25, 25, 25]; - - for (let i = 0; i < assets.length; i++) { - const config = await vault.getAssetConfig(assets[i].address); - - expect(config.decimals, `decimals ${i}`).to.equal(expectedDecimals[i]); - expect(config.isSupported, `isSupported ${i}`).to.be.true; - expect(config.unitConversion, `unitConversion ${i}`).to.be.equal( - expectedConversions[i] - ); - expect( - config.allowedOracleSlippageBps, - `allowedOracleSlippageBps ${i}` - ).to.equal(expectedSlippage[i]); - } - }); - }); - - describe("Collateral swaps (Happy paths)", async () => { - const loadFixture = createFixtureLoader(ousdCollateralSwapFixture); - beforeEach(async () => { - fixture = await loadFixture(); - }); - - const tests = [ - { - from: "USDS", - to: "USDT", - fromAmount: 1000000, - minToAssetAmount: 990000, - }, - { - from: "USDS", - to: "USDC", - fromAmount: 1000000, - minToAssetAmount: 999900, - slippage: 0.1, // Max 1Inch slippage - }, - { - from: "USDT", - to: "USDS", - fromAmount: 1000000, - minToAssetAmount: 998000, - }, - { - from: "USDT", - to: "USDC", - fromAmount: 1000000, - minToAssetAmount: 998000, - }, - { - from: "USDC", - to: "USDS", - fromAmount: 1000000, - minToAssetAmount: 999900, - slippage: 0.05, // Max 1Inch slippage - }, - { - from: "USDC", - to: "USDT", - fromAmount: 1000000, - minToAssetAmount: "990000", - slippage: 0.02, - approxFromBalance: true, - }, - ]; - for (const test of tests) { - it(`should be able to swap ${test.fromAmount} ${test.from} for a min of ${ - test.minToAssetAmount - } ${test.to} using ${test.protocols || "all"} protocols`, async () => { - const fromAsset = await resolveAsset(test.from); - const toAsset = await resolveAsset(test.to); - await assertSwap( - { - ...test, - fromAsset, - toAsset, - vault: fixture.vault, - }, - fixture - ); - }); - } - }); - - describe("Collateral swaps (Unhappy paths)", async () => { - const loadFixture = createFixtureLoader(ousdCollateralSwapFixture); - beforeEach(async () => { - fixture = await loadFixture(); - }); - - const tests = [ - { - error: "", - from: "USDS", - to: "USDC", - fromAmount: 100, - minToAssetAmount: 105, - }, - { - error: "From asset is not supported", - from: "WETH", - to: "USDT", - fromAmount: 20, - minToAssetAmount: 1, - }, - { - error: "To asset is not supported", - from: "USDS", - to: "WETH", - fromAmount: 20, - minToAssetAmount: 1, - }, - { - error: "Usds/insufficient-balance", - from: "USDS", - to: "USDC", - fromAmount: 30000000, - minToAssetAmount: 29000000, - }, - { - error: "SafeERC20: low-level call failed", - from: "USDT", - to: "USDC", - fromAmount: 50000000, - minToAssetAmount: 49900000, - }, - { - error: "ERC20: transfer amount exceeds balance", - from: "USDC", - to: "USDS", - fromAmount: 30000000, - minToAssetAmount: 29900000, - }, - ]; - - for (const test of tests) { - it(`should fail to swap ${test.fromAmount} ${test.from} for ${ - test.to - } using ${test.protocols || "all"} protocols: error ${ - test.error - }`, async () => { - const fromAsset = await resolveAsset(test.from); - const toAsset = await resolveAsset(test.to); - await assertFailedSwap( - { - ...test, - fromAsset, - toAsset, - vault: fixture.vault, - }, - fixture - ); - }); - } - }); -}); -const assertSwap = async ( - { - fromAsset, - toAsset, - fromAmount, - minToAssetAmount, - slippage, - protocols, - approxFromBalance, - vault, - }, - fixture -) => { - const { strategist, swapper } = fixture; - - const fromAssetDecimals = await decimalsFor(fromAsset); - fromAmount = await parseUnits(fromAmount.toString(), fromAssetDecimals); - const toAssetDecimals = await decimalsFor(toAsset); - minToAssetAmount = await parseUnits( - minToAssetAmount.toString(), - toAssetDecimals - ); - - const apiEncodedData = await getIInchSwapData({ - vault: vault, - fromAsset, - toAsset, - fromAmount, - slippage, - protocols, - }); - - // re-encode the 1Inch tx.data from their swap API to the executer data - const swapData = await recodeSwapData(apiEncodedData); - - const fromBalanceBefore = await fromAsset.balanceOf(vault.address); - log( - `from asset balance before ${formatUnits( - fromBalanceBefore, - fromAssetDecimals - )}` - ); - const toBalanceBefore = await toAsset.balanceOf(vault.address); - - const tx = vault - .connect(strategist) - .swapCollateral( - fromAsset.address, - toAsset.address, - fromAmount, - minToAssetAmount, - swapData - ); - - // Asset events - await expect(tx).to.emit(vault, "Swapped").withNamedArgs({ - _fromAsset: fromAsset.address, - _toAsset: toAsset.address, - _fromAssetAmount: fromAmount, - }); - await expect(tx) - .to.emit(fromAsset, "Transfer") - .withArgs(vault.address, swapper.address, fromAmount); - - // Asset balances - const fromBalanceAfter = await fromAsset.balanceOf(vault.address); - if (approxFromBalance) { - expect( - fromBalanceBefore.sub(fromBalanceAfter), - "from asset approx bal" - ).to.approxEqualTolerance(fromAmount, 0.01); - } else { - expect(fromBalanceBefore.sub(fromBalanceAfter), "from asset bal").to.eq( - fromAmount - ); - } - const toBalanceAfter = await toAsset.balanceOf(vault.address); - log( - `to assets purchased ${formatUnits( - toBalanceAfter.sub(toBalanceBefore), - toAssetDecimals - )}` - ); - const toAmount = toBalanceAfter.sub(toBalanceBefore); - expect(toAmount, "to asset bal").to.gt(minToAssetAmount); - log( - `swapped ${formatUnits(fromAmount, fromAssetDecimals)} for ${formatUnits( - toAmount, - toAssetDecimals - )}` - ); -}; -const assertFailedSwap = async ( - { - fromAsset, - toAsset, - fromAmount, - minToAssetAmount, - slippage, - protocols, - error, - vault, - }, - fixture -) => { - const { strategist } = fixture; - - const fromAssetDecimals = await decimalsFor(fromAsset); - fromAmount = await parseUnits(fromAmount.toString(), fromAssetDecimals); - const toAssetDecimals = await decimalsFor(toAsset); - minToAssetAmount = parseUnits(minToAssetAmount.toString(), toAssetDecimals); - - const apiEncodedData = await getIInchSwapData({ - vault, - fromAsset, - toAsset, - fromAmount, - slippage, - protocols, - }); - - // re-encode the 1Inch tx.data from their swap API to the executer data - const swapData = await recodeSwapData(apiEncodedData); - - const tx = vault - .connect(strategist) - .swapCollateral( - fromAsset.address, - toAsset.address, - fromAmount, - minToAssetAmount, - swapData - ); - - await expect(tx).to.be.revertedWith(error); -}; diff --git a/contracts/test/vault/compound.js b/contracts/test/vault/compound.js index ccebaca644..8705e6707e 100644 --- a/contracts/test/vault/compound.js +++ b/contracts/test/vault/compound.js @@ -8,13 +8,10 @@ const { ousdUnits, usdsUnits, usdcUnits, - usdtUnits, - tusdUnits, setOracleTokenPriceUsd, isFork, expectApproxSupply, } = require("../helpers"); -const addresses = require("../../utils/addresses"); describe("Vault with Compound strategy", function () { if (isFork) { @@ -40,12 +37,12 @@ describe("Vault with Compound strategy", function () { }); it("Governor can call setPTokenAddress", async () => { - const { usds, ousd, matt, compoundStrategy } = fixture; + const { usdc, ousd, matt, compoundStrategy } = fixture; await expect( compoundStrategy .connect(matt) - .setPTokenAddress(ousd.address, usds.address) + .setPTokenAddress(ousd.address, usdc.address) ).to.be.revertedWith("Caller is not the Governor"); }); @@ -58,181 +55,106 @@ describe("Vault with Compound strategy", function () { }); it("Should allocate unallocated assets", async () => { - const { anna, governor, usds, usdc, usdt, tusd, vault, compoundStrategy } = - fixture; - - await usds.connect(anna).transfer(vault.address, usdsUnits("100")); - await usdc.connect(anna).transfer(vault.address, usdcUnits("200")); - await usdt.connect(anna).transfer(vault.address, usdtUnits("300")); - - await tusd.connect(anna).mint(ousdUnits("1000.0")); - await tusd.connect(anna).transfer(vault.address, tusdUnits("400")); + const { anna, governor, usdc, vault, compoundStrategy } = fixture; + await vault.connect(governor).setDefaultStrategy(compoundStrategy.address); + await usdc.connect(anna).transfer(vault.address, usdcUnits("100")); await expect(vault.connect(governor).allocate()) .to.emit(vault, "AssetAllocated") - .withArgs(usds.address, compoundStrategy.address, usdsUnits("300")) - .to.emit(vault, "AssetAllocated") - .withArgs(usdc.address, compoundStrategy.address, usdcUnits("200")) - .to.emit(vault, "AssetAllocated") - .withArgs(usdt.address, compoundStrategy.address, usdcUnits("300")); - /* - TODO: There does not appear to be any support for .withoutArgs to verify - that this event doesn't get emitted. - .to.emit(vault, "AssetAllocated") - .withoutArgs(usdt.address, compoundStrategy.address, tusdUnits("400")); - */ + .withArgs(usdc.address, compoundStrategy.address, usdcUnits("300")); - // Note compoundVaultFixture sets up with 200 USDS already in the Strategy + // Note compoundVaultFixture sets up with 200 USDC already in the Strategy // 200 + 100 = 300 - await expect( - await compoundStrategy.checkBalance(usds.address) - ).to.approxEqual(usdsUnits("300")); await expect( await compoundStrategy.checkBalance(usdc.address) - ).to.approxEqual(usdcUnits("200")); - await expect( - await compoundStrategy.checkBalance(usdt.address) - ).to.approxEqual(usdtUnits("300")); - - // Strategy doesn't support TUSD - // Vault balance for TUSD should remain unchanged - expect(await tusd.balanceOf(vault.address)).to.equal(tusdUnits("400")); + ).to.approxEqual(usdcUnits("300")); }); it("Should correctly handle a deposit of USDC (6 decimals)", async function () { const { anna, ousd, usdc, vault } = fixture; await expect(anna).has.a.balanceOf("0", ousd); - // The mint process maxes out at a 1.0 price - await setOracleTokenPriceUsd("USDC", "1.25"); await usdc.connect(anna).approve(vault.address, usdcUnits("50")); - await vault.connect(anna).mint(usdc.address, usdcUnits("50"), 0); + await vault + .connect(anna) + .mint(usdc.address, usdcUnits("50"), ousdUnits("50")); await expect(anna).has.a.balanceOf("50", ousd); }); it("Should allow withdrawals", async () => { - const { anna, compoundStrategy, ousd, usdc, vault, governor } = fixture; - - await expect(anna).has.a.balanceOf("1000.00", usdc); - await usdc.connect(anna).approve(vault.address, usdcUnits("50.0")); - await vault.connect(anna).mint(usdc.address, usdcUnits("50.0"), 0); - await expect(anna).has.a.balanceOf("50.00", ousd); - - await vault.connect(governor).allocate(); - - // Verify the deposit went to Compound - expect(await compoundStrategy.checkBalance(usdc.address)).to.approxEqual( - usdcUnits("50.0") - ); - - // Note Anna will have slightly less than 50 due to deposit to Compound - // according to the MockCToken implementation - await ousd.connect(anna).approve(vault.address, ousdUnits("40.0")); - await vault.connect(anna).redeem(ousdUnits("40.0"), 0); - - await expect(anna).has.an.approxBalanceOf("10", ousd); - // Vault has 200 USDS and 50 USDC, 50/250 * 40 USDC will come back - await expect(anna).has.an.approxBalanceOf("958", usdc); - }); - - it("Should calculate the balance correctly with USDS in strategy", async () => { - const { usds, vault, josh, compoundStrategy, governor } = fixture; - - expect(await vault.totalValue()).to.approxEqual( - utils.parseUnits("200", 18) - ); - - // Josh deposits USDS, 18 decimals - await usds.connect(josh).approve(vault.address, usdsUnits("22.0")); - await vault.connect(josh).mint(usds.address, usdsUnits("22.0"), 0); - - await vault.connect(governor).allocate(); + const { strategist, ousd, usdc, vault } = fixture; - // Josh had 1000 USDS but used 100 USDS to mint OUSD in the fixture - await expect(josh).has.an.approxBalanceOf("878.0", usds, "Josh has less"); + await expect(strategist).has.a.balanceOf("1000.00", usdc); + await usdc.connect(strategist).approve(vault.address, usdcUnits("50.0")); + await vault.connect(strategist).mint(usdc.address, usdcUnits("50.0"), 0); + await expect(strategist).has.a.balanceOf("50.00", ousd); - // Verify the deposit went to Compound (as well as existing Vault assets) - expect(await compoundStrategy.checkBalance(usds.address)).to.approxEqual( - usdsUnits("222") - ); + await ousd.connect(strategist).approve(vault.address, ousdUnits("40.0")); + await vault + .connect(strategist) + .redeem(ousdUnits("40.0"), usdcUnits("35.0")); + expect(await vault.redeemFeeBps()).to.be.eq(0); - expect(await vault.totalValue()).to.approxEqual( - utils.parseUnits("222", 18) - ); + await expect(strategist).has.an.balanceOf("10", ousd); + await expect(strategist).has.an.balanceOf("990.0", usdc); }); it("Should calculate the balance correctly with USDC in strategy", async () => { - const { usdc, vault, matt, compoundStrategy, governor } = fixture; + const { usdc, vault, josh, compoundStrategy, governor } = fixture; expect(await vault.totalValue()).to.approxEqual( utils.parseUnits("200", 18) ); - // Matt deposits USDC, 6 decimals - await usdc.connect(matt).approve(vault.address, usdcUnits("8.0")); - await vault.connect(matt).mint(usdc.address, usdcUnits("8.0"), 0); + // Josh deposits USDC, 6 decimals + await usdc.connect(josh).approve(vault.address, usdcUnits("22.0")); + await vault.connect(josh).mint(usdc.address, usdcUnits("22.0"), 0); + await vault.connect(governor).setDefaultStrategy(compoundStrategy.address); await vault.connect(governor).allocate(); - // Verify the deposit went to Compound - await expect(matt).has.an.approxBalanceOf("992.0", usdc, "Matt has less"); + // Josh had 1000 USDC but used 100 USDC to mint OUSD in the fixture + await expect(josh).has.an.approxBalanceOf("878.0", usdc, "Josh has less"); + // Verify the deposit went to Compound (as well as existing Vault assets) expect(await compoundStrategy.checkBalance(usdc.address)).to.approxEqual( - usdcUnits("8.0") + usdcUnits("222") ); expect(await vault.totalValue()).to.approxEqual( - utils.parseUnits("208", 18) + utils.parseUnits("222", 18) ); }); - it("Should calculate the balance correct with TUSD in Vault and USDS, USDC, USDT in Compound strategy", async () => { - const { - tusd, - usdc, - usds, - usdt, - vault, - matt, - josh, - anna, - governor, - compoundStrategy, - } = fixture; + it("Should calculate the balance correct with USDC in Vault and USDC in Compound strategy", async () => { + const { usdc, vault, matt, anna, governor, compoundStrategy } = fixture; expect(await vault.totalValue()).to.approxEqual( utils.parseUnits("200", 18) ); - // Josh deposits USDS, 18 decimals - await usds.connect(josh).approve(vault.address, usdsUnits("22.0")); - await vault.connect(josh).mint(usds.address, usdsUnits("22.0"), 0); + await vault.connect(governor).setDefaultStrategy(compoundStrategy.address); await vault.connect(governor).allocate(); // Existing 200 also ends up in strategy due to allocate call - expect(await compoundStrategy.checkBalance(usds.address)).to.approxEqual( - usdsUnits("222") + expect(await compoundStrategy.checkBalance(usdc.address)).to.approxEqual( + usdcUnits("200") ); // Matt deposits USDC, 6 decimals await usdc.connect(matt).approve(vault.address, usdcUnits("8.0")); await vault.connect(matt).mint(usdc.address, usdcUnits("8.0"), 0); await vault.connect(governor).allocate(); expect(await compoundStrategy.checkBalance(usdc.address)).to.approxEqual( - usdcUnits("8.0") + usdcUnits("208.0") ); - // Anna deposits USDT, 6 decimals - await usdt.connect(anna).approve(vault.address, usdtUnits("10.0")); - await vault.connect(anna).mint(usdt.address, usdtUnits("10.0"), 0); - await vault.connect(governor).allocate(); - expect(await compoundStrategy.checkBalance(usdt.address)).to.approxEqual( - usdtUnits("10.0") + // Anna deposits USDC that will stay in the Vault, 6 decimals + await usdc.connect(anna).approve(vault.address, usdcUnits("10.0")); + await vault.connect(anna).mint(usdc.address, usdcUnits("10.0"), 0); + expect(await usdc.balanceOf(vault.address)).to.approxEqual( + usdcUnits("10.0") ); - // Matt deposits TUSD, 18 decimals - await tusd.connect(matt).mint(ousdUnits("100.0")); - await tusd.connect(matt).approve(vault.address, tusdUnits("9.0")); - await vault.connect(matt).mint(tusd.address, tusdUnits("9.0"), 0); expect(await vault.totalValue()).to.approxEqual( - utils.parseUnits("249", 18) + utils.parseUnits("218", 18) ); }); @@ -265,8 +187,7 @@ describe("Vault with Compound strategy", function () { }); it("Should correctly withdrawAll all assets in Compound strategy", async () => { - const { usdc, vault, matt, josh, usds, compoundStrategy, governor } = - fixture; + const { usdc, vault, matt, compoundStrategy, governor } = fixture; expect(await vault.totalValue()).to.approxEqual( utils.parseUnits("200", 18) @@ -276,112 +197,61 @@ describe("Vault with Compound strategy", function () { await usdc.connect(matt).approve(vault.address, usdcUnits("8.0")); await vault.connect(matt).mint(usdc.address, usdcUnits("8.0"), 0); + await vault.connect(governor).setDefaultStrategy(compoundStrategy.address); await vault.connect(governor).allocate(); expect(await compoundStrategy.checkBalance(usdc.address)).to.approxEqual( - usdcUnits("8") + usdcUnits("208.0") ); - expect(await vault.totalValue()).to.approxEqual( utils.parseUnits("208", 18) ); - - await usds.connect(josh).approve(vault.address, usdsUnits("22.0")); - await vault.connect(josh).mint(usds.address, usdsUnits("22.0"), 0); - - await vault.connect(governor).allocate(); - - expect(await compoundStrategy.checkBalance(usds.address)).to.approxEqual( - usdsUnits("222") - ); - - expect(await vault.totalValue()).to.approxEqual( - utils.parseUnits("230", 18) - ); - await compoundStrategy.connect(governor).withdrawAll(); // There should be no USDS or USDC left in compound strategy expect(await compoundStrategy.checkBalance(usdc.address)).to.equal(0); - expect(await compoundStrategy.checkBalance(usds.address)).to.equal(0); // Vault value should remain the same because the liquidattion sent the // assets back to the vault expect(await vault.totalValue()).to.approxEqual( - utils.parseUnits("230", 18) + utils.parseUnits("208", 18) ); }); it("Should withdrawAll assets in Strategy and return them to Vault on removal", async () => { - const { - usdt, - usdc, - comp, - vault, - matt, - josh, - usds, - harvester, - compoundStrategy, - governor, - } = fixture; + const { usdc, vault, matt, compoundStrategy, governor } = fixture; expect(await vault.totalValue()).to.approxEqual( utils.parseUnits("200", 18) ); - const mockUniswapRouter = await ethers.getContract("MockUniswapRouter"); - - await harvester.connect(governor).setRewardTokenConfig( - comp.address, // reward token - { - allowedSlippageBps: 300, - harvestRewardBps: 0, - swapPlatformAddr: mockUniswapRouter.address, - doSwapRewardToken: true, - swapPlatform: 0, - liquidationLimit: 0, - }, - utils.defaultAbiCoder.encode( - ["address[]"], - [[comp.address, usdt.address]] - ) - ); // Matt deposits USDC, 6 decimals await usdc.connect(matt).approve(vault.address, usdcUnits("8.0")); await vault.connect(matt).mint(usdc.address, usdcUnits("8.0"), 0); + await vault.connect(governor).setDefaultStrategy(compoundStrategy.address); await vault.connect(governor).allocate(); expect(await compoundStrategy.checkBalance(usdc.address)).to.approxEqual( - usdcUnits("8.0") + usdcUnits("208.0") ); - await usds.connect(josh).approve(vault.address, usdsUnits("22.0")); - await vault.connect(josh).mint(usds.address, usdsUnits("22.0"), 0); expect(await vault.totalValue()).to.approxEqual( - utils.parseUnits("230", 18) + utils.parseUnits("208", 18) ); - await expect(await vault.getStrategyCount()).to.equal(1); + expect(await vault.getStrategyCount()).to.equal(1); await vault .connect(governor) - .setAssetDefaultStrategy(usdt.address, addresses.zero); - await vault - .connect(governor) - .setAssetDefaultStrategy(usdc.address, addresses.zero); - await vault - .connect(governor) - .setAssetDefaultStrategy(usds.address, addresses.zero); + .setDefaultStrategy("0x0000000000000000000000000000000000000000"); await vault.connect(governor).removeStrategy(compoundStrategy.address); - await expect(await vault.getStrategyCount()).to.equal(0); - + expect(await vault.getStrategyCount()).to.equal(0); // Vault value should remain the same because the liquidattion sent the // assets back to the vault expect(await vault.totalValue()).to.approxEqual( - utils.parseUnits("230", 18) + utils.parseUnits("208", 18) ); // Should be able to add Strategy back. Proves the struct in the mapping @@ -390,24 +260,22 @@ describe("Vault with Compound strategy", function () { }); it("Should not alter balances after an asset price change", async () => { - let { ousd, vault, matt, usdc, usds } = fixture; + let { ousd, vault, matt, usdc } = fixture; await usdc.connect(matt).approve(vault.address, usdcUnits("200")); await vault.connect(matt).mint(usdc.address, usdcUnits("200"), 0); - await usds.connect(matt).approve(vault.address, usdsUnits("200")); - await vault.connect(matt).mint(usds.address, usdsUnits("200"), 0); // 200 OUSD was already minted in the fixture, 100 each for Matt and Josh - await expectApproxSupply(ousd, ousdUnits("600.0")); - // 100 + 200 + 200 - await expect(matt).has.an.approxBalanceOf("500", ousd, "Initial"); + await expectApproxSupply(ousd, ousdUnits("400.0")); + // 100 + 200 = 300 + await expect(matt).has.an.approxBalanceOf("300", ousd, "Initial"); await setOracleTokenPriceUsd("USDC", "1.30"); await vault.rebase(); - await expectApproxSupply(ousd, ousdUnits("600.0")); + await expectApproxSupply(ousd, ousdUnits("400.0")); await expect(matt).has.an.approxBalanceOf( - "500.00", + "300.00", ousd, "After some assets double" ); @@ -415,68 +283,14 @@ describe("Vault with Compound strategy", function () { await setOracleTokenPriceUsd("USDC", "1.00"); await vault.rebase(); - await expectApproxSupply(ousd, ousdUnits("600.0")); + await expectApproxSupply(ousd, ousdUnits("400.0")); await expect(matt).has.an.approxBalanceOf( - "500", + "300.00", ousd, "After assets go back" ); }); - it("Should handle non-standard token deposits", async () => { - let { ousd, vault, matt, nonStandardToken, oracleRouter, governor } = - fixture; - - await oracleRouter.cacheDecimals(nonStandardToken.address); - if (nonStandardToken) { - await vault.connect(governor).supportAsset(nonStandardToken.address, 0); - } - - await setOracleTokenPriceUsd("NonStandardToken", "1.00"); - - await nonStandardToken - .connect(matt) - .approve(vault.address, usdtUnits("10000")); - - // Try to mint more than balance, to check failure state - try { - await vault - .connect(matt) - .mint(nonStandardToken.address, usdtUnits("1200"), 0); - } catch (err) { - expect( - /reverted with reason string 'SafeERC20: ERC20 operation did not succeed/gi.test( - err.message - ) - ).to.be.true; - } finally { - // Make sure nothing got affected - await expectApproxSupply(ousd, ousdUnits("200.0")); - await expect(matt).has.an.approxBalanceOf("100", ousd); - await expect(matt).has.an.approxBalanceOf("1000", nonStandardToken); - } - - // Try minting with a valid balance of tokens - await vault - .connect(matt) - .mint(nonStandardToken.address, usdtUnits("100"), 0); - await expect(matt).has.an.approxBalanceOf("900", nonStandardToken); - - await expectApproxSupply(ousd, ousdUnits("300.0")); - await expect(matt).has.an.approxBalanceOf("200", ousd, "Initial"); - await vault.rebase(); - await expect(matt).has.an.approxBalanceOf("200", ousd, "After null rebase"); - await setOracleTokenPriceUsd("NonStandardToken", "1.40"); - await vault.rebase(); - - await expectApproxSupply(ousd, ousdUnits("300.0")); - await expect(matt).has.an.approxBalanceOf( - "200.00", - ousd, - "After some assets double" - ); - }); - it("Should never allocate anything when Vault buffer is 1e18 (100%)", async () => { const { usds, vault, governor, compoundStrategy } = fixture; @@ -490,74 +304,26 @@ describe("Vault with Compound strategy", function () { await expect(await compoundStrategy.checkBalance(usds.address)).to.equal(0); }); - it("Should allocate correctly with USDS when Vault buffer is 1e17 (10%)", async () => { - const { usds, vault, governor, compoundStrategy } = await loadFixture( + it("Should allocate correctly with USDC when Vault buffer is 1e17 (10%)", async () => { + const { usdc, vault, governor, compoundStrategy } = await loadFixture( compoundVaultFixture ); - await expect(await vault.getStrategyCount()).to.equal(1); + expect(await vault.getStrategyCount()).to.equal(1); // Set a Vault buffer and allocate await vault.connect(governor).setVaultBuffer(utils.parseUnits("1", 17)); + await vault.connect(governor).setDefaultStrategy(compoundStrategy.address); await vault.allocate(); // Verify 80% went to Compound await expect( - await compoundStrategy.checkBalance(usds.address) - ).to.approxEqual(ousdUnits("180")); + await compoundStrategy.checkBalance(usdc.address) + ).to.approxEqual(usdcUnits("180")); // Remaining 20 should be in Vault await expect(await vault.totalValue()).to.approxEqual(ousdUnits("200")); }); - it("Should allocate correctly with USDS, USDT, USDC when Vault Buffer is 1e17 (10%)", async () => { - const { - usds, - usdc, - usdt, - matt, - josh, - vault, - anna, - governor, - compoundStrategy, - } = fixture; - - expect(await vault.totalValue()).to.approxEqual( - utils.parseUnits("200", 18) - ); - - // Josh deposits USDS, 18 decimals - await usds.connect(josh).approve(vault.address, usdsUnits("22.0")); - await vault.connect(josh).mint(usds.address, usdsUnits("22.0"), 0); - // Matt deposits USDC, 6 decimals - await usdc.connect(matt).approve(vault.address, usdcUnits("8.0")); - await vault.connect(matt).mint(usdc.address, usdcUnits("8.0"), 0); - // Anna deposits USDT, 6 decimals - await usdt.connect(anna).approve(vault.address, usdtUnits("20.0")); - await vault.connect(anna).mint(usdt.address, usdtUnits("20.0"), 0); - - // Set a Vault buffer and allocate - await vault.connect(governor).setVaultBuffer(utils.parseUnits("1", 17)); - await vault.allocate(); - - // Verify 80% went to Compound - await expect( - await compoundStrategy.checkBalance(usds.address) - ).to.approxEqual(usdsUnits("199.8")); - - await expect( - await compoundStrategy.checkBalance(usdc.address) - ).to.approxEqual(usdcUnits("7.2")); - - await expect( - await compoundStrategy.checkBalance(usdt.address) - ).to.approxEqual(usdtUnits("18")); - - expect(await vault.totalValue()).to.approxEqual( - utils.parseUnits("250", 18) - ); - }); - it("Should allow transfer of arbitrary token by Governor", async () => { const { vault, compoundStrategy, ousd, usdc, matt, governor } = fixture; @@ -587,143 +353,35 @@ describe("Vault with Compound strategy", function () { }); it("Should have correct balances on consecutive mint and redeem", async () => { - const { ousd, vault, usdc, usds, anna, matt, josh } = fixture; - - const usersWithBalances = [ - [anna, 0], - [matt, 100], - [josh, 100], - ]; + const { ousd, vault, usdc, anna, matt, josh, governor } = fixture; - const assetsWithUnits = [ - [usds, usdsUnits], - [usdc, usdcUnits], + const testCases = [ + { user: anna, start: 0 }, + { user: matt, start: 100 }, + { user: josh, start: 100 }, ]; - - for (const [user, startBalance] of usersWithBalances) { - for (const [asset, units] of assetsWithUnits) { - for (const amount of [5.09, 10.32, 20.99, 100.01]) { - await asset - .connect(user) - .approve(vault.address, await units(amount.toString())); - await vault - .connect(user) - .mint(asset.address, await units(amount.toString()), 0); - await expect(user).has.an.approxBalanceOf( - (startBalance + amount).toString(), - ousd - ); - await vault.connect(user).redeem(ousdUnits(amount.toString()), 0); - await expect(user).has.an.approxBalanceOf( - startBalance.toString(), - ousd - ); - } + const amounts = [5.09, 10.32, 20.99, 100.01]; + + for (const { user, start } of testCases) { + for (const amount of amounts) { + const mintAmount = usdcUnits(amount.toString()); + await usdc.connect(user).approve(vault.address, mintAmount); + await vault.connect(user).mint(usdc.address, mintAmount, 0); + await expect(user).has.an.approxBalanceOf( + (start + amount).toString(), + ousd + ); + await vault.connect(governor).setStrategistAddr(user.address); + await vault.connect(user).redeem(ousdUnits(amount.toString()), 0); + await expect(user).has.an.approxBalanceOf(start.toString(), ousd); } } }); - it("Should collect reward tokens and swap via Uniswap", async () => { - const { anna, vault, harvester, governor, compoundStrategy, comp, usdt } = - fixture; - - const mockUniswapRouter = await ethers.getContract("MockUniswapRouter"); - - const compAmount = utils.parseUnits("100", 18); - await comp.connect(governor).mint(compAmount); - await comp.connect(governor).transfer(compoundStrategy.address, compAmount); - - await harvester.connect(governor).setRewardTokenConfig( - comp.address, // reward token - { - allowedSlippageBps: 0, - harvestRewardBps: 100, - swapPlatformAddr: mockUniswapRouter.address, - doSwapRewardToken: true, - swapPlatform: 0, - liquidationLimit: 0, - }, - utils.defaultAbiCoder.encode( - ["address[]"], - [[comp.address, usdt.address]] - ) - ); - - // Make sure Vault has 0 USDT balance - await expect(vault).has.a.balanceOf("0", usdt); - - // Make sure the Strategy has COMP balance - expect(await comp.balanceOf(await governor.getAddress())).to.be.equal("0"); - expect(await comp.balanceOf(compoundStrategy.address)).to.be.equal( - compAmount - ); - - const balanceBeforeAnna = await usdt.balanceOf(anna.address); - - // prettier-ignore - await harvester - .connect(anna)["harvestAndSwap(address)"](compoundStrategy.address); - - const balanceAfterAnna = await usdt.balanceOf(anna.address); - - // Make sure Vault has 100 USDT balance (the Uniswap mock converts at 1:1) - await expect(vault).has.a.balanceOf("99", usdt); - - // No COMP in Harvester or Compound strategy - await expect(harvester).has.a.balanceOf("0", comp); - expect(await comp.balanceOf(compoundStrategy.address)).to.be.equal("0"); - expect(balanceAfterAnna - balanceBeforeAnna).to.be.equal( - utils.parseUnits("1", 6) - ); - }); - - it("Should not swap if slippage is too high", async () => { - const { josh, vault, harvester, governor, compoundStrategy, comp, usdt } = - fixture; - - const mockUniswapRouter = await ethers.getContract("MockUniswapRouter"); - - // Mock router gives 1:1, if we set this to something high there will be - // too much slippage - await setOracleTokenPriceUsd("COMP", "1.3"); - - const compAmount = utils.parseUnits("100", 18); - await comp.connect(governor).mint(compAmount); - await comp.connect(governor).transfer(compoundStrategy.address, compAmount); - await mockUniswapRouter.setSlippage(utils.parseEther("0.75")); - - await harvester.connect(governor).setRewardTokenConfig( - comp.address, // reward token - { - allowedSlippageBps: 0, - harvestRewardBps: 100, - swapPlatformAddr: mockUniswapRouter.address, - doSwapRewardToken: true, - swapPlatform: 0, - liquidationLimit: 0, - }, - utils.defaultAbiCoder.encode( - ["address[]"], - [[comp.address, usdt.address]] - ) - ); - // Make sure Vault has 0 USDT balance - await expect(vault).has.a.balanceOf("0", usdt); - - // Make sure the Strategy has COMP balance - expect(await comp.balanceOf(await governor.getAddress())).to.be.equal("0"); - expect(await comp.balanceOf(compoundStrategy.address)).to.be.equal( - compAmount - ); - - // prettier-ignore - await expect(harvester - .connect(josh)["harvestAndSwap(address)"](compoundStrategy.address)).to.be.revertedWith("Slippage error"); - }); - const mintDoesAllocate = async (amount) => { - const { anna, vault, usdc, governor } = fixture; + const { anna, vault, usdc, governor, compoundStrategy } = fixture; + await vault.connect(governor).setDefaultStrategy(compoundStrategy.address); await vault.connect(governor).setVaultBuffer(0); await vault.allocate(); await usdc.connect(anna).mint(usdcUnits(amount)); @@ -743,8 +401,9 @@ describe("Vault with Compound strategy", function () { }); it("Alloc with both threshold and buffer", async () => { - const { anna, vault, usdc, usds, governor } = fixture; + const { anna, vault, usdc, usds, governor, compoundStrategy } = fixture; + await vault.connect(governor).setDefaultStrategy(compoundStrategy.address); await vault.allocate(); await vault.connect(governor).setVaultBuffer(utils.parseUnits("1", 17)); await vault.connect(governor).setAutoAllocateThreshold(ousdUnits("3")); @@ -758,11 +417,9 @@ describe("Vault with Compound strategy", function () { // 5 should be below the 10% vault buffer (4/204 * 100 = 1.96%) // All funds should remain in vault - await expect(await usdc.balanceOf(vault.address)).to.equal( - usdcUnits(amount) - ); + expect(await usdc.balanceOf(vault.address)).to.equal(usdcUnits(amount)); // USDS was allocated before the vault buffer was set - await expect(await usds.balanceOf(vault.address)).to.equal(usdsUnits("0")); + expect(await usds.balanceOf(vault.address)).to.equal(usdsUnits("0")); // Use an amount above the vault buffer size that will trigger an allocate const allocAmount = "5000"; diff --git a/contracts/test/vault/exchangeRate.js b/contracts/test/vault/exchangeRate.js deleted file mode 100644 index 037d3d1725..0000000000 --- a/contracts/test/vault/exchangeRate.js +++ /dev/null @@ -1,262 +0,0 @@ -const { expect } = require("chai"); - -const { loadDefaultFixture } = require("../_fixture"); -const { - ousdUnits, - usdsUnits, - advanceTime, - setOracleTokenPriceUsd, - isFork, -} = require("../helpers"); - -describe("Vault Redeem", function () { - if (isFork) { - this.timeout(0); - } - - let fixture; - beforeEach(async function () { - fixture = await loadDefaultFixture(); - const { vault, reth, governor } = fixture; - await vault.connect(governor).supportAsset(reth.address, 1); - await setOracleTokenPriceUsd("RETHETH", "1.2"); - }); - - it("Should mint at a positive exchange rate", async () => { - const { ousd, vault, reth, anna } = fixture; - - await reth.connect(anna).mint(usdsUnits("4.0")); - await reth.connect(anna).approve(vault.address, usdsUnits("4.0")); - await vault.connect(anna).mint(reth.address, usdsUnits("4.0"), 0); - await expect(anna).has.a.balanceOf("4.80", ousd); - }); - - it("Should mint less at low oracle, positive exchange rate", async () => { - const { ousd, vault, reth, anna } = fixture; - - await setOracleTokenPriceUsd("RETHETH", "1.199"); - await reth.connect(anna).mint(usdsUnits("4.0")); - await reth.connect(anna).approve(vault.address, usdsUnits("4.0")); - await vault.connect(anna).mint(reth.address, usdsUnits("4.0"), 0); - await expect(anna).has.a.approxBalanceOf("4.796", ousd); - }); - - it("Should revert mint at too low oracle, positive exchange rate", async () => { - const { vault, reth, anna } = fixture; - - await setOracleTokenPriceUsd("RETHETH", "1.00"); - await reth.connect(anna).mint(usdsUnits("4.0")); - await reth.connect(anna).approve(vault.address, usdsUnits("4.0")); - const tx = vault.connect(anna).mint(reth.address, usdsUnits("4.0"), 0); - await expect(tx).to.be.revertedWith("Asset price below peg"); - }); - - it("Should mint same at high oracle, positive exchange rate", async () => { - const { ousd, vault, reth, anna } = fixture; - - await setOracleTokenPriceUsd("RETHETH", "1.2"); - await reth.connect(anna).mint(usdsUnits("4.0")); - await reth.connect(anna).approve(vault.address, usdsUnits("4.0")); - await vault.connect(anna).mint(reth.address, usdsUnits("4.0"), 0); - await expect(anna).has.a.balanceOf("4.80", ousd); - }); - - it("Should rebase at a positive exchange rate", async () => { - const { ousd, vault, reth, anna } = fixture; - await vault.rebase(); - - const beforeGift = await ousd.totalSupply(); - - await reth.connect(anna).mint(usdsUnits("3.0")); - await reth.connect(anna).transfer(vault.address, usdsUnits("3.0")); - - await advanceTime(7 * 24 * 60 * 60); - await vault.rebase(); - - const afterGift = await ousd.totalSupply(); - expect(afterGift.sub(beforeGift)).to.approxEqualTolerance( - ousdUnits("3.6"), - 0.1, - "afterGift" - ); - - await setOracleTokenPriceUsd("RETHETH", "1.4"); - await reth.setExchangeRate(usdsUnits("1.4")); - await advanceTime(7 * 24 * 60 * 60); - await vault.rebase(); - const afterExchangeUp = await ousd.totalSupply(); - - expect(afterExchangeUp.sub(afterGift)).to.approxEqualTolerance( - ousdUnits("0.6"), - 0.1, - "afterExchangeUp" - ); - }); - - it("Should redeem at the expected rate", async () => { - const { ousd, vault, usds, reth, anna } = fixture; - - await setOracleTokenPriceUsd("RETHETH", "2.0"); - await reth.setExchangeRate(usdsUnits("2.0")); - - await reth.connect(anna).mint(usdsUnits("100.0")); - await reth.connect(anna).approve(vault.address, usdsUnits("100.0")); - await vault.connect(anna).mint(reth.address, usdsUnits("100.0"), 0); - await expect(anna).has.a.balanceOf("200", ousd, "post mint"); - await vault.rebase(); - await expect(anna).has.a.balanceOf("200", ousd, "post rebase"); - - await vault.connect(anna).redeem(usdsUnits("200.0"), 0); - await expect(anna).has.a.balanceOf("50", reth, "RETH"); - await expect(anna).has.a.balanceOf("1100", usds, "USDC"); - }); - - it("Should redeem less at a high oracle", async () => { - const { ousd, vault, usds, reth, anna } = fixture; - - await setOracleTokenPriceUsd("RETHETH", "2.0"); - await reth.setExchangeRate(usdsUnits("2.0")); - - await reth.connect(anna).mint(usdsUnits("100.0")); - await reth.connect(anna).approve(vault.address, usdsUnits("100.0")); - await vault.connect(anna).mint(reth.address, usdsUnits("100.0"), 0); - await expect(anna).has.a.balanceOf("200", ousd, "post mint"); - await vault.rebase(); - await expect(anna).has.a.balanceOf("200", ousd, "post rebase"); - - // Contains 100 rETH, (200 units) and 200 USDS (200 units) - // After Oracles $600 + $200 = $800 - // - // Redeeming $200 == 1/4 vault - // 25rETH and 50 USDS - - await setOracleTokenPriceUsd("RETHETH", "6.0"); - await reth.setExchangeRate(usdsUnits("6.0")); - await vault.connect(anna).redeem(usdsUnits("200.0"), 0); - await expect(anna).has.a.balanceOf("25", reth, "RETH"); - await expect(anna).has.a.balanceOf("1050", usds, "USDC"); - }); - - it("Should redeem same at a low oracle", async () => { - const { ousd, vault, usds, reth, anna } = fixture; - - await setOracleTokenPriceUsd("RETHETH", "2.0"); - await reth.setExchangeRate(usdsUnits("2.0")); - - await reth.connect(anna).mint(usdsUnits("100.0")); - await reth.connect(anna).approve(vault.address, usdsUnits("100.0")); - await vault.connect(anna).mint(reth.address, usdsUnits("100.0"), 0); - await expect(anna).has.a.balanceOf("200", ousd, "post mint"); - await vault.rebase(); - await expect(anna).has.a.balanceOf("200", ousd, "post rebase"); - - // Contains 100 rETH, (200 units) and 200 USDS (200 units) - // After Oracles $154 + $200 = $354 - // - // But since the exchange rate is still 2.0 the RETH unit price - // is snapped back to 2.0 when redeeming. Making the calculation: - // After Oracles $200 + $200 = $400 - // - // And redeeming 200 is 50% of the vault = 50 RETH & 100 USDS - - await setOracleTokenPriceUsd("RETHETH", "1.54"); - await vault.connect(anna).redeem(usdsUnits("200.0"), 0); - await expect(anna).has.a.balanceOf("50", reth, "RETH"); - await expect(anna).has.a.balanceOf("1100", usds, "USDC"); - }); - - it("Should redeem same at a low oracle v2", async () => { - const { ousd, vault, usds, reth, anna } = fixture; - - await setOracleTokenPriceUsd("RETHETH", "2.0"); - await reth.setExchangeRate(usdsUnits("2.0")); - - await reth.connect(anna).mint(usdsUnits("100.0")); - await reth.connect(anna).approve(vault.address, usdsUnits("100.0")); - await vault.connect(anna).mint(reth.address, usdsUnits("100.0"), 0); - await expect(anna).has.a.balanceOf("200", ousd, "post mint"); - await vault.rebase(); - await expect(anna).has.a.balanceOf("200", ousd, "post rebase"); - - // Contains 100 rETH, (200 units) and 200 USDS (200 units) - // After Oracles $100 + $200 = $300 - // - // Redeeming $150 == 1/2 vault - // 50rETH and 100 USDS - - await setOracleTokenPriceUsd("RETHETH", "1.0"); - await reth.setExchangeRate(usdsUnits("1.0")); - - await vault.connect(anna).redeem(usdsUnits("150.0"), 0); - await expect(anna).has.a.approxBalanceOf("50", reth, "RETH"); - await expect(anna).has.a.approxBalanceOf("1100", usds, "USDC"); - }); - - it("Should handle an exchange rate reedem attack", async () => { - const { ousd, vault, reth, anna, matt, governor } = fixture; - - await setOracleTokenPriceUsd("RETHETH", "2.0"); - await reth.setExchangeRate(usdsUnits("2.0")); - - // Old holder with RETH - await reth.connect(matt).mint(usdsUnits("500.0")); - await reth.connect(matt).approve(vault.address, usdsUnits("500.0")); - await vault.connect(matt).mint(reth.address, usdsUnits("500.0"), 0); - - // Attacker Mints before exchange change - await reth.connect(anna).mint(usdsUnits("500.0")); - await reth.connect(anna).approve(vault.address, usdsUnits("500.0")); - await vault.connect(anna).mint(reth.address, usdsUnits("500.0"), 0); - await expect(anna).has.a.balanceOf("1000", ousd, "post mint"); - - await setOracleTokenPriceUsd("RETHETH", "1.0"); - await reth.setExchangeRate(usdsUnits("1.0")); - - // console.log("----"); - // console.log((await vault.totalValue()).toString() / 1e18); - // console.log((await ousd.totalSupply()).toString() / 1e18); - - // Attacker redeems after exchange change - await vault.connect(governor).setMaxSupplyDiff(usdsUnits("0.9")); - await expect( - vault.connect(anna).redeem(usdsUnits("1000.0"), 0) - ).to.be.revertedWith("Backing supply liquidity error"); - - // console.log((await vault.totalValue()).toString() / 1e18); - // console.log((await ousd.totalSupply()).toString() / 1e18); - }); - - it("Should handle an exchange rate reedem attack, delayed oracle", async () => { - const { ousd, vault, reth, anna, matt, governor } = fixture; - - await setOracleTokenPriceUsd("RETHETH", "2.0"); - await reth.setExchangeRate(usdsUnits("2.0")); - - // Old holder with RETH - await reth.connect(matt).mint(usdsUnits("500.0")); - await reth.connect(matt).approve(vault.address, usdsUnits("500.0")); - await vault.connect(matt).mint(reth.address, usdsUnits("500.0"), 0); - - // Attacker Mints before exchange change - await reth.connect(anna).mint(usdsUnits("500.0")); - await reth.connect(anna).approve(vault.address, usdsUnits("500.0")); - await vault.connect(anna).mint(reth.address, usdsUnits("500.0"), 0); - await expect(anna).has.a.balanceOf("1000", ousd, "post mint"); - - await setOracleTokenPriceUsd("RETHETH", "1.3"); - await reth.setExchangeRate(usdsUnits("1.0")); - - // console.log("----"); - // console.log((await vault.totalValue()).toString() / 1e18); - // console.log((await ousd.totalSupply()).toString() / 1e18); - - // Attacker redeems after exchange change - await vault.connect(governor).setMaxSupplyDiff(usdsUnits("0.9")); - await expect( - vault.connect(anna).redeem(usdsUnits("1000.0"), 0) - ).to.be.revertedWith("Backing supply liquidity error"); - - // console.log((await vault.totalValue()).toString() / 1e18); - // console.log((await ousd.totalSupply()).toString() / 1e18); - }); -}); diff --git a/contracts/test/vault/index.js b/contracts/test/vault/index.js index 8c0728ef9e..7b9d80ad4f 100644 --- a/contracts/test/vault/index.js +++ b/contracts/test/vault/index.js @@ -1,18 +1,8 @@ const { expect } = require("chai"); -const hre = require("hardhat"); const { utils } = require("ethers"); const { loadDefaultFixture } = require("../_fixture"); -const { - ousdUnits, - usdsUnits, - usdcUnits, - usdtUnits, - tusdUnits, - setOracleTokenPriceUsd, - getOracleAddresses, - isFork, -} = require("../helpers"); +const { ousdUnits, usdsUnits, usdcUnits, isFork } = require("../helpers"); describe("Vault", function () { if (isFork) { @@ -21,52 +11,16 @@ describe("Vault", function () { let fixture; beforeEach(async () => { fixture = await loadDefaultFixture(); + await fixture.compoundStrategy + .connect(fixture.governor) + .setPTokenAddress(fixture.usdc.address, fixture.cusdc.address); }); it("Should support an asset", async () => { - const { vault, oracleRouter, ousd, governor } = fixture; - - const oracleAddresses = await getOracleAddresses(hre.deployments); - const origAssetCount = await vault.connect(governor).getAssetCount(); - expect(await vault.isSupportedAsset(ousd.address)).to.be.false; - - /* Mock oracle feeds report 0 for updatedAt data point. Set - * maxStaleness to 100 years from epoch to make the Oracle - * feeds valid - */ - const maxStaleness = 24 * 60 * 60 * 365 * 100; - - await oracleRouter.setFeed( - ousd.address, - oracleAddresses.chainlink.USDS_USD, - maxStaleness - ); - await oracleRouter.cacheDecimals(ousd.address); - await expect(vault.connect(governor).supportAsset(ousd.address, 0)).to.emit( - vault, - "AssetSupported" - ); - expect(await vault.getAssetCount()).to.equal(origAssetCount.add(1)); - const assets = await vault.connect(governor).getAllAssets(); - expect(assets.length).to.equal(origAssetCount.add(1)); - expect(await vault["checkBalance(address)"](ousd.address)).to.equal(0); - expect(await vault.isSupportedAsset(ousd.address)).to.be.true; - }); - - it("Should revert when adding an asset that is already supported", async function () { - const { vault, usdt, governor } = fixture; - - expect(await vault.isSupportedAsset(usdt.address)).to.be.true; - await expect( - vault.connect(governor).supportAsset(usdt.address, 0) - ).to.be.revertedWith("Asset already supported"); - }); + const { vault, usdc, usds } = fixture; - it("Should revert when attempting to support an asset and not governor", async function () { - const { vault, usdt } = fixture; - await expect(vault.supportAsset(usdt.address, 0)).to.be.revertedWith( - "Caller is not the Governor" - ); + expect(await vault.isSupportedAsset(usds.address)).to.be.false; + expect(await vault.isSupportedAsset(usdc.address)).to.be.true; }); it("Should revert when adding a strategy that is already approved", async function () { @@ -87,111 +41,22 @@ describe("Vault", function () { }); it("Should correctly ratio deposited currencies of differing decimals", async function () { - const { ousd, vault, usdc, usds, matt } = fixture; - + const { ousd, vault, usdc, matt } = fixture; await expect(matt).has.a.balanceOf("100.00", ousd); // Matt deposits USDC, 6 decimals await usdc.connect(matt).approve(vault.address, usdcUnits("2.0")); await vault.connect(matt).mint(usdc.address, usdcUnits("2.0"), 0); await expect(matt).has.a.balanceOf("102.00", ousd); - - // Matt deposits USDS, 18 decimals - await usds.connect(matt).approve(vault.address, usdsUnits("4.0")); - await vault.connect(matt).mint(usds.address, usdsUnits("4.0"), 0); - await expect(matt).has.a.balanceOf("106.00", ousd); - }); - - it("Should correctly handle a deposit of USDS (18 decimals)", async function () { - const { ousd, vault, usds, anna } = fixture; - - await expect(anna).has.a.balanceOf("0.00", ousd); - // We limit to paying to $1 OUSD for for one stable coin, - // so this will deposit at a rate of $1. - await setOracleTokenPriceUsd("USDS", "1.30"); - await usds.connect(anna).approve(vault.address, usdsUnits("3.0")); - await vault.connect(anna).mint(usds.address, usdsUnits("3.0"), 0); - await expect(anna).has.a.balanceOf("3.00", ousd); }); it("Should correctly handle a deposit of USDC (6 decimals)", async function () { const { ousd, vault, usdc, anna } = fixture; await expect(anna).has.a.balanceOf("0.00", ousd); - await setOracleTokenPriceUsd("USDC", "0.998"); await usdc.connect(anna).approve(vault.address, usdcUnits("50.0")); await vault.connect(anna).mint(usdc.address, usdcUnits("50.0"), 0); - await expect(anna).has.a.balanceOf("49.90", ousd); - }); - - it("Should not allow a below peg deposit", async function () { - const { ousd, vault, usdc, anna } = fixture; - - await expect(anna).has.a.balanceOf("0.00", ousd); - await setOracleTokenPriceUsd("USDC", "0.95"); - await usdc.connect(anna).approve(vault.address, usdcUnits("50.0")); - await expect( - vault.connect(anna).mint(usdc.address, usdcUnits("50.0"), 0) - ).to.be.revertedWith("Asset price below peg"); - }); - - it("Should correctly handle a deposit failure of Non-Standard ERC20 Token", async function () { - const { ousd, vault, anna, nonStandardToken, oracleRouter, governor } = - fixture; - - await oracleRouter.cacheDecimals(nonStandardToken.address); - await vault.connect(governor).supportAsset(nonStandardToken.address, 0); - await expect(anna).has.a.balanceOf("1000.00", nonStandardToken); - await setOracleTokenPriceUsd("NonStandardToken", "1.30"); - await nonStandardToken - .connect(anna) - .approve(vault.address, usdtUnits("1500.0")); - - // Anna has a balance of 1000 tokens and she is trying to - // transfer 1500 tokens. The contract doesn't throw but - // fails silently, so Anna's OUSD balance should be zero. - try { - await vault - .connect(anna) - .mint(nonStandardToken.address, usdtUnits("1500.0"), 0); - } catch (err) { - expect( - /reverted with reason string 'SafeERC20: ERC20 operation did not succeed/gi.test( - err.message - ) - ).to.be.true; - } finally { - // Make sure nothing got affected - await expect(anna).has.a.balanceOf("0.00", ousd); - await expect(anna).has.a.balanceOf("1000.00", nonStandardToken); - } - }); - - it("Should correctly handle a deposit of Non-Standard ERC20 Token", async function () { - const { ousd, vault, anna, nonStandardToken, oracleRouter, governor } = - fixture; - - await oracleRouter.cacheDecimals(nonStandardToken.address); - await vault.connect(governor).supportAsset(nonStandardToken.address, 0); - - await expect(anna).has.a.balanceOf("1000.00", nonStandardToken); - await setOracleTokenPriceUsd("NonStandardToken", "1.00"); - - await nonStandardToken - .connect(anna) - .approve(vault.address, usdtUnits("100.0")); - await vault - .connect(anna) - .mint(nonStandardToken.address, usdtUnits("100.0"), 0); - await expect(anna).has.a.balanceOf("100.00", ousd); - await expect(anna).has.a.balanceOf("900.00", nonStandardToken); - }); - - it("Should calculate the balance correctly with USDS", async () => { - const { vault } = fixture; - - // Vault already has USDS from default ficture - expect(await vault.totalValue()).to.equal(utils.parseUnits("200", 18)); + await expect(anna).has.a.balanceOf("50.00", ousd); }); it("Should calculate the balance correctly with USDC", async () => { @@ -204,46 +69,6 @@ describe("Vault", function () { expect(await vault.totalValue()).to.equal(utils.parseUnits("202", 18)); }); - it("Should calculate the balance correctly with USDT", async () => { - const { vault, usdt, matt } = fixture; - - // Matt deposits USDT, 6 decimals - await usdt.connect(matt).approve(vault.address, usdtUnits("5.0")); - await vault.connect(matt).mint(usdt.address, usdtUnits("5.0"), 0); - // Fixture loads 200 USDS, so result should be 205 - expect(await vault.totalValue()).to.equal(utils.parseUnits("205", 18)); - }); - - it("Should calculate the balance correctly with TUSD", async () => { - const { vault, tusd, matt } = fixture; - - await tusd.connect(matt).mint(ousdUnits("100.0")); - - // Matt deposits TUSD, 18 decimals - await tusd.connect(matt).approve(vault.address, tusdUnits("9.0")); - await vault.connect(matt).mint(tusd.address, tusdUnits("9.0"), 0); - // Fixture loads 200 USDS, so result should be 209 - expect(await vault.totalValue()).to.equal(utils.parseUnits("209", 18)); - }); - - it("Should calculate the balance correctly with USDS, USDC, USDT, TUSD", async () => { - const { vault, usdc, usdt, tusd, matt } = fixture; - - await tusd.connect(matt).mint(ousdUnits("100.0")); - - // Matt deposits USDC, 6 decimals - await usdc.connect(matt).approve(vault.address, usdcUnits("8.0")); - await vault.connect(matt).mint(usdc.address, usdcUnits("8.0"), 0); - // Matt deposits USDT, 6 decimals - await usdt.connect(matt).approve(vault.address, usdtUnits("20.0")); - await vault.connect(matt).mint(usdt.address, usdtUnits("20.0"), 0); - // Matt deposits TUSD, 18 decimals - await tusd.connect(matt).approve(vault.address, tusdUnits("9.0")); - await vault.connect(matt).mint(tusd.address, tusdUnits("9.0"), 0); - // Fixture loads 200 USDS, so result should be 237 - expect(await vault.totalValue()).to.equal(utils.parseUnits("237", 18)); - }); - it("Should allow transfer of arbitrary token by Governor", async () => { const { vault, ousd, usdc, matt, governor } = fixture; @@ -274,7 +99,7 @@ describe("Vault", function () { // Governor cannot move USDC because it is a supported token. await expect( vault.connect(governor).transferToken(usdc.address, ousdUnits("8.0")) - ).to.be.revertedWith("Only unsupported assets"); + ).to.be.revertedWith("Only unsupported backingAsset"); }); it("Should allow Governor to add Strategy", async () => { @@ -306,13 +131,10 @@ describe("Vault", function () { }); it("Should revert mint if minMintAmount check fails", async () => { - const { vault, matt, ousd, usds, usdt } = fixture; - - await usdt.connect(matt).approve(vault.address, usdtUnits("50.0")); - await usds.connect(matt).approve(vault.address, usdsUnits("25.0")); + const { vault, matt, ousd, usdc } = fixture; await expect( - vault.connect(matt).mint(usdt.address, usdtUnits("50"), usdsUnits("100")) + vault.connect(matt).mint(usdc.address, usdcUnits("50"), ousdUnits("100")) ).to.be.revertedWith("Mint amount lower than minimum"); await expect(matt).has.a.balanceOf("100.00", ousd); @@ -370,61 +192,57 @@ describe("Vault", function () { }); it("Should allow the Governor to call withdraw and then deposit", async () => { - const { vault, governor, usds, josh, compoundStrategy } = fixture; + const { vault, governor, usdc, josh, compoundStrategy } = fixture; await vault.connect(governor).approveStrategy(compoundStrategy.address); - // Send all USDS to Compound - await vault - .connect(governor) - .setAssetDefaultStrategy(usds.address, compoundStrategy.address); - await usds.connect(josh).approve(vault.address, usdsUnits("200")); - await vault.connect(josh).mint(usds.address, usdsUnits("200"), 0); + // Send all USDC to Compound + await vault.connect(governor).setDefaultStrategy(compoundStrategy.address); + await usdc.connect(josh).approve(vault.address, usdcUnits("200")); + await vault.connect(josh).mint(usdc.address, usdcUnits("200"), 0); await vault.connect(governor).allocate(); await vault .connect(governor) .withdrawFromStrategy( compoundStrategy.address, - [usds.address], - [usdsUnits("200")] + [usdc.address], + [usdcUnits("200")] ); await vault .connect(governor) .depositToStrategy( compoundStrategy.address, - [usds.address], - [usdsUnits("200")] + [usdc.address], + [usdcUnits("200")] ); }); it("Should allow the Strategist to call withdrawFromStrategy and then depositToStrategy", async () => { - const { vault, governor, usds, josh, strategist, compoundStrategy } = + const { vault, governor, usdc, josh, strategist, compoundStrategy } = fixture; await vault.connect(governor).approveStrategy(compoundStrategy.address); - // Send all USDS to Compound - await vault - .connect(governor) - .setAssetDefaultStrategy(usds.address, compoundStrategy.address); - await usds.connect(josh).approve(vault.address, usdsUnits("200")); - await vault.connect(josh).mint(usds.address, usdsUnits("200"), 0); + // Send all USDC to Compound + await vault.connect(governor).setDefaultStrategy(compoundStrategy.address); + await usdc.connect(josh).approve(vault.address, usdcUnits("200")); + await vault.connect(josh).mint(usdc.address, usdcUnits("200"), 0); await vault.connect(governor).allocate(); await vault .connect(strategist) .withdrawFromStrategy( compoundStrategy.address, - [usds.address], - [usdsUnits("200")] + [usdc.address], + [usdcUnits("200")] ); await vault .connect(strategist) .depositToStrategy( compoundStrategy.address, - [usds.address], - [usdsUnits("200")] + [usdc.address], + [usdcUnits("200")] ); }); @@ -449,35 +267,14 @@ describe("Vault", function () { }); it("Should withdrawFromStrategy the correct amount for multiple assests and redeploy them using depositToStrategy", async () => { - const { - vault, - governor, - usds, - usdc, - cusdc, - josh, - strategist, - compoundStrategy, - } = fixture; + const { vault, governor, usdc, josh, strategist, compoundStrategy } = + fixture; await vault.connect(governor).approveStrategy(compoundStrategy.address); - // Send all USDS to Compound - await vault - .connect(governor) - .setAssetDefaultStrategy(usds.address, compoundStrategy.address); - - // Add USDC - await compoundStrategy - .connect(governor) - .setPTokenAddress(usdc.address, cusdc.address); // Send all USDC to Compound - await vault - .connect(governor) - .setAssetDefaultStrategy(usdc.address, compoundStrategy.address); + await vault.connect(governor).setDefaultStrategy(compoundStrategy.address); - await usds.connect(josh).approve(vault.address, usdsUnits("200")); - await vault.connect(josh).mint(usds.address, usdsUnits("200"), 0); await usdc.connect(josh).approve(vault.address, usdcUnits("90")); await vault.connect(josh).mint(usdc.address, usdcUnits("90"), 0); await vault.connect(governor).allocate(); @@ -486,15 +283,11 @@ describe("Vault", function () { .connect(strategist) .withdrawFromStrategy( compoundStrategy.address, - [usds.address, usdc.address], - [usdsUnits("50"), usdcUnits("90")] + [usdc.address], + [usdcUnits("90")] ); // correct balances at the end - const expectedVaultUsdsBalance = usdsUnits("50"); - expect(await usds.balanceOf(vault.address)).to.equal( - expectedVaultUsdsBalance - ); const expectedVaultUsdcBalance = usdcUnits("90"); expect(await usdc.balanceOf(vault.address)).to.equal( expectedVaultUsdcBalance @@ -504,12 +297,11 @@ describe("Vault", function () { .connect(strategist) .depositToStrategy( compoundStrategy.address, - [usds.address, usdc.address], - [usdsUnits("50"), usdcUnits("90")] + [usdc.address], + [usdcUnits("90")] ); // correct balances after depositing back - expect(await usds.balanceOf(vault.address)).to.equal(usdsUnits("0")); expect(await usdc.balanceOf(vault.address)).to.equal(usdcUnits("0")); }); @@ -544,19 +336,17 @@ describe("Vault", function () { }); it("Should only allow Governor and Strategist to call withdrawAllFromStrategy", async () => { - const { vault, governor, strategist, compoundStrategy, matt, josh, usds } = + const { vault, governor, strategist, compoundStrategy, matt, josh, usdc } = fixture; await vault.connect(governor).approveStrategy(compoundStrategy.address); - // Get the vault's initial USDS balance. - const vaultUsdsBalance = await usds.balanceOf(vault.address); + // Get the vault's initial USDC balance. + const vaultUsdcBalance = await usdc.balanceOf(vault.address); - // Mint and allocate USDS to Compound. - await vault - .connect(governor) - .setAssetDefaultStrategy(usds.address, compoundStrategy.address); - await usds.connect(josh).approve(vault.address, usdsUnits("200")); - await vault.connect(josh).mint(usds.address, usdsUnits("200"), 0); + // Mint and allocate USDC to Compound. + await vault.connect(governor).setDefaultStrategy(compoundStrategy.address); + await usdc.connect(josh).approve(vault.address, usdcUnits("200")); + await vault.connect(josh).mint(usdc.address, usdcUnits("200"), 0); await vault.connect(governor).allocate(); // Call to withdrawAll by the governor should go thru. @@ -564,9 +354,9 @@ describe("Vault", function () { .connect(governor) .withdrawAllFromStrategy(compoundStrategy.address); - // All the USDS should have been moved back to the vault. - const expectedVaultUsdsBalance = vaultUsdsBalance.add(usdsUnits("200")); - await expect(await usds.balanceOf(vault.address)).to.equal( + // All the USDC should have been moved back to the vault. + const expectedVaultUsdsBalance = vaultUsdcBalance.add(usdcUnits("200")); + await expect(await usdc.balanceOf(vault.address)).to.equal( expectedVaultUsdsBalance ); @@ -580,68 +370,4 @@ describe("Vault", function () { vault.connect(matt).withdrawAllFromStrategy(compoundStrategy.address) ).to.be.revertedWith("Caller is not the Strategist or Governor"); }); - - it("Should only allow metastrategy to mint oTokens and revert when threshold is reached.", async () => { - const { vault, ousd, governor, anna, josh } = fixture; - - await vault - .connect(governor) - .setNetOusdMintForStrategyThreshold(ousdUnits("10")); - // Approve anna address as an address allowed to mint OUSD without backing - await vault.connect(governor).setOusdMetaStrategy(anna.address); - - await expect( - vault.connect(anna).mintForStrategy(ousdUnits("11")) - ).to.be.revertedWith( - "Minted ousd surpassed netOusdMintForStrategyThreshold." - ); - - await expect( - vault.connect(josh).mintForStrategy(ousdUnits("9")) - ).to.be.revertedWith("Caller is not the OUSD meta strategy"); - - await vault.connect(anna).mintForStrategy(ousdUnits("9")); - - await expect(await ousd.balanceOf(anna.address)).to.equal(ousdUnits("9")); - }); - - it("Should reset netOusdMintedForStrategy when new threshold is set", async () => { - const { vault, governor, anna } = fixture; - - await vault - .connect(governor) - .setNetOusdMintForStrategyThreshold(ousdUnits("10")); - - // Approve anna address as an address allowed to mint OUSD without backing - await vault.connect(governor).setOusdMetaStrategy(anna.address); - await vault.connect(anna).mintForStrategy(ousdUnits("9")); - - // netOusdMintedForStrategy should be equal to amount minted - await expect(await vault.netOusdMintedForStrategy()).to.equal( - ousdUnits("9") - ); - - await vault - .connect(governor) - .setNetOusdMintForStrategyThreshold(ousdUnits("10")); - - // netOusdMintedForStrategy should be reset back to 0 - await expect(await vault.netOusdMintedForStrategy()).to.equal( - ousdUnits("0") - ); - }); - it("Should re-cache decimals", async () => { - const { vault, governor, usdc } = fixture; - - const beforeAssetConfig = await vault.getAssetConfig(usdc.address); - expect(beforeAssetConfig.decimals).to.equal(6); - - // cacheDecimals is not on IVault so we need to use the admin contract - const vaultAdmin = await ethers.getContractAt("VaultAdmin", vault.address); - - await vaultAdmin.connect(governor).cacheDecimals(usdc.address); - - const afterAssetConfig = await vault.getAssetConfig(usdc.address); - expect(afterAssetConfig.decimals).to.equal(6); - }); }); diff --git a/contracts/test/vault/oeth-vault.js b/contracts/test/vault/oeth-vault.js index a9056f88ac..2a0d0e2836 100644 --- a/contracts/test/vault/oeth-vault.js +++ b/contracts/test/vault/oeth-vault.js @@ -1,5 +1,4 @@ const { expect } = require("chai"); -const hre = require("hardhat"); const { createFixtureLoader, oethDefaultFixture } = require("../_fixture"); const { parseUnits } = require("ethers/lib/utils"); @@ -155,7 +154,7 @@ describe("OETH Vault", function () { await oethVault.connect(governor).approveStrategy(mockStrategy.address); await oethVault .connect(governor) - .setAssetDefaultStrategy(weth.address, mockStrategy.address); + .setDefaultStrategy(mockStrategy.address); const fixtureWithUser = { ...fixture, user: domen }; const dataBefore = await snapData(fixtureWithUser); @@ -208,18 +207,6 @@ describe("OETH Vault", function () { } }); - it("Fail to calculateRedeemOutputs if WETH index isn't cached", async () => { - const { frxETH, weth } = fixture; - - await deployWithConfirmation("MockOETHVault", [weth.address]); - const mockVault = await hre.ethers.getContract("MockOETHVault"); - - await mockVault.supportAsset(frxETH.address); - - const tx = mockVault.calculateRedeemOutputs(oethUnits("12343")); - await expect(tx).to.be.revertedWith("WETH Asset index not cached"); - }); - it("Should update total supply correctly without redeem fee", async () => { const { oethVault, oeth, weth, daniel } = fixture; await oethVault.connect(daniel).mint(weth.address, oethUnits("10"), "0"); @@ -275,7 +262,7 @@ describe("OETH Vault", function () { await oethVault.connect(governor).approveStrategy(mockStrategy.address); await oethVault .connect(governor) - .setAssetDefaultStrategy(weth.address, mockStrategy.address); + .setDefaultStrategy(mockStrategy.address); // Mint some WETH await weth.connect(domen).approve(oethVault.address, oethUnits("10000")); @@ -314,39 +301,6 @@ describe("OETH Vault", function () { }); describe("Config", () => { - it("Should allow caching WETH index", async () => { - const { oethVault, weth, governor } = fixture; - - await oethVault.connect(governor).cacheWETHAssetIndex(); - - const index = (await oethVault.wethAssetIndex()).toNumber(); - - const assets = await oethVault.getAllAssets(); - - expect(assets[index]).to.equal(weth.address); - }); - - it("Fail to allow anyone other than Governor to change cached index", async () => { - const { oethVault, strategist } = fixture; - - const tx = oethVault.connect(strategist).cacheWETHAssetIndex(); - await expect(tx).to.be.revertedWith("Caller is not the Governor"); - }); - - it("Fail to cacheWETHAssetIndex if WETH is not a supported asset", async () => { - const { frxETH, weth } = fixture; - const { deployerAddr } = await hre.getNamedAccounts(); - const sDeployer = hre.ethers.provider.getSigner(deployerAddr); - - await deployWithConfirmation("MockOETHVault", [weth.address]); - const mockVault = await hre.ethers.getContract("MockOETHVault"); - - await mockVault.supportAsset(frxETH.address); - - const tx = mockVault.connect(sDeployer).cacheWETHAssetIndex(); - await expect(tx).to.be.revertedWith("Invalid WETH Asset Index"); - }); - it("Should return all strategies", async () => { // Mostly to increase coverage @@ -366,78 +320,6 @@ describe("OETH Vault", function () { }); describe("Remove Asset", () => { - it("Should allow removing a single asset", async () => { - const { oethVault, frxETH, governor } = fixture; - - const vaultAdmin = await ethers.getContractAt( - "OETHVaultAdmin", - oethVault.address - ); - const assetCount = (await oethVault.getAssetCount()).toNumber(); - - const tx = await oethVault.connect(governor).removeAsset(frxETH.address); - - await expect(tx) - .to.emit(vaultAdmin, "AssetRemoved") - .withArgs(frxETH.address); - await expect(tx) - .to.emit(vaultAdmin, "AssetDefaultStrategyUpdated") - .withArgs(frxETH.address, addresses.zero); - - expect(await oethVault.isSupportedAsset(frxETH.address)).to.be.false; - expect(await oethVault.checkBalance(frxETH.address)).to.equal(0); - expect(await oethVault.assetDefaultStrategies(frxETH.address)).to.equal( - addresses.zero - ); - - const allAssets = await oethVault.getAllAssets(); - expect(allAssets.length).to.equal(assetCount - 1); - - expect(allAssets).to.not.contain(frxETH.address); - - const config = await oethVault.getAssetConfig(frxETH.address); - expect(config.isSupported).to.be.false; - }); - - it("Should only allow governance to remove assets", async () => { - const { oethVault, weth, strategist, josh } = fixture; - - for (const signer of [strategist, josh]) { - let tx = oethVault.connect(signer).removeAsset(weth.address); - await expect(tx).to.be.revertedWith("Caller is not the Governor"); - - tx = oethVault.connect(signer).removeAsset(weth.address); - await expect(tx).to.be.revertedWith("Caller is not the Governor"); - } - }); - - it("Fail to remove asset if asset is not supported", async () => { - const { oethVault, usds, governor } = fixture; - const tx = oethVault.connect(governor).removeAsset(usds.address); - - await expect(tx).to.be.revertedWith("Asset not supported"); - }); - - it("Fail to remove asset if vault still holds the asset", async () => { - const { oethVault, weth, governor, daniel } = fixture; - - await oethVault.connect(daniel).mint(weth.address, oethUnits("1"), "0"); - - const tx = oethVault.connect(governor).removeAsset(weth.address); - - await expect(tx).to.be.revertedWith("Vault still holds asset"); - }); - - it("Fail to revert for smaller dust", async () => { - const { oethVault, weth, governor, daniel } = fixture; - - await oethVault.connect(daniel).mint(weth.address, "500000000000", "0"); - - const tx = oethVault.connect(governor).removeAsset(weth.address); - - await expect(tx).to.not.be.revertedWith("Vault still holds asset"); - }); - it("Should allow strategy to burnForStrategy", async () => { const { oethVault, oeth, weth, governor, daniel } = fixture; @@ -446,11 +328,6 @@ describe("OETH Vault", function () { .connect(governor) .addStrategyToMintWhitelist(daniel.address); - // First increase netOusdMintForStrategyThreshold - await oethVault - .connect(governor) - .setNetOusdMintForStrategyThreshold(oethUnits("100")); - // Then mint for strategy await oethVault.connect(daniel).mint(weth.address, oethUnits("10"), "0"); @@ -534,7 +411,7 @@ describe("OETH Vault", function () { .approveStrategy(mockStrategy.address); await oethVault .connect(await impersonateAndFund(await oethVault.governor())) - .setAssetDefaultStrategy(weth.address, mockStrategy.address); + .setDefaultStrategy(mockStrategy.address); // Mint will allocate all to default strategy bc no buffer, no threshold await oethVault.connect(daniel).mint(weth.address, oethUnits("10"), "0"); @@ -575,14 +452,14 @@ describe("OETH Vault", function () { let mockStrategy; beforeEach(async () => { // Deploy default strategy - const { oethVault, weth } = fixture; + const { oethVault } = fixture; mockStrategy = await deployWithConfirmation("MockStrategy"); await oethVault .connect(await impersonateAndFund(await oethVault.governor())) .approveStrategy(mockStrategy.address); await oethVault .connect(await impersonateAndFund(await oethVault.governor())) - .setAssetDefaultStrategy(weth.address, mockStrategy.address); + .setDefaultStrategy(mockStrategy.address); }); it("buffer is 0%, 0 WETH in queue", async () => { const { oethVault, daniel, weth } = fixture; @@ -1119,7 +996,9 @@ describe("OETH Vault", function () { [weth.address], [depositAmount] ); - await expect(tx).to.be.revertedWith("Not enough WETH available"); + await expect(tx).to.be.revertedWith( + "Not enough backing asset available" + ); }); it("Fail to deposit allocated WETH during allocate", async () => { const { oethVault, governor, weth } = fixture; @@ -1127,7 +1006,7 @@ describe("OETH Vault", function () { // Set mock strategy as default strategy await oethVault .connect(governor) - .setAssetDefaultStrategy(weth.address, mockStrategy.address); + .setDefaultStrategy(mockStrategy.address); // and buffer to 10% await oethVault.connect(governor).setVaultBuffer(oethUnits("0.1")); @@ -1647,7 +1526,9 @@ describe("OETH Vault", function () { [oethUnits("1")] ); - await expect(tx).to.be.revertedWith("Not enough WETH available"); + await expect(tx).to.be.revertedWith( + "Not enough backing asset available" + ); }); it("Fail to allocate any WETH to the default strategy", async () => { const { oethVault, domen } = fixture; @@ -1690,7 +1571,9 @@ describe("OETH Vault", function () { [oethUnits("1.1")] ); - await expect(tx).to.be.revertedWith("Not enough WETH available"); + await expect(tx).to.be.revertedWith( + "Not enough backing asset available" + ); }); it("Fail to allocate any WETH to the default strategy", async () => { const { oethVault, domen } = fixture; @@ -1733,14 +1616,16 @@ describe("OETH Vault", function () { [oethUnits("5")] ); - await expect(tx).to.be.revertedWith("Not enough WETH available"); + await expect(tx).to.be.revertedWith( + "Not enough backing asset available" + ); }); it("Should allocate 3 WETH to the default strategy", async () => { const { oethVault, governor, domen, weth } = fixture; await oethVault .connect(governor) - .setAssetDefaultStrategy(weth.address, mockStrategy.address); + .setDefaultStrategy(mockStrategy.address); const vaultBalance = await weth.balanceOf(oethVault.address); const stratBalance = await weth.balanceOf(mockStrategy.address); @@ -1965,7 +1850,7 @@ describe("OETH Vault", function () { await oethVault.connect(governor).approveStrategy(mockStrategy.address); await oethVault .connect(governor) - .setAssetDefaultStrategy(weth.address, mockStrategy.address); + .setDefaultStrategy(mockStrategy.address); // Mint 60 OETH to three users await oethVault @@ -2125,7 +2010,7 @@ describe("OETH Vault", function () { await oethVault.connect(governor).approveStrategy(mockStrategy.address); await oethVault .connect(governor) - .setAssetDefaultStrategy(weth.address, mockStrategy.address); + .setDefaultStrategy(mockStrategy.address); // Mint 100 OETH to three users await oethVault diff --git a/contracts/test/vault/oeth-vault.mainnet.fork-test.js b/contracts/test/vault/oeth-vault.mainnet.fork-test.js index 1d71e4535f..20bbdca525 100644 --- a/contracts/test/vault/oeth-vault.mainnet.fork-test.js +++ b/contracts/test/vault/oeth-vault.mainnet.fork-test.js @@ -49,14 +49,6 @@ describe("ForkTest: OETH Vault", function () { ); } }); - - it("Should have correct WETH asset index cached", async () => { - const { oethVault, weth } = fixture; - const index = await oethVault.wethAssetIndex(); - const assets = await oethVault.getAllAssets(); - - expect(assets[index]).to.equal(weth.address); - }); }); describe("user operations", () => { @@ -105,15 +97,7 @@ describe("ForkTest: OETH Vault", function () { }); it("should mint with WETH and allocate to strategy", async () => { - const { oethVault, nativeStakingSSVStrategy, weth, josh, strategist } = - fixture; - - oethVault - .connect(strategist) - .setAssetDefaultStrategy( - weth.address, - nativeStakingSSVStrategy.address - ); + const { oethVault, weth, josh } = fixture; const amount = parseUnits("11", 18); const minOeth = parseUnits("8", 18); @@ -155,26 +139,20 @@ describe("ForkTest: OETH Vault", function () { const { oethVault } = fixture; const output = await oethVault.calculateRedeemOutputs(oethUnits("123")); - const index = await oethVault.wethAssetIndex(); + const index = 0; + expect(output.length).to.equal(1); expect(output[index]).to.equal(oethUnits("123").mul("9990").div("10000")); - - output.map((x, i) => { - if (i !== index.toNumber()) { - expect(x).to.equal("0"); - } - }); }); it("should allow strategist to redeem without fee", async () => { const { oethVault, strategist, matt, weth, oeth } = fixture; + await depositDiffInWeth(fixture, matt); const sGovernor = await impersonateAndFund(addresses.mainnet.Timelock); // make sure to not trigger rebase on redeem await oethVault.connect(sGovernor).setRebaseThreshold(oethUnits("11")); - // Send a heap of WETH to the vault so it can be redeemed - await weth.connect(matt).transfer(oethVault.address, oethUnits("1000")); await weth.connect(matt).transfer(strategist.address, oethUnits("100")); const amount = oethUnits("10"); @@ -199,9 +177,7 @@ describe("ForkTest: OETH Vault", function () { it("should enforce fee on other users for instant redeem", async () => { const { oethVault, josh, matt, weth, oeth } = fixture; - - // Send a heap of WETH to the vault so it can be redeemed - await weth.connect(matt).transfer(oethVault.address, oethUnits("1000")); + await depositDiffInWeth(fixture, matt); const amount = oethUnits("10"); const expectedWETH = amount.mul("9990").div("10000"); @@ -225,7 +201,8 @@ describe("ForkTest: OETH Vault", function () { }); it("should partially redeem 10 OETH", async () => { - const { domen, oeth, oethVault, weth } = fixture; + const { domen, oeth, oethVault, weth, matt } = fixture; + await depositDiffInWeth(fixture, matt); expect(await oeth.balanceOf(oethWhaleAddress)).to.gt(10); @@ -287,10 +264,10 @@ describe("ForkTest: OETH Vault", function () { }); }); it("should claim withdraw by a OETH whale", async () => { - const { domen, oeth, oethVault, weth } = fixture; + const { domen, oeth, oethVault, weth, matt } = fixture; let oethWhaleBalance = await oeth.balanceOf(oethWhaleAddress); - + await depositDiffInWeth(fixture, matt); // Calculate how much to mint based on the WETH in the vault, // the withdrawal queue, and the WETH to be withdrawn const wethBalance = await weth.balanceOf(oethVault.address); @@ -326,13 +303,14 @@ describe("ForkTest: OETH Vault", function () { .withArgs(oethWhaleAddress, requestId, oethWhaleBalance); }); it("OETH whale can redeem after withdraw from all strategies", async () => { - const { oeth, oethVault, timelock } = fixture; + const { oeth, oethVault, timelock, matt } = fixture; const oethWhaleBalance = await oeth.balanceOf(oethWhaleAddress); log(`OETH whale balance: ${formatUnits(oethWhaleBalance)}`); expect(oethWhaleBalance, "no longer an OETH whale").to.gt( parseUnits("1000", 18) ); + await depositDiffInWeth(fixture, matt); await oethVault.connect(timelock).withdrawAllFromStrategies(); @@ -362,6 +340,28 @@ describe("ForkTest: OETH Vault", function () { }); }); + /** + * Checks the difference between withdrawalQueueMetadata[0] and [2] + * and deposits this diff in WETH. + * @param {Object} fixture + * @param {Object} depositor signer to perform the deposit + */ + async function depositDiffInWeth(fixture, depositor) { + const { oethVault, weth } = fixture; + + // Get withdrawalQueueMetadata[0] and [2] + const metadata = await oethVault.withdrawalQueueMetadata(); + const queue = metadata.queued; + const claimed = metadata.claimed; + + if (queue > claimed) { + const diff = queue.sub(claimed).mul(110).div(100); + await weth.connect(depositor).approve(oethVault.address, diff); + return await oethVault.connect(depositor).mint(weth.address, diff, 0); + } + return null; + } + // We have migrated to simplified Harvester and this is no longer relevant // shouldHaveRewardTokensConfigured(() => ({ // vault: fixture.oethVault, diff --git a/contracts/test/vault/oethb-vault.base.fork-test.js b/contracts/test/vault/oethb-vault.base.fork-test.js index 2dc81d2782..32e230db8b 100644 --- a/contracts/test/vault/oethb-vault.base.fork-test.js +++ b/contracts/test/vault/oethb-vault.base.fork-test.js @@ -58,7 +58,10 @@ describe("ForkTest: OETHb Vault", function () { // Add WETH liquidity to allow redeem await weth .connect(rafael) - .transfer(oethbVault.address, oethUnits("3000")); + .approve(oethbVault.address, oethUnits("10000")); + await oethbVault + .connect(rafael) + .mint(weth.address, oethUnits("10000"), 0); await oethbVault.rebase(); await _mint(strategist); @@ -90,7 +93,10 @@ describe("ForkTest: OETHb Vault", function () { // Add WETH liquidity to allow redeem await weth .connect(rafael) - .transfer(oethbVault.address, oethUnits("3000")); + .approve(oethbVault.address, oethUnits("10000")); + await oethbVault + .connect(rafael) + .mint(weth.address, oethUnits("10000"), 0); await oethbVault.rebase(); await _mint(governor); @@ -137,7 +143,10 @@ describe("ForkTest: OETHb Vault", function () { // Add WETH liquidity to allow withdrawal await weth .connect(rafael) - .transfer(oethbVault.address, oethUnits("3000")); + .approve(oethbVault.address, oethUnits("10000")); + await oethbVault + .connect(rafael) + .mint(weth.address, oethUnits("10000"), 0); const delayPeriod = await oethbVault.withdrawalClaimDelay(); diff --git a/contracts/test/vault/oneinch-swapper.js b/contracts/test/vault/oneinch-swapper.js deleted file mode 100644 index f8ae6466d6..0000000000 --- a/contracts/test/vault/oneinch-swapper.js +++ /dev/null @@ -1,524 +0,0 @@ -const { expect } = require("chai"); -const { utils, BigNumber } = require("ethers"); - -const { units, usdsUnits, usdtUnits } = require("../helpers"); -const { - createFixtureLoader, - oethCollateralSwapFixture, - ousdCollateralSwapFixture, - oeth1InchSwapperFixture, -} = require("../_fixture"); -const { - SWAP_SELECTOR, - UNISWAP_SELECTOR, - UNISWAPV3_SELECTOR, -} = require("../../utils/1Inch"); -const { impersonateAndFund } = require("../../utils/signers"); - -const log = require("../../utils/logger")("test:oeth:swapper"); - -describe("1Inch Swapper", () => { - describe("No OETH Collateral Swaps", () => { - let fixture; - const loadFixture = createFixtureLoader(oethCollateralSwapFixture); - beforeEach(async () => { - fixture = await loadFixture(); - }); - - it("Should revert stETH to WETH swap", async () => { - const { weth, stETH, oethVault, strategist } = fixture; - const fromAmount = utils.parseEther("100"); - const toAmount = utils.parseEther("100"); - - // Call swap method - const tx = oethVault - .connect(strategist) - .swapCollateral(stETH.address, weth.address, fromAmount, toAmount, []); - - await expect(tx).to.be.revertedWith("Collateral swap not supported"); - }); - - it("Should revert stETH to WETH swap", async () => { - const { stETH, weth, oethVault, strategist } = fixture; - const fromAmount = utils.parseEther("100"); - const toAmount = utils.parseEther("100"); - - // Call swap method - const tx = oethVault - .connect(strategist) - .swapCollateral(stETH.address, weth.address, fromAmount, toAmount, []); - - await expect(tx).to.be.revertedWith("Collateral swap not supported"); - }); - }); - describe("OUSD Collateral Swaps", () => { - let fixture; - const loadFixture = createFixtureLoader(ousdCollateralSwapFixture); - beforeEach(async () => { - fixture = await loadFixture(); - }); - - it("Should allow Governor to set slippage for assets", async () => { - const { usds, governor, vault } = fixture; - - const tx = vault.connect(governor).setOracleSlippage(usds.address, 123); - await expect(tx) - .to.emit(vault, "SwapSlippageChanged") - .withArgs(usds.address, 123); - }); - - it("Should not allow Governor to set slippage for unsupported assets", async () => { - const { governor, vault, weth } = fixture; - - const tx = vault.connect(governor).setOracleSlippage(weth.address, 123); - await expect(tx).to.be.revertedWith("Asset not supported"); - }); - - it("Should not allow anyone else to set slippage for assets", async () => { - const { usds, strategist, josh, vault } = fixture; - - for (const user of [strategist, josh]) { - const tx = vault.connect(user).setOracleSlippage(usds.address, 123); - await expect(tx).to.be.revertedWith("Caller is not the Governor"); - } - }); - - it("Should not allow Governor to set slippage above 10%", async () => { - const { usds, governor, vault } = fixture; - - const tx = vault.connect(governor).setOracleSlippage(usds.address, 1100); - await expect(tx).to.be.revertedWith("Slippage too high"); - }); - - it("Should allow to change Swapper address", async () => { - const { governor, vault, weth } = fixture; - - // Pretend WETH is swapper address - const tx = vault.connect(governor).setSwapper(weth.address); - - await expect(tx).to.emit(vault, "SwapperChanged").withArgs(weth.address); - - expect(await vault.swapper()).to.equal(weth.address); - }); - - it("Should not allow anyone else to set swapper address", async () => { - const { strategist, josh, vault, weth } = fixture; - - for (const user of [strategist, josh]) { - const tx = vault.connect(user).setSwapper(weth.address); - await expect(tx).to.be.revertedWith("Caller is not the Governor"); - } - }); - - it("Should allow the governor to change allowed swap undervalue", async () => { - const { governor, vault } = fixture; - - const tx = vault.connect(governor).setSwapAllowedUndervalue(10); - - await expect(tx) - .to.emit(vault, "SwapAllowedUndervalueChanged") - .withArgs(10); - - expect(await vault.allowedSwapUndervalue()).to.equal(10); - }); - - it("Should not allow anyone else to set allowed swap undervalue", async () => { - const { strategist, josh, vault } = fixture; - - for (const user of [strategist, josh]) { - const tx = vault.connect(user).setSwapAllowedUndervalue(10); - await expect(tx).to.be.revertedWith("Caller is not the Governor"); - } - }); - - it("Should allow the governor to set allowed swap undervalue to 100%", async () => { - const { governor, vault } = fixture; - - const hundredPercent = 10000; - const tx = vault - .connect(governor) - .setSwapAllowedUndervalue(hundredPercent); - - await expect(tx) - .to.emit(vault, "SwapAllowedUndervalueChanged") - .withArgs(hundredPercent); - - expect(await vault.allowedSwapUndervalue()).to.equal(hundredPercent); - }); - - it("Should not allow setting undervalue percentage over 100%", async () => { - const { governor, vault } = fixture; - - const tx = vault.connect(governor).setSwapAllowedUndervalue(10001); - - await expect(tx).to.be.revertedWith("Invalid basis points"); - }); - - it("Should allow to swap tokens", async () => { - const { usds, usdc, usdt, vault, strategist } = fixture; - - for (const fromAsset of [usds, usdc, usdt]) { - for (const toAsset of [usds, usdc, usdt]) { - if (fromAsset.address === toAsset.address) continue; - const fromAmount = await units("20", fromAsset); - const toAmount = await units("21", toAsset); - log( - `swapping 20 ${await fromAsset.symbol()} to ${await toAsset.symbol()}` - ); - expect(await fromAsset.balanceOf(vault.address)).to.gte(fromAmount); - - // Call swap method - const tx = await vault - .connect(strategist) - .swapCollateral( - fromAsset.address, - toAsset.address, - fromAmount, - toAmount, - [] - ); - - expect(tx) - .to.emit(vault, "Swapped") - .withArgs(fromAsset.address, toAsset.address, fromAmount, toAmount); - } - } - }); - - it("Should revert swap if received less tokens than strategist desired", async () => { - const { usds, usdt, vault, strategist, mockSwapper } = fixture; - - // Mock to return lower than slippage next time - await mockSwapper.connect(strategist).setNextOutAmount(usdsUnits("18")); - - const fromAmount = usdtUnits("20"); - const toAmount = usdsUnits("20"); - - // Call swap method - const tx = vault - .connect(strategist) - .swapCollateral(usdt.address, usds.address, fromAmount, toAmount, []); - - await expect(tx).to.be.revertedWith("Strategist slippage limit"); - }); - - it("Should revert swap if received less tokens than Oracle slippage", async () => { - const { usds, usdt, vault, strategist } = fixture; - - const fromAmount = usdtUnits("20"); - const toAmount = usdsUnits("16"); - - // Call swap method - const tx = vault - .connect(strategist) - .swapCollateral(usdt.address, usds.address, fromAmount, toAmount, []); - - await expect(tx).to.be.revertedWith("Oracle slippage limit exceeded"); - }); - - it("Should revert swap if value is under supply", async () => { - const { usds, usdt, oeth, vault, governor, strategist, mockSwapper } = - fixture; - - // Mock to return lower than slippage next time - await mockSwapper - .connect(strategist) - .setNextOutAmount(utils.parseEther("180")); - // increase the allowed Oracle slippage per asset to 9.99% - await vault.connect(governor).setOracleSlippage(usds.address, 999); - await vault.connect(governor).setOracleSlippage(usdt.address, 999); - - const fromAmount = usdtUnits("200"); - const toAmount = usdsUnits("170"); - - log(`total supply: ${await oeth.totalSupply()}`); - log(`total value : ${await vault.totalValue()}`); - - // Call swap method - const tx = vault - .connect(strategist) - .swapCollateral(usdt.address, usds.address, fromAmount, toAmount, []); - - await expect(tx).to.be.revertedWith("Allowed value < supply"); - - log(`total supply: ${await oeth.totalSupply()}`); - log(`total value : ${await vault.totalValue()}`); - }); - - it("Should allow swap if value is under supply by less than the allowed percentage", async () => { - const { usds, usdt, oeth, vault, governor, strategist, mockSwapper } = - fixture; - - // Mock to return lower than slippage next time - await mockSwapper.connect(strategist).setNextOutAmount(usdsUnits("19")); - // increase the allowed Oracle slippage per asset to 9.99% - await vault.connect(governor).setOracleSlippage(usds.address, 999); - await vault.connect(governor).setOracleSlippage(usdt.address, 999); - - const fromAmount = usdtUnits("20"); - const toAmount = usdsUnits("17"); - - log(`total supply: ${await oeth.totalSupply()}`); - log(`total value : ${await vault.totalValue()}`); - - // Call swap method - const tx = await vault - .connect(strategist) - .swapCollateral(usdt.address, usds.address, fromAmount, toAmount, []); - - await expect(tx).to.emit(vault, "Swapped"); - - log(`total supply: ${await oeth.totalSupply()}`); - log(`total value : ${await vault.totalValue()}`); - }); - - it("Should revert if fromAsset is not supported", async () => { - const { usds, weth, vault, strategist } = fixture; - const fromAmount = utils.parseEther("100"); - const toAmount = usdsUnits("100"); - - // Call swap method - const tx = vault - .connect(strategist) - .swapCollateral(weth.address, usds.address, fromAmount, toAmount, []); - - await expect(tx).to.be.revertedWith("From asset is not supported"); - }); - - it("Should revert if toAsset is not supported", async () => { - const { weth, usds, vault, strategist } = fixture; - const fromAmount = usdsUnits("100"); - const toAmount = utils.parseEther("100"); - - // Call swap method - const tx = vault - .connect(strategist) - .swapCollateral(usds.address, weth.address, fromAmount, toAmount, []); - - await expect(tx).to.be.revertedWith("To asset is not supported"); - }); - - it("Should swap if capital is paused", async () => { - const { usds, usdt, vault, strategist } = fixture; - const fromAmount = usdsUnits("100"); - const toAmount = usdtUnits("100"); - - // Fund Vault with some assets - const vaultSigner = await impersonateAndFund(vault.address); - await usds.connect(vaultSigner).mint(fromAmount); - - await vault.connect(strategist).pauseCapital(); - - // Call swap method - const tx = await vault - .connect(strategist) - .swapCollateral(usds.address, usdt.address, fromAmount, toAmount, []); - - expect(tx).to.emit(vault, "Swapped"); - }); - - it("Should revert if not called by Governor or Strategist", async () => { - const { usds, usdt, vault, josh } = fixture; - const fromAmount = usdsUnits("100"); - const toAmount = usdtUnits("100"); - - // Call swap method - const tx = vault - .connect(josh) - .swapCollateral(usds.address, usdt.address, fromAmount, toAmount, []); - - await expect(tx).to.be.revertedWith( - "Caller is not the Strategist or Governor" - ); - }); - }); - - describe.skip("1inch Swapper", () => { - let fixture; - const loadFixture = createFixtureLoader(oeth1InchSwapperFixture); - beforeEach(async () => { - fixture = await loadFixture(); - }); - - it("Should swap assets using 1inch router", async () => { - const { swapper1Inch, strategist, weth, frxETH, mock1InchSwapRouter } = - fixture; - - const deadAddr = "0x1111111111222222222233333333334444444444"; - - const data = utils.defaultAbiCoder.encode( - ["bytes4", "address", "bytes"], - [utils.arrayify(SWAP_SELECTOR), deadAddr, utils.arrayify("0xdead")] - ); - - const fromAmount = utils.parseEther("100"); - const toAmount = utils.parseEther("100"); - - await weth - .connect(strategist) - .mintTo(swapper1Inch.address, fromAmount.mul(2)); - await frxETH - .connect(strategist) - .mintTo(mock1InchSwapRouter.address, toAmount.mul(2)); - - const tx = swapper1Inch - .connect(strategist) - .swap(weth.address, frxETH.address, fromAmount, toAmount, data); - - await expect(tx) - .to.emit(mock1InchSwapRouter, "MockSwapDesc") - .withArgs( - weth.address, - frxETH.address, - deadAddr, - strategist.address, - fromAmount, - toAmount, - 4 - ); - - await expect(tx).to.emit( - mock1InchSwapRouter, - "MockSwap" - // ).withArgs( - // deadAddr, - // ['0', 'x'], - // utils.arrayify("0xdead") - ); - - const r = await (await tx).wait(); - expect(r.logs[3].data).to.equal( - "0x00000000000000000000000011111111112222222222333333333344444444440000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002dead000000000000000000000000000000000000000000000000000000000000" - ); - }); - - it("Should swap assets using Uniswap executor", async () => { - const { swapper1Inch, strategist, weth, frxETH, mock1InchSwapRouter } = - fixture; - - const data = utils.defaultAbiCoder.encode( - ["bytes4", "uint256[]"], - [ - utils.arrayify(UNISWAP_SELECTOR), - [BigNumber.from("123"), BigNumber.from("456")], - ] - ); - - const fromAmount = utils.parseEther("100"); - const toAmount = utils.parseEther("100"); - - await weth - .connect(strategist) - .mintTo(swapper1Inch.address, fromAmount.mul(2)); - await frxETH - .connect(strategist) - .mintTo(swapper1Inch.address, toAmount.mul(2)); - - const tx = swapper1Inch - .connect(strategist) - .swap(weth.address, frxETH.address, fromAmount, toAmount, data); - - await expect(tx) - .to.emit(mock1InchSwapRouter, "MockUnoswapTo") - .withArgs(strategist.address, weth.address, fromAmount, toAmount, [ - BigNumber.from("123"), - BigNumber.from("456"), - ]); - }); - - it("Should swap assets using Uniswap V3 executor", async () => { - const { swapper1Inch, strategist, weth, frxETH, mock1InchSwapRouter } = - fixture; - - const data = utils.defaultAbiCoder.encode( - ["bytes4", "uint256[]"], - [ - utils.arrayify(UNISWAPV3_SELECTOR), - [BigNumber.from("123"), BigNumber.from("456")], - ] - ); - - const fromAmount = utils.parseEther("100"); - const toAmount = utils.parseEther("100"); - - await weth - .connect(strategist) - .mintTo(swapper1Inch.address, fromAmount.mul(2)); - await frxETH - .connect(strategist) - .mintTo(swapper1Inch.address, toAmount.mul(2)); - - const tx = swapper1Inch - .connect(strategist) - .swap(weth.address, frxETH.address, fromAmount, toAmount, data); - - await expect(tx) - .to.emit(mock1InchSwapRouter, "MockUniswapV3SwapTo") - .withArgs(strategist.address, fromAmount, toAmount, [ - BigNumber.from("123"), - BigNumber.from("456"), - ]); - }); - - it("Should revert swap if fromAsset is insufficient ", async () => { - const { swapper1Inch, strategist, weth, frxETH } = fixture; - - const deadAddr = "0x1111111111222222222233333333334444444444"; - - const data = utils.defaultAbiCoder.encode( - ["bytes4", "address", "bytes"], - [utils.arrayify(SWAP_SELECTOR), deadAddr, utils.arrayify("0xdead")] - ); - - const fromAmount = utils.parseEther("100"); - const toAmount = utils.parseEther("100"); - - await frxETH - .connect(strategist) - .mintTo(swapper1Inch.address, toAmount.mul(2)); - - const tx = swapper1Inch - .connect(strategist) - .swap(weth.address, frxETH.address, fromAmount, toAmount, data); - - await expect(tx).to.be.revertedWith( - "ERC20: transfer amount exceeds balance" - ); - }); - - it("Should revert swap if router allowance is insufficient ", async () => { - const { swapper1Inch, strategist, weth, frxETH, mock1InchSwapRouter } = - fixture; - - const deadAddr = "0x1111111111222222222233333333334444444444"; - - const data = utils.defaultAbiCoder.encode( - ["bytes4", "address", "bytes"], - [utils.arrayify(SWAP_SELECTOR), deadAddr, utils.arrayify("0xdead")] - ); - - const fromAmount = utils.parseEther("100"); - const toAmount = utils.parseEther("100"); - - await weth - .connect(strategist) - .mintTo(swapper1Inch.address, toAmount.mul(2)); - await frxETH - .connect(strategist) - .mintTo(swapper1Inch.address, toAmount.mul(2)); - - // Reset allowance - await weth - .connect(await impersonateAndFund(swapper1Inch.address)) - .approve(mock1InchSwapRouter.address, 0); - - const tx = swapper1Inch - .connect(strategist) - .swap(weth.address, frxETH.address, fromAmount, toAmount, data); - - await expect(tx).to.be.revertedWith( - "ERC20: transfer amount exceeds allowance" - ); - }); - }); -}); diff --git a/contracts/test/vault/rebase.js b/contracts/test/vault/rebase.js index 67fe996539..c2aff4e085 100644 --- a/contracts/test/vault/rebase.js +++ b/contracts/test/vault/rebase.js @@ -3,11 +3,7 @@ const { expect } = require("chai"); const { loadDefaultFixture } = require("../_fixture"); const { ousdUnits, - usdsUnits, usdcUnits, - usdtUnits, - tusdUnits, - getOracleAddress, setOracleTokenPriceUsd, expectApproxSupply, } = require("../helpers"); @@ -169,24 +165,16 @@ describe("Vault rebase", () => { }); it("Should not allocate unallocated assets when no Strategy configured", async () => { - const { anna, governor, usds, usdc, usdt, tusd, vault } = fixture; + const { anna, governor, usdc, vault } = fixture; - await usds.connect(anna).transfer(vault.address, usdsUnits("100")); - await usdc.connect(anna).transfer(vault.address, usdcUnits("200")); - await usdt.connect(anna).transfer(vault.address, usdtUnits("300")); - await tusd.connect(anna).mintTo(vault.address, tusdUnits("400")); + await usdc.connect(anna).transfer(vault.address, usdcUnits("100")); expect(await vault.getStrategyCount()).to.equal(0); await vault.connect(governor).allocate(); - // All assets should still remain in Vault - - // Note defaultFixture sets up with 200 USDS already in the Strategy + // Note defaultFixture sets up with 200 USDC already in the Strategy // 200 + 100 = 300 - expect(await usds.balanceOf(vault.address)).to.equal(usdsUnits("300")); - expect(await usdc.balanceOf(vault.address)).to.equal(usdcUnits("200")); - expect(await usdt.balanceOf(vault.address)).to.equal(usdtUnits("300")); - expect(await tusd.balanceOf(vault.address)).to.equal(tusdUnits("400")); + expect(await usdc.balanceOf(vault.address)).to.equal(usdcUnits("300")); }); it("Should correctly handle a deposit of USDC (6 decimals)", async function () { @@ -199,24 +187,6 @@ describe("Vault rebase", () => { await vault.connect(anna).mint(usdc.address, usdcUnits("50"), 0); await expect(anna).has.a.balanceOf("50", ousd); }); - - it("Should allow priceProvider to be changed", async function () { - const { anna, governor, vault } = fixture; - - const oracle = await getOracleAddress(deployments); - await expect(await vault.priceProvider()).to.be.equal(oracle); - const annaAddress = await anna.getAddress(); - await vault.connect(governor).setPriceProvider(annaAddress); - await expect(await vault.priceProvider()).to.be.equal(annaAddress); - - // Only governor should be able to set it - await expect( - vault.connect(anna).setPriceProvider(oracle) - ).to.be.revertedWith("Caller is not the Governor"); - - await vault.connect(governor).setPriceProvider(oracle); - await expect(await vault.priceProvider()).to.be.equal(oracle); - }); }); describe("Vault yield accrual to OGN", async () => { @@ -230,7 +200,7 @@ describe("Vault rebase", () => { const { _yield, basis, expectedFee } = options; it(`should collect on rebase a ${expectedFee} fee from ${_yield} yield at ${basis}bp `, async function () { - const { matt, governor, ousd, usdt, vault, mockNonRebasing } = fixture; + const { matt, governor, ousd, usdc, vault, mockNonRebasing } = fixture; const trustee = mockNonRebasing; // Setup trustee on vault @@ -239,8 +209,8 @@ describe("Vault rebase", () => { await expect(trustee).has.a.balanceOf("0", ousd); // Create yield for the vault - await usdt.connect(matt).mint(usdcUnits(_yield)); - await usdt.connect(matt).transfer(vault.address, usdcUnits(_yield)); + await usdc.connect(matt).mint(usdcUnits(_yield)); + await usdc.connect(matt).transfer(vault.address, usdcUnits(_yield)); // Do rebase const supplyBefore = await ousd.totalSupply(); await vault.rebase(); diff --git a/contracts/test/vault/redeem.js b/contracts/test/vault/redeem.js index 5715cc9f51..146f619882 100644 --- a/contracts/test/vault/redeem.js +++ b/contracts/test/vault/redeem.js @@ -1,19 +1,17 @@ const { expect } = require("chai"); -const { BigNumber } = require("ethers"); - const { loadDefaultFixture } = require("../_fixture"); const { ousdUnits, - usdsUnits, usdcUnits, - usdtUnits, - setOracleTokenPriceUsd, isFork, expectApproxSupply, + advanceTime, } = require("../helpers"); +const { impersonateAndFund } = require("../../utils/signers"); +const { deployWithConfirmation } = require("../../utils/deploy"); -describe("Vault Redeem", function () { +describe("OUSD Vault Redeem", function () { if (isFork) { this.timeout(0); } @@ -23,420 +21,1925 @@ describe("Vault Redeem", function () { fixture = await loadDefaultFixture(); }); - it("Should allow a redeem", async () => { - const { ousd, vault, usdc, anna, usds } = fixture; - - await expect(anna).has.a.balanceOf("1000.00", usdc); - await expect(anna).has.a.balanceOf("1000.00", usds); - await usdc.connect(anna).approve(vault.address, usdcUnits("50.0")); - await vault.connect(anna).mint(usdc.address, usdcUnits("50.0"), 0); - await expect(anna).has.a.balanceOf("50.00", ousd); - await vault.connect(anna).redeem(ousdUnits("50.0"), 0); - await expect(anna).has.a.balanceOf("0.00", ousd); - // Redeem outputs will be 50/250 * 50 USDC and 200/250 * 50 USDS from fixture - await expect(anna).has.a.balanceOf("960.00", usdc); - await expect(anna).has.a.balanceOf("1040.00", usds); + describe("Redeem", function () {}); + + it("Should allow a redeem for strategist", async () => { + const { ousd, vault, usdc, strategist } = fixture; + + await expect(strategist).has.a.balanceOf("1000.00", usdc); + await usdc.connect(strategist).approve(vault.address, usdcUnits("50.0")); + await vault.connect(strategist).mint(usdc.address, usdcUnits("50.0"), 0); + await expect(strategist).has.a.balanceOf("50.00", ousd); + await vault.connect(strategist).redeem(ousdUnits("50.0"), 0); + await expect(strategist).has.a.balanceOf("0.00", ousd); + await expect(strategist).has.a.balanceOf("1000.00", usdc); expect(await ousd.totalSupply()).to.eq(ousdUnits("200.0")); + + it("Should allow a redeem over the rebase threshold for strategist", async () => { + const { ousd, vault, usdc, strategist, matt } = fixture; + + await expect(strategist).has.a.balanceOf("1000.00", usdc); + + await expect(strategist).has.a.balanceOf("0.00", ousd); + await expect(matt).has.a.balanceOf("100.00", ousd); + + // Strategist mints OUSD with USDC + await usdc + .connect(strategist) + .approve(vault.address, usdcUnits("1000.00")); + await vault + .connect(strategist) + .mint(usdc.address, usdcUnits("1000.00"), 0); + await expect(strategist).has.a.balanceOf("1000.00", ousd); + await expect(matt).has.a.balanceOf("100.00", ousd); + + // Rebase should do nothing + await vault.rebase(); + await expect(strategist).has.a.balanceOf("1000.00", ousd); + await expect(matt).has.a.balanceOf("100.00", ousd); + + // Strategist redeems over the rebase threshold + await vault.connect(strategist).redeem(ousdUnits("500.0"), 0); + await expect(strategist).has.a.approxBalanceOf("500.00", ousd); + await expect(matt).has.a.approxBalanceOf("100.00", ousd); + + // Redeem outputs will be 1000/2200 * 1500 USDC and 1200/2200 * 1500 USDS from fixture + await expect(strategist).has.an.approxBalanceOf("500.00", usdc); + await expectApproxSupply(ousd, ousdUnits("700.0")); + }); + + it("Should have a default redeem fee of 0", async () => { + const { vault } = fixture; + + await expect(await vault.redeemFeeBps()).to.equal("0"); + }); + + // Skipped because OUSD redeem is only available for strategist or governor + // and this is without fees. + it.skip("Should charge a redeem fee if redeem fee set", async () => { + const { ousd, vault, usdc, anna, governor } = fixture; + + // 1000 basis points = 10% + await vault.connect(governor).setRedeemFeeBps(1000); + await expect(anna).has.a.balanceOf("1000.00", usdc); + await usdc.connect(anna).approve(vault.address, usdcUnits("50.0")); + await vault.connect(anna).mint(usdc.address, usdcUnits("50.0"), 0); + await expect(anna).has.a.balanceOf("50.00", ousd); + await vault.connect(anna).redeem(ousdUnits("50.0"), 0); + await expect(anna).has.a.balanceOf("0.00", ousd); + await expect(anna).has.a.balanceOf("995.00", usdc); + }); + + it("Should revert redeem if balance is insufficient", async () => { + const { ousd, vault, usdc, strategist } = fixture; + + // Mint some OUSD tokens + await expect(strategist).has.a.balanceOf("1000.00", usdc); + await usdc.connect(strategist).approve(vault.address, usdcUnits("50.0")); + await vault.connect(strategist).mint(usdc.address, usdcUnits("50.0"), 0); + await expect(strategist).has.a.balanceOf("50.00", ousd); + + // Try to withdraw more than balance + await expect( + vault.connect(strategist).redeem(ousdUnits("100.0"), 0) + ).to.be.revertedWith("Transfer amount exceeds balance"); + }); + + it("Should only allow Governor to set a redeem fee", async () => { + const { vault, anna } = fixture; + + await expect(vault.connect(anna).setRedeemFeeBps(100)).to.be.revertedWith( + "Caller is not the Governor" + ); + }); + + it("Should redeem entire OUSD balance", async () => { + const { ousd, vault, usdc, strategist } = fixture; + + await expect(strategist).has.a.balanceOf("1000.00", usdc); + + // Mint 100 OUSD tokens using USDC + await usdc.connect(strategist).approve(vault.address, usdcUnits("100.0")); + await vault.connect(strategist).mint(usdc.address, usdcUnits("100.0"), 0); + await expect(strategist).has.a.balanceOf("100.00", ousd); + + // Withdraw all + await vault + .connect(strategist) + .redeem(ousd.balanceOf(strategist.address), 0); + + await expect(strategist).has.a.balanceOf("1000", usdc); + }); + + it("Should have correct balances on consecutive mint and redeem", async () => { + const { ousd, vault, usdc, strategist, governor } = fixture; + + const usersWithBalances = [ + [strategist, 0], + [governor, 0], + ]; + + const assetsWithUnits = [[usdc, usdcUnits]]; + + for (const [user, startBalance] of usersWithBalances) { + for (const [asset, units] of assetsWithUnits) { + for (const amount of [5.09, 10.32, 20.99, 100.01]) { + await asset + .connect(user) + .approve(vault.address, await units(amount.toString())); + await vault + .connect(user) + .mint(asset.address, await units(amount.toString()), 0); + await expect(user).has.an.approxBalanceOf( + (startBalance + amount).toString(), + ousd + ); + await vault.connect(user).redeem(ousdUnits(amount.toString()), 0); + await expect(user).has.an.approxBalanceOf( + startBalance.toString(), + ousd + ); + } + } + } + }); + + it("Should correctly handle redeem without a rebase and then full redeem", async function () { + const { ousd, vault, usdc, strategist } = fixture; + await expect(strategist).has.a.balanceOf("0.00", ousd); + await usdc.connect(strategist).mint(usdcUnits("3000.0")); + await usdc + .connect(strategist) + .approve(vault.address, usdcUnits("3000.0")); + await vault + .connect(strategist) + .mint(usdc.address, usdcUnits("3000.0"), 0); + await expect(strategist).has.a.balanceOf("3000.00", ousd); + + //redeem without rebasing (not over threshold) + await vault.connect(strategist).redeem(ousdUnits("200.00"), 0); + //redeem with rebasing (over threshold) + await vault + .connect(strategist) + .redeem(ousd.balanceOf(strategist.address), 0); + + await expect(strategist).has.a.balanceOf("0.00", ousd); + }); + + it("Should respect minimum unit amount argument in redeem", async () => { + const { ousd, vault, usdc, strategist } = fixture; + + await expect(strategist).has.a.balanceOf("1000.00", usdc); + await usdc.connect(strategist).approve(vault.address, usdcUnits("100.0")); + await vault.connect(strategist).mint(usdc.address, usdcUnits("50.0"), 0); + await expect(strategist).has.a.balanceOf("50.00", ousd); + await vault + .connect(strategist) + .redeem(ousdUnits("50.0"), usdcUnits("50")); + await vault.connect(strategist).mint(usdc.address, usdcUnits("50.0"), 0); + await expect( + vault.connect(strategist).redeem(ousdUnits("50.0"), usdcUnits("51")) + ).to.be.revertedWith("Redeem amount lower than minimum"); + }); + + it("Should calculate redeem outputs", async () => { + const { vault, anna, usdc, ousd } = fixture; + + // OUSD total supply is 200 backed by 200 USDC + expect((await vault.calculateRedeemOutputs(ousdUnits("50")))[0]).to.equal( + usdcUnits("50") + ); + + await usdc.connect(anna).approve(vault.address, usdcUnits("600")); + await vault.connect(anna).mint(usdc.address, usdcUnits("600"), 0); + await expect(anna).has.a.balanceOf("600", ousd); + + expect( + (await vault.calculateRedeemOutputs(ousdUnits("100")))[0] + ).to.equal(usdcUnits("100")); + }); }); - it("Should allow a redeem over the rebase threshold", async () => { - const { ousd, vault, usdc, anna, matt, usds } = fixture; + const snapData = async (fixture) => { + const { ousd, vault, usdc, user } = fixture; + + const ousdTotalSupply = await ousd.totalSupply(); + const ousdTotalValue = await vault.totalValue(); + const vaultCheckBalance = await vault.checkBalance(usdc.address); + const userOusd = await ousd.balanceOf(user.address); + const userUsdc = await usdc.balanceOf(user.address); + const vaultUsdc = await usdc.balanceOf(vault.address); + const queue = await vault.withdrawalQueueMetadata(); + + return { + ousdTotalSupply, + ousdTotalValue, + vaultCheckBalance, + userOusd, + userUsdc, + vaultUsdc, + queue, + }; + }; - await expect(anna).has.a.balanceOf("1000.00", usdc); - await expect(anna).has.a.balanceOf("1000.00", usds); + const assertChangedData = async (dataBefore, delta, fixture) => { + const { ousd, vault, usdc, user } = fixture; - await expect(anna).has.a.balanceOf("0.00", ousd); - await expect(matt).has.a.balanceOf("100.00", ousd); + expect(await ousd.totalSupply(), "OUSD Total Supply").to.equal( + dataBefore.ousdTotalSupply.add(delta.ousdTotalSupply) + ); + expect(await vault.totalValue(), "Vault Total Value").to.equal( + dataBefore.ousdTotalValue.add(delta.ousdTotalValue) + ); + expect( + await vault.checkBalance(usdc.address), + "Vault Check Balance of USDC" + ).to.equal(dataBefore.vaultCheckBalance.add(delta.vaultCheckBalance)); + expect(await ousd.balanceOf(user.address), "user's OUSD balance").to.equal( + dataBefore.userOusd.add(delta.userOusd) + ); + expect(await usdc.balanceOf(user.address), "user's USDC balance").to.equal( + dataBefore.userUsdc.add(delta.userUsdc) + ); + expect(await usdc.balanceOf(vault.address), "Vault USDC balance").to.equal( + dataBefore.vaultUsdc.add(delta.vaultUsdc) + ); - // Anna mints OUSD with USDC - await usdc.connect(anna).approve(vault.address, usdcUnits("1000.00")); - await vault.connect(anna).mint(usdc.address, usdcUnits("1000.00"), 0); - await expect(anna).has.a.balanceOf("1000.00", ousd); - await expect(matt).has.a.balanceOf("100.00", ousd); + const queueAfter = await vault.withdrawalQueueMetadata(); + expect(queueAfter.queued, "Queued").to.equal( + dataBefore.queue.queued.add(delta.queued) + ); + expect(queueAfter.claimable, "Claimable").to.equal( + dataBefore.queue.claimable.add(delta.claimable) + ); + expect(queueAfter.claimed, "Claimed").to.equal( + dataBefore.queue.claimed.add(delta.claimed) + ); + expect(queueAfter.nextWithdrawalIndex, "nextWithdrawalIndex").to.equal( + dataBefore.queue.nextWithdrawalIndex.add(delta.nextWithdrawalIndex) + ); + }; + + describe("Withdrawal Queue", function () { + const delayPeriod = 10 * 60; // 10 minutes + beforeEach(async () => { + const { vault, governor, strategist, josh, matt, usdc } = fixture; + await vault.connect(governor).setWithdrawalClaimDelay(delayPeriod); + + // In the fixture Matt and Josh mint 100 OUSD + // We should redeem that first to have only the 60 OUSD from USDC minting + // To do so, we have to make them strategists temporarily + await vault.connect(governor).setStrategistAddr(josh.address); + await vault.connect(josh).redeem(ousdUnits("100"), 0); + await vault.connect(governor).setStrategistAddr(matt.address); + await vault.connect(matt).redeem(ousdUnits("100"), 0); + await vault.connect(governor).setStrategistAddr(strategist.address); + // Then both send usdc to governor to keep internal balance correct + await usdc.connect(josh).transfer(governor.address, usdcUnits("100")); + await usdc.connect(matt).transfer(governor.address, usdcUnits("100")); + }); + describe("with all 60 USDC in the vault", () => { + beforeEach(async () => { + const { vault, usdc, daniel, josh, matt } = fixture; + + // Fund three users with USDC + await usdc.mintTo(daniel.address, usdcUnits("10")); + await usdc.mintTo(josh.address, usdcUnits("20")); + await usdc.mintTo(matt.address, usdcUnits("30")); + + // Approve vault to spend USDC + await usdc.connect(daniel).approve(vault.address, usdcUnits("10")); + await usdc.connect(josh).approve(vault.address, usdcUnits("20")); + await usdc.connect(matt).approve(vault.address, usdcUnits("30")); + + // Mint some OUSD to three users + await vault.connect(daniel).mint(usdc.address, usdcUnits("10"), "0"); + await vault.connect(josh).mint(usdc.address, usdcUnits("20"), "0"); + await vault.connect(matt).mint(usdc.address, usdcUnits("30"), "0"); + + // Set max supply diff to 3% to allow withdrawals + await vault + .connect(await impersonateAndFund(await vault.governor())) + .setMaxSupplyDiff(ousdUnits("0.03")); + }); + const firstRequestAmountOUSD = ousdUnits("5"); + const firstRequestAmountUSDC = usdcUnits("5"); + const secondRequestAmountOUSD = ousdUnits("18"); + const secondRequestAmountUSDC = usdcUnits("18"); + + // Positive Test + it("Should request first withdrawal by Daniel", async () => { + const { vault, daniel } = fixture; + const fixtureWithUser = { ...fixture, user: daniel }; + const dataBefore = await snapData(fixtureWithUser); + + const tx = await vault + .connect(daniel) + .requestWithdrawal(firstRequestAmountOUSD); + + await expect(tx) + .to.emit(vault, "WithdrawalRequested") + .withArgs( + daniel.address, + 0, + firstRequestAmountOUSD, + firstRequestAmountUSDC + ); - // Anna mints OUSD with USDS - await usds.connect(anna).approve(vault.address, usdsUnits("1000.00")); - await vault.connect(anna).mint(usds.address, usdsUnits("1000.00"), 0); - await expect(anna).has.a.balanceOf("2000.00", ousd); - await expect(matt).has.a.balanceOf("100.00", ousd); + await assertChangedData( + dataBefore, + { + ousdTotalSupply: firstRequestAmountOUSD.mul(-1), + ousdTotalValue: firstRequestAmountOUSD.mul(-1), + vaultCheckBalance: firstRequestAmountUSDC.mul(-1), + userOusd: firstRequestAmountOUSD.mul(-1), + userUsdc: 0, + vaultUsdc: 0, + queued: firstRequestAmountUSDC, + claimable: 0, + claimed: 0, + nextWithdrawalIndex: 1, + }, + fixtureWithUser + ); + }); + it("Should request withdrawal of zero amount", async () => { + const { vault, josh } = fixture; + const fixtureWithUser = { ...fixture, user: josh }; + await vault.connect(josh).requestWithdrawal(firstRequestAmountOUSD); + const dataBefore = await snapData(fixtureWithUser); + + const tx = await vault.connect(josh).requestWithdrawal(0); + + await expect(tx) + .to.emit(vault, "WithdrawalRequested") + .withArgs(josh.address, 1, 0, firstRequestAmountUSDC); + + await assertChangedData( + dataBefore, + { + ousdTotalSupply: 0, + ousdTotalValue: 0, + vaultCheckBalance: 0, + userOusd: 0, + userUsdc: 0, + vaultUsdc: 0, + queued: 0, + claimable: 0, + claimed: 0, + nextWithdrawalIndex: 1, + }, + fixtureWithUser + ); + }); + it("Should request first and second withdrawals with no USDC in the Vault", async () => { + const { vault, governor, josh, matt, usdc } = fixture; + const fixtureWithUser = { ...fixture, user: josh }; + + const mockStrategy = await deployWithConfirmation("MockStrategy"); + await vault.connect(governor).approveStrategy(mockStrategy.address); + + // Deposit all 10 + 20 + 30 = 60 USDC to strategy + await vault + .connect(governor) + .depositToStrategy( + mockStrategy.address, + [usdc.address], + [usdcUnits("60")] + ); - // Rebase should do nothing - await vault.rebase(); - await expect(anna).has.a.balanceOf("2000.00", ousd); - await expect(matt).has.a.balanceOf("100.00", ousd); + const dataBefore = await snapData(fixtureWithUser); - // Anna redeems over the rebase threshold - await vault.connect(anna).redeem(ousdUnits("1500.0"), 0); - await expect(anna).has.a.approxBalanceOf("500.00", ousd); - await expect(matt).has.a.approxBalanceOf("100.00", ousd); + await vault.connect(josh).requestWithdrawal(firstRequestAmountOUSD); + const tx = await vault + .connect(matt) + .requestWithdrawal(secondRequestAmountOUSD); - // Redeem outputs will be 1000/2200 * 1500 USDC and 1200/2200 * 1500 USDS from fixture - await expect(anna).has.an.approxBalanceOf("681.8181", usdc); - await expect(anna).has.a.approxBalanceOf("818.1818", usds); + await expect(tx) + .to.emit(vault, "WithdrawalRequested") + .withArgs( + matt.address, + 1, + secondRequestAmountOUSD, + firstRequestAmountUSDC.add(secondRequestAmountUSDC) + ); - await expectApproxSupply(ousd, ousdUnits("700.0")); - }); + await assertChangedData( + dataBefore, + { + ousdTotalSupply: firstRequestAmountOUSD + .add(secondRequestAmountOUSD) + .mul(-1), + ousdTotalValue: firstRequestAmountOUSD + .add(secondRequestAmountOUSD) + .mul(-1), + vaultCheckBalance: firstRequestAmountUSDC + .add(secondRequestAmountUSDC) + .mul(-1), + userOusd: firstRequestAmountOUSD.mul(-1), + userUsdc: 0, + vaultUsdc: 0, + queued: firstRequestAmountUSDC.add(secondRequestAmountUSDC), + claimable: 0, + claimed: 0, + nextWithdrawalIndex: 2, + }, + fixtureWithUser + ); + }); + it("Should request second withdrawal by matt", async () => { + const { vault, daniel, matt } = fixture; + const fixtureWithUser = { ...fixture, user: matt }; + await vault.connect(daniel).requestWithdrawal(firstRequestAmountOUSD); + const dataBefore = await snapData(fixtureWithUser); + + const tx = await vault + .connect(matt) + .requestWithdrawal(secondRequestAmountOUSD); + + await expect(tx) + .to.emit(vault, "WithdrawalRequested") + .withArgs( + matt.address, + 1, + secondRequestAmountOUSD, + firstRequestAmountUSDC.add(secondRequestAmountUSDC) + ); - it("Changing an asset price affects a redeem", async () => { - const { ousd, vault, usds, matt } = fixture; + await assertChangedData( + dataBefore, + { + ousdTotalSupply: secondRequestAmountOUSD.mul(-1), + ousdTotalValue: secondRequestAmountOUSD.mul(-1), + vaultCheckBalance: secondRequestAmountUSDC.mul(-1), + userOusd: secondRequestAmountOUSD.mul(-1), + userUsdc: 0, + vaultUsdc: 0, + queued: secondRequestAmountUSDC, + claimable: 0, + claimed: 0, + nextWithdrawalIndex: 1, + }, + fixtureWithUser + ); + }); + it("Should add claimable liquidity to the withdrawal queue", async () => { + const { vault, daniel, josh } = fixture; + const fixtureWithUser = { ...fixture, user: josh }; + await vault.connect(daniel).requestWithdrawal(firstRequestAmountOUSD); + await vault.connect(josh).requestWithdrawal(secondRequestAmountOUSD); + const dataBefore = await snapData(fixtureWithUser); + + const tx = await vault.connect(josh).addWithdrawalQueueLiquidity(); + + await expect(tx) + .to.emit(vault, "WithdrawalClaimable") + .withArgs( + firstRequestAmountUSDC.add(secondRequestAmountUSDC), + firstRequestAmountUSDC.add(secondRequestAmountUSDC) + ); - await expectApproxSupply(ousd, ousdUnits("200")); - await expect(matt).has.a.balanceOf("100.00", ousd); - await expect(matt).has.a.balanceOf("900.00", usds); + await assertChangedData( + dataBefore, + { + ousdTotalSupply: 0, + ousdTotalValue: 0, + vaultCheckBalance: 0, + userOusd: 0, + userUsdc: 0, + vaultUsdc: 0, + queued: 0, + claimable: firstRequestAmountUSDC.add(secondRequestAmountUSDC), + claimed: 0, + nextWithdrawalIndex: 0, + }, + fixtureWithUser + ); + }); + it("Should claim second request with enough liquidity", async () => { + const { vault, daniel, josh } = fixture; + const fixtureWithUser = { ...fixture, user: josh }; + await vault.connect(daniel).requestWithdrawal(firstRequestAmountOUSD); + await vault.connect(josh).requestWithdrawal(secondRequestAmountOUSD); + const requestId = 1; // ids start at 0 so the second request is at index 1 + const dataBefore = await snapData(fixtureWithUser); + + await advanceTime(delayPeriod); // Advance in time to ensure time delay between request and claim. + + const tx = await vault.connect(josh).claimWithdrawal(requestId); + + await expect(tx) + .to.emit(vault, "WithdrawalClaimed") + .withArgs(josh.address, requestId, secondRequestAmountOUSD); + await expect(tx) + .to.emit(vault, "WithdrawalClaimable") + .withArgs( + firstRequestAmountUSDC.add(secondRequestAmountUSDC), + firstRequestAmountUSDC.add(secondRequestAmountUSDC) + ); - await setOracleTokenPriceUsd("USDS", "1.25"); - await vault.rebase(); + await assertChangedData( + dataBefore, + { + ousdTotalSupply: 0, + ousdTotalValue: 0, + vaultCheckBalance: 0, + userOusd: 0, + userUsdc: secondRequestAmountUSDC, + vaultUsdc: secondRequestAmountUSDC.mul(-1), + queued: 0, + claimable: firstRequestAmountUSDC.add(secondRequestAmountUSDC), + claimed: secondRequestAmountUSDC, + nextWithdrawalIndex: 0, + }, + fixtureWithUser + ); + }); + it("Should claim multiple requests with enough liquidity", async () => { + const { vault, matt } = fixture; + const fixtureWithUser = { ...fixture, user: matt }; + await vault.connect(matt).requestWithdrawal(firstRequestAmountOUSD); + await vault.connect(matt).requestWithdrawal(secondRequestAmountOUSD); + const dataBefore = await snapData(fixtureWithUser); + + await advanceTime(delayPeriod); // Advance in time to ensure time delay between request and claim. + + const tx = await vault.connect(matt).claimWithdrawals([0, 1]); + + await expect(tx) + .to.emit(vault, "WithdrawalClaimed") + .withArgs(matt.address, 0, firstRequestAmountOUSD); + await expect(tx) + .to.emit(vault, "WithdrawalClaimed") + .withArgs(matt.address, 1, secondRequestAmountOUSD); + await expect(tx) + .to.emit(vault, "WithdrawalClaimable") + .withArgs( + firstRequestAmountUSDC.add(secondRequestAmountUSDC), + firstRequestAmountUSDC.add(secondRequestAmountUSDC) + ); - await vault.connect(matt).redeem(ousdUnits("2.0"), 0); - await expectApproxSupply(ousd, ousdUnits("198")); - // Amount of USDS collected is affected by redeem oracles - await expect(matt).has.a.approxBalanceOf("901.60", usds); - }); + await assertChangedData( + dataBefore, + { + ousdTotalSupply: 0, + ousdTotalValue: 0, + vaultCheckBalance: 0, + userOusd: 0, + userUsdc: firstRequestAmountUSDC.add(secondRequestAmountUSDC), + vaultUsdc: firstRequestAmountUSDC + .add(secondRequestAmountUSDC) + .mul(-1), + queued: 0, + claimable: firstRequestAmountUSDC.add(secondRequestAmountUSDC), + claimed: firstRequestAmountUSDC.add(secondRequestAmountUSDC), + nextWithdrawalIndex: 0, + }, + fixtureWithUser + ); + }); + it("Should claim single big request as a whale", async () => { + const { vault, ousd, matt } = fixture; + + const ousdBalanceBefore = await ousd.balanceOf(matt.address); + const totalValueBefore = await vault.totalValue(); + + await vault.connect(matt).requestWithdrawal(ousdUnits("30")); + + const ousdBalanceAfter = await ousd.balanceOf(matt.address); + const totalValueAfter = await vault.totalValue(); + await expect(ousdBalanceBefore).to.equal(ousdUnits("30")); + await expect(ousdBalanceAfter).to.equal(ousdUnits("0")); + await expect(totalValueBefore.sub(totalValueAfter)).to.equal( + ousdUnits("30") + ); + + const ousdTotalSupply = await ousd.totalSupply(); + await advanceTime(delayPeriod); // Advance in time to ensure time delay between request and claim. + const tx = await vault.connect(matt).claimWithdrawal(0); // Claim withdrawal for 50% of the supply + + await expect(tx) + .to.emit(vault, "WithdrawalClaimed") + .withArgs(matt.address, 0, ousdUnits("30")); + + await expect(ousdTotalSupply).to.equal(await ousd.totalSupply()); + await expect(totalValueAfter).to.equal(await vault.totalValue()); + }); + + // Negative tests + it("Fail to claim request because of not enough time passed", async () => { + const { vault, daniel } = fixture; + + // Daniel requests 5 OUSD to be withdrawn + await vault.connect(daniel).requestWithdrawal(firstRequestAmountOUSD); + const requestId = 0; + + // Daniel claimWithdraw request in the same block as the request + const tx = vault.connect(daniel).claimWithdrawal(requestId); + + await expect(tx).to.revertedWith("Claim delay not met"); + }); + it("Fail to request withdrawal because of solvency check too high", async () => { + const { vault, daniel, usdc } = fixture; + + await usdc.mintTo(daniel.address, ousdUnits("10")); + await usdc.connect(daniel).transfer(vault.address, ousdUnits("10")); + + const tx = vault + .connect(daniel) + .requestWithdrawal(firstRequestAmountOUSD); + + await expect(tx).to.revertedWith("Backing supply liquidity error"); + }); + it("Fail to claim request because of solvency check too high", async () => { + const { vault, daniel, usdc } = fixture; + + // Request withdrawal of 5 OUSD + await vault.connect(daniel).requestWithdrawal(firstRequestAmountOUSD); + + // Transfer 10 USDC to the vault + await usdc.mintTo(daniel.address, ousdUnits("10")); + await usdc.connect(daniel).transfer(vault.address, ousdUnits("10")); + + await advanceTime(delayPeriod); // Advance in time to ensure time delay between request and claim. + + // Claim the withdrawal + const tx = vault.connect(daniel).claimWithdrawal(0); + + await expect(tx).to.revertedWith("Backing supply liquidity error"); + }); + it("Fail multiple claim requests because of solvency check too high", async () => { + const { vault, matt, usdc } = fixture; + + // Request withdrawal of 5 OUSD + await vault.connect(matt).requestWithdrawal(firstRequestAmountOUSD); + await vault.connect(matt).requestWithdrawal(secondRequestAmountOUSD); + + // Transfer 10 USDC to the vault + await usdc.mintTo(matt.address, ousdUnits("10")); + await usdc.connect(matt).transfer(vault.address, ousdUnits("10")); + + await advanceTime(delayPeriod); // Advance in time to ensure time delay between request and claim. + + // Claim the withdrawal + const tx = vault.connect(matt).claimWithdrawals([0, 1]); + + await expect(tx).to.revertedWith("Backing supply liquidity error"); + }); + it("Fail request withdrawal because of solvency check too low", async () => { + const { vault, daniel, usdc } = fixture; + + // Simulate a loss of funds from the vault + await usdc + .connect(await impersonateAndFund(vault.address)) + .transfer(daniel.address, usdcUnits("10")); + + const tx = vault + .connect(daniel) + .requestWithdrawal(firstRequestAmountOUSD); - it("Should allow redeems of non-standard tokens", async () => { - const { ousd, vault, anna, governor, oracleRouter, nonStandardToken } = - fixture; + await expect(tx).to.revertedWith("Backing supply liquidity error"); + }); - await oracleRouter.cacheDecimals(nonStandardToken.address); - await vault.connect(governor).supportAsset(nonStandardToken.address, 0); + describe("when deposit 15 USDC to a strategy, leaving 60 - 15 = 45 USDC in the vault; request withdrawal of 5 + 18 = 23 OUSD, leaving 45 - 23 = 22 USDC unallocated", () => { + let mockStrategy; + beforeEach(async () => { + const { vault, usdc, governor, daniel, josh } = fixture; - await setOracleTokenPriceUsd("NonStandardToken", "1.00"); + const dMockStrategy = await deployWithConfirmation("MockStrategy"); + mockStrategy = await ethers.getContractAt( + "MockStrategy", + dMockStrategy.address + ); + await mockStrategy.setWithdrawAll(usdc.address, vault.address); + await vault.connect(governor).approveStrategy(mockStrategy.address); - await expect(anna).has.a.balanceOf("1000.00", nonStandardToken); + // Deposit 15 USDC of 10 + 20 + 30 = 60 USDC to strategy + // This leave 60 - 15 = 45 USDC in the vault + await vault + .connect(governor) + .depositToStrategy( + mockStrategy.address, + [usdc.address], + [usdcUnits("15")] + ); + // Request withdrawal of 5 + 18 = 23 OUSD + // This leave 45 - 23 = 22 USDC unallocated to the withdrawal queue + await vault.connect(daniel).requestWithdrawal(firstRequestAmountOUSD); + await vault.connect(josh).requestWithdrawal(secondRequestAmountOUSD); + }); + it("Fail to deposit allocated USDC to a strategy", async () => { + const { vault, usdc, governor } = fixture; + + // USDC in the vault = 60 - 15 = 45 USDC + // unallocated USDC in the Vault = 45 - 23 = 22 USDC + // 23 USDC to deposit > the 22 USDC available so it should revert + const depositAmount = usdcUnits("23"); + const tx = vault + .connect(governor) + .depositToStrategy( + mockStrategy.address, + [usdc.address], + [depositAmount] + ); + await expect(tx).to.be.revertedWith( + "Not enough backing asset available" + ); + }); + it("Fail to deposit allocated USDC during allocate", async () => { + const { vault, governor, usdc } = fixture; - // Mint 100 OUSD for 100 tokens - await nonStandardToken - .connect(anna) - .approve(vault.address, usdtUnits("100.0")); - await vault - .connect(anna) - .mint(nonStandardToken.address, usdtUnits("100.0"), 0); - await expect(anna).has.a.balanceOf("100.00", ousd); + // Set mock strategy as default strategy + await vault + .connect(governor) + .setDefaultStrategy(mockStrategy.address); - // Redeem 100 tokens for 100 OUSD - await vault.connect(anna).redeem(ousdUnits("100.0"), 0); - await expect(anna).has.a.balanceOf("0.00", ousd); - // 66.66 would have come back as USDS because there is 100 NST and 200 USDS - await expect(anna).has.an.approxBalanceOf("933.33", nonStandardToken); - }); + // and buffer to 10% + await vault.connect(governor).setVaultBuffer(ousdUnits("0.1")); - it("Should have a default redeem fee of 0", async () => { - const { vault } = fixture; + // USDC in strategy = 15 USDC + // USDC in the vault = 60 - 15 = 45 USDC + // Unallocated USDC in the vault = 45 - 23 = 22 USDC - await expect(await vault.redeemFeeBps()).to.equal("0"); - }); + await vault.connect(governor).allocate(); - it("Should charge a redeem fee if redeem fee set", async () => { - const { ousd, vault, usdc, anna, governor } = fixture; - - // 1000 basis points = 10% - await vault.connect(governor).setRedeemFeeBps(1000); - await expect(anna).has.a.balanceOf("1000.00", usdc); - await usdc.connect(anna).approve(vault.address, usdcUnits("50.0")); - await vault.connect(anna).mint(usdc.address, usdcUnits("50.0"), 0); - await expect(anna).has.a.balanceOf("50.00", ousd); - await vault.connect(anna).redeem(ousdUnits("50.0"), 0); - await expect(anna).has.a.balanceOf("0.00", ousd); - // 45 after redeem fee - // USDC is 50/250 of total assets, so balance should be 950 + 50/250 * 45 = 959 - await expect(anna).has.a.balanceOf("959.00", usdc); - }); + expect(await usdc.balanceOf(mockStrategy.address)).to.approxEqual( + // 60 - 23 = 37 Unreserved USDC + // 90% of 37 = 33.3 USDC for allocation + usdcUnits("33.3"), + "Strategy has the reserved USDC" + ); - it("Should revert redeem if balance is insufficient", async () => { - const { ousd, vault, usdc, anna } = fixture; + expect(await usdc.balanceOf(vault.address)).to.approxEqual( + // 10% of 37 = 3.7 USDC for Vault buffer + // + 23 reserved USDC + usdcUnits("23").add(usdcUnits("3.7")), + "Vault doesn't have enough USDC" + ); + }); + it("Should deposit unallocated USDC to a strategy", async () => { + const { vault, usdc, governor } = fixture; - // Mint some OUSD tokens - await expect(anna).has.a.balanceOf("1000.00", usdc); - await usdc.connect(anna).approve(vault.address, usdcUnits("50.0")); - await vault.connect(anna).mint(usdc.address, usdcUnits("50.0"), 0); - await expect(anna).has.a.balanceOf("50.00", ousd); + // USDC in the vault = 60 - 15 = 45 USDC + // unallocated USDC in the Vault = 45 - 23 = 22 USDC + const depositAmount = usdcUnits("22"); + await vault + .connect(governor) + .depositToStrategy( + mockStrategy.address, + [usdc.address], + [depositAmount] + ); + }); + it("Should claim first request with enough liquidity", async () => { + const { vault, daniel } = fixture; + const fixtureWithUser = { ...fixture, user: daniel }; + const dataBefore = await snapData(fixtureWithUser); + + await advanceTime(delayPeriod); // Advance in time to ensure time delay between request and claim. + + const tx = await vault.connect(daniel).claimWithdrawal(0); + + await expect(tx) + .to.emit(vault, "WithdrawalClaimed") + .withArgs(daniel.address, 0, firstRequestAmountOUSD); + + await assertChangedData( + dataBefore, + { + ousdTotalSupply: 0, + ousdTotalValue: 0, + vaultCheckBalance: 0, + userOusd: 0, + userUsdc: firstRequestAmountUSDC, + vaultUsdc: firstRequestAmountUSDC.mul(-1), + queued: 0, + claimable: firstRequestAmountUSDC.add(secondRequestAmountUSDC), + claimed: firstRequestAmountUSDC, + nextWithdrawalIndex: 0, + }, + fixtureWithUser + ); + }); + it("Should claim a new request with enough USDC liquidity", async () => { + const { vault, matt } = fixture; + const fixtureWithUser = { ...fixture, user: matt }; + + // Set the claimable amount to the queued amount + await vault.addWithdrawalQueueLiquidity(); + + // USDC in the vault = 60 - 15 = 45 USDC + // unallocated USDC in the Vault = 45 - 23 = 22 USDC + // Matt request all unallocated USDC to be withdrawn + const requestAmount = ousdUnits("22"); + await vault.connect(matt).requestWithdrawal(requestAmount); + + const dataBefore = await snapData(fixtureWithUser); + + await advanceTime(delayPeriod); // Advance in time to ensure time delay between request and claim. + + const tx = await vault.connect(matt).claimWithdrawal(2); + + await expect(tx) + .to.emit(vault, "WithdrawalClaimed") + .withArgs(matt.address, 2, requestAmount); + + await assertChangedData( + dataBefore, + { + ousdTotalSupply: 0, + ousdTotalValue: 0, + vaultCheckBalance: 0, + userOusd: 0, + userUsdc: requestAmount.div(1e12), // USDC has 6 decimals + vaultUsdc: requestAmount.mul(-1).div(1e12), + queued: 0, + claimable: requestAmount.div(1e12), + claimed: requestAmount.div(1e12), + nextWithdrawalIndex: 0, + }, + fixtureWithUser + ); + }); + it("Fail to claim a new request with NOT enough USDC liquidity", async () => { + const { vault, matt } = fixture; - // Try to withdraw more than balance - await expect( - vault.connect(anna).redeem(ousdUnits("100.0"), 0) - ).to.be.revertedWith("Transfer amount exceeds balance"); - }); + // Matt request 23 OUSD to be withdrawn when only 22 USDC is unallocated to existing requests + const requestAmount = ousdUnits("23"); + await vault.connect(matt).requestWithdrawal(requestAmount); - it("Should only allow Governor to set a redeem fee", async () => { - const { vault, anna } = fixture; + await advanceTime(delayPeriod); // Advance in time to ensure time delay between request and claim. - await expect(vault.connect(anna).setRedeemFeeBps(100)).to.be.revertedWith( - "Caller is not the Governor" - ); - }); + const tx = vault.connect(matt).claimWithdrawal(2); + await expect(tx).to.be.revertedWith("Queue pending liquidity"); + }); + it("Should claim a new request after withdraw from strategy adds enough liquidity", async () => { + const { vault, daniel, matt, strategist, usdc } = fixture; - it("Should redeem entire OUSD balance", async () => { - const { ousd, vault, usdc, usds, anna } = fixture; + // Set the claimable amount to the queued amount + await vault.addWithdrawalQueueLiquidity(); - await expect(anna).has.a.balanceOf("1000.00", usdc); + // Matt requests all 30 OUSD to be withdrawn which is currently 8 USDC short + const requestAmount = ousdUnits("30"); + await vault.connect(matt).requestWithdrawal(requestAmount); - // Mint 100 OUSD tokens using USDC - await usdc.connect(anna).approve(vault.address, usdcUnits("100.0")); - await vault.connect(anna).mint(usdc.address, usdcUnits("100.0"), 0); - await expect(anna).has.a.balanceOf("100.00", ousd); + const fixtureWithUser = { ...fixture, user: daniel }; + const dataBeforeMint = await snapData(fixtureWithUser); - // Mint 150 OUSD tokens using USDS - await usds.connect(anna).approve(vault.address, usdsUnits("150.0")); - await vault.connect(anna).mint(usds.address, usdsUnits("150.0"), 0); - await expect(anna).has.a.balanceOf("250.00", ousd); + // USDC in the vault = 60 - 15 = 45 USDC + // unallocated USDC in the Vault = 45 - 23 = 22 USDC + // Add another 8 USDC so the unallocated USDC is 22 + 8 = 30 USDC + const withdrawAmount = usdcUnits("8"); + await vault + .connect(strategist) + .withdrawFromStrategy( + mockStrategy.address, + [usdc.address], + [withdrawAmount] + ); - // Withdraw all - await vault.connect(anna).redeem(ousd.balanceOf(anna.address), 0); + await assertChangedData( + dataBeforeMint, + { + ousdTotalSupply: 0, + ousdTotalValue: 0, + vaultCheckBalance: 0, + userOusd: 0, + userUsdc: 0, + vaultUsdc: withdrawAmount, + queued: 0, + claimable: requestAmount.div(1e12), + claimed: 0, + nextWithdrawalIndex: 0, + }, + fixtureWithUser + ); - // 100 USDC and 350 USDS in contract - // (1000-100) + 100/450 * 250 USDC - // (1000-150) + 350/450 * 250 USDS - await expect(anna).has.an.approxBalanceOf("955.55", usdc); - await expect(anna).has.an.approxBalanceOf("1044.44", usds); - }); + await advanceTime(delayPeriod); // Advance in time to ensure time delay between request and claim. - it("Should redeem entire OUSD balance, with a higher oracle price", async () => { - const { ousd, vault, usdc, usds, anna, governor } = fixture; - - await expect(anna).has.a.balanceOf("1000.00", usdc); - - // Mint 100 OUSD tokens using USDC - await usdc.connect(anna).approve(vault.address, usdcUnits("100.0")); - await vault.connect(anna).mint(usdc.address, usdcUnits("100.0"), 0); - await expect(anna).has.a.balanceOf("100.00", ousd); - - // Mint 150 OUSD tokens using USDS - await usds.connect(anna).approve(vault.address, usdsUnits("150.0")); - await vault.connect(anna).mint(usds.address, usdsUnits("150.0"), 0); - await expect(anna).has.a.balanceOf("250.00", ousd); - - await setOracleTokenPriceUsd("USDC", "1.30"); - await setOracleTokenPriceUsd("USDS", "1.20"); - await vault.connect(governor).rebase(); - - // Anna's balance does not change with the rebase - await expect(anna).has.an.approxBalanceOf("250.00", ousd); - - // Withdraw all - await vault.connect(anna).redeem(ousd.balanceOf(anna.address), 0); - - // OUSD to Withdraw 250 - // Total Vault Coins 450 - // USDC Percentage 100 / 450 = 0.222222222222222 - // USDT Percentage 350 / 450 = 0.777777777777778 - // USDC Value Percentage 0.222222222222222 * 1.3 = 0.288888888888889 - // USDT Value Percentage 0.777777777777778 * 1.2 = 0.933333333333333 - // Output to Dollar Ratio 1.22222222222222 - // USDC Output 250 * 0.222222222222222 / 1.22222222222222 = 45.4545454545454 - // USDT Output 250 * 0.777777777777778 / 1.22222222222222 = 159.090909090909 - // Expected USDC 900 + 45.4545454545454 = 945.454545454545 - // Expected USDT 850 + 159.090909090909 = 1009.09090909091 - await expect(anna).has.an.approxBalanceOf( - "945.4545", - usdc, - "USDC has wrong balance" - ); - await expect(anna).has.an.approxBalanceOf( - "1009.09", - usds, - "USDS has wrong balance" - ); - }); + await vault.connect(matt).claimWithdrawal(2); + }); + it("Should claim a new request after withdrawAllFromStrategy adds enough liquidity", async () => { + const { vault, daniel, matt, strategist, usdc } = fixture; - it("Should redeem entire OUSD balance, with a lower oracle price", async () => { - const { ousd, vault, usdc, usds, anna, governor } = fixture; - - await expect(anna).has.a.balanceOf("1000.00", usdc); - - // Mint 100 OUSD tokens using USDC - await usdc.connect(anna).approve(vault.address, usdcUnits("100.0")); - await vault.connect(anna).mint(usdc.address, usdcUnits("100.0"), 0); - await expect(anna).has.a.balanceOf("100.00", ousd); - - // Mint 150 OUSD tokens using USDS - await usds.connect(anna).approve(vault.address, usdsUnits("150.0")); - await vault.connect(anna).mint(usds.address, usdsUnits("150.0"), 0); - await expect(anna).has.a.balanceOf("250.00", ousd); - - await setOracleTokenPriceUsd("USDC", "0.90"); - await setOracleTokenPriceUsd("USDS", "0.80"); - await vault.connect(governor).rebase(); - - // Anna's share of OUSD is unaffected - await expect(anna).has.an.approxBalanceOf("250.00", ousd); - - // Withdraw all - await ousd.connect(anna).approve(vault.address, ousdUnits("500")); - await vault.connect(anna).redeem(ousd.balanceOf(anna.address), 0); - - // OUSD to Withdraw 250 - // Total Vault Coins 450 - // USDC Percentage 100 / 450 = 0.2222 - // USDS Percentage 350 / 450 = 0.7778 - // USDC Value Percentage 0.2222 * 1 = 0.2222 - // USDS Value Percentage 0.7778 * 1 = 0.7778 - // Output to Dollar Ratio 1.0000 - // USDC Output 250 * 0.2222 / 1.0000 = 55.5556 - // USDS Output 250 * 0.7778 / 1.0000 = 194.4444 - // Expected USDC 900 + 55.5556 = 955.5556 - // Expected USDS 850 + 194.4444 = 1044.4444 - await expect(anna).has.an.approxBalanceOf( - "955.5556", - usdc, - "USDC has wrong balance" - ); - await expect(anna).has.an.approxBalanceOf( - "1044.44", - usds, - "USDS has wrong balance" - ); - }); + // Set the claimable amount to the queued amount + await vault.addWithdrawalQueueLiquidity(); + + // Matt requests all 30 OUSD to be withdrawn which is currently 8 USDC short + const requestAmount = ousdUnits("30"); + await vault.connect(matt).requestWithdrawal(requestAmount); - it("Should have correct balances on consecutive mint and redeem", async () => { - const { ousd, vault, usdc, usds, anna, matt, josh } = fixture; - - const usersWithBalances = [ - [anna, 0], - [matt, 100], - [josh, 100], - ]; - - const assetsWithUnits = [ - [usds, usdsUnits], - [usdc, usdcUnits], - ]; - - for (const [user, startBalance] of usersWithBalances) { - for (const [asset, units] of assetsWithUnits) { - for (const amount of [5.09, 10.32, 20.99, 100.01]) { - await asset - .connect(user) - .approve(vault.address, await units(amount.toString())); + const fixtureWithUser = { ...fixture, user: daniel }; + const dataBeforeMint = await snapData(fixtureWithUser); + const strategyBalanceBefore = await usdc.balanceOf( + mockStrategy.address + ); + + // USDC in the vault = 60 - 15 = 45 USDC + // unallocated USDC in the Vault = 45 - 23 = 22 USDC + // Add another 8 USDC so the unallocated USDC is 22 + 8 = 30 USDC await vault - .connect(user) - .mint(asset.address, await units(amount.toString()), 0); - await expect(user).has.an.approxBalanceOf( - (startBalance + amount).toString(), - ousd + .connect(strategist) + .withdrawAllFromStrategy(mockStrategy.address); + + await assertChangedData( + dataBeforeMint, + { + ousdTotalSupply: 0, + ousdTotalValue: 0, + vaultCheckBalance: 0, + userOusd: 0, + userUsdc: 0, + vaultUsdc: strategyBalanceBefore, + queued: 0, + claimable: requestAmount.div(1e12), + claimed: 0, + nextWithdrawalIndex: 0, + }, + fixtureWithUser ); - await vault.connect(user).redeem(ousdUnits(amount.toString()), 0); - await expect(user).has.an.approxBalanceOf( - startBalance.toString(), - ousd + + await advanceTime(delayPeriod); // Advance in time to ensure time delay between request and claim. + + await vault.connect(matt).claimWithdrawal(2); + }); + it("Should claim a new request after withdrawAll from strategies adds enough liquidity", async () => { + const { vault, daniel, matt, strategist, usdc } = fixture; + + // Set the claimable amount to the queued amount + await vault.addWithdrawalQueueLiquidity(); + + // Matt requests all 30 OUSD to be withdrawn which is currently 8 USDC short + const requestAmount = ousdUnits("30"); + await vault.connect(matt).requestWithdrawal(requestAmount); + + const fixtureWithUser = { ...fixture, user: daniel }; + const dataBeforeMint = await snapData(fixtureWithUser); + const strategyBalanceBefore = await usdc.balanceOf( + mockStrategy.address ); - } - } - } - }); - it("Should have correct balances on consecutive mint and redeem with varying oracle prices", async () => { - const { ousd, vault, usdt, usdc, matt, josh } = fixture; + // USDC in the vault = 60 - 15 = 45 USDC + // unallocated USDC in the Vault = 45 - 23 = 22 USDC + // Add another 8 USDC so the unallocated USDC is 22 + 8 = 30 USDC + await vault.connect(strategist).withdrawAllFromStrategies(); + + await assertChangedData( + dataBeforeMint, + { + ousdTotalSupply: 0, + ousdTotalValue: 0, + vaultCheckBalance: 0, + userOusd: 0, + userUsdc: 0, + vaultUsdc: strategyBalanceBefore, + queued: 0, + claimable: requestAmount.div(1e12), + claimed: 0, + nextWithdrawalIndex: 0, + }, + fixtureWithUser + ); - const users = [matt, josh]; - const assetsWithUnits = [ - [usdt, usdtUnits], - [usdc, usdcUnits], - ]; - const prices = [0.998, 1.02, 1.09]; - const amounts = [5.09, 10.32, 20.99, 100.01]; + await advanceTime(delayPeriod); // Advance in time to ensure time delay between request and claim. + + await vault.connect(matt).claimWithdrawal(2); + }); + it("Fail to claim a new request after mint with NOT enough liquidity", async () => { + const { vault, daniel, matt, usdc } = fixture; + + // Matt requests all 30 OUSD to be withdrawn which is not enough liquidity + const requestAmount = ousdUnits("30"); + await vault.connect(matt).requestWithdrawal(requestAmount); + + // USDC in the vault = 60 - 15 = 45 USDC + // unallocated USDC in the Vault = 45 - 23 = 22 USDC + // Add another 6 USDC so the unallocated USDC is 22 + 6 = 28 USDC + await usdc.mintTo(daniel.address, ousdUnits("6").div(1e12)); + await usdc + .connect(daniel) + .approve(vault.address, ousdUnits("6").div(1e12)); + await vault.connect(daniel).mint(usdc.address, usdcUnits("6"), 0); + + await advanceTime(delayPeriod); // Advance in time to ensure time delay between request and claim. + + const tx = vault.connect(matt).claimWithdrawal(2); + await expect(tx).to.be.revertedWith("Queue pending liquidity"); + }); + it("Should claim a new request after mint adds enough liquidity", async () => { + const { vault, daniel, matt, usdc } = fixture; + + // Set the claimable amount to the queued amount + await vault.addWithdrawalQueueLiquidity(); + + // Matt requests all 30 OUSD to be withdrawn which is currently 8 USDC short + const requestAmount = ousdUnits("30"); + await vault.connect(matt).requestWithdrawal(requestAmount); + + const fixtureWithUser = { ...fixture, user: daniel }; + const dataBeforeMint = await snapData(fixtureWithUser); + + // USDC in the vault = 60 - 15 = 45 USDC + // unallocated USDC in the Vault = 45 - 23 = 22 USDC + // Add another 8 USDC so the unallocated USDC is 22 + 8 = 30 USDC + const mintAmount = ousdUnits("8"); + await usdc + .connect(daniel) + .approve(vault.address, mintAmount.div(1e12)); + await vault + .connect(daniel) + .mint(usdc.address, mintAmount.div(1e12), 0); + + await assertChangedData( + dataBeforeMint, + { + ousdTotalSupply: mintAmount, + ousdTotalValue: mintAmount, + vaultCheckBalance: mintAmount.div(1e12), + userOusd: mintAmount, + userUsdc: mintAmount.mul(-1).div(1e12), + vaultUsdc: mintAmount.div(1e12), + queued: 0, + claimable: requestAmount.div(1e12), + claimed: 0, + nextWithdrawalIndex: 0, + }, + fixtureWithUser + ); - const getUserOusdBalance = async (user) => { - const bn = await ousd.balanceOf(await user.getAddress()); - return parseFloat(bn.toString() / 1e12 / 1e6); - }; + await advanceTime(delayPeriod); // Advance in time to ensure time delay between request and claim. + + await vault.connect(matt).claimWithdrawal(2); + }); + }); + + describe("Fail when", () => { + it("request doesn't have enough OUSD", async () => { + const { vault, josh } = fixture; + const fixtureWithUser = { ...fixture, user: josh }; + const dataBefore = await snapData(fixtureWithUser); + + const tx = vault + .connect(josh) + .requestWithdrawal(dataBefore.userOusd.add(1)); + + await expect(tx).to.revertedWith("Transfer amount exceeds balance"); + }); + it("capital is paused", async () => { + const { vault, governor, josh } = fixture; + + await vault.connect(governor).pauseCapital(); + + const tx = vault + .connect(josh) + .requestWithdrawal(firstRequestAmountOUSD); + + await expect(tx).to.be.revertedWith("Capital paused"); + }); + }); + }); + describe("with 1% vault buffer, 30 USDC in the queue, 15 USDC in the vault, 85 USDC in the strategy, 5 USDC already claimed", () => { + let mockStrategy; + beforeEach(async () => { + const { governor, vault, usdc, daniel, domen, josh, matt } = fixture; + // Mint USDC to users + await usdc.mintTo(daniel.address, usdcUnits("15")); + await usdc.mintTo(josh.address, usdcUnits("20")); + await usdc.mintTo(matt.address, usdcUnits("30")); + await usdc.mintTo(domen.address, usdcUnits("40")); + + // Approve USDC to Vault + await usdc.connect(daniel).approve(vault.address, usdcUnits("15")); + await usdc.connect(josh).approve(vault.address, usdcUnits("20")); + await usdc.connect(matt).approve(vault.address, usdcUnits("30")); + await usdc.connect(domen).approve(vault.address, usdcUnits("40")); + + // Mint 105 OUSD to four users + await vault.connect(daniel).mint(usdc.address, usdcUnits("15"), "0"); + await vault.connect(josh).mint(usdc.address, usdcUnits("20"), "0"); + await vault.connect(matt).mint(usdc.address, usdcUnits("30"), "0"); + await vault.connect(domen).mint(usdc.address, usdcUnits("40"), "0"); + await vault + .connect(await impersonateAndFund(await vault.governor())) + .setMaxSupplyDiff(ousdUnits("0.03")); + + // Request and claim 2 + 3 = 5 USDC from Vault + await vault.connect(daniel).requestWithdrawal(ousdUnits("2")); + await vault.connect(josh).requestWithdrawal(ousdUnits("3")); + await advanceTime(delayPeriod); // Advance in time to ensure time delay between request and claim. + await vault.connect(daniel).claimWithdrawal(0); + await vault.connect(josh).claimWithdrawal(1); + + // Deploy a mock strategy + mockStrategy = await deployWithConfirmation("MockStrategy"); + await vault.connect(governor).approveStrategy(mockStrategy.address); + + // Deposit 85 USDC to strategy + await vault + .connect(governor) + .depositToStrategy( + mockStrategy.address, + [usdc.address], + [usdcUnits("85")] + ); - for (const user of users) { - for (const [asset, units] of assetsWithUnits) { - for (const price of prices) { - await setOracleTokenPriceUsd(await asset.symbol(), price.toString()); - // Manually call rebase because not triggered by mint - await vault.rebase(); - // Rebase could have changed user balance - // as there could have been yield from different - // oracle prices on redeems during a previous loop. - let userBalance = await getUserOusdBalance(user); - for (const amount of amounts) { - const ousdToReceive = amount * Math.min(price, 1); - await expect(user).has.an.approxBalanceOf( - userBalance.toString(), - ousd + // Set vault buffer to 1% + await vault.connect(governor).setVaultBuffer(ousdUnits("0.01")); + + // Have 4 + 12 + 16 = 32 USDC outstanding requests + // So a total supply of 100 - 32 = 68 OUSD + await vault.connect(daniel).requestWithdrawal(ousdUnits("4")); + await vault.connect(josh).requestWithdrawal(ousdUnits("12")); + await vault.connect(matt).requestWithdrawal(ousdUnits("16")); + + await vault.connect(josh).addWithdrawalQueueLiquidity(); + }); + describe("Fail to claim", () => { + it("a previously claimed withdrawal", async () => { + const { vault, daniel } = fixture; + + const tx = vault.connect(daniel).claimWithdrawal(0); + + await expect(tx).to.be.revertedWith("Already claimed"); + }); + it("the first withdrawal with wrong withdrawer", async () => { + const { vault, matt } = fixture; + + // Advance in time to ensure time delay between request and claim. + await advanceTime(delayPeriod); + + const tx = vault.connect(matt).claimWithdrawal(2); + + await expect(tx).to.be.revertedWith("Not requester"); + }); + it("the first withdrawal request in the queue before 30 minutes", async () => { + const { vault, daniel } = fixture; + + const tx = vault.connect(daniel).claimWithdrawal(2); + + await expect(tx).to.be.revertedWith("Claim delay not met"); + }); + }); + describe("when waited 30 minutes", () => { + beforeEach(async () => { + // Advance in time to ensure time delay between request and claim. + await advanceTime(delayPeriod); + }); + it("Fail to claim the first withdrawal with wrong withdrawer", async () => { + const { vault, matt } = fixture; + + const tx = vault.connect(matt).claimWithdrawal(2); + + await expect(tx).to.be.revertedWith("Not requester"); + }); + it("Should claim the first withdrawal request in the queue after 30 minutes", async () => { + const { vault, daniel } = fixture; + const fixtureWithUser = { ...fixture, user: daniel }; + const dataBefore = await snapData(fixtureWithUser); + + const tx = await vault.connect(daniel).claimWithdrawal(2); + + await expect(tx) + .to.emit(vault, "WithdrawalClaimed") + .withArgs(daniel.address, 2, ousdUnits("4")); + + await assertChangedData( + dataBefore, + { + ousdTotalSupply: 0, + ousdTotalValue: 0, + vaultCheckBalance: 0, + userOusd: 0, + userUsdc: usdcUnits("4"), + vaultUsdc: usdcUnits("4").mul(-1), + queued: 0, + claimable: 0, + claimed: usdcUnits("4"), + nextWithdrawalIndex: 0, + }, + fixtureWithUser + ); + }); + it("Fail to claim the second withdrawal request in the queue after 30 minutes", async () => { + const { vault, josh } = fixture; + + const tx = vault.connect(josh).claimWithdrawal(3); + + await expect(tx).to.be.revertedWith("Queue pending liquidity"); + }); + it("Fail to claim the last (3rd) withdrawal request in the queue", async () => { + const { vault, matt } = fixture; + + const tx = vault.connect(matt).claimWithdrawal(4); + + await expect(tx).to.be.revertedWith("Queue pending liquidity"); + }); + }); + describe("when mint covers exactly outstanding requests (32 - 15 = 17 OUSD)", () => { + beforeEach(async () => { + const { vault, daniel, usdc } = fixture; + await usdc.mintTo(daniel.address, usdcUnits("17")); + await usdc.connect(daniel).approve(vault.address, usdcUnits("17")); + await vault.connect(daniel).mint(usdc.address, usdcUnits("17"), "0"); + + // Advance in time to ensure time delay between request and claim. + await advanceTime(delayPeriod); + }); + it("Should claim the 2nd and 3rd withdrawal requests in the queue", async () => { + const { vault, daniel, josh } = fixture; + const fixtureWithUser = { ...fixture, user: daniel }; + const dataBefore = await snapData(fixtureWithUser); + + const tx1 = await vault.connect(daniel).claimWithdrawal(2); + + await expect(tx1) + .to.emit(vault, "WithdrawalClaimed") + .withArgs(daniel.address, 2, ousdUnits("4")); + + const tx2 = await vault.connect(josh).claimWithdrawal(3); + + await expect(tx2) + .to.emit(vault, "WithdrawalClaimed") + .withArgs(josh.address, 3, ousdUnits("12")); + + await assertChangedData( + dataBefore, + { + ousdTotalSupply: 0, + ousdTotalValue: 0, + vaultCheckBalance: 0, + userOusd: 0, + userUsdc: usdcUnits("4"), + vaultUsdc: usdcUnits("16").mul(-1), + queued: 0, + claimable: 0, + claimed: usdcUnits("16"), + nextWithdrawalIndex: 0, + }, + fixtureWithUser + ); + }); + it("Fail to deposit 1 USDC to a strategy", async () => { + const { vault, usdc, governor } = fixture; + + const tx = vault + .connect(governor) + .depositToStrategy( + mockStrategy.address, + [usdc.address], + [usdcUnits("1")] ); - await asset - .connect(user) - .approve(vault.address, await units(amount.toString())); - await vault - .connect(user) - .mint(asset.address, await units(amount.toString()), 0); - await expect(user).has.an.approxBalanceOf( - (userBalance + ousdToReceive).toString(), - ousd + + await expect(tx).to.be.revertedWith( + "Not enough backing asset available" + ); + }); + it("Fail to allocate any USDC to the default strategy", async () => { + const { vault, domen } = fixture; + + const tx = await vault.connect(domen).allocate(); + + await expect(tx).to.not.emit(vault, "AssetAllocated"); + }); + }); + describe("when mint covers exactly outstanding requests and vault buffer (17 + 1 USDC)", () => { + beforeEach(async () => { + const { vault, daniel, usdc } = fixture; + await usdc.mintTo(daniel.address, usdcUnits("18")); + await usdc.connect(daniel).approve(vault.address, usdcUnits("18")); + await vault.connect(daniel).mint(usdc.address, usdcUnits("18"), "0"); + }); + it("Should deposit 1 USDC to a strategy which is the vault buffer", async () => { + const { vault, usdc, governor } = fixture; + + const tx = await vault + .connect(governor) + .depositToStrategy( + mockStrategy.address, + [usdc.address], + [usdcUnits("1")] ); - await vault - .connect(user) - .redeem(ousdUnits(ousdToReceive.toString()), 0); - await expect(user).has.an.approxBalanceOf( - userBalance.toString(), - ousd + + expect(tx) + .to.emit(usdc, "Transfer") + .withArgs(vault.address, mockStrategy.address, usdcUnits("1")); + }); + it("Fail to deposit 1.1 USDC to the default strategy", async () => { + const { vault, usdc, governor } = fixture; + + const tx = vault + .connect(governor) + .depositToStrategy( + mockStrategy.address, + [usdc.address], + [usdcUnits("1.1")] ); - } - } - } - } - }); - it("Should correctly handle redeem without a rebase and then full redeem", async function () { - const { ousd, vault, usdc, anna } = fixture; - await expect(anna).has.a.balanceOf("0.00", ousd); - await usdc.connect(anna).mint(usdcUnits("3000.0")); - await usdc.connect(anna).approve(vault.address, usdcUnits("3000.0")); - await vault.connect(anna).mint(usdc.address, usdcUnits("3000.0"), 0); - await expect(anna).has.a.balanceOf("3000.00", ousd); - - //peturb the oracle a slight bit. - await setOracleTokenPriceUsd("USDC", "1.000001"); - //redeem without rebasing (not over threshold) - await vault.connect(anna).redeem(ousdUnits("200.00"), 0); - //redeem with rebasing (over threshold) - await vault.connect(anna).redeem(ousd.balanceOf(anna.address), 0); - - await expect(anna).has.a.balanceOf("0.00", ousd); - }); + await expect(tx).to.be.revertedWith( + "Not enough backing asset available" + ); + }); + it("Fail to allocate any USDC to the default strategy", async () => { + const { vault, domen } = fixture; + + const tx = await vault.connect(domen).allocate(); + + await expect(tx).to.not.emit(vault, "AssetAllocated"); + }); + }); + describe("when mint more than covers outstanding requests and vault buffer (17 + 1 + 3 = 21 OUSD)", () => { + beforeEach(async () => { + const { vault, daniel, usdc } = fixture; + await usdc.mintTo(daniel.address, usdcUnits("21")); + await usdc.connect(daniel).approve(vault.address, usdcUnits("21")); + await vault.connect(daniel).mint(usdc.address, usdcUnits("21"), "0"); + }); + it("Should deposit 4 USDC to a strategy", async () => { + const { vault, usdc, governor } = fixture; + + const tx = await vault + .connect(governor) + .depositToStrategy( + mockStrategy.address, + [usdc.address], + [usdcUnits("4")] + ); - it("Should respect minimum unit amount argument in redeem", async () => { - const { ousd, vault, usdc, anna, usds } = fixture; - - await expect(anna).has.a.balanceOf("1000.00", usdc); - await expect(anna).has.a.balanceOf("1000.00", usds); - await usdc.connect(anna).approve(vault.address, usdcUnits("100.0")); - await vault.connect(anna).mint(usdc.address, usdcUnits("50.0"), 0); - await expect(anna).has.a.balanceOf("50.00", ousd); - await vault.connect(anna).redeem(ousdUnits("50.0"), ousdUnits("50")); - await vault.connect(anna).mint(usdc.address, usdcUnits("50.0"), 0); - await expect( - vault.connect(anna).redeem(ousdUnits("50.0"), ousdUnits("51")) - ).to.be.revertedWith("Redeem amount lower than minimum"); - }); + expect(tx) + .to.emit(usdc, "Transfer") + .withArgs(vault.address, mockStrategy.address, usdcUnits("4")); + }); + it("Fail to deposit 5 USDC to the default strategy", async () => { + const { vault, usdc, governor } = fixture; + + const tx = vault + .connect(governor) + .depositToStrategy( + mockStrategy.address, + [usdc.address], + [usdcUnits("5")] + ); + + await expect(tx).to.be.revertedWith( + "Not enough backing asset available" + ); + }); + it("Should allocate 3 USDC to the default strategy", async () => { + const { vault, governor, domen, usdc } = fixture; + + await vault + .connect(governor) + .setDefaultStrategy(mockStrategy.address); + + const vaultBalance = await usdc.balanceOf(vault.address); + const stratBalance = await usdc.balanceOf(mockStrategy.address); + + const tx = await vault.connect(domen).allocate(); + + // total supply is 68 starting + 21 minted = 89 OUSD + // Vault buffer is 1% of 89 = 0.89 USDC + // USDC transfer amount = 4 USDC available in vault - 0.89 USDC buffer = 3.11 USDC + await expect(tx) + .to.emit(vault, "AssetAllocated") + .withArgs(usdc.address, mockStrategy.address, usdcUnits("3.11")); + + expect(await usdc.balanceOf(vault.address)).to.eq( + vaultBalance.sub(usdcUnits("3.11")) + ); + + expect(await usdc.balanceOf(mockStrategy.address)).to.eq( + stratBalance.add(usdcUnits("3.11")) + ); + }); + }); + }); + describe("with 40 USDC in the queue, 10 USDC in the vault, 30 USDC already claimed", () => { + beforeEach(async () => { + const { vault, usdc, daniel, josh, matt } = fixture; + + // Mint USDC to users + await usdc.mintTo(daniel.address, usdcUnits("10")); + await usdc.mintTo(josh.address, usdcUnits("20")); + await usdc.mintTo(matt.address, usdcUnits("10")); + + // Approve USDC to Vault + await usdc.connect(daniel).approve(vault.address, usdcUnits("10")); + await usdc.connect(josh).approve(vault.address, usdcUnits("20")); + await usdc.connect(matt).approve(vault.address, usdcUnits("10")); + + // Mint 60 OUSD to three users + await vault.connect(daniel).mint(usdc.address, usdcUnits("10"), "0"); + await vault.connect(josh).mint(usdc.address, usdcUnits("20"), "0"); + await vault.connect(matt).mint(usdc.address, usdcUnits("10"), "0"); + + // Request and claim 10 USDC from Vault + await vault.connect(daniel).requestWithdrawal(ousdUnits("10")); + await vault.connect(josh).requestWithdrawal(ousdUnits("20")); + await advanceTime(delayPeriod); // Advance in time to ensure time delay between request and claim. + + // Claim 10 + 20 = 30 USDC from Vault + await vault.connect(daniel).claimWithdrawal(0); + await vault.connect(josh).claimWithdrawal(1); + }); + it("Should allow the last user to request the remaining 10 USDC", async () => { + const { vault, matt } = fixture; + const fixtureWithUser = { ...fixture, user: matt }; + const dataBefore = await snapData(fixtureWithUser); + + const tx = await vault.connect(matt).requestWithdrawal(ousdUnits("10")); + + await expect(tx) + .to.emit(vault, "WithdrawalRequested") + .withArgs(matt.address, 2, ousdUnits("10"), usdcUnits("40")); + + await assertChangedData( + dataBefore, + { + ousdTotalSupply: ousdUnits("10").mul(-1), + ousdTotalValue: ousdUnits("10").mul(-1), + vaultCheckBalance: usdcUnits("10").mul(-1), + userOusd: ousdUnits("10").mul(-1), + userUsdc: 0, + vaultUsdc: 0, + queued: usdcUnits("10").mul(1), + claimable: 0, + claimed: 0, + nextWithdrawalIndex: 1, + }, + fixtureWithUser + ); + }); + it("Should allow the last user to claim the request of 10 USDC", async () => { + const { vault, matt } = fixture; + const fixtureWithUser = { ...fixture, user: matt }; + await vault.connect(matt).requestWithdrawal(ousdUnits("10")); + await advanceTime(delayPeriod); // Advance in time to ensure time delay between request and claim. + + const dataBefore = await snapData(fixtureWithUser); + + const tx = await vault.connect(matt).claimWithdrawal(2); + + await expect(tx) + .to.emit(vault, "WithdrawalClaimed") + .withArgs(matt.address, 2, ousdUnits("10")); + + await assertChangedData( + dataBefore, + { + ousdTotalSupply: 0, + ousdTotalValue: 0, + vaultCheckBalance: 0, + userOusd: 0, + userUsdc: usdcUnits("10"), + vaultUsdc: usdcUnits("10").mul(-1), + queued: 0, + claimable: usdcUnits("10"), + claimed: usdcUnits("10"), + nextWithdrawalIndex: 0, + }, + fixtureWithUser + ); + + expect(await vault.totalValue()).to.equal(0); + }); + }); + describe("with 40 USDC in the queue, 100 USDC in the vault, 0 USDC in the strategy", () => { + beforeEach(async () => { + const { vault, usdc, daniel, josh, matt } = fixture; + + // Mint USDC to users + await usdc.mintTo(daniel.address, usdcUnits("10")); + await usdc.mintTo(josh.address, usdcUnits("20")); + await usdc.mintTo(matt.address, usdcUnits("70")); + + // Approve USDC to Vault + await usdc.connect(daniel).approve(vault.address, usdcUnits("10")); + await usdc.connect(josh).approve(vault.address, usdcUnits("20")); + await usdc.connect(matt).approve(vault.address, usdcUnits("70")); + + // Mint 100 OUSD to three users + await vault.connect(daniel).mint(usdc.address, usdcUnits("10"), "0"); + await vault.connect(josh).mint(usdc.address, usdcUnits("20"), "0"); + await vault.connect(matt).mint(usdc.address, usdcUnits("70"), "0"); + + // Request 40 USDC from Vault + await vault.connect(matt).requestWithdrawal(ousdUnits("40")); + await advanceTime(delayPeriod); // Advance in time to ensure time delay between request and claim. + }); + it("Should allow user to claim the request of 40 USDC", async () => { + const { vault, matt } = fixture; + const fixtureWithUser = { ...fixture, user: matt }; + const dataBefore = await snapData(fixtureWithUser); + + const tx = await vault.connect(matt).claimWithdrawal(0); + + await expect(tx) + .to.emit(vault, "WithdrawalClaimed") + .withArgs(matt.address, 0, ousdUnits("40")); + + await assertChangedData( + dataBefore, + { + ousdTotalSupply: 0, + ousdTotalValue: 0, + vaultCheckBalance: 0, + userOusd: 0, + userUsdc: usdcUnits("40"), + vaultUsdc: usdcUnits("40").mul(-1), + queued: 0, + claimable: usdcUnits("40"), + claimed: usdcUnits("40"), + nextWithdrawalIndex: 0, + }, + fixtureWithUser + ); + }); + it("Should allow user to perform a new request and claim a smaller than the USDC available", async () => { + const { vault, josh } = fixture; + + await vault.connect(josh).requestWithdrawal(ousdUnits("20")); + await advanceTime(delayPeriod); // Advance in time to ensure time delay between request and claim. + + const tx = await vault.connect(josh).claimWithdrawal(1); + + await expect(tx).to.emit(vault, "WithdrawalClaimed"); + }); + it("Should allow user to perform a new request and claim exactly the USDC available", async () => { + const { vault, ousd, josh, matt, daniel } = fixture; + await vault.connect(matt).claimWithdrawal(0); + // All user give OUSD to another user + await ousd.connect(josh).transfer(matt.address, ousdUnits("20")); + await ousd.connect(daniel).transfer(matt.address, ousdUnits("10")); + + const fixtureWithUser = { ...fixture, user: matt }; + + // Matt request the remaining 60 OUSD to be withdrawn + await vault.connect(matt).requestWithdrawal(ousdUnits("60")); + await advanceTime(delayPeriod); // Advance in time to ensure time delay between request and claim. + + const dataBefore = await snapData(fixtureWithUser); + + const tx = await vault.connect(matt).claimWithdrawal(1); + + await expect(tx) + .to.emit(vault, "WithdrawalClaimed") + .withArgs(matt.address, 1, ousdUnits("60")); + + await assertChangedData( + dataBefore, + { + ousdTotalSupply: 0, + ousdTotalValue: 0, + vaultCheckBalance: 0, + userOusd: 0, + userUsdc: usdcUnits("60"), + vaultUsdc: usdcUnits("60").mul(-1), + queued: 0, + claimable: usdcUnits("60"), + claimed: usdcUnits("60"), + nextWithdrawalIndex: 0, + }, + fixtureWithUser + ); + }); + it("Shouldn't allow user to perform a new request and claim more than the USDC available", async () => { + const { vault, ousd, usdc, josh, matt, daniel, governor } = fixture; + await vault.connect(matt).claimWithdrawal(0); + // All user give OUSD to another user + await ousd.connect(josh).transfer(matt.address, ousdUnits("20")); + await ousd.connect(daniel).transfer(matt.address, ousdUnits("10")); + + // Matt request more than the remaining 60 OUSD to be withdrawn + await vault.connect(matt).requestWithdrawal(ousdUnits("60")); + await advanceTime(delayPeriod); // Advance in time to ensure time delay between request and claim. + await usdc + .connect(await impersonateAndFund(vault.address)) + .transfer(governor.address, usdcUnits("50")); // Vault loses 50 USDC + + const tx = vault.connect(matt).claimWithdrawal(1); + await expect(tx).to.be.revertedWith("Queue pending liquidity"); + }); + }); + describe("with 40 USDC in the queue, 15 USDC in the vault, 44 USDC in the strategy, vault insolvent by 5% => Slash 1 ether (1/20 = 5%), 19 USDC total value", () => { + beforeEach(async () => { + const { governor, vault, usdc, daniel, josh, matt, strategist } = + fixture; + // Deploy a mock strategy + const mockStrategy = await deployWithConfirmation("MockStrategy"); + await vault.connect(governor).approveStrategy(mockStrategy.address); + await vault.connect(governor).setDefaultStrategy(mockStrategy.address); + + // Mint USDC to users + await usdc.mintTo(daniel.address, usdcUnits("10")); + await usdc.mintTo(josh.address, usdcUnits("20")); + await usdc.mintTo(matt.address, usdcUnits("30")); + + // Approve USDC to Vault + await usdc.connect(daniel).approve(vault.address, usdcUnits("10")); + await usdc.connect(josh).approve(vault.address, usdcUnits("20")); + await usdc.connect(matt).approve(vault.address, usdcUnits("30")); + + // Mint 60 OUSD to three users + await vault.connect(daniel).mint(usdc.address, usdcUnits("10"), "0"); + await vault.connect(josh).mint(usdc.address, usdcUnits("20"), "0"); + await vault.connect(matt).mint(usdc.address, usdcUnits("30"), "0"); + + await vault.allocate(); + // Request and claim 10 + 20 + 10 = 40 USDC from Vault + await vault.connect(daniel).requestWithdrawal(ousdUnits("10")); + await vault.connect(josh).requestWithdrawal(ousdUnits("20")); + await vault.connect(matt).requestWithdrawal(ousdUnits("10")); + await advanceTime(delayPeriod); // Advance in time to ensure time delay between request and claim. + + // Simulate slash event of 1 ethers + await usdc + .connect(await impersonateAndFund(mockStrategy.address)) + .transfer(governor.address, usdcUnits("1")); + + // Strategist sends 15 USDC to the vault + await vault + .connect(strategist) + .withdrawFromStrategy( + mockStrategy.address, + [usdc.address], + [usdcUnits("15")] + ); + + await vault.connect(josh).addWithdrawalQueueLiquidity(); + }); + it("Should allow first user to claim the request of 10 USDC", async () => { + const { vault, daniel } = fixture; + const fixtureWithUser = { ...fixture, user: daniel }; + const dataBefore = await snapData(fixtureWithUser); + + const tx = await vault.connect(daniel).claimWithdrawal(0); + + expect(tx) + .to.emit(vault, "WithdrawalClaimed") + .withArgs(daniel.address, 0, ousdUnits("10")); + + await assertChangedData( + dataBefore, + { + ousdTotalSupply: 0, + ousdTotalValue: 0, + vaultCheckBalance: 0, + userOusd: 0, + userUsdc: usdcUnits("10"), + vaultUsdc: usdcUnits("10").mul(-1), + queued: 0, + claimable: 0, + claimed: usdcUnits("10"), + nextWithdrawalIndex: 0, + }, + fixtureWithUser + ); + }); + it("Fail to allow second user to claim the request of 20 USDC, due to liquidity", async () => { + const { vault, josh } = fixture; + + const tx = vault.connect(josh).claimWithdrawal(1); + + await expect(tx).to.be.revertedWith("Queue pending liquidity"); + }); + it("Should allow a user to create a new request with solvency check off", async () => { + // maxSupplyDiff is set to 0 so no insolvency check + const { vault, matt } = fixture; + const fixtureWithUser = { ...fixture, user: matt }; + const dataBefore = await snapData(fixtureWithUser); + + const tx = vault.connect(matt).requestWithdrawal(ousdUnits("10")); + + expect(tx) + .to.emit(vault, "WithdrawalRequested") + .withArgs(matt.address, 3, ousdUnits("10"), ousdUnits("50")); + + await assertChangedData( + dataBefore, + { + ousdTotalSupply: ousdUnits("10").mul(-1), + ousdTotalValue: ousdUnits("10").mul(-1), + vaultCheckBalance: usdcUnits("10").mul(-1), + userOusd: ousdUnits("10").mul(-1), + userUsdc: 0, + vaultUsdc: 0, + queued: usdcUnits("10").mul(1), + claimable: 0, + claimed: 0, + nextWithdrawalIndex: 1, + }, + fixtureWithUser + ); + }); + describe("with solvency check at 3%", () => { + beforeEach(async () => { + const { vault } = fixture; + // Turn on insolvency check with 3% buffer + await vault + .connect(await impersonateAndFund(await vault.governor())) + .setMaxSupplyDiff(ousdUnits("0.03")); + }); + it("Fail to allow user to create a new request due to insolvency check", async () => { + const { vault, matt } = fixture; + + const tx = vault.connect(matt).requestWithdrawal(ousdUnits("1")); + + await expect(tx).to.be.revertedWith("Backing supply liquidity error"); + }); + it("Fail to allow first user to claim a withdrawal due to insolvency check", async () => { + const { vault, daniel } = fixture; + + await advanceTime(delayPeriod); + + const tx = vault.connect(daniel).claimWithdrawal(0); + + await expect(tx).to.be.revertedWith("Backing supply liquidity error"); + }); + }); + describe("with solvency check at 10%", () => { + beforeEach(async () => { + const { vault } = fixture; + // Turn on insolvency check with 10% buffer + await vault + .connect(await impersonateAndFund(await vault.governor())) + .setMaxSupplyDiff(ousdUnits("0.1")); + }); + it("Should allow user to create a new request", async () => { + const { vault, matt } = fixture; + + const tx = await vault + .connect(matt) + .requestWithdrawal(ousdUnits("1")); + + expect(tx) + .to.emit(vault, "WithdrawalRequested") + .withArgs(matt.address, 3, ousdUnits("1"), ousdUnits("41")); + }); + it("Should allow first user to claim the request of 10 USDC", async () => { + const { vault, daniel } = fixture; + + const tx = await vault.connect(daniel).claimWithdrawal(0); + + expect(tx) + .to.emit(vault, "WithdrawalClaimed") + .withArgs(daniel.address, 0, ousdUnits("10")); + }); + }); + }); + describe("with 99 USDC in the queue, 40 USDC in the vault, total supply 1, 1% insolvency buffer", () => { + let mockStrategy; + beforeEach(async () => { + const { governor, vault, usdc, daniel, josh, matt, strategist } = + fixture; + // Deploy a mock strategy + mockStrategy = await deployWithConfirmation("MockStrategy"); + await vault.connect(governor).approveStrategy(mockStrategy.address); + await vault.connect(governor).setDefaultStrategy(mockStrategy.address); + + // Mint USDC to users + await usdc.mintTo(daniel.address, usdcUnits("20")); + await usdc.mintTo(josh.address, usdcUnits("30")); + await usdc.mintTo(matt.address, usdcUnits("50")); + + // Approve USDC to Vault + await usdc.connect(daniel).approve(vault.address, usdcUnits("20")); + await usdc.connect(josh).approve(vault.address, usdcUnits("30")); + await usdc.connect(matt).approve(vault.address, usdcUnits("50")); + + // Mint 100 OUSD to three users + await vault.connect(daniel).mint(usdc.address, usdcUnits("20"), "0"); + await vault.connect(josh).mint(usdc.address, usdcUnits("30"), "0"); + await vault.connect(matt).mint(usdc.address, usdcUnits("50"), "0"); + + await vault.allocate(); + + // Request and claim 20 + 30 + 49 = 99 USDC from Vault + await vault.connect(daniel).requestWithdrawal(ousdUnits("20")); + await vault.connect(josh).requestWithdrawal(ousdUnits("30")); + await vault.connect(matt).requestWithdrawal(ousdUnits("49")); + + await advanceTime(delayPeriod); // Advance in time to ensure time delay between request and claim. + + // Strategist sends 40 USDC to the vault + await vault + .connect(strategist) + .withdrawFromStrategy( + mockStrategy.address, + [usdc.address], + [usdcUnits("40")] + ); - it("Should calculate redeem outputs", async () => { - const { vault, anna, usdc, ousd } = fixture; - - // OUSD total supply is 200 backed by 200 USDS - await expect( - await vault.calculateRedeemOutputs(ousdUnits("50")) - ).to.deep.equal([ - usdsUnits("50"), // USDS - BigNumber.from(0), // USDT - BigNumber.from(0), // USDC - BigNumber.from(0), // TUSD - ]); - - // Mint an additional 600 USDC, so OUSD is backed by 600 USDC and 200 USDS - // meaning 1/4 of any redeem should come from USDS and 2/3 from USDC - await usdc.connect(anna).approve(vault.address, usdcUnits("600")); - await vault.connect(anna).mint(usdc.address, usdcUnits("600"), 0); - await expect(anna).has.a.balanceOf("600", ousd); - await expect( - await vault.calculateRedeemOutputs(ousdUnits("100")) - ).to.deep.equal([ - usdsUnits("25"), // USDS - BigNumber.from(0), // USDT - usdcUnits("75"), // USDC - BigNumber.from(0), // TUSD - ]); + await vault.connect(josh).addWithdrawalQueueLiquidity(); + + // Turn on insolvency check with 10% buffer + await vault + .connect(await impersonateAndFund(await vault.governor())) + .setMaxSupplyDiff(ousdUnits("0.01")); + }); + describe("with 2 ether slashed leaving 100 - 40 - 2 = 58 USDC in the strategy", () => { + beforeEach(async () => { + const { usdc, governor } = fixture; + + // Simulate slash event of 2 ethers + await usdc + .connect(await impersonateAndFund(mockStrategy.address)) + .transfer(governor.address, usdcUnits("2")); + }); + it("Should have total value of zero", async () => { + // 100 from mints - 99 outstanding withdrawals - 2 from slashing = -1 value which is rounder up to zero + expect(await fixture.vault.totalValue()).to.equal(0); + }); + it("Should have check balance of zero", async () => { + const { vault, usdc } = fixture; + // 100 from mints - 99 outstanding withdrawals - 2 from slashing = -1 value which is rounder up to zero + expect(await vault.checkBalance(usdc.address)).to.equal(0); + }); + it("Fail to allow user to create a new request due to too many outstanding requests", async () => { + const { vault, matt } = fixture; + + const tx = vault.connect(matt).requestWithdrawal(ousdUnits("1")); + + await expect(tx).to.be.revertedWith("Too many outstanding requests"); + }); + it("Fail to allow first user to claim a withdrawal due to too many outstanding requests", async () => { + const { vault, daniel } = fixture; + + await advanceTime(delayPeriod); + + const tx = vault.connect(daniel).claimWithdrawal(0); + + await expect(tx).to.be.revertedWith("Too many outstanding requests"); + }); + }); + describe("with 1 ether slashed leaving 100 - 40 - 1 = 59 USDC in the strategy", () => { + beforeEach(async () => { + const { usdc, governor } = fixture; + + // Simulate slash event of 1 ethers + await usdc + .connect(await impersonateAndFund(mockStrategy.address)) + .transfer(governor.address, usdcUnits("1")); + }); + it("Should have total value of zero", async () => { + // 100 from mints - 99 outstanding withdrawals - 1 from slashing = 0 value + expect(await fixture.vault.totalValue()).to.equal(0); + }); + it("Fail to allow user to create a new request due to too many outstanding requests", async () => { + const { vault, matt } = fixture; + + const tx = vault.connect(matt).requestWithdrawal(ousdUnits("1")); + + await expect(tx).to.be.revertedWith("Too many outstanding requests"); + }); + it("Fail to allow first user to claim a withdrawal due to too many outstanding requests", async () => { + const { vault, daniel } = fixture; + + await advanceTime(delayPeriod); + + const tx = vault.connect(daniel).claimWithdrawal(0); + + await expect(tx).to.be.revertedWith("Too many outstanding requests"); + }); + }); + describe("with 0.02 ether slashed leaving 100 - 40 - 0.02 = 59.98 USDC in the strategy", () => { + beforeEach(async () => { + const { usdc, governor } = fixture; + + // Simulate slash event of 0.001 ethers + await usdc + .connect(await impersonateAndFund(mockStrategy.address)) + .transfer(governor.address, usdcUnits("0.02")); + }); + it("Should have total value of zero", async () => { + // 100 from mints - 99 outstanding withdrawals - 0.001 from slashing = 0.999 total value + expect(await fixture.vault.totalValue()).to.equal(ousdUnits("0.98")); + }); + it("Fail to allow user to create a new 1 USDC request due to too many outstanding requests", async () => { + const { vault, matt } = fixture; + + const tx = vault.connect(matt).requestWithdrawal(ousdUnits("1")); + + await expect(tx).to.be.revertedWith("Too many outstanding requests"); + }); + + it("Fail to allow user to create a new 0.01 USDC request due to insolvency check", async () => { + const { vault, matt } = fixture; + + const tx = vault.connect(matt).requestWithdrawal(ousdUnits("0.01")); + + await expect(tx).to.be.revertedWith("Backing supply liquidity error"); + }); + it("Fail to allow first user to claim a withdrawal due to insolvency check", async () => { + const { vault, daniel } = fixture; + + await advanceTime(delayPeriod); + + const tx = vault.connect(daniel).claimWithdrawal(0); + + // diff = 1 total supply / 0.98 assets = 1.020408163265306122 which is > 1 maxSupplyDiff + await expect(tx).to.be.revertedWith("Backing supply liquidity error"); + }); + }); + }); }); }); diff --git a/contracts/test/vault/vault.mainnet.fork-test.js b/contracts/test/vault/vault.mainnet.fork-test.js index a1c8b12b3d..859dff008d 100644 --- a/contracts/test/vault/vault.mainnet.fork-test.js +++ b/contracts/test/vault/vault.mainnet.fork-test.js @@ -1,5 +1,4 @@ const { expect } = require("chai"); -const { utils } = require("ethers"); const addresses = require("../../utils/addresses"); const { loadDefaultFixture } = require("./../_fixture"); @@ -17,8 +16,6 @@ const { } = require("./../behaviour/reward-tokens.fork"); const { formatUnits } = require("ethers/lib/utils"); -const log = require("../../utils/logger")("test:fork:ousd:vault"); - /** * Regarding hardcoded addresses: * The addresses are hardcoded in the test files (instead of @@ -75,10 +72,33 @@ describe("ForkTest: Vault", function () { ); }); - it("Should have the correct OUSD MetaStrategy address set", async () => { + it("Should have the OUSD/USDC AMO mint whitelist", async () => { const { vault } = fixture; - expect(await vault.ousdMetaStrategy()).to.equal( - addresses.mainnet.CurveOUSDAMOStrategy + expect( + await vault.isMintWhitelistedStrategy( + addresses.mainnet.CurveOUSDAMOStrategy + ) + ).to.be.true; + }); + + it("Should allow only governor or strategist to redeem", async () => { + const { vault, josh, strategist, usdc, ousd } = fixture; + + await vault.connect(josh).mint(usdc.address, usdcUnits("500"), 0); + + await expect( + vault.connect(josh).redeem(ousdUnits("100"), 0) + ).to.be.revertedWith("Caller is not the Strategist or Governor"); + + // Josh sends OUSD to Strategist + const strategistAddress = await strategist.getAddress(); + await ousd.connect(josh).transfer(strategistAddress, ousdUnits("100")); + + // Strategist redeems successfully + await vault.connect(strategist).redeem(ousdUnits("100"), 0); + + expect(await usdc.balanceOf(strategistAddress)).to.be.equal( + usdcUnits("100") ); }); @@ -109,7 +129,7 @@ describe("ForkTest: Vault", function () { expect(await vault.capitalPaused()).to.be.false; }); - it("Should allow to mint and redeem w/ USDC", async () => { + it("Should allow to mint w/ USDC", async () => { const { ousd, vault, josh, usdc } = fixture; const balancePreMint = await ousd .connect(josh) @@ -122,13 +142,6 @@ describe("ForkTest: Vault", function () { const balanceDiff = balancePostMint.sub(balancePreMint); expect(balanceDiff).to.approxEqualTolerance(ousdUnits("500"), 1); - - await vault.connect(josh).redeem(balanceDiff, 0); - - const balancePostRedeem = await ousd - .connect(josh) - .balanceOf(josh.getAddress()); - expect(balancePreMint).to.approxEqualTolerance(balancePostRedeem, 1); }); it("Should calculate and return redeem outputs", async () => { @@ -219,34 +232,6 @@ describe("ForkTest: Vault", function () { }); }); - describe("Oracle", () => { - it("Should have correct Price Oracle address set", async () => { - const { vault } = fixture; - expect(await vault.priceProvider()).to.equal( - "0x36CFB852d3b84afB3909BCf4ea0dbe8C82eE1C3c" - ); - }); - - it("Should return a price for minting with USDC", async () => { - const { vault, usdc } = fixture; - const price = await vault.priceUnitMint(usdc.address); - - log(`Price for minting with USDC: ${utils.formatEther(price, 6)}`); - - expect(price).to.be.lte(utils.parseEther("1")); - expect(price).to.be.gt(utils.parseEther("0.999")); - }); - - it("Should return a price for redeem with USDC", async () => { - const { vault, usdc } = fixture; - const price = await vault.priceUnitRedeem(usdc.address); - - log(`Price for redeeming with USDC: ${utils.formatEther(price, 6)}`); - - expect(price).to.be.gte(utils.parseEther("1")); - }); - }); - describe("Assets & Strategies", () => { it("Should NOT have any unknown assets", async () => { const { vault } = fixture; @@ -293,11 +278,10 @@ describe("ForkTest: Vault", function () { }); it("Should have correct default strategy set for USDC", async () => { - const { vault, usdc } = fixture; - - expect([ - "0x603CDEAEC82A60E3C4A10dA6ab546459E5f64Fa0", // Meta Morpho USDC - ]).to.include(await vault.assetDefaultStrategies(usdc.address)); + const { vault } = fixture; + expect(await vault.defaultStrategy()).to.equal( + addresses.mainnet.CurveOUSDAMOStrategy + ); }); it("Should be able to withdraw from all strategies", async () => { diff --git a/contracts/test/vault/z_mockvault.js b/contracts/test/vault/z_mockvault.js index bc28471a85..cda07c9473 100644 --- a/contracts/test/vault/z_mockvault.js +++ b/contracts/test/vault/z_mockvault.js @@ -48,12 +48,10 @@ describe("Vault mock with rebase", async () => { utils.parseUnits(`${vaultTotalValue - redeemAmount}`, 18) ); const promise = expect( - mockVault - .connect(matt) - .redeem( - utils.parseUnits(`${redeemAmount}`, 18), - utils.parseUnits(`${redeemAmount}`, 18) - ) + mockVault.connect(matt).redeem( + utils.parseUnits(`${redeemAmount}`, 18), + utils.parseUnits(`${redeemAmount}`, 6) // Only USDC in vault. + ) ); if (revertMessage) {