A lending protocol can have correctly implemented collateral and borrowing logic and still suffer a major loss when the price-verification layer accepts invalid data.
The July 2026 Bonzo Lend incident on Hedera illustrates this problem perfectly. An attacker submitted a manipulated price for the SAUCE token through a third-party pull-oracle verifier. The false price made a small collateral deposit appear valuable enough to support millions of dollars in borrowing.
The Vulnerability
According to published incident reports, the verifier failed to reject an identity element represented by zero-valued curve-point coordinates. Both the retrieved public key and the submitted signature resolved to this invalid value, causing the cryptographic verification operation to return successfully.
The Impact
Within seconds of the price update, the attacker used just 250 SAUCE as collateral to borrow approximately:
6.63 million USDC
34.5 million wrapped HBAR
Note: Note: For a detailed timeline, transaction addresses, asset movements, and impact calculations, see the full Bonzo Lend oracle exploit news on Cryip.
This post focuses specifically on what developers can learn from the failure and how similar oracle risks can be mitigated at the application layer.
1. Cryptographic Verification Must Reject Invalid Inputs Explicitly
A successful call to a cryptographic primitive does not automatically prove that every supplied value was valid.
Some signature schemes contain special values known as identity elements. Depending on the implementation, verifying an identity signature against an identity public key may produce a mathematically valid result even though neither value represents an authorized signer.
Applications should therefore validate the structure and domain of all cryptographic inputs before calling the verification function.
`// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
library OracleInputValidator {
error IdentityPublicKey();
error IdentitySignature();
error EmptyMessage();
error InvalidPrice();
struct G1Point {
uint256 x;
uint256 y;
}
struct G2Point {
uint256[2] x;
uint256[2] y;
}
function validateUpdate(
bytes memory message,
uint256 price,
G1Point memory signature,
G2Point memory publicKey
) internal pure {
if (message.length == 0) revert EmptyMessage();
if (price == 0) revert InvalidPrice();
// Simplified identity-element checks.
if (signature.x == 0 && signature.y == 0) {
revert IdentitySignature();
}
if (
publicKey.x[0] == 0 &&
publicKey.x[1] == 0 &&
publicKey.y[0] == 0 &&
publicKey.y[1] == 0
) {
revert IdentityPublicKey();
}
}
}`
Production Checklist for Input Validation
In production, developers should explicitly perform:
Curve membership checks
Subgroup checks
Canonical encoding validation
Public-key registration checks
Domain separation
Committee membership validation
Threshold and signer-count validation
Core Principle: Malformed or special-case inputs must fail before pairing or signature verification is ever attempted.
2. Do Not Treat Oracle Validity as Price Validity
A valid signature only proves that a recognized signing process authorized a message. It does not prove that the price is economically reasonable. A lending protocol should independently verify that a new value falls within acceptable operating limits.
`library PriceGuard {
error StalePrice();
error FutureTimestamp();
error ExcessiveDeviation();
error InvalidReferencePrice();
uint256 internal constant BPS = 10_000;
function validatePrice(
uint256 newPrice,
uint256 referencePrice,
uint256 updatedAt,
uint256 maxAge,
uint256 maxDeviationBps
) internal view {
if (referencePrice == 0) revert InvalidReferencePrice();
if (updatedAt > block.timestamp) revert FutureTimestamp();
if (block.timestamp - updatedAt > maxAge) revert StalePrice();
uint256 difference = newPrice > referencePrice
? newPrice - referencePrice
: referencePrice - newPrice;
uint256 deviationBps = (difference * BPS) / referencePrice;
if (deviationBps > maxDeviationBps) {
revert ExcessiveDeviation();
}
}
}`
Example Implementation: A protocol could pause collateral operations when a new price differs from the previous accepted price by more than 20%.
Asset Tailoring: The correct limit depends heavily on the asset. A volatile or thinly traded token may require tighter borrowing parameters rather than a wider deviation allowance.
3. Compare Independent Price Sources
Oracle redundancy is useful only when sources are operationally independent. Two feeds built from the same exchange data, signer infrastructure, or validation contract will fail together.
A basic median calculation can reduce dependence on a single value:
`library MedianOracle {
error InvalidSourceCount();
error InvalidSourcePrice();
function medianOfThree(
uint256 a,
uint256 b,
uint256 c
) internal pure returns (uint256) {
if (a == 0 || b == 0 || c == 0) {
revert InvalidSourcePrice();
}
if (a > b) (a, b) = (b, a);
if (b > c) (b, c) = (c, b);
if (a > b) (a, b) = (b, a);
return b;
}
}`
A Stronger Architecture Compares:
A signed pull-oracle price.
A time-weighted average price (TWAP) from a sufficiently liquid DEX.
A second, independently operated oracle network.
When the sources disagree beyond a configured threshold, the protocol should reject new borrowing rather than automatically selecting the highest or most recent value.
4. Apply a Delay to Extreme Price Changes
The Bonzo incident shows why an accepted price should not always become immediately usable as collateral. A protocol can place large price movements into a temporary pending state:
`contract DelayedPriceActivation {
struct PendingPrice {
uint256 value;
uint256 executableAt;
}
uint256 public activePrice;
uint256 public constant ACTIVATION_DELAY = 15 minutes;
PendingPrice public pendingPrice;
event PriceQueued(uint256 value, uint256 executableAt);
event PriceActivated(uint256 value);
function queuePrice(uint256 newPrice) external {
require(newPrice > 0, "invalid price");
pendingPrice = PendingPrice({
value: newPrice,
executableAt: block.timestamp + ACTIVATION_DELAY
});
emit PriceQueued(newPrice, pendingPrice.executableAt);
}
function activatePrice() external {
require(pendingPrice.value > 0, "no pending price");
require(
block.timestamp >= pendingPrice.executableAt,
"activation delay"
);
activePrice = pendingPrice.value;
delete pendingPrice;
emit PriceActivated(activePrice);
}
}`
A universal delay across all assets reduces protocol usability. Instead, trigger delayed activation only when:
- The deviation exceeds a defined percentage.
- Market liquidity falls below a specific threshold.
- The asset has recently been listed.
- Oracle sources disagree.
The signer committee or oracle configuration changes.
5. Limit Exposure Per Collateral Asset
Even a perfectly designed oracle can fail due to implementation bugs, compromised signers, or incorrect configuration. Borrow caps restrict how much damage a single compromised feed can inflict.
`contract AssetBorrowCap {
mapping(address => uint256) public borrowCaps;
mapping(address => uint256) public totalBorrowed;
error BorrowCapExceeded();
function _recordBorrow(
address collateralAsset,
uint256 borrowValue
) internal {
uint256 newTotal = totalBorrowed[collateralAsset] + borrowValue;
if (newTotal > borrowCaps[collateralAsset]) {
revert BorrowCapExceeded();
}
totalBorrowed[collateralAsset] = newTotal;
}
}`
Factors for Setting Caps
Caps should reflect more than just the protocol’s available liquidity. Essential parameters include:
- DEX liquidity & daily trading volume
- Market depth & token concentration
- Oracle update frequency & exit liquidity
- Historical volatility
- Bridge and redemption capacity
Allowing a low-liquidity token to support borrowing equal to the majority of the protocol’s stablecoin liquidity creates catastrophic asymmetric risk.
6. Rate-Limit Collateral-Powered Borrowing
While borrow caps limit total exposure, rate limits restrict how quickly exposure can grow, purchasing valuable time for response teams.
`contract BorrowRateLimiter {
struct Window {
uint256 startedAt;
uint256 borrowed;
}
uint256 public constant WINDOW_LENGTH = 10 minutes;
mapping(address => Window) public windows;
mapping(address => uint256) public windowLimits;
error RateLimitExceeded();
function _consumeLimit(
address collateralAsset,
uint256 amount
) internal {
Window storage window = windows[collateralAsset];
if (block.timestamp >= window.startedAt + WINDOW_LENGTH) {
window.startedAt = block.timestamp;
window.borrowed = 0;
}
if (window.borrowed + amount > windowLimits[collateralAsset]) {
revert RateLimitExceeded();
}
window.borrowed += amount;
}
}`
Rate limits create vital buffer space for monitoring systems, guardians, or automated circuit breakers to respond before the entire lending pool can be drained.
7. Test Negative Cryptographic Cases
Security tests often confirm that valid messages work and ordinary invalid signatures fail. However, they frequently miss identity elements, malformed points, or unusual encodings.
A standard Foundry test should explicitly verify that zero-valued keys and signatures are systematically rejected:
`// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test} from "forge-std/Test.sol";
import {OracleInputValidator} from "../src/OracleInputValidator.sol";
contract OracleInputValidatorTest is Test {
using OracleInputValidator for bytes;
function test_RevertWhenSignatureIsIdentityElement() public {
bytes memory message = abi.encode("SAUCE/HBAR", 0.2 ether);
OracleInputValidator.G1Point memory signature =
OracleInputValidator.G1Point({ x: 0, y: 0 });
OracleInputValidator.G2Point memory publicKey =
OracleInputValidator.G2Point({
x: [uint256(1), uint256(2)],
y: [uint256(3), uint256(4)]
});
vm.expectRevert(OracleInputValidator.IdentitySignature.selector);
OracleInputValidator.validateUpdate(message, 0.2 ether, signature, publicKey);
}
function test_RevertWhenPublicKeyIsIdentityElement() public {
bytes memory message = abi.encode("SAUCE/HBAR", 0.2 ether);
OracleInputValidator.G1Point memory signature =
OracleInputValidator.G1Point({ x: 1, y: 2 });
OracleInputValidator.G2Point memory publicKey =
OracleInputValidator.G2Point({
x: [uint256(0), uint256(0)],
y: [uint256(0), uint256(0)]
});
vm.expectRevert(OracleInputValidator.IdentityPublicKey.selector);
OracleInputValidator.validateUpdate(message, 0.2 ether, signature, publicKey);
}
}function invariant_ZeroSignatureNeverAccepted() public {
Recommended Invariant Tests
// Assert that every update containing an identity signature reverts.
}
function invariant_PriceDeviationCannotBypassGuard() public {
// Assert that prices outside the configured deviation range cannot be used.
}
function invariant_BorrowingNeverExceedsAssetCap() public {
// Assert that total exposure associated with an asset remains below its cap.
}`
8. Monitor Business Invariants, Not Only Contract Errors
An exploiting transaction may execute with zero technical errors or contract reverts. Monitoring layers must detect economically abnormal behavior, such as:
- Price deviation exceeding configured thresholds.Collateral value increasing by more than $X\%$ in a single block/update.Borrowed amount exceeding $Y\%$ of available liquidity.A new address borrowing up to asset-specific limits instantly.Unexpected oracle committee adjustments or public key resets.An oracle public key resolving to an empty or identity value.Cross-chain token bridging immediately following massive, sudden borrows. An alert is only as good as its response mechanism. Alerts should directly interface with automated circuits to freeze affected actions.
9. Design the Pause Mechanism Before an Incident
A crude global pause can harm healthy users by preventing them from repaying loans or avoiding liquidations during high volatility. Granular emergency controls are much safer:
mapping(address => bool) public borrowingPaused;
mapping(address => bool) public liquidationPaused;
mapping(address => bool) public collateralDepositPaused;
mapping(address => bool) public withdrawalPaused;
During an active oracle exploit, a protocol must have the modular precision to:
- Pause new borrowing against the affected asset.
- Pause liquidations relying on the disputed price feed.
- Pause new deposits of the affected collateral.
Simultaneously, the architecture should preserve:
- Debt repayments.
- Additional collateral safety padding.
- Withdrawals of completely unaffected assets.
Final Takeaway
The central lesson from the Bonzo Lend incident is not simply that oracle manipulation is dangerous. The deeper lesson is that security boundaries must overlap.
An oracle provider must safely validate cryptographic messages. A lending protocol must assume that even a successfully signed price can be incorrect. Risk controls must assume that both the oracle and application validation layers can fail simultaneously.
Signature validation, price sanity checks, independent feeds, borrow caps, rate limits, and granular circuit breakers should work as distinct, isolated layers. No single check should ever be capable of turning a collateral token worth a few dollars into millions of dollars in immediately borrowable value.
Top comments (0)