
Date: June 25, 2026
Chain: Ethereum
Loss: 4,477.72 USDC, 3,609.95 USDT and about 2.60 ETH, roughly $12.3K, plus 24,182.56 LIX
Class: a permit function that never checked the signer was the owner
Lixir Finance deployed its vaults on Ethereum in July 2021, during the first summer of Uniswap V3, promising to manage concentrated liquidity positions for depositors automatically. The project's X account went quiet in February 2022. Four and a half years after launch, dozens of holders still had vault shares in those contracts, backed by real USDC, USDT, WETH and Lixir's own LIX token.
At 01:21:23 UTC on June 25, 2026, one transaction emptied 35 of those holders' positions across six vaults, collecting about $12.3K in stablecoins and ETH plus 24,182.56 LIX. Every withdrawal was authorized by the vaults' own permit function, which accepted the same meaningless signature for every owner because it only checked that a signature recovered to some address, never that the address was the owner.
A Lixir vault took deposits in a pair of tokens, placed them in a Uniswap V3 position, and gave the depositor vault shares, tokens with names like lv_WETH-USDC, lv_LIX-WETH and lv_USDC-USDT. Each vault was a minimal proxy clone pointing at a shared implementation contract. The vault at 0xfD4c9a491DD777b8b3e13659e9E379252eC78390, for example, was created on July 20, 2021, from an implementation deployed five days earlier.
The shares supported permit, the EIP-2612 shortcut that lets a holder approve a spender by signing a message off-chain instead of sending an approval transaction. The holder signs the owner, spender, amount, nonce and deadline, anyone can submit that signature to permit(), and the contract recovers the signer's address from it with ecrecover. The whole scheme depends on one comparison at the end: the recovered address has to equal the owner whose allowance is being set. The vaults also exposed withdrawFrom() and withdrawETHFrom(), which let an approved spender burn someone else's shares and receive the underlying tokens.
None of the Lixir vault implementations are verified on Etherscan, so their source isn't public. Defimon's alert and DarkNavy's analysis, which rebuilt the logic from the deployed bytecode, both describe the same defect: permit() required only that the recovered address wasn't zero, and never required it to match the owner. Defimon quotes the single check as require(recovered != 0, "INVALID_SIGNATURE").
That turns a signature check into a formality. ecrecover returns some nonzero address for almost any well-formed signature, so a signature made up by anyone passes for any owner. After the check, according to DarkNavy, the implementation increments the owner's nonce, sets the allowance for the spender, and emits an Approval event, exactly as it would for a real signature. DarkNavy's evidence makes the gap visible: for one victim, 0xd7078d619c99799e68e0b44119d6da1c0367e43a, the signature recovered to 0xce81aa870fc7507997e0e88db075c90274e1c4cb, a different address entirely, and permit() accepted it anyway.
A notary who stamps any document as long as it carries a signature, without checking whose signature it is, provides the same protection. The takeaway for builders sits in that one line: a signature proves something only when the code compares the recovered signer to the specific account the signature is supposed to speak for.
The attacker's wallet, 0x3Fa8cF7FeA68C8E76A9838d77889464DdFb6a6cf, sent a contract creation transaction in block 25391316, and the attack ran inside the new contract, 0xEFd1b12F5E3c35D7daE0D1449674C247566f9b76. According to DarkNavy, the contract called permit() 41 times across six vault clones, covering 35 distinct owners, and reused a single signature tuple with v = 28 for every call, changing only the owner and amount.
Each forged permit gave the contract an allowance over one holder's shares, and the contract immediately spent it, calling withdrawETHFrom() 39 times on vaults settled in WETH and withdrawFrom() twice on the USDC-USDT vaults. Each withdrawal burned the victim's shares, pulled the matching liquidity out of the Uniswap V3 position, and sent the proceeds to the contract. Blockscout's record of the transaction shows the largest takes clearly: one lv_LIX-WETH vault released 23,851.32 LIX and 1.34 WETH, a lv_USDC-USDT vault released 3,378.18 USDC and 3,516.08 USDT, and two lv_WETH-USDC vaults released 381.38 and 710.04 USDC alongside 0.51 and 0.72 WETH. The vaults unwrapped the WETH and the contract forwarded it as ETH.
By the end of the transaction, the attacker's wallet held 4,477.72 USDC, 3,609.95 USDT and 24,182.56 LIX from the vaults, and DeFiHackLabs counts about 2.60 ETH alongside them. Defimon put the realizable loss at about $12.3K and noted that LIX is effectively illiquid, so the LIX taken adds little that could be sold.
The attacker moved quickly. Between 01:34 and 01:44 UTC, the wallet approved USDC and USDT and routed them through Relay. From 01:52 to 01:55 UTC, it made seven deposits of 1 ETH and five of 0.1 ETH into the Tornado Cash router. Later that morning, after transactions through LI.FI, Polygon's bridge contract and a KyberSwap router, it made a second round of six 1 ETH and nine 0.1 ETH deposits between 06:26 and 06:30 UTC.
Defimon's alert tagged Lixir's account directly. As of October 20, 2026, Lixir's X account showed no posts since February 2022 and no statement about the exploit.
Olympix reads a contract the way an attacker would, as states, transitions and entry points, and takes in the context around the code, including the standards it implements, to infer the rules the system relies on but never writes down. In a Lixir vault, the state that matters is the allowance table, and permit() is the public entry point that writes to it on behalf of someone other than the caller. The engine maps every path into that table and uses signals to decide which ones to attack. Olympix's static analysis surfaces the signal here: an ecrecover result that's compared only to zero and then used to authorize a write keyed to a different, caller-supplied address.
The invariant for this contract is the heart of EIP-2612: an allowance over an owner's shares changes only when the owner signed for it. From all the paths into permit(), the engine narrows to the feasible ones a stranger can reach with arbitrary signature bytes, and then to the ones that set an allowance for an owner who never signed.
BugPoCer runs Counterexample-Guided Invariant Refutation, starting from the vault as a whole and executing paths until one breaks that rule. The counterexample is almost trivial to read: an outside address calls permit() naming a real holder as owner and itself as spender, with signature bytes that recover to an unrelated address, and the allowance is set anyway. Extend it by one call to withdrawFrom() and the holder's shares are gone. BugPoCer returns that sequence with the minimal conditions that trigger it and a generated Forge test that replays it, which is the June 25 transaction produced in development rather than on mainnet.
Because Olympix runs automatically in development, at every merge and before release, the missing comparison would have surfaced when permit() was written in 2021, before the first vault clone was ever deployed.
Lixir's vaults outlived the team's public activity by years, and the one line they were missing stayed reachable the whole time, waiting for someone to notice that any signature would do. Signature checks are among the easiest places for a single omitted comparison to undo an entire authorization model, and among the easiest for an engine to test exhaustively. To see Olympix work through every signature and allowance path in your own contracts, book a technical walkthrough.
Protocol: Lixir Finance vaults (lv_ vault tokens)
Chain: Ethereum
Date: June 25, 2026, 01:21:23 UTC
Loss: 4,477.72 USDC, 3,609.95 USDT, about 2.60 ETH and 24,182.56 LIX (about $12.3K excluding LIX)
Attacker EOA: 0x3Fa8...a6cf
Attack contract: 0xEFd1...9b76
Vault (lv_WETH-USDC): 0xfD4c...8390
Vault (lv_LIX-WETH): 0x49bb...0465
Vault (lv_LIX-WETH): 0xC870...267d
Vault (lv_WETH-USDC): 0xF4AF...4839
Vault (lv_USDC-USDT): 0xF4aA...5556
Vault (lv_USDC-USDT): 0xecC2...DC58
Vault implementation (source unverified): 0x2Ce1...95CD
Exploit tx: 0x1702...41da
Block: 25391316
DarkNavy: Lixir Vault Forged Permit Approval Drain