Skip to content

bug(genesis): ProtocolConfig and Denylist builders accept proxy addresses the node never reads #496

Open

Description

@forumevi

Summary

The genesis builder lets ProtocolConfig and Denylist be placed at any proxy.address, but the node reads both contracts from hardcoded addresses. schemaAdminProxy(defaultAddress) only supplies a default and never enforces it. A genesis built with a different address (or, for Denylist, with the default on a non-localdev chain) builds without error, and the node then silently reads the wrong place.

ValidatorManager.ts ("proxy.address only supports ...") and NativeFiatToken.ts already reject a mismatching address; these two builders do not.


Details

ProtocolConfig

  • scripts/genesis/ProtocolConfig.ts: proxy: schemaAdminProxy(DEFAULT_PROXY_ADDRESS) accepts any address, and buildSystemContractAlloc uses proxy.address ?? DEFAULT_PROXY_ADDRESS.
  • The node reads ProtocolConfig only at 0x3600000000000000000000000000000000000001 (crates/execution-config/src/protocol_config.rs, crates/eth-engine/src/constants.rs).
  • If placed elsewhere, the EL falls back to chainspec defaults and the CL swallows the read error (unwrap_or_default in decided.rs). Fee, gas-limit, and consensus params — and every later controller update — have no effect; only an error log remains.

Denylist

  • scripts/genesis/Denylist.ts: DEFAULT_PROXY_ADDRESS = denylistAddressByNetwork.localdev regardless of ctx.chainId, and any explicit proxy.address is accepted.
  • The node reads it from ArcChainSpec::denylist_address(), which is fixed per chain (mainnet 0x3600000000000000000000000000000000000004).
  • If the Denylist is placed elsewhere, the node still reads the fixed address. Normally nothing is deployed there, so every lookup reads empty storage and nobody is denylisted (fails open, no error). If some other contract occupies that address, the node reads its storage instead and the result is undefined.
  • denylist_address_matches_genesis_builder_for_every_network only compares the Rust constants with addresses.ts, not with the builder output.

Scope

No shipped genesis is currently wrong (mainnet sets 0x36...04 explicitly). Devnet and testnet deploy the Denylist after genesis, so the Denylist part only affects genesis builds that include it: localdev, mainnet regenerations, and new networks. The ProtocolConfig part affects any genesis build.


Reproduction

Build a ProtocolConfig with proxy.address = 0x00000000000000000000000000000000DeaDBeef, and a Denylist for chain id 5042 without proxy.address:

Case Expected Actual
NativeFiatToken with proxy.address != 0x36..00 REJECT (control) REJECT
ProtocolConfig with proxy.address != 0x36..01 REJECT PASS — alloc at 0x...DeaDBeef, 0x36..01 absent
Denylist for chain 5042 without proxy.address use mainnet address 0x36..04 placed at localdev 0x3605...4221, node reads 0x36..04
Denylist with arbitrary proxy.address REJECT PASS

Suggested Fix

  • ProtocolConfig: in superRefine, reject proxy.address other than 0x36...01 (case-insensitive), mirroring ValidatorManager.
  • Denylist: derive the expected address from ctx.chainId via denylistAddressByNetwork, use it as the default, and reject a mismatch or unknown chain ID.
  • Optionally, a test that builds each assets/*/config.json and checks system-contract addresses against the Rust constants.

I'd be happy to open a PR for this if assigned.

Activity

  1. osr21 commented on Oct 29, 2026

    @osr21

    One implementation detail: schemaDenylist is module-level and its default address is fixed to denylistAddressByNetwork.localdev; it has no ctx, while buildDenylistGenesisAllocs does. A network-specific default therefore likely needs to be derived in the builder (or through a schema factory keyed by chain ID), then any explicit address checked against that expected address.

    For regression coverage, test generated allocations as well as schema validation: verify ProtocolConfig and Denylist are allocated at the canonical addresses the node reads for each supported genesis network, and reject mismatches or unsupported chain IDs. Since the current mainnet config explicitly sets the canonical Denylist address, this looks like a genesis-builder footgun rather than evidence that live mainnet is misconfigured.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions