Tornado Cash Governance Proposal-Smuggling Takeover (2023-05-20)
- Name: Tornado Cash governance proposal-smuggling takeover
- Date: 2023-05-20
- Protocol: Tornado Cash governance
- Chain: Ethereum
- Category: governance capture / payload-smuggling governance attack / metamorphic-contract proposal swap / delegatecall governance takeover
- Loss: roughly 473,000 TORN withdrawn from governance after the takeover, with public reporting describing about 430 ETH realized from early dumping; more importantly, the attacker temporarily gained majority control over Tornado Cash governance and its governed contracts
- Summary: The attacker did not rent voting power with a flash loan. Instead, they smuggled malicious execution into an apparently ordinary governance proposal, got that proposal approved, then used a hidden
selfdestructpath plus aCREATE2-deployed factory andCREATE-deployed proposal contract to redeploy different code at the exact address governance had already approved. When Tornado governance later executed the approved proposal viadelegatecall, it ran the swapped-in malicious code in governance storage, wrote fake locked balances for attacker-prepared addresses, and let the attacker withdraw large amounts of TORN while also taking effective control of the DAO. - Trigger / entry point:
- Tornado governance proposals were executable contracts whose addresses were approved by tokenholders and later run by the governance contract via
delegatecall. - The approved proposal appeared to reuse the logic of an earlier relayer-penalty proposal, but included a hidden
emergencyStop()/selfdestructpath. - Proposal deployment architecture let the attacker combine
CREATE2andCREATEso a destroyed deployer/proposal pair could be recreated at the same addresses with different bytecode.
- Tornado governance proposals were executable contracts whose addresses were approved by tokenholders and later run by the governance contract via
- Root cause:
- Tornado governance treated
this address was voted inas equivalent tothis code was voted in, even though metamorphic deployment tricks let the code behind that address change after approval. - Governance execution used
delegatecall, so once the target address was swapped to malicious code, execution happened inside governance storage rather than in an isolated proposal sandbox. - The proposal system delayed a mutable proposal shell rather than one exact machine-checkable action bundle with pinned code identity.
- Review processes failed to treat proposal bytecode, hidden destructive functions, and metamorphic deployment patterns as part of the governance attack surface.
- In practice, the DAO approved a contract address, but the attacker delivered different code at execution time and used governance’s own execution model as the privilege-escalation primitive.
- Tornado governance treated
- Exploit path:
- The attacker used address
0x092123663804f8801b9b086b03b98d706f77bd59to submit a governance proposal transaction0x34605f1d6463a48b818157f7b26d040f8dd329273702a0618e9e74fe350e6e0d. - Public analysis describes that proposal as mimicking Tornado Cash’s earlier Proposal 16 relayer-penalization logic closely enough to win approval while hiding a destructive
emergencyStop()path. - Before the malicious execution stage, the attacker had already prepared many auxiliary addresses and locked
0 TORNfrom them into governance, creating accounts whose voting-balance storage could later be falsified cheaply. - After the proposal passed, the attacker called the hidden self-destruct path in transaction
0xd3a570af795405e141988c48527a595434665089117473bc0389e83091391adb, destroying the previously approved proposal setup. - Using a
CREATE2-recreated deployer and thenCREATEfrom that deployer, the attacker redeployed a different proposal contract at the same governance-approved address0xC503893b3e3c0C6b909222b45f2a3a259a52752D. - The attacker then executed the proposal through Tornado governance in transaction
0x3274b6090685b842aca80b304a4dcee0f61ef8b6afee10b7c7533c32fb75486d. - Because governance used
delegatecallinto the proposal’sexecuteProposal()logic, the swapped-in malicious contract wrote directly into governance storage, assigning fake locked balances and votes to the attacker-controlled addresses. - With that forged voting power in place, the attacker withdrew about 473,000 TORN in transaction
0x13e2b7359dd1c13411342fd173750a19252f5b0d92af41be30f9f62167fc5b94, sold part of it, and gained effective majority control over Tornado governance.
- The attacker used address
- What the attacker needed to be true:
- Governance had to approve proposal targets by address without pinning code hash or otherwise preventing metamorphic redeployment.
- Proposal execution had to use
delegatecall, so malicious replacement code could mutate governance storage directly. - The community had to miss or discount the destructive helper hidden in the seemingly routine proposal.
- The attacker needed a deployment path where
CREATE2fixed the deployer address andCREATErecreated the child proposal at the same address after nonce reset. - Governance had to let stored voting balances be turned into withdrawable TORN once the attacker rewrote the relevant storage.
- Technical anatomy worth remembering:
- This was governance capture without flash-loaned quorum. The attacker captured the executor by swapping proposal code after approval.
- The decisive bug was not just
selfdestruct exists. It was the combination of address-based proposal approval, metamorphic redeployment, anddelegatecallexecution into governance storage. - Tornado governance approved an address, but execution safety really depended on approving immutable code. That mismatch is the preserved lesson.
- The attacker used governance itself as the privilege boundary: once malicious code ran via
delegatecall, fake votes and withdrawable balances were only storage writes away. - Unlike treasury-drain governance exploits such as Beanstalk, this incident did not directly empty Tornado’s immutable privacy pools; it compromised the governance system, governed contracts, and governance-held TORN instead.
Three governance-authority promotions this incident collapsed
-
A proposal-shaped execution object still existed at the governance-approved address
- Tokenholders approved proposal contract
0xC503893b3e3c0C6b909222b45f2a3a259a52752D, and both the social review flow and the onchain executor continued to treatthere is still a proposal at this addressas the key fact that mattered. - The attacker preserved that weak fact even after the hidden
selfdestructstep by replaying the deployer / proposal chain withCREATE2plusCREATE, so the same address label survived even though the runtime behind it did not.
- Tokenholders approved proposal contract
-
That surviving address shell was still promoted into canonical reviewed proposal truth
- Tornado governance never pinned one exact reviewed code hash or one exact action bundle whose meaning could be checked again before execution.
- After the redeploy,
same addressstill counted assame approved proposal, even though the executable bytes and the storage-writing effect set had changed materially.
-
That accepted proposal truth still retained immediate governance-root authority
- Execution still happened through
delegatecallfrom the governance contract into the proposal contract, so the malicious replacement inherited direct write access to governance storage instead of only narrow helper-call power. - Once governance accepted the swapped-in proposal as authentic, the attacker could forge locked balances, mint themselves a voting majority, and then withdraw TORN without needing any second privilege-escalation bug.
- Execution still happened through
The durable lesson is that a proposal-looking object still exists, that object still is the reviewed proposal, and that accepted proposal still deserves root governance authority are three separate promotions. Tornado collapsed all three.
- Representative transactions and addresses:
- Proposal-submitting attacker address:
0x092123663804f8801b9b086b03b98d706f77bd59 - Second attacker-linked address highlighted in public reporting:
0x592340957ebc9e4afb0e9af221d06fdddf789de9 - Governance-approved proposal address later reused for malicious code:
0xC503893b3e3c0C6b909222b45f2a3a259a52752D - Proposal submission transaction:
0x34605f1d6463a48b818157f7b26d040f8dd329273702a0618e9e74fe350e6e0d - Hidden self-destruct transaction:
0xd3a570af795405e141988c48527a595434665089117473bc0389e83091391adb - Malicious proposal execution transaction:
0x3274b6090685b842aca80b304a4dcee0f61ef8b6afee10b7c7533c32fb75486d - TORN withdrawal transaction:
0x13e2b7359dd1c13411342fd173750a19252f5b0d92af41be30f9f62167fc5b94 - Attacker’s later proposal to revert state changes:
0x1FAd009aD35689B5a9B91486148F2F32AFE31e23
- Proposal-submitting attacker address:
- Response / mitigation:
- Community discussion quickly shifted to emergency recovery proposals, including a later attacker-submitted proposal that purported to revert the hostile state changes.
- Public analyses emphasized that proposal review must consider hidden destructive paths, metamorphic contracts, and the difference between approving an address versus approving immutable bytecode.
- The incident became a sharp case study in why governance executors should avoid unrestricted
delegatecallinto replaceable proposal contracts.
Pattern tags
- approved proposal address stood in for mutable runtime code, so governance delayed an address shell rather than one fixed privileged action bundle
- hidden
selfdestructplusCREATE2/CREATEreplay preserved the same governance-approved proposal address while swapping the bytecode behind it delegatecallturned post-approval code drift into direct governance-storage mutation, letting proposal execution forge voting balances instead of merely calling helper logic- attacker pre-seeded zero-lock governance accounts so one malicious execution could convert fake balances into immediate withdrawal rights and durable voting control
Why the exploit worked
- Tornado governance authenticated the wrong object. Tokenholders approved
0xC503...as a proposal address, but the actual privileged object that mattered was the runtime bytecode and storage-writing effect set that would execute later. - The benign-looking proposal shell still contained a destructive branch. Once the hidden
selfdestructpath fired, the attacker could recreate the deployer / proposal chain and restore a contract at the exact same approved address with materially different logic. - That made the review surface non-binding. The wait between approval and execution did not freeze one exact code hash or ordered action bundle; it only protected a mutable execution slot whose meaning could still change.
delegatecallamplified the failure fromwrong code runsintogovernance storage is rewritten. The malicious replacement did not need a second privilege escalation after execution started; it inherited governance’s storage authority directly and could mint itself fake locked balances and votes.- The attacker had already prepared the state needed to cash out the forged rights. Zero-value locks across many addresses meant one storage-forgery step was enough to turn fake balances into withdrawable
TORNand majority governance control.
Detection and audit pivots
- Flag any governor or timelock that approves executable proposal contracts by address rather than by exact calldata bundle, code hash, or another machine-checkable code-identity commitment.
- Review proposal contracts for latent destructive or metamorphic branches even if the visible
executeProposal()path looks routine.selfdestruct, factory replay, redeployment helpers, or hidden emergency methods belong in the governance threat model, not just the deployment model. - Compare review-time and execution-time code identity. If a proposal can be destroyed, re-created, or re-pointed while keeping the same address, the delay protects the wrong thing.
- Trace the exact privilege boundary of proposal execution. Any governance path that uses
delegatecallinto proposal-controlled code should be reviewed like a root upgrade, with explicit analysis of which storage slots and accounting maps a malicious proposal could rewrite. - Watch for attacker preparation steps that make later governance-state forgery liquid immediately: dust or zero-value locks, pre-created voting accounts, staged delegate relationships, or other state that can be turned into live governance weight by one malicious write.
- Ask the healthier design question explicitly: if defenders noticed something odd after voting but before execution, what exact machine-checkable object could they compare against the approved one to prove that execution meaning had drifted?
Failure-prevention checklist
- Do not approve proposal contracts by address alone. Governance should commit one exact executable object: ordered calls, execution primitive, and any code identity whose drift would change privileged meaning.
- Remove or tightly constrain
delegatecallfrom proposal execution. If proposal-authored code must ever run in governance storage, treat it like an upgrade lane with stricter review, staged execution, and explicit storage-write bounds. - Recheck execution-time code identity before running delayed actions. If runtime bytecode, implementation pointer, or deployment provenance differs from what was reviewed, execution should fail closed.
- Ban proposal architectures that can preserve an approved address while changing code behind it through
selfdestruct, factory replay, metamorphic contracts, or equivalent indirection. - Treat zero-value locks, dust voting accounts, and pre-seeded governance records as part of the cash-out path. If malicious storage writes could instantly turn those records into withdrawable governance balances, the downstream withdrawal surface needs its own sanity checks.
- Give defenders an emergency halt or veto surface that can stop execution when review-time and execution-time artifacts diverge, even if the proposal technically still points to the same address.
- Make proposal-review tooling render bytecode-critical differences, hidden helper functions, and destructive branches instead of only showing top-level source resemblance or prose summaries.
Audit prompts
- What exact object do tokenholders approve here: a proposal id, a contract address, one code hash, or one ordered privileged action bundle?
- If the approved proposal address self-destructs and is recreated, what onchain check prevents governance from treating the replacement runtime as the same approved object?
- Which storage slots become writable if proposal execution uses
delegatecall, and what stops a malicious proposal from forging balances, voting power, timelock state, or withdrawal rights directly in governance storage? - Can proposal factories, deployers, or helper contracts be replayed so a supposedly reviewed proposal shell later resolves to different code at the same address?
- What machine-checkable artifact can monitors compare between vote completion and execution to prove that the proposal’s privileged meaning has not changed?
- If an attacker pre-seeds dust or zero-value governance positions, could one malicious execution convert them into immediately liquid voting balances or withdrawal rights?
- Does the protocol authenticate
same addressorsame privileged effect set? If those are not equivalent, where is the gap closed?
Lessons that should already be standard
- Governance delays need to freeze effects, not labels. A timelock on
this proposal addressis not meaningful if the code behind that address can still change. Delegatecallinto proposal-controlled code is root authority in disguise. Once execution enters governance storage, proposal review is security-critical bytecode review, not ordinary feature review.- Metamorphic deployment tricks are not edge-case wizardry; they are exactly the sort of address-versus-code confusion a governance system must be designed to survive.
- Hidden helper paths matter as much as visible execution paths. A benign-looking proposal that contains one destructive branch is still a privileged payload with a second execution story.
- Pre-seeded governance state can be part of the exploit, not just the staging. Dust locks and empty accounts become dangerous when one malicious write can promote them into live balances.
- Healthy governance should let defenders answer one simple question at any time:
what exact bytes and effects are still authorized to run?
Why this incident matters
- It broadens the governance-capture corpus beyond
borrow enough votes and execute immediatelyintoapprove benign-looking payload, swap code, then execute as governance. - It is one of the clearest examples of metamorphic-contract mechanics turning DAO governance review into an address-authentication failure.
- It is also the sharpest
delayed governance approved the wrong objectcase in the corpus: the protocol did not freeze one exact action bundle with pinned code identity, so the timelock-like wait protected a mutable execution slot rather than a stable privileged effect set. - It preserves a useful distinction for later comparisons: some governance attacks rent temporary voting power, while others smuggle malicious authority through the proposal execution machinery itself.
Internal linkages
- Core recurring pattern: action-bundle-non-commitment.
- Narrower mutable-code branch: approval-by-address-mutable-code-execution.
- Parent governance framing: governance-capture.
- Positive-design execution-envelope companion: execution-envelope-identity-pinning — the deeper missed control was authenticating one full privileged-execution object rather than a proposal address whose code identity and
delegatecallmeaning could drift before execution. - Positive-design timelock companion: timelock-action-bundle-commitment — the delayed review window never froze one exact ordered action bundle with pinned code identity, so the queue protected ambiguity instead of a stable effect set.
Control surface
-
The leverage sat in proposal-review discipline, approval-by-address instead of approval-by-code-hash,
delegatecall-based execution, and governance’s willingness to treat proposal contracts as trusted code rather than adversarial payloads. -
More narrowly, the protocol never committed one exact delayed action bundle with fixed code identity and execution primitive; it committed a proposal address whose executable meaning could drift before the timelock-like wait ended.
-
Tornado Cash governance did not merely approve a bad proposal. It approved a mutable execution slot and then let that slot run with governance’s own storage authority.
-
The preserved healthy-side boundary is sharper than
audit proposal code better: review, queueing, monitoring, and execution all needed to refer to the same machine-checkable privileged object. -
Sources:
- https://rekt.news/tornado-gov-rekt
- https://medium.com/@zan.top/unpacking-the-tornado-cash-governance-attack-15b40691ca2e
- https://composable-security.com/blog/understanding-the-tornado-cash-governance-attack/
- https://github.com/pcaversaccio/tornado-cash-exploit
- https://www.coindesk.com/tech/2023/05/21/attacker-takes-over-tornado-cash-dao-with-vote-fraud-token-slumps-40/
- https://etherscan.io/tx/0x34605f1d6463a48b818157f7b26d040f8dd329273702a0618e9e74fe350e6e0d
- https://etherscan.io/tx/0xd3a570af795405e141988c48527a595434665089117473bc0389e83091391adb
- https://etherscan.io/tx/0x3274b6090685b842aca80b304a4dcee0f61ef8b6afee10b7c7533c32fb75486d
- https://etherscan.io/tx/0x13e2b7359dd1c13411342fd173750a19252f5b0d92af41be30f9f62167fc5b94
-
Last reviewed: 2026-08-04 UTC