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)
+ })
+})