diff --git a/.gitignore b/.gitignore index 204aa80..d221ad1 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ reports/ # Node/npm node_modules/ .DS_Store +.env.local \ No newline at end of file diff --git a/contracts/Strategy0xDAOStaker.sol b/contracts/GenericMasterChefStrategy.sol similarity index 76% rename from contracts/Strategy0xDAOStaker.sol rename to contracts/GenericMasterChefStrategy.sol index 48e4d58..419b6ee 100644 --- a/contracts/Strategy0xDAOStaker.sol +++ b/contracts/GenericMasterChefStrategy.sol @@ -72,21 +72,22 @@ interface ChefLike { returns (uint256 amount, uint256 rewardDebt); } -contract Strategy0xDAOStaker is BaseStrategy { +contract GenericMasterChefStrategy is BaseStrategy { using SafeERC20 for IERC20; using Address for address; using SafeMath for uint256; /* ========== STATE VARIABLES ========== */ - ChefLike public constant masterchef = - ChefLike(0xa7821C3e9fC1bF961e280510c471031120716c3d); - IERC20 public constant emissionToken = - IERC20(0xc165d941481e68696f43EE6E99BFB2B23E0E3114); // the token we receive for staking, 0XD + ChefLike public masterchef; + IERC20 public emissionToken; + IERC20 public poolTwoSecondToken; // swap stuff address internal constant spookyRouter = 0xF491e7B69E4244ad4002BC14e878a34207E38c29; + address internal constant spiritRouter = + 0xF491e7B69E4244ad4002BC14e878a34207E38c29; ICurveFi internal constant mimPool = ICurveFi(0x2dd7C9371965472E5A5fD28fbE165007c61439E1); // Curve's MIM-USDC-USDT pool ICurveFi internal constant daiPool = @@ -106,6 +107,12 @@ contract Strategy0xDAOStaker is BaseStrategy { IERC20 internal constant mim = IERC20(0x82f0B8B456c1A451378467398982d4834b6829c1); + bool public autoSell; + uint256 public maxSell; //set to zero for unlimited + + bool public useSpiritPartOne; + bool public useSpiritPartTwo; + uint256 public pid; // the pool ID we are staking for string internal stratName; // we use this for our strategy's name on cloning @@ -119,9 +126,13 @@ contract Strategy0xDAOStaker is BaseStrategy { constructor( address _vault, uint256 _pid, - string memory _name + string memory _name, + address _masterchef, + address _emissionToken, + address _poolTwoSecondToken, + bool _autoSell ) public BaseStrategy(_vault) { - _initializeStrat(_pid, _name); + _initializeStrat(_pid, _name, _masterchef, _emissionToken, _poolTwoSecondToken, _autoSell); } /* ========== CLONING ========== */ @@ -135,7 +146,11 @@ contract Strategy0xDAOStaker is BaseStrategy { address _rewards, address _keeper, uint256 _pid, - string memory _name + string memory _name, + address _masterchef, + address _emissionToken, + address _poolTwoSecondToken, + bool _autoSell ) external returns (address newStrategy) { require(isOriginal); // Copied from https://github.com/optionality/clone-factory/blob/master/contracts/CloneFactory.sol @@ -155,13 +170,13 @@ contract Strategy0xDAOStaker is BaseStrategy { newStrategy := create(0, clone_code, 0x37) } - Strategy0xDAOStaker(newStrategy).initialize( + GenericMasterChefStrategy(newStrategy).initialize( _vault, _strategist, _rewards, _keeper, _pid, - _name + _name, _masterchef, _emissionToken, _poolTwoSecondToken, _autoSell ); emit Cloned(newStrategy); @@ -174,14 +189,22 @@ contract Strategy0xDAOStaker is BaseStrategy { address _rewards, address _keeper, uint256 _pid, - string memory _name + string memory _name, + address _masterchef, + address _emissionToken, + address _poolTwoSecondToken, + bool _autoSell ) public { _initialize(_vault, _strategist, _rewards, _keeper); - _initializeStrat(_pid, _name); + _initializeStrat(_pid, _name, _masterchef, _emissionToken, _poolTwoSecondToken, _autoSell); } // this is called by our original strategy, as well as any clones - function _initializeStrat(uint256 _pid, string memory _name) internal { + function _initializeStrat(uint256 _pid, string memory _name, address _masterchef, address _emissionToken, address _poolTwoSecondToken, bool _autoSell) internal { + + masterchef = ChefLike(_masterchef); + emissionToken = IERC20(_emissionToken); + poolTwoSecondToken = IERC20(_poolTwoSecondToken); // initialize variables maxReportDelay = 43200; // 1/2 day in seconds, if we hit this then harvestTrigger = True healthCheck = address(0xf13Cd6887C62B5beC145e30c38c4938c5E627fe0); // Fantom common health check @@ -189,6 +212,8 @@ contract Strategy0xDAOStaker is BaseStrategy { // set our strategy's name stratName = _name; + autoSell = _autoSell; + // make sure that we used the correct pid pid = _pid; (address poolToken, , , ) = masterchef.poolInfo(pid); @@ -198,11 +223,12 @@ contract Strategy0xDAOStaker is BaseStrategy { minHarvestCredit = type(uint256).max; // add approvals on all tokens - usdc.approve(spookyRouter, type(uint256).max); - usdc.approve(address(mimPool), type(uint256).max); - usdc.approve(address(daiPool), type(uint256).max); - want.approve(address(masterchef), type(uint256).max); + emissionToken.approve(spookyRouter, type(uint256).max); + poolTwoSecondToken.approve(spookyRouter, type(uint256).max); + emissionToken.approve(spiritRouter, type(uint256).max); + poolTwoSecondToken.approve(spiritRouter, type(uint256).max); + want.approve(address(masterchef), type(uint256).max); } /* ========== VIEWS ========== */ @@ -242,7 +268,7 @@ contract Strategy0xDAOStaker is BaseStrategy { // if we have emissionToken to sell, then sell some of it uint256 emissionTokenBalance = emissionToken.balanceOf(address(this)); - if (emissionTokenBalance > 0) { + if (emissionTokenBalance > 0 && autoSell) { // sell our emissionToken _sell(emissionTokenBalance); } @@ -264,7 +290,7 @@ contract Strategy0xDAOStaker is BaseStrategy { return (_profit, _loss, _debtPayment); } - if (assets > debt) { + if (assets >= debt) { _debtPayment = _debtOutstanding; _profit = assets - debt; @@ -348,10 +374,14 @@ contract Strategy0xDAOStaker is BaseStrategy { } // send our claimed emissionToken to the new strategy - emissionToken.safeTransfer( - _newStrategy, - emissionToken.balanceOf(address(this)) - ); + uint256 emis = emissionToken.balanceOf(address(this)); + if(emis > 0){ + emissionToken.safeTransfer( + _newStrategy, + emis + ); + } + } ///@notice Only do this if absolutely necessary; as assets will be withdrawn but rewards won't be claimed. @@ -359,14 +389,26 @@ contract Strategy0xDAOStaker is BaseStrategy { masterchef.emergencyWithdraw(pid); } + function manualSell(uint256 _amount) external onlyEmergencyAuthorized { + _sell(_amount); + } + // sell from reward token to want function _sell(uint256 _amount) internal { - // sell our emission token for usdc + + if(maxSell > 0){ + _amount = Math.min(maxSell, _amount); + } + + + // sell our emission token for pool two second token address[] memory emissionTokenPath = new address[](2); emissionTokenPath[0] = address(emissionToken); - emissionTokenPath[1] = address(usdc); + emissionTokenPath[1] = address(poolTwoSecondToken); - IUniswapV2Router02(spookyRouter).swapExactTokensForTokens( + address router = useSpiritPartOne? spiritRouter: spookyRouter; + + IUniswapV2Router02(router).swapExactTokensForTokens( _amount, uint256(0), emissionTokenPath, @@ -374,60 +416,40 @@ contract Strategy0xDAOStaker is BaseStrategy { block.timestamp ); - if (address(want) == address(usdc)) { + if (address(want) == address(poolTwoSecondToken)) { return; } - // sell our USDC for want - uint256 usdcBalance = usdc.balanceOf(address(this)); - if (address(want) == address(wftm)) { - // sell our usdc for want with spooky - address[] memory usdcSwapPath = new address[](2); - usdcSwapPath[0] = address(usdc); - usdcSwapPath[1] = address(want); + _amount = poolTwoSecondToken.balanceOf(address(this)); + if(address(poolTwoSecondToken) != address(wftm)){ + emissionTokenPath[0] = address(poolTwoSecondToken); + emissionTokenPath[1] = address(wftm); IUniswapV2Router02(spookyRouter).swapExactTokensForTokens( - usdcBalance, + _amount, uint256(0), - usdcSwapPath, + emissionTokenPath, address(this), block.timestamp ); - } else if (address(want) == address(weth)) { - // sell our usdc for want with spooky - address[] memory usdcSwapPath = new address[](3); - usdcSwapPath[0] = address(usdc); - usdcSwapPath[1] = address(wftm); - usdcSwapPath[2] = address(weth); + if (address(want) == address(wftm)) { + return; + } + _amount = wftm.balanceOf(address(this)); + } + - IUniswapV2Router02(spookyRouter).swapExactTokensForTokens( - usdcBalance, - uint256(0), - usdcSwapPath, - address(this), - block.timestamp - ); - } else if (address(want) == address(dai)) { - // sell our usdc for want with curve - daiPool.exchange(1, 0, usdcBalance, 0); - } else if (address(want) == address(mim)) { - // sell our usdc for want with curve - mimPool.exchange(2, 0, usdcBalance, 0); - } else if (address(want) == address(wbtc)) { - // sell our usdc for want with spooky - address[] memory usdcSwapPath = new address[](3); - usdcSwapPath[0] = address(usdc); - usdcSwapPath[1] = address(wftm); - usdcSwapPath[2] = address(wbtc); + router = useSpiritPartTwo? spiritRouter: spookyRouter; + emissionTokenPath[0] = address(wftm); + emissionTokenPath[1] = address(want); - IUniswapV2Router02(spookyRouter).swapExactTokensForTokens( - usdcBalance, - uint256(0), - usdcSwapPath, - address(this), - block.timestamp - ); - } + IUniswapV2Router02(router).swapExactTokensForTokens( + _amount, + uint256(0), + emissionTokenPath, + address(this), + block.timestamp + ); } function protectedTokens() @@ -489,4 +511,34 @@ contract Strategy0xDAOStaker is BaseStrategy { { minHarvestCredit = _minHarvestCredit; } + + ///@notice autosell if pools are liquid enough + function setAutoSell(bool _autoSell) + external + onlyEmergencyAuthorized + { + autoSell = _autoSell; + } + + ///@notice set a max sell for illiquid pools + function setMaxSell(uint256 _maxSell) + external + onlyEmergencyAuthorized + { + maxSell = _maxSell; + } + + function setUseSpiritOne(bool _useSpirit) + external + onlyEmergencyAuthorized + { + useSpiritPartOne = _useSpirit; + } + + function setUseSpiritTwo(bool _useSpirit) + external + onlyEmergencyAuthorized + { + useSpiritPartTwo = _useSpirit; + } } diff --git a/tests/conftest.py b/tests/conftest.py index 5cefcef..b7dca54 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,17 +7,17 @@ def isolation(fn_isolation): pass -# this is the pool ID that we are staking for. 1-6, wftm-mim +# this is the pool ID that we are staking for. 0-3, wftm-mim @pytest.fixture(scope="module") def pid(): - pid = 5 + pid = 2 yield pid # this is the name we want to give our strategy @pytest.fixture(scope="module") def strategy_name(): - strategy_name = "Strategy0xDAOStaker" + strategy_name = "Strategy" yield strategy_name @@ -30,6 +30,10 @@ def wftm(): def weth(): yield Contract("0x74b23882a30290451A17c44f4F05243b6b58C76d") +@pytest.fixture(scope="module") +def emissionToken(): + yield Contract("0x112dF7E3b4B7Ab424F07319D4E92F41e6608c48B") + @pytest.fixture(scope="module") def wbtc(): @@ -55,18 +59,12 @@ def mim(): @pytest.fixture(scope="module") def token(pid, wftm, weth, wbtc, dai, usdc, mim): # this should be the address of the ERC-20 used by the strategy/vault - if pid == 1: + if pid == 0: token = wftm - elif pid == 2: + elif pid == 1: token = weth - elif pid == 3: - token = wbtc - elif pid == 4: + elif pid == 2: token = usdc - elif pid == 5: - token = dai - elif pid == 6: - token = mim yield token @@ -86,14 +84,14 @@ def whale(accounts, pid): # 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(token, pid): # use today's exchange rates to have similar $$ amounts - if pid == 2: # WBTC + if pid == 4: # WBTC amount = 380 * (10 ** token.decimals()) elif pid == 1: # WETH - amount = 5000 * (10 ** token.decimals()) + amount = 50 * (10 ** token.decimals()) elif pid == 0: # WFTM - amount = 5769230 * (10 ** token.decimals()) + amount = 250_000 * (10 ** token.decimals()) else: # stables - amount = 15000000 * (10 ** token.decimals()) + amount = 1_000_000 * (10 ** token.decimals()) yield amount @@ -188,44 +186,28 @@ def vault(pm, gov, rewards, guardian, management, token, chain): # yield vault -# deploy the masterchef from 0xDAO's repo +# masterchef from ripae @pytest.fixture(scope="function") def masterchef( - MasterChef, - strategist, - keeper, - vault, - gov, - chain, - reward_token, - wftm, - weth, - wbtc, - dai, - usdc, - mim, - accounts, + Contract ): # make sure to include all constructor parameters needed here # transfer ownership of the token to our masterchef - masterchef = Contract("0xa7821C3e9fC1bF961e280510c471031120716c3d") - chain.sleep(1) - chain.mine(1) - - # sleep a day so we're into the farming time - chain.sleep(86400) + masterchef = Contract("0x42D5Ef67B686934325b100EF056d4bFe2f673f8C") yield masterchef # replace the first value with the name of your strategy @pytest.fixture(scope="function") def strategy( - Strategy0xDAOStaker, + GenericMasterChefStrategy, strategist, keeper, vault, gov, + emissionToken, + wftm, guardian, token, healthCheck, @@ -237,10 +219,14 @@ def strategy( ): # make sure to include all constructor parameters needed here strategy = strategist.deploy( - Strategy0xDAOStaker, + GenericMasterChefStrategy, vault, pid, strategy_name, + masterchef, + emissionToken, + wftm, + True ) strategy.setKeeper(keeper, {"from": gov}) # set our management fee to zero so it doesn't mess with our profit checking diff --git a/tests/test_base_strategy.py b/tests/test_base_strategy.py index fad3dd8..472d789 100644 --- a/tests/test_base_strategy.py +++ b/tests/test_base_strategy.py @@ -23,7 +23,7 @@ def test_base_strategy( newWhale = token.balanceOf(whale) to_view = [] - for i in range(6): + for i in range(3): addy = masterchef.poolInfo(i)[0] to_view.append(addy) print("These are our addresses", to_view) diff --git a/tests/test_cloning.py b/tests/test_cloning.py index 3ca4ead..ad9ebfb 100644 --- a/tests/test_cloning.py +++ b/tests/test_cloning.py @@ -12,9 +12,11 @@ def test_cloning( keeper, rewards, chain, - Strategy0xDAOStaker, + GenericMasterChefStrategy, guardian, amount, + emissionToken, + wftm, pid, masterchef, strategy_name, @@ -28,6 +30,11 @@ def test_cloning( keeper, pid, strategy_name, + masterchef, + emissionToken, + wftm, + True, + {"from": gov}, ) @@ -41,6 +48,10 @@ def test_cloning( keeper, wrong_pid, strategy_name, + masterchef, + emissionToken, + wftm, + True, {"from": gov}, ) @@ -52,9 +63,13 @@ def test_cloning( keeper, pid, strategy_name, + masterchef, + emissionToken, + wftm, + True, {"from": gov}, ) - newStrategy = Strategy0xDAOStaker.at(tx.return_value) + newStrategy = GenericMasterChefStrategy.at(tx.return_value) # Shouldn't be able to call initialize again with brownie.reverts(): @@ -65,6 +80,10 @@ def test_cloning( keeper, pid, strategy_name, + masterchef, + emissionToken, + wftm, + True, {"from": gov}, ) @@ -77,6 +96,10 @@ def test_cloning( keeper, pid, strategy_name, + masterchef, + emissionToken, + wftm, + True, {"from": gov}, ) diff --git a/tests/test_evil_or_dumb_protocol.py b/tests/test_evil_or_dumb_protocol.py index d485852..300f9d9 100644 --- a/tests/test_evil_or_dumb_protocol.py +++ b/tests/test_evil_or_dumb_protocol.py @@ -191,7 +191,7 @@ def test_protocol_dumb_masterchef_dev( chain.mine(1) # try and add a duplicate pool to bork the contract. since our strategist deployed it, he is the owner. - owner = Contract("0xa96D2F0978E317e7a97aDFf7b5A76F4600916021") + owner = Contract(masterchef.operator()) with brownie.reverts(): masterchef.add(69, token, {"from": owner}) with brownie.reverts(): diff --git a/tests/test_migration.py b/tests/test_migration.py index 3f65cb3..f57e784 100644 --- a/tests/test_migration.py +++ b/tests/test_migration.py @@ -5,7 +5,7 @@ def test_migration( - Strategy0xDAOStaker, + GenericMasterChefStrategy, gov, token, vault, @@ -19,6 +19,8 @@ def test_migration( amount, strategy_name, masterchef, + emissionToken, + wftm, pid, ): @@ -32,10 +34,14 @@ def test_migration( # deploy our new strategy new_strategy = strategist.deploy( - Strategy0xDAOStaker, + GenericMasterChefStrategy, vault, pid, strategy_name, + masterchef, + emissionToken, + wftm, + True ) total_old = strategy.estimatedTotalAssets() diff --git a/tests/test_odds_and_ends.py b/tests/test_odds_and_ends.py index fd7cba8..2adb29e 100644 --- a/tests/test_odds_and_ends.py +++ b/tests/test_odds_and_ends.py @@ -15,7 +15,9 @@ def test_odds_and_ends( strategist_ms, pid, masterchef, - Strategy0xDAOStaker, + emissionToken, + GenericMasterChefStrategy, + wftm, amount, strategy_name, ): @@ -52,10 +54,14 @@ def test_odds_and_ends( # we can try to migrate too, lol # deploy our new strategy new_strategy = strategist.deploy( - Strategy0xDAOStaker, + GenericMasterChefStrategy, vault, pid, strategy_name, + masterchef, + emissionToken, + wftm, + True ) total_old = strategy.estimatedTotalAssets() @@ -145,7 +151,7 @@ def test_odds_and_ends_2( def test_odds_and_ends_migration( - Strategy0xDAOStaker, + GenericMasterChefStrategy, gov, token, vault, @@ -158,6 +164,8 @@ def test_odds_and_ends_migration( amount, strategy_name, masterchef, + emissionToken, + wftm, pid, ): @@ -171,10 +179,14 @@ def test_odds_and_ends_migration( # deploy our new strategy new_strategy = strategist.deploy( - Strategy0xDAOStaker, + GenericMasterChefStrategy, vault, pid, strategy_name, + masterchef, + emissionToken, + wftm, + True ) total_old = strategy.estimatedTotalAssets() diff --git a/tests/test_simple_harvest.py b/tests/test_simple_harvest.py index 581eb6d..e3b11ff 100644 --- a/tests/test_simple_harvest.py +++ b/tests/test_simple_harvest.py @@ -41,7 +41,7 @@ def test_simple_harvest( chain.mine(1) # check on our pending rewards - pending = masterchef.pendingOXD(pid, strategy, {"from": whale}) + pending = masterchef.pendingReward(pid, strategy, {"from": whale}) print( "This is our pending reward after 12 hours: $" + str(pending / (10 ** reward_token.decimals()))