How to Detect Oracle Risk in Morpho Markets Using Glider

How to Detect Oracle Risk in Morpho Markets Using Glider
Share

Intro

As tokenized assets gain traction across the financial industry, investors and protocols building on them need a way to analyze contract risk and assess how safe their exposure is. This applies across reinsurance, lending protocols, vault owners, and liquidity operators.

Traditional behavior analysis (transaction flows, active addresses, net inflows and outflows) no longer cuts it. Analysts also need to understand what a smart contract can actually do, and standard web3 audits fall short here as they're time-consuming and contracts can upgrade at any time.

Glider addresses this gap. Using static code analysis, it retrieves critical on-chain contract data at scale, surfacing risk signals that other data providers overlook.

Below, we share Hexens' own research using Glider to examine Morpho, a leading modular lending protocol whose isolated-market design has made it a key destination for tokenized-asset collateral. Each Morpho market configures its own risk parameters, including its oracle feed, with no intermediation from Morpho itself. That per-market oracle configuration is powerful, but it also introduces risks that analysts, DeFi insurers, and traditional finance teams should understand before interacting with a market.

Hard coded prices

An oracle that returns a hardcoded price is one of the most severe misconfiguration patterns. A hardcoded price encodes the assumption that a value can never change. Many users falsely believe that a pegged asset will always trade at $1. As history has shown, this invariant can break due to flash crashes, exploits, and broader market dislocations.

Glider makes it easy to identify these cases. Within a Glider query, we can inspect the price function's return values, evaluate them, and flag any instance where a literal value is returned directly rather than fetched from a live source.

Use the Glider query below to check if your oracle returns hardcoded prices:

```python
from glider import *

ORACLE_ADDRESS = "" # Provide your oracle address
ORACLE_PRICE_FUNCTION_NAME = "" # Provide the oracle price function name

def query():
return (
Functions()
.with_address(ORACLE_ADDRESS)
.with_name(ORACLE_PRICE_FUNCTION_NAME)
.exec()
.filter(literals_used)
)

def literals_used(function):
return_values = function.return_tuple()[1]

for return_value in return_values:
# Covers cases where function returns 1e18
if isinstance(return_value, Literal):
return True

   \# Implies we are working with a price formula containing multiple components  
   if isinstance(return\_value, ValueExpression):  
       components \= return\_value.get\_components()

       \# Filters out cases where variables, calls, and more are inside the price formula  
       if any(components.filter(lambda component : not isinstance(component, Literal) and not "Operator" in str(component))):  
           return False  
         
       \# Returns true when the price formula is hardcoded like 10 \*\* 18 \* 100  
       return True  

return False
```

Storage-sourced prices

Similar to hardcoded prices, another risk arises when oracles store prices in state variables. In the best case, a state variable can be updated by a privileged role, but it may still be stale at the time a user interacts with the market. In the worst case, the variable is never updated at all and functions as an implicit hardcoded price. Either way, a price sourced from contract storage introduces the risk of stale or manipulated data feeding into lending calculations.

Glider can identify oracles that read directly from storage in their return instructions, flagging any price function that returns a state variable rather than fetching a live feed.

```python
from glider import *

ORACLE_ADDRESS = "" # Provide your oracle address
ORACLE_PRICE_FUNCTION_NAME = "" # Provide the oracle price function name

def query():
return (
Functions()
.with_address(ORACLE_ADDRESS)
.with_name(ORACLE_PRICE_FUNCTION_NAME)
.exec()
.filter(lambda func : any(func.instructions().exec()))
.instructions()
.return_instructions()
.exec()
.filter(hardcoded_state_var)
)

def hardcoded_state_var(inst):
return inst.is_storage_read() and not inst.is_call()
```

Missing staleness validation

Many oracles deployed on Morpho markets rely on Chainlink as their underlying price source, calling `latestRoundData` to retrieve the most recent price. Using Chainlink is not inherently unsafe, but it must be done correctly. Alongside the price, `latestRoundData` returns an `updatedAt` timestamp that indicates when the price was last recorded. Oracles must validate this timestamp to confirm the price is current. Without this check, an oracle may silently consume a stale price during a Chainlink outage, exposing Morpho users to incorrect valuations.

The Glider query below identifies oracle functions that call `latestRoundData` but do not use the `updatedAt` return value in any downstream validation.

```python
from glider import *

ORACLE_ADDRESS = "" # Provide your oracle address
ORACLE_PRICE_FUNCTION_NAME = "" # Provide the oracle price function name

def query():
return (
Functions()
.with_address(ORACLE_ADDRESS)
.with_name(ORACLE_PRICE_FUNCTION_NAME)
.exec()
.instructions()
.with_callee_name("latestRoundData")
.exec()
.filter(updatedAt_not_used)
)

def lacks_updated_at_validation(instruction):
return any(
instruction
.next_instructions_recursive()
# Ignoring try/catch and return instructions for now
.filter(lambda instruction : not instruction.is_try() and not instruction.is_return())
.filter(updatedAt_not_used)
)

def updatedAt_not_used(instruction):
# Prevent any cases where we are calling a different latestRoundData call
if len(instruction.get_dest().get_components()) != 5:
return False

# The updatedAt variable is the fourth component in the latestRoundData return value.
updatedAt_var = instruction.get_dest().get_components()[3]

return (
not any(updatedAt_var.expression) or
not any(updatedAt_var.forward_df_recursive())
)
```

Missing price circuit breaker checks

Even when an oracle fetches a live price from a trusted source, it should validate that the returned value falls within an acceptable range before using it. A price circuit breaker is a guard that rejects prices outside of predefined bounds, such as a price of zero or a value that has deviated beyond a configurable threshold. Without this check, a corrupted or manipulated price passes directly into lending calculations, potentially enabling undercollateralized borrowing or unfair liquidations.

The Glider query below identifies oracle price functions that call `latestRoundData` but do not apply any downstream `require`, `assert`, or conditional check on the returned price value.

```python
from glider import *

ORACLE_ADDRESS = "" # Provide your oracle address
ORACLE_PRICE_FUNCTION_NAME = "" # Provide the oracle price function name

def query():
return (
Functions()
.with_address(ORACLE_ADDRESS)
.with_name(ORACLE_PRICE_FUNCTION_NAME)
.exec()
.filter(lambda func : any(func.instructions().exec()))
.filter(circuit_breaker_not_used)
)

def circuit_breaker_not_used(func):
for inst in func.instructions().with_callee_name("latestRoundData").exec():
dests = inst.get_dests()

   if not any(dests):  
       return False  

   if "TupleExpression" in str(dests\[0\]):  
       dests \= dests\[0\].get\_components()  
   if len(dests) \>= 2:  
       for df in dests\[1\].forward\_df\_recursive():  
           if not isinstance(df, Instruction):  
               continue  
           if bool(set(df.callee\_names()) & set(\["require", "assert"\])):  
               return False  
           if df.is\_if() or df.is\_throw():  
               return False  

return True
```

Outro

Oracle misconfiguration is one of the most common and consequential risk vectors in DeFi lending. As protocols like Morpho continue to democratize market creation, the responsibility for oracle safety shifts increasingly to market creators and to the analysts, insurers, and TradFi teams evaluating exposure to them.

Glider makes it possible to assess these risks systematically and at scale, without waiting on manual audits or relying solely on historical transaction data. The four patterns covered here are all detectable today using the queries above: hardcoded prices, storage-sourced prices, missing staleness checks, and absent circuit breakers.

Hexens continues to expand this research across Morpho markets and other DeFi protocols. If you're building on tokenized assets or evaluating collateralized lending exposure, reach out to learn how Glider can surface the risk signals that matter.

Community members

[ Our community ]

[ Community hires ]