xanoutas/solidity-security-findings huggingface dataset:
Incorrect force-exercising fee calculation
Audit: 2024-04-panoptic-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
When force-exercising fee is calculated, the amount for loss in value in chunk is summed up so that it can be compensated for the option buyer, but by a mistake, the amount is not added up at last, thus it is ignored.

## Proof of Concept
In `CollateralTracker` contract on [L711-720](https://github.com/code-423n4/2024-04-panoptic/blob/833312ebd600665b577fbd9c03ffa0daf250ed24/contracts/CollateralTracker.sol#L711-L720), it calculates the amount of value in loss in the chunk:
```Solidity
exerciseFees = exerciseFees.sub(
    LeftRightSigned
        .wrap(0)
        .toRightSlot(
            int128(uint128(oracleValue0)) - int128(uint128(currentValue0))
        )
        .toLeftSlot(
            int128(uint128(oracleValue1)) - int128(uint128(currentValue1))
        )
);
```

However at the end of the function when it calculates final amount of exercise fee, the calculated value is ignored:
```Solidity
exerciseFees = exerciseFees
    .toRightSlot(int128((longAmounts.rightSlot() * fee) / DECIMALS_128))
    .toLeftSlot(int128((longAmounts.leftSlot() * fee) / DECIMALS_128));
```

As shown above, by using `toRightSlot` and `toLeftSlot`, original `exerciseFees` value is overwritten by `longAmounts * fee`.

## Tools Used
Manual Review

## Recommended Mitigation Steps
The pre-calculated amount has to be added to the final amount so that:
```Solidity
exerciseFees = exerciseFees.add(
    LeftRightSigned
        .wrap(0)
        .toRightSlot(int128((longAmounts.rightSlot() * fee) / DECIMALS_128))
        .toLeftSlot(int128((longAmounts.leftSlot() * fee) / DECIMALS_128));
)
```


## Assessed type

Context
Panoptic pool can be non-profitable by specific Uniswap governance
Audit: 2024-04-panoptic-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/CollateralTracker.sol#L247-L251
https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/CollateralTracker.sol#L261-L263


# Vulnerability details

## Impact
Swap commission is paid on the intrinsic value based on `s_ITMSpreadFee` in `CollateralTracker` contract.
If `s_ITMSpreadFee` is zero, then swap commission can not be paid. 

## Proof of Concept
```solidity
    function startToken(
        bool underlyingIsToken0,
        address token0,
        address token1,
        uint24 fee,
        PanopticPool panopticPool
    ) external {
        
        __SNIP__
        // cache the pool fee in basis points
        uint24 _poolFee;
        unchecked {
            _poolFee = fee / 100; // @audit below fee 0.01%, then _poolFee = 0  
        }
        s_poolFee = _poolFee;

        ...

        __SNIP__        
        // Additional risk premium charged on intrinsic value of ITM positions
        unchecked {
            s_ITMSpreadFee = uint128((ITM_SPREAD_MULTIPLIER * _poolFee) / DECIMALS);
        }
    }
```
As you can see above code snippet, If fee(Uniswap fee) is below 100, then _poolFee and s_ITMSpreadFee can be zero. 
Currently, there are no such pools that have below 0.01% fee on the UniswapV3.
But Uniswap fee level can be adjusted by the governance proposal like November 2021.
Here is the mention about it in Uniswap Protocol.
`
Uniswap v3 introduces multiple pools for each token pair, each with a different swapping fee. Liquidity providers may initially create pools at three fee levels: 0.05%, 0.30%, and 1%. More fee levels may be added by UNI governance, e.g. the 0.01% fee level added by this governance proposal in November 2021, as executed here.
`
https://dune.com/jcarnes/The-StableSwap-Wars
Competitions between Protocols like Uniswap and Carbon, more fee levels can be added in the future.

Indeed, there are several discussions on the less fee levels in stable coins pair.
https://gov.bancor.network/t/custom-taker-fee-on-stable-to-stable-trades/4370
- Carbon has a protocol wide fee of 20 BP (basis points).
- This fee, while appropriate for volatile pairs - is not in line with the market when it comes to stable to stable trades.
- For reference, Uniswap added a 1 BP fee option (0.01%) - in November 2021 (link)
- This proposal seeks to take this one step further and introduce a fee of 0.001% on stable to stable trades. This is 1/10th of a single basis point.

If protocol fee is less than 100 (i.e fee < 0.01 %), then PanopticPool's swap commission can not be taken.

## Tools Used
Manual review

## Recommended Mitigation Steps
Use Uniswap's DECIMALS (1e6) instead 10_000 and update all code related to DECIMALS.
  


## Assessed type

Uniswap
Incorrect token settlement in `CollateralTracker.sol`
Audit: 2024-04-panoptic-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

While minting/burning options, the `PanopticPool.sol` calls the `takeCommissionAddData()`/`exercise()` function in `CollateralTracker.sol`. `tokenToPay` is the premium to paid/collected when the position is closed. 
The function converts this amount into the corresponding shares that need to be minted/burned for the user. However, the current calculation does not account for the impact of the minting/burning on `totalSupply`. 
The calculation also does not account for the changes in `totalAssets()` due the swapped amount `swappedAmount`.
## Impact
If the user's shares were burnt, this would mean that the value of `totalSupply()` decreased, and therefore the user's remaining shares would be worth more.
Whereas, if they were minted, the value of `totalSupply()` would increase, and the value of the `sharesToMint` that was calculated to compensate the user for the equivalent amount of tokens would be lesser than it should be.
These inaccuracies would lead to the user being incorrectly compensated/charged while burning options.
## Proof of Concept
```solidity
Assume the totalSupply() is 100, totalAssets() is 100 and the user holds 25 shares.
The value of the user's shares: 25*100/100 tokens = 25 tokens

Case #1 : tokenToPay == 20

sharesToBurn = 20*100/100 = 20

20 shares are burnt and now the totalSupply() is 80

The value of the user's shares now in assets would be : (25-20)*100/80 = 6.25

The user effectively payed: 25-6.25 = 18.75 equivalent amount in tokens, which is 1.25 lesser than the intended value

Case #2 : tokenToPay == -20

sharesToMint = 20*100/100 = 20

20 shares are minted and now the totalSupply() is 120

The value of the user's shares now in assets would be : (25+20)*100/120 = 37.5

The user effectively received 37.5 - 20 = 17.5 tokens, which is 2.5 lesser than the intended value
```
Note: This PoC does not take into account the changes to `totalAssets()` during the exercise, however, the impact would be similar since it's not related to the change in `totalSupply()`. Additionally, it could be argued that `updatedAssets` should be used in the calculation as well as the post function call value of `totalAssets()` is directly dependent on this value.
## Tools Used
Manual Review
## Recommended Mitigation Steps
Account for the minted/burnt shares in the calculation of `sharesToBurn`/`sharesToMint` variables in `CollateralTracker.sol::exercise()` and `CollateralTracker.sol::takeCommissionAddData()`





## Assessed type

Error
Liquidation is not possible in turbulent market conditions
Audit: 2024-04-panoptic-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Accruing bad debt in case of turbulent market conditions

## Proof of Concept
`MAX_TWAP_DELTA_LIQUIDATION` is the constans used at `PanopticPool` contract for the purpose of preventing manipulated price liquidations. 
Here´s the NATSPEC of it;
```solidity
Contract: PanopticPool.sol

154:     // The maximum allowed delta between the currentTick and the Uniswap TWAP tick during a liquidation (~5% down, ~5.26% up)
155:     // Prevents manipulation of the currentTick to liquidate positions at a less favorable price
156:     int256 internal constant MAX_TWAP_DELTA_LIQUIDATION = 513; 
```
As such, if a position is liquidated, the validation is being exercised comparing to this value.


First the TWAP price is called;
```solidity
Contract: PanopticPool.sol

1026:         // Assert the account we are liquidating is actually insolvent
1027:         int24 twapTick = getUniV3TWAP();
```
Then, the current price is taken from the concerned UniV3 pool;
```solidity
Contract: PanopticPool.sol

1033:             (, int24 currentTick, , , , , ) = s_univ3pool.slot0();
```
Then it´s compared

```solidity
Contract: PanopticPool.sol

1035:             // Enforce maximum delta between TWAP and currentTick to prevent extreme price manipulation
1036:             if (Math.abs(currentTick - twapTick) > MAX_TWAP_DELTA_LIQUIDATION)
1037:                 revert Errors.StaleTWAP();
```
_0.01% of price change is correspongind to 1 tick, and 513 is accepted as 5%_

So, accordingly if the difference of current price and the 10 minutes TWAP price is greater than this value, it´s accepted as price is manipulated for more than 5%

But it can´t be the case historically, espcially in  volatile crypto markets.

UNISWAP's TWAP is a lagging oracle and in case of turbulent markets it has not been very accurate and lags behind other oracles´ price feeds. Below is a well known image showing how the oracle is being shaped during volatile market conditions.
![309644623-686c066c-9133-40e3-991e-8ffeb21ca7e3](https://gist.github.com/assets/65364747/d9a43647-50e5-4653-b5a7-75c80bc377f4)

    Green Dots: successful Compound v2 Price Oracle updates
    Blue Dots: Reporter - Chainlink SUSHI/USD Price Feed updates
    Red Dots: Anchor - Uniswap v3 TWAP
    Control: Grey Line - historic Coinbase/Binance ‘close’ price for SUSHI/USD

In this example, the Uniswap V3 TWAP oracle fell out of sync with spot market pricing during a sharp price drop around ~4:23 AM on June 10, 2023. After a sudden drop, the reporter (Chainlink Price Feed) was in the $0.50 - $0.59 range closely tracking the controls, but the TWAP anchor was above the $0.70 range.



## Tools Used
Manual Review

## Recommended Mitigation Steps
We recommend a fallback oracle in these conditions not relying only in the TWAP price


## Assessed type

Oracle
PanopticPool._validatePositionList can be tricked by overflowing position counter
Audit: 2024-04-panoptic-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-04-panoptic/blob/833312ebd600665b577fbd9c03ffa0daf250ed24/contracts/libraries/PanopticMath.sol#L100-L109
https://github.com/code-423n4/2024-04-panoptic/blob/833312ebd600665b577fbd9c03ffa0daf250ed24/contracts/PanopticPool.sol#L1367


# Vulnerability details

## Impact

A malicious actor can forge the currently held positions list and trick the system into believing that, for example, their overall position is solvent.

## Proof of Concept

The user's currently held positions list is not stored on-chain, instead, it is provided with each `mintOptions`/`burnOptions` call, hashed, and then compared to the stored hash (fingerprint) in `_validatePositionList`.

```
function _validatePositionList(
    address account,
    TokenId[] calldata positionIdList,
    uint256 offset
) internal view {
    uint256 pLength;
    uint256 currentHash = s_positionsHash[account];
    unchecked {
        pLength = positionIdList.length - offset;
    }
    uint256 fingerprintIncomingList;
    for (uint256 i = 0; i < pLength; ) {
        fingerprintIncomingList = PanopticMath.updatePositionsHash(
            fingerprintIncomingList,
            positionIdList[i],
            ADD
        );
        unchecked {
            ++i;
        }
    }
    if (fingerprintIncomingList != currentHash) revert Errors.InputListFail();
}
```

`updatePositionsHash` XORes keccak of each token in right 248 bits and stores positions count in left 8 bit.

```
function updatePositionsHash(
    uint256 existingHash,
    TokenId tokenId,
    bool addFlag
) internal pure returns (uint256) {
    unchecked {
        uint248 updatedHash = uint248(existingHash) ^ (uint248(uint256(keccak256(abi.encode(tokenId)))));
        return
            addFlag
                ? uint256(updatedHash) + (((existingHash >> 248) + 1) << 248)
                : uint256(updatedHash) + (((existingHash >> 248) - 1) << 248);
    }
}
```

This validation can be bypassed when addFlag=true by overflowing the counter and hiding tokens by doubling (x ^ x = 0, x ^ 0 = x). For example, assuming counter has only 2 bits: 
`[x, x, x, x, a] => x ^ x ^ x ^ x ^ a | 1 + 1 + 1 + 1 + 1 => a | 1`.

For coded PoC make the following changes in, for example, `test_Success_mintOptions_OTMShortCall`:
```
- TokenId[] memory posIdList = new TokenId[](1);
- posIdList[0] = tokenId;
+ TokenId[] memory posIdList = new TokenId[](257);
+ for (uint i; i < 257; i++) posIdList[i] = tokenId;
```

Validating position list will pass meaning we successfully "hide" 256 tokens from the validator, but they still counted in `_validateSolvency` and other functions.

## Tools Used

Manual review

## Recommended Mitigation Steps

Enforce `MAX_POSITIONS` limit in `_validatePositionList` by adding require: `require(positionIdList.length <= MAX_POSITIONS)`




## Assessed type

Invalid Validation
_validatePositionList() positionIdList can forgery
Audit: 2024-04-panoptic-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Vulnerability details

`_validatePositionList(positionIdList)` is called from multiple places and is primarily used to validate the legality of `positionIdList`. 

The main logic involves iterating through `tokenId`s and performing XOR operations, then comparing the result with `s_positionsHash[account]`.

```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();
    }
```
```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
                addFlag
@>                  ? uint256(updatedHash) + (((existingHash >> 248) + 1) << 248)
                    : uint256(updatedHash) + (((existingHash >> 248) - 1) << 248);
        }
    }
```

The main issue in the code is with `(((existingHash >> 248) + 1) << 248)`. The first `uint8` represents the number of `tokenId`s, and overflow is ignored.

Additionally,when `XOR` calculation, the known formula is `X^Y^Y=X`.

This means that calculating the hash for `positionIdList[1,2,3]` and `positionIdList[1,2,3,4,4,4,4......]` (256 `4` ) will result in the same hash.

## Proof of Concept

The following code demonstrates that inserting any `256` `tokenId`s will yield the same hash.
```solidity
contract CounterTest is Test {

    function updatePositionsHash(
      uint256 existingHash,
_updateSettlementPostBurn() may not correctly reduce s_grossPremiumLast[chunkKey]
Audit: 2024-04-panoptic-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Vulnerability details
`s_grossPremiumLast[]` definitions are as follows:
>    /// @dev Per-chunk `last` value that gives the aggregate amount of premium owed to all sellers when multiplied by the total amount of liquidity `totalLiquidity`
>    /// totalGrossPremium = totalLiquidity * (grossPremium(perLiquidityX64) - lastGrossPremium(perLiquidityX64)) / 2**64
>    /// Used to compute the denominator for the fraction of premium available to sellers to collect
>    /// LeftRight - right slot is token0, left slot is token1
>     mapping(bytes32 chunkKey => LeftRightUnsigned lastGrossPremium) internal s_grossPremiumLast;


a critical rule: if there is a change in `totalLiquidity`, we must recalculate this value. 

when `Pool.mintOptions()`, `s_grossPremiumLast[chunkKey]` will increase.
step: `mintOptions()`->`_mintInSFPMAndUpdateCollateral()`->`_updateSettlementPostMint()`
```solidity
    function _updateSettlementPostMint(
        TokenId tokenId,
        LeftRightUnsigned[4] memory collectedByLeg,
        uint128 positionSize
    ) internal {
...
            if (tokenId.isLong(leg) == 0) { 
                LiquidityChunk liquidityChunk = PanopticMath.getLiquidityChunk(
                    tokenId,
                    leg,
                    positionSize
                );
...
                uint256[2] memory grossCurrent;
                (grossCurrent[0], grossCurrent[1]) = SFPM.getAccountPremium(
                    address(s_univ3pool),
                    address(this),
                    tokenId.tokenType(leg),
                    liquidityChunk.tickLower(),
                    liquidityChunk.tickUpper(),
                    type(int24).max,
                    0
                );

                unchecked {
                    // L
                    LeftRightUnsigned grossPremiumLast = s_grossPremiumLast[chunkKey];
                    // R
                    uint256 positionLiquidity = liquidityChunk.liquidity();
                    // T (totalLiquidity is (T + R) after minting)
                    uint256 totalLiquidityBefore = totalLiquidity - positionLiquidity;

@>                  s_grossPremiumLast[chunkKey] = LeftRightUnsigned
                        .wrap(0)
                        .toRightSlot(
                            uint128(
                                (grossCurrent[0] *
                                    positionLiquidity +
                                    grossPremiumLast.rightSlot() *
                                    totalLiquidityBefore) / (totalLiquidity)
                            )
                        )
                        .toLeftSlot(
                            uint128(
                                (grossCurrent[1] *
                                    positionLiquidity +
                                    grossPremiumLa
When Burning a Tokenized Position `validate` should be done before flipping the `isLong` bits in `_validateAndForwardToAMM()`
Audit: 2024-04-panoptic-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-04-panoptic/blob/833312ebd600665b577fbd9c03ffa0daf250ed24/contracts/types/TokenId.sol#L535-L571
https://github.com/code-423n4/2024-04-panoptic/blob/833312ebd600665b577fbd9c03ffa0daf250ed24/contracts/SemiFungiblePositionManager.sol#L673-L702


# Vulnerability details

Each leg in a `tokenid` has a risk partner which is usually its own index but in some cases, it could be another leg (Partner in defined risk position).
In the function `validate()` if the risk partner of a specific leg is `not` its own index then some additional checks are done to ensure that they are compatible like
* Ensuring that risk partners are mutual
* Ensures that risk partners have the same asset.
* Ensures that risk partners have the same ratio.
* Plus other checks that depend on the legs `isLong` value compared to that of its risk partner.
```solidity
    function validate(TokenId self) internal pure {
        if (self.optionRatio(0) == 0) revert Errors.InvalidTokenIdParameter(1);

// More Code...
                // In the following, we check whether the risk partner of this leg is itself
                // or another leg in this position.
                // Handles case where riskPartner(i) != i ==> leg i has a risk partner that is another leg
                uint256 riskPartnerIndex = self.riskPartner(i);
                if (riskPartnerIndex != i) {
                    // Ensures that risk partners are mutual
                    if (self.riskPartner(riskPartnerIndex) != i)
                        revert Errors.InvalidTokenIdParameter(3);


                    // Ensures that risk partners have 1) the same asset, and 2) the same ratio
                    if (
                        (self.asset(riskPartnerIndex) != self.asset(i)) ||
                        (self.optionRatio(riskPartnerIndex) != self.optionRatio(i))
                    ) revert Errors.InvalidTokenIdParameter(3);


                    // long/short status of associated legs
                    uint256 _isLong = self.isLong(i);
                    uint256 isLongP = self.isLong(riskPartnerIndex);


                    // token type status of associated legs (call/put)
                    uint256 _tokenType = self.tokenType(i);
                    uint256 tokenTypeP = self.tokenType(riskPartnerIndex);


                    // if the position is the same i.e both long calls, short put's etc.
                    // then this is a regular position, not a defined risk position
                    if ((_isLong == isLongP) && (_tokenType == tokenTypeP))
                        revert Errors.InvalidTokenIdParameter(4);


                    // if the two token long-types and the tokenTypes are both different (one is a short call, the other a long put, e.g.), this is a synthetic position
                    // A synthetic long or short is more capital efficient than each leg separated because the long+short premia accumulate proportionally
                    // unl