xanoutas/solidity-security-findings huggingface dataset:
A Malicious ezETH holder can unfairly avoid the loss due to validator penalties or slashing by sandwiching eigenlayer updates
Audit: 2024-04-renzo-findings
Severity: HIGH

# Lines of code

https://github.com/Layr-Labs/eigenlayer-contracts/blob/ef2ea4a7459884f381057aa9bbcd29c7148cfb63/src/contracts/pods/EigenPod.sol#L188-L224
https://github.com/Layr-Labs/eigenlayer-contracts/blob/ef2ea4a7459884f381057aa9bbcd29c7148cfb63/src/contracts/pods/EigenPod.sol#L235-L280
https://github.com/code-423n4/2024-04-renzo/blob/519e518f2d8dec9acf6482b84a181e403070d22d/contracts/Withdraw/WithdrawQueue.sol#L206-L263


# Vulnerability details

## Vulnerability Detail
A malicious ezEth holder can sandwich eigenlayer's updates to a validator's penalty or slashing event in order to avoid the loss. 

Validators can lose some or all of their deposit via penalties or slashing events

* In case of penalties Eigenlayer can be notified of the balance drop via the permissionless function `EigenPod::verifyBalanceUpdates()`.
* In case of slashing the validator is forced to exit and Eigenlayer can be notified via the permissionless function `EigenPod::verifyAndProcessWithdrawals()`

As soon as either `EigenPod::verifyBalanceUpdates()` or `EigenPod::verifyAndProcessWithdrawals()` is called, the total TVL of the protocol drops instantly. This is because both of the functions reduce the `podOwnerShares[podOwner]` which is used in the TVL calculations.

A malicious ezEth holder can take advantage (in a single tx) of this by the following steps:

1. Identify a slashing or penalty event taking place
3. Request a withdrawal via `WithdrawQueue::withdraw()` at an inflated stale TVL to redeem all their ezETH holdings to ETH 
4. Call the permissionless function `EigenPod::verifyBalanceUpdates()` or `EigenPod::verifyAndProcessWithdrawals()`, to instantly drop the TVL
5. Deposit the same amount of ETH back into the protocol, at the new reduced TVL, so they will be minted more ezETH than they initially redeemed. 


## Impact
The attacker has avoided the loss due to validator penalties/slashing which will be paid by all other ezETH holders

The sandwich attack will end up with the attacker extracting value from all other ezETH holders.

The attack vector also involves redeeming ETH at an inflated stale TVL. This breaks the following main invariant defined by the protocol:

>ezETH should be minted or redeemed based on current supply and TVL.

## Proof of Concept
Refer to vulnerability detail

## Tools Used
Manual Review

## Recommended Mitigation Steps
One solution would be for the Renzo protocol to monitor validator slashing/penalty events and call `EigenPod::verifyBalanceUpdates()` or `EigenPod::verifyAndProcessWithdrawals()` immediately to decrease the likelihood of the attack.


## Assessed type

Other
Users can circumvent slashing penalties by front-running EigenLayer updates
Audit: 2024-04-renzo-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-04-renzo/blob/519e518f2d8dec9acf6482b84a181e403070d22d/contracts/Withdraw/WithdrawQueue.sol#L220-L224
https://github.com/code-423n4/2024-04-renzo/blob/519e518f2d8dec9acf6482b84a181e403070d22d/contracts/Withdraw/WithdrawQueue.sol#L242-L249


# Vulnerability details


## Proof of Concept
In case of penalties Eigenlayer can be notified of the balance drop via the permissionless function [EigenPod::verifyBalanceUpdates()](https://github.com/Layr-Labs/eigenlayer-contracts/blob/v0.2.1-goerli-m2/src/contracts/pods/EigenPod.sol#L185).
In case of slashing the validator is forced to exit and Eigenlayer can be notified via the permissionless function  [EigenPod::verifyAndProcessWithdrawals()](https://github.com/Layr-Labs/eigenlayer-contracts/blob/v0.2.1-goerli-m2/src/contracts/pods/EigenPod.sol#L232) because the slashing event is effectively a full withdrawal.

Changes to the actual balance of validators are updated when `verifyBalanceUpdates()` is called on an `EigenPod`. `Slashing` of an `EigenPod` is therefore known before it is updated in the `EigenLayer` system.

A user can monitor the validators and call `withdraw()` when a validator is slashed to front-run calls to `verifyBalanceUpdates()` in order to avoid the loss.

If a user calls `withdraw()` before the balance is update on `EigenLayer` they will inflate `amountToRedeem` they are entitled to since the `TVL` has still not accounted for the slashing that happened.

[WithdrawQueue.sol#L220-L224](https://github.com/code-423n4/2024-04-renzo/blob/519e518f2d8dec9acf6482b84a181e403070d22d/contracts/Withdraw/WithdrawQueue.sol#L220-L224)

```js
        uint256 amountToRedeem = renzoOracle.calculateRedeemAmount(
            _amount,
            ezETH.totalSupply(),
            totalTVL
        );
```
This is saved in the withdraw request for user

[WithdrawQueue.sol#L242-L249](https://github.com/code-423n4/2024-04-renzo/blob/519e518f2d8dec9acf6482b84a181e403070d22d/contracts/Withdraw/WithdrawQueue.sol#L242-L249)

```js
        withdrawRequests[msg.sender].push(
            WithdrawRequest(
                _assetOut,
                withdrawRequestNonce,
                amountToRedeem,
                _amount,
                block.timestamp
            )

```
        
The user will increase their `amountToRedeem` since the TVL is inflated.

## Impact
Example scenario:

Initial state:
2 validators 32 ETH each
10 users with equal LRT, 6.4 each.

Validator 1 is slashed for 16 ETH
User 1 front-runs `verifyBalanceUpdates()` with a call to `withdraw()` and has `amountToRedeem` = 6.4 since TVL is still 64 ETH.
`verifyBalanceUpdates()` is now called to update EigenLayer balance.
User 2 calls `withdraw()` and has `amountToRedeem`= 4.8 since TVL has decreased to 48.
User 1 has stolen 1.4 ETH from the rest.

Thus, slashing will be concentrated among those that don't front-run. When this issue becomes known a new "meta" will be at play, everybody will have to front-ru
Protocol allows stakers to sidestep and avoid validator penalties/slashing
Audit: 2024-04-renzo-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details


## Proof of Concept

It's a known fact that validators can indeed lose part of their deposit via [penalties](https://eth2book.info/capella/part2/incentives/penalties/) or [slashing](https://eth2book.info/capella/part2/incentives/slashing/) events:

- In case of penalties Eigenlayer can be notified of the balance drop via the permissionless [EigenPod#verifyBalanceUpdates()](https://github.com/Layr-Labs/eigenlayer-contracts/blob/v0.2.1-goerli-m2/src/contracts/pods/EigenPod.sol#L185) function.
- In case of slashing the validator is forced to exit and Eigenlayer can also be notified via the permissionless [EigenPod#verifyAndProcessWithdrawals()](https://github.com/Layr-Labs/eigenlayer-contracts/blob/v0.2.1-goerli-m2/src/contracts/pods/EigenPod.sol#L232) function because the slashing event is effectively a full withdrawal.

As soon as either [EigenPod#verifyBalanceUpdates()](https://github.com/Layr-Labs/eigenlayer-contracts/blob/v0.2.1-goerli-m2/src/contracts/pods/EigenPod.sol#L185) or [EigenPod#verifyAndProcessWithdrawals()](https://github.com/Layr-Labs/eigenlayer-contracts/blob/v0.2.1-goerli-m2/src/contracts/pods/EigenPod.sol#L232) is called the `TVL` of the Renzo protocol drops instantly. This is because both of the functions update the variable [`podOwnerShares[podOwner]`](https://github.com/Layr-Labs/eigenlayer-contracts/blob/v0.2.1-goerli-m2/src/contracts/pods/EigenPodManager.sol#L120):

- [EigenPod#verifyBalanceUpdates()](https://github.com/Layr-Labs/eigenlayer-contracts/blob/v0.2.1-goerli-m2/src/contracts/pods/EigenPod.sol#L185) will update the variable [here](https://github.com/Layr-Labs/eigenlayer-contracts/blob/v0.2.1-goerli-m2/src/contracts/pods/EigenPod.sol#L220)
- [EigenPod#verifyAndProcessWithdrawals()](https://github.com/Layr-Labs/eigenlayer-contracts/blob/v0.2.1-goerli-m2/src/contracts/pods/EigenPod.sol#L232) will update the variable [here](https://github.com/Layr-Labs/eigenlayer-contracts/blob/v0.2.1-goerli-m2/src/contracts/pods/EigenPod.sol#L275)

This makes it possible for stakers to:

1. Request a withdrawal via [WithdrawQueue#withdraw()](https://github.com/code-423n4/2024-04-renzo/blob/1c7cc4e632564349b204b4b5e5f494c9b0bc631d/contracts/Withdraw/WithdrawQueue.sol#L206-L263) for all their `ezETH` held.
2. Then call either [EigenPod#verifyBalanceUpdates()](https://github.com/Layr-Labs/eigenlayer-contracts/blob/v0.2.1-goerli-m2/src/contracts/pods/EigenPod.sol#L185) or [EigenPod#verifyAndProcessWithdrawals()](https://github.com/Layr-Labs/eigenlayer-contracts/blob/v0.2.1-goerli-m2/src/contracts/pods/EigenPod.sol#L232).

So at the point when [WithdrawQueue#withdraw()](https://github.com/code-423n4/2024-04-renzo/blob/1c7cc4e
Incorrect underflow handling in `tick::get_fee_growth_inside()` may prevent position modifications
Audit: 2024-08-superposition-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/seawater/src/tick.rs#L125-L246


# Vulnerability details

## Impact
`tick::get_fee_growth_inside()` can revert in certain situations due to not handling underflows correctly. As a result, some positions may become unmodifiable, causing users' funds to become stuck in the AMM.

## Proof of Concept
Uniswap's function `Tick.getFeeGrowthInside()` is responsible for calculating fee growth before any position-modifying actions. In the Uniswap v3 code (below), we see that all subtraction operations are subject to underflow (Uniswap v3 was deployed using Solidity versions prior to 0.8.0, it lacks built-in protections against overflow and underflow)

```solidity
function getFeeGrowthInside(
    mapping(int24 => Tick.Info) storage self,
    int24 tickLower,
    int24 tickUpper,
    int24 tickCurrent,
    uint256 feeGrowthGlobal0X128,
    uint256 feeGrowthGlobal1X128
) internal view returns (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) {
    Info storage lower = self[tickLower];
    Info storage upper = self[tickUpper];


    // calculate fee growth below
    uint256 feeGrowthBelow0X128;
    uint256 feeGrowthBelow1X128;
    if (tickCurrent >= tickLower) {
        feeGrowthBelow0X128 = lower.feeGrowthOutside0X128;
        feeGrowthBelow1X128 = lower.feeGrowthOutside1X128;
    } else {
        feeGrowthBelow0X128 = feeGrowthGlobal0X128 - lower.feeGrowthOutside0X128;
        feeGrowthBelow1X128 = feeGrowthGlobal1X128 - lower.feeGrowthOutside1X128;
    }


    // calculate fee growth above
    uint256 feeGrowthAbove0X128;
    uint256 feeGrowthAbove1X128;
    if (tickCurrent < tickUpper) {
        feeGrowthAbove0X128 = upper.feeGrowthOutside0X128;
        feeGrowthAbove1X128 = upper.feeGrowthOutside1X128;
    } else {
        feeGrowthAbove0X128 = feeGrowthGlobal0X128 - upper.feeGrowthOutside0X128;
        feeGrowthAbove1X128 = feeGrowthGlobal1X128 - upper.feeGrowthOutside1X128;
    }


    feeGrowthInside0X128 = feeGrowthGlobal0X128 - feeGrowthBelow0X128 - feeGrowthAbove0X128;
    feeGrowthInside1X128 = feeGrowthGlobal1X128 - feeGrowthBelow1X128 - feeGrowthAbove1X128;
}
```

In contrast, Seawater’s implementation of this function (`tick::get_fee_growth_inside()`) uses `checked_sub` for these operations, which causes the execution to revert if an underflow occurs. This presents a problem because the behavior of `getFeeGrowthInside()` [relies on underflow to calculate fee growth](https://github.com/Uniswap/v3-core/issues/573#issuecomment-1218370592). If underflow is prevented, as in Seawater’s version, valid fee growth calculations may revert. Consequently, some positions may become unmodifiable, potentially locking users' funds in the AMM.

## Tools Used
Manual Review.

## Recommended Mitigation Steps
To resolve this issue, consider replacing `checked_sub` with `wrapping
update_emergency_council_7_D_0_C_1_C_58() updates nft manager instead of emergency council
Audit: 2024-08-superposition-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-08-superposition/blob/main/pkg/seawater/src/lib.rs#L1121


# Vulnerability details

## Impact

Inside of `lib.rs`, there is a function `update_emergency_council_7_D_0_C_1_C_58()` that is needed to update the emergency council that can disable the pools. However, in the current implementation, `nft_manager` is updated instead.

## Proof of Concept

This is the current functionality of `update_emergency_council_7_D_0_C_1_C_58()`:

https://github.com/code-423n4/2024-08-superposition/blob/main/pkg/seawater/src/lib.rs#L1111-1124
```
pub fn update_emergency_council_7_D_0_C_1_C_58(
        &mut self,
        manager: Address,
    ) -> Result<(), Revert> {
        assert_eq_or!(
            msg::sender(),
            self.seawater_admin.get(),
            Error::SeawaterAdminOnly
        );

        self.nft_manager.set(manager);

        Ok(())
    }
```
As you can see, the function updates `nft_manager` contract instead of `emergency_council` that is needed to be updated. Above this function there is another function that updates `nft_manager`:


https://github.com/code-423n4/2024-08-superposition/blob/main/pkg/seawater/src/lib.rs#L1097-1107
```
  pub fn update_nft_manager_9_B_D_F_41_F_6(&mut self, manager: Address) -> Result<(), Revert> {
        assert_eq_or!(
            msg::sender(),
            self.seawater_admin.get(),
            Error::SeawaterAdminOnly
        );

        self.nft_manager.set(manager);

        Ok(())
    }

```

As you can see here, in both of the functions `nft_manager` is updated which is an unexpected behavior and the contract cannot update the `emergency_council` that handles emergency situations:

https://github.com/code-423n4/2024-08-superposition/blob/main/pkg/seawater/src/lib.rs#L117-118
```
 // address that's able to activate and disable emergency mode functionality
    emergency_council: StorageAddress,
```

## Tools Used

Manual review.

## Recommended Mitigation Steps

Change `update_emergency_council_7_D_0_C_1_C_58()` to update `emergency_council`


## Assessed type

Other
Unrevoked approvals allow NFT recovery by previous owner
Audit: 2024-08-superposition-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-08-superposition/blob/main/pkg/sol/OwnershipNFTs.sol#L98-L107


# Vulnerability details

## Impact
The vulnerability arises from the fact that after a token transfer, the approval status for the token is not revoked. Specifically, the `getApproved[_tokenId]` is not updated on transfer.
This allows the previous owner (or any approved address) to reclaim the NFT by using the approval mechanism to re-transfer the token back to themselves. This is critical because the new owner of the NFT may lose their asset without realizing it, leading to potential exploitation, loss of assets, and decreased trust in the platform.

## Details
In the provided `approve` function, any user can approve themselves or another address for a specific token ID:

```solidity
/// @inheritdoc IERC721Metadata
function approve(address _approved, uint256 _tokenId) external payable {
    _requireAuthorised(msg.sender, _tokenId);
    getApproved[_tokenId] = _approved;
}
```

Since the approval is not revoked upon transfer, the previous owner retains the ability to re-transfer the NFT. The `_requireAuthorised` function is the only check on transfer permission:

```solidity
function _requireAuthorised(address _from, uint256 _tokenId) internal view {
    // revert if the sender is not authorised or the owner
    bool isAllowed =
        msg.sender == _from ||
        isApprovedForAll[_from][msg.sender] ||
        msg.sender == getApproved[_tokenId];

    require(isAllowed, "not allowed");
    require(ownerOf(_tokenId) == _from, "_from is not the owner!");
}
```

### Step-by-Step PoC:
1. **Initial Approval**: The owner of a token (`owner1`) approves an address (`addr2`) to transfer their token.
2. **Token Transfer**: The token is transferred from `owner1` to `newOwner`.
3. **Approval Not Revoked**: The approval for `addr2` is not revoked after the transfer.
4. **NFT Recovery**: `addr2` can still use their approval to transfer the NFT back to themselves, effectively recovering the NFT from `newOwner`.

Note to the judge: there is no existing tests for this specific smart contract (because it is in solidity). A coded POC for this easy-to-understand vulnerability would involve to create all deployment logic.

## Tools Used
Manual code review

## Recommended Mitigation Steps
To prevent this vulnerability, any existing approvals should be revoked when a token is transferred. This can be achieved by adding a line in the transfer function to clear the approval:

```solidity
getApproved[_tokenId] = address(0);
```

This line should be added to the token transfer function to ensure that any previously approved addresses cannot transfer the NFT after it has been sold or transferred to a new owner.



## Assessed type

Token-Transfer
Missing clear token approval when the NFT is transferred.
Audit: 2024-08-superposition-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/sol/OwnershipNFTs.sol#L98-L107


# Vulnerability details

## Impact
When one NFT is transferred, the NFT token's `getApproved` still take effect. This will cause the previous NFT token approval can still work and transfer this NFT.

## Proof of Concept
When someone wants to transfer one NFT, msg.sender's permission will be checked. Only NFT's owner, owner's operators, and this NFT token's approved address can transfer this NFT. 
The problem is that after we transfer one NFT to new owner, this NFT's token's previous approved address can still transfer this NFT.

For example:
- Alice owns one NFT and Alice approve herself to this NFT.
- Alice transfers the NFT to Bob.
- Alice can still transfer this NFT because `getApproved[NFT]` equals Alice's address. 
```javascript
    function _transfer(
        address _from,
        address _to,
        uint256 _tokenId
    ) internal {
        _requireAuthorised(_from, _tokenId);
        SEAWATER.transferPositionEEC7A3CD(_tokenId, _from, _to);
    }
    function _requireAuthorised(address _from, uint256 _tokenId) internal view {
        // revert if the sender is not authorised or the owner
        bool isAllowed =
            msg.sender == _from ||
            isApprovedForAll[_from][msg.sender] ||
            msg.sender == getApproved[_tokenId];

        require(isAllowed, "not allowed");
        require(ownerOf(_tokenId) == _from, "_from is not the owner!");
    }
```
## Tools Used
Manual

## Recommended Mitigation Steps
When we transfer one NFT, clear the previous `getApproved`.


## Assessed type

Access Control
Incorrect implementation of `updateEmergencyCouncil`
Audit: 2024-08-superposition-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/seawater/src/lib.rs#L1109-L1124


# Vulnerability details

## Impact
The `emergency_council` address can never be updated once set

## Vulnerability Details

It appears that this issue is caused by a copy-paste error. The developer seems to have copied the code from the `update_nft_manager_9_B_D_F_41_F_6` function and used it as the basis for the `update_emergency_council_7_D_0_C_1_C_58` function. However, they forgot to change the variable name from `nft_manager` to `emergency_council`. As a result, the function does not work as intended. Instead of setting the address for `emergency_council`, the function mistakenly sets it for `nft_manager`.

This oversight introduces a significant security risk because the `nft_manager` has the authority to change the owner of any user's position, which is a higher level of authority compared to the `emergency_council`, which can only disable the pool. Therefore, this bug not only renders the intended function inoperative but also poses a potential security vulnerability.

## Tools Used
Manual analysis

## Recommended Mitigation Steps

Replace the `nft_manager` on L1121 with `emergency_council`





## Assessed type

Access Control
Protocol fees can't be collected due to implementation error
Audit: 2024-08-superposition-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/seawater/src/lib.rs#L1149-L1150


# Vulnerability details

## Impact
Protocol fees can't be collected

## Vulnerability Details

The current implementation contains a function, `collect_protocol`, which allows the Seawater admin to withdraw protocol fees from a specified pool. However, there is an error in the parameter order when calling the `erc20::transfer_to_addr` function. The `transfer_to_addr` function uses the second and third parameters as arguments to invoke the `transfer` function implemented by the address given as the first parameter. In the current implementation, the recipient is incorrectly passed as the first parameter.

This mistake not only prevents the transfer from occurring but could also lead to unintended behavior if the recipient has implemented their own `transfer` function.

```rust
/* pkg/seawater/src/wasm_erc20.rs */
110 | /// Calls the `transfer` function on a potentially noncomplient ERC20.
111 | fn safe_transfer(token: Address, to: Address, amount: U256) -> Result<(), Error> {
112 |     call_optional_return(token, &encode_transfer(to, amount))
113 | }
...
146 | /// Sends ERC20 tokens to the transaction sender.
147 | ///
148 | /// # Side effects
149 | /// Transfers ERC20 tokens to the transaction sender.
150 | pub fn transfer_to_sender(token: Address, amount: U256) -> Result<(), Error> {
151 |     safe_transfer(token, msg::sender(), amount)
152 | }
```

## Tools Used
Manual analysis

## Recommended Mitigation Steps

```diff
-        erc20::transfer_to_addr(recipient, pool, U256::from(token_0))?;
+        erc20::transfer_to_addr(pool, recipient, U256::from(token_0))?;
-        erc20::transfer_to_addr(recipient, FUSDC_ADDR, U256::from(token_1))?;
+        erc20::transfer_to_addr(FUSDC_ADDR, recipient, U256::from(token_1))?;
```








## Assessed type

Other
Fees calculations don’t allow overflowing/underflowing
Audit: 2024-08-superposition-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/seawater/src/tick.rs#L236-L245


# Vulnerability details

## Impact

`update_position`-related transactions may revert in some situations.

## Vulnerability Details

In the `get_fee_growth_inside` function, the calculations of `fee_growth_global_0`, `fee_growth_global_1`, etc. don’t allow under- and overflowing. However, the respective calculations in Uniswap V3 are designed to underflow and overflow (for more information, refer to https://github.com/Uniswap/v3-core/issues/573 issue and  https://github.com/Jeiwan/uniswapv3-book/issues/45). As a result, executing `update_position` can revert in some situations (such as test `incr_position_fee_growth_tick`), causing transaction reverts. 

## Tools Used
Manual analysis

## Recommended Mitigation Steps

Use `overflowing_sub` instead of `checked_sub`.





## Assessed type

Under/Overflow