diff --git a/hardhat.config.ts b/hardhat.config.ts index 59d68f29..c96e3ed4 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -37,9 +37,9 @@ const config: HardhatUserConfig = { networks: { hardhat: { forking: { - url: `https://evm.kava.io`, + url: `https://mainnet.infura.io/v3/${process.env.INFURA_KEY}`, // ignoreUnknownTxType: true, // needed to work with patched Hardhat + Arbitrum Nitro - blockNumber: 2464633, + // blockNumber: 2464633, }, accounts: { mnemonic: process.env.MNEMONIC, @@ -119,7 +119,7 @@ const config: HardhatUserConfig = { }, }, paths: { - sources: "./src/strategies/kava", + sources: "./src/strategies/frax", tests: "./src/tests/strategies", cache: "./cache", artifacts: "./artifacts", diff --git a/src/interfaces/curve.sol b/src/interfaces/curve.sol index a6f6760b..9f8ed013 100644 --- a/src/interfaces/curve.sol +++ b/src/interfaces/curve.sol @@ -107,6 +107,12 @@ interface ICurveZap { int128 i, uint256 min_uamount ) external; + + function add_liquidity( + address _pool, + uint256[3] calldata _deposit_amounts, + uint256 _min_mint_amount + ) external returns (uint256); } interface ICurveFi_Polygon_3 { diff --git a/src/strategies/frax/strategy-frax-convex-rsr-fraxbp.sol b/src/strategies/frax/strategy-frax-convex-rsr-fraxbp.sol new file mode 100644 index 00000000..76c01744 --- /dev/null +++ b/src/strategies/frax/strategy-frax-convex-rsr-fraxbp.sol @@ -0,0 +1,184 @@ +pragma solidity ^0.6.7; +pragma experimental ABIEncoderV2; + +import "../strategy-base.sol"; +import "../../interfaces/saddle-farm.sol"; +import "../../interfaces/curve.sol"; + +interface IVaultFactory { + function createVault(uint256 _pid) external returns (address); +} + +interface IConvexPersonalVault { + function stakeLockedCurveLp(uint256 _liquidity, uint256 _secs) external returns (bytes32 kek_id); + + function getReward() external; + + function withdrawLockedAndUnwrap(bytes32) external; +} + +contract StrategyFraxConvexRsrFraxBP is StrategyBase { + uint256 private PID = 37; + address private VAULT_FACTORY = 0x569f5B842B5006eC17Be02B8b94510BA8e79FbCa; + + address private RSR_FRAXBP_TOKEN = 0x3F436954afb722F5D14D868762a23faB6b0DAbF0; + address private RSR_FRAXBP_POOL = 0x6a6283aB6e31C2AeC3fA08697A8F806b740660b2; + address private CURVE_ZAP = 0x5De4EF4879F4fe3bBADF2227D2aC5d0E2D76C895; + + address private FRAX_FARM = 0xF22D3C85e41Ef4b5Ac8Cb8B89a14718e290a0561; + + address private crv = 0xD533a949740bb3306d119CC777fa900bA034cd52; + address private cvx = 0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B; + address private fxs = 0x3432B6A60D23Ca0dFCa7761B7ab56459D9C964D0; + address private frax = 0x853d955aCEf822Db058eb8505911ED77F175b99e; + + uint24 private constant poolFee = 10000; + + // Uniswap swap paths + address[] private fxs_frax_path; + + address immutable convexPersonalVault; + + constructor( + address _governance, + address _strategist, + address _controller, + address _timelock + ) public StrategyBase(RSR_FRAXBP_TOKEN, _governance, _strategist, _controller, _timelock) { + convexPersonalVault = IVaultFactory(VAULT_FACTORY).createVault(PID); + + fxs_frax_path = new address[](2); + fxs_frax_path[0] = fxs; + fxs_frax_path[1] = frax; + } + + function getName() external pure override returns (string memory) { + return "StrategyFraxConvexRsrFraxBP"; + } + + function balanceOfPool() public view override returns (uint256) { + return ICommunalFarm(FRAX_FARM).lockedLiquidityOf(convexPersonalVault); + } + + function getHarvestable() public view returns (uint256[] memory) { + return ICommunalFarm(FRAX_FARM).earned(address(this)); + } + + function deposit() public override { + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + uint256 _min = ICommunalFarm(FRAX_FARM).lock_time_min(); + IERC20(want).safeApprove(convexPersonalVault, 0); + IERC20(want).safeApprove(convexPersonalVault, _want); + IConvexPersonalVault(convexPersonalVault).stakeLockedCurveLp(_want, _min); + } + } + + function _withdrawSome(uint256 _amount) internal override returns (uint256) { + LockedStake[] memory lockedStakes = ICommunalFarm(FRAX_FARM).lockedStakesOf(convexPersonalVault); + uint256 _sum = 0; + uint256 count = 0; + uint256 i; + for (i = 0; i < lockedStakes.length; i++) { + if (lockedStakes[i].liquidity == 0) continue; + + _sum = _sum.add(lockedStakes[i].liquidity); + count++; + if (_sum >= _amount) break; + } + require(_sum >= _amount, "insufficient amount"); + + for (i = 0; i < count; i++) { + if (lockedStakes[i].liquidity > 0) + IConvexPersonalVault(convexPersonalVault).withdrawLockedAndUnwrap(lockedStakes[i].kek_id); + } + uint256 _balance = IERC20(want).balanceOf(address(this)); + + require(_balance >= _amount, "withdraw failed"); + + return _amount; + } + + function harvest() public override onlyBenevolent { + deposit(); + IConvexPersonalVault(convexPersonalVault).getReward(); + + // Step 1: Swap CRV -> ETH -> FXS + uint256 _crv = IERC20(crv).balanceOf(address(this)); + if (_crv > 0) { + IERC20(crv).safeApprove(univ3Router, 0); + IERC20(crv).safeApprove(univ3Router, _crv); + ISwapRouter(univ3Router).exactInput( + ISwapRouter.ExactInputParams({ + path: abi.encodePacked(crv, poolFee, weth, poolFee, fxs), + recipient: address(this), + deadline: block.timestamp + 300, + amountIn: _crv, + amountOutMinimum: 0 + }) + ); + } + + // Step 2: Swap CVX -> ETH -> FXS + uint256 _cvx = IERC20(cvx).balanceOf(address(this)); + + if (_cvx > 0) { + IERC20(cvx).safeApprove(univ3Router, 0); + IERC20(cvx).safeApprove(univ3Router, _cvx); + ISwapRouter(univ3Router).exactInput( + ISwapRouter.ExactInputParams({ + path: abi.encodePacked(cvx, poolFee, weth, poolFee, fxs), + recipient: address(this), + deadline: block.timestamp + 300, + amountIn: _cvx, + amountOutMinimum: 0 + }) + ); + } + + // Step 3: Swap all FXS -> FRAX + uint256 _fxs = IERC20(fxs).balanceOf(address(this)); + + if (_fxs > 0) { + IERC20(fxs).safeApprove(univ3Router, 0); + IERC20(fxs).safeApprove(univ3Router, _fxs); + ISwapRouter(univ3Router).exactInput( + ISwapRouter.ExactInputParams({ + path: abi.encodePacked(fxs, poolFee, frax), + recipient: address(this), + deadline: block.timestamp + 300, + amountIn: _fxs, + amountOutMinimum: 0 + }) + ); + } + + uint256 _frax = IERC20(frax).balanceOf(address(this)); + + // Treasury fees + IERC20(frax).safeTransfer( + IController(controller).treasury(), + _frax.mul(performanceTreasuryFee).div(performanceTreasuryMax) + ); + + _frax = IERC20(frax).balanceOf(address(this)); + if (_frax > 0) { + uint256[3] memory amounts; + amounts[1] = _frax; + IERC20(frax).safeApprove(CURVE_ZAP, 0); + IERC20(frax).safeApprove(CURVE_ZAP, _frax); + + ICurveZap(CURVE_ZAP).add_liquidity(RSR_FRAXBP_POOL, amounts, 0); + } + deposit(); + } + + function totalWithdrawable() external view returns (uint256 _sum) { + LockedStake[] memory lockedStakes = ICommunalFarm(FRAX_FARM).lockedStakesOf(convexPersonalVault); + for (uint256 i = 0; i < lockedStakes.length; i++) { + LockedStake memory stake = lockedStakes[i]; + if (stake.liquidity == 0 && block.timestamp < stake.ending_timestamp) continue; + _sum = _sum.add(lockedStakes[i].liquidity); + } + } +} diff --git a/src/tests/strategies/frax/strategy-frax-convex-rsr-fraxbp.test.js b/src/tests/strategies/frax/strategy-frax-convex-rsr-fraxbp.test.js new file mode 100644 index 00000000..d75387c5 --- /dev/null +++ b/src/tests/strategies/frax/strategy-frax-convex-rsr-fraxbp.test.js @@ -0,0 +1,138 @@ +const { + expect, + increaseTime, + deployContract, + getContractAt, + unlockAccount, + toWei, + NULL_ADDRESS, +} = require("../../utils/testHelper"); + +describe("StrategyFraxConvexRsrFraxBP Test", () => { + let alice; + let strategy, pickleJar, controller; + let governance, strategist, devfund, treasury, timelock; + let preTestSnapshotID; + let want, frax; + const want_addr = "0x3F436954afb722F5D14D868762a23faB6b0DAbF0"; + const frax_addr = "0x853d955acef822db058eb8505911ed77f175b99e"; + const want_amount = toWei(100); + + before("Deploy contracts", async () => { + [alice, devfund, treasury] = await hre.ethers.getSigners(); + governance = alice; + strategist = alice; + timelock = alice; + + controller = await deployContract( + "src/polygon/controller-v4.sol:ControllerV4", + governance.address, + strategist.address, + timelock.address, + devfund.address, + treasury.address + ); + console.log("Controller is deployed at ", controller.address); + + strategy = await deployContract( + "StrategyFraxConvexRsrFraxBP", + governance.address, + strategist.address, + controller.address, + timelock.address + ); + console.log("Strategy is deployed at ", strategy.address); + + want = await getContractAt("src/lib/erc20.sol:ERC20", want_addr); + frax = await getContractAt("src/lib/erc20.sol:ERC20", frax_addr); + + pickleJar = await deployContract( + "PickleJar", + want.address, + governance.address, + timelock.address, + controller.address + ); + console.log("PickleJar is deployed at ", pickleJar.address); + + await controller.setJar(want.address, pickleJar.address); + await controller.approveStrategy(want.address, strategy.address); + await controller.setStrategy(want.address, strategy.address); + // get want token + await getWant(); + }); + + it("Should withdraw correctly", async () => { + const _want = await want.balanceOf(alice.address); + await want.approve(pickleJar.address, _want); + await pickleJar.deposit(_want); + await pickleJar.earn(); + + await increaseTime(60 * 60 * 24 * 1); + console.log("Ratio before harvest: ", (await pickleJar.getRatio()).toString()); + await strategy.harvest(); + console.log("Ratio after harvest: ", (await pickleJar.getRatio()).toString()); + + await increaseTime(60 * 60 * 24 * 8); + let _before = await want.balanceOf(pickleJar.address); + await controller.withdrawAll(want.address); + let _after = await want.balanceOf(pickleJar.address); + expect(_after).to.be.gt(_before, "controller withdrawAll failed"); + + _before = await want.balanceOf(alice.address); + await pickleJar.withdrawAll(); + _after = await want.balanceOf(alice.address); + + expect(_after).to.be.gt(_before, "picklejar withdrawAll failed"); + expect(_after).to.be.gt(_want, "no interest earned"); + }); + + it("Should harvest correctly", async () => { + const _want = await want.balanceOf(alice.address); + await want.approve(pickleJar.address, _want); + await pickleJar.deposit(_want); + await pickleJar.earn(); + await increaseTime(60 * 60 * 24 * 1); + + const _before = await pickleJar.balance(); + console.log("Ratio before harvest: ", (await pickleJar.getRatio()).toString()); + await strategy.harvest(); + console.log("Ratio after harvest: ", (await pickleJar.getRatio()).toString()); + const _after = await pickleJar.balance(); + let _treasuryFraxBefore = await frax.balanceOf(treasury.address); + + expect(_treasuryFraxBefore).to.be.gt(0, "20% performance fee is not given"); + expect(_after).to.be.gt(_before); + + await increaseTime(60 * 60 * 24 * 8); + + //withdraw + const _devBefore = await frax.balanceOf(devfund.address); + await pickleJar.withdrawAll(); + const _treasuryFraxAfter = await frax.balanceOf(treasury.address); + const _devAfter = await frax.balanceOf(devfund.address); + + //0% goes to dev + const _devFund = _devAfter.sub(_devBefore); + expect(_devFund).to.be.eq(0, "dev've stolen money!!!!!"); + + //0% goes to treasury + const _treasuryFund = _treasuryFraxAfter.sub(_treasuryFraxBefore); + expect(_treasuryFund).to.be.eq(0, "treasury've stolen money!!!!"); + }); + + const getWant = async () => { + const whale = await unlockAccount("0x561369B3eC94D001031822011B9231e1436bcc78"); + await want.connect(whale).transfer(alice.address, want_amount); + const _balance = await want.balanceOf(alice.address); + expect(_balance).to.be.eq(want_amount, "get want failed"); + }; + + beforeEach(async () => { + preTestSnapshotID = await hre.network.provider.send("evm_snapshot"); + }); + + afterEach(async () => { + await hre.network.provider.send("evm_revert", [preTestSnapshotID]); + }); +}); diff --git a/src/tests/utils/setupHelper.ts b/src/tests/utils/setupHelper.ts index 7ca1ec6b..688c8bc4 100644 --- a/src/tests/utils/setupHelper.ts +++ b/src/tests/utils/setupHelper.ts @@ -266,11 +266,8 @@ export const getWantFromWhale = async ( whaleAddr: string ) => { const whale = await unlockAccount(whaleAddr); - console.log("here"); const want = await getContractAt("src/lib/erc20.sol:ERC20", want_addr); - console.log("can't get?", whale._address); await want.connect(whale).transfer(to.address, amount); - console.log("here2"); const _balance = await want.balanceOf(to.address); expect(_balance).to.be.gte(amount, "get want from the whale failed"); };