On May 13, 2026 at 23:22:02 UTC (BNB Chain block 98134017), attacker EOA 0xcb26b3a469c5aee911d059a25de2b26ed52826e9 executed transaction 0x2fdd6aef515fb06ce803c55086bb71de712631979809c135cf6d02be133f5cdb, which deployed bootstrap contract 0x8aa9cb61885121448f1bf9a5df80ec36c6fbd535 and executor 0xe812f2e6cdffdfa4ca496db0716a53301c37b705. The attacker used Moolah proxy 0x8f73b65b4caaf64fba2af91cc5d4a2a1318e5d8c as an unsafe flash-loan callback entrypoint, then composed nested flash loans, a large USDT borrow, and a deep Pancake/Vault routing path before unwinding the whole position. The transaction finishes with the attacker EOA netting 54,598.222194166280831143 USDT, while the Mai1/USDT liquidity path at 0xa0e4b7ade986004112a49d79fc1f8e27df4c1e03 ends down 62,544.672240586975276114 USDT and 127,438,133.706326618655250561 Mai1. The primary root cause is reentrancy through Moolah.flashLoan(address,uint256,bytes), with flash-loan abuse as the execution technique.

Root Cause

Vulnerable Contract

  • Proxy: MoolahProxy at 0x8f73b65b4caaf64fba2af91cc5d4a2a1318e5d8c
  • Implementation: Moolah at 0x9321587ea0dc8247f8f03e8696c047b2713bb79a
  • Proxy status: verified upgradeable proxy; the proxy delegates flashLoan into the verified implementation
  • Source type: verified Solidity source

Vulnerable Function

  • Function: flashLoan(address,uint256,bytes)
  • Selector: 0xe0232b42
  • Callback interface: IMoolahFlashLoanCallback.onMoolahFlashLoan(uint256,bytes)

Vulnerable Code

function flashLoan(address token, uint256 assets, bytes calldata data) external whenNotPaused {
  require(!flashLoanTokenBlacklist[token], ErrorsLib.TOKEN_BLACKLISTED);
  require(assets != 0, ErrorsLib.ZERO_ASSETS);

  emit EventsLib.FlashLoan(msg.sender, token, assets);

  IERC20(token).safeTransfer(msg.sender, assets);

  IMoolahFlashLoanCallback(msg.sender).onMoolahFlashLoan(assets, data); // <-- VULNERABILITY

  IERC20(token).safeTransferFrom(msg.sender, address(this), assets); // repayment only checked after callback
}

The callback target is explicitly exposed by the verified interface:

interface IMoolahFlashLoanCallback {
  function onMoolahFlashLoan(uint256 assets, bytes calldata data) external;
}

Why It’s Vulnerable

Expected behavior: a flash-loan entrypoint should either prevent reentrancy entirely or ensure the protocol cannot be driven through additional privileged state transitions before repayment is enforced.

Actual behavior: flashLoan() transfers assets out, yields control to an attacker-controlled callback, and only then attempts to pull repayment back. Unlike nearby stateful entrypoints such as supply(), withdraw(), borrow(), and repay(), this function is not protected by nonReentrant. The execution trace shows the attacker invoking the flash-loan entrypoint twice before the outer loan is settled, with each proxy entry delegating into the implementation and then handing control back to attacker code. That callback window let the attacker borrow additional capital, route it through supply, borrow, lock, take, sync, and settle, and still repay Moolah before the function returned.

Attack Execution

High-Level Flow

  1. The transaction is itself a contract-creation transaction from attacker EOA 0xcb26...26e9; on-chain creation data shows bootstrap contract 0x8aa9...d535, and the execution trace shows that bootstrap contract creating executor 0xe812...b705.
  2. The executor first calls MoolahProxy.flashLoan() for WBNB, which delegates into the implementation and immediately calls back into attacker-controlled onMoolahFlashLoan().
  3. Inside that callback, the executor opens a second MoolahProxy.flashLoan() for USDT, creating a nested callback window before the first flash loan has been settled.
  4. Using that reentrant window, the executor routes funds through the Aave-style pool proxy 0x6807dc923806fe8fd134338eabca509979a7e0cb, invoking supply(address,uint256,address,uint16) and then borrow(address,uint256,uint256,uint16,address).
  5. The exploit path then enters 0x238a358808379702088667322f80ac48bad5e6c4 through lock(bytes), receives lockAcquired(bytes), and executes take(address,address,uint256).
  6. The middle of the trace contains repeated Pancake flash-callback hops and downstream Mai1/pair interactions. The Mai1 token path later calls sync() on the Mai1/USDT pair, showing that the final profit realization occurs on the MAIL/Mai1 liquidity side rather than from an unpaid Moolah balance.
  7. During unwind, the executor repays the intermediate routing layer with repay(), withdraws with withdraw(), then finalizes the 0x238a...e6c4 path with sync(address) and settle().
  8. The attacker EOA ends the transaction with 54,598.222194166280831143 USDT, while Moolah finishes flat on the flash-loaned USDT and WBNB balances.

Financial Impact

Address / componentNet changeNotes
Attacker EOA 0xcb26...26e9+54,598.222194166280831143 USDT and -36,193.01499715 Mai1Final realized attacker gain
Executor 0xe812...b705effectively flat on USDT, tiny WBNB dustTemporary capital was recycled and repaid inside the same transaction
Moolah proxy 0x8f73...5d8cno lasting USDT/WBNB lossFlash-loaned balances return by the end of execution
Mai1/USDT path 0xa0e4...1e03-62,544.672240586975276114 USDT and -127,438,133.706326618655250561 Mai1Permanent downstream liquidity loss

Temporary capital was much larger than the final profit:

  • Moolah transfers 424,107.146731444623695429 WBNB to the executor at transfer log 53
  • Moolah transfers 7,265,733.22110355069872967 USDT to the executor at transfer log 55
  • The Venus vUSDT market transfers 95,247,564.226904911099771844 USDT to the executor at transfer log 68

Those large transient balances are important context: the 54.6k USDT figure is the final net attacker gain, not the gross amount routed through the exploit path.

Evidence

RoleAddressEvidence
Attacker EOA0xcb26b3a469c5aee911d059a25de2b26ed52826e9Transaction sender and profit recipient
Bootstrap contract0x8aa9cb61885121448f1bf9a5df80ec36c6fbd535Contract created by the transaction and used to deploy the executor
Attack executor0xe812f2e6cdffdfa4ca496db0716a53301c37b705Created and called by the bootstrap contract; receives both flash-loan callbacks
Vulnerable proxy0x8f73b65b4caaf64fba2af91cc5d4a2a1318e5d8cUser-facing flashLoan() target during both nested flash-loan entries
Vulnerable implementation0x9321587ea0dc8247f8f03e8696c047b2713bb79aDelegatecall target for both flashLoan() entries; contains the callback-before-repayment logic
Secondary routing pool proxy0x6807dc923806fe8fd134338eabca509979a7e0cbHandles supply, borrow, repay, and withdraw during the exploit
Secondary routing pool implementation0x00d1397960aa97f694e41c3632b74c151a00c33bDelegate target behind 0x6807...e0cb during pool interactions
Vault/adapter path0x238a358808379702088667322f80ac48bad5e6c4Handles lock, lockAcquired, take, sync(address), and settle()
MAIL / Mai1 token0x1ae83c24bb1f0968191b283237935645b4056b29Appears in final negative attacker net token flow and pair sync() path
Mai1/USDT pair0xa0e4b7ade986004112a49d79fc1f8e27df4c1e03Ends with the largest permanent liquidity loss in the traced settlement path

Additional transaction facts:

  • Transaction hash: 0x2fdd6aef515fb06ce803c55086bb71de712631979809c135cf6d02be133f5cdb
  • Chain: BNB Chain (56)
  • Block: 98134017
  • Timestamp: 2026-05-13T23:22:02Z
  • Status: success
  • Gas used: 0x73ef85 (7,597,957)

Remediation

  1. Add reentrancy protection to flashLoan() or restructure it so control is not yielded to an attacker-controlled callback before repayment is enforced.
  2. Treat flash-loan callbacks as privileged cross-protocol execution points and constrain what can happen before the loan is considered settled.
  3. Add invariant tests that explicitly forbid nested flashLoan() callback recursion from composing new borrow / settlement paths before the outer flash loan returns.
  4. Add integration tests that model callback-driven compositions with secondary lending pools and vault locks rather than testing flashLoan() in isolation.