feat(contracts): Safe-multisig PKP owner backfill tooling + verifier (#575)#579
feat(contracts): Safe-multisig PKP owner backfill tooling + verifier (#575)#579clawdbot-glitch003 wants to merge 4 commits into
Conversation
…575) The #575 backfill has to run through the diamond owner, which on Base is a 2-of-4 Safe. Add tooling to generate Safe Transaction Builder JSON and to verify it against on-chain state before signing. Ownership is reconstructed from account enumeration (listPkps), NOT from WalletDerivationRegistered events: ~436 PKPs were migrated into the diamond's storage without emitting an event, so an event-only scan (the original backfill-pkp-owners task) silently missed them. listPkps reads the exact pkpData mapping the node uses to sign, so it covers every PKP and is the authoritative owner source. A pkpId held by >1 account is an on-chain hijack (none currently) and is excluded unless --allow-conflicts, which then disambiguates via first-registrant events. - tasks/lib/pkp-owners.ts: shared ownership reconstruction, Safe batch build, and calldata/MultiSend decode used by generator and verifier. - pkp-backfill:gen-safe: emit safe-backfill-NN.json (multisend-bundled calls, sized to Base's block gas limit) + manifest. - pkp-backfill:verify-safe: decode the JSON and check every binding against account-enumeration ownership + live getPkpOwnerMaster + full coverage. - backfill-pkp-owners (EOA path): switched to account enumeration too, fixing the same event-only gap. - tests: ABI-drift guard (selectors/topic vs compiled facets), encode/decode round-trip, MultiSend unwrap, bad-selector rejection. - tasks/README-pkp-backfill.md: operator runbook. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eGas cap The EOA backfill path failed with "missing revert data" on estimateGas for batches over ~500 pairs. Root cause: the transaction is valid (eth_call succeeds for 1000 pairs at 40M gas), but Alchemy caps eth_estimateGas simulation at ~13-20M gas, so ethers' auto-estimation runs out of gas and returns the no-data OOG error. estimateGas(500)=13M works, estimateGas(750+) fails. Pass an explicit gasLimit (estimateBackfillGas * 1.25, ~33M for a 1000-batch, well under Base's 400M block limit) so the send never calls estimateGas. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The pinned 33M gasLimit fixed estimateGas but the sequencer then rejected the send with "exceeds max transaction gas limit" — Base enforces the EIP-7825 per-transaction cap of 16,777,216 gas (2^24), which also explains where the estimateGas ceiling sat. - Default batch size 1000 -> 450 pairs (~15M gasLimit with headroom, under the cap; ~32 txs for the full backfill). - Adaptive splitting as a backstop: on a "max transaction gas" rejection the batch is halved and retried. Rejected sends are never mined (no gas, no nonce), so splitting is free, and a 1-pair failure still aborts loudly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The one-time PKP owner backfill is complete on Base (all 14,070 pre-existing PKPs bound; 0 unbound, 0 conflicts). Remove the migration machinery from the contract in the next facet upgrade: - WritesFacet: drop backfillPkpOwners() and the PkpOwnerBackfilled event. - ViewsFacet.getWalletDerivation: remove the `owner == 0` compatibility fallback so enforcement is unconditional (fail-closed). registerWalletDerivation always sets the owner when it writes pkpData and the backfill bound every legacy PKP, so a non-zero derivation always has a non-zero owner; owner == 0 now reverts instead of serving the path. - Keep getPkpOwnerMaster (ongoing audit) and the core first-owner binding. - Update Foundry tests (drop backfill test; owner==0 test now asserts fail-closed), DiamondDeploy selectors, and regenerate alloy bindings. The Safe/EOA backfill tasks are kept as the historical record of the migration and marked as such; their on-chain target no longer exists post-upgrade. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Added: contract cleanup (this becomes the next facet upgrade)The one-time backfill is complete on Base mainnet — all 14,070 pre-existing PKPs are bound (verified 0 unbound, 0 conflicts, 0 bound-to-non-owner across all 486 accounts). So this PR now also removes the spent migration machinery from the contract, to ship in the next facet upgrade:
The Safe/EOA backfill tasks are kept as the historical record of how the migration was run and marked HISTORICAL — their on-chain target no longer exists after this upgrade's cut. Verification: Deploy note: this requires a diamondCut that (a) replaces WritesFacet (drops the |
|
@glitch003 - do you want this committed, or can we close ? |
Follow-up to #577 (merged). Operational tooling to actually run the #575 backfill through the diamond owner Safe, plus an independent verifier — and a correctness fix to the ownership source.
Context from running it against Base mainnet
The diamond
0xaAaA…7Aais live with the #575 facets (getPkpOwnerMasterpresent). Owner is a 2-of-4 Safe (0xF688…1098);configOperatoris an EOA. There are 14,070 PKPs across 485 accounts, all currently unbound, zero multi-account conflicts (no hijack has landed on-chain).The correctness fix: enumerate accounts, don't scan events
While validating, I found the event-only approach (the
backfill-pkp-ownerstask from #577) is incomplete: 436 PKPs exist in the on-chainallPkpIdsledger with noWalletDerivationRegisteredevent — they were migrated into the diamond's storage at deployment without emitting.allPkpIdsAt(1)(the very first PKP) is one of them. An event-only scan silently drops all 436, and its coverage check couldn't catch the gap because it only looked at event-derived pkpIds.Fix: reconstruct ownership from account enumeration (
listPkps) — the exactpkpDatamapping the node reads to sign. It covers every PKP (migrated + evented) and is authoritative. A pkpId held by >1 account is an on-chain hijack (none exist now) and is excluded unless--allow-conflicts, which then disambiguates via first-registrant events. The old EOA task is switched to the same source.What's here
tasks/lib/pkp-owners.ts— shared: account-enumeration ownership, Safe batch build, calldata + MultiSend decode.pkp-backfill:gen-safe— emitssafe-backfill-NN.json(Safe Transaction Builder format, multisend-bundled, sized to Base's 400M block limit) + a manifest.pkp-backfill:verify-safe— decodes the JSON and checks every binding against account-enumeration ownership, livegetPkpOwnerMaster(unbound or already-correct), and full coverage (no unbound PKP missing). This is the "check the Safe JSON against on-chain" tool.backfill-pkp-owners(EOA path) — switched to account enumeration, fixing the 436-miss gap.tasks/README-pkp-backfill.md— operator runbook.Verified end-to-end against mainnet (read-only)
Generated 8 Safe files covering all 14,070 bindings (~53M gas each), then
verify-safe:Spot-checked
allPkpIdsAt(1)(0x6020…, previously missed): now bound to its holding account's hash.Generated JSON lands in
.context/pkp-backfill/(gitignored); not committed.Deploy note
backfillPkpOwnersis idempotent (skip-if-set), so the 8 Safe txs can be signed/executed in any order and re-run safely. Re-runverify-safeafter execution — every pkpId should report "already correct" and 0 remaining.🤖 Generated with Claude Code