Reproduced Exploit

Virtuals — new validator receives historic voting credit and the full reward

A validator score should count actual engagement. When a validator is initialized, the audited code assigns its base score to the total number of past proposals. That makes a newly added validator appear to have participated in every historical proposal, even with zero votes.

Apr 2025Otherlogic3 min read

Chain

Other

Category

logic

Date

Apr 2025

Source

AuditVault

EVM Playground

Source-level debugger — step opcodes and Solidity in sync

evm-hack-analyzer

The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Balances (native / ERC-20 / NFT), Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.

Loading fork state…

Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 61826-h-05-validatorregistryvalidatorscoregetpastvalidatorscore-al. Standalone Foundry PoC and full write-up: 61826-h-05-validatorregistryvalidatorscoregetpastvalidatorscore-al_exp in the evm-hack-registry mirror.


Vulnerability classes: vuln/logic/reward-calculation · vuln/logic/incorrect-state-transition · vuln/logic/missing-check

Reproduction: self-contained Foundry PoC with no fork, RPC, or cheatcodes. Full trace: output.txt. Driver: test/61826-h-05-validatorregistryvalidatorscoregetpastvalidatorscore-al_exp.sol.

AuditVault taxonomy: lang/solidity · sector/gaming · sector/governance · sector/nft · sector/staking · platform/code4rena · has/github · has/poc · severity/high · genome: proposal-manipulation · role-bypass · reward-accounting

Key info#

ImpactHIGH — a validator that cast no vote receives a score equal to all past proposals and takes the complete 100-unit reward.
ProtocolVirtuals
Vulnerable codeValidatorRegistry._initValidatorScore
FindingCode4rena Virtuals, 2025-04 · #61826 (H-05) · reporter YouCrossTheLineAlfie
StatusAudit finding; local reduction connects the fabricated score to reward payout.
Compiler^0.8.24 (local reduction)

TL;DR#

A validator score should count actual engagement. When a validator is initialized, the audited code assigns its base score to the total number of past proposals. That makes a newly added validator appear to have participated in every historical proposal, even with zero votes.

The local PoC creates two historical proposals and gives an active validator one actual vote. It then initializes a new validator, which immediately has score two and receives all 100 reward units despite never voting.

The vulnerable code#

SOLIDITY
_baseValidatorScore[validator][virtualId] = _getMaxScore(virtualId); // @> VULN

_getMaxScore is the historic proposal count, so this initialization grants credits that the account never earned.

Root cause#

Initialization uses the maximum achievable score as a baseline instead of a neutral baseline. Later reward accounting cannot differentiate a recently registered validator from an account that actually participated in all prior votes.

Preconditions#

  • Historic proposals already exist when a validator is added.
  • Validator rewards use validatorScore or its historical version.
  • The new validator can be included in the relevant reward distribution.

Attack walkthrough#

  1. Two proposals exist and an incumbent validator votes only once.
  2. The attacker registers a fresh validator with no votes.
  3. The vulnerable initialization assigns the attacker two base points.
  4. The distributor reads a full score and pays the attacker all 100 local reward units.

Diagrams#

flowchart TD A[Two historical proposals] --> B[Attacker becomes validator] B --> C[Base score set to proposal count] C --> D[No votes but score equals 100 percent] D --> E[Full reward paid to new validator]

Remediation#

Initialize base score to zero. If a score needs to be normalized across epochs, snapshot the relevant start block or epoch and count only participation occurring after registration.

SOLIDITY
_baseValidatorScore[validator][virtualId] = 0;

How to reproduce#

BASH
cd /workspaces/RustroverProjects/audits/evm-hack-registry/61826-h-05-validatorregistryvalidatorscoregetpastvalidatorscore-al_exp
forge test -vvv

Sources#

Reference: Code4rena Virtuals finding H-05, curated by AuditVault.


Sources & further analysis#

Reproductions & code

Alerts & third-party analyses

  • Web3Sec X hacked database: search.
  • Rekt leaderboard: search.
  • Solodit incident search: search.

These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.