sherlock-2024-01-olympus-rbs-m04
[M-04] Price calculation can be manipulated by intentionally reverting some of price feeds
Summary
가격 계산 모듈은 요청된 자산의 가격 정보를 여러 오라클(가격 피드)에서 조회하고, 조회에 성공한(revert 되지 않은) 가격 피드의 가격을 수집하여 최종 가격을 계산한다. 공격자는 일부 가격 피드를 의도적으로 revert 시켜 조작된 가격을 만들 수 있다.
Keyword
price oracle, oracle manipulation
Vulnerability
가격 피드에서 가격을 조회할 때, 여러가지 가격 피드에서 조회하고, 조회에 성공한 가격 정보를 수집하여 최종 가격을 계산한다. 조회에 실패한 가격 피드는 사용하지 않는다. 그런데 공격자가 일부 가격 피드를 의도적으로 사용하지 않도록 할 수 있어 문제가 발생한다.
function _getCurrentPrice(address asset_) internal view returns (uint256, uint48) {
Asset storage asset = _assetData[asset_];
// Iterate through feeds to get prices to aggregate with strategy
Component[] memory feeds = abi.decode(asset.feeds, (Component[]));
uint256 numFeeds = feeds.length;
uint256[] memory prices = asset.useMovingAverage
? new uint256[](numFeeds + 1)
: new uint256[](numFeeds);
uint8 _decimals = decimals; // cache in memory to save gas
@> for (uint256 i; i < numFeeds; ) {
@> (bool success_, bytes memory data_) = address(_getSubmoduleIfInstalled(feeds[i].target))
.staticcall(
abi.encodeWithSelector(feeds[i].selector, asset_, _decimals, feeds[i].params)
);
// Store price if successful, otherwise leave as zero
// Idea is that if you have several price calls and just
// one fails, it'll DOS the contract with this revert.
// We handle faulty feeds in the strategy contract.
@> if (success_) prices[i] = abi.decode(data_, (uint256)); // 성공했을 시에만 사용
unchecked {
++i;
}
}
// If moving average is used in strategy, add to end of prices array
@> if (asset.useMovingAverage) prices[numFeeds] = asset.cumulativeObs / asset.numObservations; // 이동 평균 사용시 계산해 추가
// If there is only one price, ensure it is not zero and return
// Otherwise, send to strategy to aggregate
if (prices.length == 1) {
if (prices[0] == 0) revert PRICE_PriceZero(asset_);
return (prices[0], uint48(block.timestamp));
} else {
// Get price from strategy
Component memory strategy = abi.decode(asset.strategy, (Component));
@> (bool success, bytes memory data) = address(_getSubmoduleIfInstalled(strategy.target))
.staticcall(abi.encodeWithSelector(strategy.selector, prices, strategy.params)); // 조회한 가격을 통해 최종 가격을 계산
// Ensure call was successful
if (!success) revert PRICE_StrategyFailed(asset_, data);
// Decode asset price
uint256 price = abi.decode(data, (uint256));
// Ensure value is not zero
if (price == 0) revert PRICE_PriceZero(asset_);
return (price, uint48(block.timestamp));
}
}UniV3 가격 피드
UniswapV3Price.sol#L210-L214 에서 UniV3 가격 피드의 사용을 중단한다. 이 때 재진입 플래그 unlocked 가 설정되어 있다면 revert 시킨다. 공격자는 의도적으로 UniV3 callback에서 이를 호출하도록 하여 UniV3 가격 피드의 사용을 막을 수 있다.
// Get the current price of the lookup token in terms of the quote token
(, int24 currentTick, , , , , bool unlocked) = params.pool.slot0();
// Check for re-entrancy
@> if (unlocked == false) revert UniswapV3_PoolReentrancy(address(params.pool));Balancer 가격 피드
BalancerPoolTokenPrice.sol#L388, BalancerPoolTokenPrice.sol#L487, BalancerPoolTokenPrice.sol#L599, BalancerPoolTokenPrice.sol#L748 에서 Balancer 가격 피드의 사용을 중단한다. 역시 재진입일 시에 revert 한다. 공격자는 의도적으로 Balancer의 액션 중 호출하여 Balancer 가격 피드의 사용을 막을 수 있다.
// Prevent re-entrancy attacks
VaultReentrancyLib.ensureNotInVaultContext(balVault);BunniToken 가격 피드
BunniPrice.sol#L155-L160 에서 BunniToken 가격 피드의 사용을 중단한다. 보유량을 확인하고 편차를 충족하지 못하면 revert 한다. 편차 확인을 위해 UniV3를 이용하기 때문에, UniV3의 mint 콜백에서 이를 호출하면 의도적으로 가격 피드의 사용을 막을 수 있다.
function getBunniTokenPrice(
address bunniToken_,
uint8 outputDecimals_,
bytes calldata params_
) external view returns (uint256) {
...
@> _validateReserves(
_getBunniKey(token),
lens,
params.twapMaxDeviationsBps,
params.twapObservationWindow
);
...
}
...
function _validateReserves(
BunniKey memory key_,
BunniLens lens_,
uint16 twapMaxDeviationBps_,
uint32 twapObservationWindow_
) internal view {
uint256 reservesTokenRatio = BunniHelper.getReservesRatio(key_, lens_);
@> uint256 twapTokenRatio = UniswapV3OracleHelper.getTWAPRatio(
address(key_.pool),
twapObservationWindow_
);
// Revert if the relative deviation is greater than the maximum.
if (
// `isDeviatingWithBpsCheck()` will revert if `deviationBps` is invalid.
Deviation.isDeviatingWithBpsCheck(
reservesTokenRatio,
twapTokenRatio,
twapMaxDeviationBps_,
TWAP_MAX_DEVIATION_BASE
)
) {
revert BunniPrice_PriceMismatch(address(key_.pool), twapTokenRatio, reservesTokenRatio);
}
}추가 고려사항 및 공격 벡터
- 일반적으로 ERC20 토큰 가격의 경우 보통 3개 이상의 가격 피드를 체인링크 가격 피드와 함께 사용하며, 선택적으로 이동평균을 이용한다. 이동평균을 이용한다면 이동평균 값은 가격 배열의 맨 마지막 인덱스에 추가된다.
if (asset.useMovingAverage) prices[numFeeds] = asset.cumulativeObs / asset.numObservations; - 최종 가격을 계산하는 전략에서, 유효한(0이 아닌) 가격 정보가 3개 미만이라면 첫번째의 0이 아닌 가격 정보를 이용한다.
- getMedianPriceIfDeviation
function getMedianPriceIfDeviation( uint256[] memory prices_, bytes memory params_ ) public pure returns (uint256) { // Misconfiguration if (prices_.length < 3) revert SimpleStrategy_PriceCountInvalid(prices_.length, 3); uint256[] memory nonZeroPrices = _getNonZeroArray(prices_); // Return 0 if all prices are 0 if (nonZeroPrices.length == 0) return 0; // Cache first non-zero price since the array is sorted in place @> uint256 firstNonZeroPrice = nonZeroPrices[0]; // If there are not enough non-zero prices to calculate a median, return the first non-zero price @> if (nonZeroPrices.length < 3) return firstNonZeroPrice; - getMedianPrice
function getMedianPrice(uint256[] memory prices_, bytes memory) public pure returns (uint256) { // Misconfiguration if (prices_.length < 3) revert SimpleStrategy_PriceCountInvalid(prices_.length, 3); uint256[] memory nonZeroPrices = _getNonZeroArray(prices_); uint256 nonZeroPricesLen = nonZeroPrices.length; // Can only calculate a median if there are 3+ non-zero prices if (nonZeroPricesLen == 0) return 0; @> if (nonZeroPricesLen < 3) return nonZeroPrices[0]; // Sort the prices uint256[] memory sortedPrices = nonZeroPrices.sort(); return _getMedianPrice(sortedPrices); }
- getMedianPriceIfDeviation
즉, 잠재적인 공격 벡터는 다음과 같다.
- 체인링크 피드가 조작되었다면, 공격자는 다른 세 피드를 의도적으로 사용을 막아 조작된 피드를 이용한다.
- 체인링크 피드를 사용하지 않는다면, 세 피드 중 하나를 조작하고 다른 피드 하나를 비활성화 한다. (?)
Impact
공격자가 원하는 대로 가격 피드의 일부를 비활성화 할 수 있다. 이로 인해 조작된 가격을 이용할 수 있다.
Mitigation
가격 피드가 의도적으로 revert 되는 경우 가격 계산 작업 자체도 revert 되어야 한다.
tags: bughunting, ubiquity, smart contract, solidity, olympus dao, price oracle, uniswap integration, balancer, solo issue, oracle manipulation, severity medium