fix(genesis): byte-exact canonical Safe v1.4.1 suite (restores cross-chain Safe address parity) - #170
fix(genesis): byte-exact canonical Safe v1.4.1 suite (restores cross-chain Safe address parity)#170chasebrownn wants to merge 4 commits into
Conversation
…ete the suite The genesis generator compiled Safe/SafeProxyFactory/CompatibilityFallbackHandler from lib/safe-contracts with the repo toolchain (solc 0.8.x). Canonical Safe v1.4.1 was built with solc 0.7.6, so the genesis contracts sat at the canonical addresses with non-canonical bytes. Because createProxyWithNonce derives proxy addresses via CREATE2 over the factory's embedded proxy creation code, every counterfactual (multichain) Safe creation on TN landed at a different address than the identical call on Ethereum/Sepolia/Base — and reverted GS002 anyway, since v1.4.1 setupModules requires the initializer's delegatecall target (SafeToL2Setup, absent from genesis) to have code. - Vendor byte-exact runtime bytecode captured from Ethereum mainnet (cross- verified against Sepolia + Base) under deployments/genesis/canonical-bytecode/ and etch it instead of compiling; hashes asserted before etching - Complete the canonical suite: SafeL2, SafeToL2Setup, MultiSend, MultiSendCallOnly, SignMessageLib, CreateCall, SafeSingletonFactory - Replicate singleton constructors' threshold=1 storage for etched code - Add parity tests: codehash checks and a real cross-chain Safe creation (Ethereum tx 0x966371ac...) replayed against genesis state reproducing its canonical address 0x6012dBcb...eB03 exactly Applying this to the live Adiri testnet requires a state override at the next hard fork (code is immutable); the yaml alone fixes future networks and mainnet. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add the three remaining registry assets — SimulateTxAccessor (Safe SDK/UI tx simulation), SafeMigration, and SafeToL2Migration — vendored byte-exact from Ethereum mainnet (cross-verified against Base) with hashes asserted before etching, matching the safe-deployments canonical addresses. All 9 previously vendored addresses re-verified against the registry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drop SafeMigration and SafeToL2Migration: no current use on TN (no older-version Safes can exist), and with the Safe Singleton Factory predeployed either can be added later permissionlessly at its canonical address by replaying the official release creation code — no fork needed. Documented in the canonical-bytecode README. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mirror the Multicall3/Arachnid genesis convention per review feedback: - SafeSingletonFactory keyless deployer EOA (0xE1CB04A0...) -> nonce 1, marking its nonce-0 presigned deployment tx as spent - SafeSingletonFactory -> nonce 11 (EIP-161 initial 1 + the 10 suite CREATE2 deployments represented in genesis) - SafeProxyFactory -> nonce 1 (EIP-161 initial; live CREATE2 deployer post-genesis) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Addressed in cee76e6: deployer nonces now set following the existing Multicall3/Arachnid convention — the singleton factory's keyless deployer EOA |
grantkee
left a comment
There was a problem hiding this comment.
Good work @chasebrownn ! LGTM
Huwonk
left a comment
There was a problem hiding this comment.
Approving. I re-derived the factual claims here independently rather than taking the description at its word, and they hold up. Notes below, with one decision I would like on the record before this reaches mainnet genesis.
Verification
- All 11 vendored
.hexfiles are byte-identical to the live Ethereum mainnet deployments (cast codeper address;Safe.hexandSafeL2.hexcompared byte-for-byte rather than by hash). All 11 addresses match thesafe-deploymentsv1.4.1 canonical registry. - Storage replication is exactly right. Reading slots 0-8 of all ten suite contracts on mainnet, the only non-zero value anywhere is slot 4 = 1 on
SafeandSafeL2._etchCanonicalSingletonreplicates precisely that and nothing else, so no constructor effect is being silently dropped. - The test vector is real.
0x6012dBcb...eB03on mainnet carries the 8 owners encoded inMULTICHAIN_SAFE_INITIALIZER, threshold 2, and singleton0x41675C...(correct, sinceSafeToL2Setupno-ops on chainid 1). precompile-config.yamlregenerates byte-identically from the script on a clean checkout, so the committed file really is the generator's output.- Full suite on the branch: 339 passed, 0 failed, 2 skipped.
- The premise reproduces on live Adiri. The singleton at
0x41675C...ends in736f6c634300081a(solc 0.8.26) against736f6c63430007060033(0.7.6) on mainnet, and0xfd0732...has no code at all, matching the drift called out in the description. - Node side is clear.
crates/config/src/genesis.rsin telcoin-network honors thenoncefield, nothing there hardcodes these addresses or an account count, andtest_precompile_genesis_accountscompares the yaml against itself, so the 11 new entries do not break it. SafeSingletonFactory's runtime bytes are identical to the Arachnid proxy already in genesis, which is exactly what the same deterministic-deployment-proxy source should produce when deployed by a keyed EOA instead of via Nick's method. A useful independent signal that the vendored bytes are what they claim to be.
The assertEq hash guard does revert in script context on forge-std v1.15.0 (it routes to the vm.assertEq cheatcode), so "fails loudly" is accurate rather than aspirational.
One decision to record before mainnet genesis freezes
The governance Safe still points at the L1 Safe singleton rather than SafeL2, and after genesis that becomes permanent:
SafeToL2Migration.migrateToL2isonlyNonceZero("Safe must have not executed any tx"), so it only ever works as a Safe's very first transaction. Adiri's0x...07a0is already at Safe nonce 2. Adding the migration helpers later through the predeployed Singleton Factory, as the README suggests, does not rescue this particular account.- Every Safe created on TN after genesis flows through
SafeToL2SetupontoSafeL2, so the governance Safe would end up the only Safe on the chain that never emits the events the Safe Transaction Service indexes.
The change is one argument in instantiateGovernanceSafe and moves only slot 0 of 0x...07a0 in the yaml. Since Adiri needs a fork for this PR regardless, that fork is the cheapest moment to take it. Not a regression against master either way, so I am not blocking on it, but the choice should be deliberate and the reasoning should land in the canonical-bytecode README.
Deployment note worth sharpening
"Live Adiri needs a hard-fork state override" should say explicitly that the override is code-only at the singleton, factory and handler addresses, and must not touch storage at any live Safe proxy. Re-applying the genesis account for 0x...07a0 wholesale would also rewrite its owner set, threshold and Safe nonce back to genesis values. Adiri's Safe happens to still match genesis today (7 owners, threshold 3), so nothing would be lost right now, but that is luck rather than design, and the next fork may not be so lucky.
Smaller items
- Worth adding
deployments/genesis/canonical-bytecode/*.hex -textto.gitattributes.core.autocrlfis active in this repo. The files carry no trailing newline today so nothing is normalized, but a single stray newline would makevm.parseBytesthrow, and that failure would read as tampering rather than as a line-ending problem. - The 8 new suite addresses are absent from
deployments-testnet.jsonanddeployments-mainnet.json, and testnet already listsCompatibilityFallbackHandlerat an address with no code on Adiri. Follow-up issue rather than a blocker, alongside the CreateX predeploy idea.
Merge order
#171 is stacked here and touches only files this PR introduces, so a squash-merge of this branch will force it to re-conflict. Cleanest path is to fold its three commits in and merge one PR. Its nonce change is correct, and this should not land with the nonce: 11 convention still in the file.
Problem
The genesis generator compiles
Safe,SafeProxyFactory, andCompatibilityFallbackHandlerfromlib/safe-contractswith the repo's solc (0.8.x). Canonical Safe v1.4.1 was released with solc 0.7.6, so TN genesis ships the canonical addresses with non-canonical bytecode (verifiable on Adiri today: the on-chain metadata trailer at0x41675C…/0x4e1DCf7…names solc 0.8.26; canonical chains name 0.7.6).Two user-facing breakages, confirmed on live Adiri:
createProxyWithNoncederives the proxy address via CREATE2 over the factory's embedded proxy creation code. Recompiled factory ⇒ different creation code ⇒ every counterfactual/multichain Safe lands at a different address on TN than the identical call on Ethereum/Sepolia/Base. Real example: the Telcoin deployer/governance Safe0x6012dBcb…eB03(Ethereum tx0x966371ac…, replayed byte-identically on Sepolia) computes to0xd778877A…9852through the genesis factory.GS002. Safe v1.4.1setupModulesrequires the initializer's delegatecall target to have code; Safe{Wallet}'s default multichain initializer targetsSafeToL2Setup(0xBD89A1CE…), which is not in genesis. (Related drift:deployments-testnet.jsonlistsCompatibilityFallbackHandler, but live Adiri has no code at0xfd0732….)Fix
cast code, cross-verified byte-identical on Sepolia + Base) underdeployments/genesis/canonical-bytecode/, with provenance + expected keccak256 per file in its README. The generator now etches these bytes instead of compiling — the same pattern already used for Multicall3, the Arachnid CREATE2 factory, and the EIP-2935/4788 system contracts. Hashes are asserted before etching.SafeL2,SafeToL2Setup,MultiSend,MultiSendCallOnly,SignMessageLib,CreateCall,SimulateTxAccessor— plus theSafeSingletonFactory. Deliberately omitted:SafeMigration/SafeToL2Migration(no current use on TN; addable later permissionlessly through the predeployed Singleton Factory at their canonical addresses, no fork needed) (Safe's deterministic CREATE2 factory — with it in genesis, future canonical Safe contracts can be added permissionlessly at parity addresses with no fork).threshold = 1, slot 4) for the etchedSafe/SafeL2, preserving the can't-setup-the-singleton guard (regression-tested).to = 0) — it now simply flows through the canonical factory/impl, so its proxy runtime bytes become canonical too. All prior storage slots/values are unchanged in the regenerated yaml.Verification
test/genesis/GenesisSafeCanonicalParity.t.sol:0x6012dBcb…eB03creation calldata against the genesis state reproduces that exact address, with SafeToL2Setup switching the singleton to SafeL2 (matching its state on other chains)setup()-hijacked (GS200)GenesisSafeConfig.t.solpasses unchanged — canonical bytecode is a behavioral drop-in (same source, same storage layout).anvil --fork-url https://rpc.adiri.tel,anvil_setCodethe 5 core addresses with the vendored bytes, then the0x6012…creation call succeeds at the canonical address (revertGS002+0xd778…without them).Deployment notes
deployments-mainnet.jsonis the generator's source of truth, so mainnet genesis is affected and this should land before it freezes).0x4e1DCf7…/0x41675C…is immutable on-chain, so the recompiled contracts can't be replaced by transactions. The existing governance Safe at0x…07a0keeps working across the swap (same source version/storage layout; its singleton pointer is untouched).deployments-testnet.json/deployments-mainnet.jsonintentionally not extended with the new suite addresses (theDeploymentsstruct abi-decode is order-sensitive); can follow up if desired.0xba5Ed…) for mainnet genesis — on Adiri we deployed it via its presigned pre-EIP-155 tx, but a predeploy avoids that dance on mainnet.