fix(paymaster): auto-whitelist the real ZkEmailInvites claim selectors (#188) - #190
Merged
Conversation
OrgDeployer._appendZkEmailInvitesRules derived its four selectors by hashing PRE-Blocker-2 signature strings whose ZkEmailProof tuple ended in `string`. The live struct ends in `bytes32 fromDomainHash`, so all four were wrong: stale 0xc8864f92 / 0x50b2f726 / 0xcc1866ac / 0xebd847f2 (no function answers these) live 0x24b5e3ba / 0x8c149bab / 0x6108482e / 0x998dc9d6 Every new org deployed with autoWhitelistContracts + zk-email enabled therefore got four dead paymaster rules and zero sponsorship on the real claim entrypoints — gasless zk-email claims fail rule validation. Both the strings and the struct change landed in the same squashed commit (#170), so the drift was introduced mid-PR and never caught. Fixes #188. - src/OrgDeployer.sol: the four selectors are now compiler-derived `ZkEmailInvites.<fn>.selector`, so they cannot drift from the ABI again. Hardening in the same builder: `_appendEducationHubRules` returns the advanced index like every sibling helper (the caller no longer hand-advances `i += 4`, which would silently overwrite the zk slots if a rule were added), and an explicit `RuleCountMismatch` invariant replaces the incidental loudness that previously came only from PaymasterHub's zero-address guard. - test/OrgDeployerPaymasterRules.t.sol (new): harness over the internal pure builder asserting both-direction set equality of all 52 (target, selector) pairs against `.selector` values, per-branch counts (52/48/48/44), the zk gas hints, and a literal tripwire on the four zk selectors. This pins all 49 signature strings — the previous guard, testPaymasterSelectorAccuracy, covered only 23 and, being a copy of the same literals, could not catch source drift at all. - test/ZkEmailOrgFlow.t.sol: the test that should have caught this was tautological by its own admission ("copied verbatim from OrgDeployer"), so it asserted the bug and stayed green. It now derives selectors from the ABI. The negative test probed address(0), where a rule can never exist; it now probes real deployed modules. - script/upgrades/UpgradeOrgDeployerZkEmailRules.s.sol (new): OrgDeployer v19 rollout on the fee-free path (Satellite.upgradeBeaconDirect on Gnosis, Hub.upgradeBeaconLocal on Arbitrum). Its sims deploy a real fixture org on the fork before AND after the upgrade and read PaymasterHub back, because these selectors are folded constants that never appear literally in the bytecode — only a real deploy shows which rules land. - script/zkemail/IntegrateZkEmailTest6.s.sol: comment only. Its SUPERSEDED constants are the historical record of what was broadcast; the old comment invited copying them forward. Verification: - forge test: 1887 passed, 0 failed. - Fault injection: reintroducing the stale claimRoleByDomain string fails 3 of the 5 new tests and the ZkEmailOrgFlow E2E test; drifting a previously unpinned selector (setFolders) fails the new test while testPaymasterSelectorAccuracy stays green. - FOUNDRY_PROFILE=production sims PASS on both chains: BEFORE (v18): 4/4 stale selectors sponsored, 0/4 real claim entrypoints sponsored AFTER (v19): 4/4 real claim entrypoints sponsored (800k/800k/1.2M/1.2M), 0/4 stale - Version probe (both surfaces, both chains): v19 FREE, impl 0x90BAe532D26100a2106c3b16bEA1Ad27D2286b3A. Storage layout unchanged; production size 22,332 B (2,244 B headroom, 119 B smaller than v18). Co-Authored-By: Claude <noreply@anthropic.com>
Contributor
Coverage Report
Coverage by file
|
… v19 upgrade
`vm.envOr("PRIVATE_KEY", vm.envUint("DEPLOYER_PRIVATE_KEY"))` — the spelling the
older upgrade scripts use — evaluates its default eagerly, so it reverts with
`environment variable "DEPLOYER_PRIVATE_KEY" not found` even when PRIVATE_KEY is
set. Verified both env vars now work, and that Step3_Verify needs neither.
Co-Authored-By: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #188.
The bug
OrgDeployer._appendZkEmailInvitesRulesderived its four selectors by hashing pre-Blocker-2 signature strings whoseZkEmailProoftuple ended instring. The live struct ends inbytes32 fromDomainHash, so all four were wrong:claimRoleByDomain0xc8864f920x24b5e3baclaimRoleByEmail0x50b2f7260x8c149babregisterAndClaimByDomainWithPasskey0xcc1866ac0x6108482eregisterAndClaimByEmailWithPasskey0xebd847f20x998dc9d6Every new org with
autoWhitelistContracts: true+ zk-email enabled got four rules no function answers, and zero sponsorship on the real claim entrypoints — gasless zk-email claims fail rule validation.git log -Sshows both the stale strings and thefromDomainHashstruct landed in the same squashed commit (#170): the drift was introduced mid-PR when Blocker-2 domain binding changedZkEmailProof, andOrgDeployerwasn't updated with it.Blast radius.
_appendZkEmailInvitesRuleshas never run on-chain — both existingZkEmailInvitesmodules (Test6, KUBI on Gnosis) are retrofits whose rules were set by their own scripts. But the zk infra is wired on both chains and the beacon is registered on both, so the bug is armed for the very nextdeployFullOrgWithZkEmail— i.e.script/org/DeployComfiestHouse.s.sol, which has not been broadcast. Land the v19 upgrade before that.The fix
src/OrgDeployer.sol— the four selectors become compiler-derivedZkEmailInvites.<fn>.selector, so they cannot drift from the ABI again. The import is free:OrgDeployeralready depends onZkEmailInvitestransitively viaModulesFactory,.selectoris a compile-time constant, and no creation code is pulled in. Production size actually drops 119 B (22,332 B, 2,244 B of EIP-170 headroom); storage layout is byte-identical.Two hardening changes in the same builder, both prompted by what made this bug invisible:
_appendEducationHubRulesnow returns the advanced index like every sibling helper. It was the only one returningvoidwith the caller hand-advancingi += 4— add a rule there and it silently overwrites the zk slots.if (i != count) revert RuleCountMismatch();replaces the incidental loudness that previously came only from PaymasterHub's zero-address guard.Tests. The test that should have caught this was tautological by its own admission — its comment read "copied verbatim from OrgDeployer", so it re-derived the same wrong constant and asserted the bug. The other guard,
testPaymasterSelectorAccuracy, is a copy of the source literals that never referencesOrgDeployerat all, and covered only 23 of the 49 signature strings.test/OrgDeployerPaymasterRules.t.sol(new) replaces that pattern: a harness over the internalpurebuilder asserting both-direction set equality of all 52(target, selector)pairs against.selectorvalues, per-branch counts (52/48/48/44 — the first assertion of thecountliteral anywhere), the zk gas hints, and a literal tripwire on the four zk selectors so a future struct change fails loudly instead of moving both sides together (live orgs' existing rules would need migrating). No fork, no RPC, runs in ~20 ms.script/upgrades/UpgradeOrgDeployerZkEmailRules.s.sol(new) — OrgDeployer v19 on the fee-free path (Satellite.upgradeBeaconDirecton Gnosis,Hub.upgradeBeaconLocalon Arbitrum; both call the samepoaManager.upgradeBeacon, so per-chain bookkeeping is identical to the Hyperlane form without the 0.005 ETH fee or 5-minute relay — and both sides are fork-simulatable). Steps are idempotent. The sims deploy a real fixture org on the fork before and after the upgrade and readPaymasterHub.getRuleback, because these selectors are folded constants that never appear literally in the bytecode — grepping the impl proves nothing, only a real deploy shows which rules land.script/zkemail/IntegrateZkEmailTest6.s.sol— comment only. ItsSUPERSEDEDconstants are the historical record of what was broadcast; the old comment invited copying them forward.Verification
forge test: 1887 passed, 0 failed.forge fmt --checkclean.FOUNDRY_PROFILE=production forge build --skip testclean.claimRoleByDomainstring → 3 of the 5 new tests fail and theZkEmailOrgFlowE2E test fails;setFolders(one of the 21 previously-unpinned selectors) → the new test fails whiletestPaymasterSelectorAccuracystays green.0x90BAe532D26100a2106c3b16bEA1Ad27D2286b3A. Note the issue says "v18 candidate" — v18 is already live from feat(TaskManager v7): unclaimTask — release a claimed task back to the pool #187, so this ships as v19.Follow-ups (not in this PR)
allowedrules under the stale selectors (verified live), left by the superseded retrofit script and never revoked when the ceremony added the correct ones. Harmless today — no function answers them, and a bogus call burns the same claim budget a real selector would — but it's an unreviewed grant that a future selector collision would auto-sponsor. Cleanup is onesetRulesBatch(allowed=false)via the Satellite; worth folding into the next Test6 admin batch.script/fixes/AddCreateTasksBatchSelectorRules.s.solhardcodes0xc18aa1c9(TaskManager v5createTasksBatch; live is0xf31d148f) with no SUPERSEDED banner — same failure class, re-runnable. Ditto0x48db6f65for the v5updateTaskacross fourscript/fixes/files.🤖 Generated with Claude Code