On May 12, 2026 at 23:34:42 UTC (BNB Chain block 97915293), attacker EOA 0xa8ef8a50e1556e1b378d442fdb115e834239fd28 executed transaction 0x11a5740c5fff435e92cc3aef57d5b0526cca9ae3c96f299d7b0442c938ace132 through exploit coordinator 0x98a78ac4ce073484143a5d58568d2f25d575864a. The attacker flash-loaned WBNB and USDT from Moolah, posted WBNB as collateral on Venus, borrowed a much larger USDT balance, and then recycled that capital through a prepositioned tree of helper accounts that interacted with IEXCBP at 0x575f30b2e7b7711be16d49a50786c8edabd755f1. The core flaw is that IEXCBP.withdraw() values USD-denominated rewards using Pancake’s live getAmountsOut() spot quote, so every attacker-driven IEX sale affects the next account’s payout calculation. The exploit ends with the attacker contract holding 97,376.510550846423851866 USDT, which matches the public loss estimate of about $97.4K.
Root Cause
Vulnerable Contract
- Contract:
IEXCBPat0x575f30b2e7b7711be16d49a50786c8edabd755f1 - Proxy status: not a proxy
- Source type: verified Solidity source at
0x575f30b2e7b7711be16d49a50786c8edabd755f1/solidity/IEXCBP.sol
Vulnerable Function
- Function:
withdraw() - Selector:
0x3ccfd60b - Implementation file:
solidity/IEXCBP.sol
Vulnerable Code
function withdraw() public nonReentrant {
User storage user = users[msg.sender];
require(user.isExist, "User not exist");
uint256 roiPending = _calculatePendingROI(msg.sender);
uint256 levelPending = _calculatePendingLevelIncome(msg.sender);
uint256 uplinePending = _calculatePendingUplineIncome(msg.sender);
if (roiPending > 0) {
user.earnedFromRoi += roiPending;
_distributeRoiToInvestments(msg.sender, user);
}
if (levelPending > 0) {
user.earnedFromLevel += levelPending;
user.accumulatedLevelIncome = 0;
}
if (uplinePending > 0) {
user.earnedFromUpline += uplinePending;
}
user.lastLevelWithdraw = block.timestamp;
user.lastUplineWithdraw = block.timestamp;
_creditIncome(user, roiPending + levelPending + uplinePending); // <-- VULNERABILITY
uint256 payoutUSD = user.availableWalletUSD;
require(payoutUSD > 0, "No funds");
user.availableWalletUSD = 0;
uint256 totalIex = getIexAmountFromUsd(payoutUSD); // <-- VULNERABILITY
require(
iexToken.balanceOf(address(this)) >= totalIex,
"Contract Low Bal"
);
uint256 deduction = (totalIex * withdrawal_deduction) / 100;
uint256 userAmount = totalIex - deduction;
if (deduction > 0) {
_safeTransfer(iexToken, CONTROLLER_ID, deduction);
}
_safeTransfer(iexToken, msg.sender, userAmount);
}
function getIexAmountFromUsd(uint256 _usdAmount) public view returns (uint256) {
address[] memory path = new address[](2);
path[0] = address(usdtToken);
path[1] = address(iexToken);
return pancakeRouter.getAmountsOut(_usdAmount, path)[1]; // <-- VULNERABILITY
}
Why It’s Vulnerable
Expected behavior: USD-denominated reward balances should be settled with a robust, manipulation-resistant conversion rate, or by transferring the underlying accounting asset directly. If a protocol wants to pay rewards in IEX, it should not let the same transaction manipulate the exact spot quote used for payout.
Actual behavior: withdraw() first credits pending ROI, level income, and upline income into availableWalletUSD, then converts that USD balance into IEX using pancakeRouter.getAmountsOut() on the live IEX/USDT pair. That is a same-transaction spot quote with no TWAP, no oracle check, and no slippage guard. Because the attacker controls a large borrowed USDT balance and a fleet of helper accounts, they can repeatedly:
deposit()USDT intoIEXCBP,- earn direct / salary / fund-reward state tied to deposit volume,
withdraw()orclaim()from one account to receive IEX valued from the current pair state,- immediately dump that IEX back into USDT on Pancake, changing reserves before the next payout.
This is both a price manipulation bug and a reward-accounting logic bug: the contract treats an attacker-controlled same-block AMM quote as an authoritative exchange rate for internal reward balances.
Attack Execution
High-Level Flow
- The attacker calls exploit coordinator
0x98a78ac4ce073484143a5d58568d2f25d575864awithwithdraw()(decoded_calls.jsonindex0). - The coordinator flash-loans
424,027.908126859043165013 WBNBfrom Moolah proxy0x8f73...5d8c(flashLoan(address,uint256,bytes)at indices2/3). - Inside
onMoolahFlashLoan(uint256,bytes)(index 5), the attacker mintsvWBNBon Venus market0x6bca...16e9(mint(uint256)at6/7) and uses that collateral to borrow USDT fromvUSDTmarket0xfd58...0255(borrow(uint256)at30/31). - The attacker then opens a second Moolah flash loan for USDT (
indices 189/190) and additionally pulls USDT from0x238a358808379702088667322f80ac48bad5e6c4usinglock(bytes)andtake(address,address,uint256)(indices 193-197). - With that capital, the attacker cycles funds through
IEXCBP.deposit(uint256,uint256)and a predeployed helper tree. The first visible self-deposit is4,500 USDT(index 198), followed by larger self-deposits including21,000 USDTand2,000,000 USDT. - The exploit then fans out into 11 identical helper contracts such as
0x4806a319...,0xd4a28e37..., and0x0482d24a.... Each helper receives a largedeposit(uint256)call from the attacker, then forwards the same amount intoIEXCBP.deposit(uint256,uint256). - After seeding those helper accounts, the attacker invokes each helper’s
claim()function. The trace shows the same pattern repeatedly:- helper
claim()->IEXCBP.withdraw() - helper receives IEX from
IEXCBP - helper immediately calls Pancake
swapExactTokensForTokensSupportingFeeOnTransferTokens(...) - helper sells IEX back into USDT on pair
0x027c40e3...
- helper
- The attacker then repays
vUSDT(repayBorrow(uint256)at1008/1009), redeems the original WBNB collateral fromvWBNB(redeemUnderlying(uint256)at1026/1027), and returns both flash loans to Moolah. The profit remains at the attacker coordinator contract as97,376.510550846423851866 USDT.
Trace Anchors
| Decoded call index | Meaning |
|---|---|
2 / 3 | First Moolah WBNB flash loan through proxy + implementation |
5 | Callback onMoolahFlashLoan(uint256,bytes) into the attacker coordinator |
6 / 7 | vWBNB.mint(uint256) to create collateral |
30 / 31 | vUSDT.borrow(uint256) to source large USDT size |
189 / 190 | Second Moolah flash loan, this time in USDT |
193 - 197 | lock() / lockAcquired() / take() sequence that pulls additional USDT |
198 | First direct IEXCBP.deposit(uint256,uint256) |
214, 236, 258, … | Helper deposit(uint256) fan-out calls |
822, 840, 858, … | Helper claim() fan-out calls |
823, 841, 859, … | Nested IEXCBP.withdraw() executions from helper accounts |
830, 848, 866, … | Helper IEX -> USDT swap calls on Pancake |
1008 / 1009 | vUSDT.repayBorrow(uint256) |
1026 / 1027 | vWBNB.redeemUnderlying(uint256) |
1159 | Moolah WBNB flash-loan repayment via transferFrom |
Financial Impact
funds_flow.json does not classify attacker gains correctly for this incident, so the reliable source is net_changes plus the raw transfer sequence.
| Address / component | Net change | Notes |
|---|---|---|
Attack coordinator 0x98a78ac4ce073484143a5d58568d2f25d575864a | +97,376.510550846423851866 USDT | Final realized attacker profit |
IEXCBP | -783,058.447656677122449571 IEX | Large reward-token outflow across attacker and helper withdrawals |
Controller 0x1ff167f0... | +42,113.642016617940024397 IEX | 5% withdrawal deductions captured by protocol controller |
Burn address 0x0 | +13,739.956596309850633763 IEX | IEX burn component from fee-on-transfer token mechanics |
| Moolah proxy | flat on WBNB and USDT | Flash loans are repaid by the end of the transaction |
Venus vWBNB and vUSDT markets | flat on borrowed collateral balances | Temporary capital source, not the ultimate loss bearer |
Large transient amounts moved through the exploit path:
- Moolah flash-loaned
424,027.908126859043165013 WBNB - Moolah flash-loaned
117,203,564.21477796070389642 USDTin the second leg - The helper tree then deposited an aggregate
595,852,877.8660663 USDTnotionally across cloned child accounts, as shown by the repeateddeposit(uint256)calldata values
The attacker did not need to keep those temporary balances. They only needed them long enough to distort reward-tree state and harvest inflated IEX payouts that could be dumped back to USDT.
Evidence
| Role | Address | Evidence |
|---|---|---|
| Attacker EOA | 0xa8ef8a50e1556e1b378d442fdb115e834239fd28 | tx.json.from |
| Attack coordinator | 0x98a78ac4ce073484143a5d58568d2f25d575864a | Top-level tx.json.to; accumulates final +97,376.51 USDT in funds_flow.json net_changes |
| Vulnerable contract | 0x575f30b2e7b7711be16d49a50786c8edabd755f1 | Repeated deposit(uint256,uint256) and withdraw() calls in decoded_calls.json |
| Flash loan provider | 0x8f73b65b4caaf64fba2af91cc5d4a2a1318e5d8c | flashLoan(address,uint256,bytes) at decoded_calls.json indices 2, 189 |
| Moolah implementation | 0x9321587ea0dc8247f8f03e8696c047b2713bb79a | Proxy implementation from 0x8f73.../info.json |
| vWBNB market | 0x6bca74586218db34cdb402295796b79663d816e9 | mint(uint256) and later redeemUnderlying(uint256) |
| vUSDT market | 0xfd5840cd36d94d7229439859c0112a4185bc0255 | borrow(uint256) and repayBorrow(uint256) |
| Venus comptroller | 0xfd36e2c2a6789db23113685031d7f16329158384 | mintAllowed, borrowAllowed, and price checks |
| IEX token | 0xc2827b8c91c98ef3974474bc0fcd4628f9ecdece | Token transferred out by IEXCBP.withdraw() then sold in Pancake swaps |
| Pancake IEX/USDT pair | 0x027c40e3f62a5091a4306a449e08a12b2b42a5ff | getReserves() and swap() drive spot quote and liquidation path |
| Pancake router | 0x10ed43c718714eb63d5aa57b78b54704e256024e | Used in both reward valuation and helper sell-downs |
| Example helper contract | 0x4806a319be8a6d7167543ecfb8d2ddeb7028ac1e | Unverified clone showing claim() -> withdraw() -> swap pattern |
Additional transaction facts:
- Transaction hash:
0x11a5740c5fff435e92cc3aef57d5b0526cca9ae3c96f299d7b0442c938ace132 - Chain: BNB Chain (
56) - Block:
97915293 - Timestamp:
2026-05-12T23:34:42Z - Status: success (
receipt.status = 0x1)
Remediation
- Replace
getAmountsOut()-based payout valuation with a manipulation-resistant oracle or TWAP-based conversion. - Do not denominate internal reward balances in USD and then settle them with live spot-priced token amounts inside the same transaction.
- Add circuit breakers around repeated same-block
deposit/withdraw/claimsequences, especially across related accounts. - Treat reward-tree qualification state as resistant to flash-loaned or same-block transient volume. Time-weighted or delayed activation would have broken this exploit.
- Add tests that simulate:
- many helper accounts controlled by one attacker,
- flash-loaned deposits,
- immediate helper claims,
- same-block IEX/USDT reserve changes before subsequent withdrawals.
Artifacts
tx.json,receipt.jsontrace_callTracer.jsondecoded_calls.json,selectors.jsonfunds_flow.json- Verified
IEXCBPsource under0x575f30b2e7b7711be16d49a50786c8edabd755f1/ - Verified
Moolahimplementation under0x9321587ea0dc8247f8f03e8696c047b2713bb79a/ - Verified
Venusmarket/comptroller sources under0x6bca...,0xfd58..., and0xfd36...