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: 37/122
Findings: 4
Award: $114.85
🌟 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
https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/Delegation/OperatorDelegator.sol#L327-L335 https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/Delegation/OperatorDelegator.sol#L231-L232 https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/Delegation/OperatorDelegatorStorage.sol#L60-L61
getTokenBalanceFromStrategy
cannot record the queued shares, causing calculateTVLs
to calculate a TVL smaller than the actual value, thereby affecting ezETH's mint
and burn
.
The condition queuedShares[address(this)] == 0
is used in getTokenBalanceFromStrategy
to check if there is a queuedShare.
function getTokenBalanceFromStrategy(IERC20 token) external view returns (uint256) { return queuedShares[address(this)] == 0 ? tokenStrategyMapping[token].userUnderlyingView(address(this)) : tokenStrategyMapping[token].userUnderlyingView(address(this)) + tokenStrategyMapping[token].sharesToUnderlyingView( queuedShares[address(token)] ); }
The issue here is that the index should be address(token)
instead of address(this)
. The index of queuedShares
is the token address, not the user address.
/// @dev mapping of token shares in withdraw queue of EigenLayer mapping(address => uint256) public queuedShares;
// track shares of tokens withdraw for TVL queuedShares[address(tokens[i])] += queuedWithdrawalParams[0].shares[i];
Therefore, queuedShares[address(this)] == 0
is always satisfied, and the value of tokenStrategyMapping[token].sharesToUnderlyingView(queuedShares[address(token)])
cannot be recorded into the TVL.
vscode
queuedShares[address(this)] == 0
-> queuedShares[address(token)] == 0
Error
#0 - c4-judge
2024-05-16T10:43:59Z
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#L226-L250 https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/Withdraw/WithdrawQueue.sol#L298-L309
Users can ignore price fluctuations and exchange ezETH -> colToken at a fixed price. When users exchange more colTokens than the market price, the excess assets are the assets lost by the protocol.
In WithdrawQueue
, withdraw
and claim
are performed asynchronously.
During withdraw
, the amount of colToken
equivalent to the withdrawn ezETH is calculated through renzoOracle.lookupTokenAmountFromValue
, and both the amounts of ezETH and colToken are recorded in WithdrawRequest
, which means that the price of ezETH/colToken is locked during withdraw
.
https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/Withdraw/WithdrawQueue.sol#L226-L250
// update amount in claim asset, if claim asset is not ETH if (_assetOut != IS_NATIVE) { // Get ERC20 asset equivalent amount amountToRedeem = renzoOracle.lookupTokenAmountFromValue( IERC20(_assetOut), amountToRedeem ); } ... // add withdraw request for msg.sender withdrawRequests[msg.sender].push( WithdrawRequest( _assetOut, withdrawRequestNonce, amountToRedeem, _amount, block.timestamp ) );
In claim
, the amounts of ezETH and colToken are directly taken from the WithdrawRequest
for burn
and transfer
, which means that the price at the time of withdrawal is still used.
https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/Withdraw/WithdrawQueue.sol#L298-L309
// burn ezETH locked for withdraw request ezETH.burn(address(this), _withdrawRequest.ezETHLocked); // send selected redeem asset to user if (_withdrawRequest.collateralToken == IS_NATIVE) { payable(msg.sender).transfer(_withdrawRequest.amountToRedeem); } else { IERC20(_withdrawRequest.collateralToken).transfer( msg.sender, _withdrawRequest.amountToRedeem ); }
Suppose the colToken is stETH, and assume that the price at withdraw
is 1 ezETH == 1 stETH. If a user withdraws 100 ezETH, the WithdrawRequest
will record that 100 stETH will be withdrawn.
However, if the price of stETH changes between withdraw
and claim
, for example, if 1 ezETH == 0.8 stETH, theoretically, when burning 100 ezETH in claim
, the user should receive 80 stETH. But since the WithdrawRequest
recorded the previous price, the user can still withdraw 100 stETH, which is more than expected.
Due to the ability of users to choose the timing of executing claims, users can wait until it is advantageous to proceed with the claims.
vscode
Calculate price at the time of claim
.
Context
#0 - c4-judge
2024-05-16T10:57:52Z
alcueca marked the issue as satisfactory
#1 - c4-judge
2024-05-16T11:01:21Z
alcueca marked the issue as duplicate of #326
🌟 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#L317-L319
The totalWithdrawalQueueValue
in calculateTVLs
is calculated incorrectly, causing the TVL to be inconsistent with the actual value, which in turn affected the mint
and burn
of ezETH.
When calculating totalWithdrawalQueueValue
, the first argument collateralTokens[i]
used an incorrect index, it should be j
instead of i
, where i
is the index of OD, not the token.
https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/RestakeManager.sol#L317-L319
for (uint256 i = 0; i < odLength; ) { ... for (uint256 j = 0; j < tokenLength; ) { ... totalWithdrawalQueueValue += renzoOracle.lookupTokenValue( collateralTokens[i], // @audit wrong index i->j collateralTokens[j].balanceOf(withdrawQueue)
This causes totalWithdrawalQueueValue
to record token[i].price * token[j].amount + token[i].price * token[j+1].amount
, instead of token[j].price * token[j].amount + token[j+1].price * token[j+1].amount
.
Vscode
collateralTokens[i]
-> collateralTokens[j]
Error
#0 - c4-judge
2024-05-16T10:31:23Z
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: GalloDaSballo
Also found by: 0xabhay, GoatedAudits, SBSecurity, d3e4, jokr, p0wd3r, peanuts, zhaojohnson
100.9059 USDC - $100.91
https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/Oracle/RenzoOracle.sol#L121-L149
An attacker can exploit the discrepancy to arbitrage, depositing a certain amount and withdrawing more, thus stealing protocol assets. And since the hearbeat of chainlink is 24h, the attacker has plenty of time to execute the attack.
Since there is price discrepancy in chainlink quotes, an attacker can exploit the discrepancy to arbitrage, depositing a certain amount and withdrawing more, thus stealing protocol assets.
A similar vulnerability has been seen in KelpDAO, a program of the same type as Renzo, which is rated as a high vulnerability. https://github.com/code-423n4/2023-11-kelp-findings/issues/584
Currently there are two tokens supported in Renzo, wbETH and stETH, where stETH is quoted using chainlink's stETH/ETH oracle. https://data.chain.link/feeds/ethereum/mainnet/steth-eth
This oracle has a price discrepancy at 0.5%, and heartbeat is 24h, which means that the Chainlink price will not be updated if the stETH price deviates from the correct market price by no more than 0.5% within 24 hours.
The number of ezETH mints is calculated as follows: https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/Oracle/RenzoOracle.sol#L121-L149
/// @dev Given amount of current protocol value, new value being added, and supply of ezETH, determine amount to mint /// Values should be denominated in the same underlying currency with the same decimal precision function calculateMintAmount( uint256 _currentValueInProtocol, uint256 _newValueAdded, uint256 _existingEzETHSupply ) external pure returns (uint256) { // For first mint, just return the new value added. // Checking both current value and existing supply to guard against gaming the initial mint if (_currentValueInProtocol == 0 || _existingEzETHSupply == 0) { return _newValueAdded; // value is priced in base units, so divide by scale factor } // Calculate the percentage of value after the deposit uint256 inflationPercentaage = (SCALE_FACTOR * _newValueAdded) / (_currentValueInProtocol + _newValueAdded); // Calculate the new supply uint256 newEzETHSupply = (_existingEzETHSupply * SCALE_FACTOR) / (SCALE_FACTOR - inflationPercentaage); // Subtract the old supply from the new supply to get the amount to mint uint256 mintAmount = newEzETHSupply - _existingEzETHSupply; // Sanity check if (mintAmount == 0) revert InvalidTokenAmount(); return mintAmount; }
Since Renzo supports multiple tokens, when there are multiple tokens, the price deviation of stETH gives an attacker room for arbitrage, as shown in the following example:
vscode
Oracle
#0 - c4-judge
2024-05-17T13:42:43Z
alcueca marked the issue as duplicate of #326
#1 - c4-judge
2024-05-17T13:42:54Z
alcueca marked the issue as satisfactory
#2 - c4-judge
2024-05-28T04:28:53Z
alcueca marked the issue as not a duplicate
#3 - c4-judge
2024-05-28T04:29:05Z
alcueca marked the issue as duplicate of #424
#4 - c4-judge
2024-05-28T10:49:10Z
alcueca marked the issue as not a duplicate
#5 - c4-judge
2024-05-28T10:49:19Z
alcueca marked the issue as duplicate of #424
#6 - c4-judge
2024-05-30T06:15:51Z
alcueca marked the issue as duplicate of #13
#7 - c4-judge
2024-05-30T06:29:09Z
alcueca changed the severity to 2 (Med Risk)