Four Addresses, Unlimited Authority! This Test Code Snippet Halted Polygon Nodes on December 18, 2025

Blockchains rely on a small number of consensus invariants. Among the most fundamental is this: only active validators may produce blocks.

What happens when you override a validator list during testing and forget to remove it in production?

On December 18 2025, a hard-coded exception that allowed four specific matic addresses to be treated as valid block producers, regardless of validator set membership, span, sprint, or Heimdall state.

A later patch restricted this behavior to a narrow block range, converting a permanent consensus bypass into a temporary one. However, during the affected window, nodes running different Bor versions disagreed not on chain state, but on who was allowed to produce blocks at all.

This analysis reconstructs how that exception interacted with Bor’s snapshot mechanism, why it caused certain nodes to stall rather than fork, and why archive and reindexing nodes were in a dangerous situation. The focus is not on intent, but on mechanics: how a single conditional altered a core consensus guarantee.

The Snapshot Mechanism

Before examining the bug, we need to understand how Bor maintains consensus state. Every block validation requires answering several questions:

  • Who are the current validators?

  • Is the block signer authorized?

  • Has this validator signed too recently?

  • Has the validator set changed at a sprint boundary?

  • Who is in-turn vs out-of-turn proposer?

Recomputing all of that from genesis every time would be too slow. Snapshots are cached consensus state at a specific block.

typeSnapshotstruct {
Number uint64
Hash common.Hash
ValidatorSet *valset.ValidatorSet
Recents map[uint64]common.Address
}

A snapshot captures the validator set and recent signers at a given block height. When validating a new block, Bor loads the snapshot from the parent block, applies the new block’s changes, and checks whether the signer is authorized. This is the mechanism that the bug exploited.

The Exception

The problematic code appeared in commit 7c5e7143:

func isPartOfVeBlopSet(addr common.Address) bool {
a := addr.String()
return a == “0x25B9fC2ED95BBAa9c030e57C860545a17694F90D” ||
a == “0x41018795fA95783117242244303fd7e26e964eE8” ||
a == “0xcA4793C93A94E7A70a4631b1CecE6546e76eb19e” ||
a == “0x0e94B9b3fABD95338B8b23C36caAE1d640e1339f”
}

This function allows blocks to be accepted even if the signer is not in the active validator set, as long as it matches one of those addresses. There is no staking check, no span lookup, no Heimdall verification. It bypasses every consensus check except the address comparison.

This test code with comment TODO: hack - remove me later was allowed into the deployment.

The Patch

A later commit ee2fb871 scoped the hack down to a specific block range, turning it from a permanent consensus bypass into a temporary one:

if blockNumber < 80440819 || blockNumber > 80443486 {
return false
}

This restricted the exception to blocks 80,440,819 through 80,443,486, a window of approximately 2,667 blocks. However, the damage was already done: during that window, nodes running different Bor versions fundamentally disagreed on who could produce blocks.

The Consensus Failure

According to Polygon’s status page, those four hard-coded wallets were treated as valid block producers for every block in the affected range, regardless of:

  • Block number (within the range)

  • Span

  • Sprint

  • Validator set

  • Heimdall state

So any block at any height in that range could be validated and sealed by those wallets, even when they were not in the active validator set.

Why Validation Fails

Here’s what happens when a block producer signs a block using the patched logic:

  1. Block producer signs a block (valid per patched logic)

  2. Followers try to validate it

  3. Validation fails at: !snap.ValidatorSet.HasAddress(signer)

This failure occurs on nodes that don’t have the allowlist logic. These include:

  • Nodes running older Bor builds before the hack existed

  • Third-party or downstream Polygon clients

  • Some RPC providers using custom or lagging builds

RPC providers often run a “known-good” Bor version, only upgrading after several days or weeks, and use rolling restarts. They will NOT see the same allowlist behavior.

When validation fails, the node stalls at that height. RPCs stop advancing, explorers lag, but the chain keeps moving for nodes that have the patched code.

This is a critical distinction: nodes didn’t fork because they disagreed on chain state. They stalled because they disagreed on who was allowed to produce blocks. A fork would require nodes to accept different transactions or different state transitions. Here, nodes simply couldn’t process blocks signed by unauthorized addresses—addresses that other nodes considered authorized.

The stalling behavior occurs because Bor’s consensus engine refuses to advance past a block it considers invalid. When it encounters a block signed by an address not in the validator set (and not in the allowlist), it stops processing. The chain continues for nodes with the allowlist, but those without it are stuck.

The Snap.apply() Problem

In Bor, Snapshot.apply() is consensus-critical. If a block passes here, it is considered valid history. The function at line 707 of bor.go and the snapshot application logic at snapshot.go:194 determine whether a block becomes part of the canonical chain.

Every node replaying history would permanently accept blocks signed by those hard-coded wallets at any block height within the affected range. So those wallets were treated as valid proposers even though they were not in the validator set and had no voting power or proposer priority.

Archive & Reindexing Nodes: Most Vulnerable

Archive and reindexing nodes are especially vulnerable to this type of bug. Why? They rely heavily on:

  • snapshot.apply() for historical replay

  • Deterministic validation of every block from genesis

  • The assumption that consensus rules are consistent across all blocks

When these nodes encounter blocks signed by the allowlisted addresses during the affected range, they must either: 1. Accept them (if they have the patched code), permanently embedding the consensus bypass in their historical chain 2. Reject them (if they don’t), causing a permanent stall at that height

Neither outcome is desirable. The first violates the fundamental consensus invariant. The second makes the node unusable for historical queries.

Lessons

This incident exposes a critical failure in verification and testing processes behind node upgrade deployment. Test code with a comment TODO: hack - remove me later was allowed into production, bypassing every gate that should have caught it.

The mechanics are straightforward: a hard-coded exception bypassed validator set checks. The process failures were not: code review missed it, testing didn’t catch it, and deployment procedures allowed it to reach mainnet. The function that overrode a core consensus guarantee made it through the entire pipeline.

None of the existing processes caught the test code. The patch that restricted the exception to a block range converted a permanent consensus bypass into a temporary one, but it couldn’t undo the damage already done. Nodes that processed blocks during the affected window now have those blocks permanently in their chain, signed by addresses that were never validators.

This is why consensus code requires extreme care in both implementation and process: a single function can alter the fundamental rules that keep a blockchain secure and consistent, and the deployment pipeline must be designed to prevent test code from ever reaching production.


This material is for educational and informational purposes only and is not intended as investment advice. The content reflects the author’s personal research and understanding. While specific investments and strategies are mentioned, no endorsement or association with these entities is implied. Readers should conduct their own research and consult with qualified professionals before making any investment decisions. Bitquery is not liable for any losses or damages resulting from the application of this information.