Skip to content

Commit

Permalink
connectors: implement curve 2crv liquidity connector
Browse files Browse the repository at this point in the history
  • Loading branch information
facuspagnuolo committed Jun 26, 2023
1 parent 75ca506 commit 73ff016
Show file tree
Hide file tree
Showing 3 changed files with 201 additions and 0 deletions.
104 changes: 104 additions & 0 deletions packages/connectors/contracts/liquidity/curve/Curve2CrvConnector.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 './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');
}
}
31 changes: 31 additions & 0 deletions packages/connectors/contracts/liquidity/curve/I2CrvPool.sol
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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);
}
Original file line number Diff line number Diff line change
@@ -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)
})
})

0 comments on commit 73ff016

Please sign in to comment.