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: 57/122
Findings: 3
Award: $13.57
🌟 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
Incorrect calculation of TVL can lead to minting/burning incorrect amount of shares to user.
OperatorDelegator.getTokenBalanceFromStrategy()
function is supposed to return
underlying token amount from the amount of shares + queued withdrawal shares
Condition whether to return just underlying token amount from the amount of shares or underlying token amount from the amount of shares with queued withdrawal shares is incorrect:
/// @dev Gets the underlying token amount from the amount of shares + queued withdrawal shares 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)] ); }
In fact, it checks if queuedShares
mapping has address of this contract (OperatorDelegator
). This is wrong and doesn't make any sense as OperatorDelegator
is not an ERC20 token and can not be deposited into strategy.
Correctly check whether OperatorDelegator
has queued shares for token
or not:
@@ -326,7 +335,7 @@ contract OperatorDelegator is /// @dev Gets the underlying token amount from the amount of shares + queued withdrawal shares 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(
Other
#0 - c4-judge
2024-05-16T10:44:37Z
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
RestakeManager.calculateTVLs()
calculates incorrect amount of TVL.
Calculating totalWithdrawalQueueValue for incorrect token can lead to different TVL than it's supposed to be which may result in getting different amount of shares. Can be can be negative for the user (if price of collateralTokens[i]
is greater than collateralTokens[j]
) or can be negatve for the protocol (if price of collateralTokens[i]
is smaller than collateralTokens[j]
).
Values returned from RestakeManager.calculateTVLs()
areused in taking calculation of how many shares would be minted/burned to user.
However, when this function calculates value of tokens (in ETH) in WithdrawQueue
it gets incorrect address of token to get value for.
// record token value of withdraw queue if (!withdrawQueueTokenBalanceRecorded) { totalWithdrawalQueueValue += renzoOracle.lookupTokenValue( @> collateralTokens[i], collateralTokens[j].balanceOf(withdrawQueue) ); }
Instead of getting value of token in current loop iteration, it gets token value of incorrect collateralTokens[i]
. In this case index i
is always 0
because it's index from outer loop. That means it calculates token value of first token with amount of token it should have been calculated. RestakeManager.calculateTVLs()
is used in RestakeManager.deposit()
, RestakeManager.depositETH()
, RestakeManager.depositTokenRewardsFromProtocol()
, BalancerRateProvider.getRate()
, WithdrawQueue.withdraw()
which are main fund flow functions and they are dependent on correct return value from RestakeManager.calculateTVLs()
.
Get collateral token from array by correct index:
@@ -315,7 +315,7 @@ contract RestakeManager is Initializable, ReentrancyGuardUpgradeable, RestakeMan // record token value of withdraw queue if (!withdrawQueueTokenBalanceRecorded) { totalWithdrawalQueueValue += renzoOracle.lookupTokenValue( - collateralTokens[i], + collateralTokens[j], collateralTokens[j].balanceOf(withdrawQueue) ); }
Loop
#0 - c4-judge
2024-05-16T10:37:08Z
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/519e518f2d8dec9acf6482b84a181e403070d22d/contracts/RestakeManager.sol#L543-L546 https://github.com/code-423n4/2024-04-renzo/blob/519e518f2d8dec9acf6482b84a181e403070d22d/contracts/RestakeManager.sol#L546-L549 https://github.com/code-423n4/2024-04-renzo/blob/519e518f2d8dec9acf6482b84a181e403070d22d/contracts/RestakeManager.sol#L562
User can't deposit ERC20
collateral token into the RestakeManager
if WithdrawQueue
isn't filled with necessary (buffer) amount of tokens. This deficit can occur when user that already has deposits withdraw their funds and withdrawal buffer deficit is not filled in time.
To deposit ERC20
tokens into RestakeManager
user must call RestakeManager.deposit()
which makes next actions:
WithdrawQueue
buffer needs to be filled:uint256 bufferToFill = depositQueue.withdrawQueue().getBufferDeficit( address(_collateralToken) ); if (bufferToFill > 0) {
bufferToFill > 0
then conditional code executes to fill withdraw buffer deficit: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); }
_amount <= bufferToFill
bufferToFill
would be equal to _amount
.bufferToFill
(which is in our case equal to _amount
) is subtracted from deposited _amount
and in our case, it would be 0
._amount
which is 0
is deposited to OperatorDelegator
contract:// Call deposit on the operator delegator operatorDelegator.deposit(_collateralToken, _amount);
But this execution would revert as OperatorDelegator.deposit()
doesn't accept _amount
of 0
:
function deposit( IERC20 token, uint256 tokenAmount ) external nonReentrant onlyRestakeManager returns (uint256 shares) { if (address(tokenStrategyMapping[token]) == address(0x0) || tokenAmount == 0) revert InvalidZeroInput();
Deposit into operatorDelegator only if _amount
is more than 0:
@@ -558,8 +558,15 @@ contract RestakeManager is Initializable, ReentrancyGuardUpgradeable, RestakeMan // Approve the tokens to the operator delegator _collateralToken.safeApprove(address(operatorDelegator), _amount); // Call deposit on the operator delegator - operatorDelegator.deposit(_collateralToken, _amount); + if (_amount != 0) { + operatorDelegator.deposit(_collateralToken, _amount); + }
Invalid Validation
#0 - c4-judge
2024-05-20T05:00:59Z
alcueca marked the issue as satisfactory
#1 - c4-judge
2024-05-24T10:26:23Z
alcueca changed the severity to 2 (Med Risk)