Skip to content
This repository was archived by the owner on Apr 25, 2024. It is now read-only.
Merged
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@ethersproject/abi": "^5.0.12",
"@ethersproject/solidity": "^5.0.9",
"@uniswap/sdk-core": "^3.0.1",
"@uniswap/swap-router-contracts": "^1.2.1",
"@uniswap/v3-periphery": "^1.1.1",
"@uniswap/v3-staker": "1.0.0",
"tiny-invariant": "^1.1.0",
Expand Down
47 changes: 43 additions & 4 deletions src/quoter.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import JSBI from 'jsbi'
import { CurrencyAmount, Token, TradeType, WETH9 } from '@uniswap/sdk-core'
import { FeeAmount, TICK_SPACINGS } from './constants'
import { Pool } from './entities/pool'
import { SwapQuoter } from './quoter'
import { nearestUsableTick, TickMath } from './utils'
import { encodeSqrtRatioX96 } from './utils/encodeSqrtRatioX96'
import { Route, Trade } from './entities'
import { nearestUsableTick, encodeSqrtRatioX96, TickMath } from './utils'
import { Route, Trade, Pool } from './entities'

describe('SwapQuoter', () => {
const token0 = new Token(1, '0x0000000000000000000000000000000000000001', 18, 't0', 'token0')
Expand Down Expand Up @@ -115,5 +113,46 @@ describe('SwapQuoter', () => {
expect(value).toBe('0x00')
})
})
describe('single trade input using Quoter V2', () => {
it('single-hop exact output', async () => {
const trade = await Trade.fromRoute(
new Route([pool_0_1], token0, token1),
CurrencyAmount.fromRawAmount(token1, 100),
TradeType.EXACT_OUTPUT
)

const { calldata, value } = SwapQuoter.quoteCallParameters(
trade.swaps[0].route,
trade.outputAmount,
trade.tradeType,
{
useQuoterV2: true
}
)

expect(calldata).toBe(
'0xbd21704a0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000bb80000000000000000000000000000000000000000000000000000000000000000'
)
expect(value).toBe('0x00')
})
it('single-hop exact input', async () => {
const trade = await Trade.fromRoute(
new Route([pool_0_1], token0, token1),
CurrencyAmount.fromRawAmount(token0, 100),
TradeType.EXACT_INPUT
)
const { calldata, value } = SwapQuoter.quoteCallParameters(
trade.swaps[0].route,
trade.inputAmount,
trade.tradeType,
{ useQuoterV2: true }
)

expect(calldata).toBe(
'0xc6a5026a0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000bb80000000000000000000000000000000000000000000000000000000000000000'
)
expect(value).toBe('0x00')
})
})
})
})
71 changes: 46 additions & 25 deletions src/quoter.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Interface } from '@ethersproject/abi'
import { BigintIsh, Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { encodeRouteToPath } from './utils'
import { MethodParameters, toHex } from './utils/calldata'
import { encodeRouteToPath, MethodParameters, toHex } from './utils'
import IQuoter from '@uniswap/v3-periphery/artifacts/contracts/lens/Quoter.sol/Quoter.json'
import IQuoterV2 from '@uniswap/swap-router-contracts/artifacts/contracts/lens/QuoterV2.sol/QuoterV2.json'
import { Route } from './entities'
import invariant from 'tiny-invariant'
import { FeeAmount } from './constants'

/**
* Optional arguments to send to the quoter.
Expand All @@ -14,14 +15,27 @@ export interface QuoteOptions {
* The optional price limit for the trade.
*/
sqrtPriceLimitX96?: BigintIsh

/**
* The optional quoter interface to use
*/
useQuoterV2?: boolean
}

interface BaseQuoteParams {
fee: FeeAmount
sqrtPriceLimitX96: string
tokenIn: string
tokenOut: string
}

/**
* Represents the Uniswap V3 QuoterV1 contract with a method for returning the formatted
* calldata needed to call the quoter contract.
*/
export abstract class SwapQuoter {
public static INTERFACE: Interface = new Interface(IQuoter.abi)
public static V1INTERFACE: Interface = new Interface(IQuoter.abi)
public static V2INTERFACE: Interface = new Interface(IQuoterV2.abi)

/**
* Produces the on-chain method name of the appropriate function within QuoterV2,
Expand All @@ -31,6 +45,7 @@ export abstract class SwapQuoter {
* @param route The swap route, a list of pools through which a swap can occur
* @param amount The amount of the quote, either an amount in, or an amount out
* @param tradeType The trade type, either exact input or exact output
* @param options The optional params including price limit and Quoter contract switch
* @returns The formatted calldata
*/
public static quoteCallParameters<TInput extends Currency, TOutput extends Currency>(
Expand All @@ -42,34 +57,40 @@ export abstract class SwapQuoter {
const singleHop = route.pools.length === 1
const quoteAmount: string = toHex(amount.quotient)
let calldata: string
const swapInterface: Interface = options.useQuoterV2 ? this.V2INTERFACE : this.V1INTERFACE

if (singleHop) {
if (tradeType === TradeType.EXACT_INPUT) {
calldata = SwapQuoter.INTERFACE.encodeFunctionData(`quoteExactInputSingle`, [
route.tokenPath[0].address,
route.tokenPath[1].address,
route.pools[0].fee,
quoteAmount,
toHex(options?.sqrtPriceLimitX96 ?? 0)
])
} else {
calldata = SwapQuoter.INTERFACE.encodeFunctionData(`quoteExactOutputSingle`, [
route.tokenPath[0].address,
route.tokenPath[1].address,
route.pools[0].fee,
quoteAmount,
toHex(options?.sqrtPriceLimitX96 ?? 0)
])
const baseQuoteParams: BaseQuoteParams = {
tokenIn: route.tokenPath[0].address,
tokenOut: route.tokenPath[1].address,
fee: route.pools[0].fee,
sqrtPriceLimitX96: toHex(options?.sqrtPriceLimitX96 ?? 0)
}

const v2QuoteParams = {
...baseQuoteParams,
...(tradeType == TradeType.EXACT_INPUT ? { amountIn: quoteAmount } : { amount: quoteAmount })
}

const v1QuoteParams = [
baseQuoteParams.tokenIn,
baseQuoteParams.tokenOut,
baseQuoteParams.fee,
quoteAmount,
baseQuoteParams.sqrtPriceLimitX96
]

const tradeTypeFunctionName =
tradeType === TradeType.EXACT_INPUT ? 'quoteExactInputSingle' : 'quoteExactOutputSingle'
calldata = swapInterface.encodeFunctionData(
tradeTypeFunctionName,
options.useQuoterV2 ? [v2QuoteParams] : v1QuoteParams
)
} else {
invariant(options?.sqrtPriceLimitX96 === undefined, 'MULTIHOP_PRICE_LIMIT')
const path: string = encodeRouteToPath(route, tradeType === TradeType.EXACT_OUTPUT)

if (tradeType === TradeType.EXACT_INPUT) {
calldata = SwapQuoter.INTERFACE.encodeFunctionData('quoteExactInput', [path, quoteAmount])
} else {
calldata = SwapQuoter.INTERFACE.encodeFunctionData('quoteExactOutput', [path, quoteAmount])
}
const tradeTypeFunctionName = tradeType === TradeType.EXACT_INPUT ? 'quoteExactInput' : 'quoteExactOutput'
calldata = swapInterface.encodeFunctionData(tradeTypeFunctionName, [path, quoteAmount])
}
return {
calldata,
Expand Down
34 changes: 34 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1266,6 +1266,11 @@
resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-3.4.1-solc-0.7-2.tgz#371c67ebffe50f551c3146a9eec5fe6ffe862e92"
integrity sha512-tAG9LWg8+M2CMu7hIsqHPaTyG4uDzjr6mhvH96LvOpLZZj6tgzTluBt+LsCf1/QaYrlis6pITvpIaIhE+iZB+Q==

"@openzeppelin/contracts@3.4.2-solc-0.7":
version "3.4.2-solc-0.7"
resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-3.4.2-solc-0.7.tgz#38f4dbab672631034076ccdf2f3201fab1726635"
integrity sha512-W6QmqgkADuFcTLzHL8vVoNBtkwjvQRpYIAom7KiUNoLKghyx3FgH0GBjt8NRvigV1ZmMOBllvE1By1C+bi8WpA==

"@rollup/plugin-babel@^5.1.0":
version "5.2.2"
resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.2.2.tgz#e5623a01dd8e37e004ba87f2de218c611727d9b2"
Expand Down Expand Up @@ -1541,6 +1546,18 @@
tiny-invariant "^1.1.0"
toformat "^2.0.0"

"@uniswap/swap-router-contracts@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@uniswap/swap-router-contracts/-/swap-router-contracts-1.2.1.tgz#223c8b6672b7754080d95ca917763d98feb5e696"
integrity sha512-aRNiZYIOpJ0uYxujPxvQsUEuNJWLC4bvnmU40TlNej1rGWHPyDL1PmnVzebu8UpW9EGeKlvDjsNGTyo53dih9Q==
dependencies:
"@openzeppelin/contracts" "3.4.2-solc-0.7"
"@uniswap/v2-core" "1.0.1"
"@uniswap/v3-core" "1.0.0"
"@uniswap/v3-periphery" "1.4.1"
dotenv "^14.2.0"
hardhat-watcher "^2.1.1"

"@uniswap/v2-core@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@uniswap/v2-core/-/v2-core-1.0.1.tgz#af8f508bf183204779938969e2e54043e147d425"
Expand All @@ -1551,6 +1568,18 @@
resolved "https://registry.yarnpkg.com/@uniswap/v3-core/-/v3-core-1.0.0.tgz#6c24adacc4c25dceee0ba3ca142b35adbd7e359d"
integrity sha512-kSC4djMGKMHj7sLMYVnn61k9nu+lHjMIxgg9CDQT+s2QYLoA56GbSK9Oxr+qJXzzygbkrmuY6cwgP6cW2JXPFA==

"@uniswap/v3-periphery@1.4.1":
version "1.4.1"
resolved "https://registry.yarnpkg.com/@uniswap/v3-periphery/-/v3-periphery-1.4.1.tgz#b90f08b7386163c0abfd7258831caef6339c7862"
integrity sha512-Ab0ZCKOQrQMKIcpBTezTsEhWfQjItd0TtkCG8mPhoQu+wC67nPaf4hYUhM6wGHeFUmDiYY5MpEQuokB0ENvoTg==
dependencies:
"@openzeppelin/contracts" "3.4.2-solc-0.7"
"@uniswap/lib" "^4.0.1-alpha"
"@uniswap/v2-core" "1.0.1"
"@uniswap/v3-core" "1.0.0"
base64-sol "1.0.1"
hardhat-watcher "^2.1.1"

"@uniswap/v3-periphery@^1.0.1", "@uniswap/v3-periphery@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@uniswap/v3-periphery/-/v3-periphery-1.1.1.tgz#be6dfca7b29318ea0d76a7baf15d3b33c3c5e90a"
Expand Down Expand Up @@ -2529,6 +2558,11 @@ domexception@^1.0.1:
dependencies:
webidl-conversions "^4.0.2"

dotenv@^14.2.0:
version "14.3.2"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-14.3.2.tgz#7c30b3a5f777c79a3429cb2db358eef6751e8369"
integrity sha512-vwEppIphpFdvaMCaHfCEv9IgwcxMljMw2TnAQBB4VWPvzXQLTb82jwmdOKzlEVUL3gNFT4l4TPKO+Bn+sqcrVQ==

ecc-jsbn@~0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
Expand Down