Skip to content

fix(p0-2,p0-3): CEI order in PaymasterFactory + Registry unchecked call checks#319

Open
jhfnetboy wants to merge 4 commits into
mainfrom
fix/p0-cei-registry-callcheck
Open

fix(p0-2,p0-3): CEI order in PaymasterFactory + Registry unchecked call checks#319
jhfnetboy wants to merge 4 commits into
mainfrom
fix/p0-cei-registry-callcheck

Conversation

@jhfnetboy

Copy link
Copy Markdown
Member

Summary

Two P2 hygiene fixes from the Slither/Opus audit review (2026-06-28). Neither is a GA blocker (both guarded by nonReentrant / existing checks), but resolves the Slither findings cleanly.

P0-2 — PaymasterFactory CEI ordering (PaymasterFactory.sol)

  • deployPaymaster: state writes (paymasterByOperator, operatorByPaymaster, paymasterList, totalDeployed) now happen before _initAndVerify external calls
  • deployPaymasterDeterministic: same fix applied
  • nonReentrant already prevents re-entry; this brings the code into textbook CEI alignment

P0-3 — Registry unchecked low-level call (Registry.sol)

  • _initRole: (bool ok,) = address(GTOKEN_STAKING).call(...) — return value captured; emit SyncFailed on failure
  • _syncExitFeeForRole: same pattern — now consistent with the existing public syncExitFees() implementation

Test plan

  • forge test — 1080/1080 pass, 0 failed
  • No Sepolia deploy needed (no interface change, no storage layout change)
  • Include in next scheduled release

131 findings: 3H (all FP) / 46M (15 real) / 79L
P0 GA blockers: xPNTsToken div-before-mul, PaymasterFactory CEI,
Registry unchecked lowlevel, Chainlink stale-round missing check
Invariant suite: 5/5 pass (128k calls each)
- TC1-4 all PASS on Sepolia v5.4.1-rc.1
- RegisterEnduser re-run needed after fresh deploy (SBT reset)
- Result: script/gasless-tests/results/2026-06-28_08-58-02_run-all-tests.md
…ll checks

P0-2 (PaymasterFactory CEI):
- deployPaymaster: write paymasterByOperator/operatorByPaymaster/paymasterList/totalDeployed
  BEFORE _initAndVerify external calls (nonReentrant already blocks re-entry; this
  aligns state reads with actual ownership for any future analysis)
- deployPaymasterDeterministic: same fix applied

P0-3 (Registry low-level call return value):
- _initRole: capture (bool ok,) and emit SyncFailed on failure — consistent with
  public syncExitFees() pattern
- _syncExitFeeForRole: same fix

1080/1080 forge tests pass.
@jhfnetboy jhfnetboy requested a review from fanhousanbu as a code owner June 28, 2026 11:25
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@clestons clestons left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

APPROVE — fixes are correct; one Medium design concern noted.

P0-2: PaymasterFactory CEI (PaymasterFactory.sol)

State writes (paymasterByOperator, operatorByPaymaster, paymasterList, totalDeployed) now precede _initAndVerify external call in both deployPaymaster and deployPaymasterDeterministic. CEI pattern is correct. The existing nonReentrant modifier already blocks re-entry; the CEI reorder adds hygiene alignment and future-proofs against any removal of nonReentrant. 1080 forge tests pass. ✓

P0-3: Registry unchecked low-level call (Registry.sol)

(bool ok,) = address(GTOKEN_STAKING).call(...) + if (!ok) emit SyncFailed(...) correctly captures the return value. This is consistent with the public syncExitFees() pattern (soft failure). ✓

[Medium — design] P0-3 soft failure leaves Registry/GTokenStaking state inconsistent

When _initRole writes roleConfigs[roleId] = RoleConfig(..., isActive: true, ...) and then the GTokenStaking sync call fails, the role is permanently active in Registry with no corresponding exit-fee entry in GTokenStaking. The Slither report recommended require(ok, string(err)) (hard revert) to guarantee atomicity. The chosen soft-failure approach is intentionally tolerant — syncExitFees() can be called after recovery — but it means the window between role creation and a successful sync leaves exit-fee enforcement undefined in GTokenStaking (may default to 0, letting operators exit penalty-free until synced).

Recommend documenting the recovery path (syncExitFees() after GTokenStaking availability) or at minimum logging the failed roleId prominently so operators don't activate roles while staking is desynced.

Invariant / E2E: 5/5 invariants pass at 128k calls each; 4/4 E2E pass on Sepolia v5.4.1-rc.1. ✓

PK: F1 (re-entry through duplicate-operator path) challenged — blocked by nonReentrant. F2 (soft-failure state inconsistency) confirmed — Medium design concern, not a GA blocker.

PK Summary | 1 round · 1 challenged (F1 rejected) · 1 confirmed (F2 Medium)

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