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

fix(v4-sdk): V4 planner accommodates exactOut + granular take/settles #97

Merged
merged 13 commits into from
Sep 20, 2024
171 changes: 116 additions & 55 deletions sdks/v4-sdk/src/utils/v4Planner.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BigNumber } from 'ethers'
import JSBI from 'jsbi'
import { CurrencyAmount, TradeType, Token, WETH9 } from '@uniswap/sdk-core'
import { CurrencyAmount, Percent, TradeType, Token, WETH9 } from '@uniswap/sdk-core'
import { encodeSqrtRatioX96, nearestUsableTick, TickMath } from '@uniswap/v3-sdk'
import { Pool } from '../entities/pool'
import { Trade } from '../entities/trade'
Expand Down Expand Up @@ -48,6 +48,17 @@ const DAI_USDC = new Pool(
0,
TICKLIST
)
const DAI_WETH = new Pool(
WETH9[1],
DAI,
FEE_AMOUNT_MEDIUM,
TICK_SPACING_TEN,
ADDRESS_ZERO,
encodeSqrtRatioX96(1, 1),
0,
0,
TICKLIST
)

describe('RouterPlanner', () => {
let planner: V4Planner
Expand All @@ -56,61 +67,111 @@ describe('RouterPlanner', () => {
planner = new V4Planner()
})

it('encodes a v4 exactInSingle swap', async () => {
planner.addAction(Actions.SWAP_EXACT_IN_SINGLE, [
{
poolKey: USDC_WETH.poolKey,
zeroForOne: true,
amountIn: ONE_ETHER_BN,
amountOutMinimum: ONE_ETHER_BN.div(2),
sqrtPriceLimitX96: 0,
hookData: '0x',
},
])
planner.addAction(Actions.SETTLE_TAKE_PAIR, [USDC.address, WETH9[1].address])

expect(planner.actions).toEqual('0x0416')
expect(planner.params[0]).toEqual(
'0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000006f05b59d3b20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000000'
)
expect(planner.params[1]).toEqual(
'0x000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'
)
describe('addAction', () => {
it('encodes a v4 exactInSingle swap', async () => {
planner.addAction(Actions.SWAP_EXACT_IN_SINGLE, [
{
poolKey: USDC_WETH.poolKey,
zeroForOne: true,
amountIn: ONE_ETHER_BN,
amountOutMinimum: ONE_ETHER_BN.div(2),
sqrtPriceLimitX96: 0,
hookData: '0x',
},
])
planner.addAction(Actions.SETTLE_TAKE_PAIR, [USDC.address, WETH9[1].address])

expect(planner.actions).toEqual('0x0416')
expect(planner.params[0]).toEqual(
'0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000006f05b59d3b20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000000'
)
expect(planner.params[1]).toEqual(
'0x000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'
)
})
})

it('completes a v4 exactIn 2 hop swap', async () => {
const route = new Route([DAI_USDC, USDC_WETH], DAI, WETH9[1])

// encode with addAction function
planner.addAction(Actions.SWAP_EXACT_IN, [
{
currencyIn: DAI.address,
path: encodeRouteToPath(route),
amountIn: ONE_ETHER_BN.toString(),
amountOutMinimum: 0,
},
])
planner.addAction(Actions.SETTLE_TAKE_PAIR, [DAI.address, WETH9[1].address])

// encode with addTrade function
const tradePlanner = new V4Planner()
const trade = await Trade.fromRoute(
route,
CurrencyAmount.fromRawAmount(DAI, ONE_ETHER.toString()),
TradeType.EXACT_INPUT
)
tradePlanner.addTrade(trade)

expect(planner.actions).toEqual('0x0516')
expect(planner.params[0]).toEqual(
'0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000'
)
expect(planner.params[1]).toEqual(
'0x0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'
)

expect(planner.actions).toEqual(tradePlanner.actions)
expect(planner.params[0]).toEqual(tradePlanner.params[0])
expect(planner.params[1]).toEqual(tradePlanner.params[1])
describe('addTrade', () => {
it('completes a v4 exactIn 2 hop swap with same results as same addAction', async () => {
const route = new Route([DAI_USDC, USDC_WETH], DAI, WETH9[1])

// encode with addAction function
planner.addAction(Actions.SWAP_EXACT_IN, [
{
currencyIn: DAI.address,
path: encodeRouteToPath(route),
amountIn: ONE_ETHER_BN.toString(),
amountOutMinimum: 0,
},
])
planner.addAction(Actions.SETTLE_TAKE_PAIR, [DAI.address, WETH9[1].address])

// encode with addTrade function
const tradePlanner = new V4Planner()
const trade = await Trade.fromRoute(
route,
CurrencyAmount.fromRawAmount(DAI, ONE_ETHER.toString()),
TradeType.EXACT_INPUT
)
tradePlanner.addTrade(trade)

expect(planner.actions).toEqual('0x0516')
expect(planner.params[0]).toEqual(
'0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000'
)
expect(planner.params[1]).toEqual(
'0x0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'
)

expect(planner.actions).toEqual(tradePlanner.actions)
expect(planner.params[0]).toEqual(tradePlanner.params[0])
expect(planner.params[1]).toEqual(tradePlanner.params[1])
})

it('completes a v4 exactOut 2 hop swap', async () => {
const route = new Route([DAI_USDC, USDC_WETH], DAI, WETH9[1])
const slippageTolerance = new Percent('5')
const trade = await Trade.fromRoute(
route,
CurrencyAmount.fromRawAmount(WETH9[1], ONE_ETHER.toString()),
TradeType.EXACT_OUTPUT
)
planner.addTrade(trade, slippageTolerance)

expect(planner.actions).toEqual('0x0716')
expect(planner.params[0]).toEqual(
'0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000'
)
expect(planner.params[1]).toEqual(
'0x0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'
)
})

it('throws an error if adding exactOut trade without slippage tolerance', async () => {
const route = new Route([DAI_USDC, USDC_WETH], DAI, WETH9[1])
const trade = await Trade.fromRoute(
route,
CurrencyAmount.fromRawAmount(WETH9[1], ONE_ETHER.toString()),
TradeType.EXACT_OUTPUT
)
expect(() => planner.addTrade(trade)).toThrow('ExactOut requires slippageTolerance')
})

it('throws an error if adding exactOut trade without slippage tolerance', async () => {
const slippageTolerance = new Percent('5')
const amount = CurrencyAmount.fromRawAmount(WETH9[1], ONE_ETHER.toString())
const route1 = new Route([DAI_USDC, USDC_WETH], DAI, WETH9[1])
const route2 = new Route([DAI_WETH], DAI, WETH9[1])
const trade = await Trade.fromRoutes(
[
{ route: route1, amount },
{ route: route2, amount },
],
TradeType.EXACT_OUTPUT
)
expect(() => planner.addTrade(trade, slippageTolerance)).toThrow(
'Only accepts Trades with 1 swap (must break swaps into individual trades)'
)
})
})
})
33 changes: 22 additions & 11 deletions sdks/v4-sdk/src/utils/v4Planner.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import invariant from 'tiny-invariant'
import { defaultAbiCoder } from 'ethers/lib/utils'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { Trade } from '../entities/trade'
Expand Down Expand Up @@ -117,21 +118,31 @@ export class V4Planner {
}

addTrade(trade: Trade<Currency, Currency, TradeType>, slippageTolerance?: Percent): void {
const actionType = trade.tradeType === TradeType.EXACT_INPUT ? Actions.SWAP_EXACT_IN : Actions.SWAP_EXACT_OUT
const exactOutput = trade.tradeType === TradeType.EXACT_OUTPUT

if (exactOutput) invariant(!!slippageTolerance, 'ExactOut requires slippageTolerance')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how come we only check this for exactOuput? just curious

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good question I can leave a comment. on exactInput sometimes we do agg slippage checks so we don't need to know slippage, but we enver do that on exactOutput

invariant(trade.swaps.length === 1, 'Only accepts Trades with 1 swap (must break swaps into individual trades)')

const actionType = exactOutput ? Actions.SWAP_EXACT_OUT : Actions.SWAP_EXACT_IN

const currencyIn = currencyAddress(trade.inputAmount.currency)
const currencyOut = currencyAddress(trade.outputAmount.currency)

for (let swap of trade.swaps) {
this.addAction(actionType, [
{
currencyIn,
path: encodeRouteToPath(swap.route),
amountIn: swap.inputAmount.quotient.toString(),
amountOutMinimum: slippageTolerance ? trade.minimumAmountOut(slippageTolerance).quotient.toString() : 0,
},
])
}
this.addAction(actionType, [
exactOutput
? {
currencyOut,
path: encodeRouteToPath(trade.route, exactOutput),
amountInMaximum: trade.minimumAmountOut(slippageTolerance ?? new Percent(0)).quotient.toString(),
amountOut: trade.inputAmount.quotient.toString(),
}
: {
currencyIn,
path: encodeRouteToPath(trade.route, exactOutput),
amountIn: trade.inputAmount.quotient.toString(),
amountOutMinimum: slippageTolerance ? trade.minimumAmountOut(slippageTolerance).quotient.toString() : 0,
},
])

this.addAction(Actions.SETTLE_TAKE_PAIR, [currencyIn, currencyOut])
}
Expand Down
Loading