I kept refreshing the alert feed longer than I meant to. A number around $305,000 is not a protocol-ending hole, and yet it sat there with a particular kind of sting. Two wallets, one custom module, a caller that was never supposed to count as a real Safe. If you run leveraged loops on Ethereum, this one is worth sitting with, because the failure was not buried in some exotic math. It was a trust check that could be answered by anyone willing to say yes.
On October 1, on-chain monitors flagged a drain tied to a custom contract known as FlashLoopAdapter. The module was built to open and close leveraged positions on Aave v3 for Safes that had chosen to enable it. By the time the dust settled, security researchers put the attacker’s leftover haul near 114.09 ETH, roughly $305,000 at the prices used in the first write-ups. Aave’s founder was quick, and in my view correctly quick, to draw a hard line: this was a third-party adapter sitting on top of Aave, with zero effect on Aave v3 itself.
What Actually Broke in the FlashLoopAdapter Exploit
The short version is ugly in its simplicity. FlashLoopAdapter treated a yes from the caller as proof that the caller was a legitimate Safe that had enabled the module. The longer version is where the money moved, and where anyone managing looped collateral should pay attention.
A Safe, in this setting, is a smart-contract wallet. Owners approve transactions, or they install modules that can act for them under narrower rules. Modules are convenient. They are also a loaded permission. Once a module is enabled, it can often call execTransactionFromModule and push actions through without asking the owner again on every step. That is the feature. It is also the blast radius.
A Check That Trusted the Caller to Grade Itself
Researchers tracing the incident pointed at the access control on open() and close(). Those functions did not independently prove that msg.sender was a genuine Safe. They asked the caller, via an interface, whether the module was enabled. In plain terms, the adapter said: tell me if you have switched me on. A fake Safe, written to return true every time, sailed through.
If the door asks the visitor whether the door is allowed to open, you should not be surprised when the visitor says yes.
– A framing I keep coming back to on module checks
I have seen this pattern enough times that it no longer feels clever. It feels tired. External calls into untrusted contracts for authorization are a classic foot-gun. The caller controls the code that answers. A boolean that always returns true is not an exploit in the Hollywood sense. It is a costume. And the adapter accepted the costume.
That alone did not empty the wallets. The second hinge was _swap(). The function allowed a raw call into a router address, using swap calldata supplied by the caller. Both inputs were attacker-controlled. Instead of a real swap router, the attacker pointed the “router” at a victim Safe. The calldata was shaped to invoke the Safe’s module execution path. FlashLoopAdapter was already enabled on those Safes, so the Safe accepted the call. From there, the module was no longer a helper. It was a hand on the wheel.
Why the Callback Did Not Save Anyone
Monitors noted that checks during the callback failed to stop the flow, because the attacker’s contract also played the role of flash liquidity provider. That detail matters. A lot of DeFi defenses assume the counterparty in a callback is some known pool with fixed behavior. Here the counterparty was the attacker. When you are both the borrower and the judge of the callback, the guardrail is decorative.
Perhaps the most interesting aspect is how little novelty was required after that. No broken oracle. No governance takeover. No bug inside Aave’s core pool logic. Just a module that asked the wrong party a yes-or-no question, then handed that party a raw call.
The Money Path, Without the Myth
People glance at 1,306 weETH and assume that was the profit. It was not. Most of the capital in the transaction was a crowbar, not a prize.
The attacker took a WETH flash loan from Morpho and used it to repay about 1,335 WETH of Aave debt sitting on the first Safe, 0xcfedf95a3653a128dfc2e4288758a1a1850d169f. Repaying the debt unlocked the collateral tied to the leveraged position. Through the compromised module path, the Safe was made to withdraw roughly 1,306 weETH to an address the attacker controlled. A second Safe, 0xe3b23e47df7cd85876ac6cb05bdb9d7cd5b28520, lost another 6.4 weETH through the same module. Monitors said both Safes shared a single owner.
Part of the withdrawn weETH was swapped back toward WETH so the flash loan and the rest of the transaction could settle. What remained with the attacker was about 114.1 ETH. Security researchers put a nearly identical figure on it, around 114.09 ETH, and described roughly 1,300 WETH of debt repaid on the way to freeing collateral. The large withdrawal is the theater. The retained ETH is the take.
| Piece of the incident | What was reported | Why it matters |
| First Safe | 0xcfedf95a3653a128dfc2e4288758a1a1850d169f | Larger leveraged position, debt repaid then collateral pulled |
| Debt repaid | About 1,335 WETH | Unlocked weETH collateral on Aave v3 |
| Collateral withdrawn | About 1,306 weETH, plus 6.4 weETH from a second Safe | Gross movement, not net profit |
| Second Safe | 0xe3b23e47df7cd85876ac6cb05bdb9d7cd5b28520 | Same module, same single owner |
| Attacker address | 0x42c2633438609881c8fBAb82414eb9A0c45F9353 | Retained roughly 114 ETH after settlement |
| Vulnerable adapter | 0x16bb8b912da187870c23ec6756bb3fad061283d8 | Custom Safe module, not an Aave v3 core contract |
| Estimated loss | About $305,000 | Net retained value, not the gross weETH figure |
Detection timing was tight. An alert desk marked the attack at 15:08:57 UTC on October 1. That is fast for public monitoring, and still too late for the owner. Once a module execution lands in a block, “we saw it” is a postmortem, not a rescue.
Aave v3 Was the Stage, Not the Villain
This distinction is easy to smear and worth keeping clean. Aave v3 was the venue where the leveraged position lived. The debt was Aave debt. The collateral sat in an Aave position. None of that means the pool contracts mispriced an asset or let a stranger withdraw someone else’s funds through the protocol’s own functions.
The founder’s line was blunt. The affected contract was a third-party external adapter built on top of Aave, with zero effect on Aave v3. I buy that framing, with one caveat I will come back to: zero effect on the core contracts is not the same as zero effect on people who thought they were “using Aave.” Users experience the stack, not the org chart.
FlashLoopAdapter is a separate custom contract. It uses Aave v3 to manage looped positions held through Safe wallets. The reported weakness sat in how the adapter authenticated callers, and in what those callers could then force an enabled module to execute. The pool kept doing what pools do. Repay debt, free collateral, allow a withdrawal by an authorized caller. The authorization was the lie.
How a Leveraged Loop Is Supposed to Work
If you have never looped a position, the shape is simple enough to sketch on a napkin, and dangerous enough that the napkin should probably be thrown away.
- Deposit a yield-bearing or liquid-staking asset as collateral. Here, that collateral was weETH.
- Borrow a related asset, often WETH, against it.
- Swap or wrap the borrowed asset back into more collateral.
- Deposit again, borrow again, and repeat until the health factor is as tight as your nerve.
- Earn the spread between collateral yield and borrow cost, minus swaps, gas, and the chance that the spread flips.
A module like FlashLoopAdapter exists because doing that by hand is clumsy. Open, close, rebalance, unwind. Each step is a transaction, a signature, a chance to misclick. Automation feels like hygiene. The trade is that you have installed a robot with keys.
In my experience, the people who get hurt are rarely the ones who misunderstand leverage in the abstract. They understand the health factor. They watched the rate. What they underweight is the permission surface around the helper contract. The loop is the strategy. The module is the locksmith.
Flash Loans as a Crowbar, Not a Mystery
A flash loan is capital that must be repaid inside the same transaction. Fail to repay, and the whole thing reverts. That property makes flash loans a poor tool for “borrowing and running,” and an excellent tool for rearranging someone else’s position if you already have a way to move their assets.
Here the sequence is almost pedagogical.
- Spoof the Safe check with a contract that always claims the module is enabled.
- Abuse the swap hook so the adapter calls back into the real victim Safe as an enabled module.
- Borrow WETH from Morpho for the length of one transaction.
- Repay the Safe’s Aave debt, which releases the claim on collateral.
- Withdraw weETH to an attacker-controlled address.
- Swap enough of it to repay the flash loan and keep the residual ETH.
Nothing in that list requires breaking Aave’s interest-rate model. It requires a Safe that had already trusted the wrong robot, and a robot that trusted its caller. Flash liquidity just made the unwind atomic. Without it, the attacker would have needed a large pile of WETH up front. With it, the pile was rented for a single block.
The September Echo, and Why It Keeps Rhyming
This was not the first time a Safe module’s permissions did the heavy lifting. In September, an Ethereum Safe incident involving roughly 2,900 rsETH was traced to weak authorization in an executor contract tied to an enabled module. An MEV bot front-ran the attempted exploit and took the assets before the original attack transaction reverted. Different wrapper, same family of mistake: the thing allowed to act was not tightly bound to a proven caller.
Earlier in the year, a module associated with a cross-chain routing product drained Safes on Ethereum and Base. Reports put the loss around $3 million to $3.2 million across 86 wallets. The main router contracts and ordinary user funds on the core product were described as unaffected. Days later, users of a payments product built around Safes were urged to pull funds after a flaw in a delay module. The concern, as described at the time, was that an attacker could initiate transactions from Safes using the affected module.
I am not lumping these into one bug. The code differs. The lesson does not. Safe modules are a privilege boundary. If the contract on the other side of that boundary authenticates poorly, the Safe will faithfully execute the mistake. The wallet did its job. The job was “obey the enabled module.”
Who Shared the Owner Key, and Why That Detail Stings
Both affected Safes reportedly shared the same single owner. That does not cause the bug. It does change the story you tell yourself about “isolation.”
Two contracts can look like two vaults. If they share an owner and they share an enabled module, they are closer to two rooms with the same spare key under the mat. A single bad install propagates. The second loss, 6.4 weETH, is small next to the first. It is also proof that the module was not a one-off attachment on a single position. It was a habit.
Single-owner Safes are a design choice, not a sin. Plenty of operators use them as a smarter account than an externally owned wallet, with recovery and batching in mind. The risk shows up when that single owner enables experimental adapters across every position they care about. Convenience compounds. So does exposure.
What a Fake Safe Actually Is
It helps to demystify the costume. A “fake Safe” here is not a stolen wallet and not a phishing page. It is a contract the attacker deployed, implementing just enough of the Safe interface to answer isModuleEnabled with true. No owners. No threshold. No history of enabling FlashLoopAdapter. Just a function that lies politely.
Any check of the form “ask the caller about itself” collapses under that. Stronger patterns exist, and they are not exotic.
- Maintain an allowlist of Safes that explicitly registered, stored in the adapter’s own state, not read from the caller.
- Verify code identity, or a known factory, instead of trusting an interface response.
- Bind execution to a specific Safe address passed in and checked against storage, rather than to msg.sender’s self-report.
- Remove arbitrary calldata. If the module needs a swap, call a fixed router with fixed function selectors.
- Separate the role that can configure routes from the role that can move collateral.
None of those ideas would have surprised an auditor in 2021. The fact that a 2026 adapter still shipped the weak version is the part I find hard to shrug off. Tooling got better. The mistake stayed cheap to write.
Raw Calldata Is a Loaded Gun in a Helper
Even if the Safe check had been real, the swap path would still worry me. Caller-supplied router plus caller-supplied calldata is a generic execution primitive wearing a DeFi costume. You can call it a swap. The EVM does not care what you call it.
Point that primitive at a Safe which has already enabled you, and you have built a boomerang. The adapter calls the Safe. The Safe, seeing an enabled module, runs the payload. Assets move. From the outside it can look like a clever routing choice. From the inside it is the module instructing the wallet to rob itself.
Weak pattern: if caller.isModuleEnabled(this) then allow then call(router, swapCalldata) Stronger pattern: if registeredSafe[caller] then allow then call fixedRouter.exactFunction(fixedArgs)
I would rather a loop module be boring. Fixed venues. Fixed selectors. Explicit amounts. If a strategy needs arbitrary routing, that routing should not share a function with the right to unwind a Safe’s entire Aave position. Split the powers. Make the dangerous one ugly to call.
What Users Actually Controlled, and What They Did Not
It is tempting to file this under “don’t use random contracts,” and stop. That advice is true and incomplete. The Safes enabled the module. That was a choice. The choice was probably made because the module promised to manage a loop the owner already wanted. The owner did not choose the fake Safe. They chose a helper, and the helper chose to believe strangers.
So the responsibility splits, and I think it should stay split.
- The adapter author owned the authentication bug and the arbitrary call.
- The Safe owner owned the decision to enable an unaudited or lightly reviewed module on a live leveraged position.
- Aave owned the pool behavior, which behaved as specified when an authorized module repaid debt and withdrew collateral.
- The flash-loan venue owned nothing causal here. It rented WETH and got it back.
Blurting those lines together into “Aave was hacked” is how rumors outrun repairs. Keeping them apart is how you decide what to uninstall tonight.
A Practical Uninstall List for Anyone Looping
If you run a Safe with strategy modules, this incident is a reason to open the module tab, not a reason to panic-sell the collateral. I would walk through it in this order.
First, list every enabled module. If you cannot explain, in one sentence, what each one is allowed to do, disable it until you can. Second, ask whether any module accepts an address and calldata from the caller and forwards them. If yes, treat that module as hot until someone shows you a hard allowlist. Third, check whether two Safes share modules and owners. Shared helpers are shared fate. Fourth, look at health factors with the module removed from your mental model. Could you unwind manually if the helper vanished? If not, the helper is not a convenience. It is a single point of failure.
Fifth, and this one is dull on purpose, read the enable transaction again. Modules are often turned on in a batch with other setup. People forget. The chain does not.
Questions Worth Asking Before the Next Adapter
I keep a short list near the bottom of notes like this, because the incident details fade and the questions do not.
- Does authorization depend on a view call to msg.sender? If it does, assume it can be spoofed.
- Can any argument become a contract address that later receives a raw call?
- Is the module allowed to call execTransactionFromModule on the same Safe that enabled it, with user-supplied payloads?
- Was the adapter audited against the exact commit you are enabling, or against a cousin repository?
- What is the largest position this module can touch in one transaction?
- If the module is paused or broken, can you still repay and withdraw through Aave directly?
A yes on the first three is, for me, a no on enabling it against size. Audits help. They do not resurrect a design that lets the caller define the target of a privileged call.
The Number That Should Stick
Gross flow and net loss tell different stories, and headlines often pick the scarier one. Here the scarier number and the truer number happened to land in the same neighborhood only after the dust settled. About 1,306 weETH left the larger Safe. About 6.4 weETH left the smaller one. After the debt was repaid and the flash loan settled, roughly 114 ETH remained with the attacker. At the prices used when the incident was first tallied, that residual was about $305,000.
Why harp on the gap? Because the next alert will also show a huge collateral move, and someone will quote it as profit. Sometimes it is. Sometimes it is a loan being closed with rented money. Reading the settlement, not just the withdrawal, is the whole skill.
The collateral leaving the wallet is a scene. The balance left after the flash loan is repaid is the plot.
Where Monitoring Helped, and Where It Could Not
Public alert accounts caught the pattern quickly: access control, Safe module, Aave v3 loop, loss figure, network. That speed is genuinely better than the silence we used to get. It still arrives after inclusion. A monitor can warn the next owner. It cannot unwind a transaction that already repaid 1,335 WETH and pulled the collateral in the same block.
If you want something closer to prevention, it has to live before the enable. Simulation against a spoofed caller. A test that deploys a fake Safe and asserts that open() reverts. Those checks are cheap next to 114 ETH. They are also the kind of test teams skip when the happy path already loops neatly on a fork.
A Note on weETH, WETH, and the Asset Choice
The assets involved are familiar to anyone in liquid staking loops. weETH is a wrapped representation of staked ether yield. WETH is plain wrapped ether, the unit Aave borrowers often draw when they lever that yield. The spread is the product. The correlation is the comfort. When both legs are flavors of ether, the position feels stable right up until a module, not the market, decides to close it for you.
Nothing in the exploit required weETH to depeg or WETH to misprice. The market was a backdrop. That should bother strategy builders more than a volatile afternoon would. You can hedge price. You cannot hedge a module that is allowed to call your Safe, except by not granting the permission.
How This Sits Next to Older Module Drains
Put the incidents in a row and a shape appears, even if the code does not match line for line.
| Incident shape | Rough scale reported | Core product touched? |
| Routing-related Safe module drain across many wallets | About $3 million to $3.2 million, 86 wallets | Main router described as unaffected |
| Delay module concern on a payments Safe setup | Users urged to withdraw | Flaw framed around the module, not the whole stack |
| Executor contract tied to an enabled module, rsETH | About 2,900 rsETH, front-run by an MEV bot | Authorization weakness outside the asset itself |
| FlashLoopAdapter on Aave-linked Safes | About 114 ETH retained, ~$305,000 | Aave v3 described as unaffected |
The repeating sentence is the one operators hate, because it does not point at a single team to blame and move on. Third-party modules keep becoming the route. Core protocols keep being the scenery. Wallets keep obeying what they were told to obey.
What I Would Want From the Adapter’s Maintainers
A patch note that only says “fixed access control” is not enough. I would want the new check described in public: storage-based registration, no self-reported booleans, no arbitrary router. I would want a statement on whether other deployments of the same bytecode exist, and whether any remaining Safes still have the old module enabled. I would want a way for the affected owner to confirm, on-chain, that the module can no longer act.
Silence is also data. Custom adapters sometimes have no public team, no bug bounty, no upgrade path. If that is the case here, the practical fix is local. Disable the module. Do it from the owner path, not through the module. Then assume any position it could see is something you should be able to manage by hand.
The Builder Lesson, Stated Without Poetry
If you are shipping a Safe module this month, steal the failure, not the code. Do not call out to msg.sender to ask if you are allowed to act. Do not accept a target and a blob of calldata in the same function that can move collateral. Do not let a flash-loan callback be satisfied by a contract you have never seen. Write the spoof test before you write the happy-path demo. If the spoof test is annoying to set up, that annoyance is the point.
And label the thing honestly in the interface. “Loop helper” sounds like a calculator. “Contract that can execute arbitrary calls from your Safe” sounds like what it is. Users enable what they think they understand. Wording is part of the security boundary, even if it never shows up in the bytecode.
A Calm Read on Market Impact
$305,000 will not reprice ether, and it should not be asked to. The useful market read is narrower. Looping strategies concentrate in a handful of collateral types and a handful of helper contracts. When one helper fails closed for its users and open for an attacker, the damage stays local only if adoption stayed local. A popular module with the same check would not have stayed a six-figure story.
That is the asymmetrical part I cannot quite shake. The bug is cheap. The upside, from the attacker’s side, scales with whoever enabled the module. Two Safes, one owner, a contained loss. The pattern is not contained. It is waiting on the next install.
How to Talk About This Without Spreading a False Alarm
Words matter on days like this, mostly because people trade on them. Saying Aave v3 was exploited is false on the facts we have. Saying an Aave-linked Safe module was exploited is accurate and still easy to misread. Saying a custom adapter’s access check was spoofed, and that the Safes which enabled it lost about 114 ETH of net value, is the sentence I would actually repeat.
If you lend on Aave, supply collateral, or borrow without a third-party module in the path, this incident does not describe your position. If you enabled FlashLoopAdapter, or anything with the same shape, it describes a permission you should revisit before the next block, not after the next thread.
A Walkthrough You Can Retell Without the Jargon
Imagine a building manager who will unlock any apartment if the person at the door says, “I already authorized you.” A stranger prints that sentence on a card. The manager nods. Then the stranger hands the manager a note that says, “Now go into apartment 4B and send the furniture to this truck.” Apartment 4B really did authorize the manager last month. The stranger did not. The furniture still leaves.
That is the exploit, minus the ether. The flash loan is the truck rental that has to be returned before the streetlight changes. The 1,335 WETH repayment is clearing the lien so the furniture can pass the door. The 114 ETH is what is still on the truck when the rental is settled. The building’s main lock, Aave v3, never failed. The manager’s rule did.
What Remains Unresolved
Public alerts named the attacker address and the adapter address. They did not, in the first wave, hand the owner a recovery. On-chain theft of this kind rarely unwinds itself. The practical leftovers are documentation, a disabled module, and a clearer fear of arbitrary calls.
There is also an open question I would not pretend to close from the outside. Were there other Safes with this module enabled that the attacker did not touch, either because they held no attractive debt-and-collateral pair, or because the transaction simply stopped at two? If you enabled it, do not wait for a third address to show up in a thread. Check the module list yourself.
A Last Pass Over the Facts
FlashLoopAdapter, at 0x16bb8b912da187870c23ec6756bb3fad061283d8, was a Safe module for opening and closing Aave v3 leveraged loops. Its open() and close() paths trusted ISafe(msg.sender).isModuleEnabled. A fake Safe returned true. Its swap helper then performed a raw call with attacker-chosen target and calldata, aimed at a victim Safe’s execTransactionFromModule. The attacker, tied to 0x42c2633438609881c8fBAb82414eb9A0c45F9353, used a Morpho WETH flash loan, repaid about 1,335 WETH of debt on the larger Safe, withdrew about 1,306 weETH, took 6.4 weETH from the second Safe, and kept about 114 ETH, near $305,000. Aave v3’s own contracts were not the component researchers flagged. The founder said as much, in fewer words.
I will leave it on a narrower claim than the alerts did. The expensive part was not leverage. Leverage was the prize cabinet. The expensive part was a module that could be convinced, by a contract born the same day, that it was allowed to reach inside. Until helper contracts stop asking strangers to vouch for themselves, six-figure afternoons like this one will keep looking familiar.
If you take one action from the whole mess, make it dull. Open the Safe. Read the enabled modules. Revoke the one you cannot explain. The loop can wait an hour. The permission should not.