DBW Static-Income LP Drain
Exploit Transactions
Victim Addresses
0xbf5baea5113e9eb7009a6680747f2c7569dfc2d6BSC0x69d415fbdcd962d96257056f7fe382e432a3b540BSCLoss Breakdown
Similar Incidents
GGGTOKEN Treasury Drain via receive()
34%SNKMiner Referral Reward Drain
34%StakingDYNA Reward Backdating Drain
33%Cellframe Migration Drain
33%Sheep Burn Reserve Drain
32%QiQi Reward Quote Override Drain
32%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 currentgetAllBalance(user)over the full elapsed_staticsTimeinterval_staticIncome_(address), which writes_staticsTimeeven when the call pays zero rewardpledge_lp(uint256)andredemption_lp(uint256), which let a helper add and remove LP in the same transactionconvertLPToDBW(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 block26690535 - 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 = falseand 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.
0x17f91a8acd159d529061b7c54b4753d6df482775dda4e4ef3694e4c131ec0ea7The attacker used public CREATE2 helper deployments to call DBWgetStaticIncome()360 times from constructor context. Each helper self-destructed immediately after seeding_staticsTime.0x3b472f87431a52082bae7d8524b4e0af3cf930a105646259e1249f2218525607The same adversary cluster used public DODO flashloans plus public AMM liquidity to acquire DBW and mint temporary DBW/USDT LP.- The attacker recreated pre-aged helper addresses, transferred LP to each helper, and ran
pledge_lp -> getStaticIncome -> redemption_lpfrom the helper constructor so theisContractguard would not block execution. - Each helper pushed the claimed DBW back into the attacker-controlled path, allowing the next loop to operate on a larger LP position.
- After the loop, the attacker burned the accumulated LP, swapped residual DBW to USDT, repaid every public liquidity source, and transferred
19856552561850562288090raw USDT units to EOA0xe828d2cd28def4eb0d1b398a1972f64ceabdb99b.
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
isContractmust 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
_staticsTimewrites and deterministic fee valuation