Approval-by-Address / Mutable-Code Execution
- Name: approval-by-address / mutable-code execution
- Category: exploit pattern / authorization failure / code-identity mismatch / privileged execution smuggling
- Summary: Some systems review, vote on, whitelist, or schedule an address as if that address were the thing being trusted. The real object that later executes, however, is code. If the code behind the approved address can change after approval but before execution, the system has authenticated the wrong thing. The attacker first gets a benign-looking executable approved, then swaps in different code at the same address and cashes out the already-granted privilege. The recurring review mistake is to collapse three different promotions into one vague
this address was approvedstory: an address or executor shell exists, that address still resolves to the exact reviewed executable meaning, and that reviewed executable still deserves this much live privileged authority when execution finally happens. - What it is:
- A protocol authorizes an address for later privileged execution: governance proposal target, plugin, module, dispatcher, hook, or other executable contract.
- Review or voting is performed on the code currently at that address, or on an assumption that the address will keep representing the same code.
- Between approval and execution, the attacker changes what code that address resolves to.
- Execution still succeeds because the protocol re-checks
is this a contract at this address?rather thanis this the same code we approved?. - The exploit turns address stability into a smuggling channel for mutable logic.
- Typical preconditions:
- The protocol approves proposal or executor contracts by address instead of by code hash, creation-code hash, or immutable calldata.
- Execution happens later, through
delegatecall,call, module invocation, or another privileged path that trusts the previously approved address. - The approved contract can be replaced after review, historically through
CREATE2+CREATE+SELFDESTRUCTmetamorphic deployment and more generally through mutable indirection such as proxies or other rebindable code paths. - Review processes focus on source code at voting time, while the actual trust boundary is the runtime bytecode present at execution time.
- The privileged executor can mutate sensitive storage, move assets, or otherwise act as root authority once called.
- Common vulnerable surfaces:
- governance systems that approve a proposal contract address and later
delegatecallinto it - plugin, module, or hook registries that whitelist executor addresses without pinning code identity
- integration allowlists that treat upgradeable proxies as safe delegatecall targets because the address belongs to a reputable protocol
- dispatcher systems that assume
approved target addressimpliesapproved behavior - contracts that check only
Address.isContract(target)or similar liveness at execution time - any approval pipeline where mutable code sits behind an ostensibly stable address
- governance systems that approve a proposal contract address and later
- Exploit sequence:
- The attacker deploys benign-looking code and gets its address approved, voted in, or whitelisted.
- The system stores the address as the authorized executable.
- Before execution, the attacker replaces the code behind that address or retargets the address through mutable indirection.
- The protocol later executes the approved address under elevated privileges.
- Because the privilege check is address-based rather than code-identity-based, the malicious replacement executes as if it were the reviewed payload.
- The attacker uses that privileged execution to rewrite storage, forge balances, seize ownership, or transfer assets.
- Variants:
- Metamorphic contract swap — historical
CREATE2/CREATE/SELFDESTRUCTaddress reuse lets a different runtime bytecode appear at the same address after approval. - Proxy-indirection smuggling — the approved address stays constant, but its implementation pointer changes before the privileged call.
- Whitelisted dispatcher drift — a router, module, or plugin remains approved while its downstream behavior changes outside the original review envelope.
- Address-only governance approval — voters approve a proposal address, but execution safety really depended on pinning the exact bytecode or calldata.
- Metamorphic contract swap — historical
Three code-identity promotions reviewers should not collapse
-
Some approved executable address exists
- A governance queue, allowlist, module registry, or timelock has recorded one address as approved.
- That only proves
this address was admitted into a future privileged path.
-
That address still resolves to the exact reviewed executable meaning
- The harder question is whether the live runtime bytecode, implementation pointer, creation-code-derived artifact, and privileged call primitive still match what review or governance actually blessed.
- If the address now points to different code, a different implementation, or a different downstream action bundle, the protocol is already executing outside its reviewed object.
-
That reviewed executable still deserves this much live privileged authority right now
- Even a genuinely pinned executable should not automatically inherit unlimited
delegatecall-level storage reach, uncapped treasury touch, or open-ended module authority. - This is where bounded action bundles, scoped roles, cancellation windows, and blast-radius limits still matter.
- Even a genuinely pinned executable should not automatically inherit unlimited
Healthy review gets materially better once teams stop flattening those three promotions into one vague moment called approved address.
- Detection heuristics:
- Ask whether the protocol is really authenticating
whoorwhat code; if execution authority follows bytecode, address-only approval is suspicious by default. - Compare code hash at review time versus execution time for every delayed privileged action.
- Treat
delegatecallinto previously approved external contracts as a high-sensitivity pattern, especially when execution is delayed by voting or timelock. - Search approved payloads for destructive or redeployment-enabling branches, but do not stop there: the deeper question is whether later code replacement would matter even if the reviewed code looked clean.
- Inventory where a contract address can remain stable while implementation logic changes, whether through metamorphic deployment, proxy indirection, or other module rebinding.
- Ask whether the protocol is really authenticating
- Defenses:
- Approve immutable calldata or deterministic code hashes, not just addresses, whenever privileged execution is delayed.
- Re-check code identity at execution time against the reviewed or voted-in hash.
- Avoid unrestricted
delegatecallinto externally supplied proposal contracts; prefer tightly scoped action lists or verified executors. - Treat address creation and code mutability semantics as part of the threat model, not as deployment trivia.
- Where mutable indirection is unavoidable, bind approvals to the full execution envelope: implementation hash, proxy admin constraints, and any upgrade path that could alter behavior before execution.
- Historical note: Ethereum’s post-Cancun
SELFDESTRUCTsemantics narrowed one classic metamorphic route, but the broader class survives anywhere code behind an approved address can still change or be re-pointed before execution.
What sound design looks like
- The reviewed object and the later executed object stay identical. Healthy systems do not ask reviewers, governors, or allowlist maintainers to bless
an addresswhen the thing that actually matters is runtime bytecode, implementation pointer, or one exact calldata payload. - Delayed privileged execution keeps three facts separate on purpose:
this address or executor shell was admitted,it still resolves to the exact reviewed executable meaning, andthat reviewed executable only carries this much live authority when execution finally happens. - Delayed privileged execution should preserve an explicit execution envelope: target address, runtime or implementation hash, privileged call primitive (
callversusdelegatecall), immutable calldata or an action list, and the exact admin path that could mutate any of those before execution. - If a proxy or module is intentionally mutable, that mutability itself has to be inside the approval object. In practice that means pinning implementation hash, constraining proxy admin, or re-validating the live implementation immediately before the privileged call.
- The safest governance and allowlist designs authenticate effects, not open-ended executors: queued action lists, parameterized function calls, and tightly scoped modules age better than
delegatecall whatever code lives here later. - Even correctly pinned executables arrive on bounded authority rails. A reviewed payload should still inherit only the storage reach, asset touch, and module scope its specific job requires rather than a vague
approved executable therefore root authoritybudget.
Failure-prevention checklist
- Bind delayed privileged execution to a stable identity object: runtime bytecode hash, implementation hash, creation-code hash plus immutable constructor args, or one exact calldata bundle. Do not treat
address approvedas enough when the privilege will later execute code. - Re-validate the execution envelope immediately before execution: target, code identity, proxy implementation, call primitive, calldata hash, and any module or plugin version that can change behavior.
- Prefer queued action bundles over queued executors. If governance intends to perform
call X with params Y, store and execute exactly that action set rather thandelegatecall this shell contract later. - Keep
delegatecallout of governance and module systems unless there is a strong reason it cannot be replaced with narrowercallsemantics. Mutable external code plus caller storage authority is the explosive combination. - Treat proxies, beacons, modules, and hook registries as code-identity problems, not merely address books. If a proxy is approved, pin who can upgrade it and what implementation family is acceptable.
- Model deployment and redeployment semantics explicitly, including CREATE2 paths, factory ownership, proxy admin rights, beacon ownership, plugin manager rights, and any chain-specific code-replacement quirks.
- Make review artifacts machine-checkable. Proposal UIs, timelocks, and allowlist registries should surface the exact code hash, implementation address, selector set, and storage-touching primitive that will execute.
- Add invariant tests for
reviewed object == executed object. Governance or registry tests should fail if code changes after approval but before execution, even when the address stays constant. - Restrict the blast radius of approved executors anyway: scoped roles, module-specific storage namespaces, rate limits, timelock cancellation windows, and pauses should assume identity pinning will eventually be missed somewhere.
- Test the three-promotion ladder directly: an approved address exists, execution-time code identity still matches the reviewed object, and the surviving executable still cannot exceed its intended storage / treasury / module budget.
- Preserve forensic breadcrumbs. Emit events for approval-time code hash, execution-time code hash, implementation pointer, and any mismatch so monitoring can catch drift before or during execution.
Audit prompts
- What exact object is being approved here: address, runtime bytecode hash, implementation hash, creation code, immutable calldata bundle, or some looser human review artifact?
- Could the code behind an approved address change before execution through proxy upgrades, beacon changes, module rebinding, factory redeployments, CREATE2 reuse, or any other indirection layer?
- At execution time, what prevents the system from running different bytecode than the code reviewers or governors actually saw?
- Does any privileged path still use
delegatecallinto an externally supplied or previously approved contract? If so, what storage authority and asset authority does that callee inherit? - If the approved address is a proxy, who controls the proxy admin or implementation slot, and is that authority itself inside the review envelope?
- Are proposal UIs and review pipelines showing reviewers the actual live implementation and calldata that will execute, or only the surface address and source repo?
- Can a benign-looking shell contract later dispatch into mutable downstream logic even if its own runtime bytecode stays unchanged?
- Do timelocks or queues store one immutable ordered action bundle, or do they store a mutable executor address whose behavior can drift before the delay expires?
- What monitoring exists to compare approval-time code identity with execution-time code identity, and who is paged on mismatch?
- If code drift were detected right before execution, is there a real cancellation or pause path that can stop the privileged call in time?
Lessons that should already be standard
Approved addressis not the same thing asapproved behavior. If behavior lives in code and code can drift, the approval object was underspecified.- Reviewers should keep three promotions separate:
this address or shell was admitted,this is still the same executable we reviewed, andthis executable still deserves this much privileged reach right now. - Governance review should authenticate effects, not vibes. The more a system asks humans to reason about a mutable executor shell, the more likely it is to bless the wrong thing.
- Proxy safety is contextual. An address that is perfectly safe to call with normal user privileges can become catastrophic when the caller gives it
delegatecall-level authority or root storage reach. - Time delay only helps if the delayed object stays the same. A timelock wrapped around mutable code is often just a waiting room for smuggled privilege.
- Code-identity drift is not only a governance problem. Plugin registries, hook allowlists, bridge dispatchers, and wallet modules all recreate the same mistake when they whitelist addresses instead of stable execution envelopes.
- Review tooling is part of the security model. If the UI or proposal pipeline cannot show reviewers exactly what will execute later, the system is already asking for social-layer guesswork where machine-layer certainty should exist.
- Blast-radius controls still matter after identity pinning. Healthy systems assume one bad approval can happen and make sure it cannot instantly seize every storage slot or treasury balance.
- Chains may change deployment semantics over time, but the design lesson survives protocol churn: if later execution can resolve to a different code object than the one originally blessed, the authorization boundary is still wrong.
Where this pattern stops:
- This is not the best frame when the attacker already controls the formal upgrade key and simply installs malicious logic through the intended admin path. That belongs under privileged-upgrade abuse. The sharper question here is whether the protocol authenticated the wrong object before any formal upgrade authority was supposed to matter.
- This is not the best frame when a spender or router contract exposes arbitrary token pulls or arbitrary low-level calls while the code at the approved address never changed. Bancor, Transit, Dexible, Socket, Seneca, and LI.FI are approval-surface or arbitrary-call problems, not code-identity drift, because the dangerous logic was already live at the approved address.
- This is not mainly an initializer problem. If a contract is seized because setup stayed callable on the live instance, re-initialization is usually the cleaner lens. This note begins once review or allowlisting said
this address is the safe executable, but later execution no longer referred to the same code identity. - High-signal incidents:
- Tornado Cash governance proposal-smuggling takeover (2023-05-20) — governance approved proposal address
0xC503...after reviewing benign-looking code, the attacker destroyed and recreated the deployment path so different bytecode reappeared at that same address, and governance laterdelegatecall-executed the swapped payload into its own storage. - mutable-implementation approval drain (2021-02-27) — Furucombo whitelisted the Aave V2 LendingPool proxy by address, but
delegatecallmade that proxy read and write its implementation slot inside Furucombo storage, so the approved address later resolved to attacker code and drained victims who had approved the Furucombo proxy.
- Tornado Cash governance proposal-smuggling takeover (2023-05-20) — governance approved proposal address
Internal linkages
- Parent governance framing: governance-capture — useful when the larger question is how governance became the root key.
- Positive-design companion: Execution-envelope identity pinning — useful when the sharper question is what healthy delayed privileged execution should have pinned as the reviewed object before this exploit family ever becomes possible.
- Narrower governance / timelock companion: Timelock action-bundle commitment — useful when the sharper question is how proposal systems should freeze one exact ordered action bundle instead of delaying a mutable proposal shell.
- Adjacent pattern: privileged-upgrade-abuse — both classes end with reviewed behavior diverging from executed behavior, but privileged-upgrade abuse assumes the attacker already controls the formal upgrade key, whereas this class is about smuggling different code through an address-authentication mistake.
- Adjacent pattern: privileged message dispatcher abuse — both rely on over-trusting a privileged execution surface, but here the central mistake is authenticating an address instead of stable code identity.
- Adjacent pattern: arbitrary-call token-approval drain — useful when the same approved address becomes dangerous because it exposes arbitrary token-pull or arbitrary-call power, not because later execution resolves to different code.
- Best current case studies: tornado-cash-governance-proposal-smuggling-takeover-2023-05-20 for the metamorphic-governance branch, and furucombo-aave-v2-proxy-whitelist-mutable-implementation-approval-drain-2021-02-27 for the whitelist / proxy-indirection branch.
Control surface
-
The leverage sits in what the system records as the trusted object: raw address, runtime bytecode hash, creation-code hash, immutable calldata, proxy implementation pointer, and the execution primitive (
callversusdelegatecall) that turns approval into authority. -
The sharp ladder is
an address was admitted,that address still resolves to the reviewed executable meaning, andthat executable only carries this much live privileged reach. -
When a protocol approves an address but later executes whatever code happens to live there, the real control surface is not the address book. It is code identity over time.
-
Sources:
- https://composable-security.com/blog/understanding-the-tornado-cash-governance-attack/
- https://medium.com/@zan.top/unpacking-the-tornado-cash-governance-attack-15b40691ca2e
- https://github.com/pcaversaccio/tornado-cash-exploit
- https://medium.com/furucombo/furucombo-post-mortem-march-2021-ad19afd415e
- https://cmichel.io/replaying-ethereum-hacks-furucombo/
- https://eips.ethereum.org/EIPS/eip-1014
- https://eips.ethereum.org/EIPS/eip-6780
-
Last reviewed: 2026-08-01 UTC