xanoutas/solidity-security-findings huggingface dataset:
The update_emergency_council function updates the NFT Manager
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#L1111-L1124


# Vulnerability details

## Impact
When a user calls update_emergency_council it actually updates the nft manager.

## Proof of Concept
The parameter that the function accepts is manager: Address and inside the function self.nft_manager.set(manager); is called, which changes the nft manager and not the emergency council.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Change the input parameter to emergency_council: Address and the setter to self.emergency_council.set(emergency_council); so the function looks like this:
```
/// 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,
        emergency_council: Address,
    ) -> Result<(), Revert> {
        assert_eq_or!(
            msg::sender(),
            self.seawater_admin.get(),
            Error::SeawaterAdminOnly
        );

        self.emergency_council.set(emergency_council);

        Ok(())
    }
```


## Assessed type

Context
Missing lower tick < upper tick check 
Audit: 2024-08-superposition-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

In `pool.rs`, when creating a new position, the ticks are insufficiently validated which can lead to an unexpected behavior as the lower tick cannot be bigger than the upper tick.

## Proof of Concept

Take a look at the `create_position` functionality:

https://github.com/code-423n4/2024-08-superposition/blob/main/pkg/seawater/src/pool.rs#L81-85
```
        let min_tick = tick_math::get_min_tick(spacing);
        let max_tick = tick_math::get_max_tick(spacing);
        assert_or!(low >= min_tick && low <= max_tick, Error::InvalidTick);
        assert_or!(up >= min_tick && up <= max_tick, Error::InvalidTick);
        self.positions.new(id, low, up);

```

As you can see, the only validation that's done for both ticks is checking whether they are bigger than `min_tick` and less than `max_tick`. However, there is no check for `low < up` as it's done in UniswapV3:

https://github.com/Uniswap/v3-core/blob/main/contracts/UniswapV3Pool.sol#L126-130
```
 function checkTicks(int24 tickLower, int24 tickUpper) private pure {
        require(tickLower < tickUpper, 'TLU');
        require(tickLower >= TickMath.MIN_TICK, 'TLM');
        require(tickUpper <= TickMath.MAX_TICK, 'TUM');
    }
```

This can lead to a situation where `up < low` and an unexpected behavior.

## Tools Used

Manual review.

## Recommended Mitigation Steps

Implement `require(low < up, "")` check for the ticks.


## Assessed type

Other
The StoragePositions new function doesn't validate low and up parameters as it states it should
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#L29-L39


# Vulnerability details

## Impact
Parameters could be passed with swapped places and the function will set them.

## Proof of Concept
The comments suggest that the function requires the parameters to be in the correct places, but doesn't validate them, so the position can be created with swapped places of up and low price ticks.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Consider making up and low mutable and swapping the parameters in the beginning of the function if low > up:
```
pub fn new(&mut self, id: U256, mut low: i32, mut up: i32) {
   if low > up {
      (low, up) = (up, low)
   };
```


## Assessed type

Invalid Validation
Missing Validation for Tick Bounds in Position Creation
Audit: 2024-08-superposition-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

The `create_position` function within the pool library fails to validate that the low tick is less than the up tick when creating a new position. This oversight allows users to create positions where the lower tick is greater than the upper tick. Such a scenario disrupts the liquidity and fee calculation logic, potentially leading to incorrect fee distribution, miscalculated liquidity. Users could exploit this vulnerability to add liquidity less token than expected.

## Proof of Concept

When creating a new position external `mint_position_B_C5_B086_D` function and `pool::create_position` does not validate that low tick is less than up tick. As a result, an invalid position can be created where low > up. Such invalid positions can break liquidity and fee calculation logic, leading to potential discrepancies in the pools operations

```rust
pub fn create_position(&mut self, id: U256, low: i32, up: i32) -> Result<(), Revert> {
    assert_or!(self.enabled.get(), Error::PoolDisabled);
    let spacing = self.tick_spacing.get().sys() as i32;
    assert_or!(low % spacing == 0, Error::InvalidTickSpacing);
    assert_or!(up % spacing == 0, Error::InvalidTickSpacing);
    let spacing: u8 = spacing.try_into().map_err(|_| Error::InvalidTickSpacing)?;
    let min_tick = tick_math::get_min_tick(spacing);
    let max_tick = tick_math::get_max_tick(spacing);
    assert_or!(low >= min_tick && low <= max_tick, Error::InvalidTick);
    assert_or!(up >= min_tick && up <= max_tick, Error::InvalidTick);
    self.positions.new(id, low, up);
    Ok(())
}
```
https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/seawater/src/pool.rs#L75

A malicious user can use this attack to pool or an inocent user can add higher amount than necessary and lose money.
Example : User creates a position with low = 1000 and up = 500, while the current tick is 700. The logic will incorrectly allow the user to add only token0, even though the position should require both tokens since it is active. 


```rust
let (amount_0, amount_1) = if self.cur_tick.get().sys() < lower {
     ( sqrt_price_math::get_amount_0_delta(...)?,
       I256::zero(),)
} else if self.cur_tick.get().sys() < upper {
    // we're inside the range, the liquidity is active and we need both tokens
     ( sqrt_price_math::get_amount_0_delta(...)?,
      sqrt_price_math::get_amount_1_delta(...)?,

} else {
    // incorrect calculations continue
     ( I256::zero(),
       sqrt_price_math::get_amount_1_delta(...)?,)
}
```
https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/seawater/src/pool.rs#L172

## Tools Used
Manual

## Recommended Mitigation Steps

To prevent this vulnerability, a simple check should be added to the create_position function to en
No Validation of Input Ranges
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#L35


# Vulnerability details

The "new" function initializes a position with low and up bounds but does not validate the input values to ensure that low is less than up or that both values are within acceptable ranges. This lack of input validation can result in the creation of invalid or nonsensical positions. If such positions are initialized and later used in the contract, it can lead to unexpected behavior and potentially impact the correctness of the system.

Impact:

Incorrect Position Creation: If the low and up values are not in the correct order or if they are outside of the valid range, the position could be created incorrectly, leading to unexpected behavior and potential vulnerabilities.


Invalid Positions: Positions with low values greater than or equal to up could be created, leading to logic errors when the position is later accessed or used in other functions.



Recommended Mitigation:

Input Validation: Implement input validation checks for the low and up parameters. Ensure that:
low is less than up.
Both low and up are within the valid range for the underlying data type (i.e., i32).


pub fn new(&mut self, id: U256, low: i32, up: i32) -> Result<(), Error> {
    if low >= up {
        return Err(Error::InvalidPositionBounds);
    }

    // Additional validation for the range of low and up if necessary

    let mut info = self.positions.setter(id);
    info.lower.set(I32::lib(&low));
    info.upper.set(I32::lib(&up));

    Ok(())
}


## Assessed type

Other
fee_growth_inside calculations doesnt allow oveflow/underflow
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#L163


# Vulnerability details

## Impact

The get_fee_growth_inside function uses checked_sub to prevent overflow and underflow in fee growth calculations. However, the original Uniswap V3 contract relies on underflow/overflow as part of its fee growth logic. Since fee growth inside a tick range can be both positive and negative, using checked_sub may cause certain users to be unable to update their positions, leading to transaction reverts.


## Proof of Concept

`get_fee_growth_inside()` implements UniswapV3 math to calculate fee growth.

```rust
    /// Gets the fee growth inside a tick range.
    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 {
            (
                //@audit overflow/underflow should be allowed 
                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)?,
            )
        };
...
```
In this function, the use of checked_sub prevents underflow, which can cause the function to revert with an Error::FeeGrowthSubTick. This behavior is different from the Uniswap V3 implementation, where underflows are allowed and expected in certain scenarios.


## Tools Used
Manual

## Recommended Mitigation Steps

Modify the get_fee_growth_inside function to allow underflow/overflow by removing the checked_sub checks. This will ensure compatibility with the original Uniswap V3 logic and prevent unexpected transaction reverts for users.


## Assessed type

Under/Overflow
tick.rs does not allow for fees to under/overflow, as it is supposed to be
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#L163-L169


# Vulnerability details

## Impact

Fees will be incorrectly calculated.

## Proof of Concept

In the original UniswapV3 Tick.sol, it uses a version of solidity that allows for underflow. In `getFeeGrowthInside()`, the fee growth are supposed to allow for underflow.

```
UniswapV3 Tick.sol, getFeeGrowthInside()
        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;
        }
```

In tick.rs, the similar function `get_fee_growth_inside()` uses `checked_sub`, which returns None when underflow occurs:

```
            (
                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)?,
            )
        };
```

There are some functions in position.rs that uses `checked_sub` as well, which should not be used. 

## Tools Used

Manual Review

## Recommended Mitigation Steps

Use a equation that allows under/overflow when calculating fees/liquidity.


## Assessed type

Uniswap
Incorrect Tick Validation in create_position Function
Audit: 2024-08-superposition-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/seawater/src/pool.rs#L75-L87


# Vulnerability details

## Impact
The current implementation allows for the creation of positions with invalid tick ranges, potentially leading to logical errors in pool operations and unexpected behavior.

## Proof of Concept
In the create_position function, the validation of the low and up ticks is incomplete. While it checks that both ticks are within the valid range, it fails to ensure that the lower tick is strictly less than the upper tick.

Vulnerable Code
```
pub fn create_position(&mut self, id: U256, low: i32, up: i32) -> Result<(), Revert> {
    // ... (earlier code omitted for brevity)
    
    let min_tick = tick_math::get_min_tick(spacing);
    let max_tick = tick_math::get_max_tick(spacing);
    
    assert_or!(low >= min_tick && low <= max_tick, Error::InvalidTick);<@=
    assert_or!(up >= min_tick && up <= max_tick, Error::InvalidTick);<@=
    
    self.positions.new(id, low, up);
    Ok(())
}
```

The current implementation only checks if both low and up are within the valid tick range but doesn't enforce the correct ordering between them. This could lead to the creation of invalid positions, potentially causing issues in other parts of the system that assume a valid tick range (lower < upper).
By adding the assert_or!(low < up, Error::InvalidTickOrder) check, we ensure that positions can only be created with a valid tick range where the lower tick is strictly less than the upper tick.

With the current implementation, it's possible to create a position where low is equal to or greater than up. For example:
```
create_position(id, 1000, 1000)  // This would be allowed
create_position(id, 2000, 1000)  // This would also be allowed
```
Both of these calls would succeed, despite representing invalid position ranges.
## Tools Used
manual view
## Recommended Mitigation Steps
change to this
```
pub fn create_position(&mut self, id: U256, low: i32, up: i32) -> Result<(), Revert> {
    // ... (earlier code remains the same)
    
    assert_or!(low >= min_tick && low <= max_tick, Error::InvalidTick);
    assert_or!(up >= min_tick && up <= max_tick, Error::InvalidTick);
    assert_or!(low < up, Error::InvalidTickOrder);  // Add this line
    
    self.positions.new(id, low, up);
    Ok(())
}
```


## Assessed type

Context
Incorrect Tick Order in Position Creation Allows Protocol Accounting Manipulation
Audit: 2024-08-superposition-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

The function `create_position`  which is responsible for creating new liquidity positions within the AMM, fails to validate the order of the lower and upper tick parameters. In AMM systems, liquidity positions are typically defined by a range of prices, represented by a lower and upper tick. These ticks must be in the correct order (lower < upper) to ensure proper functioning of the liquidity provision and the overall AMM mechanics. 

## Impact

Positions with inverted ticks will lead to incorrect calculations of liquidity and therefore incorrect swap amounts making the users of the protocol to incur losses unintentionally or intentionally (attacker).

## Proof of Concept

1. An attacker calls `mintPositionBC5B086D()` with `lower = 1000` and `upper = 500`.
2. The function does not validate that `lower < upper`, and proceeds to create the position.
3. The created position now exists in the system with an inverted tick range.
4. Subsequent operations involving this position (e.g., swaps, liquidity calculations) will produce incorrect results.

## Tools Used

Manual review

## Recommended Mitigation Steps

Implement a check  to ensure that the lower tick is always less than the upper tick before.


## Assessed type

Other
Unauthorized NFT reclaiming due to uncleared approvals
Audit: 2024-08-superposition-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

The `OwnershipNFTs` contract implements an ERC721-compliant NFT system for tracking ownership of positions of the Seawater AMM. The contract includes standard ERC721 functions such as `transferFrom()`, `safeTransferFrom()`, and `approve()`. However, there is a critical issue in the implementation of the transfer functionality.

The `_transfer()` function, which is called by all transfer methods, does not clear the `getApproved` mapping for the transferred token. This oversight allows a previously approved address to retain its approval even after the token has changed ownership.

This vulnerability can be exploited by a malicious user to reclaim an NFT after transferring it to another address.

## Impact

Unauthorized reclaiming of transferred NFTs leading to loss of assets for the recipients.

## Proof of Concept

1. Alice owns TokenID 1 and approves Bob to manage this token using the `approve()` function.
2. Alice transfers TokenID 1 to Carol.
3. The `_transfer()` function is called, which updates the ownership but doesn't clear Bob's approval.
4. Bob, who is still approved for TokenID 1, can now call `transferFrom()` to move the token from Carol back to himself or any other address.

## Tools Used

Manual review

## Recommended Mitigation Steps

Modify the `_transfer()` function to clear the approval for the transferred token.


## Assessed type

Other
Emergency Council Update Function Incorrectly Modifies NFT Manager
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

The `update_emergency_council_7_D_0_C_1_C_58()` function is intended to update the emergency council address, which is a critical security role. However, the function actually 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(())
}
```

This error leads to a situation where the emergency council cannot be updated as intended, and the NFT manager can be changed unexpectedly.

## Impact

1. The emergency council cannot be updated, potentially leaving an outdated or compromised address in control of emergency functions.
2. The NFT manager can be unintentionally changed, giving unexpected privileges to transfer NFTs to an address meant for the emergency council.

## Proof of Concept

1. The `seawater_admin` calls `update_emergency_council_7_D_0_C_1_C_58()` with a new address, intending to update the emergency council.
2. The function executes successfully, but instead of updating `emergency_council`, it updates `nft_manager`.
3. The `emergency_council` remains unchanged, while `nft_manager` is now set to the new address.

## Tools Used

Manual review

## Recommended Mitigation Steps

The function should be corrected to update the `emergency_council` instead of the `nft_manager`.


## Assessed type

Other
`OwnershipNFTs` Users Can Maintain NFT Ownership After a Transfer and Transfer the NFT Back to Themselves
Audit: 2024-08-superposition-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
`OwnershipNFTs` can transfer an NFT back to themselves due to improper validation. In other words a NFT owner can transfer their NFT to seawater another address etc. However, the original owner will still be able to transfer the NFT back to themselves because the approval mapping is recorded like this ` mapping(uint256 => address) public getApproved` and not updated during a transfer. 

## Proof of Concept
Here is the line of [vulnerable code](https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/sol/OwnershipNFTs.sol#L103)

Steps to exploit:
1. The original owner Bob will call `OwnershipNFTs::approve` and approve `himself` and the `tokenId` for the token that he owns.

2. Then Bob (original owner) will transfer the NFT to any address etc.

NOTE: Since the token approval is recorded with the following mapping `mapping(uint256 => address) public getApproved` and since this is not updated in the transfer function Bob is still is still approved to spend the NFT. 

3. Since the NFT approval is mapped to the `tokenId` Bob can successfully transfer the NFT back to himself by calling the `OwnershipNFTs::transferFrom` function.

# PoC
NOTE: PoC contains some modifications to prevent deploying all the Superposition contracts for the test. No code was removed and just commented out and added code was marked with `@audit`.

To run this test place the following two files into `pkg/test/MockOwnershipNFTs.sol` and `pkg/test/TestOwnershipNFTs.t.sol` respectively and run the test with `forge test --mt test_userRetainsNFTOwnershipEvenAfterTransfer -vvvv`

MockOwnershipNFTs contract:
```solidity
// SPDX-Identifier: MIT
pragma solidity 0.8.16;

import "../sol/IERC721Metadata.sol";
import "../sol/ISeawaterAMM.sol";

import "../sol/IERC721TokenReceiver.sol";

/*
 * OwnershipNFTs is a simple interface for tracking ownership of
 * positions in the Seawater Stylus contract.
 */
contract OwnershipNFTs is IERC721Metadata {
    ISeawaterAMM public immutable SEAWATER;

    //@audit added to track ownership and mint tokens instead of using SeaWater contract for demo purposes
    mapping(uint256 => address) tokenIdToOwner;
    uint256 tokenIdCounter = 1;

    function mint(address _to) external {
        tokenIdToOwner[tokenIdCounter] = _to;
        tokenIdCounter++;
    }
    //------------------------------------------------------------------

    /**
     * @notice TOKEN_URI to set as the default token URI for every NFT
     * @dev immutable in practice (not set anywhere)
     */
    string public TOKEN_URI;

    /// @notice name of the NFT, set by the constructor
    string public name;

    /// @notice symbol of the NFT, set during the constructor
    string public symbol;

    /**
     * @notice getApproved that can spend the id of the tokens