-
Notifications
You must be signed in to change notification settings - Fork 80
feat: move to single signature ERC-3009 and use deterministic depositIds for gasless deposits #1275
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
Changes from 4 commits
18b0bc3
aa3aea6
b7bf24f
7bdb32f
78cf277
95d31f2
148d643
3711c8f
2e53f80
41621c7
e17e44d
f5a81f9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -153,6 +153,10 @@ contract SpokePoolPeriphery is SpokePoolPeripheryInterface, ReentrancyGuard, Mul | |
| // Mapping from user address to their current nonce | ||
| mapping(address => uint256) public permitNonces; | ||
|
|
||
| // Witness identifiers for the bridge and swap functions. Used to ensure collisions can't happen. | ||
| bytes32 public constant BRIDGE_AND_SWAP_WITNESS_IDENTIFIER = keccak256("BridgeAndSwapWitness"); | ||
| bytes32 public constant BRIDGE_WITNESS_IDENTIFIER = keccak256("BridgeWitness"); | ||
|
|
||
| event SwapBeforeBridge( | ||
| address exchange, | ||
| bytes exchangeCalldata, | ||
|
|
@@ -286,7 +290,10 @@ contract SpokePoolPeriphery is SpokePoolPeripheryInterface, ReentrancyGuard, Mul | |
| PeripherySigningLib.hashSwapAndDepositData(swapAndDepositData), | ||
| swapAndDepositDataSignature | ||
| ); | ||
| _swapAndBridge(swapAndDepositData); | ||
| // Copy struct to memory and set nonce to originalNonce + 1 to avoid 0 (which triggers regular deposit) | ||
| SwapAndDepositData memory modifiedData = swapAndDepositData; | ||
| modifiedData.nonce = swapAndDepositData.nonce + 1; | ||
| _swapAndBridge(modifiedData); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -341,9 +348,12 @@ contract SpokePoolPeriphery is SpokePoolPeripheryInterface, ReentrancyGuard, Mul | |
| SwapAndDepositData calldata swapAndDepositData, | ||
| uint256 validAfter, | ||
| uint256 validBefore, | ||
| bytes calldata receiveWithAuthSignature, | ||
| bytes calldata swapAndDepositDataSignature | ||
| bytes calldata receiveWithAuthSignature | ||
| ) external override nonReentrant { | ||
| bytes32 witness = keccak256( | ||
| abi.encodePacked(BRIDGE_AND_SWAP_WITNESS_IDENTIFIER, abi.encode(swapAndDepositData)) | ||
| ); | ||
|
|
||
| (bytes32 r, bytes32 s, uint8 v) = PeripherySigningLib.deserializeSignature(receiveWithAuthSignature); | ||
| uint256 _submissionFeeAmount = swapAndDepositData.submissionFees.amount; | ||
| // While any contract can vacuously implement `receiveWithAuthorization` (or just have a fallback), | ||
|
|
@@ -355,7 +365,7 @@ contract SpokePoolPeriphery is SpokePoolPeripheryInterface, ReentrancyGuard, Mul | |
| swapAndDepositData.swapTokenAmount + _submissionFeeAmount, | ||
| validAfter, | ||
| validBefore, | ||
| bytes32(swapAndDepositData.nonce), | ||
| witness, | ||
| v, | ||
| r, | ||
| s | ||
|
|
@@ -368,20 +378,10 @@ contract SpokePoolPeriphery is SpokePoolPeripheryInterface, ReentrancyGuard, Mul | |
|
|
||
| // Note: No need to validate our internal nonce for receiveWithAuthorization | ||
| // as EIP-3009 has its own nonce mechanism that prevents replay attacks. | ||
| // | ||
| // Design Decision: We reuse the receiveWithAuthorization nonce for our signatures, | ||
| // but not for permit, which creates a theoretical replay attack that we think is | ||
| // incredibly unlikely because this would require: | ||
| // 1. A token implementing both ERC-2612 and ERC-3009 | ||
| // 2. A user using the same nonces for swapAndBridgeWithPermit and for swapAndBridgeWithAuthorization | ||
| // 3. Issuing these signatures within a short amount of time (limited by fillDeadlineBuffer) | ||
| // Verify that the signatureOwner signed the input swapAndDepositData. | ||
| _validateSignature( | ||
| signatureOwner, | ||
| PeripherySigningLib.hashSwapAndDepositData(swapAndDepositData), | ||
| swapAndDepositDataSignature | ||
| ); | ||
| _swapAndBridge(swapAndDepositData); | ||
| // We use the witness (which serves as the ERC-3009 nonce) as the deposit nonce. | ||
| SwapAndDepositData memory modifiedData = swapAndDepositData; | ||
| modifiedData.nonce = uint256(witness); | ||
| _swapAndBridge(modifiedData); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -413,6 +413,7 @@ contract SpokePoolPeriphery is SpokePoolPeripheryInterface, ReentrancyGuard, Mul | |
| _validateAndIncrementNonce(signatureOwner, depositData.nonce); | ||
| // Verify that the signatureOwner signed the input depositData. | ||
| _validateSignature(signatureOwner, PeripherySigningLib.hashDepositData(depositData), depositDataSignature); | ||
| // Use nonce + 1 to avoid 0 (which triggers regular deposit) and ensure uniqueness | ||
|
||
| _deposit( | ||
| depositData.spokePool, | ||
| depositData.baseDepositData.depositor, | ||
|
|
@@ -423,6 +424,7 @@ contract SpokePoolPeriphery is SpokePoolPeripheryInterface, ReentrancyGuard, Mul | |
| depositData.baseDepositData.outputAmount, | ||
| depositData.baseDepositData.destinationChainId, | ||
| depositData.baseDepositData.exclusiveRelayer, | ||
| depositData.nonce + 1, // +1 to avoid 0 (which triggers regular deposit) and ensure uniqueness | ||
|
||
| depositData.baseDepositData.quoteTimestamp, | ||
| depositData.baseDepositData.fillDeadline, | ||
| depositData.baseDepositData.exclusivityParameter, | ||
|
|
@@ -460,6 +462,7 @@ contract SpokePoolPeriphery is SpokePoolPeripheryInterface, ReentrancyGuard, Mul | |
| _submissionFeeAmount | ||
| ); | ||
|
|
||
| // User controls the nonce in permit2 flows - if 0, uses regular deposit; if non-zero, uses unsafe deposit | ||
| _deposit( | ||
| depositData.spokePool, | ||
| depositData.baseDepositData.depositor, | ||
|
|
@@ -470,6 +473,7 @@ contract SpokePoolPeriphery is SpokePoolPeripheryInterface, ReentrancyGuard, Mul | |
| depositData.baseDepositData.outputAmount, | ||
| depositData.baseDepositData.destinationChainId, | ||
| depositData.baseDepositData.exclusiveRelayer, | ||
| depositData.nonce, // TODO: should we hash this nonce (if nonzero) with the permit2 nonce to protect the user against nonce reuse? | ||
|
||
| depositData.baseDepositData.quoteTimestamp, | ||
| depositData.baseDepositData.fillDeadline, | ||
| depositData.baseDepositData.exclusivityParameter, | ||
|
|
@@ -485,9 +489,9 @@ contract SpokePoolPeriphery is SpokePoolPeripheryInterface, ReentrancyGuard, Mul | |
| DepositData calldata depositData, | ||
| uint256 validAfter, | ||
| uint256 validBefore, | ||
| bytes calldata receiveWithAuthSignature, | ||
| bytes calldata depositDataSignature | ||
| bytes calldata receiveWithAuthSignature | ||
| ) external override nonReentrant { | ||
| bytes32 witness = keccak256(abi.encodePacked(BRIDGE_WITNESS_IDENTIFIER, abi.encode(depositData))); | ||
| // Load variables used multiple times onto the stack. | ||
| uint256 _inputAmount = depositData.inputAmount; | ||
| uint256 _submissionFeeAmount = depositData.submissionFees.amount; | ||
|
|
@@ -500,7 +504,7 @@ contract SpokePoolPeriphery is SpokePoolPeripheryInterface, ReentrancyGuard, Mul | |
| _inputAmount + _submissionFeeAmount, | ||
| validAfter, | ||
| validBefore, | ||
| bytes32(depositData.nonce), | ||
| witness, | ||
| v, | ||
| r, | ||
| s | ||
|
|
@@ -513,15 +517,7 @@ contract SpokePoolPeriphery is SpokePoolPeripheryInterface, ReentrancyGuard, Mul | |
|
|
||
| // Note: No need to validate our internal nonce for receiveWithAuthorization | ||
| // as EIP-3009 has its own nonce mechanism that prevents replay attacks. | ||
| // | ||
| // Design Decision: We reuse the receiveWithAuthorization nonce for our signatures, | ||
| // but not for permit, which creates a theoretical replay attack that we think is | ||
| // incredibly unlikely because this would require: | ||
| // 1. A token implementing both ERC-2612 and ERC-3009 | ||
| // 2. A user using the same nonces for depositWithPermit and for depositWithAuthorization | ||
| // 3. Issuing these signatures within a short amount of time (limited by fillDeadlineBuffer) | ||
| // Verify that the signatureOwner signed the input depositData. | ||
| _validateSignature(signatureOwner, PeripherySigningLib.hashDepositData(depositData), depositDataSignature); | ||
| // We use the witness (which serves as the ERC-3009 nonce) as the deposit nonce. | ||
| _deposit( | ||
| depositData.spokePool, | ||
| depositData.baseDepositData.depositor, | ||
|
|
@@ -532,6 +528,7 @@ contract SpokePoolPeriphery is SpokePoolPeripheryInterface, ReentrancyGuard, Mul | |
| depositData.baseDepositData.outputAmount, | ||
| depositData.baseDepositData.destinationChainId, | ||
| depositData.baseDepositData.exclusiveRelayer, | ||
| uint256(witness), | ||
| depositData.baseDepositData.quoteTimestamp, | ||
| depositData.baseDepositData.fillDeadline, | ||
| depositData.baseDepositData.exclusivityParameter, | ||
|
|
@@ -571,16 +568,18 @@ contract SpokePoolPeriphery is SpokePoolPeripheryInterface, ReentrancyGuard, Mul | |
| } | ||
|
|
||
| /** | ||
| * @notice Approves the spoke pool and calls `depositV3` function with the specified input parameters. | ||
| * @param depositor The address on the origin chain which should be treated as the depositor by Across, and will therefore receive refunds if this deposit | ||
| * is unfilled. | ||
| * @notice Approves the spoke pool and calls either `depositV3` or `unsafeDeposit` based on whether a nonce is provided. | ||
| * @dev When depositNonce is 0, calls the regular deposit function. When non-zero, calls the unsafe deposit variant. | ||
| * @param spokePool The address of the spoke pool to deposit into. | ||
| * @param depositor The address on the origin chain which should be treated as the depositor by Across. | ||
| * @param recipient The address on the destination chain which should receive outputAmount of outputToken. | ||
| * @param inputToken The token to deposit on the origin chain. | ||
| * @param outputToken The token to receive on the destination chain. | ||
| * @param inputAmount The amount of the input token to deposit. | ||
| * @param outputAmount The amount of the output token to receive. | ||
| * @param destinationChainId The network ID for the destination chain. | ||
| * @param exclusiveRelayer The optional address for an Across relayer which may fill the deposit exclusively. | ||
| * @param depositNonce The nonce for this deposit. If 0, calls regular deposit; if non-zero, calls unsafe deposit. | ||
| * @param quoteTimestamp The timestamp at which the relay and LP fee was calculated. | ||
| * @param fillDeadline The timestamp at which the deposit must be filled before it will be refunded by Across. | ||
| * @param exclusivityParameter The deadline or offset during which the exclusive relayer has rights to fill the deposit without contention. | ||
|
|
@@ -596,33 +595,52 @@ contract SpokePoolPeriphery is SpokePoolPeripheryInterface, ReentrancyGuard, Mul | |
| uint256 outputAmount, | ||
| uint256 destinationChainId, | ||
| bytes32 exclusiveRelayer, | ||
| uint256 depositNonce, | ||
| uint32 quoteTimestamp, | ||
| uint32 fillDeadline, | ||
| uint32 exclusivityParameter, | ||
| bytes calldata message | ||
| bytes memory message | ||
| ) private { | ||
| IERC20(inputToken).forceApprove(spokePool, inputAmount); | ||
| V3SpokePoolInterface(spokePool).deposit( | ||
| depositor.toBytes32(), | ||
| recipient, | ||
| inputToken.toBytes32(), | ||
| outputToken, | ||
| inputAmount, | ||
| outputAmount, | ||
| destinationChainId, | ||
| exclusiveRelayer, | ||
| quoteTimestamp, | ||
| fillDeadline, | ||
| exclusivityParameter, | ||
| message | ||
| ); | ||
| if (depositNonce == 0) { | ||
| V3SpokePoolInterface(spokePool).deposit( | ||
| depositor.toBytes32(), | ||
| recipient, | ||
| inputToken.toBytes32(), | ||
| outputToken, | ||
| inputAmount, | ||
| outputAmount, | ||
| destinationChainId, | ||
| exclusiveRelayer, | ||
| quoteTimestamp, | ||
| fillDeadline, | ||
| exclusivityParameter, | ||
| message | ||
| ); | ||
| } else { | ||
| V3SpokePoolInterface(spokePool).unsafeDeposit( | ||
| depositor.toBytes32(), | ||
| recipient, | ||
| inputToken.toBytes32(), | ||
| outputToken, | ||
| inputAmount, | ||
| outputAmount, | ||
| destinationChainId, | ||
| exclusiveRelayer, | ||
| depositNonce, | ||
| quoteTimestamp, | ||
| fillDeadline, | ||
| exclusivityParameter, | ||
| message | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * @notice Swaps a token on the origin chain before depositing into the Across spoke pool atomically. | ||
| * @param swapAndDepositData The parameters to use when calling both the swap on an exchange and bridging via an Across spoke pool. | ||
| */ | ||
| function _swapAndBridge(SwapAndDepositData calldata swapAndDepositData) private { | ||
| function _swapAndBridge(SwapAndDepositData memory swapAndDepositData) private { | ||
| // Load variables we use multiple times onto the stack. | ||
| IERC20 _swapToken = IERC20(swapAndDepositData.swapToken); | ||
| IERC20 _acrossInputToken = IERC20(swapAndDepositData.depositData.inputToken); | ||
|
|
@@ -681,6 +699,7 @@ contract SpokePoolPeriphery is SpokePoolPeripheryInterface, ReentrancyGuard, Mul | |
| adjustedOutputAmount, | ||
| swapAndDepositData.depositData.destinationChainId, | ||
| swapAndDepositData.depositData.exclusiveRelayer, | ||
| swapAndDepositData.nonce, | ||
| swapAndDepositData.depositData.quoteTimestamp, | ||
| swapAndDepositData.depositData.fillDeadline, | ||
| swapAndDepositData.depositData.exclusivityParameter, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how do we ensure
witnessuniqueness given that its now obtained from deposit data?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My understanding is that the token implementing ERC-3009 will revert on the
receiveWithAuthorizationcall if thewitnesshas already been usedAlso there's a
nonceinSwapAndDepositDatathats part of what gets hashed to generate thewitness- so this can create uniqueness even if all the other intent data is identicalThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, my undertsanding was the same as Taylor's,
witnesshere is essentially thenoncefor the ERC-3009 token's permit with authFrom https://eips.ethereum.org/EIPS/eip-3009 reference implementation:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed with the above. There is nonce reuse prevention that is per-user. The nonce within the hashed data means that a user should never have an identical witness hash unless they are accidentally reusing the same API response twice.