Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: base handler for POST /relay #1345

Open
wants to merge 2 commits into
base: swap-endpoint
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading