xanoutas/solidity-security-findings huggingface dataset:
Admin cannot collect protocol fee due to Incorrect Parameter Order in ERC20 Transfer Function Call 
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#L1149-L1150


# Vulnerability details

## Impact
- If the recipient address is not a valid token contract, the function will consistently revert so admin cannot collect the protocol fee.
- If the recipient address is a valid token contract , The function calls transfer tokens from the wrong addresses (the recipient address instead of the intended token address).

Either way admin cannot collect the protocol fee 

## Proof of Concept
In the `collect_protocol_7540_F_A_9_F` function, there is a misuse of the `erc20::transfer_to_addr` function. The correct parameter order for this function is (token, recipient, amount) 
https://github.com/code-423n4/2024-08-superposition/blob/main/pkg/seawater/src/wasm_erc20.rs#L158

but in the current implementation, the order is incorrect as it sending recipient in place of token Address .
```
erc20::transfer_to_addr(recipient, pool, U256::from(token_0))?;
erc20::transfer_to_addr(recipient, FUSDC_ADDR, U256::from(token_1))?;
```

## Tools Used
Manual Review

## Recommended Mitigation Steps
Correct the parameter order in the `erc20::transfer_to_addr` function calls:
```
erc20::transfer_to_addr(pool, recipient, U256::from(token_0))?;
erc20::transfer_to_addr(FUSDC_ADDR, recipient, U256::from(token_1))?;

```



## Assessed type

Token-Transfer
when minting position we can pass low tick > upper tick, unlike UniswapV3
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-L87


# Vulnerability details

## Impact

Missing check allows the user to create a position with a `lowerTick > upperTick`.

## Proof of Concept

Position is created using `mint_position_B_C5_B086_D()` - https://github.com/code-423n4/2024-08-superposition/blob/main/pkg/seawater/src/lib.rs#L495, and `pool.create_position()` - https://github.com/code-423n4/2024-08-superposition/blob/main/pkg/seawater/src/pool.rs#L75-L87. 

```rust
📁 pkg/seawater/src/lib.rs
pub fn mint_position_B_C5_B086_D(
    &mut self,
    pool: Address,
    lower: i32,
    upper: i32,
) -> Result<U256, Revert> {
    let id = self.next_position_id.get();
    self.pools.setter(pool).create_position(id, lower, upper)?;

    self.next_position_id.set(id + U256::one());

    let owner = msg::sender();

    self.grant_position(owner, id);

    #[cfg(feature = "log-events")]
    evm::log(events::MintPosition {
        id,
        owner,
        pool,
        lower,
        upper,
    });

    Ok(id)
}

📁 pkg/seawater/src/pool.rs

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

As you can see, the lower and upper ticks are not checked if `lower < upper`, they are only checked if they are between `min_tick` and `max_tick`. Therefore, if `min_tick` = 0 and `max_tick` = 20, passing `lower` = 10 and `upper` = 5 will pass.

This should be checked when creating a position, as was done in `Uniswap` **-** https://github.com/Uniswap/v3-core/blob/6562c52e8f75f0c10f9deaf44861847585fc8129/contracts/UniswapV3Pool.sol#L316

```solidity
function _modifyPosition(ModifyPositionParams memory params)
    private
    noDelegateCall
    returns (
        Position.Info storage position,
        int256 amount0,
        int256 amount1
    )
{
    checkTicks(params.tickLower, params.tickUpper); <------------------------

    ...
}
```

https://github.com/Uniswap/v3-core/blob/6562c52e8f75f0c10f9deaf44861847585fc8129/contracts/UniswapV3Pool.sol#L122-L126

```solidity
function checkTicks(int24 tickLower, int24 tickUpper) private pure {
    if (tickLower >= tickUpper) revert TLU(); <------------------------
    if (tickLower < TickMath.MIN_TICK) revert TLM();
    if (tickUpper > TickMath.MAX_TICK) revert TUM();
}
```

This can lead to a lot of different probl
position.update fees operations must be in unchecked
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#L43-L66


# Vulnerability details

## Impact

`overflow`/`underflow` when updating the position fees is prevented, which may give wrong values.

## Proof of Concept

When the position is updated, `owed_fees` are updated, but all operations (add and subtract) use functions that prevent `overflow`/`underflow` (eg `checked_sub`, `wrapping_add`).

[position.rs#L43-L66](https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/seawater/src/position.rs#L43-L66)

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

https://docs.rs/alloy-primitives/0.7.6/alloy_primitives/aliases/type.U256.html#method.checked_sub

```rust
/// 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,
    }
}
```

But this is not the case, how this is done in the `Uniswap`’s `Position` library, there all these operations are in an `unchecked` block, since `overflow` and `underflow` should not prevent these operations and are expected.

https://github.com/Uniswap/v3-core/blob/6562c52e8f75f0c10f9deaf44861847585fc8129/contracts/libraries/Position.sol#L66-L91

```rust
function update(
    Info storage self,
    int128 liquidityDelta,
    uint256 feeGrowthInside0X128,
    uint256 feeGrowthInside1X128
) internal {
    Info memory _self = self;

    uint128 liquidityNext;
    if (liquidityDelta == 0) {
        if (_self.liquidity <= 0) revert NP(); // disallow pokes for 0 liquidity positions
        liquidityNext = _self.liquidity;
    } else {
        liquidityNext = liquidityDelta < 0
            ? _self.liquidity - uint128(-liquidityDelta)
            : _self.liquidity + uint128(liquidityDelta);
    }

    // calculate accumulated fees. overflow in the subtraction of fee growth is expected
    uint128 tokensOwed0;
    uint128 tokensOwed1;
    unchecked {
        tokensOwed0 = uint128(
            FullMath.mulDiv(
                feeGrowthInside0X128 - _self.feeGrowthInside0LastX128,
                _self.liquidity,
                FixedPoint128.Q128
            )
`approve()` shouldn't be called by the approved address for that `tokenId`
Audit: 2024-08-superposition-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
The current implementation of `OwnershipNFTs::approve()` function allows the `approved` address to call `approve()` on the same `tokenId` for which he is approved. This puts the owner at risk as any approved address can proceed to approve any other users to this token without his consent.

## Proof of Concept
[EIP721](https://eips.ethereum.org/EIPS/eip-721#specification) states that `approve()`:
```solidity
    ///  Throws unless `msg.sender` is the current NFT owner, or an authorized operator of the current owner.
```
However, the [current implementation](https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/sol/OwnershipNFTs.sol#L160-L163) breaks this access control provided by the standard.
```solidity
    function approve(address _approved, uint256 _tokenId) external payable {
>>      _requireAuthorised(msg.sender, _tokenId);
        getApproved[_tokenId] = _approved;
    }
```

In the contract, `approve()` uses [`_requireAuthorised()`](https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/sol/OwnershipNFTs.sol#L98-L107) which checks if:
- The caller is owner
- caller is approved for that tokenId
- caller is an operator
```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 setup violates the set standard as it should only check the caller is `current NFT owner`, or an `authorized operator`.

## Tools Used
Manual Review

## Recommended Mitigation Steps
When `approve()`is called, check only if `msg.sender` is the `owner`, or `isApprovedForAll`.
```diff
    function approve(address _approved, uint256 _tokenId) external payable {
-       _requireAuthorised(msg.sender, _tokenId);
+       owner = ownerOf(_tokenId);
+       require(msg.sender == owner || isApprovedForAll[owner][msg.sender], "Caller not authorized");
        getApproved[_tokenId] = _approved;
    }
```


## Assessed type

Access Control
Incorrect emergency council update
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
Emergency council cannot be set because of the incorrect update.

## Proof of Concept
The seawater admin can update the emergency council address. 
The problem is that when we try to update the `emergency_council`, we set the incorrect variable `nft_manager`. This will cause that we cannot set the `emergency_council`.

```javascript
    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
Manaul

## Recommended Mitigation Steps
Update the correct variable `emergency_council` when we trigger `update_emergency_council_7_D_0_C_1_C_58`.


## Assessed type

Error
Protocol earnings are permanently lost/locked
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#L1149-L1150


# Vulnerability details

## Impact
It will be impossible to withdraw the accrued protocol fees, so funds are permanently locked inside the contract.

## Proof of Concept

The `collect_protocol` function is designed to transfer the FUSDC and pool token protocol fees to a recipient:

```rust
    #[allow(non_snake_case)]
    pub fn collect_protocol_7540_F_A_9_F(
        &mut self,
        pool: Address,
        amount_0: u128,
        amount_1: u128,
        recipient: Address,
    ) -> Result<(u128, u128), Revert> {
        assert_eq_or!(
            msg::sender(),
            self.seawater_admin.get(),
            Error::SeawaterAdminOnly
        );

        let (token_0, token_1) = self
            .pools
            .setter(pool)
            .collect_protocol(amount_0, amount_1)?;

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

        #[cfg(feature = "log-events")]
        evm::log(events::CollectProtocolFees {
            pool,
            to: recipient,
            amount0: token_0,
            amount1: token_1,
        });

        // transfer tokens
        Ok((token_0, token_1))
    }
```
https://github.com/code-423n4/2024-08-superposition/blob/main/pkg/seawater/src/lib.rs#L1149-L1150

The issue is that the token address and recipient are inverted in order, so the previous calls will always revert, as they use the recipient as the token address instead:

```rust
	pub fn transfer_to_addr(token: Address, recipient: Address, amount: U256) -> Result<(), Error> {
	    safe_transfer(token, recipient, amount)
	}
```
https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/seawater/src/wasm_erc20.rs#L158

## Tools Used
Manual Review

## Recommended Mitigation Steps

Consider fixing the argument order:

```diff
    #[allow(non_snake_case)]
    pub fn collect_protocol_7540_F_A_9_F(
        &mut self,
        pool: Address,
        amount_0: u128,
        amount_1: u128,
        recipient: Address,
    ) -> Result<(u128, u128), Revert> {
        assert_eq_or!(
            msg::sender(),
            self.seawater_admin.get(),
            Error::SeawaterAdminOnly
        );

        let (token_0, token_1) = self
            .pools
            .setter(pool)
            .collect_protocol(amount_0, amount_1)?;

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

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

        #[cfg(feature = "log-events")]
        evm::log(events::CollectProtocolFees {
            pool,
            to: recipient,
            amount0: token_0,
            amount1: token_1,
An attacker can create invalid positions to inflate a pool
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
It is possible to inflate a pool by creating a position with an invalid, inverted tick range. This can be abused to artificially change the pool price to the attacker's advantage.

## Proof of Concept

It's possible to create a position that has a low tick higher than the low tick as there isn't a check to ensure so:

```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);
->      // @audit no check for low < up, only boundaries are checked
        self.positions.new(id, low, up);
        Ok(())
    }
```
https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/seawater/src/pool.rs#L75

An attacker can abuse this by creating positions that have inverted ticks, which doesn't make sense as it breaks the Uniswap math.

A potential scenario is that this could be abused to inflate the pool by only sending token0, as this branch will always be triggered regardless of the current tick.

```rust
    let (amount_0, amount_1) = if self.cur_tick.get().sys() < lower {
        #[cfg(feature = "testing-dbg")]
        dbg!((
            "update_position, cur_tick < lower path",
            lower,
            upper,
            tick_math::get_sqrt_ratio_at_tick(lower)?,
            tick_math::get_sqrt_ratio_at_tick(upper)?,
            self.sqrt_price.get(),
            delta
        ));

        // we're below the range, we need to move right, we'll need more token0
        (
            sqrt_price_math::get_amount_0_delta(
                tick_math::get_sqrt_ratio_at_tick(lower)?,
                tick_math::get_sqrt_ratio_at_tick(upper)?,
                delta,
            )?,
            I256::zero(),
        )
    }
```
https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/seawater/src/pool.rs#L172-L193

As a result, this can be leveraged to influence the pool price to the attacker's advantage.

Run the following POC in `pkg/seawater/tests/pools.rs`:

```rust
    #[test]
    fn test_inverted_low_high() -> Result<(), Vec<u8>> {
        test_utils::with_storage::<_, StoragePool, _>(None, None, None, None, |storage| {
            storage.init(
Approvals are not revoked on transfer
Audit: 2024-08-superposition-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

Currently, approvals allow other users to transfer tokens on behalf of the owner. However, the approval is not revoked after a transfer, leaving the approved user with continued authority over the tokens, even after they are transferred to a new owner. This could enable the approved user to retain control over the tokens, even when they no longer belong to the original owner.

## Impact
- **Unauthorized Token Control:** Previously approved addresses retain permissions to operate on the token even after it has been transferred to a new owner. This could lead to unauthorized transfers or actions on the new owner’s token.

- **Non-Compliance with ERC721 Standard:** By failing to clear approvals post-transfer, the contract deviates from the ERC721 standard, impacting both its security and functionality.

## Proof of Concept
- OwnershipNFTs.sol
```
    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!");
    }

    function _transfer(
        address _from,
        address _to,
        uint256 _tokenId
    ) internal {
        _requireAuthorised(_from, _tokenId);
        SEAWATER.transferPositionEEC7A3CD(_tokenId, _from, _to);
    }

```
- pkg/seawater/src/lib.rs
```rust
    #[allow(non_snake_case)]
    pub fn transfer_position_E_E_C7_A3_C_D(
        &mut self,
        id: U256,
        from: Address,
        to: Address,
    ) -> Result<(), Revert> {
        assert_eq_or!(msg::sender(), self.nft_manager.get(), Error::NftManagerOnly);

        self.remove_position(from, id);
        self.grant_position(to, id);

        #[cfg(feature = "log-events")]
        evm::log(events::TransferPosition { from, to, id });

        Ok(())
    }
```

## Tools Used
Manual Review

## Recommended Mitigation Steps
```diff
    function _transfer(
        address _from,
        address _to,
        uint256 _tokenId
    ) internal {
        _requireAuthorised(_from, _tokenId);
        SEAWATER.transferPositionEEC7A3CD(_tokenId, _from, _to);
+       delete getApproved[_tokenId];
    }
```





## Assessed type

Token-Transfer
Incorrect Decoding of Swap Results Leads to Ineffective Slippage Protection
Audit: 2024-08-superposition-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/sol/SeawaterAMM.sol#L316
https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/sol/SeawaterAMM.sol#L316
https://github.com/code-423n4/2024-08-superposition/blob/4528c9d2dbe1550d2660dac903a8246076044905/pkg/seawater/src/lib.rs#L194


# Vulnerability details

The `swapOut5E08A399()` and `swapOutPermit23273373B()` functions in the `SeawaterAMM` contract incorrectly decode the results of a swap operation, leading to ineffective slippage protection. This issue arises from a mismatch between the swap direction and the order of decoded values.

In both functions, a swap is executed with `zeroForOne` set to `false`, indicating a swap from token1 to token0. However, when decoding the swap results, the order of `swapAmountIn` and `swapAmountOut` is reversed, causing the slippage check to be performed on the wrong value. This is because the function `swap904369BE` returns `amount0, amount1` so is the caller which should interpret which corresponds to token in and token out. This incorrect decoding leads to a situation where the slippage protection is not applied as intended, potentially exposing users to unexpected losses.
## Impact
Users may receive fewer tokens than expected without the transaction reverting, leading to financial losses. This issue could be exploited by malicious actors to manipulate swap outcomes to their advantage.

## Proof of Concept
1. A user calls `swapOut5E08A399()` or `swapOutPermit23273373B()` to swap token1 for token0.
2. The function executes the swap with `zeroForOne` set to `false`.
3. The swap is successful, returning the amounts swapped.
4. The slippage check is performed on `swapAmountOut`, which actually contains the input amount.
5. The user receives fewer tokens than expected without the transaction reverting.

## Tools Used
Manual review

## Recommended Mitigation Steps
Correct the decoding of swap results in both `swapOut5E08A399()` and `swapOutPermit23273373B()` functions.


## Assessed type

Other