BXH Bonus Oracle Manipulation
Exploit Transactions
0x9e121c8d61a2a4992e3b9f0fc9805bc352127d16885a6752af790fbdb124399c0x326c15f335215ce84ae1f2ebdb0b46cf460cd545040f6468de5f23cb3fe329a40xecac48848d39efb86967ad442314a12e422f79036286066affc2aac97582787e0x1f2993eaaf776c18be3ca544eb2ca6f32671b159d00db3210b4ce0da6ccc435c0xa13c8c7a0c97093dba3096c88044273c29cebeee109e23622cd412dcca8f50f4Victim Addresses
0x27539b1dee647b38e1b987c41c5336b1a8dce663BSCLoss Breakdown
Similar Incidents
EGD Finance Reward Oracle Manipulation
41%SellToken Reward Oracle Manipulation
40%TiFi Oracle Manipulation
39%NovaX TokenStake Oracle Manipulation Exploit
37%SellToken Short Oracle Manipulation
36%SheepFarm Bonus Replay Drain
35%Root Cause Analysis
BXH Bonus Oracle Manipulation
1. Incident Overview TL;DR
BXH staking pool 0x27539b1dee647b38e1b987c41c5336b1a8dce663 paid USDT bonuses by converting pending BXH rewards through the live reserves of BXH/USDT pair 0x919964b7f12a742e3d33176d7af9094ea4152e6f. In exploit transaction 0xa13c8c7a0c97093dba3096c88044273c29cebeee109e23622cd412dcca8f50f4 on BSC, the adversary flash-borrowed USDT, distorted that pair's reserves in the same transaction, then called deposit(0,0) to harvest an inflated USDT payout directly from the staking pool. The collected balance diff shows the staking pool lost 39885623577085020521681 USDT units net and the attacker contract ended with 31794192560957275136775 USDT units net profit.
The root cause is a manipulable spot-price oracle inside value-transfer logic. getITokenBonusAmount trusts same-transaction AMM reserves, depositIToken and withdrawIToken convert pending BXH rewards through that function, and safeBonusTransfer sends the computed USDT amount with no manipulation resistance and no payout cap.
2. Key Background
Pool 0 of TokenStakingPoolDelegate accepts vUSDT deposits, accrues BXH rewards, and can optionally pay rewards in a separate bonus token. For this pool, bonus payout was enabled with USDT as the bonus token and the BXH/USDT pair as the price source.
A PancakeSwap-style pair exposes reserves through getReserves() and supports flash-swaps. That means an unprivileged contract can borrow USDT, change the pair's reserve ratio, perform additional calls while the manipulated reserves remain live, and repay before transaction end.
The relevant victim-side pricing code is below:
function getITokenBonusAmount(uint256 _pid, uint256 _amountInToken) public view returns (uint256) {
PoolInfo storage pool = poolInfo[_pid];
(uint112 _reserve0, uint112 _reserve1, ) = IUniswapV2Pair(pool.swapPairAddress).getReserves();
uint256 amountTokenOut = 0;
uint256 _fee = 0;
if (IUniswapV2Pair(pool.swapPairAddress).token0() == address(iToken)) {
amountTokenOut = getAmountOut(_amountInToken, _reserve0, _reserve1, _fee);
} else {
amountTokenOut = getAmountOut(_amountInToken, _reserve1, _reserve0, _fee);
}
return amountTokenOut;
}
Because deposit(0,0) is also the reward-claim path, a user who already has stake can trigger payout without adding more principal.
3. Vulnerability Analysis & Root Cause Summary
The issue is an attack-class protocol bug, not pure MEV. BXH let reward payout logic trust the instantaneous state of a manipulable AMM pair during the same transaction that performs the payout. The contract treated the pair reserve ratio as a fair BXH-to-USDT conversion rate even though any sufficiently funded attacker could move that ratio immediately before claiming. The implementation also set feeFactor to zero in getAmountOut, which made the conversion even more favorable to the claimer than a normal swap path. Once the manipulated amount was computed, the staking pool did not cap or sanity-check the resulting USDT transfer. Instead, it sent the computed amount directly from pool inventory. The exploit therefore converts a small pending BXH reward into a very large USDT withdrawal by transiently changing the pair state at the exact payout breakpoint.
4. Detailed Root Cause Analysis
The vulnerable payout path is straightforward in the staking contract:
function depositIToken(uint256 _pid, uint256 _amount, address _user) private {
PoolInfo storage pool = poolInfo[_pid];
UserInfo storage user = userInfo[_pid][_user];
updatePool(_pid);
if (user.amount > 0) {
uint256 pendingAmount = user.amount.mul(pool.accITokenPerShare).div(1e12).sub(user.rewardDebt);
if (pendingAmount > 0) {
if (pool.enableBonus == false) {
safeITokenTransfer(_user, pendingAmount);
} else {
pendingAmount = getITokenBonusAmount(_pid, pendingAmount);
safeBonusTransfer(_pid, _user, pendingAmount);
}
}
}
}
function safeBonusTransfer(uint256 _pid,address _to, uint256 _amount) internal {
PoolInfo storage pool = poolInfo[_pid];
IERC20(pool.bonusToken).transfer(_to, _amount);
}
The safety invariant is: an unprivileged claimer must not be able to decide the USDT reward amount by transiently changing the reserves of the AMM pair consulted inside the same claim transaction. BXH breaks that invariant because the payout breakpoint is exactly the live getReserves() read inside getITokenBonusAmount.
The on-chain exploit trace shows the full sequence. The attacker flash-swapped 3178800000000000000000000 USDT from pair 0x16b9..., swapped 3148800000000000000000000 USDT into BXH on the configured BXH/USDT pair, then claimed rewards while the pair remained distorted:
0x16b9...::swap(3178800000000000000000000, 0, attacker, 0x00)
BEP20USDT::transfer(attacker, 3178800000000000000000000)
attacker::pancakeCall(...)
0x6A1A...::swapExactTokensForTokensSupportingFeeOnTransferTokens(
3148800000000000000000000,
0,
[USDT, BXH],
attacker,
1664375098
)
0x27539...::deposit(0, 0)
BEP20USDT::transfer(attacker, 40085623577085020521681)
That trace aligns with the victim-code breakpoint. Before manipulation, the forked PoC measured a fair bonus of only 2599385079683094565 USDT units for the accrued BXH reward. After reserve distortion, getITokenBonusAmount returned 36840242649124830773953 USDT units in the validator execution log, which is more than four orders of magnitude larger. The seed exploit trace paid 40085623577085020521681 USDT units in the real transaction, and the seed balance diff shows the staking contract finished with a net USDT loss of 39885623577085020521681.
The exploit required no privileged keys and no protocol admin action. The attacker only needed a reward-bearing position, access to a flash-swapable USDT source, and a claim path that consulted live pair reserves. Those conditions make the issue an ACT opportunity.
5. Adversary Flow Analysis
The adversary lifecycle observed in the collected window is:
- Deploy attacker contract
0x4e77df7b9cdcecec4115e59546f3eacba095a89fin tx0x9e121c8d61a2a4992e3b9f0fc9805bc352127d16885a6752af790fbdb124399c. - Fund that contract with two vUSDT transfers in txs
0x326c15f335215ce84ae1f2ebdb0b46cf460cd545040f6468de5f23cb3fe329a4and0xecac48848d39efb86967ad442314a12e422f79036286066affc2aac97582787e. - Prime the staking position with
deposit(0, stakeAmount)in tx0x1f2993eaaf776c18be3ca544eb2ca6f32671b159d00db3210b4ce0da6ccc435c. - Attempt the exploit twice and revert in txs
0xf66f8916e0985d0c6b4d79a2f35b7336879af9b05ea77ea706463671d1472fadand0x08d9a68c81651c709964b98e33563c60994bb5ff35a9903d9a3aa4dff9e93027. - Succeed in tx
0xa13c8c7a0c97093dba3096c88044273c29cebeee109e23622cd412dcca8f50f4by flash-borrowing USDT, buying BXH to skew the pair, optionally topping up the pool, callingdeposit(0,0), swapping BXH back to USDT, repaying the flash-swap, and retaining profit.
The attacker tx window ties the EOA 0x81c63d821b7cdf70c61009a81fef8db5949ac0c9 to the deployment, funding, priming, failed attempts, and successful exploit. The seed balance diff ties the profit realization to the attacker contract:
{
"token": "0x55d398326f99059ff775485246999027b3197955",
"holder": "0x4e77df7b9cdcecec4115e59546f3eacba095a89f",
"before": "0",
"after": "31794192560957275136775",
"delta": "31794192560957275136775"
}
6. Impact & Losses
The impact was direct treasury depletion from the staking contract's USDT inventory. The seed exploit transaction transferred 40085623577085020521681 USDT units out of the staking pool during the manipulated claim. After accounting for the attacker's temporary top-up and the rest of the transaction flow, the collected balance diff shows the pool's net loss was 39885623577085020521681 USDT units. The attacker contract retained 31794192560957275136775 USDT units after repaying the flash-swap.
The affected victim component is the BXH staking pool at 0x27539b1dee647b38e1b987c41c5336b1a8dce663. The manipulated BXH/USDT pair at 0x919964b7f12a742e3d33176d7af9094ea4152e6f was instrumental to the exploit, but the economic loss was borne by the staking pool inventory.
7. References
- Seed exploit transaction:
0xa13c8c7a0c97093dba3096c88044273c29cebeee109e23622cd412dcca8f50f4 - Attacker deployment transaction:
0x9e121c8d61a2a4992e3b9f0fc9805bc352127d16885a6752af790fbdb124399c - Victim staking contract:
0x27539b1dee647b38e1b987c41c5336b1a8dce663 - Manipulated BXH/USDT pair:
0x919964b7f12a742e3d33176d7af9094ea4152e6f - Flash-swap source pair:
0x16b9a82891338f9ba80e2d6970fdda79d1eb0dae - Supporting artifacts used for validation:
artifacts/collector/iter_1/contract/56/0x27539b1dee647b38e1b987c41c5336b1a8dce663/TokenStakingPoolDelegate.solartifacts/collector/iter_1/address/56/0x81c63d821b7cdf70c61009a81fef8db5949ac0c9/txlist_relevant_window.jsonartifacts/collector/seed/56/0xa13c8c7a0c97093dba3096c88044273c29cebeee109e23622cd412dcca8f50f4/trace.cast.logartifacts/collector/seed/56/0xa13c8c7a0c97093dba3096c88044273c29cebeee109e23622cd412dcca8f50f4/balance_diff.jsonartifacts/validator/forge-test.log