Crypto exploits

FlashLoopAdapter exploit analysis

A Safe module authenticated its caller by asking that caller whether the module was enabled (self-attestation) and then made an unconstrained low-level call to a caller-supplied router, so a fake "Safe" drove the module into draining two real Safes that had enabled it for ~114.09 WETH on Ethereum.

David

Security ·

Reported loss
114.096 WETH (~$305K) net to the attacker; two victim Safes emptied (1306.48 + 6.426 weETH), per DeFiHackLabs
Occurred
Logged

Logged as “2026-10-01: A Safe Module Authenticates Its Caller By Asking That Call”.

Analysis2 min read

The FlashLoopAdapter Safe module authenticates its caller with ISafe(msg.sender).isModuleEnabled(address(this)), trusting msg.sender to self-report whether it had the adapter enabled, then performs _safe = msg.sender and runs through the rest of the module as if that caller were the Safe. The router that _swap calls is also fully caller-supplied, with no allowlist. On 1 October 2026, an attacker on Ethereum used a one-line fake "Safe" whose isModuleEnabled() always returned true, then supplied the adapter with swapRouter set to two real victim Safes that had actually enabled the adapter as a module. Each call landed as a genuine module-exec transaction on the real Safe, draining their holdings. DeFiHackLabs records the attacker net as 114.096 WETH (~$305K).

The vulnerable contract is the verified FlashLoopAdapter at 0x16bb8B912da187870C23eC6756bB3FAd061283d8. Its _start runs if (!ISafe(msg.sender).isModuleEnabled(address(this))) revert ModuleNotEnabled(); _safe = msg.sender; and its _swap runs IERC20(tokenIn).approve(router, type(uint256).max); (bool ok,) = router.call(data);. Both fields are typed as caller-supplied; neither is checked against a list of permitted Safes, routers, or selectors.

The on-chain sequence

The exploit is one transaction (0x75328f9...16fc4, block 26,098,264, Ethereum). The attacker funded it with a fee-free Morpho Blue WETH flash loan, then:

  • Repaid Victim #1's entire 1,335.26 WETH variable-rate debt on Aave v3 on its behalf. Repayment-for-anyone is a permissionless Aave operation; this unlocks the 1,306.48 weETH collateral.
  • Called FlashLoopAdapter.open(...) with swapRouter = Victim #2 and swapCalldata = Victim #2.execTransactionFromModule(weETH, 0, weETH.transfer(attacker, balance), 0). Because the adapter IS an enabled module on Victim #2, Victim #2's own execTransactionFromModule runs the inner call as itself, transferring its 6.426 weETH to the attacker.
  • Called FlashLoopAdapter.open(...) with swapRouter = Victim #1 and swapCalldata = Victim #1.execTransactionFromModule(aavePool, 0, aavePool.withdraw(weETH, max, attacker), 0). Same shape: the adapter is a module on Victim #1, the inner withdraw(weETH, max, attacker) runs as Victim #1, draining the now-unlocked collateral.
  • Swapped the 1,312.908 weETH -> 1,449.352 WETH via the attacker's exact original ODOS route. (Replay of the on-chain calldata blob, pointed at the attacker's contract.)
  • Repaid the Morpho flash loan. The attacker's net is 1,449.352 WETH gross - 1,335.256 WETH flash repay = 114.096 WETH.

The PoC reproduces the sequence against a mainnet fork at block 26,098,263, replays the ODOS calldata verbatim, and asserts a WETH profit of approximately 114.092 ETH (matching the recorded 114.096 WETH pre-gas figure to within rounding). Crucially, the PoC includes a no vm.prank / no signature compromise check: the victim owners never signed anything; every drain call is authorized purely by the adapter being an enabled module on the victim and by the broken self-attestation check.

What was missing

Two independent fixes would have stopped this. First, _start should compare the caller against a set of registered Safes (e.g. an immutable registry, or a per-Safe enableModule event captured at registration) rather than asking the caller. Second, _swap should constrain router to an allowlist (or restrict data to a Safe-call selector targeting a permitted address). Either fix on its own is sufficient. The DeFiHackLabs README also notes that the adapter used TSTORE (transient storage, post-Cancun), which is itself fine; the bug is the policy, not the EVM feature.

What it does NOT involve

No key compromise. No signatures on this transaction. No owner-signed transactions from the victims. Every drain is a genuine module-exec call flowing from a real module-enabled relationship, redirected through a self-attestation gap.

Sources read (3)

Other exploits

Query every incident yourself — free, no signup

Updated 11 Nov 2026. Figures are as reported by the sources listed above and are not independently verified.