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

support transfer with auth #1341

Open
wants to merge 8 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
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
Loading