Reproduced Exploit

Suzaku dust forceUpdateNodes leaves 600 stake that a rebalance should have moved

forceUpdateNodes is called with limitStake of 1 wei across two nodes. Weight is stake / 1e12, so 1 wei does not change it, but each node is still marked pending. rebalance(600) then returns 0 and the 600 stake stays on node 0. The control ignores an update whose weight does not change, so the same…

Jul 2025•Ethereum•dos•3 min read

Loss

600 stake stays on the node. The control moves 600

Chain

Ethereum

Category

dos

Date

Jul 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: 61229-dust-limit-attack-on-forceupdatenodes-allows-dos-of-rebalanc. Standalone Foundry PoC and full write-up: 61229-dust-limit-attack-on-forceupdatenodes-allows-dos-of-rebalanc_exp in the evm-hack-registry mirror.


Vulnerability classes: vuln/dos/permanent · vuln/logic/incorrect-state-transition

Key info#

FieldValue
Loss600 stake stays on the node. The control moves 600
Vulnerable contractMiddleware.forceUpdateNodes
Attacker EOA0x1111111111111111111111111111111111111111
Attack contractExploit
Attack txExploit.run()
Chain / block / dateEthereum model · block 0 · 2025-07
Compilersolc 0.8.27, cancun, via IR
Bug classDust update sets pending without changing weight

TL;DR#

forceUpdateNodes is called with limitStake of 1 wei across two nodes. Weight is stake / 1e12, so 1 wei does not change it, but each node is still marked pending. rebalance(600) then returns 0 and the 600 stake stays on node 0. The control ignores an update whose weight does not change, so the same rebalance moves the 600.

The vulnerable code#

SOLIDITY
if (!ignoreDust || beforeW != afterW) {
    // @> VULN: pending is set even when the dust update does not change weight.
    nodes[i].setPending(true);
}

The loop is two iterations. The control requires beforeW != afterW before setting pending.

Root cause#

A pending flag is how the middleware stops a second stake update in the same epoch. The dust call sets that flag without moving stake and without changing the P-Chain weight. The real rebalance, which should pull excess stake back to the vault, sees the flag and does nothing.

Attack walkthrough#

  1. Node 0 is funded with 600 stake.
  2. forceUpdateNodes(1) runs the two-node loop. Weight before and after is equal.
  3. Both nodes are marked pending.
  4. rebalance(600) sets rebalanceReverted and returns 0. The node still holds 600.
  5. The control middleware sees equal weights and does not set pending.
  6. Its rebalance pulls 600. The control node ends at 0.

Diagrams#

flowchart TD A["Node holds 600 stake"] --> B["forceUpdateNodes called with 1 wei"] B --> C["Weight is unchanged"] C --> D["pending is set anyway"] D --> E["rebalance of 600 returns 0"] E --> F["600 stays on the node"] F --> G["Control ignores the dust and moves 600"]

Impact#

600 stake that the vault withdrawal required to be rebalanced stays on the operator node for the epoch. The control moves that 600.

PoC#

BASH
cd 61229-dust-limit-attack-on-forceupdatenodes-allows-dos-of-rebalanc_exp
forge test -vv

test_exploit_dust_blocks_rebalance asserts 600e18 stayed, the rebalance was blocked, and the control moved 600e18.

Sources#


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.