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: 18/122
Findings: 3
Award: $666.67
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: pauliax
Also found by: 0rpse, 0x73696d616f, 0xAadi, 0xCiphky, 0xPwned, 0xhacksmithh, 0xnev, 0xnightfall, 0xordersol, 14si2o_Flint, Aamir, Aymen0909, BiasedMerc, DanielArmstrong, Fassi_Security, FastChecker, GoatedAudits, Greed, KupiaSec, LessDupes, Maroutis, NentoR, OMEN, SBSecurity, Stefanov, TheFabled, adam-idarrha, ak1, aman, araj, aslanbek, b0g0, baz1ka, bigtone, blutorque, carlitox477, carrotsmuggler, crypticdefense, eeshenggoh, fyamf, gesha17, gjaldon, grearlake, guhu95, honey-k12, hunter_w3b, ilchovski, josephdara, kinda_very_good, lanrebayode77, m_Rassska, maxim371, mt030d, mussucal, oakcobalt, p0wd3r, peanuts, rbserver, shui, siguint, t0x1c, tapir, twcctop, ustazz, xg, zhaojohnson, zigtur, zzykxx
0.0026 USDC - $0.00
The incorrect usage of an array index in the RestakeManager::calculateTVLs
function leads to wrong calculations of the TVL for withdrawal queues. Using the wrong index results in referencing the wrong token for value lookup, thereby potentially inflating or deflating the actual TVL calculated. The erroneous totalTVL
returned by the function RestakeManager::calculateTVLs
is used by many functions such as RestakeManager::deposit
, WithdrawQueue::withdraw
and BalancerRateProvider::getRate
which can cause many issues down the line.
The issue arises in the following segment of the RestakeManager::calculateTVLs
function :
// record token value of withdraw queue if (!withdrawQueueTokenBalanceRecorded) { totalWithdrawalQueueValue += renzoOracle.lookupTokenValue( collateralTokens[i],//@audit using i index collateralTokens[j].balanceOf(withdrawQueue) );
As this calculation only runs for the first i loop (i=0), it will use the oracle of the first token to convert the balance of every single available token which is wrong. This leads to a wrong calculation of totalTVL
returned by the function.
This has many impacts, I will list some of them :
RestakeManager::deposit
function to not work as expected. Some checks could be or not bypassed (or the opposite revert when it should not), and the mint value will be wrong (since totalTVL
is wrong):... // Enforce TVL limit if set, 0 means the check is not enabled if (maxDepositTVL != 0 && totalTVL + collateralTokenValue > maxDepositTVL) { revert MaxTVLReached(); } ... // Determine which operator delegator to use IOperatorDelegator operatorDelegator = chooseOperatorDelegatorForDeposit( operatorDelegatorTVLs, totalTVL ); ... uint256 ezETHToMint = renzoOracle.calculateMintAmount( totalTVL, collateralTokenValue, ezETH.totalSupply() );
WithdrawQueue
contract the redeem amount will be wrong// function withdraw ... (, , uint256 totalTVL) = restakeManager.calculateTVLs(); // Calculate amount to Redeem in ETH uint256 amountToRedeem = renzoOracle.calculateRedeemAmount( _amount, ezETH.totalSupply(), totalTVL );
BalancerRateProvider
contract the rate returned will be wrong// function getRate() function getRate() external view returns (uint256) { // Get the total TVL priced in ETH from restakeManager (, , uint256 totalTVL) = restakeManager.calculateTVLs(); // Get the total supply of the ezETH token uint256 totalSupply = ezETHToken.totalSupply(); // Sanity check if (totalSupply == 0 || totalTVL == 0) revert InvalidZeroInput(); // Return the rate return (10 ** 18 * totalTVL) / totalSupply;
Manual review
Replace the index i with the correct index j in the problematic line:
totalWithdrawalQueueValue += renzoOracle.lookupTokenValue( collateralTokens[j], collateralTokens[j].balanceOf(withdrawQueue) );
Error
#0 - c4-judge
2024-05-16T10:34:58Z
alcueca marked the issue as satisfactory
#1 - c4-judge
2024-05-16T10:39:08Z
alcueca changed the severity to 3 (High Risk)
#2 - c4-judge
2024-05-20T04:26:26Z
alcueca changed the severity to 2 (Med Risk)
#3 - c4-judge
2024-05-23T13:47:20Z
alcueca changed the severity to 3 (High Risk)
665.1934 USDC - $665.19
https://github.com/code-423n4/2024-04-renzo/blob/519e518f2d8dec9acf6482b84a181e403070d22d/contracts/Bridge/L2/Oracle/RenzoOracleL2.sol#L13 https://github.com/code-423n4/2024-04-renzo/blob/519e518f2d8dec9acf6482b84a181e403070d22d/contracts/Bridge/L2/Oracle/RenzoOracleL2.sol#L52
The stale period 86400 + 60 seconds used for the oracle price validation is too short for some tokens like ezETH
for example on Arbitrum. This could lead to the protocol consuming stale prices on Arbitrum.
In both RenzoOracle
and RenzoOracleL2
, the hearbeat period MAX_TIME_WINDOW
is set to 86400 + 60; // 24 hours + 60 seconds
. In the functions RenzoOracleL2::getMintRate
and RenzoOracle::lookupTokenValue
, a validation checks sees if the price data fed by Chainlink's price feed aggregators is stale depending if the period of 24 hours + 60 seconds has passed. Example :
function getMintRate() public view returns (uint256, uint256) { (, int256 price, , uint256 timestamp, ) = oracle.latestRoundData(); if (timestamp < block.timestamp - MAX_TIME_WINDOW) revert OraclePriceExpired();
The problem is that depending on the token and the chain, the same period can be considered too small or too stale.
Let's consider the ezETH/ETH oracles on different chains:
This means that on Arbitrum, 24 hours can be considered too large for the stale period which will cause the function RenzoOracleL2::getMintRate
to return stale data.
Manual review
It is recommanded to store a mapping that would record the hearbeat parameter for the stale period of each token and for every different chain.
Oracle
#0 - c4-judge
2024-05-17T13:18:34Z
alcueca marked the issue as not a duplicate
#1 - c4-judge
2024-05-17T13:18:39Z
alcueca marked the issue as primary issue
#2 - c4-judge
2024-05-17T13:21:24Z
alcueca marked the issue as unsatisfactory: Invalid
#3 - Maroutis
2024-05-26T13:44:27Z
Hi @alcueca, is there a reason why this finding was rejected ? This issue was not caught by the bot. This submission shows that due to the fact that the heartbeat for ezETH/ETH is 6hours on Arbitrum and 24 hours on ethereum, the MAX_TIME_WINDOW
chosen would be considered stale for Arbitrum.
SImilar findings that were marked as medium :
#4 - c4-judge
2024-05-27T10:18:38Z
alcueca removed the grade
#5 - alcueca
2024-05-27T10:19:14Z
You are right, my mistake here.
#6 - c4-judge
2024-05-27T10:19:21Z
alcueca marked the issue as satisfactory
#7 - c4-judge
2024-05-27T10:19:25Z
alcueca marked the issue as selected for report
🌟 Selected for report: t0x1c
Also found by: 0xCiphky, 0xDemon, Bauchibred, DanielArmstrong, FastChecker, MSaptarshi, Maroutis, NentoR, Ocean_Sky, PNS, Rhaydden, SBSecurity, Shaheen, Tigerfrake, ZanyBonzy, atoko, btk, carlitox477, crypticdefense, honey-k12, hunter_w3b, ilchovski, jokr, ladboy233, rbserver, twcctop, umarkhatab_465
1.479 USDC - $1.48
The protocol's RestakeManager::deposit
function lacks adequate slippage protection when minting ezETH to users. This will leave users vulnerable to MEV bots and price manipulation.
In the RestakeManager::deposit
function, the mint amount for ezETH is calculated based on the current TVL and the value of the newly added collateral. However, due to price fluctuations caused by MEV bots, the actual amount of ezETH tokens returned by the function renzoOracle.calculateMintAmount
and received by the user might be significantly lower than expected, leading to financial losses.
//RestakeManager::deposit ... uint256 ezETHToMint = renzoOracle.calculateMintAmount( totalTVL, collateralTokenValue, ezETH.totalSupply() );
Manual review
It is recommended to implement slippage protection by requiring users to provide a minimu amount parameter when calling the RestakeManager::deposit
function.
After calculating the amount of ezEth to mint, the function should check if
this amount meets the user's specified minimum threshold, protecting them from potential losses due to price manipulation.
MEV
#0 - c4-judge
2024-05-17T13:28:48Z
alcueca marked the issue as satisfactory