Skip to content

Commit

Permalink
feat: support permit swap
Browse files Browse the repository at this point in the history
  • Loading branch information
dohaki committed Nov 21, 2024
1 parent 327bd92 commit 152e3d7
Show file tree
Hide file tree
Showing 6 changed files with 505 additions and 0 deletions.
49 changes: 49 additions & 0 deletions api/_abis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,52 @@ export const MINIMAL_MULTICALL3_ABI = [
type: "function",
},
];

export const ERC20_PERMIT_ABI = [
{
inputs: [],
stateMutability: "view",
type: "function",
name: "name",
outputs: [
{
internalType: "string",
name: "",
type: "string",
},
],
},
{
inputs: [
{
internalType: "address",
name: "owner",
type: "address",
},
],
stateMutability: "view",
type: "function",
name: "nonces",
outputs: [
{
internalType: "uint256",
name: "",
type: "uint256",
},
],
},
{
inputs: [],
name: "version",
outputs: [{ internalType: "string", name: "", type: "string" }],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "DOMAIN_SEPARATOR",
outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }],
stateMutability: "view",
type: "function",
},
];
58 changes: 58 additions & 0 deletions api/_dexes/cross-swap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
} from "./utils";
import { tagIntegratorId } from "../_integrator-id";
import { getMultiCallHandlerAddress } from "../_multicall-handler";
import { getPermitTypedData } from "../_permit";

export type CrossSwapType =
(typeof CROSS_SWAP_TYPE)[keyof typeof CROSS_SWAP_TYPE];
Expand Down Expand Up @@ -248,6 +249,63 @@ export async function buildCrossSwapTxForAllowanceHolder(
value: tx.value,
};
}

export async function getCrossSwapTxForPermit(
crossSwapQuotes: CrossSwapQuotes,
permitDeadline: number
) {
const originChainId = crossSwapQuotes.crossSwap.inputToken.chainId;
const swapAndBridge = getSwapAndBridge("uniswap", originChainId);
const deposit = await extractDepositDataStruct(crossSwapQuotes);

let methodName: string;
let argsWithoutSignature: Record<string, unknown>;
if (crossSwapQuotes.originSwapQuote) {
methodName = "swapAndBridgeWithPermit";
argsWithoutSignature = {
swapToken: crossSwapQuotes.originSwapQuote.tokenIn.address,
acrossInputToken: crossSwapQuotes.originSwapQuote.tokenOut.address,
routerCalldata: crossSwapQuotes.originSwapQuote.swapTx.data,
swapTokenAmount: crossSwapQuotes.originSwapQuote.maximumAmountIn,
minExpectedInputTokenAmount: crossSwapQuotes.originSwapQuote.minAmountOut,
depositData: deposit,
deadline: permitDeadline,
};
} else {
methodName = "depositWithPermit";
argsWithoutSignature = {
acrossInputToken: crossSwapQuotes.bridgeQuote.inputToken.address,
acrossInputAmount: crossSwapQuotes.bridgeQuote.inputAmount,
depositData: deposit,
deadline: permitDeadline,
};
}

const permitTypedData = await getPermitTypedData({
tokenAddress:
crossSwapQuotes.originSwapQuote?.tokenIn.address ||
crossSwapQuotes.bridgeQuote.inputToken.address,
chainId: originChainId,
ownerAddress: crossSwapQuotes.crossSwap.depositor,
spenderAddress: swapAndBridge.address,
value:
crossSwapQuotes.originSwapQuote?.maximumAmountIn ||
crossSwapQuotes.bridgeQuote.inputAmount,
deadline: permitDeadline,
});
return {
permit: {
eip712: permitTypedData.eip712,
},
tx: {
chainId: originChainId,
to: swapAndBridge.address,
methodName,
argsWithoutSignature,
},
};
}

async function extractDepositDataStruct(crossSwapQuotes: CrossSwapQuotes) {
const originChainId = crossSwapQuotes.crossSwap.inputToken.chainId;
const destinationChainId = crossSwapQuotes.crossSwap.outputToken.chainId;
Expand Down
144 changes: 144 additions & 0 deletions api/_permit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { BigNumberish, ethers } from "ethers";

import { getProvider } from "./_utils";
import { ERC20_PERMIT_ABI } from "./_abis";

export async function getPermitTypedData(params: {
tokenAddress: string;
chainId: number;
ownerAddress: string;
spenderAddress: string;
value: BigNumberish;
deadline: number;
eip712DomainVersion?: number;
}) {
const provider = getProvider(params.chainId);
const erc20Permit = new ethers.Contract(
params.tokenAddress,
ERC20_PERMIT_ABI,
provider
);

const [
nameResult,
versionFromContractResult,
nonceResult,
domainSeparatorResult,
] = await Promise.allSettled([
erc20Permit.name(),
erc20Permit.version(),
erc20Permit.nonces(params.ownerAddress),
erc20Permit.DOMAIN_SEPARATOR(),
]);

if (
nameResult.status === "rejected" ||
nonceResult.status === "rejected" ||
domainSeparatorResult.status === "rejected"
) {
const error =
nameResult.status === "rejected"
? nameResult.reason
: nonceResult.status === "rejected"
? nonceResult.reason
: domainSeparatorResult.status === "rejected"
? domainSeparatorResult.reason
: new Error("Unknown error");
throw new Error(`Contract ${params.tokenAddress} does not support permit`, {
cause: error,
});
}

const name = nameResult.value;
const versionFromContract =
versionFromContractResult.status === "fulfilled"
? versionFromContractResult.value
: undefined;
const nonce = nonceResult.value;
const domainSeparator = domainSeparatorResult.value;

const eip712DomainVersion = [1, 2, "1", "2"].includes(versionFromContract)
? Number(versionFromContract)
: params.eip712DomainVersion || 1;

const domainSeparatorHash = ethers.utils.keccak256(
ethers.utils.defaultAbiCoder.encode(
["bytes32", "bytes32", "bytes32", "uint256", "address"],
[
ethers.utils.id(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
),
ethers.utils.id(name),
ethers.utils.id(eip712DomainVersion.toString()),
params.chainId,
params.tokenAddress,
]
)
);

if (domainSeparator !== domainSeparatorHash) {
throw new Error("EIP712 domain separator mismatch");
}

return {
domainSeparator,
eip712: {
types: {
EIP712Domain: [
{
name: "name",
type: "string",
},
{
name: "version",
type: "string",
},
{
name: "chainId",
type: "uint256",
},
{
name: "verifyingContract",
type: "address",
},
],
Permit: [
{
name: "owner",
type: "address",
},
{
name: "spender",
type: "address",
},
{
name: "value",
type: "uint256",
},
{
name: "nonce",
type: "uint256",
},
{
name: "deadline",
type: "uint256",
},
],
},
primaryType: "Permit",
domain: {
name,
version: eip712DomainVersion.toString(),
chainId: params.chainId,
verifyingContract: params.tokenAddress,
},
message: {
owner: params.ownerAddress,
spender: params.spenderAddress,
value: String(params.value),
nonce: String(nonce),
deadline: String(params.deadline),
},
},
};
}
1 change: 1 addition & 0 deletions api/_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ export const getLogger = (): LoggingUtility => {
* @returns A valid URL of the current endpoint in vercel
*/
export const resolveVercelEndpoint = () => {
return "https://app.across.to";
const url = process.env.VERCEL_URL ?? "across.to";
const env = process.env.VERCEL_ENV ?? "development";
switch (env) {
Expand Down
41 changes: 41 additions & 0 deletions api/swap/permit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { VercelResponse } from "@vercel/node";

import { TypedVercelRequest } from "../_types";
import { getLogger, handleErrorCondition } from "../_utils";
import { getCrossSwapTxForPermit } from "../_dexes/cross-swap";
import { handleBaseSwapQueryParams, BaseSwapQueryParams } from "./_utils";

const handler = async (
request: TypedVercelRequest<BaseSwapQueryParams>,
response: VercelResponse
) => {
const logger = getLogger();
logger.debug({
at: "Swap/permit",
message: "Query data",
query: request.query,
});
try {
const { crossSwapQuotes } = await handleBaseSwapQueryParams(request);

const permitDeadline = Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 365; // 1 year

const crossSwapTxForPermit = await getCrossSwapTxForPermit(
crossSwapQuotes,
permitDeadline
);

const responseJson = crossSwapTxForPermit;

logger.debug({
at: "Swap/allowance",
message: "Response data",
responseJson,
});
response.status(200).json(responseJson);
} catch (error: unknown) {
return handleErrorCondition("swap/allowance", response, logger, error);
}
};

export default handler;
Loading

0 comments on commit 152e3d7

Please sign in to comment.