Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Connectors: Implement Convex connector #20

Merged
merged 1 commit into from
Jun 26, 2023
Merged
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
104 changes: 104 additions & 0 deletions packages/connectors/contracts/liquidity/convex/ConvexConnector.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

pragma solidity ^0.8.0;

import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
import '@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol';

import '@mimic-fi/v3-helpers/contracts/math/FixedPoint.sol';

import './ICvxPool.sol';
import './ICvxBooster.sol';

/**
* @title ConvexConnector
*/
contract ConvexConnector {
using FixedPoint for uint256;

// Convex booster
ICvxBooster public immutable booster;

/**
* @dev Creates a new Convex connector
*/
constructor(ICvxBooster _booster) {
booster = _booster;
}

/**
* @dev Claims Convex pool rewards for a Curve pool
*/
function claim(address pool) external returns (address[] memory tokens, uint256[] memory amounts) {
(, ICvxPool cvxPool) = _findCvxPoolInfo(pool);
IERC20 crv = IERC20(cvxPool.crv());

uint256 initialCrvBalance = crv.balanceOf(address(this));
cvxPool.getReward(address(this));
uint256 finalCrvBalance = crv.balanceOf(address(this));

amounts = new uint256[](1);
amounts[0] = finalCrvBalance - initialCrvBalance;

tokens = new address[](1);
tokens[0] = address(crv);
}

/**
* @dev Deposits Curve pool tokens into Convex
* @param pool Address of the Curve pool to join Convex
* @param amount Amount of Curve pool tokens to be deposited into Convex
*/
function join(address pool, uint256 amount) external returns (uint256) {
if (amount == 0) return 0;
(uint256 poolId, ICvxPool cvxPool) = _findCvxPoolInfo(pool);

// Stake in Convex
uint256 initialCvxPoolTokenBalance = cvxPool.balanceOf(address(this));
IERC20(pool).approve(address(booster), amount);
require(booster.deposit(poolId, amount), 'CONVEX_BOOSTER_DEPOSIT_FAILED');
uint256 finalCvxPoolTokenBalance = cvxPool.balanceOf(address(this));
return finalCvxPoolTokenBalance - initialCvxPoolTokenBalance;
}

/**
* @dev Withdraws Curve pool tokens from Convex
* @param pool Address of the Curve pool to exit from Convex
* @param amount Amount of Convex tokens to be withdrawn
*/
function exit(address pool, uint256 amount) external returns (uint256) {
if (amount == 0) return 0;
(, ICvxPool cvxPool) = _findCvxPoolInfo(pool);

// Unstake from Convex
uint256 initialPoolTokenBalance = IERC20(pool).balanceOf(address(this));
require(cvxPool.withdraw(amount, true), 'CONVEX_CVX_POOL_WITHDRAW_FAILED');
uint256 finalPoolTokenBalance = IERC20(pool).balanceOf(address(this));
return finalPoolTokenBalance - initialPoolTokenBalance;
}

/**
* @dev Finds the Convex pool information associated to the given Curve pool
*/
function _findCvxPoolInfo(address pool) internal view returns (uint256 poolId, ICvxPool cvxPool) {
for (uint256 i = 0; i < booster.poolLength(); i++) {
(address lp, , address rewards, bool shutdown, ) = booster.poolInfo(i);
if (lp == pool && !shutdown) {
return (i, ICvxPool(rewards));
}
}
revert('CONVEX_CVX_POOL_NOT_FOUND');
}
}
26 changes: 26 additions & 0 deletions packages/connectors/contracts/liquidity/convex/ICvxBooster.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

pragma solidity ^0.8.0;

interface ICvxBooster {
function poolLength() external view returns (uint256);

function poolInfo(uint256 i)
external
view
returns (address lpToken, address gauge, address rewards, bool shutdown, address factory);

function deposit(uint256 pid, uint256 amount) external returns (bool);
}
25 changes: 25 additions & 0 deletions packages/connectors/contracts/liquidity/convex/ICvxPool.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

pragma solidity ^0.8.0;

import '@openzeppelin/contracts/token/ERC20/IERC20.sol';

interface ICvxPool is IERC20 {
function crv() external view returns (address);

function getReward(address account) external;

function withdraw(uint256 amount, bool claim) external returns (bool);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { advanceTime, deploy, fp, impersonate, instanceAt, MONTH, toUSDC } from '@mimic-fi/v3-helpers'
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address'
import { expect } from 'chai'
import { Contract } from 'ethers'

/* eslint-disable no-secrets/no-secrets */

const CRV = '0x11cDb42B0EB46D95f990BeDD4695A6e3fA034978'
const POOL = '0x7f90122BF0700F9E7e1F688fe926940E8839F353'
const CVX_POOL = '0x971E732B5c91A59AEa8aa5B0c763E6d648362CF8'
const BOOSTER = '0xF403C135812408BFbE8713b5A23a04b3D48AAE31'

const WHALE = '0xf403c135812408bfbe8713b5a23a04b3d48aae31'

describe('ConvexConnector - 2CRV', function () {
let whale: SignerWithAddress
let connector: Contract, pool: Contract, cvxPool: Contract, crv: Contract

const JOIN_AMOUNT = toUSDC(100)

before('impersonate whale', async () => {
whale = await impersonate(WHALE, fp(10))
})

before('deploy connector', async () => {
connector = await deploy('ConvexConnector', [BOOSTER])
crv = await instanceAt('IERC20', CRV)
pool = await instanceAt('I2CrvPool', POOL)
cvxPool = await instanceAt('ICvxPool', CVX_POOL)
})

it('deploys the connector correctly', async () => {
expect(await connector.booster()).to.be.equal(BOOSTER)
})

it('joins the connector', async () => {
await pool.connect(whale).transfer(connector.address, JOIN_AMOUNT)

const previousPoolBalance = await pool.balanceOf(connector.address)
const previousCvxPoolBalance = await cvxPool.balanceOf(connector.address)

await connector.join(POOL, JOIN_AMOUNT)

const currentPoolBalance = await pool.balanceOf(connector.address)
expect(currentPoolBalance).to.be.equal(previousPoolBalance.sub(JOIN_AMOUNT))

const currentCvxPoolBalance = await cvxPool.balanceOf(connector.address)
expect(currentCvxPoolBalance).to.be.equal(previousCvxPoolBalance.add(JOIN_AMOUNT))
})

it('accrues rewards over time', async () => {
const previousCrvBalance = await crv.balanceOf(connector.address)

await advanceTime(MONTH)
await connector.claim(POOL)

const currentCrvBalance = await crv.balanceOf(connector.address)
expect(currentCrvBalance).to.be.gt(previousCrvBalance)
})

it('exits with a 50%', async () => {
const previousPoolBalance = await pool.balanceOf(connector.address)
const previousCvxPoolBalance = await cvxPool.balanceOf(connector.address)

const amountIn = previousCvxPoolBalance.div(2)
await connector.exit(POOL, amountIn)

const currentCvxPoolBalance = await cvxPool.balanceOf(connector.address)
expect(currentCvxPoolBalance).to.be.equal(previousCvxPoolBalance.sub(amountIn))

const currentPoolBalance = await pool.balanceOf(connector.address)
expect(currentPoolBalance).to.be.equal(previousPoolBalance.add(amountIn))
})
})