xanoutas/solidity-security-findings huggingface dataset:
Incorrect Validation for tickLimitLow and tickLimitHigh Causing Potential Slippage Check Failures
Audit: 2024-06-panoptic-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-06-panoptic/blob/153f0d82440b7e63075d55b0659706531431145f/contracts/SemiFungiblePositionManager.sol#L679


# Vulnerability details

## Impact
In _validateAndForwardToAMM function, the validation logic checks if tickLimitLow is greater than tickLimitHigh after the _createPositionInAMM function call. If this condition is true, it swaps the values and performs the ITM swap if necessary. However, the slippage check occurs after these values are potentially swapped, which can result in incorrect validation if the original tickLimitLow was indeed greater than tickLimitHigh.

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

### LOC

```solidity
function _validateAndForwardToAMM(
    TokenId tokenId,
    uint128 positionSize,
    int24 tickLimitLow,
    int24 tickLimitHigh,
    bool isBurn
) internal returns (LeftRightUnsigned[4] memory collectedByLeg, LeftRightSigned totalMoved) {
    // Reverts if positionSize is 0 and user did not own the position before minting/burning
    if (positionSize == 0) revert Errors.OptionsBalanceZero();

    // Validate tokenId
    tokenId.validate();

    /// @dev the flipToBurnToken() function flips the isLong bits
    if (isBurn) {
        tokenId = tokenId.flipToBurnToken();
    }

    // Extract univ3pool from the poolId map to Uniswap Pool
    IUniswapV3Pool univ3pool = s_poolContext[tokenId.poolId()].pool;

    // Revert if the pool not been previously initialized
    if (univ3pool == IUniswapV3Pool(address(0))) revert Errors.UniswapPoolNotInitialized();

    // initialize some variables returned by the _createPositionInAMM function
    LeftRightSigned itmAmounts;

    // calls a function that loops through each leg of tokenId and mints/burns liquidity in Uni v3 pool
    (totalMoved, collectedByLeg, itmAmounts) = _createPositionInAMM(
        univ3pool,
        tokenId,
        positionSize,
        isBurn
    );

    if (tickLimitLow > tickLimitHigh) {
        // if the in-the-money amount is not zero (i.e. positions were minted ITM) and the user did provide tick limits LOW > HIGH, then swap necessary amounts
        if ((LeftRightSigned.unwrap(itmAmounts) != 0)) {
            totalMoved = swapInAMM(univ3pool, itmAmounts).add(totalMoved);
        }

        (tickLimitLow, tickLimitHigh) = (tickLimitHigh, tickLimitLow);
    }

    // Get the current tick of the Uniswap pool, check slippage
    (, int24 currentTick, , , , , ) = univ3pool.slot0();

    if ((currentTick >= tickLimitHigh) || (currentTick <= tickLimitLow))
        revert Errors.PriceBoundFail();
}
```
## Tools Used
Manual code review.

## Recommended Mitigation Steps
Validate the `tickLimitLow` and `tickLimitHigh` values before calling `_createPositionInAMM`. If `tickLimitLow` is greater than `tickLimitHigh`, revert the transaction immediately to prevent incorrect slippage checks.


```solidity
fu
JSON injection and xss through ERC20 symbol when generating `tokenUri` 
Audit: 2024-06-panoptic-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-06-panoptic/blob/153f0d82440b7e63075d55b0659706531431145f/contracts/base/FactoryNFT.sol#L46-L47
 https://github.com/code-423n4/2024-06-panoptic/blob/153f0d82440b7e63075d55b0659706531431145f/contracts/base/FactoryNFT.sol#L69-L112


# Vulnerability details

## Impact

The `tokenURI` function uses `safeERC20Symbol` function in panoptic math to get token symbols but doesn't sanitize it. This causes that any special charcacters can be introduced into the factory NFTs token uri leading to a host of JSON injection attack vectors. The use of SVG is well known to be often vulnerable to Cross-Site Scripting (XSS). If a malicious user can inject malicious JavaScript into an SVG file, any user viewing the SVG on a website may become susceptible to XSS attacks. Considering that anyone can create an ERC20 token, a uniswap pool and a panoptic pool or use the token symbol includes special characters, this can be weaponized by attackers to execute malicious codes on the frontend for instance, running a keylogger script to collect all inputs typed by a user including his password or to create a fake Metamask pop up asking a user to sign a malicious transaction to steal his funds in users. Even while the front end processes securely, such as using the standard builtin JSON.parse() to read URI. This can also be exploited by replacing factory nft 's svg with arbitrary other ones such as creating NFTs containing same art piece data with existing high price NFTs, or other legally risky svgs like gore or pornography images. More about this can be read [here](https://www.comparitech.com/net-admin/json-injection-guide/), [here](https://rietta.com/blog/svg-xss-injection-attacks/) and [here](https://www.petercollingridge.co.uk/tutorials/svg/interactive/javascript/)

## Proof of Concept

When constructing metadata for the `tokenUri`, the metadata is constructed first getting the uniswap pools' token symbols.  It does this using the `safeERC20Symbol` function.

```solidity
    function tokenURI(uint256 tokenId) public view override returns (string memory) {
        address panopticPool = address(uint160(tokenId));

        return
            constructMetadata(
                panopticPool,
                PanopticMath.safeERC20Symbol(PanopticPool(panopticPool).univ3pool().token0()),
                PanopticMath.safeERC20Symbol(PanopticPool(panopticPool).univ3pool().token1()),
                PanopticPool(panopticPool).univ3pool().fee()
            );
    }
```
The `safeERC20Symbol` function simply queries the symbol without actually sanitizing the returned symbols. No check for symbol length, and no check for special characters.
```solidity
    function safeERC20Symbol(address token) external view returns (string memory) {
        // not guaranteed that token supports metadata extension
        // so we need to let call fail and return placeholder if not
        try IERC20Metadata(token).symbol() returns (string memory symbo
Upgraded Q -> 3 from #179 [1704126766195]
Audit: 2023-11-panoptic-findings
Severity: HIGH

Judge has assessed an item in Issue #179 as 3 risk. The relevant finding follows:

 There’s also a reentrancy in uniswapV3MintCallback() with ERC777 tokens (the project supports them). When Alice calls mintTokenizedPosition() the following will happen:
She’ll receive SFPM tokens from _mint();
_createLegInAMM() calculates and updates the liquidity held by Alice;
_mintLiquidity() calls UniswapV3Pool.mint();
Uniswap calls back to uniswapV3MintCallback();
The callback calls the token to transfer funds from Alice to Uniswap;
And if the token is ERC777, it’ll call Alice before the transfer. At this point, Alice has both SFPM tokens and liquidity to use in external systems before actually paying any money. Even if these external systems rely on non-standard liquidity counters inside SFPM (s_accountLiquidity mapping) instead of the standard ERC1155 to determine the liquidity held by Alice, they will still be vulnerable to this reentrancy. Solution here is to update the liquidity after the payment.
Users 'removedLiquidity' is not updated after they remove liquidity 
Audit: 2023-11-panoptic-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-11-panoptic/blob/aa86461c9d6e60ef75ed5a1fe36a748b952c8666/contracts/SemiFungiblePositionManager.sol#L992-L993
https://github.com/code-423n4/2023-11-panoptic/blob/aa86461c9d6e60ef75ed5a1fe36a748b952c8666/contracts/SemiFungiblePositionManager.sol#L964-L968


# Vulnerability details

## Impact
removedLiquidity is not updated after user increases their long position.

## Proof of Concept
We all know that long position involves removing liquidity from the pool, but when a user wants to increase a long position, the amount he/she is removing is not updated in the removedLiquidity variable which can lead to wrong repayment when the user wants to close the long position. 
Let's go with this scenario, where a user opens a long position

 /// @dev If the isLong flag is 1=long and the position is minted, then this is opening a long position
                /// @dev so the amount of short liquidity should increase.
                if (!_isBurn) {
                    removedLiquidity += chunkLiquidity;
                }

The chunkLiquidity which is the amount to be moved is added to the removedLiquidity then it is updated here 

   // update the starting liquidity for this position for next time around
            s_accountLiquidity[positionKey] = uint256(0).toLeftSlot(removedLiquidity).toRightSlot(
                updatedLiquidity
            );

Then the user comes back again to increase his long position then this is called 

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

which then extracts the removed liquidity and the logic to increase the long position implements like this 

else {
                // the _leg is long (buying: moving *from* uniswap to msg.sender)
                // so we seek to move the incoming liquidity chunk *out* of uniswap - but was there sufficient liquidity sitting in uniswap
                // in the first place?
                if (startingLiquidity < chunkLiquidity) {
                    // the amount we want to move (liquidityChunk.legLiquidity()) out of uniswap is greater than
                    // what the account that owns the liquidity in uniswap has (startingLiquidity)
                    // we must ensure that an account can only move its own liquidity out of uniswap
                    // so we revert in this case
                    revert Errors.NotEnoughLiquidity();
                } else {
                    // we want to move less than what already sits in uniswap, no problem:
                    updatedLiquidity = startingLiquidity - chunkLiquidity;
                }

after that it then updates the variables again with the original removedLiquidity gotten from just when the user opened the long position.
It can happen as long as the startingLiquidity is greater than the chunkliquidity (liquidity going out in that tx)

  // update the starting liquidity for this
Re: Revert Usage
Audit: 2023-11-panoptic-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-11-panoptic/blob/main/contracts/types/TokenId.sol#L463


# Vulnerability details

## Impact
The usage of multiple revert statements could consume more gas than required and lead to higher transaction costs.

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

## Tools Used

## Recommended Mitigation Steps

Review the error handling logic and consolidate checks wherever possible.
Combine related checks to reduce the number of revert statements used in the function.

function validate(uint256 self) internal pure returns (uint64) {
    uint256 errorFlag;

    if (self.optionRatio(0) == 0) {
        errorFlag = 1;
    } else {
        unchecked {
            for (uint256 i = 0; i < 4; ++i) {
                if (self.optionRatio(i) == 0) {
                    if ((self >> (64 + 48 * i)) != 0) {
                        errorFlag = 1;
                        break;
                    }

                    break;
                }

                // Validation logic for legs...

                if (/* Validation conditions */) {
                    errorFlag = 1;
                    break;
                }
            }
        }
    }

    if (errorFlag == 1) {
        revert Errors.InvalidTokenIdParameter(1);
    }

    return self.univ3pool();
}



## Assessed type

Error
Use of `slot0` to get `sqrtPriceLimitX96` can lead to price manipulation.
Audit: 2023-11-panoptic-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact:

In the `SemiFungiblePositionManager.sol` contract, the `swapInAMM` function relies on the `UniswapV3.slot0` to obtain the value of `sqrtPriceX96`. This value is then used to calculate `net0`, which subsequently affects the swap which is done in`_univ3pool.swap` function.

This manipulation can result in significant financial losses when interacting with the `_univ3pool.swap` function.



## Proof of Concept:
The issue lies in the fact that `sqrtPriceX96` is retrieved from `Uniswap.slot0`, which represents the most recent data point and can be easily manipulated by malicious actors, particularly through MEV (Miner Extractable Value) bots and Flashloans in sandwich attacks. 

The smart contract `SemiFungiblePositionManager.sol` fetches `sqrtPriceX96` from `slot0`:

```solidity
(uint160 sqrtPriceX96, , , , , , ) = _univ3pool.slot0();
```

This `sqrtPriceX96` value is then used to compute different variables like `net0`, `swapAmount` which is finally being used in `_univ3pool.swap` function.

`swapAmount` is used to swap tokens in the Uniswap pool:

```solidity
(int256 swap0, int256 swap1) = _univ3pool.swap(
    msg.sender,
    zeroForOne,
    swapAmount,
    zeroForOne
        ? Constants.MIN_V3POOL_SQRT_RATIO + 1
        : Constants.MAX_V3POOL_SQRT_RATIO - 1,
    data
);
```

According to Uniswap's documentation, `sqrtPriceX96` represents the current price of the pool as a `sqrt(token1/token0) Q64.96` value.


## Tools Used:

Manual Review

## Recommended Mitigation Steps:

To mitigate this vulnerability, it is advisable to use the `TWAP` (Time-Weighted Average Price) function to obtain the value of `sqrtPriceX96`. This would provide a more stable and less susceptible source of pricing data, reducing the risk of manipulation by malicious actors.


## Assessed type

Uniswap
Freezing of position fees due to impossibility to burn it if positionSize is zero
Audit: 2023-11-panoptic-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
It is possible to create a position inside SemiFungiblePositionManager (SFPM) that it is impossible to burn. This means that it is possible to create position with no ways to remove it and get back accumulated fees.
According to [documentation](https://panoptic.xyz/docs/developers/semifungiblepositionmanager#burntokenizedposition) "burnTokenizedPosition [...] Burns a new position containing up to 4 legs wrapped in a ERC1155 token. Auto-collect all accumulated fees."
When positionSize of a position become 0, there are no way to call burnTokenizedPosition on it.

## Proof of Concept
The issue is inside [`burnTokenizedPosition()` function](https://github.com/code-423n4/2023-11-panoptic/blob/aa86461c9d6e60ef75ed5a1fe36a748b952c8666/contracts/SemiFungiblePositionManager.sol#L476-L500):

```
function burnTokenizedPosition(
        uint256 tokenId,
        uint128 positionSize,
        int24 slippageTickLimitLow,
        int24 slippageTickLimitHigh
    )
        external
        ReentrancyLock(tokenId.univ3pool())
        returns (int256 totalCollected, int256 totalSwapped, int24 newTick)
    {
        // burn this ERC1155 token id
        _burn(msg.sender, tokenId, positionSize);

        // emit event
        emit TokenizedPositionBurnt(msg.sender, tokenId, positionSize);

        // Call a function that contains other functions to mint/burn position, collect amounts, swap if necessary
        (totalCollected, totalSwapped, newTick) = _validateAndForwardToAMM(
            tokenId,
            positionSize,
            slippageTickLimitLow,
            slippageTickLimitHigh,
            BURN
        );
    }
```

This function successfully calls `_burn()` function, but then calls `_validateAndForwardToAMM()` function:

```
function _validateAndForwardToAMM(
        uint256 tokenId,
        uint128 positionSize,
        int24 tickLimitLow,
        int24 tickLimitHigh,
        bool isBurn
    ) internal returns (int256 totalCollectedFromAMM, int256 totalMoved, int24 newTick) {
        // Reverts if positionSize is 0 and user did not own the position before minting/burning
        if (positionSize == 0) revert Errors.OptionsBalanceZero();

[...]
```

That reverts thanks to `positionSize`

We wrote test:

```diff
    function test_Success_mintTokenizedPosition(
        uint256 x,
        uint256 widthSeed,
        int256 strikeSeed,
        uint256 positionSizeSeed
    ) public {
        _initPool(x);

        (int24 width, int24 strike) = PositionUtils.getOutOfRangeSW(
            widthSeed,
            strikeSeed,
            uint24(tickSpacing),
            currentTick
        );

        populatePositionData(width, strike, positionSizeSeed);

        /// position size is denominated in the opposite of asset, so we do it in the token that is not WETH
Manipulation of premia through 0 transfer
Audit: 2023-11-panoptic-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

Its possible to transfer liquidity that only has the left slot occupied to manipulate the premia calculation of a target that does not have liquidity for that range yet, without any additional cost.

## Proof of Concept
Providing liquidity and removing it through minting a long position allows any user to create a LeftRight-liquidity of x-0 (x being amount in left slot and 0 in right slot). Doing a zero-transfer of any of the tokens minted will assign this liquidity to any target address that does not yet have a position in that tickRange:

```js
//SFPM
function registerTokenTransfer(address from, address to, uint256 id, uint256 amount) internal {
    ...
// Revert if recipient already has that position
if (
    (s_accountLiquidity[positionKey_to] != 0) ||
    (s_accountFeesBase[positionKey_to] != 0)
) revert Errors.TransferFailed();

// Revert if not all balance is transferred
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;
unchecked {
    ++leg;
}
```

Since the left slot, which represents the removed liquidity is important for the calculation of premia deltas, this can impact any upstream contracts that use them (e.g. the PanopticPool). This could for example take the form of frontrunning the initial mint on a tickRange of that pool to bypass the `s_accountLiquidity[positionKey_to] != 0` check. That position would then start out with an amount x of supposedly removed liquidity.

## Tools Used

Manual Review

## Recommended Mitigation Steps

disallow transfers of positions with x-y liquidity where x != 0


## Assessed type

Other
Transfer invariant can be broken
Audit: 2023-11-panoptic-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

The protocol has an invariant that if someone wants to transfer SFPM-tokens of a certain ID to someone else, they have to send all tokens of that ID that they possess. The checks for this invariant are insufficient.

## Proof of Concept

The `registerTokenTransfer` function performs the following check:

```js
/// @notice update user position data following a token transfer
/// @dev token transfers are only allowed if you transfer your entire balance of a given tokenId and the recipient has none
function registerTokenTransfer(address from, address to, uint256 id, uint256 amount) internal {
    ...
    for (uint256 leg = 0; leg < numLegs; ) {
        // for this leg index: extract the liquidity chunk: a 256bit word containing the liquidity amount and upper/lower tick
        // @dev see `contracts/types/LiquidityChunk.sol`
        uint256 liquidityChunk = PanopticMath.getLiquidityChunk(
            id,
            leg,
            uint128(amount),
            univ3pool.tickSpacing()
        );

        //construct the positionKey for the from and to addresses
        bytes32 positionKey_from = keccak256(
            abi.encodePacked(
                address(univ3pool),
                from,
                id.tokenType(leg),
                liquidityChunk.tickLower(),
                liquidityChunk.tickUpper()
            )
        );

        ...
        // Revert if not all balance is transferred
        uint256 fromLiq = s_accountLiquidity[positionKey_from];
        if (fromLiq.rightSlot() != liquidityChunk.liquidity()) revert Errors.TransferFailed();
```

The check to validate that all balance is transferred is insufficient. An example:
1) user adds positionSize=200 (optionRatio=1) for tickRange a-b. `s_accountLiq[from]` will be 0-200 (0 left, 200 right). He gets minted 200 tokens (say tokenId 1 for simplicity)
2) user removes 100 positionSize by minting a long for same tickRange a-b. `s_accountLiq[from]` will be 100-100. He gets minted 100 tokens with (with tokenId 2)
3) user can now transfer 100 tokens of tokenId 1 as the validation `fromLiq.rightSlot() == liquidityChunk.liquidity()` passes, even though he owns 200 tokens with tokenId 1

## Tools Used

Manual review

## Recommended Mitigation Steps

One approach would be to disallow sending tokens where any of its legs has removed liquidity (where `leftSlot() != 0`). Though this may be more restrictive than the intended design.


## Assessed type

Other
Re: Unchecked Conversion
Audit: 2023-11-panoptic-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-11-panoptic/blob/main/contracts/libraries/Math.sol#L172


# Vulnerability details

## Impact
The function performs an unchecked conversion from uint256 to uint128, potentially truncating high-order bits and causing unexpected behavior or data loss.

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

## Tools Used

## Recommended Mitigation Steps

Implement checks to ensure the conversion doesn't result in data loss or unexpected behavior. Consider handling out-of-range or oversized values gracefully.

function toUint128(uint256 toDowncast) internal pure returns (uint128 downcastedInt) {
    require(toDowncast <= type(uint128).max, "Value exceeds uint128 range");
    downcastedInt = uint128(toDowncast);
}



## Assessed type

Math