A stranger aimed the module
A Gnosis Safe lost 7.8 million dollars in restaked ETH — not because the Safe was broken, but because an enabled module let a passing keeper point it at an attacker's own pool. The class is old. The lesson is where you let a target come from.
On 15 September 2026, an Ethereum user lost about $7.8 million in restaked ETH out of a multi-signature wallet. There is a memorable twist — a front-running bot took the money before the attacker could — but the twist is not the lesson. The lesson is the four seconds before it, when the wallet did exactly what it was told to do, by someone who should not have been able to tell it anything.
Here is the shape of it, because it is one of the most common ways a smart account gets emptied and the flawed part always looks correct.
What actually happened
The victim held roughly 2,900 rsETH — a liquid restaking token — inside a Gnosis Safe, in its Aave-wrapped form. The attacker never had a key.
Instead, they used a public keeper call to point a custom Uniswap V4 liquidity module — a module the account had enabled — at a pool the attacker had just created. That pool carried a malicious hook. The hook unwrapped the position into freely transferable rsETH, and the module, acting with the Safe’s authority, moved it.
The bug is where the target came from
Strip away the restaking and the hooks and one clause is left. A trusted module was handed a destination — which pool, which contract — by its caller, and it acted on that destination from inside its own privileged context. The validation it did was not enough, because the attacker got to choose the one thing that decided where the money went.
The pool to route through. A contract address, chosen at call time, by a public keeper anyone can invoke.
Nothing that decides a destination. The venue is named from a list the owner installed; the recipient is the account, always; the module composes the call.
A module that takes its target from the caller is a module that trusts the caller to aim it. On a public keeper path, the caller is the public. The attacker did not break in. They were handed the steering wheel and asked to be polite.
The twist, since it is a good one
The exploit transaction went into the public mempool — the waiting room where unconfirmed transactions sit in the open. A generalised front-running bot named Yoink saw the pending drain, copied it with a higher fee, and executed first in the same block. The original attacker got nothing.
Can this happen to a DeFiLoops account?
This is the question a reader in our seat actually asks, so here is the honest answer: the specific move — aim a trusted module at an attacker’s contract — cannot be expressed against a DeFiLoops account, and it cannot by design rather than by luck. Not because our checks are more careful. Because the agent is never handed a target to aim.
- Choose the pool / contract to route through
- No such field. The agent submits typed parameters; the adapter composes the call and picks the target.
- Name a venue by address
- Refused. Venues are named from the set the owner installed, never as an address in the instruction.
- Redirect the payout to yourself
- The recipient is forced to the account, read from the account — never taken from the instruction.
- Prove you meant this account
- The owner is read from the account. A valid agent signature is not proof the right account, or destination, was meant.
The deeper reason sits one level up. A production DeFiLoops account gives the agent no raw-call path at all. The drained Safe had a module that could execute a caller-chosen call. Our agent cannot compose a call. It submits an amount, a venue named from a list, a range — and a whitelisted adapter builds the transaction. There is no supplied contract, because the agent never supplies one.
The attacker’s plan, step by step
Assume the strongest realistic attacker short of stealing your owner key: they hold the agent key, the one that signs the account’s day-to-day instructions. Here is what they would try, and where each step ends.
- Point a module at a pool you control
There is no module that takes a pool from the caller. Trading runs through adapters that read the venue from the owner’s installed set. An attacker-created pool is not in it, and an address in the instruction is refused as an arbitrary call.
- Fall back to a raw call and choose your own target
In production there is no raw-call path for the agent to fall back to. It is not installed. The move has nowhere to be written.
- Route through a legitimate adapter and redirect the funds
The recipient is forced to the account, the owner is read from the account, the venue comes from the owner’s set, and the spend is capped per period. Every destination is fixed by the adapter, not by the instruction.
- Install a module of your own, or change the account's root
Those selectors are refused. The agent can never install the kind of redirectable module that drained the Safe, become the owner, or hand the account away.
- Meanwhile, the owner revokes the agent
One transaction, needing nobody’s cooperation, and every future instruction from that key is refused before its target is even inspected. The stolen key is dead the moment the owner acts.
A stolen agent key, against a production account, cannot aim anything at an attacker and cannot take control. That is the entire security claim, and it holds against this exact attack.
What we do not claim
A security post that ends there is selling something. The honest boundary has three edges, and none of them is this attack.
The part that changed our own confidence
We set out to prove the guard blocks this. Writing that proof turned up that the tests we already had did not prove it — they reverted a layer too early, in account plumbing, and passed because the test framework matched the wrong revert loosely. The protection was real. The evidence for it was theatre.
A security test that cannot fail when you break the thing it guards is not a weak test. It is a decoration, and it is more dangerous than no test, because it reads as coverage.
So we rebuilt the proof against the guard directly, asserting the exact reason each call is refused, then broke each control on purpose to watch the matching test fail. Removing the self-reference line flips its test. Neutering the revoke path flips another. That is the difference between believing a boundary holds and knowing it.
How to evaluate any claim like this
You do not need to trust this page. The question that separates a real boundary from a filter is the same one it always is: what can the agent, or a passing keeper, write down?
If the answer includes a destination — a pool, a contract, an address the caller gets to choose — then every safeguard around it is a check standing between a stranger’s chosen target and your money, and checks are judged against the attacks somebody already imagined. The September drain is what one caller-chosen target costs.
If the answer is typed parameters and nothing else, the attack in this post has nowhere to land, because the thing it exploits — a target you get to name — was never handed out in the first place.
Sources: PeckShieldAlert and Blockaid monitoring reports (15 September 2026), as covered by PANews, Odaily, Foresight News and BlockBeats; on-chain transactions via Etherscan. Figures and mechanism are as reported at the time and may be revised as analysis continues.