fix(p0-2,p0-3): CEI order in PaymasterFactory + Registry unchecked call checks#319
fix(p0-2,p0-3): CEI order in PaymasterFactory + Registry unchecked call checks#319jhfnetboy wants to merge 4 commits into
Conversation
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.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
clestons
left a comment
There was a problem hiding this comment.
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)
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_initAndVerifyexternal callsdeployPaymasterDeterministic: same fix appliednonReentrantalready prevents re-entry; this brings the code into textbook CEI alignmentP0-3 — Registry unchecked low-level call (
Registry.sol)_initRole:(bool ok,) = address(GTOKEN_STAKING).call(...)— return value captured;emit SyncFailedon failure_syncExitFeeForRole: same pattern — now consistent with the existing publicsyncExitFees()implementationTest plan
forge test— 1080/1080 pass, 0 failed