fix: reject future timestamps for Lock/Decrease/Unlock modes#64
Conversation
The only future-timestamp guard lived inside _processIncreaseOrUpdate, so just the Increase mode rejected future timestamps. Lock, Decrease, and Unlock flowed through _processAllowanceOperation and never hit the check. Because _lockAllowance stored the user-supplied timestamp verbatim and Unlock requires a timestamp strictly greater than the stored lock timestamp, a Lock signed with timestamp = type(uint48).max produced a permanently un-unlockable allowance. Move the guard up into _processAllowanceOperation (after the zero-checks, before _validateLockStatus) so all timestamp-storing modes reject future timestamps, and remove the now-redundant check from _processIncreaseOrUpdate (which becomes pure). The TransferERC20 mode skips _processAllowanceOperation and carries no timestamp, so it is unaffected. Adds test_lockTimestampInFuture; updates existing Lock/Decrease/Unlock tests that incidentally passed future timestamps to advance the clock (vm.warp) or use the current block time instead. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
OverviewThis PR fixes an issue where Lock, Decrease, or Unlock operations could incorrectly store future timestamps. By moving the future-timestamp guard up to QA & Test CoverageNo issues found in this domain. Web SecuritySkipped: not applicable to this PR — No web-specific security concerns; this is a smart contract timestamp validation fix. Blockchain SecurityNo issues found in this domain. PerformanceNo issues found in this domain. Stats
Automated review by Eco's specialized review team. Architecture, business logic, and correctness remain with the human reviewer. |
There was a problem hiding this comment.
Pull request overview
This PR closes a correctness gap in Permit3’s timestamp validation by rejecting future timestamps not only for Increase permits, but also for Lock/Decrease/Unlock flows, preventing “permanently un-unlockable” locked allowances caused by user-supplied future timestamps.
Changes:
- Enforces a future-timestamp guard in
_processAllowanceOperationso Lock/Decrease/Unlock cannot supply future timestamps. - Removes the redundant future-timestamp guard from
_processIncreaseOrUpdateand updates it topure. - Adds/updates edge tests to cover future lock timestamps and adjusts existing tests to avoid unintentionally using future timestamps (via
vm.warpwhere appropriate).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
src/Permit3.sol |
Moves the future-timestamp validation to the common allowance-operation path; simplifies _processIncreaseOrUpdate accordingly. |
test/Permit3Edge.t.sol |
Adds a regression test for future Lock timestamps and updates related tests to use non-future timestamps or warp time when required. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| revert ZeroAccount(); | ||
| } | ||
|
|
||
| // Prevent setting timestamps in the future for all timestamp-storing modes |
Problem
The only future-timestamp guard lived inside
_processIncreaseOrUpdate, so only the Increase mode (modeOrExpiration > 3) rejected future timestamps. The Lock, Decrease, and Unlock modes flow through_processAllowanceOperationand never hit that check.Because
_lockAllowancestored the user-suppliedtimestampverbatim, and Unlock requires a timestamp strictly greater than the stored lock timestamp (_validateLockStatus), a Lock signed withtimestamp = type(uint48).maxproduced a permanently un-unlockable allowance — a footgun for any owner (or relayer) that signs a Lock with a bad timestamp.Fix
_processAllowanceOperation(after theZeroToken/ZeroAccountchecks, before_validateLockStatus) so all timestamp-storing modes reject future timestamps._processIncreaseOrUpdate, which no longer readsblock.timestampand is nowpure.TransferERC20mode (0) skips_processAllowanceOperationand carries no timestamp, so it is unaffected.Tests
test_lockTimestampInFuture: signs a Lock permit with a future timestamp and asserts it reverts withInvalidTimestamp.block.timestamp + 100/+ 1). These now either advance the chain clock withvm.warp(where the test legitimately needs an unlock timestamp newer than the lock timestamp) or use the current block time (where the timestamp value was immaterial to what the test verifies). The production guard was not weakened.Full suite: 214 passed, 0 failed.
🤖 Generated with Claude Code