Lixir Finance’s Ethereum vault-token system was exploited on 2026-06-25 01:21:23 UTC in transaction 0x17026faca0b8e4cb7531e4fb277c390eb165e81229628e0192923ad1d90a41da. The root cause was broken EIP-2612-style signature validation in the vault-share permit path used by the lv_* wrappers over Uniswap V3 positions. The attacker EOA 0x3fa8cf7fea68c8e76a9838d77889464ddfb6a6cf deployed helper contract 0xefd1b12f5e3c35d7dae0d1449674c247566f9b76, reused the same (v,r,s) signature 41 times across 35 different owners, and granted that helper approval over six vault clones. The helper then called withdrawETHFrom or withdrawFrom to burn victim vault shares and extract 4477.722585 USDC, 3609.950462 USDT, 2.588255642603989317 ETH, and 24182.560323103174179821 LIX; the contemporaneous alert valued the economically realizable loss at about $12.3K because the LIX leg was largely illiquid.
Root Cause
Vulnerable Contract
The vulnerable surface is the Lixir vault implementation family behind the attacked lv_* tokens. The transaction touches six EIP-1167 clone vaults: 0xfd4c9a491dd777b8b3e13659e9e379252ec78390, 0x49bba4c5c4f8a2d444ca5fda1b3137d94df40465, 0xc870d944dd883f25a1b7b3312bc2449dd5b0267d, 0xf4af7ae20eb7334cbdf334eaf850e7db42394839, 0xf4aada01bf9d44fe0d03a6b2fa6519e3d7325556, and 0xecc2b0296eae113a114ccb9f8846a7852545dc58. Their minimal-proxy bytecode resolves to four full implementations: 0x2ce186c52cf150000bf8ad2b4679d4ad619395cd, 0xe5fbef79727d8db175281feaccfa49dcfdc6bcf3, 0x8eed729352382fb06a5ffea72d2618d7eb7da89a, and 0xd0f02b9deb434ec999022d465c5e56842448069b.
None of those implementations is verified on-chain, so source confidence is medium and comes from trace-guided recovered pseudocode plus disassembly. The representative recovered implementation below is 0x2ce186c52cf150000bf8ad2b4679d4ad619395cd; the other three implementations expose the same permit, nonces, DOMAIN_SEPARATOR, and delegated withdrawal surface, and the transaction shows the same forged-signature behavior across all of them.
Vulnerable Function
The vulnerable entry point is permit(address owner,address spender,uint256 value,uint256 deadline,uint8 v,bytes32 r,bytes32 s) with selector 0xd505accf. In the attacked flow, the helper contract calls permit on a vault clone, the clone delegates into its implementation, and the implementation updates the share-token allowance for owner -> spender even though the recovered signer is not the supplied owner. That forged approval is then consumed immediately by withdrawETHFrom(address,uint256,uint256,uint256,address,uint256) on WETH-settled vaults or withdrawFrom(address,uint256,uint256,uint256,address,uint256) on the USDC-USDT vaults.
Vulnerable Code
// [recovered - approximation]
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external {
bytes32 digest = keccak256(
abi.encodePacked(
hex"1901",
DOMAIN_SEPARATOR(),
keccak256(abi.encode(owner, spender, value, nonces[owner], deadline))
)
);
address recovered = ecrecover(digest, v, r, s);
require(recovered != address(0), "INVALID_SIGNATURE"); // <-- VULNERABILITY: the implementation never requires `recovered == owner`
nonces[owner] = nonces[owner] + 1;
allowance[owner][spender] = value;
emit Approval(owner, spender, value);
}
Why It’s Vulnerable
Expected behavior: a permit-based approval path must accept a signature only when the recovered signer exactly matches the owner whose allowance is being changed. The signature must therefore authorize one specific owner, one specific spender, one specific value, and one specific nonce progression.
Actual behavior: the implementation accepts any signature whose ecrecover result is merely nonzero. The transaction proves that behavior directly: all 41 permit calls reuse the same signature tuple v = 28, r = 0xe7c93726a865578504442b1a6827f676e0ed74bdff2be3960d1e253bbcfc4462, s = 0x6aa772b878bc912bdbb33a0014ec507c4b3896ea85aa914b74dee9b7ac3e56da, while the owner field changes across 35 distinct addresses and the call still succeeds each time.
The nested ecrecover precompile calls also show that the recovered signer was not the victim owner. For example, the permit against 0xfd4c9a... with owner 0xd7078d619c99799e68e0b44119d6da1c0367e43a recovered 0xce81aa870fc7507997e0e88db075c90274e1c4cb, and the permit against 0x49bba4... with owner 0x63f75e8995c0a3b57ff5a2587bcd78d2faf5d00d recovered 0x91120446107ff069195ca494a1ef4774155ae6db. Those are different nonzero addresses, neither equal to the stated owner, yet the implementation still increments nonces[owner], writes allowance[owner][spender] = value, and emits Approval(owner, spender, value).
That gap turns the permit path into an arbitrary approval primitive. Once the helper receives allowance over the victim’s vault-share balance, it can call the delegated withdrawal path as if the owner had approved it intentionally. The later share burn and asset collection logic behaves normally; the exploit exists solely because the approval precondition can be forged.
Attack Execution
High-Level Flow
- The attacker EOA
0x3fa8cf7fea68c8e76a9838d77889464ddfb6a6cfdeploys helper contract0xefd1b12f5e3c35d7dae0d1449674c247566f9b76. - The helper iterates through six Lixir vault clones and repeatedly calls
permit(owner, helper, victimShareBalance, MAX_DEADLINE, v, r, s)using the same signature pair for every owner. - Each vault clone delegates into its implementation, accepts the forged permit, increments the owner’s nonce, and emits an approval that authorizes the helper to spend the owner’s vault shares.
- The helper immediately follows each forged permit with
withdrawETHFromon WETH-settled vaults orwithdrawFromon the USDC-USDT vaults, using the freshly minted allowance to burn the victim’s shares. - The implementations burn and collect the underlying Uniswap V3 position assets, unwrap WETH into ETH when needed, and forward USDC, USDT, ETH, and LIX to the helper and then the attacker side.
Detailed Call Trace
The transaction begins with a top-level CREATE from 0x3fa8cf7fea68c8e76a9838d77889464ddfb6a6cf, which deploys 0xefd1b12f5e3c35d7dae0d1449674c247566f9b76. The deployed helper then issues 41 permit calls and 41 matching withdrawal calls. The permit calls target six vault-share tokens: four WETH-settled wrappers and two USDC-USDT wrappers. Their minimal-proxy runtime bytecode embeds the implementation addresses directly, so the call flow is helper -> clone -> implementation for each permit and each withdrawal.
For the forged approval stage, the helper always supplies the same signature tuple while changing the owner and value arguments. The WETH-settled implementations call the ecrecover precompile and then return successfully even though the precompile output varies from one permit to the next and never matches the owner. The stablecoin-settled implementations expose the same selector surface and exhibit the same forged-permit behavior in the trace. In total, the helper successfully forges approvals across 35 distinct owners, with the clone distribution 18 + 15 + 4 + 2 + 1 + 1 across 0xc870d9..., 0x49bba4..., 0xfd4c9a..., 0xf4af7a..., 0xf4aada..., and 0xecc2b0....
Once approved, the helper invokes withdrawETHFrom 39 times and withdrawFrom 2 times. In the WETH paths, the delegated implementation reads the Uniswap V3 position state through positions(bytes32), executes burn(int24,int24,uint128) and collect(address,int24,int24,uint128,uint128), transfers the ERC-20 leg from the pool, unwraps WETH through WETH.withdraw(uint256), and forwards native ETH plus the non-WETH token to the helper. In the stablecoin paths, the delegated implementation performs the same share burn and pool collection flow but pays both underlying ERC-20 legs directly. The helper then forwards the accumulated ETH to the attacker EOA while retaining the ERC-20 proceeds on the attacker side.
Financial Impact
The transaction attributes four net gains to the attacker side: 4477.722585 USDC, 3609.950462 USDT, 2.588255642603989317 ETH, and 24182.560323103174179821 LIX. The first three legs are the economically meaningful proceeds. The additional LIX inventory came from the attacked lv_LIX-WETH wrappers but had little secondary liquidity at the time of the exploit.
Those proceeds were extracted from 35 holder positions across six Lixir vault-share clones. The drained assets correspond to three wrapper families visible in the trace and receipt events: lv_WETH-USDC, lv_LIX-WETH, and lv_USDC-USDT. Using the contemporaneous pricing quoted by the alert that surfaced this transaction, the realizable loss was about $12.3K, with most of the uncertainty coming from the illiquid LIX leg rather than the stablecoin or ETH proceeds.
Evidence
- Transaction:
0x17026faca0b8e4cb7531e4fb277c390eb165e81229628e0192923ad1d90a41daon Ethereum, block25391316, statussuccess. - Attacker:
0x3fa8cf7fea68c8e76a9838d77889464ddfb6a6cf, using helper0xefd1b12f5e3c35d7dae0d1449674c247566f9b76. - Vulnerable contract family: six EIP-1167 vault clones that resolve to implementations
0x2ce186c52cf150000bf8ad2b4679d4ad619395cd,0xe5fbef79727d8db175281feaccfa49dcfdc6bcf3,0x8eed729352382fb06a5ffea72d2618d7eb7da89a, and0xd0f02b9deb434ec999022d465c5e56842448069b. - Key on-chain fact: 41 permit calls reused the exact same signature tuple
(28, 0xe7c93726a865578504442b1a6827f676e0ed74bdff2be3960d1e253bbcfc4462, 0x6aa772b878bc912bdbb33a0014ec507c4b3896ea85aa914b74dee9b7ac3e56da)while changing theownerargument across 35 distinct addresses. - Key on-chain fact: the nested
ecrecoveroutputs were nonzero but not equal to the claimed owner, such asowner = 0xd7078d619c99799e68e0b44119d6da1c0367e43arecovering0xce81aa870fc7507997e0e88db075c90274e1c4cbandowner = 0x63f75e8995c0a3b57ff5a2587bcd78d2faf5d00drecovering0x91120446107ff069195ca494a1ef4774155ae6db. - Key on-chain fact: the forged approvals were consumed immediately by 39
withdrawETHFromcalls and 2withdrawFromcalls, which burned victim shares and paid the underlying assets to the attacker helper. - Key on-chain fact: the attacker side finished with
4477.722585 USDC,3609.950462 USDT,2.588255642603989317 ETH, and24182.560323103174179821 LIX.
Remediation
Lixir should replace the custom permit logic with a standard EIP-712 / ECDSA implementation that rejects any signature unless recovered == owner, and it should use a well-tested library rather than hand-rolled ecrecover handling. Every vault implementation in the clone family should be audited together, because this transaction exercised four different implementations that shared the same broken permit surface.
The protocol should also invalidate or disable the affected permit-enabled share tokens until patched, and it should consider rotating to fresh implementations plus fresh wrapper deployments if existing allowances and nonce state cannot be trusted. Finally, the test suite needs invariant coverage that proves a single (v,r,s) cannot approve two different owners, and that withdrawETHFrom / withdrawFrom cannot succeed unless the allowance truly came from the owner who is being debited.