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

Zentra Finance's $140K Loss and How Olympix Would Have Prevented It

Date: September 9, 2026

Chain: Citrea, Bitcoin's ZK rollup

Loss: 140,000 ctUSD and 30 USDC.e, about $140K

Class: a burn safeguard that clamped to zero while the debt it was paying for still cleared

At 12:59:37 UTC on September 9, 2026, a contract on Citrea borrowed 140,000 ctUSD from Zentra Finance and then told the lending pool to repay that loan using zctUSD, Zentra's receipt token for ctUSD deposits. The contract held no zctUSD at all. The pool cleared the debt anyway, the token contract emitted a burn event for 140,000.000001 zctUSD it never destroyed, and the borrower walked away with the ctUSD and its collateral.

The whole drain took one transaction and 200,000 USDC.e of borrowed liquidity that was returned before the transaction ended. It worked because of a safeguard added to Zentra's token code to absorb a one-wei rounding overshoot, which, with nothing to cap it, absorbed an entire repayment instead.

An Aave Fork on Bitcoin's Rollup

Zentra is a money market on Citrea built on Aave architecture. Lenders deposit assets like ctUSD, Citrea's dollar-pegged token, or wcBTC into pools and receive zTokens, yield-bearing receipts that track their share. Borrowers post collateral, borrow against it and pay interest, and liquidators clean up underwater positions.

Zentra's lending core is a fork of Aave V3 Core v3.0.x, and its zTokens don't run the stock AToken implementation. A rounding issue in pre-3.5 Aave V3 had been disclosed upstream in March 2026, and Zentra's post-mortem confirmed that the September 9 transaction used a different boundary in its own deployed token accounting.

Paying Off a Loan With a Card That Has Nothing on It

Aave V3 includes repayWithATokens, which lets a borrower repay debt with the receipt tokens they hold instead of sending the underlying asset back. The pool clears the debt, and the token contract burns the same amount of receipt tokens from the borrower. Those two steps are one ledger entry written in two places, and the design only stays honest if both always happen together.

Zentra's deployed token added a safeguard to its internal _burnScaled function. When a burn asked for more than the account held, the function reduced the burn to the available balance instead of reverting. According to the DeFiHackLabs proof of concept, nothing in the deployed code limited the clamp to a one-wei rounding overshoot, even though public reproductions quote the source's own comment describing that as its only purpose. With a balance of zero, the burn was clamped to zero and quietly did nothing, while Pool.repayWithATokens still recorded the full debt as repaid. The function had no role or modifier, so anyone could call it.

Picture a self-checkout that, when your gift card can't cover the bill, charges whatever is left on the card and prints a receipt marked paid in full. That's fine when the card is short by a cent and disastrous when the card is empty. The idea to take from this case is that a safeguard that clamps instead of reverting has to say exactly how far it's allowed to clamp, because otherwise a paired operation can complete on one side and silently do nothing on the other.

One Transaction, Two Loops

The exploit transaction was sent by 0xA73d...5C17 to an attack contract at 0x8d85...7b4b in Citrea block 12,428,145. Citrea's explorer shows each step. The contract flash-borrowed 200,000 USDC.e from an Algebra ctUSD/USDC.e pool, deposited it into Zentra and received 200,000 zUSDC, then borrowed 140,000 ctUSD against it. It called repayWithATokens on the ctUSD debt for the debt plus one base unit, and the explorer records 140,000.000001 of debt tokens and 140,000.000001 zctUSD as burned, although the contract had never held any zctUSD. With the debt gone, it withdrew its 200,000 USDC.e collateral.

The contract then ran the same trick on the USDC.e side at smaller scale. It deposited 50 ctUSD, borrowed 30 USDC.e, cleared that debt with repayWithATokens while holding effectively no zUSDC, and withdrew its 50 ctUSD. It repaid the flash loan with 200,020 USDC.e, and sent 139,999.999999 ctUSD and 9.999999 USDC.e to 0xA73d...5C17. That's the 140,000 ctUSD and 30 USDC.e Zentra lists as lost, minus the 20 USDC.e flash loan fee.

At 13:12:51 UTC, the attacker swapped the ctUSD for 139,940.792477 USDC.e and sent 139,959.692499 USDC.e out through a bridge. Zentra's operations multisig paused all four reserves, revenue distribution and superstaking at 13:16:27 UTC, and no second exploit landed before the pause.

A Proportional Haircut and a Bounty Deadline

Citrea paused its cross-chain services as a precaution and restored them on September 10 after the root cause was confirmed and the affected contracts were suspended, and Citrea's bridge and protocol weren't affected. Zentra measured the ctUSD reserve shortfall at about 139,961 ctUSD against depositor claims and proposed a proportional adjustment of about 10.19% to zctUSD balances to restore solvency.

On September 11, Zentra said it had traced the funds to a specific wallet, offered a bounty for their return with no legal action if returned under agreed terms, and set a deadline of September 14 at 12:00 UTC, naming 0x0A66...09B8 as the address for returning the funds. As of October 19, the Citrea explorer shows no token transfers into that address. Crypto Times reported that Zentra put the loss at about $143K. The deadline passed with no publicly confirmed response from the attacker, and Zentra's lending markets remain suspended with no recovery reported.

How Olympix Checks That Both Halves of a Repayment Happened

Olympix reads a contract the way an attacker does, as states, transitions and entry points, and takes in the context around the code, here an Aave V3 fork whose token accounting was changed from upstream. From that it infers the invariants the system depends on. For repayWithATokens the invariant is a single sentence: when debt is cleared using receipt tokens, the caller's receipt token balance has to fall by the same amount.

A lending pool's state space looks endless, but the engine maps it and uses signals to pick which paths to attack. Olympix's static analysis surfaces the signal in this case, a burn that clamps to the available balance with no bound on the gap, reachable from a public repay path that updates debt regardless of how much was actually burned. That narrows the search from every path, to the feasible ones, to the few where a caller's receipt balance and the burn amount can disagree.

BugPoCer takes that flagged repay path and tries to make the two halves of the ledger disagree. Its method, Counterexample-Guided Invariant Refutation, starts from the pool as a whole and runs sequences against the repayment rule until one leaves debt cleared with no receipt tokens destroyed, and here the first caller to borrow and then repay with a zero zctUSD balance does exactly that. BugPoCer returns it as a counterexample with the exact state changes, the minimal conditions that trigger it and a generated Forge test that replays it, and adding a collateral withdrawal to the same test turns it into the September 9 drain produced in development.

Olympix has flagged this class before in other codebases, where a guard meant to enforce an accounting rule passed silently in exactly the case it existed to stop. Forks are a common place for this, because a change to one inherited component can look small on its own while it alters what an untouched function elsewhere relies on, and the engine runs in development, at every merge and before release, so that interaction gets tested every time the code changes.

A Clamp Needs a Ceiling

Zentra's clamp was written to forgive one wei and ended up forgiving 140,000 ctUSD, because nothing in the code said where forgiveness stopped and the repay path never checked what the burn had actually done. Teams changing inherited accounting code in a fork should test the new behavior against every path that calls it, especially the ones they didn't touch. To watch Olympix infer the accounting invariants in your own fork and return a runnable counterexample for each one that breaks, book a technical walkthrough.

On-Chain References

Protocol: Zentra Finance

Chain: Citrea (chainId 4114)

Date: September 9, 2026, 12:59:37 UTC

Loss: 140,000 ctUSD and 30 USDC.e

Attacker EOA: 0xA73d...5C17

Attack contract: 0x8d85...7b4b

Zentra's address for returning funds (named in the September 11 notice): 0x0A66...09B8

Exploit tx: 0x9ac5...4aa1

Block: 12,428,145

zctUSD token (Zentra ctUSD reserve): 0xBA2a...cac8

zctUSD implementation at the exploit block: 0x62Ff...A694

zctUSD implementation deployed September 13, 2026 (current): 0x41B7...11F8

Zentra Pool (Aave V3 proxy): 0xfb79...06F5

zUSDC token (Zentra USDC.e reserve): 0x0146...6C0A

ctUSD token: 0x8D82...6d5D

Flash loan source (Algebra ctUSD/USDC.e pool): 0x172D...1165

Swap tx (ctUSD to USDC.e): 0xe438...36c6

Bridge-out tx: 0xa88d...4cec

Sources

Zentra Finance on X: Incident Post-Mortem

DeFiHackLabs PR #1265: ZentraFinance Exploit PoC

Crypto Times: Zentra Finance Reports $143K Exploit Affecting ctUSD Reserve

KuCoin News: Citrea Restores Cross-Chain Functionality for ctUSD and Other Assets

More from Olympix:

No items found.

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