Skip to content

pq-precompile doc claims "Zero6-gated" but hardfork_flags is never read in the dispatch or execution path #293

Description

@pplmaverick

Summary

pq-precompile's address constant is documented as gated by the Zero6 hardfork, but
tracing the full call chain from registration to execution shows hardfork_flags is
never inspected anywhere in that path. This differs from sibling precompiles in the
same dispatch table, which do read hardfork_flags internally.

Evidence

Doc claim:
crates/pq-precompile/src/lib.rs:24

/// PQ precompile address ... (Zero6-gated)

Registration — no gate:
crates/precompiles/src/precompile_provider.rs:45-65
create_precompiles_map(spec, hardfork_flags: ArcHardforkFlags) passes hardfork_flags
into all four stateful precompile closures uniformly, but the function body contains no
if hardfork_flags... / .is_active(...) branch anywhere — registration into the match
arm is unconditional for all four addresses, not just PQ.

Wrapper — parameter unused:
crates/precompiles/src/pq.rs:20-25

pub(crate) fn run_pq(input: ..., _hardfork_flags: ArcHardforkFlags) -> ...

Underscore-prefixed, confirmed unused.

Core impl — doesn't accept the parameter at all:
crates/pq-precompile/src/lib.rs::run_pq_precompile
Signature is (gas: u64, data: &[u8], reservoir: u64) — no hardfork parameter in scope.

Contrast with sibling precompile that does use it correctly:
crates/precompiles/src/native_coin_control.rs:132,146,250,264 reads
hardfork_flags.is_active(ArcHardfork::Zero8) in four places to drive real behavioral
branches, with dedicated test coverage
(delegatecall_charges_early_revert_penalty_only_under_zero8).

Test suite acknowledges the gap:
crates/execution-e2e/tests/e2e/pq_precompile.rs:26-27

//! There is no case here for Zero6 disabled (PQ precompile unavailable): the harness uses
//! LOCAL_DEV, where Zero6 is active from genesis—same scope as `p256_precompile.rs`.

This comment confirms "Zero6 disabled → PQ precompile unavailable" is the intended
behavior, but the test harness never exercises it because Zero6 is active from genesis
in every test environment.

Current impact

None on any externally reachable network. ARC_ZERO6_HARDFORK_TIMESTAMP_ACTIVATION_TESTNET
(1779894517, 2026-05-27) has already passed on testnet, and mainnet activates Zero6 from
Block(0) — so there's no point in either network's history, past or future, where
PQ_ADDRESS would be reachable while Zero6 is inactive.

Devnet (ARC_ZERO6_HARDFORK_BLOCK_ACTIVATION_DEVNET = 40033853) is block-height-gated
rather than timestamp-gated, and I did not verify its current height — but devnet has no
public RPC or block explorer (confirmed via hardhat.config.ts, where devnet/testnet/
mainnet all default to an empty URL sourced from a private env var, unlike localdev
which defaults to http://localhost:8545). It appears to be an internal CI/staging
network with no external attack surface, so its activation state doesn't affect this
report's impact assessment regardless of its current height.

This is a documentation/implementation gap rather than a live vulnerability.

Why raise it anyway

create_precompiles_map has no registration-time hardfork gating mechanism at all — this
is systemic across the dispatch layer, not specific to PQ. If a future hardfork-gated
precompile is added following the same pattern (unconditional match arm + hope the
internal run_* function checks hardfork_flags), the same gap would recur, and might
not always land on the safe side of a timestamp coincidence like this one did.

Suggested resolution (either direction seems reasonable, deferring to maintainers)

  • Either: remove the "(Zero6-gated)" claim from the doc comment if availability gating was
    never intended at the dispatch layer, or
  • Add an explicit if !hardfork_flags.is_active(ArcHardfork::Zero6) { return None } guard
    in create_precompiles_map for PQ_ADDRESS, matching the intent in the doc comment.

Happy to open a PR for either direction if useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions