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
Rank: 18/72
Findings: 3
Award: $860.58
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: osmanozdemir1
Also found by: 0xCiphky, Audinarey, Banditx0x, CRYP70, Cryptor, D1r3Wolf, KupiaSec, LokiThe5th, Sathish9098, Skylice, ThenPuli, Topmark, Udsen, ZanyBonzy, baice, ether_sky, fatherOfBlocks, foxb868, grearlake, hihen, hubble, hunter_w3b, lanrebayode77, leegh, lsaudit, minhtrng, nocoder, onchain-guardians, ptsanev, ro1sharkm, seaton0x1, sivanesh_808, t4sk, tapir, tpiliposian, ustas
11.3163 USDC - $11.32
getFinalPoolId()
FunctionThe 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); } }
Incorporating additional distinguishing parameters into the hash could also reduce collision risks. The function could use more than 64 bits from the hash
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 {
+ require(ids.length == amounts.length, "IDs and amounts length mismatch");
safeTransferFrom()
functionSince 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)
Instead of hardcoding, calculate the function selector dynamically within your contract based on the actual function signature
#0 - c4-judge
2023-12-14T17:02:28Z
Picodes marked the issue as grade-b
226.1763 USDC - $226.18
ReentrancyLock
modifier can be more gas optimizedChanging 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 + }
totalLiquidity * 2 ** 64
, netLiquidity ** 2
, totalLiquidity ** 2
repeated calculations results should be cachedCaching 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 ); }
univ3pool.tickSpacing()
to eliminate repeated external calls in loop iterationsAssign 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_ );
liquidityChunk.tickLower()
and liquidityChunk.tickUpper()
function CallsCaching 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_ ) );
amount0Delta > 0
check should be cachedamount0Delta > 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);
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)
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; } }
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;
movedInLeg.rightSlot()
and movedInLeg.leftSlot()
functions should be cachedfunction 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; }
add()
function for enhanced gas efficiencyFILE: 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)); + } + }
sub()
function for enhanced gas efficiencySame 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)); + } + }
#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
🌟 Selected for report: Sathish9098
Also found by: 0xAadi, 0xHelium, 0xSmartContract, Bulletprime, K42, Raihan, ZanyBonzy, catellatech, fouzantanveer, foxb868, tala7985
623.0797 USDC - $623.08
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
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.
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.
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.
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); } }
getFinalPoolId
uses a hash of token0
, token1
, and fee
to generate a pseudo-random number. This is added to the basePoolId
to resolve collisions.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
.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.
unchecked { s_AddrToPoolIdData[univ3pool] = uint256(poolId) + 2 ** 255; }
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
Complexity of ITM Option
s: 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.
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.
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.
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.
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.
The custom reentrancy guard logic should be thoroughly tested, especially for edge cases. Considering fallbacks or additional checks might enhance security.
Given the contract's complexity, implementing detailed revert messages that provide clarity on the failure points would be beneficial for troubleshooting and user feedback.
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.
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).
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.
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.
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.
- 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
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.
This function initializes an Automated Market Maker (AMM) pool in a Panoptic protocol. Here's a concise explanation:
Uni v3 Pool Address:
token0
, token1
) and fee tier (fee
).Initialization Check:
Already Initialized Check:
s_AddrToPoolIdData
). If so, returns, preventing duplicate initialization.Calculate PoolId:
poolId
as the last 8 bytes of the Uniswap v3 pool address.poolId
using a pseudo-random number.Unique PoolId Assignment:
poolId
to avoid collisions with existing pools.Store Pool Information:
s_poolContext
mapping using the calculated poolId
.Store PoolId Information:
poolId
mapping in s_AddrToPoolIdData
with a bit indicating initialization.Emit Initialization Event:
Return and Assembly Block:
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.
The interna function registerTokenTransfer
facilitates the transfer of liquidity between accounts in a Uniswap v3 Automated Market Maker (AMM) pool within the Panoptic protocol:
Extract Uniswap Pool:
s_poolContext
mapping using the validated pool ID.Loop Through Legs:
Extract Liquidity Chunk:
Construct Position Keys:
Revert Checks:
Update and Store:
This function ensures the secure transfer of liquidity and fee values within Uniswap v3 pools in the Panoptic protocol.
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:
Position Validation:
Flip Burn Token:
tokenId
if it represents a burn operation by flipping the isLong
bits.Uniswap Pool Extraction:
s_poolContext
mapping based on the validated tokenId
.Initialization Check:
Swap Configuration:
Position Creation in AMM:
_createPositionInAMM
to loop through each leg of the tokenId
and mints or burns liquidity in the Uniswap v3 pool.ITM Swap Check:
Current Tick Check:
Return Values:
This function ensures the proper validation and execution of minting or burning positions in a Uniswap v3 pool, considering tick limits and potential swaps.
The internal function swapInAMM
conducts token swaps within a Uniswap v3 Automated Market Maker (AMM) pool in the Panoptic protocol:
Initialization:
In-the-Money (ITM) Amount Unpacking:
Callback Data Struct Construction:
Netting Swap:
Zero-For-One Determination:
Swap Amount Calculation:
Token Swap Execution:
Total Swapped Calculation:
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.
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.
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