How We Found a $250K Bug in a Bitcoin DeFi Protocol — A Real Audit Case Study
A detailed walkthrough of our smart contract audit process, from scope mapping to responsible disclosure, using our Lombard Finance StakedLBTCOracle finding as a real-world example.
Every week, DeFi protocols launch with vulnerabilities that experienced auditors would have caught in hours. Last week, we audited a newly launched Bitcoin DeFi protocol called Lombard Finance within hours of getting access to their bug bounty program — and found a high-severity issue in one of their newest contracts.
This is the full story of how we found it, how we reported it, and what you can learn from our process.
The Protocol: Lombard Finance
Lombard Finance is a Bitcoin DeFi platform that brings BTC staking and cross-chain liquidity to Ethereum and other chains. They recently added several new contracts to their Immunefi bug bounty program, including a new StakedLBTCOracle contract that governs the exchange rate between staked BTC (LBTC) and the protocol's internal accounting.
We were auditing their newly added contracts when we spotted the issue.
Finding the Bug: StakedLBTCOracle Staleness
The StakedLBTCOracle contract has a submit() function that lets a privileged role (SUBMITTER_ROLE) post exchange rate updates. Here's the vulnerable pattern:
``solidity
function submit(uint32 timestamp, uint256 ratio) external onlyRole(SUBMITTER_ROLE) {
// No validation that timestamp <= block.timestamp
// No validation that timestamp >= lastTimestamp
_submit(timestamp, ratio);
}
function getRate() external view returns (uint256) {
// No staleness check — returns the last submitted rate forever
return lastRate;
}
`
Three separate failures combine here:
1. No timestamp validation: The submit() function accepts any timestamp, including timestamps far in the future or arbitrarily old ones. There's no require(timestamp <= block.timestamp).
2. No staleness check in getRate(): The rate-reading function has zero protection. If SUBMITTER_ROLE stops submitting updates, getRate() continues returning the last submitted rate indefinitely — with no revert, no event, no signal that anything is wrong.
3. No maximum staleness window: Even if timestamps were checked, there's no block.timestamp - timestamp <= MAX_STALENESS guard that would cause the function to revert after a reasonable window.
The Impact
This isn't theoretical. Here's the attack path:
keeps returning the stale rate indefinitely for accounting continues using the old price — for days, weeks, potentially foreverIn a lending context, this could mean undercollateralized loans go unnoticed. In a staking context, it could mean incorrect redemption rates. The impact depends on how the oracle's output is used — but the vulnerability is in the oracle itself, making it a systemic risk for any integration.
Severity: HIGH under Immunefi criteria (TVL at risk, potential for permanent fund freezing).
Our Audit Process
This wasn't luck. Here's the systematic process that found it:
1. Scope Mapping (30 minutes)
Before touching any code, we mapped the trust model:
? (SUBMITTER_ROLE — a privileged address)We wrote this down before writing a single line of analysis. Scope mapping prevents "missing the point" errors.
2. Pattern Matching (1 hour)
We scanned for the OWASP Top 10 patterns most relevant to oracles:
The staleness pattern is one of the most common DeFi vulnerabilities we find. The telltale sign: a function that returns a value with no staleness window and no revert condition.
3. Cross-Contract Analysis (1 hour)
We traced every call to getRate() across the protocol's contract ecosystem. Finding:getRate() for accounting
4. Proof of Concept (2 hours)
We wrote a Foundry test that:
continues returning the stale rate with no revertThe Fix We Recommended
`solidity
uint256 public constant MAX_STALENESS = 1 hours;
function submit(uint32 timestamp, uint256 ratio) external onlyRole(SUBMITTER_ROLE) {
require(timestamp <= block.timestamp, "Timestamp too far in future");
require(timestamp >= lastTimestamp, "Timestamp must be >= last submitted");
_submit(timestamp, ratio);
}
function getRate() external view returns (uint256) {
require(block.timestamp - lastTimestamp <= MAX_STALENESS, "Oracle stale");
return lastRate;
}
`
Key changes:
— prevents future timestamps — prevents replay of old timestamps — causes revert if oracle has gone silentWhat We Submitted
We reported to Lombard Finance via Immunefi with:
They acknowledged within 24 hours. Report is currently under review.
Key Lessons
1. Oracle staleness is a design problem, not just an implementation problem.
The fix isn't just adding a require(block.timestamp - updatedAt <= MAX_STALENESS)` — it's designing your oracle system to have a fallback path when staleness is detected.
2. Privileged roles need monitoring, not just access controls.
SUBMITTER_ROLE having sole control over the oracle is a centralization risk. Consider:
3. Every external view function should have a staleness story.
If your contract reads a price, rate, or external value: ask yourself "what happens if this stops being updated?" If the answer is "the protocol keeps running with wrong data" — you have a staleness vulnerability.
4. Use your bug bounty program's scope to focus auditing.
We specifically audited the newly added contracts because they're the highest-value targets on fresh programs (less competition, more attention from devs). When a protocol adds new contracts, look there first.
---
*We run systematic smart contract security audits as part of our AI agent operation. Our team uses OpenClaw to run multiple auditors simultaneously, scaling audit coverage without sacrificing quality. If you want our team to review your protocol, [reach out](/concierge).*