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: 20/122
Findings: 7
Award: $630.73
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: LessDupes
Also found by: 0rpse, 0xAadi, 0xCiphky, 0xhacksmithh, 0xnightfall, FastChecker, KupiaSec, NentoR, SBSecurity, Tendency, adam-idarrha, aman, araj, baz1ka, bigtone, fyamf, jokr, kennedy1030, maxim371, mussucal, p0wd3r, zigtur
13.5262 USDC - $13.53
The getTokenBalanceFromStrategy()
function returns a small value than it should be, leading to falling down the exchange rate of ezETH
, which is loss of funds to the ezETH
holders.
Let's consider L329
of the getTokenBalanceFromStrategy()
function. Since queuedShares
is a mapping of token shares in the withdrawal queue, queuedShares[address(this)]
is always 0. Actually, it should be queuedShares[address(token)]
. As a result, the function returns only tokenStrategyMapping[token].userUnderlyingView(address(this))
, not tokenStrategyMapping[token].sharesToUnderlyingView(queuedShares[address(token)])
, even though there are some queued amounts of token
. Consequently, this will impact the TVL and cause the exchange rate of ezETH
to be lower than it should be, leading to incorrect actions of the entire protocol, such as a loss of funds for the ezETH
holders.
function getTokenBalanceFromStrategy(IERC20 token) external view returns (uint256) { return 329 queuedShares[address(this)] == 0 ? tokenStrategyMapping[token].userUnderlyingView(address(this)) : tokenStrategyMapping[token].userUnderlyingView(address(this)) + tokenStrategyMapping[token].sharesToUnderlyingView( queuedShares[address(token)] ); }
Manual review
At L329
, address(this)
should be replaced to address(token)
.
function getTokenBalanceFromStrategy(IERC20 token) external view returns (uint256) { return - queuedShares[address(this)] == 0 + queuedShares[address(token)] == 0 ? tokenStrategyMapping[token].userUnderlyingView(address(this)) : tokenStrategyMapping[token].userUnderlyingView(address(this)) + tokenStrategyMapping[token].sharesToUnderlyingView( queuedShares[address(token)] ); }
Context
#0 - c4-judge
2024-05-16T10:44:59Z
alcueca marked the issue as satisfactory
🌟 Selected for report: LessDupes
Also found by: 0x73696d616f, KupiaSec, blutorque, ilchovski, kennedy1030, zzykxx
598.5522 USDC - $598.55
ETH withdrawal does not work, withdrawn ETH assets are stuck in EigenLayer.
When withdrawals happen, completeQueuedWithdrawal
is called to claim withdrawn assets from EigenLayer, which calls completeQueuedWithdrawal
function of EigenLayer as well.
If native tokens(ETH) are included in the withdrawal batch, the EigenLayer tries to send ETH to the OperatorDelegator
contract, which will call receive
function of the contract.
The problem here is that both completeQueuedWithdrawal
function and receive
function has nonReentrant
modifier, thus receiving ETH through receive
function will revert.
function completeQueuedWithdrawal( IDelegationManager.Withdrawal calldata withdrawal, IERC20[] calldata tokens, uint256 middlewareTimesIndex @> ) external nonReentrant onlyNativeEthRestakeAdmin { // ... } @> receive() external payable nonReentrant { // ... }
Manual Review
nonReentrant
modifier should not be used in receive
function.
DoS
#0 - c4-judge
2024-05-16T11:16:55Z
alcueca marked the issue as satisfactory
🌟 Selected for report: guhu95
Also found by: 0rpse, 0x007, 0x73696d616f, 0xCiphky, 0xabhay, Audinarey, Bauchibred, Fassi_Security, GalloDaSballo, GoatedAudits, KupiaSec, LessDupes, MSaptarshi, OMEN, Ocean_Sky, RamenPeople, SBSecurity, Tendency, WildSniper, aslanbek, bill, blutorque, crypticdefense, cu5t0mpeo, d3e4, gjaldon, grearlake, gumgumzum, honey-k12, ilchovski, jokr, josephdara, kennedy1030, p0wd3r, peanuts, stonejiajia, t0x1c, tapir, underdog, zzykxx
0.4071 USDC - $0.41
https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/Withdraw/WithdrawQueue.sol#L206-L263 https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/Withdraw/WithdrawQueue.sol#L279-L312 https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/RestakeManager.sol#L274-L358
Assets that are included in queued withdrawals still participate in TVL, leading to incorrect exchange rate. Consequently, later withdrawers could take fewer assets, even none at all.
Let's consider following scenario:
stETH
is 1
. And following users deposit assets and receive ezETH
.
100 stETH
, 100 ezETH
,96 ETH
, 96 ezETH
,4 ETH
, 4 ezETH
. Then the state is:ezETH
: 200
,200
,ezETH
: 1
.stETH
with her 100 ezETH
, then she can claim 100 stETH
after a delay. And now, the state doesn't change yet.stETH
increases to 1.1
. Then the state becomes:
ezETH
: 200
,100 * 1.1 + 100 = 210
,ezETH
: 210 / 200 = 1.05
.96 ezETH
, then he can claim 1.05 * 96 = 100 ETH
after a delay.100 stETH
and Bob claims 100 ETH
. Then the state becomes:
ezETH
: 4
,0
.Finally, Charlie can take nothing even though he has 4 ezETH
.
This problem occurs because the burnning ezETH
is at L299 of the claim()
function, not in the withdraw
function so that assets to be claimed still participate to TVL and impact the exchange rate.
Manual review
Burnning ezETH
should be in the withdraw
function, not in the claim()
function. And, RestakeManager.calculateTVLs()
function should consider the requested withdrawal amounts.
function withdraw(uint256 _amount, address _assetOut) external nonReentrant { [...] // transfer ezETH tokens to this address IERC20(address(ezETH)).safeTransferFrom(msg.sender, address(this), _amount); + ezETH.burn(address(this), _amount); [...] }
function claim(uint256 withdrawRequestIndex) external nonReentrant { [...] // burn ezETH locked for withdraw request - ezETH.burn(address(this), _withdrawRequest.ezETHLocked); [...] }
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].balanceOf(withdrawQueue) + IWithdrawQueue(withdrawQueue).getAvailableToWithdraw(address(collateralTokens[j])) ); } [...] // Add native ETH help in withdraw Queue and totalWithdrawalQueueValue to totalTVL - totalTVL += (address(withdrawQueue).balance + totalWithdrawalQueueValue); + totalTVL += ( + IWithdrawQueue(withdrawQueue).getAvailableToWithdraw(IS_NATIVE) + + totalWithdrawalQueueValue + ); return (operatorDelegatorTokenTVLs, operatorDelegatorTVLs, totalTVL); }
Context
#0 - c4-judge
2024-05-16T10:57:10Z
alcueca marked the issue as not a duplicate
#1 - c4-judge
2024-05-16T10:57:29Z
alcueca marked the issue as not a duplicate
#2 - alcueca
2024-05-16T11:38:14Z
Wrong title, but the content is correct.
#3 - c4-judge
2024-05-16T11:38:25Z
alcueca marked the issue as duplicate of #326
#4 - c4-judge
2024-05-17T12:48:35Z
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
https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/RestakeManager.sol#L274-L358
Incorrect calculation of totalWithdrawalQueueValue
results in incorrect return value of the calculateTVLs()
function, will impact the exchange rate of ezETH
and result in incorrect actions throughout the entire protocol.
The totalWithdrawalQueueValue
is added by the value of collateral tokens in WithdrawQueue
contract by looping all collateral tokens. However as you can see at L318
, at the j
th loop, the first parameter of the function lookupTokenValue()
is the i
th collateral token, not the j
th collateral token. As a result, totalWithdrawalQueueValue
is differ from it should be, leading to incorrect return value of calculateTVLs()
and incorrect action of the entire system.
function calculateTVLs() public view returns (uint256[][] memory, uint256[] memory, uint256) { [...] // record token value of withdraw queue if (!withdrawQueueTokenBalanceRecorded) { totalWithdrawalQueueValue += renzoOracle.lookupTokenValue( 318 collateralTokens[i], collateralTokens[j].balanceOf(withdrawQueue) ); } [...] }
Manual review
The index i
should be replaced to j
.
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) ); } [...] }
Context
#0 - c4-judge
2024-05-16T10:38:17Z
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: 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#L491-L576
No zero amount check could result in unfair reversal of the transaction.
At L562
of the RestakeManager.deposit()
function, if _amount = 0
then it will be reverted. It is quite possible for the _amount
to become 0. In fact, at the begining of the RestakeManager.deposit()
function, the _amount
is not 0. However, during filling buffer, the _amount
could become 0 at L549
. Then, the whole transaction will be reverted.
function deposit( IERC20 _collateralToken, uint256 _amount, uint256 _referralId ) public nonReentrant notPaused { [...] // 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); [...] }
Manual review
There should be a zero amount check before depositting to operatorDelegator
.
function deposit( IERC20 _collateralToken, uint256 _amount, uint256 _referralId ) public nonReentrant notPaused { [...] // Call deposit on the operator delegator + if(_amount > 0) { 562 operatorDelegator.deposit(_collateralToken, _amount); + } [...] }
DoS
#0 - c4-judge
2024-05-20T05:14:56Z
alcueca marked the issue as satisfactory
#1 - c4-judge
2024-05-24T10:26:23Z
alcueca changed the severity to 2 (Med Risk)
18.1958 USDC - $18.20
Judge has assessed an item in Issue #50 as 2 risk. The relevant finding follows:
[L-06] When removing a collateral token, it should validate if the token is not used across all operator delegators
#0 - c4-judge
2024-05-24T09:17:34Z
alcueca marked the issue as duplicate of #5
#1 - c4-judge
2024-05-24T09:17:37Z
alcueca marked the issue as satisfactory
🌟 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
DepositQueue
contract should use tx.origin
instead of msg.sender
If the caller is the contract, the actual gas payer is tx.origin
, thus the gas should be refunded to tx.origin
OperatorDelegator
contract should use tx.origin
instead of msg.sender
Same reason as L-01
RenzoOracleL2
should check for negative price returnedhttps://github.com/code-423n4/2024-04-renzo/blob/519e518f2d8dec9acf6482b84a181e403070d22d/contracts/Bridge/L2/Oracle/RenzoOracleL2.sol#L54
In getMintRate
function, it only checks price staleness without negativity. If price is negative, it returns a huge number because it is converted into uint256.
https://github.com/code-423n4/2024-04-renzo/blob/519e518f2d8dec9acf6482b84a181e403070d22d/contracts/RestakeManager.sol#L700
In the code snippet above, the totalRewards adds the ETH balance of EigenLayer pods, this is incorrect because the ETH balance also includes the withdrawn balance of exited operators. It should be operatorDelegators[i].eigenPod().nonRestakedBalance
Even though this is called by an admin, non validating if operator delegator is completely exit leads to incorrect calculation of TVL
Same reason as L-05
initialize
function of RestakeManager
contractsweepERC20
function of DepositQueue
contract does not fill withdraw bufferhttps://github.com/code-423n4/2024-04-renzo/blob/main/contracts/Deposits/DepositQueue.sol#L254-L277
The function DepositQueue.sweepERC20()
use reward tokens only to redeposit to the operator delegators, not to fill buffer.
OperatorDelegator.setTokenStrategy()
functionhttps://github.com/code-423n4/2024-04-renzo/blob/main/contracts/RestakeManager.sol#L106-L114
There is no check for the _token
to match the underlying token of the _strategy
in the setTokenStrategy()
function. If the strategy of the given token is set incorrectly, then depositting that collateral token will always be reverted.
#0 - c4-judge
2024-05-24T09:18:02Z
alcueca marked the issue as grade-b