OAK — OnChain Attack Knowledge

Worked example · 2026-09

Safe strategy executor — the permission check passed because the module asked itself for permission, and an MEV bot took the proceeds before the attacker could — Gnosis Safe / whitelisted strategy-executor module / Ethereum — 2026-09-15

Loss
~2,900 aEthrsETH, unwrapped to rsETH and valued at ~$7.73–7.8M, drained from a single Gnosis Safe in one transaction at 04:38 UTC, with roughly $160K more moved in follow-on transactions. The Safe was left holding a worthless LP NFT in place of the position. The attacker did not keep the proceeds: an MEV bot known as yoink front-ran the exploit transaction in the mempool and captured ~2,882 rsETH, routing it to 0xC70f00CD7E461686b04B0E912E309becA8b80ea0. KelpDAO placed that receiving address under a precautionary 24-hour pause; Kelp's core contracts and rsETH backing were not affected, and no other Safe is known to have been drained through the same module.
OAK Techniques observed
OAK-T9.004 (Access-Control Misconfiguration — primary, confirmed mechanism. The whitelisted strategy-executor module at 0x4f0055926c839D1d960a82CBF84E2eE933958ebC exposed a public keeper multicall(address,bytes[]) entrypoint that forwarded fully caller-supplied calldata into the Safe's execTransactionFromModuleReturnData with operation = 1 (DELEGATECALL), with no gating on the external caller. The internal permission helper, _isAuthorized, could be satisfied by setting the _contract parameter to address(this) — the module's own address — at which point it returned true unconditionally. See techniques/T9.004-access-control-misconfiguration.md). The extraction vehicle — a hooked Uniswap V4 pool the attacker deployed, holding a worthless token of their own creation (reported as "Permissionless Attacker Token", PAT), into which the Safe's whitelisted V4 LP path was routed so the custom hook could unwrap the aEthrsETH and take the rsETH out on the way through — maps to no Technique at v0.x and is not forced into one. It is not T2.001 (Single-Sided Liquidity Plant), whose subject is concealed seeding that deceives buyers into a pool; nobody was deceived into buying here, and the pool existed to host attacker code inside a legitimate-looking LP operation, not to misprice a token for a market.
Attribution
pseudonymous. On-chain identifiers only; the victim Safe's owner has not been publicly identified beyond being a large individual holder. The exploit was confirmed independently by Blockaid, PeckShield and BlockSec.
Key teaching point
A module is not a feature of a Safe, it is a second front door with no lock of its own — and here the module's lock was a function that could be persuaded to check itself. The Safe architecture is explicit that an enabled module can move assets without owner signatures; that is what modules are for. Everything therefore rests on the module gating its own callers, and this one had a public entrypoint whose authorisation check could be satisfied by naming the module as the target. _isAuthorized(address(this)) returning true is a self-referential check: the question "is this caller allowed?" was answered by "am I myself?", which is always yes and says nothing about the caller. The control is structural, not a patch: an authorisation predicate must take the caller as its subject (msg.sender), never a caller-supplied address; self must be an explicitly forbidden target in any forwarding primitive; and DELEGATECALL reached from a public entrypoint with caller-supplied calldata is a total compromise of whatever executes it — there is no partial version of that grant. For Safe owners the operational rule is narrower and harder: enumerate your enabled modules and re-review each one as if it were the account, because it is.

Explicitly not OAK-T11.003 (In-Use Multisig Smart-Contract Manipulation) and explicitly not OAK-T10.001-style key compromise. No Safe owner key was compromised, no signer set was modified, and no owner signature was forged or required. The multisig threshold was never tested, because the path used never asks for one: a module executes on the account's behalf by design. Explicitly not OAK-T5.004 (Sandwich MEV) either — the MEV bot's interception is fund disposition, not the attack mechanism, and is recorded here as metadata.

Summary

The victim was a Gnosis Safe on Ethereum holding a large aEthrsETH position — Aave-wrapped rsETH, KelpDAO's liquid restaking token. To manage that position the Safe had enabled a strategy-executor module and whitelisted it, a standard arrangement for automated Uniswap V4 LP management.

That module, at 0x4f0055926c839D1d960a82CBF84E2eE933958ebC, had two defects that compose into a full account takeover. Its keeper multicall(address,bytes[]) was publicly callable. And its internal check, _isAuthorized, returned true whenever the target _contract was address(this) — so a caller could route a call through the module's own address and clear the permission gate without holding any permission.

Past that gate, the module forwarded caller-supplied calldata to the Safe's execTransactionFromModuleReturnData using operation = 1, i.e. DELEGATECALL. At that point the attacker was executing arbitrary code in the Safe's own context, with the Safe's assets.

The extraction was staged rather than direct. The attacker deployed a Uniswap V4 pool with a custom hook and a worthless token they had minted, then drove the Safe's whitelisted V4 LP path into that pool. The hook unwrapped ~2,900 aEthrsETH into rsETH and siphoned it out during the operation, leaving the Safe holding a valueless LP NFT — a position that looks like a position and is worth nothing.

The ending is unusual. The exploit transaction sat in the public mempool, and an MEV bot called yoink front-ran it, capturing essentially the entire ~2,882 rsETH for itself. The attacker built the exploit and someone else collected. KelpDAO paused the bot's receiving address for 24 hours as a precaution; rsETH backing and Kelp's own contracts were never in scope.

Timeline (UTC, 2026-09-15)

When Event OAK ref
(standing) Safe enables and whitelists a strategy-executor module for Uniswap V4 LP management (normal operation)
(standing) Module exposes a public keeper multicall(address,bytes[]); _isAuthorized returns true when _contract == address(this) (latent T9.004 defect)
(standing) Same path forwards caller-supplied calldata to execTransactionFromModuleReturnData, operation = 1 (DELEGATECALL) (latent privilege amplifier)
before the drain Attacker deploys a hooked Uniswap V4 pool containing a worthless self-minted token (reported as "PAT") (execution vehicle — no Technique at v0.x)
04:38 Single transaction: public multicall entered with _contract = address(this); gate clears; DELEGATECALL executes attacker calldata in the Safe's context T9.004 exploitation
same tx Hook unwraps ~2,900 aEthrsETH into rsETH and removes it; Safe left holding a worthless LP NFT (extraction)
same block MEV bot yoink front-runs the exploit transaction and captures 2,882 rsETH ($7.8M) to 0xC70f00CD7E461686b04B0E912E309becA8b80ea0 (fund disposition)
shortly after ~$160K additional moved in follow-on transactions (extraction tail)
same day Blockaid, PeckShield and BlockSec confirm the exploit and publish the mechanism (external forensics)
same day KelpDAO applies a precautionary 24-hour pause to the receiving address; states core contracts and backing unaffected (third-party response)

What defenders observed

  • The whole security boundary was one helper function, and it was checking the wrong subject. _isAuthorized took an address parameter and compared it against a target rather than against msg.sender. Passing address(this) turned the check into a tautology. This is not an obscure composition bug — it is the authorisation predicate reading a value the attacker supplies, and it is findable by reading the function signature and asking whose permission is being tested.
  • A public entrypoint plus DELEGATECALL is not a vulnerability with a severity rating, it is account ownership. Once caller-supplied calldata reaches execTransactionFromModuleReturnData with operation = 1, everything downstream — which token, which pool, which hook — is decoration. The attacker's choice of a hooked V4 pool was a convenience for laundering the value out through something that looks like an LP operation, not a requirement.
  • Uniswap V4 hooks give the attacker a place to put their logic inside a legitimate-looking call. The Safe's whitelisted path was "manage a V4 LP position," and the attacker supplied the pool. The hook ran as part of normal pool mechanics. A whitelisted operation is not a whitelisted counterparty — the module was permitted to LP, and nothing constrained what it could LP into.
  • The victim's residual asset was an NFT, which delays the alarm. The Safe did not go to zero; it went to a position whose token still existed and whose value did not. Balance-based monitoring that counts positions rather than pricing them reports nothing here.
  • An MEV bot taking the proceeds changes recovery, not risk. The Safe lost the funds either way. What changes is that the value sat with an identifiable, address-stable searcher rather than with the exploiter — which is why KelpDAO's 24-hour pause on that address was worth applying, and why the disposition is recorded as metadata rather than as mitigation. Nothing about this outcome is repeatable or defensive; the next identical bug clears the mempool without a bot in the way.

Public references

Discussion

This is the third Safe-module authority failure OAK has recorded in 2026, after SquidRouterModule (2026-05-25, a counterfeit module whose "authorisation" was a publicly-known constant string) and Gnosis Pay / Zodiac (2026-06-01, where a vulnerable fallback handler on a Safe assigned as a module or role member let an audited Delay/Roles modifier authorise and execute without its cooldown predicate being satisfied). All three classify as T9.004 and all three share a shape the parent Technique does not name: the failing component is a module holding standing, signature-free execution authority over an account, and the module's own caller-gating is the entire security boundary.

The three are not the same bug. SquidRouterModule's gate was fake by design — a malicious module presented as a legitimate one. Gnosis Pay's gate was real, audited and composed wrongly — the predicate was evaluated against the wrong subject under a configuration its designers had not modelled. This one's gate was real, honestly intended and self-referential_isAuthorized(address(this)). What they share is the blast radius: in each case, clearing the module's check yields the account, because the Safe's own threshold is not in the path. A multisig's n-of-m is a statement about one execution path, and the module path is a different one.

That is enough regularity across enough distinct root causes to be worth naming, and it is proposed in TAXONOMY-GAPS.md as the forward candidate OAK-T9.004.002 — Smart-Account Module Authority Escalation, with these three as anchors. Its detection signal is distinct from generic access-control review and does not require reading the module's source: enumerate the modules enabled on an account, and treat each as an independent holder of full withdrawal authority. The Safe's own interface makes that enumeration trivial and almost nobody performs it on a schedule. The mitigation is equally concrete — module review at account-owner tier rather than integration tier, self forbidden as a forwarding target, and DELEGATECALL unreachable from any unauthenticated entrypoint.

Techniques demonstrated (1)