xanoutas/solidity-security-findings huggingface dataset:
Missing `lower<upper` check in `mint_position`
Audit: 2024-08-superposition-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-08-superposition/blob/29726230b13f1cf36c925d732d3eca459af1aff5/pkg/seawater/src/lib.rs#L506-L513


# Vulnerability details

## Details

The current implementation does not perform a check for `lower < upper` during `mint_position`, while many other functions assume `lower < upper`. This discrepancy allows malicious actors to exploit inconsistencies in handling undefined behavior in other parts of the code for their benefit.

1. **Case when `lower = upper`:**

   In the `StorageTicks::update` function, liquidity can be added without issue because only one boundary and the current tick need to be passed, meaning it should function correctly even if the current tick equals the boundary. However, when calculating the amount of tokens required from the user, due to `lower = upper`, the calculation can only fall into the first and third branches. Here, `sqrt_ratio_a_x_96 = sqrt_ratio_a_x_96`, and the difference between these two values multiplies the result, leading to a token amount of 0 regardless of liquidity. A malicious user can exploit this implementation discrepancy to open a position with arbitrary liquidity value without consuming any tokens.

```rust
/* pkg/seawater/src/pool.rs */
170 | // calculate liquidity change and the amount of each token we need
171 | if delta != 0 {
172 |     let (amount_0, amount_1) = if self.cur_tick.get().sys() < lower {
...
183 | 
184 |         // we're below the range, we need to move right, we'll need more token0
185 |         (
186 |             sqrt_price_math::get_amount_0_delta(
187 |                 tick_math::get_sqrt_ratio_at_tick(lower)?,
188 |                 tick_math::get_sqrt_ratio_at_tick(upper)?,
189 |                 delta,
190 |             )?,
191 |             I256::zero(),
192 |         )
193 |     } else if self.cur_tick.get().sys() < upper {
194 |         // we're inside the range, the liquidity is active and we need both tokens
...
224 |     } else {
...
236 |         // we're above the range, we need to move left, we'll need token1
237 |         (
238 |             I256::zero(),
239 |             sqrt_price_math::get_amount_1_delta(
240 |                 tick_math::get_sqrt_ratio_at_tick(lower)?,
241 |                 tick_math::get_sqrt_ratio_at_tick(upper)?,
242 |                 delta,
243 |             )?,
244 |         )
245 |     };
```

   While the attacker cannot directly profit by closing the position, these positions affect fee calculations. For example, in the provided PoC, the attacker can create a large number of empty positions within their liquidity range to collect significant fees from swapping users, inflating what would normally be a fee of 6 to 1605.

2. **Case when `lower > upper`:**

   Unlike the previous case, when `lower > upper`, it is possible to create a position with both liquidity and balance. However, the potential issue arises when calling the `get_fee_growth_inside` function to calculate fees. This functi
An emergency council update will be impossible and results in a wrong state
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
The `emergency_council` role should be mutable, but the setter function is wrongly implemented; thus it's impossible to change it after initialization.

Moreover, the new manager will receive the `nft_manager` permission instead of the intended `emergency_council`, i.e. they would technically be able to steal the assets, while normally they would be only able to disable a pool.


## Proof of Concept

`update_emergency_council` wrongly updates the `nft_manager` instead of the `emergency_council`:

```rust
    #[allow(non_snake_case)]
    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(())
    }
```
https://github.com/code-423n4/2024-08-superposition/blob/main/pkg/seawater/src/lib.rs#L1121


## Tools Used
Manual Review

## Recommended Mitigation Steps

```diff
    #[allow(non_snake_case)]
    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);
+       self.emergency_council.set(manager);

        Ok(())
    }
```


## Assessed type

Invalid Validation
Position's owed fees should allow underflow but it reverts instead, resulting in locked funds
Audit: 2024-08-superposition-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/seawater/src/position.rs#L52-L66


# Vulnerability details

## Impact
The math used to calculate how many fees are owed to the user does not allow underflows, but this is a necessary due to how the Uniswap logic works as fees can be negative. This impact adding/removing funds to a position, resulting in permanently locked funds due to a revert.

## Context
Similar to `Position's fee growth can revert resulting in funds permanently locked` but with a different function/root cause, both issues must be fixed separately.

## Proof of Concept

In `position.update` underflows are not allowed:

```rust
    pub fn update(
        &mut self,
        id: U256,
        delta: i128,
        fee_growth_inside_0: U256,
        fee_growth_inside_1: U256,
    ) -> Result<(), Error> {
        let mut info = self.positions.setter(id);

        let owed_fees_0 = full_math::mul_div(
            fee_growth_inside_0
->              .checked_sub(info.fee_growth_inside_0.get())
                .ok_or(Error::FeeGrowthSubPos)?,
            U256::from(info.liquidity.get()),
            full_math::Q128,
        )?;

        let owed_fees_1 = full_math::mul_div(
            fee_growth_inside_1
->              .checked_sub(info.fee_growth_inside_1.get())
                .ok_or(Error::FeeGrowthSubPos)?,
            U256::from(info.liquidity.get()),
            full_math::Q128,
        )?;

        let liquidity_next = liquidity_math::add_delta(info.liquidity.get().sys(), delta)?;

        if delta != 0 { 
            info.liquidity.set(U128::lib(&liquidity_next));
        }

        info.fee_growth_inside_0.set(fee_growth_inside_0);
        info.fee_growth_inside_1.set(fee_growth_inside_1);
        if !owed_fees_0.is_zero() {
            // overflow is the user's problem, they should withdraw earlier
            let new_fees_0 = info
                .token_owed_0
                .get()
                .wrapping_add(U128::wrapping_from(owed_fees_0));
            info.token_owed_0.set(new_fees_0);
        }
        if !owed_fees_1.is_zero() {
            let new_fees_1 = info
                .token_owed_1
                .get()
                .wrapping_add(U128::wrapping_from(owed_fees_1));
            info.token_owed_1.set(new_fees_1);
        }

        Ok(())
    }
```
https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/seawater/src/position.rs#L52-L66

While the original Uniswap version allows underflows:

```solidity
    uint128 tokensOwed0 =
        uint128(
            FullMath.mulDiv(
                feeGrowthInside0X128 - _self.feeGrowthInside0LastX128,
                _self.liquidity,
                FixedPoint128.Q128
            )
        );
    uint128 tokensOwed1 =
        uint128(
            FullMath.mulDiv(
                feeGrowthInside1X128 - _self.feeGrowthInside1LastX128,
                _s
Position's fee growth can revert resulting in funds permanently locked
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
To determine the fee growth for a position, a function similar to the one used by Uniswap V3 is used. The new logic differs from the original logic, as it does not allow underflows.

Due to this, certain operations that depend on fee growth calculations may not execute properly and could revert (removing and adding liquidity), resulting in locked funds.

## Context
Similar to `Position's owed fees should allow underflow but it reverts instead, resulting in locked funds` but with a different function/root cause, both issues must be fixed separately.

## Proof of Concept

In `tick.get_fee_growth_inside`, Superposition's fee logic does not allow underflows:

```rust
    //@audit I've removed the testing-dbg flags to make it easier to read
    pub fn get_fee_growth_inside(
        &mut self,
        lower_tick: i32,
        upper_tick: i32,
        cur_tick: i32,
        fee_growth_global_0: &U256,
        fee_growth_global_1: &U256,
    ) -> Result<(U256, U256), Error> {
        // the fee growth inside this tick is the total fee
        // growth, minus the fee growth outside this tick
        let lower = self.ticks.get(lower_tick);
        let upper = self.ticks.get(upper_tick);

        let (fee_growth_below_0, fee_growth_below_1) = if cur_tick >= lower_tick {
            (
                lower.fee_growth_outside_0.get(),
                lower.fee_growth_outside_1.get(),
            )
        } else {
            (
                fee_growth_global_0
->                  .checked_sub(lower.fee_growth_outside_0.get())
                    .ok_or(Error::FeeGrowthSubTick)?,
                fee_growth_global_1
->                  .checked_sub(lower.fee_growth_outside_1.get())
                    .ok_or(Error::FeeGrowthSubTick)?,
            )
        };

        let (fee_growth_above_0, fee_growth_above_1) = if cur_tick < upper_tick {
            (
                upper.fee_growth_outside_0.get(),
                upper.fee_growth_outside_1.get(),
            )
        } else {
            (
                fee_growth_global_0
->                  .checked_sub(upper.fee_growth_outside_0.get())
                    .ok_or(Error::FeeGrowthSubTick)?,
                fee_growth_global_1
->                  .checked_sub(upper.fee_growth_outside_1.get())
                    .ok_or(Error::FeeGrowthSubTick)?,
            )
        };

        Ok((
            fee_growth_global_0
->              .checked_sub(fee_growth_below_0)
->              .and_then(|x| x.checked_sub(fee_growth_above_0))
                .ok_or(Error::FeeGrowthSubTick)?,
            fee_growth_global_1
->              .checked_sub(fee_growth_below_1)
->              .and_then(|x| x.checked_sub(fee_growth_above_1))
                .ok_or(Error::FeeGrowthSubTick)?,
        ))
    }
```
https://github.com/code-42
Token approvals are not revoked upon transfer which can cause position theft
Audit: 2024-08-superposition-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

### Impact

Transferring the tokens from one user to the other doesn't revoke the token's approval which opens up the NFT to various attacks by the approved party. For example, the NFT/position can be stolen from receivers by owners by simply approving themselves as a spender of the token, transferring the tokens to the victims, and then transferring it back to themselves. Also, due to the flawed implementation of the `_requireAuthorised` function, they can also appprove external users/contracts to act as approved parties to the tokens.

### Proof of Concept

1. Context

NFT tokens hold the ablity to be approved to users on an individual basis. This is seen in the OwnershipNFT, which holds the approve function that allows any of the approved users (owner, operator or token approved user) to approve a new user to spend the token. These users will always pass the `_requireAuthorised` check and as such can always [transfer](https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/sol/OwnershipNFTs.sol#L109)/[approve](https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/sol/OwnershipNFTs.sol#L161) the token.

```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!");
    }
```

This approval, though, for security reasons, must be revoked when the NFTs are transferred to the new users. This concern can also be noted in various [ERC721 implementations](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/dbb6104ce834628e473d2173bbc9d47f81a9eec3/contracts/token/ERC721/ERC721.sol#L252) which clear the token approvals upon transfer to prevent unintended consequences.

2. Bug Location

OwnershipNFT.sol holds the internal `_transfer` function, which is called by the [`transferFrom`](https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/sol/OwnershipNFTs.sol#L125)1, [`transferFrom`](https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/sol/OwnershipNFTs.sol#L135)2, [`safeTransferFrom`](https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/sol/OwnershipNFTs.sol#L144)1 and [`safeTransferFrom`](https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/sol/OwnershipNFTs.sol#L155)2 functions, which is used by the owner/approved parties to trans
Incorrectly assigned roles when updating emergency council
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#L1121


# Vulnerability details

### Impact

Emergency council cannot be updated if need be and NFTManager contract is replaced, potentially dossing all processes that occur from it.

### Proof of Concept

1. Context

The emergency council is reponsible for disabling the pools, potentially in cases of emergency. There's also the `nft_manager` contract through which positions can be transferred from one user to another. These addresses can be updated through the `update_emergency_council_7_D_0_C_1_C_58` and `update_nft_manager_9_B_D_F_41_F_6` functions.

2. Bug Location

When updating the emergency council through the `update_emergency_council_7_D_0_C_1_C_58` function, we can see at the @audit tag, that instead of setting the address as emergency council, it is set instead as nft manager. 

```rust
    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); //@audit

        Ok(())
    }
```

3. Final Effect

- Emergency council cannot be updated to a different address especially if the initial address goes rouge, potentially compromising protcol integrity.
- NFT manager role mismatch as the `transfer_position_E_E_C7_A3_C_D` function is expected to be callable through the nft manager contract, but cannot be called. Users attempting to transfer their positions, including transferring their OwnershipNFTs will not be able to, halting a core part of protocol operation. 

### Tools Used
Manual code review
 
### Recommended Mitigation Steps

Set the address to the emergency council instead. 

```diff
    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);
+        self.emergency_council.set(manager);        

        Ok(())
    }
```


## Assessed type

Access Control
Use of checked arithmetic when handling fee growth
Audit: 2024-08-superposition-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/seawater/src/position.rs#L52-L66
https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/seawater/src/tick.rs#L162-L169
https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/seawater/src/tick.rs#L197-L203
https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/seawater/src/tick.rs#L236-L245
https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/seawater/src/tick.rs#L257-L262


# Vulnerability details

### Impact

Operations that rely on fee growth may sometimes revert due to lack of support for the overflow/underflow wrapping behaviour.

### Proof of Concept


1. Context


Based on [this](https://github.com/Uniswap/v3-core/issues?q=is%3Aissue+573+is%3Aclosed) issue reported to uniswap, handling fee growth relies on int256 underflow/overflow behaviour. Therefore when protocols fork/implement similar functions, they're required to take into consideration the necessity of this wrapping behaviour. 

2. Bug Location

When analyzing the contracts, calculations involving fee_growth are handled with "checked" arithmetic, mostly the `checked_sub` function. The function performs an overflowing sub and returns None (potentially returning an error in integrations) if overflow or underflow occurs.

```rs
    /// Computes `self - rhs`, returning [`None`] if overflow occurred.
    #[inline(always)]
    #[must_use]
    pub const fn checked_sub(self, rhs: Self) -> Option<Self> {
        match self.overflowing_sub(rhs) {
            (value, false) => Some(value),
            _ => None,
        }
    }
```

And looking through various places where fee growth is calculated, most importantly in the `get_fee_growth_inside` (others include the `cross` function and `update` functions). We can see that `checked_sub` function is used to calculate the fee growth, potentially limiting its wrapping underflow potential. 


```rust
    pub fn get_fee_growth_inside(
        &mut self,
        lower_tick: i32,
        upper_tick: i32,
        cur_tick: i32,
        fee_growth_global_0: &U256,
        fee_growth_global_1: &U256,
    ) -> Result<(U256, U256), Error> {
//...
            (
                fee_growth_global_0
                    .checked_sub(lower.fee_growth_outside_0.get())
                    .ok_or(Error::FeeGrowthSubTick)?,
                fee_growth_global_1
                    .checked_sub(lower.fee_growth_outside_1.get())
                    .ok_or(Error::FeeGrowthSubTick)?,
            )
        };
//...
            (
                fee_growth_global_0
                    .checked_sub(upper.fee_growth_outside_0.get())
                    .ok_or(Error::FeeGrowthSubTick)?,
                fee_growth_global_1
                    .checked_sub(upper.fee_growth_ou
Emergency council update function  incorrectly 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/4528c9d2dbe1550d2660dac903a8246076044905/pkg/seawater/src/lib.rs#L1110-L1124


# Vulnerability details

## Impact
The `update_emergency_council_7_D_0_C_1_C_58` function is intended to update the `emergency_council` field, but it mistakenly updates the `nft_manager` field instead. This could result in the emergency council not being updated as expected, while the NFT manager is inadvertently changed. This misconfiguration will cause governance issues, where the emergency council is unable to perform its intended role, and the NFT manager is incorrectly reassigned.


## Proof of Concept

https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/seawater/src/lib.rs#L1110-L1124
```rust
#[allow(non_snake_case)]
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);        <---- // This line is incorrect

    Ok(())
}
```

The function is supposed to update the `emergency_council`, but it's updating the `nft_manager` instead. This is likely due to a copy-paste error where the wrong field is being set.


## Tools Used
Manual review

## Recommended Mitigation Steps
Update the function to correctly modify the `emergency_council` field:

```diff
#[allow(non_snake_case)]
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);
+   self.emergency_council.set(manager);

    Ok(())
}
```


## Assessed type

Governance
Incorrect Update of NFT Manager in Update Emergency Council Function
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

In the `update_emergency_council_7_D_0_C_1_C_58` function, there is an incorrect update where the **NFT manager (self.nft_manager)** is being set instead of the **emergency council**. The function is intended to update the **emergency council**, which has the authority to disable pools in critical situations. However, due to this error, the function modifies the wrong storage variable.

This mistake leads to unintended behavior where the emergency council remains unchanged, and the NFT manager, which is responsible for managing NFTs, is incorrectly overwritten

## Impact
- **Operational Issues:** Failure to update the proper entities may result in allowing unauthorized or misconfigured entities to hold control, causing operational risks.

- **Inability to update the emergency council:** The emergency council, which should have the authority to disable pools, remains unchanged.

- **Overwriting the NFT manager:** The incorrect assignment changes the NFT manager, which may lead to improper management of NFT-related functions, such as token transfers or user permissions.

## Proof of Concept
```solidity
    /// Update the emergency council that can disable the pools.
    #[allow(non_snake_case)]
    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(())
    }
```

## Tools Used
Manual Review 

## Recommended Mitigation Steps
```diff
    /// Update the emergency council that can disable the pools.
    #[allow(non_snake_case)]
    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);
+       self.emergency_council.set(manager);

        Ok(())
    }
```


## Assessed type

Error