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: 12/122
Findings: 7
Award: $1,384.21
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: LessDupes
Also found by: 0x73696d616f, KupiaSec, blutorque, ilchovski, kennedy1030, zzykxx
598.5522 USDC - $598.55
It's impossible to withdraw ETH from Eigenlayer because OperatorDelegator::completeQueuedWithdrawal() will always revert due to nonReentrant
modifiers.
When completing a withdrawal for ETH the function OperatorDelegator::completeQueuedWithdrawal() needs to be called, which will internally perform the following calls:
The call flow starts and ends in the same contract: OperatorDelegator
. Both the functions OperatorDelegator::completeQueuedWithdrawal() and OperatorDelegator::receive() have a nonReentrant
modifier, which will make the call revert.
ETH can't be withdrawn from Eigenlayer to Renzo, making it impossible for stakers to exchange their ezETH
for ETH
.
It's possible to verify this by following the flow outlined above. Note that the function EigenPod::withdrawRestakedBeaconChainETH()
(4
) transfers ETH
to the EigenPod
owner, which is the OperatorDelegator
of which the receive()
function will be triggered.
Remove the nonReentrant
modifer from the OperatorDelegator::receive() function.
DoS
#0 - c4-judge
2024-05-16T11:15:52Z
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#L299 https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/Withdraw/WithdrawQueue.sol#L303
Executing WithdrawQueue::claim() to complete the withdrawal process will almost always lead to instant changes in ezETH
valuation.
Redeeming ezETH
for underlying assets involves two steps:
ezETH
from the caller to the contractezETH
valuationezETH
previously transferred1
Because there is an uncapped delay in between step 1
and step 2
, when step 2
is executed the amount of ezETH
burned might not be equivalent to the assets transferred out anymore, which will cause an instant change in the ezETH
valuation.
The valuation of ezETH
should not increase (or decrease) instantly because this is not a situation where the protocol is receiving rewards (or penalties).
The instant increase of ezETH
value can be abused by an attacker by depositing ETH
in the protocol via RestakeManager::depositETH() before a call to WithdrawQueue::claim() and withdrawing after the call.
Important to note that the delay between a call to withdraw()
and claim()
is uncapped and can be very long.
Let's assume the protocol TVL is 2 ETH
and there are a total of 2 ezETH
in circulation, which implies 1 ezETH
is valued at 1 ETH
.
1 ezETH
, the protocol caches the current equivalent amount to be redeemed, 1 ETH
.ezETH
in circulation stays constant, due to rewards. Let's assume the TVL doubled and is now 4 ETH
(unrealistic, but simple math).1 ezETH
and transfers out 1 ETH
.Before step 3
was executed the protocol state was:
4 ETH
TVL2 ezETH
in circulation1 ezETH
= 2 ETH
After step 3
is executed the protocol state is:
3 ETH
TVL1 ezETH
in circulation1 ezETH
= 3 ETH
The valuation of ezETH
instantly increased by 1ETH
after claim()
was executed.
When WithdrawQueue::withdraw() is executed the protocol should:
ezETH
tokensIf this is implemented calls to WithdrawQueue::claim() will have no impact on the ezETH
valuation, because both the amount of ezETH
and the TVL won't change after the call.
Other
#0 - c4-judge
2024-05-16T10:58:47Z
alcueca marked the issue as not a duplicate
#1 - alcueca
2024-05-16T11:37:05Z
The root cause is the valuation of ezETH on withdraw
, while the contract still holds the tokens.
#2 - c4-judge
2024-05-16T11:37:13Z
alcueca marked the issue as duplicate of #326
#3 - c4-judge
2024-05-17T12:48:33Z
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/RestakeManager.sol#L491
The way the protocol is designed allows an attacker to take advantage of a sudden TVL increase. In Renzo a sudden TVL increase might happen for multiple reasons:
DepositQueue
, whose tokens are not accouted for in the protocol TVL, and deposits them in the protocol via the RestakeManager
without minting extra ezETH
tokens.An user can exploit this for profit by minting ezETH
tokens before the TVL increases, by doing the following:
ezETH
tokens via RestakeManager::deposit()ezETH
tokens worth more.ezETH
tokens via WithdrawQueue::withdraw().An attacker can take advantage of a suddent TVL increase to capture extra profit, which leads to fair users earning less than they should.
Alice notices there are validator rewards in an EigenPod, and the rewards can be transferred to Renzo:
ezETH
via RestakeManager::deposit().ezETH
are now valued more, she calls WithdrawQueue::withdraw() to schedule a future withdrawal of her ezETH
.1
, 2
and 3
can be performed atomically. 4
enforces a delay.
Sudden TVL increases are unavoidable because at some point rewards have to be added to the TVL, and oracles updates are discrete. Similar systems generally use a deposit queue. Users should deposit their assets (ex. ETH
) first, and be able to claim their ezETH
after a delay. The amount of ezETH
to mint should be calculated at claim time.
Other
#0 - jatinj615
2024-05-13T21:06:35Z
Similar to - #420
It is the expected behaviour in the protocol as the rewards come in periodically into the protocol the arbitrage opportunities will be there but when the user withdraws there will be a coolDownPeriod in which they won't be earning any rewards.
#1 - C4-Staff
2024-05-15T14:37:34Z
CloudEllie marked the issue as primary issue
#2 - alcueca
2024-05-16T05:25:25Z
The issue exists and is acknowledged by the sponsor. It is debatable how profitable the attack is given the cooldown period, and therefore how large the loss of yield to users.
@jatinj615, note again that a zero cooldown period would make this exploit completely viable.
#3 - c4-judge
2024-05-16T05:31:20Z
alcueca marked issue #326 as primary and marked this issue as a duplicate of 326
#4 - c4-judge
2024-05-17T12:48:06Z
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#L220-L224
The way the protocol is designed allows ezETH
holders to avoid incurring into losses when the protocol TVL has a instant decrease. In Renzo a sudden TVL decrease might happen for multiple (mostly unavoidable) reasons:
wbETH
, mETH
, etc.) has a sudden value drop, this can happen for penalties, slashings or exploits on the LSD side.To avoid the loss, a staker can withdraw his ezETH
via WithdrawQueue::withdraw() before the loss is reflected in the protocol TVL, this is possible because WithdrawQueue::withdraw() calculates and caches the amount of ETH/Tokens to redeem based on the current value of the ezETH
, which doesn't account for losses yet:
...SNIP... @> uint256 amountToRedeem = renzoOracle.calculateRedeemAmount( _amount, ezETH.totalSupply(), totalTVL ); ...SNIP... // add withdraw request for msg.sender withdrawRequests[msg.sender].push( WithdrawRequest( _assetOut, withdrawRequestNonce, @> amountToRedeem, _amount, block.timestamp ) ); ...SNIP...
After a time delay, the staker can call WithdrawQueue::claim() to claim the amount of assets calculated during the execution of WithdrawQueue::withdraw(), which is unaffected by the losses.
Important to note that, in case of penalties/slashings of validators controlled by Renzo both functions used to notify Renzo of the loss (EigenPod::verifyAndProcessWithdrawals() and/or EigenPod::verifyBalanceUpdates()) are permissionless, which allows a staker to avoid the loss by executing the calls atomically.
Stakers can avoid being affected by instant TVL drops, this will also make fair users incur into more losses than they should.
Alice, a staker in the Renzo protocol, is monitoring the beacon chain and notices a validator is being slashed:
ezETH
ezETH
, being unaffected by the slashingThe amount of ETH/Tokens to be claimed should be calculated in WithdrawQueue::claim() instead of WithdrawQueue::withdraw(). This way the funds that are currently queued for withdrawals will be still subject to losses.
Other
#0 - c4-judge
2024-05-17T12:48:03Z
alcueca marked the issue as satisfactory
🌟 Selected for report: zzykxx
Also found by: 0x007, 0xCiphky, GalloDaSballo, GoatedAudits, LessDupes, fyamf, jokr, mt030d
437.259 USDC - $437.26
https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/Bridge/L1/xRenzoBridge.sol#L139
The protocol allows to deposit ETH
/WETH
(or the specific chain native currency) on a supported L2 in order to mint ezETH
tokens, this is the process:
xezETH
on L2s via xRenzoDeposit::deposit() in exchange for either ETH
or WETH
. The xezETH
are minted based on the current ezETH
valuation.ETH
/WETH
collected to the L1, via xRenzoDeposit::sweep(). The funds are transferred to the xRenzoBridge
contract on L1 via Connext.ETH
/WETH
and deposit them in the protocol via RestakeManager::depositETH(). This will mint ezETH
tokens based on the current ezETH
valuation, the ezETH
tokens will then be locked in the lockbox in exchange for xezETH
, which are then immediately burned because an equivalent amount should have already been minted on the L2 during step 1
.Theoretically the amount of xezETH
tokens minted during step 1
should be the same as the amount of ezETH
tokens minted during step 3
, but because on both steps the tokens are minted at the current valuation, and the valuation changes over time, there will be a discrepancy between the amount of xezETH
and ezETH
tokens in circulation.
This is an issue because XERC20Lockbox::withdraw() always exchanges xezETH
for ezETH
1:1.
The price of ezETH
is expected to increase over time. This will create a situation where there will be more xezETH
in circulation than ezETH
, rendering some xezETH
worthless and impossible to redeem for ezETH
.
A situation in which the ezETH
valuation decreases instead of increasing is also problematic, because the protocol will mint less xezETH
than it should.
The discrepancy will become bigger and bigger with time.
As an example, let's assume ezETH
valuation is currently 1 ETH
:
1 ETH
on L2 and receives 1 xezETH
ezETH
increases to 2ETH
(unrealistic, but allows simple math)sweep()
, transferring the 1 ETH
to the xRenzoBridge
contract on L1xRenzoBridge
deposits the received 1 ETH
in the RestakeManager
, which mints 0.5 ezETH
because the ezETH
valuation doubled0.5 ezETH
is locked in the lockbox, 0.5 xezETH
are minted in return and immediately burnedThe situation now is the following:
1 xezETH
on L20.5 ezETH
on L1Alice can at best redeem 0.5 xezETH
in exchange for 0.5 ezETH
, and the remaning 0.5 xezETH
is worthless. The amount of value she can redeem is the correct one, (1 ETH
), but now there are xezETH
in circulation that are not backed by ezETH
.
The protocol should track the amount of xezETH
tokens minted via xRenzoDeposit::deposit() on the L2 and mint the equivalent amount of ezETH
tokens on L1 when xRenzoBridge::xReceive() is executed by passing the necessary data on the xcall() to Connext.
If this is implemented it's possible for the valuation of ezETH
tokens to change instantly after xRenzoBridge::xReceive() is executed, this is because the amount of ezETH
tokens is not minted based on the current valuation anymore. As explained in other reports, the protocol will be subject to instant ezETH
valuation changes (increase/decrease) no matter what (rewards, slashing, penalties), and should gracefully handle these situations via appropriate deposit and withdrawals queues.
Other
#0 - jatinj615
2024-05-14T12:09:03Z
Yes, theoretically it is correct. But the protocol tackles this by sending the updated mint rate of ezETH frequently to L2s and also sweeps funds every hour if above batch size keeping the collateralization in place for ezETH. Also the bridgeRouterFee (5 bps) deducted on L2 if the transactions goes through slow path (during which mint rate of ezETH can rise) the extra 5 bps routerFee deducted on L2 is accumulated in minting ezETH on L1 in xRenzoBridge::xReceive
instead of gettign deducted by connext routers.
#1 - C4-Staff
2024-05-15T16:23:19Z
CloudEllie marked the issue as primary issue
#2 - c4-judge
2024-05-16T08:47:59Z
alcueca marked the issue as satisfactory
#3 - c4-judge
2024-05-16T08:49:52Z
alcueca marked issue #14 as primary and marked this issue as a duplicate of 14
#4 - c4-judge
2024-05-16T08:52:35Z
alcueca marked the issue as selected for report
#5 - alcueca
2024-05-16T08:53:14Z
Special mention to #14 for showing the lockbox does have more ezETH than it should in production.
#6 - alcueca
2024-05-23T13:45:09Z
The long term effect of this is akin to bad debt in lending protocols. Eventually, the protocol or the (last) users need to assume the losses.
#7 - 0xTenma
2024-05-26T06:27:19Z
Hi @alcueca ! Thanks for judging this contest. I think severity of this issue should be Medium instead of High because there is no direct loss of funds possible here. If the ratio of xezETH to ezETH is not equal 1:1 in any case then it is only possible and the protocol sends the updated mint rate of ezETH frequently to L2s to avoid this situation. Furthermore, According to C4 docs:
2 — Med: Assets not at direct risk, but the function of the protocol or its availability could be impacted, or leak value with a hypothetical attack path with stated assumptions, but external requirements.
Stated assumptions and external requirement such as oracle delay are required to face to this issue. That's why I believe severity should be medium instead of high.
#8 - jatinj615
2024-05-26T07:30:42Z
@alcueca , to add more to it as I have explained in the comment above and discussion in #70. The ezETH in prod is over collateralised due to the fact that in some cases (when connext routers don't have enough liquidity to process fast path) the 5bps deducted on L2 is used to collateralise ezETH on L1 which is why lockbox currently has more ezETH againstthe xezETH minted on L1. Team has been monitoring and keeping a track on it.
Considering the above argument, please confirm on the severity.
#9 - 0xCiphky
2024-05-26T18:27:19Z
Respectfully, I believe this should remain as High severity:
#10 - 0xTenma
2024-05-26T19:59:32Z
- The finding clearly demonstrates a realistic scenario where the system could become insolvent, resulting in a loss of funds for users. Despite the current over-collateralization and precautions, there remains a long-term risk.
- The audited implementation contains blockages that will affect efforts to maintain collateralization for ezETH. For example, if the MaxTVL is reached, the protocol cannot deposit in L1, causing an accumulation of minted L2 tokens until it becomes possible to deposit again, which could take a significant amount of time.
- This issue can be intentionally exploited by users whenever the price on the L1 side is higher than on the L2 side.
With due respect, All of these points mentioned are possible if the ratio of xezETH to ezETH is not equal 1:1 (basically depeg).
A/c to C4 docs, High Severity is defined as:
3 — High: Assets can be stolen/lost/compromised directly (or indirectly if there is a valid attack path that does not have hand-wavy hypotheticals).
Here I don't see any valid attack path that lead to Assets be stolen/lost/compromised. I think the ratio of xezETH to ezETH is not equal 1:1 falls under Stated assumptions and external requirement as we are assuming the value ratio changes. Should be medium severity.
🌟 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
RestakeManager::calculateTVLs() calculates the value of the tokens held in the protocol by, among other things, calculating the value of all tokens held in the WithdrawQueue
contract and summing them.
This calculation is performed in a nested loop, but the indices used to calculate the values are incorrect. The function uses the index i
instead of j
:
if (!withdrawQueueTokenBalanceRecorded) { totalWithdrawalQueueValue += renzoOracle.lookupTokenValue( @> collateralTokens[i], collateralTokens[j].balanceOf(withdrawQueue) ); }
It calculates the value of the tokens in the WithdrawQueue
based on the value of collateralTokens[i]
instead of collateralTokens[j]
.
In this case i
will always be 0
, the code it's only executed once because of the if statement. Depending on the valuation of the token collateralTokens[0]
and the amount of tokens collateralTokens[j]
in the WithdrawQueue
contract the TVL calculation could end up either being higher or lower than expected.
The TVL will be calculated incorrectly. An attacker can move funds in/out from/to the WithdrawQueue
in order to manipulate the TVL for profit.
Let's suppose:
collateralTokens[0]
is valued at 1000$
collateralTokens[j]
is valued at 990$
If an attacker transfers 5 collateralTokens[j]
to the WithdrawQueue
, which is worth 5 * $990 = $4950
, the TVL will increase by 5 * 1000$ = 5000$
, an overvaluation of $50
. An attacker can move tokens in such a way to overvalue the TVL before withdrawing and undervalue the TVL before depositing, which can be achieved either by withdrawing tokens and/or by depositing tokens if the withdrawal buffer needs to be filled.
At RestakeManager.sol#L318 use j
index:
totalWithdrawalQueueValue += renzoOracle.lookupTokenValue( @> collateralTokens[j], collateralTokens[j].balanceOf(withdrawQueue) );
Error
#0 - c4-judge
2024-05-16T10:34:38Z
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:21Z
alcueca changed the severity to 3 (High Risk)
🌟 Selected for report: guhu95
Also found by: Bauchibred, LessDupes, ilchovski, zzykxx
347.9473 USDC - $347.95
Judge has assessed an item in Issue #547 as 2 risk. The relevant finding follows:
[9] Adding too many operator delegators and/or collateral tokens could DOS the protocol
#0 - c4-judge
2024-05-24T09:50:52Z
alcueca marked the issue as duplicate of #514
#1 - c4-judge
2024-05-24T09:50:58Z
alcueca marked the issue as satisfactory
🌟 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#L558-L562
The function RestakeManager::deposit() checks if the WithdrawQueue
has a buffer deficit, and sends tokens to it when necessary:
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); }
When the WithdrawQueue
buffer deficit is bigger (or equal) to the amount of assets being deposited all of the deposit will be transferred to the WithdrawQueue
and because there are no assets left, the variable _amount
will be set to 0
.
After this the function tries to deposit 0
tokens in the OperatorDelegator
:
..SNIP.. // Approve the tokens to the operator delegator _collateralToken.safeApprove(address(operatorDelegator), _amount); // Call deposit on the operator delegator operatorDelegator.deposit(_collateralToken, _amount); ..SNIP..
but the call to OperatorDelegator::deposit() will revert when the amount that is being deposited is 0
:
if (address(tokenStrategyMapping[token]) == address(0x0) || tokenAmount == 0) revert InvalidZeroInput();
It's impossible to deposit token amounts that are smaller or equal to the current WithdrawQueue
buffer deficit.
Don't perform the deposit to the operator delegator when the amount to be deposited is 0
:
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); }
Keep in mind that deposits being 0
is not the only issue here, small amounts of deposit might also revert on Eigenlayer side if the amount of shares minted from the deposit are 0
.
DoS
#0 - c4-judge
2024-05-20T05:03:14Z
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
Operators can undelegate shares delegated to them via DelegationManager::undelegate() in Eigenlayer. An operator can undelegate all of the shares delegate to him by Renzo OperatorDelegator(s)
, which will cause an instant drop in the TVL, potentially causing damage.
There are two instances of this:
RenzoOracleL2::getMintRate() always reverts if the reported price ezETH
price is less than 1 ether.
Altought unlikely, it's possible for the price of ezETH
to drop below 1 ether.
If this happens, prices on L2s won't be able to be reported anymore, and minting of xezETH
tokens will be bricked.
xRenzoDeposit::_updatePrice() always reverts if the reported price change differs by at 10% from the previous one. Altought unlikely, it's possible for the price of ezETH
to swing 10%. A big slashing event might cause a 10% drop, if this happens prices on L2 will become (and stay) stale until the ezETH
goes back within a 10% range of the last reported price.
Introduce admin functionality to adjust the oracle contraints instead of using hardcoded values.
xRenzoDeposit
xRenzoDeposit
collects router fees and leaves them in the contract in order to be withdrawn later. The system doesn't track the amount of router fees that are being collected and the only way to withdraw them is via xRenzoDeposit::recoverERC20.
Introduce a proper router fees accouting mechanism, and funcionality to withdraw router fees.
EnumerableSetUpgradeable
return values are never checkedThe openzeppelin EnumerableSetUpgradeable
library doesn't revert when trying to add an item that already exists to a set, but returns false
instead.
The contract XERC20Factory
uses the library in two instances, both times without ensuring the returned value is true
:
Require the returned value to be true
and revert if this is not the case.
stakeEthFromQueue()
allows to deposit twice in the same validatorDepositQueue::stakeEthFromQueue() is admin callable function used to take ETH from the DepositQueue
and deposit them into an OperatorDelegator
's validator. The OperatorDelegator
to deposit to is passed by an admin as an input parameter, but the function never checks if the selected OperatorDelegator
validator has already been deposited into.
If an OperatorDelegator
validator gets two deposits, the extra 32 ETH
will be queued as a partial withdrawal by the beacon chain, causing the Renzo protocol to collect the "rewards" fee over it, causing a loss to users.
In DepositQueue::stakeEthFromQueue() ensure the selected validator has never been deposited into before.
approve
used instead of safeApprove
in sweepERC20()
In DepositQueue::sweepERC20() approvals are given via approve()
. This might lead to the function reverting in case the asset being transferred is not fully ERC20 compliant.
Use safeApprove()
instead.
RenzoOracle
always uses ~24 hours to ensure a price feed is not expiredThe functions:
Both reverts if the last price update for the given token was performed more than MAX_TIME_WINDOW
(24 hours + 60 seconds) seconds before the function execution. Different chainlink price feeds can have different heartbeat updates. For instance a chainlink price feed with an heartbeat of 1 hour should be considered stale after 1 hour passed, instead of 24 hours.
Introduce a mapping that uses the correct heartbeat for each supported chainlink price feed to ensure the price is not stale.
The functions RestakeManager::addOperatorDelegator() and RestakeManager::removeOperatorDelegator() allow to add and remove operator delegators to Renzo.
Both functions don't ensure that operators added and removed are currently holding no shares. If an operator delegator that is currently holding shares is added, the TVL will instantly increase. If an operator delegator that is currently holding shares is removed, the TVL will instantly decrease.
Both functions should ensure that the operator delegators that are being added/removed should not hold shares. This is especially true for operator delegators being added. Operator delegator being removed might need to be removed no-matter-what for being deemed malicious.
A critical component of the protocol is the TVL calculation, which is perfomed in multiple instances:
To perform the calculations, the protocol loops over all of the operator delegators, and for each operator delegator it loops over all of the collateral tokens. Both the amount of collateral tokens and operator delegators that can be added to the protocol is unbounded. This could cause the TVL calculation to run out of gas, DOSing all of the protocol operations.
To understand what the maximum caps should be it's necessary to perform some tests to understand the gas usage of the protocol.
RestakeManager::setOperatorDelegatorAllocation() allows to set the percentage of TVL that an operator delegator is expected to manage. The function never ensures the sum of the TVL allocations is 100%:
If the sum of TVLs allocation is not 100% (either higher or lower), the extra TVL will always be allocated to the first operator in the operatorDelegators
array, which is unfavourable to other operator delegators.
Change RestakeManager::setOperatorDelegatorAllocation() into a function that accepts as input an array of uint256
, which should be the same lenght as the current amount of operator delegators in the protocol. Each value in the array represents the TVL percentage to be allocated to the respective operator delegator. The function should ensure the sum of these values to always be 100%.
ReentrancyGuardUpgradeable
contract is not initialized in WithdrawQueue
The function WithdrawQueue::initialize() doesn't initialize the ReentrancyGuardUpgradeable
.
Add the following to the function:
__ReentrancyGuard_init();
Withdrawals requests are tracked via an array, withdrawRequests
.
The function WithdrawQueue::claim() accepts as input the position in the array of the withdrawal request to be claimed. When the claim is perfomed the corresponding element in the array is deleted by swapping it with the last element, and then popping the array.
This might lead to unexpected results when two claims are perfomed at about the same time. It's impossible to predict which transaction will be executed first, and once a transaction is executed the elements in the array are swapped around, leading to the second transaction potentially claiming a different withdrawal than the intended one.
#0 - CloudEllie
2024-05-13T14:04:49Z
#1 - c4-judge
2024-05-24T09:51:20Z
alcueca marked the issue as grade-a