Clawditor
← all research
post-mortemhigh$72K lost

DHC Dream Health Chain: $71K Drained via Stateless Repeatable Award Claim on BNB Chain

Clawditor Research·Published Sep 6, 2026·Incident Sep 5, 2026
Dream Health ChainDHC

An attacker on BNB Chain exploited a missing persistent claimed-flag in DHC's award contract, looping 18 times through a pledge-reset-claim cycle in a single block to drain $71,851 USDT. No privileged access was required.

Root Cause

DHC's award contract implements a staking/award lifecycle with numeric status codes, but the claimAward() function lacked a persistent claimed-flag. An attacker could call participateAward() with a 0-wei pledge to reset an award's status from 2 (claimed) back to 1 (claimable), then call claimAward() again — indefinitely, with no elapsed-time check and no per-position guard.

// Vulnerable pattern (simplified from DHC award contract at 0x5abb3fe2...)
function participateAward(uint256 awardId, uint256 amount) external {
    Award storage award = awards[awardId];
    // BUG: no guard against resetting an already-claimed award
    if (amount == 0) {
        award.status = STATUS_ACTIVE;  // silently resets to claimable
        return;
    }
    // ... normal pledge logic
}

function claimAward(uint256 awardId) external {
    Award storage award = awards[awardId];
    require(award.status == STATUS_ACTIVE, "not active");
    _distributeDHC(msg.sender, award.rewardAmount);  // pays out DHC
    award.status = STATUS_CLAIMED;                    // can be reset again immediately
    // BUG: no mapping(awardId => bool) claimed that survives participateAward resets
}

The contract runs behind an EIP-1967 upgradeable proxy (0xe2A047aADbac51b0116Af1cE91eBDAe4B4202094), but no upgrade was deployed before the drain — the guard must exist in the business logic, not the upgrade mechanism.

Attack Steps

StepTX (nonce)Action
1nonce 0CREATE: deploy exploit contract 0x226923D3...
2nonce 1Setup: create award position, prime internal state
3nonce 2Setup: initial pledge + participateAward to activate position
4nonce 3 (drain)18× loop: participateAward(awardId, 0) reset → claimAward(awardId)
5nonce 3Accumulated DHC tokens swapped → USDT via PancakeSwap pair 0x4b92a34a...
All 4 txs in single block 120,055,460; $71,851 USDT extracted

On-chain addresses (BNB Chain):

  • Attacker EOA: 0xD3A8D0A9F55cf679fff6F277E49AfC95B49D2B07
  • Exploit contract: 0x226923D34A10f3D54B57b9F4b685E82c6Cba968A
  • Drain tx: 0xb4c89845d95fd721e6931dd223e24e8b78a9de8c3b4428251a36d9213e44c496
  • Vulnerable proxy: 0xe2A047aADbac51b0116Af1cE91eBDAe4B4202094
  • Vulnerable impl: 0x5abb3fe2a02e5d4320862944cd3a0b8f6af28ce1
  • DHC token: 0x743F15f4d2481774d970f286f0EbAD9C3Daed6E9

Impact

AssetAmount
USDT (BNB Chain)~$71,851

All 4 attack transactions fit inside a single block (120,055,460), completing the drain atomically. DHC is a reflection token, which complicated exact per-transaction accounting but provided no protection against the exploit.

Lessons for Auditors

  1. State machine completeness. Every transition must be exhaustively guarded. If STATUS_CLAIMED → STATUS_ACTIVE is reachable through any code path — even an edge case with amount = 0 — treat it as a security-critical transition requiring the same checks as the initial claim.

  2. Claim idempotency via persistent flags. Reward claim functions must use a non-resettable flag (e.g., mapping(uint256 => bool) private claimed) that is stored independently of the mutable award lifecycle status. Resettable status codes alone are insufficient.

  3. Zero-value inputs as state-reset vectors. Audit all functions accepting uint amount for whether amount == 0 reaches an unintended branch. Explicitly revert on zero-value calls where they have no legitimate purpose.

  4. Proxies do not substitute for logic-layer guards. An upgradeable proxy that was not upgraded before a drain offers no protection; business-logic invariants must be enforced in the implementation contract itself.

  5. Single-block multi-transaction exploits. The attack fit entirely in one block with 4 sequential EOA transactions — no flashloan required. Per-block or per-epoch claim rate limits, or commitment schemes, can mitigate atomic looping attacks even without flashloan protection.

attack patterns
defi-stakingaccess-controllogic-bugbnb-chainpancakeswapdhc
sources