xanoutas/solidity-security-findings huggingface dataset:
OperatorDelegator will leak ezETH yield due to integrating an inconsistent gas refunds mechanism
Audit: 2024-06-renzo-mitigation-findings
Severity: HIGH

# Lines of code

https://github.com/Renzo-Protocol/Contracts/blob/ez-withdraw/contracts/Delegation/OperatorDelegator.sol#L323
https://github.com/Renzo-Protocol/Contracts/blob/ez-withdraw/contracts/Delegation/OperatorDelegator.sol#L399
https://github.com/Renzo-Protocol/Contracts/blob/ez-withdraw/contracts/Delegation/OperatorDelegator.sol#L502


# Vulnerability details


##  Proof of Concept

> NB: During the mitigation review, I spent time going across the findings repo to see if there is any valid findings the sponsors might have missed due to wrong duplications or misunderstanding from the validators. Now see: https://github.com/code-423n4/2024-04-renzo-findings/issues/84, the sponsor didn't go through this issue due to it's initial [wrong duplication tag `duplicate-81`](https://github.com/code-423n4/2024-04-renzo-findings/issues/84#issuecomment-2115616698), considering it wasn't the primary. Now the [scoping rules](https://github.com/code-423n4/2024-06-renzo-mitigation?tab=readme-ov-file#out-of-scope) for this mitigation review, explicitly states that disputed issues are `OOS`, however this bug case never got to the sponsor for them to review so it wasn't disputed by them which I assume makes this finding in-scope.

Now going through the issue, it indeed is valid and would lead to heavy leakage of ezETH yield.

In the `OperatorDelegator` contract : https://github.com/Renzo-Protocol/Contracts/blob/ez-withdraw/contracts/Delegation/OperatorDelegator.sol, the `baseGasAmountSpent` value is applied to all admin gas refunds recorded for methods like: _queueWithdrawals()_, _completeQueuedWithdrawal()_, _verifyWithdrawalCredentials()_, and _verifyAndProcessWithdrawals()_. However, the initial cost of transactions mostly depends on calldata size and is not the same for these methods/functions.

Checking Etherscan, we can see that `baseGasAmountSpent` is set to 1,350,000 gas: https://etherscan.io/address/0xbAf5f3A05BD7Af6f3a0BBA207803bf77e2657c8F#readProxyContract#F2, which in our case would match the base cost of `verifyAndProcessWithdrawals` ( for example see the gas profiling for the [linked example transaction](https://app.blocksec.com/explorer/tx/eth/0xd98f085430513f1b96f541c6ff830fa19817f70752f14927066ec787c855267e) from the previous finding). This example transaction has 101,952 bytes of calldata and pays 1,330,000 gas in initial costs, which is short of just `20,000` to match the `baseGasAmountSpent`. So if this periodic transaction is to properly be refunded, it must continue using the same value.
![t](https://cdn.discordapp.com/attachments/1235966308542185483/1247918863753023488/Screenshot_2024-06-05_at_17.23.57.png?ex=6661c682&is=66607502&hm=f84dd232b5b560a2dd99ff94e872c9abed04bce741c9a65e0a1cbf98b8c2f0b8&)

However, issue with this  implementation is that within the `queueWithdrawals` and `completeQueuedWithdrawal` methods which will be called periodically to process withdrawals, they will have a much much lower initial gas cost. But protocol
xRenzoDeposit can mint at invalid rates
Audit: 2024-06-renzo-mitigation-findings
Severity: MEDIUM

# Lines of code

https://github.com/Renzo-Protocol/Contracts/blob/7dabe5478ec6dbf084135737bb0d88960e80b63e/contracts/Bridge/L2/xRenzoDeposit.sol#L299


# Vulnerability details

RenzoOracleL2 has a check implemented, meant to block xezETH mints on L2 whenever the price goes below `1e18`:

```Solidity
File: RenzoOracleL2.sol
50:     function getMintRate() public view returns (uint256, uint256) {
---
55:         if (_scaledPrice < 1 ether) revert InvalidOraclePrice();
```

This check however can be bypassed in `xRenzoDeposit` because this contract can also work without an oracle configured and with prices streamed from L1:

```Solidity
File: xRenzoDeposit.sol
289:     function getMintRate() public view returns (uint256, uint256) {
290:         // revert if PriceFeedNotAvailable
291:         if (receiver == address(0) && address(oracle) == address(0)) revert PriceFeedNotAvailable();
292:         if (address(oracle) != address(0)) {
293:             (uint256 oraclePrice, uint256 oracleTimestamp) = oracle.getMintRate();
294:             return
295:                 oracleTimestamp > lastPriceTimestamp
296:                     ? (oraclePrice, oracleTimestamp)
297:                     : (lastPrice, lastPriceTimestamp);
298:         } else {
299:             return (lastPrice, lastPriceTimestamp);
300:         }
301:     }
302: 
```

It's worth noting that while prices are streamed by an authority in L1, these prices can't be controlled by this authority, and their source (BalancerRateProvider) does not have either a `price < 1e18` check:

```Solidity
File: BalancerRateProvider.sol
29:     function getRate() external view returns (uint256) {
30:         // Get the total TVL priced in ETH from restakeManager
31:         (, , uint256 totalTVL) = restakeManager.calculateTVLs();
32: 
33:         // Get the total supply of the ezETH token
34:         uint256 totalSupply = ezETHToken.totalSupply();
35: 
36:         // Sanity check
37:         if (totalSupply == 0 || totalTVL == 0) revert InvalidZeroInput();
38: 
39:         // Return the rate
40:         return (10 ** 18 * totalTVL) / totalSupply;
41:     }
```

## Impact
When operating without an oracle, xRenzoDeposit can mint at prices below `1e18`, bypassing the checks implemented in RenzoOracleL2.

## Proof of Concept
- Have xRenzoDeposit implemented with `oracle == address(0)`
- Have some slashing in L1 cause the ezETH price go below that of Ether
- At this point, the protocol will be in two situations, both incorrect:
  - L2 will mint at an incorrect rate (outdated)
  - L2 price will be updated, but this will inevitably allow minting at below 1e18

## Tools Used
Code review

## Recommended Mitigation Steps
Consider adding a `price < 1e18` check also in `xRenzoDeposit.getMintRate()`.



## Assessed type

Invalid Validation
[M-12] mitigation error: `getRate()` should not revert in all cases
Audit: 2024-06-renzo-mitigation-findings
Severity: MEDIUM

# Lines of code

https://github.com/Renzo-Protocol/Contracts/blob/consistent-price-fetch/contracts/Bridge/L2/xRenzoDeposit.sol#L293
https://github.com/Renzo-Protocol/Contracts/blob/consistent-price-fetch/contracts/Bridge/L2/Oracle/RenzoOracleL2.sol#L52
https://github.com/Renzo-Protocol/Contracts/blob/consistent-price-fetch/contracts/Bridge/L2/Oracle/RenzoOracleL2.sol#L55


# Vulnerability details

The [applied mitigation](https://github.com/Renzo-Protocol/Contracts/pull/113/files) of calling `getMintRate()` does not take into account that the `RenzoOracleL2` contract was tailored to provide a price used to facilitate deposits into the protocol. This is evidenced by the fact that the [`RenzoOracleL2.getMintRate()`](https://github.com/Renzo-Protocol/Contracts/blob/consistent-price-fetch/contracts/Bridge/L2/Oracle/RenzoOracleL2.sol#L50) function reverts in the following scenarios:
- if the timestamp returned by `oracle.latestRoundData()` is older than `MAX_TIME_WINDOW`. This revert will bubble up to `xRenzoDeposit.getMintRate()` regardless of whether a newer price was reported via `updatePrice()` or `updatePriceByOwner()`. This was the intended behaviour as per this [comment](https://github.com/code-423n4/2024-04-renzo-findings/issues/111#issuecomment-2122634600) by the sponsor because:
  > If any configured price feeds are stale or incorrect. As a protocol we would prefer to stop deposits until configs are fixed.
- if the price returned is below 1 ETH. The reasoning for this as per the sponsor's [FAQ](https://discord.com/channels/810916927919620096/1233459941474435102/1235930082036547624) in the competition Discord was:
  > As, ezETH is a yieldBearing Token which means exchangeRate of ezETH >= 1 ETH. The revert condition is placed to provide a protection of protocol collateral against any wrong oracle feed and restricts user to deposit and mint more ezETH then they are supposed to.
  
  As pointed out in [this](https://github.com/code-423n4/2024-04-renzo-findings/issues/20) finding, the current [ezETH/ETH price](https://data.chain.link/feeds/ethereum/mainnet/ezeth-eth) is nevertheless below 1 and can fall below 1 again in case Renzo or any of the configured collateral assets suffers a loss.

However, these two cases do not apply in providing a price to Balancer pools, and:
- it would be more sensible to return a newer price if available in `xRenzoDeposit` rather than reverting if the oracle is stale
- the price should continue to be provided even if it falls (or stays) below 1 ETH

## Impact
In Balancer's `ComposableStablePool`s such as the one [in use](https://etherscan.io/address/0x596192bb6e41802428ac943d2f1476c1af25cc0e#code) for ezETH/WETH on mainnet, `provider.getRate()` is called before all swaps, joins and exits once the configured cache duration is expired via [`_beforeSwapJoinExit()`](https://github.com/balancer/balancer-v2-monorepo/blob/master/pkg/pool-stable/contracts/ComposableStablePool.sol#L163) -> [`_updateTokenRateCache()`](https:/
[M-02] mitigation error: `WithdrawQueue.claim()` should not be pausable
Audit: 2024-06-renzo-mitigation-findings
Severity: MEDIUM

# Lines of code

https://github.com/Renzo-Protocol/Contracts/blob/7dabe5478ec6dbf084135737bb0d88960e80b63e/contracts/Withdraw/WithdrawQueue.sol#L271


# Vulnerability details

The fix introduced for [M-02](https://github.com/code-423n4/2024-04-renzo-findings/issues/569) added a `whenNotPaused` modifier to both the `withdraw` and `claim` functions.

While pausing the `withdraw` function is an important security feature, having `claim` calls to be pausable, can harm users who initiated a withdraw and were expecting funds to be available at a certain point in time.

## Impact
Calls to `claim` can be retroactively, unfairly blocked for users.

## Proof of Concept
- Have a user initiate a withdraw
- Have an admin call `WithdrawQueue.pause()`
- The user can't call `claim()`

## Tools Used
Code review

## Recommended Mitigation Steps
Consider applying `whenNotPaused` to `withdraw` calls only, and leaving `claim` calls not pausable.





## Assessed type

Governance
`OperatorDelegator` withdrawals can be blocked by a single failing strategy
Audit: 2024-06-renzo-mitigation-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

The [`OperatorDelegator`](https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/Delegation/OperatorDelegator.sol) contract allows an admin to queue withdrawals of multiple tokens from different strategies in the EigenLayer protocol via the [`queueWithdrawals()`](https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/Delegation/OperatorDelegator.sol#L193) function. These queued withdrawals are later completed by the admin calling the [`completeQueuedWithdrawal()`](https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/Delegation/OperatorDelegator.sol#L265) function.

However, the `completeQueuedWithdrawal()` function calls [`DelegationManager.completeQueuedWithdrawal()`](https://github.com/Layr-Labs/eigenlayer-contracts/blob/84ca899c715dbc577af1c8db5965188b061fe464/src/contracts/core/DelegationManager.sol#L305) with the `receiveAsTokens` parameter hardcoded to `true`. If any of the strategies included in the queued withdrawal fails or reverts, this will cause the entire withdrawal to revert. 

As mentioned in this [comment](https://github.com/code-423n4/2023-04-eigenlayer-findings/issues/132#issuecomment-1575599410) and further confirmed [here](https://github.com/code-423n4/2023-04-eigenlayer-findings/issues/132#issuecomment-1582291971), users can work around this by setting `receiveAsTokens` to `false`, allowing them to queue a new withdrawal excluding the failing strategy. However, the `OperatorDelegator` has no way to do this, as it can only call `completeQueuedWithdrawal()` with `receiveAsTokens == true`. 

This means a single failing strategy can block queued `OperatorDelegator` withdrawals indefinitely.

## Impact
Admin withdrawals from the `OperatorDelegator` can be blocked if any of the strategies included in their queued withdrawal fails or reverts when completing the withdrawal. The only way to recover is to upgrade the `OperatorDelegator` contract.

## Proof of Concept
1. Admin calls `OperatorDelegator.queueWithdrawals()`, including a withdrawal from `StrategyA` and `StrategyB`
2. Later, when trying to complete the withdrawal via `OperatorDelegator.completeQueuedWithdrawal()`, `StrategyB` is paused or fails
3. The call to `DelegationManager.completeQueuedWithdrawal()` with `receiveAsTokens == true` reverts due to the failure in `StrategyB`
4. The admin's entire withdrawal, including from `StrategyA`, is blocked
5. There is no way for `OperatorDelegator` to recover the withdrawal from `StrategyA`, short of upgrading the contract

## Tools Used
Manual review

## Recommended Mitigation Steps
Consider adding a recovery function in `OperatorDelegator` that the admin can call in case a queued withdrawal fails. This function should call `DelegationManager.completeQueuedWithdrawal()` with `receiveAsTokens == false`, allowing the shares to be transferred to the withdrawing a
An OperatorDelegator allocation limit can easily be exceeded by a very large arbitrarily amount
Audit: 2024-06-renzo-mitigation-findings
Severity: MEDIUM

# Lines of code

https://github.com/Renzo-Protocol/Contracts/blob/74a6c9963d4e9359c542901fdd2dd3f5a6864f16/contracts/RestakeManager.sol#L339-L370


# Vulnerability details


## Proof of Concept

> NB: First, would be key to note that, this bug case was submitted here: https://github.com/code-423n4/2024-04-renzo-validation/issues/741 and didn't get through to sponsor review, cause the validator misunderstood the issue and thought it to be invalid. The [scoping rules](https://github.com/code-423n4/2024-06-renzo-mitigation?tab=readme-ov-file#out-of-scope) for this mitigation review, explicitly states that disputed issues are `OOS`, however this bug case never got to the Sponsors to review so it wasn't disputed by them which I assume confirms it as in-scope.

Now to dive into the issue, let's take a look at the RestakeManager's `chooseOperatorDelegatorForDeposit()` function here https://github.com/Renzo-Protocol/Contracts/blob/74a6c9963d4e9359c542901fdd2dd3f5a6864f16/contracts/RestakeManager.sol#L339-L370

```solidity
    function chooseOperatorDelegatorForDeposit(
        uint256[] memory tvls,
        uint256 totalTVL
    ) public view returns (IOperatorDelegator) {
        // Ensure OperatorDelegator list is not empty
        if (operatorDelegators.length == 0) revert NotFound();

        // If there is only one operator delegator, return it
        if (operatorDelegators.length == 1) {
            return operatorDelegators[0];
        }

        // Otherwise, find the operator delegator with TVL below the threshold
        uint256 tvlLength = tvls.length;
        for (uint256 i = 0; i < tvlLength; ) {
            if (
                tvls[i] <
                (operatorDelegatorAllocations[operatorDelegators[i]] * totalTVL) /
                    BASIS_POINTS /
                    BASIS_POINTS
            ) {
                return operatorDelegators[i];
            }

            unchecked {
                ++i;
            }
        }

        // Default to the first operator delegator
        return operatorDelegators[0];
    }
```

We can see that this function  picks the `OperatorDelegator` that's to be chosen for the deposit.

Now from the previous contest the trusted roles in the protocol have been listed, with one being the `RESTAKE_MANAGER_ADMIN` that could as well allocate a specific max tvl for an `operatorDelegator`, see https://github.com/code-423n4/2024-04-renzo/blob/1c7cc4e632564349b204b4b5e5f494c9b0bc631d/README.md#L343

```markdown
| RESTAKE_MANAGER_ADMIN              | allows the admin to configure operatorDelegators(add/remove, allocations), supported collateral tokens and their respective TVL            |
```

We can consider this a subtle invariant, considering if a max allocation is placed on an `operatorDelegator` it shouldn't be able to hold more than this value, problem however is that with the way `chooseOperatorDelegatorForDeposit` has been coded, it could actually  cause the deposit to exceed the operator delegator's all
anyone can burn the governance NFT
Audit: 2023-10-party-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-10-party/blob/main/contracts/party/PartyGovernanceNFT.sol#L310


# Vulnerability details

## Impact
Impact
The impact of this finding centers on the PartyGovernanceNFT.sol smart contract, specifically within the context of the burn(uint256 tokenId) function. This critical function operates without any access control mechanisms in place. Consequently, it permits any user, regardless of ownership, to invoke the function and potentially burn someone else's NFTs by supplying the corresponding tokenId. This omission of access control introduces a significant security vulnerability that can have dire consequences.

The potential repercussions of this vulnerability are substantial:

**Unauthorized NFT Destruction:** Malicious users can exploit this oversight to intentionally destroy NFTs owned by other participants in the contract. This act could result in irreversible losses of valuable digital assets.

**Loss of Trust:** Users may lose confidence in the contract's security, affecting the reputation of the platform or service that relies on this contract for NFT governance.

**Financial Implications:** The destruction of NFTs can have financial implications for users who have invested in or depend on the NFTs' functionality. It could lead to legal disputes and financial losses.


## Proof of Concept
Demonstrating by referring to the provided code snippet:

```solidity
function burn(uint256 tokenId) external {
    uint256[] memory tokenIds = new uint256[](1);
    tokenIds[0] = tokenId;
    burn(tokenIds);
}

```

In this code, there are no access control checks to ensure that only the rightful owner of an NFT can burn it. Any user can call this function with a specific tokenId and effectively burn the associated NFT.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Make sure that only the respective authority can call this function







## Assessed type

Access Control
members can get less funds out than they should have in a rageQuit
Audit: 2023-10-party-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-10-party/blob/b23c65d62a20921c709582b0b76b387f2bb9ebb5/contracts/party/PartyGovernanceNFT.sol#L154-L158
https://github.com/code-423n4/2023-10-party/blob/b23c65d62a20921c709582b0b76b387f2bb9ebb5/contracts/party/PartyGovernanceNFT.sol#L391-L395


# Vulnerability details


## Vulnerability details:
### Details:

the function `getVotingPowerShareOf` in `PartyGovernanceNft` get's the voting power share of a tokenId and it does that by dividing the voting power that nft has by the `totalVotingPower`: 

```solidity=154
function getVotingPowerShareOf(uint256 tokenId) public view returns (uint256) {
        uint256 totalVotingPower = _getSharedProposalStorage().governanceValues.totalVotingPower;
        return
            totalVotingPower == 0 ? 0 : (votingPowerByTokenId[tokenId] * 1e18) / totalVotingPower;
    }
```

but `totalVotingPower` is the voting power with which the party got created and not the currently minted voting power.



### Impact:

bcs we are considering the total voting power as the currently availble minting power, when a contributor burns his tokens and decreases currently available voting power, everybody's voting power is going to stay the same and not increase based on how much was burned. this function is called in `rageQuit`: 

```solidity=391
for (uint256 j; j < tokenIds.length; ++j) {
                    // Must be retrieved before burning the token.
                    withdrawAmounts[i] += (balance * getVotingPowerShareOf(tokenIds[j])) / 1e18;
                }
            }
```

which is going to make that people are getting less than what they should get.

there is a clear loss of funds here so that's why it warrants a high.


### Proof of Concept:

to simplify let's consider a party with `totalVotingPower` set to 100 ETH and Bob has 50 ETH and Alice has 50 ETH. let's consider that the ragequit is enabled:

- Bob calls `rageQuit` which is going to do the following calculation `(balance * getVotingPowerShareOf(tokenIds[j])) / 1e18`.
- his votingPowerShareOf is 50/100 = 0.5 so he is going to get half of the funds in the party.
- now Alice comes to call `rageQuit` , and the same calculation applies `(balance * getVotingPowerShareOf(tokenIds[j])) / 1e18`.
- her getVotingPowerShareOf is still 50/100 = 0.5 because `totalVotingPower` didn't change.
- so she is going to get half of the funds left in the contract .
- so she will basically leave her funds behind , which is a clear loss of funds


### Tools Used:

vscode

### Recommended Mitigation Steps:

should use `mintedVotingPower` instead of`totalVotingPower`


## Assessed type

Math
Arthimetic operation result in loss of voting power
Audit: 2023-10-party-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-10-party/blob/main/contracts/crowdfund/ETHCrowdfundBase.sol#L329


# Vulnerability details

## Impact
Carrying out calculation of voting power in function ```_finalize``` based on division with value ```1e4```, then subtracting it with ``` totalContributions_```  and after that multiplying  and dividing again will be a resultant that cause large precision error or even loss of voting power for users.

## Proof of Concept
The ```_finalize``` function have uint96 ```totalContributions_``` variable at line 325 which is calculated by multiplying with ```fundingSplitBps_``` and divided with ```1e4``` and then subtracted with itself. 
The resultant is then multiplied and divided in line 329 in ```uint96 newVotingPower ```. 
```
    function _finalize(uint96 totalContributions_) internal {
        // Finalize the crowdfund.
        delete expiry;


        // Transfer funding split to recipient if applicable.
        address payable fundingSplitRecipient_ = fundingSplitRecipient;
        uint16 fundingSplitBps_ = fundingSplitBps;
        if (fundingSplitRecipient_ != address(0) && fundingSplitBps_ > 0) {
            totalContributions_ -= (totalContributions_ * fundingSplitBps_) / 1e4;
        }


        // Update the party's total voting power.
        uint96 newVotingPower = (totalContributions_ * exchangeRateBps) / 1e4;
        party.increaseTotalVotingPower(newVotingPower);


        // Transfer ETH to the party.
        payable(address(party)).transferEth(totalContributions_);


        emit Finalized();
    }
```

## Tools Used
Manual Review

## Recommended Mitigation Steps
The recommendation is made in area of separately conducting multiplication of all values then subtracted in the condition and carries out all other division operation afterwards for ```newVotingPower```.

```diff

    function _finalize(uint96 totalContributions_) internal {
        // Finalize the crowdfund.
        delete expiry;


        // Transfer funding split to recipient if applicable.
        address payable fundingSplitRecipient_ = fundingSplitRecipient;
        uint16 fundingSplitBps_ = fundingSplitBps;
        if (fundingSplitRecipient_ != address(0) && fundingSplitBps_ > 0) {
+       uint96 newVotingPower = totalContributions_ - (totalContributions_ * exchangeRateBps);
            totalContributions_ -= (totalContributions_ * fundingSplitBps_) / 1e4;
        }


        // Update the party's total voting power.
-       uint96 newVotingPower = (totalContributions_ * exchangeRateBps) / 1e4;
+       newVotingPower = (newVotingPower * exchangeRateBps) / 1e8;

        party.increaseTotalVotingPower(newVotingPower);


        // Transfer ETH to the party.
        payable(address(party)).transferEth(totalContributions_);


        emit Finalized();
    }
```



## Assessed type

Other