fix(p0-16): future-timestamp guard on cache writes#107
Conversation
P0-16 (Codex B-N1): three cache writers accepted any `updatedAt` value, including timestamps in the future. Two attack effects: 1. Staleness bypass — `block.timestamp - cachedPrice.updatedAt < threshold` stays true forever once the cache holds a future timestamp, so the stale price is treated as fresh until overwritten. 2. PostOp brick — `block.timestamp - cachedPrice.updatedAt` underflows in 0.8 checked arithmetic, reverting every postOp call for that operator until the cache is replaced. Defense: - SP.updatePriceDVT: require updatedAt <= block.timestamp - PaymasterBase.setCachedPrice: same guard + reject zero price - PaymasterBase postOp staleness check: defensive fallback that treats a future-timestamped cache as stale (triggering refresh) instead of reverting on subtraction underflow — handles any pre-fix proxy state Tests: - contracts/test/v3/PriceCache_FutureTimestamp.t.sol (5 cases) Spec: docs/security/2026-04-26-p0-prelaunch.md (security/audit-2026-04-25) Refs: P0-16
代码审查通过,含 MEDIUM 设计建议 三处修复点均正确: MEDIUM — 未考虑 keeper 时钟偏差(Clock Skew) 请团队确认:keeper 是否始终使用链上时间源?若否,建议考虑加入 15 秒容忍窗口: uint256 constant MAX_FUTURE_TOLERANCE = 15;
if (updatedAt > block.timestamp + MAX_FUTURE_TOLERANCE) revert OracleError();若确认 keeper 使用链上时间,请在注释中明确说明这一假设。 LOW — 缺失测试
|
…hecks Add TIMESTAMP_GRACE_SECONDS = 15 constant to PaymasterBase and relax the future-timestamp guard in setCachedPrice and updatePriceDVT from strict block.timestamp to block.timestamp + TIMESTAMP_GRACE_SECONDS. Ethereum block.timestamp can lag real wall-clock by up to ~12 s; a keeper reading its system clock may arrive slightly ahead of the on-chain value and be spuriously rejected. 15 s (> 12 s slot upper bound) prevents false rejections without meaningfully weakening the far-future-timestamp attack guard introduced by P0-16. Update PriceCache_FutureTimestamp test: add AcceptsTimestampWithinGrace test; adjust RejectsFutureTimestamp to use the constant boundary. Closes #107
修复说明问题: 修复: PaymasterBase.sol:新增常量并放宽检查: /// @notice 15 s > 12 s slot upper bound, prevents spurious keeper rejections.
uint256 public constant TIMESTAMP_GRACE_SECONDS = 15;
// setCachedPrice — 原来:
if (timestamp > block.timestamp) revert Paymaster__InvalidOraclePrice();
// 修改为:
if (timestamp > block.timestamp + TIMESTAMP_GRACE_SECONDS) revert Paymaster__InvalidOraclePrice();SuperPaymaster.sol(updatePriceDVT): // 原来:
if (updatedAt > block.timestamp) revert OracleError();
// 修改为:
if (updatedAt > block.timestamp + 15) revert OracleError();测试更新(
测试结果: |
|
已补充完善: MEDIUM — keeper 时钟偏差(Clock Skew) 之前的修复已加入 15 秒容忍窗口,本次额外将 SuperPaymaster.sol 中的硬编码 6 个时间戳边界测试全部通过(0 failures)。 请 re-review,谢谢 🙏 |
2 similar comments
|
已补充完善: MEDIUM — keeper 时钟偏差(Clock Skew) 之前的修复已加入 15 秒容忍窗口,本次额外将 SuperPaymaster.sol 中的硬编码 6 个时间戳边界测试全部通过(0 failures)。 请 re-review,谢谢 🙏 |
|
已补充完善: MEDIUM — keeper 时钟偏差(Clock Skew) 之前的修复已加入 15 秒容忍窗口,本次额外将 SuperPaymaster.sol 中的硬编码 6 个时间戳边界测试全部通过(0 failures)。 请 re-review,谢谢 🙏 |
|
已补充完善:MEDIUM — keeper 时钟偏差(Clock Skew) 之前的修复已加入 15 秒容忍窗口,本次额外将 SuperPaymaster.sol 中的硬编码 + 15 提取为具名常量 TIMESTAMP_GRACE_SECONDS = 15,与 PaymasterBase.sol 中的同名常量保持语义对齐。请 re-review,谢谢 |
… degradation tests - SuperPaymaster.updatePrice(): add NatSpec comment explaining why no future-timestamp guard is needed on the Chainlink path (updatedAt comes from a validated oracle response, not an untrusted caller; contrast with updatePriceDVT which is caller-supplied and requires the P0-16 guard). - test_UpdatePriceDVT_RejectsFutureTimestamp: verifies that calling updatePriceDVT with updatedAt > block.timestamp + TIMESTAMP_GRACE_SECONDS reverts with OracleError(), closing the DVT future-timestamp attack vector. - test_UpdatePriceDVT_AcceptsTimestampAtGraceBoundary: verifies keepers within the 15-second clock-skew window are not spuriously rejected. - test_PostOp_DegradesToRealtimeOnFutureCachedPrice: uses vm.store to inject a future updatedAt into Paymaster (v4) cachedPrice slot, then calls postOp from the entry point address and asserts it succeeds without underflow and refreshes the cache — proving the defensive staleness guard in postOp (cachedPrice.updatedAt > block.timestamp → fallback to realtime) works. All 308 forge tests pass.
P0-16 补充修复 — 审阅者遗留问题本次提交解决了审阅者提出的两个剩余问题: Issue 1 (LOW):updatePrice() Chainlink 路径未修改的说明在
Issue 2:新增测试(全部通过)新增了以下测试到
测试结果:全部 308 个 forge 测试通过,0 失败。 |
P0-16 (Codex Phase 6, B-N1)
3 处 cache 写入(SP.updatePriceDVT, PaymasterBase.setCachedPrice, postOp staleness 读取)接受任意 `updatedAt` 值,包括未来时间戳。两个攻击效果:
Defense
Tests
5 新测试(拒绝未来 / 接受当前 / 接受过去 / 拒绝零价 / 拒绝远未来)= 5/5 passed
Spec
`docs/security/2026-04-26-p0-prelaunch.md` §3 P0-16
`docs/security/2026-04-25-review.md` §6.B Codex Phase 6