2026-05-13 · Loss: ~54,598.22 USDT · Reentrancy
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.
MoolahProxy at 0x8f73b65b4caaf64fba2af91cc5d4a2a1318e5d8cMoolah at 0x9321587ea0dc8247f8f03e8696c047b2713bb79aflashLoan into the verified Moolah.sol implementation0x9321587ea0dc8247f8f03e8696c047b2713bb79a/src/moolah/Moolah.solflashLoan(address,uint256,bytes)0xe0232b42src/moolah/Moolah.solIMoolahFlashLoanCallback.onMoolahFlashLoan(uint256,bytes) in src/moolah/interfaces/IMoolahCallbacks.solfunction 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;
}
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.
0xcb26...26e9; receipt metadata shows bootstrap contract 0x8aa9...d535 as contractAddress, and the trace shows that bootstrap contract creating executor 0xe812...b705.MoolahProxy.flashLoan() for WBNB at decoded call 18, which delegates into the implementation at 19 and calls back into attacker-controlled onMoolahFlashLoan() at 21.MoolahProxy.flashLoan() again for USDT at decoded call 22, which delegates at 23 and reenters attacker code again at 25.0x6807dc923806fe8fd134338eabca509979a7e0cb, including supply(address,uint256,address,uint16) at 225 and borrow(address,uint256,uint256,uint16,address) at 238.0x238a358808379702088667322f80ac48bad5e6c4 with lock(bytes) at 268, receives lockAcquired(bytes) at 269, and executes take(address,address,uint256) at 270.pancakeV3FlashCallback(uint256,uint256,bytes)) and downstream Mai1/pair interactions. The Mai1 token path itself later calls sync() on the Mai1/USDT pair at decoded call 474, showing that the final profit realization occurs on the MAIL/Mai1 liquidity side, not from an unrepaid Moolah balance.repay() at 636/637, withdraws with withdraw() at 650/651, then finalizes the 0x238a...e6c4 path with sync(address) at 689 and settle() at 692.funds_flow.json credits the final attacker EOA with 54,598.222194166280831143 USDT, while Moolah ends the transaction flat on the flash-loaned USDT/WBNB balances.| 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 |
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:
424,107.146731444623695429 WBNB to the executor at transfer log 537,265,733.22110355069872967 USDT to the executor at transfer log 5595,247,564.226904911099771844 USDT to the executor at transfer log 68Those 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.
| 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:
0x2fdd6aef515fb06ce803c55086bb71de712631979809c135cf6d02be133f5cdb56)981340172026-05-13T23:22:02Zreceipt.status = 0x1)0x73ef85 (7,597,957)flashLoan() or restructure it so control is not yielded to an attacker-controlled callback before repayment is enforced.flashLoan() callback recursion from composing new borrow / settlement paths before the outer flash loan returns.flashLoan() in isolation.tx.json, receipt.jsontrace_callTracer.json, trace_prestateTracer.jsondecoded_calls.json, selectors.jsonfunds_flow.json0x9321587ea0dc8247f8f03e8696c047b2713bb79a/0x00d1397960aa97f694e41c3632b74c151a00c33b/