codehawks-2023-07-dsc-m03

보고서

Summary

Chainlink의 가격 정보에는 최대/최소값 제한이 있다. 실제 가격이 최대/최소 범위를 넘더라도 최대/최소 값까지만 설정할 수 있다. 따라서 Chainlink price oracle을 이용하는 경우 이를 확인해야 한다.

Keyword

chainlink price oracle, lack of input validation

Vulnerability

Chainlink aggregator에는 가격이 미리 정해진 가격대를 벗어날 경우 미리 정한 최소 가격을 리턴하도록 서킷 브레이커가 내장되어 있다. (정확히는 가격 정보를 등록할 때 min, max 범위 내가 아닌 값으로 설정할 수 없다.)

따라서 자산 가치가 크게 하락하는 경우 오라클은 자산의 실제 가격이 아닌 최소 가격을 계속 반환하며, 그 반대의 경우도 마찬가지이다.

Chainlink Datafeed에 데이터를 업데이트하는 코드는 다음과 같다.

    int192 median = r.observations[r.observations.length/2];
    require(minAnswer <= median && median <= maxAnswer, "median is out of min-max range");
    r.hotVars.latestAggregatorRoundId++;
    s_transmissions[r.hotVars.latestAggregatorRoundId] =
    Transmission(median, uint64(block.timestamp));

staleCheckLatestRoundData에서는 가격 정보가 오래되었는지만 확인하며, 이것이 실제 가격이 아닌 Chainlink 피드의 최소/최대값인지는 확인하지 않는다.

    function staleCheckLatestRoundData(AggregatorV3Interface priceFeed)
        public
        view
        returns (uint80, int256, uint256, uint256, uint80)
    {
        (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) =
            priceFeed.latestRoundData();
 
        uint256 secondsSince = block.timestamp - updatedAt;
        if (secondsSince > TIMEOUT) revert OracleLib__StalePrice();
 
        return (roundId, answer, startedAt, updatedAt, answeredInRound);
    }

또한 staleCheckLatestRoundData의 호출자 역시 이를 확인하지 않는다.

    function getTokenAmountFromUsd(address token, uint256 usdAmountInWei) public view returns (uint256) {
        // price of ETH (token)
        // $/ETH ETH ??
        // $2000 / ETH. $1000 = 0.5 ETH
        AggregatorV3Interface priceFeed = AggregatorV3Interface(s_priceFeeds[token]);
@>      (, int256 price,,,) = priceFeed.staleCheckLatestRoundData();
        // ($10e18 * 1e18) / ($2000e8 * 1e10)
        return (usdAmountInWei * PRECISION) / (uint256(price) * ADDITIONAL_FEED_PRECISION);
    }
 
    function getUsdValue(address token, uint256 amount) public view returns (uint256) {
        AggregatorV3Interface priceFeed = AggregatorV3Interface(s_priceFeeds[token]);
@>      (, int256 price,,,) = priceFeed.staleCheckLatestRoundData();
        // 1 ETH = $1000
        // The returned value from CL will be 1000 * 1e8
        return ((uint256(price) * ADDITIONAL_FEED_PRECISION) * amount) / PRECISION;
    }

Impact

가격이 갑자기 크게 변동했을 때에도 프로토콜이 멈추지 않고 DSC를 발행 또는 소각할 수 있다. 이는 정확히 LUNA 대폭락 때 BSC의 Venus에서 발생한 일과 동일한 문제를 발생시킬 수 있다.

Mitigation

minAnswer 또는 maxPrice를 값으로 리턴한 경우 revert 한다.

    (uint80, int256 answer, uint, uint, uint80) = oracle.latestRoundData();
 
    // minPrice check
    require(answer > minPrice, "Min price exceeded");
    // maxPrice check
    require(answer < maxPrice, "Max price exceeded");

Memo

Chainlink docs 에서는 대부분의 데이터 피드에서는 더이상 min, max를 사용하지 않는다고 한다. 대신 커스텀 서킷 브레이커를 작성하라고 권고했다.

비상시 서킷 브레이커가 필요하다 라는 논지 자체는 유효한 것 같다.


tags: bughunting, codehawks, smart contract, solidity, chainlink, price oracle, lack-of-input-validation-vul, chainlink oracle, severity medium