Skip to content

[Issue-3550] feat(supra-extension): predeploy canonical EVM singleton contracts at genesis - #45

Merged
aregng merged 4 commits into
feature/evm_automationfrom
task/issue-3550
Aug 27, 2026
Merged

[Issue-3550] feat(supra-extension): predeploy canonical EVM singleton contracts at genesis#45
aregng merged 4 commits into
feature/evm_automationfrom
task/issue-3550

Conversation

@aregng

@aregng aregng commented Aug 21, 2026

Copy link
Copy Markdown

Extend the genesis transaction generator to deploy Multicall3, the ERC-2470 SingletonFactory, CreateX, and the ERC-1820 Registry at their canonical addresses, alongside the existing CREATE2 factory. Ecosystem tooling (Foundry, hardhat-deploy, ERC-777, account-abstraction stacks, etc.) expects these well-known contracts at fixed addresses on any EVM chain, so they're predeployed the same way the CREATE2 factory already is rather than relying on users/tooling to deploy them after genesis.

Each contract's init-code is embedded as a static binary asset, extracted verbatim from its canonical historical deployment transaction (sender, nonce, and init-code independently verified by decoding the real presigned transactions and re-deriving each address) rather than compiled from vendored Solidity source, since these are third-party contracts we don't own or modify and any compiler/ optimizer difference risks producing bytecode that isn't byte-identical to what's deployed elsewhere.

All five predeploys (Create2Factory + the four new ones) are moved into a dedicated canonical_singletons module, separate from Supra's own system/application genesis contracts, and are unconditional regardless of the full_set config flag since none relate to that feature set. The new GenesisTransactionTags variants are appended after the last existing named variant rather than interleaved, since Ord/serde's enum encoding key off declaration order rather than the explicit discriminant.

Also fixes a pre-existing clippy::derive_ord_xor_partial_ord violation on ContractCustomTag (manual Ord alongside a derived PartialOrd) that was blocking a clean clippy run independent of this change.

Fixed automation payload decoding bug caused by abi_decode API usage on an encoded sequence instead of abi_decode_sequence

… genesis

Extend the genesis transaction generator to deploy Multicall3, the
ERC-2470 SingletonFactory, CreateX, and the ERC-1820 Registry at their
canonical addresses, alongside the existing CREATE2 factory. Ecosystem
tooling (Foundry, hardhat-deploy, ERC-777, account-abstraction stacks,
etc.) expects these well-known contracts at fixed addresses on any EVM
chain, so they're predeployed the same way the CREATE2 factory already
is rather than relying on users/tooling to deploy them after genesis.

Each contract's init-code is embedded as a static binary asset,
extracted verbatim from its canonical historical deployment
transaction (sender, nonce, and init-code independently verified by
decoding the real presigned transactions and re-deriving each address)
rather than compiled from vendored Solidity source, since these are
third-party contracts we don't own or modify and any compiler/
optimizer difference risks producing bytecode that isn't byte-identical
to what's deployed elsewhere.

All five predeploys (Create2Factory + the four new ones) are moved into
a dedicated `canonical_singletons` module, separate from Supra's own
system/application genesis contracts, and are unconditional regardless
of the `full_set` config flag since none relate to that feature set.
The new `GenesisTransactionTags` variants are appended after the last
existing named variant rather than interleaved, since `Ord`/serde's
enum encoding key off declaration order rather than the explicit
discriminant.

Also fixes a pre-existing clippy::derive_ord_xor_partial_ord violation
on `ContractCustomTag` (manual `Ord` alongside a derived `PartialOrd`)
that was blocking a clean clippy run independent of this change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@isaacdoidge

Copy link
Copy Markdown
Collaborator

Reviewed at da341531..pr-45. The core of this change — the provenance of the five predeploys — is correct, and I verified it against chain data rather than the linked sources.

Verified

Each deployer/nonce/init-code tuple was checked against the real canonical deployment transaction:

Contract Address Deployer / nonce Init-code
Arachnid proxy 0x4e59b448… 0x3fab1846… / 0 ✓ 83 B, identical to tx 0xeddf9e61…
ERC-2470 SingletonFactory 0xce0042B8… 0xBb6e024b… / 0 ✓ 340 B, identical to tx 0x803351de…
ERC-1820 Registry 0x1820a4b7… 0xa990077c… / 0 ✓ 2533 B, identical to tx 0xfefb2da5…
CreateX 0xba5Ed099… 0xeD456e05… / 0 ✓ 12054 B, identical to tx 0xceec66d7…
Multicall3 0xcA11bde0… 0x05f32B3c… / 0 ✓ 3840 B, identical to the README presigned blob ✓

Also checked: every deployer.create(0) derivation reproduces its canonical address; each blob's PUSH2 <len> DUP1 PUSH2 <off> PUSH1 0 CODECOPY prologue is arithmetically consistent with the file size, so none is truncated; all runtimes are under EIP-170 and all init-codes under EIP-3860; genesis executes a real TxKind::Create with gas_limit = BLOCK_LIMIT (300M, against CreateX's ~2.4M deposit cost) and value = 0 against CALLVALUE-guarded constructors. Because genesis performs a genuine CREATE rather than writing runtime bytes, CreateX's immutable _SELF resolves to its canonical address — worth stating explicitly, since the embedded assets are init-code and storing them directly would have been silently wrong.

The abi_decodeabi_decode_sequence change is also correct: LibRegistry.sol:39 builds the payload with abi.decode(_payloadTx, (uint128, address, bytes, AccessListEntry[])), i.e. sequence encoding, whereas SolType::abi_decode treats the input as a single-element sequence and so consumed the first word (value) as an offset. Before this fix, any task registered with a non-zero value mis-decoded or failed.

Suggestions

None of these block the change; EVM is pre-release, so the format change above is fine on its own terms.

1. Nothing binds the embedded bytecode to its canonical source. The only guards are byte-length canaries and the deployer.create(0) tests — and as the file's own doc comment notes, the CREATE address depends solely on sender and nonce, not on init-code. So no test observes the bytecode content at all. If createx.bin were regenerated from the 25M-gas presigned variant, or multicall3.bin re-extracted from a recompiled artifact with different metadata, the length could match and every test would stay green — leaving a different contract at a canonical, ecosystem-trusted address. A keccak256 assertion on each expected deployed runtime code (the publicly documented extcodehash) would make the canary exact. The bytes are correct today; this is about locking the property in.

2. ContractCustomTag's Ord disagrees with its derived Eq (transaction.rs:113). Ord compares only nonce; PartialEq/Eq compare (nonce, name). The new PartialOrd correctly delegates to Ord, which is the right fix for the lint, but the type still violates the Ord/Eq contract: a != b while a.cmp(&b) == Equal. Since GenesisTransactionTags is a BTreeMap key and BTreeMap resolves through Ord alone, two Custom tags sharing a nonce with different names collide and one genesis transaction silently overwrites the other, with no error anywhere. Latent today only because SupraNovaContractsGenerator assigns strictly increasing nonces (supra_nova_contracts.rs:308ff) — an invariant that lives in another repository rather than in the type. Either make Ord compare (nonce, name), or reduce Eq to nonce.

3. The decode fix has no test that can detect it. Every test payload uses value == 0, and at zero the two encodings are indistinguishable to the decoder: the offset word is 0, so the body starts at byte 0 and yields the same four head words. The suite therefore passes under either abi_decode or abi_decode_sequence. A future revert of that line would be green in CI while every automation task carrying a non-zero native transfer silently failed to build. Worth a round-trip test with a non-zero value asserting the decoded to/value/input.

4. check_payload_decode (automated_transaction.rs:1375) was left on the old abi_decode while production moved on, and it has no assertions — only println!. It now exercises a path the node no longer uses and cannot fail. Pointing it at TaskPayload::try_from and asserting the decoded fields would make it earn its place.

5. cargo fmt --all --check looks like it will fail the fmt job on canonical_singletons.rs: line 41 (MULTICALL3_CODE split across two lines but fits on one) and line 117 (generate_createx_transaction's single-line GenesisTransaction::create(...) exceeds fn_call_width). The base versions of the other touched files format clean under the same rustfmt, so this appears newly introduced rather than a toolchain mismatch.

6. The tag-ordering comment is imprecise (transaction.rs:158). It says "Ord/serde's enum encoding key off declaration order, not the explicit = N discriminant". Derived Ord on a #[repr(u8)] enum compares discriminant values, which for this enum are the explicit = N; only serde's variant index follows declaration order. The two coincide here purely because the numbering is dense and sequential. The risk worth warning about is the inverse of what's written: a maintainer trusting the comment might renumber the = N values believing them inert and silently reorder genesis deployment. Suggest saying that the discriminants and the declaration order must stay in lockstep, and that the enum should only ever be appended to.

One note outside this diff

The genesis deploy helper on the smr-moonshot side (consensus/execution/src/evm/executor.rs) destructures let ExecResultAndState { mut state, .. } = evm.transact(tx)?, discarding the execution result — so a reverted or halted CREATE contributes no code and genesis continues silently. Gas isn't the trigger here, but this PR takes the predeploy count from one to five including a 12 KB init-code contract, so the consequence of that silence is now "chain launches without CreateX at its canonical address, no error". An assertion per predeploy would be cheap insurance. Happy to raise it separately.

Also worth a line in the PR description explaining the automation decode fix and the bug it addresses — it's unrelated to genesis predeploys, and a reader hitting it in git log would have no context otherwise.

- Embeded Create2Factory init-code as a binary asset
- Add a keccak256 hash assertion per predeploy as a tripwire against
  accidental corruption of the embedded init-code assets. For CreateX,
  the expected hash is the value pcaversaccio/createx's own README
  publishes and tells the community to verify against before trusting a
  CreateX deployment on any chain; for the other four, no such value is
  published by the upstream project, so those hashes are self-computed
  and pinned as a regression guard rather than an externally-published
  reference.

- Ordered canonical singleton predeploys before Supra's own genesis contracts

- Corrected GenesisTransactionTags ordering comment

- Updated automation payload decoding test to cover decoding with
  non-zero value payload

- Made Ord and Eq contracts of ContractCustomTag to agree on equality

- Fixed formatting of the files

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@aregng

aregng commented Aug 25, 2026

Copy link
Copy Markdown
Author

The genesis deploy helper on the smr-moonshot side (consensus/execution/src/evm/executor.rs) destructures let ExecResultAndState { mut state, .. } = evm.transact(tx)?,

This is outdated, during genesis setup transaction execution fails loudly in feature/evm_automation branch: https://github.com/Entropy-Foundation/smr-moonshot/blob/2ce1a97ddbaa3ebb3a16b3b857749ed86586ba59/consensus/execution/src/evm/executor.rs#L267

@isaacdoidge

Copy link
Copy Markdown
Collaborator

Re-reviewed at 04fa9d12. All six points are addressed. I re-verified the substantive ones rather than taking the commit message's word for them.

Verified fixed

keccak assertions. I recomputed all five hashes from the embedded bytes; every pinned value matches. CreateX's is genuinely externally anchored — 0x12ec8615… is the value published in pcaversaccio/createx's README, which I fetched and confirmed. The comment is also honest that the other four are self-computed regression guards rather than published references, which is the right way to describe them.

Bytes unchanged. All four original blobs are still byte-identical to the mainnet deployment-transaction inputs I checked in the first pass, and the newly extracted create2_factory.bin is byte-identical to the hex literal it replaced (itself verified against tx 0xeddf9e61…). Moving it to a binary asset lost nothing, and it makes the five predeploys uniform.

Ord/Eq now agree. The tie-break on name closes the contract violation, and check_tag_ordering covers the equal-nonce/different-name case that was the silent-overwrite path through BTreeMap.

Tag comment corrected, and now accurate: derived Ord compares discriminants, serde keys off declaration position, and the two coincide only because the numbering is dense. The lockstep warning is the right thing to have written down.

check_payload_decode retargeted to abi_decode_sequence, with the fixture's first word changed from 0x00 to 0x11. That second change is the one that matters: with a non-zero value the single-element and sequence encodings are no longer indistinguishable, so a future revert to abi_decode now fails on the .unwrap() instead of passing silently. That was the gap worth closing.

Formatting is clean, and the sweep picked up pre-existing debt in cfg.rs, result.rs, errors.rs and build.rs. Worth noting for reviewers that this is why the diff now reaches into crates/context/interface — all of it is formatting, nothing semantic.

On the reorder

Moving the singletons to discriminants 0–4 and shifting Supra's contracts to 5–21 is safe as far as I can tell:

  • Each singleton deploys from its own account at nonce 0, so they cannot perturb the generator's own nonce sequence.
  • The Supra tags keep their relative order, so that deployer's transactions still execute in ascending-nonce order.
  • Create2Factory remains first, ahead of the create2-based SupraNova deploys, which sort last under Custom.
  • Ordering survives into execution: the BTreeMap is collected into a Vec<GenesisEvmTransaction> in Ord order on the smr-moonshot side.

It does renumber every discriminant, which the new comment itself warns against — but that warning is scoped to "once this chain has live deployments", and pre-release is precisely when this change is free to make.

Two small residuals

Neither is worth holding the PR for.

  1. check_payload_decode still has only println! and no assertions, so it now detects a decode failure but not a silent mis-decode. Asserting the decoded to / value / input would close that last gap.
  2. The comment leans on the discriminants and the declaration order staying in lockstep, but nothing enforces it. A test asserting the expected sort order — and that each variant's serde index equals its discriminant — would make the invariant executable rather than advisory. Worth having precisely because the next person to touch this enum will be reading that comment and trusting it.

I did not build or run the suite (the build script compiles the Solidity contracts and needs the solc toolchain), but recomputing the hashes independently is stronger evidence than running the assertions would have been.

@aregng
aregng merged commit 49363af into feature/evm_automation Aug 27, 2026
1 check failed
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.

2 participants