diff --git a/.github/workflows/monorepo-checks.yml b/.github/workflows/monorepo-checks.yml index 1acc04679..4e4bb887f 100644 --- a/.github/workflows/monorepo-checks.yml +++ b/.github/workflows/monorepo-checks.yml @@ -59,6 +59,7 @@ jobs: uses: nick-fields/retry@7d4a37704547a311dbb66ebdf5b23ec19374a767 env: FORK_URL: ${{ secrets.FORK_URL }} + FORK_URL_8453: ${{ secrets.FORK_URL_8453 }} with: timeout_minutes: 20 retry_wait_seconds: 2 diff --git a/sdks/uniswapx-sdk/integration/.gitignore b/sdks/uniswapx-sdk/integration/.gitignore new file mode 100644 index 000000000..4c49bd78f --- /dev/null +++ b/sdks/uniswapx-sdk/integration/.gitignore @@ -0,0 +1 @@ +.env diff --git a/sdks/uniswapx-sdk/integration/hardhat.config.ts b/sdks/uniswapx-sdk/integration/hardhat.config.ts index 64f3d8083..f9b26faf3 100644 --- a/sdks/uniswapx-sdk/integration/hardhat.config.ts +++ b/sdks/uniswapx-sdk/integration/hardhat.config.ts @@ -3,11 +3,17 @@ import "@nomiclabs/hardhat-ethers"; import "@nomicfoundation/hardhat-chai-matchers"; import "@typechain/hardhat"; +import 'dotenv/config' + const config: HardhatUserConfig = { solidity: "0.8.16", networks: { hardhat: { chainId: 1, + forking: { + enabled: true, + url: process.env.FORK_URL! + } }, }, }; diff --git a/sdks/uniswapx-sdk/integration/package.json b/sdks/uniswapx-sdk/integration/package.json index 0d246052e..389a441fd 100644 --- a/sdks/uniswapx-sdk/integration/package.json +++ b/sdks/uniswapx-sdk/integration/package.json @@ -32,6 +32,7 @@ "@typechain/ethers-v5": "^10.1.0", "@typechain/hardhat": "^6.1.2", "@uniswap/sdk-core": "^5.0.0", + "dotenv": "^16.0.3", "ethers": "^5.7.0", "typechain": "^8.1.0" } diff --git a/sdks/uniswapx-sdk/integration/test/PriorityOrderValidator.spec.ts b/sdks/uniswapx-sdk/integration/test/PriorityOrderValidator.spec.ts new file mode 100644 index 000000000..5a5f80c47 --- /dev/null +++ b/sdks/uniswapx-sdk/integration/test/PriorityOrderValidator.spec.ts @@ -0,0 +1,409 @@ +import { expect } from "chai"; +import { BigNumber, Contract, ethers } from "ethers"; + +import PriorityOrderReactorAbi from "../../abis/PriorityOrderReactor.json"; +import OrderQuoterAbi from "../../abis/OrderQuoter.json"; +import MockERC20Abi from "../../abis/MockERC20.json"; + +import { + OrderQuoter, + Permit2, + PriorityOrderReactor, + MockERC20, + Permit2__factory, +} from "../../src/contracts"; +import { + PriorityOrderBuilder, + OrderValidator, + UniswapXOrderQuoter as OrderQuoterLib, + OrderValidation, + PriorityCosignerData, + CosignedPriorityOrder, +} from "../../dist/src"; +import { StaticJsonRpcProvider } from "@ethersproject/providers"; +import { REACTOR_ADDRESS_MAPPING, UNISWAPX_ORDER_QUOTER_MAPPING } from "../../src/constants"; +import { parseEther } from "ethers/lib/utils"; +import { PERMIT2_ADDRESS } from "@uniswap/permit2-sdk"; + +if(!process.env.FORK_URL_8453) { + throw new Error("FORK_URL_8453 not defined in environment"); +} + +// Priority order integration tests do not run on hardhat because they require +// a full JsonRpcProvider which supports block overrides +describe("PriorityOrderValidator", () => { + let reactor: PriorityOrderReactor; + let permit2: Permit2; + let quoter: OrderQuoter; + let chainId: number; + let builder: PriorityOrderBuilder; + let validator: OrderValidator; + let tokenIn: MockERC20; + let cosigner: ethers.Wallet; + let swapper: ethers.Wallet; + let blockNumber: BigNumber; + let swapperAddress: string; + let cosignerAddress: string; + + const provider = new StaticJsonRpcProvider(process.env.FORK_URL_8453); + const USDC_BASE = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"; + const ZERO_ADDRESS = ethers.constants.AddressZero; // tokenOut for simplicity + + beforeEach(async () => { + chainId = 8453; + + permit2 = new Contract(PERMIT2_ADDRESS, Permit2__factory.abi, provider) as Permit2; + reactor = new Contract(REACTOR_ADDRESS_MAPPING[chainId].Priority!, PriorityOrderReactorAbi.abi, provider) as PriorityOrderReactor; + quoter = new Contract(UNISWAPX_ORDER_QUOTER_MAPPING[chainId], OrderQuoterAbi.abi, provider) as OrderQuoter; + + builder = new PriorityOrderBuilder( + chainId, + reactor.address, + permit2.address + ); + + swapper = ethers.Wallet.createRandom().connect(provider); + swapperAddress = await swapper.getAddress(); + cosigner = ethers.Wallet.createRandom().connect(provider); + cosignerAddress = await cosigner.getAddress(); + + validator = new OrderValidator(provider, chainId, quoter.address); + + tokenIn = new Contract(USDC_BASE, MockERC20Abi.abi, provider) as MockERC20; + + blockNumber = BigNumber.from(await provider.getBlockNumber()); + }); + + const getCosignerData = ( + blockNumber: BigNumber, + overrides: Partial = {} + ): PriorityCosignerData => { + const defaultData: PriorityCosignerData = { + auctionTargetBlock: blockNumber + }; + return Object.assign(defaultData, overrides); + }; + + it("quotes a valid order", async () => { + const deadline = Math.floor(new Date().getTime() / 1000) + 1000; + const preBuildOrder = builder + .deadline(deadline) + .auctionStartBlock(blockNumber) + .cosigner(cosignerAddress) + .baselinePriorityFeeWei(BigNumber.from(0)) + .swapper(swapperAddress) + .nonce(BigNumber.from(98)) + .input({ + token: tokenIn.address, + amount: BigNumber.from(0), + mpsPerPriorityFeeWei: BigNumber.from(0) + }) + .output({ + token: ZERO_ADDRESS, + amount: BigNumber.from("1000000000000000000"), + mpsPerPriorityFeeWei: BigNumber.from(1), + recipient: "0x0000000000000000000000000000000000000000", + }); + + let unsignedPriorityOrder = preBuildOrder.buildPartial(); + + const cosignerData = getCosignerData(blockNumber, {}); + const cosignerHash = unsignedPriorityOrder.cosignatureHash(cosignerData); + const cosignature = ethers.utils.joinSignature( + cosigner._signingKey().signDigest(cosignerHash) + ); + + const order = preBuildOrder + .cosignerData(cosignerData) + .cosignature(cosignature) + .build(); + + const { domain, types, values } = order.permitData(); + const signature = await swapper._signTypedData(domain, types, values); + + const quoterLib = new OrderQuoterLib( + provider, + chainId, + quoter.address + ); + const { validation, quote } = await quoterLib.quote({ order, signature }); + expect(validation).to.equal(OrderValidation.OK); + if (!quote) { + throw new Error("Invalid quote"); + } + + expect(quote.input.amount.toString()).to.equal("0"); + }); + + it("validates a valid order", async () => { + const deadline = Math.floor(new Date().getTime() / 1000) + 1000; + const preBuildOrder = builder + .deadline(deadline) + .auctionStartBlock(blockNumber) + .cosigner(cosignerAddress) + .baselinePriorityFeeWei(BigNumber.from(0)) + .nonce(BigNumber.from(100)) + .swapper(swapperAddress) + .input({ + token: tokenIn.address, + amount: BigNumber.from(0), + mpsPerPriorityFeeWei: BigNumber.from(0) + }) + .output({ + token: ZERO_ADDRESS, + amount: BigNumber.from(0), + mpsPerPriorityFeeWei: BigNumber.from(1), + recipient: "0x0000000000000000000000000000000000000000", + }); + + let unsignedPriorityOrder = preBuildOrder.buildPartial(); + + const cosignerData = getCosignerData(blockNumber, {}); + const cosignerHash = unsignedPriorityOrder.cosignatureHash(cosignerData); + const cosignature = ethers.utils.joinSignature( + cosigner._signingKey().signDigest(cosignerHash) + ); + + const order = preBuildOrder + .cosignerData(cosignerData) + .cosignature(cosignature) + .build(); + + const { domain, types, values } = order.permitData(); + const signature = await swapper._signTypedData(domain, types, values); + + expect(await validator.validate({ order, signature })).to.equal( + OrderValidation.OK + ); + }); + + it("validates a valid order with auctionStartBlock in the future", async () => { + const auctionStartBlock = blockNumber.add(10); + const deadline = Math.floor(new Date().getTime() / 1000) + 1000; + const preBuildOrder = builder + .deadline(deadline) + .auctionStartBlock(auctionStartBlock) + .cosigner(cosignerAddress) + .baselinePriorityFeeWei(BigNumber.from(0)) + .swapper(swapperAddress) + .nonce(BigNumber.from(98)) + .input({ + token: tokenIn.address, + amount: BigNumber.from(0), + mpsPerPriorityFeeWei: BigNumber.from(0) + }) + .output({ + token: ZERO_ADDRESS, + amount: BigNumber.from(0), + mpsPerPriorityFeeWei: BigNumber.from(1), + recipient: "0x0000000000000000000000000000000000000000", + }); + + let unsignedPriorityOrder = preBuildOrder.buildPartial(); + + const cosignerData = getCosignerData(auctionStartBlock, {}); + const cosignerHash = unsignedPriorityOrder.cosignatureHash(cosignerData); + const cosignature = ethers.utils.joinSignature( + cosigner._signingKey().signDigest(cosignerHash) + ); + + const order = preBuildOrder + .cosignerData(cosignerData) + .cosignature(cosignature) + .build(); + + const { domain, types, values } = order.permitData(); + const signature = await swapper._signTypedData(domain, types, values); + + expect(await validator.validate({ order, signature })).to.equal( + OrderValidation.OrderNotFillableYet + ); + }); + + it("validates an order with input and output scaling", async () => { + const deadline = Math.floor(new Date().getTime() / 1000) + 1000; + const preBuildOrder = builder + .deadline(deadline) + .auctionStartBlock(blockNumber) + .cosigner(cosignerAddress) + .baselinePriorityFeeWei(BigNumber.from(0)) + .nonce(BigNumber.from(100)) + .swapper(swapperAddress) + .input({ + token: tokenIn.address, + amount: BigNumber.from(0), + mpsPerPriorityFeeWei: BigNumber.from(0) + }) + .output({ + token: ZERO_ADDRESS, + amount: BigNumber.from(0), + mpsPerPriorityFeeWei: BigNumber.from(1), + recipient: "0x0000000000000000000000000000000000000000", + }); + + let unsignedPriorityOrder = preBuildOrder.buildPartial(); + + const cosignerData = getCosignerData(blockNumber, {}); + const cosignerHash = unsignedPriorityOrder.cosignatureHash(cosignerData); + const cosignature = ethers.utils.joinSignature( + cosigner._signingKey().signDigest(cosignerHash) + ); + + let order = preBuildOrder + .cosignerData(cosignerData) + .cosignature(cosignature) + .build(); + + order = new CosignedPriorityOrder( + Object.assign(order.info, { + input: { + token: tokenIn.address, + amount: BigNumber.from(0), + mpsPerPriorityFeeWei: BigNumber.from(1), + }, + }), + chainId, + permit2.address + ) + + const { domain, types, values } = order.permitData(); + const signature = await swapper._signTypedData(domain, types, values); + + expect(await validator.validate({ order, signature })).to.equal( + OrderValidation.InvalidOrderFields + ); + }); + + it("validates an order with insufficient funds", async () => { + const deadline = Math.floor(new Date().getTime() / 1000) + 1000; + const preBuildOrder = builder + .deadline(deadline) + .auctionStartBlock(blockNumber) + .cosigner(cosignerAddress) + .baselinePriorityFeeWei(BigNumber.from(0)) + .nonce(BigNumber.from(100)) + .swapper(swapperAddress) + .input({ + token: tokenIn.address, + amount: parseEther('2'), + mpsPerPriorityFeeWei: BigNumber.from(0), + }) + .output({ + token: ZERO_ADDRESS, + amount: BigNumber.from(0), + mpsPerPriorityFeeWei: BigNumber.from(1), + recipient: "0x0000000000000000000000000000000000000000", + }); + + let unsignedPriorityOrder = preBuildOrder.buildPartial(); + const cosignerData = getCosignerData(blockNumber, {}); + const cosignerHash = unsignedPriorityOrder.cosignatureHash(cosignerData); + const cosignature = ethers.utils.joinSignature( + cosigner._signingKey().signDigest(cosignerHash) + ); + + const order = preBuildOrder + .cosignerData(cosignerData) + .cosignature(cosignature) + .build(); + + const { domain, types, values } = order.permitData(); + const signature = await swapper._signTypedData(domain, types, values); + + expect(await validator.validate({ order, signature })).to.equal( + OrderValidation.InsufficientFunds + ); + }); + + it("validates an order with insufficient funds with auctionStartBlock in the future", async () => { + const auctionStartBlock = blockNumber.add(10); + const deadline = Math.floor(new Date().getTime() / 1000) + 1000; + const preBuildOrder = builder + .deadline(deadline) + .auctionStartBlock(auctionStartBlock) + .cosigner(cosignerAddress) + .baselinePriorityFeeWei(BigNumber.from(0)) + .nonce(BigNumber.from(100)) + .swapper(swapperAddress) + .input({ + token: tokenIn.address, + amount: parseEther('2'), + mpsPerPriorityFeeWei: BigNumber.from(0), + }) + .output({ + token: ZERO_ADDRESS, + amount: BigNumber.from(0), + mpsPerPriorityFeeWei: BigNumber.from(1), + recipient: "0x0000000000000000000000000000000000000000", + }); + + let unsignedPriorityOrder = preBuildOrder.buildPartial(); + const cosignerData = getCosignerData(auctionStartBlock, {}); + const cosignerHash = unsignedPriorityOrder.cosignatureHash(cosignerData); + const cosignature = ethers.utils.joinSignature( + cosigner._signingKey().signDigest(cosignerHash) + ); + + const order = preBuildOrder + .cosignerData(cosignerData) + .cosignature(cosignature) + .build(); + + const { domain, types, values } = order.permitData(); + const signature = await swapper._signTypedData(domain, types, values); + + // even though the auctionStartBlock is in the future, we expect to bubble up all other errors before that one + expect(await validator.validate({ order, signature })).to.equal( + OrderValidation.InsufficientFunds + ); + }); + + it("validates an expired order", async () => { + const deadline = Math.floor(new Date().getTime() / 1000) + 1; + const preBuildOrder = builder + .deadline(deadline) + .auctionStartBlock(blockNumber) + .cosigner(cosignerAddress) + .baselinePriorityFeeWei(BigNumber.from(0)) + .swapper(swapperAddress) + .nonce(BigNumber.from(100)) + .input({ + token: tokenIn.address, + amount: BigNumber.from(0), + mpsPerPriorityFeeWei: BigNumber.from(0) + }) + .output({ + token: ZERO_ADDRESS, + amount: BigNumber.from(0), + mpsPerPriorityFeeWei: BigNumber.from(1), + recipient: "0x0000000000000000000000000000000000000000", + }); + + + const cosignerData = getCosignerData(blockNumber, {}); + const cosignerHash = preBuildOrder.buildPartial().cosignatureHash(cosignerData); + const cosignature = ethers.utils.joinSignature( + cosigner._signingKey().signDigest(cosignerHash) + ); + + let order = preBuildOrder + .cosignerData(cosignerData) + .cosignature(cosignature) + .build(); + + order = new CosignedPriorityOrder( + Object.assign(order.info, { + deadline: deadline - 100 + }), + chainId, + permit2.address + ) + + const { domain, types, values } = order.permitData(); + const signature = await swapper._signTypedData(domain, types, values); + + expect(await validator.validate({ order, signature })).to.equal( + OrderValidation.Expired + ); + }); +}); diff --git a/sdks/uniswapx-sdk/src/order/DutchOrder.ts b/sdks/uniswapx-sdk/src/order/DutchOrder.ts index 5e1b21735..dc9e49db4 100644 --- a/sdks/uniswapx-sdk/src/order/DutchOrder.ts +++ b/sdks/uniswapx-sdk/src/order/DutchOrder.ts @@ -14,6 +14,7 @@ import { ResolvedUniswapXOrder } from "../utils/OrderQuoter"; import { getDecayedAmount } from "../utils/dutchDecay"; import { + BlockOverrides, DutchInput, DutchInputJSON, DutchOutput, @@ -238,6 +239,13 @@ export class DutchOrder implements OffChainOrder { }; } + /** + * @inheritdoc order + */ + get blockOverrides(): BlockOverrides { + return undefined + } + /** * @inheritdoc order */ diff --git a/sdks/uniswapx-sdk/src/order/PriorityOrder.ts b/sdks/uniswapx-sdk/src/order/PriorityOrder.ts index 1549c54b8..50eaceb2f 100644 --- a/sdks/uniswapx-sdk/src/order/PriorityOrder.ts +++ b/sdks/uniswapx-sdk/src/order/PriorityOrder.ts @@ -1,4 +1,4 @@ -import { SignatureLike } from "@ethersproject/bytes"; +import { hexStripZeros, SignatureLike } from "@ethersproject/bytes"; import { PermitTransferFrom, PermitTransferFromData, @@ -11,6 +11,7 @@ import { MPS } from "../constants"; import { getPermit2, ResolvedUniswapXOrder } from "../utils"; import { + BlockOverrides, OffChainOrder, OrderInfo, PriorityInput, @@ -205,6 +206,16 @@ export class UnsignedPriorityOrder implements OffChainOrder { }; } + /** + * @inheritdoc Order + */ + get blockOverrides(): BlockOverrides { + return { + number: hexStripZeros(this.info.auctionStartBlock.toHexString()), + }; + } + + /** * @inheritdoc order */ @@ -469,6 +480,15 @@ export class CosignedPriorityOrder extends UnsignedPriorityOrder { }; } + /** + * @inheritdoc Order + */ + get blockOverrides(): BlockOverrides { + return { + number: hexStripZeros(this.info.cosignerData.auctionTargetBlock.toHexString()), + }; + } + /** * @inheritdoc order */ diff --git a/sdks/uniswapx-sdk/src/order/RelayOrder.ts b/sdks/uniswapx-sdk/src/order/RelayOrder.ts index 7a8e34d64..566169835 100644 --- a/sdks/uniswapx-sdk/src/order/RelayOrder.ts +++ b/sdks/uniswapx-sdk/src/order/RelayOrder.ts @@ -12,7 +12,7 @@ import { MissingConfiguration } from "../errors"; import { ResolvedRelayOrder } from "../utils/OrderQuoter"; import { getDecayedAmount } from "../utils/dutchDecay"; -import { OffChainOrder, OrderInfo, OrderResolutionOptions } from "./types"; +import { BlockOverrides, OffChainOrder, OrderInfo, OrderResolutionOptions } from "./types"; export type RelayInput = { readonly token: string; @@ -210,6 +210,13 @@ export class RelayOrder implements OffChainOrder { }; } + /** + * @inheritdoc order + */ + get blockOverrides(): BlockOverrides { + return undefined + } + serialize(): string { const abiCoder = new ethers.utils.AbiCoder(); return abiCoder.encode(RELAY_ORDER_ABI, [ diff --git a/sdks/uniswapx-sdk/src/order/V2DutchOrder.ts b/sdks/uniswapx-sdk/src/order/V2DutchOrder.ts index 28fccd599..105047c2a 100644 --- a/sdks/uniswapx-sdk/src/order/V2DutchOrder.ts +++ b/sdks/uniswapx-sdk/src/order/V2DutchOrder.ts @@ -12,6 +12,7 @@ import { ResolvedUniswapXOrder } from "../utils/OrderQuoter"; import { getDecayedAmount } from "../utils/dutchDecay"; import { + BlockOverrides, DutchInput, DutchInputJSON, DutchOutput, @@ -195,6 +196,13 @@ export class UnsignedV2DutchOrder implements OffChainOrder { }; } + /** + * @inheritdoc order + */ + get blockOverrides(): BlockOverrides { + return undefined + } + /** * @inheritdoc order */ diff --git a/sdks/uniswapx-sdk/src/order/types.ts b/sdks/uniswapx-sdk/src/order/types.ts index aaadeb3f2..7e497ea5b 100644 --- a/sdks/uniswapx-sdk/src/order/types.ts +++ b/sdks/uniswapx-sdk/src/order/types.ts @@ -5,9 +5,12 @@ import { } from "@uniswap/permit2-sdk"; import { BigNumber } from "ethers"; +export type BlockOverrides = { number?: string } | undefined; + // General interface implemented by off chain orders export interface OffChainOrder { chainId: number; + /** * Returns the abi encoded order * @return The abi encoded serialized order which can be submitted on-chain @@ -29,6 +32,12 @@ export interface OffChainOrder { * @return The order hash which is used as a key on-chain */ hash(): string; + + /** + * Returns any block overrides to be applied when quoting the order on chain + * @return The block overrides + */ + get blockOverrides(): BlockOverrides } export type TokenAmount = { diff --git a/sdks/uniswapx-sdk/src/utils/OrderQuoter.ts b/sdks/uniswapx-sdk/src/utils/OrderQuoter.ts index 40189d575..f93fd35b1 100644 --- a/sdks/uniswapx-sdk/src/utils/OrderQuoter.ts +++ b/sdks/uniswapx-sdk/src/utils/OrderQuoter.ts @@ -1,4 +1,4 @@ -import { BaseProvider } from "@ethersproject/providers"; +import { StaticJsonRpcProvider } from "@ethersproject/providers"; import { ethers } from "ethers"; import { @@ -38,7 +38,7 @@ export enum OrderValidation { UnknownError, ValidationFailed, ExclusivityPeriod, - OrderNotFillable, + OrderNotFillableYet, InvalidGasPrice, InvalidCosignature, OK, @@ -106,7 +106,7 @@ const KNOWN_ERRORS: { [key: string]: OrderValidation } = { // PriorityOrderReactor:InvalidDeadline() "769d11e4": OrderValidation.Expired, // PriorityOrderReactor:OrderNotFillable() - c6035520: OrderValidation.OrderNotFillable, + c6035520: OrderValidation.OrderNotFillableYet, // PriorityOrderReactor:InputOutputScaling() a6b844f5: OrderValidation.InvalidOrderFields, // PriorityOrderReactor:InvalidGasPrice() @@ -138,6 +138,7 @@ interface OrderQuoter { // all reactors check expiry before anything else, so old but already filled orders will return as expired // so this function takes orders in expired state and double checks them async function checkTerminalStates( + provider: StaticJsonRpcProvider, nonceManager: NonceManager, orders: (SignedUniswapXOrder | SignedRelayOrder)[], validations: OrderValidation[] @@ -155,9 +156,15 @@ async function checkTerminalStates( order.order.info.nonce ); return cancelled ? OrderValidation.NonceUsed : OrderValidation.Expired; - } else { - return validation; } + // if the order has block overrides AND order validation is OK, it is invalid if current block number is < block override + else if (order.order.blockOverrides && order.order.blockOverrides.number && validation === OrderValidation.OK) { + const blockNumber = await provider.getBlockNumber(); + if (blockNumber < parseInt(order.order.blockOverrides.number, 16)) { + return OrderValidation.OrderNotFillableYet; + } + } + return validation; }) ); } @@ -171,7 +178,7 @@ export class UniswapXOrderQuoter protected quoter: OrderQuoterContract; constructor( - protected provider: BaseProvider, + protected provider: StaticJsonRpcProvider, protected chainId: number, orderQuoterAddress?: string ) { @@ -194,7 +201,10 @@ export class UniswapXOrderQuoter async quoteBatch( orders: SignedUniswapXOrder[] ): Promise { - const results = await this.getMulticallResults("quote", orders); + const results = await this.getMulticallResults( + "quote", + orders + ); const validations = await this.getValidations(orders, results); const quotes: (ResolvedUniswapXOrder | undefined)[] = results.map( @@ -256,6 +266,7 @@ export class UniswapXOrderQuoter }); return await checkTerminalStates( + this.provider, new NonceManager( this.provider, this.chainId, @@ -267,20 +278,39 @@ export class UniswapXOrderQuoter } /// Get the results of a multicall for a given function + /// Each order with a blockOverride is multicalled separately private async getMulticallResults( functionName: string, orders: SignedOrder[] ): Promise { - const calls = orders.map((order) => { - return [order.order.serialize(), order.signature]; + const ordersWithBlockOverrides = orders.filter((order) => order.order.blockOverrides); + const promises = []; + ordersWithBlockOverrides.map((order) => { + promises.push( + multicallSameContractManyFunctions(this.provider, { + address: this.quoter.address, + contractInterface: this.quoter.interface, + functionName: functionName, + functionParams: [[order.order.serialize(), order.signature]], + }, undefined, order.order.blockOverrides) + ) }); - return await multicallSameContractManyFunctions(this.provider, { + const ordersWithoutBlockOverrides = orders.filter((order) => !order.order.blockOverrides); + + const calls = ordersWithoutBlockOverrides.map((order) => { + return [order.order.serialize(), order.signature]; + }); + + promises.push(multicallSameContractManyFunctions(this.provider, { address: this.quoter.address, contractInterface: this.quoter.interface, functionName: functionName, functionParams: calls, - }); + })); + + const results = await Promise.all(promises); + return results.flat(); } get orderQuoterAddress(): string { @@ -298,7 +328,7 @@ export class RelayOrderQuoter private quoteFunctionSelector = "0x3f62192e"; // function execute((bytes, bytes)) constructor( - protected provider: BaseProvider, + protected provider: StaticJsonRpcProvider, protected chainId: number, reactorAddress?: string ) { @@ -352,25 +382,51 @@ export class RelayOrderQuoter } /// Get the results of a multicall for a given function + /// Each order with a blockOverride is multicalled separately private async getMulticallResults( functionName: string, orders: SignedRelayOrder[] ): Promise { - const calls = orders.map((order) => { + const ordersWithBlockOverrides = orders.filter((order) => order.order.blockOverrides); + const promises = []; + ordersWithBlockOverrides.map((order) => { + promises.push( + multicallSameContractManyFunctions(this.provider, { + address: this.quoter.address, + contractInterface: this.quoter.interface, + functionName: functionName, + functionParams: [ + [ + { + order: order.order.serialize(), + sig: order.signature, + }, + ], + ], + }, undefined, order.order.blockOverrides) + ) + }); + + const ordersWithoutBlockOverrides = orders.filter((order) => !order.order.blockOverrides); + + const calls = ordersWithoutBlockOverrides.map((order) => { return [ { - order: order.order.serialize(), + order: order.order.serialize(), sig: order.signature, }, ]; }); - - return await multicallSameContractManyFunctions(this.provider, { + + promises.push(multicallSameContractManyFunctions(this.provider, { address: this.quoter.address, contractInterface: this.quoter.interface, functionName: functionName, functionParams: calls, - }); + })); + + const results = await Promise.all(promises); + return results.flat(); } private async getValidations( @@ -401,6 +457,7 @@ export class RelayOrderQuoter }); return await checkTerminalStates( + this.provider, new NonceManager( this.provider, this.chainId, diff --git a/sdks/uniswapx-sdk/src/utils/multicall.ts b/sdks/uniswapx-sdk/src/utils/multicall.ts index 456145632..1797d495e 100644 --- a/sdks/uniswapx-sdk/src/utils/multicall.ts +++ b/sdks/uniswapx-sdk/src/utils/multicall.ts @@ -3,12 +3,14 @@ import { Interface } from "@ethersproject/abi"; import { hexConcat } from "@ethersproject/bytes"; -import { BaseProvider } from "@ethersproject/providers"; +import { StaticJsonRpcProvider } from "@ethersproject/providers"; +import { ethers } from "ethers"; import deploylessMulticall2Abi from "../../abis/deploylessMulticall2.json"; import multicall2Abi from "../../abis/multicall2.json"; import { multicallAddressOn } from "../constants"; import { Multicall2__factory } from "../contracts"; +import { BlockOverrides } from "../order"; const DEPLOYLESS_MULTICALL_BYTECODE = "0x608060405234801561001057600080fd5b5060405161087538038061087583398181016040528101906100329190610666565b6000815167ffffffffffffffff81111561004f5761004e610358565b5b60405190808252806020026020018201604052801561008857816020015b6100756102da565b81526020019060019003908161006d5790505b50905060005b82518110156101d3576000808483815181106100ad576100ac6106c2565b5b60200260200101516000015173ffffffffffffffffffffffffffffffffffffffff168584815181106100e2576100e16106c2565b5b6020026020010151602001516040516100fb9190610738565b6000604051808303816000865af19150503d8060008114610138576040519150601f19603f3d011682016040523d82523d6000602084013e61013d565b606091505b509150915085156101895781610188576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161017f906107d2565b60405180910390fd5b5b60405180604001604052808315158152602001828152508484815181106101b3576101b26106c2565b5b6020026020010181905250505080806101cb9061082b565b91505061008e565b50602081516040028260405103030160408160405103036001835111156102535760005b8351811015610251578060200260208501018160200260400183018261021f57855160200281525b6000831115610244576020808303510151602083510151038060208303510180835250505b50506001810190506101f7565b505b60005b8351811015610281578060200260208501018051516040602083510151035250600181019050610256565b5060005b83518110156102ae57806020026020850101604060208083510151035250600181019050610285565b506001835114156102cb5760208301604082018451602002815250505b60208152825160208201528181f35b6040518060400160405280600015158152602001606081525090565b6000604051905090565b600080fd5b600080fd5b60008115159050919050565b61031f8161030a565b811461032a57600080fd5b50565b60008151905061033c81610316565b92915050565b600080fd5b6000601f19601f8301169050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b61039082610347565b810181811067ffffffffffffffff821117156103af576103ae610358565b5b80604052505050565b60006103c26102f6565b90506103ce8282610387565b919050565b600067ffffffffffffffff8211156103ee576103ed610358565b5b602082029050602081019050919050565b600080fd5b600080fd5b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006104398261040e565b9050919050565b6104498161042e565b811461045457600080fd5b50565b60008151905061046681610440565b92915050565b600080fd5b600067ffffffffffffffff82111561048c5761048b610358565b5b61049582610347565b9050602081019050919050565b60005b838110156104c05780820151818401526020810190506104a5565b838111156104cf576000848401525b50505050565b60006104e86104e384610471565b6103b8565b9050828152602081018484840111156105045761050361046c565b5b61050f8482856104a2565b509392505050565b600082601f83011261052c5761052b610342565b5b815161053c8482602086016104d5565b91505092915050565b60006040828403121561055b5761055a610404565b5b61056560406103b8565b9050600061057584828501610457565b600083015250602082015167ffffffffffffffff81111561059957610598610409565b5b6105a584828501610517565b60208301525092915050565b60006105c46105bf846103d3565b6103b8565b905080838252602082019050602084028301858111156105e7576105e66103ff565b5b835b8181101561062e57805167ffffffffffffffff81111561060c5761060b610342565b5b8086016106198982610545565b855260208501945050506020810190506105e9565b5050509392505050565b600082601f83011261064d5761064c610342565b5b815161065d8482602086016105b1565b91505092915050565b6000806040838503121561067d5761067c610300565b5b600061068b8582860161032d565b925050602083015167ffffffffffffffff8111156106ac576106ab610305565b5b6106b885828601610638565b9150509250929050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b600081519050919050565b600081905092915050565b6000610712826106f1565b61071c81856106fc565b935061072c8185602086016104a2565b80840191505092915050565b60006107448284610707565b915081905092915050565b600082825260208201905092915050565b7f4d756c746963616c6c32206167677265676174653a2063616c6c206661696c6560008201527f6400000000000000000000000000000000000000000000000000000000000000602082015250565b60006107bc60218361074f565b91506107c782610760565b604082019050919050565b600060208201905081810360008301526107eb816107af565b9050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000819050919050565b600061083682610821565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff821415610869576108686107f2565b5b60018201905091905056fe"; @@ -45,8 +47,13 @@ export async function multicallSameContractManyFunctions< // eslint-disable-next-line TFunctionParams extends any[] | undefined >( - provider: BaseProvider, - params: MulticallSameContractParams + provider: StaticJsonRpcProvider, + params: MulticallSameContractParams, + stateOverrrides?: { + code?: string; + state?: any + }, + blockOverrides?: BlockOverrides ): Promise { const { address, contractInterface, functionName, functionParams } = params; @@ -63,15 +70,20 @@ export async function multicallSameContractManyFunctions< }; }); - return multicall(provider, calls); + return multicall(provider, calls, stateOverrrides, blockOverrides); } export async function multicallSameFunctionManyContracts< // eslint-disable-next-line TFunctionParams extends any[] | undefined >( - provider: BaseProvider, - params: MulticallSameFunctionParams + provider: StaticJsonRpcProvider, + params: MulticallSameFunctionParams, + stateOverrrides?: { + code?: string; + state?: any + }, + blockOverrides?: BlockOverrides ): Promise { const { addresses, contractInterface, functionName, functionParam } = params; @@ -87,28 +99,54 @@ export async function multicallSameFunctionManyContracts< }; }); - return multicall(provider, calls); + return multicall(provider, calls, stateOverrrides, blockOverrides); } export async function multicall( - provider: BaseProvider, - calls: Call[] + provider: StaticJsonRpcProvider, + calls: Call[], + stateOverrides?: { + code?: string; + state?: any + }, + blockOverrides?: BlockOverrides ): Promise { const chainId = (await provider.getNetwork()).chainId const code = await provider.getCode(multicallAddressOn(chainId)); + let response; if (code.length > 2) { const multicall = Multicall2__factory.connect(multicallAddressOn(chainId), provider); - return await multicall.callStatic.tryAggregate(false, calls); + const params: any[] = [ + { + from: ethers.constants.AddressZero, + to: multicall.address, + data: multicall.interface.encodeFunctionData("tryAggregate", [false, calls]), + }, + 'latest', + (stateOverrides ? stateOverrides : {}), + ] + if(blockOverrides) params.push(blockOverrides); + + response = await provider.send("eth_call", params); } else { const deploylessInterface = new Interface(deploylessMulticall2Abi); const args = deploylessInterface.encodeDeploy([false, calls]); const data = hexConcat([DEPLOYLESS_MULTICALL_BYTECODE, args]); - const response = await provider.call({ - data, - }); - const multicallInterface = new Interface(multicall2Abi); + const params: any[] = [ + { + from: ethers.constants.AddressZero, + to: ethers.constants.AddressZero, + data, + }, + 'latest', + (stateOverrides ? stateOverrides : {}), + ] + if(blockOverrides) params.push(blockOverrides); + + response = await provider.send("eth_call", params); + } + const multicallInterface = new Interface(multicall2Abi); return multicallInterface.decodeFunctionResult("tryAggregate", response) .returnData; - } } diff --git a/yarn.lock b/yarn.lock index 2cf285935..10167627c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17883,6 +17883,7 @@ __metadata: "@types/node": ^18.7.16 "@uniswap/sdk-core": ^5.0.0 chai: ^4.3.6 + dotenv: ^16.0.3 ethers: ^5.7.0 hardhat: ^2.14.0 husky: ^8.0.3