From ec6cb33d5862239b2533cf0775e370e17a72eb96 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Tue, 3 Sep 2024 15:19:16 -0400 Subject: [PATCH 01/41] feat: V3DutchOrder --- .../src/order/V3DutchOrder.test.ts | 75 ++++ sdks/uniswapx-sdk/src/order/V3DutchOrder.ts | 390 ++++++++++++++++++ sdks/uniswapx-sdk/src/order/types.ts | 31 ++ 3 files changed, 496 insertions(+) create mode 100644 sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts create mode 100644 sdks/uniswapx-sdk/src/order/V3DutchOrder.ts diff --git a/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts b/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts new file mode 100644 index 00000000..311dc742 --- /dev/null +++ b/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts @@ -0,0 +1,75 @@ +import { BigNumber, ethers } from "ethers"; +import { expect } from "chai"; +import { CosignedV3DutchOrder, CosignedV3DutchOrderInfo } from "./V3DutchOrder"; + +const TIME= 1725379823; +const BLOCK_NUMBER = 20671221; +const RAW_AMOUNT = BigNumber.from("1000000"); +const INPUT_TOKEN = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; +const OUTPUT_TOKEN = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"; +const CHAIN_ID = 1; + +const COSIGNER_DATA = { + decayStartBlock: BLOCK_NUMBER, + exclusiveFiller: ethers.constants.AddressZero, + exclusivityOverrideBps: BigNumber.from(0), + inputOverride: RAW_AMOUNT, + outputOverrides: [RAW_AMOUNT.mul(102).div(100)] +}; + +describe("V3DutchOrder", () => { + it("should get block number", () => { + expect(BLOCK_NUMBER).to.be.greaterThan(0); + }); + + const getFullOrderInfo = ( data: Partial): CosignedV3DutchOrderInfo => { + return Object.assign( + { + reactor: ethers.constants.AddressZero, + swapper: ethers.constants.AddressZero, + nonce: BigNumber.from(21), + deadline: TIME + 1000, + additionalValidationContract: ethers.constants.AddressZero, + additionalValidationData: "0x", + cosigner: ethers.constants.AddressZero, + cosignerData: COSIGNER_DATA, + input: { + token: INPUT_TOKEN, + startAmount: RAW_AMOUNT, + curve: { + relativeBlocks: [1], + relativeAmount: [BigNumber.from(1), BigNumber.from(2), BigNumber.from(3), BigNumber.from(4)] // 1e-18, 2e-18, 3e-18, 4e-18 + }, + maxAmount: RAW_AMOUNT.add(1) + }, + outputs: [ + { + token: OUTPUT_TOKEN, + startAmount: RAW_AMOUNT, + curve: { + relativeBlocks: [1], + relativeAmount: [BigNumber.from(1), BigNumber.from(2), BigNumber.from(3), BigNumber.from(4)] // 1e-18, 2e-18, 3e-18, 4e-18 + }, + recipient: ethers.constants.AddressZero, + } + ], + cosignature: "0x", + }, + data + ); + }; + + + + it("Parses a serialized v3 order", () => { + const orderInfo = getFullOrderInfo({}); + const order = new CosignedV3DutchOrder(orderInfo, CHAIN_ID); + console.log(order); + const seralized = order.serialize(); + console.log(seralized); + const parsed = CosignedV3DutchOrder.parse(seralized, CHAIN_ID); + expect(parsed.info).to.deep.eq(orderInfo); + } + ); + +}); \ No newline at end of file diff --git a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts new file mode 100644 index 00000000..5f36388e --- /dev/null +++ b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts @@ -0,0 +1,390 @@ +import { getPermit2 } from "../utils"; +import { V3DutchInput, OffChainOrder, OrderInfo, V3DutchInputJSON, V3DutchOutput, V3DutchOutputJSON, BlockOverrides } from "./types"; +import { BigNumber, ethers } from "ethers"; +import { PermitTransferFromData, SignatureTransfer, PermitTransferFrom, Witness } from "@uniswap/permit2-sdk"; +import { SignatureLike } from "@ethersproject/bytes"; + +export type CosignerDataJSON = { + decayStartBlock: number; + exclusiveFiller: string; + exclusivityOverrideBps: number; + inputOverride: string; + outputOverrides: string[]; +} + +export type UnsignedV3DutchOrderInfoJSON = Omit & { + nonce: string; + input: V3DutchInputJSON; + outputs: V3DutchOutputJSON[]; +}; + +export type CosignerData = { + decayStartBlock: number; + //No end in cosignerData + exclusiveFiller: string; + exclusivityOverrideBps: BigNumber; + inputOverride: BigNumber; + outputOverrides: BigNumber[]; +} + +export type UnsignedV3DutchOrderInfo = OrderInfo & { + cosigner: string; + input: V3DutchInput; //different from V2DutchOrder + outputs: V3DutchOutput[]; +}; + +export type CosignedV3DutchOrderInfo = UnsignedV3DutchOrderInfo & { + cosignerData: CosignerData; + cosignature: string; +}; + +type V3WitnessInfo = { + info: OrderInfo, + cosigner: string, + baseInput: V3DutchInput, + baseOutputs: V3DutchOutput[], +}; + + +const COSIGNER_DATA_TUPLE_ABI = "tuple(uint256,address,uint256,uint256,uint256[])"; + +const V3_DUTCH_ORDER_TYPES = { + V3DutchOrder: [ + { name: "info", type: "OrderInfo" }, + { name: "cosigner", type: "address" }, + { name: "baseInput", type: "V3DutchInput" }, + { name: "baseOutputs", type: "V3DutchOutput[]" }, + ], + OrderInfo: [ + { name: "reactor", type: "address" }, + { name: "swapper", type: "address" }, + { name: "nonce", type: "uint256" }, + { name: "deadline", type: "uint256" }, + { name: "additionalValidationContract", type: "address" }, + { name: "additionalValidationData", type: "bytes" }, + ], + V3DutchInput: [ + { name: "token", type: "address" }, + { name: "startAmount", type: "uint256" }, + { name: "curve", type: "V3Decay" }, + { name: "maxAmount", type: "uint256" }, + ], + V3DutchOutput: [ + { name: "token", type: "address" }, + { name: "startAmount", type: "uint256" }, + { name: "curve", type: "V3Decay" }, + { name: "recipient", type: "address" }, + ], + V3Decay: [ + { name: "relativeBlocks", type: "uint256" }, + { name: "relativeAmount", type: "int256[]" }, + ] +}; + +const V3_DUTCH_ORDER_ABI = [ + "tuple(" + + [ + "tuple(address,address,uint256,uint256,address,bytes)", // OrderInfo + "address", // Cosigner + "tuple(address,uint256,tuple(uint256,int256[]),uint256)", // V3DutchInput + "tuple(address,uint256,tuple(uint256,int256[]),address)[]", // V3DutchOutput + COSIGNER_DATA_TUPLE_ABI, + "bytes" // Cosignature + ].join(",") + ")" +]; + +export class UnsignedV3DutchOrder implements OffChainOrder { + public permit2Address: string; + + constructor ( + public readonly info: UnsignedV3DutchOrderInfo, + public readonly chainId: number, + _permit2Address?: string + ) { + this.permit2Address = getPermit2(chainId, _permit2Address); + } + + /** + * @inheritdoc order + */ + get blockOverrides(): BlockOverrides { + return undefined + } + + /** + * @inheritdoc order + */ + serialize(): string { + const encodedRelativeBlocks = encodeRelativeBlocks(this.info.input.curve.relativeBlocks); + const abiCoder = new ethers.utils.AbiCoder(); + return abiCoder.encode(V3_DUTCH_ORDER_ABI, [ + [ + [ + this.info.reactor, + this.info.swapper, + this.info.nonce, + this.info.deadline, + this.info.additionalValidationContract, + this.info.additionalValidationData + ], + this.info.cosigner, + [ + this.info.input.token, + this.info.input.startAmount, + [encodedRelativeBlocks, this.info.input.curve.relativeAmount], + this.info.input.maxAmount + ], + this.info.outputs.map(output => [ + output.token, + output.startAmount, + [encodedRelativeBlocks, output.curve.relativeAmount], + output.recipient + ]), + [0, ethers.constants.AddressZero, 0, 0, [0]], + "0x" + ], + ]); + } + + /** + * @inheritdoc order + */ + toJSON(): UnsignedV3DutchOrderInfoJSON { + return { + reactor: this.info.reactor, + swapper: this.info.swapper, + nonce: this.info.nonce.toString(), + deadline: this.info.deadline, + additionalValidationContract: this.info.additionalValidationContract, + additionalValidationData: this.info.additionalValidationData, + cosigner: this.info.cosigner, + input: { + token: this.info.input.token, + startAmount: this.info.input.startAmount.toString(), + curve: this.info.input.curve, + maxAmount: this.info.input.maxAmount.toString() + }, + outputs: this.info.outputs.map(output => ({ + token: output.token, + startAmount: output.startAmount.toString(), + curve: output.curve, + recipient: output.recipient + })) + } + }; + + permitData(): PermitTransferFromData { + return SignatureTransfer.getPermitData( + this.toPermit(), + this.permit2Address, + this.chainId, + this.witness() + ) as PermitTransferFromData; + } + + private toPermit(): PermitTransferFrom { + return { + permitted: { + token: this.info.input.token, + amount: this.info.input.maxAmount + }, + spender: this.info.reactor, + nonce: this.info.nonce, + deadline: this.info.deadline, + } + } + + private witnessInfo(): V3WitnessInfo { + return { + info: { + reactor: this.info.reactor, + swapper: this.info.swapper, + nonce: this.info.nonce, + deadline: this.info.deadline, + additionalValidationContract: this.info.additionalValidationContract, + additionalValidationData: this.info.additionalValidationData + }, + cosigner: this.info.cosigner, + baseInput: this.info.input, + baseOutputs: this.info.outputs + } + } + + private witness(): Witness { + return { + witness: this.witnessInfo(), + witnessTypeName: "V3DutchOrder", + witnessType: V3_DUTCH_ORDER_TYPES, + }; + } + + getSigner(signature: SignatureLike): string { + return ethers.utils.computeAddress( + ethers.utils.recoverPublicKey( + SignatureTransfer.hash( + this.toPermit(), + this.permit2Address, + this.chainId, + this.witness() + ), + signature + ) + ); + } + + hash(): string { + return ethers.utils._TypedDataEncoder + .from(V3_DUTCH_ORDER_TYPES) + .hash(this.witnessInfo()); + } + +} + +export class CosignedV3DutchOrder extends UnsignedV3DutchOrder { + constructor( + public readonly info: CosignedV3DutchOrderInfo, + public readonly chainId: number, + _permit2Address?: string + ) { + super(info, chainId, _permit2Address); + } + + static parse( + encoded: string, + chainId: number, + permit2?: string + ): CosignedV3DutchOrder { + return new CosignedV3DutchOrder( + parseSerializedOrder(encoded), + chainId, + permit2 + ); + } + + serialize(): string { + const encodedRelativeBlocks = encodeRelativeBlocks(this.info.input.curve.relativeBlocks); + const abiCoder = new ethers.utils.AbiCoder(); + return abiCoder.encode(V3_DUTCH_ORDER_ABI, [ + [ + [ + this.info.reactor, + this.info.swapper, + this.info.nonce, + this.info.deadline, + this.info.additionalValidationContract, + this.info.additionalValidationData + ], + this.info.cosigner, + [ + this.info.input.token, + this.info.input.startAmount, + [encodedRelativeBlocks, this.info.input.curve.relativeAmount], + this.info.input.maxAmount + ], + this.info.outputs.map(output => [ + output.token, + output.startAmount, + [encodedRelativeBlocks, output.curve.relativeAmount], + output.recipient + ]), + [ + this.info.cosignerData.decayStartBlock, + this.info.cosignerData.exclusiveFiller, + this.info.cosignerData.exclusivityOverrideBps, + this.info.cosignerData.inputOverride.toString(), + this.info.cosignerData.outputOverrides.map(override => override.toString()) + ], + this.info.cosignature + ], + ]); + } + + +} + +function parseSerializedOrder(serialized: string): CosignedV3DutchOrderInfo { + const abiCoder = new ethers.utils.AbiCoder(); + const decoded = abiCoder.decode(V3_DUTCH_ORDER_ABI, serialized); + const [ + [ + [ + reactor, + swapper, + nonce, + deadline, + additionalValidationContract, + additionalValidationData + ], + cosigner, + [ + token, + startAmount, + [relativeBlocks, relativeAmount], + maxAmount + ], + outputs, + [decayStartBlock, exclusiveFiller, exclusivityOverrideBps, inputOverride, outputOverrides], + cosignature + ], + ] = decoded; + + const decodedRelativeBlocks = decodeRelativeBlocks(relativeBlocks); + + return { + reactor, + swapper, + nonce, + deadline: deadline.toNumber(), + additionalValidationContract, + additionalValidationData, + cosigner, + input: { + token, + startAmount, + curve: { + relativeBlocks: decodedRelativeBlocks, + relativeAmount + }, + maxAmount + }, + outputs: outputs.map( + ([token, startAmount,[relativeBlocks, relativeAmount],recipient]: [ + string, number, [BigNumber, number[]], string, boolean + ]) => ({ + token, + startAmount, + curve: { + relativeBlocks: decodeRelativeBlocks(relativeBlocks), + relativeAmount + }, + recipient + })), + cosignerData: { + decayStartBlock: decayStartBlock.toNumber(), + exclusiveFiller, + exclusivityOverrideBps: exclusivityOverrideBps, + inputOverride: inputOverride, + outputOverrides + }, + cosignature, + }; +} + +function encodeRelativeBlocks(relativeBlocks: number[]): BigNumber { + let packedData = BigNumber.from(0); + for (let i = 0; i < relativeBlocks.length; i++) { + packedData = packedData.or(BigNumber.from(relativeBlocks[i]).shl(i * 16)); + } + return packedData; +} + +function decodeRelativeBlocks(packedData: BigNumber): number[] { + let relativeBlocks: number[] = []; + for (let i = 0; i < 16; i++) { + const block = packedData.shr(i * 16).toNumber() & 0xFFFF; + if (block !== 0) { + relativeBlocks.push(block); + } + } + return relativeBlocks; +} \ No newline at end of file diff --git a/sdks/uniswapx-sdk/src/order/types.ts b/sdks/uniswapx-sdk/src/order/types.ts index 7e497ea5..39b8e712 100644 --- a/sdks/uniswapx-sdk/src/order/types.ts +++ b/sdks/uniswapx-sdk/src/order/types.ts @@ -114,3 +114,34 @@ export type PriorityInputJSON = Omit< export type PriorityOutputJSON = PriorityInputJSON & { recipient: string; }; + +export type V3DutchInput = { + readonly token: string; + readonly startAmount: BigNumber; + readonly curve: V3Decay; + readonly maxAmount: BigNumber; +}; + +export type V3DutchInputJSON = Omit & { + startAmount: string; + curve: V3Decay; + maxAmount: string; +}; + +export type V3Decay = { + relativeBlocks: number[]; + relativeAmount: BigNumber[]; //amounts plural + // relativeBlocks: number[]; + // relativeAmount: number[]; +}; + +export type V3DutchOutput = { + readonly token: string; + readonly startAmount: BigNumber; + readonly curve: V3Decay; + readonly recipient: string; +}; + +export type V3DutchOutputJSON = Omit & { + startAmount: string; +}; \ No newline at end of file From 5ea0ce8dacd981f080d7fe3245cdf4e511e51ac1 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Tue, 3 Sep 2024 15:49:39 -0400 Subject: [PATCH 02/41] feat, test: create orders from JSON --- .../src/order/V3DutchOrder.test.ts | 34 +++++++++++++++++-- sdks/uniswapx-sdk/src/order/V3DutchOrder.ts | 32 +++++++++++++++++ 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts b/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts index 311dc742..131c51b1 100644 --- a/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts +++ b/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts @@ -1,6 +1,6 @@ import { BigNumber, ethers } from "ethers"; import { expect } from "chai"; -import { CosignedV3DutchOrder, CosignedV3DutchOrderInfo } from "./V3DutchOrder"; +import { CosignedV3DutchOrder, CosignedV3DutchOrderInfo, UnsignedV3DutchOrder, UnsignedV3DutchOrderInfoJSON } from "./V3DutchOrder"; const TIME= 1725379823; const BLOCK_NUMBER = 20671221; @@ -64,12 +64,40 @@ describe("V3DutchOrder", () => { it("Parses a serialized v3 order", () => { const orderInfo = getFullOrderInfo({}); const order = new CosignedV3DutchOrder(orderInfo, CHAIN_ID); - console.log(order); const seralized = order.serialize(); - console.log(seralized); const parsed = CosignedV3DutchOrder.parse(seralized, CHAIN_ID); expect(parsed.info).to.deep.eq(orderInfo); } ); + it("parses inner v3 order with no cosigner overrides", () => { + const orderInfoJSON : UnsignedV3DutchOrderInfoJSON = { + ...getFullOrderInfo({}), + nonce: "21", + input: { + token: INPUT_TOKEN, + startAmount: "1000000", + curve: { + relativeBlocks: [1], + relativeAmount: [BigNumber.from(1), BigNumber.from(2), BigNumber.from(3), BigNumber.from(4)] + }, + maxAmount: "1000001" + }, + outputs: [ + { + token: OUTPUT_TOKEN, + startAmount: "1000000", + curve: { + relativeBlocks: [1], + relativeAmount: [BigNumber.from(1), BigNumber.from(2), BigNumber.from(3), BigNumber.from(4)] + }, + recipient: ethers.constants.AddressZero, + } + ] + }; + const order = UnsignedV3DutchOrder.fromJSON(orderInfoJSON, CHAIN_ID); + expect(order.info.input.startAmount.toString()).to.equal("1000000"); + expect(order.info.outputs[0].startAmount.toString()).to.eq("1000000"); + }); + }); \ No newline at end of file diff --git a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts index 5f36388e..32fdfc33 100644 --- a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts +++ b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts @@ -104,6 +104,38 @@ export class UnsignedV3DutchOrder implements OffChainOrder { this.permit2Address = getPermit2(chainId, _permit2Address); } + static fromJSON( + json: UnsignedV3DutchOrderInfoJSON, + chainId: number, + _permit2Address?: string + ): UnsignedV3DutchOrder { + return new UnsignedV3DutchOrder( + { + ...json, + nonce: BigNumber.from(json.nonce), + input: { + ...json.input, + startAmount: BigNumber.from(json.input.startAmount), + curve: { + relativeBlocks: json.input.curve.relativeBlocks, + relativeAmount: json.input.curve.relativeAmount.map(BigNumber.from) + }, + maxAmount: BigNumber.from(json.input.maxAmount) + }, + outputs: json.outputs.map(output => ({ + ...output, + startAmount: BigNumber.from(output.startAmount), + curve: { + relativeBlocks: output.curve.relativeBlocks, + relativeAmount: output.curve.relativeAmount.map(BigNumber.from) + } + })) + }, + chainId, + _permit2Address + ); + } + /** * @inheritdoc order */ From 89cbd0efc48bd29af88a87bd3d26fd74442e3810 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Tue, 3 Sep 2024 16:33:55 -0400 Subject: [PATCH 03/41] feat, test: cosigning v3 orders --- .../src/order/V3DutchOrder.test.ts | 38 ++++++++++++++-- sdks/uniswapx-sdk/src/order/V3DutchOrder.ts | 45 ++++++++++++++++++- 2 files changed, 79 insertions(+), 4 deletions(-) diff --git a/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts b/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts index 131c51b1..ea251133 100644 --- a/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts +++ b/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts @@ -22,7 +22,7 @@ describe("V3DutchOrder", () => { expect(BLOCK_NUMBER).to.be.greaterThan(0); }); - const getFullOrderInfo = ( data: Partial): CosignedV3DutchOrderInfo => { + const getFullOrderInfo = ( data: Partial): CosignedV3DutchOrderInfo => { return Object.assign( { reactor: ethers.constants.AddressZero, @@ -59,8 +59,6 @@ describe("V3DutchOrder", () => { ); }; - - it("Parses a serialized v3 order", () => { const orderInfo = getFullOrderInfo({}); const order = new CosignedV3DutchOrder(orderInfo, CHAIN_ID); @@ -99,5 +97,39 @@ describe("V3DutchOrder", () => { expect(order.info.input.startAmount.toString()).to.equal("1000000"); expect(order.info.outputs[0].startAmount.toString()).to.eq("1000000"); }); + + it("valid signature over inner order", async () => { + const fullOrderInfo = getFullOrderInfo({}); + const order = new UnsignedV3DutchOrder(fullOrderInfo, 1); + const wallet = ethers.Wallet.createRandom(); + + const { domain, types, values } = order.permitData(); + const signature = await wallet._signTypedData(domain, types, values); + expect(order.getSigner(signature)).equal(await wallet.getAddress()); + const fullOrder = CosignedV3DutchOrder.fromUnsignedOrder( + order, + fullOrderInfo.cosignerData, + fullOrderInfo.cosignature + ); + expect(fullOrder.getSigner(signature)).equal(await wallet.getAddress()); + }); + + it("validates cosignature over (hash || cosignerData)", async () => { + const wallet = ethers.Wallet.createRandom(); + const orderInfo = getFullOrderInfo({ + cosigner: await wallet.getAddress(), + }); + const order = new UnsignedV3DutchOrder(orderInfo, 1); + const fullOrderHash = order.cosignatureHash(orderInfo.cosignerData); + const cosignature = await wallet.signMessage(fullOrderHash); + const signedOrder = CosignedV3DutchOrder.fromUnsignedOrder( + order, + COSIGNER_DATA, + cosignature + ); + + expect(signedOrder.recoverCosigner()).equal(await wallet.getAddress()); + }); + }); \ No newline at end of file diff --git a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts index 32fdfc33..bbe13d9a 100644 --- a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts +++ b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts @@ -270,9 +270,47 @@ export class UnsignedV3DutchOrder implements OffChainOrder { .hash(this.witnessInfo()); } + cosignatureHash(cosignerData: CosignerData): string { + const abiCoder = new ethers.utils.AbiCoder(); + return ethers.utils.solidityKeccak256( + ["bytes32", "bytes"], + [ + this.hash(), + abiCoder.encode( + [COSIGNER_DATA_TUPLE_ABI], + [ + [ + cosignerData.decayStartBlock, + cosignerData.exclusiveFiller, + cosignerData.exclusivityOverrideBps, + cosignerData.inputOverride, + cosignerData.outputOverrides + ] + ] + ) + ] + ) + } + } export class CosignedV3DutchOrder extends UnsignedV3DutchOrder { + static fromUnsignedOrder( + order: UnsignedV3DutchOrder, + cosignerData: CosignerData, + cosignature: string + ): CosignedV3DutchOrder { + return new CosignedV3DutchOrder( + { + ...order.info, + cosignerData, + cosignature + }, + order.chainId, + order.permit2Address + ); + } + constructor( public readonly info: CosignedV3DutchOrderInfo, public readonly chainId: number, @@ -330,8 +368,13 @@ export class CosignedV3DutchOrder extends UnsignedV3DutchOrder { ], ]); } - + recoverCosigner(): string { + return ethers.utils.verifyMessage( + this.cosignatureHash(this.info.cosignerData), + this.info.cosignature + ); + } } function parseSerializedOrder(serialized: string): CosignedV3DutchOrderInfo { From c409254c74b89de87c9802d02c309d6b16d11ade Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Tue, 3 Sep 2024 16:43:40 -0400 Subject: [PATCH 04/41] fix: linting --- .../uniswapx-sdk/src/order/V3DutchOrder.test.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts b/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts index ea251133..79db4f26 100644 --- a/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts +++ b/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts @@ -1,5 +1,6 @@ -import { BigNumber, ethers } from "ethers"; import { expect } from "chai"; +import { BigNumber, ethers } from "ethers"; + import { CosignedV3DutchOrder, CosignedV3DutchOrderInfo, UnsignedV3DutchOrder, UnsignedV3DutchOrderInfoJSON } from "./V3DutchOrder"; const TIME= 1725379823; @@ -38,9 +39,9 @@ describe("V3DutchOrder", () => { startAmount: RAW_AMOUNT, curve: { relativeBlocks: [1], - relativeAmount: [BigNumber.from(1), BigNumber.from(2), BigNumber.from(3), BigNumber.from(4)] // 1e-18, 2e-18, 3e-18, 4e-18 + relativeAmount: [BigNumber.from(1), BigNumber.from(2), BigNumber.from(3), BigNumber.from(4)], // 1e-18, 2e-18, 3e-18, 4e-18 }, - maxAmount: RAW_AMOUNT.add(1) + maxAmount: RAW_AMOUNT.add(1), }, outputs: [ { @@ -48,10 +49,10 @@ describe("V3DutchOrder", () => { startAmount: RAW_AMOUNT, curve: { relativeBlocks: [1], - relativeAmount: [BigNumber.from(1), BigNumber.from(2), BigNumber.from(3), BigNumber.from(4)] // 1e-18, 2e-18, 3e-18, 4e-18 + relativeAmount: [BigNumber.from(1), BigNumber.from(2), BigNumber.from(3), BigNumber.from(4)], // 1e-18, 2e-18, 3e-18, 4e-18 }, recipient: ethers.constants.AddressZero, - } + }, ], cosignature: "0x", }, @@ -77,9 +78,9 @@ describe("V3DutchOrder", () => { startAmount: "1000000", curve: { relativeBlocks: [1], - relativeAmount: [BigNumber.from(1), BigNumber.from(2), BigNumber.from(3), BigNumber.from(4)] + relativeAmount: [BigNumber.from(1), BigNumber.from(2), BigNumber.from(3), BigNumber.from(4)], }, - maxAmount: "1000001" + maxAmount: "1000001", }, outputs: [ { @@ -87,7 +88,7 @@ describe("V3DutchOrder", () => { startAmount: "1000000", curve: { relativeBlocks: [1], - relativeAmount: [BigNumber.from(1), BigNumber.from(2), BigNumber.from(3), BigNumber.from(4)] + relativeAmount: [BigNumber.from(1), BigNumber.from(2), BigNumber.from(3), BigNumber.from(4)], }, recipient: ethers.constants.AddressZero, } From fdd909e3a1579fb28d9d0b3c57a1a648b86e594a Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Tue, 3 Sep 2024 16:49:18 -0400 Subject: [PATCH 05/41] fix: more linting --- sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts | 6 +++--- sdks/uniswapx-sdk/src/order/V3DutchOrder.ts | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts b/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts index 79db4f26..9944ed08 100644 --- a/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts +++ b/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts @@ -15,7 +15,7 @@ const COSIGNER_DATA = { exclusiveFiller: ethers.constants.AddressZero, exclusivityOverrideBps: BigNumber.from(0), inputOverride: RAW_AMOUNT, - outputOverrides: [RAW_AMOUNT.mul(102).div(100)] + outputOverrides: [RAW_AMOUNT.mul(102).div(100)], }; describe("V3DutchOrder", () => { @@ -91,8 +91,8 @@ describe("V3DutchOrder", () => { relativeAmount: [BigNumber.from(1), BigNumber.from(2), BigNumber.from(3), BigNumber.from(4)], }, recipient: ethers.constants.AddressZero, - } - ] + }, + ], }; const order = UnsignedV3DutchOrder.fromJSON(orderInfoJSON, CHAIN_ID); expect(order.info.input.startAmount.toString()).to.equal("1000000"); diff --git a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts index bbe13d9a..a9e1a889 100644 --- a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts +++ b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts @@ -1,8 +1,9 @@ -import { getPermit2 } from "../utils"; -import { V3DutchInput, OffChainOrder, OrderInfo, V3DutchInputJSON, V3DutchOutput, V3DutchOutputJSON, BlockOverrides } from "./types"; import { BigNumber, ethers } from "ethers"; -import { PermitTransferFromData, SignatureTransfer, PermitTransferFrom, Witness } from "@uniswap/permit2-sdk"; import { SignatureLike } from "@ethersproject/bytes"; +import { PermitTransferFrom, PermitTransferFromData, SignatureTransfer, Witness } from "@uniswap/permit2-sdk"; + +import { V3DutchInput, OffChainOrder, OrderInfo, V3DutchInputJSON, V3DutchOutput, V3DutchOutputJSON, BlockOverrides } from "./types"; +import { getPermit2 } from "../utils"; export type CosignerDataJSON = { decayStartBlock: number; From 612d1bff82fd2c32b6e2d2faa19e1309497cce69 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Tue, 3 Sep 2024 17:15:58 -0400 Subject: [PATCH 06/41] refactor: update relativeAmounts name --- .../src/order/V3DutchOrder.test.ts | 19 +++-- sdks/uniswapx-sdk/src/order/V3DutchOrder.ts | 81 ++++++++++--------- sdks/uniswapx-sdk/src/order/types.ts | 4 +- 3 files changed, 58 insertions(+), 46 deletions(-) diff --git a/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts b/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts index 9944ed08..161c2c99 100644 --- a/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts +++ b/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts @@ -39,7 +39,7 @@ describe("V3DutchOrder", () => { startAmount: RAW_AMOUNT, curve: { relativeBlocks: [1], - relativeAmount: [BigNumber.from(1), BigNumber.from(2), BigNumber.from(3), BigNumber.from(4)], // 1e-18, 2e-18, 3e-18, 4e-18 + relativeAmounts: [BigNumber.from(1), BigNumber.from(2), BigNumber.from(3), BigNumber.from(4)], // 1e-18, 2e-18, 3e-18, 4e-18 }, maxAmount: RAW_AMOUNT.add(1), }, @@ -49,7 +49,7 @@ describe("V3DutchOrder", () => { startAmount: RAW_AMOUNT, curve: { relativeBlocks: [1], - relativeAmount: [BigNumber.from(1), BigNumber.from(2), BigNumber.from(3), BigNumber.from(4)], // 1e-18, 2e-18, 3e-18, 4e-18 + relativeAmounts: [BigNumber.from(1), BigNumber.from(2), BigNumber.from(3), BigNumber.from(4)], // 1e-18, 2e-18, 3e-18, 4e-18 }, recipient: ethers.constants.AddressZero, }, @@ -78,7 +78,7 @@ describe("V3DutchOrder", () => { startAmount: "1000000", curve: { relativeBlocks: [1], - relativeAmount: [BigNumber.from(1), BigNumber.from(2), BigNumber.from(3), BigNumber.from(4)], + relativeAmounts: [BigNumber.from(1), BigNumber.from(2), BigNumber.from(3), BigNumber.from(4)], }, maxAmount: "1000001", }, @@ -88,7 +88,7 @@ describe("V3DutchOrder", () => { startAmount: "1000000", curve: { relativeBlocks: [1], - relativeAmount: [BigNumber.from(1), BigNumber.from(2), BigNumber.from(3), BigNumber.from(4)], + relativeAmounts: [BigNumber.from(1), BigNumber.from(2), BigNumber.from(3), BigNumber.from(4)], }, recipient: ethers.constants.AddressZero, }, @@ -132,5 +132,12 @@ describe("V3DutchOrder", () => { expect(signedOrder.recoverCosigner()).equal(await wallet.getAddress()); }); - -}); \ No newline at end of file + // describe("resolve DutchV3 orders", () => { + // it("resolves before decayStartTime", () => { + // const order = new CosignedV3DutchOrder(getFullOrderInfo({}), CHAIN_ID); + // const resolved = order.resolve( + // }); + // }); TODO + +}); + diff --git a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts index a9e1a889..71d7114b 100644 --- a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts +++ b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts @@ -1,10 +1,11 @@ -import { BigNumber, ethers } from "ethers"; import { SignatureLike } from "@ethersproject/bytes"; import { PermitTransferFrom, PermitTransferFromData, SignatureTransfer, Witness } from "@uniswap/permit2-sdk"; +import { BigNumber, ethers } from "ethers"; -import { V3DutchInput, OffChainOrder, OrderInfo, V3DutchInputJSON, V3DutchOutput, V3DutchOutputJSON, BlockOverrides } from "./types"; import { getPermit2 } from "../utils"; +import { BlockOverrides, OffChainOrder, OrderInfo, V3DutchInput, V3DutchInputJSON, V3DutchOutput, V3DutchOutputJSON } from "./types"; + export type CosignerDataJSON = { decayStartBlock: number; exclusiveFiller: string; @@ -78,8 +79,8 @@ const V3_DUTCH_ORDER_TYPES = { ], V3Decay: [ { name: "relativeBlocks", type: "uint256" }, - { name: "relativeAmount", type: "int256[]" }, - ] + { name: "relativeAmounts", type: "int256[]" }, + ], }; const V3_DUTCH_ORDER_ABI = [ @@ -90,8 +91,8 @@ const V3_DUTCH_ORDER_ABI = [ "tuple(address,uint256,tuple(uint256,int256[]),uint256)", // V3DutchInput "tuple(address,uint256,tuple(uint256,int256[]),address)[]", // V3DutchOutput COSIGNER_DATA_TUPLE_ABI, - "bytes" // Cosignature - ].join(",") + ")" + "bytes", // Cosignature + ].join(",") + ")", ]; export class UnsignedV3DutchOrder implements OffChainOrder { @@ -119,17 +120,17 @@ export class UnsignedV3DutchOrder implements OffChainOrder { startAmount: BigNumber.from(json.input.startAmount), curve: { relativeBlocks: json.input.curve.relativeBlocks, - relativeAmount: json.input.curve.relativeAmount.map(BigNumber.from) + relativeAmounts: json.input.curve.relativeAmounts.map(BigNumber.from), }, - maxAmount: BigNumber.from(json.input.maxAmount) + maxAmount: BigNumber.from(json.input.maxAmount), }, outputs: json.outputs.map(output => ({ ...output, startAmount: BigNumber.from(output.startAmount), curve: { relativeBlocks: output.curve.relativeBlocks, - relativeAmount: output.curve.relativeAmount.map(BigNumber.from) - } + relativeAmounts: output.curve.relativeAmounts.map(BigNumber.from), + }, })) }, chainId, @@ -158,23 +159,23 @@ export class UnsignedV3DutchOrder implements OffChainOrder { this.info.nonce, this.info.deadline, this.info.additionalValidationContract, - this.info.additionalValidationData + this.info.additionalValidationData, ], this.info.cosigner, [ this.info.input.token, this.info.input.startAmount, - [encodedRelativeBlocks, this.info.input.curve.relativeAmount], - this.info.input.maxAmount + [encodedRelativeBlocks, this.info.input.curve.relativeAmounts], + this.info.input.maxAmount, ], this.info.outputs.map(output => [ output.token, output.startAmount, - [encodedRelativeBlocks, output.curve.relativeAmount], - output.recipient + [encodedRelativeBlocks, output.curve.relativeAmounts], + output.recipient, ]), [0, ethers.constants.AddressZero, 0, 0, [0]], - "0x" + "0x", ], ]); } @@ -195,14 +196,14 @@ export class UnsignedV3DutchOrder implements OffChainOrder { token: this.info.input.token, startAmount: this.info.input.startAmount.toString(), curve: this.info.input.curve, - maxAmount: this.info.input.maxAmount.toString() + maxAmount: this.info.input.maxAmount.toString(), }, outputs: this.info.outputs.map(output => ({ token: output.token, startAmount: output.startAmount.toString(), curve: output.curve, - recipient: output.recipient - })) + recipient: output.recipient, + })), } }; @@ -211,7 +212,7 @@ export class UnsignedV3DutchOrder implements OffChainOrder { this.toPermit(), this.permit2Address, this.chainId, - this.witness() + this.witness(), ) as PermitTransferFromData; } @@ -235,11 +236,11 @@ export class UnsignedV3DutchOrder implements OffChainOrder { nonce: this.info.nonce, deadline: this.info.deadline, additionalValidationContract: this.info.additionalValidationContract, - additionalValidationData: this.info.additionalValidationData + additionalValidationData: this.info.additionalValidationData, }, cosigner: this.info.cosigner, baseInput: this.info.input, - baseOutputs: this.info.outputs + baseOutputs: this.info.outputs, } } @@ -258,7 +259,7 @@ export class UnsignedV3DutchOrder implements OffChainOrder { this.toPermit(), this.permit2Address, this.chainId, - this.witness() + this.witness(), ), signature ) @@ -286,9 +287,9 @@ export class UnsignedV3DutchOrder implements OffChainOrder { cosignerData.exclusivityOverrideBps, cosignerData.inputOverride, cosignerData.outputOverrides - ] - ] - ) + ], + ], + ), ] ) } @@ -343,29 +344,29 @@ export class CosignedV3DutchOrder extends UnsignedV3DutchOrder { this.info.nonce, this.info.deadline, this.info.additionalValidationContract, - this.info.additionalValidationData + this.info.additionalValidationData, ], this.info.cosigner, [ this.info.input.token, this.info.input.startAmount, - [encodedRelativeBlocks, this.info.input.curve.relativeAmount], - this.info.input.maxAmount + [encodedRelativeBlocks, this.info.input.curve.relativeAmounts], + this.info.input.maxAmount, ], this.info.outputs.map(output => [ output.token, output.startAmount, - [encodedRelativeBlocks, output.curve.relativeAmount], - output.recipient + [encodedRelativeBlocks, output.curve.relativeAmounts], + output.recipient, ]), [ this.info.cosignerData.decayStartBlock, this.info.cosignerData.exclusiveFiller, this.info.cosignerData.exclusivityOverrideBps, this.info.cosignerData.inputOverride.toString(), - this.info.cosignerData.outputOverrides.map(override => override.toString()) + this.info.cosignerData.outputOverrides.map(override => override.toString()), ], - this.info.cosignature + this.info.cosignature, ], ]); } @@ -376,6 +377,12 @@ export class CosignedV3DutchOrder extends UnsignedV3DutchOrder { this.info.cosignature ); } + + // resolve(options: OrderResolutionOptions): ResolvedUniswapXOrder { + // return { + + // } + // } TODO } function parseSerializedOrder(serialized: string): CosignedV3DutchOrderInfo { @@ -395,7 +402,7 @@ function parseSerializedOrder(serialized: string): CosignedV3DutchOrderInfo { [ token, startAmount, - [relativeBlocks, relativeAmount], + [relativeBlocks, relativeAmounts], maxAmount ], outputs, @@ -419,19 +426,19 @@ function parseSerializedOrder(serialized: string): CosignedV3DutchOrderInfo { startAmount, curve: { relativeBlocks: decodedRelativeBlocks, - relativeAmount + relativeAmounts }, maxAmount }, outputs: outputs.map( - ([token, startAmount,[relativeBlocks, relativeAmount],recipient]: [ + ([token, startAmount,[relativeBlocks, relativeAmounts],recipient]: [ string, number, [BigNumber, number[]], string, boolean ]) => ({ token, startAmount, curve: { relativeBlocks: decodeRelativeBlocks(relativeBlocks), - relativeAmount + relativeAmounts }, recipient })), diff --git a/sdks/uniswapx-sdk/src/order/types.ts b/sdks/uniswapx-sdk/src/order/types.ts index 39b8e712..e93a5f3a 100644 --- a/sdks/uniswapx-sdk/src/order/types.ts +++ b/sdks/uniswapx-sdk/src/order/types.ts @@ -130,9 +130,7 @@ export type V3DutchInputJSON = Omit Date: Thu, 5 Sep 2024 14:38:05 -0400 Subject: [PATCH 07/41] feat, test: resolve & decay with math tests --- sdks/uniswapx-sdk/src/order/V2DutchOrder.ts | 5 +- .../src/order/V3DutchOrder.test.ts | 122 +++++++++++++++--- sdks/uniswapx-sdk/src/order/V3DutchOrder.ts | 65 +++++++--- sdks/uniswapx-sdk/src/order/index.ts | 5 + sdks/uniswapx-sdk/src/order/types.ts | 5 + .../uniswapx-sdk/src/utils/dutchBlockDecay.ts | 115 +++++++++++++++++ 6 files changed, 276 insertions(+), 41 deletions(-) create mode 100644 sdks/uniswapx-sdk/src/utils/dutchBlockDecay.ts diff --git a/sdks/uniswapx-sdk/src/order/V2DutchOrder.ts b/sdks/uniswapx-sdk/src/order/V2DutchOrder.ts index 105047c2..a62102af 100644 --- a/sdks/uniswapx-sdk/src/order/V2DutchOrder.ts +++ b/sdks/uniswapx-sdk/src/order/V2DutchOrder.ts @@ -22,6 +22,7 @@ import { OrderResolutionOptions, } from "./types"; import { CustomOrderValidation, parseValidation } from "./validation"; +import { originalIfZero } from "."; export type CosignerData = { decayStartTime: number; @@ -557,10 +558,6 @@ export class CosignedV2DutchOrder extends UnsignedV2DutchOrder { } } -function originalIfZero(value: BigNumber, original: BigNumber): BigNumber { - return value.isZero() ? original : value; -} - function parseSerializedOrder(serialized: string): CosignedV2DutchOrderInfo { const abiCoder = new ethers.utils.AbiCoder(); const decoded = abiCoder.decode(V2_DUTCH_ORDER_ABI, serialized); diff --git a/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts b/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts index 161c2c99..df9f9f88 100644 --- a/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts +++ b/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts @@ -2,15 +2,16 @@ import { expect } from "chai"; import { BigNumber, ethers } from "ethers"; import { CosignedV3DutchOrder, CosignedV3DutchOrderInfo, UnsignedV3DutchOrder, UnsignedV3DutchOrderInfoJSON } from "./V3DutchOrder"; +import { getEndAmount } from "../utils/dutchBlockDecay"; const TIME= 1725379823; const BLOCK_NUMBER = 20671221; -const RAW_AMOUNT = BigNumber.from("1000000"); +const RAW_AMOUNT = BigNumber.from("2121000"); const INPUT_TOKEN = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; const OUTPUT_TOKEN = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"; const CHAIN_ID = 1; -const COSIGNER_DATA = { +const COSIGNER_DATA_WITH_OVERRIDES = { decayStartBlock: BLOCK_NUMBER, exclusiveFiller: ethers.constants.AddressZero, exclusivityOverrideBps: BigNumber.from(0), @@ -18,6 +19,14 @@ const COSIGNER_DATA = { outputOverrides: [RAW_AMOUNT.mul(102).div(100)], }; +const COSIGNER_DATA_WITHOUT_OVERRIDES = { + decayStartBlock: BLOCK_NUMBER, + exclusiveFiller: ethers.constants.AddressZero, + exclusivityOverrideBps: BigNumber.from(0), + inputOverride: BigNumber.from(0), + outputOverrides: [BigNumber.from(0)], +}; + describe("V3DutchOrder", () => { it("should get block number", () => { expect(BLOCK_NUMBER).to.be.greaterThan(0); @@ -33,22 +42,22 @@ describe("V3DutchOrder", () => { additionalValidationContract: ethers.constants.AddressZero, additionalValidationData: "0x", cosigner: ethers.constants.AddressZero, - cosignerData: COSIGNER_DATA, + cosignerData: COSIGNER_DATA_WITH_OVERRIDES, input: { token: INPUT_TOKEN, startAmount: RAW_AMOUNT, curve: { - relativeBlocks: [1], - relativeAmounts: [BigNumber.from(1), BigNumber.from(2), BigNumber.from(3), BigNumber.from(4)], // 1e-18, 2e-18, 3e-18, 4e-18 + relativeBlocks: [1], //TODO: can we have relativeblocks be an array of just 0 + relativeAmounts: [BigNumber.from(0)], // 1e-18, 2e-18, 3e-18, 4e-18 }, - maxAmount: RAW_AMOUNT.add(1), + maxAmount: RAW_AMOUNT, //we don't want input to change, we're testing for decaying output }, outputs: [ { token: OUTPUT_TOKEN, startAmount: RAW_AMOUNT, curve: { - relativeBlocks: [1], + relativeBlocks: [1,2,3,4], relativeAmounts: [BigNumber.from(1), BigNumber.from(2), BigNumber.from(3), BigNumber.from(4)], // 1e-18, 2e-18, 3e-18, 4e-18 }, recipient: ethers.constants.AddressZero, @@ -60,6 +69,11 @@ describe("V3DutchOrder", () => { ); }; + const getFullOrderInfoWithoutOverrides: CosignedV3DutchOrderInfo = { + ...getFullOrderInfo({}), + cosignerData: COSIGNER_DATA_WITHOUT_OVERRIDES, + } + it("Parses a serialized v3 order", () => { const orderInfo = getFullOrderInfo({}); const order = new CosignedV3DutchOrder(orderInfo, CHAIN_ID); @@ -69,7 +83,7 @@ describe("V3DutchOrder", () => { } ); - it("parses inner v3 order with no cosigner overrides", () => { + it("parses inner v3 order with no cosigner overrides, both input and output curves", () => { const orderInfoJSON : UnsignedV3DutchOrderInfoJSON = { ...getFullOrderInfo({}), nonce: "21", @@ -77,7 +91,7 @@ describe("V3DutchOrder", () => { token: INPUT_TOKEN, startAmount: "1000000", curve: { - relativeBlocks: [1], + relativeBlocks: [1,2,3,4], relativeAmounts: [BigNumber.from(1), BigNumber.from(2), BigNumber.from(3), BigNumber.from(4)], }, maxAmount: "1000001", @@ -87,7 +101,7 @@ describe("V3DutchOrder", () => { token: OUTPUT_TOKEN, startAmount: "1000000", curve: { - relativeBlocks: [1], + relativeBlocks: [1,2,3,4], relativeAmounts: [BigNumber.from(1), BigNumber.from(2), BigNumber.from(3), BigNumber.from(4)], }, recipient: ethers.constants.AddressZero, @@ -125,19 +139,91 @@ describe("V3DutchOrder", () => { const cosignature = await wallet.signMessage(fullOrderHash); const signedOrder = CosignedV3DutchOrder.fromUnsignedOrder( order, - COSIGNER_DATA, + COSIGNER_DATA_WITH_OVERRIDES, cosignature ); expect(signedOrder.recoverCosigner()).equal(await wallet.getAddress()); }); - // describe("resolve DutchV3 orders", () => { - // it("resolves before decayStartTime", () => { - // const order = new CosignedV3DutchOrder(getFullOrderInfo({}), CHAIN_ID); - // const resolved = order.resolve( - // }); - // }); TODO + describe("resolve DutchV3 orders", () => { + it("resolves before decayStartTime", () => { + const order = new CosignedV3DutchOrder(getFullOrderInfo({}), CHAIN_ID); + const resolved = order.resolve({ + currentBlock: BLOCK_NUMBER - 1, //no decay yet + }); + expect(resolved.input.token).eq(order.info.input.token); + expect(resolved.input.amount).eq(order.info.cosignerData.inputOverride); + expect(resolved.outputs[0].token).eq(order.info.outputs[0].token); + expect(resolved.outputs[0].amount).eq( + order.info.cosignerData.outputOverrides[0] + ); + }); -}); + it("resolves with original value when overrides==0", () => { + const order = new CosignedV3DutchOrder( + getFullOrderInfo({ + cosignerData: { + ...COSIGNER_DATA_WITH_OVERRIDES, + inputOverride: BigNumber.from(0), + outputOverrides: [BigNumber.from(0)], + }, + }), + CHAIN_ID + ); + const resolved = order.resolve({ + currentBlock: BLOCK_NUMBER - 1, //no decay yet + }); + expect(resolved.input.token).eq(order.info.input.token); + expect(resolved.input.amount).eq(order.info.input.startAmount); + expect(resolved.outputs[0].token).eq(order.info.outputs[0].token); + expect(resolved.outputs[0].amount).eq(order.info.outputs[0].startAmount); + }); + + it("resolves at decayStartTime", () => { + const order = new CosignedV3DutchOrder(getFullOrderInfo({}), CHAIN_ID); + const resolved = order.resolve({ + currentBlock: BLOCK_NUMBER, + }); + expect(resolved.input.token).eq(order.info.input.token); + expect(resolved.input.amount).eq(order.info.cosignerData.inputOverride); + expect(resolved.outputs.length).eq(1); + expect(resolved.outputs[0].token).eq(order.info.outputs[0].token); + expect(resolved.outputs[0].amount).eq(order.info.cosignerData.outputOverrides[0]); + }); + it("resolves at decayEndtime without overrides", () => { + const order = new CosignedV3DutchOrder(getFullOrderInfoWithoutOverrides, CHAIN_ID); + const relativeBlocks = order.info.outputs[0].curve.relativeBlocks; + const resolved = order.resolve({ + currentBlock: BLOCK_NUMBER + relativeBlocks[relativeBlocks.length - 1], + }); + expect(resolved.input.token).eq(order.info.input.token); + expect(resolved.outputs[0].token).eq(order.info.outputs[0].token); + const endAmount = getEndAmount({ + decayStartBlock: BLOCK_NUMBER, + startAmount: order.info.outputs[0].startAmount, + relativeBlocks: order.info.outputs[0].curve.relativeBlocks, + relativeAmounts: order.info.outputs[0].curve.relativeAmounts, + }); + expect(resolved.outputs[0].amount.toNumber()).eq(endAmount.toNumber()); + }); + + it("resolves after decayEndTime without overrides", () => { + const order = new CosignedV3DutchOrder(getFullOrderInfoWithoutOverrides, CHAIN_ID); + const resolved = order.resolve({ + currentBlock: BLOCK_NUMBER + 42, + }); + expect(resolved.input.token).eq(order.info.input.token); + expect(resolved.outputs[0].token).eq(order.info.outputs[0].token); + const endAmount = getEndAmount({ + decayStartBlock: BLOCK_NUMBER, + startAmount: order.info.outputs[0].startAmount, + relativeBlocks: order.info.outputs[0].curve.relativeBlocks, + relativeAmounts: order.info.outputs[0].curve.relativeAmounts, + }); + expect(resolved.outputs[0].amount.toNumber()).eq(endAmount.toNumber()); //deep eq on bignumber failed + }); + //TODO: resolves for overrides + }); +}); diff --git a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts index 71d7114b..299978fa 100644 --- a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts +++ b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts @@ -2,9 +2,11 @@ import { SignatureLike } from "@ethersproject/bytes"; import { PermitTransferFrom, PermitTransferFromData, SignatureTransfer, Witness } from "@uniswap/permit2-sdk"; import { BigNumber, ethers } from "ethers"; -import { getPermit2 } from "../utils"; +import { getPermit2, ResolvedUniswapXOrder } from "../utils"; -import { BlockOverrides, OffChainOrder, OrderInfo, V3DutchInput, V3DutchInputJSON, V3DutchOutput, V3DutchOutputJSON } from "./types"; +import { BlockOverrides, OffChainOrder, OrderInfo, V3DutchInput, V3DutchInputJSON, V3DutchOutput, V3DutchOutputJSON, V3OrderResolutionOptions } from "./types"; +import { getBlockDecayedAmount } from "../utils/dutchBlockDecay"; +import { originalIfZero } from "."; export type CosignerDataJSON = { decayStartBlock: number; @@ -131,7 +133,7 @@ export class UnsignedV3DutchOrder implements OffChainOrder { relativeBlocks: output.curve.relativeBlocks, relativeAmounts: output.curve.relativeAmounts.map(BigNumber.from), }, - })) + })), }, chainId, _permit2Address @@ -220,7 +222,7 @@ export class UnsignedV3DutchOrder implements OffChainOrder { return { permitted: { token: this.info.input.token, - amount: this.info.input.maxAmount + amount: this.info.input.maxAmount, }, spender: this.info.reactor, nonce: this.info.nonce, @@ -286,7 +288,7 @@ export class UnsignedV3DutchOrder implements OffChainOrder { cosignerData.exclusiveFiller, cosignerData.exclusivityOverrideBps, cosignerData.inputOverride, - cosignerData.outputOverrides + cosignerData.outputOverrides, ], ], ), @@ -306,7 +308,7 @@ export class CosignedV3DutchOrder extends UnsignedV3DutchOrder { { ...order.info, cosignerData, - cosignature + cosignature, }, order.chainId, order.permit2Address @@ -334,7 +336,7 @@ export class CosignedV3DutchOrder extends UnsignedV3DutchOrder { } serialize(): string { - const encodedRelativeBlocks = encodeRelativeBlocks(this.info.input.curve.relativeBlocks); + const encodedInputRelativeBlocks = encodeRelativeBlocks(this.info.input.curve.relativeBlocks); const abiCoder = new ethers.utils.AbiCoder(); return abiCoder.encode(V3_DUTCH_ORDER_ABI, [ [ @@ -350,13 +352,13 @@ export class CosignedV3DutchOrder extends UnsignedV3DutchOrder { [ this.info.input.token, this.info.input.startAmount, - [encodedRelativeBlocks, this.info.input.curve.relativeAmounts], + [encodedInputRelativeBlocks, this.info.input.curve.relativeAmounts], this.info.input.maxAmount, ], this.info.outputs.map(output => [ output.token, output.startAmount, - [encodedRelativeBlocks, output.curve.relativeAmounts], + [encodeRelativeBlocks(output.curve.relativeBlocks), output.curve.relativeAmounts], output.recipient, ]), [ @@ -378,11 +380,36 @@ export class CosignedV3DutchOrder extends UnsignedV3DutchOrder { ); } - // resolve(options: OrderResolutionOptions): ResolvedUniswapXOrder { - // return { - - // } - // } TODO + resolve(options: V3OrderResolutionOptions): ResolvedUniswapXOrder { + return { + input: { + token: this.info.input.token, + amount: getBlockDecayedAmount( + { + decayStartBlock: this.info.cosignerData.decayStartBlock, + startAmount: originalIfZero(this.info.cosignerData.inputOverride, this.info.input.startAmount), + relativeBlocks: this.info.input.curve.relativeBlocks, + relativeAmounts: this.info.input.curve.relativeAmounts, + }, + options.currentBlock + ), + }, + outputs: this.info.outputs.map((output, idx) => { + return { + token: output.token, + amount: getBlockDecayedAmount( + { + decayStartBlock: this.info.cosignerData.decayStartBlock, + startAmount: originalIfZero(this.info.cosignerData!.outputOverrides[idx], output.startAmount), + relativeBlocks: output.curve.relativeBlocks, + relativeAmounts: output.curve.relativeAmounts, + }, + options.currentBlock + ), + } + }), + }; + } } function parseSerializedOrder(serialized: string): CosignedV3DutchOrderInfo { @@ -396,18 +423,18 @@ function parseSerializedOrder(serialized: string): CosignedV3DutchOrderInfo { nonce, deadline, additionalValidationContract, - additionalValidationData + additionalValidationData, ], cosigner, [ token, startAmount, [relativeBlocks, relativeAmounts], - maxAmount + maxAmount, ], outputs, [decayStartBlock, exclusiveFiller, exclusivityOverrideBps, inputOverride, outputOverrides], - cosignature + cosignature, ], ] = decoded; @@ -426,9 +453,9 @@ function parseSerializedOrder(serialized: string): CosignedV3DutchOrderInfo { startAmount, curve: { relativeBlocks: decodedRelativeBlocks, - relativeAmounts + relativeAmounts, }, - maxAmount + maxAmount, }, outputs: outputs.map( ([token, startAmount,[relativeBlocks, relativeAmounts],recipient]: [ diff --git a/sdks/uniswapx-sdk/src/order/index.ts b/sdks/uniswapx-sdk/src/order/index.ts index 310ae522..5d6680fb 100644 --- a/sdks/uniswapx-sdk/src/order/index.ts +++ b/sdks/uniswapx-sdk/src/order/index.ts @@ -1,3 +1,4 @@ +import { BigNumber } from "ethers"; import { DutchOrder } from "./DutchOrder"; import { CosignedPriorityOrder, UnsignedPriorityOrder } from "./PriorityOrder"; import { RelayOrder } from "./RelayOrder"; @@ -18,3 +19,7 @@ export type UniswapXOrder = | CosignedPriorityOrder; export type Order = UniswapXOrder | RelayOrder; + +export function originalIfZero(value: BigNumber, original: BigNumber): BigNumber { + return value.isZero() ? original : value; +} \ No newline at end of file diff --git a/sdks/uniswapx-sdk/src/order/types.ts b/sdks/uniswapx-sdk/src/order/types.ts index e93a5f3a..bb04e39d 100644 --- a/sdks/uniswapx-sdk/src/order/types.ts +++ b/sdks/uniswapx-sdk/src/order/types.ts @@ -70,6 +70,11 @@ export type PriorityOrderResolutionOptions = { currentBlock?: BigNumber; }; +export type V3OrderResolutionOptions = { + currentBlock: number; + filler?: string; +} + export type DutchOutput = { readonly token: string; readonly startAmount: BigNumber; diff --git a/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.ts b/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.ts new file mode 100644 index 00000000..9e67e18c --- /dev/null +++ b/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.ts @@ -0,0 +1,115 @@ +import { BigNumber } from "ethers"; +import { V3Decay } from "../order"; + +/* +These functions mimic the smart contract functions as closely as possible to ensure that the same results are produced. +Essentially Solidity translated to TypeScript. +*/ +function locateArrayPosition( + curve: V3Decay, + targetValue: number + ): [number, number] { + const relativeBlocks = curve.relativeBlocks; + let prev = 0; + let next = 0; + while (next < curve.relativeAmounts.length) { + if (relativeBlocks[next] >= targetValue) { + return [prev, next]; + } + prev = next; + next++; + } + return [next - 1, next - 1]; + } + class NonLinearDutchDecayLib { + static decay( + curve: V3Decay, + startAmount: BigNumber, + decayStartBlock: number, + currentBlock: number + ): BigNumber { + // mismatch of relativeAmounts and relativeBlocks + if (curve.relativeAmounts.length > 16) { + throw new Error('InvalidDecayCurve'); + } + + // handle current block before decay or no decay + if (decayStartBlock >= currentBlock || curve.relativeAmounts.length === 0) { + return startAmount; + } + + const blockDelta = currentBlock - decayStartBlock; + + // Special case for when we need to use the decayStartBlock (0) + if (curve.relativeBlocks[0] > blockDelta) { + return this.linearDecay( + 0, + curve.relativeBlocks[0], + blockDelta, + startAmount, + startAmount.sub(curve.relativeAmounts[0]) + ); + } + + // the current pos is within or after the curve + let [prev, next] = locateArrayPosition(curve, blockDelta); + const lastAmount = startAmount.sub(curve.relativeAmounts[prev]); + const nextAmount = startAmount.sub(curve.relativeAmounts[next]); + return this.linearDecay( + curve.relativeBlocks[prev], + curve.relativeBlocks[next], + blockDelta, + lastAmount, + nextAmount + ); + } + + static linearDecay( + startPoint: number, + endPoint: number, + currentPoint: number, + startAmount: BigNumber, + endAmount: BigNumber + ): BigNumber { + if (currentPoint >= endPoint) { + return endAmount; + } + + const elapsed = BigNumber.from(currentPoint - startPoint); + const duration = BigNumber.from(endPoint - startPoint); + if (endAmount.lt(startAmount)) { + return startAmount.sub( + startAmount.sub(endAmount.mul(elapsed).div(duration)) //muldivdown in contract + ); + } else { + return startAmount.add( + endAmount.sub(startAmount.mul(elapsed).div(duration)) //muldivup in contract + //TODO: How can we do muldivup in JS? + ); + } + } + } + + export { NonLinearDutchDecayLib }; + +export interface DutchBlockDecayConfig { + decayStartBlock: number; + startAmount: BigNumber; + relativeBlocks: number[]; + relativeAmounts: BigNumber[]; +} + +export function getBlockDecayedAmount( + config: DutchBlockDecayConfig, + atBlock: number +): BigNumber { + const {decayStartBlock, startAmount, relativeBlocks, relativeAmounts} = config; + return NonLinearDutchDecayLib.decay({relativeAmounts, relativeBlocks}, startAmount, decayStartBlock, atBlock); +} + +export function getEndAmount( + config: DutchBlockDecayConfig +): BigNumber { + const { startAmount, relativeAmounts } = config; + return startAmount.sub(relativeAmounts[relativeAmounts.length - 1]); +} \ No newline at end of file From 080d616754867285850c11bb01c664b68dcbd8a9 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Fri, 6 Sep 2024 11:17:16 -0400 Subject: [PATCH 08/41] test: extra resolve v3 test, comments --- .../src/order/V2DutchOrder.test.ts | 1 + sdks/uniswapx-sdk/src/order/V2DutchOrder.ts | 1 + .../src/order/V3DutchOrder.test.ts | 25 +++++++++++++++++-- sdks/uniswapx-sdk/src/order/V3DutchOrder.ts | 8 +++--- .../uniswapx-sdk/src/utils/dutchBlockDecay.ts | 5 +++- 5 files changed, 33 insertions(+), 7 deletions(-) diff --git a/sdks/uniswapx-sdk/src/order/V2DutchOrder.test.ts b/sdks/uniswapx-sdk/src/order/V2DutchOrder.test.ts index 738ee711..ac1589b1 100644 --- a/sdks/uniswapx-sdk/src/order/V2DutchOrder.test.ts +++ b/sdks/uniswapx-sdk/src/order/V2DutchOrder.test.ts @@ -245,6 +245,7 @@ describe("V2DutchOrder", () => { ); }); + //TODO: The tests below this line are not testing anything it("resolves when filler has exclusivity", () => { const exclusiveFiller = "0x0000000000000000000000000000000000000001"; const order = new CosignedV2DutchOrder( diff --git a/sdks/uniswapx-sdk/src/order/V2DutchOrder.ts b/sdks/uniswapx-sdk/src/order/V2DutchOrder.ts index a62102af..9a8645c7 100644 --- a/sdks/uniswapx-sdk/src/order/V2DutchOrder.ts +++ b/sdks/uniswapx-sdk/src/order/V2DutchOrder.ts @@ -22,6 +22,7 @@ import { OrderResolutionOptions, } from "./types"; import { CustomOrderValidation, parseValidation } from "./validation"; + import { originalIfZero } from "."; export type CosignerData = { diff --git a/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts b/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts index df9f9f88..2847ef30 100644 --- a/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts +++ b/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts @@ -1,8 +1,8 @@ import { expect } from "chai"; import { BigNumber, ethers } from "ethers"; -import { CosignedV3DutchOrder, CosignedV3DutchOrderInfo, UnsignedV3DutchOrder, UnsignedV3DutchOrderInfoJSON } from "./V3DutchOrder"; import { getEndAmount } from "../utils/dutchBlockDecay"; +import { CosignedV3DutchOrder, CosignedV3DutchOrderInfo, UnsignedV3DutchOrder, UnsignedV3DutchOrderInfoJSON } from "./V3DutchOrder"; const TIME= 1725379823; const BLOCK_NUMBER = 20671221; @@ -192,7 +192,7 @@ describe("V3DutchOrder", () => { expect(resolved.outputs[0].amount).eq(order.info.cosignerData.outputOverrides[0]); }); - it("resolves at decayEndtime without overrides", () => { + it("resolves at last decay block without overrides", () => { const order = new CosignedV3DutchOrder(getFullOrderInfoWithoutOverrides, CHAIN_ID); const relativeBlocks = order.info.outputs[0].curve.relativeBlocks; const resolved = order.resolve({ @@ -225,5 +225,26 @@ describe("V3DutchOrder", () => { expect(resolved.outputs[0].amount.toNumber()).eq(endAmount.toNumber()); //deep eq on bignumber failed }); //TODO: resolves for overrides + it("resolves when filler has exclusivity: Before Decay Start", () => { + const exclusiveFiller = ethers.Wallet.createRandom().address; + const order = new CosignedV3DutchOrder( + getFullOrderInfo({ + cosignerData: { + ...COSIGNER_DATA_WITH_OVERRIDES, + exclusiveFiller, + }, + }), + CHAIN_ID + ); + const resolved = order.resolve({ + currentBlock: BLOCK_NUMBER - 1, + filler: exclusiveFiller, + }); + expect(resolved.input.token).eq(order.info.input.token); + expect(resolved.input.amount).eq(order.info.cosignerData.inputOverride); + expect(resolved.outputs.length).eq(1); + expect(resolved.outputs[0].token).eq(order.info.outputs[0].token); + expect(resolved.outputs[0].amount).eq(order.info.cosignerData.outputOverrides[0]); + }); }); }); diff --git a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts index 299978fa..310b8a21 100644 --- a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts +++ b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts @@ -3,9 +3,9 @@ import { PermitTransferFrom, PermitTransferFromData, SignatureTransfer, Witness import { BigNumber, ethers } from "ethers"; import { getPermit2, ResolvedUniswapXOrder } from "../utils"; +import { getBlockDecayedAmount } from "../utils/dutchBlockDecay"; import { BlockOverrides, OffChainOrder, OrderInfo, V3DutchInput, V3DutchInputJSON, V3DutchOutput, V3DutchOutputJSON, V3OrderResolutionOptions } from "./types"; -import { getBlockDecayedAmount } from "../utils/dutchBlockDecay"; import { originalIfZero } from "."; export type CosignerDataJSON = { @@ -465,16 +465,16 @@ function parseSerializedOrder(serialized: string): CosignedV3DutchOrderInfo { startAmount, curve: { relativeBlocks: decodeRelativeBlocks(relativeBlocks), - relativeAmounts + relativeAmounts, }, - recipient + recipient, })), cosignerData: { decayStartBlock: decayStartBlock.toNumber(), exclusiveFiller, exclusivityOverrideBps: exclusivityOverrideBps, inputOverride: inputOverride, - outputOverrides + outputOverrides, }, cosignature, }; diff --git a/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.ts b/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.ts index 9e67e18c..b1d51e3b 100644 --- a/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.ts +++ b/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.ts @@ -108,8 +108,11 @@ export function getBlockDecayedAmount( } export function getEndAmount( - config: DutchBlockDecayConfig + config: Partial ): BigNumber { const { startAmount, relativeAmounts } = config; + if (!startAmount || !relativeAmounts) { + throw new Error("Invalid config for getting V3 decay end amount"); //TODO: Should we throw? + } return startAmount.sub(relativeAmounts[relativeAmounts.length - 1]); } \ No newline at end of file From cebaa8ad4ab9053747c2e703c2df65d41637bc32 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Fri, 6 Sep 2024 11:36:04 -0400 Subject: [PATCH 09/41] feat, test: MVP v3 OrderBuilder --- .../src/builder/V3DutchOrderBuilder.test.ts | 51 ++++++ .../src/builder/V3DutchOrderBuilder.ts | 169 ++++++++++++++++++ sdks/uniswapx-sdk/src/constants.test.ts | 1 + sdks/uniswapx-sdk/src/constants.ts | 2 + .../src/order/V3DutchOrder.test.ts | 1 + sdks/uniswapx-sdk/src/order/V3DutchOrder.ts | 2 + sdks/uniswapx-sdk/src/order/index.ts | 4 + .../uniswapx-sdk/src/utils/dutchBlockDecay.ts | 3 +- 8 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.test.ts create mode 100644 sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts diff --git a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.test.ts b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.test.ts new file mode 100644 index 00000000..8780609e --- /dev/null +++ b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.test.ts @@ -0,0 +1,51 @@ +import { BigNumber, constants } from "ethers"; +import { V3DutchOrderBuilder } from "./V3DutchOrderBuilder"; + +const INPUT_TOKEN = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"; +const OUTPUT_TOKEN = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"; + +const INPUT_START_AMOUNT = BigNumber.from("1000000"); +const OUTPUT_START_AMOUNT = BigNumber.from("1000000000000000000"); + +describe("V3DutchOrderBuilder", () => { + let builder: V3DutchOrderBuilder; + + beforeEach(() => { + builder = new V3DutchOrderBuilder(1, constants.AddressZero); + }); + + it("should build a valid order", () => { + const deadline = Date.now() + 1000; + const order = builder + .cosigner(constants.AddressZero) + .cosignature("0x") + .decayStartBlock(212121) + .input({ + token: INPUT_TOKEN, + startAmount: INPUT_START_AMOUNT, + curve: { + relativeBlocks: [1], + relativeAmounts: [BigNumber.from(0)], + }, + maxAmount: INPUT_START_AMOUNT.add(1), + }) + .output({ + token: OUTPUT_TOKEN, + startAmount: OUTPUT_START_AMOUNT, + curve: { + relativeBlocks: [4], + relativeAmounts: [BigNumber.from(4)], + }, + recipient: constants.AddressZero, + }) + .inputOverride(INPUT_START_AMOUNT.mul(99).div(100)) + .outputOverrides([OUTPUT_START_AMOUNT]) + .deadline(deadline) + .swapper(constants.AddressZero) + .nonce(BigNumber.from(100)) + .build(); + + expect(order.info.cosignerData.decayStartBlock).toEqual(212121); + expect(order.info.outputs.length).toEqual(1); + }); +}); \ No newline at end of file diff --git a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts new file mode 100644 index 00000000..1335cd79 --- /dev/null +++ b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts @@ -0,0 +1,169 @@ +import { BigNumber, ethers } from "ethers"; +import invariant from "tiny-invariant"; + +import { OrderType } from "../constants"; +import { CosignedV3DutchOrder, CosignedV3DutchOrderInfo, CosignerData } from "../order/V3DutchOrder"; +import { getPermit2, getReactor } from "../utils"; + +import { OrderBuilder } from "./OrderBuilder"; +import { V3DutchInput, V3DutchOutput } from "../order/types"; +import { getEndAmount } from "../utils/dutchBlockDecay"; + +export class V3DutchOrderBuilder extends OrderBuilder { + build(): CosignedV3DutchOrder { + invariant(this.info.cosigner !== undefined, "cosigner not set"); + invariant(this.info.cosignature !== undefined, "cosignature not set"); + invariant(this.info.input !== undefined, "input not set"); + invariant( + this.info.outputs && this.info.outputs.length > 0, + "outputs not set" + ); + invariant(this.info.cosignerData !== undefined, "cosignerData not set"); + invariant(this.info.cosignerData.decayStartBlock !== undefined, "decayStartBlock not set"); + // In V3, we don't have a decayEndTime field and use OrderInfo.deadline field for Permit2 + invariant(this.orderInfo.deadline !== undefined, "deadline not set"); + invariant( + this.info.cosignerData.exclusiveFiller !== undefined, + "exclusiveFiller not set" + ); + invariant( + this.info.cosignerData.exclusivityOverrideBps !== undefined, + "exclusivityOverrideBps not set" + ); + invariant( + this.info.cosignerData.inputOverride !== undefined && + this.info.cosignerData.inputOverride.lte(this.info.input.startAmount), + "inputOverride not set or larger than original input" + ); + invariant( + this.info.cosignerData.outputOverrides.length > 0, + "outputOverrides not set" + ); + this.info.cosignerData.outputOverrides.forEach((override, idx) => { + invariant( + override.gte(this.info.outputs![idx].startAmount), + "outputOverride not set or smaller than original output" + ); + }); + invariant(this.info.input !== undefined, "original input not set"); + //TODO: We need to check if the decayStartTime is before the deadline but it's hard because we have block unit vs timestamp unit + + return new CosignedV3DutchOrder( + Object.assign(this.getOrderInfo(), { + cosignerData: this.info.cosignerData, + input: this.info.input, + outputs: this.info.outputs, + cosigner: this.info.cosigner, + cosignature: this.info.cosignature, + }), + this.chainId, + this.permit2Address + ); + + } + private permit2Address: string; + private info: Partial; + + constructor( + private chainId: number, + reactorAddress?: string, + _permit2Address?: string + ) { + super(); + + this.reactor(getReactor(chainId, OrderType.Dutch_V3, reactorAddress)); + this.permit2Address = getPermit2(chainId, _permit2Address); + + this.info = { + outputs: [], + cosignerData: { + decayStartBlock: 0, + exclusiveFiller: ethers.constants.AddressZero, + exclusivityOverrideBps: BigNumber.from(0), + inputOverride: BigNumber.from(0), + outputOverrides: [], + }, + }; + } + + cosigner(cosigner: string): this { + this.info.cosigner = cosigner; + return this; + } + + cosignature(cosignature: string | undefined): this { + this.info.cosignature = cosignature; + return this; + } + + decayStartBlock(decayStartBlock: number): this { + if (!this.info.cosignerData) { + this.initializeCosignerData({ decayStartBlock }); + } else { + this.info.cosignerData.decayStartBlock = decayStartBlock; + } + return this; + } + + private initializeCosignerData(overrides: Partial): void { + this.info.cosignerData = { + decayStartBlock: 0, + exclusiveFiller: ethers.constants.AddressZero, + exclusivityOverrideBps: BigNumber.from(0), + inputOverride: BigNumber.from(0), + outputOverrides: [], + ...overrides, + }; + } + + input(input: V3DutchInput): this { + this.info.input = input; + return this; + } + + output(output: V3DutchOutput): this { + invariant( + output.startAmount.gte(getEndAmount({ + startAmount: output.startAmount, + relativeAmounts: output.curve.relativeAmounts, + relativeBlocks: output.curve.relativeBlocks, + })), "startAmount must be greater than the endAmount" + ); + this.info.outputs?.push(output); + return this; + } + + inputOverride(inputOverride: BigNumber): this { + if (!this.info.cosignerData) { + this.initializeCosignerData({ inputOverride }); + } else { + this.info.cosignerData.inputOverride = inputOverride; + } + return this; + } + + outputOverrides(outputOverrides: BigNumber[]): this { + if (!this.info.cosignerData) { + this.initializeCosignerData({ outputOverrides }); + } else { + this.info.cosignerData.outputOverrides = outputOverrides; + } + return this; + } + + deadline(deadline: number): this { + super.deadline(deadline); + return this; + } + + swapper(swapper: string): this { + super.swapper(swapper); + return this; + } + + nonce(nonce: BigNumber): this { + super.nonce(nonce); + return this; + } + +} \ No newline at end of file diff --git a/sdks/uniswapx-sdk/src/constants.test.ts b/sdks/uniswapx-sdk/src/constants.test.ts index 886f8309..dd608284 100644 --- a/sdks/uniswapx-sdk/src/constants.test.ts +++ b/sdks/uniswapx-sdk/src/constants.test.ts @@ -28,6 +28,7 @@ describe("REACTOR_ADDRESS_MAPPING", () => { "42161": Object { "Dutch": "0x0000000000000000000000000000000000000000", "Dutch_V2": "0x1bd1aAdc9E230626C44a139d7E70d842749351eb", + "Dutch_V3": "0x4200000000000000000000000000000000000000", "Relay": "0x0000000000000000000000000000000000000000", }, "5": Object { diff --git a/sdks/uniswapx-sdk/src/constants.ts b/sdks/uniswapx-sdk/src/constants.ts index 50f7bc25..17387946 100644 --- a/sdks/uniswapx-sdk/src/constants.ts +++ b/sdks/uniswapx-sdk/src/constants.ts @@ -54,6 +54,7 @@ export enum OrderType { Dutch = "Dutch", Relay = "Relay", Dutch_V2 = "Dutch_V2", + Dutch_V3 = "Dutch_V3", Limit = "Limit", Priority = "Priority", } @@ -94,6 +95,7 @@ export const REACTOR_ADDRESS_MAPPING: ReactorMapping = { [OrderType.Dutch_V2]: "0x1bd1aAdc9E230626C44a139d7E70d842749351eb", [OrderType.Dutch]: "0x0000000000000000000000000000000000000000", [OrderType.Relay]: "0x0000000000000000000000000000000000000000", + [OrderType.Dutch_V3]: "0x4200000000000000000000000000000000000000", }, 8453: { [OrderType.Dutch]: "0x0000000000000000000000000000000000000000", diff --git a/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts b/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts index 2847ef30..5a60b490 100644 --- a/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts +++ b/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts @@ -2,6 +2,7 @@ import { expect } from "chai"; import { BigNumber, ethers } from "ethers"; import { getEndAmount } from "../utils/dutchBlockDecay"; + import { CosignedV3DutchOrder, CosignedV3DutchOrderInfo, UnsignedV3DutchOrder, UnsignedV3DutchOrderInfoJSON } from "./V3DutchOrder"; const TIME= 1725379823; diff --git a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts index 310b8a21..3b273bbc 100644 --- a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts +++ b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts @@ -6,6 +6,7 @@ import { getPermit2, ResolvedUniswapXOrder } from "../utils"; import { getBlockDecayedAmount } from "../utils/dutchBlockDecay"; import { BlockOverrides, OffChainOrder, OrderInfo, V3DutchInput, V3DutchInputJSON, V3DutchOutput, V3DutchOutputJSON, V3OrderResolutionOptions } from "./types"; + import { originalIfZero } from "."; export type CosignerDataJSON = { @@ -489,6 +490,7 @@ function encodeRelativeBlocks(relativeBlocks: number[]): BigNumber { } function decodeRelativeBlocks(packedData: BigNumber): number[] { + /*es-lint-disable-next-line*/ let relativeBlocks: number[] = []; for (let i = 0; i < 16; i++) { const block = packedData.shr(i * 16).toNumber() & 0xFFFF; diff --git a/sdks/uniswapx-sdk/src/order/index.ts b/sdks/uniswapx-sdk/src/order/index.ts index 5d6680fb..af5f6679 100644 --- a/sdks/uniswapx-sdk/src/order/index.ts +++ b/sdks/uniswapx-sdk/src/order/index.ts @@ -1,8 +1,10 @@ import { BigNumber } from "ethers"; + import { DutchOrder } from "./DutchOrder"; import { CosignedPriorityOrder, UnsignedPriorityOrder } from "./PriorityOrder"; import { RelayOrder } from "./RelayOrder"; import { CosignedV2DutchOrder, UnsignedV2DutchOrder } from "./V2DutchOrder"; +import { CosignedV3DutchOrder, UnsignedV3DutchOrder } from "./V3DutchOrder"; export * from "./DutchOrder"; export * from "./PriorityOrder"; @@ -15,6 +17,8 @@ export type UniswapXOrder = | DutchOrder | UnsignedV2DutchOrder | CosignedV2DutchOrder + | UnsignedV3DutchOrder + | CosignedV3DutchOrder | UnsignedPriorityOrder | CosignedPriorityOrder; diff --git a/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.ts b/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.ts index b1d51e3b..0ba6c7c5 100644 --- a/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.ts +++ b/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.ts @@ -1,10 +1,11 @@ import { BigNumber } from "ethers"; -import { V3Decay } from "../order"; +import { V3Decay } from "../order"; /* These functions mimic the smart contract functions as closely as possible to ensure that the same results are produced. Essentially Solidity translated to TypeScript. */ +/* eslint-disable */ function locateArrayPosition( curve: V3Decay, targetValue: number From f8430de61aed4a84f10d8ca275909d20591e5972 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Fri, 6 Sep 2024 14:16:19 -0400 Subject: [PATCH 10/41] refactor: clean reuse of v2 for v3 --- .../src/builder/V2DutchOrderBuilder.test.ts | 2 +- sdks/uniswapx-sdk/src/builder/V2DutchOrderBuilder.ts | 12 +++--------- sdks/uniswapx-sdk/src/utils/order.ts | 6 ++++++ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/sdks/uniswapx-sdk/src/builder/V2DutchOrderBuilder.test.ts b/sdks/uniswapx-sdk/src/builder/V2DutchOrderBuilder.test.ts index d5d54af3..d7d0e701 100644 --- a/sdks/uniswapx-sdk/src/builder/V2DutchOrderBuilder.test.ts +++ b/sdks/uniswapx-sdk/src/builder/V2DutchOrderBuilder.test.ts @@ -349,7 +349,7 @@ describe("V2DutchOrderBuilder", () => { .outputOverrides([OUTPUT_START_AMOUNT.mul(102).div(100)]) .build() ).toThrow( - "Invariant failed: inputOverride not set or larger than original input" + "Invariant failed: inputOverride larger than original input" ); }); diff --git a/sdks/uniswapx-sdk/src/builder/V2DutchOrderBuilder.ts b/sdks/uniswapx-sdk/src/builder/V2DutchOrderBuilder.ts index d8a9c8df..61b74c13 100644 --- a/sdks/uniswapx-sdk/src/builder/V2DutchOrderBuilder.ts +++ b/sdks/uniswapx-sdk/src/builder/V2DutchOrderBuilder.ts @@ -11,7 +11,7 @@ import { UnsignedV2DutchOrder, } from "../order"; import { ValidationInfo } from "../order/validation"; -import { getPermit2, getReactor } from "../utils"; +import { getPermit2, getReactor, isCosigned } from "../utils"; import { OrderBuilder } from "./OrderBuilder"; @@ -287,9 +287,9 @@ export class V2DutchOrderBuilder extends OrderBuilder { "exclusivityOverrideBps not set" ); invariant( - this.info.cosignerData.inputOverride !== undefined && + this.info.cosignerData.inputOverride !== undefined && // inputOverride is defaulted to 0 because enforced to be of type BigNumber this.info.cosignerData.inputOverride.lte(this.info.input.startAmount), - "inputOverride not set or larger than original input" + "inputOverride larger than original input" ); invariant( this.info.cosignerData.outputOverrides.length > 0, @@ -339,9 +339,3 @@ export class V2DutchOrderBuilder extends OrderBuilder { }; } } - -function isCosigned( - order: UnsignedV2DutchOrder | CosignedV2DutchOrder -): order is CosignedV2DutchOrder { - return (order as CosignedV2DutchOrder).info.cosignature !== undefined; -} diff --git a/sdks/uniswapx-sdk/src/utils/order.ts b/sdks/uniswapx-sdk/src/utils/order.ts index 7fe65328..5f70c69c 100644 --- a/sdks/uniswapx-sdk/src/utils/order.ts +++ b/sdks/uniswapx-sdk/src/utils/order.ts @@ -147,3 +147,9 @@ export class RelayOrderParser extends OrderParser { return RelayOrder.parse(order, chainId); } } + +export function isCosigned( + order: UnsignedV2DutchOrder | CosignedV2DutchOrder +): order is CosignedV2DutchOrder { + return (order as CosignedV2DutchOrder).info.cosignature !== undefined; +} From f50e745abab08961c5820d27f0a51b80ab281e47 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Fri, 6 Sep 2024 14:17:19 -0400 Subject: [PATCH 11/41] feat, test: robust testing, helpers for v3 cosignerData building --- .../src/builder/V3DutchOrderBuilder.test.ts | 335 +++++++++++++++++- .../src/builder/V3DutchOrderBuilder.ts | 70 +++- 2 files changed, 396 insertions(+), 9 deletions(-) diff --git a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.test.ts b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.test.ts index 8780609e..b6fe554e 100644 --- a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.test.ts +++ b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.test.ts @@ -14,7 +14,7 @@ describe("V3DutchOrderBuilder", () => { builder = new V3DutchOrderBuilder(1, constants.AddressZero); }); - it("should build a valid order", () => { + it("Build a valid order", () => { const deadline = Date.now() + 1000; const order = builder .cosigner(constants.AddressZero) @@ -48,4 +48,337 @@ describe("V3DutchOrderBuilder", () => { expect(order.info.cosignerData.decayStartBlock).toEqual(212121); expect(order.info.outputs.length).toEqual(1); }); + //TODO: Add tests that uses the validation contract once it is implemented + + it("Build a valid order with multiple outputs", () => { + const deadline = Math.floor(Date.now() / 1000) + 1000; + const order = builder + .cosigner(constants.AddressZero) + .cosignature("0x") + .deadline(deadline) + .decayStartBlock(212121) + .swapper(constants.AddressZero) + .nonce(BigNumber.from(100)) + .input({ + token: INPUT_TOKEN, + startAmount: INPUT_START_AMOUNT, + curve: { + relativeBlocks: [1], + relativeAmounts: [BigNumber.from(0)], + }, + maxAmount: INPUT_START_AMOUNT.add(1), + }) + .output({ + token: OUTPUT_TOKEN, + startAmount: OUTPUT_START_AMOUNT, + curve: { + relativeBlocks: [4], + relativeAmounts: [BigNumber.from(4)], + }, + recipient: constants.AddressZero, + }) + .output({ + token: OUTPUT_TOKEN, + startAmount: OUTPUT_START_AMOUNT, + curve: { + relativeBlocks: [17], + relativeAmounts: [BigNumber.from(17)], + }, + recipient: constants.AddressZero, + }) + .inputOverride(INPUT_START_AMOUNT.mul(99).div(100)) + .outputOverrides([OUTPUT_START_AMOUNT.mul(101).div(100), OUTPUT_START_AMOUNT]) + .build(); + expect(order.info.outputs.length).toEqual(2); + expect(order.info.cosignerData.decayStartBlock).toEqual(212121); + }); + + it("Throw if cosigner is not set", () => { + const deadline = Math.floor(Date.now() / 1000) + 1000; + expect(() => + builder + .cosignature("0x") + .decayStartBlock(212121) + .input({ + token: INPUT_TOKEN, + startAmount: INPUT_START_AMOUNT, + curve: { + relativeBlocks: [1], + relativeAmounts: [BigNumber.from(0)], + }, + maxAmount: INPUT_START_AMOUNT.add(1), + }) + .output({ + token: OUTPUT_TOKEN, + startAmount: OUTPUT_START_AMOUNT, + curve: { + relativeBlocks: [4], + relativeAmounts: [BigNumber.from(4)], + }, + recipient: constants.AddressZero, + }) + .inputOverride(INPUT_START_AMOUNT.mul(99).div(100)) + .outputOverrides([OUTPUT_START_AMOUNT]) + .deadline(deadline) + .swapper(constants.AddressZero) + .nonce(BigNumber.from(100)) + .build() + ).toThrow("Invariant failed: cosigner not set"); + }); + + it("Throw if swapper is not set", () => { + const deadline = Math.floor(Date.now() / 1000) + 1000; + expect(() => + builder + .cosignature("0x") + .cosigner(constants.AddressZero) + .decayStartBlock(212121) + .input({ + token: INPUT_TOKEN, + startAmount: INPUT_START_AMOUNT, + curve: { + relativeBlocks: [1], + relativeAmounts: [BigNumber.from(0)], + }, + maxAmount: INPUT_START_AMOUNT.add(1), + }) + .output({ + token: OUTPUT_TOKEN, + startAmount: OUTPUT_START_AMOUNT, + curve: { + relativeBlocks: [4], + relativeAmounts: [BigNumber.from(4)], + }, + recipient: constants.AddressZero, + }) + .inputOverride(INPUT_START_AMOUNT.mul(99).div(100)) + .outputOverrides([OUTPUT_START_AMOUNT]) + .deadline(deadline) + .nonce(BigNumber.from(100)) + .build() + ).toThrow("Invariant failed: swapper not set"); + }); + + it("Deadline not set", () => { + expect(() => builder + .cosigner(constants.AddressZero) + .cosignature("0x") + .decayStartBlock(212121) + .input({ + token: INPUT_TOKEN, + startAmount: INPUT_START_AMOUNT, + curve: { + relativeBlocks: [1], + relativeAmounts: [BigNumber.from(0)], + }, + maxAmount: INPUT_START_AMOUNT.add(1), + }) + .output({ + token: OUTPUT_TOKEN, + startAmount: OUTPUT_START_AMOUNT, + curve: { + relativeBlocks: [4], + relativeAmounts: [BigNumber.from(4)], + }, + recipient: constants.AddressZero, + }) + .inputOverride(INPUT_START_AMOUNT.mul(99).div(100)) + .outputOverrides([OUTPUT_START_AMOUNT]) + .swapper(constants.AddressZero) + .nonce(BigNumber.from(100)) + .build() + ).toThrow("Invariant failed: deadline not set"); + }); + + it("Nonce not set", () => { + const deadline = Math.floor(Date.now() / 1000) + 1000; + expect(() => + builder + .cosignature("0x") + .cosigner(constants.AddressZero) + .decayStartBlock(212121) + .input({ + token: INPUT_TOKEN, + startAmount: INPUT_START_AMOUNT, + curve: { + relativeBlocks: [1], + relativeAmounts: [BigNumber.from(0)], + }, + maxAmount: INPUT_START_AMOUNT.add(1), + }) + .output({ + token: OUTPUT_TOKEN, + startAmount: OUTPUT_START_AMOUNT, + curve: { + relativeBlocks: [4], + relativeAmounts: [BigNumber.from(4)], + }, + recipient: constants.AddressZero, + }) + .inputOverride(INPUT_START_AMOUNT.mul(99).div(100)) + .outputOverrides([OUTPUT_START_AMOUNT]) + .deadline(deadline) + .swapper(constants.AddressZero) + // omitting nonce + .build() + ).toThrow("Invariant failed: nonce not set"); + }); + + it("Throw if input is not set", () => { + const deadline = Math.floor(Date.now() / 1000) + 1000; + expect(() => + builder + .cosignature("0x") + .cosigner(constants.AddressZero) + .decayStartBlock(212121) + // omitting input + .output({ + token: OUTPUT_TOKEN, + startAmount: OUTPUT_START_AMOUNT, + curve: { + relativeBlocks: [4], + relativeAmounts: [BigNumber.from(4)], + }, + recipient: constants.AddressZero, + }) + .inputOverride(INPUT_START_AMOUNT.mul(99).div(100)) + .outputOverrides([OUTPUT_START_AMOUNT]) + .deadline(deadline) + .swapper(constants.AddressZero) + .nonce(BigNumber.from(100)) + .build() + ).toThrow("Invariant failed: input not set"); + }); + + it("Throw if output is not set", () => { + const deadline = Math.floor(Date.now() / 1000) + 1000; + expect(() => + builder + .cosignature("0x") + .cosigner(constants.AddressZero) + .decayStartBlock(212121) + .input({ + token: INPUT_TOKEN, + startAmount: INPUT_START_AMOUNT, + curve: { + relativeBlocks: [1], + relativeAmounts: [BigNumber.from(0)], + }, + maxAmount: INPUT_START_AMOUNT.add(1), + }) + // omitting output + .inputOverride(INPUT_START_AMOUNT.mul(99).div(100)) + .outputOverrides([OUTPUT_START_AMOUNT]) + .deadline(deadline) + .swapper(constants.AddressZero) + .nonce(BigNumber.from(100)) + .build() + ).toThrow("Invariant failed: outputs not set"); + }); + + it("Throw if inputOverride larger than input", () => { + const deadline = Math.floor(Date.now() / 1000) + 1000; + expect(() => + builder + .cosignature("0x") + .cosigner(constants.AddressZero) + .decayStartBlock(212121) + .input({ + token: INPUT_TOKEN, + startAmount: INPUT_START_AMOUNT, + curve: { + relativeBlocks: [1], + relativeAmounts: [BigNumber.from(0)], + }, + maxAmount: INPUT_START_AMOUNT.add(1), + }) + .output({ + token: OUTPUT_TOKEN, + startAmount: OUTPUT_START_AMOUNT, + curve: { + relativeBlocks: [4], + relativeAmounts: [BigNumber.from(4)], + }, + recipient: constants.AddressZero, + }) + .inputOverride(INPUT_START_AMOUNT.add(1)) + .outputOverrides([OUTPUT_START_AMOUNT]) + .deadline(deadline) + .swapper(constants.AddressZero) + .nonce(BigNumber.from(100)) + .build() + ).toThrow("Invariant failed: inputOverride larger than original input"); + }); + + it("Throw if outputOverride smaller than output", () => { + const deadline = Math.floor(Date.now() / 1000) + 1000; + expect(() => + builder + .cosignature("0x") + .cosigner(constants.AddressZero) + .decayStartBlock(212121) + .input({ + token: INPUT_TOKEN, + startAmount: INPUT_START_AMOUNT, + curve: { + relativeBlocks: [1], + relativeAmounts: [BigNumber.from(0)], + }, + maxAmount: INPUT_START_AMOUNT.add(1), + }) + .output({ + token: OUTPUT_TOKEN, + startAmount: OUTPUT_START_AMOUNT, + curve: { + relativeBlocks: [4], + relativeAmounts: [BigNumber.from(4)], + }, + recipient: constants.AddressZero, + }) + .inputOverride(INPUT_START_AMOUNT.mul(99).div(100)) + .outputOverrides([OUTPUT_START_AMOUNT.sub(2121)]) + .deadline(deadline) + .swapper(constants.AddressZero) + .nonce(BigNumber.from(100)) + .build() + ).toThrow("Invariant failed: outputOverride smaller than original output"); + }); + + //TODO: double check that we don't enforce endamount < startamount + + it("Throw if deadline already passed", () => { + const deadline = 2121; + expect(() => + builder + .cosignature("0x") + .cosigner(constants.AddressZero) + .decayStartBlock(212121) + .input({ + token: INPUT_TOKEN, + startAmount: INPUT_START_AMOUNT, + curve: { + relativeBlocks: [1], + relativeAmounts: [BigNumber.from(0)], + }, + maxAmount: INPUT_START_AMOUNT.add(1), + }) + .output({ + token: OUTPUT_TOKEN, + startAmount: OUTPUT_START_AMOUNT, + curve: { + relativeBlocks: [4], + relativeAmounts: [BigNumber.from(4)], + }, + recipient: constants.AddressZero, + }) + .inputOverride(INPUT_START_AMOUNT.mul(99).div(100)) + .outputOverrides([OUTPUT_START_AMOUNT]) + .deadline(deadline) + .swapper(constants.AddressZero) + .nonce(BigNumber.from(100)) + .build() + ).toThrow("Invariant failed: Deadline must be in the future: 2121"); + }); + + }); \ No newline at end of file diff --git a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts index 1335cd79..ab330bc4 100644 --- a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts +++ b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts @@ -18,6 +18,7 @@ export class V3DutchOrderBuilder extends OrderBuilder { this.info.outputs && this.info.outputs.length > 0, "outputs not set" ); + // In V3, we are not enforcing that the startAmount is greater than the endAmount invariant(this.info.cosignerData !== undefined, "cosignerData not set"); invariant(this.info.cosignerData.decayStartBlock !== undefined, "decayStartBlock not set"); // In V3, we don't have a decayEndTime field and use OrderInfo.deadline field for Permit2 @@ -31,9 +32,8 @@ export class V3DutchOrderBuilder extends OrderBuilder { "exclusivityOverrideBps not set" ); invariant( - this.info.cosignerData.inputOverride !== undefined && this.info.cosignerData.inputOverride.lte(this.info.input.startAmount), - "inputOverride not set or larger than original input" + "inputOverride larger than original input" ); invariant( this.info.cosignerData.outputOverrides.length > 0, @@ -42,11 +42,11 @@ export class V3DutchOrderBuilder extends OrderBuilder { this.info.cosignerData.outputOverrides.forEach((override, idx) => { invariant( override.gte(this.info.outputs![idx].startAmount), - "outputOverride not set or smaller than original output" + "outputOverride smaller than original output" ); }); invariant(this.info.input !== undefined, "original input not set"); - //TODO: We need to check if the decayStartTime is before the deadline but it's hard because we have block unit vs timestamp unit + // We are not checking if the decayStartTime is before the deadline because it is not enforced in the smart contract return new CosignedV3DutchOrder( Object.assign(this.getOrderInfo(), { @@ -105,14 +105,14 @@ export class V3DutchOrderBuilder extends OrderBuilder { return this; } - private initializeCosignerData(overrides: Partial): void { + private initializeCosignerData(data: Partial): void { this.info.cosignerData = { decayStartBlock: 0, exclusiveFiller: ethers.constants.AddressZero, exclusivityOverrideBps: BigNumber.from(0), inputOverride: BigNumber.from(0), outputOverrides: [], - ...overrides, + ...data, }; } @@ -135,9 +135,9 @@ export class V3DutchOrderBuilder extends OrderBuilder { inputOverride(inputOverride: BigNumber): this { if (!this.info.cosignerData) { - this.initializeCosignerData({ inputOverride }); + this.initializeCosignerData({ inputOverride }); } else { - this.info.cosignerData.inputOverride = inputOverride; + this.info.cosignerData.inputOverride = inputOverride; } return this; } @@ -166,4 +166,58 @@ export class V3DutchOrderBuilder extends OrderBuilder { return this; } + cosignerData(cosignerData: CosignerData): this { + this.decayStartBlock(cosignerData.decayStartBlock); + this.exclusiveFiller(cosignerData.exclusiveFiller); + this.exclusivityOverrideBps(cosignerData.exclusivityOverrideBps); + this.inputOverride(cosignerData.inputOverride); + this.outputOverrides(cosignerData.outputOverrides); + return this; + } + + exclusiveFiller(exclusiveFiller: string): this { + if (!this.info.cosignerData) { + this.initializeCosignerData({ exclusiveFiller }); + } else { + this.info.cosignerData.exclusiveFiller = exclusiveFiller; + } + return this; + } + + exclusivityOverrideBps(exclusivityOverrideBps: BigNumber): this { + if (!this.info.cosignerData) { + this.initializeCosignerData({ exclusivityOverrideBps }); + } else { + this.info.cosignerData.exclusivityOverrideBps = exclusivityOverrideBps; + } + return this; + } + + // ensures that we only change non fee outputs + nonFeeRecipient(newRecipient: string, feeRecipient?: string): this { + invariant( + newRecipient !== feeRecipient, + `newRecipient must be different from feeRecipient: ${newRecipient}` + ); + if (!this.info.outputs) { + return this; + } + this.info.outputs = this.info.outputs.map((output) => { + // if fee output then pass through + if ( + feeRecipient && + output.recipient.toLowerCase() === feeRecipient.toLowerCase() + ) { + return output; + } + + return { + ...output, + recipient: newRecipient, + }; + }); + return this; + } + + //TODO: buildPartial(), fromOrder() } \ No newline at end of file From 4138f2c69bb4f41f6b7244c3dafaa02b8d364478 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Fri, 6 Sep 2024 15:07:24 -0400 Subject: [PATCH 12/41] feat, test: v3 buildPartial --- .../src/builder/V2DutchOrderBuilder.test.ts | 2 +- .../src/builder/V3DutchOrderBuilder.test.ts | 78 ++++++++++++++++++- .../src/builder/V3DutchOrderBuilder.ts | 29 ++++++- sdks/uniswapx-sdk/src/order/V3DutchOrder.ts | 8 +- .../uniswapx-sdk/src/utils/dutchBlockDecay.ts | 3 +- 5 files changed, 108 insertions(+), 12 deletions(-) diff --git a/sdks/uniswapx-sdk/src/builder/V2DutchOrderBuilder.test.ts b/sdks/uniswapx-sdk/src/builder/V2DutchOrderBuilder.test.ts index d7d0e701..6ea58401 100644 --- a/sdks/uniswapx-sdk/src/builder/V2DutchOrderBuilder.test.ts +++ b/sdks/uniswapx-sdk/src/builder/V2DutchOrderBuilder.test.ts @@ -574,7 +574,7 @@ describe("V2DutchOrderBuilder", () => { }); describe("partial order tests", () => { - it("builds an unsigned partial order with default cosignerData values", () => { + it("builds an unsigned partial order with default cosignerData values", () => { //TODO: partial orders don't have cosignerData... const deadline = Math.floor(Date.now() / 1000) + 1000; const order = builder .cosigner(constants.AddressZero) diff --git a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.test.ts b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.test.ts index b6fe554e..a9982bbb 100644 --- a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.test.ts +++ b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.test.ts @@ -1,4 +1,5 @@ import { BigNumber, constants } from "ethers"; + import { V3DutchOrderBuilder } from "./V3DutchOrderBuilder"; const INPUT_TOKEN = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"; @@ -377,8 +378,81 @@ describe("V3DutchOrderBuilder", () => { .swapper(constants.AddressZero) .nonce(BigNumber.from(100)) .build() - ).toThrow("Invariant failed: Deadline must be in the future: 2121"); - }); + ).toThrow("Invariant failed: Deadline must be in the future: 2121"); + }); + it("Does not throw before an order has not been finished building", () => { + const deadline = Math.floor(Date.now() / 1000) + 1000; + expect(() => + builder.deadline(deadline).decayStartBlock(21212121212121) + ).not.toThrowError(); + }); + it("Unknown chainId", () => { + const chainId = 99999999; + expect(() => new V3DutchOrderBuilder(chainId)).toThrow( + `Missing configuration for reactor: ${chainId}` + ); + }); + + describe("Partial order tests", () => { + it("Test valid order with buildPartial", () => { + const order = builder + .cosigner(constants.AddressZero) + .input({ + token: INPUT_TOKEN, + startAmount: INPUT_START_AMOUNT, + curve: { + relativeBlocks: [1], + relativeAmounts: [BigNumber.from(0)], + }, + maxAmount: INPUT_START_AMOUNT.add(1), + }) + .output({ + token: OUTPUT_TOKEN, + startAmount: OUTPUT_START_AMOUNT, + curve: { + relativeBlocks: [4], + relativeAmounts: [BigNumber.from(4)], + }, + recipient: constants.AddressZero, + }) + .swapper(constants.AddressZero) + .nonce(BigNumber.from(100)) + .deadline(Math.floor(Date.now() / 1000) + 1000) + .buildPartial(); + expect(order.info.outputs.length).toEqual(1); + expect(order.chainId).toBeDefined(); + expect(order.info.reactor).toBeDefined(); + }); + + it("Test invalid order with buildPartial", () => { + expect(() => + builder + .cosigner(constants.AddressZero) + .input({ + token: INPUT_TOKEN, + startAmount: INPUT_START_AMOUNT, + curve: { + relativeBlocks: [1], + relativeAmounts: [BigNumber.from(0)], + }, + maxAmount: INPUT_START_AMOUNT.add(1), + }) + .output({ + token: OUTPUT_TOKEN, + startAmount: OUTPUT_START_AMOUNT, + curve: { + relativeBlocks: [4], + relativeAmounts: [BigNumber.from(4)], + }, + recipient: constants.AddressZero, + }) + // omitting swapper + .deadline(Math.floor(Date.now() / 1000) + 1000) + .nonce(BigNumber.from(100)) + .buildPartial() + ).toThrow("Invariant failed: swapper not set"); + }); + }); }); \ No newline at end of file diff --git a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts index ab330bc4..62182bc9 100644 --- a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts +++ b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts @@ -2,12 +2,12 @@ import { BigNumber, ethers } from "ethers"; import invariant from "tiny-invariant"; import { OrderType } from "../constants"; -import { CosignedV3DutchOrder, CosignedV3DutchOrderInfo, CosignerData } from "../order/V3DutchOrder"; +import { CosignedV3DutchOrder, CosignedV3DutchOrderInfo, CosignerData, UnsignedV3DutchOrder } from "../order/V3DutchOrder"; +import { V3DutchInput, V3DutchOutput } from "../order/types"; import { getPermit2, getReactor } from "../utils"; +import { getEndAmount } from "../utils/dutchBlockDecay"; import { OrderBuilder } from "./OrderBuilder"; -import { V3DutchInput, V3DutchOutput } from "../order/types"; -import { getEndAmount } from "../utils/dutchBlockDecay"; export class V3DutchOrderBuilder extends OrderBuilder { build(): CosignedV3DutchOrder { @@ -41,6 +41,7 @@ export class V3DutchOrderBuilder extends OrderBuilder { ); this.info.cosignerData.outputOverrides.forEach((override, idx) => { invariant( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion override.gte(this.info.outputs![idx].startAmount), "outputOverride smaller than original output" ); @@ -219,5 +220,25 @@ export class V3DutchOrderBuilder extends OrderBuilder { return this; } - //TODO: buildPartial(), fromOrder() + buildPartial(): UnsignedV3DutchOrder { //build an unsigned order + invariant(this.info.cosigner !== undefined, "cosigner not set"); + invariant(this.info.input !== undefined, "input not set"); + invariant( + this.info.outputs && this.info.outputs.length > 0, + "outputs not set" + ); + invariant(this.info.input !== undefined, "original input not set"); + invariant(!this.info.deadline, "deadline not set"); + invariant(!this.info.swapper, "swapper not set"); + return new UnsignedV3DutchOrder( + Object.assign(this.getOrderInfo(), { + input: this.info.input, + outputs: this.info.outputs, + cosigner: this.info.cosigner, + }), + this.chainId, + this.permit2Address + ); + } + //TODO: fromOrder() } \ No newline at end of file diff --git a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts index 3b273bbc..001a7a5d 100644 --- a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts +++ b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts @@ -401,7 +401,7 @@ export class CosignedV3DutchOrder extends UnsignedV3DutchOrder { amount: getBlockDecayedAmount( { decayStartBlock: this.info.cosignerData.decayStartBlock, - startAmount: originalIfZero(this.info.cosignerData!.outputOverrides[idx], output.startAmount), + startAmount: originalIfZero(this.info.cosignerData.outputOverrides[idx], output.startAmount), relativeBlocks: output.curve.relativeBlocks, relativeAmounts: output.curve.relativeAmounts, }, @@ -488,9 +488,8 @@ function encodeRelativeBlocks(relativeBlocks: number[]): BigNumber { } return packedData; } - +/* eslint-disable */ function decodeRelativeBlocks(packedData: BigNumber): number[] { - /*es-lint-disable-next-line*/ let relativeBlocks: number[] = []; for (let i = 0; i < 16; i++) { const block = packedData.shr(i * 16).toNumber() & 0xFFFF; @@ -499,4 +498,5 @@ function decodeRelativeBlocks(packedData: BigNumber): number[] { } } return relativeBlocks; -} \ No newline at end of file +} +/* eslint-enable */ \ No newline at end of file diff --git a/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.ts b/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.ts index 0ba6c7c5..4596d663 100644 --- a/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.ts +++ b/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.ts @@ -116,4 +116,5 @@ export function getEndAmount( throw new Error("Invalid config for getting V3 decay end amount"); //TODO: Should we throw? } return startAmount.sub(relativeAmounts[relativeAmounts.length - 1]); -} \ No newline at end of file +} +/* eslint-enable */ \ No newline at end of file From 4a93058dc548241b042d8c2a1d079c67127e9cff Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Fri, 6 Sep 2024 15:45:29 -0400 Subject: [PATCH 13/41] feat, test: v3 builder fromOrder --- .../src/builder/V3DutchOrderBuilder.test.ts | 81 +++++++++++++++++++ .../src/builder/V3DutchOrderBuilder.ts | 32 +++++++- sdks/uniswapx-sdk/src/utils/order.ts | 14 +++- 3 files changed, 123 insertions(+), 4 deletions(-) diff --git a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.test.ts b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.test.ts index a9982bbb..3d0b8cf6 100644 --- a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.test.ts +++ b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.test.ts @@ -1,6 +1,7 @@ import { BigNumber, constants } from "ethers"; import { V3DutchOrderBuilder } from "./V3DutchOrderBuilder"; +import { CosignedV3DutchOrder, UnsignedV3DutchOrder } from "../order/V3DutchOrder"; const INPUT_TOKEN = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"; const OUTPUT_TOKEN = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"; @@ -455,4 +456,84 @@ describe("V3DutchOrderBuilder", () => { ).toThrow("Invariant failed: swapper not set"); }); }); + + describe("fromOrder", () => { + let builder: V3DutchOrderBuilder; + + beforeEach(() => { + builder = new V3DutchOrderBuilder(1, constants.AddressZero); + }); + + it("should create a V3DutchOrderBuilder from an UnsignedV3DutchOrder", () => { + const order: UnsignedV3DutchOrder = builder + .cosigner(constants.AddressZero) + .input({ + token: INPUT_TOKEN, + startAmount: INPUT_START_AMOUNT, + curve: { + relativeBlocks: [1], + relativeAmounts: [BigNumber.from(0)], + }, + maxAmount: INPUT_START_AMOUNT.add(1), + }) + .output({ + token: OUTPUT_TOKEN, + startAmount: OUTPUT_START_AMOUNT, + curve: { + relativeBlocks: [4], + relativeAmounts: [BigNumber.from(4)], + }, + recipient: constants.AddressZero, + }) + .swapper(constants.AddressZero) + .nonce(BigNumber.from(100)) + .deadline(Math.floor(Date.now() / 1000) + 1000) + .buildPartial(); + + const created_builder = V3DutchOrderBuilder.fromOrder(order); + const created_order = created_builder.buildPartial(); + + expect(created_order.chainId).toEqual(1); + expect(created_order.info.input.token).toEqual(INPUT_TOKEN); + expect(created_order.info.outputs.length).toEqual(1); + }); + + it("should create a V3DutchOrderBuilder from a CosignedV3DutchOrder", () => { + const deadline = Date.now() + 1000; + const order: CosignedV3DutchOrder = builder + .cosigner(constants.AddressZero) + .cosignature("0x") + .decayStartBlock(212121) + .input({ + token: INPUT_TOKEN, + startAmount: INPUT_START_AMOUNT, + curve: { + relativeBlocks: [1], + relativeAmounts: [BigNumber.from(0)], + }, + maxAmount: INPUT_START_AMOUNT.add(1), + }) + .output({ + token: OUTPUT_TOKEN, + startAmount: OUTPUT_START_AMOUNT, + curve: { + relativeBlocks: [4], + relativeAmounts: [BigNumber.from(4)], + }, + recipient: constants.AddressZero, + }) + .inputOverride(INPUT_START_AMOUNT.mul(99).div(100)) + .outputOverrides([OUTPUT_START_AMOUNT]) + .deadline(deadline) + .swapper(constants.AddressZero) + .nonce(BigNumber.from(100)) + .build(); + const created_builder = V3DutchOrderBuilder.fromOrder(order); + const created_order = created_builder.build(); + expect(created_order.chainId).toEqual(1); + expect(created_order.info.input.token).toEqual(INPUT_TOKEN); + expect(created_order.info.outputs.length).toEqual(1); + expect(created_order.info.cosignerData.decayStartBlock).toEqual(212121); + }); + }); }); \ No newline at end of file diff --git a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts index 62182bc9..8da3bf39 100644 --- a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts +++ b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts @@ -4,12 +4,42 @@ import invariant from "tiny-invariant"; import { OrderType } from "../constants"; import { CosignedV3DutchOrder, CosignedV3DutchOrderInfo, CosignerData, UnsignedV3DutchOrder } from "../order/V3DutchOrder"; import { V3DutchInput, V3DutchOutput } from "../order/types"; -import { getPermit2, getReactor } from "../utils"; +import { getPermit2, getReactor, isCosigned } from "../utils"; import { getEndAmount } from "../utils/dutchBlockDecay"; import { OrderBuilder } from "./OrderBuilder"; export class V3DutchOrderBuilder extends OrderBuilder { + static fromOrder( + order: T + ): V3DutchOrderBuilder { + const builder = new V3DutchOrderBuilder(order.chainId, order.info.reactor); + builder + .cosigner(order.info.cosigner) + .input(order.info.input) + .deadline(order.info.deadline) + .nonce(order.info.nonce) + .swapper(order.info.swapper) + .validation({ + additionalValidationContract: order.info.additionalValidationContract, + additionalValidationData: order.info.additionalValidationData, + }); + + order.info.outputs.forEach((output) => { + builder.output(output); + }); + + if (isCosigned(order)) { + builder.cosignature(order.info.cosignature); + builder.decayStartBlock(order.info.cosignerData.decayStartBlock); + builder.exclusiveFiller(order.info.cosignerData.exclusiveFiller); + builder.inputOverride(order.info.cosignerData.inputOverride); + builder.exclusivityOverrideBps(order.info.cosignerData.exclusivityOverrideBps); + builder.outputOverrides(order.info.cosignerData.outputOverrides); + } + return builder; + } + build(): CosignedV3DutchOrder { invariant(this.info.cosigner !== undefined, "cosigner not set"); invariant(this.info.cosignature !== undefined, "cosignature not set"); diff --git a/sdks/uniswapx-sdk/src/utils/order.ts b/sdks/uniswapx-sdk/src/utils/order.ts index 5f70c69c..3b07674a 100644 --- a/sdks/uniswapx-sdk/src/utils/order.ts +++ b/sdks/uniswapx-sdk/src/utils/order.ts @@ -14,6 +14,7 @@ import { } from "../order"; import { stripHexPrefix } from "."; +import { CosignedV3DutchOrder, UnsignedV3DutchOrder } from "../order/V3DutchOrder"; const UNISWAPX_ORDER_INFO_OFFSET = 64; const RELAY_ORDER_INFO_OFFSET = 64; @@ -149,7 +150,14 @@ export class RelayOrderParser extends OrderParser { } export function isCosigned( - order: UnsignedV2DutchOrder | CosignedV2DutchOrder -): order is CosignedV2DutchOrder { - return (order as CosignedV2DutchOrder).info.cosignature !== undefined; + order: UnsignedV2DutchOrder | CosignedV2DutchOrder | UnsignedV3DutchOrder | CosignedV3DutchOrder +): order is CosignedV2DutchOrder | CosignedV3DutchOrder { + const parser = new UniswapXOrderParser(); + if (parser.getOrderType(order) === OrderType.Dutch_V2) { + return (order as CosignedV2DutchOrder).info.cosignature !== undefined; + } else if (parser.getOrderType(order) === OrderType.Dutch_V3) { + return (order as CosignedV3DutchOrder).info.cosignature !== undefined; + } else { + return false; + } } From e221b4acc1d23b0b0a86744261cffee6dac29493 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Fri, 6 Sep 2024 16:41:16 -0400 Subject: [PATCH 14/41] feat, test: v3 orderTrade --- .../src/builder/V3DutchOrderBuilder.test.ts | 3 +- sdks/uniswapx-sdk/src/order/index.ts | 1 + .../src/trade/V3DutchOrderTrade.test.ts | 141 ++++++++++++++++ .../src/trade/V3DutchOrderTrade.ts | 159 ++++++++++++++++++ sdks/uniswapx-sdk/src/utils/order.ts | 2 +- 5 files changed, 304 insertions(+), 2 deletions(-) create mode 100644 sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.test.ts create mode 100644 sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.ts diff --git a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.test.ts b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.test.ts index 3d0b8cf6..0452d781 100644 --- a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.test.ts +++ b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.test.ts @@ -1,8 +1,9 @@ import { BigNumber, constants } from "ethers"; -import { V3DutchOrderBuilder } from "./V3DutchOrderBuilder"; import { CosignedV3DutchOrder, UnsignedV3DutchOrder } from "../order/V3DutchOrder"; +import { V3DutchOrderBuilder } from "./V3DutchOrderBuilder"; + const INPUT_TOKEN = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"; const OUTPUT_TOKEN = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"; diff --git a/sdks/uniswapx-sdk/src/order/index.ts b/sdks/uniswapx-sdk/src/order/index.ts index af5f6679..5da03345 100644 --- a/sdks/uniswapx-sdk/src/order/index.ts +++ b/sdks/uniswapx-sdk/src/order/index.ts @@ -12,6 +12,7 @@ export * from "./RelayOrder"; export * from "./types"; export * from "./validation"; export * from "./V2DutchOrder"; +// TODO: To make v3 exports cleaner, export here but resolve ambiguous names vs V2 export type UniswapXOrder = | DutchOrder diff --git a/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.test.ts b/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.test.ts new file mode 100644 index 00000000..fe0ea5df --- /dev/null +++ b/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.test.ts @@ -0,0 +1,141 @@ +import { Currency, Ether, Token, TradeType } from "@uniswap/sdk-core"; +import { BigNumber, constants, ethers } from "ethers"; + +import { UnsignedV3DutchOrderInfo } from "../order/V3DutchOrder"; + +import { V3DutchOrderTrade } from "./V3DutchOrderTrade"; +import { NativeAssets } from "./utils"; + +const USDC = new Token( + 1, + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + 6, + "USDC" + ); + const DAI = new Token( + 1, + "0x6B175474E89094C44Da98b954EedeAC495271d0F", + 18, + "DAI" + ); + + describe("V3DutchOrderTrade", () => { + const NON_FEE_OUTPUT_AMOUNT = BigNumber.from("1000000000000000000"); + const NON_FEE_MINIMUM_AMOUNT_OUT = BigNumber.from("900000000000000000"); + + const orderInfo: UnsignedV3DutchOrderInfo = { + deadline: Math.floor(new Date().getTime() / 1000) + 1000, + reactor: "0x0000000000000000000000000000000000000000", + swapper: "0x0000000000000000000000000000000000000000", + nonce: BigNumber.from(10), + cosigner: "0x0000000000000000000000000000000000000000", + additionalValidationContract: ethers.constants.AddressZero, + additionalValidationData: "0x", + input: { + token: USDC.address, + startAmount: BigNumber.from(1000), + curve: { + relativeBlocks: [1], + relativeAmounts: [BigNumber.from(0)], + }, + maxAmount: BigNumber.from(1000), + }, + outputs: [ + { + token: DAI.address, + startAmount: NON_FEE_OUTPUT_AMOUNT, + curve: { + relativeBlocks: [21], + relativeAmounts: [BigNumber.from("100000000000000000")], + }, + recipient: "0x0000000000000000000000000000000000000000", + }, + { + token: DAI.address, + startAmount: BigNumber.from("1000"), + curve: { + relativeBlocks: [21], + relativeAmounts: [BigNumber.from("100")], + }, + recipient: "0x0000000000000000000000000000000000000000", + }, + ], + }; + + const trade = new V3DutchOrderTrade({ + currencyIn: USDC, + currenciesOut: [DAI], + orderInfo, + tradeType: TradeType.EXACT_INPUT, + }); + + it("returns the right input amount for an exact-in trade", () => { + expect(trade.inputAmount.quotient.toString()).toEqual( + orderInfo.input.startAmount.toString() + ); + }); + + it("returns the correct non-fee output amount", () => { + expect(trade.outputAmount.quotient.toString()).toEqual( + NON_FEE_OUTPUT_AMOUNT.toString() + ); + }); + + it("returns the correct minimum amount out", () => { + expect(trade.minimumAmountOut().quotient.toString()).toEqual( + NON_FEE_MINIMUM_AMOUNT_OUT.toString() + ); + }); + + it("works for native output trades", () => { + const ethOutputOrderInfo = { + ...orderInfo, + outputs: [ + { + token: NativeAssets.ETH, + startAmount: NON_FEE_OUTPUT_AMOUNT, + curve: { + relativeBlocks: [21], + relativeAmounts: [BigNumber.from("100000000000000000")], + }, + recipient: "0x0000000000000000000000000000000000000000", + }, + ], + }; + const ethOutputTrade = new V3DutchOrderTrade( + { + currencyIn: USDC, + currenciesOut: [Ether.onChain(1)], + orderInfo: ethOutputOrderInfo, + tradeType: TradeType.EXACT_INPUT, + } + ); + expect(ethOutputTrade.outputAmount.currency).toEqual(Ether.onChain(1)); + }); + + it("works for native output trades where order info has 0 address", () => { + const ethOutputOrderInfo = { + ...orderInfo, + outputs: [ + { + token: constants.AddressZero, + startAmount: NON_FEE_OUTPUT_AMOUNT, + curve: { + relativeBlocks: [21], + relativeAmounts: [BigNumber.from("100000000000000000")], + }, + recipient: "0x0000000000000000000000000000000000000000", + }, + ], + }; + const ethOutputTrade = new V3DutchOrderTrade( + { + currencyIn: USDC, + currenciesOut: [Ether.onChain(1)], + orderInfo: ethOutputOrderInfo, + tradeType: TradeType.EXACT_INPUT, + } + ); + expect(ethOutputTrade.outputAmount.currency).toEqual(Ether.onChain(1)); + }); +}); \ No newline at end of file diff --git a/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.ts b/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.ts new file mode 100644 index 00000000..005e0e52 --- /dev/null +++ b/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.ts @@ -0,0 +1,159 @@ +import { Currency, CurrencyAmount, Price, TradeType } from "@uniswap/sdk-core"; + +import { UnsignedV3DutchOrder, UnsignedV3DutchOrderInfo } from "../order/V3DutchOrder"; +import { getEndAmount } from "../utils/dutchBlockDecay"; + +import { areCurrenciesEqual } from "./utils"; + +export class V3DutchOrderTrade< + TInput extends Currency, + TOutput extends Currency, + TTradeType extends TradeType +> { + public readonly tradeType: TTradeType + public readonly order: UnsignedV3DutchOrder + + private _inputAmount: CurrencyAmount | undefined + private _outputAmounts: CurrencyAmount[] | undefined + + private _currencyIn: TInput + private _currenciesOut: TOutput[] + + public constructor({ + currencyIn, + currenciesOut, + orderInfo, + tradeType, + }: { + currencyIn: TInput + currenciesOut: TOutput[] + orderInfo: UnsignedV3DutchOrderInfo + tradeType: TTradeType + }) { + this._currencyIn = currencyIn + this._currenciesOut = currenciesOut + this.tradeType = tradeType + + // assume single-chain for now + this.order = new UnsignedV3DutchOrder(orderInfo, currencyIn.chainId) + } + + public get inputAmount(): CurrencyAmount { + if (this._inputAmount) return this._inputAmount + + const amount = CurrencyAmount.fromRawAmount( + this._currencyIn, + this.order.info.input.startAmount.toString() + ) + this._inputAmount = amount + return amount + } + + public get outputAmounts(): CurrencyAmount[] { + if (this._outputAmounts) return this._outputAmounts + + const amounts = this.order.info.outputs.map((output) => { + // assume single chain ids across all outputs for now + const currencyOut = this._currenciesOut.find((currency) => + areCurrenciesEqual(currency, output.token, currency.chainId) + ) + + if (!currencyOut) { + throw new Error("Currency out not found") + } + + return CurrencyAmount.fromRawAmount(currencyOut, output.startAmount.toString()) + }) + + this._outputAmounts = amounts + return amounts + } + + private _firstNonFeeOutputStartEndAmounts: + | { startAmount: CurrencyAmount; endAmount: CurrencyAmount } + | undefined; + + private getFirstNonFeeOutputStartEndAmounts(): { + startAmount: CurrencyAmount + endAmount: CurrencyAmount + } { + if (this._firstNonFeeOutputStartEndAmounts) + return this._firstNonFeeOutputStartEndAmounts; + + if (this.order.info.outputs.length === 0) { + throw new Error("there must be at least one output token"); + } + const output = this.order.info.outputs[0]; + + // assume single chain ids across all outputs for now + const currencyOut = this._currenciesOut.find((currency) => + areCurrenciesEqual(currency, output.token, currency.chainId) + ); + + if (!currencyOut) { + throw new Error( + "currency output from order must exist in currenciesOut list" + ); + } + + const startAmount = CurrencyAmount.fromRawAmount(currencyOut, output.startAmount.toString()) + const nonFeeOutputEndAmount = getEndAmount({ + startAmount: output.startAmount, + relativeAmounts: output.curve.relativeAmounts, + }); + const endAmount = CurrencyAmount.fromRawAmount(currencyOut, nonFeeOutputEndAmount.toString()) + + this._firstNonFeeOutputStartEndAmounts = { startAmount, endAmount } + return { startAmount, endAmount } + } + + // TODO: revise when there are actually multiple output amounts. for now, assume only one non-fee output at a time + public get outputAmount(): CurrencyAmount { + return this.getFirstNonFeeOutputStartEndAmounts().startAmount; + } + + public minimumAmountOut(): CurrencyAmount { + return this.getFirstNonFeeOutputStartEndAmounts().endAmount; + } + + public maximumAmountIn(): CurrencyAmount { + const endAmount = getEndAmount({ + startAmount: this.order.info.input.startAmount, + relativeAmounts: this.order.info.input.curve.relativeAmounts, + }); + return CurrencyAmount.fromRawAmount( + this._currencyIn, + endAmount.toString() + ); + } + + private _executionPrice: Price | undefined; + + /** + * The price expressed in terms of output amount/input amount. + */ + public get executionPrice(): Price { + return ( + this._executionPrice ?? + (this._executionPrice = new Price( + this.inputAmount.currency, + this.outputAmount.currency, + this.inputAmount.quotient, + this.outputAmount.quotient + )) + ); + } + + /** + * Return the execution price after accounting for slippage tolerance + * @returns The execution price + */ + public worstExecutionPrice(): Price { + return new Price( + this.inputAmount.currency, + this.outputAmount.currency, + this.maximumAmountIn().quotient, + this.minimumAmountOut().quotient + ); + } +} \ No newline at end of file diff --git a/sdks/uniswapx-sdk/src/utils/order.ts b/sdks/uniswapx-sdk/src/utils/order.ts index 3b07674a..e5e197f5 100644 --- a/sdks/uniswapx-sdk/src/utils/order.ts +++ b/sdks/uniswapx-sdk/src/utils/order.ts @@ -12,9 +12,9 @@ import { UnsignedPriorityOrder, UnsignedV2DutchOrder, } from "../order"; +import { CosignedV3DutchOrder, UnsignedV3DutchOrder } from "../order/V3DutchOrder"; import { stripHexPrefix } from "."; -import { CosignedV3DutchOrder, UnsignedV3DutchOrder } from "../order/V3DutchOrder"; const UNISWAPX_ORDER_INFO_OFFSET = 64; const RELAY_ORDER_INFO_OFFSET = 64; From 73db68763c9081c87a0de8e6a299d7c329880db9 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Fri, 6 Sep 2024 17:10:41 -0400 Subject: [PATCH 15/41] fix: cosigned order detection --- .../src/builder/V2DutchOrderBuilder.ts | 4 +-- .../src/builder/V3DutchOrderBuilder.ts | 4 +-- sdks/uniswapx-sdk/src/order/V3DutchOrder.ts | 12 +++++++ sdks/uniswapx-sdk/src/utils/order.ts | 31 ++++++++++++------- 4 files changed, 36 insertions(+), 15 deletions(-) diff --git a/sdks/uniswapx-sdk/src/builder/V2DutchOrderBuilder.ts b/sdks/uniswapx-sdk/src/builder/V2DutchOrderBuilder.ts index 61b74c13..e5ad715d 100644 --- a/sdks/uniswapx-sdk/src/builder/V2DutchOrderBuilder.ts +++ b/sdks/uniswapx-sdk/src/builder/V2DutchOrderBuilder.ts @@ -11,7 +11,7 @@ import { UnsignedV2DutchOrder, } from "../order"; import { ValidationInfo } from "../order/validation"; -import { getPermit2, getReactor, isCosigned } from "../utils"; +import { getPermit2, getReactor, isCosignedV2 } from "../utils"; import { OrderBuilder } from "./OrderBuilder"; @@ -40,7 +40,7 @@ export class V2DutchOrderBuilder extends OrderBuilder { builder.output(output); } - if (isCosigned(order)) { + if (isCosignedV2(order)) { builder.cosignature(order.info.cosignature); builder.decayEndTime(order.info.cosignerData.decayEndTime); builder.decayStartTime(order.info.cosignerData.decayStartTime); diff --git a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts index 8da3bf39..b3a09e3b 100644 --- a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts +++ b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts @@ -4,7 +4,7 @@ import invariant from "tiny-invariant"; import { OrderType } from "../constants"; import { CosignedV3DutchOrder, CosignedV3DutchOrderInfo, CosignerData, UnsignedV3DutchOrder } from "../order/V3DutchOrder"; import { V3DutchInput, V3DutchOutput } from "../order/types"; -import { getPermit2, getReactor, isCosigned } from "../utils"; +import { getPermit2, getReactor, isCosignedV3 } from "../utils"; import { getEndAmount } from "../utils/dutchBlockDecay"; import { OrderBuilder } from "./OrderBuilder"; @@ -29,7 +29,7 @@ export class V3DutchOrderBuilder extends OrderBuilder { builder.output(output); }); - if (isCosigned(order)) { + if (isCosignedV3(order)) { builder.cosignature(order.info.cosignature); builder.decayStartBlock(order.info.cosignerData.decayStartBlock); builder.exclusiveFiller(order.info.cosignerData.exclusiveFiller); diff --git a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts index 001a7a5d..3d3d4766 100644 --- a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts +++ b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts @@ -297,6 +297,18 @@ export class UnsignedV3DutchOrder implements OffChainOrder { ) } + static parse( + encoded: string, + chainId: number, + permit2?: string + ): UnsignedV3DutchOrder { + return new UnsignedV3DutchOrder( + parseSerializedOrder(encoded), + chainId, + permit2 + ); + } + } export class CosignedV3DutchOrder extends UnsignedV3DutchOrder { diff --git a/sdks/uniswapx-sdk/src/utils/order.ts b/sdks/uniswapx-sdk/src/utils/order.ts index e5e197f5..4f41a88f 100644 --- a/sdks/uniswapx-sdk/src/utils/order.ts +++ b/sdks/uniswapx-sdk/src/utils/order.ts @@ -101,6 +101,16 @@ export class UniswapXOrderParser extends OrderParser { // if cosignature exists then returned cosigned version return cosignedOrder; } + case OrderType.Dutch_V3: { + // cosigned and unsigned serialized versions are the same format + const cosignedOrder = CosignedV3DutchOrder.parse(order, chainId); + // if no cosignature, returned unsigned variant + if (cosignedOrder.info.cosignature === "0x") { + return UnsignedV3DutchOrder.parse(order, chainId); + } + // if cosignature exists then returned cosigned version + return cosignedOrder; + } case OrderType.Priority: { // cosigned and unsigned serialized versions are the same format const cosignedOrder = CosignedPriorityOrder.parse(order, chainId); @@ -149,15 +159,14 @@ export class RelayOrderParser extends OrderParser { } } -export function isCosigned( - order: UnsignedV2DutchOrder | CosignedV2DutchOrder | UnsignedV3DutchOrder | CosignedV3DutchOrder -): order is CosignedV2DutchOrder | CosignedV3DutchOrder { - const parser = new UniswapXOrderParser(); - if (parser.getOrderType(order) === OrderType.Dutch_V2) { - return (order as CosignedV2DutchOrder).info.cosignature !== undefined; - } else if (parser.getOrderType(order) === OrderType.Dutch_V3) { - return (order as CosignedV3DutchOrder).info.cosignature !== undefined; - } else { - return false; - } +export function isCosignedV2( + order: UnsignedV2DutchOrder | CosignedV2DutchOrder +): order is CosignedV2DutchOrder { + return (order as CosignedV2DutchOrder).info.cosignature !== undefined; } + +export function isCosignedV3( + order: UnsignedV3DutchOrder | CosignedV3DutchOrder +): order is CosignedV3DutchOrder { + return (order as CosignedV3DutchOrder).info.cosignature !== undefined; +} \ No newline at end of file From 54888da27d4c9f0545ae734805f0b7d929ab9692 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Wed, 11 Sep 2024 11:55:15 -0400 Subject: [PATCH 16/41] fix: tiny, linting --- .../src/builder/V3DutchOrderBuilder.ts | 1 - sdks/uniswapx-sdk/src/constants.test.ts | 2 +- sdks/uniswapx-sdk/src/constants.ts | 2 +- sdks/uniswapx-sdk/src/order/V3DutchOrder.ts | 35 +++++++++++-------- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts index b3a09e3b..5a8a8d15 100644 --- a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts +++ b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts @@ -270,5 +270,4 @@ export class V3DutchOrderBuilder extends OrderBuilder { this.permit2Address ); } - //TODO: fromOrder() } \ No newline at end of file diff --git a/sdks/uniswapx-sdk/src/constants.test.ts b/sdks/uniswapx-sdk/src/constants.test.ts index dd608284..ffd32958 100644 --- a/sdks/uniswapx-sdk/src/constants.test.ts +++ b/sdks/uniswapx-sdk/src/constants.test.ts @@ -28,7 +28,7 @@ describe("REACTOR_ADDRESS_MAPPING", () => { "42161": Object { "Dutch": "0x0000000000000000000000000000000000000000", "Dutch_V2": "0x1bd1aAdc9E230626C44a139d7E70d842749351eb", - "Dutch_V3": "0x4200000000000000000000000000000000000000", + "Dutch_V3": "0x0000000000000000000000000000000000000000", "Relay": "0x0000000000000000000000000000000000000000", }, "5": Object { diff --git a/sdks/uniswapx-sdk/src/constants.ts b/sdks/uniswapx-sdk/src/constants.ts index 17387946..dbf1b54d 100644 --- a/sdks/uniswapx-sdk/src/constants.ts +++ b/sdks/uniswapx-sdk/src/constants.ts @@ -95,7 +95,7 @@ export const REACTOR_ADDRESS_MAPPING: ReactorMapping = { [OrderType.Dutch_V2]: "0x1bd1aAdc9E230626C44a139d7E70d842749351eb", [OrderType.Dutch]: "0x0000000000000000000000000000000000000000", [OrderType.Relay]: "0x0000000000000000000000000000000000000000", - [OrderType.Dutch_V3]: "0x4200000000000000000000000000000000000000", + [OrderType.Dutch_V3]: "0x0000000000000000000000000000000000000000", }, 8453: { [OrderType.Dutch]: "0x0000000000000000000000000000000000000000", diff --git a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts index 3d3d4766..08d0ccfa 100644 --- a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts +++ b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts @@ -285,11 +285,11 @@ export class UnsignedV3DutchOrder implements OffChainOrder { [COSIGNER_DATA_TUPLE_ABI], [ [ - cosignerData.decayStartBlock, - cosignerData.exclusiveFiller, - cosignerData.exclusivityOverrideBps, - cosignerData.inputOverride, - cosignerData.outputOverrides, + cosignerData.decayStartBlock, + cosignerData.exclusiveFiller, + cosignerData.exclusivityOverrideBps, + cosignerData.inputOverride, + cosignerData.outputOverrides, ], ], ), @@ -471,17 +471,22 @@ function parseSerializedOrder(serialized: string): CosignedV3DutchOrderInfo { maxAmount, }, outputs: outputs.map( - ([token, startAmount,[relativeBlocks, relativeAmounts],recipient]: [ - string, number, [BigNumber, number[]], string, boolean + ([token, startAmount, [relativeBlocks, relativeAmounts], recipient]: [ + string, + number, + [BigNumber, number[]], + string, + boolean ]) => ({ - token, - startAmount, - curve: { - relativeBlocks: decodeRelativeBlocks(relativeBlocks), - relativeAmounts, - }, - recipient, - })), + token, + startAmount, + curve: { + relativeBlocks: decodeRelativeBlocks(relativeBlocks), + relativeAmounts, + }, + recipient, + }) + ), cosignerData: { decayStartBlock: decayStartBlock.toNumber(), exclusiveFiller, From 8ef21c76b53faee1bfffbf7de223909e38470be4 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Wed, 11 Sep 2024 15:12:18 -0400 Subject: [PATCH 17/41] refactor: rename CosignerData types --- sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts | 6 +++--- sdks/uniswapx-sdk/src/order/V3DutchOrder.ts | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts index 5a8a8d15..822944f2 100644 --- a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts +++ b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts @@ -2,7 +2,7 @@ import { BigNumber, ethers } from "ethers"; import invariant from "tiny-invariant"; import { OrderType } from "../constants"; -import { CosignedV3DutchOrder, CosignedV3DutchOrderInfo, CosignerData, UnsignedV3DutchOrder } from "../order/V3DutchOrder"; +import { CosignedV3DutchOrder, CosignedV3DutchOrderInfo, V3CosignerData, UnsignedV3DutchOrder } from "../order/V3DutchOrder"; import { V3DutchInput, V3DutchOutput } from "../order/types"; import { getPermit2, getReactor, isCosignedV3 } from "../utils"; import { getEndAmount } from "../utils/dutchBlockDecay"; @@ -136,7 +136,7 @@ export class V3DutchOrderBuilder extends OrderBuilder { return this; } - private initializeCosignerData(data: Partial): void { + private initializeCosignerData(data: Partial): void { this.info.cosignerData = { decayStartBlock: 0, exclusiveFiller: ethers.constants.AddressZero, @@ -197,7 +197,7 @@ export class V3DutchOrderBuilder extends OrderBuilder { return this; } - cosignerData(cosignerData: CosignerData): this { + cosignerData(cosignerData: V3CosignerData): this { this.decayStartBlock(cosignerData.decayStartBlock); this.exclusiveFiller(cosignerData.exclusiveFiller); this.exclusivityOverrideBps(cosignerData.exclusivityOverrideBps); diff --git a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts index 08d0ccfa..e232ed95 100644 --- a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts +++ b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts @@ -9,7 +9,7 @@ import { BlockOverrides, OffChainOrder, OrderInfo, V3DutchInput, V3DutchInputJSO import { originalIfZero } from "."; -export type CosignerDataJSON = { +export type V3CosignerDataJSON = { decayStartBlock: number; exclusiveFiller: string; exclusivityOverrideBps: number; @@ -23,7 +23,7 @@ export type UnsignedV3DutchOrderInfoJSON = Omit Date: Wed, 11 Sep 2024 15:27:13 -0400 Subject: [PATCH 18/41] refactor: move order amount helper to utils --- sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts | 2 +- sdks/uniswapx-sdk/src/order/V2DutchOrder.ts | 3 +-- sdks/uniswapx-sdk/src/order/V3DutchOrder.ts | 3 +-- sdks/uniswapx-sdk/src/order/index.ts | 10 ++-------- sdks/uniswapx-sdk/src/utils/order.ts | 6 +++++- 5 files changed, 10 insertions(+), 14 deletions(-) diff --git a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts index 822944f2..a9701278 100644 --- a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts +++ b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts @@ -2,7 +2,7 @@ import { BigNumber, ethers } from "ethers"; import invariant from "tiny-invariant"; import { OrderType } from "../constants"; -import { CosignedV3DutchOrder, CosignedV3DutchOrderInfo, V3CosignerData, UnsignedV3DutchOrder } from "../order/V3DutchOrder"; +import { CosignedV3DutchOrder, CosignedV3DutchOrderInfo, UnsignedV3DutchOrder, V3CosignerData } from "../order/V3DutchOrder"; import { V3DutchInput, V3DutchOutput } from "../order/types"; import { getPermit2, getReactor, isCosignedV3 } from "../utils"; import { getEndAmount } from "../utils/dutchBlockDecay"; diff --git a/sdks/uniswapx-sdk/src/order/V2DutchOrder.ts b/sdks/uniswapx-sdk/src/order/V2DutchOrder.ts index 9a8645c7..72522003 100644 --- a/sdks/uniswapx-sdk/src/order/V2DutchOrder.ts +++ b/sdks/uniswapx-sdk/src/order/V2DutchOrder.ts @@ -10,6 +10,7 @@ import { BigNumber, ethers } from "ethers"; import { getPermit2 } from "../utils"; import { ResolvedUniswapXOrder } from "../utils/OrderQuoter"; import { getDecayedAmount } from "../utils/dutchDecay"; +import { originalIfZero } from "../utils/order"; import { BlockOverrides, @@ -23,8 +24,6 @@ import { } from "./types"; import { CustomOrderValidation, parseValidation } from "./validation"; -import { originalIfZero } from "."; - export type CosignerData = { decayStartTime: number; decayEndTime: number; diff --git a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts index e232ed95..75a3eeb1 100644 --- a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts +++ b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts @@ -4,11 +4,10 @@ import { BigNumber, ethers } from "ethers"; import { getPermit2, ResolvedUniswapXOrder } from "../utils"; import { getBlockDecayedAmount } from "../utils/dutchBlockDecay"; +import { originalIfZero } from "../utils/order"; import { BlockOverrides, OffChainOrder, OrderInfo, V3DutchInput, V3DutchInputJSON, V3DutchOutput, V3DutchOutputJSON, V3OrderResolutionOptions } from "./types"; -import { originalIfZero } from "."; - export type V3CosignerDataJSON = { decayStartBlock: number; exclusiveFiller: string; diff --git a/sdks/uniswapx-sdk/src/order/index.ts b/sdks/uniswapx-sdk/src/order/index.ts index 5da03345..899c53a3 100644 --- a/sdks/uniswapx-sdk/src/order/index.ts +++ b/sdks/uniswapx-sdk/src/order/index.ts @@ -1,5 +1,3 @@ -import { BigNumber } from "ethers"; - import { DutchOrder } from "./DutchOrder"; import { CosignedPriorityOrder, UnsignedPriorityOrder } from "./PriorityOrder"; import { RelayOrder } from "./RelayOrder"; @@ -12,7 +10,7 @@ export * from "./RelayOrder"; export * from "./types"; export * from "./validation"; export * from "./V2DutchOrder"; -// TODO: To make v3 exports cleaner, export here but resolve ambiguous names vs V2 +// TODO: To make V3 exports cleaner, export here but resolve ambiguous names vs V2 export type UniswapXOrder = | DutchOrder @@ -23,8 +21,4 @@ export type UniswapXOrder = | UnsignedPriorityOrder | CosignedPriorityOrder; -export type Order = UniswapXOrder | RelayOrder; - -export function originalIfZero(value: BigNumber, original: BigNumber): BigNumber { - return value.isZero() ? original : value; -} \ No newline at end of file +export type Order = UniswapXOrder | RelayOrder; \ No newline at end of file diff --git a/sdks/uniswapx-sdk/src/utils/order.ts b/sdks/uniswapx-sdk/src/utils/order.ts index 4f41a88f..5943d019 100644 --- a/sdks/uniswapx-sdk/src/utils/order.ts +++ b/sdks/uniswapx-sdk/src/utils/order.ts @@ -1,4 +1,4 @@ -import { ethers } from "ethers"; +import { BigNumber, ethers } from "ethers"; import { OrderType, REVERSE_REACTOR_MAPPING } from "../constants"; import { MissingConfiguration } from "../errors"; @@ -169,4 +169,8 @@ export function isCosignedV3( order: UnsignedV3DutchOrder | CosignedV3DutchOrder ): order is CosignedV3DutchOrder { return (order as CosignedV3DutchOrder).info.cosignature !== undefined; +} + +export function originalIfZero(value: BigNumber, original: BigNumber): BigNumber { + return value.isZero() ? original : value; } \ No newline at end of file From d21c5801ffcfc92e11e1c54dcbd071f28cdea10a Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Wed, 11 Sep 2024 15:58:55 -0400 Subject: [PATCH 19/41] refactor: relativeBlocks decoding for clarity --- sdks/uniswapx-sdk/src/order/V3DutchOrder.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts index 75a3eeb1..5230ec65 100644 --- a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts +++ b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts @@ -441,7 +441,7 @@ function parseSerializedOrder(serialized: string): CosignedV3DutchOrderInfo { [ token, startAmount, - [relativeBlocks, relativeAmounts], + [inputRelativeBlocks, relativeAmounts], maxAmount, ], outputs, @@ -450,8 +450,6 @@ function parseSerializedOrder(serialized: string): CosignedV3DutchOrderInfo { ], ] = decoded; - const decodedRelativeBlocks = decodeRelativeBlocks(relativeBlocks); - return { reactor, swapper, @@ -464,13 +462,13 @@ function parseSerializedOrder(serialized: string): CosignedV3DutchOrderInfo { token, startAmount, curve: { - relativeBlocks: decodedRelativeBlocks, + relativeBlocks: decodeRelativeBlocks(inputRelativeBlocks), relativeAmounts, }, maxAmount, }, outputs: outputs.map( - ([token, startAmount, [relativeBlocks, relativeAmounts], recipient]: [ + ([token, startAmount, [outputRelativeBlocks, relativeAmounts], recipient]: [ string, number, [BigNumber, number[]], @@ -480,7 +478,7 @@ function parseSerializedOrder(serialized: string): CosignedV3DutchOrderInfo { token, startAmount, curve: { - relativeBlocks: decodeRelativeBlocks(relativeBlocks), + relativeBlocks: decodeRelativeBlocks(outputRelativeBlocks), relativeAmounts, }, recipient, @@ -504,9 +502,9 @@ function encodeRelativeBlocks(relativeBlocks: number[]): BigNumber { } return packedData; } -/* eslint-disable */ + function decodeRelativeBlocks(packedData: BigNumber): number[] { - let relativeBlocks: number[] = []; + const relativeBlocks: number[] = []; for (let i = 0; i < 16; i++) { const block = packedData.shr(i * 16).toNumber() & 0xFFFF; if (block !== 0) { @@ -514,5 +512,4 @@ function decodeRelativeBlocks(packedData: BigNumber): number[] { } } return relativeBlocks; -} -/* eslint-enable */ \ No newline at end of file +} \ No newline at end of file From 69fa51604a15f46bcb45a45c0de2b56e16662142 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Wed, 11 Sep 2024 16:31:59 -0400 Subject: [PATCH 20/41] refactor: update v3 curve type name to new name in contract --- sdks/uniswapx-sdk/src/order/V3DutchOrder.ts | 6 +++--- sdks/uniswapx-sdk/src/order/types.ts | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts index 5230ec65..1e84b0ff 100644 --- a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts +++ b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts @@ -70,16 +70,16 @@ const V3_DUTCH_ORDER_TYPES = { V3DutchInput: [ { name: "token", type: "address" }, { name: "startAmount", type: "uint256" }, - { name: "curve", type: "V3Decay" }, + { name: "curve", type: "NonlinearDutchDecay" }, { name: "maxAmount", type: "uint256" }, ], V3DutchOutput: [ { name: "token", type: "address" }, { name: "startAmount", type: "uint256" }, - { name: "curve", type: "V3Decay" }, + { name: "curve", type: "NonlinearDutchDecay" }, { name: "recipient", type: "address" }, ], - V3Decay: [ + NonlinearDutchDecay: [ { name: "relativeBlocks", type: "uint256" }, { name: "relativeAmounts", type: "int256[]" }, ], diff --git a/sdks/uniswapx-sdk/src/order/types.ts b/sdks/uniswapx-sdk/src/order/types.ts index bb04e39d..daf25aec 100644 --- a/sdks/uniswapx-sdk/src/order/types.ts +++ b/sdks/uniswapx-sdk/src/order/types.ts @@ -123,17 +123,17 @@ export type PriorityOutputJSON = PriorityInputJSON & { export type V3DutchInput = { readonly token: string; readonly startAmount: BigNumber; - readonly curve: V3Decay; + readonly curve: NonlinearDutchDecay; readonly maxAmount: BigNumber; }; export type V3DutchInputJSON = Omit & { startAmount: string; - curve: V3Decay; + curve: NonlinearDutchDecay; maxAmount: string; }; -export type V3Decay = { +export type NonlinearDutchDecay = { relativeBlocks: number[]; relativeAmounts: BigNumber[]; //amounts plural }; @@ -141,7 +141,7 @@ export type V3Decay = { export type V3DutchOutput = { readonly token: string; readonly startAmount: BigNumber; - readonly curve: V3Decay; + readonly curve: NonlinearDutchDecay; readonly recipient: string; }; From 81710abefdac9e718fc7daa2ef2e6c821455840d Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Wed, 11 Sep 2024 16:32:19 -0400 Subject: [PATCH 21/41] style: linting --- .../uniswapx-sdk/src/utils/dutchBlockDecay.ts | 207 +++++++++--------- 1 file changed, 105 insertions(+), 102 deletions(-) diff --git a/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.ts b/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.ts index 4596d663..a4eb8593 100644 --- a/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.ts +++ b/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.ts @@ -1,120 +1,123 @@ import { BigNumber } from "ethers"; -import { V3Decay } from "../order"; +import { NonlinearDutchDecay } from "../order"; /* These functions mimic the smart contract functions as closely as possible to ensure that the same results are produced. Essentially Solidity translated to TypeScript. */ -/* eslint-disable */ function locateArrayPosition( - curve: V3Decay, - targetValue: number - ): [number, number] { - const relativeBlocks = curve.relativeBlocks; - let prev = 0; - let next = 0; - while (next < curve.relativeAmounts.length) { - if (relativeBlocks[next] >= targetValue) { - return [prev, next]; - } - prev = next; - next++; - } - return [next - 1, next - 1]; - } - class NonLinearDutchDecayLib { - static decay( - curve: V3Decay, - startAmount: BigNumber, - decayStartBlock: number, - currentBlock: number - ): BigNumber { - // mismatch of relativeAmounts and relativeBlocks - if (curve.relativeAmounts.length > 16) { - throw new Error('InvalidDecayCurve'); - } - - // handle current block before decay or no decay - if (decayStartBlock >= currentBlock || curve.relativeAmounts.length === 0) { - return startAmount; - } - - const blockDelta = currentBlock - decayStartBlock; - - // Special case for when we need to use the decayStartBlock (0) - if (curve.relativeBlocks[0] > blockDelta) { - return this.linearDecay( - 0, - curve.relativeBlocks[0], - blockDelta, - startAmount, - startAmount.sub(curve.relativeAmounts[0]) - ); - } - - // the current pos is within or after the curve - let [prev, next] = locateArrayPosition(curve, blockDelta); - const lastAmount = startAmount.sub(curve.relativeAmounts[prev]); - const nextAmount = startAmount.sub(curve.relativeAmounts[next]); - return this.linearDecay( - curve.relativeBlocks[prev], - curve.relativeBlocks[next], - blockDelta, - lastAmount, - nextAmount - ); - } - - static linearDecay( - startPoint: number, - endPoint: number, - currentPoint: number, - startAmount: BigNumber, - endAmount: BigNumber - ): BigNumber { - if (currentPoint >= endPoint) { - return endAmount; - } - - const elapsed = BigNumber.from(currentPoint - startPoint); - const duration = BigNumber.from(endPoint - startPoint); - if (endAmount.lt(startAmount)) { - return startAmount.sub( - startAmount.sub(endAmount.mul(elapsed).div(duration)) //muldivdown in contract - ); - } else { - return startAmount.add( - endAmount.sub(startAmount.mul(elapsed).div(duration)) //muldivup in contract - //TODO: How can we do muldivup in JS? - ); - } - } - } - - export { NonLinearDutchDecayLib }; + curve: NonlinearDutchDecay, + targetValue: number +): [number, number] { + const relativeBlocks = curve.relativeBlocks; + // eslint-disable-next-line prefer-const + let prev = 0; + // eslint-disable-next-line prefer-const + let next = 0; + + while (next < curve.relativeAmounts.length) { + if (relativeBlocks[next] >= targetValue) { + return [prev, next]; + } + prev = next; + next++; + } + + return [next - 1, next - 1]; +} + +class NonLinearDutchDecayLib { + static decay( + curve: NonlinearDutchDecay, + startAmount: BigNumber, + decayStartBlock: number, + currentBlock: number + ): BigNumber { + // mismatch of relativeAmounts and relativeBlocks + if (curve.relativeAmounts.length > 16) { + throw new Error('InvalidDecayCurve'); + } + + // handle current block before decay or no decay + if (decayStartBlock >= currentBlock || curve.relativeAmounts.length === 0) { + return startAmount; + } + + const blockDelta = currentBlock - decayStartBlock; + + // Special case for when we need to use the decayStartBlock (0) + if (curve.relativeBlocks[0] > blockDelta) { + return this.linearDecay( + 0, + curve.relativeBlocks[0], + blockDelta, + startAmount, + startAmount.sub(curve.relativeAmounts[0]) + ); + } + + // the current pos is within or after the curve + const [prev, next] = locateArrayPosition(curve, blockDelta); + const lastAmount = startAmount.sub(curve.relativeAmounts[prev]); + const nextAmount = startAmount.sub(curve.relativeAmounts[next]); + return this.linearDecay( + curve.relativeBlocks[prev], + curve.relativeBlocks[next], + blockDelta, + lastAmount, + nextAmount + ); + } + + static linearDecay( + startPoint: number, + endPoint: number, + currentPoint: number, + startAmount: BigNumber, + endAmount: BigNumber + ): BigNumber { + if (currentPoint >= endPoint) { + return endAmount; + } + + const elapsed = BigNumber.from(currentPoint - startPoint); + const duration = BigNumber.from(endPoint - startPoint); + if (endAmount.lt(startAmount)) { + return startAmount.sub( + startAmount.sub(endAmount.mul(elapsed).div(duration)) //muldivdown in contract + ); + } else { + return startAmount.add( + endAmount.sub(startAmount.mul(elapsed).div(duration)) //muldivup in contract + //TODO: How can we do muldivup in JS? + ); + } + } +} + +export { NonLinearDutchDecayLib }; export interface DutchBlockDecayConfig { - decayStartBlock: number; - startAmount: BigNumber; - relativeBlocks: number[]; - relativeAmounts: BigNumber[]; + decayStartBlock: number; + startAmount: BigNumber; + relativeBlocks: number[]; + relativeAmounts: BigNumber[]; } export function getBlockDecayedAmount( - config: DutchBlockDecayConfig, - atBlock: number + config: DutchBlockDecayConfig, + atBlock: number ): BigNumber { - const {decayStartBlock, startAmount, relativeBlocks, relativeAmounts} = config; - return NonLinearDutchDecayLib.decay({relativeAmounts, relativeBlocks}, startAmount, decayStartBlock, atBlock); + const { decayStartBlock, startAmount, relativeBlocks, relativeAmounts } = config; + return NonLinearDutchDecayLib.decay({ relativeAmounts, relativeBlocks }, startAmount, decayStartBlock, atBlock); } export function getEndAmount( - config: Partial + config: Partial ): BigNumber { - const { startAmount, relativeAmounts } = config; - if (!startAmount || !relativeAmounts) { - throw new Error("Invalid config for getting V3 decay end amount"); //TODO: Should we throw? - } - return startAmount.sub(relativeAmounts[relativeAmounts.length - 1]); + const { startAmount, relativeAmounts } = config; + if (!startAmount || !relativeAmounts) { + throw new Error("Invalid config for getting V3 decay end amount"); //TODO: Should we throw? + } + return startAmount.sub(relativeAmounts[relativeAmounts.length - 1]); } -/* eslint-enable */ \ No newline at end of file From d63c65e325b329631e50e94a4b8adc571a80f20c Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Wed, 11 Sep 2024 17:44:06 -0400 Subject: [PATCH 22/41] fix, test: dutchBlockDecay rounding math --- .../src/utils/dutchBlockDecay.test.ts | 26 +++++++++++++++++++ .../uniswapx-sdk/src/utils/dutchBlockDecay.ts | 4 +-- 2 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 sdks/uniswapx-sdk/src/utils/dutchBlockDecay.test.ts diff --git a/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.test.ts b/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.test.ts new file mode 100644 index 00000000..f8d0ff80 --- /dev/null +++ b/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.test.ts @@ -0,0 +1,26 @@ +import { BigNumber } from "ethers"; + +import { NonLinearDutchDecayLib } from "./dutchBlockDecay"; + +describe("NonLinearDutchDecayLib", () => { + it("Simple linearDecay", () => { + const result = NonLinearDutchDecayLib.linearDecay(0, 10, 5, BigNumber.from(100), BigNumber.from(50)); + expect(result.toString()).toEqual('75'); + }); + + it("Test for mulDivDown for endAmount < startAmount", () => { + const result = NonLinearDutchDecayLib.linearDecay(0, 10, 5, BigNumber.from(100), BigNumber.from(75)); + expect(result.toString()).toEqual('88'); //If we successfully emulated mulDivDown then this is 88. + }); + + it("Simple linearDecay but with endAmount > startAmount", () => { + const result = NonLinearDutchDecayLib.linearDecay(0, 10, 5, BigNumber.from(100), BigNumber.from(120)); + expect(result.toString()).toEqual('110'); + }); + + it("Test for mulDivUp for endAmount > startAmount", () => { + const result = NonLinearDutchDecayLib.linearDecay(0, 10, 5, BigNumber.from(100), BigNumber.from(125)); + //if we successfully emulated mulDivUp then this is 113 + expect(result.toString()).toEqual('113'); + }); +}); \ No newline at end of file diff --git a/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.ts b/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.ts index a4eb8593..e61eb362 100644 --- a/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.ts +++ b/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.ts @@ -84,11 +84,11 @@ class NonLinearDutchDecayLib { const duration = BigNumber.from(endPoint - startPoint); if (endAmount.lt(startAmount)) { return startAmount.sub( - startAmount.sub(endAmount.mul(elapsed).div(duration)) //muldivdown in contract + (startAmount.sub(endAmount)).mul(elapsed).div(duration) //muldivdown in contract ); } else { return startAmount.add( - endAmount.sub(startAmount.mul(elapsed).div(duration)) //muldivup in contract + (endAmount.sub(startAmount)).mul(elapsed).div(duration) //muldivup in contract //TODO: How can we do muldivup in JS? ); } From 5af9cf1a2ee422b97e7e56d073fc0071faadfb51 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Thu, 12 Sep 2024 11:36:49 -0400 Subject: [PATCH 23/41] feat, test: support negative relativeAmounts with BigInt --- .../src/builder/V3DutchOrderBuilder.test.ts | 58 +++++++++---------- .../src/order/V3DutchOrder.test.ts | 31 ++++++++-- sdks/uniswapx-sdk/src/order/V3DutchOrder.ts | 11 ++-- sdks/uniswapx-sdk/src/order/types.ts | 2 +- .../src/trade/V3DutchOrderTrade.test.ts | 10 ++-- .../uniswapx-sdk/src/utils/dutchBlockDecay.ts | 11 ++-- 6 files changed, 71 insertions(+), 52 deletions(-) diff --git a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.test.ts b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.test.ts index 0452d781..d7be04f9 100644 --- a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.test.ts +++ b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.test.ts @@ -28,7 +28,7 @@ describe("V3DutchOrderBuilder", () => { startAmount: INPUT_START_AMOUNT, curve: { relativeBlocks: [1], - relativeAmounts: [BigNumber.from(0)], + relativeAmounts: [BigInt(0)], }, maxAmount: INPUT_START_AMOUNT.add(1), }) @@ -37,7 +37,7 @@ describe("V3DutchOrderBuilder", () => { startAmount: OUTPUT_START_AMOUNT, curve: { relativeBlocks: [4], - relativeAmounts: [BigNumber.from(4)], + relativeAmounts: [BigInt(4)], }, recipient: constants.AddressZero, }) @@ -67,7 +67,7 @@ describe("V3DutchOrderBuilder", () => { startAmount: INPUT_START_AMOUNT, curve: { relativeBlocks: [1], - relativeAmounts: [BigNumber.from(0)], + relativeAmounts: [BigInt(0)], }, maxAmount: INPUT_START_AMOUNT.add(1), }) @@ -76,7 +76,7 @@ describe("V3DutchOrderBuilder", () => { startAmount: OUTPUT_START_AMOUNT, curve: { relativeBlocks: [4], - relativeAmounts: [BigNumber.from(4)], + relativeAmounts: [BigInt(4)], }, recipient: constants.AddressZero, }) @@ -85,7 +85,7 @@ describe("V3DutchOrderBuilder", () => { startAmount: OUTPUT_START_AMOUNT, curve: { relativeBlocks: [17], - relativeAmounts: [BigNumber.from(17)], + relativeAmounts: [BigInt(17)], }, recipient: constants.AddressZero, }) @@ -107,7 +107,7 @@ describe("V3DutchOrderBuilder", () => { startAmount: INPUT_START_AMOUNT, curve: { relativeBlocks: [1], - relativeAmounts: [BigNumber.from(0)], + relativeAmounts: [BigInt(0)], }, maxAmount: INPUT_START_AMOUNT.add(1), }) @@ -116,7 +116,7 @@ describe("V3DutchOrderBuilder", () => { startAmount: OUTPUT_START_AMOUNT, curve: { relativeBlocks: [4], - relativeAmounts: [BigNumber.from(4)], + relativeAmounts: [BigInt(4)], }, recipient: constants.AddressZero, }) @@ -141,7 +141,7 @@ describe("V3DutchOrderBuilder", () => { startAmount: INPUT_START_AMOUNT, curve: { relativeBlocks: [1], - relativeAmounts: [BigNumber.from(0)], + relativeAmounts: [BigInt(0)], }, maxAmount: INPUT_START_AMOUNT.add(1), }) @@ -150,7 +150,7 @@ describe("V3DutchOrderBuilder", () => { startAmount: OUTPUT_START_AMOUNT, curve: { relativeBlocks: [4], - relativeAmounts: [BigNumber.from(4)], + relativeAmounts: [BigInt(4)], }, recipient: constants.AddressZero, }) @@ -172,7 +172,7 @@ describe("V3DutchOrderBuilder", () => { startAmount: INPUT_START_AMOUNT, curve: { relativeBlocks: [1], - relativeAmounts: [BigNumber.from(0)], + relativeAmounts: [BigInt(0)], }, maxAmount: INPUT_START_AMOUNT.add(1), }) @@ -181,7 +181,7 @@ describe("V3DutchOrderBuilder", () => { startAmount: OUTPUT_START_AMOUNT, curve: { relativeBlocks: [4], - relativeAmounts: [BigNumber.from(4)], + relativeAmounts: [BigInt(4)], }, recipient: constants.AddressZero, }) @@ -205,7 +205,7 @@ describe("V3DutchOrderBuilder", () => { startAmount: INPUT_START_AMOUNT, curve: { relativeBlocks: [1], - relativeAmounts: [BigNumber.from(0)], + relativeAmounts: [BigInt(0)], }, maxAmount: INPUT_START_AMOUNT.add(1), }) @@ -214,7 +214,7 @@ describe("V3DutchOrderBuilder", () => { startAmount: OUTPUT_START_AMOUNT, curve: { relativeBlocks: [4], - relativeAmounts: [BigNumber.from(4)], + relativeAmounts: [BigInt(4)], }, recipient: constants.AddressZero, }) @@ -240,7 +240,7 @@ describe("V3DutchOrderBuilder", () => { startAmount: OUTPUT_START_AMOUNT, curve: { relativeBlocks: [4], - relativeAmounts: [BigNumber.from(4)], + relativeAmounts: [BigInt(4)], }, recipient: constants.AddressZero, }) @@ -265,7 +265,7 @@ describe("V3DutchOrderBuilder", () => { startAmount: INPUT_START_AMOUNT, curve: { relativeBlocks: [1], - relativeAmounts: [BigNumber.from(0)], + relativeAmounts: [BigInt(0)], }, maxAmount: INPUT_START_AMOUNT.add(1), }) @@ -291,7 +291,7 @@ describe("V3DutchOrderBuilder", () => { startAmount: INPUT_START_AMOUNT, curve: { relativeBlocks: [1], - relativeAmounts: [BigNumber.from(0)], + relativeAmounts: [BigInt(0)], }, maxAmount: INPUT_START_AMOUNT.add(1), }) @@ -300,7 +300,7 @@ describe("V3DutchOrderBuilder", () => { startAmount: OUTPUT_START_AMOUNT, curve: { relativeBlocks: [4], - relativeAmounts: [BigNumber.from(4)], + relativeAmounts: [BigInt(4)], }, recipient: constants.AddressZero, }) @@ -325,7 +325,7 @@ describe("V3DutchOrderBuilder", () => { startAmount: INPUT_START_AMOUNT, curve: { relativeBlocks: [1], - relativeAmounts: [BigNumber.from(0)], + relativeAmounts: [BigInt(0)], }, maxAmount: INPUT_START_AMOUNT.add(1), }) @@ -334,7 +334,7 @@ describe("V3DutchOrderBuilder", () => { startAmount: OUTPUT_START_AMOUNT, curve: { relativeBlocks: [4], - relativeAmounts: [BigNumber.from(4)], + relativeAmounts: [BigInt(4)], }, recipient: constants.AddressZero, }) @@ -361,7 +361,7 @@ describe("V3DutchOrderBuilder", () => { startAmount: INPUT_START_AMOUNT, curve: { relativeBlocks: [1], - relativeAmounts: [BigNumber.from(0)], + relativeAmounts: [BigInt(0)], }, maxAmount: INPUT_START_AMOUNT.add(1), }) @@ -370,7 +370,7 @@ describe("V3DutchOrderBuilder", () => { startAmount: OUTPUT_START_AMOUNT, curve: { relativeBlocks: [4], - relativeAmounts: [BigNumber.from(4)], + relativeAmounts: [BigInt(4)], }, recipient: constants.AddressZero, }) @@ -406,7 +406,7 @@ describe("V3DutchOrderBuilder", () => { startAmount: INPUT_START_AMOUNT, curve: { relativeBlocks: [1], - relativeAmounts: [BigNumber.from(0)], + relativeAmounts: [BigInt(0)], }, maxAmount: INPUT_START_AMOUNT.add(1), }) @@ -415,7 +415,7 @@ describe("V3DutchOrderBuilder", () => { startAmount: OUTPUT_START_AMOUNT, curve: { relativeBlocks: [4], - relativeAmounts: [BigNumber.from(4)], + relativeAmounts: [BigInt(4)], }, recipient: constants.AddressZero, }) @@ -437,7 +437,7 @@ describe("V3DutchOrderBuilder", () => { startAmount: INPUT_START_AMOUNT, curve: { relativeBlocks: [1], - relativeAmounts: [BigNumber.from(0)], + relativeAmounts: [BigInt(0)], }, maxAmount: INPUT_START_AMOUNT.add(1), }) @@ -446,7 +446,7 @@ describe("V3DutchOrderBuilder", () => { startAmount: OUTPUT_START_AMOUNT, curve: { relativeBlocks: [4], - relativeAmounts: [BigNumber.from(4)], + relativeAmounts: [BigInt(4)], }, recipient: constants.AddressZero, }) @@ -473,7 +473,7 @@ describe("V3DutchOrderBuilder", () => { startAmount: INPUT_START_AMOUNT, curve: { relativeBlocks: [1], - relativeAmounts: [BigNumber.from(0)], + relativeAmounts: [BigInt(0)], }, maxAmount: INPUT_START_AMOUNT.add(1), }) @@ -482,7 +482,7 @@ describe("V3DutchOrderBuilder", () => { startAmount: OUTPUT_START_AMOUNT, curve: { relativeBlocks: [4], - relativeAmounts: [BigNumber.from(4)], + relativeAmounts: [BigInt(4)], }, recipient: constants.AddressZero, }) @@ -510,7 +510,7 @@ describe("V3DutchOrderBuilder", () => { startAmount: INPUT_START_AMOUNT, curve: { relativeBlocks: [1], - relativeAmounts: [BigNumber.from(0)], + relativeAmounts: [BigInt(0)], }, maxAmount: INPUT_START_AMOUNT.add(1), }) @@ -519,7 +519,7 @@ describe("V3DutchOrderBuilder", () => { startAmount: OUTPUT_START_AMOUNT, curve: { relativeBlocks: [4], - relativeAmounts: [BigNumber.from(4)], + relativeAmounts: [BigInt(4)], }, recipient: constants.AddressZero, }) diff --git a/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts b/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts index 5a60b490..c8c12aec 100644 --- a/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts +++ b/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts @@ -49,7 +49,7 @@ describe("V3DutchOrder", () => { startAmount: RAW_AMOUNT, curve: { relativeBlocks: [1], //TODO: can we have relativeblocks be an array of just 0 - relativeAmounts: [BigNumber.from(0)], // 1e-18, 2e-18, 3e-18, 4e-18 + relativeAmounts: [BigInt(0)], }, maxAmount: RAW_AMOUNT, //we don't want input to change, we're testing for decaying output }, @@ -59,7 +59,7 @@ describe("V3DutchOrder", () => { startAmount: RAW_AMOUNT, curve: { relativeBlocks: [1,2,3,4], - relativeAmounts: [BigNumber.from(1), BigNumber.from(2), BigNumber.from(3), BigNumber.from(4)], // 1e-18, 2e-18, 3e-18, 4e-18 + relativeAmounts: [BigInt(1), BigInt(2), BigInt(3), BigInt(4)], // 1e-18, 2e-18, 3e-18, 4e-18 }, recipient: ethers.constants.AddressZero, }, @@ -81,8 +81,27 @@ describe("V3DutchOrder", () => { const seralized = order.serialize(); const parsed = CosignedV3DutchOrder.parse(seralized, CHAIN_ID); expect(parsed.info).to.deep.eq(orderInfo); - } - ); + }); + + it("Parses a serialized v3 order with negative relativeAmounts", () => { + const orderInfo = getFullOrderInfo({ + outputs: [ + { + token: OUTPUT_TOKEN, + startAmount: RAW_AMOUNT, + curve: { + relativeBlocks: [1,2,3,4], + relativeAmounts: [BigInt(-1), BigInt(-2), BigInt(-3), BigInt(-4)], + }, + recipient: ethers.constants.AddressZero, + }, + ], + }); + const order = new CosignedV3DutchOrder(orderInfo, CHAIN_ID); + const seralized = order.serialize(); + const parsed = CosignedV3DutchOrder.parse(seralized, CHAIN_ID); + expect(parsed.info).to.deep.eq(orderInfo); + }); it("parses inner v3 order with no cosigner overrides, both input and output curves", () => { const orderInfoJSON : UnsignedV3DutchOrderInfoJSON = { @@ -93,7 +112,7 @@ describe("V3DutchOrder", () => { startAmount: "1000000", curve: { relativeBlocks: [1,2,3,4], - relativeAmounts: [BigNumber.from(1), BigNumber.from(2), BigNumber.from(3), BigNumber.from(4)], + relativeAmounts: [BigInt(1), BigInt(2), BigInt(3), BigInt(4)], }, maxAmount: "1000001", }, @@ -103,7 +122,7 @@ describe("V3DutchOrder", () => { startAmount: "1000000", curve: { relativeBlocks: [1,2,3,4], - relativeAmounts: [BigNumber.from(1), BigNumber.from(2), BigNumber.from(3), BigNumber.from(4)], + relativeAmounts: [BigInt(1), BigInt(2), BigInt(3), BigInt(4)], }, recipient: ethers.constants.AddressZero, }, diff --git a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts index 1e84b0ff..3b4ff6f3 100644 --- a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts +++ b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts @@ -122,7 +122,7 @@ export class UnsignedV3DutchOrder implements OffChainOrder { startAmount: BigNumber.from(json.input.startAmount), curve: { relativeBlocks: json.input.curve.relativeBlocks, - relativeAmounts: json.input.curve.relativeAmounts.map(BigNumber.from), + relativeAmounts: json.input.curve.relativeAmounts, }, maxAmount: BigNumber.from(json.input.maxAmount), }, @@ -131,7 +131,7 @@ export class UnsignedV3DutchOrder implements OffChainOrder { startAmount: BigNumber.from(output.startAmount), curve: { relativeBlocks: output.curve.relativeBlocks, - relativeAmounts: output.curve.relativeAmounts.map(BigNumber.from), + relativeAmounts: output.curve.relativeAmounts, }, })), }, @@ -463,7 +463,7 @@ function parseSerializedOrder(serialized: string): CosignedV3DutchOrderInfo { startAmount, curve: { relativeBlocks: decodeRelativeBlocks(inputRelativeBlocks), - relativeAmounts, + relativeAmounts: relativeAmounts.map((amount: BigNumber) => amount.toBigInt()), }, maxAmount, }, @@ -471,15 +471,14 @@ function parseSerializedOrder(serialized: string): CosignedV3DutchOrderInfo { ([token, startAmount, [outputRelativeBlocks, relativeAmounts], recipient]: [ string, number, - [BigNumber, number[]], + [BigNumber, BigNumber[]], //abiDecode automatically converts to BigNumber string, - boolean ]) => ({ token, startAmount, curve: { relativeBlocks: decodeRelativeBlocks(outputRelativeBlocks), - relativeAmounts, + relativeAmounts: relativeAmounts.map((amount: BigNumber) => amount.toBigInt()), }, recipient, }) diff --git a/sdks/uniswapx-sdk/src/order/types.ts b/sdks/uniswapx-sdk/src/order/types.ts index daf25aec..b14c75d7 100644 --- a/sdks/uniswapx-sdk/src/order/types.ts +++ b/sdks/uniswapx-sdk/src/order/types.ts @@ -135,7 +135,7 @@ export type V3DutchInputJSON = Omit Date: Thu, 12 Sep 2024 12:30:46 -0400 Subject: [PATCH 24/41] fix: BigInt -> bigint --- sdks/uniswapx-sdk/src/order/types.ts | 2 +- sdks/uniswapx-sdk/src/utils/dutchBlockDecay.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdks/uniswapx-sdk/src/order/types.ts b/sdks/uniswapx-sdk/src/order/types.ts index b14c75d7..24d6fa45 100644 --- a/sdks/uniswapx-sdk/src/order/types.ts +++ b/sdks/uniswapx-sdk/src/order/types.ts @@ -135,7 +135,7 @@ export type V3DutchInputJSON = Omit Date: Thu, 12 Sep 2024 12:32:34 -0400 Subject: [PATCH 25/41] test: dutchBlockDecay --- .../src/utils/dutchBlockDecay.test.ts | 107 +++++++++++++++--- 1 file changed, 92 insertions(+), 15 deletions(-) diff --git a/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.test.ts b/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.test.ts index f8d0ff80..154b02ae 100644 --- a/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.test.ts +++ b/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.test.ts @@ -3,24 +3,101 @@ import { BigNumber } from "ethers"; import { NonLinearDutchDecayLib } from "./dutchBlockDecay"; describe("NonLinearDutchDecayLib", () => { - it("Simple linearDecay", () => { - const result = NonLinearDutchDecayLib.linearDecay(0, 10, 5, BigNumber.from(100), BigNumber.from(50)); - expect(result.toString()).toEqual('75'); - }); + describe("linearDecay", () => { + it("Simple linearDecay", () => { + const result = NonLinearDutchDecayLib.linearDecay(0, 10, 5, BigNumber.from(100), BigNumber.from(50)); + expect(result.toString()).toEqual('75'); + }); - it("Test for mulDivDown for endAmount < startAmount", () => { - const result = NonLinearDutchDecayLib.linearDecay(0, 10, 5, BigNumber.from(100), BigNumber.from(75)); - expect(result.toString()).toEqual('88'); //If we successfully emulated mulDivDown then this is 88. - }); + it("Test for mulDivDown for endAmount < startAmount", () => { + const result = NonLinearDutchDecayLib.linearDecay(0, 10, 5, BigNumber.from(100), BigNumber.from(75)); + expect(result.toString()).toEqual('88'); //If we successfully emulated mulDivDown then this is 88. + }); + + it("Simple linearDecay but with endAmount > startAmount", () => { + const result = NonLinearDutchDecayLib.linearDecay(0, 10, 5, BigNumber.from(100), BigNumber.from(120)); + expect(result.toString()).toEqual('110'); + }); - it("Simple linearDecay but with endAmount > startAmount", () => { - const result = NonLinearDutchDecayLib.linearDecay(0, 10, 5, BigNumber.from(100), BigNumber.from(120)); - expect(result.toString()).toEqual('110'); + it.skip("Test for mulDivUp for endAmount > startAmount", () => { + const result = NonLinearDutchDecayLib.linearDecay(0, 10, 5, BigNumber.from(100), BigNumber.from(125)); + //if we successfully emulated mulDivUp then this is 113 + expect(result.toString()).toEqual('113'); + }); }); - it("Test for mulDivUp for endAmount > startAmount", () => { - const result = NonLinearDutchDecayLib.linearDecay(0, 10, 5, BigNumber.from(100), BigNumber.from(125)); - //if we successfully emulated mulDivUp then this is 113 - expect(result.toString()).toEqual('113'); + describe("decay", () => { + it("Returns startAmount if decay hasnt started", () => { + const test_payload = { + curve: { + relativeBlocks: [1, 2, 3, 4, 5], + relativeAmounts: [0, 10, 20, 30, 40].map(BigInt), + }, + startAmount: BigNumber.from(100), + decayStartBlock: 0, + currentBlock: 0, + }; + const result = NonLinearDutchDecayLib.decay(test_payload.curve, test_payload.startAmount, test_payload.decayStartBlock, test_payload.currentBlock); + expect(result.toString()).toEqual('100'); + }); + + it("Correctly calculates non-rounding decay", () => { + const test_payload = { + curve: { + relativeBlocks: [4], + relativeAmounts: [40].map(BigInt), + }, + startAmount: BigNumber.from(100), + decayStartBlock: 0, + currentBlock: 2, + }; + const result = NonLinearDutchDecayLib.decay(test_payload.curve, test_payload.startAmount, test_payload.decayStartBlock, test_payload.currentBlock); + expect(result.toString()).toEqual('80'); + }); + + // Add a rounding decay once we clarify mulDivDown/mulDivUp + + it("Correctly calculates non-rounding decay with multiple points", () => { + const test_payload = { + curve: { + relativeBlocks: [4, 6], + relativeAmounts: [40, 20].map(BigInt), + }, + startAmount: BigNumber.from(100), + decayStartBlock: 0, + currentBlock: 5, + }; + const result = NonLinearDutchDecayLib.decay(test_payload.curve, test_payload.startAmount, test_payload.decayStartBlock, test_payload.currentBlock); + expect(result.toString()).toEqual('70'); + }); + + it("Correctly calculates non-rounding negative decay", () => { + const test_payload = { + curve: { + relativeBlocks: [4], + relativeAmounts: [BigInt(-40)], + }, + startAmount: BigNumber.from(100), + decayStartBlock: 0, + currentBlock: 2, + }; + const result = NonLinearDutchDecayLib.decay(test_payload.curve, test_payload.startAmount, test_payload.decayStartBlock, test_payload.currentBlock); + expect(result.toString()).toEqual('120'); + }); + + it("Correctly calculates non-rounding negative decay with multiple points", () => { + const test_payload = { + curve: { + relativeBlocks: [4, 6], + relativeAmounts: [BigInt(-40), BigInt(-20)], + }, + startAmount: BigNumber.from(100), + decayStartBlock: 0, + currentBlock: 5, + }; + const result = NonLinearDutchDecayLib.decay(test_payload.curve, test_payload.startAmount, test_payload.decayStartBlock, test_payload.currentBlock); + expect(result.toString()).toEqual('130'); + }); + }); }); \ No newline at end of file From ed7ad9cec73b28698dbc78d53cdfd75fb5c3395c Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Thu, 12 Sep 2024 12:55:07 -0400 Subject: [PATCH 26/41] fix: fully serialize V3 JSONs --- sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts | 4 ++-- sdks/uniswapx-sdk/src/order/V3DutchOrder.ts | 14 ++++++++++---- sdks/uniswapx-sdk/src/order/types.ts | 10 ++++++++-- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts b/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts index c8c12aec..0b14a02d 100644 --- a/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts +++ b/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts @@ -112,7 +112,7 @@ describe("V3DutchOrder", () => { startAmount: "1000000", curve: { relativeBlocks: [1,2,3,4], - relativeAmounts: [BigInt(1), BigInt(2), BigInt(3), BigInt(4)], + relativeAmounts: ["1", "2", "3", "4"], }, maxAmount: "1000001", }, @@ -122,7 +122,7 @@ describe("V3DutchOrder", () => { startAmount: "1000000", curve: { relativeBlocks: [1,2,3,4], - relativeAmounts: [BigInt(1), BigInt(2), BigInt(3), BigInt(4)], + relativeAmounts: ["1", "2", "3", "4"], }, recipient: ethers.constants.AddressZero, }, diff --git a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts index 3b4ff6f3..f1d87c51 100644 --- a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts +++ b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts @@ -122,7 +122,7 @@ export class UnsignedV3DutchOrder implements OffChainOrder { startAmount: BigNumber.from(json.input.startAmount), curve: { relativeBlocks: json.input.curve.relativeBlocks, - relativeAmounts: json.input.curve.relativeAmounts, + relativeAmounts: json.input.curve.relativeAmounts.map(amount => BigInt(amount)), }, maxAmount: BigNumber.from(json.input.maxAmount), }, @@ -131,7 +131,7 @@ export class UnsignedV3DutchOrder implements OffChainOrder { startAmount: BigNumber.from(output.startAmount), curve: { relativeBlocks: output.curve.relativeBlocks, - relativeAmounts: output.curve.relativeAmounts, + relativeAmounts: output.curve.relativeAmounts.map(amount => BigInt(amount)), }, })), }, @@ -197,13 +197,19 @@ export class UnsignedV3DutchOrder implements OffChainOrder { input: { token: this.info.input.token, startAmount: this.info.input.startAmount.toString(), - curve: this.info.input.curve, + curve: { + relativeBlocks: this.info.input.curve.relativeBlocks, + relativeAmounts: this.info.input.curve.relativeAmounts.map(amount => amount.toString()), + }, maxAmount: this.info.input.maxAmount.toString(), }, outputs: this.info.outputs.map(output => ({ token: output.token, startAmount: output.startAmount.toString(), - curve: output.curve, + curve: { + relativeBlocks: output.curve.relativeBlocks, + relativeAmounts: output.curve.relativeAmounts.map(amount => amount.toString()), + }, recipient: output.recipient, })), } diff --git a/sdks/uniswapx-sdk/src/order/types.ts b/sdks/uniswapx-sdk/src/order/types.ts index 24d6fa45..ff849f68 100644 --- a/sdks/uniswapx-sdk/src/order/types.ts +++ b/sdks/uniswapx-sdk/src/order/types.ts @@ -129,7 +129,7 @@ export type V3DutchInput = { export type V3DutchInputJSON = Omit & { startAmount: string; - curve: NonlinearDutchDecay; + curve: NonlinearDutchDecayJSON; maxAmount: string; }; @@ -138,6 +138,11 @@ export type NonlinearDutchDecay = { relativeAmounts: bigint[]; // Cannot be BigNumber because could be negative }; +export type NonlinearDutchDecayJSON = { + relativeBlocks: number[]; + relativeAmounts: string[]; +}; + export type V3DutchOutput = { readonly token: string; readonly startAmount: BigNumber; @@ -145,6 +150,7 @@ export type V3DutchOutput = { readonly recipient: string; }; -export type V3DutchOutputJSON = Omit & { +export type V3DutchOutputJSON = Omit & { startAmount: string; + curve: NonlinearDutchDecayJSON; }; \ No newline at end of file From e85054c8428cadfb00fd1916baafae2da34ed312 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Thu, 12 Sep 2024 13:23:00 -0400 Subject: [PATCH 27/41] feat: relax V3 upward decay restraint --- .../src/builder/V3DutchOrderBuilder.test.ts | 33 ++++++++++++++++++- .../src/builder/V3DutchOrderBuilder.ts | 8 ----- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.test.ts b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.test.ts index d7be04f9..e94cac61 100644 --- a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.test.ts +++ b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.test.ts @@ -347,7 +347,38 @@ describe("V3DutchOrderBuilder", () => { ).toThrow("Invariant failed: outputOverride smaller than original output"); }); - //TODO: double check that we don't enforce endamount < startamount + it("Do not enforce endAmount < startAmount for V3", () => { + const deadline = Math.floor(Date.now() / 1000) + 1000; + expect(() => + builder + .cosignature("0x") + .cosigner(constants.AddressZero) + .decayStartBlock(212121) + .input({ + token: INPUT_TOKEN, + startAmount: INPUT_START_AMOUNT, + curve: { + relativeBlocks: [1], + relativeAmounts: [BigInt(0)], + }, + maxAmount: INPUT_START_AMOUNT, + }) + .output({ + token: OUTPUT_TOKEN, + startAmount: OUTPUT_START_AMOUNT, + curve: { + relativeBlocks: [4], + relativeAmounts: [BigInt(-4)], + }, + recipient: constants.AddressZero, + }) + .deadline(deadline) + .outputOverrides([OUTPUT_START_AMOUNT]) + .swapper(constants.AddressZero) + .nonce(BigNumber.from(100)) + .build() + ).not.toThrow(); + }); it("Throw if deadline already passed", () => { const deadline = 2121; diff --git a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts index a9701278..4733d3ab 100644 --- a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts +++ b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts @@ -5,7 +5,6 @@ import { OrderType } from "../constants"; import { CosignedV3DutchOrder, CosignedV3DutchOrderInfo, UnsignedV3DutchOrder, V3CosignerData } from "../order/V3DutchOrder"; import { V3DutchInput, V3DutchOutput } from "../order/types"; import { getPermit2, getReactor, isCosignedV3 } from "../utils"; -import { getEndAmount } from "../utils/dutchBlockDecay"; import { OrderBuilder } from "./OrderBuilder"; @@ -153,13 +152,6 @@ export class V3DutchOrderBuilder extends OrderBuilder { } output(output: V3DutchOutput): this { - invariant( - output.startAmount.gte(getEndAmount({ - startAmount: output.startAmount, - relativeAmounts: output.curve.relativeAmounts, - relativeBlocks: output.curve.relativeBlocks, - })), "startAmount must be greater than the endAmount" - ); this.info.outputs?.push(output); return this; } From 3c8690ed0db55b124713340e84c5a36eab559008 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Thu, 12 Sep 2024 13:29:20 -0400 Subject: [PATCH 28/41] style: linting --- sdks/uniswapx-sdk/src/order/V3DutchOrder.ts | 1 - .../uniswapx-sdk/src/utils/dutchBlockDecay.ts | 22 +++++++++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts index f1d87c51..fd4c8b59 100644 --- a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts +++ b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts @@ -49,7 +49,6 @@ type V3WitnessInfo = { baseOutputs: V3DutchOutput[], }; - const COSIGNER_DATA_TUPLE_ABI = "tuple(uint256,address,uint256,uint256,uint256[])"; const V3_DUTCH_ORDER_TYPES = { diff --git a/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.ts b/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.ts index 603ad793..d5db3ba6 100644 --- a/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.ts +++ b/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.ts @@ -35,7 +35,7 @@ class NonLinearDutchDecayLib { ): BigNumber { // mismatch of relativeAmounts and relativeBlocks if (curve.relativeAmounts.length > 16) { - throw new Error('InvalidDecayCurve'); + throw new Error("InvalidDecayCurve"); } // handle current block before decay or no decay @@ -85,11 +85,11 @@ class NonLinearDutchDecayLib { const duration = BigNumber.from(endPoint - startPoint); if (endAmount.lt(startAmount)) { return startAmount.sub( - (startAmount.sub(endAmount)).mul(elapsed).div(duration) //muldivdown in contract + startAmount.sub(endAmount).mul(elapsed).div(duration) //muldivdown in contract ); } else { return startAmount.add( - (endAmount.sub(startAmount)).mul(elapsed).div(duration) //muldivup in contract + endAmount.sub(startAmount).mul(elapsed).div(duration) //muldivup in contract //TODO: How can we do muldivup in JS? ); } @@ -109,8 +109,14 @@ export function getBlockDecayedAmount( config: DutchBlockDecayConfig, atBlock: number ): BigNumber { - const { decayStartBlock, startAmount, relativeBlocks, relativeAmounts } = config; - return NonLinearDutchDecayLib.decay({ relativeAmounts, relativeBlocks }, startAmount, decayStartBlock, atBlock); + const { decayStartBlock, startAmount, relativeBlocks, relativeAmounts } = + config; + return NonLinearDutchDecayLib.decay( + { relativeAmounts, relativeBlocks }, + startAmount, + decayStartBlock, + atBlock + ); } export function getEndAmount( @@ -120,5 +126,7 @@ export function getEndAmount( if (!startAmount || !relativeAmounts) { throw new Error("Invalid config for getting V3 decay end amount"); //TODO: Should we throw? } - return startAmount.sub(relativeAmounts[relativeAmounts.length - 1].toString()); -} + return startAmount.sub( + relativeAmounts[relativeAmounts.length - 1].toString() + ); +} \ No newline at end of file From ad14a17fb0e8f93b29caf13041a90d2b32ee3e58 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Thu, 12 Sep 2024 13:56:40 -0400 Subject: [PATCH 29/41] refactor: cleaner type & reuse --- .../src/builder/V3DutchOrderBuilder.ts | 9 +------- sdks/uniswapx-sdk/src/order/V2DutchOrder.ts | 20 ++-------------- .../src/order/V3DutchOrder.test.ts | 10 +------- sdks/uniswapx-sdk/src/order/V3DutchOrder.ts | 23 ++++++------------- sdks/uniswapx-sdk/src/order/types.ts | 18 +++++++++++++++ .../uniswapx-sdk/src/utils/dutchBlockDecay.ts | 2 +- 6 files changed, 30 insertions(+), 52 deletions(-) diff --git a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts index 4733d3ab..b17ee36b 100644 --- a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts +++ b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts @@ -103,17 +103,10 @@ export class V3DutchOrderBuilder extends OrderBuilder { this.reactor(getReactor(chainId, OrderType.Dutch_V3, reactorAddress)); this.permit2Address = getPermit2(chainId, _permit2Address); - this.info = { outputs: [], - cosignerData: { - decayStartBlock: 0, - exclusiveFiller: ethers.constants.AddressZero, - exclusivityOverrideBps: BigNumber.from(0), - inputOverride: BigNumber.from(0), - outputOverrides: [], - }, }; + this.initializeCosignerData({}); } cosigner(cosigner: string): this { diff --git a/sdks/uniswapx-sdk/src/order/V2DutchOrder.ts b/sdks/uniswapx-sdk/src/order/V2DutchOrder.ts index 72522003..ac9be1b2 100644 --- a/sdks/uniswapx-sdk/src/order/V2DutchOrder.ts +++ b/sdks/uniswapx-sdk/src/order/V2DutchOrder.ts @@ -14,6 +14,8 @@ import { originalIfZero } from "../utils/order"; import { BlockOverrides, + CosignerData, + CosignerDataJSON, DutchInput, DutchInputJSON, DutchOutput, @@ -24,24 +26,6 @@ import { } from "./types"; import { CustomOrderValidation, parseValidation } from "./validation"; -export type CosignerData = { - decayStartTime: number; - decayEndTime: number; - exclusiveFiller: string; - exclusivityOverrideBps: BigNumber; - inputOverride: BigNumber; - outputOverrides: BigNumber[]; -}; - -export type CosignerDataJSON = { - decayStartTime: number; - decayEndTime: number; - exclusiveFiller: string; - exclusivityOverrideBps: number; - inputOverride: string; - outputOverrides: string[]; -}; - export type UnsignedV2DutchOrderInfo = OrderInfo & { cosigner: string; input: DutchInput; diff --git a/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts b/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts index 0b14a02d..003736db 100644 --- a/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts +++ b/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts @@ -29,10 +29,6 @@ const COSIGNER_DATA_WITHOUT_OVERRIDES = { }; describe("V3DutchOrder", () => { - it("should get block number", () => { - expect(BLOCK_NUMBER).to.be.greaterThan(0); - }); - const getFullOrderInfo = ( data: Partial): CosignedV3DutchOrderInfo => { return Object.assign( { @@ -183,11 +179,7 @@ describe("V3DutchOrder", () => { it("resolves with original value when overrides==0", () => { const order = new CosignedV3DutchOrder( getFullOrderInfo({ - cosignerData: { - ...COSIGNER_DATA_WITH_OVERRIDES, - inputOverride: BigNumber.from(0), - outputOverrides: [BigNumber.from(0)], - }, + cosignerData: COSIGNER_DATA_WITHOUT_OVERRIDES, }), CHAIN_ID ); diff --git a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts index fd4c8b59..13566401 100644 --- a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts +++ b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts @@ -6,15 +6,15 @@ import { getPermit2, ResolvedUniswapXOrder } from "../utils"; import { getBlockDecayedAmount } from "../utils/dutchBlockDecay"; import { originalIfZero } from "../utils/order"; -import { BlockOverrides, OffChainOrder, OrderInfo, V3DutchInput, V3DutchInputJSON, V3DutchOutput, V3DutchOutputJSON, V3OrderResolutionOptions } from "./types"; +import { BlockOverrides, CosignerData, CosignerDataJSON, OffChainOrder, OrderInfo, V3DutchInput, V3DutchInputJSON, V3DutchOutput, V3DutchOutputJSON, V3OrderResolutionOptions } from "./types"; -export type V3CosignerDataJSON = { +export type V3CosignerDataJSON = Omit & { decayStartBlock: number; - exclusiveFiller: string; - exclusivityOverrideBps: number; - inputOverride: string; - outputOverrides: string[]; -} +}; + +export type V3CosignerData = Omit & { + decayStartBlock: number; +}; export type UnsignedV3DutchOrderInfoJSON = Omit & { nonce: string; @@ -22,15 +22,6 @@ export type UnsignedV3DutchOrderInfoJSON = Omit & { endAmount: string; }; +export type CosignerData = { + decayStartTime: number; + decayEndTime: number; + exclusiveFiller: string; + exclusivityOverrideBps: BigNumber; + inputOverride: BigNumber; + outputOverrides: BigNumber[]; +}; + +export type CosignerDataJSON = { + decayStartTime: number; + decayEndTime: number; + exclusiveFiller: string; + exclusivityOverrideBps: number; + inputOverride: string; + outputOverrides: string[]; +}; + export type PriorityInput = { readonly token: string; readonly amount: BigNumber; diff --git a/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.ts b/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.ts index d5db3ba6..1430ca01 100644 --- a/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.ts +++ b/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.ts @@ -124,7 +124,7 @@ export function getEndAmount( ): BigNumber { const { startAmount, relativeAmounts } = config; if (!startAmount || !relativeAmounts) { - throw new Error("Invalid config for getting V3 decay end amount"); //TODO: Should we throw? + throw new Error("Invalid config for getting V3 decay end amount"); } return startAmount.sub( relativeAmounts[relativeAmounts.length - 1].toString() From 64f8d35fffcc4426faaed915ef0eaf5adadccd04 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Thu, 12 Sep 2024 16:25:39 -0400 Subject: [PATCH 30/41] feat: align dutchBlockDecay with updated contract --- .../src/utils/dutchBlockDecay.test.ts | 6 ++--- .../uniswapx-sdk/src/utils/dutchBlockDecay.ts | 24 +++++++------------ 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.test.ts b/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.test.ts index 154b02ae..3014ef20 100644 --- a/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.test.ts +++ b/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.test.ts @@ -19,10 +19,10 @@ describe("NonLinearDutchDecayLib", () => { expect(result.toString()).toEqual('110'); }); - it.skip("Test for mulDivUp for endAmount > startAmount", () => { + it("Test for mulDivDown for endAmount > startAmount", () => { const result = NonLinearDutchDecayLib.linearDecay(0, 10, 5, BigNumber.from(100), BigNumber.from(125)); - //if we successfully emulated mulDivUp then this is 113 - expect(result.toString()).toEqual('113'); + //if we successfully emulated mulDivDown then this is 112 + expect(result.toString()).toEqual('112'); }); }); diff --git a/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.ts b/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.ts index 1430ca01..7f74405a 100644 --- a/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.ts +++ b/sdks/uniswapx-sdk/src/utils/dutchBlockDecay.ts @@ -7,22 +7,17 @@ Essentially Solidity translated to TypeScript. */ function locateArrayPosition( curve: NonlinearDutchDecay, - targetValue: number + currentRelativeBlock: number ): [number, number] { const relativeBlocks = curve.relativeBlocks; - // eslint-disable-next-line prefer-const let prev = 0; - // eslint-disable-next-line prefer-const - let next = 0; - - while (next < curve.relativeAmounts.length) { - if (relativeBlocks[next] >= targetValue) { + let next = 0; + for (; next < relativeBlocks.length; next++) { + if(relativeBlocks[next] >= currentRelativeBlock) { return [prev, next]; } prev = next; - next++; } - return [next - 1, next - 1]; } @@ -83,16 +78,13 @@ class NonLinearDutchDecayLib { const elapsed = BigNumber.from(currentPoint - startPoint); const duration = BigNumber.from(endPoint - startPoint); + let delta; if (endAmount.lt(startAmount)) { - return startAmount.sub( - startAmount.sub(endAmount).mul(elapsed).div(duration) //muldivdown in contract - ); + delta = BigNumber.from(0).sub((startAmount.sub(endAmount)).mul(elapsed).div(duration)); // mulDivDown in contract } else { - return startAmount.add( - endAmount.sub(startAmount).mul(elapsed).div(duration) //muldivup in contract - //TODO: How can we do muldivup in JS? - ); + delta = (endAmount.sub(startAmount)).mul(elapsed).div(duration); // mulDivDown in contract } + return startAmount.add(delta); } } From 438218183e3a4f47c428c0752f6d4dbe19592d83 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Thu, 12 Sep 2024 18:47:26 -0400 Subject: [PATCH 31/41] fix, test: stricter calculation of minOut maxIn --- .../src/trade/V3DutchOrderTrade.test.ts | 146 ++++++++++++------ .../src/trade/V3DutchOrderTrade.ts | 65 ++------ 2 files changed, 119 insertions(+), 92 deletions(-) diff --git a/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.test.ts b/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.test.ts index cd02d448..fa61ce45 100644 --- a/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.test.ts +++ b/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.test.ts @@ -69,6 +69,9 @@ const USDC = new Token( tradeType: TradeType.EXACT_INPUT, }); + describe("Exact input", () => { + + it("returns the right input amount for an exact-in trade", () => { expect(trade.inputAmount.quotient.toString()).toEqual( orderInfo.input.startAmount.toString() @@ -86,56 +89,113 @@ const USDC = new Token( NON_FEE_MINIMUM_AMOUNT_OUT.toString() ); }); + }); - it("works for native output trades", () => { - const ethOutputOrderInfo = { - ...orderInfo, - outputs: [ - { - token: NativeAssets.ETH, + describe("Exact output", () => { + const outOrderInfo: UnsignedV3DutchOrderInfo = { + deadline: Math.floor(new Date().getTime() / 1000) + 1000, + reactor: "0x0000000000000000000000000000000000000000", + swapper: "0x0000000000000000000000000000000000000000", + nonce: BigNumber.from(10), + cosigner: "0x0000000000000000000000000000000000000000", + additionalValidationContract: ethers.constants.AddressZero, + additionalValidationData: "0x", + input: { + token: USDC.address, + startAmount: BigNumber.from(1000), + curve: { + relativeBlocks: [10], + relativeAmounts: [BigInt(-100)], + }, + maxAmount: BigNumber.from(1100), + }, + outputs: [ + { + token: DAI.address, startAmount: NON_FEE_OUTPUT_AMOUNT, curve: { - relativeBlocks: [21], - relativeAmounts: [BigInt("100000000000000000")], + relativeBlocks: [10], + relativeAmounts: [BigInt(0)], }, recipient: "0x0000000000000000000000000000000000000000", - }, - ], - }; - const ethOutputTrade = new V3DutchOrderTrade( + }, { - currencyIn: USDC, - currenciesOut: [Ether.onChain(1)], - orderInfo: ethOutputOrderInfo, - tradeType: TradeType.EXACT_INPUT, - } - ); - expect(ethOutputTrade.outputAmount.currency).toEqual(Ether.onChain(1)); - }); + token: DAI.address, + startAmount: BigNumber.from("1000"), + curve: { + relativeBlocks: [10], + relativeAmounts: [BigInt(0)], + }, + recipient: "0x0000000000000000000000000000000000000000", + }, + ], + }; + const trade = new V3DutchOrderTrade({ + currencyIn: USDC, + currenciesOut: [DAI], + orderInfo: outOrderInfo, + tradeType: TradeType.EXACT_OUTPUT, + }); - it("works for native output trades where order info has 0 address", () => { - const ethOutputOrderInfo = { - ...orderInfo, - outputs: [ + it("returns the correct maximum amount in", () => { + expect(trade.maximumAmountIn().quotient.toString()).toEqual( + outOrderInfo.input.maxAmount.toString() + ); + }); + }); + + describe("Qualitative tests", () => { + + it("works for native output trades", () => { + const ethOutputOrderInfo = { + ...orderInfo, + outputs: [ + { + token: NativeAssets.ETH, + startAmount: NON_FEE_OUTPUT_AMOUNT, + curve: { + relativeBlocks: [21], + relativeAmounts: [BigInt("100000000000000000")], + }, + recipient: "0x0000000000000000000000000000000000000000", + }, + ], + }; + const ethOutputTrade = new V3DutchOrderTrade( { - token: constants.AddressZero, - startAmount: NON_FEE_OUTPUT_AMOUNT, - curve: { - relativeBlocks: [21], - relativeAmounts: [BigInt("100000000000000000")], + currencyIn: USDC, + currenciesOut: [Ether.onChain(1)], + orderInfo: ethOutputOrderInfo, + tradeType: TradeType.EXACT_INPUT, + } + ); + expect(ethOutputTrade.outputAmount.currency).toEqual(Ether.onChain(1)); + }); + + it("works for native output trades where order info has 0 address", () => { + const ethOutputOrderInfo = { + ...orderInfo, + outputs: [ + { + token: constants.AddressZero, + startAmount: NON_FEE_OUTPUT_AMOUNT, + curve: { + relativeBlocks: [21], + relativeAmounts: [BigInt("100000000000000000")], + }, + recipient: "0x0000000000000000000000000000000000000000", }, - recipient: "0x0000000000000000000000000000000000000000", - }, - ], - }; - const ethOutputTrade = new V3DutchOrderTrade( - { - currencyIn: USDC, - currenciesOut: [Ether.onChain(1)], - orderInfo: ethOutputOrderInfo, - tradeType: TradeType.EXACT_INPUT, - } - ); - expect(ethOutputTrade.outputAmount.currency).toEqual(Ether.onChain(1)); + ], + }; + const ethOutputTrade = new V3DutchOrderTrade( + { + currencyIn: USDC, + currenciesOut: [Ether.onChain(1)], + orderInfo: ethOutputOrderInfo, + tradeType: TradeType.EXACT_INPUT, + } + ); + expect(ethOutputTrade.outputAmount.currency).toEqual(Ether.onChain(1)); + }); }); -}); \ 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 005e0e52..f5ae7294 100644 --- a/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.ts +++ b/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.ts @@ -1,9 +1,10 @@ import { Currency, CurrencyAmount, Price, TradeType } from "@uniswap/sdk-core"; import { UnsignedV3DutchOrder, UnsignedV3DutchOrderInfo } from "../order/V3DutchOrder"; -import { getEndAmount } from "../utils/dutchBlockDecay"; import { areCurrenciesEqual } from "./utils"; +import { V3DutchOutput } from "../order"; +import { BigNumber } from "ethers"; export class V3DutchOrderTrade< TInput extends Currency, @@ -34,7 +35,7 @@ export class V3DutchOrderTrade< this._currenciesOut = currenciesOut this.tradeType = tradeType - // assume single-chain for now + // Assuming not cross-chain this.order = new UnsignedV3DutchOrder(orderInfo, currencyIn.chainId) } @@ -53,7 +54,7 @@ export class V3DutchOrderTrade< if (this._outputAmounts) return this._outputAmounts const amounts = this.order.info.outputs.map((output) => { - // assume single chain ids across all outputs for now + // Assuming all outputs on the same chain const currencyOut = this._currenciesOut.find((currency) => areCurrenciesEqual(currency, output.token, currency.chainId) ) @@ -68,62 +69,28 @@ export class V3DutchOrderTrade< this._outputAmounts = amounts return amounts } - - private _firstNonFeeOutputStartEndAmounts: - | { startAmount: CurrencyAmount; endAmount: CurrencyAmount } - | undefined; - - private getFirstNonFeeOutputStartEndAmounts(): { - startAmount: CurrencyAmount - endAmount: CurrencyAmount - } { - if (this._firstNonFeeOutputStartEndAmounts) - return this._firstNonFeeOutputStartEndAmounts; - - if (this.order.info.outputs.length === 0) { - throw new Error("there must be at least one output token"); - } - const output = this.order.info.outputs[0]; - - // assume single chain ids across all outputs for now - const currencyOut = this._currenciesOut.find((currency) => - areCurrenciesEqual(currency, output.token, currency.chainId) - ); - - if (!currencyOut) { - throw new Error( - "currency output from order must exist in currenciesOut list" - ); - } - - const startAmount = CurrencyAmount.fromRawAmount(currencyOut, output.startAmount.toString()) - const nonFeeOutputEndAmount = getEndAmount({ - startAmount: output.startAmount, - relativeAmounts: output.curve.relativeAmounts, - }); - const endAmount = CurrencyAmount.fromRawAmount(currencyOut, nonFeeOutputEndAmount.toString()) - - this._firstNonFeeOutputStartEndAmounts = { startAmount, endAmount } - return { startAmount, endAmount } - } - // TODO: revise when there are actually multiple output amounts. for now, assume only one non-fee output at a time + // 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.getFirstNonFeeOutputStartEndAmounts().startAmount; + return this.outputAmounts[0]; } public minimumAmountOut(): CurrencyAmount { - return this.getFirstNonFeeOutputStartEndAmounts().endAmount; + const nonFeeOutput: V3DutchOutput = this.order.info.outputs[0]; + const relativeAmounts: bigint[] = nonFeeOutput.curve.relativeAmounts; + const startAmount: BigNumber = nonFeeOutput.startAmount; + // Get the maximum of the relative amounts + const maxRelativeAmount = relativeAmounts.reduce((max, amount) => amount > max ? amount : max, BigInt(0)); + // minimum is the start - the max of the relative amounts + const minOut = startAmount.sub(maxRelativeAmount.toString()); + return CurrencyAmount.fromRawAmount(this.outputAmount.currency, minOut.toString()); } public maximumAmountIn(): CurrencyAmount { - const endAmount = getEndAmount({ - startAmount: this.order.info.input.startAmount, - relativeAmounts: this.order.info.input.curve.relativeAmounts, - }); + const maxAmountIn = this.order.info.input.maxAmount; return CurrencyAmount.fromRawAmount( this._currencyIn, - endAmount.toString() + maxAmountIn.toString() ); } From 10516d7c7ab64455eef50714538a2a964b6e6c69 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Thu, 12 Sep 2024 18:50:59 -0400 Subject: [PATCH 32/41] style: linting --- .../src/trade/V3DutchOrderTrade.test.ts | 361 +++++++++--------- .../src/trade/V3DutchOrderTrade.ts | 4 +- 2 files changed, 181 insertions(+), 184 deletions(-) diff --git a/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.test.ts b/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.test.ts index fa61ce45..f8ae8d99 100644 --- a/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.test.ts +++ b/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.test.ts @@ -7,195 +7,192 @@ import { V3DutchOrderTrade } from "./V3DutchOrderTrade"; import { NativeAssets } from "./utils"; const USDC = new Token( - 1, - "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", - 6, - "USDC" - ); - const DAI = new Token( - 1, - "0x6B175474E89094C44Da98b954EedeAC495271d0F", - 18, - "DAI" - ); + 1, + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + 6, + "USDC" +); +const DAI = new Token( + 1, + "0x6B175474E89094C44Da98b954EedeAC495271d0F", + 18, + "DAI" +); - describe("V3DutchOrderTrade", () => { - const NON_FEE_OUTPUT_AMOUNT = BigNumber.from("1000000000000000000"); - const NON_FEE_MINIMUM_AMOUNT_OUT = BigNumber.from("900000000000000000"); - - const orderInfo: UnsignedV3DutchOrderInfo = { - deadline: Math.floor(new Date().getTime() / 1000) + 1000, - reactor: "0x0000000000000000000000000000000000000000", - swapper: "0x0000000000000000000000000000000000000000", - nonce: BigNumber.from(10), - cosigner: "0x0000000000000000000000000000000000000000", - additionalValidationContract: ethers.constants.AddressZero, - additionalValidationData: "0x", - input: { - token: USDC.address, - startAmount: BigNumber.from(1000), - curve: { - relativeBlocks: [1], - relativeAmounts: [BigInt(0)], - }, - maxAmount: BigNumber.from(1000), - }, - outputs: [ - { - token: DAI.address, - startAmount: NON_FEE_OUTPUT_AMOUNT, - curve: { - relativeBlocks: [21], - relativeAmounts: [BigInt("100000000000000000")], - }, - recipient: "0x0000000000000000000000000000000000000000", - }, - { - token: DAI.address, - startAmount: BigNumber.from("1000"), - curve: { - relativeBlocks: [21], - relativeAmounts: [BigInt("100")], - }, - recipient: "0x0000000000000000000000000000000000000000", - }, - ], - }; +describe("V3DutchOrderTrade", () => { + const NON_FEE_OUTPUT_AMOUNT = BigNumber.from("1000000000000000000"); + const NON_FEE_MINIMUM_AMOUNT_OUT = BigNumber.from("900000000000000000"); - const trade = new V3DutchOrderTrade({ - currencyIn: USDC, - currenciesOut: [DAI], - orderInfo, - tradeType: TradeType.EXACT_INPUT, - }); + const orderInfo: UnsignedV3DutchOrderInfo = { + deadline: Math.floor(new Date().getTime() / 1000) + 1000, + reactor: "0x0000000000000000000000000000000000000000", + swapper: "0x0000000000000000000000000000000000000000", + nonce: BigNumber.from(10), + cosigner: "0x0000000000000000000000000000000000000000", + additionalValidationContract: ethers.constants.AddressZero, + additionalValidationData: "0x", + input: { + token: USDC.address, + startAmount: BigNumber.from(1000), + curve: { + relativeBlocks: [1], + relativeAmounts: [BigInt(0)], + }, + maxAmount: BigNumber.from(1000), + }, + outputs: [ + { + token: DAI.address, + startAmount: NON_FEE_OUTPUT_AMOUNT, + curve: { + relativeBlocks: [21], + relativeAmounts: [BigInt("100000000000000000")], + }, + recipient: "0x0000000000000000000000000000000000000000", + }, + { + token: DAI.address, + startAmount: BigNumber.from("1000"), + curve: { + relativeBlocks: [21], + relativeAmounts: [BigInt("100")], + }, + recipient: "0x0000000000000000000000000000000000000000", + }, + ], + }; - describe("Exact input", () => { - + const trade = new V3DutchOrderTrade({ + currencyIn: USDC, + currenciesOut: [DAI], + orderInfo, + tradeType: TradeType.EXACT_INPUT, + }); - it("returns the right input amount for an exact-in trade", () => { - expect(trade.inputAmount.quotient.toString()).toEqual( - orderInfo.input.startAmount.toString() - ); - }); + describe("Exact input", () => { + it("returns the right input amount for an exact-in trade", () => { + expect(trade.inputAmount.quotient.toString()).toEqual( + orderInfo.input.startAmount.toString() + ); + }); - it("returns the correct non-fee output amount", () => { - expect(trade.outputAmount.quotient.toString()).toEqual( - NON_FEE_OUTPUT_AMOUNT.toString() - ); - }); - - it("returns the correct minimum amount out", () => { - expect(trade.minimumAmountOut().quotient.toString()).toEqual( - NON_FEE_MINIMUM_AMOUNT_OUT.toString() - ); - }); - }); + it("returns the correct non-fee output amount", () => { + expect(trade.outputAmount.quotient.toString()).toEqual( + NON_FEE_OUTPUT_AMOUNT.toString() + ); + }); - describe("Exact output", () => { - const outOrderInfo: UnsignedV3DutchOrderInfo = { - deadline: Math.floor(new Date().getTime() / 1000) + 1000, - reactor: "0x0000000000000000000000000000000000000000", - swapper: "0x0000000000000000000000000000000000000000", - nonce: BigNumber.from(10), - cosigner: "0x0000000000000000000000000000000000000000", - additionalValidationContract: ethers.constants.AddressZero, - additionalValidationData: "0x", - input: { - token: USDC.address, - startAmount: BigNumber.from(1000), - curve: { - relativeBlocks: [10], - relativeAmounts: [BigInt(-100)], - }, - maxAmount: BigNumber.from(1100), - }, - outputs: [ - { - token: DAI.address, - startAmount: NON_FEE_OUTPUT_AMOUNT, - curve: { - relativeBlocks: [10], - relativeAmounts: [BigInt(0)], - }, - recipient: "0x0000000000000000000000000000000000000000", - }, - { - token: DAI.address, - startAmount: BigNumber.from("1000"), - curve: { - relativeBlocks: [10], - relativeAmounts: [BigInt(0)], - }, - recipient: "0x0000000000000000000000000000000000000000", - }, - ], - }; - const trade = new V3DutchOrderTrade({ - currencyIn: USDC, - currenciesOut: [DAI], - orderInfo: outOrderInfo, - tradeType: TradeType.EXACT_OUTPUT, - }); + it("returns the correct minimum amount out", () => { + expect(trade.minimumAmountOut().quotient.toString()).toEqual( + NON_FEE_MINIMUM_AMOUNT_OUT.toString() + ); + }); + }); - it("returns the correct maximum amount in", () => { - expect(trade.maximumAmountIn().quotient.toString()).toEqual( - outOrderInfo.input.maxAmount.toString() - ); - }); - }); + describe("Exact output", () => { + const outOrderInfo: UnsignedV3DutchOrderInfo = { + deadline: Math.floor(new Date().getTime() / 1000) + 1000, + reactor: "0x0000000000000000000000000000000000000000", + swapper: "0x0000000000000000000000000000000000000000", + nonce: BigNumber.from(10), + cosigner: "0x0000000000000000000000000000000000000000", + additionalValidationContract: ethers.constants.AddressZero, + additionalValidationData: "0x", + input: { + token: USDC.address, + startAmount: BigNumber.from(1000), + curve: { + relativeBlocks: [10], + relativeAmounts: [BigInt(-100)], + }, + maxAmount: BigNumber.from(1100), + }, + outputs: [ + { + token: DAI.address, + startAmount: NON_FEE_OUTPUT_AMOUNT, + curve: { + relativeBlocks: [10], + relativeAmounts: [BigInt(0)], + }, + recipient: "0x0000000000000000000000000000000000000000", + }, + { + token: DAI.address, + startAmount: BigNumber.from("1000"), + curve: { + relativeBlocks: [10], + relativeAmounts: [BigInt(0)], + }, + recipient: "0x0000000000000000000000000000000000000000", + }, + ], + }; + const trade = new V3DutchOrderTrade({ + currencyIn: USDC, + currenciesOut: [DAI], + orderInfo: outOrderInfo, + tradeType: TradeType.EXACT_OUTPUT, + }); - describe("Qualitative tests", () => { + it("returns the correct maximum amount in", () => { + expect(trade.maximumAmountIn().quotient.toString()).toEqual( + outOrderInfo.input.maxAmount.toString() + ); + }); + }); - it("works for native output trades", () => { - const ethOutputOrderInfo = { - ...orderInfo, - outputs: [ - { - token: NativeAssets.ETH, - startAmount: NON_FEE_OUTPUT_AMOUNT, - curve: { - relativeBlocks: [21], - relativeAmounts: [BigInt("100000000000000000")], - }, - recipient: "0x0000000000000000000000000000000000000000", - }, - ], - }; - const ethOutputTrade = new V3DutchOrderTrade( - { - currencyIn: USDC, - currenciesOut: [Ether.onChain(1)], - orderInfo: ethOutputOrderInfo, - tradeType: TradeType.EXACT_INPUT, - } - ); - expect(ethOutputTrade.outputAmount.currency).toEqual(Ether.onChain(1)); - }); + describe("Qualitative tests", () => { + it("works for native output trades", () => { + const ethOutputOrderInfo = { + ...orderInfo, + outputs: [ + { + token: NativeAssets.ETH, + startAmount: NON_FEE_OUTPUT_AMOUNT, + curve: { + relativeBlocks: [21], + relativeAmounts: [BigInt("100000000000000000")], + }, + recipient: "0x0000000000000000000000000000000000000000", + }, + ], + }; + const ethOutputTrade = new V3DutchOrderTrade( + { + currencyIn: USDC, + currenciesOut: [Ether.onChain(1)], + orderInfo: ethOutputOrderInfo, + tradeType: TradeType.EXACT_INPUT, + } + ); + expect(ethOutputTrade.outputAmount.currency).toEqual(Ether.onChain(1)); + }); - it("works for native output trades where order info has 0 address", () => { - const ethOutputOrderInfo = { - ...orderInfo, - outputs: [ - { - token: constants.AddressZero, - startAmount: NON_FEE_OUTPUT_AMOUNT, - curve: { - relativeBlocks: [21], - relativeAmounts: [BigInt("100000000000000000")], - }, - recipient: "0x0000000000000000000000000000000000000000", - }, - ], - }; - const ethOutputTrade = new V3DutchOrderTrade( - { - currencyIn: USDC, - currenciesOut: [Ether.onChain(1)], - orderInfo: ethOutputOrderInfo, - tradeType: TradeType.EXACT_INPUT, - } - ); - expect(ethOutputTrade.outputAmount.currency).toEqual(Ether.onChain(1)); - }); - }); -}); + it("works for native output trades where order info has 0 address", () => { + const ethOutputOrderInfo = { + ...orderInfo, + outputs: [ + { + token: constants.AddressZero, + startAmount: NON_FEE_OUTPUT_AMOUNT, + curve: { + relativeBlocks: [21], + relativeAmounts: [BigInt("100000000000000000")], + }, + recipient: "0x0000000000000000000000000000000000000000", + }, + ], + }; + const ethOutputTrade = new V3DutchOrderTrade( + { + currencyIn: USDC, + currenciesOut: [Ether.onChain(1)], + orderInfo: ethOutputOrderInfo, + tradeType: TradeType.EXACT_INPUT, + } + ); + expect(ethOutputTrade.outputAmount.currency).toEqual(Ether.onChain(1)); + }); + }); +}); \ 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 f5ae7294..0da74624 100644 --- a/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.ts +++ b/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.ts @@ -1,10 +1,10 @@ import { Currency, CurrencyAmount, Price, TradeType } from "@uniswap/sdk-core"; +import { BigNumber } from "ethers"; +import { V3DutchOutput } from "../order"; import { UnsignedV3DutchOrder, UnsignedV3DutchOrderInfo } from "../order/V3DutchOrder"; import { areCurrenciesEqual } from "./utils"; -import { V3DutchOutput } from "../order"; -import { BigNumber } from "ethers"; export class V3DutchOrderTrade< TInput extends Currency, From fcf99b4c9611cdbcfd3081e19046279af61d3083 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Fri, 13 Sep 2024 14:33:44 -0400 Subject: [PATCH 33/41] feat: generic for checking cosigned status --- .../src/builder/V2DutchOrderBuilder.ts | 4 ++-- .../src/builder/V3DutchOrderBuilder.ts | 4 ++-- sdks/uniswapx-sdk/src/utils/order.ts | 16 ++++++---------- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/sdks/uniswapx-sdk/src/builder/V2DutchOrderBuilder.ts b/sdks/uniswapx-sdk/src/builder/V2DutchOrderBuilder.ts index e5ad715d..509997ec 100644 --- a/sdks/uniswapx-sdk/src/builder/V2DutchOrderBuilder.ts +++ b/sdks/uniswapx-sdk/src/builder/V2DutchOrderBuilder.ts @@ -11,7 +11,7 @@ import { UnsignedV2DutchOrder, } from "../order"; import { ValidationInfo } from "../order/validation"; -import { getPermit2, getReactor, isCosignedV2 } from "../utils"; +import { getPermit2, getReactor, isCosigned } from "../utils"; import { OrderBuilder } from "./OrderBuilder"; @@ -40,7 +40,7 @@ export class V2DutchOrderBuilder extends OrderBuilder { builder.output(output); } - if (isCosignedV2(order)) { + if (isCosigned(order)) { builder.cosignature(order.info.cosignature); builder.decayEndTime(order.info.cosignerData.decayEndTime); builder.decayStartTime(order.info.cosignerData.decayStartTime); diff --git a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts index b17ee36b..c392bc4b 100644 --- a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts +++ b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts @@ -4,7 +4,7 @@ import invariant from "tiny-invariant"; import { OrderType } from "../constants"; import { CosignedV3DutchOrder, CosignedV3DutchOrderInfo, UnsignedV3DutchOrder, V3CosignerData } from "../order/V3DutchOrder"; import { V3DutchInput, V3DutchOutput } from "../order/types"; -import { getPermit2, getReactor, isCosignedV3 } from "../utils"; +import { getPermit2, getReactor, isCosigned } from "../utils"; import { OrderBuilder } from "./OrderBuilder"; @@ -28,7 +28,7 @@ export class V3DutchOrderBuilder extends OrderBuilder { builder.output(output); }); - if (isCosignedV3(order)) { + if (isCosigned(order)) { builder.cosignature(order.info.cosignature); builder.decayStartBlock(order.info.cosignerData.decayStartBlock); builder.exclusiveFiller(order.info.cosignerData.exclusiveFiller); diff --git a/sdks/uniswapx-sdk/src/utils/order.ts b/sdks/uniswapx-sdk/src/utils/order.ts index 5943d019..13a9d78a 100644 --- a/sdks/uniswapx-sdk/src/utils/order.ts +++ b/sdks/uniswapx-sdk/src/utils/order.ts @@ -159,16 +159,12 @@ export class RelayOrderParser extends OrderParser { } } -export function isCosignedV2( - order: UnsignedV2DutchOrder | CosignedV2DutchOrder -): order is CosignedV2DutchOrder { - return (order as CosignedV2DutchOrder).info.cosignature !== undefined; -} - -export function isCosignedV3( - order: UnsignedV3DutchOrder | CosignedV3DutchOrder -): order is CosignedV3DutchOrder { - return (order as CosignedV3DutchOrder).info.cosignature !== undefined; +type UnsignedOrder = UnsignedV2DutchOrder | UnsignedV3DutchOrder; +type CosignedOrder = CosignedV2DutchOrder | CosignedV3DutchOrder; +export function isCosigned( + order: T | U +): order is U { + return 'cosignature' in (order as U).info; } export function originalIfZero(value: BigNumber, original: BigNumber): BigNumber { From efecbc0789eb18e3046bca5a1799c662c02034e9 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Fri, 13 Sep 2024 15:16:47 -0400 Subject: [PATCH 34/41] feat, test: V3 decay curve validity invariants --- .../src/builder/V3DutchOrderBuilder.test.ts | 136 ++++++++++++++++++ .../src/builder/V3DutchOrderBuilder.ts | 23 +++ 2 files changed, 159 insertions(+) diff --git a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.test.ts b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.test.ts index e94cac61..58001751 100644 --- a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.test.ts +++ b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.test.ts @@ -129,6 +129,142 @@ describe("V3DutchOrderBuilder", () => { ).toThrow("Invariant failed: cosigner not set"); }); + it("Throw if relativeBlocks and relativeAmounts length mismatch in output", () => { + const deadline = Math.floor(Date.now() / 1000) + 1000; + expect(() => + builder + .cosignature("0x") + .cosigner(constants.AddressZero) + .decayStartBlock(212121) + .input({ + token: INPUT_TOKEN, + startAmount: INPUT_START_AMOUNT, + curve: { + relativeBlocks: [1], + relativeAmounts: [BigInt(1)], + }, + maxAmount: INPUT_START_AMOUNT.add(1), + }) + .output({ + token: OUTPUT_TOKEN, + startAmount: OUTPUT_START_AMOUNT, + curve: { + relativeBlocks: [4,5], + relativeAmounts: [BigInt(4)], + }, + recipient: constants.AddressZero, + }) + .inputOverride(INPUT_START_AMOUNT.mul(99).div(100)) + .outputOverrides([OUTPUT_START_AMOUNT]) + .deadline(deadline) + .swapper(constants.AddressZero) + .nonce(BigNumber.from(100)) + .build() + ).toThrow("Invariant failed: relativeBlocks and relativeAmounts length mismatch"); + }); + + it("Throw if relativeBlocks and relativeAmounts length mismatch in input", () => { + const deadline = Math.floor(Date.now() / 1000) + 1000; + expect(() => + builder + .cosignature("0x") + .cosigner(constants.AddressZero) + .decayStartBlock(212121) + .input({ + token: INPUT_TOKEN, + startAmount: INPUT_START_AMOUNT, + curve: { + relativeBlocks: [1], + relativeAmounts: [BigInt(0), BigInt(1)], + }, + maxAmount: INPUT_START_AMOUNT.add(1), + }) + .output({ + token: OUTPUT_TOKEN, + startAmount: OUTPUT_START_AMOUNT, + curve: { + relativeBlocks: [4], + relativeAmounts: [BigInt(4)], + }, + recipient: constants.AddressZero, + }) + .inputOverride(BigNumber.from(0)) + .outputOverrides([OUTPUT_START_AMOUNT]) + .deadline(deadline) + .swapper(constants.AddressZero) + .nonce(BigNumber.from(100)) + .build() + ).toThrow("Invariant failed: relativeBlocks and relativeAmounts length mismatch"); + }); + + it("Throw if relativeBlocks is not strictly increasing in input", () => { + const deadline = Math.floor(Date.now() / 1000) + 1000; + expect(() => + builder + .cosignature("0x") + .cosigner(constants.AddressZero) + .decayStartBlock(212121) + .input({ + token: INPUT_TOKEN, + startAmount: INPUT_START_AMOUNT, + curve: { + relativeBlocks: [1, 2, 1], + relativeAmounts: [BigInt(1), BigInt(2), BigInt(3)], + }, + maxAmount: INPUT_START_AMOUNT.add(1), + }) + .output({ + token: OUTPUT_TOKEN, + startAmount: OUTPUT_START_AMOUNT, + curve: { + relativeBlocks: [], + relativeAmounts: [], + }, + recipient: constants.AddressZero, + }) + .inputOverride(BigNumber.from(0)) + .outputOverrides([OUTPUT_START_AMOUNT]) + .deadline(deadline) + .nonce(BigNumber.from(100)) + .swapper(constants.AddressZero) + .build() + ).toThrow("Invariant failed: relativeBlocks not strictly increasing"); + }); + + it("Throw if relativeBlocks is not strictly increasing in output", () => { + const deadline = Math.floor(Date.now() / 1000) + 1000; + expect(() => + builder + .cosignature("0x") + .cosigner(constants.AddressZero) + .decayStartBlock(212121) + .input({ + token: INPUT_TOKEN, + startAmount: INPUT_START_AMOUNT, + curve: { + relativeBlocks: [1], + relativeAmounts: [BigInt(0)], + }, + maxAmount: INPUT_START_AMOUNT.add(1), + }) + .output({ + token: OUTPUT_TOKEN, + startAmount: OUTPUT_START_AMOUNT, + curve: { + relativeBlocks: [5, 5], + relativeAmounts: [BigInt(4), BigInt(22)], + }, + recipient: constants.AddressZero, + }) + .inputOverride(INPUT_START_AMOUNT.mul(99).div(100)) + .outputOverrides([OUTPUT_START_AMOUNT]) + .deadline(deadline) + .nonce(BigNumber.from(100)) + .swapper(constants.AddressZero) + .build() + ).toThrow("Invariant failed: relativeBlocks not strictly increasing"); + }); + it("Throw if swapper is not set", () => { const deadline = Math.floor(Date.now() / 1000) + 1000; expect(() => diff --git a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts index c392bc4b..c3ec04e7 100644 --- a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts +++ b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts @@ -47,6 +47,18 @@ export class V3DutchOrderBuilder extends OrderBuilder { this.info.outputs && this.info.outputs.length > 0, "outputs not set" ); + // Check if input curve is valid + invariant(this.info.input.curve.relativeAmounts.length === this.info.input.curve.relativeBlocks.length, "relativeBlocks and relativeAmounts length mismatch"); + invariant(this.isRelativeBlocksIncreasing(this.info.input.curve.relativeBlocks), "relativeBlocks not strictly increasing"); + // For each output's curve, we need to make sure relativeBlocks is strictly increasing + this.info.outputs.forEach((output) => { + invariant( + output.curve.relativeBlocks.length === output.curve.relativeAmounts.length, + "relativeBlocks and relativeAmounts length mismatch" + ); + // For each output's curve, we need to make sure relativeBlocks is strictly increasing + invariant(this.isRelativeBlocksIncreasing(output.curve.relativeBlocks), "relativeBlocks not strictly increasing"); + }); // In V3, we are not enforcing that the startAmount is greater than the endAmount invariant(this.info.cosignerData !== undefined, "cosignerData not set"); invariant(this.info.cosignerData.decayStartBlock !== undefined, "decayStartBlock not set"); @@ -139,6 +151,17 @@ export class V3DutchOrderBuilder extends OrderBuilder { }; } + private isRelativeBlocksIncreasing(relativeBlocks: number[]): boolean { + let prevBlock = 0; + for (const block of relativeBlocks) { + if (block <= prevBlock) { + return false; + } + prevBlock = block; + } + return true; + } + input(input: V3DutchInput): this { this.info.input = input; return this; From 0cec87db2000b2b418947c568205de2630459156 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Fri, 13 Sep 2024 15:19:40 -0400 Subject: [PATCH 35/41] style: linting --- .../src/trade/V3DutchOrderTrade.ts | 230 +++++++++--------- 1 file changed, 115 insertions(+), 115 deletions(-) diff --git a/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.ts b/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.ts index 0da74624..74789f18 100644 --- a/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.ts +++ b/sdks/uniswapx-sdk/src/trade/V3DutchOrderTrade.ts @@ -7,120 +7,120 @@ import { UnsignedV3DutchOrder, UnsignedV3DutchOrderInfo } from "../order/V3Dutch import { areCurrenciesEqual } from "./utils"; export class V3DutchOrderTrade< - TInput extends Currency, - TOutput extends Currency, - TTradeType extends TradeType + TInput extends Currency, + TOutput extends Currency, + TTradeType extends TradeType > { - public readonly tradeType: TTradeType - public readonly order: UnsignedV3DutchOrder - - private _inputAmount: CurrencyAmount | undefined - private _outputAmounts: CurrencyAmount[] | undefined - - private _currencyIn: TInput - private _currenciesOut: TOutput[] - - public constructor({ - currencyIn, - currenciesOut, - orderInfo, - tradeType, - }: { - currencyIn: TInput - currenciesOut: TOutput[] - orderInfo: UnsignedV3DutchOrderInfo - tradeType: TTradeType - }) { - this._currencyIn = currencyIn - this._currenciesOut = currenciesOut - this.tradeType = tradeType - - // Assuming not cross-chain - this.order = new UnsignedV3DutchOrder(orderInfo, currencyIn.chainId) - } - - public get inputAmount(): CurrencyAmount { - if (this._inputAmount) return this._inputAmount - - const amount = CurrencyAmount.fromRawAmount( - this._currencyIn, - this.order.info.input.startAmount.toString() - ) - this._inputAmount = amount - return amount - } - - public get outputAmounts(): CurrencyAmount[] { - if (this._outputAmounts) return this._outputAmounts - - const amounts = this.order.info.outputs.map((output) => { - // Assuming all outputs on the same chain - const currencyOut = this._currenciesOut.find((currency) => - areCurrenciesEqual(currency, output.token, currency.chainId) - ) - - if (!currencyOut) { - throw new Error("Currency out not found") - } - - return CurrencyAmount.fromRawAmount(currencyOut, output.startAmount.toString()) - }) - - this._outputAmounts = amounts - return amounts - } - - // 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]; - } - - public minimumAmountOut(): CurrencyAmount { - const nonFeeOutput: V3DutchOutput = this.order.info.outputs[0]; - const relativeAmounts: bigint[] = nonFeeOutput.curve.relativeAmounts; - const startAmount: BigNumber = nonFeeOutput.startAmount; - // Get the maximum of the relative amounts - const maxRelativeAmount = relativeAmounts.reduce((max, amount) => amount > max ? amount : max, BigInt(0)); - // minimum is the start - the max of the relative amounts - const minOut = startAmount.sub(maxRelativeAmount.toString()); - return CurrencyAmount.fromRawAmount(this.outputAmount.currency, minOut.toString()); - } - - public maximumAmountIn(): CurrencyAmount { - const maxAmountIn = this.order.info.input.maxAmount; - return CurrencyAmount.fromRawAmount( - this._currencyIn, - maxAmountIn.toString() - ); - } - - private _executionPrice: Price | undefined; - - /** - * The price expressed in terms of output amount/input amount. - */ - public get executionPrice(): Price { - return ( - this._executionPrice ?? - (this._executionPrice = new Price( - this.inputAmount.currency, - this.outputAmount.currency, - this.inputAmount.quotient, - this.outputAmount.quotient - )) - ); - } - - /** - * Return the execution price after accounting for slippage tolerance - * @returns The execution price - */ - public worstExecutionPrice(): Price { - return new Price( - this.inputAmount.currency, - this.outputAmount.currency, - this.maximumAmountIn().quotient, - this.minimumAmountOut().quotient - ); - } + public readonly tradeType: TTradeType + public readonly order: UnsignedV3DutchOrder + + private _inputAmount: CurrencyAmount | undefined + private _outputAmounts: CurrencyAmount[] | undefined + + private _currencyIn: TInput + private _currenciesOut: TOutput[] + + public constructor({ + currencyIn, + currenciesOut, + orderInfo, + tradeType, + }: { + currencyIn: TInput + currenciesOut: TOutput[] + orderInfo: UnsignedV3DutchOrderInfo + tradeType: TTradeType + }) { + this._currencyIn = currencyIn + this._currenciesOut = currenciesOut + this.tradeType = tradeType + + // Assuming not cross-chain + this.order = new UnsignedV3DutchOrder(orderInfo, currencyIn.chainId) + } + + public get inputAmount(): CurrencyAmount { + if (this._inputAmount) return this._inputAmount + + const amount = CurrencyAmount.fromRawAmount( + this._currencyIn, + this.order.info.input.startAmount.toString() + ) + this._inputAmount = amount + return amount + } + + public get outputAmounts(): CurrencyAmount[] { + if (this._outputAmounts) return this._outputAmounts + + const amounts = this.order.info.outputs.map((output) => { + // Assuming all outputs on the same chain + const currencyOut = this._currenciesOut.find((currency) => + areCurrenciesEqual(currency, output.token, currency.chainId) + ) + + if (!currencyOut) { + throw new Error("Currency out not found") + } + + return CurrencyAmount.fromRawAmount(currencyOut, output.startAmount.toString()) + }) + + this._outputAmounts = amounts + return amounts + } + + // 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]; + } + + public minimumAmountOut(): CurrencyAmount { + const nonFeeOutput: V3DutchOutput = this.order.info.outputs[0]; + const relativeAmounts: bigint[] = nonFeeOutput.curve.relativeAmounts; + const startAmount: BigNumber = nonFeeOutput.startAmount; + // Get the maximum of the relative amounts + const maxRelativeAmount = relativeAmounts.reduce((max, amount) => amount > max ? amount : max, BigInt(0)); + // minimum is the start - the max of the relative amounts + const minOut = startAmount.sub(maxRelativeAmount.toString()); + return CurrencyAmount.fromRawAmount(this.outputAmount.currency, minOut.toString()); + } + + public maximumAmountIn(): CurrencyAmount { + const maxAmountIn = this.order.info.input.maxAmount; + return CurrencyAmount.fromRawAmount( + this._currencyIn, + maxAmountIn.toString() + ); + } + + private _executionPrice: Price | undefined; + + /** + * The price expressed in terms of output amount/input amount. + */ + public get executionPrice(): Price { + return ( + this._executionPrice ?? + (this._executionPrice = new Price( + this.inputAmount.currency, + this.outputAmount.currency, + this.inputAmount.quotient, + this.outputAmount.quotient + )) + ); + } + + /** + * Return the execution price after accounting for slippage tolerance + * @returns The execution price + */ + public worstExecutionPrice(): Price { + return new Price( + this.inputAmount.currency, + this.outputAmount.currency, + this.maximumAmountIn().quotient, + this.minimumAmountOut().quotient + ); + } } \ No newline at end of file From 7c8d8c44c74ee9a7b25b02570348ddc8c8bcc37b Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Fri, 13 Sep 2024 17:59:00 -0400 Subject: [PATCH 36/41] refactor: clean invariants --- .../src/builder/V3DutchOrderBuilder.ts | 143 +++++++++--------- 1 file changed, 71 insertions(+), 72 deletions(-) diff --git a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts index c3ec04e7..c1581beb 100644 --- a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts +++ b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts @@ -2,7 +2,7 @@ import { BigNumber, ethers } from "ethers"; import invariant from "tiny-invariant"; import { OrderType } from "../constants"; -import { CosignedV3DutchOrder, CosignedV3DutchOrderInfo, UnsignedV3DutchOrder, V3CosignerData } from "../order/V3DutchOrder"; +import { CosignedV3DutchOrder, CosignedV3DutchOrderInfo, UnsignedV3DutchOrder, UnsignedV3DutchOrderInfo, V3CosignerData } from "../order/V3DutchOrder"; import { V3DutchInput, V3DutchOutput } from "../order/types"; import { getPermit2, getReactor, isCosigned } from "../utils"; @@ -40,56 +40,9 @@ export class V3DutchOrderBuilder extends OrderBuilder { } build(): CosignedV3DutchOrder { - invariant(this.info.cosigner !== undefined, "cosigner not set"); invariant(this.info.cosignature !== undefined, "cosignature not set"); - invariant(this.info.input !== undefined, "input not set"); - invariant( - this.info.outputs && this.info.outputs.length > 0, - "outputs not set" - ); - // Check if input curve is valid - invariant(this.info.input.curve.relativeAmounts.length === this.info.input.curve.relativeBlocks.length, "relativeBlocks and relativeAmounts length mismatch"); - invariant(this.isRelativeBlocksIncreasing(this.info.input.curve.relativeBlocks), "relativeBlocks not strictly increasing"); - // For each output's curve, we need to make sure relativeBlocks is strictly increasing - this.info.outputs.forEach((output) => { - invariant( - output.curve.relativeBlocks.length === output.curve.relativeAmounts.length, - "relativeBlocks and relativeAmounts length mismatch" - ); - // For each output's curve, we need to make sure relativeBlocks is strictly increasing - invariant(this.isRelativeBlocksIncreasing(output.curve.relativeBlocks), "relativeBlocks not strictly increasing"); - }); - // In V3, we are not enforcing that the startAmount is greater than the endAmount - invariant(this.info.cosignerData !== undefined, "cosignerData not set"); - invariant(this.info.cosignerData.decayStartBlock !== undefined, "decayStartBlock not set"); - // In V3, we don't have a decayEndTime field and use OrderInfo.deadline field for Permit2 - invariant(this.orderInfo.deadline !== undefined, "deadline not set"); - invariant( - this.info.cosignerData.exclusiveFiller !== undefined, - "exclusiveFiller not set" - ); - invariant( - this.info.cosignerData.exclusivityOverrideBps !== undefined, - "exclusivityOverrideBps not set" - ); - invariant( - this.info.cosignerData.inputOverride.lte(this.info.input.startAmount), - "inputOverride larger than original input" - ); - invariant( - this.info.cosignerData.outputOverrides.length > 0, - "outputOverrides not set" - ); - this.info.cosignerData.outputOverrides.forEach((override, idx) => { - invariant( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - override.gte(this.info.outputs![idx].startAmount), - "outputOverride smaller than original output" - ); - }); - invariant(this.info.input !== undefined, "original input not set"); - // We are not checking if the decayStartTime is before the deadline because it is not enforced in the smart contract - + this.checkUnsignedInvariants(this.info); + this.checkCosignedInvariants(this.info); return new CosignedV3DutchOrder( Object.assign(this.getOrderInfo(), { cosignerData: this.info.cosignerData, @@ -101,7 +54,6 @@ export class V3DutchOrderBuilder extends OrderBuilder { this.chainId, this.permit2Address ); - } private permit2Address: string; private info: Partial; @@ -142,12 +94,12 @@ export class V3DutchOrderBuilder extends OrderBuilder { private initializeCosignerData(data: Partial): void { this.info.cosignerData = { - decayStartBlock: 0, - exclusiveFiller: ethers.constants.AddressZero, - exclusivityOverrideBps: BigNumber.from(0), - inputOverride: BigNumber.from(0), - outputOverrides: [], - ...data, + decayStartBlock: 0, + exclusiveFiller: ethers.constants.AddressZero, + exclusivityOverrideBps: BigNumber.from(0), + inputOverride: BigNumber.from(0), + outputOverrides: [], + ...data, }; } @@ -162,6 +114,61 @@ export class V3DutchOrderBuilder extends OrderBuilder { return true; } + private checkUnsignedInvariants(info: Partial): asserts info is UnsignedV3DutchOrderInfo { + invariant(info.cosigner !== undefined, "cosigner not set"); + invariant(info.input !== undefined, "input not set"); + invariant( + info.outputs && info.outputs.length > 0, + "outputs not set" + ); + // Check if input curve is valid + invariant(info.input.curve.relativeAmounts.length === info.input.curve.relativeBlocks.length, "relativeBlocks and relativeAmounts length mismatch"); + invariant(this.isRelativeBlocksIncreasing(info.input.curve.relativeBlocks), "relativeBlocks not strictly increasing"); + // For each output's curve, we need to make sure relativeBlocks is strictly increasing + info.outputs.forEach((output) => { + invariant( + output.curve.relativeBlocks.length === output.curve.relativeAmounts.length, + "relativeBlocks and relativeAmounts length mismatch" + ); + // For each output's curve, we need to make sure relativeBlocks is strictly increasing + invariant(this.isRelativeBlocksIncreasing(output.curve.relativeBlocks), "relativeBlocks not strictly increasing"); + }); + // In V3, we don't have a decayEndTime field and use OrderInfo.deadline field for Permit2 + invariant(this.orderInfo.deadline !== undefined, "deadline not set"); + invariant(this.orderInfo.swapper !== undefined, "swapper not set"); + } + + private checkCosignedInvariants(info: Partial): asserts info is CosignedV3DutchOrderInfo { + // In V3, we are not enforcing that the startAmount is greater than the endAmount + invariant(info.cosignerData !== undefined, "cosignerData not set"); + invariant(info.cosignerData.decayStartBlock !== undefined, "decayStartBlock not set"); + invariant( + info.cosignerData.exclusiveFiller !== undefined, + "exclusiveFiller not set" + ); + invariant( + info.cosignerData.exclusivityOverrideBps !== undefined, + "exclusivityOverrideBps not set" + ); + invariant( + info.cosignerData.outputOverrides.length > 0, + "outputOverrides not set" + ); + invariant( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + info.cosignerData.inputOverride.lte(this.info.input!.startAmount), + "inputOverride larger than original input" + ); + info.cosignerData.outputOverrides.forEach((override, idx) => { + invariant( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + override.gte(this.info.outputs![idx].startAmount), + "outputOverride smaller than original output" + ); + }); + // We are not checking if the decayStartTime is before the deadline because it is not enforced in the smart contract + } + input(input: V3DutchInput): this { this.info.input = input; return this; @@ -171,7 +178,7 @@ export class V3DutchOrderBuilder extends OrderBuilder { this.info.outputs?.push(output); return this; } - + inputOverride(inputOverride: BigNumber): this { if (!this.info.cosignerData) { this.initializeCosignerData({ inputOverride }); @@ -183,9 +190,9 @@ export class V3DutchOrderBuilder extends OrderBuilder { outputOverrides(outputOverrides: BigNumber[]): this { if (!this.info.cosignerData) { - this.initializeCosignerData({ outputOverrides }); + this.initializeCosignerData({ outputOverrides }); } else { - this.info.cosignerData.outputOverrides = outputOverrides; + this.info.cosignerData.outputOverrides = outputOverrides; } return this; } @@ -199,7 +206,7 @@ export class V3DutchOrderBuilder extends OrderBuilder { super.swapper(swapper); return this; } - + nonce(nonce: BigNumber): this { super.nonce(nonce); return this; @@ -235,8 +242,8 @@ export class V3DutchOrderBuilder extends OrderBuilder { // ensures that we only change non fee outputs nonFeeRecipient(newRecipient: string, feeRecipient?: string): this { invariant( - newRecipient !== feeRecipient, - `newRecipient must be different from feeRecipient: ${newRecipient}` + newRecipient !== feeRecipient, + `newRecipient must be different from feeRecipient: ${newRecipient}` ); if (!this.info.outputs) { return this; @@ -259,15 +266,7 @@ export class V3DutchOrderBuilder extends OrderBuilder { } buildPartial(): UnsignedV3DutchOrder { //build an unsigned order - invariant(this.info.cosigner !== undefined, "cosigner not set"); - invariant(this.info.input !== undefined, "input not set"); - invariant( - this.info.outputs && this.info.outputs.length > 0, - "outputs not set" - ); - invariant(this.info.input !== undefined, "original input not set"); - invariant(!this.info.deadline, "deadline not set"); - invariant(!this.info.swapper, "swapper not set"); + this.checkUnsignedInvariants(this.info); return new UnsignedV3DutchOrder( Object.assign(this.getOrderInfo(), { input: this.info.input, From 361f559c1459e14fb907fbd2c6a2a7fdaa1f7009 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Mon, 16 Sep 2024 14:54:16 -0400 Subject: [PATCH 37/41] feat, test: Cosigned toJSON, fromJSON --- .../src/builder/V3DutchOrderBuilder.test.ts | 98 +++++++++++++++++++ .../src/builder/V3DutchOrderBuilder.ts | 6 ++ sdks/uniswapx-sdk/src/order/V3DutchOrder.ts | 75 +++++++++++++- 3 files changed, 178 insertions(+), 1 deletion(-) diff --git a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.test.ts b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.test.ts index 58001751..faa108e0 100644 --- a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.test.ts +++ b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.test.ts @@ -1,6 +1,7 @@ import { BigNumber, constants } from "ethers"; import { CosignedV3DutchOrder, UnsignedV3DutchOrder } from "../order/V3DutchOrder"; +import { encodeExclusiveFillerData } from "../order/validation"; import { V3DutchOrderBuilder } from "./V3DutchOrderBuilder"; @@ -564,6 +565,103 @@ describe("V3DutchOrderBuilder", () => { ); }); + it("Regenerate builder from order JSON", () => { + const deadline = Math.floor(Date.now() / 1000) + 1000; + const fillerAddress = "0x1111111111111111111111111111111111111111"; + const additionalValidationContract = + "0x2222222222222222222222222222222222222222"; + const timestamp = Math.floor(Date.now() / 1000) + 100; + const validationInfo = encodeExclusiveFillerData( + fillerAddress, + timestamp, + 1, + additionalValidationContract + ); + const order = builder + .cosigner(constants.AddressZero) + .cosignature("0x") + .decayStartBlock(212121) + .input({ + token: INPUT_TOKEN, + startAmount: INPUT_START_AMOUNT, + curve: { + relativeBlocks: [1], + relativeAmounts: [BigInt(0)], + }, + maxAmount: INPUT_START_AMOUNT.add(1), + }) + .output({ + token: OUTPUT_TOKEN, + startAmount: OUTPUT_START_AMOUNT, + curve: { + relativeBlocks: [4], + relativeAmounts: [BigInt(4)], + }, + recipient: constants.AddressZero, + }) + .inputOverride(INPUT_START_AMOUNT.mul(99).div(100)) + .outputOverrides([OUTPUT_START_AMOUNT]) + .deadline(deadline) + .swapper(constants.AddressZero) + .nonce(BigNumber.from(100)) + .validation(validationInfo) + .build(); + + const json = order.toJSON(); + const jsonToOrder = CosignedV3DutchOrder.fromJSON(json, 1); + const regeneratedBuilder = V3DutchOrderBuilder.fromOrder(jsonToOrder); + const regeneratedOrder = regeneratedBuilder.build(); + expect(regeneratedOrder.toJSON()).toMatchObject(order.toJSON()); + }); + + it("Regenerate builder and modify", () => { + const deadline = Math.floor(Date.now() / 1000) + 1000; + const fillerAddress = "0x1111111111111111111111111111111111111111"; + const additionalValidationContract = + "0x2222222222222222222222222222222222222222"; + const timestamp = Math.floor(Date.now() / 1000) + 100; + const validationInfo = encodeExclusiveFillerData( + fillerAddress, + timestamp, + 1, + additionalValidationContract + ); + const order = builder + .cosigner(constants.AddressZero) + .cosignature("0x") + .decayStartBlock(212121) + .input({ + token: INPUT_TOKEN, + startAmount: INPUT_START_AMOUNT, + curve: { + relativeBlocks: [1], + relativeAmounts: [BigInt(0)], + }, + maxAmount: INPUT_START_AMOUNT.add(1), + }) + .output({ + token: OUTPUT_TOKEN, + startAmount: OUTPUT_START_AMOUNT, + curve: { + relativeBlocks: [4], + relativeAmounts: [BigInt(4)], + }, + recipient: constants.AddressZero, + }) + .inputOverride(INPUT_START_AMOUNT.mul(99).div(100)) + .outputOverrides([OUTPUT_START_AMOUNT]) + .deadline(deadline) + .swapper(constants.AddressZero) + .nonce(BigNumber.from(100)) + .validation(validationInfo) + .build(); + + const regeneratedBuilder = V3DutchOrderBuilder.fromOrder(order); + regeneratedBuilder.decayStartBlock(214221422142); + const regeneratedOrder = regeneratedBuilder.build(); + expect(regeneratedOrder.info.cosignerData.decayStartBlock).toEqual(214221422142); + }); + describe("Partial order tests", () => { it("Test valid order with buildPartial", () => { const order = builder diff --git a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts index c1581beb..15031b8f 100644 --- a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts +++ b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts @@ -4,6 +4,7 @@ import invariant from "tiny-invariant"; import { OrderType } from "../constants"; import { CosignedV3DutchOrder, CosignedV3DutchOrderInfo, UnsignedV3DutchOrder, UnsignedV3DutchOrderInfo, V3CosignerData } from "../order/V3DutchOrder"; import { V3DutchInput, V3DutchOutput } from "../order/types"; +import { ValidationInfo } from "../order/validation"; import { getPermit2, getReactor, isCosigned } from "../utils"; import { OrderBuilder } from "./OrderBuilder"; @@ -212,6 +213,11 @@ export class V3DutchOrderBuilder extends OrderBuilder { return this; } + validation(info: ValidationInfo): this { + super.validation(info); + return this; + } + cosignerData(cosignerData: V3CosignerData): this { this.decayStartBlock(cosignerData.decayStartBlock); this.exclusiveFiller(cosignerData.exclusiveFiller); diff --git a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts index 13566401..1a41ec3e 100644 --- a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts +++ b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts @@ -28,6 +28,11 @@ export type UnsignedV3DutchOrderInfo = OrderInfo & { outputs: V3DutchOutput[]; }; +export type CosignedV3DutchOrderInfoJSON = UnsignedV3DutchOrderInfoJSON & { + cosignerData: V3CosignerDataJSON; + cosignature: string; +}; + export type CosignedV3DutchOrderInfo = UnsignedV3DutchOrderInfo & { cosignerData: V3CosignerData; cosignature: string; @@ -175,7 +180,10 @@ export class UnsignedV3DutchOrder implements OffChainOrder { /** * @inheritdoc order */ - toJSON(): UnsignedV3DutchOrderInfoJSON { + toJSON(): UnsignedV3DutchOrderInfoJSON & { + permit2Address: string; + chainId: number; + } { return { reactor: this.info.reactor, swapper: this.info.swapper, @@ -202,6 +210,8 @@ export class UnsignedV3DutchOrder implements OffChainOrder { }, recipient: output.recipient, })), + chainId: this.chainId, + permit2Address: this.permit2Address, } }; @@ -323,6 +333,49 @@ export class CosignedV3DutchOrder extends UnsignedV3DutchOrder { ); } + static fromJSON( + json: CosignedV3DutchOrderInfoJSON, + chainId: number, + _permit2Address?: string + ): CosignedV3DutchOrder { + return new CosignedV3DutchOrder( + { + ...json, + nonce: BigNumber.from(json.nonce), + input: { + token: json.input.token, + startAmount: BigNumber.from(json.input.startAmount), + curve: { + relativeBlocks: json.input.curve.relativeBlocks, + relativeAmounts: json.input.curve.relativeAmounts.map(amount => BigInt(amount)), + }, + maxAmount: BigNumber.from(json.input.maxAmount), + }, + outputs: json.outputs.map(output => ({ + token: output.token, + startAmount: BigNumber.from(output.startAmount), + curve: { + relativeBlocks: output.curve.relativeBlocks, + relativeAmounts: output.curve.relativeAmounts.map(amount => BigInt(amount)), + }, + recipient: output.recipient, + })), + cosignerData: { + decayStartBlock: json.cosignerData.decayStartBlock, + exclusiveFiller: json.cosignerData.exclusiveFiller, + exclusivityOverrideBps: BigNumber.from( + json.cosignerData.exclusivityOverrideBps + ), + inputOverride: BigNumber.from(json.cosignerData.inputOverride), + outputOverrides: json.cosignerData.outputOverrides.map(BigNumber.from), + }, + cosignature: json.cosignature, + }, + chainId, + _permit2Address + ); + } + constructor( public readonly info: CosignedV3DutchOrderInfo, public readonly chainId: number, @@ -331,6 +384,26 @@ export class CosignedV3DutchOrder extends UnsignedV3DutchOrder { super(info, chainId, _permit2Address); } + /** + * @inheritdoc order + */ + toJSON(): CosignedV3DutchOrderInfoJSON & { + permit2Address: string; + chainId: number; + } { + return { + ...super.toJSON(), + cosignerData: { + decayStartBlock: this.info.cosignerData.decayStartBlock, + exclusiveFiller: this.info.cosignerData.exclusiveFiller, + exclusivityOverrideBps: this.info.cosignerData.exclusivityOverrideBps.toNumber(), + inputOverride: this.info.cosignerData.inputOverride.toString(), + outputOverrides: this.info.cosignerData.outputOverrides.map(override => override.toString()), + }, + cosignature: this.info.cosignature, + }; + } + static parse( encoded: string, chainId: number, From 0ca5a19dae8d3ac2a919b53a9135137615e92343 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Thu, 19 Sep 2024 15:10:23 -0400 Subject: [PATCH 38/41] refactor: use length to decode relativeBlocks --- sdks/uniswapx-sdk/src/order/V3DutchOrder.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts index 1a41ec3e..45823d5b 100644 --- a/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts +++ b/sdks/uniswapx-sdk/src/order/V3DutchOrder.ts @@ -531,7 +531,7 @@ function parseSerializedOrder(serialized: string): CosignedV3DutchOrderInfo { token, startAmount, curve: { - relativeBlocks: decodeRelativeBlocks(inputRelativeBlocks), + relativeBlocks: decodeRelativeBlocks(inputRelativeBlocks, relativeAmounts.length), relativeAmounts: relativeAmounts.map((amount: BigNumber) => amount.toBigInt()), }, maxAmount, @@ -546,7 +546,7 @@ function parseSerializedOrder(serialized: string): CosignedV3DutchOrderInfo { token, startAmount, curve: { - relativeBlocks: decodeRelativeBlocks(outputRelativeBlocks), + relativeBlocks: decodeRelativeBlocks(outputRelativeBlocks, relativeAmounts.length), relativeAmounts: relativeAmounts.map((amount: BigNumber) => amount.toBigInt()), }, recipient, @@ -571,13 +571,11 @@ function encodeRelativeBlocks(relativeBlocks: number[]): BigNumber { return packedData; } -function decodeRelativeBlocks(packedData: BigNumber): number[] { +function decodeRelativeBlocks(packedData: BigNumber, relativeAmountsLength: number): number[] { const relativeBlocks: number[] = []; - for (let i = 0; i < 16; i++) { + for (let i = 0; i < relativeAmountsLength; i++) { const block = packedData.shr(i * 16).toNumber() & 0xFFFF; - if (block !== 0) { - relativeBlocks.push(block); - } + relativeBlocks.push(block); } return relativeBlocks; } \ No newline at end of file From e18e677ec17df09520a3991362b2828ae8494c50 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Thu, 19 Sep 2024 15:19:11 -0400 Subject: [PATCH 39/41] refactor: clean determination of cosigned status --- sdks/uniswapx-sdk/src/builder/V2DutchOrderBuilder.ts | 5 ++--- sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts | 6 +++--- sdks/uniswapx-sdk/src/utils/order.ts | 8 -------- 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/sdks/uniswapx-sdk/src/builder/V2DutchOrderBuilder.ts b/sdks/uniswapx-sdk/src/builder/V2DutchOrderBuilder.ts index 509997ec..79e396a2 100644 --- a/sdks/uniswapx-sdk/src/builder/V2DutchOrderBuilder.ts +++ b/sdks/uniswapx-sdk/src/builder/V2DutchOrderBuilder.ts @@ -11,7 +11,7 @@ import { UnsignedV2DutchOrder, } from "../order"; import { ValidationInfo } from "../order/validation"; -import { getPermit2, getReactor, isCosigned } from "../utils"; +import { getPermit2, getReactor } from "../utils"; import { OrderBuilder } from "./OrderBuilder"; @@ -40,7 +40,7 @@ export class V2DutchOrderBuilder extends OrderBuilder { builder.output(output); } - if (isCosigned(order)) { + if (order instanceof CosignedV2DutchOrder) { builder.cosignature(order.info.cosignature); builder.decayEndTime(order.info.cosignerData.decayEndTime); builder.decayStartTime(order.info.cosignerData.decayStartTime); @@ -287,7 +287,6 @@ export class V2DutchOrderBuilder extends OrderBuilder { "exclusivityOverrideBps not set" ); invariant( - this.info.cosignerData.inputOverride !== undefined && // inputOverride is defaulted to 0 because enforced to be of type BigNumber this.info.cosignerData.inputOverride.lte(this.info.input.startAmount), "inputOverride larger than original input" ); diff --git a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts index 15031b8f..907349c6 100644 --- a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts +++ b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts @@ -5,7 +5,7 @@ import { OrderType } from "../constants"; import { CosignedV3DutchOrder, CosignedV3DutchOrderInfo, UnsignedV3DutchOrder, UnsignedV3DutchOrderInfo, V3CosignerData } from "../order/V3DutchOrder"; import { V3DutchInput, V3DutchOutput } from "../order/types"; import { ValidationInfo } from "../order/validation"; -import { getPermit2, getReactor, isCosigned } from "../utils"; +import { getPermit2, getReactor } from "../utils"; import { OrderBuilder } from "./OrderBuilder"; @@ -29,7 +29,7 @@ export class V3DutchOrderBuilder extends OrderBuilder { builder.output(output); }); - if (isCosigned(order)) { + if (order instanceof CosignedV3DutchOrder) { builder.cosignature(order.info.cosignature); builder.decayStartBlock(order.info.cosignerData.decayStartBlock); builder.exclusiveFiller(order.info.cosignerData.exclusiveFiller); @@ -167,7 +167,7 @@ export class V3DutchOrderBuilder extends OrderBuilder { "outputOverride smaller than original output" ); }); - // We are not checking if the decayStartTime is before the deadline because it is not enforced in the smart contract + // We are not checking if the decayStartBlock is before the deadline because it is not enforced in the smart contract } input(input: V3DutchInput): this { diff --git a/sdks/uniswapx-sdk/src/utils/order.ts b/sdks/uniswapx-sdk/src/utils/order.ts index 13a9d78a..67a2bf38 100644 --- a/sdks/uniswapx-sdk/src/utils/order.ts +++ b/sdks/uniswapx-sdk/src/utils/order.ts @@ -159,14 +159,6 @@ export class RelayOrderParser extends OrderParser { } } -type UnsignedOrder = UnsignedV2DutchOrder | UnsignedV3DutchOrder; -type CosignedOrder = CosignedV2DutchOrder | CosignedV3DutchOrder; -export function isCosigned( - order: T | U -): order is U { - return 'cosignature' in (order as U).info; -} - export function originalIfZero(value: BigNumber, original: BigNumber): BigNumber { return value.isZero() ? original : value; } \ No newline at end of file From a8759dc5ae5c6984c243f18b869a40966592b936 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Thu, 19 Sep 2024 15:22:04 -0400 Subject: [PATCH 40/41] fix: test description typos --- sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts b/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts index 003736db..f3e306f7 100644 --- a/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts +++ b/sdks/uniswapx-sdk/src/order/V3DutchOrder.test.ts @@ -163,7 +163,7 @@ describe("V3DutchOrder", () => { }); describe("resolve DutchV3 orders", () => { - it("resolves before decayStartTime", () => { + it("resolves before decayStartBlock", () => { const order = new CosignedV3DutchOrder(getFullOrderInfo({}), CHAIN_ID); const resolved = order.resolve({ currentBlock: BLOCK_NUMBER - 1, //no decay yet @@ -192,7 +192,7 @@ describe("V3DutchOrder", () => { expect(resolved.outputs[0].amount).eq(order.info.outputs[0].startAmount); }); - it("resolves at decayStartTime", () => { + it("resolves at decayStartBlock", () => { const order = new CosignedV3DutchOrder(getFullOrderInfo({}), CHAIN_ID); const resolved = order.resolve({ currentBlock: BLOCK_NUMBER, @@ -221,7 +221,7 @@ describe("V3DutchOrder", () => { expect(resolved.outputs[0].amount.toNumber()).eq(endAmount.toNumber()); }); - it("resolves after decayEndTime without overrides", () => { + it("resolves after last decay without overrides", () => { const order = new CosignedV3DutchOrder(getFullOrderInfoWithoutOverrides, CHAIN_ID); const resolved = order.resolve({ currentBlock: BLOCK_NUMBER + 42, From 40d17f02a4abf685c618d4dd12b1737ad5f29ba8 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Thu, 19 Sep 2024 17:49:43 -0400 Subject: [PATCH 41/41] feat: helper for getting maxAmountOut from V3 curve --- .../src/builder/V3DutchOrderBuilder.test.ts | 7 +++++++ sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts | 12 ++++++++++++ 2 files changed, 19 insertions(+) diff --git a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.test.ts b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.test.ts index faa108e0..e3b1bab9 100644 --- a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.test.ts +++ b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.test.ts @@ -721,6 +721,13 @@ describe("V3DutchOrderBuilder", () => { .buildPartial() ).toThrow("Invariant failed: swapper not set"); }); + + it("Test maxAmountOut", () => { + const startAmount = INPUT_START_AMOUNT; + const relativeAmounts = [BigInt(0), BigInt(3), BigInt(-2), BigInt(-4), BigInt(-3)]; + const maxout = V3DutchOrderBuilder.getMaxAmountOut(startAmount, relativeAmounts); + expect(maxout).toEqual(startAmount.add(4)); + }); }); describe("fromOrder", () => { diff --git a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts index 907349c6..a1474695 100644 --- a/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts +++ b/sdks/uniswapx-sdk/src/builder/V3DutchOrderBuilder.ts @@ -283,4 +283,16 @@ export class V3DutchOrderBuilder extends OrderBuilder { this.permit2Address ); } + + // A helper function for users of the class to easily the value to pass to maxAmount in an input + static getMaxAmountOut(startAmount: BigNumber, relativeAmounts: bigint[]) : BigNumber { + if (relativeAmounts.length == 0) { + throw new Error("relativeAmounts cannot be empty"); + } + // Find the minimum of the relative amounts + const minRelativeAmount = relativeAmounts.reduce((min, amount) => amount < min ? amount : min, relativeAmounts[0]); + // Maximum is the start - the min of the relative amounts + const maxOut = startAmount.sub(minRelativeAmount.toString()); + return maxOut; + } } \ No newline at end of file