diff --git a/packages/connectors/contracts/liquidity/convex/ConvexConnector.sol b/packages/connectors/contracts/liquidity/convex/ConvexConnector.sol new file mode 100644 index 00000000..91f2b008 --- /dev/null +++ b/packages/connectors/contracts/liquidity/convex/ConvexConnector.sol @@ -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 . + +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'); + } +} diff --git a/packages/connectors/contracts/liquidity/convex/ICvxBooster.sol b/packages/connectors/contracts/liquidity/convex/ICvxBooster.sol new file mode 100644 index 00000000..d5b7cd17 --- /dev/null +++ b/packages/connectors/contracts/liquidity/convex/ICvxBooster.sol @@ -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 . + +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); +} diff --git a/packages/connectors/contracts/liquidity/convex/ICvxPool.sol b/packages/connectors/contracts/liquidity/convex/ICvxPool.sol new file mode 100644 index 00000000..4f98dd4f --- /dev/null +++ b/packages/connectors/contracts/liquidity/convex/ICvxPool.sol @@ -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 . + +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); +} diff --git a/packages/connectors/test/liquidity/convex/ConvexConnector.arbitrum.ts b/packages/connectors/test/liquidity/convex/ConvexConnector.arbitrum.ts new file mode 100644 index 00000000..4b985f6f --- /dev/null +++ b/packages/connectors/test/liquidity/convex/ConvexConnector.arbitrum.ts @@ -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)) + }) +})