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:
MoolahProxyat0x8f73b65b4caaf64fba2af91cc5d4a2a1318e5d8c - Implementation:
Moolahat0x9321587ea0dc8247f8f03e8696c047b2713bb79a - Proxy status: verified upgradeable proxy; the proxy delegates
flashLoaninto 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
- The transaction is itself a contract-creation transaction from attacker EOA
0xcb26...26e9; on-chain creation data shows bootstrap contract0x8aa9...d535, and the execution trace shows that bootstrap contract creating executor0xe812...b705. - The executor first calls
MoolahProxy.flashLoan()for WBNB, which delegates into the implementation and immediately calls back into attacker-controlledonMoolahFlashLoan(). - 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. - Using that reentrant window, the executor routes funds through the Aave-style pool proxy
0x6807dc923806fe8fd134338eabca509979a7e0cb, invokingsupply(address,uint256,address,uint16)and thenborrow(address,uint256,uint256,uint16,address). - The exploit path then enters
0x238a358808379702088667322f80ac48bad5e6c4throughlock(bytes), receiveslockAcquired(bytes), and executestake(address,address,uint256). - 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. - During unwind, the executor repays the intermediate routing layer with
repay(), withdraws withwithdraw(), then finalizes the0x238a...e6c4path withsync(address)andsettle(). - 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 / component | Net change | Notes |
|---|---|---|
Attacker EOA 0xcb26...26e9 | +54,598.222194166280831143 USDT and -36,193.01499715 Mai1 | Final realized attacker gain |
Executor 0xe812...b705 | effectively flat on USDT, tiny WBNB dust | Temporary capital was recycled and repaid inside the same transaction |
Moolah proxy 0x8f73...5d8c | no lasting USDT/WBNB loss | Flash-loaned balances return by the end of execution |
Mai1/USDT path 0xa0e4...1e03 | -62,544.672240586975276114 USDT and -127,438,133.706326618655250561 Mai1 | Permanent downstream liquidity loss |
Temporary capital was much larger than the final profit:
- Moolah transfers
424,107.146731444623695429 WBNBto the executor at transfer log53 - Moolah transfers
7,265,733.22110355069872967 USDTto the executor at transfer log55 - The Venus vUSDT market transfers
95,247,564.226904911099771844 USDTto the executor at transfer log68
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
| Role | Address | Evidence |
|---|---|---|
| Attacker EOA | 0xcb26b3a469c5aee911d059a25de2b26ed52826e9 | Transaction sender and profit recipient |
| Bootstrap contract | 0x8aa9cb61885121448f1bf9a5df80ec36c6fbd535 | Contract created by the transaction and used to deploy the executor |
| Attack executor | 0xe812f2e6cdffdfa4ca496db0716a53301c37b705 | Created and called by the bootstrap contract; receives both flash-loan callbacks |
| Vulnerable proxy | 0x8f73b65b4caaf64fba2af91cc5d4a2a1318e5d8c | User-facing flashLoan() target during both nested flash-loan entries |
| Vulnerable implementation | 0x9321587ea0dc8247f8f03e8696c047b2713bb79a | Delegatecall target for both flashLoan() entries; contains the callback-before-repayment logic |
| Secondary routing pool proxy | 0x6807dc923806fe8fd134338eabca509979a7e0cb | Handles supply, borrow, repay, and withdraw during the exploit |
| Secondary routing pool implementation | 0x00d1397960aa97f694e41c3632b74c151a00c33b | Delegate target behind 0x6807...e0cb during pool interactions |
| Vault/adapter path | 0x238a358808379702088667322f80ac48bad5e6c4 | Handles lock, lockAcquired, take, sync(address), and settle() |
| MAIL / Mai1 token | 0x1ae83c24bb1f0968191b283237935645b4056b29 | Appears in final negative attacker net token flow and pair sync() path |
| Mai1/USDT pair | 0xa0e4b7ade986004112a49d79fc1f8e27df4c1e03 | Ends 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
- Add reentrancy protection to
flashLoan()or restructure it so control is not yielded to an attacker-controlled callback before repayment is enforced. - Treat flash-loan callbacks as privileged cross-protocol execution points and constrain what can happen before the loan is considered settled.
- Add invariant tests that explicitly forbid nested
flashLoan()callback recursion from composing new borrow / settlement paths before the outer flash loan returns. - Add integration tests that model callback-driven compositions with secondary lending pools and vault locks rather than testing
flashLoan()in isolation.