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