diff --git a/packages/connectors/contracts/liquidity/curve/Curve2CrvConnector.sol b/packages/connectors/contracts/liquidity/curve/Curve2CrvConnector.sol new file mode 100644 index 00000000..64cad225 --- /dev/null +++ b/packages/connectors/contracts/liquidity/curve/Curve2CrvConnector.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 './I2CrvPool.sol'; + +/** + * @title Curve2CrvConnector + */ +contract Curve2CrvConnector { + using FixedPoint for uint256; + + // 2CRV pool address + I2CrvPool public immutable pool; + + /** + * @dev Creates a new Curve 2CRV connector + */ + constructor(I2CrvPool _pool) { + pool = _pool; + } + + /** + * @dev Adds liquidity to the 2CRV pool + * @param tokenIn Address of the token to join the 2CRV pool + * @param amountIn Amount of tokens to join the 2CRV pool + * @param slippage Slippage value to be used to compute the desired min amount out of pool tokens + */ + function join(address tokenIn, uint256 amountIn, uint256 slippage) external returns (uint256) { + if (amountIn == 0) return 0; + require(slippage <= FixedPoint.ONE, '2CRV_SLIPPAGE_ABOVE_ONE'); + (uint256 tokenIndex, uint256 tokenScale) = _findTokenInfo(tokenIn); + + // Compute min amount out + uint256 expectedAmountOut = (amountIn * tokenScale).divUp(pool.get_virtual_price()); + uint256 minAmountOut = expectedAmountOut.mulUp(FixedPoint.ONE - slippage); + + // Join pool + uint256 initialPoolTokenBalance = pool.balanceOf(address(this)); + IERC20(tokenIn).approve(address(pool), amountIn); + uint256[2] memory amounts; + amounts[tokenIndex] = amountIn; + pool.add_liquidity(amounts, minAmountOut); + uint256 finalPoolTokenBalance = pool.balanceOf(address(this)); + return finalPoolTokenBalance - initialPoolTokenBalance; + } + + /** + * @dev Removes liquidity from 2CRV pool + * @param amountIn Amount of pool tokens to exit from the 2CRV pool + * @param tokenOut Address of the token to exit the pool + * @param slippage Slippage value to be used to compute the desired min amount out of tokens + */ + function exit(uint256 amountIn, address tokenOut, uint256 slippage) external returns (uint256 amountOut) { + if (amountIn == 0) return 0; + require(slippage <= FixedPoint.ONE, '2CRV_INVALID_SLIPPAGE'); + (uint256 tokenIndex, uint256 tokenScale) = _findTokenInfo(tokenOut); + + // Compute min amount out + uint256 expectedAmountOut = amountIn.mulUp(pool.get_virtual_price()) / tokenScale; + uint256 minAmountOut = expectedAmountOut.mulUp(FixedPoint.ONE - slippage); + + // Exit pool + uint256 initialTokenOutBalance = IERC20(tokenOut).balanceOf(address(this)); + pool.remove_liquidity_one_coin(amountIn, int128(int256(tokenIndex)), minAmountOut); + uint256 finalTokenOutBalance = IERC20(tokenOut).balanceOf(address(this)); + return finalTokenOutBalance - initialTokenOutBalance; + } + + /** + * @dev Finds the index and scale factor of the entry token in the 2CRV pool + */ + function _findTokenInfo(address token) internal view returns (uint256 index, uint256 scale) { + for (uint256 i = 0; true; i++) { + try pool.coins(i) returns (address coin) { + if (token == coin) { + uint256 decimals = IERC20Metadata(token).decimals(); + require(decimals <= 18, '2CRV_TOKEN_ABOVE_18_DECIMALS'); + return (i, 10**(18 - decimals)); + } + } catch { + revert('2CRV_TOKEN_NOT_FOUND'); + } + } + revert('2CRV_TOKEN_NOT_FOUND'); + } +} diff --git a/packages/connectors/contracts/liquidity/curve/I2CrvPool.sol b/packages/connectors/contracts/liquidity/curve/I2CrvPool.sol new file mode 100644 index 00000000..99576ef4 --- /dev/null +++ b/packages/connectors/contracts/liquidity/curve/I2CrvPool.sol @@ -0,0 +1,31 @@ +// 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'; + +// solhint-disable func-name-mixedcase + +interface I2CrvPool is IERC20 { + function get_virtual_price() external view returns (uint256); + + function coins(uint256 index) external view returns (address); + + function exchange(int128 i, int128 j, uint256 dx, uint256 minDy) external; + + function add_liquidity(uint256[2] memory amountsIn, uint256 minAmountOut) external returns (uint256); + + function remove_liquidity_one_coin(uint256 amountIn, int128 index, uint256 minAmountOut) external returns (uint256); +} diff --git a/packages/connectors/test/liquidity/curve/Curve2CrvConnector.arbitrum.ts b/packages/connectors/test/liquidity/curve/Curve2CrvConnector.arbitrum.ts new file mode 100644 index 00000000..b4d9f372 --- /dev/null +++ b/packages/connectors/test/liquidity/curve/Curve2CrvConnector.arbitrum.ts @@ -0,0 +1,66 @@ +import { assertAlmostEqual, deploy, fp, impersonate, instanceAt, 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 USDC = '0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8' +const POOL = '0x7f90122BF0700F9E7e1F688fe926940E8839F353' + +const WHALE = '0x62383739d68dd0f844103db8dfb05a7eded5bbe6' + +describe('Curve2CrvConnector - USDC', function () { + let whale: SignerWithAddress + let connector: Contract, pool: Contract, usdc: Contract + + const SLIPPAGE = fp(0.001) + const JOIN_AMOUNT = toUSDC(100) + + before('impersonate whale', async () => { + whale = await impersonate(WHALE, fp(10)) + }) + + before('deploy connector', async () => { + connector = await deploy('Curve2CrvConnector', [POOL]) + usdc = await instanceAt('IERC20', USDC) + pool = await instanceAt('I2CrvPool', POOL) + }) + + it('deploys the connector correctly', async () => { + expect(await connector.pool()).to.be.equal(POOL) + }) + + it('joins curve', async () => { + await usdc.connect(whale).transfer(connector.address, JOIN_AMOUNT) + + const previousUsdcBalance = await usdc.balanceOf(connector.address) + const previousPoolBalance = await pool.balanceOf(connector.address) + + await connector.join(USDC, JOIN_AMOUNT, SLIPPAGE) + + const currentUsdcBalance = await usdc.balanceOf(connector.address) + expect(currentUsdcBalance).to.be.equal(previousUsdcBalance.sub(JOIN_AMOUNT)) + + const poolTokenPrice = await pool.get_virtual_price() + const currentPoolBalance = await pool.balanceOf(connector.address) + const expectedPoolAmount = JOIN_AMOUNT.mul(1e12).mul(fp(1)).div(poolTokenPrice) + assertAlmostEqual(expectedPoolAmount, currentPoolBalance.sub(previousPoolBalance), 0.0005) + }) + + it('exits with a 50%', async () => { + const previousUsdcBalance = await usdc.balanceOf(connector.address) + const previousPoolBalance = await pool.balanceOf(connector.address) + + const amountIn = previousPoolBalance.div(2) + await connector.exit(amountIn, USDC, SLIPPAGE) + + const currentPoolBalance = await pool.balanceOf(connector.address) + expect(currentPoolBalance).to.be.equal(previousPoolBalance.sub(amountIn)) + + const poolTokenPrice = await pool.get_virtual_price() + const currentUsdcBalance = await usdc.balanceOf(connector.address) + const expectedUsdcBalance = amountIn.mul(poolTokenPrice).div(fp(1)).div(1e12) + assertAlmostEqual(expectedUsdcBalance, currentUsdcBalance.sub(previousUsdcBalance), 0.0005) + }) +})