Reproduced Exploit
Subsquid: workers withdraw their bond without deregistering
Chain
Other
Category
untagged
Date
Jan 1970
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: Subsquid-security-review. The historical source/toolchain is unavailable; this entry is documentation only and claims no executable Forge PoC.
Vulnerability classes: vuln/logic
Reproduction: a faithful minimal reproduction of the vulnerable finding — the vulnerable code is reproduced verbatim (marked
@>) with faithful minimal doubles; local deploy, no fork.
Root cause#
A worker registers and immediately withdraws its bond without ever calling deregister() and without serving the lock period, because withdraw only checks block.number >= deregisteredAt + lockPeriod() and deregisteredAt is still 0; the worker is never removed from activeWorkerIds, so each cycle plants a dangling ghost entry (unbounded-loop / DoS vector) at zero net cost while the full bond is returned
uint256 workerId = workerIds[peerId];
require(workerId != 0, "Worker not registered");
Worker storage worker = workers[workerId];
require(!isWorkerActive(worker), "Worker is active");
require(worker.creator == msg.sender, "Not worker creator");
require(block.number >= worker.deregisteredAt + lockPeriod(), "Worker is locked"); // @> VULN (this line)
Why it's exploitable here#
A worker registers and immediately withdraws its bond without ever calling deregister() and without serving the lock period, because withdraw only checks block.number >= deregisteredAt + lockPeriod() and deregisteredAt is still 0; the worker is never removed from activeWorkerIds, so each cycle plants a dangling ghost entry (unbounded-loop / DoS vector) at zero net cost while the full bond is returned
Attack path#
Marked-line walkthrough (Playground)#
The EVM Playground pins each step to the exact executed source line in 0xce01759b82…:
- L112 — Setup: deregister event declared: Setup: the registry emits WorkerDeregistered on a proper exit — the path this attack skips entirely.
- L132 — Setup: epoch boundary helper: Setup: registration timestamps are snapped forward to the next epoch boundary.
- L144 — Setup: worker-active predicate: Setup: a worker counts as active once its registration epoch has started.
- L158 — Setup: worker registers a bond: Setup: the attacker registers a worker, locking a bond and joining activeWorkerIds.
- L174 — Withdraw requires only active worker: withdraw checks the worker is active but never requires that it was actually deregistered first.
- L206 — Bond returned without deregister: Root cause: the lock check reads deregisteredAt (still 0) so it passes at once; the worker struct is deleted and the bond returned though it never deregistered.
- L208 — Ghost entry left in active set: The bond is transferred back but the worker is never removed from activeWorkerIds, planting a dangling entry that bloats every iteration.
PoC#
Registry (Foundry, local deploy — verbatim vulnerable source + harm-asserting test):
cd 58247-h-03-workers-could-withdraw-without-deregister-and-waiting-f_exp
forge test -vvv
The browser Playground replays the same synthetic opcode-for-opcode and measures the harm. Both gates are green (registry forge test PASS + Playground _verify-poc VERDICT: PASS).
Sources & further analysis#
Reproductions & code
- No executable Forge reproduction is claimed; the historical source/toolchain was unavailable for this finding.
- AuditVault finding: Subsquid-security-review.
- Upstream DeFiHackLabs PoC directory: src/test.
Alerts & third-party analyses
- DeFiHackLabs incident explorer: search "Subsquid: workers withdraw their bond without deregistering".
- 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.