Panoptic - Sathish9098's results

Effortless options trading on any token, any strike, any size.

General Information

Platform: Code4rena

Start Date: 27/11/2023

Pot Size: $60,500 USDC

Total HM: 7

Participants: 72

Period: 7 days

Judge: Picodes

Total Solo HM: 2

Id: 309

League: ETH

Panoptic

Findings Distribution

Researcher Performance

Rank: 18/72

Findings: 3

Award: $860.58

🌟 Selected for report: 1

🚀 Solo Findings: 0

Awards

11.3163 USDC - $11.32

Labels

bug
grade-b
QA (Quality Assurance)
edited-by-warden
Q-35

External Links

[L-1] Collision risks in pool ID generation in getFinalPoolId() Function

Impact

The probability of a collision depends on the number of pools and the variety of token0, token1, and fee combinations. The more pools and varied combinations, the higher the chance of a collision.

If a collision occurs, two different sets of parameters would lead to the same finalPoolId, potentially causing confusion or errors in pool identification and management.

FILE: 2023-11-panoptic/contracts/libraries/PanopticMath.sol

function getFinalPoolId(
        uint64 basePoolId,
        address token0,
        address token1,
        uint24 fee
    ) internal pure returns (uint64) {
        unchecked {
            return
                basePoolId +
                (uint64(uint256(keccak256(abi.encodePacked(token0, token1, fee)))) >> 32);
        }
    }

https://github.com/code-423n4/2023-11-panoptic/blob/f75d07c345fd795f907385868c39bafcd6a56624/contracts/libraries/PanopticMath.sol#L48-L59

Incorporating additional distinguishing parameters into the hash could also reduce collision risks. The function could use more than 64 bits from the hash

[L-2] Array Lengths not checked

Impact

If the lengths of the ids and amounts arrays are not explicitly checked and enforced to be equal, the function may behave unpredictably. This can lead to scenarios where the contract processes an unequal number of ids and amounts, potentially resulting in incorrect token transfers.

FILE: 2023-11-panoptic/contracts/tokens/ERC1155Minimal.sol

function safeBatchTransferFrom(
        address from,
        address to,
        uint256[] calldata ids,
        uint256[] calldata amounts,
        bytes calldata data
    ) public virtual {

https://github.com/code-423n4/2023-11-panoptic/blob/f75d07c345fd795f907385868c39bafcd6a56624/contracts/tokens/ERC1155Minimal.sol#L128-L171

+  require(ids.length == amounts.length, "IDs and amounts length mismatch");

[L-3] Hardcoded function selector in safeTransferFrom() function

Since the function selector is hardcoded, your safeTransferFrom function can only interact with contracts that have a transferFrom method matching the exact signature transferFrom(address,address,uint256). If you encounter a token contract with a slightly different transferFrom method (even if just the parameter names are different), this hardcoded selector won't work.

Even though this function does the same thing, the signature is slightly different (sender and recipient instead of from and to). The function selector for this version of transferFrom will be different from 0x23b872dd. Therefore, your safeTransferFrom function will not be able to interact with this token contract because the hardcoded selector won't match.

FILE: 2023-11-panoptic/contracts/libraries/SafeTransferLib.sol

20: mstore(p, 0x23b872dd00000000000000000000000000000000000000000000000000000000)

https://github.com/code-423n4/2023-11-panoptic/blob/aa86461c9d6e60ef75ed5a1fe36a748b952c8666/contracts/libraries/SafeTransferLib.sol#L24

Instead of hardcoding, calculate the function selector dynamically within your contract based on the actual function signature

[L-4]

#0 - c4-judge

2023-12-14T17:02:28Z

Picodes marked the issue as grade-b

Findings Information

🌟 Selected for report: sivanesh_808

Also found by: 0x6980, 0x886699, 0xAnah, 0xhex, 0xta, Eurovickk, JCK, K42, SAQ, SY_S, Sathish9098, alix40, arjun16, fnanni, naman1778, nisedo, unique

Awards

226.1763 USDC - $226.18

Labels

bug
G (Gas Optimization)
grade-a
sponsor confirmed
edited-by-warden
G-17

External Links

GAS OPTIMIZATION

[G-1] ReentrancyLock modifier can be more gas optimized

Changing a storage value from 0 to a non-zero value (like 1) does indeed incur a higher gas cost (20,000 gas) compared to changing it from one non-zero value to another (5,000 gas). However, this higher cost is a one-time expense per storage slot when it first transitions from 0 to a non-zero value. Once a storage slot is set to a non-zero value, subsequent changes between non-zero values are less expensive.

Initial Transition from 0 to 1: The first time the locked field is set from 0 to 1, it will incur the higher gas cost. This happens only once per pool.

Subsequent Toggles Between Non-Zero Values: Once locked has been set to 1, subsequent toggles between 1 and another non-zero value (e.g., 2) will incur the lower cost.

This approach assumes that the reentrancy lock will be used multiple times over the lifecycle of each pool. The initial higher cost can be considered an investment that leads to savings in gas costs over time, as each subsequent lock and unlock operation will be cheaper

FILE: 2023-11-panoptic/contracts/SemiFungiblePositionManager.sol

 struct PoolAddressAndLock {
        IUniswapV3Pool pool;
-        bool locked;
+       uint32 locked;
    }


- /// @notice Modifier that prohibits reentrant calls for a specific pool
-    /// @dev We piggyback the reentrancy lock on the (pool id => pool) mapping to save gas
-    /// @dev (there's an extra 96 bits of storage available in the mapping slot and it's almost always warm)
-    /// @param poolId The poolId of the pool to activate the reentrancy lock on
-    modifier ReentrancyLock(uint64 poolId) {
-        // check if the pool is already locked
-        // init lock if not
-        beginReentrancyLock(poolId);
-
-        // execute function
-        _;
-
-        // remove lock
-        endReentrancyLock(poolId);
-    }
-
-    /// @notice Add reentrancy lock on pool
-    /// @dev reverts if the pool is already locked
-    /// @param poolId The poolId of the pool to add the reentrancy lock to
-    function beginReentrancyLock(uint64 poolId) internal {
-        // check if the pool is already locked, if so, revert
-        if (s_poolContext[poolId].locked) revert Errors.ReentrantCall();
-
-        // activate lock
-        s_poolContext[poolId].locked = true;
-    }
-
-    /// @notice Remove reentrancy lock on pool
-    /// @param poolId The poolId of the pool to remove the reentrancy lock from
-    function endReentrancyLock(uint64 poolId) internal {
-        // gas refund is triggered here by returning the slot to its original value
-        s_poolContext[poolId].locked = false;
-    }

+ modifier ReentrancyLock(uint64 poolId) {
+    // Inline the logic of beginReentrancyLock to save on function call overhead
+    // Check if the pool is already locked, if so, revert
+    if (s_poolContext[poolId].locked != 1) { // Assuming 'locked' is initialized to 1
+        s_poolContext[poolId].locked = 2; // Use different non-zero values to toggle
+    } else {
+        revert Errors.ReentrantCall();
+    }
+
+    _; // Execute function
+
+    // Inline the logic of endReentrancyLock
+    s_poolContext[poolId].locked = 1; // Toggle back
+  }

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

[G-2] totalLiquidity * 2 ** 64 , netLiquidity ** 2 , totalLiquidity ** 2 repeated calculations results should be cached

Caching the results of repeated calculations like totalLiquidity * 2 ** 64, netLiquidity ** 2, and totalLiquidity ** 2 in a Solidity contract can be a good practice for optimizing gas usage. This will approximately saves 1000 - 1500 GAS

FILE: 2023-11-panoptic/contracts/SemiFungiblePositionManager.sol

{
                uint128 collected0 = uint128(collectedAmounts.rightSlot());
                uint128 collected1 = uint128(collectedAmounts.leftSlot());
                
+               uint256 totalLiquidityResult_  = totalLiquidity * 2 ** 64 ;
+               uint256 netLiquidity_ = netLiquidity ** 2 ; 

                // compute the base premium as collected * total / net^2 (from Eqn 3)
                premium0X64_base = Math
-                    .mulDiv(collected0, totalLiquidity * 2 ** 64, netLiquidity ** 2)
+                    .mulDiv(collected0, totalLiquidityResult_ , netLiquidity_ )
                    .toUint128();
                premium1X64_base = Math
-                    .mulDiv(collected1, totalLiquidity * 2 ** 64, netLiquidity ** 2)
+                    .mulDiv(collected1, totalLiquidityResult_, netLiquidity_)
                    .toUint128();
         }


 {

+              uint256  totalLiquidity_ = totalLiquidity ** 2 ;
                    // compute the gross premium (from Eqn 4)
-                    uint256 numerator = totalLiquidity ** 2 -
+                    uint256 numerator = totalLiquidity_  -
                        totalLiquidity *
                        removedLiquidity +
                        ((removedLiquidity ** 2) / 2 ** (VEGOID));
                    premium0X64_gross = Math
-                        .mulDiv(premium0X64_base, numerator, totalLiquidity ** 2)
+                        .mulDiv(premium0X64_base, numerator, totalLiquidity_)
                        .toUint128();
                    premium1X64_gross = Math
-                        .mulDiv(premium1X64_base, numerator, totalLiquidity ** 2)
+                        .mulDiv(premium1X64_base, numerator, totalLiquidity_)
                        .toUint128();
                    deltaPremiumGross = uint256(0).toRightSlot(premium0X64_gross).toLeftSlot(
                        premium1X64_gross
                    );
                }

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

[G-3] Caching univ3pool.tickSpacing() to eliminate repeated external calls in loop iterations

Assign the value from univ3pool.tickSpacing() to this variable before the loop begins. Use this cached value inside the loop instead of calling the function repeatedly.

FILE: 2023-11-panoptic/contracts/SemiFungiblePositionManager.sol

uint256 numLegs = id.countLegs();
+ uint24 tickSpacing_ = univ3pool.tickSpacing() ;
        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()
+                tickSpacing_
            );

         

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

[G-4] Caching Repeated liquidityChunk.tickLower() and liquidityChunk.tickUpper()function Calls

Caching values like liquidityChunk.tickLower() and liquidityChunk.tickUpper() inside the loop can significantly improve gas efficiency, especially these methods are called multiple times within the loop. This optimization reduces the number of function calls, each of which consumes gas.

FILE: 2023-11-panoptic/contracts/SemiFungiblePositionManager.sol

 uint256 liquidityChunk = PanopticMath.getLiquidityChunk(
                id,
                leg,
                uint128(amount),
                univ3pool.tickSpacing()
            );

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

https://github.com/code-423n4/2023-11-panoptic/blob/f75d07c345fd795f907385868c39bafcd6a56624/contracts/SemiFungiblePositionManager.sol#L599-L609

[G-5] amount0Delta > 0 check should be cached

amount0Delta > 0 result should be cached with memory variable instead of checking multiple times

FILE: 2023-11-panoptic/contracts/SemiFungiblePositionManager.sol

+  bool amount0Delta_ = amount0Delta > 0 ;
// Extract the address of the token to be sent (amount0 -> token0, amount1 -> token1)
-        address token = amount0Delta > 0
+        address token = amount0Delta_ 
            ? address(decoded.poolFeatures.token0)
            : address(decoded.poolFeatures.token1);

        // Transform the amount to pay to uint256 (take positive one from amount0 and amount1)
-        uint256 amountToPay = amount0Delta > 0 ? uint256(amount0Delta) : 
+        uint256 amountToPay = amount0Delta_  ? uint256(amount0Delta) : uint256(amount1Delta);

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

[G-6] Use bitwise operations directly instead of calling these functions separately

The getPoolId() functions can be optimized by using bitwise operations directly in the function that calls them, instead of calling these functions separately. This will save the gas used for function calls.

This saves 200-400 gas per function call (depends on the EVM version and gas schedule)

FILE: 2023-11-panoptic/contracts/SemiFungiblePositionManager.sol

- 368:   uint64 poolId = PanopticMath.getPoolId(univ3pool);

+        uint64 poolId= uint64(uint160(univ3pool) >> 96)

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

[G-7] Memory variables should created outside of loop

Declaring variables inside loops, especially when the loop iterates many times, can be inefficient. This is because each iteration of the loop creates new instances of these variables.

the variables _moved, _itmAmounts, and _totalCollected are declared inside the for loop but outside the inner block {...}. This is still within the loop's scope, so these variables are redeclared on each iteration. It might be more efficient to declare them outside of the loop.

The variables _univ3pool, _tokenId, _isBurn, _positionSize, and _leg are declared within an inner block. This is a common practice to avoid the "stack too deep" error in Solidity. These variables seem to be derivatives of state variables or function arguments, which are read once and then used multiple times in the loop. If these variables are not expected to change during the loop iterations, declaring them outside the loop and before entering it could be more efficient. This way, you only read from state variables or compute their values once.

FILE: 2023-11-panoptic/contracts/SemiFungiblePositionManager.sol

   for (uint256 leg = 0; leg < numLegs; ) {
            int256 _moved;
            int256 _itmAmounts;
            int256 _totalCollected;

            {
                // cache the univ3pool, tokenId, isBurn, and _positionSize variables to get rid of stack too deep error
                IUniswapV3Pool _univ3pool = univ3pool;
                uint256 _tokenId = tokenId;
                bool _isBurn = isBurn;
                uint128 _positionSize = positionSize;
                uint256 _leg;

                unchecked {
                    // Reverse the order of the legs if this call is burning a position (LIFO)
                    // We loop in reverse order if burning a position so that any dependent long liquidity is returned to the pool first,
                    // allowing the corresponding short liquidity to be removed
                    _leg = _isBurn ? numLegs - leg - 1 : leg;
                }

                // 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(
                    _tokenId,
                    _leg,
                    _positionSize,
                    _univ3pool.tickSpacing()
                );

                (_moved, _itmAmounts, _totalCollected) = _createLegInAMM(
                    _univ3pool,
                    _tokenId,
                    _leg,
                    liquidityChunk,
                    _isBurn
                );

                unchecked {
                    // increment accumulators of the upper bound on tokens contained across all legs of the position at any given tick
                    amount0 += Math.getAmount0ForLiquidity(liquidityChunk);

                    amount1 += Math.getAmount1ForLiquidity(liquidityChunk);
                }
            }

            totalMoved = totalMoved.add(_moved);
            itmAmounts = itmAmounts.add(_itmAmounts);
            totalCollected = totalCollected.add(_totalCollected);

            unchecked {
                ++leg;
            }
        }

https://github.com/code-423n4/2023-11-panoptic/blob/f75d07c345fd795f907385868c39bafcd6a56624/contracts/SemiFungiblePositionManager.sol#L860-L912

[G-8] Don't cache memory variables

Function parameters in Solidity are stored in memory, not in contract storage.Assigning the parameter univ3pool to another memory variable _univ3pool doesn't fundamentally change how the data is accessed or stored. It's still a memory-to-memory operation, which is relatively low in gas cost. The additional assignment might even add a small overhead.

FILE: 2023-11-panoptic/contracts/SemiFungiblePositionManager.sol

752: IUniswapV3Pool _univ3pool = univ3pool;

867:  IUniswapV3Pool _univ3pool = univ3pool;

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

[G-9] movedInLeg.rightSlot() and movedInLeg.leftSlot() functions should be cached

function calls can have a non-negligible gas cost, especially if they are external calls or if they read from contract storage. If movedInLeg.rightSlot() and movedInLeg.leftSlot() are either external calls or involve reading from storage, and they are called multiple times, caching their results in a local variable could save gas.

FILE: 2023-11-panoptic/contracts/SemiFungiblePositionManager.sol

 // moved will be negative if the leg was long (funds left the caller, don't count it in collected fees)
            uint128 collected0;
            uint128 collected1;
+      int128 rightSlot_ = movedInLeg.rightSlot();
+      int128 leftSlot_ = movedInLeg.leftSlot();
            unchecked {
-                collected0 = movedInLeg.rightSlot() < 0
+                collected0 = rightSlot_ < 0
-                    ? receivedAmount0 - uint128(-movedInLeg.rightSlot())
+                    ? receivedAmount0 - uint128(-rightSlot_)
                    : receivedAmount0;
-                collected1 = movedInLeg.leftSlot() < 0
+                collected1 = leftSlot_  < 0
-                    ? receivedAmount1 - uint128(-movedInLeg.leftSlot())
+                    ? receivedAmount1 - uint128(-leftSlot_)
                    : receivedAmount1;
            }

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

[G-10] Optimizing the add() function for enhanced gas efficiency

  • The addition of left and right slots is performed directly in int128. This approach minimizes the number of type conversions and uses the native type of the slots
  • The overflow and underflow checks are simplified. The conditions check if the sign of the sum is different from what it should be if no overflow or underflow occurred. This is a more efficient way to perform these checks
  • The final result z is constructed by shifting the left sum (leftSum) to the left by 128 bits and then combining it with the right sum (rightSum) using a bitwise OR operation. This approach avoids additional function calls and directly manipulates the bits
FILE: 2023-11-panoptic/contracts/types/LeftRight.sol

- function add(int256 x, int256 y) internal pure returns (int256 z) {
-        unchecked {
-            int256 left256 = int256(x.leftSlot()) + y.leftSlot();
-            int128 left128 = int128(left256);
-
-            int256 right256 = int256(x.rightSlot()) + y.rightSlot();
-            int128 right128 = int128(right256);
-
-            if (left128 != left256 || right128 != right256) revert Errors.UnderOverFlow();
-
-            return z.toRightSlot(right128).toLeftSlot(left128);
-        }
-    }

+ function add(int256 x, int256 y) internal pure returns (int256 z) {
+    unchecked {
+        int128 leftSum = int128(x.leftSlot()) + int128(y.leftSlot());
+        int128 rightSum = int128(x.rightSlot()) + int128(y.rightSlot());
+
+        // Overflow/underflow checks
+        if ((leftSum < x.leftSlot()) != (y.leftSlot() < 0) || 
+            (rightSum < x.rightSlot()) != (y.rightSlot() < 0)) {
+            revert Errors.UnderOverFlow();
+        }
+
+        // Combine left and right sums into one 256-bit integer
+        z = (int256(leftSum) << 128) | int256(uint128(rightSum));
+    }
+ }

https://github.com/code-423n4/2023-11-panoptic/blob/aa86461c9d6e60ef75ed5a1fe36a748b952c8666/contracts/types/LeftRight.sol#L159-L171

[G-11] Optimizing the sub() function for enhanced gas efficiency

Same like add() function sub() also can be more gas optimized

FILE: 2023-11-panoptic/contracts/types/LeftRight.sol

- function sub(int256 x, int256 y) internal pure returns (int256 z) {
-        unchecked {
-            int256 left256 = int256(x.leftSlot()) - y.leftSlot();
-            int128 left128 = int128(left256);
-
-            int256 right256 = int256(x.rightSlot()) - y.rightSlot();
-            int128 right128 = int128(right256);
-
-            if (left128 != left256 || right128 != right256) revert Errors.UnderOverFlow();
-
-            return z.toRightSlot(right128).toLeftSlot(left128);
-        }
-    }

+ function optimizedSub(int256 x, int256 y) internal pure returns (int256 z) {
+    unchecked {
+        int128 leftDifference = int128(x.leftSlot()) - int128(y.leftSlot());
+        int128 rightDifference = int128(x.rightSlot()) - int128(y.rightSlot());
+
+        // Overflow/underflow checks
+        if (int256(leftDifference) != x.leftSlot() - y.leftSlot() || 
+            int256(rightDifference) != x.rightSlot() - y.rightSlot()) {
+            revert Errors.UnderOverFlow();
+        }
+
+        // Combine left and right differences into one 256-bit integer
+        z = (int256(leftDifference) << 128) | int256(uint128(rightDifference));
+    }
+  }

https://github.com/code-423n4/2023-11-panoptic/blob/aa86461c9d6e60ef75ed5a1fe36a748b952c8666/contracts/types/LeftRight.sol#L177-L189

#0 - c4-judge

2023-12-14T17:11:50Z

Picodes marked the issue as grade-a

#1 - dyedm1

2023-12-14T19:05:25Z

ReentrancyLock modifier can be more gas optimized

The entire struct here actually fits in one storage slot, so the slot is made nonzero by the pool address. That's the reason they're packed in the first place.

#2 - c4-sponsor

2023-12-14T20:00:36Z

dyedm1 (sponsor) confirmed

#3 - c4-judge

2023-12-26T23:36:21Z

Picodes marked the issue as selected for report

#4 - c4-judge

2023-12-26T23:36:58Z

Picodes marked the issue as not selected for report

Findings Information

Labels

analysis-advanced
grade-a
selected for report
edited-by-warden
A-08

Awards

623.0797 USDC - $623.08

External Links

Analysis Panoptic

Overview

Panoptic is a platform designed to facilitate effortless options trading on any crypto asset. It offers a comprehensive suite of tools for the decentralized finance (DeFi) community, accommodating a wide range of strategies and roles within the DeFi ecosystem. The platform enables users to trade any token, at any strike price, and of any size, without the need for intermediaries like banks, brokerage firms, clearinghouses, market makers, or centralized exchanges

Panoptic risk model

Systemic Risks

Complexity in Position Managements

  • Handling multiple transaction legs increases the complexity of state management and transaction execution logic
  • This standard allows for representing multiple token types within a single contract, adding complexity to tracking and managing these diverse assets
  • The intricate logic required for managing such diverse and multifaceted positions could lead to bugs, especially in edge cases or unexpected market conditions.
  • The interaction of these complex positions with Uniswap's dynamic and variable liquidity pools might result in unforeseen outcomes, especially under high market volatility or liquidity changes

Liquidity Provision and Burn Mechanisms

The Liquidity Provision and Burn Mechanisms in the Panoptic protocol involve two distinct operations: minting typical Liquidity Provider (LP) positions and creating "long" positions by burning Uniswap liquidity. This dual approach presents risks.

Liquidity Imbalance: Constantly changing liquidity due to frequent minting and burning can lead to imbalances, affecting price stability and the overall health of the liquidity pool.

Impact on Withdrawals: Significant liquidity burning might reduce the pool's size, impacting other users' ability to withdraw their funds under favorable conditions.

Interactions with Uniswap

The dependency of the Panoptic protocol on Uniswap's infrastructure and market dynamics as a risk factor

Protocol Changes: If Uniswap updates its protocol, these changes could impact how the Panoptic protocol interacts with Uniswap, potentially disrupting existing mechanisms.

Market Dynamics: Uniswap's liquidity and price dynamics directly influence Panoptic's operations. Sudden market shifts on Uniswap, such as large trades or liquidity changes, could adversely affect Panoptic's performance and stability.

ERC1155 Token Standards

The use of the ERC1155 standard in the Panoptic protocol introduces risks mainly due to its relative novelty and the complexity it brings.

Unexplored Vulnerabilities: Being newer than ERC20, ERC1155 hasn't been as extensively tested in real-world scenarios, especially in complex DeFi environments. This could mean there are vulnerabilities that have not yet been discovered or addressed.

Complexity in DeFi Transactions: ERC1155's ability to handle multiple asset types in a single contract adds complexity, which in a DeFi setting could lead to unforeseen issues in transaction handling, token tracking, and interoperability with other protocols or tokens.

Technical risks

PoolId collision Risks


uint64 poolId = PanopticMath.getPoolId(univ3pool);

        while (address(s_poolContext[poolId].pool) != address(0)) {
            poolId = PanopticMath.getFinalPoolId(poolId, token0, token1, fee);
        }
        
function getFinalPoolId(
        uint64 basePoolId,
        address token0,
        address token1,
        uint24 fee
    ) internal pure returns (uint64) {
        unchecked {
            return
                basePoolId +
                (uint64(uint256(keccak256(abi.encodePacked(token0, token1, fee)))) >> 32);
        }
    }
    

Discussion

  • The method getFinalPoolId uses a hash of token0, token1, and fee to generate a pseudo-random number. This is added to the basePoolId to resolve collisions.
  • While this is a creative solution to avoid collisions, it relies on pseudo-randomness. Pseudo-randomness in smart contracts, especially when derived from predictable variables like token addresses and fees, can be less reliable than true randomness and may be predictable to some extent.
Efficiency Consideration:

The method adds a computational overhead due to the hashing operation (keccak256). While this might not be significant for a single call, it could add up in scenarios where this function is called frequently. The shift operation (>> 32) is relatively efficient, but the overall efficiency depends on how often collisions actually occur and how frequently this function needs to be called.

Alternative Approaches
  • Consider using a simple mapping to track pool addresses and their associated data. This might be more straightforward and equally effective, especially if the collision risk is low.

Risks usage of non standard higher-order bit as a flag


unchecked {
            s_AddrToPoolIdData[univ3pool] = uint256(poolId) + 2 ** 255;
        }

Discussion

  • This approach is non-standard and not immediately intuitive. Developers who are new to the codebase, or even those familiar with it, might not understand the purpose of the high-order bit without proper documentation. Misunderstanding how this flag works could lead to incorrect assumptions or misuse of the data.

  • When manipulating or reading poolId values, developers must remember to correctly handle the initialization flag. If they treat the entire uint256 value as a regular integer without accounting for the high-order bit, it could lead to incorrect calculations or logic errors

Integration risks

Integration of ITM Swapping

Complexity of ITM Options: In-the-money options are those where the current price of the underlying asset is favorable compared to the strike price of the option. In the context of liquidity pools and DeFi, managing ITM options involves handling a mix of assets (like token0 and token1 in Uniswap V3) within specific price ranges. This complexity increases the difficulty in accurately executing and managing these positions.

Swapping for Balance Adjustment: When an ITM option is executed, there might be a need to swap between the assets (token0 and token1) to balance the position accurately. This involves interacting with the liquidity pool to execute a swap that aligns with the current state of the option. Ensuring this swap is done efficiently and accurately, without causing slippage or unfavorable rates, is challenging.

Impact on Liquidity Pool: Executing swaps for ITM options can have an impact on the liquidity pool, especially if the volume of the swap is significant compared to the pool's size. Large swaps could move the price, affect liquidity depth, or even trigger cascading trades, which might not be in the best interest of the liquidity providers or other traders in the pool.

Precision and Timing: The accuracy of executing these swaps is crucial. The timing and rate at which the swap occurs can significantly impact the profitability and risk profile of the option. Delays or inaccuracies could lead to less favorable conditions and potential losses.

Integration of Complex Fee and Premium Calculation:

Complex Fee Structure: The contract handles the calculation of fees and premiums based on multiple factors like liquidity position, transaction size, and market conditions. The complexity of this fee structure increases the risk of calculation errors, which could lead to incorrect fee charges or premium distributions.

High-Transaction-Volume Impact: In scenarios with high transaction volumes, there's a risk that the contract may not accurately account for rapid changes in liquidity or price movements. This could result in outdated or incorrect fee calculations.

Liquidity Pool Integration

Market Condition Sensitivity: Liquidity pools are highly sensitive to market conditions. During periods of high volatility or irregular market movements, the liquidity dynamics can change rapidly, affecting the stability and efficiency of the Panoptic protocol's interactions with these pools.

Imbalanced Liquidity Provision and Removal: The process of adding (minting) and removing (burning) liquidity needs to be well-managed to maintain pool health. Imbalances caused by excessive liquidity removal or provision could lead to adverse effects like price slippage, reduced pool efficiency, or increased impermanent loss risk for liquidity providers.

Liquidity Pool Solvency Risks: If the liquidity pools that Panoptic interacts with face solvency issues, it could endanger the positions managed by Panoptic. For instance, a significant decline in a pool's liquidity could impact the ability to execute trades or manage positions effectively.

Software Engineering Considerations

Smart Contract Modularity

The contract encompasses diverse functionalities like liquidity management and ITM swapping. Refactoring to enhance modularity, where logical components are isolated (e.g., separate modules for ERC1155 handling, liquidity management, fee calculation), could improve maintainability and readability.

Gas Optimization in Complex Functions

Functions like _validateAndForwardToAMM and _createLegInAMM are complex and likely gas-intensive. Optimizing these by reducing state changes, using memory variables efficiently, and simplifying computations where possible is recommended.

Security in Custom Reentrancy Guard

The custom reentrancy guard logic should be thoroughly tested, especially for edge cases. Considering fallbacks or additional checks might enhance security.

Robust Error Handling and Reversion Messages

Given the contract's complexity, implementing detailed revert messages that provide clarity on the failure points would be beneficial for troubleshooting and user feedback.

Testing for Edge Cases in Fee and Premium Calculations

The fee and premium calculation mechanisms are intricate and thus prone to errors. Unit tests should cover a range of edge cases, including extreme market conditions and unusual liquidity scenarios.

Integration Testing with Uniswap V3:

Since the contract interacts extensively with Uniswap V3, integration tests simulating real-world Uniswap interactions are crucial. This includes testing how the contract responds to changes in Uniswap's state (like liquidity shifts or price changes).

Code Comments and Documentation

Enhance inline documentation and code comments, especially in complex sections like premium calculations and liquidity chunk management, to aid future developers in understanding the contract's logic.

Handling ERC1155 Specifics

Given the use of ERC1155, ensure that the contract handles batch transfers, minting, and burning correctly and efficiently. This includes compatibility with wallets and interfaces that may primarily support ERC20 or ERC721.

Upgrade and Maintenance Strategy

If the contract is not upgradable, having a clear strategy for migrating to a new version in case of significant updates or bug fixes is important. If it is upgradable, ensure the security and integrity of the upgrade mechanism.

Test Coverage

- What is the overall line coverage percentage provided by your tests?: 100

As per docs the reported line coverage for your tests is 100% for the in-scope contracts, but there are other contracts not covered by these tests, it's important to extend your testing

Single Point of failure Admin Risks

There is no single point of failures and admin risks because many functions not depend any of Owners and Admins. Most of the functions are external and public and open to to any one call this.

Architecture and code illustrations

SemiFungiblePositionManager Contract

Panpotic_Analysis

Functions Illustrations

''initializeAMMPool() ''

This function initializes an Automated Market Maker (AMM) pool in a Panoptic protocol. Here's a concise explanation:

  1. Uni v3 Pool Address:

    • Computes the address of the Uniswap v3 pool for given tokens (token0, token1) and fee tier (fee).
  2. Initialization Check:

    • Reverts if the Uniswap v3 pool has not been initialized, indicating a potential error.
  3. Already Initialized Check:

    • Checks if the pool has already been initialized in the Panoptic protocol (s_AddrToPoolIdData). If so, returns, preventing duplicate initialization.
  4. Calculate PoolId:

    • Sets the base poolId as the last 8 bytes of the Uniswap v3 pool address.
    • In case of a collision, it increments the poolId using a pseudo-random number.
  5. Unique PoolId Assignment:

    • Iteratively finds a unique poolId to avoid collisions with existing pools.
  6. Store Pool Information:

    • Stores the UniswapV3Pool and lock status in s_poolContext mapping using the calculated poolId.
  7. Store PoolId Information:

    • Records the UniswapV3Pool to poolId mapping in s_AddrToPoolIdData with a bit indicating initialization.
  8. Emit Initialization Event:

    • Emits an event signaling the successful initialization of the Uniswap v3 pool.
  9. Return and Assembly Block:

    • Returns from the function.
    • Includes an assembly block that disables memoryguard during compilation, addressing size limit concerns for the contract.

This function initializes a Uniswap v3 pool, ensures uniqueness, and handles various checks for proper execution in the Panoptic protocol.

registerTokenTransfer()

The interna function registerTokenTransfer facilitates the transfer of liquidity between accounts in a Uniswap v3 Automated Market Maker (AMM) pool within the Panoptic protocol:

  1. Extract Uniswap Pool:

    • Retrieves the Uniswap v3 pool from the s_poolContext mapping using the validated pool ID.
  2. Loop Through Legs:

    • Iterates through the legs of the pool to process liquidity transfers.
  3. Extract Liquidity Chunk:

    • Utilizes PanopticMath to extract a liquidity chunk, representing the liquidity amount and tick range.
  4. Construct Position Keys:

    • Forms unique position keys for the sender and recipient based on pool address, addresses, token types, and tick range.
  5. Revert Checks:

    • Reverts if the recipient already holds a position or if the balance transfer is incomplete.
  6. Update and Store:

    • Updates and stores liquidity and fee values between accounts, effectively transferring the liquidity.

This function ensures the secure transfer of liquidity and fee values within Uniswap v3 pools in the Panoptic protocol.

validateAndForwardToAMM()

The internal function _validateAndForwardToAMM facilitates the validation and execution of position minting or burning within a Uniswap v3 Automated Market Maker (AMM) pool in the Panoptic protocol:

  1. Position Validation:

    • Reverts if the provided position size is zero, ensuring a non-zero balance.
  2. Flip Burn Token:

    • Adjusts the tokenId if it represents a burn operation by flipping the isLong bits.
  3. Uniswap Pool Extraction:

    • Retrieves the Uniswap v3 pool from the s_poolContext mapping based on the validated tokenId.
  4. Initialization Check:

    • Reverts if the Uniswap pool has not been previously initialized.
  5. Swap Configuration:

    • Determines whether a swap should occur at mint based on tick limits.
  6. Position Creation in AMM:

    • Calls _createPositionInAMM to loop through each leg of the tokenId and mints or burns liquidity in the Uniswap v3 pool.
  7. ITM Swap Check:

    • If in-the-money (ITM) amounts are non-zero and tick limits are inverted, swaps necessary amounts to address slippage.
  8. Current Tick Check:

    • Retrieves the current tick of the Uniswap pool and checks if it falls within specified tick limits, reverting if not.
  9. Return Values:

    • Returns the total collected from the AMM, total moved, and the new tick after the operation.

This function ensures the proper validation and execution of minting or burning positions in a Uniswap v3 pool, considering tick limits and potential swaps.

swapInAMM()

The internal function swapInAMM conducts token swaps within a Uniswap v3 Automated Market Maker (AMM) pool in the Panoptic protocol:

  1. Initialization:

    • Initializes variables for swap direction, swap amount, and callback data.
  2. In-the-Money (ITM) Amount Unpacking:

    • Unpacks the positive and negative ITM amounts for token0 and token1.
  3. Callback Data Struct Construction:

    • Constructs callback data containing pool features, such as token0, token1, and fee.
  4. Netting Swap:

    • Computes a single "netting" swap to address ITM amounts, considering token0 and token1 surpluses or shortages.
  5. Zero-For-One Determination:

    • Determines the direction of the swap (token0 to token1 or vice versa) based on the netting result.
  6. Swap Amount Calculation:

    • Computes the exact swap amount needed to address the net surplus or shortage.
  7. Token Swap Execution:

    • Executes the token swap in the Uniswap pool, triggering a callback function.
  8. Total Swapped Calculation:

    • Adds the amounts swapped to the totalSwapped variable, considering both token0 and token1.

This function performs netting swaps to efficiently manage ITM amounts and executes token swaps in a Uniswap v3 pool, ensuring proper accounting of swapped quantities.

Code WeakSpots


function afterTokenTransfer(
        address from,
        address to,
        uint256[] memory ids,
        uint256[] memory amounts
    ) internal override {
        for (uint256 i = 0; i < ids.length; ) {
            registerTokenTransfer(from, to, ids[i], amounts[i]);
            unchecked {
                ++i;
            }
        }
    }
  • Uniswap V3 primarily operates with ERC20 tokens, which represent a single asset type per contract. ERC1155, on the other hand, supports both fungible and non-fungible tokens within a single contract. This fundamental difference in how these token standards operate can lead to complexities when integrating ERC1155 tokens with Uniswap V3's ERC20-focused mechanisms.

  • Managing liquidity for ERC1155 tokens within Uniswap V3 pools could introduce additional challenges. Since Uniswap V3 is designed around the liquidity dynamics of ERC20 tokens, accommodating the unique aspects of ERC1155 (like managing multiple token types in one contract) might complicate liquidity provision, withdrawal, and pricing.


constructor(IUniswapV3Factory _factory) {
        FACTORY = _factory;
    }

Confirm that the IUniswapV3Factory interface in your codebase aligns with the actual ABI of the Uniswap V3 Factory. Any mismatch could result in failed transactions or incorrect behaviors.


 modifier ReentrancyLock(uint64 poolId) {
        // check if the pool is already locked
        // init lock if not
        beginReentrancyLock(poolId);

        // execute function
        _;

        // remove lock
        endReentrancyLock(poolId);
    }

    /// @notice Add reentrancy lock on pool
    /// @dev reverts if the pool is already locked
    /// @param poolId The poolId of the pool to add the reentrancy lock to
    function beginReentrancyLock(uint64 poolId) internal {
        // check if the pool is already locked, if so, revert
        if (s_poolContext[poolId].locked) revert Errors.ReentrantCall();

        // activate lock
        s_poolContext[poolId].locked = true;
    }

    /// @notice Remove reentrancy lock on pool
    /// @param poolId The poolId of the pool to remove the reentrancy lock from
    function endReentrancyLock(uint64 poolId) internal {
        // gas refund is triggered here by returning the slot to its original value
        s_poolContext[poolId].locked = false;
    }

Custom implementations of security features like reentrancy guards are critical and can be potential points of failure.

Time spent:

20 hours

#0 - c4-judge

2023-12-14T17:20:15Z

Picodes marked the issue as grade-a

#1 - c4-judge

2023-12-26T23:25:23Z

Picodes marked the issue as selected for report

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter