Renzo - stonejiajia's results

A protocol that abstracts all staking complexity from the end-user and enables easy collaboration with EigenLayer node operators and a Validated Services (AVSs).

General Information

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

Renzo

Findings Distribution

Researcher Performance

Rank: 90/122

Findings: 1

Award: $0.41

🌟 Selected for report: 0

πŸš€ Solo Findings: 0

Awards

0.4071 USDC - $0.41

Labels

bug
3 (High Risk)
satisfactory
sufficient quality report
:robot:_34_group
duplicate-326

External Links

Lines of code

https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/Withdraw/WithdrawQueue.sol#L206

Vulnerability details

Impact

Users withdrawing in ezETH do no earn any rewards when withdrawing. Yield should accumulated while in the withdraw waiting queue. It is fair to assume that withdrawing users have a proportional claim to the yield.

Proof of Concept

According to the documentation:

ezETH is the liquid restaking token representing a user’s restaked position at Renzo

The underlying restaking positions earn rewards which are reflected in the price of ezETH

The value ezETH increase relative to the underlying LSTs as it earns more rewards in AVS tokens

Therefore, the price of ezETH increases over time, and the value of the rewards is reflected in the value of ezETH.

When extracting rewards, Renzo handles it as follows:

/**
 * @notice  Creates a withdraw request for user
 * @param   _amount  amount of ezETH to withdraw
 * @param   _assetOut  output token to receive on claim
 */
function withdraw(uint256 _amount, address _assetOut) external nonReentrant {
    // check for 0 values
    if (_amount == 0 || _assetOut == address(0)) revert InvalidZeroInput();

    // check if provided assetOut is supported
    if (withdrawalBufferTarget[_assetOut] == 0)
        revert UnsupportedWithdrawAsset();

    // transfer ezETH tokens to this address
    IERC20(address(ezETH)).safeTransferFrom(msg.sender, address(this), _amount);

    // calculate totalTVL
    (, , uint256 totalTVL) = restakeManager.calculateTVLs();

    // Calculate amount to Redeem in ETH
    uint256 amountToRedeem = renzoOracle.calculateRedeemAmount(
        _amount,
        ezETH.totalSupply(),
        totalTVL
    );
    .......
}

The calculateRedeemAmount function calculates the reward amount.

// Given the amount of ezETH to burn, the supply of ezETH, and the total value in the protocol, determine amount of value to return to user
function calculateRedeemAmount(
    uint256 _ezETHBeingBurned,
    uint256 _existingEzETHSupply,
    uint256 _currentValueInProtocol
) external pure returns (uint256) {
    // This is just returning the percentage of TVL that matches the percentage of ezETH being burned
    uint256 redeemAmount = (_currentValueInProtocol * _ezETHBeingBurned) /
        _existingEzETHSupply;

    // Sanity check
    if (redeemAmount == 0) revert InvalidTokenAmount();

    return redeemAmount;
}

Finally, after the coolDownPeriod users can claim their rewards.

    function claim(uint256 withdrawRequestIndex) external nonReentrant {
        // check if provided withdrawRequest Index is valid
        if (withdrawRequestIndex >= withdrawRequests[msg.sender].length)
            revert InvalidWithdrawIndex();

        WithdrawRequest memory _withdrawRequest = withdrawRequests[msg.sender][
            withdrawRequestIndex
        ];
        if (block.timestamp - _withdrawRequest.createdAt < coolDownPeriod) revert EarlyClaim();

        // subtract value from claim reserve for claim asset
        claimReserve[_withdrawRequest.collateralToken] -= _withdrawRequest.amountToRedeem;

        // delete the withdraw request
        withdrawRequests[msg.sender][withdrawRequestIndex] = withdrawRequests[msg.sender][
            withdrawRequests[msg.sender].length - 1
        ];
        withdrawRequests[msg.sender].pop();

        // 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
            );
        }
        // emit the event
        emit WithdrawRequestClaimed(_withdrawRequest);
    }
}

Here, there is a problem. The reward amount calculates the current value of the withdrawn ezETH, but according to the documentation, there is a waiting period of at least seven days before rewards can be extracted. The calculation method above does not consider the waiting time, which results in users receiving rewards based on the value of ezETH that is seven days ago, leading to unnecessary losses for users.

Tools Used

vscode

Account for the accumulate rewards during the withdrawal period that belongs to the deposit pool

Assessed type

Other

#0 - C4-Staff

2024-05-15T14:33:23Z

CloudEllie marked the issue as duplicate of #259

#1 - c4-judge

2024-05-17T12:28:21Z

alcueca marked the issue as satisfactory

#2 - c4-judge

2024-05-17T12:35:51Z

alcueca marked the issue as duplicate of #326

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax Β© 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter