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: 31/122
Findings: 3
Award: $260.01
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: Aymen0909
Also found by: 0x73696d616f, GoatedAudits, LessDupes, crypticdefense, eeshenggoh, gjaldon, gumgumzum, pauliax, tapir
257.3101 USDC - $257.31
https://github.com/code-423n4/2024-04-renzo/blob/519e518f2d8dec9acf6482b84a181e403070d22d/contracts/Delegation/OperatorDelegator.sol#L300 https://github.com/code-423n4/2024-04-renzo/blob/519e518f2d8dec9acf6482b84a181e403070d22d/contracts/Deposits/DepositQueue.sol#L137
Renzo has a buffer that acts as a pool of assets to support withdrawal requests. There is a need for buffer in order to support withdrawal of assets.
The withdrawQueue contract get filled by 3 ways ->
If options 1 and 2 are not sufficient to fulfill withdraw requests of Users then admin accounts will manually unstake from EigenLayer through 2 step process:
// OperatorDelegator.sol function completeQueuedWithdrawal( IDelegationManager.Withdrawal calldata withdrawal, IERC20[] calldata tokens, uint256 middlewareTimesIndex ) external nonReentrant onlyNativeEthRestakeAdmin { -- SNIP -- restakeManager.depositQueue().fillERC20withdrawBuffer( //@audit address(tokens[i]), bufferToFill ); -- SNIP --
The fillERC20withdrawBuffer
is called to fill up ERC20 withdraw buffer. However the function call will revert because that function required to be called by the restake manager admin. In Renzo, each and every roles are unique and have their own functionalities to perform. This means that whenever there is a need to fill up the buffer, the 3rd option will not be able to do so, bricking the protocol if there isn't enough assets to cover the withdrawal requests.
Furthermore, the Operator will not be able to unstake delegated funds from Eigenlayer. Assets remain frozen due to the inability to complete the steps required for withdrawal.
// DepositQueue.sol /// @dev Allows only the RestakeManager address to call functions modifier onlyRestakeManager() { if (msg.sender != address(restakeManager)) revert NotRestakeManager(); _; } function fillERC20withdrawBuffer( address _asset, uint256 _amount ) external nonReentrant onlyRestakeManager { //@audit -- SNIP --
Manual Review
Remove the access control for only restake manager, leaving the function similar to a donation function.
function fillERC20withdrawBuffer( address _asset, uint256 _amount -- ) external nonReentrant onlyRestakeManager { ++ ) external nonReentrant {
Access Control
#0 - c4-judge
2024-05-20T04:07:53Z
alcueca marked the issue as satisfactory
🌟 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 calculateTVLs()
function in RestakeManager calculates TVLs for each operator delegator per token, total per OD, and total for the protocol.
It employs two for loops using elements i
and j
, where i
is used for the outer loop and j
for the inner loop. The function calls RenzoOracle::lookupTokenValue()
to get the asset value in the underlying currency based on a single token and balance.
However, due to always using the outer loop element i
, it only loops through the first collateral token, resulting in the return value for that token. This affects the totalWithdrawalQueueValue
and the overall asset value locked.
For example, with whitelisted tokens [ezETH, stETH, wBETH], using i
for ezETH
means the loop always uses the ezETH
price feed. This can lead to incorrect calculations when considering the amounts and price feeds of other tokens like stETH
and wBETH
.
Whenever user submit withdraw request, it will always produce the wrong amount converted to underlying asset.
This can produce false results in the total value locked calculations, impacting crucial state variables like the amount to redeem in ETH/LST.
function calculateTVLs() public view returns (uint256[][] memory, uint256[] memory, uint256) { -- SNIP -- if (!withdrawQueueTokenBalanceRecorded) { totalWithdrawalQueueValue += renzoOracle.lookupTokenValue( collateralTokens[i], collateralTokens[j].balanceOf(withdrawQueue) //@audit redundant since it was already added in getTokenBalanceFromStrategy ); } -- SNIP -- // Add native ETH help in withdraw Queue and totalWithdrawalQueueValue to totalTVL totalTVL += (address(withdrawQueue).balance + totalWithdrawalQueueValue);
Manual Review
function calculateTVLs() public view returns (uint256[][] memory, uint256[] memory, uint256) { if (!withdrawQueueTokenBalanceRecorded) { totalWithdrawalQueueValue += renzoOracle.lookupTokenValue( -- collateralTokens[i], ++ collateralTokens[j], collateralTokens[j].balanceOf(withdrawQueue) //@audit redundant since it was already added in getTokenBalanceFromStrategy ); }
Loop
#0 - c4-judge
2024-05-16T10:31:18Z
alcueca marked the issue as satisfactory
#1 - c4-judge
2024-05-16T10:38:47Z
alcueca changed the severity to 2 (Med Risk)
#2 - c4-judge
2024-05-16T10:39:08Z
alcueca changed the severity to 3 (High Risk)
#3 - c4-judge
2024-05-20T04:26:26Z
alcueca changed the severity to 2 (Med Risk)
#4 - c4-judge
2024-05-23T13:47:20Z
alcueca changed the severity to 3 (High Risk)
🌟 Selected for report: zigtur
Also found by: 0x73696d616f, 0xBeastBoy, 0xCiphky, Aymen0909, FastChecker, LessDupes, NentoR, Sathish9098, TECHFUND, TheFabled, ak1, bigtone, cu5t0mpeo, eeshenggoh, guhu95, ilchovski, josephdara, ladboy233, mt030d, oakcobalt, rbserver, t0x1c, tapir, xg
2.6973 USDC - $2.70
https://github.com/code-423n4/2024-04-renzo/blob/519e518f2d8dec9acf6482b84a181e403070d22d/contracts/Withdraw/WithdrawQueue.sol#L206 https://github.com/code-423n4/2024-04-renzo/blob/519e518f2d8dec9acf6482b84a181e403070d22d/contracts/Withdraw/WithdrawQueue.sol#L279
The protocol can be paused by the admin DEPOSIT_WITHDRAW_PAUSER
and WITHDRAW_QUEUE_ADMIN
for DepositQueue.sol
and WithdrawalQueue.sol
. It allows the admin to pause/unpause deposits and withdraws. Users are not allowed to deposit or withdraw when paused. However, the user is still able to withdraw even if protocol is paused.
The WithdrawalQueue::withdraw()
and WithdrawalQueue::claim()
did not enforce the Openzeppelins whenNotPaused
modifier. This means that if the admin were to call the WithdrawalQueue::pause()
, it does not affect the withdraw flow. Hence, users are still able to submit withdrawal request and withdraw their underlying assets after 7 days.
function withdraw(uint256 _amount, address _assetOut) external nonReentrant { function claim(uint256 withdrawRequestIndex) external nonReentrant {
Users can withdraw underlying assets even if the Renzo is paused.
Manual Review
Include the whenNotPaused
modifier to vital functions.
-- function withdraw(uint256 _amount, address _assetOut) external nonReentrant { ++ function withdraw(uint256 _amount, address _assetOut) external nonReentrant whenNotPaused{ -- function claim(uint256 withdrawRequestIndex) external nonReentrant { ++ function claim(uint256 withdrawRequestIndex) external nonReentrant whenNotPaused{
Access Control
#0 - c4-judge
2024-05-16T10:48:13Z
alcueca changed the severity to 2 (Med Risk)
#1 - c4-judge
2024-05-16T10:50:31Z
alcueca marked the issue as satisfactory