diff --git a/.gitignore b/.gitignore index 0ff2835..3e9ec3d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ __pycache__ build/ reports/ .env +env # Node/npm node_modules/ diff --git a/brownie-config.yml b/brownie-config.yml index d386fdb..00ac495 100644 --- a/brownie-config.yml +++ b/brownie-config.yml @@ -8,7 +8,7 @@ autofetch_sources: True # require OpenZepplin Contracts dependencies: - - yearn/yearn-vaults@0.4.3 + - yearn/yearn-vaults@0.3.0-2 - OpenZeppelin/openzeppelin-contracts@3.1.0 # path remapping to support imports from GitHub/NPM @@ -16,7 +16,7 @@ compiler: solc: version: 0.6.12 remappings: - - "@yearnvaults=yearn/yearn-vaults@0.4.3" + - "@yearnvaults=yearn/yearn-vaults@0.3.0-2" - "@openzeppelin=OpenZeppelin/openzeppelin-contracts@3.1.0" reports: diff --git a/contracts/LossOnFeeChecker.vy b/contracts/LossOnFeeChecker.vy new file mode 100644 index 0000000..24c31a0 --- /dev/null +++ b/contracts/LossOnFeeChecker.vy @@ -0,0 +1,190 @@ +# @version 0.3.7 +""" +@title Loss On Fee Checker +@author Yearn Finance +@license MIT +@dev + Blocks harvests on strategies that would experiece a loss due to fees. + Standard healthcheck in unable to catch such losses, thus this special + purpose contract is useful. +""" + +from vyper.interfaces import ERC20 +from vyper.interfaces import ERC20Detailed + +interface IVault: + def totalAssets() -> uint256: view + def managementFee() -> uint256: view + def performanceFee() -> uint256: view + def strategies(strategy: address) -> StrategyParams: view + def lastReport() -> uint256: view + def totalDebt() -> uint256: view + def apiVersion() -> String[28]: view + +interface IVaultNew: + def strategies(strategy: address) -> StrategyParamsNew: view + +interface IStrategy: + def vault() -> address: view + def want() -> address: view + +event Sweep: + sweeper: indexed(address) + token: indexed(address) + amount: uint256 + +struct StrategyParams: + performanceFee: uint256 + activation: uint256 + debtRatio: uint256 + rateLimit: uint256 + lastReport: uint256 + totalDebt: uint256 + totalGain: uint256 + totalLoss: uint256 + +struct StrategyParamsNew: + performanceFee: uint256 + activation: uint256 + debtRatio: uint256 + minDebtPerHarvest: uint256 + maxDebtPerHarvest: uint256 + lastReport: uint256 + totalDebt: uint256 + totalGain: uint256 + totalLoss: uint256 + +MAX_BPS: constant(uint256) = 10_000 +SECS_PER_YEAR: constant(uint256) = 31_557_600 + +@external +@view +def check_loss(gain: uint256, loss: uint256, strategy: address = msg.sender) -> uint256: + return self._check(gain, loss, strategy) + +@internal +@view +def _check(gain: uint256, loss: uint256, strategy: address) -> uint256: + """ + @notice + Check for losses generated by fees and return a precise amount. + @param gain - amount of reported gain + @param loss - amount of reported loss + @param strategy - strategy to compute for. defaults to msg.sender for use on-chain. + @return amount of projected loss + @dev + Fee calculations replicate logic from vault versions + 0.3.0 - mgmt fee calculated based on total assets in vault + 0.3.1 - mgmt calculated based on total debt of vault + 0.3.2 - fee calculations are same as 0.3.1, but StrategyParams are different + """ + + vault: IVault = IVault(IStrategy(strategy).vault()) + + management_fee: uint256 = vault.managementFee() + if management_fee == 0: + return 0 + + total_assets: uint256 = vault.totalAssets() + if total_assets == 0: + return 0 + + last_report: uint256 = vault.lastReport() + if last_report == 0: + return 0 + + time_since: uint256 = block.timestamp - last_report + if time_since == 0: + return 0 + + api: String[28] = vault.apiVersion() + if api == "0.3.0": + params: StrategyParams = vault.strategies(strategy) + return self._calc030(vault, gain, loss, management_fee, params.performanceFee, total_assets, time_since) + if api == "0.3.1": + params: StrategyParams = vault.strategies(strategy) + return self._calc031(vault, gain, loss, management_fee, params.performanceFee, time_since) + if api == "0.3.2": + params: StrategyParamsNew = IVaultNew(vault.address).strategies(strategy) + return self._calc031(vault, gain, loss, management_fee, params.performanceFee, time_since) + else: + raise # @dev: Vault api version not supported + + return 0 + +@internal +@view +def _calc030( + vault: IVault, + gain: uint256, + loss: uint256, + management_fee: uint256, + strat_perf_fee: uint256, + total_assets: uint256, + time_since: uint256 +) -> uint256: + total_fee: uint256 = ( + total_assets * time_since * management_fee + / MAX_BPS + / SECS_PER_YEAR + ) + + if gain > 0: + strategist_fee: uint256 = gain * strat_perf_fee / MAX_BPS + performance_fee: uint256 = gain * vault.performanceFee() / MAX_BPS + total_fee = total_fee + strategist_fee + performance_fee + + if gain >= loss: + gross_profit: uint256 = gain - loss + if gross_profit >= total_fee: + return 0 + else: + return total_fee - gross_profit + else: + gross_loss: uint256 = loss - gain + return gross_loss + total_fee + + return 0 + +@internal +@view +def _calc031( + vault: IVault, + gain: uint256, + loss: uint256, + management_fee: uint256, + strat_perf_fee: uint256, + time_since: uint256 +) -> uint256: + vault_debt: uint256 = vault.totalDebt() + total_fee: uint256 = ( + vault_debt * time_since * management_fee + / MAX_BPS + / SECS_PER_YEAR + ) + + if gain > 0: + strategist_fee: uint256 = gain * strat_perf_fee / MAX_BPS + performance_fee: uint256 = gain * vault.performanceFee() / MAX_BPS + total_fee = total_fee + strategist_fee + performance_fee + + if gain >= loss: + gross_profit: uint256 = gain - loss + if gross_profit >= total_fee: + return 0 + else: + return total_fee - gross_profit + else: + gross_loss: uint256 = loss - gain + return gross_loss + total_fee + + return 0 + +@external +def sweep(token: address): + """ + @notice Allow governance ms to sweep + """ + assert msg.sender == 0xFEB4acf3df3cDEA7399794D0869ef76A6EfAff52 # @dev: !approved + bal: uint256 = ERC20(token).balanceOf(self) + ERC20(token).transfer(msg.sender, bal, default_return_value=True) \ No newline at end of file diff --git a/contracts/RouterStrategy.sol b/contracts/RouterStrategy.sol index bcbe7ce..4d77cf3 100644 --- a/contracts/RouterStrategy.sol +++ b/contracts/RouterStrategy.sol @@ -12,15 +12,19 @@ import { } from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; import "@openzeppelin/contracts/math/Math.sol"; +interface ILossChecker { + function check_loss(uint, uint) external view returns (uint); +} + +interface ISharesHelper { + function sharesToAmount(address, uint) external view returns (uint); + function amountToShares(address, uint) external view returns (uint); +} + interface IVault is IERC20 { function token() external view returns (address); - function decimals() external view returns (uint256); - function deposit() external; - - function pricePerShare() external view returns (uint256); - function withdraw( uint256 amount, address account, @@ -35,8 +39,12 @@ contract RouterStrategy is BaseStrategy { string internal strategyName; IVault public yVault; + ILossChecker public constant lossChecker = ILossChecker(0x6b6003d4Bc320Ed25E8E2be49600EC1006676239); + uint256 public feeLossTolerance; uint256 public maxLoss; bool internal isOriginal = true; + ISharesHelper public constant sharesHelper = + ISharesHelper(0x444443bae5bB8640677A8cdF94CB8879Fec948Ec); // CREATE2 generated address, deployable to all chains constructor( address _vault, @@ -104,6 +112,7 @@ contract RouterStrategy is BaseStrategy { { yVault = IVault(_yVault); strategyName = _strategyName; + IERC20(address(want)).approve(_yVault, uint256(-1)); } function name() external view override returns (string memory) { @@ -120,10 +129,6 @@ contract RouterStrategy is BaseStrategy { return balanceOfWant().add(valueOfInvestment()); } - function delegatedAssets() external view override returns (uint256) { - return vault.strategies(address(this)).totalDebt; - } - function prepareReturn(uint256 _debtOutstanding) internal virtual @@ -138,7 +143,7 @@ contract RouterStrategy is BaseStrategy { uint256 _totalAsset = estimatedTotalAssets(); // Estimate the profit we have so far - if (_totalDebt <= _totalAsset) { + if (_totalDebt < _totalAsset) { _profit = _totalAsset.sub(_totalDebt); } @@ -164,6 +169,9 @@ contract RouterStrategy is BaseStrategy { _profit = _profit.sub(_loss); _loss = 0; } + + uint expectedLoss = lossChecker.check_loss(_profit, _loss); + require(feeLossTolerance >= expectedLoss, "LossyWithFees"); } function adjustPosition(uint256 _debtOutstanding) @@ -177,7 +185,6 @@ contract RouterStrategy is BaseStrategy { uint256 balance = balanceOfWant(); if (balance > 0) { - _checkAllowance(address(yVault), address(want), balance); yVault.deposit(); } } @@ -221,20 +228,6 @@ contract RouterStrategy is BaseStrategy { yVault.withdraw(sharesToWithdraw, address(this), maxLoss); } - function liquidateAllPositions() - internal - virtual - override - returns (uint256 _amountFreed) - { - return - yVault.withdraw( - yVault.balanceOf(address(this)), - address(this), - maxLoss - ); - } - function prepareMigration(address _newStrategy) internal virtual override { IERC20(yVault).safeTransfer( _newStrategy, @@ -252,29 +245,12 @@ contract RouterStrategy is BaseStrategy { ret[0] = address(yVault); } - function ethToWant(uint256 _amtInWei) - public - view - virtual - override - returns (uint256) - { - return _amtInWei; - } - - function setMaxLoss(uint256 _maxLoss) public onlyVaultManagers { + function setMaxLoss(uint256 _maxLoss) public onlyAuthorized { maxLoss = _maxLoss; } - function _checkAllowance( - address _contract, - address _token, - uint256 _amount - ) internal { - if (IERC20(_token).allowance(address(this), _contract) < _amount) { - IERC20(_token).safeApprove(_contract, 0); - IERC20(_token).safeApprove(_contract, type(uint256).max); - } + function setFeeLossTolerance(uint256 _tolerance) public onlyAuthorized { + feeLossTolerance = _tolerance; } function balanceOfWant() public view returns (uint256) { @@ -286,13 +262,10 @@ contract RouterStrategy is BaseStrategy { view returns (uint256) { - return amount.mul(10**yVault.decimals()).div(yVault.pricePerShare()); + return sharesHelper.amountToShares(address(yVault), amount); } function valueOfInvestment() public view virtual returns (uint256) { - return - yVault.balanceOf(address(this)).mul(yVault.pricePerShare()).div( - 10**yVault.decimals() - ); + return sharesHelper.sharesToAmount(address(yVault), yVault.balanceOf(address(this))); } } diff --git a/contracts/Synthetix.sol b/contracts/Synthetix.sol deleted file mode 100644 index e8467d0..0000000 --- a/contracts/Synthetix.sol +++ /dev/null @@ -1,177 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity 0.6.12; -pragma experimental ABIEncoderV2; - -import "@openzeppelin/contracts/math/SafeMath.sol"; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import "./Interfaces/synthetix/ISynth.sol"; -import "./Interfaces/synthetix/IReadProxy.sol"; -import "./Interfaces/synthetix/ISynthetix.sol"; -import "./Interfaces/synthetix/IExchanger.sol"; -import "./Interfaces/synthetix/IVirtualSynth.sol"; -import "./Interfaces/synthetix/IExchangeRates.sol"; -import "./Interfaces/synthetix/IAddressResolver.sol"; - -contract Synthetix { - using SafeMath for uint256; - - // ========== SYNTHETIX CONFIGURATION ========== - bytes32 public constant sUSD = "sUSD"; - bytes32 public synthCurrencyKey; - - bytes32 internal constant TRACKING_CODE = "YEARN"; - - // ========== ADDRESS RESOLVER CONFIGURATION ========== - bytes32 private constant CONTRACT_SYNTHETIX = "Synthetix"; - bytes32 private constant CONTRACT_EXCHANGER = "Exchanger"; - bytes32 private constant CONTRACT_EXCHANGERATES = "ExchangeRates"; - bytes32 private constant CONTRACT_SYNTHSUSD = "ProxyERC20sUSD"; - bytes32 private contractSynth; - - IReadProxy public constant readProxy = - IReadProxy(0x4E3b31eB0E5CB73641EE1E65E7dCEFe520bA3ef2); - - function _initializeSynthetix(bytes32 _synth) internal { - // sETH / sBTC / sEUR / sLINK - require(contractSynth == 0, "Synth already assigned."); - contractSynth = _synth; - synthCurrencyKey = ISynth( - IReadProxy(address(resolver().getAddress(_synth))).target() - ) - .currencyKey(); - - require(synthCurrencyKey != 0x0, "!key"); - } - - function _balanceOfSynth() internal view returns (uint256) { - return IERC20(address(_synthCoin())).balanceOf(address(this)); - } - - function _balanceOfSUSD() internal view returns (uint256) { - return IERC20(address(_synthsUSD())).balanceOf(address(this)); - } - - function _synthToSUSD(uint256 _amountToSend) - internal - view - returns (uint256 amountReceived) - { - if (_amountToSend == 0 || _amountToSend == type(uint256).max) { - return _amountToSend; - } - (amountReceived, , ) = _exchanger().getAmountsForExchange( - _amountToSend, - synthCurrencyKey, - sUSD - ); - } - - function _sUSDToSynth(uint256 _amountToSend) - internal - view - returns (uint256 amountReceived) - { - if (_amountToSend == 0 || _amountToSend == type(uint256).max) { - return _amountToSend; - } - (amountReceived, , ) = _exchanger().getAmountsForExchange( - _amountToSend, - sUSD, - synthCurrencyKey - ); - } - - function _sUSDFromSynth(uint256 _amountToReceive) - internal - view - returns (uint256 amountToSend) - { - if (_amountToReceive == 0 || _amountToReceive == type(uint256).max) { - return _amountToReceive; - } - // NOTE: the fee of the trade that would be done (sUSD => synth) in this case - uint256 feeRate = - _exchanger().feeRateForExchange(sUSD, synthCurrencyKey); // in base 1e18 - // formula => amountToReceive (Synth) * price (sUSD/Synth) / (1 - feeRate) - return - _exchangeRates() - .effectiveValue(synthCurrencyKey, _amountToReceive, sUSD) - .mul(1e18) - .div(uint256(1e18).sub(feeRate)); - } - - function _synthFromSUSD(uint256 _amountToReceive) - internal - view - returns (uint256 amountToSend) - { - if (_amountToReceive == 0 || _amountToReceive == type(uint256).max) { - return _amountToReceive; - } - // NOTE: the fee of the trade that would be done (synth => sUSD) in this case - uint256 feeRate = - _exchanger().feeRateForExchange(synthCurrencyKey, sUSD); // in base 1e18 - // formula => amountToReceive (sUSD) * price (Synth/sUSD) / (1 - feeRate) - return - _exchangeRates() - .effectiveValue(sUSD, _amountToReceive, synthCurrencyKey) - .mul(1e18) - .div(uint256(1e18).sub(feeRate)); - } - - function exchangeSynthToSUSD(uint256 amount) internal returns (uint256) { - if (amount == 0) { - return 0; - } - - return - _synthetix().exchangeWithTracking( - synthCurrencyKey, - amount, - sUSD, - address(this), - TRACKING_CODE - ); - } - - function exchangeSUSDToSynth(uint256 amount) internal returns (uint256) { - // swap amount of sUSD for Synth - if (amount == 0) { - return 0; - } - - return - _synthetix().exchangeWithTracking( - sUSD, - amount, - synthCurrencyKey, - address(this), - TRACKING_CODE - ); - } - - function resolver() internal view returns (IAddressResolver) { - return IAddressResolver(readProxy.target()); - } - - function _synthCoin() internal view returns (ISynth) { - return ISynth(resolver().getAddress(contractSynth)); - } - - function _synthsUSD() internal view returns (ISynth) { - return ISynth(resolver().getAddress(CONTRACT_SYNTHSUSD)); - } - - function _synthetix() internal view returns (ISynthetix) { - return ISynthetix(resolver().getAddress(CONTRACT_SYNTHETIX)); - } - - function _exchangeRates() internal view returns (IExchangeRates) { - return IExchangeRates(resolver().getAddress(CONTRACT_EXCHANGERATES)); - } - - function _exchanger() internal view returns (IExchanger) { - return IExchanger(resolver().getAddress(CONTRACT_EXCHANGER)); - } -} diff --git a/contracts/SynthetixRouterStrategy.sol b/contracts/SynthetixRouterStrategy.sol deleted file mode 100644 index 6fa5162..0000000 --- a/contracts/SynthetixRouterStrategy.sol +++ /dev/null @@ -1,341 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity 0.6.12; -pragma experimental ABIEncoderV2; - -import "./RouterStrategy.sol"; -import "./Synthetix.sol"; - -interface IUni { - function getAmountsOut(uint256 amountIn, address[] calldata path) - external - view - returns (uint256[] memory amounts); -} - -contract SynthetixRouterStrategy is RouterStrategy, Synthetix { - uint256 internal constant DENOMINATOR = 10_000; - uint256 internal constant DUST_THRESHOLD = 10_000; - address public constant weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; - address public constant uniswapRouter = - 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; - - // This is the amount of sUSD that should not be exchanged for synth - // Usually 100 for 1%. - uint256 public susdBuffer; - - constructor( - address _vault, - address _yVault, - string memory _strategyName, - bytes32 _synth, - uint256 _susdBuffer - ) public RouterStrategy(_vault, _yVault, _strategyName) { - _initializeSynthetixRouter(_synth, _susdBuffer); - } - - function cloneRouter( - address _vault, - address _strategist, - address _rewards, - address _keeper, - address _yVault, - string memory _strategyName - ) external override returns (address newStrategy) { - revert(); - } - - function cloneSynthetixRouter( - address _vault, - address _strategist, - address _rewards, - address _keeper, - address _yVault, - string memory _strategyName, - bytes32 _synth, - uint256 _susdBuffer - ) external returns (address newStrategy) { - require(isOriginal); - // Copied from https://github.com/optionality/clone-factory/blob/master/contracts/CloneFactory.sol - bytes20 addressBytes = bytes20(address(this)); - assembly { - // EIP-1167 bytecode - let clone_code := mload(0x40) - mstore( - clone_code, - 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000 - ) - mstore(add(clone_code, 0x14), addressBytes) - mstore( - add(clone_code, 0x28), - 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000 - ) - newStrategy := create(0, clone_code, 0x37) - } - - SynthetixRouterStrategy(newStrategy).initialize( - _vault, - _strategist, - _rewards, - _keeper, - _yVault, - _strategyName, - _synth, - _susdBuffer - ); - - emit Cloned(newStrategy); - } - - function initialize( - address _vault, - address _strategist, - address _rewards, - address _keeper, - address _yVault, - string memory _strategyName, - bytes32 _synth, - uint256 _susdBuffer - ) public { - super.initialize( - _vault, - _strategist, - _rewards, - _keeper, - _yVault, - _strategyName - ); - _initializeSynthetixRouter(_synth, _susdBuffer); - } - - function _initializeSynthetixRouter(bytes32 _synth, uint256 _susdBuffer) - internal - { - _initializeSynthetix(_synth); - susdBuffer = _susdBuffer; - } - - function adjustPosition(uint256 _debtOutstanding) internal override { - if (emergencyExit) { - return; - } - uint256 looseSynth = _balanceOfSynth(); - uint256 _sUSDBalance = balanceOfWant(); - - // this will tell us how much we need to keep in the buffer - uint256 totalDebt = vault.strategies(address(this)).totalDebt; // in sUSD (want) - uint256 buffer = totalDebt.mul(susdBuffer).div(DENOMINATOR); - - uint256 _sUSDToInvest = - _sUSDBalance > buffer ? _sUSDBalance.sub(buffer) : 0; - uint256 _sUSDNeeded = _sUSDToInvest == 0 ? buffer.sub(_sUSDBalance) : 0; - uint256 _synthToSell = - _sUSDNeeded > 0 ? _synthFromSUSD(_sUSDNeeded) : 0; // amount of Synth that we need to sell to refill buffer - - if (_synthToSell == 0) { - // This will first deposit any loose synth in the vault if it not locked - // Then will invest all available sUSD (exchanging to Synth) - // After this, user has to manually call depositInVault to deposit synth after settlement period - if (_sUSDToInvest == 0) { - return; - } - if (isWaitingPeriodFinished() && looseSynth > DUST_THRESHOLD) { - depositInVault(); - } - exchangeSUSDToSynth(_sUSDToInvest); - // now the waiting period starts - } else if (_synthToSell >= DUST_THRESHOLD) { - // this means that we need to refill the buffer - // we may have already some uninvested Synth so we use it - uint256 available = _synthToSUSD(looseSynth); - uint256 sUSDToWithdraw = - _sUSDNeeded > available ? _sUSDNeeded.sub(available) : 0; - // this will withdraw and sell full balance of Synth (inside withdrawSomeWant) - if (sUSDToWithdraw > 0) { - withdrawSomeWant(sUSDToWithdraw, true); - } - } - } - - function liquidatePosition(uint256 _amountNeeded) - internal - override - returns (uint256 _liquidatedAmount, uint256 _loss) - { - uint256 wantBal = balanceOfWant(); // want is always sUSD - - if (wantBal < _amountNeeded) { - (_liquidatedAmount, _loss) = withdrawSomeWant(_amountNeeded, false); - } - - _liquidatedAmount = Math.min( - _amountNeeded, - _liquidatedAmount == 0 ? wantBal : _liquidatedAmount - ); - } - - function updateSUSDBuffer(uint256 _susdBuffer) public onlyVaultManagers { - require(_susdBuffer <= 10_000, "!too high"); - susdBuffer = _susdBuffer; - } - - function isWaitingPeriodFinished() public view returns (bool freeToMove) { - return - _exchanger().maxSecsLeftInWaitingPeriod( - address(this), - synthCurrencyKey - ) == 0; - } - - function depositInVault() public onlyVaultManagers { - uint256 balanceOfSynth = _balanceOfSynth(); - if (balanceOfSynth > DUST_THRESHOLD && isWaitingPeriodFinished()) { - _checkAllowance( - address(yVault), - address(_synthCoin()), - balanceOfSynth - ); - yVault.deposit(); - } - } - - //safe to enter more than we have - function withdrawSomeWant(uint256 _amount, bool performExchanges) - private - returns (uint256 _liquidatedAmount, uint256 _loss) - { - if (performExchanges) { - // we exchange synths to susd - uint256 synthBalanceBefore = _balanceOfSynth(); - uint256 sUSDBalanceBefore = balanceOfWant(); - - if (sUSDBalanceBefore < _amount) { - uint256 _newAmount = _amount.sub(sUSDBalanceBefore); - - uint256 _synthAmount = _synthFromSUSD(_newAmount); - if (isWaitingPeriodFinished()) { - if (_synthAmount <= synthBalanceBefore) { - exchangeSynthToSUSD(_synthAmount); - return (_amount, 0); - } - - _synthAmount = _synthAmount.sub(synthBalanceBefore); - } - _withdrawFromYVault(_synthAmount); - uint256 newBalanceOfSynth = _balanceOfSynth(); - if ( - newBalanceOfSynth > DUST_THRESHOLD && - isWaitingPeriodFinished() - ) { - exchangeSynthToSUSD(newBalanceOfSynth); - } - } - } - - uint256 totalAssets = balanceOfWant(); - if (_amount > totalAssets) { - _liquidatedAmount = totalAssets; - _loss = _amount.sub(totalAssets); - } else { - _liquidatedAmount = _amount; - } - } - - function liquidateAllPositions() - internal - override - returns (uint256 _amountFreed) - { - // In order to work, manualRemoveFullLiquidity needs to be call 6 min in advance - require(isWaitingPeriodFinished(), "settlement period"); - require(valueOfInvestment() < DUST_THRESHOLD, "remove liquidity first"); - require( - _balanceOfSynth() < DUST_THRESHOLD, - "exchange synth to want first" - ); - _amountFreed = balanceOfWant(); - } - - function manualRemoveFullLiquidity() - external - onlyVaultManagers - returns (uint256 _liquidatedAmount, uint256 _loss) - { - // It will withdraw all the assets from the yvault and the exchange them to want - (_liquidatedAmount, _loss) = withdrawSomeWant( - estimatedTotalAssets(), - true - ); - } - - function manualRemoveLiquidity(uint256 _liquidityToRemove) - external - onlyVaultManagers - returns (uint256 _liquidatedAmount, uint256 _loss) - { - _liquidityToRemove = Math.min( - _liquidityToRemove, - estimatedTotalAssets() - ); - // It will withdraw _liquidityToRemove assets from the yvault and the exchange them to want - (_liquidatedAmount, _loss) = withdrawSomeWant(_liquidityToRemove, true); - } - - function estimatedTotalAssets() public view override returns (uint256) { - return - balanceOfWant().add(_sUSDFromSynth(_balanceOfSynth())).add( - valueOfInvestment() - ); - } - - function ethToWant(uint256 _amtInWei) - public - view - override - returns (uint256) - { - address[] memory path = new address[](2); - path[0] = weth; - path[1] = address(want); - - uint256[] memory amounts = - IUni(uniswapRouter).getAmountsOut(_amtInWei, path); - - return amounts[amounts.length - 1]; - } - - function valueOfInvestment() public view override returns (uint256) { - return - _sUSDFromSynth( - yVault.balanceOf(address(this)).mul(yVault.pricePerShare()).div( - 10**yVault.decimals() - ) - ); - } - - function prepareMigration(address _newStrategy) internal override { - super.prepareMigration(_newStrategy); - _synthCoin().transferAndSettle(_newStrategy, _balanceOfSynth()); - } - - function prepareReturn(uint256 _debtOutstanding) - internal - override - returns ( - uint256 _profit, - uint256 _loss, - uint256 _debtPayment - ) - { - uint256 totalDebt = vault.strategies(address(this)).totalDebt; - uint256 totalAssetsAfterProfit = estimatedTotalAssets(); - uint256 _balanceOfWant = balanceOfWant(); - - _debtPayment = Math.min(_debtOutstanding, _balanceOfWant); - - if (totalDebt <= totalAssetsAfterProfit) { - _profit = _balanceOfWant.sub(_debtPayment); - } else { - _loss = totalDebt.sub(totalAssetsAfterProfit); - } - } -} diff --git a/tests/conftest.py b/tests/conftest.py index 5fca23c..0a53caa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,5 @@ import pytest -from brownie import config, Contract, ZERO_ADDRESS +from brownie import config, Contract, ZERO_ADDRESS, LossOnFeeChecker from eth_abi import encode_single @@ -52,29 +52,26 @@ def yvweth_032(): def yvweth_042(): yield Contract("0xa258C4606Ca8206D8aA700cE2143D7db854D168c") - @pytest.fixture -def origin_vault(): - # origin vault of the route - yield Contract("0xa9fE4601811213c340e850ea305481afF02f5b28") +def yvsteth_030(gov): + yield Contract("0xdCD90C7f6324cfa40d7169ef80b12031770B4325",owner=gov) @pytest.fixture -def destination_vault(): - # destination vault of the route - yield Contract("0xa258C4606Ca8206D8aA700cE2143D7db854D168c") +def yvsteth_045(gov): + yield Contract("0x5B8C556B8b2a78696F0B9B830B3d67623122E270",owner=gov) @pytest.fixture -def origin_vault(): +def origin_vault(yvsteth_030): # origin vault of the route - yield Contract("0xa9fE4601811213c340e850ea305481afF02f5b28") + yield yvsteth_030 @pytest.fixture -def destination_vault(): +def destination_vault(yvsteth_045): # destination vault of the route - yield Contract("0xa258C4606Ca8206D8aA700cE2143D7db854D168c") + yield yvsteth_045 @pytest.fixture @@ -114,7 +111,6 @@ def weth_amout(user, weth): def health_check(): yield Contract("0xddcea799ff1699e98edf118e0629a974df7df012") - @pytest.fixture def vault(pm, gov, rewards, guardian, management, token): Vault = pm(config["dependencies"][0]).Vault @@ -125,6 +121,12 @@ def vault(pm, gov, rewards, guardian, management, token): yield vault yield Contract("0xa9fE4601811213c340e850ea305481afF02f5b28") +@pytest.fixture +def loss_checker(strategist, gov): + # loss_checker = strategist.deploy(LossOnFeeChecker) + # loss_checker.approve_sweeper(gov,True,{'from':gov}) + loss_checker = Contract('0x6b6003d4Bc320Ed25E8E2be49600EC1006676239') + yield loss_checker @pytest.fixture def strategy( @@ -134,10 +136,11 @@ def strategy( destination_vault, RouterStrategy, gov, + loss_checker, health_check, ): strategy = strategist.deploy( - RouterStrategy, origin_vault, destination_vault, "Route yvWETH 042" + RouterStrategy, origin_vault, destination_vault, "Strat "+origin_vault.symbol() ) strategy.setKeeper(keeper) @@ -147,13 +150,17 @@ def strategy( break origin_vault.updateStrategyDebtRatio(strat_address, 0, {"from": gov}) + try: + Contract(strat_address,owner=gov).setDoHealthCheck(False) + except: + pass + Contract(strat_address,owner=gov).harvest() strategy.setHealthCheck(health_check, {"from": origin_vault.governance()}) - origin_vault.addStrategy(strategy, 10_000, 0, 2 ** 256 - 1, 0, {"from": gov}) - + origin_vault.addStrategy(strategy, 10_000, 0, 0, {"from": gov}) + # loss_checker.approve_sweeper(strategy,True,{'from':gov}) yield strategy - @pytest.fixture def unique_strategy( strategist, keeper, yvweth_032, yvweth_042, RouterStrategy, gov, health_check diff --git a/tests/test_operation_steth.py b/tests/test_operation_steth.py new file mode 100644 index 0000000..34ef9bc --- /dev/null +++ b/tests/test_operation_steth.py @@ -0,0 +1,73 @@ +import pytest +from brownie import Contract, ZERO_ADDRESS, Wei, chain, accounts, reverts + + +def test_migrate(origin_vault, destination_vault, strategy, gov, loss_checker): + whale = accounts.at('0x99ac10631F69C753DDb595D074422a0922D9056B',force=True) + old_vault = origin_vault + new_vault = destination_vault + old_strategy = Contract(old_vault.withdrawalQueue(0),owner=gov) + new_strategy = strategy + + # At this point, all fund are already removed from existing strats + # We only have to harvest into our router strat, which is already at 100% DR + strategy.setFeeLossTolerance(1e18,{"from": gov}) + strategy.harvest({"from": gov}) + + chain.sleep(60 * 60 * 24 * 7) + + # No sells, means no profit yet + # Let's set mgmt fee pretty high + origin_vault.setManagementFee(500, {"from": gov}) + + expectedLoss = loss_checker.check_loss(0, 0, strategy) + print(f'EXPECTED LOSS AMOUNT = {expectedLoss}') + expected_loss = emulate_fees(strategy, origin_vault) + + # Harvest should fail due to loss + with reverts('LossyWithFees'): + tx = strategy.harvest({"from": gov}) + + chain.sleep(60 * 60 * 24 * 7) + # Let's transfer some want to the checker that it can sweep + est = loss_checker.check_loss(0, 0, strategy) + whale = accounts.at('0x99ac10631F69C753DDb595D074422a0922D9056B', force=True) + want = Contract(origin_vault.token(),owner=whale) + + pps = origin_vault.pricePerShare() + strategy.setFeeLossTolerance(100e18,{"from": gov}) + + tx = strategy.harvest({"from": gov}) + + strategy.setFeeLossTolerance(100e18,{"from": gov}) + # Airdrop to offset profit + want.transfer(origin_vault, est) + assert origin_vault.pricePerShare() == pps + + + totalShares = origin_vault.totalSupply() + tx = strategy.harvest({"from": gov}) + totalSharesAfter = origin_vault.totalSupply() + assert totalSharesAfter <= totalShares + ( + strategy.feeLossTolerance() * + origin_vault.pricePerShare() / + 1e18 + ) + + print(tx.events['Harvested']) + # origin_vault.updateStrategyDebtRatio(strategy, 10_000, {'from':gov}) + + + +def emulate_fees(strategy, origin_vault): + SECS_PER_YEAR = 31_557_600 + MAX_BPS = 10_000 + v = origin_vault + mgmt_fee = v.managementFee() + params = v.strategies(strategy).dict() + last = v.lastReport() + current = chain.time() + time_since = current - last + total_assets = v.totalAssets() + gov_fee = total_assets * time_since * mgmt_fee / MAX_BPS / SECS_PER_YEAR + return gov_fee \ No newline at end of file