Reproduced Exploit
Roots Staker — the wrong activateBoost selector leaves a 7 reward unpaid
When queued > 0 and blockDelta > 8191, _tryToBoost calls activateBoost(address). BGT implements activateBoost(address user, bytes pubkey) and pays the user. The one-argument selector reverts. The 7 reward stays in the vault and the user receives 0. The two-argument call pays the 7. blockDelta is an…
Chain
Other
Category
logic
Date
Feb 2025
Source
AuditVault
EVM Playground
Source-level debugger — step opcodes and Solidity in sync
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.
Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 55115-h-05-incorrect-activateboost-interface-is-used-pashov-audit. Standalone Foundry PoC and full write-up: 55115-h-05-incorrect-activateboost-interface-is-used-pashov-audit_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/logic/wrong-condition · vuln/logic/reward-calculation
Key info#
| Protocol | Roots Staker / BGT |
| Finding | Pashov H-05, AuditVault 55115 |
| Vulnerable code | rewardCache.activateBoost(validator) |
| Harm | 7 boost reward stays in the vault. The user receives 0. |
| Chain | local synthetic |
TL;DR#
When queued > 0 and blockDelta > 8191, _tryToBoost calls activateBoost(address). BGT implements activateBoost(address user, bytes pubkey) and pays the user. The one-argument selector reverts. The 7 reward stays in the vault and the user receives 0. The two-argument call pays the 7. blockDelta is an integer stored in the same transaction. run() does not warp time.
The vulnerable code#
function _tryToBoost(address user) internal {
if (queued > 0 && blockDelta > 8191) {
// @> VULN: BGT.activateBoost takes (address user, bytes pubkey), not a single address.
rewardCache.activateBoost(validator);
}
}
Root cause#
The interface has one address argument. The BGT function has two: the user and the validator pubkey. The call cannot succeed, so a queued boost is never activated and the reward is never transferred.
Preconditions#
queued is 7 and blockDelta is 8192. The vault holds 7 reward tokens. The user pokes the staker.
Attack walkthrough#
- 7 reward tokens are minted to the vault.
setBoost(7, 8192)stores the queue and the delta. pokeenters_tryToBoostbecause 8192 > 8191.activateBoost(validator)reverts. The user receives nothing.- The vault still holds 7.
The control calls activateBoost(user, pubkey). The vault transfers the 7 to the user.
Diagrams#
Impact#
The user loses the 7 boost reward. It remains in the vault.
Remediation#
Call activateBoost(address user, bytes pubkey) with the staker as the user and the validator pubkey as the second argument.
Sources#
- AuditVault finding: https://github.com/Auditware/AuditVault/blob/main/findings/55115-h-05-incorrect-activateboost-interface-is-used-pashov-audit.md
- Pashov report (Roots, 2025-02-09): https://github.com/pashov/audits/blob/master/team/md/Roots-security-review_2025-02-09.md
- Blamed call: Staker
_tryToBoostrewardCache.activateBoost(validator)against BGTactivateBoost(address user, bytes pubkey).
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 55115-h-05-incorrect-activateboost-interface-is-used-pashov-audit_exp (evm-hack-registry mirror).
- AuditVault finding: 55115-h-05-incorrect-activateboost-interface-is-used-pashov-audit.
Alerts & third-party analyses
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.