Custom oracle control

View Markdown

Summary

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.

A depositor supplies a vault that allocates to a Morpho market configured with USDC as the loan asset, AZND as the collateral asset, an LLTV, and an oracle.

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

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

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: