-
Notifications
You must be signed in to change notification settings - Fork 43
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
b487bac
commit 4ff0ce4
Showing
6 changed files
with
428 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | ||
}; | ||
} |
Oops, something went wrong.