Platform: Code4rena
Start Date: 30/04/2024
Pot Size: $112,500 USDC
Total HM: 22
Participants: 122
Period: 8 days
Judge: alcueca
Total Solo HM: 1
Id: 372
League: ETH
Rank: 119/122
Findings: 1
Award: $0.00
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: Sathish9098
Also found by: 0x73696d616f, 0xCiphky, 0xmystery, ABAIKUNANBAEV, Bauchibred, BiasedMerc, Fassi_Security, FastChecker, GalloDaSballo, GoatedAudits, K42, KupiaSec, LessDupes, Limbooo, ReadyPlayer2, Rhaydden, SBSecurity, Sabit, Sparrow, WildSniper, ZanyBonzy, adam-idarrha, adeolu, araj, aslanbek, atoko, b0g0, carlitox477, crypticdefense, fyamf, gesha17, gjaldon, grearlake, gumgumzum, hihen, honey-k12, hunter_w3b, inzinko, jesjupyter, jokr, kennedy1030, kind0dev, kinda_very_good, ladboy233, lanrebayode77, oakcobalt, oualidpro, pauliax, rbserver, t0x1c, tapir, underdog, xg, zzykxx
0 USDC - $0.00
https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/Oracle/RenzoOracle.sol#L79-L80 https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/Oracle/RenzoOracle.sol#L96-L97
The impact of this issue stems from HAL-01
of the Halborn audit for an inadequate mitigation. Currently, RenzoOracle.sol
assumes that all Chainlink oracles will return prices with a decimal precision of 18. This assumption is hardcoded into the contract’s logic for price calculations. If Chainlink or any other used oracle service changes the decimal precision for any price feed, the smart contract will still perform calculations assuming 18 decimals, which can lead to significant financial discrepancies. This could result in incorrect asset valuations, under or over-issuance of tokens, and potentially exploitable conditions if the discrepancy is identified by malicious actors. The financial stakes and trust in the protocol could be severely compromised.
Specifically, the following two calls will be affected when referencing/calling:
renzoOracle.lookupTokenValue
by RestakeManager.calculateTVLs()renzoOracle.lookupTokenAmountFromValue
by WithdrawQueue.withdraw()The setOracleAddress
function explicitly checks and enforces that the decimals for a newly set oracle address are 18:
https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/Oracle/RenzoOracle.sol#L60-L62
// Verify that the pricing of the oracle is 18 decimals - pricing calculations will be off otherwise if (_oracleAddress.decimals() != 18) revert InvalidTokenDecimals(18, _oracleAddress.decimals());
However, if the decimals of an oracle were to change after this initial setting (which is possible if Chainlink updates an oracle contract), the lookupTokenValue
and lookupTokenAmountFromValue
functions do not check or adapt to this change, as they use a hardcoded scale factor based on 18 decimals:
https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/Oracle/RenzoOracle.sol#L79-L80
// Price is times 10**18 ensure value amount is scaled return (uint256(price) * _balance) / SCALE_FACTOR;
https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/Oracle/RenzoOracle.sol#L96-L97
// Price is times 10**18 ensure token amount is scaled return (_value * SCALE_FACTOR) / uint256(price);
As an example how Chainlink could possibly change decimal()
(say from 18 to 8 or some other integer), let's dive into the verified contract of STETH / ETH price feed. As seen in the current code snippet, decimals()
of the price feed contract is dependent on currentPhase.aggregator.decimals()
:
https://etherscan.io/address/0x86392dC19c0b719886221c78AB11eb8Cf5c52812#code#L431
/** * @notice represents the number of decimals the aggregator responses represent. */ function decimals() external view override returns (uint8) { return currentPhase.aggregator.decimals(); }
And notably, the contract owner is allowed to propose and confirm/change the aggregator:
https://etherscan.io/address/0x86392dC19c0b719886221c78AB11eb8Cf5c52812#code#L481
/** * @notice Allows the owner to propose a new address for the aggregator * @param _aggregator The new address for the aggregator contract */ function proposeAggregator(address _aggregator) external onlyOwner() { proposedAggregator = AggregatorV2V3Interface(_aggregator); } /** * @notice Allows the owner to confirm and change the address * to the proposed aggregator * @dev Reverts if the given address doesn't match what was previously * proposed * @param _aggregator The new address for the aggregator contract */ function confirmAggregator(address _aggregator) external onlyOwner() { require(_aggregator == address(proposedAggregator), "Invalid proposed aggregator"); delete proposedAggregator; setAggregator(_aggregator); }
Consequently, currentPhase
will be changed by invoking setAggregator()
in the code block above:
https://etherscan.io/address/0x86392dC19c0b719886221c78AB11eb8Cf5c52812#code#L495
function setAggregator(address _aggregator) internal { uint16 id = currentPhase.id + 1; currentPhase = Phase(id, AggregatorV2V3Interface(_aggregator)); phaseAggregators[id] = AggregatorV2V3Interface(_aggregator); }
In conclusion, while the hardcoding approach has some gas saving benefit, it does not justify a high/critical impact ahead albeit at a low likelihood.
Manual
Modify the contract to retrieve and use the decimal value from the oracle dynamically within each function that requires price data. This can ensure that any changes in decimal precision are handled correctly at runtime.
For instance, lookupTokenValue()
may be refactored as follows:
https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/Oracle/RenzoOracle.sol#L71-L81
function lookupTokenValue(IERC20 _token, uint256 _balance) public view returns (uint256) { AggregatorV3Interface oracle = tokenOracleLookup[_token]; if (address(oracle) == address(0x0)) revert OracleNotFound(); (, int256 price, , uint256 timestamp, ) = oracle.latestRoundData(); if (timestamp < block.timestamp - MAX_TIME_WINDOW) revert OraclePriceExpired(); if (price <= 0) revert InvalidOraclePrice(); - // Price is times 10**18 ensure value amount is scaled - return (uint256(price) * _balance) / SCALE_FACTOR; + uint256 decimals = oracle.decimals(); + return (uint256(price) * _balance) / (10**decimals); }
Decimal
#0 - jatinj615
2024-05-14T14:31:08Z
External dependency creating a technical debt.
#1 - alcueca
2024-05-20T03:38:38Z
If Chainlink or any other used oracle service changes the decimal precision for any price feed
The chances of that are quite minimal, and everyone would get rekt everywhere.
#2 - c4-judge
2024-05-20T03:38:43Z
alcueca changed the severity to QA (Quality Assurance)
#3 - c4-judge
2024-05-20T03:38:46Z
alcueca marked the issue as grade-a
#4 - c4-judge
2024-05-24T09:20:15Z
alcueca marked the issue as grade-b
#5 - mystery0x
2024-05-26T02:41:10Z
If Chainlink or any other used oracle service changes the decimal precision for any price feed
The chances of that are quite minimal, and everyone would get rekt everywhere.
Exactly like you said, the impact will be critical albeit the chances of that are quite minimal. A combination of high/critical impact and low likelihood should be classified as a medium finding IMO. The report is value added for better code engineering despite the very fact that the contract can be upgraded to rectify it. Thank you for reconsidering.