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 verifiedMoolah.solimplementation - Source type: verified Solidity source in
0x9321587ea0dc8247f8f03e8696c047b2713bb79a/src/moolah/Moolah.sol
Vulnerable Function
- Function:
flashLoan(address,uint256,bytes) - Selector:
0xe0232b42 - Implementation file:
src/moolah/Moolah.sol - Callback interface:
IMoolahFlashLoanCallback.onMoolahFlashLoan(uint256,bytes)insrc/moolah/interfaces/IMoolahCallbacks.sol
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 trace shows the executor entering the proxy-level flashLoan() twice (decoded_calls.json indices 18 and 22) before the outer loan is settled; the four 0xe0232b42 appearances are two executor-to-proxy calls plus two proxy-to-implementation delegatecalls (18/19 and 22/23). 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; receipt metadata shows bootstrap contract0x8aa9...d535ascontractAddress, and the trace shows that bootstrap contract creating executor0xe812...b705. - The executor calls
MoolahProxy.flashLoan()for WBNB at decoded call18, which delegates into the implementation at19and calls back into attacker-controlledonMoolahFlashLoan()at21. - Inside that callback, the executor calls
MoolahProxy.flashLoan()again for USDT at decoded call22, which delegates at23and reenters attacker code again at25. - Using the callback window, the executor routes funds through the Aave-style pool proxy
0x6807dc923806fe8fd134338eabca509979a7e0cb, includingsupply(address,uint256,address,uint16)at225andborrow(address,uint256,uint256,uint16,address)at238. - The exploit path then enters
0x238a358808379702088667322f80ac48bad5e6c4withlock(bytes)at268, receiveslockAcquired(bytes)at269, and executestake(address,address,uint256)at270. - The deep middle of the trace contains repeated Pancake flash-callback hops (
pancakeV3FlashCallback(uint256,uint256,bytes)) and downstream Mai1/pair interactions. The Mai1 token path itself later callssync()on the Mai1/USDT pair at decoded call474, showing that the final profit realization occurs on the MAIL/Mai1 liquidity side, not from an unrepaid Moolah balance. - During unwind, the executor repays the intermediate routing layer with
repay()at636/637, withdraws withwithdraw()at650/651, then finalizes the0x238a...e6c4path withsync(address)at689andsettle()at692. funds_flow.jsoncredits the final attacker EOA with54,598.222194166280831143 USDT, while Moolah ends the transaction flat on the flash-loaned USDT/WBNB balances.
Trace Anchors
| Decoded call index | Meaning |
|---|---|
18 / 19 | First user-facing flashLoan() call plus proxy delegatecall |
21 | Moolah callback into attacker executor |
22 / 23 | Nested user-facing flashLoan() call plus proxy delegatecall |
225 | supply(address,uint256,address,uint16) on 0x6807...e0cb |
238 | borrow(address,uint256,uint256,uint16,address) on 0x6807...e0cb |
268 / 269 / 270 | lock -> lockAcquired -> take on 0x238a...e6c4 |
636 / 650 | repay and withdraw during unwind |
689 / 692 | sync(address) and settle() during final settlement |
Financial Impact
funds_flow.json is the primary source for the profit calculation and address-level balance changes.
| Address / component | Net change | Notes |
|---|---|---|
Attacker EOA 0xcb26...26e9 | +54,598.222194166280831143 USDT and -36,193.01499715 Mai1 | Final realized attacker gain reported by attacker_gains / net_changes |
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 | tx.json.from |
| Bootstrap contract | 0x8aa9cb61885121448f1bf9a5df80ec36c6fbd535 | receipt.json.contractAddress |
| Attack executor | 0xe812f2e6cdffdfa4ca496db0716a53301c37b705 | Created and called by bootstrap in trace_callTracer.json; receives both flash-loan callbacks |
| Vulnerable proxy | 0x8f73b65b4caaf64fba2af91cc5d4a2a1318e5d8c | User-facing flashLoan() target at decoded calls 18 and 22 |
| Vulnerable implementation | 0x9321587ea0dc8247f8f03e8696c047b2713bb79a | Delegatecall target at decoded calls 19 and 23; verified Moolah.sol source |
| Secondary routing pool proxy | 0x6807dc923806fe8fd134338eabca509979a7e0cb | Handles supply, borrow, repay, and withdraw during the exploit |
| Secondary routing pool implementation | 0x00d1397960aa97f694e41c3632b74c151a00c33b | Verified PoolInstance implementation for 0x6807...e0cb |
| 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 funds_flow.json |
Additional transaction facts:
- Transaction hash:
0x2fdd6aef515fb06ce803c55086bb71de712631979809c135cf6d02be133f5cdb - Chain: BNB Chain (
56) - Block:
98134017 - Timestamp:
2026-05-13T23:22:02Z - Status: success (
receipt.status = 0x1) - 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.
Artifacts
tx.json,receipt.jsontrace_callTracer.json,trace_prestateTracer.jsondecoded_calls.json,selectors.jsonfunds_flow.json- Verified Moolah source under
0x9321587ea0dc8247f8f03e8696c047b2713bb79a/ - Verified pool implementation under
0x00d1397960aa97f694e41c3632b74c151a00c33b/