Skip to content

Fix/genesis nonce parity and docs - #171

Open
grantkee wants to merge 3 commits into
fix/canonical-safe-genesisfrom
fix/genesis-nonce-parity-and-docs
Open

Fix/genesis nonce parity and docs#171
grantkee wants to merge 3 commits into
fix/canonical-safe-genesisfrom
fix/genesis-nonce-parity-and-docs

Conversation

@grantkee

Copy link
Copy Markdown
Contributor

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.

  • Every predeploy with contract code now gets nonce 1. EIP-161 gives a contract account nonce 1 at creation, and all of these sit at nonce 1 on Sepolia. Genesis was writing three different conventions at once: 0 for nine Safe contracts, 1 for SafeProxyFactory, and 11 for SafeSingletonFactory.
  • This is load-bearing for two accounts, not just tidiness. CreateCall.performCreate called directly does a CREATE from CreateCall's own account, and a Safe that delegatecalls CreateCall does a CREATE from the Safe's account — the governance Safe at 0x…07a0 included. Both derive 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 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 is at 10619 on Sepolia. Nothing observable corresponds to 11.
  • Same fix applied to the pre-existing predeploys (second commit, droppable on its own): Multicall3, the Arachnid CREATE2 proxy, and the EIP-2935 / EIP-4788 system contracts were all still on nonce 0. All four are nonce 1 on Sepolia, and EIP-2935 and EIP-4788 both specify "nonce": "0x01" in the genesis allocation they recommend — so 0 was wrong on its own terms, not merely inconsistent with the Safe suite.
  • The TEL precompile at 0x…07e1 deliberately 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.
  • Fixed the re-verify command in canonical-bytecode/README.md, which never worked. cast code terminates its output with a newline and the .hex files do not, so the documented cast code … | diff - Safe.hex always reports a difference. Anyone following the procedure would have concluded the vendored bytes were tampered with.
  • Corrected the immutables list, wrong in both directions. The contracts that bake address(this) into 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 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.yaml touches only nonce: lines, and all 11 vendored contracts remain byte-exact against Sepolia, mainnet and Base. forge test is 339 passed / 0 failed / 2 skipped.

I did not fix here: the governance Safe still points at the L1 Safe singleton rather than SafeL2.

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 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.

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 11 was indeed invented. Mainnet has SafeSingletonFactory at nonce 11200 and SafeProxyFactory at 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 code re-verify bug is real. Its output is newline-terminated and the .hex files are not, so the documented diff always 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, MultiSend and SimulateTxAccessor bake address(this) into their runtime bytes; SignMessageLib v1.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:

  1. 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.
  2. EIP-2935 and EIP-4788 specify "nonce": "0x01" outright, so 0 was wrong on its own terms.
  3. The governance Safe at 0x...07a0, which this PR also moves from 0 to 1. That one genuinely is load-bearing: a Safe delegatecalling CreateCall does the CREATE from its own account, and a fresh Safe proxy is nonce 1 everywhere else, 0x6012dBcb...eB03 on 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.

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.

Cleanup for mainnet genesis

2 participants