Anchor: getPreconfMetadata existence sentinel collides with a valid anchorBlockNumber == 0
security
## 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:36` — `uint48 anchorBlockNumber; // L1 block number to anchor (0 to skip)`
- `contracts/layer2/core/Anchor.sol:208` — `require(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._
2 条评论