From d043df0e1f37dd4c5699e9e228cb2c4e1729eab3 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Sat, 28 Dec 2024 08:50:16 +0100 Subject: [PATCH] feat: base handler for `POST /relay` (#1345) * refactor: deposit and swap types * feat: add base /relay handler --- api/_dexes/utils.ts | 55 +++++++++- api/_permit.ts | 18 ---- api/_spoke-pool-periphery.ts | 44 +------- api/_utils.ts | 8 +- api/relay/_utils.ts | 175 ++++++++++++++++++++++++++++++++ api/relay/index.ts | 65 ++++++++++++ api/swap/_utils.ts | 14 +++ api/swap/approval/_utils.ts | 84 ++++++++------- api/swap/permit/_utils.ts | 115 +++++++++++++-------- api/swap/permit/index.ts | 12 ++- scripts/tests/_swap-utils.ts | 43 +------- scripts/tests/swap-allowance.ts | 44 +++++++- scripts/tests/swap-permit.ts | 42 +++++++- 13 files changed, 521 insertions(+), 198 deletions(-) create mode 100644 api/relay/_utils.ts create mode 100644 api/relay/index.ts diff --git a/api/_dexes/utils.ts b/api/_dexes/utils.ts index f8d0675d6..1546c9e61 100644 --- a/api/_dexes/utils.ts +++ b/api/_dexes/utils.ts @@ -1,4 +1,4 @@ -import { BigNumber, constants } from "ethers"; +import { BigNumber, BigNumberish, constants } from "ethers"; import { utils } from "@across-protocol/sdk"; import { SpokePool } from "@across-protocol/contracts/dist/typechain"; @@ -24,7 +24,9 @@ import { isOutputTokenBridgeable, getSpokePool, } from "../_utils"; - +import { GAS_SPONSOR_ADDRESS } from "../relay/_utils"; +import { SpokePoolV3PeripheryInterface } from "../_typechain/SpokePoolV3Periphery"; +import { TransferType } from "../_spoke-pool-periphery"; export type CrossSwapType = (typeof CROSS_SWAP_TYPE)[keyof typeof CROSS_SWAP_TYPE]; @@ -195,7 +197,11 @@ export function getFallbackRecipient(crossSwap: CrossSwap) { } export async function extractDepositDataStruct( - crossSwapQuotes: CrossSwapQuotes + crossSwapQuotes: CrossSwapQuotes, + submissionFees?: { + amount: BigNumberish; + recipient: string; + } ) { const originChainId = crossSwapQuotes.crossSwap.inputToken.chainId; const destinationChainId = crossSwapQuotes.crossSwap.outputToken.chainId; @@ -204,7 +210,7 @@ export async function extractDepositDataStruct( const refundAddress = crossSwapQuotes.crossSwap.refundAddress ?? crossSwapQuotes.crossSwap.depositor; - const deposit = { + const baseDepositData = { depositor: crossSwapQuotes.crossSwap.refundOnOrigin ? refundAddress : crossSwapQuotes.crossSwap.depositor, @@ -226,7 +232,46 @@ export async function extractDepositDataStruct( crossSwapQuotes.bridgeQuote.suggestedFees.exclusivityDeadline, message, }; - return deposit; + return { + inputAmount: baseDepositData.inputAmount, + baseDepositData, + submissionFees: submissionFees || { + amount: "0", + recipient: GAS_SPONSOR_ADDRESS, + }, + }; +} + +export async function extractSwapAndDepositDataStruct( + crossSwapQuotes: CrossSwapQuotes, + submissionFees?: { + amount: BigNumberish; + recipient: string; + } +): Promise { + const { originSwapQuote, contracts } = crossSwapQuotes; + const { originRouter } = contracts; + if (!originSwapQuote || !originRouter) { + throw new Error( + "Can not extract 'SwapAndDepositDataStruct' without originSwapQuote and originRouter" + ); + } + + const { baseDepositData, submissionFees: _submissionFees } = + await extractDepositDataStruct(crossSwapQuotes, submissionFees); + return { + submissionFees: submissionFees || _submissionFees, + depositData: baseDepositData, + swapToken: originSwapQuote.tokenIn.address, + swapTokenAmount: originSwapQuote.maximumAmountIn, + minExpectedInputTokenAmount: originSwapQuote.minAmountOut, + routerCalldata: originSwapQuote.swapTx.data, + exchange: originRouter.address, + transferType: + originRouter.name === "UniswapV3UniversalRouter" + ? TransferType.Transfer + : TransferType.Approval, + }; } async function getFillDeadline(spokePool: SpokePool): Promise { diff --git a/api/_permit.ts b/api/_permit.ts index f03f3f799..9f2521fcc 100644 --- a/api/_permit.ts +++ b/api/_permit.ts @@ -87,24 +87,6 @@ export async function getPermitTypedData(params: { domainSeparator, eip712: { types: { - EIP712Domain: [ - { - name: "name", - type: "string", - }, - { - name: "version", - type: "string", - }, - { - name: "chainId", - type: "uint256", - }, - { - name: "verifyingContract", - type: "address", - }, - ], Permit: [ { name: "owner", diff --git a/api/_spoke-pool-periphery.ts b/api/_spoke-pool-periphery.ts index a399f75ca..58928c5eb 100644 --- a/api/_spoke-pool-periphery.ts +++ b/api/_spoke-pool-periphery.ts @@ -1,28 +1,9 @@ -import { BigNumber } from "ethers"; -import { extractDepositDataStruct } from "./_dexes/utils"; import { SpokePoolPeripheryProxy__factory } from "./_typechain/factories/SpokePoolPeripheryProxy__factory"; import { SpokePoolV3Periphery__factory } from "./_typechain/factories/SpokePoolV3Periphery__factory"; import { ENABLED_ROUTES, getProvider } from "./_utils"; +import { SpokePoolV3PeripheryInterface } from "./_typechain/SpokePoolV3Periphery"; const sharedEIP712Types = { - EIP712Domain: [ - { - name: "name", - type: "string", - }, - { - name: "version", - type: "string", - }, - { - name: "chainId", - type: "uint256", - }, - { - name: "verifyingContract", - type: "address", - }, - ], Fees: [ { name: "amount", @@ -133,14 +114,7 @@ export function getSpokePoolPeripheryProxy(address: string, chainId: number) { } export async function getDepositTypedData(params: { - depositData: { - submissionFees: { - amount: BigNumber; - recipient: string; - }; - baseDepositData: Awaited>; - inputAmount: BigNumber; - }; + depositData: SpokePoolV3PeripheryInterface.DepositDataStruct; chainId: number; }) { const spokePoolPeriphery = getSpokePoolPeriphery( @@ -185,19 +159,7 @@ export async function getDepositTypedData(params: { } export async function getSwapAndDepositTypedData(params: { - swapAndDepositData: { - submissionFees: { - amount: BigNumber; - recipient: string; - }; - depositData: Awaited>; - swapToken: string; - exchange: string; - transferType: TransferType; - swapTokenAmount: BigNumber; - minExpectedInputTokenAmount: BigNumber; - routerCalldata: string; - }; + swapAndDepositData: SpokePoolV3PeripheryInterface.SwapAndDepositDataStruct; chainId: number; }) { const spokePoolPeriphery = getSpokePoolPeriphery( diff --git a/api/_utils.ts b/api/_utils.ts index 1fe12c9ea..c271413f6 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -1487,7 +1487,7 @@ export function validAddressOrENS() { export function positiveIntStr() { return define("positiveIntStr", (value) => { - return Number.isInteger(Number(value)) && Number(value) > 0; + return Number.isInteger(Number(value)) && Number(value) >= 0; }); } @@ -1503,6 +1503,12 @@ export function boolStr() { }); } +export function hexString() { + return define("hexString", (value) => { + return utils.isHexString(value); + }); +} + /** * Returns the cushion for a given token symbol and route. If no route is specified, the cushion for the token symbol * @param symbol The token symbol diff --git a/api/relay/_utils.ts b/api/relay/_utils.ts new file mode 100644 index 000000000..f46783d63 --- /dev/null +++ b/api/relay/_utils.ts @@ -0,0 +1,175 @@ +import { assert, Infer, type } from "superstruct"; +import { utils } from "ethers"; + +import { hexString, positiveIntStr, validAddress } from "../_utils"; +import { getPermitTypedData } from "../_permit"; +import { InvalidParamError } from "../_errors"; +import { + getDepositTypedData, + getSwapAndDepositTypedData, +} from "../_spoke-pool-periphery"; + +export const GAS_SPONSOR_ADDRESS = "0x0000000000000000000000000000000000000000"; + +const SubmissionFeesSchema = type({ + amount: positiveIntStr(), + recipient: validAddress(), +}); + +const BaseDepositDataSchema = type({ + inputToken: validAddress(), + outputToken: validAddress(), + outputAmount: positiveIntStr(), + depositor: validAddress(), + recipient: validAddress(), + destinationChainId: positiveIntStr(), + exclusiveRelayer: validAddress(), + quoteTimestamp: positiveIntStr(), + fillDeadline: positiveIntStr(), + exclusivityParameter: positiveIntStr(), + message: hexString(), +}); + +const SwapAndDepositDataSchema = type({ + submissionFees: SubmissionFeesSchema, + depositData: BaseDepositDataSchema, + swapToken: validAddress(), + exchange: validAddress(), + transferType: positiveIntStr(), + swapTokenAmount: positiveIntStr(), + minExpectedInputTokenAmount: positiveIntStr(), + routerCalldata: hexString(), +}); + +export const DepositWithPermitArgsSchema = type({ + signatureOwner: validAddress(), + depositData: type({ + submissionFees: SubmissionFeesSchema, + baseDepositData: BaseDepositDataSchema, + inputAmount: positiveIntStr(), + }), + deadline: positiveIntStr(), +}); + +export const SwapAndDepositWithPermitArgsSchema = type({ + signatureOwner: validAddress(), + swapAndDepositData: SwapAndDepositDataSchema, + deadline: positiveIntStr(), +}); + +export const allowedMethodNames = [ + "depositWithPermit", + "swapAndBridgeWithPermit", +]; + +export function validateMethodArgs(methodName: string, args: any) { + if (methodName === "depositWithPermit") { + assert(args, DepositWithPermitArgsSchema); + return { + args: args as Infer, + methodName, + } as const; + } else if (methodName === "swapAndBridgeWithPermit") { + assert(args, SwapAndDepositWithPermitArgsSchema); + return { + args: args as Infer, + methodName, + } as const; + } + throw new Error(`Invalid method name: ${methodName}`); +} + +export async function verifySignatures(params: { + methodNameAndArgs: ReturnType; + signatures: { + permit: string; + deposit: string; + }; + originChainId: number; + entryPointContractAddress: string; +}) { + const { + methodNameAndArgs, + signatures, + originChainId, + entryPointContractAddress, + } = params; + const { methodName, args } = methodNameAndArgs; + + let signatureOwner: string; + let getPermitTypedDataPromise: ReturnType; + let getDepositTypedDataPromise: ReturnType< + typeof getDepositTypedData | typeof getSwapAndDepositTypedData + >; + + if (methodName === "depositWithPermit") { + const { signatureOwner: _signatureOwner, deadline, depositData } = args; + signatureOwner = _signatureOwner; + getPermitTypedDataPromise = getPermitTypedData({ + tokenAddress: depositData.baseDepositData.inputToken, + chainId: originChainId, + ownerAddress: signatureOwner, + spenderAddress: entryPointContractAddress, + value: depositData.inputAmount, + deadline: Number(deadline), + }); + getDepositTypedDataPromise = getDepositTypedData({ + chainId: originChainId, + depositData, + }); + } else if (methodName === "swapAndBridgeWithPermit") { + const { + signatureOwner: _signatureOwner, + deadline, + swapAndDepositData, + } = args; + signatureOwner = _signatureOwner; + getPermitTypedDataPromise = getPermitTypedData({ + tokenAddress: swapAndDepositData.swapToken, + chainId: originChainId, + ownerAddress: signatureOwner, + spenderAddress: entryPointContractAddress, + value: swapAndDepositData.swapTokenAmount, + deadline: Number(deadline), + }); + getDepositTypedDataPromise = getSwapAndDepositTypedData({ + chainId: originChainId, + swapAndDepositData, + }); + } else { + throw new Error( + `Can not verify signatures for invalid method name: ${methodName}` + ); + } + + const [permitTypedData, depositTypedData] = await Promise.all([ + getPermitTypedDataPromise, + getDepositTypedDataPromise, + ]); + + const recoveredPermitSignerAddress = utils.verifyTypedData( + permitTypedData.eip712.domain, + permitTypedData.eip712.types, + permitTypedData.eip712.message, + signatures.permit + ); + if (recoveredPermitSignerAddress !== signatureOwner) { + throw new InvalidParamError({ + message: "Invalid permit signature", + param: "signatures.permit", + }); + } + + const recoveredDepositSignerAddress = utils.verifyTypedData( + depositTypedData.eip712.domain, + depositTypedData.eip712.types, + depositTypedData.eip712.message, + signatures.deposit + ); + if (recoveredDepositSignerAddress !== signatureOwner) { + throw new InvalidParamError({ + message: "Invalid deposit signature", + param: "signatures.deposit", + }); + } +} diff --git a/api/relay/index.ts b/api/relay/index.ts new file mode 100644 index 000000000..1716a2acc --- /dev/null +++ b/api/relay/index.ts @@ -0,0 +1,65 @@ +import { VercelRequest, VercelResponse } from "@vercel/node"; +import { object, number, assert, enums } from "superstruct"; + +import { handleErrorCondition } from "../_errors"; +import { getLogger, hexString, validAddress } from "../_utils"; +import { + allowedMethodNames, + validateMethodArgs, + verifySignatures, +} from "./_utils"; + +const BaseRelayRequestBodySchema = object({ + chainId: number(), + to: validAddress(), + methodName: enums(allowedMethodNames), + argsWithoutSignatures: object(), + signatures: object({ + permit: hexString(), + deposit: hexString(), + }), +}); + +export default async function handler( + request: VercelRequest, + response: VercelResponse +) { + const logger = getLogger(); + logger.debug({ + at: "Relay", + message: "Request body", + body: request.body, + }); + + try { + if (request.method !== "POST") { + return response.status(405).json({ error: "Method not allowed" }); + } + + assert(request.body, BaseRelayRequestBodySchema); + + // Validate method-specific request body + const methodNameAndArgs = validateMethodArgs( + request.body.methodName, + request.body.argsWithoutSignatures + ); + + // Verify signatures + const { signatures } = request.body; + await verifySignatures({ + methodNameAndArgs, + signatures, + originChainId: request.body.chainId, + entryPointContractAddress: request.body.to, + }); + + // TODO: Execute transaction based on configured strategies + + return response.status(200).json({ + success: true, + // Add relevant response data + }); + } catch (error) { + return handleErrorCondition("api/relay", response, logger, error); + } +} diff --git a/api/swap/_utils.ts b/api/swap/_utils.ts index 055b7093d..bc5eda0ba 100644 --- a/api/swap/_utils.ts +++ b/api/swap/_utils.ts @@ -236,3 +236,17 @@ export async function calculateCrossSwapFees( destinationSwapFees, }; } + +export function stringifyBigNumProps(value: object): object { + return Object.fromEntries( + Object.entries(value).map(([key, value]) => { + if (value instanceof BigNumber) { + return [key, value.toString()]; + } + if (typeof value === "object" && value !== null) { + return [key, stringifyBigNumProps(value)]; + } + return [key, value]; + }) + ); +} diff --git a/api/swap/approval/_utils.ts b/api/swap/approval/_utils.ts index da8b19800..15f0715f7 100644 --- a/api/swap/approval/_utils.ts +++ b/api/swap/approval/_utils.ts @@ -6,9 +6,11 @@ import { getSpokePool } from "../../_utils"; import { getSpokePoolPeriphery, getSpokePoolPeripheryProxy, - TransferType, } from "../../_spoke-pool-periphery"; -import { extractDepositDataStruct } from "../../_dexes/utils"; +import { + extractDepositDataStruct, + extractSwapAndDepositDataStruct, +} from "../../_dexes/utils"; import { getUniversalSwapAndBridge } from "../../_swap-and-bridge"; export async function buildCrossSwapTxForAllowanceHolder( @@ -19,8 +21,6 @@ export async function buildCrossSwapTxForAllowanceHolder( const { originSwapEntryPoint, originRouter, depositEntryPoint } = contracts; const originChainId = crossSwap.inputToken.chainId; - const deposit = await extractDepositDataStruct(crossSwapQuotes); - let tx: PopulatedTransaction; let toAddress: string; @@ -31,25 +31,17 @@ export async function buildCrossSwapTxForAllowanceHolder( `'originSwapEntryPoint' and 'originRouter' need to be defined for origin swap quotes` ); } + + const swapAndDepositData = + await extractSwapAndDepositDataStruct(crossSwapQuotes); + if (originSwapEntryPoint.name === "SpokePoolPeripheryProxy") { const spokePoolPeripheryProxy = getSpokePoolPeripheryProxy( originSwapEntryPoint.address, originChainId ); tx = await spokePoolPeripheryProxy.populateTransaction.swapAndBridge( - { - submissionFees: { - amount: 0, - recipient: crossSwap.depositor, - }, - depositData: deposit, - swapToken: originSwapQuote.tokenIn.address, - exchange: originRouter.address, - transferType: TransferType.Approval, - swapTokenAmount: originSwapQuote.maximumAmountIn, - minExpectedInputTokenAmount: originSwapQuote.minAmountOut, - routerCalldata: originSwapQuote.swapTx.data, - } + swapAndDepositData // TODO: Add payable modifier to SpokePoolPeripheryProxy swapAndBridge function // { // value: crossSwap.isInputNative ? originSwapQuote.maximumAmountIn : 0, @@ -68,9 +60,11 @@ export async function buildCrossSwapTxForAllowanceHolder( originSwapQuote.maximumAmountIn, originSwapQuote.minAmountOut, { - ...deposit, + ...swapAndDepositData.depositData, + exclusivityDeadline: + swapAndDepositData.depositData.exclusivityParameter, // Typo in the contract - destinationChainid: deposit.destinationChainId, + destinationChainid: swapAndDepositData.depositData.destinationChainId, } ); toAddress = universalSwapAndBridge.address; @@ -88,26 +82,28 @@ export async function buildCrossSwapTxForAllowanceHolder( ); } + const { baseDepositData } = await extractDepositDataStruct(crossSwapQuotes); + if (depositEntryPoint.name === "SpokePoolPeriphery") { const spokePoolPeriphery = getSpokePoolPeriphery( depositEntryPoint.address, originChainId ); tx = await spokePoolPeriphery.populateTransaction.deposit( - deposit.recipient, - deposit.inputToken, - // deposit.outputToken, // TODO: allow for output token in periphery contract - deposit.inputAmount, - deposit.outputAmount, - deposit.destinationChainId, - deposit.exclusiveRelayer, - deposit.quoteTimestamp, - deposit.fillDeadline, - deposit.exclusivityDeadline, - deposit.message, + baseDepositData.recipient, + baseDepositData.inputToken, + // baseDepositData.outputToken, // TODO: allow for output token in periphery contract + baseDepositData.inputAmount, + baseDepositData.outputAmount, + baseDepositData.destinationChainId, + baseDepositData.exclusiveRelayer, + baseDepositData.quoteTimestamp, + baseDepositData.fillDeadline, + baseDepositData.exclusivityDeadline, + baseDepositData.message, { value: crossSwapQuotes.crossSwap.isInputNative - ? deposit.inputAmount + ? baseDepositData.inputAmount : 0, } ); @@ -115,21 +111,21 @@ export async function buildCrossSwapTxForAllowanceHolder( } else if (depositEntryPoint.name === "SpokePool") { const spokePool = getSpokePool(originChainId); tx = await spokePool.populateTransaction.depositV3( - deposit.depositor, - deposit.recipient, - deposit.inputToken, - deposit.outputToken, - deposit.inputAmount, - deposit.outputAmount, - deposit.destinationChainId, - deposit.exclusiveRelayer, - deposit.quoteTimestamp, - deposit.fillDeadline, - deposit.exclusivityDeadline, - deposit.message, + baseDepositData.depositor, + baseDepositData.recipient, + baseDepositData.inputToken, + baseDepositData.outputToken, + baseDepositData.inputAmount, + baseDepositData.outputAmount, + baseDepositData.destinationChainId, + baseDepositData.exclusiveRelayer, + baseDepositData.quoteTimestamp, + baseDepositData.fillDeadline, + baseDepositData.exclusivityDeadline, + baseDepositData.message, { value: crossSwapQuotes.crossSwap.isInputNative - ? deposit.inputAmount + ? baseDepositData.inputAmount : 0, } ); diff --git a/api/swap/permit/_utils.ts b/api/swap/permit/_utils.ts index 49a231945..d1bb441e1 100644 --- a/api/swap/permit/_utils.ts +++ b/api/swap/permit/_utils.ts @@ -1,3 +1,5 @@ +import { BigNumberish } from "ethers"; + import { CrossSwapQuotes, DepositEntryPointContract, @@ -7,29 +9,54 @@ import { getPermitTypedData } from "../../_permit"; import { getDepositTypedData, getSwapAndDepositTypedData, - TransferType, } from "../../_spoke-pool-periphery"; -import { extractDepositDataStruct } from "../../_dexes/utils"; -import { BigNumber } from "ethers"; +import { + extractDepositDataStruct, + extractSwapAndDepositDataStruct, +} from "../../_dexes/utils"; +import { SpokePoolV3PeripheryInterface } from "../../_typechain/SpokePoolV3Periphery"; +import { stringifyBigNumProps } from "../_utils"; -export async function buildPermitTxPayload( - crossSwapQuotes: CrossSwapQuotes, - permitDeadline: number -) { +export async function buildPermitTxPayload({ + crossSwapQuotes, + permitDeadline, + submissionFees, +}: { + crossSwapQuotes: CrossSwapQuotes; + permitDeadline: number; + submissionFees?: { + amount: BigNumberish; + recipient: string; + }; +}) { const { originSwapQuote, bridgeQuote, crossSwap, contracts } = crossSwapQuotes; const originChainId = crossSwap.inputToken.chainId; const { originSwapEntryPoint, depositEntryPoint, originRouter } = contracts; - const baseDepositData = await extractDepositDataStruct(crossSwapQuotes); - let entryPointContract: | DepositEntryPointContract | OriginSwapEntryPointContract; let getDepositTypedDataPromise: | ReturnType | ReturnType; - let methodName: string; + let methodNameAndArgsWithoutSignatures: + | { + methodName: "depositWithPermit"; + argsWithoutSignatures: { + signatureOwner: string; + depositData: SpokePoolV3PeripheryInterface.DepositDataStruct; + deadline: BigNumberish; + }; + } + | { + methodName: "swapAndBridgeWithPermit"; + argsWithoutSignatures: { + signatureOwner: string; + swapAndDepositData: SpokePoolV3PeripheryInterface.SwapAndDepositDataStruct; + deadline: BigNumberish; + }; + }; if (originSwapQuote) { if (!originSwapEntryPoint) { @@ -50,28 +77,21 @@ export async function buildPermitTxPayload( ); } + const swapAndDepositData = + await extractSwapAndDepositDataStruct(crossSwapQuotes); entryPointContract = originSwapEntryPoint; getDepositTypedDataPromise = getSwapAndDepositTypedData({ - swapAndDepositData: { - // TODO: Make this dynamic - submissionFees: { - amount: BigNumber.from(0), - recipient: crossSwapQuotes.crossSwap.depositor, - }, - depositData: baseDepositData, - swapToken: originSwapQuote.tokenIn.address, - swapTokenAmount: originSwapQuote.maximumAmountIn, - minExpectedInputTokenAmount: originSwapQuote.minAmountOut, - routerCalldata: originSwapQuote.swapTx.data, - exchange: originRouter.address, - transferType: - originRouter.name === "UniswapV3UniversalRouter" - ? TransferType.Transfer - : TransferType.Approval, - }, + swapAndDepositData: swapAndDepositData, chainId: originChainId, }); - methodName = "swapAndBridgeWithPermit"; + methodNameAndArgsWithoutSignatures = { + methodName: "swapAndBridgeWithPermit", + argsWithoutSignatures: { + signatureOwner: crossSwap.depositor, + swapAndDepositData, + deadline: permitDeadline, + }, + }; } else { if (!depositEntryPoint) { throw new Error( @@ -84,21 +104,23 @@ export async function buildPermitTxPayload( `Permit is not supported for deposit entry point contract '${depositEntryPoint.name}'` ); } - + const depositDataStruct = await extractDepositDataStruct( + crossSwapQuotes, + submissionFees + ); entryPointContract = depositEntryPoint; getDepositTypedDataPromise = getDepositTypedData({ - depositData: { - // TODO: Make this dynamic - submissionFees: { - amount: BigNumber.from(0), - recipient: crossSwap.depositor, - }, - baseDepositData, - inputAmount: BigNumber.from(bridgeQuote.inputAmount), - }, + depositData: depositDataStruct, chainId: originChainId, }); - methodName = "depositWithPermit"; + methodNameAndArgsWithoutSignatures = { + methodName: "depositWithPermit", + argsWithoutSignatures: { + signatureOwner: crossSwap.depositor, + depositData: depositDataStruct, + deadline: permitDeadline, + }, + }; } const [permitTypedData, depositTypedData] = await Promise.all([ @@ -115,13 +137,22 @@ export async function buildPermitTxPayload( ]); return { eip712: { - permit: permitTypedData.eip712, - deposit: depositTypedData.eip712, + permit: { + ...permitTypedData.eip712, + message: stringifyBigNumProps(permitTypedData.eip712.message), + }, + deposit: { + ...depositTypedData.eip712, + message: stringifyBigNumProps(depositTypedData.eip712.message), + }, }, swapTx: { chainId: originChainId, to: entryPointContract.address, - methodName, + methodName: methodNameAndArgsWithoutSignatures.methodName, + argsWithoutSignatures: stringifyBigNumProps( + methodNameAndArgsWithoutSignatures.argsWithoutSignatures + ), }, }; } diff --git a/api/swap/permit/index.ts b/api/swap/permit/index.ts index bb141c6d0..3738d340d 100644 --- a/api/swap/permit/index.ts +++ b/api/swap/permit/index.ts @@ -9,6 +9,7 @@ import { getSwapRouter02Strategy } from "../../_dexes/uniswap/swap-router-02"; import { InvalidParamError } from "../../_errors"; import { buildPermitTxPayload } from "./_utils"; import { QuoteFetchStrategies } from "../../_dexes/utils"; +import { GAS_SPONSOR_ADDRESS } from "../../relay/_utils"; export const PermitSwapQueryParamsSchema = type({ permitDeadline: optional(positiveIntStr()), @@ -85,10 +86,15 @@ const handler = async ( quoteFetchStrategies ); // Build tx for permit - const crossSwapTxForPermit = await buildPermitTxPayload( + const crossSwapTxForPermit = await buildPermitTxPayload({ crossSwapQuotes, - permitDeadline - ); + permitDeadline, + // FIXME: Calculate proper fees + submissionFees: { + amount: "0", + recipient: GAS_SPONSOR_ADDRESS, + }, + }); const responseJson = crossSwapTxForPermit; diff --git a/scripts/tests/_swap-utils.ts b/scripts/tests/_swap-utils.ts index 48d0fa14f..d0b776c92 100644 --- a/scripts/tests/_swap-utils.ts +++ b/scripts/tests/_swap-utils.ts @@ -1,8 +1,7 @@ import { CHAIN_IDs, TOKEN_SYMBOLS_MAP } from "@across-protocol/constants"; -import { ethers, Wallet } from "ethers"; +import { ethers } from "ethers"; import dotenv from "dotenv"; import axios from "axios"; -import { getProvider } from "../../api/_utils"; dotenv.config(); export const { SWAP_API_BASE_URL = "http://localhost:3000" } = process.env; @@ -170,7 +169,7 @@ export function filterTestCases( return filteredTestCases; } -export async function swap(slug: "approval" | "permit") { +export async function fetchSwapQuote(slug: "approval" | "permit") { const filterString = process.argv[2]; const testCases = [...MIN_OUTPUT_CASES, ...EXACT_OUTPUT_CASES]; const filteredTestCases = filterTestCases(testCases, filterString); @@ -181,42 +180,6 @@ export async function swap(slug: "approval" | "permit") { params: testCase.params, }); console.log(response.data); - - if (process.env.DEV_WALLET_PK) { - const wallet = new Wallet(process.env.DEV_WALLET_PK!).connect( - getProvider(testCase.params.originChainId) - ); - - if (response.data.approvalTxns) { - console.log("Approval needed..."); - let step = 1; - for (const approvalTxn of response.data.approvalTxns) { - const stepLabel = `(${step}/${response.data.approvalTxns.length})`; - const tx = await wallet.sendTransaction({ - to: approvalTxn.to, - data: approvalTxn.data, - }); - console.log(`${stepLabel} Approval tx hash:`, tx.hash); - await tx.wait(); - console.log(`${stepLabel} Approval tx mined`); - step++; - } - } - - try { - const tx = await wallet.sendTransaction({ - to: response.data.swapTx.to, - data: response.data.swapTx.data, - value: response.data.swapTx.value, - gasLimit: response.data.swapTx.gas, - gasPrice: response.data.swapTx.gasPrice, - }); - console.log("Tx hash: ", tx.hash); - await tx.wait(); - console.log("Tx mined"); - } catch (e) { - console.error("Tx reverted", e); - } - } + return response.data; } } diff --git a/scripts/tests/swap-allowance.ts b/scripts/tests/swap-allowance.ts index 05162cba1..91bbdad27 100644 --- a/scripts/tests/swap-allowance.ts +++ b/scripts/tests/swap-allowance.ts @@ -1,8 +1,48 @@ -import { swap } from "./_swap-utils"; +import { Wallet } from "ethers"; + +import { getProvider } from "../../api/_utils"; +import { fetchSwapQuote } from "./_swap-utils"; async function swapWithAllowance() { console.log("Swapping with allowance..."); - await swap("approval"); + const swapQuote = await fetchSwapQuote("approval"); + + if (process.env.DEV_WALLET_PK) { + const wallet = new Wallet(process.env.DEV_WALLET_PK!).connect( + getProvider(swapQuote.params.originChainId) + ); + + if (swapQuote.approvalTxns) { + console.log("Approval needed..."); + let step = 1; + for (const approvalTxn of swapQuote.approvalTxns) { + const stepLabel = `(${step}/${swapQuote.approvalTxns.length})`; + const tx = await wallet.sendTransaction({ + to: approvalTxn.to, + data: approvalTxn.data, + }); + console.log(`${stepLabel} Approval tx hash:`, tx.hash); + await tx.wait(); + console.log(`${stepLabel} Approval tx mined`); + step++; + } + } + + try { + const tx = await wallet.sendTransaction({ + to: swapQuote.swapTx.to, + data: swapQuote.swapTx.data, + value: swapQuote.swapTx.value, + gasLimit: swapQuote.swapTx.gas, + gasPrice: swapQuote.swapTx.gasPrice, + }); + console.log("Tx hash: ", tx.hash); + await tx.wait(); + console.log("Tx mined"); + } catch (e) { + console.error("Tx reverted", e); + } + } } swapWithAllowance() diff --git a/scripts/tests/swap-permit.ts b/scripts/tests/swap-permit.ts index 29c8296eb..ecc9a9ea3 100644 --- a/scripts/tests/swap-permit.ts +++ b/scripts/tests/swap-permit.ts @@ -1,8 +1,46 @@ -import { swap } from "./_swap-utils"; +import { Wallet } from "ethers"; + +import { getProvider } from "../../api/_utils"; +import { fetchSwapQuote, SWAP_API_BASE_URL } from "./_swap-utils"; +import axios from "axios"; async function swapWithPermit() { console.log("Swapping with permit..."); - await swap("permit"); + const swapQuote = await fetchSwapQuote("permit"); + + if (process.env.DEV_WALLET_PK) { + const wallet = new Wallet(process.env.DEV_WALLET_PK!).connect( + getProvider(swapQuote.swapTx.chainId) + ); + + console.log("EIP712 Permit:", swapQuote.eip712.permit); + console.log( + "EIP712 Deposit:", + swapQuote.eip712.deposit.message.submissionFees + ); + + // sign permit + deposit + const permitSig = await wallet._signTypedData( + swapQuote.eip712.permit.domain, + swapQuote.eip712.permit.types, + swapQuote.eip712.permit.message + ); + console.log("Signed permit:", permitSig); + + const depositSig = await wallet._signTypedData( + swapQuote.eip712.deposit.domain, + swapQuote.eip712.deposit.types, + swapQuote.eip712.deposit.message + ); + console.log("Signed deposit:", depositSig); + + // relay + const relayResponse = await axios.post(`${SWAP_API_BASE_URL}/api/relay`, { + ...swapQuote.swapTx, + signatures: { permit: permitSig, deposit: depositSig }, + }); + console.log("Relay response:", relayResponse.data); + } } swapWithPermit()