MAIL token drain via Moolah flash-loan callback reentrancy
2026-5-12 23:59:53 Author: www.darknavy.org(查看原文) 阅读量:0 收藏

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.

Root Cause

Vulnerable Contract

  • Proxy: MoolahProxy at 0x8f73b65b4caaf64fba2af91cc5d4a2a1318e5d8c
  • Implementation: Moolah at 0x9321587ea0dc8247f8f03e8696c047b2713bb79a
  • Proxy status: verified upgradeable proxy; the proxy delegates flashLoan into the verified Moolah.sol implementation
  • 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) in src/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

  1. The transaction is itself a contract-creation transaction from attacker EOA 0xcb26...26e9; receipt metadata shows bootstrap contract 0x8aa9...d535 as contractAddress, and the trace shows that bootstrap contract creating executor 0xe812...b705.
  2. The executor calls MoolahProxy.flashLoan() for WBNB at decoded call 18, which delegates into the implementation at 19 and calls back into attacker-controlled onMoolahFlashLoan() at 21.
  3. Inside that callback, the executor calls MoolahProxy.flashLoan() again for USDT at decoded call 22, which delegates at 23 and reenters attacker code again at 25.
  4. Using the callback window, the executor routes funds through the Aave-style pool proxy 0x6807dc923806fe8fd134338eabca509979a7e0cb, including supply(address,uint256,address,uint16) at 225 and borrow(address,uint256,uint256,uint16,address) at 238.
  5. The exploit path then enters 0x238a358808379702088667322f80ac48bad5e6c4 with lock(bytes) at 268, receives lockAcquired(bytes) at 269, and executes take(address,address,uint256) at 270.
  6. 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 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.
  7. During unwind, the executor repays the intermediate routing layer with 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.
  8. 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.

Trace Anchors

Decoded call indexMeaning
18 / 19First user-facing flashLoan() call plus proxy delegatecall
21Moolah callback into attacker executor
22 / 23Nested user-facing flashLoan() call plus proxy delegatecall
225supply(address,uint256,address,uint16) on 0x6807...e0cb
238borrow(address,uint256,uint256,uint16,address) on 0x6807...e0cb
268 / 269 / 270lock -> lockAcquired -> take on 0x238a...e6c4
636 / 650repay and withdraw during unwind
689 / 692sync(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 / componentNet changeNotes
Attacker EOA 0xcb26...26e9+54,598.222194166280831143 USDT and -36,193.01499715 Mai1Final realized attacker gain reported by attacker_gains / net_changes
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 EOA0xcb26b3a469c5aee911d059a25de2b26ed52826e9tx.json.from
Bootstrap contract0x8aa9cb61885121448f1bf9a5df80ec36c6fbd535receipt.json.contractAddress
Attack executor0xe812f2e6cdffdfa4ca496db0716a53301c37b705Created and called by bootstrap in trace_callTracer.json; receives both flash-loan callbacks
Vulnerable proxy0x8f73b65b4caaf64fba2af91cc5d4a2a1318e5d8cUser-facing flashLoan() target at decoded calls 18 and 22
Vulnerable implementation0x9321587ea0dc8247f8f03e8696c047b2713bb79aDelegatecall target at decoded calls 19 and 23; verified Moolah.sol source
Secondary routing pool proxy0x6807dc923806fe8fd134338eabca509979a7e0cbHandles supply, borrow, repay, and withdraw during the exploit
Secondary routing pool implementation0x00d1397960aa97f694e41c3632b74c151a00c33bVerified PoolInstance implementation for 0x6807...e0cb
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 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)
  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.

Artifacts

  • tx.json, receipt.json
  • trace_callTracer.json, trace_prestateTracer.json
  • decoded_calls.json, selectors.json
  • funds_flow.json
  • Verified Moolah source under 0x9321587ea0dc8247f8f03e8696c047b2713bb79a/
  • Verified pool implementation under 0x00d1397960aa97f694e41c3632b74c151a00c33b/

文章来源: https://www.darknavy.org/web3/exploits/mail-token-moolah-flash-loan-callback-reentrancy/
如有侵权请联系:admin#unsafe.sh