Calculated from recorded token losses using historical USD prices at the incident time.
0xbf5baea5113e9eb7009a6680747f2c7569dfc2d6BSC0x69d415fbdcd962d96257056f7fe382e432a3b540BSCDBW / 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.
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.
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]);
}
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 rewardpledge_lp(uint256) and redemption_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 exposureThe ACT exploit conditions were also concrete and public:
_staticsTime, which the helper-aging transaction created at block 26690535count + countLP_is_static_dynamic = false and the helper addresses could not hold the VIP roleThe 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.
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.0x3b472f87431a52082bae7d8524b4e0af3cf930a105646259e1249f2218525607
The same adversary cluster used public DODO flashloans plus public AMM liquidity to acquire DBW and mint temporary DBW/USDT LP.pledge_lp -> getStaticIncome -> redemption_lp from the helper constructor so the isContract guard would not block execution.19856552561850562288090 raw USDT units to EOA 0xe828d2cd28def4eb0d1b398a1972f64ceabdb99b.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:
isContract must not be treated as a security boundary for economic claim flowsAffected public components were:
0xbf5baea5113e9eb7009a6680747f2c7569dfc2d60xb9ea86ca6ee0b2f4030c26ab54b6c5eb62d5c6290x69d415fbdcd962d96257056f7fe382e432a3b5400x17f91a8acd159d529061b7c54b4753d6df482775dda4e4ef3694e4c131ec0ea70x3b472f87431a52082bae7d8524b4e0af3cf930a105646259e1249f22185256070xb9ea86ca6ee0b2f4030c26ab54b6c5eb62d5c6290xbf5baea5113e9eb7009a6680747f2c7569dfc2d60x69d415fbdcd962d96257056f7fe382e432a3b540_staticsTime writes and deterministic fee valuation