Custom oracle control
View MarkdownSummary
Morpho markets allow custom oracles to be used. There is nothing stopping a market creator from deploying a completely custom oracle with strange, unexpected, or outright malicious behaviour. Below, we show how a market using legitimate tokens can have its collateral price manipulated through a custom oracle.
Context
Skip this if you already know how Morpho works.
There are three useful pieces to keep in mind when thinking about Morpho: Assets, Markets and Vaults.
Assets can be used permissionlessly. Anyone creating a market can choose its loan asset and collateral asset.
Markets are also permissionless. A market creator chooses the assets, LLTV, interest-rate model and, importantly here, the oracle used to value the collateral.
Vaults sit one level above markets. Allocators choose which markets vault capital can enter and therefore which assets and market configurations depositors are exposed to.

Example
For this POC, we deployed a vault and a Morpho market on Base. The market uses two legitimate tokens: USDC as the loan asset and LayerZero’s ZRO as collateral. We deployed a custom oracle for the market instead of using an independent price feed.
Contracts
-
app.morpho.orgThe vault deployed for the POC.
-
app.morpho.orgThe market with USDC as its loan asset and ZRO as its collateral asset.
-
basescan.orgThe LayerZero token used as collateral.
-
basescan.orgThe verified oracle source used by the market.
The key point here is that this is a valid price oracle from Morpho’s perspective because it implements IOracle. Morpho’s interface documentation says that users are responsible for selecting markets with safe oracles. Here, the owner is the deployer, so the deployer can update the price.
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.13;
import {IOracle} from "./IOracle.sol";
/// @title SettableOracle
/// @notice A Morpho-compatible oracle whose price can be updated by its deployer.
contract SettableOracle is IOracle {
/// @notice Thrown when an address other than the owner attempts to set the price.
error NotOwner();
/// @notice Emitted whenever the oracle price is updated.
event PriceSet(uint256 oldPrice, uint256 newPrice);
/// @notice The only address permitted to update the price.
address public immutable owner;
/// @inheritdoc IOracle
uint256 public override price;
constructor() {
owner = msg.sender;
}
/// @notice Sets the oracle price.
/// @param newPrice The new price in Morpho's oracle scale.
function setPrice(uint256 newPrice) external {
if (msg.sender != owner) revert NotOwner();
emit PriceSet(price, newPrice);
price = newPrice;
}
}
Example
- ZRO trades at
$1and the oracle initially reports$1. - The vault allocates
$1,000,000USDC to the market. - A borrower supplies
500,000ZRO as collateral, worth$500,000at its real market price. - At the market’s
98%LLTV, that collateral should support up to$490,000of USDC borrowing. - The deployer changes the oracle price to
$2. - Morpho now values the same collateral at
$1,000,000, allowing the borrower to take up to$980,000USDC. - Liquidators can only sell the ZRO for its real value of
$500,000, leaving insufficient value to repay the USDC.
How to address
Go to the market and check its linked oracle. If a curator is allocating to a new market, or a vault has caps that allow it to allocate to that market, confirm that the oracle has been checked.
Work backwards from price().
Check:
- Is the oracle contract verified?
- Is it a proxy or otherwise upgradeable?
- What ultimately determines the value returned by
price()? - Does it depend on recognizable external feeds such as Chainlink or RedStone?
- Are those feeds themselves the expected contracts?
- Does the oracle contain privileged setters or mutable configuration?
- Can an owner, admin or other address materially influence the returned price?
- Is the price effectively hardcoded?
- If a fixed-price assumption is being made, what economically enforces that price?