diff --git a/contracts/RouterStrategy.sol b/contracts/RouterStrategy.sol index bcbe7ce..7b7b214 100644 --- a/contracts/RouterStrategy.sol +++ b/contracts/RouterStrategy.sol @@ -21,6 +21,14 @@ interface IVault is IERC20 { function pricePerShare() external view returns (uint256); + function totalAssets() external view returns (uint256); + + function lockedProfit() external view returns (uint256); + + function lockedProfitDegradation() external view returns (uint256); + + function lastReport() external view returns (uint256); + function withdraw( uint256 amount, address account, @@ -205,6 +213,10 @@ contract RouterStrategy is BaseStrategy { } } + function withdrawFromYVault(uint256 _amount) external onlyVaultManagers { + _withdrawFromYVault(_amount); + } + function _withdrawFromYVault(uint256 _amount) internal { if (_amount == 0) { return; @@ -286,13 +298,52 @@ contract RouterStrategy is BaseStrategy { view returns (uint256) { - return amount.mul(10**yVault.decimals()).div(yVault.pricePerShare()); + return _valueInYVaultShares(amount); } function valueOfInvestment() public view virtual returns (uint256) { - return - yVault.balanceOf(address(this)).mul(yVault.pricePerShare()).div( - 10**yVault.decimals() + return _yVaultSharesValue(yVault.balanceOf(address(this))); + } + + function _valueInYVaultShares(uint256 value) + internal + view + returns (uint256) + { + uint256 totalSupply = yVault.totalSupply(); + uint256 freeFunds = _calculateYVaultFreeFunds(); + if (freeFunds == 0) return 0; + return (value.mul(totalSupply).div(freeFunds)); + } + + function _yVaultSharesValue(uint256 shares) + internal + view + returns (uint256) + { + uint256 totalSupply = yVault.totalSupply(); + if (totalSupply == 0) return shares; + + uint256 freeFunds = _calculateYVaultFreeFunds(); + return (shares.mul(freeFunds).div(totalSupply)); + } + + function _calculateYVaultFreeFunds() + private + view + returns (uint256 freeFunds) + { + freeFunds = yVault.totalAssets(); + uint256 lockedFundsRatio = + (block.timestamp.sub(yVault.lastReport())).mul( + yVault.lockedProfitDegradation() ); + + if (lockedFundsRatio < 10**18) { + uint256 lockedProfit = yVault.lockedProfit(); + freeFunds = freeFunds.sub( + lockedProfit.sub(lockedFundsRatio.mul(lockedProfit).div(10**18)) + ); + } } } diff --git a/tests/SynthetixRouter/test_sbtc_router_deploy.py b/tests/SynthetixRouter/test_sbtc_router_deploy.py deleted file mode 100644 index 775f693..0000000 --- a/tests/SynthetixRouter/test_sbtc_router_deploy.py +++ /dev/null @@ -1,203 +0,0 @@ -import pytest -from brownie import chain, Contract, accounts, ZERO_ADDRESS, Wei -from eth_abi import encode_single - -DUST_THRESHOLD = 10_000 - - -def test_sbtc_router_deploy_with_profit( - SynthetixRouterStrategy, strategist, sbtc, sbtc_whale -): - - hedging_vault = Contract("0xcE0F1Ef5aAAB82547acc699d3Ab93c069bb6e547") - sbtc_vault = Contract("0x8472E9914C0813C4b465927f82E213EA34839173") - gov = accounts.at(hedging_vault.governance(), True) - - strategy = strategist.deploy( - SynthetixRouterStrategy, - hedging_vault, - sbtc_vault, - "RoutersUSDtosBTC", - encode_single("bytes32", b"ProxysBTC"), - 100, - ) - - susd_router = Contract(hedging_vault.withdrawalQueue(0)) - hedging_vault.updateStrategyDebtRatio(susd_router, 8_000, {"from": gov}) - - susd_router.setMaxLoss(10_000, {"from": gov}) - susd_router.harvest({"from": gov}) - hedging_vault.addStrategy(strategy, 2_000, 0, 2 ** 256 - 1, 0, {"from": gov}) - - chain.sleep(360 + 1) - chain.mine(1) - - tx = strategy.harvest({"from": gov}) - print(f"First harvest {tx.events['Harvested']}") - assert tx.events["Harvested"]["loss"] == 0 - - # There needs to be a buffer - assert strategy.balanceOfWant() > 0 - - # Even though we can't move the funds, sbtc should be already in the strategy - assert sbtc.balanceOf(strategy) > 0 - - # Buffer should be 1% of debt aprox - total_debt = hedging_vault.strategies(strategy).dict()["totalDebt"] / 1e18 - assert total_debt * 0.009 < strategy.balanceOfWant() / 1e18 - assert total_debt * 0.02 > strategy.balanceOfWant() / 1e18 - balance_of_want_before = strategy.balanceOfWant() - # deposit sbtc into new strategy - chain.sleep(360 + 1) - chain.mine(1) - - strategy.depositInVault({"from": gov}) - - assert strategy.valueOfInvestment() > 0 - assert strategy.balanceOfWant() == balance_of_want_before - assert sbtc.balanceOf(strategy) == 0 - - sbtc.transfer( - sbtc_vault, int(sbtc_vault.totalAssets() * 0.05), {"from": sbtc_whale} - ) - capital_to_withdraw = ( - strategy.estimatedTotalAssets() - - hedging_vault.strategies(strategy).dict()["totalDebt"] - ) - - # Should this touch the buffer or not? - strategy.manualRemoveLiquidity(capital_to_withdraw, {"from": gov}) - - # Check that we withdrew enough sUSD - assert abs(strategy.balanceOfWant() - capital_to_withdraw) < Wei("1 ether") - assert sbtc.balanceOf(strategy) == 0 - - chain.sleep(360 + 1) - chain.mine(1) - tx = strategy.harvest({"from": gov}) - print(f"Second harvest {tx.events['Harvested']}") - - assert tx.events["Harvested"]["loss"] == 0 - assert hedging_vault.strategies(strategy).dict()["totalLoss"] == 0 - - hedging_vault.revokeStrategy(strategy, {"from": gov}) - chain.sleep(360 + 1) - chain.mine(1) - - strategy.manualRemoveFullLiquidity({"from": gov}) - - assert strategy.balanceOfWant() > 0 - assert sbtc.balanceOf(strategy) < DUST_THRESHOLD - - chain.sleep(360 + 1) - chain.mine(1) - tx = strategy.harvest({"from": gov}) - print(f"Third harvest {tx.events['Harvested']}") - assert tx.events["Harvested"]["profit"] == 0 - assert tx.events["Harvested"]["loss"] > 0 # due to fees - - chain.sleep(360 + 1) - chain.mine(1) - - assert hedging_vault.strategies(strategy).dict()["totalDebt"] == 0 - - -def test_sbtc_router_deploy_with_loss( - SynthetixRouterStrategy, strategist, sbtc, sbtc_whale, susd, susd_whale -): - - hedging_vault = Contract("0xcE0F1Ef5aAAB82547acc699d3Ab93c069bb6e547") - sbtc_vault = Contract("0x8472E9914C0813C4b465927f82E213EA34839173") - gov = accounts.at(hedging_vault.governance(), True) - - strategy = strategist.deploy( - SynthetixRouterStrategy, - hedging_vault, - sbtc_vault, - "RoutersUSDtosBTC", - encode_single("bytes32", b"ProxysBTC"), - 100, - ) - - susd.approve(hedging_vault, 2 ** 256 - 1, {"from": susd_whale}) - susd_router = Contract(hedging_vault.withdrawalQueue(0)) - hedging_vault.updateStrategyDebtRatio(susd_router, 8_000, {"from": gov}) - - susd_router.setMaxLoss(10_000, {"from": gov}) - tx = susd_router.harvest({"from": gov}) - print(f"sUSDRouter harvest {tx.events['Harvested']}") - hedging_vault.addStrategy(strategy, 2_000, 0, 2 ** 256 - 1, 0, {"from": gov}) - chain.sleep(360 + 1) - chain.mine(1) - tx = strategy.harvest({"from": gov}) - print(f"First harvest {tx.events['Harvested']}") - assert tx.events["Harvested"]["loss"] == 0 - - # There needs to be a buffer - assert strategy.balanceOfWant() > 0 - - # Even though we can't move the funds, sbtc should be already in the strategy - assert sbtc.balanceOf(strategy) > 0 - - # Buffer should be 1% of debt aprox - total_debt = hedging_vault.strategies(strategy).dict()["totalDebt"] / 1e18 - assert total_debt * 0.009 < strategy.balanceOfWant() / 1e18 - assert total_debt * 0.02 > strategy.balanceOfWant() / 1e18 - balance_of_want_before = strategy.balanceOfWant() - # deposit sbtc into new strategy - chain.sleep(360 + 1) - chain.mine(1) - strategy.depositInVault({"from": gov}) - - assert strategy.valueOfInvestment() > 0 - assert strategy.balanceOfWant() == balance_of_want_before - assert sbtc.balanceOf(strategy) == 0 - - # Should this touch the buffer or not? - strategy.manualRemoveLiquidity(strategy.estimatedTotalAssets(), {"from": gov}) - - # Check that we withdrew enough sUSD - assert sbtc.balanceOf(strategy) == 0 - - chain.sleep(360 + 1) - chain.mine(1) - tx = strategy.harvest({"from": gov}) - print(f"Second harvest {tx.events['Harvested']}") - - # loss due to the exchange fees - assert ( - tx.events["Harvested"]["loss"] - > strategy.estimatedTotalAssets() * 0.25 / 100 * 2 - ) - assert ( - tx.events["Harvested"]["loss"] - <= strategy.estimatedTotalAssets() * 0.32 / 100 * 2 - ) - assert ( - hedging_vault.strategies(strategy).dict()["totalLoss"] - > strategy.estimatedTotalAssets() * 0.25 / 100 * 2 - ) - assert ( - hedging_vault.strategies(strategy).dict()["totalLoss"] - <= strategy.estimatedTotalAssets() * 0.32 / 100 * 2 - ) - - hedging_vault.revokeStrategy(strategy, {"from": gov}) - chain.sleep(360 + 1) - chain.mine(1) - - strategy.manualRemoveFullLiquidity({"from": gov}) - - assert strategy.balanceOfWant() > 0 - assert sbtc.balanceOf(strategy) < DUST_THRESHOLD - - chain.sleep(360 + 1) - chain.mine(1) - tx = strategy.harvest({"from": gov}) - print(f"Third harvest {tx.events['Harvested']}") - assert tx.events["Harvested"]["loss"] > 0 - - chain.sleep(360 + 1) - chain.mine(1) - - assert hedging_vault.strategies(strategy).dict()["totalDebt"] < Wei("1 ether") diff --git a/tests/conftest.py b/tests/conftest.py index 5fca23c..e037c75 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -79,7 +79,7 @@ def destination_vault(): @pytest.fixture def weth_whale(accounts): - yield accounts.at("0xc1aae9d18bbe386b102435a8632c8063d31e747c", True) + yield accounts.at("0x2feb1512183545f48f6b9c5b4ebfcaf49cfca6f3", True) @pytest.fixture @@ -89,11 +89,11 @@ def token(): @pytest.fixture -def amount(accounts, token, user): - amount = 10_000 * 10 ** token.decimals() +def amount(accounts, token, user, weth_whale): + amount = 1000 * 10 ** token.decimals() # In order to get some funds for the token you are about to use, # it impersonate an exchange address to use it's funds. - reserve = accounts.at("0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643", force=True) + reserve = accounts.at(weth_whale, force=True) token.transfer(user, amount, {"from": reserve}) yield amount @@ -146,7 +146,9 @@ def strategy( if ZERO_ADDRESS == strat_address: break - origin_vault.updateStrategyDebtRatio(strat_address, 0, {"from": gov}) + if origin_vault.strategies(strat_address)['debtRatio'] > 0: + origin_vault.updateStrategyDebtRatio(strat_address, 0, {"from": gov}) + Contract(strat_address).harvest({"from":gov}) strategy.setHealthCheck(health_check, {"from": origin_vault.governance()}) origin_vault.addStrategy(strategy, 10_000, 0, 2 ** 256 - 1, 0, {"from": gov}) @@ -169,7 +171,9 @@ def unique_strategy( if ZERO_ADDRESS == strat_address: break - yvweth_032.updateStrategyDebtRatio(strat_address, 0, {"from": gov}) + if yvweth_032.strategies(strat_address)['debtRatio'] > 0: + yvweth_032.updateStrategyDebtRatio(strat_address, 0, {"from": gov}) + Contract(strat_address).harvest({"from":gov}) yvweth_032.setPerformanceFee(0, {"from": gov}) yvweth_032.setManagementFee(0, {"from": gov}) diff --git a/tests/test_clone.py b/tests/test_clone.py index 89dd15d..e11858b 100644 --- a/tests/test_clone.py +++ b/tests/test_clone.py @@ -2,7 +2,7 @@ from brownie import chain, Wei, reverts, Contract, ZERO_ADDRESS -def move_funds(vault, dest_vault, strategy, gov, weth, weth_whale): +def move_funds(vault, dest_vault, strategy, gov, weth, weth_whale, RELATIVE_APPROX): print(strategy.name()) strategy.harvest({"from": gov}) @@ -27,9 +27,9 @@ def move_funds(vault, dest_vault, strategy, gov, weth, weth_whale): chain.sleep(3600 * 8) chain.mine(1) - assert vault.strategies(strategy).dict()["totalGain"] == total_gain - assert vault.strategies(strategy).dict()["totalLoss"] == 0 - assert vault.strategies(strategy).dict()["totalDebt"] == 0 + assert pytest.approx(vault.strategies(strategy).dict()["totalGain"], rel=RELATIVE_APPROX) == total_gain + assert pytest.approx(vault.strategies(strategy).dict()["totalLoss"], rel=RELATIVE_APPROX) == 0 + assert pytest.approx(vault.strategies(strategy).dict()["totalDebt"], rel=RELATIVE_APPROX) == 0 def test_original_strategy( @@ -42,9 +42,10 @@ def test_original_strategy( gov, token, weth_whale, + RELATIVE_APPROX ): - move_funds(origin_vault, destination_vault, strategy, gov, token, weth_whale) + move_funds(origin_vault, destination_vault, strategy, gov, token, weth_whale, RELATIVE_APPROX) def test_cloned_strategy( @@ -57,6 +58,7 @@ def test_cloned_strategy( gov, token, weth_whale, + RELATIVE_APPROX ): clone_tx = strategy.cloneRouter( @@ -80,6 +82,7 @@ def test_cloned_strategy( gov, token, weth_whale, + RELATIVE_APPROX ) diff --git a/tests/test_move_funds_to_042.py b/tests/test_move_funds_to_042.py index 1aa2cc2..fa3d5ef 100644 --- a/tests/test_move_funds_to_042.py +++ b/tests/test_move_funds_to_042.py @@ -3,7 +3,7 @@ def test_move_funds_to_042( - yvweth_032, yvweth_042, unique_strategy, gov, weth, weth_whale + yvweth_032, yvweth_042, unique_strategy, gov, weth, weth_whale, RELATIVE_APPROX ): strategy = unique_strategy @@ -32,6 +32,6 @@ def test_move_funds_to_042( chain.sleep(3600 * 8) chain.mine(1) - assert yvweth_032.strategies(strategy).dict()["totalGain"] == total_gain - assert yvweth_032.strategies(strategy).dict()["totalLoss"] == 0 - assert yvweth_032.strategies(strategy).dict()["totalDebt"] == 0 + assert pytest.approx(yvweth_032.strategies(strategy).dict()["totalGain"], rel=RELATIVE_APPROX) == total_gain + assert pytest.approx(yvweth_032.strategies(strategy).dict()["totalLoss"], rel=RELATIVE_APPROX) == 0 + assert pytest.approx(yvweth_032.strategies(strategy).dict()["totalDebt"], rel=RELATIVE_APPROX) == 0 diff --git a/tests/test_operation.py b/tests/test_operation.py new file mode 100644 index 0000000..4779892 --- /dev/null +++ b/tests/test_operation.py @@ -0,0 +1,16 @@ +import pytest +from brownie import Contract, ZERO_ADDRESS, Wei, chain + + +def test_yvault_shares_conversion(unique_strategy, gov, weth, weth_whale, RELATIVE_APPROX): + + strategy = unique_strategy + strategy.harvest({"from": gov}) + assert strategy.balanceOfWant() == 0 + assert strategy.valueOfInvestment() > 0 + + strategy.setMaxLoss(0, {"from": gov}) + strategy.withdrawFromYVault(10 * 1e18, {"from": gov}) + + assert strategy.balanceOfWant() == 10 * (10 ** 18) + assert pytest.approx(strategy.balanceOfWant(), rel=RELATIVE_APPROX) == 10 * (10 ** 18)