Public Dust Conversion Over-Burn
Exploit Transactions
0xe7b7c974e51d8bca3617f927f86bf907a25991fe654f457991cbf656b190fe94Victim Addresses
0xb2b1dc3204ee8899d6575f419e72b53e370f6b20BSC0xc6747954a9b3a074d8e4168b444d7f397fee76aaBSC0xd35378e477d5d32f87ab977067561daf9a2c32aaBSC0x5587ba40b8b1ce090d1a61b293640a7d86fc4c2dBSCLoss Breakdown
Similar Incidents
Bearn Dust Sweep Exploit
39%UN Burn-Skim Exploit
36%ZS Pair Burn Drain
36%CoinToken Burn Reserve Drain
36%SafeMoon LP Burn Drain
36%CS Pair Balance Burn Drain
35%Root Cause Analysis
Public Dust Conversion Over-Burn
1. Incident Overview TL;DR
On BSC block 22629432, EOA 0x5bfaa396c6fb7278024c6d7230b17d97ce8ab62d executed transaction 0xe7b7c974e51d8bca3617f927f86bf907a25991fe654f457991cbf656b190fe94 through attacker-deployed helpers and reached BvaultsStrategy.convertDustToEarned() at 0xb2b1dc3204ee8899d6575f419e72b53e370f6b20. The sequence first sold 10,000 IEarn into the IEarn/WBNB pair, then triggered the strategy's public dust-conversion swap through BdexRouter, and finally bought back and burned 10,229.179233811368474425 IEarn. The excess burn over the attacker-supplied amount was 229.179233811368474425 IEarn, and that exact quantity was depleted from the IEarn/WBNB pair.
The root cause is a two-part public exploit path. BvaultsStrategy.convertDustToEarned() is publicly callable when the strategy is unpaused, and its conversion route reaches BdexRouter. BdexRouter misprices reflective fee-on-transfer swaps by computing effective input as IERC20(input).balanceOf(pair) - reserveInput, so reflection-driven BDEX balance growth at the pair is miscounted as user-supplied input. That misaccounting lets any unprivileged actor turn public state into deterministic IEarn over-burn.
2. Key Background
BvaultsStrategy is an auto-compounding strategy that accumulates dust balances in token0 and token1 and exposes a public maintenance function, convertDustToEarned(), to swap those balances into the configured earned token. In the verified strategy source, the function is public whenNotPaused, requires isAutoComp, reads the strategy's dust balances, and routes each non-zero dust balance through _vswapSwapToken(...).
function convertDustToEarned() public whenNotPaused {
require(isAutoComp, "!isAutoComp");
uint256 _token0Amt = IERC20(token0Address).balanceOf(address(this));
if (token0Address != earnedAddress && _token0Amt > 0) {
_vswapSwapToken(token0Address, earnedAddress, _token0Amt);
}
uint256 _token1Amt = IERC20(token1Address).balanceOf(address(this));
if (token1Address != earnedAddress && _token1Amt > 0) {
_vswapSwapToken(token1Address, earnedAddress, _token1Amt);
}
}
The relevant router-side path is BdexRouter at 0xc6747954a9b3a074d8e4168b444d7f397fee76aa, which supports fee-on-transfer assets. Its swap-support code does not measure actual trader input; instead it infers input from the pair's observed balance delta. That design is unsafe for reflective tokens because pair balances can drift for reasons other than the caller's transfer.
BdexToken is reflective. Its balanceOf returns tokenFromReflection(_rOwned[account]), and _reflectFee reduces _rTotal, changing the rate used by tokenFromReflection. As a result, balanceOf(pair) can change because the reflection rate changes, not because the trader actually transferred more BDEX to the pair.
3. Vulnerability Analysis & Root Cause Summary
This is an ATTACK-category ACT issue: a public strategy helper reaches a router accounting bug that any unprivileged actor can trigger from public chain state. The vulnerability is not an access-control bypass; the strategy intentionally exposes convertDustToEarned() publicly, and the attacker only needed to set up pair state and then call public contracts. The critical invariant is that fee-on-transfer support must calculate output from the trader's true net input into the pair. Reflection-side balance drift at the pair must not be treated as if the trader provided it.
The code-level breakpoint is inside BdexRouter support-fee-on-transfer logic. Both _swapSupportingFeeOnTransferTokens() and _swapSingleSupportFeeOnTransferTokens() derive amountInput from IERC20(input).balanceOf(address(pair)).sub(reserveInput) (or the same formula using pool). For a reflective token such as BDEX, that number can exceed the trader's actual input because reflection updates the pair's observable balance.
amountInput = IERC20(input).balanceOf(address(pair)).sub(reserveInput);
amountOutput = IBdexFormula(formula).getAmountOut(
amountInput,
reserveInput,
reserveOutput,
tokenWeightInput,
tokenWeightOutput,
swapFee
);
When the public strategy call routes WBNB dust through the Bdex/WBNB pair, the reflective token's balance drift causes the router to over-credit BDEX input and release too much output. The attacker then converts that distortion back through the IEarn/WBNB pair and burns more IEarn than originally supplied. The seed transaction and balance diff show the exact realization: 10,000 IEarn supplied, 10,229.179233811368474425 IEarn burned, and 229.179233811368474425 IEarn drained from the IEarn/WBNB pair.
4. Detailed Root Cause Analysis
The exploit begins from the publicly reconstructible pre-state immediately before transaction 0xe7b7c974e51d8bca3617f927f86bf907a25991fe654f457991cbf656b190fe94 in block 22629432. The relevant contracts are BvaultsStrategy (0xb2b1dc3204ee8899d6575f419e72b53e370f6b20), BdexRouter (0xc6747954a9b3a074d8e4168b444d7f397fee76aa), BdexToken proxy/token (0x7e0f01918d92b2750bbb18fcebeedd5b94ebb867), the IEarn/WBNB pair (0xd35378e477d5d32f87ab977067561daf9a2c32aa), and the Bdex/WBNB pair (0x5587ba40b8b1ce090d1a61b293640a7d86fc4c2d).
First, the attacker-controlled flow sells 10,000 IEarn into the IEarn/WBNB pair. The trace shows IEarnToken::transferFrom(..., 10000000000000000000000) into the pair and then a pair Swap emitting amount1Out = 34534837254230472565 WBNB to the attacker-controlled helper. This step provides the WBNB used to interact with the Bdex/WBNB side of the route.
Second, the attacker buys BDEX with that WBNB and positions the reflective-token state. The BDEX token is reflective because balanceOf depends on tokenFromReflection(_rOwned[account]), where tokenFromReflection uses _getRate(), and _reflectFee changes _rTotal. That means pair balances can increase or decrease when fees are reflected, even without a corresponding direct trader transfer.
function balanceOf(address account) public view override returns (uint256) {
if (_isExcluded[account]) return _tOwned[account];
return tokenFromReflection(_rOwned[account]);
}
function tokenFromReflection(uint256 rAmount) public view returns (uint256) {
uint256 currentRate = _getRate();
return rAmount.div(currentRate);
}
function _reflectFee(uint256 rFee, uint256 tFee) private {
_rTotal = _rTotal.sub(rFee);
_tFeeTotal = _tFeeTotal.add(tFee);
}
Third, the attacker triggers BvaultsStrategy.convertDustToEarned(). The trace shows the public strategy call and then the strategy invoking BdexRouter::swapExactTokensForTokens(WBNB, BDEX, 18924462198662965022, 1, [0x5587...], strategy, ...). This is the public maintenance entrypoint that exposes the vulnerable router path to any unprivileged caller as long as the strategy is unpaused and auto-compounding.
Fourth, the router miscomputes the effective BDEX input. In BdexRouter, amountInput is calculated from pair balanceOf - reserveInput, which is valid only if pair balance changes reflect trader transfers alone. Here, reflective BDEX accounting breaks that assumption. The router therefore computes a larger effective input than the trader actually supplied and releases too much output from the Bdex/WBNB pair.
Finally, the attacker sells the resulting BDEX back to WBNB, buys IEarn from the IEarn/WBNB pair, and burns the acquired IEarn. The seed trace records the final IEarn/WBNB pair swap(10229179233811368474425, 0, ...) and then IEarnToken::burn(10229179233811368474425). The balance diff confirms that the attacker EOA's IEarn balance decreased by exactly 10000000000000000000000, while the IEarn/WBNB pair lost 229179233811368474425 IEarn. That exact delta matches the excess burn over attacker input, proving the exploit predicate:
burnedAmount = 10229179233811368474425
inputAmount = 10000000000000000000000
excessBurn = 229179233811368474425
pairDepletion= 229179233811368474425
5. Adversary Flow Analysis
The adversary cluster consists of EOA 0x5bfaa396c6fb7278024c6d7230b17d97ce8ab62d and three helper contracts: 0x4a7c762d9af1066c9241c8c1b63681fd1b438d05, 0xa3b006df35217c5338afc7b1752cb43b4b3c5f9f, and 0x1958d75c082d7f10991d07e0016b45a0904d2eb1. The tx history around blocks 22629350 to 22629550 shows the EOA deploying helper logic before the exploit transaction, and the seed transaction metadata shows the exploit call being sent to helper 0x4a7c....
The end-to-end on-chain flow in transaction 0xe7b7c974... is:
- The EOA calls attacker helper
0x4a7c..., which dispatches into attacker helper0xa3b0.... - The helper sells
10,000IEarn into the IEarn/WBNB pair and receives34.534837254230472565WBNB. - The helper uses that WBNB to buy BDEX from the Bdex/WBNB pair.
- The helper triggers public
BvaultsStrategy.convertDustToEarned(), causing the strategy to route18.924462198662965022WBNB dust throughBdexRouterinto the reflective BDEX pair. - Because the router over-counts reflected BDEX as input, the attacker then sells BDEX back to WBNB and realizes a larger WBNB balance before the final IEarn buyback.
- The helper buys back
10229179233811368474425IEarn from the IEarn/WBNB pair and immediately burns it, causing the over-burn condition and equivalent pair depletion.
This sequence is permissionless. No privileged strategy operator call, private key compromise, or governance action is present in the trace. The attacker-controlled helper contracts are ordinary deployed contracts used for orchestration and do not change ACT classification.
6. Impact & Losses
The measurable impact in the validated seed transaction is a deterministic over-burn of 229179233811368474425 IEarn above the attacker's supplied amount. That same quantity is depleted from the IEarn/WBNB pair, which is the concrete victim-side asset loss. The public strategy call and router misaccounting together create a repeatable opportunity whenever the required preconditions are present: strategy callable, route configured through the reflective Bdex/WBNB pair, and dust/state positioned so the router observes reflective balance drift as synthetic input.
The validated loss for this incident record is:
[
{
"token_symbol": "IEarn",
"amount": "229179233811368474425",
"decimal": 18
}
]
7. References
- Seed transaction metadata for
0xe7b7c974e51d8bca3617f927f86bf907a25991fe654f457991cbf656b190fe94. - Seed full trace for the same transaction, including the public
convertDustToEarned()call, the router swap, the final IEarn buyback, andburn. - Seed balance diff showing
-10000000000000000000000IEarn from the attacker EOA and-229179233811368474425IEarn from the IEarn/WBNB pair. - Verified
BvaultsStrategysource showingconvertDustToEarned()is public. - Verified
BdexRoutersource showing the vulnerableamountInput = IERC20(input).balanceOf(pair) - reserveInputlogic. - BdexToken source showing
balanceOf,tokenFromReflection,_getRate, and_reflectFeereflection accounting. - Adversary tx history showing deployment and use of the helper contract cluster around the exploit block.