Skip to content

Stale Oracle Price Accepted by Pool Leading to Incorrect Health Factor and Blocked Liquidations #43

Description

@Alwi-Mujahidin

Description

The lending pool relies on price data obtained from an external oracle implementing the SEP-40 interface. However, there is a critical inconsistency in how price freshness is enforced between the oracle and the pool.
The oracle contract (RWAOracle) provides two relevant methods:

  • lastprice() → returns the most recent price without checking staleness
  • lastprice_if_fresh() → returns price only if within max_staleness threshold

While the oracle correctly enforces staleness through lastprice_if_fresh, the pool integration (in Oracles module) does not use this protection. Instead, it directly calls:

oracle_client.lastprice(...)

and applies its own validation:

if oracle_price_data.timestamp + 7 * 24 * 60 * 60 < current_time {
    return Err(Error::InvalidOraclePrice);
}

This creates a mismatch in staleness assumptions:

  • Oracle-defined staleness (configurable via max_staleness, e.g., 5 minutes for volatile assets)
  • Pool-defined staleness (hardcoded to 7 days)

Root Cause

The vulnerability arises because:

  1. The oracle explicitly marks prices as stale using:
lastprice_if_fresh()
  1. The pool bypasses this mechanism entirely and instead:
  • fetches raw price via lastprice()
  • applies a much weaker, hardcoded staleness check (7 days)

As a result:
Prices that are considered invalid (stale) by the oracle are still considered valid by the pool.

Inconsistent Trust Model

This leads to a broken trust boundary:

  • The oracle is designed to be the source of truth for price validity
  • The pool overrides this logic with its own weaker rules

This violates the intended design assumption that:
"Consumers of the oracle should respect its freshness guarantees"

Impact on Protocol Logic

The stale price is then used in core financial calculations:

  • Collateral valuation
  • Borrow limits
  • Health factor (HF)
  • Liquidation eligibility

Because the pool accepts stale prices:

  • Collateral values can be overestimated
  • Health factors can be artificially inflated

Critical Outcome

This results in the following condition:
Condition == Value
Real market price == Low (position unsafe)
Oracle freshness == Stale (invalid)
Pool price used == Old (high)
Calculated HF == ≥ 1.0 (healthy)
Actual HF == < 1.0 (liquidatable)

Security Consequence

Positions that should be liquidated become unliquidatable because:

if health_factor >= 1.0 {
    revert(); // liquidation blocked
}

This leads to:

  • Liquidation failure
  • Accumulation of bad debt
  • Protocol insolvency risk

Summary

The vulnerability is caused by:

  • Ignoring oracle-level freshness guarantees
  • Replacing them with a weaker, hardcoded validation
  • Using stale data in critical financial logic

This creates a systemic pricing inconsistency that directly impacts liquidation correctness and protocol solvency.

Impact (HIGH Severity Justification + Economic Exploit)

Severity: HIGH
This vulnerability directly affects the core solvency and liquidation mechanism of the protocol.

Why HIGH Severity?

  1. ❌ Liquidation Failure
    The pool calculates the health factor (HF) using stale prices.
    As a result:
  • Positions that are actually undercollateralized (HF < 1.0)
  • Are considered healthy (HF ≥ 1.0) by the protocol
if health_factor >= 1.0 {
    revert(); // liquidation blocked
}

Liquidators are unable to execute liquidation.

2.Collateral Mispricing
Because stale prices are used:

  • Collateral is overvalued
  • Borrowers appear sufficiently collateralized when they are not

3.Bad Debt Accumulation
Since liquidation is blocked:

  • Positions remain open
  • Collateral value continues to drop (real market)
  • Debt cannot be repaid via liquidation

Eventually:

collateral < debt → bad debt

4.Protocol Insolvency Risk
If multiple positions are affected:

  • The protocol accumulates under-collateralized loans
  • Losses are absorbed by:
    - Liquidity providers (LPs)
    - Protocol treasury

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions