xanoutas/solidity-security-findings huggingface dataset:
Price is not correctly updated in the swap() function
Audit: 2024-08-superposition-findings
Severity: MEDIUM

# Lines of code

https://github.com/Uniswap/v3-core/blob/main/contracts/UniswapV3Pool.sol#L751


# Vulnerability details

## Impact

In the current implementation of the `swap()` function the global price is updated after calling `compute_swap_step()` deviating from the Uniswap implementation (as swap() function is similar to that) where price is only updated when the new tick != current tick. This can lead to a situation where price can change but the tick to stay the same which is an unexpected behavior.

## Proof of Concept

Take a look at how price is updated in the protocol:

https://github.com/code-423n4/2024-08-superposition/blob/main/pkg/seawater/src/pool.rs#L496-499
```
  self.sqrt_price.set(state.price);
        if state.tick != self.cur_tick.get().sys() {
            self.cur_tick.set(I32::unchecked_from(state.tick));
        }

```


However, this can create a situation where price was updated but the tick was not (also unexpected):

https://github.com/Uniswap/v3-core/blob/main/contracts/UniswapV3Pool.sol#L753-752
```
        if (state.tick != slot0Start.tick) {
            (uint16 observationIndex, uint16 observationCardinality) =
                observations.write(
                    slot0Start.observationIndex,
                    cache.blockTimestamp,
                    slot0Start.tick,
                    cache.liquidityStart,
                    slot0Start.observationCardinality,
                    slot0Start.observationCardinalityNext
                );
            (slot0.sqrtPriceX96, slot0.tick, slot0.observationIndex, slot0.observationCardinality) = (
                state.sqrtPriceX96,
                state.tick,
                observationIndex,
                observationCardinality
            );
        } else {
            // otherwise just update the price
            slot0.sqrtPriceX96 = state.sqrtPriceX96;
        }
```

The code above is the implementation that's done by UniswapV3 Pool contract where first we check ticks and if they are not equal, we don't update the price, otherwise we just push the price to the slot0 (in our case, just global `sqrt_price` as there is no slot0).

## Tools Used

Manual review.

## Recommended Mitigation Steps

Implement the price update correctly.


## Assessed type

Other
One of the Main Invraiant is broken
Audit: 2024-08-superposition-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-08-superposition/blob/main/pkg/seawater/src/maths/sqrt_price_math.rs#L84
https://github.com/code-423n4/2024-08-superposition/blob/main/pkg/seawater/src/maths/sqrt_price_math.rs#L88-L91
https://github.com/code-423n4/2024-08-superposition/blob/main/pkg/seawater/src/maths/sqrt_price_math.rs#L97
https://github.com/code-423n4/2024-08-superposition/blob/main/pkg/seawater/src/maths/sqrt_price_math.rs#L139


# Vulnerability details

## Description
In projects contest documentation they stated as Main invariant Quote:"We should follow Uniswap V3's math faithfully", however in the current implementation of `get_next_sqrt_price_from_amount_0_rounding_up` this is not exactly as uniswap v3. If we looked at `getNextSqrtPriceFromAmount0RoundingUp` in SqrPriceMath.sol [of uniswap v3](https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/SqrtPriceMath.sol#L28-L56) we will see that on each return statement we always fit the result in 160 bits but this is not being followed in sqrt_price_math.rs as you can verify from the code bellow 
```rust
pub fn get_next_sqrt_price_from_amount_0_rounding_up(
    sqrt_price_x_96: U256,
    liquidity: u128,
    amount: U256,
    add: bool,
) -> Result<U256, Error> {
    ...
                //@audit against uniswap you need to fit 160 bit look at 1st return in getNextSqrtPriceFromAmount0RoundingUp in SqrtPriceMath.sol
                return mul_div_rounding_up(numerator_1, sqrt_price_x_96, denominator);
    ...
    .......
```
The same issue also exist in get_next_sqrt_price_from_amount_1_rounding_down since at the end we dont fit the result in 160 bit as  [this line from uni v3 states](https://github.com/Uniswap/v3-core/blob/d8b1c635c275d2a9450bd6a78f3fa2484fef73eb/contracts/libraries/SqrtPriceMath.sol#L94), the project function is using overflowing_sub.

## Impact
The project isnt faithfully following uniswap math.

## Tools Used
Manual Analysis

## Recommended Mitigation Steps
Change the way getNextSqrtPriceFromAmount0RoundingUp and get_next_sqrt_price_from_amount_1_rounding_down return the results so that it always fits in 160bits


## Assessed type

Uniswap
Liquidity cannot be removed if the pool is disabled
Audit: 2024-08-superposition-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/seawater/src/lib.rs#L687-L688
https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/seawater/src/pool.rs#L91-L92


# Vulnerability details

The comment for the `update_position_internal` function in `lib.rs` states that the pool should allow liquidity removal, even when disabled:

```rust
    /// position owner. Requires the pool to be enabled unless removing liquidity.
    pub fn update_position_internal(
```
[lib.rs#L687-L688](https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/seawater/src/lib.rs#L687-L688)

However, when updating a position in the pool, there is a requirement that only pools with enabled status can be updated:

```rust
    /// Updates a position in this pool, refreshing fees earned and updating liquidity.
    pub fn update_position(&mut self, id: U256, delta: i128) -Result<(I256, I256), Revert{
        // the pool must be enabled
        assert_or!(self.enabled.get(), Error::PoolDisabled);
```
[pool.rs#L91-L92](https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/seawater/src/pool.rs#L91-L92)

This leads to a revert if the pool is disabled, regardless of whether the`delta`parameter is positive or negative (i.e., whether liquidity is being added or removed). Therefore,**liquidity cannot be removed if the pool is disabled**, as the`update_position`function will revert the transaction in that case.

## Impact

In case a pool with added liquidity gets disabled, all users’ funds will remain locked as long as the pool is not enabled again by the seawater admin.

## Tools Used

Manual review.

## Recommended Mitigation Steps

To mitigate this, in`update_position`, only check the pool status if the user wants to add liquidity. This would allow removal of liquidity regardless of the pool status. The`delta`parameter should be utilized to determine this. For instance, in`pool.rs`, replace:

```rust
    pub fn update_position(&mut self, id: U256, delta: i128) -Result<(I256, I256), Revert{
        // the pool must be enabled
        assert_or!(self.enabled.get(), Error::PoolDisabled);
```

[pool.rs#L90-L92](https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/seawater/src/pool.rs#L90-L92)
with:

```rust
    pub fn update_position(&mut self, id: U256, delta: i128) -Result<(I256, I256), Revert{
        // the pool must be enabled to add liquidity
        if delta 0 {
            assert_or!(self.enabled.get(), Error::PoolDisabled); 
        }
```


## Assessed type

Invalid Validation
Inverted Selector Check Causes All ERC721 Safe Transfers to Contract Addresses to Fail
Audit: 2024-08-superposition-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Summary

There is a critical bug in the `_onTransferReceived` function. The function incorrectly checks the return value from the `onERC721Received` callback, causing all safe transfers to contract addresses to fail, even when the receiving contract correctly implements the ERC721 receiver interface.

### Code Snippet

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

```solidity
function _onTransferReceived(
    address _sender,
    address _from,
    address _to,
    uint256 _tokenId
) internal {
    if (_to.code.length == 0) return;

    bytes4 data = IERC721TokenReceiver(_to).onERC721Received(
        _sender,
        _from,
        _tokenId,
        ""
    );

    require(
        data != IERC721TokenReceiver.onERC721Received.selector,
        "bad nft transfer received data"
    );
}
```

The bug is in the `require` statement. It's checking if the returned data is NOT equal to the expected selector, which is the opposite of what it should do.

## Impact
1. All safe transfers to contract addresses will fail, even if the receiving contract correctly implements `onERC721Received`.
2. Users will be unable to transfer NFTs to smart contract wallets or other contracts designed to hold NFTs.
3. This severely limits the functionality and interoperability of the NFT contract.

## Scenario
1. A user attempts to transfer an NFT to a smart contract wallet using `safeTransferFrom`.
2. The smart contract wallet correctly implements `onERC721Received` and returns the expected selector.
3. The transfer fails because the `require` statement in `_onTransferReceived` reverts the transaction.

## Fix
The fix is to change the `!=` operator to `==` in the `require` statement:

```solidity
require(
    data == IERC721TokenReceiver.onERC721Received.selector,
    "bad nft transfer received data"
);
```

This change ensures that the function only proceeds if the receiving contract returns the correct selector, indicating it can handle ERC721 tokens.


## Assessed type

Invalid Validation
Wrong liquidity formula used
Audit: 2024-08-superposition-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Overview
The protocol provides a function to increment a position's liquidity:

```solidity
    function incrPositionC3AC7CAA(
        address pool,
        uint256 id,
        uint256 amount0Min,
        uint256 amount1Min,
        uint256 amount0Desired,
        uint256 amount1Desired
    ) external returns (uint256, uint256);
```

It is present in the SeawaterAmm.sol:

https://github.com/code-423n4/2024-08-superposition/blob/main/pkg/sol/SeawaterAMM.sol#L464

```solidity
    function incrPositionC3AC7CAA(
        address /* pool */,
        uint256 /* id */,
        uint256 /* amount0Min */,
        uint256 /* amount1Min */,
        uint256 /* amount0Desired */,
        uint256 /* amount1Desired */
    ) external returns (uint256, uint256) {
        directDelegate(_getExecutorUpdatePosition());
    }
```

It calls a function in lib.rs:

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

```rust
    #[allow(non_snake_case)]
    pub fn incr_position_C_3_A_C_7_C_A_A(
        &mut self,
        pool: Address,
        id: U256,
        amount_0_min: U256,
        amount_1_min: U256,
        amount_0_desired: U256,
        amount_1_desired: U256,
    ) -> Result<(U256, U256), Revert> {
        self.adjust_position_internal(
            pool,
            id,
            amount_0_min,
            amount_1_min,
            amount_0_desired,
            amount_1_desired,
            false,
            None,
        )
    }
```

This calls an internal function in lib.rs:

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

```solidity
    #[allow(clippy::too_many_arguments)]
    pub fn adjust_position_internal(
        &mut self,
        pool: Address,
        id: U256,
        amount_0_min: U256,
        amount_1_min: U256,
        amount_0_desired: U256,
        amount_1_desired: U256,
        giving: bool,
        permit2: Option<(Permit2Args, Permit2Args)>,
    ) -> Result<(U256, U256), Revert> {
        assert_eq_or!(
            msg::sender(),
            self.position_owners.get(id),
            Error::PositionOwnerOnly
        );

        let (amount_0, amount_1) = self.pools.setter(pool).adjust_position(
            id,
            amount_0_desired,
            amount_1_desired,
            giving,
        )?;
        .
        .
        .
    }
```

This function calls a function in pool.rs:

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

```rust
    pub fn adjust_position(
        &mut self,
        id: U256,
        amount_0: U256,
        amount_1: U256,
        giving: bool,
    ) -> Result<(I256, I256), Revert> {
        // calculate the delta using the amounts that we have here, guaranteeing
        // that we don't dip below the amount that's supplied as the minimum.
swap2ExactIn41203F1D() function won't work
Audit: 2024-08-superposition-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Overview
The protocol provides a facility for users to execute a 2-stage swap:

https://github.com/code-423n4/2024-08-superposition/blob/main/pkg/sol/SeawaterAMM.sol#L244

```solidity
    function swap2ExactIn41203F1D(address /* tokenA */, address /* tokenB */, uint256 /* amountIn */, uint256 /* minAmountOut */) external returns (uint256, uint256) {
        directDelegate(_getExecutorSwap());
    }
```

This function delegate calls the following function in lib.rs:

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

```rust
    #[allow(non_snake_case)]
    pub fn swap_2_exact_in_41203_F1_D(
        &mut self,
        from: Address,
        to: Address,
        amount: U256,
        min_out: U256,
    ) -> Result<(U256, U256), Revert> {
        Pools::swap_2_internal_erc20(self, from, to, amount, min_out, None)
    }
}
```

During execution the following internal function is called in lib.rs:

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

```rust
    fn swap_2_internal(
        pools: &mut Pools,
        from: Address,
        to: Address,
        amount: U256,
        min_out: U256,
    ) -> Result<(U256, U256, U256, I256, i32, i32), Revert> {
    
    .
    .
    .
    
    }
```

Everything goes fine until LOC-241:

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

```rust
assert_eq_or!(interim_usdc_out, interim_usdc_in, Error::InterimSwapNotEq);
```

This assert statement would not always hold and therefore, the transaction would revert.

## Proof of Concept
The following is a detail step-by-step PoC.

The issue at LOC-241 is:

```rust
assert_eq_or!(interim_usdc_out, interim_usdc_in, Error::InterimSwapNotEq);
```

Basically, the equality is forced to have between amountSpecified and final amount1 of a swap operation:

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

```rust
        let (amount_out, interim_usdc_in, final_tick_out) = pools.pools.setter(to).swap(
            false,
            interim_usdc_out,
            tick_math::MAX_SQRT_RATIO - U256::one(),
        )?;
```

This would not always hold as in the swap operation:

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

```rust
        let token0_is_input = (zero_for_one && exact_in) || (!zero_for_one && !exact_in);
        let (amount_0, amount_1) = match token0_is_input {
            true => (amount - state.amount_remaining, state.amount_calculated),
            false => (state.amount_calculated, amount - state.amount_remaining),
        };
```

As can be seen in the false arm:

```python
amount1 = amountSpecified - state.amount_remaining

Therefore, amount1 != amountSpecified if state.amount_remaining != 0
```

So, the 2-stage swap wo
callback of OwnershipNFT's safe operation has wrong require check
Audit: 2024-08-superposition-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

A wrong callback result check on an NFT transfer will block all NFT transfers.

## Proof of Concept

When a position owner decides to transfer his position to someone, he will prefer to call the safe operations. `OwnershipNFT` decided to implement these features themselves instead of using `OpenZeppelin`, and now the safeOperations callback will never work due to faulty validation, causing safeOperations to fail.

When `onERC721Received()` is called, **it must return the `onERC721Received.selector`**, but `_onTransferReceived()` wants the result of this to be different. This will cause the contract that uses `onERC721Received` validly and for some purpose to fail and perhaps force it to use a normal transfer, or if the contract that owns the position strictly uses safeTransfer in it (ie. is not an EOA which can attempt a normal transfer instead) the position will not be transferable.

```solidity
function _onTransferReceived(
    address _sender,
    address _from,
    address _to,
    uint256 _tokenId
) internal {
    // only call the callback if the receiver is a contract
    if (_to.code.length == 0) return;

    bytes4 data = IERC721TokenReceiver(_to).onERC721Received(
        _sender,
        _from,
        _tokenId,

        // this is empty byte data that can be optionally passed to
        // the contract we're confirming is able to receive NFTs
        ""
    );

    require(
        data != IERC721TokenReceiver.onERC721Received.selector,  <-------------------------------
        "bad nft transfer received data"
    );
}
```

## Tools Used

Manual Review

## Recommended Mitigation Steps

```diff
function _onTransferReceived(
    address _sender,
    address _from,
    address _to,
    uint256 _tokenId
) internal {
    // only call the callback if the receiver is a contract
    if (_to.code.length == 0) return;

    bytes4 data = IERC721TokenReceiver(_to).onERC721Received(
        _sender,
        _from,
        _tokenId,

        // this is empty byte data that can be optionally passed to
        // the contract we're confirming is able to receive NFTs
        ""
    );

    require(
-       data != IERC721TokenReceiver.onERC721Received.selector,
+       data == IERC721TokenReceiver.onERC721Received.selector,
        "bad nft transfer received data"
    );
}
```


## Assessed type

ERC721
Missing check if liquidity is not increasing when updating positions
Audit: 2024-08-superposition-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-08-superposition/blob/main/pkg/seawater/src/pool.rs#L94-L255
https://github.com/code-423n4/2024-08-superposition/blob/main/pkg/seawater/src/position.rs#L41-L72


# Vulnerability details

## Impact

Missing check allows users to create positions where they never deposit and never add liquidity.

## Proof of Concept

When an user adds `liquidity`, it will call `adjust_position()` which will calculate the delta and then call `update_position()` where all the calculations are done and the position is updated.

- [pool.rs#L94-L255](https://github.com/code-423n4/2024-08-superposition/blob/main/pkg/seawater/src/pool.rs#L94-L255) - `update_position()`
    
    ```rust
    pub fn update_position(&mut self, id: U256, delta: i128) -> Result<(I256, I256), Revert> {
        // the pool must be enabled
        assert_or!(self.enabled.get(), Error::PoolDisabled);
    
        let position = self.positions.positions.get(id);
        let lower = position.lower.get().sys();
        let upper = position.upper.get().sys();
    
        // update the ticks
        let cur_tick = self.cur_tick.get().sys();
        let fee_growth_global_0 = self.fee_growth_global_0.get();
        let fee_growth_global_1 = self.fee_growth_global_1.get();
        let max_liquidity_per_tick = self.max_liquidity_per_tick.get().sys();
    
        #[cfg(feature = "testing-dbg")]
        dbg!((
            "inside update_position before delta check",
            delta,
            cur_tick,
            fee_growth_global_0,
            fee_growth_global_1,
        ));
    
        let mut flipped_lower = false;
        let mut flipped_upper = false;
    
        if delta != 0 {
            flipped_lower = self.ticks.update(
                lower,
                cur_tick,
                delta,
                &fee_growth_global_0,
                &fee_growth_global_1,
                false,
                max_liquidity_per_tick,
            )?;
    
            flipped_upper = self.ticks.update(
                upper,
                cur_tick,
                delta,
                &fee_growth_global_0,
                &fee_growth_global_1,
                true,
                max_liquidity_per_tick,
            )?;
    
            #[cfg(feature = "testing-dbg")]
            dbg!(("before if flip lower", flipped_lower, flipped_upper));
    
            // clear unneeded storage
            if flipped_lower {
                self.tick_bitmap.flip(lower, self.tick_spacing.get().sys());
            }
            if flipped_upper {
                self.tick_bitmap.flip(upper, self.tick_spacing.get().sys());
            }
        }
    
        // update the position
        let (fee_growth_inside_0, fee_growth_inside_1) = self.ticks.get_fee_growth_inside(
            lower,
            upper,
            self.cur_tick.get().sys(),
            &self.fee_growth_global_0.get(),
            &self.fee_growth_global_1.get(),
        )?;