diff --git a/contracts/MorphoAaveStrategy.sol b/contracts/MorphoAaveStrategy.sol index e04cfd2..a4acf88 100644 --- a/contracts/MorphoAaveStrategy.sol +++ b/contracts/MorphoAaveStrategy.sol @@ -8,6 +8,7 @@ pragma experimental ABIEncoderV2; import "./MorphoStrategy.sol"; contract MorphoAaveStrategy is MorphoStrategy { + // TODO: change the reward token (last param) if needed, current is AAVE constructor( address _vault, address _poolToken, @@ -19,7 +20,12 @@ contract MorphoAaveStrategy is MorphoStrategy { _poolToken, _strategyName, 0x777777c9898D384F785Ee44Acfe945efDFf5f3E0, - 0x507fA343d0A90786d86C7cd885f5C49263A91FF4 + 0x507fA343d0A90786d86C7cd885f5C49263A91FF4, + 0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9 ) {} + + function claimRewardToken() internal override { + // TODO: implement function for claiming rewards when added to Morpho Aave + } } diff --git a/contracts/MorphoCompoundStrategy.sol b/contracts/MorphoCompoundStrategy.sol index e46f82d..36437a0 100644 --- a/contracts/MorphoCompoundStrategy.sol +++ b/contracts/MorphoCompoundStrategy.sol @@ -6,24 +6,8 @@ pragma solidity 0.6.12; pragma experimental ABIEncoderV2; import "./MorphoStrategy.sol"; -import "../interfaces/IUniswapV2Router01.sol"; -import "../interfaces/ySwap/ITradeFactory.sol"; contract MorphoCompoundStrategy is MorphoStrategy { - // ySwap TradeFactory: - address public tradeFactory; - // Router used for swapping reward token (COMP) - IUniswapV2Router01 public currentV2Router; - // Minimum amount of COMP to be claimed or sold - uint256 public minCompToClaimOrSell = 0.1 ether; - - address private constant COMP = 0xc00e94Cb662C3520282E6f5717214004A7f26888; - address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; - IUniswapV2Router01 private constant UNI_V2_ROUTER = - IUniswapV2Router01(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D); - IUniswapV2Router01 private constant SUSHI_V2_ROUTER = - IUniswapV2Router01(0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F); - constructor( address _vault, address _poolToken, @@ -35,124 +19,20 @@ contract MorphoCompoundStrategy is MorphoStrategy { _poolToken, _strategyName, 0x8888882f8f843896699869179fB6E4f7e3B58888, - 0x930f1b46e1D081Ec1524efD95752bE3eCe51EF67 + 0x930f1b46e1D081Ec1524efD95752bE3eCe51EF67, + 0xc00e94Cb662C3520282E6f5717214004A7f26888 ) - { - currentV2Router = SUSHI_V2_ROUTER; - IERC20 comp = IERC20(COMP); - // COMP max allowance is uint96 - comp.safeApprove(address(SUSHI_V2_ROUTER), type(uint96).max); - comp.safeApprove(address(UNI_V2_ROUTER), type(uint96).max); - } - - // ---------------------- MorphoStrategy overriden contract function ---------------- - function prepareReturn(uint256 _debtOutstanding) - internal - override - returns ( - uint256 _profit, - uint256 _loss, - uint256 _debtPayment - ) - { - claimComp(); - sellComp(); - - return super.prepareReturn(_debtOutstanding); - } + {} - function prepareMigration(address _newStrategy) internal override { - super.prepareMigration(_newStrategy); - - claimComp(); - IERC20 comp = IERC20(COMP); - comp.safeTransfer(_newStrategy, comp.balanceOf(address(this))); - } - - // ---------------------- functions for claiming reward token COMP ------------------ - function claimComp() internal { + function claimRewardToken() internal override { address[] memory pools = new address[](1); pools[0] = poolToken; if ( lens.getUserUnclaimedRewards(pools, address(this)) > - minCompToClaimOrSell + minRewardToClaimOrSell ) { // claim the underlying pool's rewards, currently COMP token morpho.claimRewards(pools, false); } } - - // ---------------------- functions for selling reward token COMP ------------------- - /** - * @notice - * Set toggle v2 swap router between sushiv2 and univ2 - */ - function setToggleV2Router() external onlyAuthorized { - currentV2Router = currentV2Router == SUSHI_V2_ROUTER - ? UNI_V2_ROUTER - : SUSHI_V2_ROUTER; - } - - /** - * @notice - * Set the minimum amount of compount token need to claim or sell it for `want` token. - */ - function setMinCompToClaimOrSell(uint256 _minCompToClaimOrSell) - external - onlyAuthorized - { - minCompToClaimOrSell = _minCompToClaimOrSell; - } - - function sellComp() internal { - if (tradeFactory == address(0)) { - uint256 compBalance = IERC20(COMP).balanceOf(address(this)); - if (compBalance > minCompToClaimOrSell) { - currentV2Router.swapExactTokensForTokens( - compBalance, - 0, - getTokenOutPathV2(COMP, address(want)), - address(this), - block.timestamp - ); - } - } - } - - function getTokenOutPathV2(address _tokenIn, address _tokenOut) - internal - pure - returns (address[] memory _path) - { - bool isWeth = _tokenIn == address(WETH) || _tokenOut == address(WETH); - _path = new address[](isWeth ? 2 : 3); - _path[0] = _tokenIn; - - if (isWeth) { - _path[1] = _tokenOut; - } else { - _path[1] = address(WETH); - _path[2] = _tokenOut; - } - } - - // ---------------------- YSWAPS FUNCTIONS ---------------------- - function setTradeFactory(address _tradeFactory) external onlyGovernance { - if (tradeFactory != address(0)) { - _removeTradeFactoryPermissions(); - } - IERC20(COMP).safeApprove(_tradeFactory, type(uint96).max); - ITradeFactory tf = ITradeFactory(_tradeFactory); - tf.enable(COMP, address(want)); - tradeFactory = _tradeFactory; - } - - function removeTradeFactoryPermissions() external onlyEmergencyAuthorized { - _removeTradeFactoryPermissions(); - } - - function _removeTradeFactoryPermissions() internal { - IERC20(COMP).safeApprove(tradeFactory, 0); - tradeFactory = address(0); - } } diff --git a/contracts/MorphoStrategy.sol b/contracts/MorphoStrategy.sol index 9650a67..c686b86 100644 --- a/contracts/MorphoStrategy.sol +++ b/contracts/MorphoStrategy.sol @@ -20,12 +20,27 @@ import "@openzeppelin/contracts/math/Math.sol"; import "../interfaces/IMorpho.sol"; import "../interfaces/ILens.sol"; +import "../interfaces/IUniswapV2Router01.sol"; +import "../interfaces/ySwap/ITradeFactory.sol"; abstract contract MorphoStrategy is BaseStrategy { using SafeERC20 for IERC20; using Address for address; using SafeMath for uint256; + address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + IUniswapV2Router01 private constant UNI_V2_ROUTER = + IUniswapV2Router01(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D); + IUniswapV2Router01 private constant SUSHI_V2_ROUTER = + IUniswapV2Router01(0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F); + + // ySwap TradeFactory: + address public tradeFactory; + // Router used for swapping reward token (COMP) + IUniswapV2Router01 public currentV2Router; + // Minimum amount of COMP to be claimed or sold + uint256 public minRewardToClaimOrSell = 0.1 ether; + address public rewardToken; // Morpho is a contract to handle interaction with the protocol IMorpho public immutable morpho; // Lens is a contract to fetch data about Morpho protocol @@ -41,15 +56,24 @@ abstract contract MorphoStrategy is BaseStrategy { address _poolToken, string memory _strategyName, address _morpho, - address _lens + address _lens, + address _rewardToken ) public BaseStrategy(_vault) { poolToken = _poolToken; strategyName = _strategyName; lens = ILens(_lens); morpho = IMorpho(_morpho); want.safeApprove(_morpho, type(uint256).max); + + currentV2Router = SUSHI_V2_ROUTER; + rewardToken = _rewardToken; + IERC20 iRewardToken = IERC20(_rewardToken); + iRewardToken.safeApprove(address(SUSHI_V2_ROUTER), type(uint256).max); + iRewardToken.safeApprove(address(UNI_V2_ROUTER), type(uint256).max); } + function claimRewardToken() internal virtual; + // ******** BaseStrategy overriden contract function ************ function name() external view override returns (string memory) { @@ -72,6 +96,9 @@ abstract contract MorphoStrategy is BaseStrategy { uint256 _debtPayment ) { + claimRewardToken(); + sellRewardToken(); + uint256 totalDebt = vault.strategies(address(this)).totalDebt; uint256 totalAssetsAfterProfit = estimatedTotalAssets(); _profit = totalAssetsAfterProfit > totalDebt @@ -141,6 +168,13 @@ abstract contract MorphoStrategy is BaseStrategy { // NOTE: Can override `tendTrigger` and `harvestTrigger` if necessary function prepareMigration(address _newStrategy) internal virtual override { liquidateAllPositions(); + + claimRewardToken(); + IERC20 iRewardToken = IERC20(rewardToken); + iRewardToken.safeTransfer( + _newStrategy, + iRewardToken.balanceOf(address(this)) + ); } function protectedTokens() @@ -205,4 +239,79 @@ abstract contract MorphoStrategy is BaseStrategy { { maxGasForMatching = _maxGasForMatching; } + + // ---------------------- functions for selling reward token COMP ------------------- + /** + * @notice + * Set toggle v2 swap router between sushiv2 and univ2 + */ + function setToggleV2Router() external onlyAuthorized { + currentV2Router = currentV2Router == SUSHI_V2_ROUTER + ? UNI_V2_ROUTER + : SUSHI_V2_ROUTER; + } + + /** + * @notice + * Set the minimum amount of compount token need to claim or sell it for `want` token. + */ + function setMinRewardToClaimOrSell(uint256 _minRewardToClaimOrSell) + external + onlyAuthorized + { + minRewardToClaimOrSell = _minRewardToClaimOrSell; + } + + function sellRewardToken() internal { + if (tradeFactory == address(0)) { + uint256 rewardTokenBalance = + IERC20(rewardToken).balanceOf(address(this)); + if (rewardTokenBalance > minRewardToClaimOrSell) { + currentV2Router.swapExactTokensForTokens( + rewardTokenBalance, + 0, + getTokenOutPathV2(rewardToken, address(want)), + address(this), + block.timestamp + ); + } + } + } + + function getTokenOutPathV2(address _tokenIn, address _tokenOut) + internal + pure + returns (address[] memory _path) + { + bool isWeth = _tokenIn == address(WETH) || _tokenOut == address(WETH); + _path = new address[](isWeth ? 2 : 3); + _path[0] = _tokenIn; + + if (isWeth) { + _path[1] = _tokenOut; + } else { + _path[1] = address(WETH); + _path[2] = _tokenOut; + } + } + + // ---------------------- YSWAPS FUNCTIONS ---------------------- + function setTradeFactory(address _tradeFactory) external onlyGovernance { + if (tradeFactory != address(0)) { + _removeTradeFactoryPermissions(); + } + IERC20(rewardToken).safeApprove(_tradeFactory, type(uint256).max); + ITradeFactory tf = ITradeFactory(_tradeFactory); + tf.enable(rewardToken, address(want)); + tradeFactory = _tradeFactory; + } + + function removeTradeFactoryPermissions() external onlyEmergencyAuthorized { + _removeTradeFactoryPermissions(); + } + + function _removeTradeFactoryPermissions() internal { + IERC20(rewardToken).safeApprove(tradeFactory, 0); + tradeFactory = address(0); + } } diff --git a/tests/aave/conftest.py b/tests/aave/conftest.py index ff98b33..de70e28 100644 --- a/tests/aave/conftest.py +++ b/tests/aave/conftest.py @@ -46,6 +46,7 @@ def keeper(accounts): "USDC": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", # USDC } + # TODO: uncomment those tokens you want to test as want @pytest.fixture( params=[ @@ -112,6 +113,27 @@ def poolToken(token): yield aave_pool_token_addresses[token.symbol()] +@pytest.fixture +def trade_factory(): + yield Contract("0x7BAF843e06095f68F4990Ca50161C2C4E4e01ec6") + + +@pytest.fixture +def ymechs_safe(): + yield Contract("0x2C01B4AD51a67E2d8F02208F54dF9aC4c0B778B6") + + +@pytest.fixture +def aave_token(): + token_address = "0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9" + yield Contract(token_address) + + +@pytest.fixture +def aave_whale(accounts): + yield accounts.at("0x4da27a545c0c5b758a6ba100e3a049001de870f5", force=True) + + @pytest.fixture def weth(): yield Contract(token_addresses["WETH"]) @@ -151,15 +173,44 @@ def vault(pm, gov, rewards, guardian, management, token): @pytest.fixture -def strategy(strategist, keeper, vault, poolToken, token, MorphoAaveStrategy, gov): +def strategy( + strategist, + keeper, + vault, + poolToken, + MorphoAaveStrategy, + gov, + trade_factory, + ymechs_safe, + token, +): strategy = strategist.deploy( - MorphoAaveStrategy, vault, poolToken, "StrategyMorphoAave" + token.symbol() + MorphoAaveStrategy, + vault, + poolToken, + "StrategyMorphoAave" + token.symbol(), ) strategy.setKeeper(keeper) vault.addStrategy(strategy, 10_000, 0, 2**256 - 1, 1_000, {"from": gov}) + trade_factory.grantRole( + trade_factory.STRATEGY(), + strategy.address, + {"from": ymechs_safe, "gas_price": "0 gwei"}, + ) + # strategy.setTradeFactory(trade_factory.address, {"from": gov}) yield strategy +@pytest.fixture +def uni_address(): + yield "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D" + + +@pytest.fixture +def sushi_address(): + yield "0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F" + + @pytest.fixture(scope="session") def RELATIVE_APPROX(): yield 1e-5 diff --git a/tests/aave/test_yswap_aave.py b/tests/aave/test_yswap_aave.py new file mode 100644 index 0000000..4ef6d63 --- /dev/null +++ b/tests/aave/test_yswap_aave.py @@ -0,0 +1,177 @@ +import brownie +from brownie import Contract +from brownie import ZERO_ADDRESS +import pytest + + +def test_profitable_harvest_using_yswap( + chain, + token, + vault, + strategy, + user, + gov, + amount, + RELATIVE_APPROX, + aave_token, + aave_whale, + trade_factory, + ymechs_safe, +): + # Enable trade factory + strategy.setTradeFactory(trade_factory.address, {"from": gov}) + + # Deposit to the vault + token.approve(vault.address, amount, {"from": user}) + vault.deposit(amount, {"from": user}) + assert token.balanceOf(vault.address) == amount + + before_pps = vault.pricePerShare() + + # Harvest 1: Send funds through the strategy + chain.sleep(1) + strategy.harvest() + assert pytest.approx(strategy.estimatedTotalAssets(), rel=RELATIVE_APPROX) == amount + + # Strategy earned reward tokens + aave_token.transfer( + strategy, 2 * strategy.minRewardToClaimOrSell(), {"from": aave_whale} + ) + + tx_swap = execute_yswap( + trade_factory, + ymechs_safe, + strategy.address, + aave_token.address, + token.address, + aave_token.balanceOf(strategy), + ) + assert tx_swap.return_value > 0 + + # Harvest 2: Realize profit + chain.sleep(1) + strategy.harvest() + chain.sleep(3600 * 6) # 6 hrs needed for profits to unlock + chain.mine(1) + profit = token.balanceOf(vault.address) # Profits go to vault + assert strategy.estimatedTotalAssets() + profit > amount + assert vault.pricePerShare() > before_pps + + +def test_disabling_trade_factory(strategy, aave_token, gov, trade_factory): + # Enable trade factory + strategy.setTradeFactory(trade_factory.address, {"from": gov}) + + assert strategy.tradeFactory() == trade_factory.address + strategy.removeTradeFactoryPermissions({"from": gov}) + assert strategy.tradeFactory() == ZERO_ADDRESS + assert aave_token.allowance(strategy.address, trade_factory.address) == 0 + + +def test_profitable_harvest_exit_using_yswap( + chain, + token, + vault, + strategy, + strategist, + user, + gov, + amount, + RELATIVE_APPROX, + aave_token, + aave_whale, + trade_factory, + ymechs_safe, +): + # Enable trade factory + strategy.setTradeFactory(trade_factory.address, {"from": gov}) + + # Deposit to the vault + token.approve(vault.address, amount, {"from": user}) + vault.deposit(amount, {"from": user}) + assert token.balanceOf(vault.address) == amount + + before_pps = vault.pricePerShare() + + # Harvest 1: Send funds through the strategy + chain.sleep(1) + strategy.harvest() + assert pytest.approx(strategy.estimatedTotalAssets(), rel=RELATIVE_APPROX) == amount + + # Strategy earned reward tokens + aave_token.transfer( + strategy, 2 * strategy.minRewardToClaimOrSell(), {"from": aave_whale} + ) + + tx_swap = execute_yswap( + trade_factory, + ymechs_safe, + strategy.address, + aave_token.address, + token.address, + aave_token.balanceOf(strategy), + ) + assert tx_swap.return_value > 0 + + # Harvest 2: Realize profit + chain.sleep(1) + strategy.harvest() + chain.sleep(3600 * 6) # 6 hrs needed for profits to unlock + chain.mine(1) + profit = token.balanceOf(vault.address) # Profits go to vault + assert strategy.estimatedTotalAssets() + profit > amount + assert vault.pricePerShare() > before_pps + + ## Set emergency - return all founds to vault + strategy.setEmergencyExit({"from": strategist}) + strategy.harvest() ## Remove funds from strategy + + assert strategy.estimatedTotalAssets() == 0 + assert token.balanceOf(strategy) == 0 + assert token.balanceOf(vault) + profit > amount + assert vault.pricePerShare() > before_pps + + +def execute_yswap( + trade_factory, + ymechs_safe, + strategy_address, + token_in_address, + token_out_address, + amount_in, +): + asyncTradeExecutionDetails = [ + strategy_address, + token_in_address, + token_out_address, + amount_in, + 1, + ] + + if token_out_address == "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2": + path_in_bytes = ( + "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000" + + token_in_address[2:] + + "000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000" + ) + else: + path_in_bytes = ( + "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003000000000000000000000000" + + token_in_address[2:] + + "000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000" + + token_out_address[2:] + ) + + # Trigger ySwap + # trade_factory.execute( + # AsyncTradeExecutionDetails calldata _tradeExecutionDetails, + # address _swapper, + # bytes calldata _data + # ) external returns (uint256 _receivedAmount); + tx_swap = trade_factory.execute["tuple,address,bytes"]( + asyncTradeExecutionDetails, + "0x408Ec47533aEF482DC8fA568c36EC0De00593f44", + path_in_bytes, + {"from": ymechs_safe}, + ) + return tx_swap diff --git a/tests/compound/test_harvest_exit.py b/tests/compound/test_harvest_exit.py index a2c10e5..cb8a674 100644 --- a/tests/compound/test_harvest_exit.py +++ b/tests/compound/test_harvest_exit.py @@ -34,7 +34,7 @@ def test_harvest_exit( # Strategy earned reward tokens comp_token.transfer( - strategy, 2 * strategy.minCompToClaimOrSell(), {"from": comp_whale} + strategy, 2 * strategy.minRewardToClaimOrSell(), {"from": comp_whale} ) # Harvest 2: Realize profit diff --git a/tests/compound/test_operation.py b/tests/compound/test_operation.py index 2a73db7..9a0e113 100644 --- a/tests/compound/test_operation.py +++ b/tests/compound/test_operation.py @@ -75,7 +75,7 @@ def test_profitable_harvest( # Strategy earned reward tokens comp_token.transfer( - strategy, 2 * strategy.minCompToClaimOrSell(), {"from": comp_whale} + strategy, 2 * strategy.minRewardToClaimOrSell(), {"from": comp_whale} ) # Harvest 2: Realize profit diff --git a/tests/compound/test_setup.py b/tests/compound/test_setup.py index 2a07a19..d2e25ac 100644 --- a/tests/compound/test_setup.py +++ b/tests/compound/test_setup.py @@ -27,10 +27,10 @@ def test_toggle_swap_router(strategy, sushi_address, uni_address): def test_set_min_comp_to_claim(strategy): - assert strategy.minCompToClaimOrSell() == Wei("0.1 ether") + assert strategy.minRewardToClaimOrSell() == Wei("0.1 ether") new_value = Wei("11.11 ether") - strategy.setMinCompToClaimOrSell(new_value) - assert strategy.minCompToClaimOrSell() == new_value + strategy.setMinRewardToClaimOrSell(new_value) + assert strategy.minRewardToClaimOrSell() == new_value def test_set_max_gas_for_matching(strategy): diff --git a/tests/compound/test_yswap.py b/tests/compound/test_yswap.py index 4ae7bab..8a969a4 100644 --- a/tests/compound/test_yswap.py +++ b/tests/compound/test_yswap.py @@ -31,7 +31,7 @@ def test_profitable_harvest_using_yswap( # Strategy earned reward tokens comp_token.transfer( - strategy, 2 * strategy.minCompToClaimOrSell(), {"from": comp_whale} + strategy, 2 * strategy.minRewardToClaimOrSell(), {"from": comp_whale} ) tx_swap = execute_yswap( @@ -89,7 +89,7 @@ def test_profitable_harvest_exit_using_yswap( # Strategy earned reward tokens comp_token.transfer( - strategy, 2 * strategy.minCompToClaimOrSell(), {"from": comp_whale} + strategy, 2 * strategy.minRewardToClaimOrSell(), {"from": comp_whale} ) tx_swap = execute_yswap(