All incidents

DBW Static-Income LP Drain

Share
Mar 22, 2023 16:51 UTCAttackLoss: 20,190.65 USDTPending manual check2 exploit txWindow: 1d 22h
Estimated Impact
20,190.65 USDT
Label
Attack
Exploit Tx
2
Addresses
2
Attack Window
1d 22h
Mar 22, 2023 16:51 UTC → Mar 24, 2023 15:12 UTC

Exploit Transactions

TX 1BSC
0x17f91a8acd159d529061b7c54b4753d6df482775dda4e4ef3694e4c131ec0ea7
Mar 22, 2023 16:51 UTCExplorer
TX 2BSC
0x3b472f87431a52082bae7d8524b4e0af3cf930a105646259e1249f2218525607
Mar 24, 2023 15:12 UTCExplorer

Victim Addresses

0xbf5baea5113e9eb7009a6680747f2c7569dfc2d6BSC
0x69d415fbdcd962d96257056f7fe382e432a3b540BSC

Loss Breakdown

20,190.65USDT

Similar Incidents

Root Cause Analysis

DBW Static-Income LP Drain

1. Incident Overview TL;DR

DBW / Big Winner on BNB Smart Chain was drained through a two-transaction ACT sequence. In transaction 0x17f91a8acd159d529061b7c54b4753d6df482775dda4e4ef3694e4c131ec0ea7, the attacker pre-aged hundreds of helper addresses by having short-lived CREATE2 contracts call DBW getStaticIncome() while the addresses had no lasting capital exposure. In transaction 0x3b472f87431a52082bae7d8524b4e0af3cf930a105646259e1249f2218525607, the attacker flash-borrowed public liquidity, bought DBW, minted DBW/USDT LP, temporarily pledged that LP through the pre-aged helpers, claimed retroactive static income, redeemed the LP in the same transaction, and exited in USDT. The root cause is that DBW pays static income against the claimant's current LP-inflated balance over the full elapsed _staticsTime interval, while also allowing _staticsTime to be seeded in advance through a zero-reward path.

2. Key Background

DBW tracks "static income" against getAllBalance(user), which includes both wallet DBW and LP converted into DBW-equivalent units. The LP conversion is live-market based: convertLPToDBW(count) = reserve1 * count / totalSupply, so a large LP position created just before claiming immediately increases the claimant's reward weight.

DBW also exposes a timestamp side effect. getStaticIncome() calls _staticIncome_(msg.sender), and _staticIncome_ writes _staticsTime[useraddr_] = block.timestamp before checking whether any reward is payable. That means an address can become "aged" even if the call transfers no DBW at all.

Finally, DBW relies on Address.isContract(msg.sender) in pledge_lp, getStaticIncome, and redemption_lp. That check is ineffective during contract construction, so a helper contract can call those functions from its constructor while still appearing as a non-contract caller.

3. Vulnerability Analysis & Root Cause Summary

This incident is an ATTACK-class ACT exploit, not an MEV-only liquidation. The broken invariant is straightforward: static-income rewards should accrue only against capital that actually remained exposed during the accrual window. DBW violates that invariant because staticIncome() multiplies the claimant's current getAllBalance() by the full elapsed time since _staticsTime. The contract then makes the problem operationally exploitable by letting _staticIncome_ seed _staticsTime even when count + countLP == 0. In practice, the attacker first created stale helper timestamps, then later recreated those same helper addresses, pledged temporary LP, and claimed rewards as though that LP had been present since block 26690535. Constructor-phase helpers bypassed DBW's intended EOA-only guard in both the helper-aging phase and the claim phase. The treasury payout was therefore retroactive windfall, not earned yield.

The relevant verified DBW implementation logic is:

function _staticIncome_(address useraddr_) internal virtual {
    uint256 count;
    uint256 countLP;
    (count, countLP) = staticIncome(useraddr_);
    _staticsTime[useraddr_] = block.timestamp;
    if (!_is_static_dynamic && count + countLP > 0 && !Address.isContract(useraddr_)) {
        _transfer(address(this), useraddr_, count + countLP);
    }
}

function staticIncome(address userAddress) public view returns (uint256, uint256) {
    uint256 _temp = _user_convertLPToDBW[userAddress];
    uint256 allBalance = getAllBalance(userAddress);
    (_time, p) = _getPtime(_staticsTime[userAddress]);
    return (allBalance * _time * (8 + p) / 100 / 2505600, _temp * _time * 12 / 100 / 2505600);
}

function pledge_lp(uint256 count) public {
    require(!Address.isContract(msg.sender), "the address is contract");
    _balances_lp[msg.sender] += count;
    _user_convertLPToDBW[msg.sender] = convertLPToDBW(_balances_lp[msg.sender]);
}

4. Detailed Root Cause Analysis

The first necessary state transition happened in transaction 0x17f91a8acd159d529061b7c54b4753d6df482775dda4e4ef3694e4c131ec0ea7 at block 26690535. That transaction came from EOA 0xe828d2cd28def4eb0d1b398a1972f64ceabdb99b, targeted attacker contract 0xe95b5d8bb0985ee17a1cbf70cd8dc968ed256fc8, used selector 0x4817bd92, emitted no logs, and still succeeded with gasUsed = 20870411. Because there were no logs, the relevant evidence is trace- and state-based rather than event-based.

Validator replay of that transaction confirms the helper-aging loop:

{
  "txhash": "0x17f91a8acd159d529061b7c54b4753d6df482775dda4e4ef3694e4c131ec0ea7",
  "create2_count": 360,
  "getStaticIncome_call_count": 360,
  "selfdestruct_count": 360,
  "sample_helpers": [
    "0x629c5970b7ea00b1abd37d03717a6ea18621e16c",
    "0x4916bd200f312d40b5f590c770cc9630b64452e0",
    "0xca1a8093ecdfe8b1cd198efb1462b25e64e93ca5"
  ]
}

Archival state confirms what that loop achieved. At block 26690534, _staticsTime for helpers 0x629c..., 0x4916..., and 0xca1a... was zero. At block 26690535, all three read as 1679503867. By block 26745691, the same helpers again had zero code, zero pledged LP, zero converted LP balance, and zero DBW balance, which proves the aging transaction seeded timestamps without leaving persistent capital behind.

The exploit transaction then monetized that state. The attacker flash-borrowed public liquidity, bought DBW, and minted DBW/USDT LP. The first confirmed helper loop from the seed trace is:

DBW::pledge_lp(2140982506786661818331008)
DBW::getStaticIncome()
emit Transfer(
  from: DBW,
  to:   0x4916bd200F312d40b5f590C770cC9630b64452E0,
  value: 25091186565161452343791
)
DBW::redemption_lp(2140982506786661818331008)

That same helper started the transaction with no LP and finished with the LP redeemed back out in the same transaction. The only reason the helper could receive 25091186565161452343791 raw DBW units was that pledge_lp() updated _user_convertLPToDBW immediately before staticIncome() multiplied the new LP-inflated balance across the entire elapsed time since 1679503867. Repeating that loop across many aged helpers drained 503464944192130199908004 raw DBW units from the treasury.

The concrete vulnerable components were:

  • staticIncome(address), which prices rewards from the claimant's current getAllBalance(user) over the full elapsed _staticsTime interval
  • _staticIncome_(address), which writes _staticsTime even when the call pays zero reward
  • pledge_lp(uint256) and redemption_lp(uint256), which let a helper add and remove LP in the same transaction
  • convertLPToDBW(uint256), which derives reward weight from live AMM reserves instead of time-weighted exposure

The ACT exploit conditions were also concrete and public:

  • the attacker needed at least one helper address with stale _staticsTime, which the helper-aging transaction created at block 26690535
  • the DBW treasury needed enough DBW balance to fund count + countLP
  • the DBW/USDT Pancake pair needed enough liquidity to buy DBW, mint temporary LP, and unwind back to USDT
  • DBW had to remain in static-income mode with _is_static_dynamic = false and the helper addresses could not hold the VIP role

5. Adversary Flow Analysis

The adversary strategy was a two-phase public attack: first seed stale timestamps through constructor-phase helper contracts, then later exploit those stale timestamps with flash-funded temporary LP and an AMM unwind.

  1. 0x17f91a8acd159d529061b7c54b4753d6df482775dda4e4ef3694e4c131ec0ea7 The attacker used public CREATE2 helper deployments to call DBW getStaticIncome() 360 times from constructor context. Each helper self-destructed immediately after seeding _staticsTime.
  2. 0x3b472f87431a52082bae7d8524b4e0af3cf930a105646259e1249f2218525607 The same adversary cluster used public DODO flashloans plus public AMM liquidity to acquire DBW and mint temporary DBW/USDT LP.
  3. The attacker recreated pre-aged helper addresses, transferred LP to each helper, and ran pledge_lp -> getStaticIncome -> redemption_lp from the helper constructor so the isContract guard would not block execution.
  4. Each helper pushed the claimed DBW back into the attacker-controlled path, allowing the next loop to operate on a larger LP position.
  5. After the loop, the attacker burned the accumulated LP, swapped residual DBW to USDT, repaid every public liquidity source, and transferred 19856552561850562288090 raw USDT units to EOA 0xe828d2cd28def4eb0d1b398a1972f64ceabdb99b.

6. Impact & Losses

The directly measured pool-side loss was 20190646127499113020914 raw USDT units from the DBW/USDT Pancake pair, using decimal = 18. Gross attacker proceeds were 19856552561850562288090 raw USDT units. The exploit transaction paid 0.51707939252 BNB in gas; valuing that gas against the public Pancake USDT/WBNB pair at block 26745691 gives 165807436576783368473 raw USDT units of fee. Net attacker profit was therefore 19690745125273778919617 raw USDT units.

The violated security principles were:

  • rewards must track time-weighted exposure rather than the claimant's instantaneous balance at claim time
  • timestamp initialization must not let empty addresses accumulate age before they hold qualifying capital
  • isContract must not be treated as a security boundary for economic claim flows

Affected public components were:

  • DBW proxy 0xbf5baea5113e9eb7009a6680747f2c7569dfc2d6
  • DBW implementation 0xb9ea86ca6ee0b2f4030c26ab54b6c5eb62d5c629
  • Pancake DBW/USDT pair 0x69d415fbdcd962d96257056f7fe382e432a3b540

7. References

  • Helper-aging transaction: 0x17f91a8acd159d529061b7c54b4753d6df482775dda4e4ef3694e4c131ec0ea7
  • Exploit transaction: 0x3b472f87431a52082bae7d8524b4e0af3cf930a105646259e1249f2218525607
  • Verified DBW implementation: 0xb9ea86ca6ee0b2f4030c26ab54b6c5eb62d5c629
  • DBW proxy victim contract: 0xbf5baea5113e9eb7009a6680747f2c7569dfc2d6
  • Pancake DBW/USDT pair: 0x69d415fbdcd962d96257056f7fe382e432a3b540
  • Seed exploit trace and balance diff from collector artifacts
  • Independent validator replay of the helper-aging transaction and archival state reads used to confirm _staticsTime writes and deterministic fee valuation