Skip to content

fix(p0-16): future-timestamp guard on cache writes#107

Merged
jhfnetboy merged 4 commits into
mainfrom
fix/p0-16-future-timestamp
May 5, 2026
Merged

fix(p0-16): future-timestamp guard on cache writes#107
jhfnetboy merged 4 commits into
mainfrom
fix/p0-16-future-timestamp

Conversation

@jhfnetboy
Copy link
Copy Markdown
Member

P0-16 (Codex Phase 6, B-N1)

3 处 cache 写入(SP.updatePriceDVT, PaymasterBase.setCachedPrice, postOp staleness 读取)接受任意 `updatedAt` 值,包括未来时间戳。两个攻击效果:

  1. Staleness bypass — `block.timestamp - cachedPrice.updatedAt < threshold` 永远满足,stale 价格被当作 fresh
  2. PostOp brick — `block.timestamp - cachedPrice.updatedAt` 在 0.8 checked 算术下下溢 revert,该 operator 的 postOp 永久死锁直到 cache 被覆盖

Defense

  • SP.updatePriceDVT: `require(updatedAt <= block.timestamp)`
  • PaymasterBase.setCachedPrice: 同 guard + reject zero price
  • PaymasterBase postOp staleness 检查:防御性 fallback 把未来时间戳当作 stale(触发 refresh)而非下溢 revert —— 处理任何 pre-fix proxy state

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

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
@jhfnetboy jhfnetboy requested a review from fanhousanbu as a code owner April 28, 2026 07:45
@fanhousanbu
Copy link
Copy Markdown
Contributor

代码审查

通过,含 MEDIUM 设计建议

三处修复点均正确updatePriceDVTsetCachedPrice 的入口守卫,以及 _postOp 中的防御性降级处理均实现正确。_postOp 将已存在的未来时间戳视为 stale 的处理可防止合约被永久 brick。

MEDIUM — 未考虑 keeper 时钟偏差(Clock Skew)
当前严格拒绝 updatedAt > block.timestamp(不允许任何未来时间戳)。以太坊 block.timestamp 与现实时钟存在最多约 12 秒的偏差,若 keeper 使用系统时钟推送价格且时钟略快于矿工,会产生意外 revert。

请团队确认:keeper 是否始终使用链上时间源?若否,建议考虑加入 15 秒容忍窗口:

uint256 constant MAX_FUTURE_TOLERANCE = 15;
if (updatedAt > block.timestamp + MAX_FUTURE_TOLERANCE) revert OracleError();

若确认 keeper 使用链上时间,请在注释中明确说明这一假设。

LOW — updatePrice()(Chainlink 拉取路径)未修改的原因未说明
该路径使用 block.timestamp 故不存在未来时间戳问题,但建议在 PR 注释中明确说明,避免未来维护者产生疑问。

缺失测试

  • updatePriceDVT 的未来时间戳 revert 测试
  • _postOp 发现 cachedPrice.updatedAt > block.timestamp 时降级为 realtime 的行为测试

…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
@jhfnetboy
Copy link
Copy Markdown
Member Author

修复说明

问题setCachedPrice(PaymasterBase.sol)和 updatePriceDVT(SuperPaymaster.sol)严格拒绝 timestamp > block.timestamp。以太坊 block.timestamp 与现实时钟最多有 ~12 秒偏差,导致诚实的 keeper 可能被误拒。

修复

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();

测试更新PriceCache_FutureTimestamp.t.sol):

  • test_SetCachedPrice_RejectsFutureTimestamp:改用常量边界(grace+1),语义更清晰
  • 新增 test_SetCachedPrice_AcceptsTimestampWithinGrace:验证 grace 边界内接受,边界外拒绝

测试结果forge test --match-path "*FutureTimestamp*" — 6/6 通过(含新增测试)。

@jhfnetboy
Copy link
Copy Markdown
Member Author

已补充完善:

MEDIUM — keeper 时钟偏差(Clock Skew)

之前的修复已加入 15 秒容忍窗口,本次额外将 SuperPaymaster.sol 中的硬编码 + 15 提取为具名常量 TIMESTAMP_GRACE_SECONDS = 15,与 PaymasterBase.sol 中的同名常量保持语义对齐。两个合约现在都用具名常量,未来修改只需改一处。

6 个时间戳边界测试全部通过(0 failures)。

请 re-review,谢谢 🙏

2 similar comments
@jhfnetboy
Copy link
Copy Markdown
Member Author

已补充完善:

MEDIUM — keeper 时钟偏差(Clock Skew)

之前的修复已加入 15 秒容忍窗口,本次额外将 SuperPaymaster.sol 中的硬编码 + 15 提取为具名常量 TIMESTAMP_GRACE_SECONDS = 15,与 PaymasterBase.sol 中的同名常量保持语义对齐。两个合约现在都用具名常量,未来修改只需改一处。

6 个时间戳边界测试全部通过(0 failures)。

请 re-review,谢谢 🙏

@jhfnetboy
Copy link
Copy Markdown
Member Author

已补充完善:

MEDIUM — keeper 时钟偏差(Clock Skew)

之前的修复已加入 15 秒容忍窗口,本次额外将 SuperPaymaster.sol 中的硬编码 + 15 提取为具名常量 TIMESTAMP_GRACE_SECONDS = 15,与 PaymasterBase.sol 中的同名常量保持语义对齐。两个合约现在都用具名常量,未来修改只需改一处。

6 个时间戳边界测试全部通过(0 failures)。

请 re-review,谢谢 🙏

@jhfnetboy
Copy link
Copy Markdown
Member Author

已补充完善: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.
@jhfnetboy
Copy link
Copy Markdown
Member Author

P0-16 补充修复 — 审阅者遗留问题

本次提交解决了审阅者提出的两个剩余问题:

Issue 1 (LOW):updatePrice() Chainlink 路径未修改的说明

SuperPaymaster.updatePrice() 函数上方新增了 NatSpec 注释,解释为何此路径不需要未来时间戳防护:

  • updatedAt 直接来自 Chainlink 预言机的已验证响应,不是由外部调用者提供的
  • Chainlink 节点始终将 updatedAt 设置为轮次的区块时间戳,其值天然 ≤ 当前 block.timestamp
  • 现有的过时检查(updatedAt < block.timestamp - priceStalenessThreshold)已足够
  • 对比 updatePriceDVT:其 updatedAt 由调用者提供,因此需要 P0-16 的明确防护

Issue 2:新增测试(全部通过)

新增了以下测试到 contracts/test/v3/PriceCache_FutureTimestamp.t.sol

test_UpdatePriceDVT_RejectsFutureTimestamp(SuperPaymaster DVT 路径)

  • updatedAt = block.timestamp + GRACE + 1 调用 updatePriceDVT()
  • 验证 revert OracleError(),确认未来时间戳攻击向量已关闭

test_UpdatePriceDVT_AcceptsTimestampAtGraceBoundary(正向验证)

  • 验证恰好在 15 秒宽限边界内的 keeper 时钟偏差不会被误拒

test_PostOp_DegradesToRealtimeOnFutureCachedPrice(Paymaster v4 postOp 降级)

  • 使用 vm.store 将未来 updatedAt 强制写入 cachedPrice 槽位(slot 5)
  • 从 EntryPoint 地址调用 postOp()
  • 验证 postOp 成功执行(无算术下溢),并刷新缓存为有效时间戳
  • 证明 postOp 中的防御性稳定性保护(cachedPrice.updatedAt > block.timestamp → 降级到实时)正常工作

测试结果:全部 308 个 forge 测试通过,0 失败。

Copy link
Copy Markdown
Contributor

@fanhousanbu fanhousanbu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Approved

@jhfnetboy jhfnetboy merged commit 78c7be9 into main May 5, 2026
2 checks passed
@jhfnetboy jhfnetboy deleted the fix/p0-16-future-timestamp branch May 5, 2026 11:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants