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

Comment on lines +106 to +109
Copy link
Member

Choose a reason for hiding this comment

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

ooh nice I made an ActionsConstant utils file, I can put these in there later when my PR gets merged

Copy link
Member Author

Choose a reason for hiding this comment

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

yah feel free to move around. this file is def pretty cray

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')
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(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(),
Copy link
Member

Choose a reason for hiding this comment

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

it looks like on 128 we check that slippageTolerance is required? so can we just pass it in directly here?

Copy link
Member Author

Choose a reason for hiding this comment

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

typescript gets mad

Copy link
Member

Choose a reason for hiding this comment

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

hahah ok got it

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 {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: should we add comment headers?

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
Loading