October 20, 2026
|
Exploit Postmortem
Cover image for "DeFiTuna's $570K Loss and How Olympix Would Have Prevented It"

DeFiTuna's $570K Loss and How Olympix Would Have Prevented It

Date: July 16, 2026

Chain: Solana

Loss: $569,601 USDC, about $570K

Class: a solvency check that read a rounded-down zero as a healthy position

On July 16, 2026, a DeFiTuna position borrowed about 570,000 USDC from the protocol's lending vault, swapped it, and came back holding 494 raw units of TUNA, which is 0.000494 TUNA. DeFiTuna valued that balance in USDC terms, got 0.9077, dropped the fraction, and recorded the position's total assets as zero. Its health check looked at that zero and approved the position.

The USDC never stayed with the position. It had gone into a pool the attackers built themselves, and two of their wallets withdrew it, leaving DeFiTuna's USDC lending pool short by $569,601. Every step relied on one assumption written into the health check, that a position worth zero is an empty position, and this one was carrying roughly $570K of debt.

Borrowing to Buy, Then Proving You're Good for It

DeFiTuna is a Solana DeFi platform that combines a spot AMM, leveraged liquidity provision and a lending market. In the market that was attacked, a user posts USDC as collateral, borrows more USDC from a protocol vault, and swaps it into TUNA through Jupiter, a swap aggregator. Once the swap lands, DeFiTuna checks the position's assets against its debt to decide whether it's healthy, meaning whether there's enough value in it to cover what it owes.

According to BlockSec's analysis, the caller supplies the swap route accounts, so the caller also decides which pool executes the swap. DeFiTuna supports Fusion pools, a pool type anyone can create, and the protocol placed no minimum liquidity requirement on the pool a route could send funds through.

When Zero Means Empty and When It Doesn't

CertiK's incident analysis traces the health check to TunaPosition.is_healthy(), which calls compute_total_and_debt() to work out what a position holds and owes. That function converts the position's TUNA into USDC using a reference price and rounds the result down to a whole number. For the attacker's position, the math came to 494 times 0.0018375338, or 0.9077417, and the conversion discarded everything after the decimal point, so the total became zero.

is_healthy() then handled that zero under the assumption, in CertiK's words, that "the leverage of an empty position is always 1.0x." A position with nothing in it and nothing owed really is healthy, and that's the case the branch was written for. BlockSec describes the gap precisely: the check treated a total of zero as healthy without also requiring the debt to be zero.

A bank teller deciding whether a customer's account can cover a withdrawal would never read a balance of zero as proof the account is fine without also checking whether there's a loan on it. DeFiTuna's check skipped exactly that second look. The single idea to take from this case is that a zero produced by rounding is a number the code computed, and any branch that reads zero as "nothing here" has to confirm that nothing is owed either.

Building a Pool Only the Attacker Could Profit From

The attackers created a separate TUNA/USDC Fusion pool, distinct from DeFiTuna's normal market pool, and placed two limit orders in it at tick 208,640, each holding 0.000526 TUNA, for 0.001052 TUNA in total. The orders belonged to attacker wallets 7hiH...FJst and BK9a...QpH3c and were positioned to receive whatever USDC entered the pool.

DeFiTuna runs a price-deviation check before every swap, and it passed. The sources disagree on why. CertiK's analysis, which DeFiTuna's team called accurate, says the attackers set the pool's initial price close to the legitimate oracle price, so the check validated price without ever looking at depth. BlockSec's on-chain reading finds the pool was initialized at an extreme price, around 1.149 billion USDC per TUNA, and that the check passed because it compared the normal market pool's price with the oracle and never examined the Fusion pool the route actually used. Both readings end with a guard that let the swap through without measuring how much liquidity sat in the pool the money was about to enter.

One Transaction, Two Withdrawals

In the transaction CertiK analyzes, timestamped 05:48:12 on July 16, the attacker called Open_and_increase_tuna_spot_position_jupiter to open a spot position with zero collateral. About 570,000 USDC was borrowed from the lending vault, and after fees, 569,601 USDC went through the embedded Jupiter route into the attacker's pool with the minimum output effectively set to zero. At that pool's price, the position received 0.000494 TUNA.

is_healthy() valued the position at zero assets, read zero as healthy, and let the position stand with its debt. The USDC now sat in the pool, where it belonged economically to the two attacker limit orders, and each of the two wallets called DecreaseLimitOrder and withdrew 284,280.483231 USDC. BlockSec notes that multiple attack transactions were executed, and CertiK's address table lists nine attacker wallets in total.

From Solana to Railgun

CertiK followed the money out of Solana. The funds from 7hiH...FJst and BK9a...QpH3c were bridged to Ethereum through Mayan, 140 ETH went into Railgun, a privacy protocol, and as of July 20, 291,696 DAI and 5 ETH were still sitting in Ethereum wallet 0x509B...6d3e.

DeFiTuna's first post, at 15:09 UTC on July 16, said an attacker had extracted $580,000 from its lending pools, leaving the USDC lending pool $580,000 short, and that the attack vector had been identified and mitigated while the team worked to recover the funds. A day later it put the deficit at $570,000, said the vulnerability was patched and borrowing was paused, and said the DefiTuna AMM itself was unaffected. On July 28 it reopened withdrawals for affected lenders, with the losses socialized across all USDC lending pool depositors and the remaining deficit left on their accounts. On August 20, the team said protocol revenue would be redirected into a designated wallet to cover the outstanding debt, and that the funds would go to affected users once the target amount was reached.

How Olympix Follows a Rounded Number Into the Branch That Trusts It

Olympix reads a program the way an attacker does, as states, transitions and entry points, and takes in the design around the code to infer the invariants the system depends on. For a lending market, the one that matters here fits in a sentence: a position that owes anything can only be called healthy if its assets, as the program computes them, are greater than zero and cover what it owes. DeFiTuna's own code implies that rule, since the whole point of is_healthy() is to stop a borrower from walking away with more than they put in.

A position's state space looks infinite, with every swap route, pool price and balance the caller can choose, but the engine maps the paths and uses signals to decide which are worth attacking. The signal in this case is a valuation that truncates toward zero and feeds straight into a solvency branch that gives zero special treatment. That narrows the search from every path, to the feasible ones, to the few where a caller can steer the computed total down to zero while debt stays open.

With the dust-to-zero path flagged, BugPoCer starts from the lending market's solvency rule and tries to falsify it, which is what Counterexample-Guided Invariant Refutation means in practice: it executes paths from the system outward until one produces an indebted position that still passes is_healthy(). Rounding to zero that feeds a solvency check is a class it flags directly, and here the failing run is the attack itself: open a position with no collateral, borrow, swap through a pool that returns dust, let the total round to zero, and pass is_healthy() with the debt still on the books. BugPoCer returns that sequence with the minimal conditions that trigger it and a test that replays it, and since its scope covers Rust and Anchor programs, a Solana codebase like DeFiTuna's is in range.

This bug lives in how arithmetic results travel from one function into a branch in another, which is why it took execution to reach it, and why it's the kind of edge a review can pass over when each function looks correct on its own. Olympix runs in development, at every merge and before release, so the counterexample shows up while the fix is a one-line change to the zero branch.

Zero Is an Answer That Needs a Second Question

DeFiTuna's health check was right about empty positions and wrong about positions that only looked empty after rounding, and $569,601 left through that difference. Any lending code that converts balances, rounds them and then branches on the result deserves a test where the result is zero and the debt isn't. To see Olympix infer the solvency rules in your own programs and hand back the exact sequence that breaks one, book a technical walkthrough.

On-Chain References

Protocol: DeFiTuna

Chain: Solana

Date: July 16, 2026, 05:48:12 (CertiK timeline)

Loss: $569,601 USDC (CertiK)

Exploit tx (analyzed by CertiK): 124ibr...kvEG

Exploit tx (analyzed by BlockSec): 4x33Dq...EXj1

DeFiTuna program: tuna4u...nogD

Damaged USDC vault: D76dDc...Dv55

Attacker 1: 9ytGWP...vESb

Attacker 2 (also listed as fake pool): 917DKT...Ejtg

Attacker 3 (limit-order owner): 7hiHL8...FJst

Attacker 4 (limit-order owner): BK9aTn...pH3c

Attacker 5: 8P3H7H...wvqD

Attacker 6: GrXBhM...aDpo

Attacker 7: F4xUro...E53p

Attacker 8: ETGhos...9vEX

Attacker 9: 4ZKkGZ...5oQ3

Ethereum wallet holding remaining funds (July 20): 0x509B...6d3e

Sources

CertiK: DeFiTuna Incident Analysis

BlockSec: Weekly Web3 Security Incident Roundup (BarnBridge, DeFiTuna)

DeFiTuna on X: July 16 exploit statement

DeFiTuna on X: August 20 update on the USDC lending pool deficit

CertiK Alert on X: DeFiTuna Exploit Alert

More from Olympix:

No items found.

Ready to Shift Security Assurance In-House? Talk to Our Security Experts Today.