Skip to content

Anchor: getPreconfMetadata existence sentinel collides with a valid anchorBlockNumber == 0 #21762

@dantaik

Description

@dantaik

Summary

Anchor.getPreconfMetadata() uses anchorBlockNumber != 0 as its sentinel for "metadata exists", but BlockParams.anchorBlockNumber is documented as "0 to skip" (a legitimate input meaning "do not advance the L1 checkpoint this block"). A block legitimately recorded with anchorBlockNumber == 0 is therefore indistinguishable from a block that was never recorded, so getPreconfMetadata reverts for it. _validateBlock performs no anchorBlockNumber != 0 guard, so nothing on-chain prevents the collision.

Code references

  • contracts/layer2/core/Anchor.sol:36uint48 anchorBlockNumber; // L1 block number to anchor (0 to skip)
  • contracts/layer2/core/Anchor.sol:208require(preconfMetadata.anchorBlockNumber != 0, InvalidBlockNumber()); (the overloaded sentinel)
  • contracts/layer2/core/Anchor.sol:250-256_storePreconfMetadata stores anchorBlockNumber verbatim, unconditionally, for every block.
  • contracts/layer2/core/Anchor.sol:218-237_validateBlock has no anchorBlockNumber != 0 validation.
  • Consumers: contracts/layer2/preconf/PreconfSlasherL2.sol:45,106,174 call getPreconfMetadata(...) and rely on its revert-on-absent behavior.

Impact

If a preconf-relevant block is ever produced with anchorBlockNumber == 0, every getPreconfMetadata(thatBlock) reverts, so PreconfSlasherL2 cannot validate any fault for that block. Because _validateMissingEOPFault / _validateInvalidEOPFault also read getPreconfMetadata(blockNumber + 1), a block produced with anchorBlockNumber == 0 can additionally block slashing of the prior block. Net effect: slashing evasion / fault-proof DoS.

Reachability

Currently latent. The driver passes the actual (possibly unchanged) L1 block number rather than 0 — test_anchorV4_allowsMultipleAnchorsAcrossBlocks confirms a non-advancing block reuses the real number (1000), and the only anchorBlockNumber: 0 usage in the tree is the genesis generator (test/genesis/generate/taikoAnchor.ts), which is not slashable. It becomes exploitable only if anchorBlockNumber == 0 can reach a preconfirmed block via a driver change or proposer-influenced input. The point of the fix is to remove the safety dependence on off-chain driver behavior.

Fix

Decouple metadata-existence from anchorBlockNumber by adding an explicit bool stored marker to PreconfMetadata (packed into the existing uint48 slot, so the contract-level storage layout and the bytes32 sub-slots are unchanged) and using it as the sentinel. This preserves the documented "anchorBlockNumber == 0 to skip" semantic rather than removing it (which a naive require(anchorBlockNumber != 0) in _validateBlock would do).

PR with the minimal change + regression tests: see linked PR below.

Found during a recursive audit of the core protocol contracts and their dependencies.

Metadata

Metadata

Labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions