# 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.

- Source: https://defiloops.com/blog/the-executor-that-trusted-itself
- Published: 2026-09-15
- Category: Security
- Tags: security, agentic-wallet, authorization, smart-accounts, restaking
- Author: DeFiLoops

---
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.

<Callout type="danger" title="The Safe was not the bug">
  This matters, and it is the first thing people get wrong. Gnosis Safe did what a
  multi-signature wallet is built to do: it let an enabled module act. The flaw was in the
  **module** — a third-party contract installed on top of the Safe that accepted a target
  from whoever called it. Blaming the wallet misses where the mistake lived, and the mistake
  is one anyone can repeat on any account framework.
</Callout>

## 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.

<Compare left="What the module let a caller supply" right="What a caller should never supply" verdict>
  <Fragment slot="left">
    The pool to route through. A contract address, chosen at call time, by a public keeper
    anyone can invoke.
  </Fragment>
  <Fragment slot="right">
    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.
  </Fragment>
</Compare>

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.

<Callout type="warn" title="Two failures for the price of one">
  The wallet was drainable because a module could be aimed by a stranger. The *attacker* was
  robbed because a valuable transaction is visible to everyone the moment it is broadcast.
  The victim lost the money either way. On a public chain, being first matters more than
  being the one who started it — which is its own reason not to leave a drain sitting in the
  mempool for anyone to claim.
</Callout>

## 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.

<Spec title="Where the attacker's inputs have nowhere to go" rows={[
  ['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.

<StatRow>
  <Stat value="$0" label="an attacker-chosen target can move" accent />
  <Stat value="0" label="raw-call paths for the agent in production" />
  <Stat value="1" label="transaction to revoke the agent entirely" />
</StatRow>

## 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.

<Steps>
  <Step title="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.
  </Step>
  <Step title="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.
  </Step>
  <Step title="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.
  </Step>
  <Step title="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.
  </Step>
  <Step title="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.
  </Step>
</Steps>

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.

<Callout type="warn" title="The residual risks, named">
  **Your owner key.** Whoever holds it can do anything — install modules, change the root.
  True of every smart account, and no design closes it. Guard that key.

  **Development mode.** A raw call path exists on forks, behind a weaker set of rules, so we
  can test against a real chain. It is refused at boot in production. If it ever reached
  production, the value surface would open.

  **Adapter discipline.** The whole model rests on *every* adapter fixing the target and the
  recipient itself. One adapter that took a destination from the instruction would be exactly
  the redirectable module that drained the Safe. We hold every adapter to the same checklist,
  and prove each one with a test that points the recipient at an attacker and watches the
  assertion fail.
</Callout>

## 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.

<Quote>
  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.
</Quote>

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.*