Disclosure: I'm an external community contributor . I am not affiliated with Circle, have no write access to this repository, and nothing here is an official review — it's advisory only. Everything below was verified locally against the committed tree; commands are included so you can reproduce or refute any of it.
Summary
With mainnet approaching, I ran an independent pre-launch check of the committed mainnet genesis, on the reasoning that genesis is the one artifact that cannot be patched after launch.
Headline result is positive: assets/mainnet/genesis.json reproduces byte-for-byte from assets/mainnet/config.json at the pinned toolchain. No drift between the committed inputs and the committed output.
The rest of this issue is three observations about bootstrap sequencing that I could not resolve from the repo alone, plus a short list of things that look alarming under a vanilla-EVM harness but are not findings — flagged explicitly so nobody else burns time chasing them.
1. Reproducibility (verified)
git clone https://github.com/circlefin/arc-node.git && cd arc-node
git submodule update --init --recursive
foundryup -i v1.4.4 # matches .foundry-version
npm install
cp assets/mainnet/genesis.json /tmp/genesis.committed.json
npx hardhat --config hardhat.config.ts compile
npx hardhat --config hardhat.config.ts genesis --network mainnet
Result:
2d33355dcb12b47d3e36f8bceb5945643445b80c6ca763d6ee8829512f15e035 /tmp/genesis.committed.json
2d33355dcb12b47d3e36f8bceb5945643445b80c6ca763d6ee8829512f15e035 assets/mainnet/genesis.json
$ git diff --stat assets/mainnet/ # (empty)
config.json is also byte-identical after the run. Two things make this work, and both are worth keeping: the mainnet builder hardcodes every address rather than deriving from a mnemonic the way assets/testnet/genesis.config.ts does, and the genesis timestamp is a fixed constant (1778544000 = 2026-05-12T00:00:00Z) rather than currentTimestamp(). Together those make the mainnet genesis independently auditable by anyone. That's a genuinely good property and I'd suggest a CI job asserting it stays true.
Booting the committed file under anvil --init and reading state back confirms the declared config is faithfully encoded — chainId 5042, and all six feeParams plus all eight consensusParams decode to exactly the declared values (alpha 20, kRate 200, inverseElasticityMultiplier 5000, minBaseFee 20 gwei, maxBaseFee 20000 gwei, blockGasLimit 30,000,000; targetBlockTimeMs 500). 11 validators, each 2000 voting power, no duplicate public keys, each with the documented (activate=2000, remove=0) controller pair. Across the 40 distinct addresses in the config there is no key reuse between roles — every role has its own address.
2. NativeFiatToken emergency roles are zero at genesis
owner 0xf5a5658b55983E2Aa037cAC7A8431B510E8A97F4
pauser 0x0000000000000000000000000000000000000000
masterMinter 0x0000000000000000000000000000000000000000
blacklister 0x0000000000000000000000000000000000000000
rescuer 0x0000000000000000000000000000000000000000
minters []
Testnet assigns a real account to each of these, so the difference is deliberate rather than a copy-paste slip. I checked the obvious failure mode first and it does not apply: owner is set, and all four roles are recoverable. Impersonating owner against the booted genesis, updateMasterMinter / updatePauser / updateBlacklister / updateRescuer all succeed, and the guards hold under negative control (a non-owner gets Ownable: caller is not the owner; pause() from a random caller gets Pausable: caller is not the pauser). So this is not a bricking bug.
The part I'd still raise: until owner sends those transactions, the chain has no pause capability, no blacklist capability, and no minting capability, and total genesis supply is exactly 10,000 USDC to a single prefund account. If that interval is intended to be closed in the first block or two by a launch runbook, it would be worth saying so somewhere in-repo, because reading genesis alone the natural conclusion is that these were left unset by mistake.
3. Every privileged account is unfunded at genesis
This is the one I'd most like a maintainer's read on. Cross-referencing each role address against alloc:
| Role |
Balance |
Code |
NativeFiatToken.owner |
0 |
no |
NativeFiatToken.proxy.admin |
0 |
no |
ProtocolConfig.owner / .controller / .pauser |
0 |
no |
PermissionedValidatorManager.owner / .pauser |
0 |
no |
Denylist.owner |
0 |
no |
validatorRegisterer[0] / [1] |
0 |
no |
prefund[0] 0x50A2…bE57 |
10,000 USDC |
no |
Ten privileged accounts, all zero balance and no code; one funded EOA. Since gas on Arc is paid in the native token and minBaseFee is 20 gwei — I searched crates/ for a fee-exemption, gasless, or sponsored-transaction path and found none, and executor.rs carries a hardcoded GENESIS_MIN_BASE_FEE = 20_000_000_000 fallback — none of those ten accounts can transact until 0x50A2…bE57 funds them. Every administrative capability of the chain, including the role recovery in §2, therefore depends on that one key being available at launch. A 21,000-gas transfer at the floor costs ~0.00042 USDC, so the amounts are trivial; the dependency is structural rather than economic.
Also worth noting that none of these are contracts at genesis, so if any are intended to be multisigs they'd need deploying and the roles re-pointed afterwards.
Is the funding of these accounts part of a launch runbook that lives outside this repo? If so I think it's worth a line in docs/, since the genesis file currently reads as if the admin set is inert.
4. USDC is 6 decimals as an ERC-20 and 18 decimals as the native unit
Verified on the booted chain, for the single prefunded account:
eth_getBalance 10000000000000000000000 (1e22, 18dp)
balanceOf() 10000000000 (1e10, 6dp)
decimals() 6
Both correctly represent 10,000 USDC, and the ethereum-lists/chains entry for 5042 declaring nativeCurrency.decimals: 18 is right for the native unit — I'd initially suspected a mismatch here and it turned out to be consistent, so I want to be clear this is not a bug. But integrators reading decimals() == 6 and then scaling a native-value transfer by 1e6 will be off by 1e12. Given USDC is both the gas token and the ERC-20 here, a short note in the docs would prevent a predictable class of integration error.
Not findings — vanilla-EVM harness artifacts
Because anvil has none of the Arc precompiles (0x1800…00–0x1800…04 are 1-byte 0xef markers in genesis, with the real implementations in the node), these calls revert under the harness and are expected, not defects: totalSupply(), ProtocolConfig.blockGasLimit()/minBaseFee()/maxBaseFee(), Denylist.paused(), and the ValidatorRegistry getters. The struct-returning feeParams() / consensusParams() accessors do work, which is how §1 was checked. Anyone repeating this should read those reverts as harness limits.
Minor note: the 0xef stub prefix is EIP-3541-forbidden for newly deployed code, so those markers can only ever originate from genesis. That looks intentional and I mention it only because it's a nice invariant — nothing can later deploy a lookalike at a precompile-adjacent address.
Suggestions
- A CI job asserting the mainnet genesis reproduces byte-for-byte from its committed config, so the property in §1 can't silently regress before launch.
- Document the genesis→operational bootstrap sequence (§2, §3), or point at where it lives.
- A docs note on the 6-vs-18 decimal duality (§4).
Happy to open PRs for the CI check or the docs note if that's useful. Related but distinct: #344 covers validator sets with no positive voting power — the mainnet set is fine on that front (11 × 2000).
Summary
With mainnet approaching, I ran an independent pre-launch check of the committed mainnet genesis, on the reasoning that genesis is the one artifact that cannot be patched after launch.
Headline result is positive:
assets/mainnet/genesis.jsonreproduces byte-for-byte fromassets/mainnet/config.jsonat the pinned toolchain. No drift between the committed inputs and the committed output.The rest of this issue is three observations about bootstrap sequencing that I could not resolve from the repo alone, plus a short list of things that look alarming under a vanilla-EVM harness but are not findings — flagged explicitly so nobody else burns time chasing them.
1. Reproducibility (verified)
Result:
config.jsonis also byte-identical after the run. Two things make this work, and both are worth keeping: the mainnet builder hardcodes every address rather than deriving from a mnemonic the wayassets/testnet/genesis.config.tsdoes, and the genesis timestamp is a fixed constant (1778544000= 2026-05-12T00:00:00Z) rather thancurrentTimestamp(). Together those make the mainnet genesis independently auditable by anyone. That's a genuinely good property and I'd suggest a CI job asserting it stays true.Booting the committed file under
anvil --initand reading state back confirms the declared config is faithfully encoded —chainId5042, and all sixfeeParamsplus all eightconsensusParamsdecode to exactly the declared values (alpha20,kRate200,inverseElasticityMultiplier5000,minBaseFee20 gwei,maxBaseFee20000 gwei,blockGasLimit30,000,000;targetBlockTimeMs500). 11 validators, each 2000 voting power, no duplicate public keys, each with the documented(activate=2000, remove=0)controller pair. Across the 40 distinct addresses in the config there is no key reuse between roles — every role has its own address.2.
NativeFiatTokenemergency roles are zero at genesisTestnet assigns a real account to each of these, so the difference is deliberate rather than a copy-paste slip. I checked the obvious failure mode first and it does not apply:
owneris set, and all four roles are recoverable. Impersonatingowneragainst the booted genesis,updateMasterMinter/updatePauser/updateBlacklister/updateRescuerall succeed, and the guards hold under negative control (a non-owner getsOwnable: caller is not the owner;pause()from a random caller getsPausable: caller is not the pauser). So this is not a bricking bug.The part I'd still raise: until
ownersends those transactions, the chain has no pause capability, no blacklist capability, and no minting capability, and total genesis supply is exactly 10,000 USDC to a single prefund account. If that interval is intended to be closed in the first block or two by a launch runbook, it would be worth saying so somewhere in-repo, because reading genesis alone the natural conclusion is that these were left unset by mistake.3. Every privileged account is unfunded at genesis
This is the one I'd most like a maintainer's read on. Cross-referencing each role address against
alloc:NativeFiatToken.ownerNativeFiatToken.proxy.adminProtocolConfig.owner/.controller/.pauserPermissionedValidatorManager.owner/.pauserDenylist.ownervalidatorRegisterer[0]/[1]prefund[0]0x50A2…bE57Ten privileged accounts, all zero balance and no code; one funded EOA. Since gas on Arc is paid in the native token and
minBaseFeeis 20 gwei — I searchedcrates/for a fee-exemption, gasless, or sponsored-transaction path and found none, andexecutor.rscarries a hardcodedGENESIS_MIN_BASE_FEE = 20_000_000_000fallback — none of those ten accounts can transact until0x50A2…bE57funds them. Every administrative capability of the chain, including the role recovery in §2, therefore depends on that one key being available at launch. A 21,000-gas transfer at the floor costs ~0.00042 USDC, so the amounts are trivial; the dependency is structural rather than economic.Also worth noting that none of these are contracts at genesis, so if any are intended to be multisigs they'd need deploying and the roles re-pointed afterwards.
Is the funding of these accounts part of a launch runbook that lives outside this repo? If so I think it's worth a line in
docs/, since the genesis file currently reads as if the admin set is inert.4. USDC is 6 decimals as an ERC-20 and 18 decimals as the native unit
Verified on the booted chain, for the single prefunded account:
Both correctly represent 10,000 USDC, and the
ethereum-lists/chainsentry for 5042 declaringnativeCurrency.decimals: 18is right for the native unit — I'd initially suspected a mismatch here and it turned out to be consistent, so I want to be clear this is not a bug. But integrators readingdecimals() == 6and then scaling a native-value transfer by1e6will be off by1e12. Given USDC is both the gas token and the ERC-20 here, a short note in the docs would prevent a predictable class of integration error.Not findings — vanilla-EVM harness artifacts
Because
anvilhas none of the Arc precompiles (0x1800…00–0x1800…04are 1-byte0xefmarkers in genesis, with the real implementations in the node), these calls revert under the harness and are expected, not defects:totalSupply(),ProtocolConfig.blockGasLimit()/minBaseFee()/maxBaseFee(),Denylist.paused(), and theValidatorRegistrygetters. The struct-returningfeeParams()/consensusParams()accessors do work, which is how §1 was checked. Anyone repeating this should read those reverts as harness limits.Minor note: the
0xefstub prefix is EIP-3541-forbidden for newly deployed code, so those markers can only ever originate from genesis. That looks intentional and I mention it only because it's a nice invariant — nothing can later deploy a lookalike at a precompile-adjacent address.Suggestions
Happy to open PRs for the CI check or the docs note if that's useful. Related but distinct: #344 covers validator sets with no positive voting power — the mainnet set is fine on that front (11 × 2000).