Assessment reports>Benqi Oracle>Medium findings>Missing staleness check in ,BenqiDualOracle::getUnderlyingPrice, when ,oracleMode, is ,EDGE_ONLY, or ,CL_ONLY
Category: Business Logic

Missing staleness check in BenqiDualOracle::getUnderlyingPrice when oracleMode is EDGE_ONLY or CL_ONLY

Medium Impact
Medium Severity
Medium Likelihood

Description

BenqiDualOracle implements a dual oracle system with Edge as primary and Chainlink as backup. It has three different modes:

/// @notice Enum representing different oracle modes
enum OracleMode {
    EDGE_ONLY, // Use only Edge oracle
    CL_ONLY, // Use only Chainlink oracle
    DUAL // Use both oracles (default)
}

In the comments of the BenqiDualOracle contract, it is mentioned that staleness detection is one of the responsibilities of this contract:

* @dev Dual oracle system with mechanisms for:
*      - Staleness detection

However, in the BenqiDualOracle::getUnderlyingPrice implementation, when the oracleMode is EDGE_ONLY or CL_ONLY, no staleness detection is performed:

// If in Edge-only mode, return Edge price without checks
if (oracleMode == OracleMode.EDGE_ONLY) {
    (uint256 edgePriceResult,) = edgeOracle.getUnderlyingPriceWithTimestamp(qiToken);
    if (edgePriceResult == 0) revert EdgeOracleZeroPrice();
    return edgePriceResult;
}

// If in Chainlink-only mode, return Chainlink price without checks
if (oracleMode == OracleMode.CL_ONLY) {
    (uint256 chainlinkPriceResult,) = chainlinkOracle.getUnderlyingPriceWithTimestamp(qiToken);
    if (chainlinkPriceResult == 0) revert ChainlinkOracleZeroPrice();
    return chainlinkPriceResult;
}

For example, when oracleMode == OracleMode.EDGE_ONLY, the BenqiPriceOracle::getUnderlyingPriceWithTimestamp function will be called to obtain price information:

function getUnderlyingPriceWithTimestamp(IQiToken qiToken) external view override returns (uint256, uint256) {
    address qiTokenAddress = address(qiToken);
    IAggregatorV2V3Interface feed;

    if (qiTokenAddress == qiAvaxAddress) {
        feed = getFeed(AVAX_UNDERLYING);
    } else {
        address underlying = address(IQiErc20(address(qiToken)).underlying());
        feed = getFeed(underlying);
    }

    return getPriceAndTimestamp(feed, qiToken);
}

Finally, the BenqiPriceOracle::getPriceAndTimestamp function is called. In the BenqiEdgeOracle::getPriceAndTimestamp function, although updatedAt is obtained, no stale price check is performed based on it:

function getPriceAndTimestamp(IAggregatorV2V3Interface feed, IQiToken qiToken)
    internal
    view
    returns (uint256, uint256)
{
    address tokenAddress =
        address(qiToken) == qiAvaxAddress ? AVAX_UNDERLYING : address(IQiErc20(address(qiToken)).underlying());
    IEIP20Interface token = IEIP20Interface(tokenAddress);

    // Check for manually set price first
    if (prices[tokenAddress] != 0) {
        uint256 manualPrice = prices[tokenAddress];
        // Only adjust for decimals if it's not AVAX
        if (address(qiToken) != qiAvaxAddress) {
            uint256 tokenDecimalDelta = 18 - token.decimals();
            if (tokenDecimalDelta > 0) {
                manualPrice = manualPrice * (10 ** tokenDecimalDelta);
            }
        }
        return (manualPrice, block.timestamp);
    }

    // If no manual price, proceed with feed data
    (, int256 answer,, uint256 updatedAt,) = feed.latestRoundData();

    require(answer > 0, "negative or zero price not allowed");

    uint256 feedPrice = uint256(answer);

    // If it's not AVAX, adjust for token decimals
    if (address(qiToken) != qiAvaxAddress) {
        uint256 tokenDecimalDelta = 18 - token.decimals();
        if (tokenDecimalDelta > 0) {
            feedPrice = feedPrice * (10 ** tokenDecimalDelta);
        }
    }

    // Convert to 18 decimals if needed
    uint256 feedDecimalDelta = 18 - feed.decimals();
    if (feedDecimalDelta > 0) {
        feedPrice = feedPrice * (10 ** feedDecimalDelta);
    }

    return (feedPrice, updatedAt);
}

Impact

When oracleMode is EDGE_ONLY or CL_ONLY, code will execute with prices that do not reflect the current pricing, resulting in a potential loss of funds for users.

Recommendations

Consider adding a staleness check when OracleMode is EDGE_ONLY or CL_ONLY.

Remediation

This issue has been acknowledged by Chaos Labs, and a fix was implemented in commit aa97b6fd.

Zellic © 2025Back to top ↑