Skip to content

Genesis follow-ups: correct a migration comment, harden vendored .hex, fix the nonce rationale, record the adiri procedure #176

Description

@Huwonk

Follow-ups from reviewing #170, #171 and #174. None of them change genesis state or behavior, which is why they were not held against #174, but all four are things a future maintainer will read and act on, so they should not be lost.

1. SafeToL2Migration test comment draws the wrong conclusion

In test/genesis/GenesisSafeCanonicalParity.t.sol, test_safeToL2MigrationAsFirstTx carries this doc comment:

A Safe that has already executed any transaction can never use it, which is why migrating the live adiri governance Safe (nonce 2) needs a protocol fork instead.

The first clause is correct: SafeToL2Migration.migrateToL2 is onlyNonceZero, and adiri's governance Safe is at storage nonce 2.

The conclusion is not. SafeMigration.migrateL2Singleton(), predeployed as of #174 and exercised by test_safeMigrationMigratesL1SafeToL2 immediately above, carries only onlyDelegateCall. No nonce guard, no version guard, no check on the current singleton:

function migrateL2Singleton() public onlyDelegateCall {
    singleton = SAFE_L2_SINGLETON;
    emit ChangedMasterCopy(SAFE_L2_SINGLETON);
}

Any Safe sitting on the canonical suite can therefore flip its singleton at any nonce with an ordinary owner-approved delegatecall.

This matters operationally rather than academically. Adiri needs a fork for the bytecode regardless, since the code at the canonical addresses is immutable on-chain. But once that fork places the suite, slot 0 of the governance Safe at 0x...07a0 does not need to be part of the fork's state override. Governance can flip it itself with a 3-of-7 transaction, which is preferable: visible, owner-approved, and auditable, rather than silently rewritten by a fork.

Suggested fix: name SafeMigration as the path available to a used Safe, and drop the "needs a protocol fork" claim.

2. Harden the vendored bytecode files against EOL normalization

.gitattributes currently covers only *.sh. The 13 files under deployments/genesis/canonical-bytecode/ are single-line with no trailing newline, so nothing is normalized today, but core.autocrlf is active for contributors on Windows and a single stray newline would make vm.parseBytes throw. That failure would read as a tampered vendored file rather than as a line-ending problem.

deployments/genesis/canonical-bytecode/*.hex -text

One line, and it removes the whole class of confusion.

3. The CreateCall nonce rationale in canonical-bytecode/README.md does not hold

The nonce paragraph currently reads:

It is load-bearing for CreateCall, whose performCreate does a CREATE from its own account, and for any Safe that delegatecalls it

CreateCall is at nonce 95 on Ethereum mainnet. "The Nth direct performCreate lands at the same address on every chain" is already false and unachievable by construction, not merely off by one, so cross-chain parity for that account is not something available at any nonce.

The three arguments that do survive, and that the paragraph should lead with instead:

  1. EIP-161 gives a contract account nonce 1 at creation, and every one of these carries nonce 1 on Ethereum, Sepolia and Base. Genesis should not invent a state the EVM never produces.
  2. EIP-2935 and EIP-4788 both specify "nonce": "0x01" in the genesis allocation their specs recommend, so 0 was wrong on its own terms.
  3. The governance Safe at 0x...07a0. A Safe that delegatecalls CreateCall does the CREATE from its own account, and a fresh Safe proxy is nonce 1 everywhere else (0x6012dBcb...eB03 on mainnet included). This is the surviving and stronger version of the CreateCall claim.

4. Record the adiri regenesis procedure somewhere in the repo

The constraint currently lives only in #170's PR description, so it will not survive the merge. It belongs beside the genesis files. Two points, and with item 1 above resolved it is short:

  • The override swaps code only, at the singleton, factory, handler and library addresses. It must not re-apply storage at any live Safe proxy: re-applying the genesis account for 0x...07a0 wholesale would reset its owner set, threshold and Safe nonce to genesis values. Adiri's Safe still matches genesis today (3-of-7, the same seven owners), so nothing would be lost right now, but that is luck rather than design.
  • Slot 0 does not need to be in the override at all. Governance flips it afterwards via SafeMigration.migrateL2Singleton().

5. Minor: the drift gate is masked by a failing test

In .github/workflows/test.yml, Check genesis precompile-config.yaml drift runs after Run Forge tests, so a test failure means the drift gate never reports. if: always() on that step makes both signals visible from one run.

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentation

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions