diff --git a/contracts/interfaces/curve.sol b/contracts/interfaces/curve.sol index 0d5e6283a..3708b35e2 100644 --- a/contracts/interfaces/curve.sol +++ b/contracts/interfaces/curve.sol @@ -31,6 +31,9 @@ interface ICurveFi_3 { function add_liquidity(uint256[3] calldata amounts, uint256 min_mint_amount) external; + function add_liquidity(uint256[3] calldata amounts, uint256 min_mint_amount, bool _use_underlying) + external returns (uint256); + function remove_liquidity_imbalance( uint256[3] calldata amounts, uint256 max_burn_amount @@ -79,6 +82,14 @@ interface ICurveFi_4 { function balances(int128) external view returns (uint256); } +interface ICurveFi_5 { + function add_liquidity( + uint256[] calldata _amounts, + uint256 _min_mint_amount, + bool _use_underlying + ) external returns (uint256); +} + interface ICurveZap_4 { function add_liquidity( uint256[4] calldata uamounts, @@ -151,6 +162,10 @@ interface ICurveGauge { function claimable_reward(address, address) external view returns (uint256); function integrate_fraction(address arg0) external view returns (uint256); + + function bridge(address _token) external returns (bool); + + function add_liquidity(uint256[] calldata _amounts, uint256 _min_mint_amount) external returns (uint256); } interface ICurveMintr { diff --git a/contracts/snowglobes/curve/snowglobe-crv-aave.sol b/contracts/snowglobes/curve/snowglobe-crv-aave.sol new file mode 100644 index 000000000..6181f5918 --- /dev/null +++ b/contracts/snowglobes/curve/snowglobe-crv-aave.sol @@ -0,0 +1,131 @@ +// https://github.com/iearn-finance/vaults/blob/master/contracts/vaults/yVault.sol + +pragma solidity ^0.6.7; + +import "../../interfaces/controller.sol"; + +import "../../lib/erc20.sol"; +import "../../lib/safe-math.sol"; + +contract SnowGlobeCrvAave is ERC20 { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + IERC20 public token; + + uint256 public min = 9500; + uint256 public constant max = 10000; + + address public governance; + address public timelock; + address public controller; + + constructor(address _token, address _governance, address _timelock, address _controller) + public + ERC20( + string(abi.encodePacked("freezing ", ERC20(_token).name())), + string(abi.encodePacked("s", ERC20(_token).symbol())) + ) + { + _setupDecimals(ERC20(_token).decimals()); + token = IERC20(_token); + governance = _governance; + timelock = _timelock; + controller = _controller; + } + + function balance() public view returns (uint256) { + return + token.balanceOf(address(this)).add( + IController(controller).balanceOf(address(token)) + ); + } + + function setMin(uint256 _min) external { + require(msg.sender == governance, "!governance"); + require(_min <= max, "numerator cannot be greater than denominator"); + min = _min; + } + + function setGovernance(address _governance) public { + require(msg.sender == governance, "!governance"); + governance = _governance; + } + + function setTimelock(address _timelock) public { + require(msg.sender == timelock, "!timelock"); + timelock = _timelock; + } + + function setController(address _controller) public { + require(msg.sender == timelock, "!timelock"); + controller = _controller; + } + + // Custom logic in here for how much the globes allows to be borrowed + // Sets minimum required on-hand to keep small withdrawals cheap + function available() public view returns (uint256) { + return token.balanceOf(address(this)).mul(min).div(max); + } + + function earn() public { + uint256 _bal = available(); + token.safeTransfer(controller, _bal); + IController(controller).earn(address(token), _bal); + } + + function depositAll() external { + deposit(token.balanceOf(msg.sender)); + } + + function deposit(uint256 _amount) public { + uint256 _pool = balance(); + uint256 _before = token.balanceOf(address(this)); + token.safeTransferFrom(msg.sender, address(this), _amount); + uint256 _after = token.balanceOf(address(this)); + _amount = _after.sub(_before); // Additional check for deflationary tokens + uint256 shares = 0; + if (totalSupply() == 0) { + shares = _amount; + } else { + shares = (_amount.mul(totalSupply())).div(_pool); + } + _mint(msg.sender, shares); + } + + function withdrawAll() external { + withdraw(balanceOf(msg.sender)); + } + + // Used to swap any borrowed reserve over the debt limit to liquidate to 'token' + function harvest(address reserve, uint256 amount) external { + require(msg.sender == controller, "!controller"); + require(reserve != address(token), "token"); + IERC20(reserve).safeTransfer(controller, amount); + } + + // No rebalance implementation for lower fees and faster swaps + function withdraw(uint256 _shares) public { + uint256 r = (balance().mul(_shares)).div(totalSupply()); + _burn(msg.sender, _shares); + + // Check balance + uint256 b = token.balanceOf(address(this)); + if (b < r) { + uint256 _withdraw = r.sub(b); + IController(controller).withdraw(address(token), _withdraw); + uint256 _after = token.balanceOf(address(this)); + uint256 _diff = _after.sub(b); + if (_diff < _withdraw) { + r = b.add(_diff); + } + } + + token.safeTransfer(msg.sender, r); + } + + function getRatio() public view returns (uint256) { + return balance().mul(1e18).div(totalSupply()); + } +} diff --git a/contracts/strategies/bases/strategy-crv-base.sol b/contracts/strategies/bases/strategy-crv-base.sol new file mode 100644 index 000000000..e0c6cc754 --- /dev/null +++ b/contracts/strategies/bases/strategy-crv-base.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.6.7; + +import "../strategy-joe-base.sol"; +import "../../interfaces/curve.sol"; +import "../../interfaces/wavax.sol"; +import "../../interfaces/aave.sol"; + +abstract contract StrategyCrvBase is StrategyJoeBase { + address public gauge = 0x5B5CFE992AdAC0C9D48E05854B2d91C73a003858; + address public constant crv = 0x47536F17F4fF30e64A96a7555826b8f9e66ec468; + + address public constant lendingPool = 0x4F01AeD16D97E3aB5ab2B501154DC9bb0F1A5A2C; + uint16 public constant REFERRAL_CODE = 0xaa; + + address public liquidity; + + // Underlying tokens in the pool + address public token1; + address public token2; + address public token3; + + constructor( + address _token1, + address _token2, + address _token3, + address _liquidity, + address _want, + address _governance, + address _strategist, + address _controller, + address _timelock + ) + public StrategyJoeBase( + _want, + _governance, + _strategist, + _controller, + _timelock + ) + { + liquidity = _liquidity; + token1 = _token1; + token2 = _token2; + token3 = _token3; + } + + // **** Getters **** + + function balanceOfPool() public override view returns (uint256) { + uint256 amount = ICurveGauge(gauge).balanceOf(address(this)); + return amount; + } + + function getHarvestable() external view returns (uint256, uint256) { + uint256 pendingCrv = ICurveGauge(gauge).claimable_reward(address(this), crv); + uint256 pendingWavax = ICurveGauge(gauge).claimable_reward(address(this), wavax); + return (pendingCrv, pendingWavax); + } + + + // **** State Mutation functions **** + + function deposit() public override { + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + IERC20(want).safeApprove(gauge, 0); + IERC20(want).safeApprove(gauge, _want); + ICurveGauge(gauge).deposit(_want); + } + } + + function _withdrawSome(uint256 _amount) + internal + override + returns (uint256) + { + ICurveGauge(gauge).withdraw(_amount, true); + return _amount; + } + + // **** This function checks the balances of the underlying tokens in **** // + // **** the pool and returns the one with the least liquidity present **** // + function getMostPremium() + public + view + returns (address, uint256) + { + uint256[] memory balances = new uint256[](3); + balances[0] = ICurveFi_3(liquidity).balances(0); + balances[1] = ICurveFi_3(liquidity).balances(1); + balances[2] = ICurveFi_3(liquidity).balances(2); + + if ( + balances[0] < balances[1] && + balances[0] < balances[2] + ) { + return (token1, 0); + } + + if ( + balances[1] < balances[0] && + balances[1] < balances[2] + ) { + return (token2, 1); + } + + if ( + balances[2] < balances[0] && + balances[2] < balances[1] + ) { + return (token3, 2); + } + + // If they're somehow equal, we just want any underlying token + return (token1, 0); + } + +} \ No newline at end of file diff --git a/contracts/strategies/curve/strategy-crv-aave.sol b/contracts/strategies/curve/strategy-crv-aave.sol new file mode 100644 index 000000000..6f1b049a1 --- /dev/null +++ b/contracts/strategies/curve/strategy-crv-aave.sol @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.6.7; + +import "../bases/strategy-crv-base.sol"; + +import "hardhat/console.sol"; + +contract StrategyCrvAave is StrategyCrvBase { + // stablecoins + address public daie = 0xd586E7F844cEa2F87f50152665BCbc2C279D8d70; + address public usdce = 0xA7D7079b0FEaD91F3e65f86E8915Cb59c1a4C664; + address public usdte = 0xc7198437980c041c805A1EDcbA50c1Ce5db95118; + + // avstablecoins + address public constant avdaie = 0x47AFa96Cdc9fAb46904A55a6ad4bf6660B53c38a; + address public constant avusdce = 0x46A51127C3ce23fb7AB1DE06226147F446e4a857; + address public constant avusdte = 0x532E6537FEA298397212F09A61e03311686f548e; + + address public constant aDaie_aUsdce_aUsdte_lp = 0x1337BedC9D22ecbe766dF105c9623922A27963EC; + + address public curve = 0x7f90122BF0700F9E7e1F688fe926940E8839F353; + + constructor( + address _governance, + address _strategist, + address _controller, + address _timelock + ) + public + StrategyCrvBase( + daie, + usdce, + usdte, + curve, + aDaie_aUsdce_aUsdte_lp, + _governance, + _strategist, + _controller, + _timelock + + ){} + + // **** State Mutations **** + + function harvest() public onlyBenevolent override { + // retrieve underlying token we want to add liquidity with + (address to, ) = getMostPremium(); + + // Collects reward tokens + ICurveGauge(gauge).claim_rewards(address(this)); + + // Take Avax Rewards + uint256 _avax = address(this).balance; // get balance of native AVAX + if (_avax > 0) { // wrap AVAX into ERC20 + WAVAX(wavax).deposit{value: _avax}(); + } + + // Swap rewards for underlying token from getMostPremium(); + uint256 _crv = IERC20(crv).balanceOf(address(this)); // get balance of crv tokens + if (_crv > 0) { + _swapTraderJoe(crv, wavax, _crv); + } + + uint256 _wavax = IERC20(wavax).balanceOf(address(this)); // get balance of wavax tokens + if (_wavax > 0) { + uint256 _keep = _wavax.mul(keep).div(keepMax); + if (_keep > 0) { + _takeFeeWavaxToSnob(_keep); + } + + _wavax = IERC20(wavax).balanceOf(address(this)); + + IERC20(wavax).safeApprove(joeRouter, 0); + IERC20(wavax).safeApprove(joeRouter, _wavax); + + _swapTraderJoe(wavax, to, _wavax); + + } + + // Adds liquidity to curve's aave pool + uint256 _to = IERC20(to).balanceOf(address(this)); // the balance of the token we want to add liquidity with + if (_to > 0) { + IERC20(to).safeApprove(lendingPool, 0); + IERC20(to).safeApprove(lendingPool, _to); + + ILendingPool(lendingPool).deposit( + to, + _to, + address(this), + REFERRAL_CODE + ); + + uint256[] memory amounts = new uint256[](3); + + uint256 _avdaie = IERC20(avdaie).balanceOf(address(this)); + uint256 _avusdce = IERC20(avusdce).balanceOf(address(this)); + uint256 _avusdte = IERC20(avusdte).balanceOf(address(this)); + + amounts[0] = _avdaie; + amounts[1] = _avusdce; + amounts[2] = _avusdte; + + IERC20(avdaie).safeApprove(curve, 0); + IERC20(avdaie).safeApprove(curve, _avdaie); + + IERC20(avusdce).safeApprove(curve, 0); + IERC20(avusdce).safeApprove(curve, _avusdce); + + IERC20(avusdte).safeApprove(curve, 0); + IERC20(avusdte).safeApprove(curve, _avusdte); + + ICurveGauge(curve).add_liquidity(amounts, 0); + } + + // Donates DUST + _wavax = IERC20(wavax).balanceOf(address(this)); + _crv = IERC20(crv).balanceOf(address(this)); + uint256 _daie = IERC20(daie).balanceOf(address(this)); + uint256 _usdce = IERC20(usdce).balanceOf(address(this)); + uint256 _usdte = IERC20(usdte).balanceOf(address(this)); + if (_wavax > 0){ + IERC20(wavax).transfer( + IController(controller).treasury(), + _wavax + ); + } + + if (_crv > 0){ + IERC20(crv).safeTransfer( + IController(controller).treasury(), + _crv + ); + } + + if (_daie > 0){ + IERC20(daie).safeTransfer( + IController(controller).treasury(), + _daie + ); + } + + if (_usdce > 0){ + IERC20(usdce).safeTransfer( + IController(controller).treasury(), + _usdce + ); + } + + if (_usdte > 0){ + IERC20(usdte).safeTransfer( + IController(controller).treasury(), + _usdte + ); + } + + // We want to get back sCRV + _distributePerformanceFeesAndDeposit(); + } + + function getName() external override pure returns (string memory) { + return "StrategyCrvAave"; + } +} \ No newline at end of file diff --git a/test/LPStrats/curve.test.ts b/test/LPStrats/curve.test.ts new file mode 100644 index 000000000..d1036e48e --- /dev/null +++ b/test/LPStrats/curve.test.ts @@ -0,0 +1,19 @@ +import { doStrategyTest } from "./../strategy-test"; +import { TestableStrategy, LPTestDefault } from "./../strategy-test-case"; + + +const tests = [ + { + name: "CrvAave", + controller: "backup", + timelockIsStrategist: true, + slot: 2 + }, +]; + +describe("Curve Strategy test", function() { + for (const test of tests) { + let Test: TestableStrategy = { ...LPTestDefault, ...test }; + doStrategyTest(Test); + } +}); \ No newline at end of file diff --git a/test/utils/helpers.ts b/test/utils/helpers.ts index e1a1de320..198f3ea93 100644 --- a/test/utils/helpers.ts +++ b/test/utils/helpers.ts @@ -55,7 +55,7 @@ export function toGwei(amount: BigNumber): string { }; export async function overwriteTokenAmount(assetAddr: string, walletAddr: string, amount: string, slot: number = 0) { - const index = ethers.utils.solidityKeccak256(["uint256", "uint256"], [walletAddr, slot]); + const index = ethers.utils.solidityKeccak256(["uint256", "uint256"], [slot, walletAddr]); const BN = ethers.BigNumber.from(amount)._hex.toString(); const number = ethers.utils.hexZeroPad(BN, 32);