xanoutas/solidity-security-findings huggingface dataset:
`_validatePositionList()` does not check for duplicate tokenIds, allowing attackers to bypass solvency checks
Audit: 2024-04-panoptic-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/PanopticPool.sol#L1367-L1391
https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/PanopticPool.sol#L887-L893


# Vulnerability details


## Impact

The underlying issue is that `_validatePositionList()` does not check for duplicate tokenIds. Attackers can use this issue to bypass solvency checks, which leads to several impacts:

1. Users can mint/burn/liquidate/forceExercise when they are insolvent.
2. Users can settleLongPremium/forceExercise another user when the other user is insolvent.

This also conflicts a main invariant stated in contest readme: `Users should not be allowed to mint/burn options or pay premium if their end state is insolvent`.

## Bug Description

First, let's see why `_validatePositionList` does not check for duplicate tokenIds. For a user position hash, the first 8 bits is the length of tokenIds which overflows, last 248 bits is the xor hash. However, we can easily add 256 tokenIds of the same kind to create a duplicate positionHash.

For example: `Hash(key0, key1, key2) == Hash(key0, key1, key2, key0, key0, ..., 256 more key0)`. This way, we can add any tokenId we want while still arriving the same position hash.

PanopticPool.sol
```solidity
    function _validatePositionList(
        address account,
        TokenId[] calldata positionIdList,
        uint256 offset
    ) internal view {
        uint256 pLength;
        uint256 currentHash = s_positionsHash[account];

        unchecked {
            pLength = positionIdList.length - offset;
        }
        // note that if pLength == 0 even if a user has existing position(s) the below will fail b/c the fingerprints will mismatch
        // Check that position hash (the fingerprint of option positions) matches the one stored for the '_account'
        uint256 fingerprintIncomingList;

        for (uint256 i = 0; i < pLength; ) {
>           fingerprintIncomingList = PanopticMath.updatePositionsHash(
                fingerprintIncomingList,
                positionIdList[i],
                ADD
            );
            unchecked {
                ++i;
            }
        }

        // revert if fingerprint for provided '_positionIdList' does not match the one stored for the '_account'
        if (fingerprintIncomingList != currentHash) revert Errors.InputListFail();
    }
```

PanopticMath.sol
```solidity
    function updatePositionsHash(
        uint256 existingHash,
        TokenId tokenId,
        bool addFlag
    ) internal pure returns (uint256) {
        // add the XOR`ed hash of the single option position `tokenId` to the `existingHash`
        // @dev 0 ^ x = x

        unchecked {
            // update hash by taking the XOR of the new tokenId
            uint248 updatedHash = uint248(existingHash) ^
                (uint248(uint256(keccak256(abi.encode(tokenId)))));
            // increment the top 8 bit if addflag=true, decrement otherwise
            return
Hash collision in the `PanopticMath.updatePositionsHash` function could prompt wrong positions in the `positionIdList` to be validated
Audit: 2024-04-panoptic-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/libraries/PanopticMath.sol#L100-L109
https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/PanopticPool.sol#L1382-L1394


# Vulnerability details

## Impact

The `PanopticPool._validatePositionList` is a critical function in the `Panoptic protocol`. This function is used to ensure that the positions in the incoming user's list match the existing active option positions. 

The `_validatePositionList` function is used by multiple critical functions in the protocol such as `_mintOptions`, `_validateSolvency`, `liquidate`, `forceExercise` and `settleLongPremium`. These functions ensure the solvency of the protocol and hence can be considered as highly critical functions for the health and sustainability of the protocol.

The `PanopticPool._validatePositionList` function calls the `PanopticMath.updatePositionsHash` in its execution flow. The `PanopticMath.updatePositionsHash` function is used to update an existing account's "positions hash" with a new single position `tokenId`. This updated position hash is calculated as shown below:

```solidity
            uint248 updatedHash = uint248(existingHash) ^ 
                (uint248(uint256(keccak256(abi.encode(tokenId))))); 
```

But there is an issue with the above calculation since the `hashed` value of the `encoded tokenId`, by the `keccak256` is truncated to a uint248 (from uint256) before performing `XOR` with the `existingHash`. This truncation is performed to keep the `most significant 8 bytes` to store the `count of the positions`. 

Due to the above truncation a hash collision could occur where two different `tokenIds` with two different option positions could result in the same truncated `uint248` hash value. As a result a malicious user can provide a `manipulated tokenId inplace of a valid tokenId` in the `positionIdList`, as an input to the `PanopticPool._validatePositionList` function such that when truncated (hash value) to `uint248`, the resulting `updatedHash` would equal to the `existing active option positions` stored in the `s_positionsHash[account]` mapping (stored in the storage).

As it was explained earlier since the `_validatePositionList` is used by critical functions such as `_mintOptions`, `_validateSolvency`, `liquidate`, `forceExercise` and `settleLongPremium`, the above vulnerability could create many undesired behaviors in the protocol and could break the protocol.

For example during the `liquidate` function call a malicious liquidator can use the above vulnerability to provide malicious `positionIdListLiquidator` and `positionIdList` as input `TokenId[]` arrays to the `PanopticPool.liquidate` function. If the malcious tokenId or tokenIds result in the same truncated hash as of the valid tokenIds in the storage option position, the transaction will proceed without revert.

As a result the malicious liquidator can `liquidate` a position with different position parameters for a g
difference in calculation of values in `previewDeposit` and `previewMint` will return different vaules
Audit: 2024-04-panoptic-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/CollateralTracker.sol#L399-L408
https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/CollateralTracker.sol#L453-L468


# Vulnerability details

## Impact
The Protocol implements two different formula for calculation of `assets` and `share` values.

```solidity
    function previewMint(uint256 shares) public view returns (uint256 assets) {
        unchecked {
            assets = Math.mulDivRoundingUp(
                shares * DECIMALS,
                totalAssets(),
                totalSupply * (DECIMALS - COMMISSION_FEE)
            );
        }
    }

    function previewDeposit(uint256 assets) public view returns (uint256 shares) {
        // compute the MEV tax, which is equal to a single payment of the commissionRate on the FINAL (post mev-tax) assets paid
        unchecked {
            shares = Math.mulDiv(
                assets * (DECIMALS - COMMISSION_FEE),
                totalSupply,
                totalAssets() * DECIMALS
            );
        }
    }

```
## Proof of Concept
for simplicity i will ignore the decimal portion here suppose we have following values:
```javascript
DECIMALS = 10000;
COMMISSION_FEE = 10;
totalSupply = 12;
totalAssets = 20;

PreviewDeposit(assets = 864) => returns shares =  517;

PreviewMint(shares= 517) => returns assets = 863;

```

The user lose 1 assets due to difference in the formula
## Tools Used
Manual Review

## Recommended Mitigation Steps
Try to implement same formula for both cases.


## Assessed type

ERC4626
Depositors may lose entire assets deposited in `CollateralTracker` due to overflow in poolAssets
Audit: 2024-04-panoptic-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-04-panoptic/blob/833312ebd600665b577fbd9c03ffa0daf250ed24/contracts/CollateralTracker.sol#L436


# Vulnerability details

## Details
Users can make deposits of Collateral tokens into the pool through the `CollateralTracker.deposit` and receive shares for their deposits. These shares can later be redeemed for their initial deposits. When deposits are made, the `s_poolAssets` is incremented with the assets deposited while the totalSupply is incremented with the shares minted to the user.
```
 function deposit(uint256 assets, address receiver) external returns (uint256 shares) {
        if (assets > type(uint104).max) revert Errors.DepositTooLarge(); 
       

        shares = previewDeposit(assets);

        // transfer assets (underlying token funds) from the user/the LP to the PanopticPool
        // in return for the shares to be minted
        SafeTransferLib.safeTransferFrom(
            s_underlyingToken,
            msg.sender,
            address(s_panopticPool),
            assets
        );

        _mint(receiver, shares);

        // update tracked asset balance
       @> unchecked {
            s_poolAssets += uint128(assets);
        } 
        // 340282366920938463463374607431768211455
        //audit-note: 16 mil deposits (16777216) would need to be completed to make s_poolAssets overflow on this last deposit

        emit Deposit(msg.sender, receiver, assets, shares);
    }
```
The reverse happens when the shares are redeemed or withdrawn. s_poolAssets is incremented in an `unchecked` keyword allowing overflow. Since `s_poolAssets` is a uint128, and maxDeposit is uint104.max, it is highly possible to get s_poolAssets to uint128.max. Only 16 million max deposits are required to get this value. In a very active pool, getting to this value is a relatively small feat.
## Impact
It is expected that users should be able to always redeem their shares for the same amount of asset initially deposited no matter the conditions in the pool. What happens when `s_poolAssets` gets to uint128.max, and a user deposits enough to cause an overflow (This can be as little as 100 assets deposit), that user would never get their assets back and their assets would be lost forever. Infact, all other users of the pool  would also get seriously impacted as well.
This breaks core protocol functionality. However, I believe the impact is a medium due to the edge case that `s_poolAssets` would eventually get to max value especially during periods of high deposits and low redeem or withdrawal. But still, this is still very likely to happen.

## Proof of Concept
Insert the following into `CollateralTracker.t.sol`
I have mocked the s_poolAssets to be close to type(uint128).max and also totalSupply in `CollateralTracker` has been modified close to type(uint128).max to manually balance the accounting.
```
function testAuditAssetsOverflow() public {
        // initalize world state
        _initWorld(0);

        //
In the addCapped there is no mechanism to reset the unfreeze the accumulators, and it will silentghly return the outdated/wrong values.
Audit: 2024-04-panoptic-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-04-panoptic/blob/833312ebd600665b577fbd9c03ffa0daf250ed24/contracts/types/LeftRight.sol#L279-L302


# Vulnerability details

## Impact
addCapped plays important role in the updating the accumulated premia. The problem is, that there will be a point in time when the accumulators will overflow. Once upon a time, when this occur there will be no option to reset this number as well as it will continuously return the outdated data (last accumulator before the overflow), without notification

## Tools Used
Manual review

## Recommended Mitigation Steps
I highly encourage you to create the mechanism that could reset the overflowed amounts, as well as prevent from returning the outdated data


## Assessed type

Error
Lack of slippage protection allows to steal from users
Audit: 2024-04-panoptic-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-04-panoptic/blob/833312ebd600665b577fbd9c03ffa0daf250ed24/contracts/SemiFungiblePositionManager.sol#L1203-L1209
https://github.com/code-423n4/2024-04-panoptic/blob/833312ebd600665b577fbd9c03ffa0daf250ed24/contracts/SemiFungiblePositionManager.sol#L788-L850
https://github.com/code-423n4/2024-04-panoptic/blob/833312ebd600665b577fbd9c03ffa0daf250ed24/contracts/PanopticFactory.sol#L341-L410


# Vulnerability details

There are multiple places where Panoptic does not protect against slippage and uses spot price (e.g. reading price from `slot0`). We identified 3 most severe cases that impact users and allow for huge losses for the protocol users:

### Case 1

When position is created, either via PanopticPool or via SFPM, `UniswapPool.mint()` is called. It is a lo level function, that usually should be called via periphery contracts and verify if user liquidity was added with the amount expected. However in case of Panoptic there is no protection against that, because user passes only "liquidity" that will be minted and then UniswapPool calculated how much erc20 to get from user, but this amount is highly manipulable. The code in Panoptic:

```javascript
    function _mintLiquidity(
        LiquidityChunk liquidityChunk,
        IUniswapV3Pool univ3pool
    ) internal returns (LeftRightSigned movedAmounts) {
        // [...]
        (uint256 amount0, uint256 amount1) = univ3pool.mint(
            address(this),
            liquidityChunk.tickLower(),
            liquidityChunk.tickUpper(),
            liquidityChunk.liquidity(),
            mintdata
        );

//[...]
    function uniswapV3MintCallback(
        uint256 amount0Owed,
        uint256 amount1Owed,
        bytes calldata data
    ) external {
        // Decode the mint callback data
        CallbackLib.CallbackData memory decoded = abi.decode(data, (CallbackLib.CallbackData));
        // Validate caller to ensure we got called from the AMM pool
        CallbackLib.validateCallback(msg.sender, FACTORY, decoded.poolFeatures);
        // Sends the amount0Owed and amount1Owed quantities provided
        if (amount0Owed > 0)
            SafeTransferLib.safeTransferFrom(
                decoded.poolFeatures.token0,
                decoded.payer,
                msg.sender,
                amount0Owed
            );
        if (amount1Owed > 0)
            SafeTransferLib.safeTransferFrom(
                decoded.poolFeatures.token1,
                decoded.payer,
                msg.sender,
                amount1Owed
            );
    }
```

In case of Uniswap, mint funciton looks like this:
```javascript
    function mint(
        address recipient,
        int24 tickLower,
        int24 tickUpper,
        uint128 amount,
        bytes calldata data
    ) external override lock returns (uint256 amount0, uint256 amount1) {
        require(amount > 0);
        (, int256 amount0Int, int256 amount1Int) =
            _modifyPosition(
`CREATE2` address collision during pool deployment allows for complete draining of the pool
Audit: 2024-04-panoptic-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/PanopticFactory.sol#L237
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/0a25c1940ca220686588c4af3ec526f725fe2582/contracts/proxy/Clones.sol#L53


# Vulnerability details

(NOTE: This report is very highly inspired from [this past valid report.](https://github.com/sherlock-audit/2023-12-arcadia-judging/issues/59) Necessary changes have been made to suit the Panoptic Protocol.)

## Vulnerability Detailed

The attack consists of two parts: Finding a collision, and actually draining the lending pool. We describe both here:

#### PoC: Finding a collision

Note that in `PanopticFactory::deployNewPool`, `CREATE2` salt is user-supplied which is then passed to `Clones::cloneDeterministic`:

```javascript
    function deployNewPool(address token0, address token1, uint24 fee, bytes32 salt)
        external
        returns (PanopticPool newPoolContract)
    {
        // sort the tokens, if necessary:
        (token0, token1) = token0 < token1 ? (token0, token1) : (token1, token0);

        .
        .
        .

        // This creates a new Panoptic Pool (proxy to the PanopticPool implementation)
        // Users can specify a salt, the aim is to incentivize the mining of addresses with leading zeros
@>      newPoolContract = PanopticPool(POOL_REFERENCE.cloneDeterministic(salt));

        .
        .
        .
    }
```

```javascript
    function cloneDeterministic(address implementation, bytes32 salt) internal returns (address instance) {
        /// @solidity memory-safe-assembly
        assembly {
            // Cleans the upper 96 bits of the `implementation` word, then packs the first 3 bytes
            // of the `implementation` address with the bytecode before the address.
            mstore(0x00, or(shr(0xe8, shl(0x60, implementation)), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
            // Packs the remaining 17 bytes of `implementation` with the bytecode after the address.
            mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3))
@>          instance := create2(0, 0x09, 0x37, salt)
        }
        require(instance != address(0), "ERC1167: create2 failed");
    }
```

The address collision an attacker will need to find are:

- One undeployed Panoptic Pool address (1).
- Arbitrary attacker-controlled wallet contract (2).

Both sets of addresses can be brute-force searched because:

- As shown above, `salt` is a user-supplied parameter. By brute-forcing many `salt` values, we have obtained many different (undeployed) wallet accounts for (1). The user can know the address of the Panoptic Pool before deploying it, since as shown in the above code snippet, the result is deterministic.
- (2) can be searched the same way. The contract just has to be deployed using `CREATE2`, and the `salt` is in the attacker's control by definition.

An attacker can find any single address collision between (1) and (2) with high p
Incorrect validation during checking liquidity spread
Audit: 2024-04-panoptic-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-04-panoptic/blob/833312ebd600665b577fbd9c03ffa0daf250ed24/contracts/PanopticPool.sol#L1483


# Vulnerability details

## Impact
Because of incorrect validation, it allows option buyers not to pay premium.

## Proof of Concept
When long leg is minted or short leg is burnt, the protocol checks liquidity spread by calculating `TotalLiquidity / NetLiquidity` and allows it not exceed `9`.
However in the check function, the validation is ignored when `NetLiquidity` is zero.

This means when a user mints long leg that buys whole selling amount, the liquidity spread is not checked.
This issue allows the option buyer not to pay premium, and here is why:
1. When options are minted, last accumulated premium is stored to `s_grossPremiumLast`. Refer to `_updateSettlementPostMint` function of `PanopticPool` contract.
2. When options are burned, new accumulated premium is fetched and calculates the premium by multiplying liquidity with difference in accumulated premium. Refer to `_updateSettlementPostBurn` function of `PanopticPool` contract.
3. However, when a long leg of T(total liquidity) amount is minted, N becomes zero.
4. Later, when the minted long leg is burnt, premium values are not updated in SFPM, because N is zero. Refer to [SFPM:L1085](https://github.com/code-423n4/2024-04-panoptic/blob/833312ebd600665b577fbd9c03ffa0daf250ed24/contracts/SemiFungiblePositionManager.sol#L1085-L1094)

Since there is no difference in owed premium value, the option buyer will not pay the premium when burning the option.

## Tools Used
Manual Review

## Recommended Mitigation Steps
When checking liquidity spread, it should revert when N is zero and T is positive:
```Solidity
+   if(netLiquidity == 0 && totalLiquidity > 0) revert;
    if(netLiquidity == 0) return;
```


## Assessed type

Context
The Donated Full-range liquidity is minted to the `PanopticFactory` contract.
Audit: 2024-04-panoptic-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-04-panoptic/blob/833312ebd600665b577fbd9c03ffa0daf250ed24/contracts/PanopticFactory.sol#L335-L411


# Vulnerability details

After the creation of a new panoptic pool Full-range liquidity is minted using assets from the deployer to the address of the `PanopticFactory` this occurs in the function [mintFullRange()](https://github.com/code-423n4/2024-04-panoptic/blob/833312ebd600665b577fbd9c03ffa0daf250ed24/contracts/PanopticFactory.sol#L335-L411)
```solidity
    function _mintFullRange(
        IUniswapV3Pool v3Pool,
        address token0,
        address token1,
        uint24 fee
    ) internal returns (uint256, uint256) {
        (uint160 currentSqrtPriceX96, , , , , , ) = v3Pool.slot0();
//More Code...


        return
            IUniswapV3Pool(v3Pool).mint(
                address(this),
                tickLower,
                tickUpper,
                fullRangeLiquidity,
                mintCallback
            );
    }

```
The `Panopticfactory` contract lacks the functions and mechanisms to successfully manage the liquidity deposited into the AMM under its address.
example claiming earned fees from the liquidity.

## Impact
The donated liquidity may become wasted as no fees are being collected from it or it will be deployed in the wrong address.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Mint the liquidity under the panoptic pool address or create functions in the `PanopticFactory.sol` to manage these deposited assets.



## Assessed type

Error
Incorrect OTM check in required collateral calculation
Audit: 2024-04-panoptic-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-04-panoptic/blob/833312ebd600665b577fbd9c03ffa0daf250ed24/contracts/CollateralTracker.sol#L1346-L1347
https://github.com/code-423n4/2024-04-panoptic/blob/833312ebd600665b577fbd9c03ffa0daf250ed24/contracts/CollateralTracker.sol#L1374-L1375


# Vulnerability details

## Impact
Required collateral is calculated wrongly, which leads to incorrect amount of collateral required.

## Proof of Concept
In `_getRequiredCollateralSingleLegNoPartner` function of `CollateralTracker` contract, it calculates required collateral for an option leg based on the status of the leg whether it be OTM, ITM, or ATM.
And the check looks like follows:
```Solidity
if (
    ((atTick >= tickUpper) && (tokenType == 1)) || // strike OTM when price >= upperTick for tokenType=1
    ((atTick < tickLower) && (tokenType == 0)) // strike OTM when price < lowerTick for tokenType=0
) {
    // ...
}
```
It considers a position OTM when `price >= upperTick for tokenType=1`.
This assumption is wrong because based on the `asset` flag, the status becomes different.
When asset flag is zero, it means PUT option, and yes in this case the position is OTM.
When asset flag is one, it means CALL option, and in this case the position is ITM.

This applies same for other checks

## Tools Used
Manual Review

## Recommended Mitigation Steps
It should compare `tokenType` with `asset` flag to determine the status of a leg, like:
```Solidity
if (
    ((atTick >= tickUpper) && (tokenType != asset)) ||
    ((atTick < tickLower) && (tokenType == asset))
) {
    // ...
}
```


## Assessed type

Context