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…

Feb 2025Otherlogic2 min read

Chain

Other

Category

logic

Date

Feb 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: 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-registry mirror.


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#

ImpactHIGHbatchRelease marks allocations as fully released but never transfers tokens; users are permanently unpaid
ProtocolTreasury Vesting / BlockDAG
Vulnerable codebatchRelease — second loop re-calls getReleasableAmount after state update → 0
Bug classMis-applied checks-effects-interactions (effects before caching transfer amounts)
FindingHalborn — BlockDAG Treasury Vesting · #52680
Reporthalborn.com/audits/blockdag/treasury-vesting
SourceAuditVault
StatusAudit finding — fixed by combining update+transfer in one loop. Local synthetic PoC.
Compiler^0.8.24 (PoC)

TL;DR#

  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 vesting.

The vulnerable code#

SOLIDITY
// 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 batchRelease for those users.

Attack walkthrough#

  1. Allocate 1000 + 2000 to user1/user2.
  2. Admin batchRelease for both.
  3. totalReleased == 3000 but both balances still 0.
  4. HARM: users can never claim those tokens via releasable math again.

Diagrams#

flowchart TD L1["Loop 1: userReleased += X"] --> Zero["getReleasableAmount now 0"] Zero --> L2["Loop 2: if releasable > 0 transfer"] L2 --> Skip["condition false #59; no transfer"] Skip --> Harm["accounting released #59; users unpaid"]

Impact#

Vested tokens never reach users; protocol accounting falsely reports successful release.

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.