Fix/genesis nonce parity and docs - #171
Conversation
Every one of these contracts carries nonce 1 on Ethereum, Sepolia and Base - EIP-161 gives a contract account nonce 1 the moment it is created. Genesis was writing 0 for nine of them, 1 for SafeProxyFactory, and 11 for the singleton factory, so the file asserted three different conventions at once. For most of the suite that is cosmetic, but two accounts derive addresses from it. CreateCall.performCreate called directly does a CREATE from CreateCall's own account, and a Safe that delegatecalls CreateCall - the governance Safe included - does a CREATE from the Safe's account. Both take the address from keccak256(rlp([account, nonce])), so a genesis nonce of 0 puts the first deployment through either path one slot off wherever the identical call lands on every other chain. The 11 on the singleton factory was invented: it was documented as "EIP-161 initial 1 + the 10 suite CREATE2 deployments", but CREATE2 never reads the nonce, and the live factory sits at 10619 on Sepolia. Nothing observable corresponds to 11. Replaces the sharedNonce variable and the two literals with PREDEPLOY_NONCE, and regenerates the yaml. Also drops the "keyless" description of 0xE1CB04A0...3cBC37 - that is Safe's own signing key, not a Nick's-method address, so burning nonce 0 guards against a chain-id-matching replay rather than an unowned presigned transaction.
…loys Multicall3, the Arachnid CREATE2 proxy, and the EIP-2935 / EIP-4788 system contracts were all still on nonce 0. All four sit at nonce 1 on Sepolia, and EIP-2935 and EIP-4788 both specify "nonce": "0x01" in the genesis allocation their spec recommends, so 0 was wrong on its own terms - not just inconsistent with the Safe suite. Routes the four hardcoded entries through _writePredeployNonceAndBalance so PREDEPLOY_NONCE is the single source of truth for the file. The deployer-EOA entries keep their own literal nonce: 1, which asserts something different - that the presigned deployment tx has been spent - and should not silently track the predeploy constant. The TEL precompile at 0x...07e1 stays on nonce 0. Its "0xfe" code is a marker so EXTCODESIZE is non-zero for a native Rust handler, not a deployed contract, and it has no cross-chain counterpart to match.
Two errors in the vendored-bytecode provenance notes, which exist to let a reviewer independently confirm the bytes: The re-verify command never worked. `cast code` terminates its output with a newline and the .hex files do not, so `cast code … | diff - Safe.hex` always reports a difference - anyone following the documented procedure would conclude the bytes had been tampered with. Switched to a `$(...)` comparison, which strips both, and said why. The immutables list was wrong in both directions. The contracts that bake address(this) into their runtime bytes are SafeToL2Setup (SELF), MultiSend (multisendSingleton) and SimulateTxAccessor (accessorSingleton). SignMessageLib was listed but has no such immutable in v1.4.1; SimulateTxAccessor was missing from the copy in the script. That note is the justification for "these bytes are only valid at this exact address", so it should name the right contracts. Also updates the nonce paragraph to describe what the generator now does.
Huwonk
left a comment
There was a problem hiding this comment.
Reviewed alongside #170. The nonce change is right and both doc corrections are real catches. Confirmed each independently, with one ask on the rationale.
Confirmed
- The
11was indeed invented. Mainnet hasSafeSingletonFactoryat nonce 11200 andSafeProxyFactoryat 151963. Worth noting the mechanism precisely: CREATE2 does bump the creator's nonce, it just never reads it for address derivation, which is why a pure-CREATE2 factory climbs into six figures. Nothing observable corresponds to 11, as stated. - Nonce 1 is right for the rest. All ten suite contracts, Multicall3, the Arachnid proxy and both system contracts sit at nonce 1 on mainnet, and EIP-2935 and EIP-4788 specify
"nonce": "0x01"in their recommended genesis allocation. - The
cast codere-verify bug is real. Its output is newline-terminated and the.hexfiles are not, so the documenteddiffalways reported a difference. I hit exactly that while checking the vendored bytes and had to strip the newline to compare. - The immutables correction is right.
SafeToL2Setup,MultiSendandSimulateTxAccessorbakeaddress(this)into their runtime bytes;SignMessageLibv1.4.1 does not. - Regenerated yaml is byte-identical to the committed file, and the diff against the base branch is 16
nonce:lines and nothing else. Suite is 339 passed, 0 failed, 2 skipped.
One correction to the rationale
The CreateCall.performCreate argument, which leads both the PR body and the README paragraph, does not hold. CreateCall is at nonce 95 on mainnet, so "the Nth direct performCreate lands at the same address on every chain" is already false and unachievable by construction, not merely off by one. Cross-chain parity for that account is not something we can have at any nonce.
The arguments that do hold, and that the paragraph should lead with instead:
- EIP-161 gives a contract account nonce 1 at creation, and every one of these carries nonce 1 on live chains. Genesis should not be inventing a state the EVM never produces.
- EIP-2935 and EIP-4788 specify
"nonce": "0x01"outright, so 0 was wrong on its own terms. - The governance Safe at
0x...07a0, which this PR also moves from 0 to 1. That one genuinely is load-bearing: a Safe delegatecallingCreateCalldoes the CREATE from its own account, and a fresh Safe proxy is nonce 1 everywhere else,0x6012dBcb...eB03on mainnet included.
Point 3 is the surviving version of the CreateCall claim, and it is the stronger argument anyway. Worth fixing in both places before this lands, since the README is the artifact a future maintainer will actually read.
Smaller
0x...07e1 staying at nonce 0 leaves the file asserting two conventions again, which was this PR's own opening complaint. The reasoning for the exception is sound, it just belongs in a comment beside instantiateTelPrecompile rather than only in the PR description.
On the governance Safe singleton
Noted that this deliberately leaves 0x...07a0 on the L1 Safe singleton. Worth flagging that it is not deferrable: SafeToL2Migration.migrateToL2 is onlyNonceZero, and Adiri's Safe is already at Safe nonce 2, so the predeployed Singleton Factory does not give us a later escape hatch for that account. Detailed on #170 since it is a genesis decision for that branch to settle.
Merge order
Given how squash-merge behaves here, folding these three commits into #170 and merging one PR avoids a re-conflict. Flagged the same over there.
Genesis predeploy nonce parity + provenance doc fixes
Stacked on
fix/canonical-safe-genesis. Follow-up from reviewing that branch against live Sepolia (eth-sepolia), cross-checked on Ethereum mainnet and Base.SafeProxyFactory, and 11 forSafeSingletonFactory.CreateCall.performCreatecalled directly does a CREATE fromCreateCall's own account, and a Safe that delegatecallsCreateCalldoes a CREATE from the Safe's account — the governance Safe at0x…07a0included. Both derive the address fromkeccak256(rlp([account, nonce])), so a genesis nonce of 0 puts the first deployment through either path one slot off wherever the identical call lands on every other chain.11was invented. It was documented as "EIP-161 initial 1 + the 10 suite CREATE2 deployments", but CREATE2 never reads the nonce, and the live factory is at 10619 on Sepolia. Nothing observable corresponds to 11."nonce": "0x01"in the genesis allocation they recommend — so 0 was wrong on its own terms, not merely inconsistent with the Safe suite.0x…07e1deliberately stays on nonce 0. Its0xfecode is a marker soEXTCODESIZEis non-zero for a native Rust handler, not a deployed contract, and it has no cross-chain counterpart to match.canonical-bytecode/README.md, which never worked.cast codeterminates its output with a newline and the.hexfiles do not, so the documentedcast code … | diff - Safe.hexalways reports a difference. Anyone following the procedure would have concluded the vendored bytes were tampered with.address(this)into runtime bytes areSafeToL2Setup(SELF),MultiSend(multisendSingleton) andSimulateTxAccessor(accessorSingleton).SignMessageLibwas listed but has no such immutable in v1.4.1;SimulateTxAccessorwas missing from the copy in the generator. That note is the justification for "these bytes are only valid at this exact address", so it should name the right contracts.Behavior is unchanged apart from the nonces: regenerating
precompile-config.yamltouches onlynonce:lines, and all 11 vendored contracts remain byte-exact against Sepolia, mainnet and Base.forge testis 339 passed / 0 failed / 2 skipped.I did not fix here: the governance Safe still points at the L1
Safesingleton rather thanSafeL2.