From e48eaf46134f1ef04067f61f9ff7070056eaf501 Mon Sep 17 00:00:00 2001 From: Cody Born Date: Tue, 25 Mar 2025 14:38:13 -0400 Subject: [PATCH 1/4] Add expected amouts to DutchV3 --- .../src/trade/V3DutchOrderTrade.test.ts | 64 +++++++++++++++++++ .../src/trade/V3DutchOrderTrade.ts | 46 +++++++++++-- 2 files changed, 105 insertions(+), 5 deletions(-) diff --git a/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.test.ts b/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.test.ts index 832b76549..3fa05911f 100644 --- a/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.test.ts +++ b/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.test.ts @@ -211,4 +211,68 @@ describe("V3DutchOrderTrade", () => { expect(ethOutputTrade.outputAmount.currency).toEqual(Ether.onChain(1)); }); }); + + describe("Expected amounts", () => { + const expectedAmounts = { + expectedAmountIn: "800", + expectedAmountOut: "900" + }; + + const tradeWithExpectedAmounts = new V3DutchOrderTrade({ + currencyIn: USDC, + currenciesOut: [DAI], + orderInfo, + tradeType: TradeType.EXACT_INPUT, + expectedAmounts + }); + + it("uses expectedAmountIn when provided", () => { + expect(tradeWithExpectedAmounts.inputAmount.quotient.toString()).toEqual( + expectedAmounts.expectedAmountIn + ); + }); + + it("uses expectedAmountOut when provided", () => { + expect(tradeWithExpectedAmounts.outputAmount.quotient.toString()).toEqual( + expectedAmounts.expectedAmountOut + ); + }); + + it("falls back to order amounts when expectedAmounts is not provided", () => { + expect(trade.inputAmount.quotient.toString()).toEqual( + orderInfo.input.startAmount.toString() + ); + expect(trade.outputAmount.quotient.toString()).toEqual( + NON_FEE_OUTPUT_AMOUNT.toString() + ); + }); + + it("throws when accessing expectedAmountIn that wasn't provided", () => { + const tradeWithoutExpected = new V3DutchOrderTrade({ + currencyIn: USDC, + currenciesOut: [DAI], + orderInfo, + tradeType: TradeType.EXACT_INPUT + }); + + // Using private method through any to test error case + expect(() => { + (tradeWithoutExpected as any).getExpectedAmountIn(); + }).toThrow("expectedAmountIn not set"); + }); + + it("throws when accessing expectedAmountOut that wasn't provided", () => { + const tradeWithoutExpected = new V3DutchOrderTrade({ + currencyIn: USDC, + currenciesOut: [DAI], + orderInfo, + tradeType: TradeType.EXACT_INPUT + }); + + // Using private method through any to test error case + expect(() => { + (tradeWithoutExpected as any).getExpectedAmountOut(); + }).toThrow("expectedAmountOut not set"); + }); + }); }); \ No newline at end of file diff --git a/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.ts b/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.ts index 74789f18a..822039f19 100644 --- a/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.ts +++ b/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.ts @@ -13,6 +13,10 @@ export class V3DutchOrderTrade< > { public readonly tradeType: TTradeType public readonly order: UnsignedV3DutchOrder + public readonly expectedAmounts: { + expectedAmountIn: string; + expectedAmountOut: string; + } | undefined; private _inputAmount: CurrencyAmount | undefined private _outputAmounts: CurrencyAmount[] | undefined @@ -25,15 +29,21 @@ export class V3DutchOrderTrade< currenciesOut, orderInfo, tradeType, + expectedAmounts, }: { currencyIn: TInput currenciesOut: TOutput[] orderInfo: UnsignedV3DutchOrderInfo tradeType: TTradeType + expectedAmounts?: { + expectedAmountIn: string; + expectedAmountOut: string; + } }) { this._currencyIn = currencyIn this._currenciesOut = currenciesOut this.tradeType = tradeType + this.expectedAmounts = expectedAmounts; // Assuming not cross-chain this.order = new UnsignedV3DutchOrder(orderInfo, currencyIn.chainId) @@ -42,10 +52,12 @@ export class V3DutchOrderTrade< public get inputAmount(): CurrencyAmount { if (this._inputAmount) return this._inputAmount - const amount = CurrencyAmount.fromRawAmount( - this._currencyIn, - this.order.info.input.startAmount.toString() - ) + const amount = this.expectedAmounts?.expectedAmountIn + ? this.getExpectedAmountIn() + : CurrencyAmount.fromRawAmount( + this._currencyIn, + this.order.info.input.startAmount.toString() + ) this._inputAmount = amount return amount } @@ -72,7 +84,9 @@ export class V3DutchOrderTrade< // Same assumption as V2 that there is only one non-fee output at a time, and it exists at index 0 public get outputAmount(): CurrencyAmount { - return this.outputAmounts[0]; + return this.expectedAmounts?.expectedAmountOut + ? this.getExpectedAmountOut() + : this.outputAmounts[0]; } public minimumAmountOut(): CurrencyAmount { @@ -123,4 +137,26 @@ export class V3DutchOrderTrade< this.minimumAmountOut().quotient ); } + + private getExpectedAmountIn(): CurrencyAmount { + if (!this.expectedAmounts?.expectedAmountIn) { + throw new Error("expectedAmountIn not set"); + } + + return CurrencyAmount.fromRawAmount( + this._currencyIn, + this.expectedAmounts.expectedAmountIn + ); + } + + private getExpectedAmountOut(): CurrencyAmount { + if (!this.expectedAmounts?.expectedAmountOut) { + throw new Error("expectedAmountOut not set"); + } + + return CurrencyAmount.fromRawAmount( + this._currenciesOut[0], + this.expectedAmounts.expectedAmountOut + ); + } } \ No newline at end of file From 7806424b5ab84608638b293b00a60a1c9f297678 Mon Sep 17 00:00:00 2001 From: Cody Born Date: Tue, 25 Mar 2025 15:24:40 -0400 Subject: [PATCH 2/4] lint fixes --- sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.test.ts b/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.test.ts index 3fa05911f..4e36ed121 100644 --- a/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.test.ts +++ b/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.test.ts @@ -215,7 +215,7 @@ describe("V3DutchOrderTrade", () => { describe("Expected amounts", () => { const expectedAmounts = { expectedAmountIn: "800", - expectedAmountOut: "900" + expectedAmountOut: "900", }; const tradeWithExpectedAmounts = new V3DutchOrderTrade({ @@ -223,7 +223,7 @@ describe("V3DutchOrderTrade", () => { currenciesOut: [DAI], orderInfo, tradeType: TradeType.EXACT_INPUT, - expectedAmounts + expectedAmounts, }); it("uses expectedAmountIn when provided", () => { @@ -252,7 +252,7 @@ describe("V3DutchOrderTrade", () => { currencyIn: USDC, currenciesOut: [DAI], orderInfo, - tradeType: TradeType.EXACT_INPUT + tradeType: TradeType.EXACT_INPUT, }); // Using private method through any to test error case @@ -266,7 +266,7 @@ describe("V3DutchOrderTrade", () => { currencyIn: USDC, currenciesOut: [DAI], orderInfo, - tradeType: TradeType.EXACT_INPUT + tradeType: TradeType.EXACT_INPUT, }); // Using private method through any to test error case From afeb80134528d1c5b1e1b08ff62be89cff086749 Mon Sep 17 00:00:00 2001 From: Cody Born Date: Tue, 15 Apr 2025 13:30:41 -0400 Subject: [PATCH 3/4] Add v3 trade tests --- .../src/trade/V3DutchOrderTrade.test.ts | 203 +++++++++++++++++- 1 file changed, 202 insertions(+), 1 deletion(-) diff --git a/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.test.ts b/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.test.ts index 4e36ed121..0f26b64f5 100644 --- a/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.test.ts +++ b/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.test.ts @@ -1,4 +1,4 @@ -import { Currency, Ether, Token, TradeType } from "@uniswap/sdk-core"; +import { Currency, CurrencyAmount, Ether, Price, Token, TradeType } from "@uniswap/sdk-core"; import { BigNumber, constants, ethers } from "ethers"; import { UnsignedV3DutchOrderInfo } from "../order/V3DutchOrder"; @@ -274,5 +274,206 @@ describe("V3DutchOrderTrade", () => { (tradeWithoutExpected as any).getExpectedAmountOut(); }).toThrow("expectedAmountOut not set"); }); + + describe("Execution price", () => { + it("expected amounts are used when provided", () => { + const inputAmount = BigNumber.from(1000); + const decay = 100000; + const outOrderInfo: UnsignedV3DutchOrderInfo = { + ...orderInfo, + input: { + token: USDC.address, + startAmount: inputAmount, + curve: { + relativeBlocks: [], + relativeAmounts: [], + }, + maxAmount: inputAmount, + adjustmentPerGweiBaseFee: BigNumber.from(0), + }, + outputs: [ + { + token: DAI.address, + startAmount: NON_FEE_OUTPUT_AMOUNT, + curve: { + relativeBlocks: [10], + relativeAmounts: [BigInt(decay)], + }, + recipient: "0x0000000000000000000000000000000000000000", + minAmount: NON_FEE_OUTPUT_AMOUNT, + adjustmentPerGweiBaseFee: BigNumber.from(0), + } + ], + }; + const exactInputTrade = new V3DutchOrderTrade({ + currencyIn: USDC, + currenciesOut: [DAI], + orderInfo: outOrderInfo, + tradeType: TradeType.EXACT_INPUT, + expectedAmounts: { + expectedAmountIn: inputAmount.toString(), + expectedAmountOut: NON_FEE_OUTPUT_AMOUNT.sub(BigNumber.from(decay).div(2)).toString(), + }, + }); + expect(exactInputTrade.inputAmount.quotient.toString()).toEqual(inputAmount.toString()); + expect(exactInputTrade.outputAmount.quotient.toString()).toEqual(NON_FEE_OUTPUT_AMOUNT.sub(BigNumber.from(decay).div(2)).toString()); + const executionPrice = exactInputTrade.executionPrice; + const inputCurrencyAmount = CurrencyAmount.fromRawAmount(USDC, inputAmount.toString()); + const outputCurrencyAmount = CurrencyAmount.fromRawAmount(DAI, NON_FEE_OUTPUT_AMOUNT.sub(BigNumber.from(decay).div(2)).toString()); + const expectedPrice = new Price(USDC, DAI, inputCurrencyAmount.quotient, outputCurrencyAmount.quotient); + expect(executionPrice.quotient.toString()).toEqual(expectedPrice.quotient.toString()); + }); + + it("order amounts are used when expected amounts are not provided", () => { + + expect(trade.inputAmount.quotient.toString()).toEqual(trade.order.info.input.startAmount.toString()); + expect(trade.outputAmount.quotient.toString()).toEqual(trade.order.info.outputs[0].startAmount.toString()); + }); + + }); + + describe("Worst execution price", () => { + it("calculates worst execution price correctly for exact input", () => { + const decay = 100000; + const exactInputOrderInfo: UnsignedV3DutchOrderInfo = { + ...orderInfo, + input: { + token: USDC.address, + startAmount: BigNumber.from(1000), + curve: { + relativeBlocks: [], + relativeAmounts: [], + }, + maxAmount: BigNumber.from(1000), + adjustmentPerGweiBaseFee: BigNumber.from(0), + }, + outputs: [ + { + token: DAI.address, + startAmount: NON_FEE_OUTPUT_AMOUNT, + curve: { + relativeBlocks: [10], + relativeAmounts: [BigInt(decay)], + }, + recipient: "0x0000000000000000000000000000000000000000", + minAmount: NON_FEE_OUTPUT_AMOUNT.sub(BigNumber.from(decay)), + adjustmentPerGweiBaseFee: BigNumber.from(0), + } + ], + }; + const halfDecay = BigNumber.from(decay).div(2); + const exactInputTrade = new V3DutchOrderTrade({ + currencyIn: USDC, + currenciesOut: [DAI], + orderInfo: exactInputOrderInfo, + tradeType: TradeType.EXACT_INPUT, + expectedAmounts: { + expectedAmountIn: "1000", + expectedAmountOut: NON_FEE_OUTPUT_AMOUNT.sub(halfDecay).toString(), + }, + }); + const worstPrice = exactInputTrade.worstExecutionPrice(); + const maxAmountIn = 1000; + const minAmountOut = NON_FEE_OUTPUT_AMOUNT.sub(decay); + + expect(worstPrice.quotient.toString()).toEqual( + minAmountOut.div(maxAmountIn).toString() + ); + + // Verify worst price is worse than execution price + expect(worstPrice.lessThan(exactInputTrade.executionPrice)).toBe(true); + expect(worstPrice.baseCurrency).toEqual(USDC); + expect(worstPrice.quoteCurrency).toEqual(DAI); + }); + + it("matches execution price when min/max amounts equal start amounts", () => { + const orderInfoNoSlippage = { + ...orderInfo, + input: { + ...orderInfo.input, + maxAmount: orderInfo.input.startAmount, // Same as start amount + }, + outputs: [ + { + ...orderInfo.outputs[0], + minAmount: orderInfo.outputs[0].startAmount, // Same as start amount + curve: { + relativeBlocks: [], + relativeAmounts: [], + } + } + ] + }; + + const tradeNoSlippage = new V3DutchOrderTrade({ + currencyIn: USDC, + currenciesOut: [DAI], + orderInfo: orderInfoNoSlippage, + tradeType: TradeType.EXACT_INPUT, + expectedAmounts: { + expectedAmountIn: orderInfo.input.startAmount.toString(), + expectedAmountOut: orderInfo.outputs[0].startAmount.toString(), + }, + }); + + expect(tradeNoSlippage.worstExecutionPrice().quotient.toString()) + .toEqual(tradeNoSlippage.executionPrice.quotient.toString()); + }); + }); + }); + + describe("Worst execution price", () => { + it("calculates worst execution price correctly for exact output", () => { + const outOrderInfo: UnsignedV3DutchOrderInfo = { + ...orderInfo, + input: { + token: USDC.address, + startAmount: BigNumber.from(1000), + curve: { + relativeBlocks: [10], + relativeAmounts: [BigInt(-100)], + }, + maxAmount: BigNumber.from(1100), + adjustmentPerGweiBaseFee: BigNumber.from(0), + }, + outputs: [ + { + token: DAI.address, + startAmount: NON_FEE_OUTPUT_AMOUNT, + curve: { + relativeBlocks: [], + relativeAmounts: [], + }, + recipient: "0x0000000000000000000000000000000000000000", + minAmount: NON_FEE_OUTPUT_AMOUNT, + adjustmentPerGweiBaseFee: BigNumber.from(0), + } + ], + }; + + const exactOutputTrade = new V3DutchOrderTrade({ + currencyIn: USDC, + currenciesOut: [DAI], + orderInfo: outOrderInfo, + tradeType: TradeType.EXACT_OUTPUT, + expectedAmounts: { + expectedAmountIn: "1050", + expectedAmountOut: NON_FEE_OUTPUT_AMOUNT.toString(), + }, + }); + + const worstPrice = exactOutputTrade.worstExecutionPrice(); + const maxAmountIn = 1100; + const minAmountOut = NON_FEE_OUTPUT_AMOUNT; + + expect(worstPrice.quotient.toString()).toEqual( + minAmountOut.div(maxAmountIn).toString() + ); + + // Verify worst price is worse than execution price + expect(worstPrice.lessThan(exactOutputTrade.executionPrice)).toBe(true); + expect(worstPrice.baseCurrency).toEqual(USDC); + expect(worstPrice.quoteCurrency).toEqual(DAI); + }); }); }); \ No newline at end of file From 988d96693e17b02bc4326519e6c65fe09a3dbb2a Mon Sep 17 00:00:00 2001 From: Cody Born Date: Wed, 4 Jun 2025 14:41:57 +0200 Subject: [PATCH 4/4] lint fix --- .../uniswapx-sdk/src/trade/V3DutchOrderTrade.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.test.ts b/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.test.ts index 0f26b64f5..4dd250089 100644 --- a/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.test.ts +++ b/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.test.ts @@ -302,7 +302,7 @@ describe("V3DutchOrderTrade", () => { recipient: "0x0000000000000000000000000000000000000000", minAmount: NON_FEE_OUTPUT_AMOUNT, adjustmentPerGweiBaseFee: BigNumber.from(0), - } + }, ], }; const exactInputTrade = new V3DutchOrderTrade({ @@ -358,7 +358,7 @@ describe("V3DutchOrderTrade", () => { recipient: "0x0000000000000000000000000000000000000000", minAmount: NON_FEE_OUTPUT_AMOUNT.sub(BigNumber.from(decay)), adjustmentPerGweiBaseFee: BigNumber.from(0), - } + }, ], }; const halfDecay = BigNumber.from(decay).div(2); @@ -400,9 +400,9 @@ describe("V3DutchOrderTrade", () => { curve: { relativeBlocks: [], relativeAmounts: [], - } - } - ] + }, + }, + ], }; const tradeNoSlippage = new V3DutchOrderTrade({ @@ -447,7 +447,7 @@ describe("V3DutchOrderTrade", () => { recipient: "0x0000000000000000000000000000000000000000", minAmount: NON_FEE_OUTPUT_AMOUNT, adjustmentPerGweiBaseFee: BigNumber.from(0), - } + }, ], };