DittoETH

Ditto
DeFiFoundryOracle
55,000 USDC
View results
Submission Details
Severity: high
Invalid

Incorrect asset price if oracle does not use 8 decimals

Summary

The LibOracle.sol:getOraclePrice() function can return an incorrect price for assets that have a Chainlink oracle with a different number of decimals than the Chainlink oracle for the ETH/USD pair.

Vulnerability Details

LibOracle.sol:getOraclePrice() is used to get the price of an asset in ETH.

First, the price for the ETH/USD pair is fetched from the Chainlink oracle. If asset is other than USD a second call is made to the Chainlink oracle to get the price of asset/USD pair. The price of asset/ETH is then calculated by dividing the price of asset/USD by the price of ETH/USD.

However, it is not taken into account that the number of decimals of the asset/USD pair may be different from the number of decimals of the ETH/USD pair. This can lead to an incorrect price of asset/ETH being returned.

Note also that the resultant price could be greater than type(uint80).max, which would cause an overflow in the setPriceAndTime() function.

132 function setPriceAndTime(address asset, uint256 oraclePrice, uint32 oracleTime)
133 internal
134 {
135 AppStorage storage s = appStorage();
136 s.bids[asset][Constants.HEAD].ercAmount = uint80(oraclePrice); 👈
137 s.bids[asset][Constants.HEAD].creationTime = oracleTime;
138 }

Proof of Concept

Add a new file with the following code into test/fork and run forge test -vv --mt testFork_getOraclePrice.

// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.21;
import {ConstantsTest} from "test/utils/ConstantsTest.sol";
import {U256} from "contracts/libraries/PRBMathHelper.sol";
import {Modifiers} from "contracts/libraries/AppStorage.sol";
import {LibOracle} from "contracts/libraries/LibOracle.sol";
import {AggregatorV3Interface} from
"@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
import "forge-std/console2.sol";
contract MockDiamond is Modifiers {
constructor(address baseAsset, address baseOracle, address asset, address assetOracle) {
s.baseOracle = baseOracle;
s.asset[baseAsset].oracle = baseOracle;
s.asset[asset].oracle = assetOracle;
}
function getOraclePrice(address asset) public view returns (uint256) {
return LibOracle.getOraclePrice(asset);
}
}
contract ChainlinkForkTest is ConstantsTest {
using U256 for uint256;
uint256 public mainnetFork;
address public baseAsset = address(0x111);
address public baseOracle = 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419; // ETH/USD
address public asset = address(0x222);
address public assetOracle = 0xe20CA8D7546932360e37E9D72c1a47334af57706; // AMPL/USD
MockDiamond diamond;
uint256 public forkBlock = 18_111_111;
function setUp() public {
try vm.envString("MAINNET_RPC_URL") returns (string memory rpcUrl) {
mainnetFork = vm.createSelectFork(rpcUrl, forkBlock);
} catch {
revert("env: MAINNET_RPC_URL failure");
}
assertEq(vm.activeFork(), mainnetFork);
diamond = new MockDiamond(baseAsset, baseOracle, asset, assetOracle);
}
function testFork_getOraclePrice() public {
uint256 assetPrice = diamond.getOraclePrice(asset);
console2.log("assetPrice: %s", assetPrice);
}
}

The console output is:

Logs:
assetPrice: 6748028520519842969307637

This translates to a price of 6,748,028.52 ETH per AMPL, while the value of the AMPL is just above 1 USD.

Impact

Assets with a Chainlink oracle that has a different number of decimals than the Chainlink oracle for the ETH/USD pair will have an incorrect price returned by LibOracle.sol:getOraclePrice(), which can lead to many problems, including users being required to deposit more/less collateral than expected or being liquidated when they shouldn't be.

Tools Used

Manual inspection.

Recommendations

File: contracts/libraries/LibOracle.sol
) = oracle.latestRoundData();
- uint256 priceInEth = uint256(price).div(uint256(basePrice));
+ uint8 BASE_DECIMALS = 8;
+ uint8 oracleDecimals = oracle.decimals();
+ uint256 normalizedPrice;
+ if (oracleDecimals > BASE_DECIMALS) {
+ normalizedPrice = uint256(price) / (10 ** (oracleDecimals - BASE_DECIMALS));
+ } else {
+ normalizedPrice = uint256(price) * (10 ** (8 - oracleDecimals));
+ }
+ uint256 priceInEth = normalizedPrice.div(uint256(basePrice));

It would also be a good idea to add a check to ensure that the price is not greater than type(uint80).max.

Updates

Lead Judging Commences

0xnevi Lead Judge
almost 2 years ago
0xnevi Lead Judge almost 2 years ago
Submission Judgement Published
Invalidated
Reason: Known issues

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.