diff --git a/brownie-config.yml b/brownie-config.yml index f1c0e48..d64cadf 100644 --- a/brownie-config.yml +++ b/brownie-config.yml @@ -1,7 +1,7 @@ # use Ganache's forked mainnet mode as the default network # NOTE: You don't *have* to do this, but it is often helpful for testing networks: - default: mainnet-fork + default: ftm-main-fork # automatically fetch contract sources from Etherscan autofetch_sources: True diff --git a/contracts/StrategyCurveTricrypto.sol b/contracts/StrategyCurveTricrypto.sol index efd3871..aecd456 100644 --- a/contracts/StrategyCurveTricrypto.sol +++ b/contracts/StrategyCurveTricrypto.sol @@ -9,32 +9,9 @@ import "@openzeppelin/contracts/utils/Address.sol"; import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; import "@openzeppelin/contracts/math/Math.sol"; -import "./interfaces/curve.sol"; -import "./interfaces/yearn.sol"; -import {IUniswapV2Router02} from "./interfaces/uniswap.sol"; -import { - BaseStrategy, - StrategyParams -} from "@yearnvaults/contracts/BaseStrategy.sol"; - -interface IBaseFee { - function isCurrentBaseFeeAcceptable() external view returns (bool); -} - -interface IUniV3 { - struct ExactInputParams { - bytes path; - address recipient; - uint256 deadline; - uint256 amountIn; - uint256 amountOutMinimum; - } - - function exactInput(ExactInputParams calldata params) - external - payable - returns (uint256 amountOut); -} +import { IGauge, IGaugeFactory, ICurveFi } from "./interfaces/curve.sol"; +import { IUniswapV2Router02 } from "./interfaces/uniswap.sol"; +import { BaseStrategy, StrategyParams } from "@yearnvaults/contracts/BaseStrategy.sol"; abstract contract StrategyCurveBase is BaseStrategy { using SafeERC20 for IERC20; @@ -46,7 +23,10 @@ abstract contract StrategyCurveBase is BaseStrategy { // Curve stuff IGauge public constant gauge = - IGauge(0x00702BbDEaD24C40647f235F15971dB0867F6bdB); // Curve gauge contract, most are tokenized, held by strategy + IGauge(0x319E268f0A4C85D404734ee7958857F5891506d7); // Curve gauge contract, most are tokenized, held by strategy + + IGaugeFactory public constant gaugeFactory = + IGaugeFactory(0xabC000d88f23Bb45525E447528DBF656A9D55bf5); // keepCRV stuff uint256 public keepCRV; // the percentage of CRV we re-lock for boost (in basis points) @@ -130,7 +110,22 @@ abstract contract StrategyCurveBase is BaseStrategy { return balanceOfWant(); } + function _claimRewards() internal { + gaugeFactory.mint(address(gauge)); + } + + function claimRewards() external onlyVaultManagers { + // Claims any pending CRV + // + // Mints claimable CRV from the factory gauge. Reward tokens are sent to `msg.sender` + // The method claim_rewards() from the old gauge now only applies to third-party tokens. + // There are no third-party tokens in this strategy. + _claimRewards(); + } + function prepareMigration(address _newStrategy) internal override { + // Withdraw LP tokens from the gauge. The transfer to the new strategy is done + // by migrate() in BaseStrategy.sol uint256 _stakedBal = stakedBalance(); if (_stakedBal > 0) { gauge.withdraw(_stakedBal); @@ -199,9 +194,7 @@ contract StrategyCurveTricrypto is StrategyCurveBase { address spirit = 0x16327E3FbDaCA3bcF7E38F5Af2599D2DDc33aE52; want.approve(address(gauge), type(uint256).max); crv.approve(spooky, type(uint256).max); - wftm.approve(spooky, type(uint256).max); crv.approve(spirit, type(uint256).max); - wftm.approve(spirit, type(uint256).max); // set our strategy's name stratName = _name; @@ -227,11 +220,12 @@ contract StrategyCurveTricrypto is StrategyCurveBase { uint256 _debtPayment ) { - // harvest our rewards from the gauge - gauge.claim_rewards(); + // Claim and get a fresh snapshot of the strategy's CRV balance + _claimRewards(); + uint256 crvBalance = crv.balanceOf(address(this)); - uint256 wftmBalance = wftm.balanceOf(address(this)); - // if we claimed any CRV, then sell it + + // Sell CRV if we have any if (crvBalance > 0) { // keep some of our CRV to increase our boost uint256 sendToVoter = crvBalance.mul(keepCRV).div(FEE_DENOMINATOR); @@ -247,10 +241,6 @@ contract StrategyCurveTricrypto is StrategyCurveBase { _sellToken(address(crv), crvBalance); } } - // sell WFTM if we have any - if (wftmBalance > 0) { - _sellToken(address(wftm), wftmBalance); - } uint256 wethBalance = weth.balanceOf(address(this)); uint256 wbtcBalance = wbtc.balanceOf(address(this)); @@ -294,32 +284,19 @@ contract StrategyCurveTricrypto is StrategyCurveBase { forceHarvestTriggerOnce = false; } - // Sells our CRV, WFTM, or GEIST for our target token + // Sells our CRV for our target token function _sellToken(address token, uint256 _amount) internal { - if (token == address(wftm)) { - address[] memory tokenPath = new address[](2); - tokenPath[0] = address(wftm); - tokenPath[1] = address(targetToken); - IUniswapV2Router02(router).swapExactTokensForTokens( - _amount, - uint256(0), - tokenPath, - address(this), - block.timestamp - ); - } else { - address[] memory tokenPath = new address[](3); - tokenPath[0] = address(token); - tokenPath[1] = address(wftm); - tokenPath[2] = address(targetToken); - IUniswapV2Router02(router).swapExactTokensForTokens( - _amount, - uint256(0), - tokenPath, - address(this), - block.timestamp - ); - } + address[] memory tokenPath = new address[](3); + tokenPath[0] = address(token); + tokenPath[1] = address(wftm); + tokenPath[2] = address(targetToken); + IUniswapV2Router02(router).swapExactTokensForTokens( + _amount, + uint256(0), + tokenPath, + address(this), + block.timestamp + ); } /* ========== KEEP3RS ========== */ diff --git a/contracts/interfaces/curve.sol b/contracts/interfaces/curve.sol index f6a02de..486ed3c 100644 --- a/contracts/interfaces/curve.sol +++ b/contracts/interfaces/curve.sol @@ -2,138 +2,22 @@ pragma solidity 0.6.12; pragma experimental ABIEncoderV2; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; - interface IGauge { function deposit(uint256) external; function balanceOf(address) external view returns (uint256); - function claim_rewards() external; - - function claimable_tokens(address) external view returns (uint256); - - function claimable_reward(address _addressToCheck, address _rewardToken) - external - view - returns (uint256); + function claimable_tokens(address) external returns (uint256); function withdraw(uint256) external; } -interface ICurveFi { - function get_virtual_price() external view returns (uint256); - - function add_liquidity( - // EURt - uint256[2] calldata amounts, - uint256 min_mint_amount - ) external payable; - - function add_liquidity( - // Compound, sAave - uint256[2] calldata amounts, - uint256 min_mint_amount, - bool _use_underlying - ) external payable returns (uint256); - - function add_liquidity( - // Iron Bank, Aave - uint256[3] calldata amounts, - uint256 min_mint_amount, - bool _use_underlying - ) external payable returns (uint256); - - function add_liquidity( - // 3Crv Metapools - address pool, - uint256[4] calldata amounts, - uint256 min_mint_amount - ) external; - - function add_liquidity( - // Y and yBUSD - uint256[4] calldata amounts, - uint256 min_mint_amount, - bool _use_underlying - ) external payable returns (uint256); - - function add_liquidity( - // 3pool - uint256[3] calldata amounts, - uint256 min_mint_amount - ) external payable; - - function add_liquidity( - // sUSD - uint256[4] calldata amounts, - uint256 min_mint_amount - ) external payable; - - function remove_liquidity_imbalance( - uint256[2] calldata amounts, - uint256 max_burn_amount - ) external; - - function remove_liquidity(uint256 _amount, uint256[2] calldata amounts) - external; - - function remove_liquidity_one_coin( - uint256 _token_amount, - int128 i, - uint256 min_amount - ) external; - - function exchange( - int128 from, - int128 to, - uint256 _from_amount, - uint256 _min_to_amount - ) external; - - function balances(uint256) external view returns (uint256); - - function get_dy( - int128 from, - int128 to, - uint256 _from_amount - ) external view returns (uint256); - - // EURt - function calc_token_amount(uint256[2] calldata _amounts, bool _is_deposit) - external - view - returns (uint256); - - // 3Crv Metapools - function calc_token_amount( - address _pool, - uint256[4] calldata _amounts, - bool _is_deposit - ) external view returns (uint256); - - // sUSD, Y pool, etc - function calc_token_amount(uint256[4] calldata _amounts, bool _is_deposit) - external - view - returns (uint256); - - // 3pool, Iron Bank, etc - function calc_token_amount(uint256[3] calldata _amounts, bool _is_deposit) - external - view - returns (uint256); - - function calc_withdraw_one_coin(uint256 amount, int128 i) - external - view - returns (uint256); +interface IGaugeFactory { + function mint(address _gauge) external; } -interface ICrvV3 is IERC20 { - function minter() external view returns (address); -} - -interface IMinter { - function mint(address) external; +interface ICurveFi { + function add_liquidity(uint256[3] calldata amounts, uint256 min_mint_amount) + external + payable; } diff --git a/contracts/interfaces/yearn.sol b/contracts/interfaces/yearn.sol deleted file mode 100644 index fa8104e..0000000 --- a/contracts/interfaces/yearn.sol +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity 0.6.12; -pragma experimental ABIEncoderV2; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; - -interface ICurveStrategyProxy { - function proxy() external returns (address); - - function balanceOf(address _gauge) external view returns (uint256); - - function deposit(address _gauge, address _token) external; - - function withdraw( - address _gauge, - address _token, - uint256 _amount - ) external returns (uint256); - - function withdrawAll(address _gauge, address _token) - external - returns (uint256); - - function harvest(address _gauge) external; - - function lock() external; - - function approveStrategy(address) external; - - function revokeStrategy(address) external; - - function claimRewards(address _gauge, address _token) external; -} - -interface IVoter { - function execute( - address to, - uint256 value, - bytes calldata data - ) external returns (bool, bytes memory); - - function increaseAmount(uint256) external; -} diff --git a/tests/conftest.py b/tests/conftest.py index ed58bb5..12642a2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,14 +11,14 @@ def isolation(fn_isolation): def whale(accounts): # Totally in it for the tech # Update this with a large holder of your want token (the largest EOA holder of LP) - whale = accounts.at("0x4d51B782DA9e2cD073916bd4e9eC6d06916B049e", force=True) + whale = accounts.at("0x7A5ea5F5D1bEB6dD9f9De01487043FD9BCa81996", force=True) yield whale # this is the amount of funds we have our whale deposit. adjust this as needed based on their wallet balance @pytest.fixture(scope="module") -def amount(): - amount = 50e18 +def amount(token, whale): + amount = token.balanceOf(whale) // 2 yield amount @@ -42,11 +42,16 @@ def voter(): def crv(): yield Contract("0x1E4F97b9f9F913c46F1632781732927B9019C68b") - @pytest.fixture(scope="module") def other_vault_strategy(): yield Contract("0xfF8bb7261E4D51678cB403092Ae219bbEC52aa51") +# only applicable if you are migrating an existing strategy (i.e., you are not +# deploying a brand new one). This strat is using an old version of a curve gauge +@pytest.fixture(scope="module") +def strategy_to_migrate_from(): + yield Contract("0xcF3b91D83cD5FE15269E6461098fDa7d69138570") + @pytest.fixture(scope="module") def farmed(): @@ -77,7 +82,7 @@ def zero_address(): @pytest.fixture(scope="module") def gauge(): # this should be the address of the convex deposit token - gauge = "0x00702BbDEaD24C40647f235F15971dB0867F6bdB" + gauge = "0x319E268f0A4C85D404734ee7958857F5891506d7" yield Contract(gauge) @@ -146,10 +151,10 @@ def vault(pm, gov, rewards, guardian, management, token, chain): # use this if your vault is already deployed -# @pytest.fixture(scope="function") -# def vault(pm, gov, rewards, guardian, management, token, chain): -# vault = Contract("0x497590d2d57f05cf8B42A36062fA53eBAe283498") -# yield vault +@pytest.fixture(scope="function") +def vaultDeployed(): + vaultDeployed = Contract("0xCbCaF8cB8cbeAFA927ECEE0c5C56560F83E9B7D9") + yield vaultDeployed # replace the first value with the name of your strategy diff --git a/tests/test_liq_gauge.py b/tests/test_liq_gauge.py new file mode 100644 index 0000000..cd7fb47 --- /dev/null +++ b/tests/test_liq_gauge.py @@ -0,0 +1,18 @@ +from brownie import chain + + +def test_gauge_is_properly_setup(gauge): + + assert ( + gauge.reward_count() == 0 + ), "Third-party reward tokens not expected for this strategy" + + WEEK = 86400 * 7 + + weeksNum = chain.time() // WEEK + + # Pull current inflation_rate and verify it's not 0; i.e., the + # reward APY is greater than 0%. + assert ( + gauge.inflation_rate(weeksNum) is not 0 + ), "This gauge is not currently rewarding any CRV" diff --git a/tests/test_migration.py b/tests/test_migration.py index b513d99..82e91b8 100644 --- a/tests/test_migration.py +++ b/tests/test_migration.py @@ -1,6 +1,4 @@ import brownie -from brownie import Contract -from brownie import config import math @@ -20,10 +18,11 @@ def test_migration( pool, strategy_name, gauge, + crv, ): ## deposit to the vault after approving - token.approve(vault, 2 ** 256 - 1, {"from": whale}) + token.approve(vault, 2**256 - 1, {"from": whale}) vault.deposit(amount, {"from": whale}) chain.sleep(1) strategy.harvest({"from": gov}) @@ -47,11 +46,27 @@ def test_migration( chain.sleep(86400) chain.mine(1) + claimable_tokens = gauge.claimable_tokens.call(strategy) + crv_balance_old_strat = crv.balanceOf(strategy) + + assert claimable_tokens > 0, "No tokens to be claimed" + # migrate our old strategy vault.migrateStrategy(strategy, new_strategy, {"from": gov}) new_strategy.setHealthCheck(healthCheck, {"from": gov}) new_strategy.setDoHealthCheck(True, {"from": gov}) + with brownie.reverts("!authorized"): + strategy.claimRewards({"from": whale}) + + strategy.claimRewards({"from": gov}) + + assert crv.balanceOf(strategy) > 0, "No tokens were claimed" + + strategy.sweep(crv, {"from": gov}) + + assert crv.balanceOf(strategy) == 0, "Tokens were not swept" + # assert that our old strategy is empty updated_total_old = strategy.estimatedTotalAssets() assert updated_total_old == 0 @@ -66,8 +81,8 @@ def test_migration( new_strat_balance, total_old, abs_tol=5 ) - startingVault = vault.totalAssets() - print("\nVault starting assets with new strategy: ", startingVault) + starting_vault_assets = vault.totalAssets() + print("\nVault starting assets with new strategy: ", starting_vault_assets) # simulate one day of earnings chain.sleep(86400) @@ -77,7 +92,59 @@ def test_migration( new_strategy.harvest({"from": gov}) vaultAssets_2 = vault.totalAssets() # confirm we made money, or at least that we have about the same - assert vaultAssets_2 >= startingVault or math.isclose( - vaultAssets_2, startingVault, abs_tol=5 + assert vaultAssets_2 >= starting_vault_assets or math.isclose( + vaultAssets_2, starting_vault_assets, abs_tol=5 ) print("\nAssets after 1 day harvest: ", vaultAssets_2) + + +def test_migration_from_real_strat( + gov, + vaultDeployed, + strategist, + chain, + healthCheck, + strategy_to_migrate_from, + StrategyCurveTricrypto, + strategy_name, +): + + strategy_to_migrate_from.harvest({"from": gov}) + + total_old = strategy_to_migrate_from.estimatedTotalAssets() + + # deploy our new strategy + new_strategy = strategist.deploy( + StrategyCurveTricrypto, + vaultDeployed, + strategy_name, + ) + + # migrate our old strategy + vaultDeployed.migrateStrategy(strategy_to_migrate_from, new_strategy, {"from": gov}) + new_strategy.setDoHealthCheck(True, {"from": gov}) + + # assert that our old strategy is empty + updated_total_old = strategy_to_migrate_from.estimatedTotalAssets() + assert updated_total_old == 0 + + # harvest to get funds back in strategy + new_strategy.harvest({"from": gov}) + new_strat_balance = new_strategy.estimatedTotalAssets() + + # confirm the same amount of assets were moved to the new strat + assert new_strat_balance == total_old + + starting_vault_assets = vaultDeployed.totalAssets() + print("\nVault starting assets with new strategy: ", starting_vault_assets) + + # simulate one day of earnings + chain.sleep(86400) + chain.mine(1) + + # Test out our migrated strategy, confirm we're making a profit + new_strategy.harvest({"from": gov}) + vaultAssets_2 = vaultDeployed.totalAssets() + + # confirm we made money + assert vaultAssets_2 > starting_vault_assets diff --git a/tests/test_odds_and_ends.py b/tests/test_odds_and_ends.py index e8952dc..7dd912b 100644 --- a/tests/test_odds_and_ends.py +++ b/tests/test_odds_and_ends.py @@ -75,8 +75,8 @@ def test_odds_and_ends( new_strat_balance = new_strategy.estimatedTotalAssets() assert new_strat_balance >= total_old - startingVault = vault.totalAssets() - print("\nVault starting assets with new strategy: ", startingVault) + starting_vault_assets = vault.totalAssets() + print("\nVault starting assets with new strategy: ", starting_vault_assets) # simulate one day of earnings chain.sleep(86400) @@ -85,7 +85,7 @@ def test_odds_and_ends( # Test out our migrated strategy, confirm we're making a profit new_strategy.harvest({"from": gov}) vaultAssets_2 = vault.totalAssets() - assert vaultAssets_2 >= startingVault + assert vaultAssets_2 >= starting_vault_assets print("\nAssets after 1 day harvest: ", vaultAssets_2) # check our oracle @@ -201,8 +201,8 @@ def test_odds_and_ends_migration( new_strat_balance, total_old, abs_tol=5 ) - startingVault = vault.totalAssets() - print("\nVault starting assets with new strategy: ", startingVault) + starting_vault_assets = vault.totalAssets() + print("\nVault starting assets with new strategy: ", starting_vault_assets) # simulate one day of earnings chain.sleep(86400) @@ -216,8 +216,8 @@ def test_odds_and_ends_migration( new_strategy.harvest({"from": gov}) vaultAssets_2 = vault.totalAssets() # confirm we made money, or at least that we have about the same - assert vaultAssets_2 >= startingVault or math.isclose( - vaultAssets_2, startingVault, abs_tol=5 + assert vaultAssets_2 >= starting_vault_assets or math.isclose( + vaultAssets_2, starting_vault_assets, abs_tol=5 ) print("\nAssets after 1 day harvest: ", vaultAssets_2) diff --git a/tests/test_simple_harvest.py b/tests/test_simple_harvest.py index 0d50ea6..bdb4561 100644 --- a/tests/test_simple_harvest.py +++ b/tests/test_simple_harvest.py @@ -52,7 +52,8 @@ def test_simple_harvest( chain.sleep(1) new_assets = vault.totalAssets() # confirm we made money, or at least that we have about the same - assert new_assets >= old_assets + assert new_assets > old_assets + print("\nAssets after 1 day: ", new_assets / 1e18) # Display estimated APR @@ -83,7 +84,7 @@ def test_simple_harvest( chain.sleep(1) after_usdc_assets = vault.totalAssets() # confirm we made money, or at least that we have about the same - assert after_usdc_assets >= before_usdc_assets + assert after_usdc_assets > before_usdc_assets # Display estimated APR print( @@ -114,7 +115,7 @@ def test_simple_harvest( chain.sleep(1) after_usdc_assets = vault.totalAssets() # confirm we made money, or at least that we have about the same - assert after_usdc_assets >= before_usdc_assets + assert after_usdc_assets > before_usdc_assets # Display estimated APR print( @@ -131,4 +132,4 @@ def test_simple_harvest( # withdraw and confirm we made money, or at least that we have about the same vault.withdraw({"from": whale}) - assert token.balanceOf(whale) >= startingWhale + assert token.balanceOf(whale) > startingWhale diff --git a/tests/test_withdraw_after_donation.py b/tests/test_withdraw_after_donation.py index ec41a2d..55cb79f 100644 --- a/tests/test_withdraw_after_donation.py +++ b/tests/test_withdraw_after_donation.py @@ -479,7 +479,7 @@ def test_withdraw_after_donation_7( current_assets = vault.totalAssets() # assert that our total assets have gone up or stayed the same when accounting for the donation and withdrawal - assert current_assets >= donation - withdrawal + prev_assets + assert current_assets > donation - withdrawal + prev_assets new_params = vault.strategies(strategy).dict() @@ -562,7 +562,7 @@ def test_withdraw_after_donation_8( current_assets = vault.totalAssets() # assert that our total assets have gone up or stayed the same when accounting for the donation and withdrawal - assert current_assets >= donation - withdrawal + prev_assets + assert current_assets > donation - withdrawal + prev_assets new_params = vault.strategies(strategy).dict()