Renzo - p0wd3r'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: 37/122

Findings: 4

Award: $114.85

🌟 Selected for report: 0

🚀 Solo Findings: 0

Awards

13.5262 USDC - $13.53

Labels

bug
3 (High Risk)
satisfactory
sufficient quality report
:robot:_97_group
duplicate-395

External Links

Lines of code

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

Vulnerability details

Impact

getTokenBalanceFromStrategy cannot record the queued shares, causing calculateTVLs to calculate a TVL smaller than the actual value, thereby affecting ezETH's mint and burn.

Proof of Concept

The condition queuedShares[address(this)] == 0 is used in getTokenBalanceFromStrategy to check if there is a queuedShare.

https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/Delegation/OperatorDelegator.sol#L327-L335

    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.

https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/Delegation/OperatorDelegatorStorage.sol#L60-L61

    /// @dev mapping of token shares in withdraw queue of EigenLayer
    mapping(address => uint256) public queuedShares;

https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/Delegation/OperatorDelegator.sol#L231-L232

            // 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.

Tools Used

vscode

queuedShares[address(this)] == 0 -> queuedShares[address(token)] == 0

Assessed type

Error

#0 - c4-judge

2024-05-16T10:43:59Z

alcueca marked the issue as satisfactory

Awards

0.4071 USDC - $0.41

Labels

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

External Links

Lines of code

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

Vulnerability details

Impact

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.

Proof of Concept

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.

Tools Used

vscode

Calculate price at the time of claim.

Assessed type

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

Lines of code

https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/RestakeManager.sol#L317-L319

Vulnerability details

Impact

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.

Proof of Concept

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.

Tools Used

Vscode

collateralTokens[i] -> collateralTokens[j]

Assessed type

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)

Findings Information

🌟 Selected for report: GalloDaSballo

Also found by: 0xabhay, GoatedAudits, SBSecurity, d3e4, jokr, p0wd3r, peanuts, zhaojohnson

Labels

bug
2 (Med Risk)
downgraded by judge
satisfactory
sufficient quality report
:robot:_57_group
duplicate-13

Awards

100.9059 USDC - $100.91

External Links

Lines of code

https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/Oracle/RenzoOracle.sol#L121-L149

Vulnerability details

Impact

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.

Proof of Concept

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:

  1. Alice deposits 1000 wbETH and gets 1000 ezETH.
  2. Now oracle stETH/ETH = 1.004 but true value is 1. The chainlink offer is not updated due to the existence of price deviation threshold.
  3. Bob buys 1000 stETH with 1000 ETH outside the protocol.
  4. Bob deposit 1000 stETH -> inflationPercentaage = FACTOR * (1000 * 1.004) / (1000 + 1004) = FACTOR * 1004 / 2004
  5. newEzSupply = 1000 * FACTOR / (FACTOR - FACTOR * 1004 / 2004) = 1000 * FACTOR / (FACTOR * 1000 / 2004) = 2004
  6. mintAmount = 2004 - 1000 = 1004
  7. At this point, the ezETH/ETH rate is 1. Bob's 1004 ezETH is worth 1004 ETH, and if he could withdraw it right away, he would get 1004 ETH, which is 4 ETH more than his cost of 1000 ETH, which he illegally siphoned off from the protocol through the price deviation.

Tools Used

vscode

Assessed type

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)

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