diff --git a/sdks/universal-router-sdk/package.json b/sdks/universal-router-sdk/package.json index 2c8e12bd..236c94d9 100644 --- a/sdks/universal-router-sdk/package.json +++ b/sdks/universal-router-sdk/package.json @@ -38,7 +38,7 @@ "@uniswap/v2-sdk": "^4.6.0", "@uniswap/v3-core": "1.0.0", "@uniswap/v3-sdk": "^3.17.0", - "@uniswap/v4-sdk": "^1.6.3", + "@uniswap/v4-sdk": "^1.10.0", "bignumber.js": "^9.0.2", "ethers": "^5.7.0" }, diff --git a/sdks/universal-router-sdk/src/utils/commandParser.ts b/sdks/universal-router-sdk/src/utils/commandParser.ts index 121df4d6..df0f2e77 100644 --- a/sdks/universal-router-sdk/src/utils/commandParser.ts +++ b/sdks/universal-router-sdk/src/utils/commandParser.ts @@ -1,7 +1,8 @@ import { ethers } from 'ethers' import { abi } from '@uniswap/universal-router/artifacts/contracts/UniversalRouter.sol/UniversalRouter.json' import { Interface } from '@ethersproject/abi' -import { CommandType, COMMAND_ABI_DEFINITION, Subparser } from '../utils/routerCommands' +import { V4BaseActionsParser, V4RouterAction } from '@uniswap/v4-sdk' +import { CommandType, COMMAND_DEFINITION, Subparser, Parser } from '../utils/routerCommands' export type Param = { readonly name: string @@ -36,35 +37,55 @@ export abstract class CommandParser { return { commands: commandTypes.map((commandType: CommandType, i: number) => { - const abiDef = COMMAND_ABI_DEFINITION[commandType] - const rawParams = ethers.utils.defaultAbiCoder.decode( - abiDef.map((command) => command.type), - inputs[i] - ) - const params = rawParams.map((param: any, j: number) => { - switch (abiDef[j].subparser) { - case Subparser.V3PathExactIn: - return { - name: abiDef[j].name, - value: parseV3PathExactIn(param), - } - case Subparser.V3PathExactOut: - return { - name: abiDef[j].name, - value: parseV3PathExactOut(param), - } - default: - return { - name: abiDef[j].name, - value: param, - } + const commandDef = COMMAND_DEFINITION[commandType] + + if (commandDef.parser === Parser.V4Actions) { + const { actions } = V4BaseActionsParser.parseCalldata(inputs[i]) + return { + commandName: CommandType[commandType], + commandType, + params: v4RouterCallToParams(actions), } - }) - - return { - commandName: CommandType[commandType], - commandType, - params, + } else if (commandDef.parser === Parser.Abi) { + const abiDef = commandDef.params + const rawParams = ethers.utils.defaultAbiCoder.decode( + abiDef.map((command) => command.type), + inputs[i] + ) + + const params = rawParams.map((param: any, j: number) => { + switch (abiDef[j].subparser) { + case Subparser.V3PathExactIn: + return { + name: abiDef[j].name, + value: parseV3PathExactIn(param), + } + case Subparser.V3PathExactOut: + return { + name: abiDef[j].name, + value: parseV3PathExactOut(param), + } + default: + return { + name: abiDef[j].name, + value: param, + } + } + }) + return { + commandName: CommandType[commandType], + commandType, + params, + } + } else if (commandDef.parser === Parser.V3Actions) { + // TODO: implement better parsing here + return { + commandName: CommandType[commandType], + commandType, + params: inputs, + } + } else { + throw new Error(`Unsupported parser: ${commandDef}`) } }), } @@ -127,3 +148,17 @@ export function parseV3PathExactOut(path: string): readonly V3PathItem[] { return res } + +function v4RouterCallToParams(actions: readonly V4RouterAction[]): readonly Param[] { + return actions.map((action) => { + return { + name: action.actionName, + value: action.params.map((param) => { + return { + name: param.name, + value: param.value, + } + }), + } + }) +} diff --git a/sdks/universal-router-sdk/src/utils/routerCommands.ts b/sdks/universal-router-sdk/src/utils/routerCommands.ts index 2e7148b6..8888c1bf 100644 --- a/sdks/universal-router-sdk/src/utils/routerCommands.ts +++ b/sdks/universal-router-sdk/src/utils/routerCommands.ts @@ -35,12 +35,30 @@ export enum Subparser { V3PathExactOut, } +export enum Parser { + Abi, + V4Actions, + V3Actions, +} + export type ParamType = { readonly name: string readonly type: string readonly subparser?: Subparser } +export type CommandDefinition = + | { + parser: Parser.Abi + params: ParamType[] + } + | { + parser: Parser.V4Actions + } + | { + parser: Parser.V3Actions + } + const ALLOW_REVERT_FLAG = 0x80 const REVERTIBLE_COMMANDS = new Set([CommandType.EXECUTE_SUB_PLAN]) @@ -53,99 +71,144 @@ const PERMIT_BATCH_STRUCT = const PERMIT2_TRANSFER_FROM_STRUCT = '(address from,address to,uint160 amount,address token)' const PERMIT2_TRANSFER_FROM_BATCH_STRUCT = PERMIT2_TRANSFER_FROM_STRUCT + '[]' -export const COMMAND_ABI_DEFINITION: { [key in CommandType]: readonly ParamType[] } = { +export const COMMAND_DEFINITION: { [key in CommandType]: CommandDefinition } = { // Batch Reverts - [CommandType.EXECUTE_SUB_PLAN]: [ - { name: 'commands', type: 'bytes' }, - { name: 'inputs', type: 'bytes[]' }, - ], + [CommandType.EXECUTE_SUB_PLAN]: { + parser: Parser.Abi, + params: [ + { name: 'commands', type: 'bytes' }, + { name: 'inputs', type: 'bytes[]' }, + ], + }, // Permit2 Actions - [CommandType.PERMIT2_PERMIT]: [ - { name: 'permit', type: PERMIT_STRUCT }, - { name: 'signature', type: 'bytes' }, - ], - [CommandType.PERMIT2_PERMIT_BATCH]: [ - { name: 'permit', type: PERMIT_BATCH_STRUCT }, - { name: 'signature', type: 'bytes' }, - ], - [CommandType.PERMIT2_TRANSFER_FROM]: [ - { name: 'token', type: 'address' }, - { name: 'recipient', type: 'address' }, - { name: 'amount', type: 'uint160' }, - ], - [CommandType.PERMIT2_TRANSFER_FROM_BATCH]: [ - { - name: 'transferFrom', - type: PERMIT2_TRANSFER_FROM_BATCH_STRUCT, - }, - ], + [CommandType.PERMIT2_PERMIT]: { + parser: Parser.Abi, + params: [ + { name: 'permit', type: PERMIT_STRUCT }, + { name: 'signature', type: 'bytes' }, + ], + }, + [CommandType.PERMIT2_PERMIT_BATCH]: { + parser: Parser.Abi, + params: [ + { name: 'permit', type: PERMIT_BATCH_STRUCT }, + { name: 'signature', type: 'bytes' }, + ], + }, + [CommandType.PERMIT2_TRANSFER_FROM]: { + parser: Parser.Abi, + params: [ + { name: 'token', type: 'address' }, + { name: 'recipient', type: 'address' }, + { name: 'amount', type: 'uint160' }, + ], + }, + [CommandType.PERMIT2_TRANSFER_FROM_BATCH]: { + parser: Parser.Abi, + params: [ + { + name: 'transferFrom', + type: PERMIT2_TRANSFER_FROM_BATCH_STRUCT, + }, + ], + }, // Uniswap Actions - [CommandType.V3_SWAP_EXACT_IN]: [ - { name: 'recipient', type: 'address' }, - { name: 'amountIn', type: 'uint256' }, - { name: 'amountOutMin', type: 'uint256' }, - { name: 'path', subparser: Subparser.V3PathExactIn, type: 'bytes' }, - { name: 'payerIsUser', type: 'bool' }, - ], - [CommandType.V3_SWAP_EXACT_OUT]: [ - { name: 'recipient', type: 'address' }, - { name: 'amountOut', type: 'uint256' }, - { name: 'amountInMax', type: 'uint256' }, - { name: 'path', subparser: Subparser.V3PathExactOut, type: 'bytes' }, - { name: 'payerIsUser', type: 'bool' }, - ], - [CommandType.V2_SWAP_EXACT_IN]: [ - { name: 'recipient', type: 'address' }, - { name: 'amountIn', type: 'uint256' }, - { name: 'amountOutMin', type: 'uint256' }, - { name: 'path', type: 'address[]' }, - { name: 'payerIsUser', type: 'bool' }, - ], - [CommandType.V2_SWAP_EXACT_OUT]: [ - { name: 'recipient', type: 'address' }, - { name: 'amountOut', type: 'uint256' }, - { name: 'amountInMax', type: 'uint256' }, - { name: 'path', type: 'address[]' }, - { name: 'payerIsUser', type: 'bool' }, - ], - [CommandType.V4_SWAP]: [{ name: 'command', type: 'bytes' }], + [CommandType.V3_SWAP_EXACT_IN]: { + parser: Parser.Abi, + params: [ + { name: 'recipient', type: 'address' }, + { name: 'amountIn', type: 'uint256' }, + { name: 'amountOutMin', type: 'uint256' }, + { name: 'path', subparser: Subparser.V3PathExactIn, type: 'bytes' }, + { name: 'payerIsUser', type: 'bool' }, + ], + }, + [CommandType.V3_SWAP_EXACT_OUT]: { + parser: Parser.Abi, + params: [ + { name: 'recipient', type: 'address' }, + { name: 'amountOut', type: 'uint256' }, + { name: 'amountInMax', type: 'uint256' }, + { name: 'path', subparser: Subparser.V3PathExactOut, type: 'bytes' }, + { name: 'payerIsUser', type: 'bool' }, + ], + }, + [CommandType.V2_SWAP_EXACT_IN]: { + parser: Parser.Abi, + params: [ + { name: 'recipient', type: 'address' }, + { name: 'amountIn', type: 'uint256' }, + { name: 'amountOutMin', type: 'uint256' }, + { name: 'path', type: 'address[]' }, + { name: 'payerIsUser', type: 'bool' }, + ], + }, + [CommandType.V2_SWAP_EXACT_OUT]: { + parser: Parser.Abi, + params: [ + { name: 'recipient', type: 'address' }, + { name: 'amountOut', type: 'uint256' }, + { name: 'amountInMax', type: 'uint256' }, + { name: 'path', type: 'address[]' }, + { name: 'payerIsUser', type: 'bool' }, + ], + }, + [CommandType.V4_SWAP]: { parser: Parser.V4Actions }, // Token Actions and Checks - [CommandType.WRAP_ETH]: [ - { name: 'recipient', type: 'address' }, - { name: 'amount', type: 'uint256' }, - ], - [CommandType.UNWRAP_WETH]: [ - { name: 'recipient', type: 'address' }, - { name: 'amountMin', type: 'uint256' }, - ], - [CommandType.SWEEP]: [ - { name: 'token', type: 'address' }, - { name: 'recipient', type: 'address' }, - { name: 'amountMin', type: 'uint256' }, - ], - [CommandType.TRANSFER]: [ - { name: 'token', type: 'address' }, - { name: 'recipient', type: 'address' }, - { name: 'value', type: 'uint256' }, - ], - [CommandType.PAY_PORTION]: [ - { name: 'token', type: 'address' }, - { name: 'recipient', type: 'address' }, - { name: 'bips', type: 'uint256' }, - ], - [CommandType.BALANCE_CHECK_ERC20]: [ - { name: 'owner', type: 'address' }, - { name: 'token', type: 'address' }, - { name: 'minBalance', type: 'uint256' }, - ], + [CommandType.WRAP_ETH]: { + parser: Parser.Abi, + params: [ + { name: 'recipient', type: 'address' }, + { name: 'amount', type: 'uint256' }, + ], + }, + [CommandType.UNWRAP_WETH]: { + parser: Parser.Abi, + params: [ + { name: 'recipient', type: 'address' }, + { name: 'amountMin', type: 'uint256' }, + ], + }, + [CommandType.SWEEP]: { + parser: Parser.Abi, + params: [ + { name: 'token', type: 'address' }, + { name: 'recipient', type: 'address' }, + { name: 'amountMin', type: 'uint256' }, + ], + }, + [CommandType.TRANSFER]: { + parser: Parser.Abi, + params: [ + { name: 'token', type: 'address' }, + { name: 'recipient', type: 'address' }, + { name: 'value', type: 'uint256' }, + ], + }, + [CommandType.PAY_PORTION]: { + parser: Parser.Abi, + params: [ + { name: 'token', type: 'address' }, + { name: 'recipient', type: 'address' }, + { name: 'bips', type: 'uint256' }, + ], + }, + [CommandType.BALANCE_CHECK_ERC20]: { + parser: Parser.Abi, + params: [ + { name: 'owner', type: 'address' }, + { name: 'token', type: 'address' }, + { name: 'minBalance', type: 'uint256' }, + ], + }, // Position Actions - [CommandType.V3_POSITION_MANAGER_PERMIT]: [{ name: 'calldata', type: 'bytes' }], - [CommandType.V3_POSITION_MANAGER_CALL]: [{ name: 'calldata', type: 'bytes' }], - [CommandType.V4_POSITION_CALL]: [{ name: 'calldata', type: 'bytes' }], + [CommandType.V3_POSITION_MANAGER_PERMIT]: { parser: Parser.V3Actions }, + [CommandType.V3_POSITION_MANAGER_CALL]: { parser: Parser.V3Actions }, + [CommandType.V4_POSITION_CALL]: { parser: Parser.V4Actions }, } export class RoutePlanner { @@ -183,17 +246,19 @@ export type RouterCommand = { } export function createCommand(type: CommandType, parameters: any[]): RouterCommand { - if ( - type === CommandType.V4_SWAP || - type === CommandType.V3_POSITION_MANAGER_CALL || - type === CommandType.V3_POSITION_MANAGER_PERMIT || - type === CommandType.V4_POSITION_CALL - ) { - return { type, encodedInput: parameters[0] } + const commandDef = COMMAND_DEFINITION[type] + switch (commandDef.parser) { + case Parser.Abi: + const encodedInput = defaultAbiCoder.encode( + commandDef.params.map((abi) => abi.type), + parameters + ) + return { type, encodedInput } + case Parser.V4Actions: + // v4 swap data comes pre-encoded at index 0 + return { type, encodedInput: parameters[0] } + case Parser.V3Actions: + // v4 swap data comes pre-encoded at index 0 + return { type, encodedInput: parameters[0] } } - const encodedInput = defaultAbiCoder.encode( - COMMAND_ABI_DEFINITION[type].map((abi) => abi.type), - parameters - ) - return { type, encodedInput } } diff --git a/sdks/universal-router-sdk/test/utils/commandParser.test.ts b/sdks/universal-router-sdk/test/utils/commandParser.test.ts index 53e74804..49842883 100644 --- a/sdks/universal-router-sdk/test/utils/commandParser.test.ts +++ b/sdks/universal-router-sdk/test/utils/commandParser.test.ts @@ -1,12 +1,39 @@ import { expect } from 'chai' -import { ethers } from 'ethers' +import { Token, WETH9 } from '@uniswap/sdk-core' +import { encodeSqrtRatioX96, nearestUsableTick, TickMath } from '@uniswap/v3-sdk' +import { ethers, BigNumber } from 'ethers' import { CommandParser, UniversalRouterCall } from '../../src/utils/commandParser' import { RoutePlanner, CommandType } from '../../src/utils/routerCommands' import { SwapRouter } from '../../src/swapRouter' +import { V4Planner, Actions, Pool } from '@uniswap/v4-sdk' const addressOne = '0x0000000000000000000000000000000000000001' const addressTwo = '0x0000000000000000000000000000000000000002' const amount = ethers.utils.parseEther('1') +const TICKLIST = [ + { + index: nearestUsableTick(TickMath.MIN_TICK, 10), + liquidityNet: amount, + liquidityGross: amount, + }, + { + index: nearestUsableTick(TickMath.MAX_TICK, 10), + liquidityNet: amount.mul(-1), + liquidityGross: amount, + }, +] +const USDC = new Token(1, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD Coin') +const USDC_WETH = new Pool( + USDC, + WETH9[1], + 3000, + 10, + ethers.constants.AddressZero, + encodeSqrtRatioX96(1, 1), + 0, + 0, + TICKLIST +) describe('Command Parser', () => { type ParserTest = { @@ -230,6 +257,130 @@ describe('Command Parser', () => { ], }, }, + { + input: new RoutePlanner().addCommand(CommandType.V4_SWAP, [ + new V4Planner() + .addAction(Actions.SWAP_EXACT_IN_SINGLE, [ + { + poolKey: USDC_WETH.poolKey, + zeroForOne: true, + amountIn: amount, + amountOutMinimum: amount, + sqrtPriceLimitX96: 0, + hookData: '0x', + }, + ]) + .finalize(), + ]), + result: { + commands: [ + { + commandName: 'V4_SWAP', + commandType: CommandType.V4_SWAP, + params: [ + { + name: 'SWAP_EXACT_IN_SINGLE', + value: [ + { + name: 'swap', + value: { + poolKey: USDC_WETH.poolKey, + zeroForOne: true, + amountIn: amount, + amountOutMinimum: amount, + sqrtPriceLimitX96: BigNumber.from(0), + hookData: '0x', + }, + }, + ], + }, + ], + }, + ], + }, + }, + { + input: new RoutePlanner().addCommand(CommandType.V4_SWAP, [ + new V4Planner().addAction(Actions.TAKE_ALL, [addressOne, amount]).finalize(), + ]), + result: { + commands: [ + { + commandName: 'V4_SWAP', + commandType: CommandType.V4_SWAP, + params: [ + { + name: 'TAKE_ALL', + value: [ + { + name: 'currency', + value: addressOne, + }, + { + name: 'minAmount', + value: amount, + }, + ], + }, + ], + }, + ], + }, + }, + { + input: new RoutePlanner().addCommand(CommandType.V4_POSITION_CALL, [ + new V4Planner() + .addAction(Actions.MINT_POSITION, [USDC_WETH.poolKey, -60, 60, 5000000, amount, amount, addressOne, '0x']) + .finalize(), + ]), + result: { + commands: [ + { + commandName: 'V4_POSITION_CALL', + commandType: CommandType.V4_POSITION_CALL, + params: [ + { + name: 'MINT_POSITION', + value: [ + { + name: 'poolKey', + value: USDC_WETH.poolKey, + }, + { + name: 'tickLower', + value: -60, + }, + { + name: 'tickUpper', + value: 60, + }, + { + name: 'liquidity', + value: BigNumber.from(5000000), + }, + { + name: 'amount0Max', + value: amount, + }, + { + name: 'amount1Max', + value: amount, + }, + { + name: 'owner', + value: addressOne, + }, + { + name: 'hookData', + value: '0x', + }, + ], + }, + ], + }, + ], + }, + }, ] for (const test of tests) { diff --git a/yarn.lock b/yarn.lock index adf469dd..7f189548 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4737,7 +4737,7 @@ __metadata: "@uniswap/v2-sdk": ^4.6.0 "@uniswap/v3-core": 1.0.0 "@uniswap/v3-sdk": ^3.17.0 - "@uniswap/v4-sdk": ^1.6.3 + "@uniswap/v4-sdk": ^1.10.0 bignumber.js: ^9.0.2 chai: ^4.3.6 dotenv: ^16.0.3 @@ -4890,16 +4890,16 @@ __metadata: languageName: node linkType: hard -"@uniswap/v4-sdk@npm:^1.6.0, @uniswap/v4-sdk@npm:^1.6.3, @uniswap/v4-sdk@npm:^1.9.0": - version: 1.9.0 - resolution: "@uniswap/v4-sdk@npm:1.9.0" +"@uniswap/v4-sdk@npm:^1.10.0, @uniswap/v4-sdk@npm:^1.6.0, @uniswap/v4-sdk@npm:^1.9.0": + version: 1.10.0 + resolution: "@uniswap/v4-sdk@npm:1.10.0" dependencies: "@ethersproject/solidity": ^5.0.9 "@uniswap/sdk-core": ^5.3.1 "@uniswap/v3-sdk": 3.12.0 tiny-invariant: ^1.1.0 tiny-warning: ^1.0.3 - checksum: 1483b4ab207c4ffd3f144f848884247189f0dc807f0b2fc149b7cc0bfcb1de5b758e080ed3b829008d6384b498218b9e6f4d3e5fdcaf04cb3721b5b5cef44a70 + checksum: c1a55b0e52e2db986c463e75d9de337c48a76b90076705bc4a50da2a27de2f4c91a7c68f79bb04ac7fc7c7b3244fd8c955645e6dcac7e8b9af70354fd905e164 languageName: node linkType: hard