diff --git a/brownie-config.yml b/brownie-config.yml index 3f06db3..03e3ea9 100644 --- a/brownie-config.yml +++ b/brownie-config.yml @@ -4,7 +4,7 @@ networks: default: mainnet-fork # automatically fetch contract sources from Etherscan -autofetch_sources: False +autofetch_sources: True # require OpenZepplin Contracts dependencies: diff --git a/contracts/Joint.sol b/contracts/Joint.sol index ca3e885..33300fb 100644 --- a/contracts/Joint.sol +++ b/contracts/Joint.sol @@ -29,6 +29,12 @@ interface ProviderStrategy { function want() external view returns (address); function totalDebt() external view returns (uint256); + + function borrowedToken() external view returns (address); + + function updatedBalanceOfDebt() external returns (uint256); + + function balanceOfDebt() external view returns (uint256); } abstract contract Joint { @@ -124,17 +130,18 @@ abstract contract Joint { address _reward ) internal virtual { require(address(providerA) == address(0), "Joint already initialized"); + require(_providerA == _providerB, "!lev-providers");// should be the same for this version providerA = ProviderStrategy(_providerA); - providerB = ProviderStrategy(_providerB); + providerB = ProviderStrategy(_providerB); router = _router; WETH = _weth; reward = _reward; // NOTE: we let some loss to avoid getting locked in the position if something goes slightly wrong - maxPercentageLoss = 500; // 0.1% + maxPercentageLoss = 500; // 0.5% tokenA = address(providerA.want()); - tokenB = address(providerB.want()); + tokenB = address(providerB.borrowedToken()); require(tokenA != tokenB, "!same-want"); pair = IUniswapV2Pair(getPair()); @@ -227,12 +234,29 @@ abstract contract Joint { ); } + // Poorman's rebalance + // if we ended up with more tokenB than required, we sell it for tokenA + // if we ended up with less tokenB than required, we buy it with tokenA + // TODO: improve gas efficiency by merging with previous block !! + currentBalanceB = IERC20(tokenB).balanceOf(address(this)); + uint256 requiredBalanceB = providerA.updatedBalanceOfDebt(); + if(requiredBalanceB > currentBalanceB) { + uint256[] memory inAmounts = + IUniswapV2Router02(router).getAmountsIn( + requiredBalanceB.sub(currentBalanceB), + getTokenOutPath(tokenA, tokenB) + ); + sellCapital(tokenA, tokenB, inAmounts[0]); + } else if (currentBalanceB > requiredBalanceB){ + sellCapital(tokenB, tokenA, currentBalanceB.sub(requiredBalanceB)); + } + // reset invested balances investedA = investedB = 0; _returnLooseToProviders(); + // Check that we have returned with no losses - // require( IERC20(tokenA).balanceOf(address(providerA)) >= providerA @@ -242,11 +266,8 @@ abstract contract Joint { "!wrong-balanceA" ); require( - IERC20(tokenB).balanceOf(address(providerB)) >= - providerB - .totalDebt() - .mul(RATIO_PRECISION.sub(maxPercentageLoss)) - .div(RATIO_PRECISION), + IERC20(tokenB).balanceOf(address(providerA)) >= + providerA.balanceOfDebt(), "!wrong-balanceB" ); } diff --git a/contracts/ProviderStrategy.sol b/contracts/ProviderStrategy.sol index 8da72fe..079a52e 100644 --- a/contracts/ProviderStrategy.sol +++ b/contracts/ProviderStrategy.sol @@ -15,6 +15,16 @@ import { BaseStrategyInitializable } from "@yearnvaults/contracts/BaseStrategy.sol"; +import "../interfaces/ironbank/CErc20Interface.sol"; +import "../interfaces/ironbank/ComptrollerInterface.sol"; + +interface IPriceOracle { + function getUnderlyingPrice(CErc20Interface ibToken) + external + view + returns (uint256); +} + interface JointAPI { function closePositionReturnFunds() external; @@ -40,16 +50,32 @@ interface JointAPI { function dontInvestWant() external view returns (bool); } -contract ProviderStrategy is BaseStrategyInitializable { +interface IPriceProvider { + function latestAnswer() external view returns (uint); +} + +contract LevProviderStrategy is BaseStrategyInitializable { using SafeERC20 for IERC20; using Address for address; using SafeMath for uint256; + CErc20Interface public ibToken; + ComptrollerInterface public comptrollerIB; + + IPriceProvider public priceProvider = IPriceProvider(0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419); + address public joint; bool public forceLiquidate; - constructor(address _vault) public BaseStrategyInitializable(_vault) {} + uint256 internal constant BLOCKS_PER_YEAR = 2_102_400; + + constructor(address _vault, CErc20Interface _ibToken, ComptrollerInterface _comptrollerIB) public BaseStrategyInitializable(_vault) { + comptrollerIB = _comptrollerIB; + ibToken = _ibToken; + IERC20 _borrowedToken = IERC20(_ibToken.underlying()); + _borrowedToken.safeApprove(address(_ibToken), type(uint256).max); + } function name() external view override returns (string memory) { return @@ -87,6 +113,11 @@ contract ProviderStrategy is BaseStrategyInitializable { // The Provider will always ask the joint to close the position before harvesting JointAPI(joint).closePositionReturnFunds(); + // Repay debt + if(balanceOfDebt() > 0) { + repayFullDebt(); + } + // After closePosition, the provider will always have funds in its own balance (not in joint) uint256 _totalDebt = totalDebt(); uint256 totalAssets = balanceOfWant(); @@ -144,12 +175,20 @@ contract ProviderStrategy is BaseStrategyInitializable { return; } + // Take debt: this function will borrow an equivalent amount to the amount of want + borrowRequiredAmountTokenB(); + uint256 bTokenBalance = balanceOfBorrowedToken(); + if(bTokenBalance > 0) { + IERC20(borrowedToken()).transfer(joint, bTokenBalance); + } + // Using a push approach (instead of pull) uint256 wantBalance = balanceOfWant(); if (wantBalance > 0) { want.transfer(joint, wantBalance); } JointAPI(joint).openPosition(); + } function liquidatePosition(uint256 _amountNeeded) @@ -167,6 +206,7 @@ contract ProviderStrategy is BaseStrategyInitializable { } function prepareMigration(address _newStrategy) internal override { + // TODO: return debt! (handle levered balance before migrating) JointAPI(joint).migrateProvider(_newStrategy); } @@ -181,6 +221,8 @@ contract ProviderStrategy is BaseStrategyInitializable { return IERC20(want).balanceOf(address(this)); } + + function setJoint(address _joint) external onlyGovernance { require( JointAPI(_joint).providerA() == address(this) || @@ -205,6 +247,7 @@ contract ProviderStrategy is BaseStrategyInitializable { { uint256 expectedBalance = estimatedTotalAssets(); JointAPI(joint).closePositionReturnFunds(); + _amountFreed = balanceOfWant(); // NOTE: we accept a 1% difference before reverting require( @@ -261,4 +304,91 @@ contract ProviderStrategy is BaseStrategyInitializable { _path[2] = _token_out; } } + + function borrowedToken() public view returns(address) { + return ibToken.underlying(); + } + + function balanceOfBorrowedToken() public view returns (uint) { + return IERC20(borrowedToken()).balanceOf(address(this)); + } + + function updatedBalanceOfDebt() public returns (uint256) { + return ibToken.borrowBalanceCurrent(address(this)); + } + + function balanceOfDebt() public view returns (uint256) { + return ibToken.borrowBalanceStored(address(this)); + } + + function repayFullDebt() internal { + repayBorrow(balanceOfDebt()); + } + + + function borrowRequiredAmountTokenB() internal { + // TODO: make this generic + uint256 amountBToBorrow = balanceOfWant().mul(priceProvider.latestAnswer()).mul(uint(10)**IERC20Extended(borrowedToken()).decimals()).div(1e26); + borrow(amountBToBorrow); + } + + function borrow(uint256 amount) internal returns (uint256) { + uint256 currentBorrow = updatedBalanceOfDebt(); + uint256 creditLimit = + getCreditLimitInBorrowedToken(address(this)); + uint256 availableLimit = creditLimit > currentBorrow ? creditLimit - currentBorrow : 0; + uint256 maxBorrow = Math.min(ibToken.getCash(), availableLimit); + uint256 borrowAmount = Math.min(amount, maxBorrow); + require(ibToken.borrow(borrowAmount) == 0); + return borrowAmount; + } + + function repayBorrow(uint256 amount) internal returns (uint256) { + uint256 maxRepay = Math.min(balanceOfBorrowedToken(), balanceOfDebt()); + uint256 repayAmount = Math.min(amount, maxRepay); + require(ibToken.repayBorrow(repayAmount) == 0); + return repayAmount; + } + + function currentBorrowingCosts() public view returns (uint256) { + return ironBankBorrowRateAfterChange(0, false); + } + + function ironBankBorrowRateAfterChange(uint256 amount, bool repay) + public + view + returns (uint256 annualBorrowingCost) + { + uint256 borrowRatePerBlock = + ibToken.estimateBorrowRatePerBlockAfterChange(amount, repay); + + // calculate estimated annual costs + annualBorrowingCost = borrowRatePerBlock * BLOCKS_PER_YEAR; + } + + function getCreditLimitInBorrowedToken(address account) + public + view + returns (uint256) + { + // returns USD value in mantissa (1e18) + uint256 usdCreditLimit = comptrollerIB.creditLimits(account); + // if credit limit is infinite, we don't need to check anything else (otherwise, we will get an overflow) + if (usdCreditLimit == type(uint256).max) { + return type(uint256).max; + } + uint256 priceUSD = getBorrowedTokenPriceUSD(); + // we need to adjust price AND decimals + // Using simplified version of: + // uint256 wantCreditLimit = usdCreditLimit * 1e18 * (10 ** want.decimals()) / priceUSD / 1e18; + uint256 wantCreditLimit = + (usdCreditLimit * (uint256(10) ** IERC20Extended(borrowedToken()).decimals())) / priceUSD; + return wantCreditLimit; + } + + function getBorrowedTokenPriceUSD() public view returns (uint256) { + return IPriceOracle(comptrollerIB.oracle()).getUnderlyingPrice(ibToken); + } + + } diff --git a/interfaces/ironbank/CErc20Interface.sol b/interfaces/ironbank/CErc20Interface.sol new file mode 100644 index 0000000..c1810e4 --- /dev/null +++ b/interfaces/ironbank/CErc20Interface.sol @@ -0,0 +1,252 @@ +pragma solidity 0.6.12; + +import "./ComptrollerInterface.sol"; +import "./InterestRateModel.sol"; + +interface CTokenInterface { + /*** Market Events ***/ + + /** + * @notice Event emitted when interest is accrued + */ + event AccrueInterest( + uint256 cashPrior, + uint256 interestAccumulated, + uint256 borrowIndex, + uint256 totalBorrows + ); + + /** + * @notice Event emitted when tokens are minted + */ + event Mint(address minter, uint256 mintAmount, uint256 mintTokens); + + /** + * @notice Event emitted when tokens are redeemed + */ + event Redeem(address redeemer, uint256 redeemAmount, uint256 redeemTokens); + + /** + * @notice Event emitted when underlying is borrowed + */ + event Borrow( + address borrower, + uint256 borrowAmount, + uint256 accountBorrows, + uint256 totalBorrows + ); + + /** + * @notice Event emitted when a borrow is repaid + */ + event RepayBorrow( + address payer, + address borrower, + uint256 repayAmount, + uint256 accountBorrows, + uint256 totalBorrows + ); + + /** + * @notice Event emitted when a borrow is liquidated + */ + event LiquidateBorrow( + address liquidator, + address borrower, + uint256 repayAmount, + address cTokenCollateral, + uint256 seizeTokens + ); + + /*** Admin Events ***/ + + /** + * @notice Event emitted when pendingAdmin is changed + */ + event NewPendingAdmin(address oldPendingAdmin, address newPendingAdmin); + + /** + * @notice Event emitted when pendingAdmin is accepted, which means admin is updated + */ + event NewAdmin(address oldAdmin, address newAdmin); + + /** + * @notice Event emitted when comptroller is changed + */ + event NewComptroller( + ComptrollerInterface oldComptroller, + ComptrollerInterface newComptroller + ); + + /** + * @notice Event emitted when the reserve factor is changed + */ + event NewReserveFactor( + uint256 oldReserveFactorMantissa, + uint256 newReserveFactorMantissa + ); + + /** + * @notice Event emitted when the reserves are added + */ + event ReservesAdded( + address benefactor, + uint256 addAmount, + uint256 newTotalReserves + ); + + /** + * @notice Event emitted when the reserves are reduced + */ + event ReservesReduced( + address admin, + uint256 reduceAmount, + uint256 newTotalReserves + ); + + /** + * @notice EIP20 Transfer event + */ + event Transfer(address indexed from, address indexed to, uint256 amount); + + /** + * @notice EIP20 Approval event + */ + event Approval( + address indexed owner, + address indexed spender, + uint256 amount + ); + + /** + * @notice Failure event + */ + event Failure(uint256 error, uint256 info, uint256 detail); + + /*** User Interface ***/ + + function transfer(address dst, uint256 amount) external returns (bool); + + function transferFrom( + address src, + address dst, + uint256 amount + ) external returns (bool); + + function approve(address spender, uint256 amount) external returns (bool); + + function allowance(address owner, address spender) + external + view + returns (uint256); + + function balanceOf(address owner) external view returns (uint256); + + function balanceOfUnderlying(address owner) external returns (uint256); + + function getAccountSnapshot(address account) + external + view + returns ( + uint256, + uint256, + uint256, + uint256 + ); + + function borrowRatePerBlock() external view returns (uint256); + + function supplyRatePerBlock() external view returns (uint256); + + function totalBorrowsCurrent() external returns (uint256); + + function borrowBalanceCurrent(address account) external returns (uint256); + + function borrowBalanceStored(address account) + external + view + returns (uint256); + + function exchangeRateCurrent() external returns (uint256); + + function exchangeRateStored() external view returns (uint256); + + function getCash() external view returns (uint256); + + function totalReserves() external view returns (uint256); + + function accrueInterest() external returns (uint256); + + function seize( + address liquidator, + address borrower, + uint256 seizeTokens + ) external returns (uint256); + + function totalSupply() external view returns (uint256); + + function totalBorrows() external view returns (uint256); + + function interestRateModel() external view returns (InterestRateModel); + + function reserveFactorMantissa() external view returns (uint256); + + /*** Admin Functions ***/ + + function _setPendingAdmin(address payable newPendingAdmin) + external + returns (uint256); + + function _acceptAdmin() external returns (uint256); + + function _setComptroller(ComptrollerInterface newComptroller) + external + returns (uint256); + + function _setReserveFactor(uint256 newReserveFactorMantissa) + external + returns (uint256); + + function _reduceReserves(uint256 reduceAmount) external returns (uint256); +} + +interface IronBankCTokenI is CTokenInterface { + function estimateBorrowRatePerBlockAfterChange(uint256 change, bool repay) + external + view + returns (uint256); + + function estimateSupplyRatePerBlockAfterChange(uint256 change, bool repay) + external + view + returns (uint256); +} + +interface CErc20Interface is IronBankCTokenI { + /*** User Interface ***/ + function underlying() external view returns (address); + + function mint(uint256 mintAmount) external returns (uint256); + + function redeem(uint256 redeemTokens) external returns (uint256); + + function redeemUnderlying(uint256 redeemAmount) external returns (uint256); + + function borrow(uint256 borrowAmount) external returns (uint256); + + function repayBorrow(uint256 repayAmount) external returns (uint256); + + function repayBorrowBehalf(address borrower, uint256 repayAmount) + external + returns (uint256); + + function liquidateBorrow( + address borrower, + uint256 repayAmount, + CTokenInterface cTokenCollateral + ) external returns (uint256); + + /*** Admin Functions ***/ + + function _addReserves(uint256 addAmount) external returns (uint256); +} diff --git a/interfaces/ironbank/ComptrollerInterface.sol b/interfaces/ironbank/ComptrollerInterface.sol new file mode 100644 index 0000000..4c3d933 --- /dev/null +++ b/interfaces/ironbank/ComptrollerInterface.sol @@ -0,0 +1,148 @@ +pragma solidity 0.6.12; +import "./CErc20Interface.sol"; + +interface ComptrollerInterface { + /*** Assets You Are In ***/ + + function enterMarkets(address[] calldata cTokens) + external + returns (uint256[] memory); + + function exitMarket(address cToken) external returns (uint256); + + /*** Policy Hooks ***/ + + function mintAllowed( + address cToken, + address minter, + uint256 mintAmount + ) external returns (uint256); + + function mintVerify( + address cToken, + address minter, + uint256 mintAmount, + uint256 mintTokens + ) external; + + function redeemAllowed( + address cToken, + address redeemer, + uint256 redeemTokens + ) external returns (uint256); + + function redeemVerify( + address cToken, + address redeemer, + uint256 redeemAmount, + uint256 redeemTokens + ) external; + + function borrowAllowed( + address cToken, + address borrower, + uint256 borrowAmount + ) external returns (uint256); + + function borrowVerify( + address cToken, + address borrower, + uint256 borrowAmount + ) external; + + function repayBorrowAllowed( + address cToken, + address payer, + address borrower, + uint256 repayAmount + ) external returns (uint256); + + function repayBorrowVerify( + address cToken, + address payer, + address borrower, + uint256 repayAmount, + uint256 borrowerIndex + ) external; + + function liquidateBorrowAllowed( + address cTokenBorrowed, + address cTokenCollateral, + address liquidator, + address borrower, + uint256 repayAmount + ) external returns (uint256); + + function liquidateBorrowVerify( + address cTokenBorrowed, + address cTokenCollateral, + address liquidator, + address borrower, + uint256 repayAmount, + uint256 seizeTokens + ) external; + + function seizeAllowed( + address cTokenCollateral, + address cTokenBorrowed, + address liquidator, + address borrower, + uint256 seizeTokens + ) external returns (uint256); + + function seizeVerify( + address cTokenCollateral, + address cTokenBorrowed, + address liquidator, + address borrower, + uint256 seizeTokens + ) external; + + function transferAllowed( + address cToken, + address src, + address dst, + uint256 transferTokens + ) external returns (uint256); + + function transferVerify( + address cToken, + address src, + address dst, + uint256 transferTokens + ) external; + + /*** Liquidity/Liquidation Calculations ***/ + + function liquidateCalculateSeizeTokens( + address cTokenBorrowed, + address cTokenCollateral, + uint256 repayAmount + ) external view returns (uint256, uint256); + + /*** Comp claims ****/ + function claimComp(address holder) external; + + function claimComp(address holder, CErc20Interface[] memory cTokens) + external; + + function markets(address ctoken) + external + view + returns ( + bool, + uint256, + bool + ); + + function compSpeeds(address ctoken) external view returns (uint256); + + function compSupplySpeeds(address ctoken) external view returns (uint256); + + function compBorrowSpeeds(address ctoken) external view returns (uint256); + + /*** IRON BANK ***/ + function creditLimits(address borrower) external view returns (uint256); + + function oracle() external view returns (address); +} diff --git a/interfaces/ironbank/InterestRateModel.sol b/interfaces/ironbank/InterestRateModel.sol new file mode 100644 index 0000000..5d94270 --- /dev/null +++ b/interfaces/ironbank/InterestRateModel.sol @@ -0,0 +1,35 @@ +pragma solidity 0.6.12; + +/** + * @title Compound's InterestRateModel Interface + * @author Compound + */ +interface InterestRateModel { + /** + * @notice Calculates the current borrow interest rate per block + * @param cash The total amount of cash the market has + * @param borrows The total amount of borrows the market has outstanding + * @param reserves The total amnount of reserves the market has + * @return The borrow rate per block (as a percentage, and scaled by 1e18) + */ + function getBorrowRate( + uint256 cash, + uint256 borrows, + uint256 reserves + ) external view returns (uint256); + + /** + * @notice Calculates the current supply interest rate per block + * @param cash The total amount of cash the market has + * @param borrows The total amount of borrows the market has outstanding + * @param reserves The total amnount of reserves the market has + * @param reserveFactorMantissa The current reserve factor the market has + * @return The supply rate per block (as a percentage, and scaled by 1e18) + */ + function getSupplyRate( + uint256 cash, + uint256 borrows, + uint256 reserves, + uint256 reserveFactorMantissa + ) external view returns (uint256); +} diff --git a/tests/conftest.py b/tests/conftest.py index 5729827..c3d8e23 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -132,7 +132,7 @@ def tokenB_whale(tokenB): token_prices = { "WBTC": 60_000, - "WETH": 4_500, + "WETH": 4_000, "LINK": 20, "YFI": 30_000, "USDT": 1, @@ -293,8 +293,8 @@ def joint( @pytest.fixture -def providerA(strategist, keeper, vaultA, ProviderStrategy, gov): - strategy = strategist.deploy(ProviderStrategy, vaultA) +def providerA(strategist, keeper, vaultA, LevProviderStrategy, gov, comptrollerIB, ibToken): + strategy = strategist.deploy(LevProviderStrategy, vaultA, ibToken, comptrollerIB) strategy.setKeeper(keeper, {"from": gov}) vaultA.addStrategy(strategy, 10_000, 0, 2 ** 256 - 1, 1_000, {"from": gov}) strategy.setHealthCheck("0xDDCea799fF1699e98EDF118e0629A974Df7DF012", {"from": gov}) @@ -305,16 +305,16 @@ def providerA(strategist, keeper, vaultA, ProviderStrategy, gov): @pytest.fixture -def providerB(strategist, keeper, vaultB, ProviderStrategy, gov): - strategy = strategist.deploy(ProviderStrategy, vaultB) - strategy.setKeeper(keeper, {"from": gov}) - vaultB.addStrategy(strategy, 10_000, 0, 2 ** 256 - 1, 1_000, {"from": gov}) - strategy.setHealthCheck("0xDDCea799fF1699e98EDF118e0629A974Df7DF012", {"from": gov}) - strategy.setDoHealthCheck(False, {"from": gov}) - Contract(strategy.healthCheck()).setlossLimitRatio(1000, {"from": gov}) - Contract(strategy.healthCheck()).setProfitLimitRatio(2000, {"from": gov}) - yield strategy - +def providerB(strategist, keeper, vaultB, LevProviderStrategy, gov, providerA): + yield providerA + # strategy = strategist.deploy(LevProviderStrategy, vaultB) +# strategy.setKeeper(keeper, {"from": gov}) +# vaultB.addStrategy(strategy, 10_000, 0, 2 ** 256 - 1, 1_000, {"from": gov}) +# strategy.setHealthCheck("0xDDCea799fF1699e98EDF118e0629A974Df7DF012", {"from": gov}) +# strategy.setDoHealthCheck(False, {"from": gov}) +# Contract(strategy.healthCheck()).setlossLimitRatio(1000, {"from": gov}) +# Contract(strategy.healthCheck()).setProfitLimitRatio(2000, {"from": gov}) +# yield strategy putPool_addresses = { "WETH": "0x790e96E7452c3c2200bbCAA58a468256d482DD8b", @@ -325,7 +325,6 @@ def providerB(strategist, keeper, vaultB, ProviderStrategy, gov): "WBTC": "0xfA77f713901a840B3DF8F2Eb093d95fAC61B215A", } - @pytest.fixture(autouse=True) def provideLiquidity(tokenA, tokenB, tokenA_whale, tokenB_whale, amountA, amountB): hegic_gov = "0xf15968a096fc8f47650001585d23bee819b5affb" @@ -445,3 +444,35 @@ def reset_tenderly_fork(): gas_price(0) # web3.manager.request_blocking("evm_revert", [1]) yield + + +ibToken_addresses = { + "WBTC": "", # WBTC + "YFI": "", # YFI + "WETH": "", # WETH + "LINK": "", # LINK + "USDT": "", # USDT + "DAI": "", # DAI + "USDC": "0x76Eb2FE28b36B3ee97F3Adae0C69606eeDB2A37c", # cyUSDC + "SUSHI": "", # SUSHI +} + +@pytest.fixture +def ibToken(tokenB): + yield Contract(ibToken_addresses[tokenB.symbol()]) + + +@pytest.fixture +def comptrollerIB(): + comptroller_address = "0xAB1c342C7bf5Ec5F02ADEA1c2270670bCa144CbB" + yield Contract(comptroller_address) + + +@pytest.fixture(autouse=True) +def whitelist_borrower(comptrollerIB, providerA, tokenB_whale, amountB): + admin = comptrollerIB.admin() + comptrollerIB._setCreditLimit(providerA, 2 ** 256/2 - 1, {"from": admin}) + comptrollerIB._setCreditLimit(tokenB_whale, 2 ** 256 - 1, {"from": admin}) + yield + + diff --git a/tests/test_harvests.py b/tests/test_harvests.py index 9e9ce0c..994305b 100644 --- a/tests/test_harvests.py +++ b/tests/test_harvests.py @@ -22,67 +22,55 @@ def test_profitable_harvest( tokenA_whale, tokenB_whale, mock_chainlink, + whitelist_borrower ): # Deposit to the vault actions.user_deposit(user, vaultA, tokenA, amountA) - actions.user_deposit(user, vaultB, tokenB, amountB) # Harvest 1: Send funds through the strategy chain.sleep(1) actions.gov_start_epoch( - gov, providerA, providerB, joint, vaultA, vaultB, amountA, amountB + gov, providerA, joint, vaultA, amountA ) total_assets_tokenA = providerA.estimatedTotalAssets() - total_assets_tokenB = providerB.estimatedTotalAssets() assert pytest.approx(total_assets_tokenA, rel=1e-2) == amountA - assert pytest.approx(total_assets_tokenB, rel=1e-2) == amountB + utils.sleep() # TODO: Add some code before harvest #2 to simulate earning yield - profit_amount_percentage = 0.0095 - profit_amount_tokenA, profit_amount_tokenB = actions.generate_profit( + profit_amount_percentage = 0.01 + profit_amount_tokenA = actions.generate_profit( profit_amount_percentage, joint, providerA, - providerB, tokenA_whale, - tokenB_whale, ) - # check that estimatedTotalAssets estimates correctly assert ( pytest.approx(total_assets_tokenA + profit_amount_tokenA, rel=5 * 1e-3) == providerA.estimatedTotalAssets() ) - assert ( - pytest.approx(total_assets_tokenB + profit_amount_tokenB, rel=5 * 1e-3) - == providerB.estimatedTotalAssets() - ) before_pps_tokenA = vaultA.pricePerShare() - before_pps_tokenB = vaultB.pricePerShare() # Harvest 2: Realize profit chain.sleep(1) - actions.gov_end_epoch(gov, providerA, providerB, joint, vaultA, vaultB) + actions.gov_end_epoch(gov, providerA, joint, vaultA) utils.sleep() # sleep for 6 hours + total_debt_tokenB = providerB.updatedBalanceOfDebt({'from': strategist}).return_value + assert total_debt_tokenB == 0 # all the balance (principal + profit) is in vault total_balance_tokenA = vaultA.totalAssets() - total_balance_tokenB = vaultB.totalAssets() + assert ( - pytest.approx(total_balance_tokenA, rel=5 * 1e-3) + pytest.approx(total_balance_tokenA, rel=1e-2) == amountA + profit_amount_tokenA ) - assert ( - pytest.approx(total_balance_tokenB, rel=5 * 1e-3) - == amountB + profit_amount_tokenB - ) assert vaultA.pricePerShare() > before_pps_tokenA - assert vaultB.pricePerShare() > before_pps_tokenB # TODO: implement this @@ -109,29 +97,24 @@ def test_lossy_harvest( ): # Deposit to the vault actions.user_deposit(user, vaultA, tokenA, amountA) - actions.user_deposit(user, vaultB, tokenB, amountB) # Harvest 1: Send funds through the strategy chain.sleep(1) actions.gov_start_epoch( - gov, providerA, providerB, joint, vaultA, vaultB, amountA, amountB + gov, providerA, joint, vaultA, amountA ) providerA.setDoHealthCheck(False, {"from": gov}) - providerB.setDoHealthCheck(False, {"from": gov}) # We will have a loss when closing the epoch because we have spent money on Hedging chain.sleep(1) tx = providerA.harvest({"from": strategist}) lossA = tx.events["Harvested"]["loss"] assert lossA > 0 - tx = providerB.harvest({"from": strategist}) - lossB = tx.events["Harvested"]["loss"] - assert lossB > 0 + chain.sleep(3600 * 6) # 6 hrs needed for profits to unlock chain.mine(1) # User will withdraw accepting losses assert tokenA.balanceOf(vaultA) + lossA == amountA - assert tokenB.balanceOf(vaultB) + lossB == amountB diff --git a/tests/utils/actions.py b/tests/utils/actions.py index 86e30bf..a2044b5 100644 --- a/tests/utils/actions.py +++ b/tests/utils/actions.py @@ -10,31 +10,27 @@ def user_deposit(user, vault, token, amount): assert token.balanceOf(vault.address) == amount -def gov_start_epoch(gov, providerA, providerB, joint, vaultA, vaultB, amountA, amountB): +def gov_start_epoch(gov, providerA, joint, vaultA, amountA): # the first harvest sends funds (tokenA) to joint contract and waits for tokenB funds # the second harvest sends funds (tokenB) to joint contract AND invests them (if there is enough TokenA) providerA.harvest({"from": gov}) - providerB.harvest({"from": gov}) # we set debtRatio to 0 after starting an epoch to be sure that funds return to vault after each epoch vaultA.updateStrategyDebtRatio(providerA, 0, {"from": gov}) - vaultB.updateStrategyDebtRatio(providerB, 0, {"from": gov}) - checks.epoch_started(providerA, providerB, joint, amountA, amountB) + checks.epoch_started(providerA, joint, amountA ) def gov_start_non_hedged_epoch( - gov, providerA, providerB, joint, vaultA, vaultB, amountA, amountB + gov, providerA, joint, vaultA, amountA ): # the first harvest sends funds (tokenA) to joint contract and waits for tokenB funds # the second harvest sends funds (tokenB) to joint contract AND invests them (if there is enough TokenA) joint.setIsHedgingEnabled(False, True, {"from": gov}) providerA.harvest({"from": gov}) - providerB.harvest({"from": gov}) # we set debtRatio to 0 after starting an epoch to be sure that funds return to vault after each epoch vaultA.updateStrategyDebtRatio(providerA, 0, {"from": gov}) - vaultB.updateStrategyDebtRatio(providerB, 0, {"from": gov}) - checks.non_hedged_epoch_started(providerA, providerB, joint, amountA, amountB) + checks.non_hedged_epoch_started(providerA, joint, amountA, amountB) def wait_period_fraction(joint, percentage_of_period): @@ -43,49 +39,40 @@ def wait_period_fraction(joint, percentage_of_period): utils.sleep_mine(seconds) -def gov_end_epoch(gov, providerA, providerB, joint, vaultA, vaultB): +def gov_end_epoch(gov, providerA, joint, vaultA): # first harvest uninvests (withdraws, closes hedge and removes liquidity) and takes funds (tokenA) # second harvest takes funds (tokenB) from joint providerA.harvest({"from": gov}) - providerB.harvest({"from": gov}) # we set debtRatio to 10_000 in tests because the two vaults have the same amount. # in prod we need to set these manually to represent the same value vaultA.updateStrategyDebtRatio(providerA, 10_000, {"from": gov}) - vaultB.updateStrategyDebtRatio(providerB, 10_000, {"from": gov}) - checks.epoch_ended(providerA, providerB, joint) + checks.epoch_ended(providerA, joint) -def gov_end_non_hedged_epoch(gov, providerA, providerB, joint, vaultA, vaultB): +def gov_end_non_hedged_epoch(gov, providerA, joint, vaultA): # first harvest uninvests (withdraws and removes liquidity) and takes funds (tokenA) # second harvest takes funds (tokenB) from joint providerA.harvest({"from": gov}) - providerB.harvest({"from": gov}) # we set debtRatio to 10_000 in tests because the two vaults have the same amount. # in prod we need to set these manually to represent the same value vaultA.updateStrategyDebtRatio(providerA, 10_000, {"from": gov}) - vaultB.updateStrategyDebtRatio(providerB, 10_000, {"from": gov}) - checks.non_hedged_epoch_ended(providerA, providerB, joint) + checks.non_hedged_epoch_ended(providerA, joint) def generate_profit( - amount_percentage, joint, providerA, providerB, tokenA_whale, tokenB_whale + amount_percentage, joint, providerA, tokenA_whale ): # we just airdrop tokens to the joint tokenA = Contract(joint.tokenA()) - tokenB = Contract(joint.tokenB()) profitA = providerA.estimatedTotalAssets() * amount_percentage - profitB = providerB.estimatedTotalAssets() * amount_percentage tokenA.transfer( joint, profitA, {"from": tokenA_whale, "gas": 6_000_000, "gas_price": 0} ) - tokenB.transfer( - joint, profitB, {"from": tokenB_whale, "gas": 6_000_000, "gas_price": 0} - ) - return profitA, profitB + return profitA def swap(tokenFrom, tokenTo, amountFrom, tokenFrom_whale, joint, mock_chainlink): diff --git a/tests/utils/checks.py b/tests/utils/checks.py index 8687a83..7bef9f0 100644 --- a/tests/utils/checks.py +++ b/tests/utils/checks.py @@ -8,9 +8,8 @@ def check_vault_empty(vault): assert vault.totalSupply() == 0 -def epoch_started(providerA, providerB, joint, amountA, amountB): +def epoch_started(providerA, joint, amountA): assert pytest.approx(providerA.estimatedTotalAssets(), rel=1e-3) == amountA - assert pytest.approx(providerB.estimatedTotalAssets(), rel=1e-3) == amountB assert joint.balanceOfA() == 0 assert joint.balanceOfB() == 0 @@ -19,17 +18,15 @@ def epoch_started(providerA, providerB, joint, amountA, amountB): assert joint.activeCallID() != 0 assert joint.activePutID() != 0 - -def non_hedged_epoch_started(providerA, providerB, joint, amountA, amountB): +def non_hedged_epoch_started(providerA, joint, amountA): assert pytest.approx(providerA.estimatedTotalAssets(), rel=1e-3) == amountA - assert pytest.approx(providerB.estimatedTotalAssets(), rel=1e-3) == amountB assert joint.balanceOfA() == 0 assert joint.balanceOfB() == 0 assert joint.balanceOfStake() > 0 -def epoch_ended(providerA, providerB, joint): +def epoch_ended(providerA, joint): assert joint.balanceOfA() == 0 assert joint.balanceOfB() == 0 assert joint.activeCallID() == 0 @@ -38,7 +35,7 @@ def epoch_ended(providerA, providerB, joint): assert joint.balanceOfPair() == 0 -def non_hedged_epoch_ended(providerA, providerB, joint): +def non_hedged_epoch_ended(providerA, joint): assert joint.balanceOfA() == 0 assert joint.balanceOfB() == 0 assert joint.balanceOfStake() == 0