Skip to content

fix(p0-11): inline MIN/MAX/delta guards on all three price setters#114

Merged
jhfnetboy merged 7 commits into
mainfrom
fix/p0-11-price-setter-bounds
May 6, 2026
Merged

fix(p0-11): inline MIN/MAX/delta guards on all three price setters#114
jhfnetboy merged 7 commits into
mainfrom
fix/p0-11-price-setter-bounds

Conversation

@jhfnetboy
Copy link
Copy Markdown
Member

@jhfnetboy jhfnetboy commented Apr 28, 2026

Summary

P0-11 (B2-N3 / B4-M2): Three setter functions previously accepted arbitrary values — a single mis-typed owner call could distort pricing for all operators at once.

Three setters hardened:

Setter Absolute Range Per-tx Delta
SuperPaymaster.setAPNTSPrice [1e15, 1e21] ether/aPNTs ±10%
xPNTsToken.updateExchangeRate [1e14, 1e22] ±20%
PaymasterBase.setCachedPrice [$100e8, $1Me8] ±30%

Delta check is skipped when oldPrice == 0 (first deployment set). All three setters share the pattern: zero check → absolute bounds → delta bounds.

Stack dependency: This PR is stacked on fix/p0-10-breakglass (P0-10). GitHub will auto-rebase the base when P0-10 merges.

Test plan

  • 26 new tests in PriceSetter_Bounds.t.sol (3 suites, one per setter)
  • Covers: zero rejection, below-MIN, above-MAX, delta-above, delta-below, delta-accepted, uninit bypass, onlyOwner
  • 4 existing tests updated to use values within new windows
  • 492/496 passing (4 pre-existing P0-9 baseline failures unrelated to this PR)

P2 顺风车追加(B2-N11)

priceStalenessThreshold 无边界检查:设为 0 让所有价格立即过期;设为极大值让过期检测失效。

追加修复:

  • PaymasterBase.sol setPriceStalenessThreshold():加 [60, 86400] bounds,违反时 revert Paymaster__InvalidStalenessThreshold()
  • SuperPaymaster.sol:新增 setPriceStalenessThreshold(uint256 val) setter(原来缺失),复用 InvalidConfiguration() error;initialize() 超出范围时 fallback 到 3600 秒默认值(构造时容错,setter 时严格 revert)

496 tests pass.

@jhfnetboy jhfnetboy requested a review from fanhousanbu as a code owner April 28, 2026 11:59
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 64cebef572

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +569 to +572
if (oldRate != 0) {
uint256 lower = oldRate * (10000 - EXCHANGE_RATE_DELTA_BPS) / 10000;
uint256 upper = oldRate * (10000 + EXCHANGE_RATE_DELTA_BPS) / 10000;
if (_newRate < lower || _newRate > upper) revert ExchangeRateCannotBeZero();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Skip delta guard when existing exchangeRate is out of band

initialize still accepts any _exchangeRate, but this new delta check always runs when oldRate != 0; if a token is deployed with a typoed rate far outside [1e14, 1e22] (for example 1e30), no future value can satisfy both the absolute bounds and the ±20% window, so updateExchangeRate becomes permanently unusable. This turns a recoverable misconfiguration into an irrecoverable one for affected communities.

Useful? React with 👍 / 👎.

/// cachedPrice is uninitialised (first push).
function setCachedPrice(uint256 price, uint48 timestamp) external onlyOwner {
if (price == 0) revert Paymaster__InvalidOraclePrice();
if (price < CACHED_PRICE_MIN || price > CACHED_PRICE_MAX) revert Paymaster__InvalidOraclePrice();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Restrict cached-price max to the runtime accepted range

setCachedPrice now allows values up to CACHED_PRICE_MAX (1_000_000e8), but the paymaster runtime path later rejects cached ETH prices above MAX_ETH_USD_PRICE (100_000e8) in _calculateTokenCost; setting a value in that gap can therefore make validation fail, and the new ±30% delta guard can prevent stepping back below 100_000e8 in one manual update. This creates a self-inflicted DoS path from a single owner typo.

Useful? React with 👍 / 👎.

@fanhousanbu
Copy link
Copy Markdown
Contributor

代码审查

通过,含 MEDIUM 问题

三个 setter 的三层保护(zero → bounds → delta)均正确实现,检查顺序正确(先验证后写入),绝对边界和 delta 值选取合理。

MEDIUM — xPNTsToken.updateExchangeRate 使用硬编码 10000 而非 BPS_DENOMINATOR

// xPNTsToken.sol — 硬编码
uint256 lower = oldRate * (10000 - EXCHANGE_RATE_DELTA_BPS) / 10000;

// SuperPaymaster.sol — 使用常量
uint256 lower = oldPrice * (BPS_DENOMINATOR - APNTS_PRICE_DELTA_BPS) / BPS_DENOMINATOR;

两处不一致。若将来 BPS_DENOMINATOR 被调整,xPNTsToken 的 delta 计算会静默偏离。建议 xPNTsToken.sol 中也引用 BPS_DENOMINATOR 常量,或定义等价的本地常量。

MEDIUM — setCachedPrice ±30% delta 与 Break-Glass ±20% delta 使用同一函数但不同限制
两条价格写入路径的 delta 上限不同(break-glass ±20% vs setCachedPrice ±30%),需确认不会相互绕过。从当前代码看两者是独立函数,不存在绕过,但建议在注释中明确说明两条路径的边界限制设计意图。

LOW — aPNTsPriceUSD 归零时 delta 检查被跳过
if (oldPrice != 0) 分支的注释应说明:此假设依赖 initialize()aPNTsPriceUSD 被正确初始化为非零值,升级时若存储布局变化导致归零,delta check 会被意外跳过。

@jhfnetboy
Copy link
Copy Markdown
Member Author

已根据 reviewer 建议修复:将 updateExchangeRate delta 计算中的硬编码 10000 替换为具名常量 BPS_DENOMINATOR

变更

  • xPNTsToken.sol 添加 private constant BPS_DENOMINATOR = 10_000
  • delta-bound 计算改用 BPS_DENOMINATOR,与 EXCHANGE_RATE_DELTA_BPS 语义对齐
  • 所有 26 个 bounds 测试继续通过(0 failures)

请 re-review,谢谢 🙏

@jhfnetboy
Copy link
Copy Markdown
Member Author

NatSpec 补充:价格路径独立性 + 零价 delta skip 说明

修复内容

MEDIUM — 两条路径 delta 上限独立:在 setAPNTSPrice NatSpec 中新增说明:

  • ±10% delta(setAPNTSPrice)作用于 aPNTsPriceUSD 存储变量
  • ±20% cap(emergencySetPrice)作用于 cachedPrice
  • 两条路径操作不同存储,无法相互绕过对方的偏差限制

LOW — delta skip 零价条件依赖初始化:在 if (oldPrice != 0) 分支前添加内联注释:

  • 说明此假设依赖 initialize()aPNTsPriceUSD 初始化为非零值(当前 0.02 ether)
  • 若升级后存储布局损坏导致该槽归零,首次写入时 delta 保护会被跳过
  • 缓解措施:升级后检查脚本需验证 aPNTsPriceUSD > 0

测试结果:38/38 PASS

fanhousanbu
fanhousanbu previously approved these changes May 4, 2026
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 force-pushed the fix/p0-10-breakglass branch from fd874fe to 061484c Compare May 5, 2026 07:35
@jhfnetboy jhfnetboy force-pushed the fix/p0-11-price-setter-bounds branch from 4e72ba5 to 413892b Compare May 5, 2026 07:44
@jhfnetboy jhfnetboy force-pushed the fix/p0-10-breakglass branch 2 times, most recently from 2f40260 to 8dc6f01 Compare May 5, 2026 12:51
@jhfnetboy jhfnetboy force-pushed the fix/p0-11-price-setter-bounds branch from 413892b to e91860f Compare May 5, 2026 13:09
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 5, 2026

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@jhfnetboy jhfnetboy force-pushed the fix/p0-10-breakglass branch from 8dc6f01 to 391b1e1 Compare May 5, 2026 13:35
Base automatically changed from fix/p0-10-breakglass to main May 5, 2026 13:48
@jhfnetboy jhfnetboy dismissed fanhousanbu’s stale review May 5, 2026 13:48

The base branch was changed.

@jhfnetboy jhfnetboy force-pushed the fix/p0-11-price-setter-bounds branch from e91860f to 91b5b33 Compare May 5, 2026 13:57
@jhfnetboy
Copy link
Copy Markdown
Member Author

@fanhousanbu 已 rebase 到最新 main(跳过了 P0-12a/b + P0-10 共 6 个已合并 commits)。只剩 P0-11 的 5 个 commits。请 approve。

@jhfnetboy
Copy link
Copy Markdown
Member Author

Waiting on PR #118 + size adjustment needed

CI failing with EIP-170 size overflow in main's SuperPaymaster.

After rebasing onto main (post #118 merge), this PR will still be ~42 bytes over EIP-170. To land cleanly:

  1. Rebase onto main after fix(eip170): shrink SuperPaymaster to 24,537B — under EIP-170 limit #118 merges
  2. Make the three new constants internal instead of public — they're never called as getters in tests, and internal eliminates the getter bytecode:
    uint256 internal constant APNTS_PRICE_MIN = 1e15;
    uint256 internal constant APNTS_PRICE_MAX = 1e21;
    uint256 internal constant APNTS_PRICE_DELTA_BPS = 1000;
  3. That alone saves ~130 bytes and brings SuperPaymaster to ~24,508 bytes (68 under limit) after rebase

The P2 setPriceStalenessThreshold() function can stay if you want, but it needs the above constant visibility change to fit.

@fanhousanbu re-review needed after these adjustments — the previous approval was dismissed by force-push.

@jhfnetboy
Copy link
Copy Markdown
Member Author

EIP-170 优化已推送 (commit 26eb558)

PR #114 在 rebase 到 main(待 #118 合并后)之前,已在本分支预先应用以下字节裁剪:

优化 节省估算
APNTS_PRICE_MIN/MAX/DELTA_BPSinternal ~150B
删除 setPriceStalenessThreshold() 函数(P2 增强,非 P0 安全必要) ~73B
staleness init 恢复为单行三元运算 ~20B
delta 数学改 unchecked{} ~47B
合并冗余 newPrice == 0 检查 ~5B

当前在旧 main 基准 (26,166B) 上的净增量:+81B

Rebase 后预期大小

24,541B (security 分支基准) + 81B = 24,622B → 超 EIP-170 46 bytes

还需 46 bytes — Rebase 时处理

  1. 检查 updatePrice() / updatePriceDVT() 是否在本 PR 中也加了 guard(如有则评估能否简化)
  2. 可以考虑把 absolute MIN/MAX 检查与 delta 检查合并成一个条件分支
  3. 或在 getAgentSponsorshipRate 的 try/catch 上找余量(但那是 P0 fix,不建议移除)

SDK 注意:三个常量已改为 internal,ABI 不再暴露 getter:

  • APNTS_PRICE_MIN = 1e15
  • APNTS_PRICE_MAX = 1e21
  • APNTS_PRICE_DELTA_BPS = 1000

请先 merge #118 再 rebase 此 PR,并重新测量大小后 merge。

jhfnetboy added a commit that referenced this pull request May 6, 2026
…d with SDK migration guide

Remove from on-chain ABI to gain ~230B headroom for P0 PRs #110/#113/#114:
- isChainlinkStale() → SDK reads cachedPrice().updatedAt + priceStalenessThreshold
- getAvailableCredit() → SDK computes off-chain from pendingDebts + registry.getCreditLimit
- getSlashHistory() → use getSlashCount + getSlashRecord(operator, index) loop
- getLatestSlash() → use getSlashRecord(operator, count - 1)

Add getSlashRecord(address,uint256) returns (SlashRecord) to work around Solidity
mapping-to-struct-array tuple limitation. Net cost: ~30B. Net savings: ~200B.

Update test files to use new API:
- EmergencyBreakGlass.t.sol: replace isChainlinkStale() with local _chainlinkStale()
- SuperPaymasterV3Query.t.sol: full rewrite using getSlashCount + getSlashRecord
- SuperPaymasterV3_Admin.t.sol: use getSlashRecord() instead of removed functions

Docs:
- API_SUPERPAYMASTER.md: mark removed functions with migration paths; update
  context encoding (6→5 field), add emergencySetPrice/cancel/execute, add
  updatePriceDVT chainlinkRecovered param; add ⚠️ SDK MAINTAINER banner at top
- eip170-impact-analysis: append Section 7 with SDK migration code samples,
  off-chain indexing guidance, and ABI update instructions

All 617 tests pass.
jhfnetboy added 7 commits May 6, 2026 11:28
Three setter functions previously accepted arbitrary values (P0-11 /
B2-N3 / B4-M2):

  SuperPaymaster.setAPNTSPrice     — absolute [1e15, 1e21], ±10% delta
  xPNTsToken.updateExchangeRate    — absolute [1e14, 1e22], ±20% delta
  PaymasterBase.setCachedPrice     — absolute [$100e8, $1Me8], ±30% delta

Delta check is skipped on first set (oldPrice == 0) to allow initial
deployment. Existing tests that passed out-of-range values updated to
stay within the new windows; 26 new PriceSetter_Bounds tests added.
…hangeRate

Adds a private BPS_DENOMINATOR = 10_000 constant to xPNTsToken.sol and
uses it in the delta-bound calculation instead of the bare literal, so
EXCHANGE_RATE_DELTA_BPS and the denominator stay in sync if the constant
changes in the future.
…ta-skip invariant

Add NatSpec to setAPNTSPrice() addressing two reviewer findings:
- MEDIUM: explain that the ±10% delta in setAPNTSPrice and the ±20% cap in
  emergencySetPrice are independent paths that operate on different storage
  slots (aPNTsPriceUSD vs cachedPrice) and cannot be used to bypass each other.
- LOW: document that the `if (oldPrice != 0)` delta-skip relies on initialize()
  setting a non-zero initial price, and that storage layout corruption on upgrade
  could cause the delta guard to be bypassed for the first write post-upgrade.
UUPSUpgrade.t.sol::test_SuperPaymaster_BusinessLogicAfterUpgrade called
setAPNTSPrice(0.05 ether) with an initial price of 0.02 ether.
That is a +150% move — far outside the ±10% per-tx delta cap added by P0-11.
Changed to 0.021 ether (+5%), which is within the allowed window.

Tests: 496/496.
Prevents setting staleness threshold to 0 (instant expiry breaks all ops)
or an extreme value (disables the check for a day+). Hitchhike on P0-11.
- Make APNTS_PRICE_MIN/MAX/DELTA_BPS internal (removes 3 public getters, ~150B)
- Remove setPriceStalenessThreshold() function (P2 enhancement, not P0 security; ~73B)
- Simplify staleness init to single ternary (vs 3-line bounds check; ~20B)
- Use unchecked{} for delta lower/upper arithmetic (known-safe mul; ~47B)
- Merge redundant `newPrice == 0` check into `< APNTS_PRICE_MIN` (~5B)

Estimated net addition to SuperPaymaster after these changes: +81B over
EIP-170 baseline (24,541B). Expected final size after rebase on #118:
~24,622B (46B over). Size must be re-measured after rebasing on #118.

SDK note: APNTS_PRICE_MIN = 1e15, APNTS_PRICE_MAX = 1e21,
APNTS_PRICE_DELTA_BPS = 1000 — hardcode in SDK constants.
…grace period

- SuperPaymaster: TIMESTAMP_GRACE_SECONDS public→internal (saves ~39B getter)
- SuperPaymaster: EMERGENCY_EXPIRY public→internal (saves ~15B getter)
- PaymasterBase.setCachedPrice: remove 15s grace period (admin path must
  reject any future timestamp; grace is for keeper/DVT paths only)
- Tests: replace paymaster.TIMESTAMP_GRACE_SECONDS() calls with literal 15
- Tests: rewrite test_SetCachedPrice_AcceptsTimestampWithinGrace →
  test_SetCachedPrice_RejectsAnyFutureTimestamp (semantics changed)
- Tests: replace paymaster.EMERGENCY_EXPIRY() calls with literal 7 days

Result: SuperPaymaster 24,564B (12B under EIP-170 limit), 631/631 tests pass
@jhfnetboy jhfnetboy force-pushed the fix/p0-11-price-setter-bounds branch from 5487042 to acd96a9 Compare May 6, 2026 04:29
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

P0-11 三处价格 setter 新增 MIN/MAX/DELTA bounds,验证如下:

SuperPaymaster.setAPNTSPrice

  • MIN=1e15 / MAX=1e21 / DELTA=±10%(BPS 1000)
  • unchecked 乘法安全:oldPrice ≤ MAX=1e21,oldPrice * 11000 / 10000 ≤ 1.1e24,远低于 uint256 上限

PaymasterBase(V4)setCachedPrice / updatePriceDVT

  • MIN=100e8($100) / MAX=1_000_000e8($1M) / DELTA=±30%(BPS 3000)
  • TIMESTAMP_GRACE_SECONDS / EMERGENCY_EXPIRY 改为 internal(EIP-170 优化),测试改用硬编码值

xPNTsToken.updateExchangeRate

  • MIN=1e14 / MAX=1e22 / DELTA=±20%(BPS 2000)
  • 旧测试中 updateExchangeRate(2e18)(+100%超限)已删除,替换为合规值

测试值更新正确(setAPNTSPrice 0.021 ether ≈ +5%,在±10%窗口内)。

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

P0-11 三处 setter 新增 MIN/MAX/DELTA bounds,逻辑正确:

  • setAPNTSPrice: MIN=1e15 / MAX=1e21 / DELTA=±10%,unchecked 乘法安全(值有上界,不会溢出 uint256)
  • setCachedPrice/updatePriceDVT: MIN=$100 / MAX=$1M / DELTA=±30%,TIMESTAMP_GRACE_SECONDS 改 internal(EIP-170)
  • updateExchangeRate: MIN=1e14 / MAX=1e22 / DELTA=±20%,旧超限测试值已删除换为合规值

@jhfnetboy jhfnetboy merged commit 814735d into main May 6, 2026
3 checks passed
@jhfnetboy jhfnetboy deleted the fix/p0-11-price-setter-bounds branch May 6, 2026 06:52
@github-actions github-actions Bot locked and limited conversation to collaborators May 6, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants