Skip to content

Commit

Permalink
feat: base handler for POST /relay (#1345)
Browse files Browse the repository at this point in the history
* refactor: deposit and swap types

* feat: add base /relay handler
  • Loading branch information
dohaki authored Dec 28, 2024
1 parent bf1f33e commit d043df0
Show file tree
Hide file tree
Showing 13 changed files with 521 additions and 198 deletions.
55 changes: 50 additions & 5 deletions api/_dexes/utils.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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];

Expand Down Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -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<SpokePoolV3PeripheryInterface.SwapAndDepositDataStruct> {
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<number> {
Expand Down
18 changes: 0 additions & 18 deletions api/_permit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
44 changes: 3 additions & 41 deletions api/_spoke-pool-periphery.ts
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -133,14 +114,7 @@ export function getSpokePoolPeripheryProxy(address: string, chainId: number) {
}

export async function getDepositTypedData(params: {
depositData: {
submissionFees: {
amount: BigNumber;
recipient: string;
};
baseDepositData: Awaited<ReturnType<typeof extractDepositDataStruct>>;
inputAmount: BigNumber;
};
depositData: SpokePoolV3PeripheryInterface.DepositDataStruct;
chainId: number;
}) {
const spokePoolPeriphery = getSpokePoolPeriphery(
Expand Down Expand Up @@ -185,19 +159,7 @@ export async function getDepositTypedData(params: {
}

export async function getSwapAndDepositTypedData(params: {
swapAndDepositData: {
submissionFees: {
amount: BigNumber;
recipient: string;
};
depositData: Awaited<ReturnType<typeof extractDepositDataStruct>>;
swapToken: string;
exchange: string;
transferType: TransferType;
swapTokenAmount: BigNumber;
minExpectedInputTokenAmount: BigNumber;
routerCalldata: string;
};
swapAndDepositData: SpokePoolV3PeripheryInterface.SwapAndDepositDataStruct;
chainId: number;
}) {
const spokePoolPeriphery = getSpokePoolPeriphery(
Expand Down
8 changes: 7 additions & 1 deletion api/_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1487,7 +1487,7 @@ export function validAddressOrENS() {

export function positiveIntStr() {
return define<string>("positiveIntStr", (value) => {
return Number.isInteger(Number(value)) && Number(value) > 0;
return Number.isInteger(Number(value)) && Number(value) >= 0;
});
}

Expand All @@ -1503,6 +1503,12 @@ export function boolStr() {
});
}

export function hexString() {
return define<string>("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
Expand Down
175 changes: 175 additions & 0 deletions api/relay/_utils.ts
Original file line number Diff line number Diff line change
@@ -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<typeof DepositWithPermitArgsSchema>,
methodName,
} as const;
} else if (methodName === "swapAndBridgeWithPermit") {
assert(args, SwapAndDepositWithPermitArgsSchema);
return {
args: args as Infer<typeof SwapAndDepositWithPermitArgsSchema>,
methodName,
} as const;
}
throw new Error(`Invalid method name: ${methodName}`);
}

export async function verifySignatures(params: {
methodNameAndArgs: ReturnType<typeof validateMethodArgs>;
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<typeof getPermitTypedData>;
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",
});
}
}
Loading

0 comments on commit d043df0

Please sign in to comment.