code4rena-2024-03-dittoeth-m05

[M-05] oracleCircuitBreaker: Not checking if price information of asset is stale

보고서

Summary

Chainlink 오라클을 사용하는데, 가격 정보가 오래되었는지를 확인하지 않는다. 따라서 유저가 잘못된 가격 정보를 사용하여 거래할 수 있다.

Keyword

chainlink oracle, price oracle, staled oracle

Vulnerability

getOraclePrice 함수에서는 Chainlink 오라클의 latestRoundData를 호출하여 가격을 조회한다. 그런데 latestRoundData 리턴값 중 timestamp가 오래되었는지를 확인하지 않는다. 만약 체인링크의 데이터가 오래되었다면 유저는 잘못된 가격으로 거래를 하게 될 수 있다.

function getOraclePrice(address asset) internal view returns (uint256) {
    AppStorage storage s = appStorage();
    AggregatorV3Interface baseOracle = AggregatorV3Interface(s.baseOracle);
    uint256 protocolPrice = getPrice(asset);
 
    AggregatorV3Interface oracle = AggregatorV3Interface(s.asset[asset].oracle);
    if (address(oracle) == address(0)) revert Errors.InvalidAsset();
 
    try baseOracle.latestRoundData() returns (uint80 baseRoundID, int256 basePrice, uint256, uint256 baseTimeStamp, uint80) {
        if (oracle == baseOracle) {
            // @dev multiply base oracle by 10**10 to give it 18 decimals of precision
            uint256 basePriceInEth = basePrice > 0 ? uint256(basePrice * C.BASE_ORACLE_DECIMALS).inv() : 0;
            basePriceInEth = baseOracleCircuitBreaker(protocolPrice, baseRoundID, basePrice, baseTimeStamp, basePriceInEth);
            return basePriceInEth;
        } else {
            // prettier-ignore
            (
                uint80 roundID,
                int256 price,
                /*uint256 startedAt*/
                ,
@>              uint256 timeStamp,
                /*uint80 answeredInRound*/
            ) = oracle.latestRoundData();
            uint256 priceInEth = uint256(price).div(uint256(basePrice));
@>          oracleCircuitBreaker(roundID, baseRoundID, price, basePrice, timeStamp, baseTimeStamp);
            return priceInEth;
        }
    } catch {
        if (oracle == baseOracle) {
            return twapCircuitBreaker();
        } else {
            // prettier-ignore
            (
                uint80 roundID,
                int256 price,
                /*uint256 startedAt*/
                ,
@>              uint256 timeStamp,
                /*uint80 answeredInRound*/
            ) = oracle.latestRoundData();
@>          if (roundID == 0 || price == 0 || timeStamp > block.timestamp) revert Errors.InvalidPrice();
 
            uint256 twapInv = twapCircuitBreaker();
            uint256 priceInEth = uint256(price * C.BASE_ORACLE_DECIMALS).mul(twapInv);
            return priceInEth;
        }
    }

oracleCircuitBreaker 함수에서는 Chainlink 오라클 데이터를 확인하는데 이 함수에서도 역시 timestamp 가 오래되었는지는 확인하지 않는다.

function oracleCircuitBreaker(
    uint80 roundId,
    uint80 baseRoundId,
    int256 chainlinkPrice,
    int256 baseChainlinkPrice,
    uint256 timeStamp,
    uint256 baseTimeStamp
) private view {
@>  bool invalidFetchData = roundId == 0 || timeStamp == 0 || timeStamp > block.timestamp || chainlinkPrice <= 0
        || baseRoundId == 0 || baseTimeStamp == 0 || baseTimeStamp > block.timestamp || baseChainlinkPrice <= 0;
 
    if (invalidFetchData) revert Errors.InvalidPrice();
}

Impact

유저가 잘못된 가격 정보를 사용하여 거래할 수 있다.

Mitigation

각 토큰별로 얼마나 오래된 데이터를 허용할지 지정하는 chainlinkStaleLimit 변수를 추가하고 이를 기반으로 timestamp를 확인한다.

function getOraclePrice(address asset) internal view returns (uint256) {
    AppStorage storage s = appStorage();
    AggregatorV3Interface baseOracle = AggregatorV3Interface(s.baseOracle);
    uint256 protocolPrice = getPrice(asset);
 
    AggregatorV3Interface oracle = AggregatorV3Interface(s.asset[asset].oracle);
    if (address(oracle) == address(0)) revert Errors.InvalidAsset();
 
    try baseOracle.latestRoundData() returns (uint80 baseRoundID, int256 basePrice, uint256, uint256 baseTimeStamp, uint80) {
        ...
    } catch {
        if (oracle == baseOracle) {
            return twapCircuitBreaker();
        } else {
            // prettier-ignore
            (
                uint80 roundID,
                int256 price,
                /*uint256 startedAt*/
                ,
                uint256 timeStamp,
                /*uint80 answeredInRound*/
            ) = oracle.latestRoundData();
-           if (roundID == 0 || price == 0 || timeStamp > block.timestamp) revert Errors.InvalidPrice();
+           if (roundID == 0 || price == 0 || timeStamp > block.timestamp || block.timestamp > chainlinkStaleLimit[asset] + timeStamp) revert Errors.InvalidPrice();
 
            uint256 twapInv = twapCircuitBreaker();
            uint256 priceInEth = uint256(price * C.BASE_ORACLE_DECIMALS).mul(twapInv);
            return priceInEth;
        }
    }
 
function oracleCircuitBreaker(
    uint80 roundId,
    uint80 baseRoundId,
    int256 chainlinkPrice,
    int256 baseChainlinkPrice,
    uint256 timeStamp,
-   uint256 baseTimeStamp
+   uint256 baseTimeStamp,
+   address asset
) private view {
    bool invalidFetchData = roundId == 0 || timeStamp == 0 || timeStamp > block.timestamp || chainlinkPrice <= 0
-       || baseRoundId == 0 || baseTimeStamp == 0 || baseTimeStamp > block.timestamp || baseChainlinkPrice <= 0;
+       || baseRoundId == 0 || baseTimeStamp == 0 || baseTimeStamp > block.timestamp || baseChainlinkPrice <= 0 || block.timestamp > chainlinkStaleLimit[asset] + timeStamp;
    if (invalidFetchData) revert Errors.InvalidPrice();
}

tags: bughunting, dittoeth, smart contract, solidity, chainlink oracle, staled oracle, severity medium