Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -119,7 +119,7 @@ const config: HardhatUserConfig = {
},
},
paths: {
sources: "./src/strategies/kava",
sources: "./src/strategies/frax",
tests: "./src/tests/strategies",
cache: "./cache",
artifacts: "./artifacts",
Expand Down
6 changes: 6 additions & 0 deletions src/interfaces/curve.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
184 changes: 184 additions & 0 deletions src/strategies/frax/strategy-frax-convex-rsr-fraxbp.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
138 changes: 138 additions & 0 deletions src/tests/strategies/frax/strategy-frax-convex-rsr-fraxbp.test.js
Original file line number Diff line number Diff line change
@@ -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]);
});
});
3 changes: 0 additions & 3 deletions src/tests/utils/setupHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
};