Skip to content

Commit

Permalink
connectors: implement uniswap v3 swap connector
Browse files Browse the repository at this point in the history
  • Loading branch information
facuspagnuolo committed Jun 26, 2023
1 parent 75ca506 commit 81949d0
Show file tree
Hide file tree
Showing 13 changed files with 573 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: GPL-2.0-or-later

pragma solidity >=0.5.0;

/// @title The interface for the Uniswap V3 Factory
/// @notice The Uniswap V3 Factory facilitates creation of Uniswap V3 pools and control over the protocol fees
interface IUniswapV3Factory {
/// @notice Returns the pool address for a given pair of tokens and a fee, or address 0 if it does not exist
/// @dev tokenA and tokenB may be passed in either token0/token1 or token1/token0 order
/// @param tokenA The contract address of either token0 or token1
/// @param tokenB The contract address of the other token
/// @param fee The fee collected upon every swap in the pool, denominated in hundredths of a bip
/// @return pool The pool address
function getPool(address tokenA, address tokenB, uint24 fee) external view returns (address pool);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: GPL-2.0-or-later

pragma solidity >=0.5.0;

/// @title Immutable state
/// @notice Functions that return immutable state of the router
interface IUniswapV3PeripheryImmutableState {
/// @return Returns the address of the Uniswap V3 factory
function factory() external view returns (address);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// SPDX-License-Identifier: GPL-2.0-or-later

pragma solidity >=0.7.5;

/// @title Router token swapping functionality
/// @notice Functions for swapping tokens via Uniswap V3
interface IUniswapV3SwapRouter {
struct ExactInputSingleParams {
address tokenIn;
address tokenOut;
uint24 fee;
address recipient;
uint256 deadline;
uint256 amountIn;
uint256 amountOutMinimum;
uint160 sqrtPriceLimitX96;
}

/// @notice Swaps `amountIn` of one token for as much as possible of another token
/// @param params The parameters necessary for the swap, encoded as `ExactInputSingleParams` in calldata
/// @return amountOut The amount of the received token
function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut);

struct ExactInputParams {
bytes path;
address recipient;
uint256 deadline;
uint256 amountIn;
uint256 amountOutMinimum;
}

/// @notice Swaps `amountIn` of one token for as much as possible of another along the specified path
/// @param params The parameters necessary for the multi-hop swap, encoded as `ExactInputParams` in calldata
/// @return amountOut The amount of the received token
function exactInput(ExactInputParams calldata params) external payable returns (uint256 amountOut);
}
178 changes: 178 additions & 0 deletions packages/connectors/contracts/swap/uniswap-v3/UniswapV3Connector.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// 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 '@mimic-fi/v3-helpers/contracts/math/UncheckedMath.sol';
import '@mimic-fi/v3-helpers/contracts/utils/Arrays.sol';
import '@mimic-fi/v3-helpers/contracts/utils/BytesHelpers.sol';
import '@mimic-fi/v3-helpers/contracts/utils/ERC20Helpers.sol';

import './IUniswapV3Factory.sol';
import './IUniswapV3SwapRouter.sol';
import './IUniswapV3PeripheryImmutableState.sol';

/**
* @title UniswapV3Connector
* @dev Interfaces with Uniswap V3 to swap tokens
*/
contract UniswapV3Connector {
using BytesHelpers for bytes;
using UncheckedMath for uint256;

// Reference to UniswapV3 router
IUniswapV3SwapRouter public immutable uniswapV3Router;

/**
* @dev Initializes the UniswapV3Connector contract
* @param _uniswapV3Router Uniswap V3 router reference
*/
constructor(address _uniswapV3Router) {
uniswapV3Router = IUniswapV3SwapRouter(_uniswapV3Router);
}

/**
* @dev Executes a token swap in Uniswap V3
* @param tokenIn Token being sent
* @param tokenOut Token being received
* @param amountIn Amount of tokenIn being swapped
* @param minAmountOut Minimum amount of tokenOut willing to receive
* @param fee Fee to be used
* @param hopTokens Optional list of hop-tokens between tokenIn and tokenOut, only used for multi-hops
* @param hopFees Optional list of hop-fees between tokenIn and tokenOut, only used for multi-hops
*/
function execute(
address tokenIn,
address tokenOut,
uint256 amountIn,
uint256 minAmountOut,
uint24 fee,
address[] memory hopTokens,
uint24[] memory hopFees
) external returns (uint256 amountOut) {
require(tokenIn != tokenOut, 'UNI_V3_SWAP_SAME_TOKEN');
require(hopTokens.length == hopFees.length, 'UNI_V3_BAD_HOP_TOKENS_FEES_LEN');

uint256 preBalanceIn = IERC20(tokenIn).balanceOf(address(this));
uint256 preBalanceOut = IERC20(tokenOut).balanceOf(address(this));

ERC20Helpers.approve(tokenIn, address(uniswapV3Router), amountIn);
hopTokens.length == 0
? _singleSwap(tokenIn, tokenOut, amountIn, minAmountOut, fee)
: _batchSwap(tokenIn, tokenOut, amountIn, minAmountOut, fee, hopTokens, hopFees);

uint256 postBalanceIn = IERC20(tokenIn).balanceOf(address(this));
require(postBalanceIn >= preBalanceIn - amountIn, 'UNI_V3_BAD_TOKEN_IN_BALANCE');

uint256 postBalanceOut = IERC20(tokenOut).balanceOf(address(this));
amountOut = postBalanceOut - preBalanceOut;
require(amountOut >= minAmountOut, 'UNI_V3_MIN_AMOUNT_OUT');
}

/**
* @dev Swap two tokens through UniswapV3 using a single hop
* @param tokenIn Token being sent
* @param tokenOut Token being received
* @param amountIn Amount of tokenIn being swapped
* @param minAmountOut Minimum amount of tokenOut willing to receive
* @param fee Fee to be used
*/
function _singleSwap(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, uint24 fee)
internal
returns (uint256 amountOut)
{
_validatePool(_uniswapV3Factory(), tokenIn, tokenOut, fee);

IUniswapV3SwapRouter.ExactInputSingleParams memory input;
input.tokenIn = tokenIn;
input.tokenOut = tokenOut;
input.fee = fee;
input.recipient = address(this);
input.deadline = block.timestamp;
input.amountIn = amountIn;
input.amountOutMinimum = minAmountOut;
input.sqrtPriceLimitX96 = 0;
return uniswapV3Router.exactInputSingle(input);
}

/**
* @dev Swap two tokens through UniswapV3 using a multi hop
* @param tokenIn Token being sent
* @param tokenOut Token being received
* @param amountIn Amount of the first token in the path to be swapped
* @param minAmountOut Minimum amount of the last token in the path willing to receive
* @param fee Fee to be used
* @param hopTokens List of hop-tokens between tokenIn and tokenOut
* @param hopFees List of hop-fees between tokenIn and tokenOut
*/
function _batchSwap(
address tokenIn,
address tokenOut,
uint256 amountIn,
uint256 minAmountOut,
uint24 fee,
address[] memory hopTokens,
uint24[] memory hopFees
) internal returns (uint256 amountOut) {
address factory = _uniswapV3Factory();
address[] memory tokens = Arrays.from(tokenIn, hopTokens, tokenOut);
uint24[] memory fees = Arrays.from(fee, hopFees);

// No need for checked math since we are using it to compute indexes manually, always within boundaries
for (uint256 i = 0; i < fees.length; i = i.uncheckedAdd(1)) {
_validatePool(factory, tokens[i], tokens[i.uncheckedAdd(1)], fees[i]);
}

IUniswapV3SwapRouter.ExactInputParams memory input;
input.path = _encodePoolPath(tokens, fees);
input.amountIn = amountIn;
input.amountOutMinimum = minAmountOut;
input.recipient = address(this);
input.deadline = block.timestamp;
return uniswapV3Router.exactInput(input);
}

/**
* @dev Tells the Uniswap V3 factory contract address
* @return Address of the Uniswap V3 factory contract
*/
function _uniswapV3Factory() internal view returns (address) {
return IUniswapV3PeripheryImmutableState(address(uniswapV3Router)).factory();
}

/**
* @dev Validates that there is a pool created for tokenA and tokenB with a requested fee
* @param factory UniswapV3 factory to check against
* @param tokenA One of the tokens in the pool
* @param tokenB The other token in the pool
* @param fee Fee used by the pool
*/
function _validatePool(address factory, address tokenA, address tokenB, uint24 fee) internal view {
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
require(IUniswapV3Factory(factory).getPool(token0, token1, fee) != address(0), 'UNI_V3_INVALID_POOL_FEE');
}

/**
* @dev Encodes a path of tokens with their corresponding fees
* @param tokens List of tokens to be encoded
* @param fees List of fees to use for each token pair
*/
function _encodePoolPath(address[] memory tokens, uint24[] memory fees) internal pure returns (bytes memory path) {
path = new bytes(0);
for (uint256 i = 0; i < fees.length; i = i.uncheckedAdd(1)) path = path.concat(tokens[i]).concat(fees[i]);
path = path.concat(tokens[fees.length]);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { deployProxy, fp, impersonate, instanceAt, pct, toUSDC, toWBTC, ZERO_ADDRESS } from '@mimic-fi/v3-helpers'
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'
import { expect } from 'chai'
import { BigNumber, Contract } from 'ethers'

export function itBehavesLikeUniswapV3Connector(
USDC: string,
WETH: string,
WBTC: string,
WHALE: string,
SLIPPAGE: number,
WETH_USDC_FEE: number,
WETH_WBTC_FEE: number,
CHAINLINK_USDC_ETH: string,
CHAINLINK_WBTC_ETH: string
): void {
let weth: Contract, usdc: Contract, wbtc: Contract, whale: SignerWithAddress, priceOracle: Contract

before('load tokens and accounts', async function () {
weth = await instanceAt('IERC20Metadata', WETH)
wbtc = await instanceAt('IERC20Metadata', WBTC)
usdc = await instanceAt('IERC20Metadata', USDC)
whale = await impersonate(WHALE, fp(100))
})

before('create price oracle', async function () {
priceOracle = await deployProxy(
'@mimic-fi/v3-price-oracle/artifacts/contracts/PriceOracle.sol/PriceOracle',
[],
[
ZERO_ADDRESS,
ZERO_ADDRESS,
WETH,
[
{ base: USDC, quote: WETH, feed: CHAINLINK_USDC_ETH },
{ base: WBTC, quote: WETH, feed: CHAINLINK_WBTC_ETH },
],
]
)
})

const getExpectedMinAmountOut = async (
tokenIn: string,
tokenOut: string,
amountIn: BigNumber,
slippage: number
): Promise<BigNumber> => {
const price = await priceOracle['getPrice(address,address)'](tokenIn, tokenOut)
const expectedAmountOut = price.mul(amountIn).div(fp(1))
return expectedAmountOut.sub(pct(expectedAmountOut, slippage))
}

context('single swap', () => {
context('USDC-WETH', () => {
const amountIn = toUSDC(10e3)

it('swaps correctly', async function () {
const previousBalance = await weth.balanceOf(this.connector.address)
await usdc.connect(whale).transfer(this.connector.address, amountIn)

await this.connector.connect(whale).execute(USDC, WETH, amountIn, 0, WETH_USDC_FEE, [], [])

const currentBalance = await weth.balanceOf(this.connector.address)
const expectedMinAmountOut = await getExpectedMinAmountOut(USDC, WETH, amountIn, SLIPPAGE)
expect(currentBalance.sub(previousBalance)).to.be.at.least(expectedMinAmountOut)
})
})

context('WETH-USDC', () => {
const amountIn = fp(1)

it('swaps correctly', async function () {
const previousBalance = await usdc.balanceOf(this.connector.address)
await weth.connect(whale).transfer(this.connector.address, amountIn)

await this.connector.connect(whale).execute(WETH, USDC, amountIn, 0, WETH_USDC_FEE, [], [])

const currentBalance = await usdc.balanceOf(this.connector.address)
const expectedMinAmountOut = await getExpectedMinAmountOut(WETH, USDC, amountIn, SLIPPAGE)
expect(currentBalance.sub(previousBalance)).to.be.at.least(expectedMinAmountOut)
})
})
})

context('batch swap', () => {
context('USDC-WBTC', () => {
const amountIn = toUSDC(10e3)

it('swaps correctly', async function () {
const previousBalance = await wbtc.balanceOf(this.connector.address)
await usdc.connect(whale).transfer(this.connector.address, amountIn)

await this.connector.connect(whale).execute(USDC, WBTC, amountIn, 0, WETH_USDC_FEE, [WETH], [WETH_WBTC_FEE])

const currentBalance = await wbtc.balanceOf(this.connector.address)
const expectedMinAmountOut = await getExpectedMinAmountOut(USDC, WBTC, amountIn, SLIPPAGE)
expect(currentBalance.sub(previousBalance)).to.be.at.least(expectedMinAmountOut)
})
})

context('WBTC-USDC', () => {
const amountIn = toWBTC(1)

it('swaps correctly', async function () {
const previousBalance = await usdc.balanceOf(this.connector.address)
await wbtc.connect(whale).transfer(this.connector.address, amountIn)

await this.connector.connect(whale).execute(WBTC, USDC, amountIn, 0, WETH_USDC_FEE, [WETH], [WETH_WBTC_FEE])

const currentBalance = await usdc.balanceOf(this.connector.address)
const expectedMinAmountOut = await getExpectedMinAmountOut(WBTC, USDC, amountIn, SLIPPAGE)
expect(currentBalance.sub(previousBalance)).to.be.at.least(expectedMinAmountOut)
})
})
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { deploy } from '@mimic-fi/v3-helpers'

import { itBehavesLikeUniswapV3Connector } from './UniswapV3Connector.behavior'

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

const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'
const WETH = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'
const WBTC = '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599'
const WHALE = '0xf584f8728b874a6a5c7a8d4d387c9aae9172d621'

const UNISWAP_V3_ROUTER = '0xE592427A0AEce92De3Edee1F18E0157C05861564'

const CHAINLINK_USDC_ETH = '0x986b5E1e1755e3C2440e960477f25201B0a8bbD4'
const CHAINLINK_WBTC_ETH = '0xdeb288F737066589598e9214E782fa5A8eD689e8'

describe('UniswapV3Connector', () => {
const SLIPPAGE = 0.02
const WETH_USDC_FEE = 3000
const WETH_WBTC_FEE = 3000

before('create uniswap v3 connector', async function () {
this.connector = await deploy('UniswapV3Connector', [UNISWAP_V3_ROUTER])
})

itBehavesLikeUniswapV3Connector(
USDC,
WETH,
WBTC,
WHALE,
SLIPPAGE,
WETH_USDC_FEE,
WETH_WBTC_FEE,
CHAINLINK_USDC_ETH,
CHAINLINK_WBTC_ETH
)
})
Loading

0 comments on commit 81949d0

Please sign in to comment.