Reproduced Exploit
Term Finance Governance Takeover — Thin gtmvETH Majority → Zero Delay Cooldown → Yearn Vault Drain
Term Finance parked ETH in a Yearn V3 ETH Meta Vault, administered by a Gnosis Safe sitting behind a Zodiac Delay module (txCooldown = 608,400 seconds ≈ 7 days). DAO governance could change that Delay.
Loss
~$8.5M — this PoC reproduces 2,841.743517563533112109 WETH (~$6.86M at ~$2,414/ETH). A second executor later…
Chain
Ethereum
Category
governance
Date
Aug 2026
Source
Crypto Training
EVM Playground
Source-level debugger — step opcodes and Solidity in sync
The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Balances (native / ERC-20 / NFT), Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.
Source & credit. Crypto Training original detection and analysis (live Twitter/X security-alert intake — not from DeFiHackLabs). Standalone Foundry PoC, offline
anvil_state.json, and full write-up: 2026-08-TermFinanceGovernanceTakeover_exp in theevm-hack-registrymirror.
Vulnerability classes: vuln/governance/proposal-manipulation · vuln/governance/timelock-bypass · vuln/access-control/centralization
Reproduction: the PoC compiles & runs in an isolated Foundry project at this project folder. Full verbose trace: output.txt. Verified sources live under sources/ (Yearn V3 ETH Meta Vault, Zodiac Delay, Gnosis Safe, Aragon OSx DAO, WETH). The attacker-controlled governor implementation at
0x3e30DDF3…is unverified;executeProposal()behaviour below is reconstructed from the on-chain trace.
Key info#
| Loss | ~$8.5M — this PoC reproduces 2,841.743517563533112109 WETH (~$6.86M at ~$2,414/ETH). A second executor later drained ~1.68M USDC. Live tx #1 credited 2,841.743535791961701401 WETH (one extra block of Aave/Morpho yield). |
| Vulnerable contract | Zodiac Delay (clone) 0x35C99CF4…9e33 → master 0xd54895B1…1BED6; Yearn V3 ETH Meta Vault 0x26fCb50e…7Db2; protocol Safe 0x46DA347d…8613 |
| Attacker EOA (tx #1) | 0xa908b347…612B |
| Attacker EOA (tx #2) | 0x686457a7…0691 |
| Governor / proposal proxy | 0x64E47780…b4dF (unverified impl 0x3e30DDF3…7cF1) |
| Vote token | gtmvETH — GovernanceWrappedERC20 of tmvETH vault shares (~0.535 total supply) |
| TokenVoting plugin | 0x21377169…2171 (Aragon OSx) |
| Propose / vote (2026-08-17) | 0x284fc544…a8b8 / 0x6e735333…2a8d |
| Attacker Zodiac / Aragon module | 0x0ae12AF3…c0a6 (ERC1967 → Aragon OSx DAO 0x52Af1666…2D3a) |
| WETH exit strategy | 0x184f2E57…6338 ("Fixed Recipient WETH Exit Strategy") |
| Attack tx #1 | 0xd354a15b…4129 — executeProposal(), block 25816049 (2026-08-23) |
| Attack tx #2 | 0x9f273f9a…e8a0 — block 25816159, ~1.68M USDC |
| Chain / block / date | Ethereum mainnet (chainId 1) / fork block 25,816,048 (pre-tx #1) / 23 Aug 2026 |
| Compiler | Delay: Solidity v0.8.0, optimizer off; Yearn V3 vault: vyper:0.3.7; Safe: v0.7.6; Aragon DAO: v0.8.17 |
| Bug class | Cheap majority of thinly held gtmvETH (wrapped vault shares, ~0.535 supply) → pass a malicious Aragon proposal that zeros Zodiac Delay txCooldown (was 608,400 s ≈ 7 days) → enable attacker module → update_debt Yearn/Morpho/Aave positions into a hardcoded-recipient exit strategy |
| Alert | DefimonAlerts · Term Labs · CertiK · @PeckShieldAlert · @GoPlusSecurity |
TL;DR#
Term Finance parked ETH in a Yearn V3 ETH Meta Vault, administered by a Gnosis Safe sitting behind a Zodiac Delay module (txCooldown = 608,400 seconds ≈ 7 days). DAO governance could change that Delay.
Vote power is gtmvETH, a wrap of Yearn tmvETH vault shares. Almost nobody wrapped: totalSupply ≈ 0.535. The attacker spent ~0.5 ETH for ~0.485 tmvETH, wrapped it, and held ~90% of voting power. They proposed on 2026-08-17, voted, then six days later called executeProposal():
- Zero the timelock —
Delay.setTxCooldown(0)thensetTxExpiration(0)(output.txt:427, output.txt:437). Storage slot 105:608400 → 0. - Install a backdoor —
Delay.enableModule(0x0ae12AF3…)(output.txt:448). - Unwind every strategy — Delay
execTransactionFromModule+ immediateexecuteNextTx(legal now that cooldown is 0) calls Yearnupdate_debt(strategy, 0)against Aave stataWETH and Morpho positions, thenadd_strategy+update_max_debt+update_debt(max)on a Fixed Recipient WETH Exit Strategy. - Walk away with WETH — the exit strategy
transfers 2,841.743517563533112109 WETH to the attacker EOA (output.txt:1435, output.txt:1484).
A follow-up tx ~22 minutes later drained ~1.68M USDC from a sibling vault via a second governor (0x4F4B614d…).
Background#
Term Finance (@term_labs) is a fixed-rate lending protocol. Idle WETH sat in Yearn V3 vaults (the ETH Meta Vault at 0x26fCb50e…) that allocated into Aave static-aToken wrappers and Morpho Blue markets. Admin of those vaults is a Safe, and the Safe is gated by a Zodiac Delay so that a module transaction is queued and can only be executed after txCooldown.
That design is sound only if:
- governance that can call
setTxCooldownis expensive to capture, and - the cooldown is not itself a governance-settable parameter that a captured DAO can zero in the same proposal that drains.
Both failed. The vote token was sparsely held; capturing a majority was cheap. The Delay owner (the protocol Safe / role router) accepted setTxCooldown(0) as a legitimate governance action, after which executeNextTx no longer waits.
The vulnerable code#
Zodiac Delay queues module transactions and is supposed to enforce a cooldown before executeNextTx:
function execTransactionFromModule(
address to,
uint256 value,
bytes calldata data,
Enum.Operation operation
) public override moduleOnly returns (bool success) {
bytes32 hash = getTransactionHash(to, value, data, operation);
txHash[queueNonce] = hash;
txCreatedAt[queueNonce] = block.timestamp;
emit TransactionAdded(queueNonce, hash, to, value, data, operation);
queueNonce++;
success = true;
}
function executeNextTx(
address to,
uint256 value,
bytes calldata data,
Enum.Operation operation
) public {
require(txNonce < queueNonce, "Transaction queue is empty");
uint256 txCreationTimestamp = txCreatedAt[txNonce];
require(
block.timestamp - txCreationTimestamp >= txCooldown,
"Transaction is still in cooldown"
);
// ...
require(exec(to, value, data, operation), "Module transaction failed");
}
Source: sources/Delay_35C99C/contracts_Delay.sol (and the master copy at 0xd54895B1…).
The cooldown is not a constant. The owner can set it:
function setTxCooldown(uint256 cooldown) public onlyOwner {
txCooldown = cooldown;
}
At the fork block, txCooldown was 608,400. Proposal #5's first action set it to 0 (output.txt:427). After that, block.timestamp - txCreationTimestamp >= 0 is always true, so the attacker queued and executed the vault update_debt calls in the same transaction.
Yearn V3 then does exactly what a DEBT_MANAGER is allowed to do — pull assets out of strategies:
@external
@nonreentrant("lock")
def update_debt(
strategy: address,
target_debt: uint256,
max_loss: uint256 = MAX_BPS
) -> uint256:
self._enforce_role(msg.sender, Roles.DEBT_MANAGER)
return self._update_debt(strategy, target_debt, max_loss)
Source: sources/Yearn V3 Vault_26fCb5. The Safe is the debt manager. Once Delay executes as the Safe, update_debt(strategy, 0) unwinds Aave/Morpho into idle WETH, and update_debt(exitStrategy, type(uint256).max) pushes that WETH into the attacker-controlled "Fixed Recipient" strategy, which transfers it to 0xa908…612B.
The governor implementation that stores proposal #5 and exposes parameterless executeProposal() is unverified. The trace shows it checking canExecute(5) then calling Aragon OSx DAO.execute on the attacker module (output.txt:412–output.txt:419).
Root cause#
- Cheap majority. Vote power is gtmvETH, an optional wrap of tmvETH vault shares. Almost nobody wrapped (
totalSupply ≈ 0.535), so ~0.5 ETH of shares bought ~90% of the DAO. Same class as Beanstalk (2022) and the StrongBlock takeover in this registry. - Governance can disable its own timelock. Delay's only safety property is
txCooldown. That value is owner-settable, and the owner is the same Safe/DAO the Delay is meant to slow down. A single passed proposal cansetTxCooldown(0)and then drain. - Vault admin = governance. Yearn
DEBT_MANAGER/ADD_STRATEGY_MANAGER/MAX_DEBT_MANAGERwere all reachable from the captured Safe, so there was no second key or immutable strategy allowlist standing between "we passed a vote" and "we redirect 2,842 WETH".
Preconditions#
- Attacker already holds a majority of the DAO token and has passed proposal #5 (propose + vote completed on-chain in the six days before the drain; this PoC starts at the executable state).
- Delay still has
txCooldown = 608400andtxExpiration = 604800until the proposal's first two calls. - Yearn ETH Meta Vault holds Aave stataWETH + Morpho WETH positions, and the Safe is
DEBT_MANAGER. - A "Fixed Recipient WETH Exit Strategy" (
0x184f2E57…) already exists (or is added in the same proposal) with the attacker EOA baked in as recipient.
Attack walkthrough#
Numbers from the offline [PASS] run in output.txt. Fork is block 25,816,048.
- Prank the attacker and fire
executeProposal(). Gas used 2,830,646. The unverified governor impl delegatecallsexecuteProposal(),canExecute(5)returns true, then AragonDAO.executeruns the action list (output.txt:410–output.txt:419). - Kill the 7-day Delay.
setTxCooldown(0)writes slot 105608400 → 0(output.txt:427).setTxExpiration(0)writes slot 106604800 → 0(output.txt:437). - Enable the attacker module on Delay (
enableModule(0x0ae12AF3…), output.txt:448). From this point the attacker module can queue Delay transactions. - Unwind Aave/Morpho strategies with
update_debt(strategy, 0)via DelayexecuteNextTxin the same tx (cooldown is now 0):0x33073258…→ 44.3748 WETH (output.txt:474, output.txt:589)0xfC36C2Ed…→ 43.8777 WETH (output.txt:663)0x9F1c3173…Morpho → 1,445.511 WETH (output.txt:851, output.txt:1035)0x76dD9671…Morpho → 1,307.980 WETH (output.txt:1107, output.txt:1269)
- Install and fill the exit strategy.
add_strategy(0x184f2E57…)thenupdate_max_debt_for_strategy(…, type(uint256).max)thenupdate_debt(exit, max)(output.txt:1404). - Profit.
WETH.transfer(attacker, 2841743517563533112109)(output.txt:1435). ProposalExecuted(id: 5) (output.txt:1472). Attacker WETH profit 2841.743517563533112109 (output.txt:1484).
Tx #2 (not in this PoC) later drains ~1,679,642 USDC via governor 0x4F4B614d….
Diagrams#
Remediation#
- Do not let the same vote that captures the Safe also disable the Delay.
setTxCooldown/setTxExpiration/enableModuleshould require a second, longer timelock (or an immutable minimum cooldown, e.g. 7 days that governance cannot lower). - Separate vault admin from token-weighted DAO. Yearn
DEBT_MANAGERandADD_STRATEGY_MANAGERshould sit on a dedicated multisig or a Delay whose cooldown cannot be zeroed by the captured governor. - Strategy allowlists. Do not allow governance to
add_strategyan arbitrary "Fixed Recipient" adapter that forwardsasset()to an EOA. - Raise the cost of a majority. If a governance token is thinly held, it is not a security boundary for eight-figure TVL. Use a well-distributed token, a high quorum and a veto council, or take the vaults off token voting.
- Monitor
setTxCooldownandEnabledModule. A cooldown drop from 7 days to 0 is an emergency signal; pause deposits / revoke debt-manager roles.
How to reproduce#
cd 2026-08-TermFinanceGovernanceTakeover_exp
# offline (anvil serves anvil_state.json):
../../_shared/run_poc.sh 2026-08-TermFinanceGovernanceTakeover_exp -vvvvv
Expected: [PASS] testExploit() with 2841.743517563533112109 WETH profit (output.txt:368, output.txt:371).
Online fork (archive RPC):
# temporarily point createSelectFork at an archive RPC alias, then:
ETH_RPC_URL=<archive> forge test --match-test testExploit -vvv
Reference: https://x.com/DefimonAlerts/status/2091422624249217259
References#
-
https://x.com/GoPlusSecurity/status/2091701988174200969 (@GoPlusSecurity secondary analysis)
-
https://x.com/GoPlusSecurity/status/2091701993136115802 (@GoPlusSecurity secondary analysis)
-
https://x.com/GoPlusSecurity/status/2091702003059798123 (@GoPlusSecurity secondary analysis)
-
https://x.com/GoPlusSecurity/status/2091702007728140293 (@GoPlusSecurity secondary analysis)
-
https://x.com/PeckShieldAlert/status/2091452165932175659 (@PeckShieldAlert secondary analysis)
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 2026-08-TermFinanceGovernanceTakeover_exp (evm-hack-registry mirror).
- Attack transaction: view on explorer.
Alerts & third-party analyses
- Original alert / thread: post on X.
- Web3Sec X hacked database: search.
- Rekt leaderboard: search.
- Solodit incident search: search.
These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.