xanoutas/solidity-security-findings huggingface dataset:
Token balances can be made to not correctly reflect the underlying position
Audit: 2023-11-panoptic-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-11-panoptic/blob/main/contracts/SemiFungiblePositionManager.sol#L621-L630


# Vulnerability details

## Impact
Token balances won't reflect underlying position

## Proof of Concept
When transferring, the entire liquidity position is transferred to the other user. The checks done for the sender are:
1. the user has enough of the token (if transferring x amount of token, the user is supposed to have x amount)
2. the liquidity amount corresponding to the token amount is equal to the current `netLiquidity` of the position
https://github.com/code-423n4/2023-11-panoptic/blob/main/contracts/SemiFungiblePositionManager.sol#L621-L630
```solidity
    function registerTokenTransfer(address from, address to, uint256 id, uint256 amount) internal {
        
            ........

            uint256 fromLiq = s_accountLiquidity[positionKey_from];
            if (fromLiq.rightSlot() != liquidityChunk.liquidity()) revert Errors.TransferFailed();


            int256 fromBase = s_accountFeesBase[positionKey_from];


            //update+store liquidity and fee values between accounts
            s_accountLiquidity[positionKey_to] = fromLiq;
            s_accountLiquidity[positionKey_from] = 0;


            s_accountFeesBase[positionKey_to] = fromBase;
            s_accountFeesBase[positionKey_from] = 0;
```
This makes many paths possible where the token balances of an address doesn't represent the underlying position
Eg: 
1. User mints short with posSize = 100 getting liquidity 100
2. User mints long with posSize = 50 making netLiquidity = 50 and removedLiquidity = 50
3. User transfers the short token with amount 50 amount.
Now the user has 50 short token and 100 long token although the user's liquidity position is 0

It is also not checked whether the token being transferred represents a `short` even though the `netLiquidity` is moved
 
## Tools Used
Manual review

## Recommended Mitigation Steps
If this is not intended behaviour, seperate the transfer of `removedLiquidity` and `netLiquidity` based on `long/short` nature of the token leg and decrease the `feeBase` only for `short` transfers.


## Assessed type

Token-Transfer
ERC777 token reentrancy allows to steal fees
Audit: 2023-11-panoptic-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-11-panoptic/blob/main/contracts/SemiFungiblePositionManager.sol#L1031-L1066


# Vulnerability details

## Impact
An attacker can steal fees in case of ERC-777 tokens

## Proof of Concept
When minting a position, `s_accountFeesBase` is updated only after the token transfer to Uniswap.
https://github.com/code-423n4/2023-11-panoptic/blob/main/contracts/SemiFungiblePositionManager.sol#L1031-L1066
```solidity
    function _createLegInAMM(
        IUniswapV3Pool _univ3pool,
        uint256 _tokenId,
        uint256 _leg,
        uint256 _liquidityChunk,
        bool _isBurn
    ) internal returns (int256 _moved, int256 _itmAmounts, int256 _totalCollected) {
        
        ..........

            s_accountLiquidity[positionKey] = uint256(0).toLeftSlot(removedLiquidity).toRightSlot(
                updatedLiquidity
            );
        }

        {
            
            .......... 


            _moved = isLong == 0
=>              ? _mintLiquidity(_liquidityChunk, _univ3pool)
                : _burnLiquidity(_liquidityChunk, _univ3pool); // from msg.sender to Uniswap
            
        ............

=>          s_accountFeesBase[positionKey] = _getFeesBase(
            _univ3pool,
            updatedLiquidity,
            _liquidityChunk
        );
    }
```

In case of an ERC-777 token a user can reenter `SemiFungiblePositionManager` and transfer the position/token to another one of his controlled address. 
This will cause the transferred address to have the updated liquidity but the old `feesBase` and the remaining calculation in the original address to have a reduced `feeBase`. This can be used to earn higher fees than what should actually be distributed to the user
https://github.com/code-423n4/2023-11-panoptic/blob/main/contracts/SemiFungiblePositionManager.sol#L626-L630
```solidity
    function registerTokenTransfer(address from, address to, uint256 id, uint256 amount) internal {
        
            ...........

            int256 fromBase = s_accountFeesBase[positionKey_from];

            s_accountLiquidity[positionKey_to] = fromLiq;
            s_accountLiquidity[positionKey_from] = 0;

            // @audit sets feeBase of current address to 0. the fees calculation after reentry will use this value
            s_accountFeesBase[positionKey_to] = fromBase;
            s_accountFeesBase[positionKey_from] = 0;
```
https://github.com/code-423n4/2023-11-panoptic/blob/main/contracts/SemiFungiblePositionManager.sol#L1209
```solidity
    function _collectAndWritePositionData(
        uint256 liquidityChunk,
        IUniswapV3Pool univ3pool,
        uint256 currentLiquidity,
        bytes32 positionKey,
        int256 movedInLeg,
        uint256 isLong
    ) internal returns (int256 collectedOut) {
        uint128 startingLiquidity = currentLiquidity.rightSlot();
        int256 amountToCollect = _getFeesBase(univ3pool, startingLiquidity, liquidityChunk).sub(
            s_accountFeesBase[pos
Exploit reentrancy in transfer function, can get double postion token in mintTokenizedPosition function.
Audit: 2023-11-panoptic-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-11-panoptic/blob/aa86461c9d6e60ef75ed5a1fe36a748b952c8666/contracts/SemiFungiblePositionManager.sol#L1031
https://github.com/code-423n4/2023-11-panoptic/blob/aa86461c9d6e60ef75ed5a1fe36a748b952c8666/contracts/SemiFungiblePositionManager.sol#L1051
https://github.com/code-423n4/2023-11-panoptic/blob/aa86461c9d6e60ef75ed5a1fe36a748b952c8666/contracts/SemiFungiblePositionManager.sol#L1209
https://github.com/code-423n4/2023-11-panoptic/blob/aa86461c9d6e60ef75ed5a1fe36a748b952c8666/contracts/SemiFungiblePositionManager.sol#L1222
https://github.com/code-423n4/2023-11-panoptic/blob/aa86461c9d6e60ef75ed5a1fe36a748b952c8666/contracts/SemiFungiblePositionManager.sol#L1062


# Vulnerability details

## Impact
If erc20 token in uniswap v3 pool has transfer funtion which will call other contract,it will be exploit to reentrancy in "mintTokenizedPosition" ，and transfer a "backup" of the erc1155 position baseFee state before it update, and update one in postion key. As a effect of this operation, it will get more collect fee from uniswap V3 to msg.sender.

## Proof of Concept
we asseume a uniswap v3 pool called P0,and it has one can be exploited token A.

Token A has a tranfer function which can call attack contract function. Like onERC721Receiver() to check receiver,it has an onERC20Spender() to check spender,or this token is used to phishing attack,it has a call to arbitrary contract logic.

When someone mint one tokened position with one leg  which's isLong = 0 in P0 pool,it will call mintTokenizedPosition function, and mint ERC1155 token to msg.sender before create:

[https://github.com/code-423n4/2023-11-panoptic/blob/aa86461c9d6e60ef75ed5a1fe36a748b952c8666/contracts/SemiFungiblePositionManager.sol#L510](https://github.com/code-423n4/2023-11-panoptic/blob/aa86461c9d6e60ef75ed5a1fe36a748b952c8666/contracts/SemiFungiblePositionManager.sol#L510)
```solidity
function mintTokenizedPosition(
        uint256 tokenId,
        uint128 positionSize,
        int24 slippageTickLimitLow,
        int24 slippageTickLimitHigh
    )
        external
        ReentrancyLock(tokenId.univ3pool())
        returns (int256 totalCollected, int256 totalSwapped, int24 newTick)
    {
        _mint(msg.sender, tokenId, positionSize);

        emit TokenizedPositionMinted(msg.sender, tokenId, positionSize);
      	(totalCollected, totalSwapped, newTick) = _validateAndForwardToAMM(
            tokenId,
            positionSize,
            slippageTickLimitLow,
            slippageTickLimitHigh,
            MINT
        );
    }
```
then call internal _createLegInAMM function,it will record one state:

[https://github.com/code-423n4/2023-11-panoptic/blob/aa86461c9d6e60ef75ed5a1fe36a748b952c8666/contracts/SemiFungiblePositionManager.sol#L959](https://github.com/code-423n4/2023-11-panoptic/blob/aa86461c9d6e60ef75ed5a1fe36a748b952c8666/contracts/SemiFungiblePositionManager.sol#L959)
```solidity
uint256 currentLiquidity = s_accountLiquidit
Transferring 0 amounts of non-existent tokens when all liquidity is removed results in unexpected behaviors
Audit: 2023-11-panoptic-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-11-panoptic/blob/aa86461c9d6e60ef75ed5a1fe36a748b952c8666/contracts/tokens/ERC1155Minimal.sol#L90-L118
https://github.com/code-423n4/2023-11-panoptic/blob/aa86461c9d6e60ef75ed5a1fe36a748b952c8666/contracts/SemiFungiblePositionManager.sol#L586-L630


# Vulnerability details

## Impact
Currently, ERC1155Minimal.sol allows 0 transfers, which means that tokens that were not minted can be "transferred". Normally this wouldn't be a problem, but in `registerTokenTransfer` a 0 transfer can actually transfer non-zero liquidity due to the possibility of different token ids sharing the same liquidity position key.

This could lead to unexpected scenarios in which liquidity gets trapped or removedLiquidity underflows, affecting the calculation of premiums and fees.

## Proof of Concept
Here is a simple test example that exploits the mentioned behavior to reach an unexpected state https://gist.github.com/fnanni-0/8ebb660b1d4ef518a409e2b82b293113.
Running `forge test -vv --match-test test_Success_ZeroTransfers` outputs a step by step explanation of what is happening:

```
Alice mints 1010 of token C (short put, i.e. adds liquidity to AMM).

  Minted positionSize: 1010
  Alice balance token C: 1010
  Added liquidity: 50
  Removed liquidity: 0
  

Alice mints 1010 of token A (long put, i.e. removes all liquidity from AMM). Token A and Token C have the same position key.

  Minted positionSize: 1010
  Alice balance token A: 1010
  Added liquidity: 0
  Removed liquidity: 50
  

Zero transfer of non-existent token B to Bob. Alice is left with active positions (1010 of token C and 1010 of token A) but liquidity is now empty.

  Alice balance token C: 1010
  Alice balance token B: 0
  Alice balance token A: 1010
  Bob balance token C: 0
  Bob balance token B: 0
  Bob balance token A: 0
  Added liquidity: 0
  Removed liquidity: 0
  Bob's added liquidity: 0
  Bob's removed liquidity: 50
  

Alice burns 210 of token A --> removed liquidity underflows.

  Alice balance token A: 800
  Alice balance token C: 1010
  Added liquidity: 10
  Removed liquidity: 340282366920938463463374607431768211446
```

## Tools Used
Code inspection.

## Recommended Mitigation Steps
Block transfers of non-existent tokens (i.e. block transfers with zero amounts).


## Assessed type

Token-Transfer
Malicious User can dangerously accumulate fees not earned due to accounting and protocol nature 
Audit: 2023-11-panoptic-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-11-panoptic/blob/aa86461c9d6e60ef75ed5a1fe36a748b952c8666/contracts/SemiFungiblePositionManager.sol#L1169-L1191


# Vulnerability details

## Impact
The main issue of how a user can accumulate fees larger than normal, comes down to two different factors, 

- The Fact that users can burn and take up the same positions they just burnt again
- The Fact that after burning the positions, the account fees base mapping that tracks the total amount of fees the liquidity has earned in uniswap, is not reset to zero after burning/exiting

Both factors leads to a situation that when a user burns a position, retake that position again, get access to the already stored fees, earn fees again, burn the position, and repeat, accumulating previous positions earned fees every single new position

## Proof of Concept

After Providing the overview impact previously, lets explore the way and the components/code of the protocol, that will enable the malicious user to achieve this

First lets take a look at how positions key are being calculated in panoptic 

```solidity
 uint256 _tokenType = TokenId.tokenType(_tokenId, _leg);
        // unique key to identify the liquidity chunk in this uniswap pool
        bytes32 positionKey = keccak256(
            abi.encodePacked(
                address(_univ3pool),
                msg.sender,
                _tokenType,
                _liquidityChunk.tickLower(),
                _liquidityChunk.tickUpper()
            )
        );
// @audit Here we can see how the values that calculates position keys are calculated 
```
Now Looking closer, we can see that none of this values are actually difficult to recreate 

- `address(_univ3pool)` Just take a position in the same pool
- `msg.sender` the same user can take up the position again
- `_tokenType` token moved for a given leg 
- ` _liquidityChunk.tickLower() & _liquidityChunk.tickUpper()` just set the same limits

By retaking positions as same as this values every single time a user can actually, get the same positionKey computed every single time

Now if that was where it ended, that would not be an issue for the protocol, but it isnt the positionKey is used to store vital information for users, like the 
` s_accountFeesBase[positionKey]` to store the "Compute the feesGrowth * liquidity / 2**128 by reading feeGrowthInside0LastX128 and feeGrowthInside1LastX128 from univ3pool.positions." as shown below when 

```solidity
 s_accountFeesBase[positionKey] = _getFeesBase(
            _univ3pool,
            updatedLiquidity,
            _liquidityChunk
        );
```

The `_getFeesBase` return the total fees accumulated by the liquidity provided in a particular position as shown below 

```solidity
    function _getFeesBase(
        IUniswapV3Pool univ3pool,
        uint128 liquidity,
        uint256 liquidityChunk
    ) private view returns (int256 feesBase) {

        (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1Last
s_accountLiquidity[positionKey].leftSlot() (a.k.a. removedLiquidity) can underflow after certain partial transfers
Audit: 2023-11-panoptic-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-11-panoptic/blob/aa86461c9d6e60ef75ed5a1fe36a748b952c8666/contracts/SemiFungiblePositionManager.sol#L979
https://github.com/code-423n4/2023-11-panoptic/blob/aa86461c9d6e60ef75ed5a1fe36a748b952c8666/contracts/SemiFungiblePositionManager.sol#L586-L630
https://github.com/code-423n4/2023-11-panoptic/blob/aa86461c9d6e60ef75ed5a1fe36a748b952c8666/contracts/tokens/ERC1155Minimal.sol#L222-L229


# Vulnerability details

## Impact
By minting, burning and partial transferring positions that share position keys but not token ids, it's possible to reach states in which account's liquidities and minted positions are mismatched. 

For example, non-existent positions could have positive removedLiquidities and positions could have huge, corrupted removedLiquidities due to underflowing. In both cases, the calculation of the premium gets corrupted, which could freeze some positions if the calculation reverts or could assign absurd premiums and fees that might lead to economic losses depending on how other smart contracts (not part of this audit) in Panoptic use the SemiFungiblePositionManager contract.

## Proof of Concept
Here's a simple test example that puts the contract into the state mentioned above: https://gist.github.com/fnanni-0/e966047e79c87755575de88344661de9. Executing `forge test -vv --match-test test_Success_RemovedLiquidityManipulation` results in the following output which explains step by step what is happening.

```
Alice mints 1010 of token C (short put, i.e. adds liquidity to AMM).

  Minted positionSize: 1010
  Alice balance token C: 1010
  Added liquidity: 50
  Removed liquidity: 0
  

Alice mints 810 of token A (long put, i.e. removes liquidity from AMM). 
Token A and Token C have the same position key.

  Minted positionSize: 810
  Alice balance token A: 810
  Added liquidity: 10
  Removed liquidity: 40
  

Partial transfer 210 of token C to Bob. 
Alice is left with active positions (800 of token C and 810 of token A) but liquidity is now empty.

  Alice balance token C: 800
  Alice balance token A: 810
  Added liquidity: 0
  Removed liquidity: 0
  

Alice burns token A --> removed liquidity underflows.

  Alice balance token A: 0
  Alice balance token C: 800
  Added liquidity: 40
  Removed liquidity: 340282366920938463463374607431768211416
  

Bob burns token C --> removed liquidity is still non zero, but Bob doesn't have active positions.

  Bob balance token A: 0
  Bob balance token C: 0
  Bob's added liquidity: 0
  Bob's removed liquidity: 40
```

Alternatively, a similar scenario can be reached by reentering SemiFungiblePositionManager's transfer function. During the mint of short positions, ERC1155's onERC1155Received() can be used to safeTransferFrom() the tokens that are being minted before the liquidity is actually minted. If the token being minted has the same position key as previously minted short and long positions, safeTransferFrom() can be called with an amount that doesn't
OTM position is incorrectly considered partially ITM, this may lead to liquidity shortage
Audit: 2023-11-panoptic-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-11-panoptic/blob/main/contracts/SemiFungiblePositionManager.sol#L1036-L1046


# Vulnerability details

## Impact

The `_createLegInAMM()` [incorrectly evaluates](https://github.com/code-423n4/2023-11-panoptic/blob/main/contracts/SemiFungiblePositionManager.sol#L1036-L1046) an option during creation as is In-The-Money (ITM) when in reality it's Out-of-The-Money (OTM).

```
// Is this _leg ITM?
// if tokenType is 1, and we transacted some token0: then this leg is ITM!
if (_tokenType == 1) {
    // extract amount moved out of UniswapV3 pool
    _itmAmounts = _itmAmounts.toRightSlot(_moved.rightSlot());
}
// if tokenType is 0, and we transacted some token1: then this leg is ITM
if (_tokenType == 0) {
    // Add this in-the-money amount transacted.
    _itmAmounts = _itmAmounts.toLeftSlot(_moved.leftSlot());
}
```

Consider creating a Put option in `USDC<>ETH` pool for **USDC**; normally the strike price is below the current price. Initially, the newly created PUT option is OTM. As the price of **USDC** goes down and crosses the boundaries of the liquidity chunk for the position, it becomes partially ITM. And if the price falls even more, the position is fully ITM and could be exercised. See [the docs](https://panoptic.xyz/docs/panoptic-protocol/protocol-roles) for more details.

When the position for Put options in `USDC<>ETH` pool for **USDC** is OTM, it's fully in **ETH** tokens. Similarly, when the position is fully ITM, it's completely in **USDC** tokens. For partially ITM position, there is a mixture of **USDC** and **ETH** tokens.

 There could be a rare case when a position is created for partially ITM case, so the current price is inside position's boundaries. To correctly process such situation, the [`swapInAMM()` function](https://github.com/code-423n4/2023-11-panoptic/blob/main/contracts/SemiFungiblePositionManager.sol#L743) has been introduced, this one is called from `_validateAndForwardToAMM()` during position creation.

 However, the logic to determine if the position is ITM during creation is incorrect. As a result, OTM position for a Put option is always considered ITM and a swap happens. Consider creation of a Put option for **USDC** where initially it's OTM and fully in **ETH**, however an incorrect logic assumes it's ITM and mistakenly swaps **USDC** amount to **ETH**.

 Therefore, for any created Put option for **USDC** there will be a swap from **USDC** to **ETH** and the Panoptic protocol may face liquidity shortage in **USDC**.


## Proof of Concept
1. Create a Put option is `USDC<>ETH` Univ3 pool for **USDC**. Strike and width parameters are selected in a way that position boundaries are below the current **USDC** price, and the position is OTM.
2. Create the position. Notice that the position considered ITM and **USDC** amount (in amount very close to the position size) is swapped to **ETH**.

```
// Put this function inside SemiFungiblePositionManager.t.sol
function test_S
Users cannot get transferred a tokenized position they have already burnt and exited 
Audit: 2023-11-panoptic-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-11-panoptic/blob/aa86461c9d6e60ef75ed5a1fe36a748b952c8666/contracts/SemiFungiblePositionManager.sol#L613-L617


# Vulnerability details

## Impact
When  a user wants to exit a position, they burn their tokens and remove all liquidity provided from the 4 legs, ending their position, this means that they no longer have access to that position and everything, that connects them to that position is burnt along with it, but a lack of update of state after burning of these position, makes sure that users will never get pass the check even if they are not actively participating in that position they want to get transferred.

## Proof of Concept

 Lets try to explain this from when a user wants to exit/burn a tokenized position and track why this issue occurs 

When a user want to burn they call the `burnTokenizedPosition` function, and these calls all other functional functions, and they also perform their function 

```solidity
function burnTokenizedPosition(
        uint256 tokenId,
        uint128 positionSize,
        int24 slippageTickLimitLow,
        int24 slippageTickLimitHigh
    )
```
But if we go to end of the sequences of calls to burn a position, which is the `_createLegInAMM` we will see as shown below that after complete burning a position can still no be zero and still contain some data like removed liquidity and updated liquidity which are not zero value 

```solidity 
            s_accountLiquidity[positionKey] = uint256(0).toLeftSlot(removedLiquidity).toRightSlot(
                updatedLiquidity
            );
// @audit When you study the sequence of the code you will realize that some values might still remain in there which will make the `s_accountLiquidity[positionKey]` non zero
```
But the issue is not that, it is that when burning a position the protocol should make sure that it is cleared of any value that they might hold whatsoever.

Let Explore indepth more to complete the burning process 

```soldity
 _moved = isLong == 0
                ? _mintLiquidity(_liquidityChunk, _univ3pool)
                : _burnLiquidity(_liquidityChunk, _univ3pool); 
```
If we Go deeper to explore the `_burnLiquidity` method we will realize that the zeroing out of the values of a burnt position is not carried out as shown below 

```solidity
function _burnLiquidity(
        uint256 liquidityChunk,
        IUniswapV3Pool univ3pool
    ) internal returns (int256 movedAmounts) {
        (uint256 amount0, uint256 amount1) = univ3pool.burn(
            liquidityChunk.tickLower(),
            liquidityChunk.tickUpper(),
            liquidityChunk.liquidity()
        );
// @audit after burning the position, the protocol did not make sure to check if all positional values are zero or attempt to reset them, this will have a huge impact as we progress

        unchecked {
            movedAmounts = int256(0).toRightSlot(-int128(int256(amount0))).toLeftSlot(
                -int128(int256(amount1))