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: 75/122
Findings: 3
Award: $1.52
🌟 Selected for report: 0
🚀 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
https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/RestakeManager.sol#L318
The total TVL is miscalculated. The issue will cause many unexpected errors.
RestakeManager.sol#calculateTVLs
function is the following.
function calculateTVLs() public view returns (uint256[][] memory, uint256[] memory, uint256) { uint256[][] memory operatorDelegatorTokenTVLs = new uint256[][](operatorDelegators.length); uint256[] memory operatorDelegatorTVLs = new uint256[](operatorDelegators.length); uint256 totalTVL = 0; // Iterate through the ODs uint256 odLength = operatorDelegators.length; // flag for withdrawal queue balance set bool withdrawQueueTokenBalanceRecorded = false; address withdrawQueue = address(depositQueue.withdrawQueue()); // withdrawalQueue total value uint256 totalWithdrawalQueueValue = 0; for (uint256 i = 0; i < odLength; ) { // Track the TVL for this OD uint256 operatorTVL = 0; // Track the individual token TVLs for this OD - native ETH will be last item in the array uint256[] memory operatorValues = new uint256[](collateralTokens.length + 1); operatorDelegatorTokenTVLs[i] = operatorValues; // Iterate through the tokens and get the value of each uint256 tokenLength = collateralTokens.length; for (uint256 j = 0; j < tokenLength; ) { // Get the value of this token uint256 operatorBalance = operatorDelegators[i].getTokenBalanceFromStrategy( collateralTokens[j] ); // Set the value in the array for this OD operatorValues[j] = renzoOracle.lookupTokenValue( collateralTokens[j], operatorBalance ); // Add it to the total TVL for this OD operatorTVL += operatorValues[j]; // record token value of withdraw queue if (!withdrawQueueTokenBalanceRecorded) { totalWithdrawalQueueValue += renzoOracle.lookupTokenValue( 318: collateralTokens[i], collateralTokens[j].balanceOf(withdrawQueue) ); } unchecked { ++j; } } // Get the value of native ETH staked for the OD uint256 operatorEthBalance = operatorDelegators[i].getStakedETHBalance(); // Save it to the array for the OD operatorValues[operatorValues.length - 1] = operatorEthBalance; // Add it to the total TVL for this OD operatorTVL += operatorEthBalance; // Add it to the total TVL for the protocol totalTVL += operatorTVL; // Save the TVL for this OD operatorDelegatorTVLs[i] = operatorTVL; // Set withdrawQueueTokenBalanceRecorded flag to true withdrawQueueTokenBalanceRecorded = true; unchecked { ++i; } } // Get the value of native ETH held in the deposit queue and add it to the total TVL totalTVL += address(depositQueue).balance; // Add native ETH help in withdraw Queue and totalWithdrawalQueueValue to totalTVL totalTVL += (address(withdrawQueue).balance + totalWithdrawalQueueValue); return (operatorDelegatorTokenTVLs, operatorDelegatorTVLs, totalTVL); }
totalWithdrawalQueueValue
means the total sum of values of collaterals in the withdrawQueue
.
But L318
multiplies the value of collateralTokens[i]
with collateralTokens[j].balanceOf(withdrawQueue)
.
Since the value of collateralTokens[i]
differs from the value of collateralTokens[j]
, the totalWithdrawalQueueValue
is miscalculated.
Manual Review
Modify RestakeManager.sol#calculateTVLs
function as follows.
function calculateTVLs() public view returns (uint256[][] memory, uint256[] memory, uint256) { ...... // record token value of withdraw queue if (!withdrawQueueTokenBalanceRecorded) { totalWithdrawalQueueValue += renzoOracle.lookupTokenValue( -- collateralTokens[i], ++ collateralTokens[j], collateralTokens[j].balanceOf(withdrawQueue) ); } ...... }
Error
#0 - c4-judge
2024-05-16T10:27:27Z
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: 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
https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/RestakeManager.sol#L491 https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/RestakeManager.sol#L592 https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/Withdraw/WithdrawQueue.sol#L206
When a user call RestakeManager.sol#depositETH
function, slippage can arise and the user will not receive the expected amount of ezETH.
The same problem also exists in RestakeManager.sol#deposit
and WithdrawQueue.sol#withdraw
functions.
RestakeManager.sol#depositETH
function is the following.
function depositETH(uint256 _referralId) public payable nonReentrant notPaused { // Get the total TVL 594: (, , uint256 totalTVL) = calculateTVLs(); // Enforce TVL limit if set if (maxDepositTVL != 0 && totalTVL + msg.value > maxDepositTVL) { revert MaxTVLReached(); } // Deposit the remaining ETH into the DepositQueue depositQueue.depositETHFromProtocol{ value: msg.value }(); // Calculate how much ezETH to mint 605: uint256 ezETHToMint = renzoOracle.calculateMintAmount( totalTVL, msg.value, ezETH.totalSupply() ); // Mint the ezETH ezETH.mint(msg.sender, ezETHToMint); // Emit the deposit event emit Deposit(msg.sender, IERC20(address(0x0)), msg.value, ezETHToMint, _referralId); }
As can be seen, the above function has no parameter for slippage control.
Therefore, if the prices of collaterals change while the function call stays in the mempool, then totalTVL
will change in L594
and thus ezETHToMint
will change in L605
too.
That is, the slippage can arise and cause damage to the caller.
The same problem also exists in RestakeManager.sol#deposit
and WithdrawQueue.sol#withdraw
functions.
Manual Review
Insert a parameter for slippage control into RestakeManager.sol#depositETH
function as the following.
-- function depositETH(uint256 _referralId) public payable nonReentrant notPaused { ++ function depositETH(uint256 _referralId, uint256 minAmount) public payable nonReentrant notPaused { // Get the total TVL (, , uint256 totalTVL) = calculateTVLs(); // Enforce TVL limit if set if (maxDepositTVL != 0 && totalTVL + msg.value > maxDepositTVL) { revert MaxTVLReached(); } // Deposit the remaining ETH into the DepositQueue depositQueue.depositETHFromProtocol{ value: msg.value }(); // Calculate how much ezETH to mint uint256 ezETHToMint = renzoOracle.calculateMintAmount( totalTVL, msg.value, ezETH.totalSupply() ); ++ if (ezETHToMint < minAmount) { ++ revert; ++ } // Mint the ezETH ezETH.mint(msg.sender, ezETHToMint); // Emit the deposit event emit Deposit(msg.sender, IERC20(address(0x0)), msg.value, ezETHToMint, _referralId); } }
Modify RestakeManager.sol#deposit
and WithdrawQueue.sol#withdraw
functions also in the same way.
Other
#0 - c4-judge
2024-05-17T13:28:51Z
alcueca marked the issue as satisfactory
#1 - c4-judge
2024-05-24T10:26:05Z
alcueca changed the severity to 2 (Med Risk)
🌟 Selected for report: 0xCiphky
Also found by: 0rpse, 0x007, 0xAadi, 14si2o_Flint, ADM, Aamir, Aymen0909, BiasedMerc, DanielArmstrong, Fassi_Security, FastChecker, KupiaSec, LessDupes, MaslarovK, Neon2835, RamenPeople, SBSecurity, Shaheen, Tendency, ZanyBonzy, adam-idarrha, araj, b0g0, baz1ka, bigtone, bill, blutorque, carrotsmuggler, cu5t0mpeo, fyamf, gesha17, gumgumzum, hunter_w3b, inzinko, jokr, josephdara, kennedy1030, kinda_very_good, lanrebayode77, m_Rassska, mt030d, mussucal, tapir, underdog, xg, zzykxx
0.0402 USDC - $0.04
https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/RestakeManager.sol#L562
Depsits of amount which is smaller than bufferToFill
will be reverted.
RestakeManager.sol#deposit
function is the following.
function deposit( IERC20 _collateralToken, uint256 _amount, uint256 _referralId ) public nonReentrant notPaused { // Verify collateral token is in the list - call will revert if not found uint256 tokenIndex = getCollateralTokenIndex(_collateralToken); // Get the TVLs for each operator delegator and the total TVL ( uint256[][] memory operatorDelegatorTokenTVLs, uint256[] memory operatorDelegatorTVLs, uint256 totalTVL ) = calculateTVLs(); // Get the value of the collateral token being deposited uint256 collateralTokenValue = renzoOracle.lookupTokenValue(_collateralToken, _amount); // Enforce TVL limit if set, 0 means the check is not enabled if (maxDepositTVL != 0 && totalTVL + collateralTokenValue > maxDepositTVL) { revert MaxTVLReached(); } // Enforce individual token TVL limit if set, 0 means the check is not enabled if (collateralTokenTvlLimits[_collateralToken] != 0) { // Track the current token's TVL uint256 currentTokenTVL = 0; // For each OD, add up the token TVLs uint256 odLength = operatorDelegatorTokenTVLs.length; for (uint256 i = 0; i < odLength; ) { currentTokenTVL += operatorDelegatorTokenTVLs[i][tokenIndex]; unchecked { ++i; } } // Check if it is over the limit if (currentTokenTVL + collateralTokenValue > collateralTokenTvlLimits[_collateralToken]) revert MaxTokenTVLReached(); } // Determine which operator delegator to use IOperatorDelegator operatorDelegator = chooseOperatorDelegatorForDeposit( operatorDelegatorTVLs, totalTVL ); // Transfer the collateral token to this address _collateralToken.safeTransferFrom(msg.sender, address(this), _amount); // Check the withdraw buffer and fill if below buffer target uint256 bufferToFill = depositQueue.withdrawQueue().getBufferDeficit( address(_collateralToken) ); if (bufferToFill > 0) { bufferToFill = (_amount <= bufferToFill) ? _amount : bufferToFill; // update amount to send to the operator Delegator 549: _amount -= bufferToFill; // safe Approve for depositQueue _collateralToken.safeApprove(address(depositQueue), bufferToFill); // fill Withdraw Buffer via depositQueue depositQueue.fillERC20withdrawBuffer(address(_collateralToken), bufferToFill); } // Approve the tokens to the operator delegator _collateralToken.safeApprove(address(operatorDelegator), _amount); // Call deposit on the operator delegator 562: operatorDelegator.deposit(_collateralToken, _amount); // Calculate how much ezETH to mint uint256 ezETHToMint = renzoOracle.calculateMintAmount( totalTVL, collateralTokenValue, ezETH.totalSupply() ); // Mint the ezETH ezETH.mint(msg.sender, ezETHToMint); // Emit the deposit event emit Deposit(msg.sender, _collateralToken, _amount, ezETHToMint, _referralId); }
When bufferToFill > amount
, _amount
will become zero in L549
.
Therefore, _amount == 0
holds true in L562
.
In the meantime, the OperatorDelegator.sol#deposit
function is the following.
function deposit( IERC20 token, uint256 tokenAmount ) external nonReentrant onlyRestakeManager returns (uint256 shares) { 147: if (address(tokenStrategyMapping[token]) == address(0x0) || tokenAmount == 0) revert InvalidZeroInput(); // Move the tokens into this contract token.safeTransferFrom(msg.sender, address(this), tokenAmount); return _deposit(token, tokenAmount); }
As we can see, when tokenAmount == 0
in L147
, the above function will be reverted.
Manual Review
Modify RestakeManager.sol#deposit
function as follows.
function deposit( IERC20 _collateralToken, uint256 _amount, uint256 _referralId ) public nonReentrant notPaused { // Verify collateral token is in the list - call will revert if not found uint256 tokenIndex = getCollateralTokenIndex(_collateralToken); // Get the TVLs for each operator delegator and the total TVL ( uint256[][] memory operatorDelegatorTokenTVLs, uint256[] memory operatorDelegatorTVLs, uint256 totalTVL ) = calculateTVLs(); // Get the value of the collateral token being deposited uint256 collateralTokenValue = renzoOracle.lookupTokenValue(_collateralToken, _amount); // Enforce TVL limit if set, 0 means the check is not enabled if (maxDepositTVL != 0 && totalTVL + collateralTokenValue > maxDepositTVL) { revert MaxTVLReached(); } // Enforce individual token TVL limit if set, 0 means the check is not enabled if (collateralTokenTvlLimits[_collateralToken] != 0) { // Track the current token's TVL uint256 currentTokenTVL = 0; // For each OD, add up the token TVLs uint256 odLength = operatorDelegatorTokenTVLs.length; for (uint256 i = 0; i < odLength; ) { currentTokenTVL += operatorDelegatorTokenTVLs[i][tokenIndex]; unchecked { ++i; } } // Check if it is over the limit if (currentTokenTVL + collateralTokenValue > collateralTokenTvlLimits[_collateralToken]) revert MaxTokenTVLReached(); } // Determine which operator delegator to use IOperatorDelegator operatorDelegator = chooseOperatorDelegatorForDeposit( operatorDelegatorTVLs, totalTVL ); // Transfer the collateral token to this address _collateralToken.safeTransferFrom(msg.sender, address(this), _amount); // Check the withdraw buffer and fill if below buffer target uint256 bufferToFill = depositQueue.withdrawQueue().getBufferDeficit( address(_collateralToken) ); if (bufferToFill > 0) { bufferToFill = (_amount <= bufferToFill) ? _amount : bufferToFill; // update amount to send to the operator Delegator _amount -= bufferToFill; // safe Approve for depositQueue _collateralToken.safeApprove(address(depositQueue), bufferToFill); // fill Withdraw Buffer via depositQueue depositQueue.fillERC20withdrawBuffer(address(_collateralToken), bufferToFill); } ++ if (_amount > 0) { // Approve the tokens to the operator delegator _collateralToken.safeApprove(address(operatorDelegator), _amount); // Call deposit on the operator delegator operatorDelegator.deposit(_collateralToken, _amount); ++ } // Calculate how much ezETH to mint uint256 ezETHToMint = renzoOracle.calculateMintAmount( totalTVL, collateralTokenValue, ezETH.totalSupply() ); // Mint the ezETH ezETH.mint(msg.sender, ezETHToMint); // Emit the deposit event emit Deposit(msg.sender, _collateralToken, _amount, ezETHToMint, _referralId); }
Math
#0 - c4-judge
2024-05-20T05:06:28Z
alcueca marked the issue as satisfactory
#1 - c4-judge
2024-05-24T10:26:23Z
alcueca changed the severity to 2 (Med Risk)