Reproduced Exploit
Treasury Vesting — `batchRelease` updates state before transfer, users get nothing
1. Loop 1: for each user, compute releasable, add to userReleased / totalReleased. 2. Loop 2: recompute releasable — now 0 because loop 1 already credited releases — skip transfers. 3. Accounting says 3000 tokens released; users hold 0; releasable stays 0 forever. 4. HARM: permanent lock / unpaid v…
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: 52680-no-token-distribution-in-batchrelease-due-to-premature-state. Standalone Foundry PoC and full write-up: 52680-no-token-distribution-in-batchrelease-due-to-premature-state_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/logic/checks-effects-misapplied · vuln/vesting/skipped-transfer · genome: locked-funds · single-function
Reproduction: self-contained Foundry PoC with only
forge-std. Full trace: output.txt. PoC: test/52680-no-token-distribution-in-batchrelease-due-to-premature-state_exp.sol.
AuditVault taxonomy: lang/solidity · platform/halborn · has/poc · severity/high · sector/token · genome: single-function · use-reentrancy-guard · locked-funds · reentrancy-guard · timestamp-dependence
Key info#
| Impact | HIGH — batchRelease marks allocations as fully released but never transfers tokens; users are permanently unpaid |
| Protocol | Treasury Vesting / BlockDAG |
| Vulnerable code | batchRelease — second loop re-calls getReleasableAmount after state update → 0 |
| Bug class | Mis-applied checks-effects-interactions (effects before caching transfer amounts) |
| Finding | Halborn — BlockDAG Treasury Vesting · #52680 |
| Report | halborn.com/audits/blockdag/treasury-vesting |
| Source | AuditVault |
| Status | Audit finding — fixed by combining update+transfer in one loop. Local synthetic PoC. |
| Compiler | ^0.8.24 (PoC) |
TL;DR#
- Loop 1: for each user, compute releasable, add to
userReleased/totalReleased. - Loop 2: recompute releasable — now 0 because loop 1 already credited releases — skip transfers.
- Accounting says 3000 tokens released; users hold 0; releasable stays 0 forever.
- HARM: permanent lock / unpaid vesting.
The vulnerable code#
// Loop 1: update state
userReleased[users[i]][category] += releasable;
...
// Loop 2:
uint256 releasable = getReleasableAmount(users[i], category); // @> VULN returns 0
if (releasable > 0) {
bdagToken.safeTransferFrom(msg.sender, users[i], releasable);
}
Root cause#
Transfer amounts were not cached; CEI was applied by splitting loops without preserving the amounts to send.
Preconditions#
- Users have positive allocations and zero prior
userReleased. - Admin calls
batchReleasefor those users.
Attack walkthrough#
- Allocate 1000 + 2000 to user1/user2.
- Admin
batchReleasefor both. totalReleased == 3000but both balances still 0.- HARM: users can never claim those tokens via releasable math again.
Diagrams#
Impact#
Vested tokens never reach users; protocol accounting falsely reports successful release.
Sources#
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 52680-no-token-distribution-in-batchrelease-due-to-premature-state_exp (evm-hack-registry mirror).
- AuditVault finding: 52680-no-token-distribution-in-batchrelease-due-to-premature-state.
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.