Skip to content

fix: F-2026-18829 | [Dual Defense] UEA_SVM Hardcodes Ed25519 Verifier 0x…00ca After Runtime Moved to 0xEC…01 - #320

Merged
0xNilesh merged 5 commits into
audit-fixesfrom
F-2026-18829
Aug 24, 2026
Merged

fix: F-2026-18829 | [Dual Defense] UEA_SVM Hardcodes Ed25519 Verifier 0x…00ca After Runtime Moved to 0xEC…01#320
0xNilesh merged 5 commits into
audit-fixesfrom
F-2026-18829

Conversation

@0xNilesh

@0xNilesh 0xNilesh commented Aug 21, 2026

Copy link
Copy Markdown
Member

Finding

The Ed25519 signature-verifier precompile moved from the legacy 0x00000000000000000000000000000000000000ca to 0xEC00000000000000000000000000000000000001. The node registers only the new address:

  • app/app.go instantiates usigverifierprecompile.NewPrecompile() (precompiles/usigverifier, USigVerifierPrecompileAddress = 0xEC…01) and puts it into the static-precompile map handed to EVMKeeper.WithStaticPrecompiles.
  • app/upgrades/ai-audit-fixes-2's registerPrecompileV2 appends 0xEC…01 to ActiveStaticPrecompiles on chains that took that upgrade.

Nothing is bound to 0x…00ca anywhere in the node. But every genesis setup script still declared 0x…00ca active and omitted 0xEC…01 entirely. ActiveStaticPrecompiles is what gates callability (Keeper.IsAvailableStaticPrecompile), so on a fresh genesis this produced two distinct failures:

  1. 0x…00ca was declared but unimplemented. Keeper.GetStaticPrecompileInstance treats an address that is active in params but absent from the in-memory precompile map as memory corruption and panics with precompiled contract not stored in memory: 0x…00ca. baseapp's recover contains it — the node survives and the tx fails — but every call to 0x…00ca aborts.
  2. 0xEC…01 was not active at all, so the real verifier was unreachable and Ed25519 verification could not work on a fresh chain.

Per the decision on this finding, 0x…00ca is removed, not re-registered.

Changes

Genesis lists — dropped 0x…00ca, appended 0xEC…01. The address is appended (rather than substituted in place) because x/vm's ValidatePrecompiles requires the list to be sorted and 0xEC… sorts after every 0x00… entry; the rest of each list is byte-identical.

  • scripts/test_node.sh
  • local-native/scripts/setup-genesis-auto.sh
  • local-multi-validator/scripts/setup-genesis-auto.sh
  • testnet/core/setup/setup_genesis_validator.sh

No upgrade handler — deliberate. audit-fixes targets mainnet, which starts from a fresh genesis, so the scripts above are the whole fix there: there are no live EVM params carrying 0x…00ca to migrate. A usigverifier-precompile-fix handler was written and then dropped from this PR (commit 0faece52) for that reason. It belongs on the testnet branch, where donut is a running chain whose params still declare the legacy address; it will be raised there against a real upgrade height.

Docsx/uexecutor/README.md said the Ed25519 precompile lives at 0x00…00ca; corrected to 0xEC…01.

Dead fixturetest/utils/bytecode.go's UEA_SVM_BYTECODE was referenced by nothing in the repo and still embedded the old target (60ca5afaPUSH1 0xca; STATICCALL — and selector bbdd8207). Removed; nothing else in the tree referenced it.

Tests

precompiles/usigverifier/genesis_scripts_test.goTestGenesisScriptsActivateCurrentVerifier reads all four genesis scripts and asserts each active_static_precompiles line contains usigverifier.USigVerifierPrecompileAddress (taken from the source of truth, not re-typed) and does not contain the legacy 0x…00ca. One sub-test per script.

Regression-checked in both directions: re-introducing 0x…00ca into one script fails the sub-test, and removing 0xEC…01 from one script fails it on the other assertion.

Follow-ups (deliberately out of scope here)

  1. 0x00000000000000000000000000000000000000CB (utxhashverifier) is the same defect. app/upgrades/remove-utxverifier deregisters it from live chains and the utxverifier module store is deleted, but all four genesis lists still declare it active — and there is no utxhashverifier implementation anywhere in precompiles/. A fresh genesis therefore re-introduces a declared-but-unimplemented address on exactly the panic path described above. Left in place so this PR stays scoped to the Ed25519 verifier.

  2. 0x0000000000000000000000000000000000000803 (vesting) — confirmed, same defect. It is declared active in all four genesis lists and is part of the evm fork's types.AvailableStaticPrecompiles, but there is no vesting precompile package in the evm fork (precompiles/ has bank, bech32, callbacks, distribution, erc20, gov, ics20, p256, slashing, staking, werc20 — no vesting) and app/precompiles.go's NewAvailableStaticPrecompiles never registers it. So 0x803 is active-but-unimplemented → precompiled contract not stored in memory on any call. This matches Hacken #438 / RC-09.

  3. Bonus, opposite direction: 0x0000000000000000000000000000000000000806 (slashing) is registered but never activated. app/precompiles.go builds and registers slashingPrecompile, but 0x806 appears in none of the four genesis lists, so the slashing precompile is unreachable on a fresh chain. Not a panic — just a silently dead feature.

Contract half (tracked separately)

The durable fix is in push-chain-core-contracts: src/uea/UEA_SVM.sol:42 still has

address public constant VERIFIER_PRECOMPILE =
    0x00000000000000000000000000000000000000ca;

which must become 0xEC00000000000000000000000000000000000001, followed by a registerUEA for the new implementation. That change is not in this PR.

Note that this is not self-healing for existing accounts: UEAProxy snapshots its implementation address into UEA_LOGIC_SLOT at initializeUEA time and _implementation() reads that slot on every delegatecall. Already-deployed SVM UEA proxies therefore keep delegating to the old UEA_SVM implementation even after registerUEA points the factory at a new one, and need migrating through the module-sponsored migration path.

… 0x..00ca

Nothing is registered at 0x..00ca, so declaring it active panics the EVM
precompile lookup, and the real verifier was never activated.
Drops the legacy ed25519 verifier address from EVM ActiveStaticPrecompiles
and adds 0xEC..01 if missing, for chains already past genesis.
audit-fixes targets mainnet, which starts from a fresh genesis, so the
genesis scripts already carry the correct active_static_precompiles list
and there are no live params to migrate. The upgrade handler belongs on
the testnet branch instead, where donut has live params still declaring
the legacy 0x...00ca address.

Moves TestGenesisScriptsActivateCurrentVerifier out of the handler's
test file into precompiles/usigverifier so the genesis half of the fix
stays covered.
0x..00CB has no implementation anywhere and remove-utxverifier strips it from
live chains, so leaving it in genesis re-introduced it on every fresh chain.
Confirmed absent from live donut params, so this is script-only cleanup.
@0xNilesh
0xNilesh merged commit c63e0af into audit-fixes Aug 24, 2026
4 checks passed
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.

1 participant