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: IEXCBP at 0x575f30b2e7b7711be16d49a50786c8edabd755f1
  • 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:

  1. deposit() USDT into IEXCBP,
  2. earn direct / salary / fund-reward state tied to deposit volume,
  3. withdraw() or claim() from one account to receive IEX valued from the current pair state,
  4. 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

  1. The attacker calls exploit coordinator 0x98a78ac4ce073484143a5d58568d2f25d575864a with withdraw() (decoded_calls.json index 0).
  2. The coordinator flash-loans 424,027.908126859043165013 WBNB from Moolah proxy 0x8f73...5d8c (flashLoan(address,uint256,bytes) at indices 2/3).
  3. Inside onMoolahFlashLoan(uint256,bytes) (index 5), the attacker mints vWBNB on Venus market 0x6bca...16e9 (mint(uint256) at 6/7) and uses that collateral to borrow USDT from vUSDT market 0xfd58...0255 (borrow(uint256) at 30/31).
  4. The attacker then opens a second Moolah flash loan for USDT (indices 189/190) and additionally pulls USDT from 0x238a358808379702088667322f80ac48bad5e6c4 using lock(bytes) and take(address,address,uint256) (indices 193-197).
  5. With that capital, the attacker cycles funds through IEXCBP.deposit(uint256,uint256) and a predeployed helper tree. The first visible self-deposit is 4,500 USDT (index 198), followed by larger self-deposits including 21,000 USDT and 2,000,000 USDT.
  6. The exploit then fans out into 11 identical helper contracts such as 0x4806a319..., 0xd4a28e37..., and 0x0482d24a.... Each helper receives a large deposit(uint256) call from the attacker, then forwards the same amount into IEXCBP.deposit(uint256,uint256).
  7. 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...
  8. The attacker then repays vUSDT (repayBorrow(uint256) at 1008/1009), redeems the original WBNB collateral from vWBNB (redeemUnderlying(uint256) at 1026/1027), and returns both flash loans to Moolah. The profit remains at the attacker coordinator contract as 97,376.510550846423851866 USDT.

Trace Anchors

Decoded call indexMeaning
2 / 3First Moolah WBNB flash loan through proxy + implementation
5Callback onMoolahFlashLoan(uint256,bytes) into the attacker coordinator
6 / 7vWBNB.mint(uint256) to create collateral
30 / 31vUSDT.borrow(uint256) to source large USDT size
189 / 190Second Moolah flash loan, this time in USDT
193 - 197lock() / lockAcquired() / take() sequence that pulls additional USDT
198First 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 / 1009vUSDT.repayBorrow(uint256)
1026 / 1027vWBNB.redeemUnderlying(uint256)
1159Moolah 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 / componentNet changeNotes
Attack coordinator 0x98a78ac4ce073484143a5d58568d2f25d575864a+97,376.510550846423851866 USDTFinal realized attacker profit
IEXCBP-783,058.447656677122449571 IEXLarge reward-token outflow across attacker and helper withdrawals
Controller 0x1ff167f0...+42,113.642016617940024397 IEX5% withdrawal deductions captured by protocol controller
Burn address 0x0+13,739.956596309850633763 IEXIEX burn component from fee-on-transfer token mechanics
Moolah proxyflat on WBNB and USDTFlash loans are repaid by the end of the transaction
Venus vWBNB and vUSDT marketsflat on borrowed collateral balancesTemporary 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 USDT in the second leg
  • The helper tree then deposited an aggregate 595,852,877.8660663 USDT notionally across cloned child accounts, as shown by the repeated deposit(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

RoleAddressEvidence
Attacker EOA0xa8ef8a50e1556e1b378d442fdb115e834239fd28tx.json.from
Attack coordinator0x98a78ac4ce073484143a5d58568d2f25d575864aTop-level tx.json.to; accumulates final +97,376.51 USDT in funds_flow.json net_changes
Vulnerable contract0x575f30b2e7b7711be16d49a50786c8edabd755f1Repeated deposit(uint256,uint256) and withdraw() calls in decoded_calls.json
Flash loan provider0x8f73b65b4caaf64fba2af91cc5d4a2a1318e5d8cflashLoan(address,uint256,bytes) at decoded_calls.json indices 2, 189
Moolah implementation0x9321587ea0dc8247f8f03e8696c047b2713bb79aProxy implementation from 0x8f73.../info.json
vWBNB market0x6bca74586218db34cdb402295796b79663d816e9mint(uint256) and later redeemUnderlying(uint256)
vUSDT market0xfd5840cd36d94d7229439859c0112a4185bc0255borrow(uint256) and repayBorrow(uint256)
Venus comptroller0xfd36e2c2a6789db23113685031d7f16329158384mintAllowed, borrowAllowed, and price checks
IEX token0xc2827b8c91c98ef3974474bc0fcd4628f9ecdeceToken transferred out by IEXCBP.withdraw() then sold in Pancake swaps
Pancake IEX/USDT pair0x027c40e3f62a5091a4306a449e08a12b2b42a5ffgetReserves() and swap() drive spot quote and liquidation path
Pancake router0x10ed43c718714eb63d5aa57b78b54704e256024eUsed in both reward valuation and helper sell-downs
Example helper contract0x4806a319be8a6d7167543ecfb8d2ddeb7028ac1eUnverified 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

  1. Replace getAmountsOut()-based payout valuation with a manipulation-resistant oracle or TWAP-based conversion.
  2. Do not denominate internal reward balances in USD and then settle them with live spot-priced token amounts inside the same transaction.
  3. Add circuit breakers around repeated same-block deposit / withdraw / claim sequences, especially across related accounts.
  4. 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.
  5. 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.json
  • trace_callTracer.json
  • decoded_calls.json, selectors.json
  • funds_flow.json
  • Verified IEXCBP source under 0x575f30b2e7b7711be16d49a50786c8edabd755f1/
  • Verified Moolah implementation under 0x9321587ea0dc8247f8f03e8696c047b2713bb79a/
  • Verified Venus market/comptroller sources under 0x6bca..., 0xfd58..., and 0xfd36...