xanoutas/solidity-security-findings huggingface dataset:
Incorrect checking of the returned data from the callback results in a safeTransferFrom() method revert
Audit: 2024-08-superposition-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

In the current implementation of `OwnershipNFTs` smart contract, there is a `_onTransferReceived()` function that checks whether the smart contract can receive a tokenId and it's used by the `safeTransferFrom()` method. The problem is that the function contains incorrect require statement that makes all the calls revert if the destination is the smart contract address that implements `IERC721TokenReceiver` functionality.

## Proof of Concept

Take a look at the current implementation of `_onTransferReceived()`:

https://github.com/code-423n4/2024-08-superposition/blob/main/pkg/sol/OwnershipNFTs.sol#L73-96
```
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"
        );
    }

```

It checks if the `_to` address implements `onERC721Received()` that enables it to receive ERC721 tokens. But the require statement checks if the data is NOT a `onERC721Received()` function selector making it revert every time the `_to` address is the smart contract address that actually does everything right:

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/IERC721Receiver.sol#L20-27
```
  * The selector can be obtained in Solidity with `IERC721Receiver.onERC721Received.selector`.
     */
    function onERC721Received(
        address operator,
        address from,
        uint256 tokenId,
        bytes calldata data
    ) external returns (bytes4);

```

As you can see, this selector will be exactly what is returned from the callback.

## Tools Used

Manual review.

## Recommended Mitigation Steps

The check should be the following:

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


## Assessed type

Other
approve() function in the OwnershipNFT contract has incorrect authorization 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#L161


# Vulnerability details

## Impact

The current functionality of `OwnershipNFTs` smart contract allows to make approvals of the NFT positions to other addresses. The problem is that the contract is supposed to be ERC-721 compliant but its implementation of authorization checks differs from the one realized in the ERC-721 contract itself.

## Proof of Concept

Here's the current `approve()` behavior:

https://github.com/code-423n4/2024-08-superposition/blob/main/pkg/sol/OwnershipNFTs.sol#L160-163
```
   /// @inheritdoc IERC721Metadata
    function approve(address _approved, uint256 _tokenId) external payable {
        _requireAuthorised(msg.sender, _tokenId);
        getApproved[_tokenId] = _approved;
    }
```

It calls `_requireAuthorised()` function as well as the functions for NFT transfer do for authorization purposes. But, in comparison with ERC-721 standard, it uses different implementation:

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol#L415-417
```
// We do not use _isAuthorized because single-token approvals should not be able to call approve

            if (auth != address(0) && owner != auth && !isApprovedForAll(owner, auth)) {
                revert ERC721InvalidApprover(auth);
            }
```

As you can see, there are several deviations from the ERC-721 standard:


1. The function `requireAuthorized()` requires for the `from` address (that is a `msg.sender` who calls `approve()`) to be the owner while in the ERC-721 standard it's `auth` address.

2. It checks whether `msg.sender` is in the `getApproved` mapping for the given tokenID while in the ERC-721 standard it's only `isApprovedForAll()`.

3. `isApprovedForAll()` checks if the `isApprovedForAll[_from][msg.sender]` statement is true (where `_from` is always msg.sender) where in the implementation of the standard it checks for the `isApprovedForAll(owner, auth)` where `auth` can differ from the `owner`.

This makes the contract non ERC-721 compatible and leads to the users not being able to give approvals of their tokenIDs to other addresses as it's required

## Tools Used

Manual review.

## Recommended Mitigation Steps

Change the check for authorization inside of the `approve()` function to make it ERC-721 compatible.





## Assessed type

Other
OwnershipNFTs contract has Unchecked Return Value of onERC721Received function
Audit: 2024-08-superposition-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/sol/OwnershipNFTs.sol#L73-L96
https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/sol/OwnershipNFTs.sol#L139-L146
https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/sol/OwnershipNFTs.sol#L148-L157


# Vulnerability details

## Impact
Detailed description of the impact of this finding.

Title: OwnershipNFTs contract has Unchecked Return Value of onERC721Received function.
Contract: OwnershipNFTs.sol.
Functions: _onTransferReceived() & X2 safeTransferFrom(). 
Severity: Medium.
Issue Type: Unchecked return value in safeTransferFrom.

The internal function _onTransferReceived has an unchecked IERC function called IERC721TokenReceiver(_to).onERC721Received(_sender, _from, _tokenId, "") which in turn calls 2 duplicate external safeTransferFrom functions.

The safeTransferFrom function calls the internal _onTransferReceived function to handle transferring NFTs to a contract. However, the return value of the onERC721Received function call is not properly checked. Instead of verifying that the returned value matches the expected selector (IERC721TokenReceiver.onERC721Received.selector), the current code logic treats any value not matching the selector as a valid response, resulting in a logic error.

This unchecked return value can allow contracts that are not compliant with ERC-721 to receive NFTs without correctly implementing the onERC721Received function. This could lead to the NFTs being locked in a contract that is unable to return them or interact with them correctly. Malicious contracts could exploit this by falsely claiming compatibility with the ERC-721 interface.

## Proof of Concept
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.

A malicious contract could declare a different selector in the onERC721Received function, bypassing the check and receiving NFTs even though it doesn’t fully comply with ERC-721.
```sol
contract MaliciousContract {
    // Malicious contract that returns a wrong selector
    function onERC721Received(
        address, 
        address, 
        uint256, 
        bytes memory
    ) external pure returns (bytes4) {
        // Return a wrong selector intentionally
        return bytes4(keccak256("wrongSelector()"));
    }
}

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
Incorrect amount taken users may lead to paying excess during erc20 swaps
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#L290


# Vulnerability details

### Impact

Users are charged more than the calculated `amount_in` and excess tokens taken from the user is not refunded leading to loss of funds for the users.

### Proof of Concept

1. Context

`swap_2_internal_erc20` allows users to swap tokens between two pools. The function queries the `swap_2_internal` function which performs the two step swap, first pool to usdc tokens, then usdc tokens to second pool. The thing about this function is that it acts as a two way step involving the amalgamation of a pseudo "swapPoolATokensForExactUSDCTokens" and "swapExactUSDCTokensForPoolBTokens".  As can be seen below, this `amount` specified by the user is passed in to the `swap_2_internal` function and eventually returned as the `original_amount` parameter.

```rust
    pub fn swap_2_internal_erc20(
        pools: &mut Pools,
        from: Address,
        to: Address,
        amount: U256,
        min_out: U256,
        permit2: Option<Permit2Args>,
    ) -> Result<(U256, U256), Revert> {
//...
        let (
            original_amount,
            amount_in,
            amount_out,
            _interim_usdc_out,
            _final_tick_in,
            _final_tick_out,
        ) = Self::swap_2_internal(pools, from, to, amount, min_out)?;
//...
```

The function first attempts to swap tokens in for usdc, passing in the various params includiding the `amount` passed in from `swap_2_internal_erc20` which is the amount specified by the user. After this, the `amount_in` parameter is returned from the `swap` function. Further down the line, we can see that `amount_in` is checked to ensure its psitive before its finally returned as the result of the function.

```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> {
        let original_amount = amount;
//...
        // swap in -> usdc
>>        let (amount_in, interim_usdc_out, final_tick_in) = pools.pools.setter(from).swap(
            true,
>>            amount,
            // swap with no price limit, since we use min_out instead
            tick_math::MIN_SQRT_RATIO + U256::one(),
        )?;
//...
>>        let amount_in = amount_in.abs_pos()?;
//...
        Ok((
>>            original_amount,
>>            amount_in,
            amount_out,
            interim_usdc_out,
            final_tick_in,
            final_tick_out,
        ))
    }
```

Now, during the first swap, it can be observed that the original `amount` of tokens specified by the user is not necessarily be the `amount_in` that is returned during the swap. For the most part, its either the calculated amount or the difference between the amount specified by the user, and the calculated remaining amount. As such, in certain
update_position in pools.rs always checks if pool is enabled
Audit: 2024-08-superposition-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
When the pool is disabled users won't be able to remove their liquidity.

## Proof of Concept
update_position function in pool.rs requires the pool to be enabled however the comment on update_position_internal in lib.rs, which is the caller of update_position states that this check should be performed only when adding liquidity - https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/seawater/src/lib.rs#L687.This will prevent the users from removing their liquidity when the pool is disabled.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Consider checking if the pool is enabled only when the delta is >= 0


## Assessed type

Invalid Validation
get_sqrt_ratio_at_tick() has different implementation than the one in UniV3
Audit: 2024-08-superposition-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

In the current implementation of `tick_math.rs`, `getSqrtRatioAtTick()` differentiates from the implementation in UniswapV3. As the protocol claims to follow UniswapV3 math faithfully, this can result in unexpected behavior due to such deviations.

## Proof of Concept

Firstly, `getSqrtRatioAtTick()` returns a value of different type (uint256) rather than uint160 sqrtPriceX96:

https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/TickMath.sol#L23
```
function getSqrtRatioAtTick(int24 tick) internal pure returns (uint160 sqrtPriceX96) 
```

This is the function from uniswapV3 code. This is the protocol's implementation:

https://github.com/code-423n4/2024-08-superposition/blob/main/pkg/seawater/src/maths/tick_math.rs#L20
```
pub fn get_sqrt_ratio_at_tick(tick: i32) -> Result<U256, Error> 
```

Additionally, tick value has type of `i32` instead of `i24`.

Such deviations can lead to mistakes in calculations as different types for values are used and possible DoS.

## Tools Used

Manual review.

## Recommended Mitigation Steps

Consider changing the current implementation of the function to be UniV3 compliant.




## Assessed type

Other
MAX_SQRT_RATIO has different value than the one in UniV3
Audit: 2024-08-superposition-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

In the current implementation of `tick_math.rs` library, `MAX_SQRT_RATIO` that's initialized as the constant has different value than the same variable in UniswapV3.

## Proof of Concept

`MAX_SQRT_RATIO` represents the maximum price at maximum tick. But its value differs from the UniV3 value. And as the protocol must be UniV3 compliant in terms of math, such deviations can potentially lead to an unexpected behavior as the incorrect values are used:

https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/TickMath.sol#L16
```
  uint160 internal constant MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342;
```

This is the value from UniV3 `TickMath` library and this is the value set by the protocol:

https://github.com/code-423n4/2024-08-superposition/blob/main/pkg/seawater/src/maths/tick_math.rs#L16-17
```
pub const MAX_SQRT_RATIO: U256 =
    U256::from_limbs([6743328256752651558, 17280870778742802505, 4294805859, 0]);

```

Moreover, the variable has type of `uint256` instead of `uint160`.

## Tools Used

Manual review.

## Recommended Mitigation Steps

Initialize `MAX_SQRT_RATIO` with the value from UniV3.


## Assessed type

Other
Values are not casted into uint160 in multiple occasions in sqrt_price_math library
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
https://github.com/code-423n4/2024-08-superposition/blob/main/pkg/seawater/src/maths/sqrt_price_math.rs#L97


# Vulnerability details

## Impact

The current implementation of `sqrt_price_math` library deviates from the one from UniswapV3 as it does not check if the values fit in uint160 (they always should) as it's done in the `SqrtPriceMath` library of Uniswap.

## Proof of Concept

Let's take a look into `getNextSqrtPriceFromAmount0RoundingUp()`. The formula function returns the price after adding or removing amount. Note that it returns `uint256` instead of `uint160` that's needed to be returned:

https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/SqrtPriceMath.sol#L28-33
```
   function getNextSqrtPriceFromAmount0RoundingUp(
        uint160 sqrtPX96,
        uint128 liquidity,
        uint256 amount,
        bool add
    ) internal pure returns (uint160)

```

The values that are returned should always fit into `uint160` as noted by Uniswap:

https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/SqrtPriceMath.sol#L43
```
 // always fits in 160 bits
```

However, due to the protocol deviation from the standard implementation of this library, different kind of issues may arise as the value may not always fit in 160 bits:

https://github.com/code-423n4/2024-08-superposition/blob/main/pkg/seawater/src/maths/sqrt_price_math.rs#L65-70
```
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> 
```

https://github.com/code-423n4/2024-08-superposition/blob/main/pkg/seawater/src/maths/sqrt_price_math.rs#L84
```
return mul_div_rounding_up(numerator_1, sqrt_price_x_96, denominator);
```

https://github.com/code-423n4/2024-08-superposition/blob/main/pkg/seawater/src/maths/sqrt_price_math.rs#L88-91
```
 Ok(div_rounding_up(
            numerator_1,
            (numerator_1.wrapping_div(sqrt_price_x_96)).wrapping_add(amount),
        ))
```

https://github.com/code-423n4/2024-08-superposition/blob/main/pkg/seawater/src/maths/sqrt_price_math.rs#L97
```
   mul_div_rounding_up(numerator_1, sqrt_price_x_96, denominator)
```


## Tools Used

Manual review.

## Recommended Mitigation Steps

Always cast the returning value of this function to the uint160 type making sure it always fits into 160 bits.





## Assessed type

Other
get_next_sqrt_price_from_amount_1_rounding_down() uses overflowing_sub not making sure the value always fits into uint160
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#L139


# Vulnerability details

## Impact

In the current functionality, `get_next_sqrt_price_from_amount_1_rounding_down()` uses `overflowing_sub` functionality that does not revert in the case of overflow making the function not always fit into uint160.

## Proof of Concept

Take a look into `get_next_sqrt_price_from_amount_1_rounding_down()` functionality:

https://github.com/code-423n4/2024-08-superposition/blob/main/pkg/seawater/src/maths/sqrt_price_math.rs#L122-126
```
   if next_sqrt_price > MAX_U160 {
            Err(Error::SafeCastToU160Overflow)
        } else {
            Ok(next_sqrt_price)
        }
```

But at the end of the function `overflowing_sub` is used that will not revert if there is overflow:

https://github.com/code-423n4/2024-08-superposition/blob/main/pkg/seawater/src/maths/sqrt_price_math.rs#L139
```
Ok(sqrt_price_x_96.overflowing_sub(quotient).0)

```

The value `sqrt_price_x_96` has to always fit into uint160.

## Tools Used

Manual review.

## Recommended Mitigation Steps

Change the last line on `Ok(sqrt_price_x_96 - quotient)`



## Assessed type

Other
add_delta() has incorrect checks for the result of the add/sub operations
Audit: 2024-08-superposition-findings
Severity: MEDIUM

# Lines of code

https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/LiquidityMath.sol#L12
https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/LiquidityMath.sol#L14


# Vulnerability details

## Impact

In the current implementation of `liquidity_math` library, the function `add_delta()` makes incorrect checks when adding delta. This can lead to both calculation errors and it makes the protocol not compatible with UniswapV3 Math (as it must be according to the specification).

## Proof of Concept

`add_delta()` current implementation:

https://github.com/code-423n4/2024-08-superposition/blob/main/pkg/seawater/src/maths/liquidity_math.rs#L7-24
```
pub fn add_delta(x: u128, y: i128) -> Result<u128, Error> {
    if y < 0 {
        let z = x.overflowing_sub(-y as u128);

        if z.1 {
            Err(Error::LiquiditySub)
        } else {
            Ok(z.0)
        }
    } else {
        let z = x.overflowing_add(y as u128);
        if z.0 < x {
            Err(Error::LiquidityAdd)
        } else {
            Ok(z.0)
        }
    }
}

```

Now let's take a look on UniV3 `add_delta()`:

```
library LiquidityMath {
    /// @notice Add a signed liquidity delta to liquidity and revert if it overflows or underflows
    /// @param x The liquidity before change
    /// @param y The delta by which liquidity should be changed
    /// @return z The liquidity delta
    function addDelta(uint128 x, int128 y) internal pure returns (uint128 z) {
        if (y < 0) {
            require((z = x - uint128(-y)) < x, 'LS');
        } else {
            require((z = x + uint128(y)) >= x, 'LA');
        }
    }
}

```

As you can see, in the first condition statement it's checked whether the value < x while in the implementation provided by the protocol is different: it checks whether the value < 0 only in the else-statement and skips the check in the if-statement. This makes the contract not following UniswapV3 Math as stated by the spec:

```
We should follow Uniswap V3's math faithfully.
```

Moreover, the function uses `overflowing_sub()` and `overflowing_add()` that are not reverted in the case of underflow / overflow that are supposed to (according to the UniV3 implementation).

## Tools Used

Manual review.

## Recommended Mitigation Steps

Change the implementation of `add_delta()` to be compatible with UniV3 `LiquidityMath` library.


## Assessed type

Other
mulDivRoundingUp() function from full_math library deviates from the Uniswap implementation
Audit: 2024-08-superposition-findings
Severity: MEDIUM

# Lines of code

https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/FullMath.sol#L113


# Vulnerability details

## Impact

The current implementation of `mulDivRoundingUp()` deviates from the one specified in the Uniswap code. This makes the code behave unexpectedly in comparison with the Uniswap implementation.

## Proof of Concept

`full_math` library implements `mulDivRoundingUp()` the following way:

https://github.com/code-423n4/2024-08-superposition/blob/main/pkg/seawater/src/maths/full_math.rs#L59-71
```
pub fn mul_div_rounding_up(a: U256, b: U256, denominator: U256) -> Result<U256, Error> {
    let (result, rem) = _mul_div(a, b, denominator)?;

    if rem {
        if result == U256::MAX {
            Err(Error::ResultIsU256MAX)
        } else {
            Ok(result + U256::one())
        }
    } else {
        Ok(result)
    }
}
```

However, UniV3 library is implemented slightly differently:

https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/FullMath.sol#L113-123
```
    function mulDivRoundingUp(
        uint256 a,
        uint256 b,
        uint256 denominator
    ) internal pure returns (uint256 result) {
        result = mulDiv(a, b, denominator);
        if (mulmod(a, b, denominator) > 0) {
            require(result < type(uint256).max);
            result++;
        }
    }

```

It checks for the `mulmod` operation to be > 0 before comparing the result with type(uint256).max which is not done in the current code of the protocol's library.

## Tools Used

Manual review.

## Recommended Mitigation Steps

Deviation from the Uniswap's library implementation can result in an unexpected behavior when dealing with the actual code of the core functionality.


## Assessed type

Other