Skip to content

Commit

Permalink
fix(v4-sdk): V4 planner accommodates exactOut + granular take/settles (
Browse files Browse the repository at this point in the history
  • Loading branch information
ewilz authored Sep 20, 2024
1 parent b989270 commit 984b0f1
Show file tree
Hide file tree
Showing 2 changed files with 211 additions and 70 deletions.
226 changes: 171 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,166 @@ 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',
},
])

expect(planner.actions).toEqual('0x04')
expect(planner.params[0]).toEqual(
'0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000006f05b59d3b20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000000'
)
})
})

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,
},
])

// 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('0x05')
expect(planner.params[0]).toEqual(
'0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000'
)

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

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('0x07')
expect(planner.params[0]).toEqual(
'0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000'
)
})

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

describe('addSettle', () => {
it('completes a settle without a specified amount', async () => {
const payerIsUser = true
planner.addSettle(DAI, payerIsUser)

expect(planner.actions).toEqual('0x09')
expect(planner.params[0]).toEqual(
'0x0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001'
)
})

it('completes a settle with a specified amount', async () => {
const payerIsUser = true
const amount = BigNumber.from('8')
planner.addSettle(DAI, payerIsUser, amount)

expect(planner.actions).toEqual('0x09')
expect(planner.params[0]).toEqual(
'0x0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000001'
)
})

it('completes a settle with payerIsUser as false', async () => {
const payerIsUser = false
const amount = BigNumber.from('8')
planner.addSettle(DAI, payerIsUser, amount)

expect(planner.actions).toEqual('0x09')
expect(planner.params[0]).toEqual(
'0x0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000'
)
})
})

describe('addTrade', () => {
it('completes a take without a specified amount', async () => {
const routerMustCustody = true
planner.addTake(DAI, routerMustCustody)

expect(planner.actions).toEqual('0x12')
expect(planner.params[0]).toEqual(
'0x0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000'
)
})

it('completes a take with a specified amount', async () => {
const routerMustCustody = true
const amount = BigNumber.from('8')
planner.addTake(DAI, routerMustCustody, amount)

expect(planner.actions).toEqual('0x12')
expect(planner.params[0]).toEqual(
'0x0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000008'
)
})

it('completes a take when router will not custody', async () => {
const routerMustCustody = false
planner.addTake(DAI, routerMustCustody)

expect(planner.actions).toEqual('0x12')
expect(planner.params[0]).toEqual(
'0x0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000'
)
})
})
})
55 changes: 40 additions & 15 deletions sdks/v4-sdk/src/utils/v4Planner.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import invariant from 'tiny-invariant'
import { defaultAbiCoder } from 'ethers/lib/utils'
import { BigNumber } from 'ethers'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { Trade } from '../entities/trade'
import { ADDRESS_ZERO } from '../internalConstants'
Expand Down Expand Up @@ -91,16 +93,20 @@ const ABI_DEFINITION: { [key in Actions]: string[] } = {
[Actions.SWAP_EXACT_OUT]: [SWAP_EXACT_OUT_STRUCT],

// Payments commands
[Actions.SETTLE]: ['address', 'uint256', 'bool'],
[Actions.SETTLE]: ['address', 'uint256', 'bool'], // currency, amount, payerIsUser
[Actions.SETTLE_ALL]: ['address', 'uint256'],
[Actions.TAKE]: ['address', 'address', 'uint256'],
[Actions.TAKE]: ['address', 'address', 'uint256'], // currency, receiver, amount
[Actions.TAKE_ALL]: ['address', 'uint256'],
[Actions.TAKE_PORTION]: ['address', 'address', 'uint256'],
[Actions.SETTLE_TAKE_PAIR]: ['address', 'address'],
[Actions.CLOSE_CURRENCY]: ['address'],
[Actions.SWEEP]: ['address', 'address'],
}

const FULL_DELTA_AMOUNT = 0
const MSG_SENDER = '0x0000000000000000000000000000000000000001'
const ADDRESS_THIS = '0x0000000000000000000000000000000000000002'

export class V4Planner {
actions: string
params: string[]
Expand All @@ -117,23 +123,42 @@ 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

// exactInput we sometimes perform aggregated slippage checks, but not with exactOutput
if (exactOutput) invariant(!!slippageTolerance, 'ExactOut requires slippageTolerance')
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(Actions.SETTLE_TAKE_PAIR, [currencyIn, currencyOut])
this.addAction(actionType, [
exactOutput
? {
currencyOut,
path: encodeRouteToPath(trade.route, exactOutput),
amountInMaximum: trade.maximumAmountIn(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,
},
])
}

addSettle(currency: Currency, payerIsUser: boolean, amount?: BigNumber): void {
this.addAction(Actions.SETTLE, [currencyAddress(currency), amount ?? FULL_DELTA_AMOUNT, payerIsUser])
}

addTake(currency: Currency, routerMustCustody: boolean, amount?: BigNumber): void {
const receiver = routerMustCustody ? ADDRESS_THIS : MSG_SENDER
const takeAmount = amount ?? FULL_DELTA_AMOUNT
this.addAction(Actions.TAKE, [currencyAddress(currency), receiver, takeAmount])
}

finalize(): string {
Expand Down

0 comments on commit 984b0f1

Please sign in to comment.