Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Connectors: Implement Uniswap v3 swap connector #18

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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