October 20, 2026
|
Exploit Postmortem
Cover image for "Bonzo Lend's $9.05M Loss and How Olympix Would Have Prevented It"

Bonzo Lend's $9.05M Loss and How Olympix Would Have Prevented It

Date: July 11, 2026

Chain: Hedera

Loss: about $9.05M in borrowed principal, 6.63M USDC and 34.52M WHBAR

Class: oracle signature verifier that accepted a zero signature against an unset committee key

At 00:39:53 UTC on July 11, 2026, a Hedera account deposited 250 SAUCE into Bonzo Lend, a few dollars of collateral. Twelve minutes later the same account sent a price update to Supra's oracle contract claiming that one SAUCE was worth a 1 followed by thirty zeroes, about twelve orders of magnitude above its real price of roughly 0.2 HBAR. Supra's verifier accepted it. Eight seconds after that, the account borrowed 6,634,528 USDC from Bonzo Lend, and ten seconds later it borrowed 34,518,389 WHBAR.

Bonzo's own incident report puts the loss at about $9.05M in borrowed principal. Bonzo Lend's contracts did exactly what they were written to do with the price they were given. The failure sat one layer upstream, in a Solidity verifier that Supra runs on Hedera, which checked a BLS signature by asking a pairing precompile whether an equation held and never checked that the signature and the public key were real points to begin with. A signature of zero against a key of zero satisfies that equation every time.

Hedera's Largest Lending Market and the Feed It Trusted

Bonzo Lend was the largest lending protocol on Hedera, and according to Bonzo's recovery update it was the only provider of on-chain credit markets in that ecosystem. Users supplied assets such as USDC, WHBAR and SAUCE, SaucerSwap's token, and borrowed against them. Like most lending markets, Bonzo decides how much a deposit can borrow by multiplying its amount by a price and a loan-to-value ratio.

Those prices come from oracles. Bonzo's incident report explains that Bonzo used both Chainlink and Supra on Hedera, with most ecosystem asset prices powered by Supra. According to Rekt, Chainlink covered HBAR and USDC, while SAUCE and WHBAR relied on Supra alone. Bonzo's SupraOracle adapter, a contract Bonzo owns, reads the latest value Supra has stored for a pair and hands it to the LendingPool. Bonzo submits nothing to the feed and asks for nothing on demand; it reads what's there.

Supra's side has three pieces on Hedera. A storage contract holds the latest price for each pair. A pull-oracle contract, 0.0.4323024, accepts price updates from anyone, because anyone is allowed to deliver a signed update. And a verifier, 0.0.4323006, decides whether an update really carries a signature from Supra's oracle committee before the pull contract writes it into storage. That verifier is the only thing standing between "anyone can submit" and "only the committee can set prices", so every lending position priced by Supra on Hedera depended on it.

How a Zero Passed for a Signature

Supra's committees sign price data with BLS signatures over the BN254 curve. A BLS signature is a point on a curve, and checking it means asking whether two pairings of curve points are equal, one built from the signature and one built from the signer's public key and the message. EVM chains, Hedera included, expose a pairing precompile at address 8 that answers exactly that question: given a list of point pairs, does the product of their pairings equal one.

The verifier behind the proxy at the time of the exploit was SupraSValueFeedVerifier, at 0.0.10414935. From its verified source on Sourcify:

    function requireHashVerified_V2(bytes32 _message, uint256[2] calldata _signature, uint256 committee_id)
        public
        view
    {
        bool callSuccess;
        bool checkSuccess;
        (checkSuccess, callSuccess) = BLS.verifySingle(
            _signature,
            committee_public_key[committee_id],
            BLS.hashToPoint(domain, abi.encode(_message)),
            blsPrecompileGasCost
        );
        if (!callSuccess) {
            revert BLSInvalidPublicKeyorSignaturePoints();
        }
        if (!checkSuccess) {
            revert BLSIncorrectInputMessaage();
        }
    }

The function takes the message, a two-number signature, and a committee ID. It looks up the committee's public key with committee_public_key[committee_id], hashes the message to a curve point, and passes all three to BLS.verifySingle. If the precompile call fails, it reverts, and if the pairing check comes back false, it reverts. Everything else passes.

The lookup is where it starts to go wrong. committee_public_key is a Solidity mapping, and reading a key that was never set returns zeros instead of failing. Nothing in the function checks that committee_id refers to a committee Supra actually registered, so an unused ID quietly produces a public key of four zeros. The same contract's admin function for setting keys, updatePublicKey, even carries the developer comment "WARN: The validity of the public key is not verified".

The second half lives in the BLS library the verifier imports. From the same verified source:

        bool callSuccess;
        // solium-disable-next-line security/no-inline-assembly
        assembly {
            callSuccess := staticcall(precompileGasCost, 8, input, 384, out, 0x20)
        }
        if (!callSuccess) {
            return (false, false);
        }
        return (out[0] != 0, true);

verifySingle packs the signature, a fixed curve constant, the hashed message and the public key into a 384-byte input, calls precompile 8, and returns whatever the precompile says. There's no check before the call that the signature or the public key is anything other than zero. In this encoding, all zeros is the point at infinity, the identity element of the curve group. Pair the identity with anything and you get one, so with a zero signature and a zero public key both pairings equal one, their product equals one, and the precompile returns true, exactly as EIP-197 specifies.

Bonzo's report puts it well: the precompile answered the narrow question it was handed correctly, and the verifier treated that answer as proof of a committee signature it never was. For builders, the teaching moment is the gap between a cryptographic primitive and an authorization check. A primitive tells you whether an equation holds for the inputs you gave it, and it's the caller's job to make sure those inputs are ones only the real signer could have produced. Here that meant rejecting unregistered committee IDs and rejecting the identity point before ever calling the precompile.

Eight Seconds on July 11

The full timeline in Bonzo's report, cross-checked against Hedera's mirror node, runs like this. At 00:39:53 UTC, Wallet A, Hedera account 0.0.10633526 with EVM alias 0x9a49...a494, deposited 250 SAUCE into Bonzo Lend. At 00:40:00 it submitted a first, normal-valued price update to Supra's pull contract, which Bonzo describes as consistent with reconnaissance.

At 00:51:39.646 UTC, the account submitted the real attack, Hedera transaction 0.0.995584-1783731093-686041919, an Ethereum-style transaction to the pull contract that the mirror node records as successful. Bonzo decoded its contents: a price for pair 425, SAUCE against WHBAR, set to a 1 followed by thirty zeroes; a reference to committee ID 2, which QuillAudits and Rekt both describe as outside the range Supra had populated; a message root of 0xd4e6...3f7f; and a signature of [0,0]. BlockSec's analysis traces the path the update took through the pull contract's verifyOracleProofV2(), its requireRootVerified() check on the root, and the verifier, before packData() wrote the price into storage. The internal trace, according to Bonzo, shows the pairing precompile at 0.0.8 returning true.

The SAUCE price that Bonzo's adapter read was now absurd, and the 250 SAUCE behind Wallet A's account looked like collateral worth far more than anything in the pool. At 00:51:47 UTC the account borrowed 6,634,528.202695 USDC, and at 00:51:57 UTC it borrowed 34,518,389.36109841 WHBAR. Valued the way Bonzo's report values them, with USDC at $1.00 and HBAR at $0.06998, the two borrows come to $6,634,528 plus $2,415,597, or about $9.05M. No flash loan was involved; the attack needed two transactions, one to write the price and one to borrow against it.

Between about 01:11 and 01:36 UTC, a second account, Wallet B, 0.0.683607, deposited SAUCE and borrowed further assets while the bad price was still live. At 01:36 UTC Supra's next scheduled publication overwrote the SAUCE price with a normal value of about 0.1964 HBAR. Bonzo paused the LendingPool at 01:41 UTC, about 49 minutes after the first abnormal transaction, and paused Bonzo Points at 05:50 UTC.

Following the Money

Wallet A didn't leave the borrowed funds on Hedera. According to Rekt, the funds left through LayerZero's Stargate bridge to Arbitrum, Base and Ethereum, were swapped into ETH along the way, and at one point the receiving wallet on Ethereum, 0xaf20...d93e, held roughly 2,360 ETH and 15.58 WBTC, about $5.25M. Rekt cites PeckShield as finding that this wallet was seeded with 1 ETH from Tornado Cash about ten hours before the exploit, and QuillAudits reports that the funds ultimately went back into Tornado Cash. As of October 20, 2026, Blockscout shows that Ethereum address holding about 0.09 ETH.

Supra offered the attacker a deal, according to Rekt: keep $100,000, return the rest, and face no charges, with an escalating bounty on the attacker's identity if they refused within 72 hours. Rekt reported no funds recovered and the attacker unnamed as of July 14.

Wallet B went the other way. It contacted Bonzo in Discord right after the incident, identified itself as a white-hat responder, and said it would return what it had borrowed, about $1.0M in principal by Bonzo's estimate. Bonzo kept that amount out of the headline loss. Its August 18 recovery update reports that the white-hat account completed four settlement repayments on August 6 between 14:56 and 15:05 UTC, and that the repayments and reconciliation were complete.

Response, Repair and Recovery

According to Rekt, Bonzo's first public posts after the pause cited volatile markets without naming a cause. At 09:49 UTC on July 11 it published a detailed incident report that named the oracle verifier as the cause, laid out the timeline, and listed every account and contract involved. Hedera stated that its consensus and core network services weren't compromised, and Rekt notes that Hedera named Supra publicly a day before Supra commented.

Supra moved fast on the fix. Hedera's mirror node shows a new SupraSValueFeedVerifier implementation, 0.0.10633768, created at 07:05:59 UTC on July 11, and an Upgraded event pointing the verifier proxy at it at 07:08:24 UTC, a little over six hours after the exploit. According to Rekt, Supra's post-mortem describes three added checks, range validation on the committee lookup, rejection of identity-element keys and signatures, and curve validation before the pairing check, and says the identity-element check alone would have stopped the attack.

On July 16, Bonzo announced that the Bonzo Finance Foundation would fund the recovery of all affected user positions as they stood before the incident, backed by a recovery facility committed by the Hedera Foundation. The August 18 recovery update lists Stage A, cutting interest rates to about 0% across all 14 lending pools, as completed on July 15, and Stage B, the white-hat repayment, as completed on August 6. Stage C, a voluntary repayment window followed by user claims through a redemption contract, was still in progress, pending Halborn's final review of the public repayment contract fixes. As of October 20, 2026, Bonzo's most recent public recovery post was that August update, and on October 16 the team was pointing users to its Discord for further recovery news.

A Dependency Outside Every Scope

The verifier had been running on Hedera for years. Hedera's mirror node shows the verifier proxy was created on December 19, 2023, and the implementation behind it at the time of the exploit was deployed on March 31, 2026. Supra's CEO Joshua Tobkin said the flawed verifier had been live and unpatched for two years, according to Rekt.

Each team's review covered what each team owned. Rekt reports that Halborn audited Bonzo's lending contracts, liquidity incentives, staking module, LayerZero bridge connector and vaults, and that none of those reports identified the cause, which makes sense because the cause wasn't in any of them. The only Supra audit Rekt could find was a MoveBit report scoped to Supra's Aptos contracts written in Move, which didn't cover the Solidity verifier on Hedera. Bonzo's bug bounty listed third-party contracts as out of scope. Audits stay necessary, and each of these did its job within its scope; this exploit lived in the space between those scopes, in a contract one protocol depended on completely and another team owned.

How Olympix Turns the Verifier's Missing Check Into a Counterexample

The flaw was in Supra's verifier, so it's in the verifier's code that the engine finds it; a scan limited to Bonzo's own contracts reads a price from an adapter and has no view of how that price was authenticated. Olympix reads a contract the way an attacker does, as states, transitions and entry points, and takes in the context around it to infer the rules it relies on. For SupraSValueFeedVerifier and the pull contract that calls it, the state that matters is the stored price for each pair, and the transitions are the entry points that can write it, every one of which runs through the signature check.

The signal in the quoted code is specific: an authorization decision that rests on the boolean returned by a cryptographic precompile, fed by a public key read from a mapping at an index the caller chooses, with no check that the index is registered and no check that the key or the signature is a valid non-identity point. A contract's state space looks infinite, but it isn't. The engine maps every path into the price storage, keeps the ones that are feasible for an arbitrary caller of the public pull contract, and narrows to the ones where the signature check is the only thing between the caller's input and the stored price. That leaves a short list, and the zero-key path is on it.

The invariant is one sentence: a price can enter storage only if it carries a valid signature from a committee Supra has registered. The engine infers that rule from the verifier's own purpose and from functions like updatePublicKey that show how committees are meant to come into existence, then executes paths against it until one breaks it, and Olympix reports the break as a counterexample.

For this verifier, the counterexample would contain the July 11 sequence in its smallest form: a call into the pull contract with an arbitrary price for any pair, a committee ID that was never registered, and a signature of [0,0], followed by the stored price for that pair changing to the caller's value. The minimal conditions are just two, an unregistered committee ID and the identity point as the signature, with no collateral, timing or market state required. Olympix packages that sequence with a generated Forge test that replays it, so the team sees the forged price land in storage in a local test run rather than in Bonzo's LendingPool.

That finding belongs at the merge that introduced requireHashVerified_V2 with per-committee keys, and Olympix runs at every merge and before release, so the same invariant would have been checked again when the March 2026 implementation went in. For a protocol like Bonzo, the takeaway is about where the system boundary sits: when a verifier decides what every collateral price is, that verifier's code is part of the system whose invariants need testing, whoever wrote it.

Check the Inputs Before Trusting the Math

The pairing precompile on Hedera did nothing wrong, and neither did Bonzo's LendingPool; $9.05M left because a verifier treated "this equation holds" as "the committee signed this" without first making sure the committee and the signature were real. For builders who verify signatures on-chain, the inputs to the math need their own checks, and for builders who consume someone else's verified data, that verifier belongs inside the scope you test. To see how Olympix maps the paths from a public entry point to the state your protocol trusts most, book a technical walkthrough.

On-Chain References

Protocol: Bonzo Lend, via Supra's on-chain oracle verifier

Chain: Hedera

Date: July 11, 2026, 00:51:39 UTC (manipulated price update)

Loss: about $9.05M (6,634,528.20 USDC and 34,518,389.36 WHBAR borrowed by Wallet A)

Attacker (Wallet A): 0.0.10633526

Attacker EVM alias: 0x9a49...a494

Attacker post-bridge wallet (Ethereum): 0xaf20...d93e

White-hat responder (Wallet B): 0.0.683607

Supra pull-oracle contract: 0.0.4323024

Supra verifier proxy: 0.0.4323006

Verifier implementation at the time of the exploit: 0.0.10414935

Patched verifier implementation: 0.0.10633768

Bonzo LendingPool proxy: 0.0.7308459

Bonzo SupraOracle adapter: 0.0.7308480

Manipulated price update tx: 0.0.995584-1783731093-686041919

SAUCE collateral deposit: 1783730393.018023002

USDC borrow: 1783731107.330069002

WHBAR borrow: 1783731117.865661002

Sources

Bonzo Finance: Bonzo Lend Incident Report: Oracle Provider Exploit

Bonzo Finance: Recovery and Relaunch: What's Been Done, When, and What's Next

Rekt: Bonzo Finance

QuillAudits: Bonzo Finance Zero-Signature Exploit Analysis

BlockSec: Weekly security review covering the Summer.fi and Bonzo Lend exploits

Sourcify: Verified SupraSValueFeedVerifier source on Hedera

More from Olympix:

No items found.

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