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.
Summary
pq-precompile's address constant is documented as gated by the Zero6 hardfork, buttracing the full call chain from registration to execution shows
hardfork_flagsisnever inspected anywhere in that path. This differs from sibling precompiles in the
same dispatch table, which do read
hardfork_flagsinternally.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-65create_precompiles_map(spec, hardfork_flags: ArcHardforkFlags)passeshardfork_flagsinto all four stateful precompile closures uniformly, but the function body contains no
if hardfork_flags.../.is_active(...)branch anywhere — registration into the matcharm is unconditional for all four addresses, not just PQ.
Wrapper — parameter unused:
crates/precompiles/src/pq.rs:20-25Underscore-prefixed, confirmed unused.
Core impl — doesn't accept the parameter at all:
crates/pq-precompile/src/lib.rs::run_pq_precompileSignature 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,264readshardfork_flags.is_active(ArcHardfork::Zero8)in four places to drive real behavioralbranches, 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-27This 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-gatedrather 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, wheredevnet/testnet/mainnetall default to an empty URL sourced from a private env var, unlikelocaldevwhich defaults to
http://localhost:8545). It appears to be an internal CI/stagingnetwork 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_maphas no registration-time hardfork gating mechanism at all — thisis 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 checkshardfork_flags), the same gap would recur, and mightnot always land on the safe side of a timestamp coincidence like this one did.
Suggested resolution (either direction seems reasonable, deferring to maintainers)
never intended at the dispatch layer, or
if !hardfork_flags.is_active(ArcHardfork::Zero6) { return None }guardin
create_precompiles_mapforPQ_ADDRESS, matching the intent in the doc comment.Happy to open a PR for either direction if useful.