Signature verification bypass without a forged signature
Three protocols lost at least $28 million in July 2026 to the same failure, and nobody forged a signature or collided a hash anywhere along the way. The cryptography worked exactly as designed in all three cases.
Each verifier answered a narrow question correctly, and none of them answered the question that actually mattered.
Bottom line: A signature check proves a message is authentic. It does not prove the message is true. Bonzo Finance, Ostium and VerusCoin each lost funds to a verifier that answered its own question correctly and was never asked the right one.
Search for signature verification bugs and you find malleability, replay, and missing checks. Every one describes a signature somebody forged or reused. This class has no name in that literature, because nobody forged anything.
What happened in these three exploits?
Every protocol ran a check that passed honestly on input that should never have reached it. Supra's verifier accepted a degenerate BLS value. Ostium's price feed accepted a signed price that physics rules out. VerusCoin's bridge accepted a receipt whose hash matched, while nobody checked whether value stood behind it.
| Protocol | The question it asked | Answer | The question that mattered |
|---|---|---|---|
| Bonzo / Supra | Does this BLS pairing hold? | Yes | Are these inputs degenerate? |
| Ostium | Did an authorized signer sign this? | Yes | Is this price physically possible? |
| VerusCoin | Does the hash match the payload? | Yes | Does real value back this receipt? |
The shape repeats across three cryptographic primitives, three chains, and three unrelated teams.
Flow diagram: a signature enters a check labelled "authentic?", which answers yes and releases the payload. An annotation reads: nothing checked whether the payload was true.
How did a zero public key pass a BLS check?
A zero signature paired against a zero public key turns both sides of the pairing equation into the identity element, so the check returns true. On 11 July 2026 an attacker drained $9.05 million from Bonzo Lend on Hedera by arranging exactly that condition inside Supra's oracle verifier.
Flow diagram: sig = 0 paired with pubkey = 0 yields pairing == true. An annotation reads: both sides became the identity element.
Supra's requireHashVerified_V2 looked up a public key by committee ID, and the attacker referenced committee ID 2, which sat outside the range Supra had populated. That lookup returned an all-zero key rather than reverting, according to QuillAudits and rekt.news.
Bonzo's own incident report does not say where the zero key came from, so treat the lookup mechanism as reported rather than confirmed.
The attacker then supplied a zero signature, and the pairing check passed without complaint. A $3 position of 250 SAUCE tokens became 6.63 million USDC and 34.5 million WHBAR in eight seconds.
Bonzo's incident report puts the price inflation at roughly twelve orders of magnitude. SAUCE traded near 0.2 HBAR at the time, and the submitted price was the integer 1 followed by thirty zeroes. A bound of almost any kind would have caught that, which makes its absence the whole story.
Supra shipped range validation, identity-element rejection and curve validation afterwards, and any one of the three would have stopped the attack.
The BLS specification already required two of them. The IETF draft makes KeyValidate REQUIRED, and its third step reads "If xP is the identity element, return INVALID". CoreVerify separately mandates a signature subgroup check.
The spec states the reason plainly. The identity public key corresponds to a secret key of zero, which makes the identity point "the unique valid signature for every message under this key". That verifier ran unpatched for two years.
How did a valid signature deliver a fake price?
Ostium checked who signed the price and never checked the price itself. On 15 July 2026 an attacker moved roughly $11.86 million out in the primary transaction, and published estimates of the total loss still range from $12 million to the high teens.
The PrivatePriceUpKeep contract recovered each signer with ecrecover and matched the result against a registry of authorized addresses. That check worked exactly as intended, and nobody bypassed it at any point.
No code asked whether the price could be real. Bitcoin opened one transaction at $5,000 and closed the same transaction at $60,000. A twelvefold move inside a single block passed without comment.
The attacker looped five times through executeBatch, opening a leveraged position at the fake low and closing it at the fake high.
Nobody has settled how the attacker came to control an accepted signer. Reporting points variously at a compromised key, a maliciously registered forwarder, or a gap in report checking. A price bound would have rejected the report in every one of those cases, which is why the authorization question matters less here than it first appears.
Zellic flagged the risk in November 2025, warning that "by design, forwarders can cancel any order or action." Ostium patched the concrete example the audit named and left the architecture where it was.
How did a bridge pay out on nothing?
VerusCoin's bridge proved the receipt was real and never proved that anyone had funded it. On 23 July 2026 an attacker took $7.54 million in ETH, tBTC, USDC and four other assets, using proofs that verified correctly at every step of the path.
checkExportAndTransfers confirmed that the transfer hash matched the export it belonged to, and that check passed for the right reasons because the payload genuinely agreed with itself.
No code confirmed that value sat behind the claim. The bridge never validated totalamounts, totalfees, totalburned, or whether the prior export carried enough value to cover the claim.
A legitimate 0.01 VRSC transfer satisfied the linkage check, and a hand-crafted export carrying eight transfers payable to the attacker drew a payout against entirely valid proofs.
Why did all three patches fail?
Each patch repaired the instance somebody had reported and left the underlying invariant unstated. Supra's verifier ran two years before anyone tested a degenerate input, and Ostium fixed the single abuse its auditors had named while leaving the architecture intact.
VerusCoin is the clearest case, and it needs stating precisely. SlowMist separates the two attacks: May decoupled the validated proof from the executed payload, while July hash-bound the transfers and skipped their economic backing.
Both attacks exploit flawed cross-chain import validation, and that shared class is the point. The May patch closed the path that May had used.
Invariants written after an incident tend to encode what the team already believed rather than what the system requires. The next attacker walked through the other door onto the same import logic.
Naming a function is not the same as naming an invariant. The first closes one route, and the second closes the failure itself.
What should you check when you review a verifier?
Start with the checks the literature already prescribes, because all three protocols failed one. The cryptographic call is almost never where the bug lives, so read past it and look at what the surrounding code does with the answer.
For BLS, KeyValidate and the signature subgroup check are REQUIRED by the specification, and rejecting the identity element is step three of the first one.
For ECDSA, the list is older than most protocols using it. Reject address(0) from ecrecover, constrain s to the lower half order against malleability, and bind a nonce, chain ID, deadline and verifying contract into the signed digest. Use a reviewed library rather than raw ecrecover.
For price feeds, audit guidance is equally explicit: bound accepted values to a plausible range, trip a circuit breaker on abnormal deviation, and check staleness on every read.
Each of those checks would have prevented one of these exploits on its own.
What the standard lists do not cover, and these three add:
- ›Does internal consistency stand in for external validity? A payload that agrees with its own hash agrees with itself, and says nothing about whether the value it claims exists anywhere. VerusCoin is the case no checklist catches.
- ›What can a trusted role do once compromised? Zellic asked this about Ostium's forwarders eight months early, which put the answer in scope while the fix stayed out of it.
- ›Does the implementation perform the validation its own specification requires? Not whether the cryptography is sound, but whether the mandated pre-checks are present at all.
They read as obvious in hindsight because they genuinely are. In review they slip past, because the verification code sits right in front of you and it is correct.
Frequently asked questions
What is a degenerate input attack?
A degenerate input is technically valid and mathematically meaningless, such as a zero public key or an identity element in an elliptic curve group. Cryptographic operations on degenerate inputs often succeed trivially, so a verifier that fails to reject them can be made to return true without any forgery at all.
Why did a zero signature pass BLS verification?
A zero signature paired with a zero public key makes both sides of the pairing equation the identity element, so the equation balances and the check returns true. The precompile computes this correctly, which puts the fix in the calling contract rather than in the cryptography.
Is this the same as signature malleability or replay?
No, because malleability and replay both reuse or alter a genuine signature. Nobody forged or altered anything here, and verification simply answered a narrower question than the surrounding code assumed.
How do you prevent this class of bug?
Run the validation your specification already mandates, and bound the content separately from the signature over it. Then write the property down as an invariant, such as "every payout is backed by conserved value", and fuzz it.
Why do these patches keep failing?
They fix the function somebody reported instead of the assumption that turned out to be wrong. VerusCoin lost funds in May 2026 and again in July, and SlowMist notes that the two attacks differed in mechanism while both exploited flawed cross-chain import validation.
All three teams read their verification code and found it correct, because it was. What would have made any of them look at the line after it?
