Ambient Finance's $115K Loss and How Olympix Would Have Prevented It
Every concentrated-liquidity DEX keeps a quiet set of counters at each price tick, recording how much in fees has accrued on either side of it. Ambient Finance calls them fee odometers, and they only work if each one flips correctly every time the price crosses its tick. On June 7, 2026, an attacker found a crossing where Ambient's odometer didn't flip. Two transactions twelve seconds apart turned that missed update into a reward rate so large that a single harvest paid out nearly all of a pool's ambient liquidity.
The transactions landed at 15:26:23 and 15:26:35 UTC in blocks 25266404 and 25266405. Between them, CrocSwapDex, the one contract that holds every Ambient pool, lost 35.04 ETH, 55,913.81 USDC and 2,142.01 USDT. Valuing the ETH at the rates the attacker got when it swapped its own stablecoins back to WETH in the same transactions, that's about $115,448. TenArmor's alert, as The Crypto Times reported, put the second, larger transaction at about $110.6K.
One Contract, Many Ledgers
Ambient, formerly CrocSwap, runs every pool out of a single core contract. CrocSwapDex keeps each pool's state in its own storage and hands work to sidecar contracts called callpaths: a cold path for account operations, a warm path for liquidity, and others. Users talk to it through userCmd, naming a callpath and passing an encoded command.
Two kinds of liquidity live side by side in each pool. Concentrated positions sit between two price ticks and earn fees only while the price is inside their range. Ambient liquidity spans the whole curve. To pay concentrated positions fairly, the pool tracks fee growth globally and at each tick, and works out what a range earned by comparing those numbers. The tick-level number is the odometer. When the price crosses a tick, the odometer has to be flipped so it measures fees from the other side, or every range that uses that tick computes the wrong reward.
Users can also hold an internal balance inside the exchange, which Ambient calls surplus collateral, and settle commands against it instead of moving tokens on every call. That piece matters to the money trail, but it wasn't where the accounting broke.
The Crossing That Didn't Flip
Olympix's engineers traced both transactions after the exploit. Their trace places the bug in LevelBook.crossLevel, the function that updates a tick when a swap moves the price across it. When a swap crossed a tick while the pool's global fee counter hadn't changed, crossLevel skipped flipping that tick's fee odometer. On its own, that left a tick holding a value that was correct for one side of the price but stale for the other.
The second half of the bug came later. When a position's rewards were computed, the code subtracted odometer values in an unchecked block. With one odometer left unflipped, the subtraction ran below zero and wrapped around to an enormous number. The position's reward rate was no longer a small fraction. It was close to the maximum value the type could hold.
Harvesting that position did the rest. The harvest call ran through WarmPath.harvest into PositionRegistrar.harvestPosLiq, which converted the wrapped reward rate into liquidity owed to the position's owner. According to the trace, one harvest paid out about 99.9% of the pool's ambient liquidity, money that belonged to everyone who had provided full-range liquidity.
Surplus collateral was only the wrapper around the trade. The attacker deposited into surplus at the start, settled the swaps and the harvest against it, and pulled the proceeds out at the end through ColdPath.disburseSurplus.
Twelve Seconds, Two Pools
The first transaction came from one attacker EOA and hit the ETH and USDT pool. A fresh helper contract borrowed 3 WETH from the Balancer vault, ran a sequence of swaps that set up the unflipped crossing, then harvested. Thirteen separate USDT transfers flowed out of CrocSwapDex during the run, followed by a single 1,273.79 USDT transfer. The helper repaid the loan and left with about 1.32 ETH and 2,142.01 USDT, which it sold for 1.31 WETH.
The second transaction, from a second EOA calling the same attacker contract, ran the same play against the ETH and USDC pool with a 50 WETH flash loan. The harvest paid 33,016.99 USDC on top of the swap outflows, and when the helper closed its surplus balance, CrocSwapDex sent it 83.72 ETH, 33.72 ETH more than it had deposited. After repaying Balancer, the helper sold 55,913.81 USDC for 34.13 WETH on Uniswap v3. Roughly half of the proceeds went to an attacker wallet and half to the block's fee recipient, an address The Crypto Times identifies as Titan Builder.
As of November 12, 2026, Ambient hadn't published a post-mortem, and no recovery had been reported.
How Olympix Would Have Caught the Wrapped Reward
Olympix reads a pool as states, transitions and entry points. Here the states that matter are the global fee counter, each tick's odometer and each position's reward snapshot, and the transitions are every swap that crosses a tick and every harvest that reads those values. From that structure the engine can infer the invariant the pool depends on: a position can never be owed more in rewards than the fees that were actually paid into its range, and a harvest can never pay out liquidity that belongs to other positions.
The engine narrows from every path to the feasible ones that touch that invariant. Had the team run Olympix on this code, static analysis would have surfaced two signals sitting on those paths. One is a conditional skip in crossLevel, where a tick's odometer update depends on the global fee counter having moved. The other is an unchecked subtraction of odometer values in the reward calculation, with nothing guaranteeing the result stays non-negative.
BugPoCer would have tied the two together. It starts from the invariant and searches for a sequence of calls that breaks it, and the sequence here needs no special permissions: open a concentrated position, push the price across one of its ticks with a swap that leaves the global fee counter unchanged, then harvest. BugPoCer would have returned that sequence as a counterexample, showing the reward rate wrapping and the harvest paying out nearly all of the pool's ambient liquidity, along with the minimal conditions and a generated Forge test that replays it. That test would have been the June 7 drain, produced in development, when the fix was still a matter of always flipping the odometer on a crossing and checking the subtraction.
Because Olympix runs at every merge and before release, any later change to crossLevel or to the reward math would have been checked against the same invariant.
Two Small Mistakes That Needed Each Other
Neither half of this bug looks dangerous alone. A skipped update under a narrow condition reads like an optimization, and unchecked arithmetic is a common gas saving in code that's assumed to be consistent. Together they turned a fee counter into a payout of a pool's entire ambient liquidity. That's the case for writing reward invariants down explicitly and testing them as sequences of swaps and harvests, rather than trusting each function to be right in isolation.
Shared-contract DEXs raise the stakes further, because one pool's books sit next to every other pool's in the same storage. If your team runs tick-based fee accounting and wants to see how those invariants would hold up, book a technical walkthrough.
On-Chain References
Exploit Transaction 1 (ETH/USDT pool): 0xfc0e...8a30
Exploit Transaction 2 (ETH/USDC pool): 0xb2fc...ff52
Attacker EOA 1: 0x0000...1694
Attacker EOA 2: 0x0000...02aF
Attacker Contract: 0x0000...dD0F
Helper Contract (Tx 2): 0xAAC1...Ec1B
Attacker Receiving Wallet: 0x0000...394a
CrocSwapDex: 0xAaAa...f688
ColdPath Callpath: 0xf8fe...d710
More post-mortems
See every post-mortem →See what Olympix finds on your own code
Send us a repo that's already been audited, and we'll run Olympix at the same commit and show you what comes back.




