Skip to content

fix(genesis): byte-exact canonical Safe v1.4.1 suite (restores cross-chain Safe address parity) - #170

Open
chasebrownn wants to merge 4 commits into
masterfrom
fix/canonical-safe-genesis
Open

fix(genesis): byte-exact canonical Safe v1.4.1 suite (restores cross-chain Safe address parity)#170
chasebrownn wants to merge 4 commits into
masterfrom
fix/canonical-safe-genesis

Conversation

@chasebrownn

@chasebrownn chasebrownn commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Problem

The genesis generator compiles Safe, SafeProxyFactory, and CompatibilityFallbackHandler from lib/safe-contracts with 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 at 0x41675C…/0x4e1DCf7… names solc 0.8.26; canonical chains name 0.7.6).

Two user-facing breakages, confirmed on live Adiri:

  1. Cross-chain Safe address parity is broken. createProxyWithNonce derives 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 Safe 0x6012dBcb…eB03 (Ethereum tx 0x966371ac…, replayed byte-identically on Sepolia) computes to 0xd778877A…9852 through the genesis factory.
  2. Multichain Safe creations revert GS002. Safe v1.4.1 setupModules requires the initializer's delegatecall target to have code; Safe{Wallet}'s default multichain initializer targets SafeToL2Setup (0xBD89A1CE…), which is not in genesis. (Related drift: deployments-testnet.json lists CompatibilityFallbackHandler, but live Adiri has no code at 0xfd0732….)

Fix

  • Vendor byte-exact canonical runtime bytecode (captured from Ethereum mainnet via cast code, cross-verified byte-identical on Sepolia + Base) under deployments/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.
  • Complete the canonical v1.4.1 suite at canonical addresses per the official safe-deployments registry: SafeL2, SafeToL2Setup, MultiSend, MultiSendCallOnly, SignMessageLib, CreateCall, SimulateTxAccessor — plus the SafeSingletonFactory. 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).
  • Replicate singleton constructor storage (threshold = 1, slot 4) for the etched Safe/SafeL2, preserving the can't-setup-the-singleton guard (regression-tested).
  • Governance Safe generation is unchanged in inputs (same owners/threshold/handler, 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:
    • codehashes of all 10 contracts == canonical mainnet deployments
    • end-to-end parity: replaying the real 0x6012dBcb…eB03 creation calldata against the genesis state reproduces that exact address, with SafeToL2Setup switching the singleton to SafeL2 (matching its state on other chains)
    • singletons cannot be setup()-hijacked (GS200)
    • MultiSendCallOnly functional through a Safe delegatecall batch
  • Existing GenesisSafeConfig.t.sol passes unchanged — canonical bytecode is a behavioral drop-in (same source, same storage layout).
  • Full suite: 339 passed / 0 failed.
  • Independently reproducible on a Adiri fork: anvil --fork-url https://rpc.adiri.tel, anvil_setCode the 5 core addresses with the vendored bytes, then the 0x6012… creation call succeeds at the canonical address (revert GS002 + 0xd778… without them).

Deployment notes

  • This yaml only fixes networks created from it (devnets, future testnets, mainnet — deployments-mainnet.json is the generator's source of truth, so mainnet genesis is affected and this should land before it freezes).
  • Live Adiri needs a hard-fork state override: code at 0x4e1DCf7…/0x41675C… is immutable on-chain, so the recompiled contracts can't be replaced by transactions. The existing governance Safe at 0x…07a0 keeps working across the swap (same source version/storage layout; its singleton pointer is untouched).
  • deployments-testnet.json/deployments-mainnet.json intentionally not extended with the new suite addresses (the Deployments struct abi-decode is order-sensitive); can follow up if desired.
  • Possible follow-up: also predeploy CreateX (0xba5Ed…) for mainnet genesis — on Adiri we deployed it via its presigned pre-EIP-155 tx, but a predeploy avoids that dance on mainnet.

chasebrownn and others added 3 commits August 24, 2026 11:21
…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>
@chasebrownn
chasebrownn marked this pull request as ready for review August 24, 2026 22:36
@chasebrownn
chasebrownn requested review from Huwonk and grantkee August 24, 2026 22:36
@chasebrownn chasebrownn self-assigned this Aug 24, 2026
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>
@chasebrownn

Copy link
Copy Markdown
Collaborator Author

Addressed in cee76e6: deployer nonces now set following the existing Multicall3/Arachnid convention — the singleton factory's keyless deployer EOA 0xE1CB04A0…3cBC37 gets nonce: 1 (its nonce-0 presigned tx marked spent), SafeSingletonFactory gets nonce: 11 (EIP-161 initial 1 + the 10 suite CREATE2 deployments represented in genesis), and SafeProxyFactory gets nonce: 1 (EIP-161 initial — it's a live CREATE2 deployer post-genesis). The non-deployer suite contracts keep nonce: 0 per the file's existing convention for predeployed contracts — happy to bump those to 1 as well for strict EIP-161 faithfulness if preferred.

@grantkee grantkee left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Good work @chasebrownn ! LGTM

@Huwonk Huwonk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 .hex files are byte-identical to the live Ethereum mainnet deployments (cast code per address; Safe.hex and SafeL2.hex compared byte-for-byte rather than by hash). All 11 addresses match the safe-deployments v1.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 Safe and SafeL2. _etchCanonicalSingleton replicates precisely that and nothing else, so no constructor effect is being silently dropped.
  • The test vector is real. 0x6012dBcb...eB03 on mainnet carries the 8 owners encoded in MULTICHAIN_SAFE_INITIALIZER, threshold 2, and singleton 0x41675C... (correct, since SafeToL2Setup no-ops on chainid 1).
  • precompile-config.yaml regenerates 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 in 736f6c634300081a (solc 0.8.26) against 736f6c63430007060033 (0.7.6) on mainnet, and 0xfd0732... has no code at all, matching the drift called out in the description.
  • Node side is clear. crates/config/src/genesis.rs in telcoin-network honors the nonce field, nothing there hardcodes these addresses or an account count, and test_precompile_genesis_accounts compares 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.migrateToL2 is onlyNonceZero ("Safe must have not executed any tx"), so it only ever works as a Safe's very first transaction. Adiri's 0x...07a0 is 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 SafeToL2Setup onto SafeL2, 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 -text to .gitattributes. core.autocrlf is active in this repo. The files carry no trailing newline today so nothing is normalized, but a single stray newline would make vm.parseBytes throw, and that failure would read as tampering rather than as a line-ending problem.
  • The 8 new suite addresses are absent from deployments-testnet.json and deployments-mainnet.json, and testnet already lists CompatibilityFallbackHandler at 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.

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.

3 participants