Skip to content

Commit

Permalink
support transfer with auth
Browse files Browse the repository at this point in the history
  • Loading branch information
gsteenkamp89 committed Dec 20, 2024
1 parent b487bac commit 4ff0ce4
Show file tree
Hide file tree
Showing 6 changed files with 428 additions and 1 deletion.
30 changes: 30 additions & 0 deletions api/_abis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,33 @@ export const ERC20_PERMIT_ABI = [
type: "function",
},
];

export const ERC_TRANSFER_WITH_AUTH_ABI = [
{
inputs: [],
stateMutability: "view",
type: "function",
name: "name",
outputs: [
{
internalType: "string",
name: "",
type: "string",
},
],
},
{
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",
},
];
137 changes: 137 additions & 0 deletions api/_transfer-with-auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { BigNumberish, ethers } from "ethers";
import { getProvider } from "./_utils";
import { ERC_TRANSFER_WITH_AUTH_ABI } from "./_abis";
import { utils } from "ethers";

export function hashDomainSeparator(params: {
name: string;
version: string | number;
chainId: number;
verifyingContract: string;
}): string {
return utils.keccak256(
utils.defaultAbiCoder.encode(
["bytes32", "bytes32", "bytes32", "uint256", "address"],
[
utils.id(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
),
utils.id(params.name),
utils.id(params.version.toString()),
params.chainId,
params.verifyingContract,
]
)
);
}

export async function getTransferWithAuthTypedData(params: {
tokenAddress: string;
chainId: number;
ownerAddress: string;
spenderAddress: string;
value: BigNumberish;
validBefore: number;
nonce: string;
validAfter?: number;
eip712DomainVersion?: number;
}) {
const provider = getProvider(params.chainId);

const erc20Permit = new ethers.Contract(
params.tokenAddress,
ERC_TRANSFER_WITH_AUTH_ABI,
provider
);

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

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

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

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

const domainSeparatorHash = hashDomainSeparator({
name,
version: eip712DomainVersion,
chainId: params.chainId,
verifyingContract: 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" },
],
TransferWithAuthorization: [
{ name: "from", type: "address" },
{ name: "to", type: "address" },
{ name: "value", type: "uint256" },
{ name: "validAfter", type: "uint256" },
{ name: "validBefore", type: "uint256" },
{ name: "nonce", type: "bytes32" },
],
},
domain: {
name,
version: eip712DomainVersion.toString(),
chainId: params.chainId,
verifyingContract: params.tokenAddress,
},
primaryType: "TransferWithAuthorization",
message: {
from: params.ownerAddress,
to: params.spenderAddress,
value: String(params.value),
validAfter: params?.validAfter
? convertMaybeMillisecondsToSeconds(params.validAfter)
: 0,
validBefore: convertMaybeMillisecondsToSeconds(params.validBefore),
nonce: params.nonce, // non-sequential nonce, random 32 byte hex string
},
},
};
}

export function convertMaybeMillisecondsToSeconds(timestamp: number): number {
const isMilliseconds = timestamp > 1_000_000_000; // rough approximation
return isMilliseconds ? Math.floor(timestamp / 1000) : Math.floor(timestamp);
}
133 changes: 133 additions & 0 deletions api/swap/auth/_utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import {
CrossSwapQuotes,
DepositEntryPointContract,
OriginSwapEntryPointContract,
} from "../../_dexes/types";
import { getTransferWithAuthTypedData } from "../../_transfer-with-auth";
import {
getDepositTypedData,
getSwapAndDepositTypedData,
TransferType,
} from "../../_spoke-pool-periphery";
import { extractDepositDataStruct } from "../../_dexes/utils";
import { BigNumber, utils } from "ethers";

export async function buildAuthTxPayload(
crossSwapQuotes: CrossSwapQuotes,
authDeadline: number, // maybe milliseconds
authStart = 0 // maybe milliseconds
) {
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<typeof getDepositTypedData>
| ReturnType<typeof getSwapAndDepositTypedData>;
let methodName: string;

if (originSwapQuote) {
if (!originSwapEntryPoint) {
throw new Error(
`'originSwapEntryPoint' needs to be defined for origin swap quotes`
);
}
// Only SpokePoolPeriphery supports transfer with auth
if (originSwapEntryPoint.name !== "SpokePoolPeriphery") {
throw new Error(
`Transfer with auth is not supported for origin swap entry point contract '${originSwapEntryPoint.name}'`
);
}

if (!originRouter) {
throw new Error(
`'originRouter' needs to be defined for origin swap quotes`
);
}

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,
},
chainId: originChainId,
});
methodName = "swapAndBridgeWithAuthorization";
} else {
if (!depositEntryPoint) {
throw new Error(
`'depositEntryPoint' needs to be defined for bridge quotes`
);
}

if (depositEntryPoint.name !== "SpokePoolPeriphery") {
throw new Error(
`auth is not supported for deposit entry point contract '${depositEntryPoint.name}'`
);
}

entryPointContract = depositEntryPoint;
getDepositTypedDataPromise = getDepositTypedData({
depositData: {
// TODO: Make this dynamic
submissionFees: {
amount: BigNumber.from(0),
recipient: crossSwap.depositor,
},
baseDepositData,
inputAmount: BigNumber.from(bridgeQuote.inputAmount),
},
chainId: originChainId,
});
methodName = "depositWithAuthorization";
}

// random non-sequesntial nonce
const nonce = utils.hexlify(utils.randomBytes(32));

const [authTypedData, depositTypedData] = await Promise.all([
getTransferWithAuthTypedData({
tokenAddress:
originSwapQuote?.tokenIn.address || bridgeQuote.inputToken.address,
chainId: originChainId,
ownerAddress: crossSwap.depositor,
spenderAddress: entryPointContract.address,
value: originSwapQuote?.maximumAmountIn || bridgeQuote.inputAmount,
nonce,
validAfter: authStart,
validBefore: authDeadline,
}),
getDepositTypedDataPromise,
]);
return {
eip712: {
transferWithAuthorization: authTypedData.eip712,
deposit: depositTypedData.eip712,
},
swapTx: {
chainId: originChainId,
to: entryPointContract.address,
methodName,
},
};
}
Loading

0 comments on commit 4ff0ce4

Please sign in to comment.