Skip to content

Commit e4a5828

Browse files
committed
add signature deadline
1 parent cfdae77 commit e4a5828

File tree

4 files changed

+114
-25
lines changed

4 files changed

+114
-25
lines changed

contracts/interfaces/ICounterfactualDeposit.sol

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ interface ICounterfactualDeposit {
1616
error InvalidSignature();
1717
/// @dev Native ETH transfer failed.
1818
error NativeTransferFailed();
19+
/// @dev EIP-712 signature deadline has passed. SpokePool only.
20+
error SignatureExpired();
1921

2022
event CounterfactualDepositExecuted(
2123
uint256 amount,

contracts/periphery/counterfactual/CounterfactualDepositSpokePool.sol

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ contract CounterfactualDepositSpokePool is CounterfactualDepositBase, EIP712 {
5151

5252
bytes32 public constant EXECUTE_DEPOSIT_TYPEHASH =
5353
keccak256(
54-
"ExecuteDeposit(uint256 inputAmount,uint256 outputAmount,bytes32 exclusiveRelayer,uint32 exclusivityDeadline,uint32 quoteTimestamp,uint32 fillDeadline)"
54+
"ExecuteDeposit(uint256 inputAmount,uint256 outputAmount,bytes32 exclusiveRelayer,uint32 exclusivityDeadline,uint32 quoteTimestamp,uint32 fillDeadline,uint32 signatureDeadline)"
5555
);
5656

5757
/// @notice Across SpokePool contract
@@ -92,6 +92,7 @@ contract CounterfactualDepositSpokePool is CounterfactualDepositBase, EIP712 {
9292
* @param executionFeeRecipient Address that receives the execution fee
9393
* @param quoteTimestamp Quote timestamp from Across API (SpokePool validates recency)
9494
* @param fillDeadline Timestamp by which the deposit must be filled
95+
* @param signatureDeadline Timestamp after which the signature is no longer valid
9596
* @param signature EIP-712 signature from signer over signed arguments
9697
*/
9798
function executeDeposit(
@@ -103,15 +104,18 @@ contract CounterfactualDepositSpokePool is CounterfactualDepositBase, EIP712 {
103104
address executionFeeRecipient,
104105
uint32 quoteTimestamp,
105106
uint32 fillDeadline,
107+
uint32 signatureDeadline,
106108
bytes calldata signature
107109
) external verifyParams(params) {
110+
if (block.timestamp > signatureDeadline) revert SignatureExpired();
108111
_verifySignature(
109112
inputAmount,
110113
outputAmount,
111114
exclusiveRelayer,
112115
exclusivityDeadline,
113116
quoteTimestamp,
114117
fillDeadline,
118+
signatureDeadline,
115119
signature
116120
);
117121

@@ -202,6 +206,7 @@ contract CounterfactualDepositSpokePool is CounterfactualDepositBase, EIP712 {
202206
* @param exclusivityDeadline Seconds of relayer exclusivity (signed by signer).
203207
* @param quoteTimestamp Quote timestamp from Across API (signed by signer).
204208
* @param fillDeadline Fill deadline timestamp (signed by signer).
209+
* @param signatureDeadline Signature expiry timestamp (signed by signer).
205210
* @param signature EIP-712 signature from signer.
206211
*/
207212
function _verifySignature(
@@ -211,6 +216,7 @@ contract CounterfactualDepositSpokePool is CounterfactualDepositBase, EIP712 {
211216
uint32 exclusivityDeadline,
212217
uint32 quoteTimestamp,
213218
uint32 fillDeadline,
219+
uint32 signatureDeadline,
214220
bytes calldata signature
215221
) internal view {
216222
bytes32 structHash = keccak256(
@@ -221,7 +227,8 @@ contract CounterfactualDepositSpokePool is CounterfactualDepositBase, EIP712 {
221227
exclusiveRelayer,
222228
exclusivityDeadline,
223229
quoteTimestamp,
224-
fillDeadline
230+
fillDeadline,
231+
signatureDeadline
225232
)
226233
);
227234
if (ECDSA.recover(_hashTypedDataV4(structHash), signature) != signer) revert InvalidSignature();

contracts/periphery/counterfactual/README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ Signature verification, nonce tracking, and `oftDeadline` enforcement are handle
123123
| `executionFeeRecipient` | Argument | Address that receives the execution fee |
124124
| `quoteTimestamp` | Argument (signed) | Quote timestamp from Across API (SpokePool validates recency) |
125125
| `fillDeadline` | Argument (signed) | Timestamp by which the deposit must be filled |
126+
| `signatureDeadline` | Argument (signed) | Timestamp after which the signature is no longer valid |
126127
| `signature` | Argument | EIP-712 signature from signer over signed arguments |
127128

128129
### EIP-712 Signature Verification
@@ -131,7 +132,7 @@ Unlike CCTP/OFT (where `SrcPeriphery` verifies signatures), the SpokePool implem
131132

132133
- **Domain separator** uses OpenZeppelin's `EIP712` base contract with `address(this)` (the clone address) — prevents cross-clone replay
133134
- **No nonce needed**: token balance is consumed on execution (natural replay protection), and short deadlines bound the replay window for re-funded clones
134-
- **Typehash**: `ExecuteDeposit(uint256 inputAmount,uint256 outputAmount,bytes32 exclusiveRelayer,uint32 exclusivityDeadline,uint32 quoteTimestamp,uint32 fillDeadline)`
135+
- **Typehash**: `ExecuteDeposit(uint256 inputAmount,uint256 outputAmount,bytes32 exclusiveRelayer,uint32 exclusivityDeadline,uint32 quoteTimestamp,uint32 fillDeadline,uint32 signatureDeadline)`
135136
- **Signer** is an immutable set in the implementation constructor, shared across all clones
136137

137138
### Fee Check
@@ -205,7 +206,7 @@ Storing full params as immutable args would cost ~595+ bytes of deployed code. W
205206

206207
Why: CCTP and OFT implementations forward deposits to a `SrcPeriphery` contract, which already validates the quote signature, nonce, and deadline before bridging. The implementation is just a pass-through, so adding its own signature check would be redundant.
207208

208-
The SpokePool implementation calls `SpokePool.deposit()` directly, and `deposit()` does not validate quotes — it accepts whatever parameters it receives. Without a signature check, anyone could call `executeDeposit` with an inflated `outputAmount` (causing the deposit to never fill) or a manipulated `fillDeadline`. The implementation's EIP-712 signature over `(inputAmount, outputAmount, exclusiveRelayer, exclusivityDeadline, quoteTimestamp, fillDeadline)` ensures only signer-approved values are used. The domain separator includes the clone address to prevent cross-clone replay.
209+
The SpokePool implementation calls `SpokePool.deposit()` directly, and `deposit()` does not validate quotes — it accepts whatever parameters it receives. Without a signature check, anyone could call `executeDeposit` with an inflated `outputAmount` (causing the deposit to never fill) or a manipulated `fillDeadline`. The implementation's EIP-712 signature over `(inputAmount, outputAmount, exclusiveRelayer, exclusivityDeadline, quoteTimestamp, fillDeadline, signatureDeadline)` ensures only signer-approved values are used. The `signatureDeadline` bounds the window during which a signature can be replayed against a re-funded clone. The domain separator includes the clone address to prevent cross-clone replay.
209210

210211
### 7. cctpMaxFeeBps / maxOftFeeBps / maxRelayerFee
211212

@@ -232,7 +233,7 @@ For subsequent deposits, callers can call the clone directly or use `factory.exe
232233
## Security Model
233234

234235
- **SponsoredCCTP/OFT Signer**: Trusted address that signs bridge quotes. Compromise allows bad quotes but fees are bounded by user-set `cctpMaxFeeBps`/`maxOftFeeBps`.
235-
- **SpokePool Signer**: Signs `(inputAmount, outputAmount, exclusiveRelayer, exclusivityDeadline, quoteTimestamp, fillDeadline)` for SpokePool executions. Compromise allows bad `outputAmount` values but bounded by `maxFeeBps`.
236+
- **SpokePool Signer**: Signs `(inputAmount, outputAmount, exclusiveRelayer, exclusivityDeadline, quoteTimestamp, fillDeadline, signatureDeadline)` for SpokePool executions. Compromise allows bad `outputAmount` values but bounded by `maxFeeBps`.
236237
- **Admin**: Per-clone admin (set in route params). Can withdraw any tokens from its clone via `adminWithdraw` (for recovery of wrongly sent tokens). Can be a multisig or TimelockController.
237238
- **userWithdrawAddress**: Can withdraw tokens from the clone via `userWithdraw` (escape hatch before execution).
238239
- **Execution Fee**: Fixed `executionFee` (route param, in token units) paid to relayer. User commits to this fee at address-generation time.

0 commit comments

Comments
 (0)