Panoptic - hihen's results

Permissionless, perpetual options trading on any token, any strike, any size.

General Information

Platform: Code4rena

Start Date: 01/04/2024

Pot Size: $120,000 USDC

Total HM: 11

Participants: 55

Period: 21 days

Judge: Picodes

Total Solo HM: 6

Id: 354

League: ETH

Panoptic

Findings Distribution

Researcher Performance

Rank: 25/55

Findings: 1

Award: $212.89

🌟 Selected for report: 0

🚀 Solo Findings: 0

QA Report

Summary

Low Issues

Total 138 instances over 17 issues:

IDIssueInstances
[L-01]Array length is not checked before access its index12
[L-02]Return values of approve() not checked4
[L-03]Events may be emitted out of order due to reentrancy11
[L-04]Missing zero address check in constructor8
[L-05]Missing zero address check in initializer1
[L-06]Named return variable used before assignment5
[L-07]Revert on transfer to the zero address7
[L-08]Written only contract variables1
[L-09]Downcasting other types to an address can cause collisions1
[L-10]Vulnerable versions of packages are being used2
[L-11]Constructor / initialization function lacks parameter validation7
[L-12]Minting in a loop may lead to a DOS2
[L-13]Contracts are vulnerable to fee-on-transfer accounting-related issues7
[L-14]Functions calling contracts/addresses with transfer hooks should be protected by reentrancy guard11
[L-15]Critical functions should be controlled by time locks7
[L-16]Tokens may be minted to the zero address13
[L-17]Code does not follow the best practice of check-effects-interaction39

Non Critical Issues

Total 850 instances over 67 issues:

IDIssueInstances
[N-01]Visibility of state variables is not explicitly defined8
[N-02]Names of private/internal functions should be prefixed with an underscore104
[N-03]Names of private/internal state variables should be prefixed with an underscore75
[N-04]Use of override is unnecessary4
[N-05]Custom errors should be used rather than revert()/require()9
[N-06]Add inline comments for unnamed parameters2
[N-07]Assembly blocks should have extensive comments21
[N-08]Consider splitting complex checks into multiple steps2
[N-09]Complex casting20
[N-10]Complex math should be split into multiple steps26
[N-11]Consider adding a block/deny-list3
[N-12]Constants/Immutables redefined elsewhere4
[N-13]Convert simple if-statements to ternary expressions2
[N-14]Contract name does not match its filename4
[N-15]UPPER_CASE names should be reserved for constant/immutable variables2
[N-16]Consider emitting an event at the end of the constructor4
[N-17]Events are emitted without the sender information3
[N-18]Inconsistent floating version pragma2
[N-19]Contract implements interface without extending the interface1
[N-20]Imports could be organized more systematically4
[N-21]There is no need to initialize bool variables with false5
[N-22]Invalid NatSpec comment style6
[N-23]@openzeppelin/contracts should be upgraded to a newer version5
[N-24]Lib @uniswap/v3-core should be upgraded to a newer version1
[N-25]Lib @uniswap/v3-periphery should be upgraded to a newer version1
[N-26]Expressions for constant values should use immutable rather than constant3
[N-27]Consider moving duplicated strings to constants5
[N-28]Contract uses both require()/revert() as well as custom errors1
[N-29]Functions should be named in mixedCase style11
[N-30]Modifiers should be named in mixedCase style1
[N-31]Constants should be put on the left side of comparisons78
[N-32]else-block not required10
[N-33]Large multiples of ten should use scientific notation8
[N-34]Non-interface files should use fixed compiler versions18
[N-35]High cyclomatic complexity2
[N-36]Typos17
[N-37]Consider bounding input array length9
[N-38]Unnecessary casting15
[N-39]Unused contract variables1
[N-40]Unused import29
[N-41]Unused named return18
[N-42]Use delete instead of assigning values to false2
[N-43]Consider using delete rather than assigning zero to clear values3
[N-44]Use the latest Solidity version18
[N-45]Use a struct to encapsulate multiple function parameters9
[N-46]Returning a struct instead of a bunch of variables is better5
[N-47]Contract variables should have comments1
[N-48]Empty bytes check is missing7
[N-49]Don't define functions with the same name in a contract16
[N-50]Assembly block creates dirty bits2
[N-51]Multiple mappings with same keys can be combined into a single struct mapping for readability13
[N-52]Do not cache immutable variables1
[N-53]Function state mutability can be restricted to view1
[N-54]Missing event for critical changes9
[N-55]Non-assembly method available6
[N-56]Duplicated require()/revert() checks should be refactored13
[N-57]Consider adding emergency-stop functionality1
[N-58]Missing checks for uint state variable assignments4
[N-59]Use the Modern Upgradeable Contract Paradigm3
[N-60]Large or complicated code bases should implement invariant tests1
[N-61]The default value is manually set when it is declared31
[N-62]Contracts should have all public/external functions exposed by interfaces4
[N-63]Top-level declarations should be separated by at least two lines2
[N-64]Consider adding formal verification proofs20
[N-65]Use scopes sparingly21
[N-66]Prevent re-setting a state variable with the same value15
[N-67]Whitespace in Expressions98

Low Issues

[L-01] Array length is not checked before access its index

Accessing the elements of the array without checking or ensuring the validity of the access index in advance. It may result in an unexpected out-of-bounds error, or may result in missing elements when trying to traverse the entire array.

<details> <summary>There are 12 instances (click to show):</summary>
  • SemiFungiblePositionManager.sol ( 577-577 ):
577:             registerTokenTransfer(from, to, TokenId.wrap(ids[i]), amounts[i]);
758:             int256 pivot = arr[uint256(left + (right - left) / 2)];

760:                 while (arr[uint256(i)] < pivot) i++;

761:                 while (pivot < arr[uint256(j)]) j--;

763:                     (arr[uint256(i)], arr[uint256(j)]) = (arr[uint256(j)], arr[uint256(i)]);

763:                     (arr[uint256(i)], arr[uint256(j)]) = (arr[uint256(j)], arr[uint256(i)]);

763:                     (arr[uint256(i)], arr[uint256(j)]) = (arr[uint256(j)], arr[uint256(i)]);

763:                     (arr[uint256(i)], arr[uint256(j)]) = (arr[uint256(j)], arr[uint256(i)]);
786:                         longPremium = longPremium.sub(premiasByLeg[i][leg]);

786:                         longPremium = longPremium.sub(premiasByLeg[i][leg]);
145:             amount = amounts[i];

188:                 balances[i] = balanceOf[owners[i]][ids[i]];
</details>

[L-02] Return values of approve() not checked

Not all ERC20 implementations revert() when there's a failure in approve(). The function signature has a boolean return value and they indicate errors that way instead. By not checking the return value, operations that should have marked as failed, may potentially go through without actually approving anything.

There are 4 instances:

32:         IERC20Partial(token0).approve(address(sfpm), type(uint256).max);

33:         IERC20Partial(token1).approve(address(sfpm), type(uint256).max);

36:         IERC20Partial(token0).approve(address(ct0), type(uint256).max);

37:         IERC20Partial(token1).approve(address(ct1), type(uint256).max);

[L-03] Events may be emitted out of order due to reentrancy

Ensure that events follow the best practice of check-effects-interaction, and are emitted before external calls.

<details> <summary>There are 11 instances (click to show):</summary>
/// @audit safeTransferFrom() is called prior to this emission in deposit()
439:         emit Deposit(msg.sender, receiver, assets, shares);

/// @audit safeTransferFrom() is called prior to this emission in mint()
499:         emit Deposit(msg.sender, receiver, assets, shares);

/// @audit safeTransferFrom() is called prior to this emission in withdraw()
563:         emit Withdraw(msg.sender, receiver, owner, assets, shares);

/// @audit safeTransferFrom() is called prior to this emission in redeem()
623:         emit Withdraw(msg.sender, receiver, owner, assets, shares);
/// @audit initializeAMMPool() is called prior to this emission in deployNewPool()
268:         emit PoolDeployed(
269:             newPoolContract,
270:             v3Pool,
271:             collateralTracker0,
272:             collateralTracker1,
273:             amount0,
274:             amount1
275:         );
/// @audit _mintInSFPMAndUpdateCollateral() is called prior to this emission in _mintOptions(), which will call `mintTokenizedPosition()`
666:         emit OptionMinted(msg.sender, positionSize, tokenId, poolUtilizations);

/// @audit _burnAndHandleExercise() is called prior to this emission in _burnOptions(), which will call `burnTokenizedPosition()`
853:         emit OptionBurnt(owner, positionSize, tokenId, premiaOwed);

/// @audit delegate() is called prior to this emission in liquidate()
1170:         emit AccountLiquidated(msg.sender, liquidatee, bonusAmounts);

/// @audit delegate() is called prior to this emission in forceExercise()
1277:         emit ForcedExercised(msg.sender, account, touchedId[0], exerciseFees);

/// @audit exercise() is called prior to this emission in settleLongPremium()
1654:             emit PremiumSettled(owner, tokenId, realizedPremia);
  • SemiFungiblePositionManager.sol ( 517-517 ):
/// @audit _mint() is called prior to this emission in mintTokenizedPosition(), which will call `onERC1155Received()`
517:         emit TokenizedPositionMinted(msg.sender, tokenId, positionSize);
</details>

[L-04] Missing zero address check in constructor

Constructors often take address parameters to initialize important components of a contract, such as owner or linked contracts. However, without a checking, there's a risk that an address parameter could be mistakenly set to the zero address (0x0). This could be due to an error or oversight during contract deployment. A zero address in a crucial role can cause serious issues, as it cannot perform actions like a normal address, and any funds sent to it will be irretrievable. It's therefore crucial to include a zero address check in constructors to prevent such potential problems. If a zero address is detected, the constructor should revert the transaction.

<details> <summary>There are 8 instances (click to show):</summary>
/// @audit `_WETH9 not checked`
/// @audit `_SFPM not checked`
/// @audit `_univ3Factory not checked`
/// @audit `_donorNFT not checked`
/// @audit `_poolReference not checked`
/// @audit `_collateralReference not checked`
115:     constructor(
116:         address _WETH9,
117:         SemiFungiblePositionManager _SFPM,
118:         IUniswapV3Factory _univ3Factory,
119:         IDonorNFT _donorNFT,
120:         address _poolReference,
121:         address _collateralReference
122:     ) {
/// @audit `_sfpm not checked`
280:     constructor(SemiFungiblePositionManager _sfpm) {
  • SemiFungiblePositionManager.sol ( 341-341 ):
/// @audit `_factory not checked`
341:     constructor(IUniswapV3Factory _factory) {
</details>

[L-05] Missing zero address check in initializer

Consider adding a zero address check for each address type parameter in initializer.

There is 1 instance:

/// @audit `_owner not checked`
134:     function initialize(address _owner) public {

[L-06] Named return variable used before assignment

As no value is written to the variable, the default value is always read. This is usually due to a bug in the code logic that causes an invalid value to be used.

There are 5 instances:

/// @audit downcastedInt
319:         if (!((downcastedInt = int128(toCast)) == toCast)) revert Errors.CastingError();
/// @audit z
206:             return z.toRightSlot(right128).toLeftSlot(left128);

/// @audit z
224:             return z.toRightSlot(right128).toLeftSlot(left128);

/// @audit z
242:             return z.toRightSlot(right128).toLeftSlot(left128);

/// @audit z
265:                 z.toRightSlot(int128(Math.max(right128, 0))).toLeftSlot(

[L-07] Revert on transfer to the zero address

It's good practice to revert a token transfer transaction if the recipient's address is the zero address. This can prevent unintentional transfers to the zero address due to accidental operations or programming errors. Many token contracts implement such a safeguard, such as OpenZeppelin - ERC20, OpenZeppelin - ERC721.

<details> <summary>There are 7 instances (click to show):</summary>
333:         return ERC20Minimal.transfer(recipient, amount);

352:         return ERC20Minimal.transferFrom(from, to, amount);

424:         SafeTransferLib.safeTransferFrom(
425:             s_underlyingToken,
426:             msg.sender,
427:             address(s_panopticPool),
428:             assets
429:         );

484:         SafeTransferLib.safeTransferFrom(
485:             s_underlyingToken,
486:             msg.sender,
487:             address(s_panopticPool),
488:             assets
489:         );

556:         SafeTransferLib.safeTransferFrom(
557:             s_underlyingToken,
558:             address(s_panopticPool),
559:             receiver,
560:             assets
561:         );

616:         SafeTransferLib.safeTransferFrom(
617:             s_underlyingToken,
618:             address(s_panopticPool),
619:             receiver,
620:             assets
621:         );
  • SemiFungiblePositionManager.sol ( 555-555 ):
555:         super.safeTransferFrom(from, to, id, amount, data);
</details>

[L-08] Written only contract variables

Write only contract variables are only written in the contract and are never read and cannot be accessed through an interface. It is recommended to check if there is a bug causing the missing readings, or if some view interfaces should be provided. If not, consider removing them to improve code clarity, avoid confusion, and save gas.

There is 1 instance:

  • CollateralTracker.sol ( 201-209 ):
201:             TICK_DEVIATION = uint256(
202:                 2230 +
203:                     (12500 * ratioTick) /
204:                     10_000 +
205:                     (7812 * ratioTick ** 2) /
206:                     10_000 ** 2 +
207:                     (6510 * ratioTick ** 3) /
208:                     10_000 ** 3
209:             );

[L-09] Downcasting other types to an address can cause collisions

Downcasting other types to an address will truncates the upper bytes, which means that multiple values can be mapped to an address, i.e. address collisions can occur.

There is 1 instance:

220:         if (address(bytes20(salt)) != msg.sender) revert Errors.InvalidSalt();

[L-10] Vulnerable versions of packages are being used

This project is using specific package versions which are vulnerable to the specific CVEs listed below. Consider switching to more recent versions of these packages that don't have these vulnerabilities.

  • CVE-2023-40014 - MEDIUM - (openzeppelin-solidity >=4.0.0 <4.9.3): OpenZeppelin Contracts is a library for secure smart contract development. Starting in version 4.0.0 and prior to version 4.9.3, contracts using ERC2771Context along with a custom trusted forwarder may see _msgSender return address(0) in calls that originate from the forwarder with calldata shorter than 20 bytes. This combination of circumstances does not appear to be common, in particular it is not the case for MinimalForwarder from OpenZeppelin Contracts, or any deployed forwarder the team is aware of, given that the signer address is appended to all calls that originate from these forwarders. The problem has been patched in v4.9.3.

  • CVE-2023-34459 - MEDIUM - (openzeppelin-solidity >=4.7.0 <4.9.2): OpenZeppelin Contracts is a library for smart contract development. Starting in version 4.7.0 and prior to version 4.9.2, when the verifyMultiProof, verifyMultiProofCalldata, procesprocessMultiProof, or processMultiProofCalldat functions are in use, it is possible to construct merkle trees that allow forging a valid multiproof for an arbitrary set of leaves. A contract may be vulnerable if it uses multiproofs for verification and the merkle tree that is processed includes a node with value 0 at depth 1 (just under the root). This could happen inadvertedly for balanced trees with 3 leaves or less, if the leaves are not hashed. This could happen deliberately if a malicious tree builder includes such a node in the tree. A contract is not vulnerable if it uses single-leaf proving (verify, verifyCalldata, processProof, or processProofCalldata), or if it uses multiproofs with a known tree that has hashed leaves. Standard merkle trees produced or validated with the @openzeppelin/merkle-tree library are safe. The problem has been patched in version 4.9.2. Some workarounds are available. For those using multiproofs: When constructing merkle trees hash the leaves and do not insert empty nodes in your trees. Using the @openzeppelin/merkle-tree package eliminates this issue. Do not accept user-provided merkle roots without reconstructing at least the first level of the tree. Verify the merkle tree structure by reconstructing it from the leaves.

There are 2 instances:

  • Global finding

[L-11] Constructor / initialization function lacks parameter validation

Constructors and initialization functions play a critical role in contracts by setting important initial states when the contract is first deployed before the system starts. The parameters passed to the constructor and initialization functions directly affect the behavior of the contract / protocol. If incorrect parameters are provided, the system may fail to run, behave abnormally, be unstable, or lack security. Therefore, it's crucial to carefully check each parameter in the constructor and initialization functions. If an exception is found, the transaction should be rolled back.

There are 7 instances:

  • CollateralTracker.sol ( 178-186 ):
/// @audit `_commissionFee`
/// @audit `_sellerCollateralRatio`
/// @audit `_buyerCollateralRatio`
/// @audit `_forceExerciseCost`
/// @audit `_targetPoolUtilization`
/// @audit `_saturatedPoolUtilization`
/// @audit `_ITMSpreadMultiplier`
178:     constructor(
179:         uint256 _commissionFee,
180:         uint256 _sellerCollateralRatio,
181:         uint256 _buyerCollateralRatio,
182:         int256 _forceExerciseCost,
183:         uint256 _targetPoolUtilization,
184:         uint256 _saturatedPoolUtilization,
185:         uint256 _ITMSpreadMultiplier
186:     ) {

[L-12] Minting in a loop may lead to a DOS

The signature used seems to require all the minting operations to be done at once. If there are a lot of them, there may be too many to mint in one block.

<details> <summary>There are 2 instances (click to show):</summary>
/// @audit mint by _burnOptions()
802:         for (uint256 i = 0; i < positionIdList.length; ) {
803:             LeftRightSigned paidAmounts;
804:             (paidAmounts, premiasByLeg[i]) = _burnOptions(
805:                 commitLongSettled,
806:                 positionIdList[i],
807:                 owner,
808:                 tickLimitLow,
809:                 tickLimitHigh
810:             );
811:             netPaid = netPaid.add(paidAmounts);
812:             unchecked {
813:                 ++i;
814:             }
815:         }
  • SemiFungiblePositionManager.sol ( 882-934 ):
/// @audit mint by _createLegInAMM()
882:         for (uint256 leg = 0; leg < numLegs; ) {
883:             LeftRightSigned _moved;
884:             LeftRightSigned _itmAmounts;
885:             LeftRightUnsigned _collectedSingleLeg;
886: 
887:             {
888:                 // cache the univ3pool, tokenId, isBurn, and _positionSize variables to get rid of stack too deep error
889:                 IUniswapV3Pool _univ3pool = univ3pool;
890:                 TokenId _tokenId = tokenId;
891:                 bool _isBurn = isBurn;
892:                 uint128 _positionSize = positionSize;
893:                 uint256 _leg;
894: 
895:                 unchecked {
896:                     // Reverse the order of the legs if this call is burning a position (LIFO)
897:                     // We loop in reverse order if burning a position so that any dependent long liquidity is returned to the pool first,
898:                     // allowing the corresponding short liquidity to be removed
899:                     _leg = _isBurn ? numLegs - leg - 1 : leg;
900:                 }
901: 
902:                 // for this _leg index: extract the liquidity chunk: a 256bit word containing the liquidity amount and upper/lower tick
903:                 // @dev see `contracts/types/LiquidityChunk.sol`
904:                 LiquidityChunk liquidityChunk = PanopticMath.getLiquidityChunk(
905:                     _tokenId,
906:                     _leg,
...... OMITTED ......
910:                 (_moved, _itmAmounts, _collectedSingleLeg) = _createLegInAMM(
911:                     _univ3pool,
912:                     _tokenId,
913:                     _leg,
914:                     liquidityChunk,
915:                     _isBurn
916:                 );
917: 
918:                 collectedByLeg[_leg] = _collectedSingleLeg;
919: 
920:                 unchecked {
921:                     // increment accumulators of the upper bound on tokens contained across all legs of the position at any given tick
922:                     amount0 += Math.getAmount0ForLiquidity(liquidityChunk);
923: 
924:                     amount1 += Math.getAmount1ForLiquidity(liquidityChunk);
925:                 }
926:             }
927: 
928:             totalMoved = totalMoved.add(_moved);
929:             itmAmounts = itmAmounts.add(_itmAmounts);
930: 
931:             unchecked {
932:                 ++leg;
933:             }
934:         }
</details>

[L-13] Contracts are vulnerable to fee-on-transfer accounting-related issues

Some tokens take a transfer fee (e.g. STA, PAXG), some do not currently charge a fee but may do so in the future (e.g. USDT, USDC). The functions below transfer funds from the caller to the receiver via transferFrom(), but do not ensure that the actual number of tokens received is the same as the input amount to the transfer. If the token is a fee-on-transfer token, the balance after the transfer will be smaller than expected, leading to accounting issues. Even if there are checks later, related to a secondary transfer, an attacker may be able to use latent funds (e.g. mistakenly sent by another user) in order to get a free credit. One way to solve this problem is to measure the balance before and after the transfer, and use the difference as the amount, rather than the stated amount.

<details> <summary>There are 7 instances (click to show):</summary>
333:         return ERC20Minimal.transfer(recipient, amount);

352:         return ERC20Minimal.transferFrom(from, to, amount);
182:             SafeTransferLib.safeTransferFrom(
183:                 decoded.poolFeatures.token0,
184:                 decoded.payer,
185:                 msg.sender,
186:                 amount0Owed
187:             );

189:             SafeTransferLib.safeTransferFrom(
190:                 decoded.poolFeatures.token1,
191:                 decoded.payer,
192:                 msg.sender,
193:                 amount1Owed
194:             );
413:             SafeTransferLib.safeTransferFrom(
414:                 decoded.poolFeatures.token0,
415:                 decoded.payer,
416:                 msg.sender,
417:                 amount0Owed
418:             );

420:             SafeTransferLib.safeTransferFrom(
421:                 decoded.poolFeatures.token1,
422:                 decoded.payer,
423:                 msg.sender,
424:                 amount1Owed
425:             );

456:         SafeTransferLib.safeTransferFrom(token, decoded.payer, msg.sender, amountToPay);
</details>

[L-14] Functions calling contracts/addresses with transfer hooks should be protected by reentrancy guard

Even if the function follows the best practice of check-effects-interaction, not using a reentrancy guard when there may be transfer hooks opens the users of this protocol up to read-only reentrancy vulnerability with no way to protect them except by block-listing the entire protocol.

<details> <summary>There are 11 instances (click to show):</summary>
333:         return ERC20Minimal.transfer(recipient, amount);

352:         return ERC20Minimal.transferFrom(from, to, amount);

424:         SafeTransferLib.safeTransferFrom(
425:             s_underlyingToken,
426:             msg.sender,
427:             address(s_panopticPool),
428:             assets
429:         );

484:         SafeTransferLib.safeTransferFrom(
485:             s_underlyingToken,
486:             msg.sender,
487:             address(s_panopticPool),
488:             assets
489:         );

556:         SafeTransferLib.safeTransferFrom(
557:             s_underlyingToken,
558:             address(s_panopticPool),
559:             receiver,
560:             assets
561:         );

616:         SafeTransferLib.safeTransferFrom(
617:             s_underlyingToken,
618:             address(s_panopticPool),
619:             receiver,
620:             assets
621:         );
182:             SafeTransferLib.safeTransferFrom(
183:                 decoded.poolFeatures.token0,
184:                 decoded.payer,
185:                 msg.sender,
186:                 amount0Owed
187:             );

189:             SafeTransferLib.safeTransferFrom(
190:                 decoded.poolFeatures.token1,
191:                 decoded.payer,
192:                 msg.sender,
193:                 amount1Owed
194:             );
413:             SafeTransferLib.safeTransferFrom(
414:                 decoded.poolFeatures.token0,
415:                 decoded.payer,
416:                 msg.sender,
417:                 amount0Owed
418:             );

420:             SafeTransferLib.safeTransferFrom(
421:                 decoded.poolFeatures.token1,
422:                 decoded.payer,
423:                 msg.sender,
424:                 amount1Owed
425:             );

456:         SafeTransferLib.safeTransferFrom(token, decoded.payer, msg.sender, amountToPay);
</details>

[L-15] Critical functions should be controlled by time locks

It is a good practice to give time for users to react and adjust to critical changes. A timelock provides more guarantees and reduces the level of trust required, thus decreasing risk for users. It also indicates that the project is legitimate (less risk of a malicious owner making a sandwich attack on a user).

<details> <summary>There are 7 instances (click to show):</summary>
221:     function startToken(
222:         bool underlyingIsToken0,
223:         address token0,
224:         address token1,
225:         uint24 fee,
226:         PanopticPool panopticPool
227:     ) external {

995:     function takeCommissionAddData(
996:         address optionOwner,
997:         int128 longAmount,
998:         int128 shortAmount,
999:         int128 swappedAmount
1000:     ) external onlyPanopticPool returns (int256 utilization) {
291:     function startPool(
292:         IUniswapV3Pool _univ3pool,
293:         address token0,
294:         address token1,
295:         CollateralTracker collateralTracker0,
296:         CollateralTracker collateralTracker1
297:     ) external {

859:     function _updatePositionDataBurn(address owner, TokenId tokenId) internal {

1405:     function _updatePositionsHash(address account, TokenId tokenId, bool addFlag) internal {

1833:     function _updateSettlementPostBurn(
1834:         address owner,
1835:         TokenId tokenId,
1836:         LeftRightUnsigned[4] memory collectedByLeg,
1837:         uint128 positionSize,
1838:         bool commitLongSettled
1839:     ) internal returns (LeftRightSigned realizedPremia, LeftRightSigned[4] memory premiaByLeg) {
  • ERC1155Minimal.sol ( 81-81 ):
81:     function setApprovalForAll(address operator, bool approved) public {
</details>

[L-16] Tokens may be minted to the zero address

Neither the listed functions, nor _mint() prevent minting to address(0x0)

<details> <summary>There are 13 instances (click to show):</summary>
432:         _mint(receiver, shares);

477:     function mint(uint256 shares, address receiver) external returns (uint256 assets) {

954:             _mint(
955:                 delegator,
956:                 Math.mulDiv(
957:                     assets,
958:                     totalSupply - delegateeBalance,
959:                     uint256(Math.max(1, int256(totalAssets()) - int256(assets)))
960:                 ) - delegateeBalance
961:             );

1021:                 _mint(optionOwner, sharesToMint);

1078:                 _mint(optionOwner, sharesToMint);
335:     function _mintFullRange(
336:         IUniswapV3Pool v3Pool,
337:         address token0,
338:         address token1,
339:         uint24 fee
340:     ) internal returns (uint256, uint256) {
547:     function mintOptions(
548:         TokenId[] calldata positionIdList,
549:         uint128 positionSize,
550:         uint64 effectiveLiquidityLimitX32,
551:         int24 tickLimitLow,
552:         int24 tickLimitHigh
553:     ) external {

614:     function _mintOptions(
615:         TokenId[] calldata positionIdList,
616:         uint128 positionSize,
617:         uint64 effectiveLiquidityLimitX32,
618:         int24 tickLimitLow,
619:         int24 tickLimitHigh
620:     ) internal {

677:     function _mintInSFPMAndUpdateCollateral(
678:         TokenId tokenId,
679:         uint128 positionSize,
680:         int24 tickLimitLow,
681:         int24 tickLimitHigh
682:     ) internal returns (uint128) {
504:     function mintTokenizedPosition(
505:         TokenId tokenId,
506:         uint128 positionSize,
507:         int24 slippageTickLimitLow,
508:         int24 slippageTickLimitHigh
509:     )
510:         external
511:         ReentrancyLock(tokenId.poolId())
512:         returns (LeftRightUnsigned[4] memory collectedByLeg, LeftRightSigned totalSwapped)

1067:                 ? _mintLiquidity(liquidityChunk, univ3pool)

1185:     function _mintLiquidity(
1186:         LiquidityChunk liquidityChunk,
1187:         IUniswapV3Pool univ3pool
1188:     ) internal returns (LeftRightSigned movedAmounts) {
122:     function _mint(address to, uint256 amount) internal {
</details>

[L-17] Code does not follow the best practice of check-effects-interaction

Code should follow the best-practice of check-effects-interaction, where state variables are updated before any external calls are made. Doing so prevents a large class of reentrancy bugs.

<details> <summary>There are 39 instances (click to show):</summary>
/// @audit `safeTransferFrom()` is called on line 424
436:             s_poolAssets += uint128(assets);

/// @audit `safeTransferFrom()` is called on line 484
496:             s_poolAssets += uint128(assets);
/// @audit `initializeAMMPool()` is called on line 233
237:         newPoolContract = PanopticPool(POOL_REFERENCE.cloneDeterministic(salt));

/// @audit `initializeAMMPool()` is called on line 233
253:         s_getPanopticPool[v3Pool] = newPoolContract;
/// @audit `_mintInSFPMAndUpdateCollateral()` is called on line 642, triggering an external call - `mintTokenizedPosition()`
654:         s_positionBalance[msg.sender][tokenId] = LeftRightUnsigned
655:             .wrap(0)
656:             .toLeftSlot(poolUtilizations)
657:             .toRightSlot(positionSize);

/// @audit `_mintInSFPMAndUpdateCollateral()` is called on line 642, triggering an external call - `mintTokenizedPosition()`
664:         if (medianData != 0) s_miniMedian = medianData;

/// @audit `_burnOptions()` is called on line 804, triggering an external call - `burnTokenizedPosition()`
811:             netPaid = netPaid.add(paidAmounts);

/// @audit `_burnOptions()` is called on line 804, triggering an external call - `burnTokenizedPosition()`
813:                 ++i;

/// @audit `burnTokenizedPosition()` is called on line 970
973:         (realizedPremia, premiaByLeg) = _updateSettlementPostBurn(
974:             owner,
975:             tokenId,
976:             collectedByLeg,
977:             positionSize,
978:             commitLongSettled
979:         );

/// @audit `burnTokenizedPosition()` is called on line 970
992:             paidAmounts = paidAmounts.toRightSlot(paid0);

/// @audit `burnTokenizedPosition()` is called on line 970
1003:             paidAmounts = paidAmounts.toLeftSlot(paid1);

/// @audit `getUniV3TWAP()` is called on line 1026, triggering an external call - `observe()`
1086:             (netExchanged, premiasByLeg) = _burnAllOptionsFrom(
1087:                 liquidatee,
1088:                 Constants.MIN_V3POOL_TICK,
1089:                 Constants.MAX_V3POOL_TICK,
1090:                 DONOT_COMMIT_LONG_SETTLED,
1091:                 positionIdList
1092:             );

/// @audit `getUniV3TWAP()` is called on line 1026, triggering an external call - `observe()`
1094:             (, finalTick, , , , , ) = s_univ3pool.slot0();

/// @audit `getUniV3TWAP()` is called on line 1026, triggering an external call - `observe()`
1098:             (liquidationBonus0, liquidationBonus1, collateralRemaining) = PanopticMath
1099:                 .getLiquidationBonus(
1100:                     tokenData0,
1101:                     tokenData1,
1102:                     Math.getSqrtRatioAtTick(twapTick),
1103:                     Math.getSqrtRatioAtTick(finalTick),
1104:                     netExchanged,
1105:                     premia
1106:                 );

/// @audit `getUniV3TWAP()` is called on line 1026, triggering an external call - `observe()`
1122:             (deltaBonus0, deltaBonus1) = PanopticMath.haircutPremia(
1123:                 _liquidatee,
1124:                 _positionIdList,
1125:                 premiasByLeg,
1126:                 collateralRemaining,
1127:                 s_collateralToken0,
1128:                 s_collateralToken1,
1129:                 Math.getSqrtRatioAtTick(_finalTick),
1130:                 s_settledTokens
1131:             );

/// @audit `getUniV3TWAP()` is called on line 1026, triggering an external call - `observe()`
1134:                 liquidationBonus0 += deltaBonus0;

/// @audit `getUniV3TWAP()` is called on line 1026, triggering an external call - `observe()`
1135:                 liquidationBonus1 += deltaBonus1;

/// @audit `getUniV3TWAP()` is called on line 1200, triggering an external call - `observe()`
1242:         refundAmounts = PanopticMath.getRefundAmounts(
1243:             account,
1244:             refundAmounts,
1245:             twapTick,
1246:             s_collateralToken0,
1247:             s_collateralToken1
1248:         );

/// @audit `slot0()` is called on line 1598
1650:             s_settledTokens[chunkKey] = s_settledTokens[chunkKey].add(
1651:                 LeftRightUnsigned.wrap(uint256(LeftRightSigned.unwrap(realizedPremia)))
1652:             );
/// @audit `_mint()` is called on line 515, triggering an external call - `onERC1155Received()`
520:         (collectedByLeg, totalSwapped) = _validateAndForwardToAMM(
521:             tokenId,
522:             positionSize,
523:             slippageTickLimitLow,
524:             slippageTickLimitHigh,
525:             MINT
526:         );

/// @audit `slot0()` is called on line 725
721:             (tickLimitLow, tickLimitHigh) = (tickLimitHigh, tickLimitLow);

/// @audit `swap()` is called on line 837
848:             totalSwapped = LeftRightSigned.wrap(0).toRightSlot(swap0.toInt128()).toLeftSlot(
849:                 swap1.toInt128()
850:             );

/// @audit `_createLegInAMM()` is called on line 910, triggering an external call - `positions()`
918:                 collectedByLeg[_leg] = _collectedSingleLeg;

/// @audit `_createLegInAMM()` is called on line 910, triggering an external call - `positions()`
922:                     amount0 += Math.getAmount0ForLiquidity(liquidityChunk);

/// @audit `_createLegInAMM()` is called on line 910, triggering an external call - `positions()`
924:                     amount1 += Math.getAmount1ForLiquidity(liquidityChunk);

/// @audit `_createLegInAMM()` is called on line 910, triggering an external call - `positions()`
928:             totalMoved = totalMoved.add(_moved);

/// @audit `_createLegInAMM()` is called on line 910, triggering an external call - `positions()`
929:             itmAmounts = itmAmounts.add(_itmAmounts);

/// @audit `_createLegInAMM()` is called on line 910, triggering an external call - `positions()`
932:                 ++leg;

/// @audit `_getFeesBase()` is called on line 1098, triggering an external call - `positions()`
1075:                 itmAmounts = itmAmounts.toRightSlot(moved.rightSlot());

/// @audit `_getFeesBase()` is called on line 1098, triggering an external call - `positions()`
1080:                 itmAmounts = itmAmounts.toLeftSlot(moved.leftSlot());

/// @audit `_getFeesBase()` is called on line 1098, triggering an external call - `positions()`
1086:             collectedSingleLeg = _collectAndWritePositionData(
1087:                 liquidityChunk,
1088:                 univ3pool,
1089:                 currentLiquidity,
1090:                 positionKey,
1091:                 moved,
1092:                 isLong
1093:             );

/// @audit `_getFeesBase()` is called on line 1098, triggering an external call - `positions()`
1098:         s_accountFeesBase[positionKey] = _getFeesBase(
1099:             univ3pool,
1100:             updatedLiquidity,
1101:             liquidityChunk,
1102:             true
1103:         );

/// @audit `mint()` is called on line 1203
1214:         movedAmounts = LeftRightSigned.wrap(0).toRightSlot(int128(int256(amount0))).toLeftSlot(
1215:             int128(int256(amount1))
1216:         );

/// @audit `burn()` is called on line 1230
1241:             movedAmounts = LeftRightSigned.wrap(0).toRightSlot(-int128(int256(amount0))).toLeftSlot(
1242:                 -int128(int256(amount1))
1243:             );

/// @audit `_getFeesBase()` is called on line 1268, triggering an external call - `positions()`
1296:                 collected0 = movedInLeg.rightSlot() < 0
1297:                     ? receivedAmount0 - uint128(-movedInLeg.rightSlot())
1298:                     : receivedAmount0;

/// @audit `_getFeesBase()` is called on line 1268, triggering an external call - `positions()`
1299:                 collected1 = movedInLeg.leftSlot() < 0
1300:                     ? receivedAmount1 - uint128(-movedInLeg.leftSlot())
1301:                     : receivedAmount1;

/// @audit `_getFeesBase()` is called on line 1268, triggering an external call - `positions()`
1306:             collectedChunk = LeftRightUnsigned.wrap(0).toRightSlot(collected0).toLeftSlot(
1307:                 collected1
1308:             );
/// @audit `delegatecall()` is called on line 15
30:             results[i] = result;

/// @audit `delegatecall()` is called on line 15
33:                 ++i;
</details>

Non Critical Issues

[N-01] Visibility of state variables is not explicitly defined

To avoid misunderstandings and unexpected state accesses, it is recommended to explicitly define the visibility of each state variable.

There are 8 instances:

131:     uint256 immutable TICK_DEVIATION;

136:     uint256 immutable COMMISSION_FEE;

141:     uint256 immutable SELLER_COLLATERAL_RATIO;

145:     uint256 immutable BUYER_COLLATERAL_RATIO;

149:     int256 immutable FORCE_EXERCISE_COST;

154:     uint256 immutable TARGET_POOL_UTIL;

158:     uint256 immutable SATURATED_POOL_UTIL;

162:     uint256 immutable ITM_SPREAD_MULTIPLIER;

[N-02] Names of private/internal functions should be prefixed with an underscore

It is recommended by the Solidity Style Guide.

<details> <summary>There are 104 instances (click to show):</summary>
1450:     function getUniV3TWAP() internal view returns (int24 twapTick) {
320:     function beginReentrancyLock(uint64 poolId) internal {

330:     function endReentrancyLock(uint64 poolId) internal {

593:     function registerTokenTransfer(address from, address to, TokenId id, uint256 amount) internal {

756:     function swapInAMM(
757:         IUniswapV3Pool univ3pool,
758:         LeftRightSigned itmAmounts
759:     ) internal returns (LeftRightSigned totalSwapped) {
  • CallbackLib.sol ( 30-34 ):
30:     function validateCallback(
31:         address sender,
32:         IUniswapV3Factory factory,
33:         PoolFeatures memory features
34:     ) internal view {
25:     function min24(int24 a, int24 b) internal pure returns (int24) {

33:     function max24(int24 a, int24 b) internal pure returns (int24) {

41:     function min(uint256 a, uint256 b) internal pure returns (uint256) {

49:     function min(int256 a, int256 b) internal pure returns (int256) {

57:     function max(uint256 a, uint256 b) internal pure returns (uint256) {

65:     function max(int256 a, int256 b) internal pure returns (int256) {

73:     function abs(int256 x) internal pure returns (int256) {

81:     function absUint(int256 x) internal pure returns (uint256) {

91:     function mostSignificantNibble(uint160 x) internal pure returns (uint256 r) {

128:     function getSqrtRatioAtTick(int24 tick) internal pure returns (uint160) {

191:     function getAmount0ForLiquidity(LiquidityChunk liquidityChunk) internal pure returns (uint256) {

207:     function getAmount1ForLiquidity(LiquidityChunk liquidityChunk) internal pure returns (uint256) {

221:     function getAmountsForLiquidity(
222:         int24 currentTick,
223:         LiquidityChunk liquidityChunk
224:     ) internal pure returns (uint256 amount0, uint256 amount1) {

241:     function getLiquidityForAmount0(
242:         int24 tickLower,
243:         int24 tickUpper,
244:         uint256 amount0
245:     ) internal pure returns (LiquidityChunk) {

271:     function getLiquidityForAmount1(
272:         int24 tickLower,
273:         int24 tickUpper,
274:         uint256 amount1
275:     ) internal pure returns (LiquidityChunk) {

296:     function toUint128(uint256 toDowncast) internal pure returns (uint128 downcastedInt) {

302:     function toUint128Capped(uint256 toDowncast) internal pure returns (uint128 downcastedInt) {

311:     function toInt128(uint128 toCast) internal pure returns (int128 downcastedInt) {

318:     function toInt128(int256 toCast) internal pure returns (int128 downcastedInt) {

325:     function toInt256(uint256 toCast) internal pure returns (int256) {

340:     function mulDiv(
341:         uint256 a,
342:         uint256 b,
343:         uint256 denominator
344:     ) internal pure returns (uint256 result) {

440:     function mulDivRoundingUp(
441:         uint256 a,
442:         uint256 b,
443:         uint256 denominator
444:     ) internal pure returns (uint256 result) {

458:     function mulDiv64(uint256 a, uint256 b) internal pure returns (uint256) {

521:     function mulDiv96(uint256 a, uint256 b) internal pure returns (uint256) {

584:     function mulDiv96RoundingUp(uint256 a, uint256 b) internal pure returns (uint256 result) {

598:     function mulDiv128(uint256 a, uint256 b) internal pure returns (uint256) {

661:     function mulDiv128RoundingUp(uint256 a, uint256 b) internal pure returns (uint256 result) {

675:     function mulDiv192(uint256 a, uint256 b) internal pure returns (uint256) {

738:     function unsafeDivRoundingUp(uint256 a, uint256 b) internal pure returns (uint256 result) {

753:     function quickSort(int256[] memory arr, int256 left, int256 right) internal pure {

776:     function sort(int256[] memory data) internal pure returns (int256[] memory) {
47:     function getPoolId(address univ3pool) internal view returns (uint64) {

59:     function incrementPoolPattern(uint64 poolId) internal pure returns (uint64) {

92:     function updatePositionsHash(
93:         uint256 existingHash,
94:         TokenId tokenId,
95:         bool addFlag
96:     ) internal pure returns (uint256) {

289:     function getLiquidityChunk(
290:         TokenId tokenId,
291:         uint256 legIndex,
292:         uint128 positionSize
293:     ) internal pure returns (LiquidityChunk) {

338:     function getTicks(
339:         int24 strike,
340:         int24 width,
341:         int24 tickSpacing
342:     ) internal pure returns (int24 tickLower, int24 tickUpper) {

371:     function getRangesFromStrike(
372:         int24 width,
373:         int24 tickSpacing
374:     ) internal pure returns (int24, int24) {

390:     function computeExercisedAmounts(
391:         TokenId tokenId,
392:         uint128 positionSize
393:     ) internal pure returns (LeftRightSigned longAmounts, LeftRightSigned shortAmounts) {

419:     function convertCollateralData(
420:         LeftRightUnsigned tokenData0,
421:         LeftRightUnsigned tokenData1,
422:         uint256 tokenType,
423:         uint160 sqrtPriceX96
424:     ) internal pure returns (uint256, uint256) {

445:     function convertCollateralData(
446:         LeftRightUnsigned tokenData0,
447:         LeftRightUnsigned tokenData1,
448:         uint256 tokenType,
449:         int24 tick
450:     ) internal pure returns (uint256, uint256) {

468:     function convertNotional(
469:         uint128 contractSize,
470:         int24 tickLower,
471:         int24 tickUpper,
472:         uint256 asset
473:     ) internal pure returns (uint128) {

490:     function convert0to1(uint256 amount, uint160 sqrtPriceX96) internal pure returns (uint256) {

507:     function convert1to0(uint256 amount, uint160 sqrtPriceX96) internal pure returns (uint256) {

524:     function convert0to1(int256 amount, uint160 sqrtPriceX96) internal pure returns (int256) {

547:     function convert1to0(int256 amount, uint160 sqrtPriceX96) internal pure returns (int256) {

574:     function getAmountsMoved(
575:         TokenId tokenId,
576:         uint128 positionSize,
577:         uint256 legIndex
578:     ) internal pure returns (LeftRightUnsigned) {
21:     function safeTransferFrom(address token, address from, address to, uint256 amount) internal {

52:     function safeTransfer(address token, address to, uint256 amount) internal {
39:     function rightSlot(LeftRightUnsigned self) internal pure returns (uint128) {

46:     function rightSlot(LeftRightSigned self) internal pure returns (int128) {

59:     function toRightSlot(
60:         LeftRightUnsigned self,
61:         uint128 right
62:     ) internal pure returns (LeftRightUnsigned) {

78:     function toRightSlot(
79:         LeftRightSigned self,
80:         int128 right
81:     ) internal pure returns (LeftRightSigned) {

101:     function leftSlot(LeftRightUnsigned self) internal pure returns (uint128) {

108:     function leftSlot(LeftRightSigned self) internal pure returns (int128) {

121:     function toLeftSlot(
122:         LeftRightUnsigned self,
123:         uint128 left
124:     ) internal pure returns (LeftRightUnsigned) {

134:     function toLeftSlot(LeftRightSigned self, int128 left) internal pure returns (LeftRightSigned) {

148:     function add(
149:         LeftRightUnsigned x,
150:         LeftRightUnsigned y
151:     ) internal pure returns (LeftRightUnsigned z) {

171:     function sub(
172:         LeftRightUnsigned x,
173:         LeftRightUnsigned y
174:     ) internal pure returns (LeftRightUnsigned z) {

194:     function add(LeftRightUnsigned x, LeftRightSigned y) internal pure returns (LeftRightSigned z) {

214:     function add(LeftRightSigned x, LeftRightSigned y) internal pure returns (LeftRightSigned z) {

232:     function sub(LeftRightSigned x, LeftRightSigned y) internal pure returns (LeftRightSigned z) {

251:     function subRect(
252:         LeftRightSigned x,
253:         LeftRightSigned y
254:     ) internal pure returns (LeftRightSigned z) {

279:     function addCapped(
280:         LeftRightUnsigned x,
281:         LeftRightUnsigned dx,
282:         LeftRightUnsigned y,
283:         LeftRightUnsigned dy
284:     ) internal pure returns (LeftRightUnsigned, LeftRightUnsigned) {
70:     function createChunk(
71:         int24 _tickLower,
72:         int24 _tickUpper,
73:         uint128 amount
74:     ) internal pure returns (LiquidityChunk) {

89:     function addLiquidity(
90:         LiquidityChunk self,
91:         uint128 amount
92:     ) internal pure returns (LiquidityChunk) {

102:     function addTickLower(
103:         LiquidityChunk self,
104:         int24 _tickLower
105:     ) internal pure returns (LiquidityChunk) {

118:     function addTickUpper(
119:         LiquidityChunk self,
120:         int24 _tickUpper
121:     ) internal pure returns (LiquidityChunk) {

135:     function updateTickLower(
136:         LiquidityChunk self,
137:         int24 _tickLower
138:     ) internal pure returns (LiquidityChunk) {

151:     function updateTickUpper(
152:         LiquidityChunk self,
153:         int24 _tickUpper
154:     ) internal pure returns (LiquidityChunk) {

171:     function tickLower(LiquidityChunk self) internal pure returns (int24) {

180:     function tickUpper(LiquidityChunk self) internal pure returns (int24) {

189:     function liquidity(LiquidityChunk self) internal pure returns (uint128) {
87:     function poolId(TokenId self) internal pure returns (uint64) {

96:     function tickSpacing(TokenId self) internal pure returns (int24) {

108:     function asset(TokenId self, uint256 legIndex) internal pure returns (uint256) {

118:     function optionRatio(TokenId self, uint256 legIndex) internal pure returns (uint256) {

128:     function isLong(TokenId self, uint256 legIndex) internal pure returns (uint256) {

138:     function tokenType(TokenId self, uint256 legIndex) internal pure returns (uint256) {

148:     function riskPartner(TokenId self, uint256 legIndex) internal pure returns (uint256) {

158:     function strike(TokenId self, uint256 legIndex) internal pure returns (int24) {

169:     function width(TokenId self, uint256 legIndex) internal pure returns (int24) {

183:     function addPoolId(TokenId self, uint64 _poolId) internal pure returns (TokenId) {

193:     function addTickSpacing(TokenId self, int24 _tickSpacing) internal pure returns (TokenId) {

205:     function addAsset(
206:         TokenId self,
207:         uint256 _asset,
208:         uint256 legIndex
209:     ) internal pure returns (TokenId) {

221:     function addOptionRatio(
222:         TokenId self,
223:         uint256 _optionRatio,
224:         uint256 legIndex
225:     ) internal pure returns (TokenId) {

240:     function addIsLong(
241:         TokenId self,
242:         uint256 _isLong,
243:         uint256 legIndex
244:     ) internal pure returns (TokenId) {

255:     function addTokenType(
256:         TokenId self,
257:         uint256 _tokenType,
258:         uint256 legIndex
259:     ) internal pure returns (TokenId) {

273:     function addRiskPartner(
274:         TokenId self,
275:         uint256 _riskPartner,
276:         uint256 legIndex
277:     ) internal pure returns (TokenId) {

291:     function addStrike(
292:         TokenId self,
293:         int24 _strike,
294:         uint256 legIndex
295:     ) internal pure returns (TokenId) {

310:     function addWidth(
311:         TokenId self,
312:         int24 _width,
313:         uint256 legIndex
314:     ) internal pure returns (TokenId) {

336:     function addLeg(
337:         TokenId self,
338:         uint256 legIndex,
339:         uint256 _optionRatio,
340:         uint256 _asset,
341:         uint256 _isLong,
342:         uint256 _tokenType,
343:         uint256 _riskPartner,
344:         int24 _strike,
345:         int24 _width
346:     ) internal pure returns (TokenId tokenId) {

366:     function flipToBurnToken(TokenId self) internal pure returns (TokenId) {

404:     function countLongs(TokenId self) internal pure returns (uint256) {

416:     function asTicks(
417:         TokenId self,
418:         uint256 legIndex
419:     ) internal pure returns (int24 legLowerTick, int24 legUpperTick) {

432:     function countLegs(TokenId self) internal pure returns (uint256) {

464:     function clearLeg(TokenId self, uint256 i) internal pure returns (TokenId) {

500:     function validate(TokenId self) internal pure {

578:     function validateIsExercisable(TokenId self, int24 currentTick) internal pure {
</details>

[N-03] Names of private/internal state variables should be prefixed with an underscore

It is recommended by the Solidity Style Guide.

<details> <summary>There are 75 instances (click to show):</summary>
70:     string internal constant TICKER_PREFIX = "po";

73:     string internal constant NAME_PREFIX = "POPT-V1";

77:     uint256 internal constant DECIMALS = 10_000;

81:     int128 internal constant DECIMALS_128 = 10_000;

89:     address internal s_underlyingToken;

93:     bool internal s_initialized;

96:     address internal s_univ3token0;

99:     address internal s_univ3token1;

102:     bool internal s_underlyingIsToken0;

109:     PanopticPool internal s_panopticPool;

112:     uint128 internal s_poolAssets;

115:     uint128 internal s_inAMM;

121:     uint128 internal s_ITMSpreadFee;

124:     uint24 internal s_poolFee;

131:     uint256 immutable TICK_DEVIATION;

136:     uint256 immutable COMMISSION_FEE;

141:     uint256 immutable SELLER_COLLATERAL_RATIO;

145:     uint256 immutable BUYER_COLLATERAL_RATIO;

149:     int256 immutable FORCE_EXERCISE_COST;

154:     uint256 immutable TARGET_POOL_UTIL;

158:     uint256 immutable SATURATED_POOL_UTIL;

162:     uint256 immutable ITM_SPREAD_MULTIPLIER;
63:     IUniswapV3Factory internal immutable UNIV3_FACTORY;

66:     SemiFungiblePositionManager internal immutable SFPM;

69:     IDonorNFT internal immutable DONOR_NFT;

72:     address internal immutable POOL_REFERENCE;

75:     address internal immutable COLLATERAL_REFERENCE;

78:     address internal immutable WETH;

82:     uint256 internal constant FULL_RANGE_LIQUIDITY_AMOUNT_WETH = 0.1 ether;

86:     uint256 internal constant FULL_RANGE_LIQUIDITY_AMOUNT_TOKEN = 1e6;

89:     uint16 internal constant CARDINALITY_INCREASE = 100;

96:     address internal s_owner;

99:     bool internal s_initialized;

102:     mapping(IUniswapV3Pool univ3pool => PanopticPool panopticPool) internal s_getPanopticPool;
103:     int24 internal constant MIN_SWAP_TICK = Constants.MIN_V3POOL_TICK + 1;

105:     int24 internal constant MAX_SWAP_TICK = Constants.MAX_V3POOL_TICK - 1;

109:     bool internal constant COMPUTE_ALL_PREMIA = true;

111:     bool internal constant COMPUTE_LONG_PREMIA = false;

114:     bool internal constant ONLY_AVAILABLE_PREMIUM = false;

119:     bool internal constant COMMIT_LONG_SETTLED = true;

120:     bool internal constant DONOT_COMMIT_LONG_SETTLED = false;

123:     bool internal constant ADD = true;

128:     uint32 internal constant TWAP_WINDOW = 600;

133:     bool internal constant SLOW_ORACLE_UNISWAP_MODE = false;

136:     uint256 internal constant MEDIAN_PERIOD = 60;

139:     uint256 internal constant FAST_ORACLE_CARDINALITY = 3;

145:     uint256 internal constant FAST_ORACLE_PERIOD = 1;

148:     uint256 internal constant SLOW_ORACLE_CARDINALITY = 7;

152:     uint256 internal constant SLOW_ORACLE_PERIOD = 5;

156:     int256 internal constant MAX_TWAP_DELTA_LIQUIDATION = 513;

160:     int256 internal constant MAX_SLOW_FAST_DELTA = 1800;

165:     uint64 internal constant MAX_SPREAD = 9 * (2 ** 32);

168:     uint64 internal constant MAX_POSITIONS = 32;

171:     uint256 internal constant BP_DECREASE_BUFFER = 13_333;

174:     uint256 internal constant NO_BUFFER = 10_000;

179:     SemiFungiblePositionManager internal immutable SFPM;

186:     IUniswapV3Pool internal s_univ3pool;

225:     uint256 internal s_miniMedian;

231:     CollateralTracker internal s_collateralToken0;

233:     CollateralTracker internal s_collateralToken1;

238:     mapping(address account => mapping(TokenId tokenId => mapping(uint256 leg => LeftRightUnsigned premiaGrowth)))
239:         internal s_options;

245:     mapping(bytes32 chunkKey => LeftRightUnsigned lastGrossPremium) internal s_grossPremiumLast;

251:     mapping(bytes32 chunkKey => LeftRightUnsigned settledTokens) internal s_settledTokens;

258:     mapping(address account => mapping(TokenId tokenId => LeftRightUnsigned balanceAndUtilizations))
259:         internal s_positionBalance;

272:     mapping(address account => uint256 positionsHash) internal s_positionsHash;
125:     bool internal constant MINT = false;

126:     bool internal constant BURN = true;

133:     uint128 private constant VEGOID = 2;

137:     IUniswapV3Factory internal immutable FACTORY;

145:     mapping(address univ3pool => uint256 poolIdData) internal s_AddrToPoolIdData;

150:     mapping(uint64 poolId => PoolAddressAndLock contextData) internal s_poolContext;

177:     mapping(bytes32 positionKey => LeftRightUnsigned removedAndNetLiquidity)
178:         internal s_accountLiquidity;

287:     mapping(bytes32 positionKey => LeftRightUnsigned accountPremium) private s_accountPremiumOwed;

289:     mapping(bytes32 positionKey => LeftRightUnsigned accountPremium) private s_accountPremiumGross;

295:     mapping(bytes32 positionKey => LeftRightSigned baseFees0And1) internal s_accountFeesBase;
</details>

[N-04] Use of override is unnecessary

Starting from Solidity 0.8.8, the override keyword is not required when overriding an interface function, except for the case where the function is defined in multiple bases.

<details> <summary>There are 4 instances (click to show):</summary>
323:     function transfer(
324:         address recipient,
325:         uint256 amount
326:     ) public override(ERC20Minimal) returns (bool) {

341:     function transferFrom(
342:         address from,
343:         address to,
344:         uint256 amount
345:     ) public override(ERC20Minimal) returns (bool) {
540:     function safeTransferFrom(
541:         address from,
542:         address to,
543:         uint256 id,
544:         uint256 amount,
545:         bytes calldata data
546:     ) public override {

566:     function safeBatchTransferFrom(
567:         address from,
568:         address to,
569:         uint256[] calldata ids,
570:         uint256[] calldata amounts,
571:         bytes calldata data
572:     ) public override {
</details>

[N-05] Custom errors should be used rather than revert()/require()

Custom errors are available from solidity version 0.8.4. Custom errors are more easily processed in try-catch blocks, and are easier to re-use and maintain.

There are 9 instances:

361:                 require(denominator > 0);

370:             require(denominator > prod1);

448:                 require(result < type(uint256).max);

484:             require(2 ** 64 > prod1);

547:             require(2 ** 96 > prod1);

588:                 require(result < type(uint256).max);

624:             require(2 ** 128 > prod1);

665:                 require(result < type(uint256).max);

701:             require(2 ** 192 > prod1);

[N-06] Add inline comments for unnamed parameters

function func(address a, address) -> function func(address a, address /* b */)

There are 2 instances:

392:     function maxDeposit(address) external pure returns (uint256 maxAssets) {

444:     function maxMint(address) external view returns (uint256 maxShares) {

[N-07] Assembly blocks should have extensive comments

Assembly blocks take a lot more time to audit than normal Solidity code, and often have gotchas and side-effects that the Solidity versions of the same code do not. Consider adding more comments explaining what is being done in every step of the assembly code, and describe why assembly is being used instead of Solidity.

<details> <summary>There are 21 instances (click to show):</summary>
353:             assembly ("memory-safe") {
354:                 let mm := mulmod(a, b, not(0))
355:                 prod0 := mul(a, b)
356:                 prod1 := sub(sub(mm, prod0), lt(mm, prod0))
357:             }

362:                 assembly ("memory-safe") {
363:                     result := div(prod0, denominator)
364:                 }

379:             assembly ("memory-safe") {
380:                 remainder := mulmod(a, b, denominator)
381:             }

383:             assembly ("memory-safe") {
384:                 prod1 := sub(prod1, gt(remainder, prod0))
385:                 prod0 := sub(prod0, remainder)
386:             }

393:             assembly ("memory-safe") {
394:                 denominator := div(denominator, twos)
395:             }

398:             assembly ("memory-safe") {
399:                 prod0 := div(prod0, twos)
400:             }

404:             assembly ("memory-safe") {
405:                 twos := add(div(sub(0, twos), twos), 1)
406:             }

467:             assembly ("memory-safe") {
468:                 let mm := mulmod(a, b, not(0))
469:                 prod0 := mul(a, b)
470:                 prod1 := sub(sub(mm, prod0), lt(mm, prod0))
471:             }

493:             assembly ("memory-safe") {
494:                 remainder := mulmod(a, b, 0x10000000000000000)
495:             }

497:             assembly ("memory-safe") {
498:                 prod1 := sub(prod1, gt(remainder, prod0))
499:                 prod0 := sub(prod0, remainder)
500:             }

530:             assembly ("memory-safe") {
531:                 let mm := mulmod(a, b, not(0))
532:                 prod0 := mul(a, b)
533:                 prod1 := sub(sub(mm, prod0), lt(mm, prod0))
534:             }

556:             assembly ("memory-safe") {
557:                 remainder := mulmod(a, b, 0x1000000000000000000000000)
558:             }

560:             assembly ("memory-safe") {
561:                 prod1 := sub(prod1, gt(remainder, prod0))
562:                 prod0 := sub(prod0, remainder)
563:             }

607:             assembly ("memory-safe") {
608:                 let mm := mulmod(a, b, not(0))
609:                 prod0 := mul(a, b)
610:                 prod1 := sub(sub(mm, prod0), lt(mm, prod0))
611:             }

633:             assembly ("memory-safe") {
634:                 remainder := mulmod(a, b, 0x100000000000000000000000000000000)
635:             }

637:             assembly ("memory-safe") {
638:                 prod1 := sub(prod1, gt(remainder, prod0))
639:                 prod0 := sub(prod0, remainder)
640:             }

684:             assembly ("memory-safe") {
685:                 let mm := mulmod(a, b, not(0))
686:                 prod0 := mul(a, b)
687:                 prod1 := sub(sub(mm, prod0), lt(mm, prod0))
688:             }

710:             assembly ("memory-safe") {
711:                 remainder := mulmod(a, b, 0x1000000000000000000000000000000000000000000000000)
712:             }

714:             assembly ("memory-safe") {
715:                 prod1 := sub(prod1, gt(remainder, prod0))
716:                 prod0 := sub(prod0, remainder)
717:             }

739:         assembly ("memory-safe") {
740:             result := add(div(a, b), gt(mod(a, b), 0))
741:         }
  • Multicall.sol ( 25-27 ):
25:                 assembly ("memory-safe") {
26:                     revert(add(result, 32), mload(result))
27:                 }
</details>

[N-08] Consider splitting complex checks into multiple steps

Assign the expression's parts to intermediate local variables, and check against those instead.

There are 2 instances:

1060:             if ((intrinsicValue != 0) && ((shortAmount != 0) || (longAmount != 0))) {
566:                     if (((_isLong != isLongP) || _isLong == 1) && (_tokenType != tokenTypeP))

[N-09] Complex casting

Consider whether the number of casts is really necessary, or whether using a different type would be more appropriate. Alternatively, add comments to explain in detail why the casts are necessary, and any implicit reasons why the cast does not introduce an overflow.

<details> <summary>There are 20 instances (click to show):</summary>
667:                     int24 range = int24(
668:                         int256(
669:                             Math.unsafeDivRoundingUp(
670:                                 uint24(positionId.width(leg) * positionId.tickSpacing()),
671:                                 2
672:                             )
673:                         )
674:                     );

959:                     uint256(Math.max(1, int256(totalAssets()) - int256(assets)))

1029:             s_inAMM = uint128(uint256(int256(uint256(s_inAMM)) + (shortAmount - longAmount)));

1085:             s_inAMM = uint128(uint256(int256(uint256(s_inAMM)) - (shortAmount - longAmount)));

1119:             exchangedAmount += int256(
1120:                 Math.unsafeDivRoundingUp(
1121:                     uint256(uint128(shortAmount + longAmount)) * COMMISSION_FEE,
1122:                     DECIMALS
1123:                 )
1124:             );

1637:                 uint128(uint64(-int64(poolUtilization0 == 0 ? 1 : poolUtilization0))) +

1638:                 (uint128(uint64(-int64(poolUtilization1 == 0 ? 1 : poolUtilization1))) << 64);
730:             return uint128(uint256(utilization0) + uint128(uint256(utilization1) << 64));

1144:             uint256(int256(uint256(_delegations.rightSlot())) + liquidationBonus0)

1149:             uint256(int256(uint256(_delegations.leftSlot())) + liquidationBonus1)

1932:                                     uint128(
1933:                                         uint256(
1934:                                             Math.max(
1935:                                                 (int256(
1936:                                                     grossPremiumLast.rightSlot() *
1937:                                                         totalLiquidityBefore
1938:                                                 ) -
1939:                                                     int256(
1940:                                                         _premiumAccumulatorsByLeg[_leg][0] *
1941:                                                             positionLiquidity
1942:                                                     )) + int256(legPremia.rightSlot() * 2 ** 64),
1943:                                                 0
1944:                                             )
1945:                                         ) / totalLiquidity
1946:                                     )

1949:                                     uint128(
1950:                                         uint256(
1951:                                             Math.max(
1952:                                                 (int256(
1953:                                                     grossPremiumLast.leftSlot() *
1954:                                                         totalLiquidityBefore
1955:                                                 ) -
1956:                                                     int256(
1957:                                                         _premiumAccumulatorsByLeg[_leg][1] *
1958:                                                             positionLiquidity
1959:                                                     )) + int256(legPremia.leftSlot()) * 2 ** 64,
1960:                                                 0
1961:                                             )
1962:                                         ) / totalLiquidity
1963:                                     )
139:                     uint256(
140:                         (int256(observationIndex) - int256(i * period)) +
141:                             int256(observationCardinality)
142:                     ) % observationCardinality

178:                 (int24(uint24(medianData >> ((uint24(medianData >> (192 + 3 * 3)) % 8) * 24))) +

179:                     int24(uint24(medianData >> ((uint24(medianData >> (192 + 3 * 4)) % 8) * 24)))) /

187:                         uint256(
188:                             int256(observationIndex) - int256(1) + int256(observationCardinality)
189:                         ) % observationCardinality

257:                 twapMeasurement[i] = int24(
258:                     (tickCumulatives[i] - tickCumulatives[i + 1]) / int56(uint56(twapWindow / 20))
259:                 );

377:             int24(int256(Math.unsafeDivRoundingUp(uint24(width) * uint24(tickSpacing), 2)))

937:                             int128(
938:                                 int256(
939:                                     PanopticMath.convert0to1(uint256(balanceShortage), sqrtPriceX96)
940:                                 ) + refundValues.leftSlot()
941:                             )

955:                             int128(
956:                                 int256(
957:                                     PanopticMath.convert1to0(uint256(balanceShortage), sqrtPriceX96)
958:                                 ) + refundValues.rightSlot()
959:                             )
</details>

[N-10] Complex math should be split into multiple steps

Consider splitting long arithmetic calculations into multiple steps to improve the code readability.

<details> <summary>There are 26 instances (click to show):</summary>
201:             TICK_DEVIATION = uint256(
202:                 2230 +
203:                     (12500 * ratioTick) /
204:                     10_000 +
205:                     (7812 * ratioTick ** 2) /
206:                     10_000 ** 2 +
207:                     (6510 * ratioTick ** 3) /
208:                     10_000 ** 3
209:             );

402:             shares = Math.mulDiv(
403:                 assets * (DECIMALS - COMMISSION_FEE),
404:                 totalSupply,
405:                 totalAssets() * DECIMALS
406:             );

730:             exerciseFees = exerciseFees
731:                 .toRightSlot(int128((longAmounts.rightSlot() * fee) / DECIMALS_128))
732:                 .toLeftSlot(int128((longAmounts.leftSlot() * fee) / DECIMALS_128));

954:             _mint(
955:                 delegator,
956:                 Math.mulDiv(
957:                     assets,
958:                     totalSupply - delegateeBalance,
959:                     uint256(Math.max(1, int256(totalAssets()) - int256(assets)))
960:                 ) - delegateeBalance
961:             );

1362:                     uint160 ratio = tokenType == 1 // tokenType
1363:                         ? Math.getSqrtRatioAtTick(
1364:                             Math.max24(2 * (atTick - strike), Constants.MIN_V3POOL_TICK)
1365:                         ) // puts ->  price/strike
1366:                         : Math.getSqrtRatioAtTick(
1367:                             Math.max24(2 * (strike - atTick), Constants.MIN_V3POOL_TICK)
1368:                         ); // calls -> strike/price

1570:                 spreadRequirement = (notional < notionalP)
1571:                     ? Math.unsafeDivRoundingUp((notionalP - notional) * contracts, notional)
1572:                     : Math.unsafeDivRoundingUp((notional - notionalP) * contracts, notionalP);

1636:             poolUtilization =
1637:                 uint128(uint64(-int64(poolUtilization0 == 0 ? 1 : poolUtilization0))) +
1638:                 (uint128(uint64(-int64(poolUtilization1 == 0 ? 1 : poolUtilization1))) << 64);
308:             s_miniMedian =
309:                 (uint256(block.timestamp) << 216) +
310:                 // magic number which adds (7,5,3,1,0,2,4,6) order and minTick in positions 7, 5, 3 and maxTick in 6, 4, 2
311:                 // see comment on s_miniMedian initialization for format of this magic number
312:                 (uint256(0xF590A6F276170D89E9F276170D89E9F276170D89E9000000000000)) +
313:                 (uint256(uint24(currentTick)) << 24) + // add to slot 4
314:                 (uint256(uint24(currentTick))); // add to slot 3

1545:                     premiaByLeg[leg] = LeftRightSigned
1546:                         .wrap(0)
1547:                         .toRightSlot(
1548:                             int128(
1549:                                 int256(
1550:                                     ((premiumAccumulatorsByLeg[leg][0] -
1551:                                         premiumAccumulatorLast.rightSlot()) *
1552:                                         (liquidityChunk.liquidity())) / 2 ** 64
1553:                                 )
1554:                             )
1555:                         )
1556:                         .toLeftSlot(
1557:                             int128(
1558:                                 int256(
1559:                                     ((premiumAccumulatorsByLeg[leg][1] -
1560:                                         premiumAccumulatorLast.leftSlot()) *
1561:                                         (liquidityChunk.liquidity())) / 2 ** 64
1562:                                 )
1563:                             )
1564:                         );

1633:             LeftRightSigned realizedPremia = LeftRightSigned
1634:                 .wrap(0)
1635:                 .toRightSlot(int128(int256((accumulatedPremium.rightSlot() * liquidity) / 2 ** 64)))
1636:                 .toLeftSlot(int128(int256((accumulatedPremium.leftSlot() * liquidity) / 2 ** 64)));

1725:                     s_grossPremiumLast[chunkKey] = LeftRightUnsigned
1726:                         .wrap(0)
1727:                         .toRightSlot(
1728:                             uint128(
1729:                                 (grossCurrent[0] *
1730:                                     positionLiquidity +
1731:                                     grossPremiumLast.rightSlot() *
1732:                                     totalLiquidityBefore) / (totalLiquidity)
1733:                             )
1734:                         )
1735:                         .toLeftSlot(
1736:                             uint128(
1737:                                 (grossCurrent[1] *
1738:                                     positionLiquidity +
1739:                                     grossPremiumLast.leftSlot() *
1740:                                     totalLiquidityBefore) / (totalLiquidity)
1741:                             )
1742:                         );

1768:             uint256 accumulated0 = ((premiumAccumulators[0] - grossPremiumLast.rightSlot()) *
1769:                 totalLiquidity) / 2 ** 64;

1770:             uint256 accumulated1 = ((premiumAccumulators[1] - grossPremiumLast.leftSlot()) *
1771:                 totalLiquidity) / 2 ** 64;

1928:                         s_grossPremiumLast[chunkKey] = totalLiquidity != 0
1929:                             ? LeftRightUnsigned
1930:                                 .wrap(0)
1931:                                 .toRightSlot(
1932:                                     uint128(
1933:                                         uint256(
1934:                                             Math.max(
1935:                                                 (int256(
1936:                                                     grossPremiumLast.rightSlot() *
1937:                                                         totalLiquidityBefore
1938:                                                 ) -
1939:                                                     int256(
1940:                                                         _premiumAccumulatorsByLeg[_leg][0] *
1941:                                                             positionLiquidity
1942:                                                     )) + int256(legPremia.rightSlot() * 2 ** 64),
1943:                                                 0
1944:                                             )
1945:                                         ) / totalLiquidity
1946:                                     )
1947:                                 )
1948:                                 .toLeftSlot(
1949:                                     uint128(
1950:                                         uint256(
1951:                                             Math.max(
1952:                                                 (int256(
1953:                                                     grossPremiumLast.leftSlot() *
1954:                                                         totalLiquidityBefore
1955:                                                 ) -
1956:                                                     int256(
1957:                                                         _premiumAccumulatorsByLeg[_leg][1] *
1958:                                                             positionLiquidity
1959:                                                     )) + int256(legPremia.leftSlot()) * 2 ** 64,
1960:                                                 0
1961:                                             )
1962:                                         ) / totalLiquidity
1963:                                     )
1964:                                 )
1965:                             : LeftRightUnsigned
1966:                                 .wrap(0)
1967:                                 .toRightSlot(uint128(premiumAccumulatorsByLeg[_leg][0]))
1968:                                 .toLeftSlot(uint128(premiumAccumulatorsByLeg[_leg][1]));
1350:                 premium0X64_base = Math.mulDiv(
1351:                     collected0,
1352:                     totalLiquidity * 2 ** 64,
1353:                     netLiquidity ** 2
1354:                 );

1355:                 premium1X64_base = Math.mulDiv(
1356:                     collected1,
1357:                     totalLiquidity * 2 ** 64,
1358:                     netLiquidity ** 2
1359:                 );

1388:                     uint256 numerator = totalLiquidity ** 2 -
1389:                         totalLiquidity *
1390:                         removedLiquidity +
1391:                         ((removedLiquidity ** 2) / 2 ** (VEGOID));
138:                 (timestamps[i], tickCumulatives[i], , ) = univ3pool.observations(
139:                     uint256(
140:                         (int256(observationIndex) - int256(i * period)) +
141:                             int256(observationCardinality)
142:                     ) % observationCardinality
143:                 );

149:                 ticks[i] =
150:                     (tickCumulatives[i] - tickCumulatives[i + 1]) /
151:                     int256(timestamps[i] - timestamps[i + 1]);

177:             medianTick =
178:                 (int24(uint24(medianData >> ((uint24(medianData >> (192 + 3 * 3)) % 8) * 24))) +
179:                     int24(uint24(medianData >> ((uint24(medianData >> (192 + 3 * 4)) % 8) * 24)))) /
180:                 2;

223:                     newOrderMap = newOrderMap + ((rank + 1) << (3 * (i + shift - 1)));

226:                 updatedMedianData =
227:                     (block.timestamp << 216) +
228:                     (uint256(newOrderMap) << 192) +
229:                     uint256(uint192(medianData << 24)) +
230:                     uint256(uint24(lastObservedTick));

257:                 twapMeasurement[i] = int24(
258:                     (tickCumulatives[i] - tickCumulatives[i + 1]) / int56(uint56(twapWindow / 20))
259:                 );

475:             uint256 notional = asset == 0
476:                 ? convert0to1(contractSize, Math.getSqrtRatioAtTick((tickUpper + tickLower) / 2))
477:                 : convert1to0(contractSize, Math.getSqrtRatioAtTick((tickUpper + tickLower) / 2));

802:                 (collateralDelta0, collateralDelta1) = (
803:                     -Math.min(
804:                         collateralDelta0 - longPremium.rightSlot(),
805:                         PanopticMath.convert1to0(
806:                             longPremium.leftSlot() - collateralDelta1,
807:                             sqrtPriceX96Final
808:                         )
809:                     ),
810:                     Math.min(
811:                         longPremium.leftSlot() - collateralDelta1,
812:                         PanopticMath.convert0to1(
813:                             collateralDelta0 - longPremium.rightSlot(),
814:                             sqrtPriceX96Final
815:                         )
816:                     )
817:                 );

826:                 (collateralDelta0, collateralDelta1) = (
827:                     Math.min(
828:                         longPremium.rightSlot() - collateralDelta0,
829:                         PanopticMath.convert1to0(
830:                             collateralDelta1 - longPremium.leftSlot(),
831:                             sqrtPriceX96Final
832:                         )
833:                     ),
834:                     -Math.min(
835:                         collateralDelta1 - longPremium.leftSlot(),
836:                         PanopticMath.convert0to1(
837:                             longPremium.rightSlot() - collateralDelta0,
838:                             sqrtPriceX96Final
839:                         )
840:                     )
841:                 );
</details>

[N-11] Consider adding a block/deny-list

Doing so will significantly increase centralization, but will help to prevent hackers from using stolen tokens

There are 3 instances:

  • CollateralTracker.sol ( 36 ):
36: contract CollateralTracker is ERC20Minimal, Multicall {
  • PanopticFactory.sol ( 26 ):
26: contract PanopticFactory is Multicall {
  • SemiFungiblePositionManager.sol ( 72 ):
72: contract SemiFungiblePositionManager is ERC1155, Multicall {

[N-12] Constants/Immutables redefined elsewhere

Consider defining in only one contract so that values cannot become out of sync when only one location is updated. A cheap way to store constants/immutables in a single location is to create an internal constant in a library. If the variable is a local cache of another contract's value, consider making the cache variable internal or private, which will require external users to query the contract with the source of truth, so that callers don't get out of sync.

There are 4 instances:

  • PanopticFactory.sol ( 66-66 ):
/// @audit Seen in ./contracts/PanopticPool.sol#179
66:     SemiFungiblePositionManager internal immutable SFPM;
/// @audit Seen in ./contracts/PanopticFactory.sol#66
179:     SemiFungiblePositionManager internal immutable SFPM;
/// @audit Seen in ./contracts/libraries/PanopticMath.sol#23
15:     uint256 internal constant MAX_UINT256 = 2 ** 256 - 1;
  • PanopticMath.sol ( 23-23 ):
/// @audit Seen in ./contracts/libraries/Math.sol#15
23:     uint256 internal constant MAX_UINT256 = 2 ** 256 - 1;

[N-13] Convert simple if-statements to ternary expressions

Converting some if statements to ternaries (such as z = (a < b) ? x : y) can make the code more concise and easier to read.

There are 2 instances:

1544:                 if (tokenType == 0) {
1545:                     spreadRequirement = movedRight < movedPartnerRight
1546:                         ? movedPartnerRight - movedRight
1547:                         : movedRight - movedPartnerRight;
1548:                 } else {
1549:                     spreadRequirement = movedLeft < movedPartnerLeft
1550:                         ? movedPartnerLeft - movedLeft
1551:                         : movedLeft - movedPartnerLeft;
1552:                 }
382:             } else if (optionRatios < 2 ** 208) {
383:                 optionRatios = 3;
384:             } else {
385:                 optionRatios = 4;
386:             }

[N-14] Contract name does not match its filename

According to the Solidity Style Guide, contract and library names should also match their filenames.

There are 4 instances:

  • ERC1155Minimal.sol ( 11 ):
/// @audit Not match filename `ERC1155Minimal.sol`
11: abstract contract ERC1155 {
  • LeftRight.sol ( 17 ):
/// @audit Not match filename `LeftRight.sol`
17: library LeftRightLibrary {
  • LiquidityChunk.sol ( 52 ):
/// @audit Not match filename `LiquidityChunk.sol`
52: library LiquidityChunkLibrary {
  • TokenId.sol ( 60 ):
/// @audit Not match filename `TokenId.sol`
60: library TokenIdLibrary {

[N-15] UPPER_CASE names should be reserved for constant/immutable variables

If the variable needs to be different based on which class it comes from, a view/pure function should be used instead (e.g. like this).

There are 2 instances:

  • PanopticFactory.sol ( 116, 117 ):
116:         address _WETH9,

117:         SemiFungiblePositionManager _SFPM,

[N-16] Consider emitting an event at the end of the constructor

This will allow users to easily exactly pinpoint when and by whom a contract was constructed.

<details> <summary>There are 4 instances (click to show):</summary>
  • CollateralTracker.sol ( 178-186 ):
178:     constructor(
179:         uint256 _commissionFee,
180:         uint256 _sellerCollateralRatio,
181:         uint256 _buyerCollateralRatio,
182:         int256 _forceExerciseCost,
183:         uint256 _targetPoolUtilization,
184:         uint256 _saturatedPoolUtilization,
185:         uint256 _ITMSpreadMultiplier
186:     ) {
115:     constructor(
116:         address _WETH9,
117:         SemiFungiblePositionManager _SFPM,
118:         IUniswapV3Factory _univ3Factory,
119:         IDonorNFT _donorNFT,
120:         address _poolReference,
121:         address _collateralReference
122:     ) {
280:     constructor(SemiFungiblePositionManager _sfpm) {
  • SemiFungiblePositionManager.sol ( 341-341 ):
341:     constructor(IUniswapV3Factory _factory) {
</details>

[N-17] Events are emitted without the sender information

When an action is triggered based on a user's action, not being able to filter based on who triggered the action makes event processing a lot more cumbersome. Including the msg.sender the events of these types of action will make events much more useful to end users, especially when msg.sender is not tx.origin.

There are 3 instances:

268:         emit PoolDeployed(
269:             newPoolContract,
270:             v3Pool,
271:             collateralTracker0,
272:             collateralTracker1,
273:             amount0,
274:             amount1
275:         );
1654:             emit PremiumSettled(owner, tokenId, realizedPremia);
  • SemiFungiblePositionManager.sol ( 390-390 ):
390:         emit PoolInitialized(univ3pool, poolId);

[N-18] Inconsistent floating version pragma

Source files are using different floating version syntax, this is prone to compilation errors, and is not conducive to the code reliability and maintainability.

There are 2 instances:

  • CollateralTracker.sol ( 2 ):
2: pragma solidity ^0.8.18;
  • CallbackLib.sol ( 2 ):
2: pragma solidity ^0.8.0;

[N-19] Contract implements interface without extending the interface

Not extending the interface may lead to the wrong function signature being used, leading to unexpected behavior. If the interface is in fact being implemented, use the override keyword to indicate that fact.

There is 1 instance:

  • ERC1155Minimal.sol ( 11-21 ):
/// @audit IERC165
11: abstract contract ERC1155 {
12:     /*//////////////////////////////////////////////////////////////
13:                                  EVENTS
14:     //////////////////////////////////////////////////////////////*/
15: 
16:     /// @notice Emitted when only a single token is transferred.
17:     /// @param operator The user who initiated the transfer
18:     /// @param from The user who sent the tokens
19:     /// @param to The user who received the tokens
20:     /// @param id The ERC1155 token id
21:     /// @param amount The amount of tokens transferred

[N-20] Imports could be organized more systematically

The contract's interface should be imported first, followed by each of the interfaces it uses, followed by all other files. The examples below do not follow this layout.

There are 4 instances:

  • PanopticFactory.sol ( 8-8 ):
/// @audit Out of order with the prev import️ ⬆
8: import {IDonorNFT} from "@contracts/tokens/interfaces/IDonorNFT.sol";
  • PanopticPool.sol ( 7-7 ):
/// @audit Out of order with the prev import️ ⬆
7: import {IUniswapV3Pool} from "univ3-core/interfaces/IUniswapV3Pool.sol";
  • InteractionHelper.sol ( 6-6 ):
/// @audit Out of order with the prev import️ ⬆
6: import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
  • PanopticMath.sol ( 6-6 ):
/// @audit Out of order with the prev import️ ⬆
6: import {IUniswapV3Pool} from "univ3-core/interfaces/IUniswapV3Pool.sol";

[N-21] There is no need to initialize bool variables with false

Since the bool variables are automatically set to false when created, it is redundant to initialize it with false again.

There are 5 instances:

111:     bool internal constant COMPUTE_LONG_PREMIA = false;

114:     bool internal constant ONLY_AVAILABLE_PREMIUM = false;

120:     bool internal constant DONOT_COMMIT_LONG_SETTLED = false;

133:     bool internal constant SLOW_ORACLE_UNISWAP_MODE = false;
  • SemiFungiblePositionManager.sol ( 125-125 ):
125:     bool internal constant MINT = false;

[N-22] Invalid NatSpec comment style

NatSpec comment in solidity should use /// or /* ... */ syntax.

There are 6 instances:

358:         // @dev pools can be initialized from a Panoptic pool or by calling initializeAMMPool directly, reverting

360:         // @dev some pools may not be deployable if the poolId has a collision (since we take only 8 bytes)

603:             // @dev see `contracts/types/LiquidityChunk.sol`

836:             // @dev note this triggers our swap callback function

903:                 // @dev see `contracts/types/LiquidityChunk.sol`
  • PanopticMath.sol ( 98-98 ):
98:         // @dev 0 ^ x = x

[N-23] @openzeppelin/contracts should be upgraded to a newer version

These contracts import contracts from @openzeppelin/contracts, but they are not using the latest version. Using the latest version ensures contract security with fixes for known vulnerabilities, access to the latest feature updates, and enhanced community support and engagement.

The imported version is 4.8.3.

There are 5 instances:

  • PanopticFactory.sol ( 14 ):
14: import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
  • PanopticPool.sol ( 9 ):
9: import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol";
  • InteractionHelper.sol ( 6, 10 ):
6: import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";

10: import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
  • ERC1155Minimal.sol ( 5 ):
5: import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol";

[N-24] Lib @uniswap/v3-core should be upgraded to a newer version

These contracts import contracts from lib @uniswap/v3-core, but they are not using the latest version.

The imported version is 1.0.1-solc-0.8.

There is 1 instance:

  • Global finding

[N-25] Lib @uniswap/v3-periphery should be upgraded to a newer version

These contracts import contracts from lib @uniswap/v3-periphery, but they are not using the latest version.

The imported version is 1.4.2-solc-0.8.

There is 1 instance:

  • Global finding

[N-26] Expressions for constant values should use immutable rather than constant

While it doesn't save any gas because the compiler knows that developers often make this mistake, it's still best to use the right tool for the task at hand. There is a difference between constant variables and immutable variables, and they should each be used in their appropriate contexts. constants should be used for literal values written into the code, and immutable variables should be used for expressions, or values calculated in, or passed into the constructor.

There are 3 instances:

103:     int24 internal constant MIN_SWAP_TICK = Constants.MIN_V3POOL_TICK + 1;

105:     int24 internal constant MAX_SWAP_TICK = Constants.MAX_V3POOL_TICK - 1;

165:     uint64 internal constant MAX_SPREAD = 9 * (2 ** 32);

[N-27] Consider moving duplicated strings to constants

Moving duplicate strings to constants can improve code maintainability and readability.

There are 5 instances:

63:             symbol0 = "???";

68:             symbol1 = "???";

74:                     " ",

80:                     " ",

100:             return string.concat(prefix, "???");

[N-28] Contract uses both require()/revert() as well as custom errors

Consider using just one method in a single file.

There is 1 instance:

  • Math.sol ( 13 ):
13: library Math {

[N-29] Functions should be named in mixedCase style

As the Solidity Style Guide suggests: functions should be named in mixedCase style.

<details> <summary>There are 11 instances (click to show):</summary>
677:     function _mintInSFPMAndUpdateCollateral(

1450:     function getUniV3TWAP() internal view returns (int24 twapTick) {
350:     function initializeAMMPool(address token0, address token1, uint24 fee) external {

680:     function _validateAndForwardToAMM(

756:     function swapInAMM(

863:     function _createPositionInAMM(

958:     function _createLegInAMM(
  • FeesCalc.sol ( 97, 130 ):
97:     function calculateAMMSwapFees(

130:     function _getAMMSwapFeesPerLiquidityCollected(
  • PanopticMath.sol ( 607 ):
607:     function _calculateIOAmounts(
  • IDonorNFT.sol ( 13 ):
13:     function issueNFT(
</details>

[N-30] Modifiers should be named in mixedCase style

As the Solidity Style Guide suggests: modifiers should be named in mixedCase style. Use mixedCase. Examples: onlyBy, onlyAfter, onlyDuringThePreSale.

There is 1 instance:

  • SemiFungiblePositionManager.sol ( 305 ):
305:     modifier ReentrancyLock(uint64 poolId) {

[N-31] Constants should be put on the left side of comparisons

Putting constants on the left side of comparison statements is a best practice known as Yoda conditions. Although solidity's static typing system prevents accidental assignments within conditionals, adopting this practice can improve code readability and consistency, especially when working across multiple languages.

<details> <summary>There are 78 instances (click to show):</summary>
/// @audit put `0` on the left
708:                     (tokenType == 0 && currentValue1 < oracleValue1) ||

/// @audit put `1` on the left
709:                     (tokenType == 1 && currentValue0 < oracleValue0)

/// @audit put `0` on the left
1060:             if ((intrinsicValue != 0) && ((shortAmount != 0) || (longAmount != 0))) {

/// @audit put `0` on the left
1060:             if ((intrinsicValue != 0) && ((shortAmount != 0) || (longAmount != 0))) {

/// @audit put `0` on the left
1060:             if ((intrinsicValue != 0) && ((shortAmount != 0) || (longAmount != 0))) {

/// @audit put `0` on the left
1107:             if (intrinsicValue != 0) {

/// @audit put `0` on the left
1325:         uint128 amountMoved = tokenType == 0 ? amountsMoved.rightSlot() : amountsMoved.leftSlot();

/// @audit put `0` on the left
1328:         int64 utilization = tokenType == 0

/// @audit put `0` on the left
1339:             if (isLong == 0) {

/// @audit put `1` on the left
1346:                     ((atTick >= tickUpper) && (tokenType == 1)) || // strike OTM when price >= upperTick for tokenType=1

/// @audit put `0` on the left
1347:                     ((atTick < tickLower) && (tokenType == 0)) // strike OTM when price < lowerTick for tokenType=0

/// @audit put `1` on the left
1362:                     uint160 ratio = tokenType == 1 // tokenType

/// @audit put `1` on the left
1374:                         ((atTick < tickLower) && (tokenType == 1)) || // strike ITM but out of range price < lowerTick for tokenType=1

/// @audit put `0` on the left
1375:                         ((atTick >= tickUpper) && (tokenType == 0)) // strike ITM but out of range when price >= upperTick for tokenType=0

/// @audit put `1` on the left
1451:             if (isLong == 1) {

/// @audit put `0` on the left
1479:         if (isLong == 0) {

/// @audit put `1` on the left
1488:         } else if (isLong == 1) {

/// @audit put `0` on the left
1544:                 if (tokenType == 0) {

/// @audit put `1` on the left
1559:                 if (tokenType == 1) {

/// @audit put `0` on the left
1582:                 tokenType == 0 ? movedRight : movedLeft,

/// @audit put `0` on the left
1584:                 tokenType == 0

/// @audit put `0` on the left
1637:                 uint128(uint64(-int64(poolUtilization0 == 0 ? 1 : poolUtilization0))) +

/// @audit put `0` on the left
1638:                 (uint128(uint64(-int64(poolUtilization1 == 0 ? 1 : poolUtilization1))) << 64);
  • PanopticFactory.sol ( 224 ):
/// @audit put `address(0)` on the left
224:         if (_owner != address(0) && _owner != msg.sender) revert Errors.NotOwner();
/// @audit put `0` on the left
533:         if (medianData != 0) s_miniMedian = medianData;

/// @audit put `0` on the left
664:         if (medianData != 0) s_miniMedian = medianData;

/// @audit put `1` on the left
768:             if (isLong == 1) {

/// @audit put `1` on the left
1188:         if (touchedId.length != 1) revert Errors.InputListFail();

/// @audit put `0` on the left
1483:         if (netLiquidity == 0) return;

/// @audit put `1` on the left
1520:             if ((isLong == 1) || computeAllPremia) {

/// @audit put `1` on the left
1566:                     if (isLong == 1) {

/// @audit put `0` on the left
1780:                                     (accumulated0 == 0 ? type(uint256).max : accumulated0),

/// @audit put `0` on the left
1789:                                     (accumulated1 == 0 ? type(uint256).max : accumulated1),

/// @audit put `0` on the left
1928:                         s_grossPremiumLast[chunkKey] = totalLiquidity != 0
/// @audit put `address(0)` on the left
355:         if (univ3pool == address(0)) revert Errors.UniswapPoolNotInitialized();

/// @audit put `0` on the left
362:         if (s_AddrToPoolIdData[univ3pool] != 0) return;

/// @audit put `0` on the left
688:         if (positionSize == 0) revert Errors.OptionsBalanceZero();

/// @audit put `IUniswapV3Pool(address(0))` on the left
702:         if (univ3pool == IUniswapV3Pool(address(0))) revert Errors.UniswapPoolNotInitialized();

/// @audit put `0` on the left
787:             if ((itm0 != 0) && (itm1 != 0)) {

/// @audit put `0` on the left
787:             if ((itm0 != 0) && (itm1 != 0)) {

/// @audit put `0` on the left
823:             } else if (itm0 != 0) {

/// @audit put `0` on the left
833:             if (swapAmount == 0) return LeftRightSigned.wrap(0);

/// @audit put `0` on the left
999:             if (isLong == 0) {

/// @audit put `0` on the left
1066:             moved = isLong == 0

/// @audit put `1` on the left
1073:             if (tokenType == 1) {

/// @audit put `0` on the left
1078:             if (tokenType == 0) {

/// @audit put `1` on the left
1275:         if (isLong == 1) {

/// @audit put `0` on the left
1469:             if (netLiquidity != 0) {

/// @audit put `1` on the left
1514:                 acctPremia = isLong == 1 ? premiumOwed : premiumGross;

/// @audit put `1` on the left
1518:             acctPremia = isLong == 1
/// @audit put `0x100000000000000000000000000000000` on the left
93:             if (x >= 0x100000000000000000000000000000000) {

/// @audit put `0x10000000000000000` on the left
97:             if (x >= 0x10000000000000000) {

/// @audit put `0x100000000` on the left
101:             if (x >= 0x100000000) {

/// @audit put `0x10000` on the left
105:             if (x >= 0x10000) {

/// @audit put `0x100` on the left
109:             if (x >= 0x100) {

/// @audit put `0x10` on the left
113:             if (x >= 0x10) {

/// @audit put `0` on the left
360:             if (prod1 == 0) {

/// @audit put `0` on the left
474:             if (prod1 == 0) {

/// @audit put `0` on the left
537:             if (prod1 == 0) {

/// @audit put `0` on the left
614:             if (prod1 == 0) {

/// @audit put `0` on the left
691:             if (prod1 == 0) {
/// @audit put `address(0)` on the left
77:             return addr == address(0) ? 40 : 39 - Math.mostSignificantNibble(uint160(addr));

/// @audit put `7` on the left
211:                     if (rank == 7) {

/// @audit put `0` on the left
425:         if (tokenType == 0) {

/// @audit put `0` on the left
475:             uint256 notional = asset == 0

/// @audit put `0` on the left
479:             if (notional == 0 || notional > type(uint128).max) revert Errors.InvalidNotionalValue();

/// @audit put `0` on the left
856:                 if (haircut0 != 0) collateral0.exercise(_liquidatee, 0, 0, 0, int128(haircut0));

/// @audit put `0` on the left
857:                 if (haircut1 != 0) collateral1.exercise(_liquidatee, 0, 0, 0, int128(haircut1));
/// @audit put `0` on the left
112:         if (to.code.length != 0) {

/// @audit put `0` on the left
163:         if (to.code.length != 0) {

/// @audit put `0x01ffc9a7` on the left
202:             interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165

/// @audit put `0xd9b67a26` on the left
203:             interfaceId == 0xd9b67a26; // ERC165 Interface ID for ERC1155

/// @audit put `0` on the left
222:         if (to.code.length != 0) {
/// @audit put `0` on the left
465:         if (i == 0)

/// @audit put `1` on the left
471:         if (i == 1)

/// @audit put `2` on the left
477:         if (i == 2)

/// @audit put `3` on the left
483:         if (i == 3)

/// @audit put `1` on the left
566:                     if (((_isLong != isLongP) || _isLong == 1) && (_tokenType != tokenTypeP))
</details>

[N-32] else-block not required

One level of nesting can be removed by not having an else block when the if-block always jumps at the end. For example:

if (condition) {
    body1...
    return x;
} else {
    body2...
}

can be changed to:

if (condition) {
    body1...
    return x;
}
body2...
<details> <summary>There are 10 instances (click to show):</summary>
  • SemiFungiblePositionManager.sol ( 1013-1019 ):
1013:                 if (startingLiquidity < chunkLiquidity) {
1014:                     // the amount we want to move (liquidityChunk.legLiquidity()) out of uniswap is greater than
1015:                     // what the account that owns the liquidity in uniswap has (startingLiquidity)
1016:                     // we must ensure that an account can only move its own liquidity out of uniswap
1017:                     // so we revert in this case
1018:                     revert Errors.NotEnoughLiquidity();
1019:                 } else {
325:         if (tokenId.asset(legIndex) == 0) {
326:             return Math.getLiquidityForAmount0(tickLower, tickUpper, amount);
327:         } else {

425:         if (tokenType == 0) {
426:             return (
427:                 tokenData0.rightSlot() + convert1to0(tokenData1.rightSlot(), sqrtPriceX96),
428:                 tokenData0.leftSlot() + convert1to0(tokenData1.leftSlot(), sqrtPriceX96)
429:             );
430:         } else {

494:             if (sqrtPriceX96 < type(uint128).max) {
495:                 return Math.mulDiv192(amount, uint256(sqrtPriceX96) ** 2);
496:             } else {

511:             if (sqrtPriceX96 < type(uint128).max) {
512:                 return Math.mulDiv(amount, 2 ** 192, uint256(sqrtPriceX96) ** 2);
513:             } else {

528:             if (sqrtPriceX96 < type(uint128).max) {
529:                 int256 absResult = Math
530:                     .mulDiv192(Math.absUint(amount), uint256(sqrtPriceX96) ** 2)
531:                     .toInt256();
532:                 return amount < 0 ? -absResult : absResult;
533:             } else {

551:             if (sqrtPriceX96 < type(uint128).max) {
552:                 int256 absResult = Math
553:                     .mulDiv(Math.absUint(amount), 2 ** 192, uint256(sqrtPriceX96) ** 2)
554:                     .toInt256();
555:                 return amount < 0 ? -absResult : absResult;
556:             } else {
439:         if (optionRatios < 2 ** 64) {
440:             return 0;
441:         } else if (optionRatios < 2 ** 112) {

441:         } else if (optionRatios < 2 ** 112) {
442:             return 1;
443:         } else if (optionRatios < 2 ** 160) {

443:         } else if (optionRatios < 2 ** 160) {
444:             return 2;
445:         } else if (optionRatios < 2 ** 208) {
</details>

[N-33] Large multiples of ten should use scientific notation

Use a scientific notation rather than decimal literals (e.g. 1e6 instead of 1000000), for better code readability.

<details> <summary>There are 8 instances (click to show):</summary>
/// @audit 10_000 -> 1e4
77:     uint256 internal constant DECIMALS = 10_000;

/// @audit 10_000 -> 1e4
81:     int128 internal constant DECIMALS_128 = 10_000;

/// @audit 2000 -> 2e3
200:             int256 ratioTick = (int256(_sellerCollateralRatio) - 2000);

/// @audit 10_000 -> 1e4
204:                     10_000 +

/// @audit 10_000 -> 1e4
206:                     10_000 ** 2 +

/// @audit 10_000 -> 1e4
208:                     10_000 ** 3
/// @audit 10_000 -> 1e4
174:     uint256 internal constant NO_BUFFER = 10_000;

/// @audit 10_000 -> 1e4
1329:             return balanceCross >= Math.unsafeDivRoundingUp(thresholdCross * buffer, 10_000);
</details>

[N-34] Non-interface files should use fixed compiler versions

To prevent the actual contracts being deployed from behaving differently depending on the compiler version, it is recommended to use fixed solidity versions for contracts and libraries.

Although we can configure a specific version through config (like hardhat, forge config files), it is recommended to set the fixed version in the solidity pragma directly before deploying to the mainnet.

<details> <summary>There are 18 instances (click to show):</summary>
  • CollateralTracker.sol ( 2 ):
2: pragma solidity ^0.8.18;
  • PanopticFactory.sol ( 2 ):
2: pragma solidity ^0.8.18;
  • PanopticPool.sol ( 2 ):
2: pragma solidity ^0.8.18;
  • SemiFungiblePositionManager.sol ( 2 ):
2: pragma solidity ^0.8.18;
  • CallbackLib.sol ( 2 ):
2: pragma solidity ^0.8.0;
  • Constants.sol ( 2 ):
2: pragma solidity ^0.8.0;
  • Errors.sol ( 2 ):
2: pragma solidity ^0.8.0;
  • FeesCalc.sol ( 2 ):
2: pragma solidity ^0.8.0;
  • InteractionHelper.sol ( 2 ):
2: pragma solidity ^0.8.0;
  • Math.sol ( 2 ):
2: pragma solidity ^0.8.0;
  • PanopticMath.sol ( 2 ):
2: pragma solidity ^0.8.0;
  • SafeTransferLib.sol ( 2 ):
2: pragma solidity ^0.8.0;
  • Multicall.sol ( 2 ):
2: pragma solidity ^0.8.18;
  • ERC1155Minimal.sol ( 2 ):
2: pragma solidity ^0.8.0;
  • ERC20Minimal.sol ( 2 ):
2: pragma solidity ^0.8.0;
  • LeftRight.sol ( 2 ):
2: pragma solidity ^0.8.0;
  • LiquidityChunk.sol ( 2 ):
2: pragma solidity ^0.8.0;
  • TokenId.sol ( 2 ):
2: pragma solidity ^0.8.0;
</details>

[N-35] High cyclomatic complexity

Consider breaking down these blocks into more manageable units, by splitting things into utility functions, by reducing nesting, and by using early returns.

<details> <summary>There are 2 instances (click to show):</summary>
1311:     function _getRequiredCollateralSingleLegNoPartner(
1312:         TokenId tokenId,
1313:         uint256 index,
1314:         uint128 positionSize,
1315:         int24 atTick,
1316:         uint128 poolUtilization
1317:     ) internal view returns (uint256 required) {
1318:         // extract the tokenType (token0 or token1)
1319:         uint256 tokenType = tokenId.tokenType(index);
1320: 
1321:         // compute the total amount of funds moved for that position
1322:         LeftRightUnsigned amountsMoved = PanopticMath.getAmountsMoved(tokenId, positionSize, index);
1323: 
1324:         // amount moved is right slot if tokenType=0, left slot otherwise
1325:         uint128 amountMoved = tokenType == 0 ? amountsMoved.rightSlot() : amountsMoved.leftSlot();
1326: 
1327:         // match tokenType with the correct pool utilization
1328:         int64 utilization = tokenType == 0
1329:             ? int64(uint64(poolUtilization))
1330:             : int64(uint64(poolUtilization >> 64));
1331: 
1332:         uint256 isLong = tokenId.isLong(index);
1333: 
1334:         // start with base requirement, which is based on isLong value
1335:         required = _getRequiredCollateralAtUtilization(amountMoved, isLong, utilization);
...... OMITTED ......
1398: 
1399:                         // compute the tokens required
1400:                         // position is in-the-money, collateral requirement = amountMoved*(1-ratio) + SCR*amountMoved
1401:                         required += Math.mulDiv96RoundingUp(amountMoved, c2);
1402:                     } else {
1403:                         // position is in-range (ie. current tick is between upper+lower tick): we draw a line between the
1404:                         // collateral requirement at the lowerTick and the one at the upperTick. We use that interpolation as
1405:                         // the collateral requirement when in-range, which always over-estimates the amount of token required
1406:                         // Specifically:
1407:                         //  required = amountMoved * (scaleFactor - ratio) / (scaleFactor + 1) + sellCollateralRatio*amountMoved
1408:                         uint160 scaleFactor = Math.getSqrtRatioAtTick(
1409:                             (tickUpper - strike) + (strike - tickLower)
1410:                         );
1411:                         uint256 c3 = Math.mulDivRoundingUp(
1412:                             amountMoved,
1413:                             scaleFactor - ratio,
1414:                             scaleFactor + Constants.FP96
1415:                         );
1416:                         // position is in-the-money, collateral requirement = amountMoved*(1-SRC)*(scaleFactor-ratio)/(scaleFactor+1) + SCR*amountMoved
1417:                         required += c3;
1418:                     }
1419:                 }
1420:             }
1421:         }
1422:     }
  • SemiFungiblePositionManager.sol ( 958-1104 ):
958:     function _createLegInAMM(
959:         IUniswapV3Pool univ3pool,
960:         TokenId tokenId,
961:         uint256 leg,
962:         LiquidityChunk liquidityChunk,
963:         bool isBurn
964:     )
965:         internal
966:         returns (
967:             LeftRightSigned moved,
968:             LeftRightSigned itmAmounts,
969:             LeftRightUnsigned collectedSingleLeg
970:         )
971:     {
972:         uint256 tokenType = tokenId.tokenType(leg);
973:         // unique key to identify the liquidity chunk in this uniswap pool
974:         bytes32 positionKey = keccak256(
975:             abi.encodePacked(
976:                 address(univ3pool),
977:                 msg.sender,
978:                 tokenType,
979:                 liquidityChunk.tickLower(),
980:                 liquidityChunk.tickUpper()
981:             )
982:         );
...... OMITTED ......
1080:                 itmAmounts = itmAmounts.toLeftSlot(moved.leftSlot());
1081:             }
1082:         }
1083: 
1084:         // if there was liquidity at that tick before the transaction, collect any accumulated fees
1085:         if (currentLiquidity.rightSlot() > 0) {
1086:             collectedSingleLeg = _collectAndWritePositionData(
1087:                 liquidityChunk,
1088:                 univ3pool,
1089:                 currentLiquidity,
1090:                 positionKey,
1091:                 moved,
1092:                 isLong
1093:             );
1094:         }
1095: 
1096:         // position has been touched, update s_accountFeesBase with the latest values from the pool.positions
1097:         // round up the stored feesbase to minimize Δfeesbase when we next calculate it
1098:         s_accountFeesBase[positionKey] = _getFeesBase(
1099:             univ3pool,
1100:             updatedLiquidity,
1101:             liquidityChunk,
1102:             true
1103:         );
1104:     }
</details>

[N-36] Typos

All typos should be corrected.

<details> <summary>There are 17 instances (click to show):</summary>
  • CollateralTracker.sol ( 92 ):
/// @audit initalized
92:     /// @dev As each instance is deployed via proxy clone, initial parameters must only be initalized once via startToken().
/// @audit caluculation
107:     // Flags used as arguments to premia caluculation functions

/// @audit wether
122:     /// @dev Boolean flag to determine wether a position is added (true) or not (!ADD = false)

/// @audit Mananger
278:     /// @notice During construction: sets the address of the panoptic factory smart contract and the SemiFungiblePositionMananger (SFPM).
  • SemiFungiblePositionManager.sol ( 1106, 1337 ):
/// @audit postion
1106:     /// @notice caches/stores the accumulated premia values for the specified postion.

/// @audit seperated
1337:         // premia, and is only seperated to simplify the calculation
  • Errors.sol ( 62 ):
/// @audit avaiable
62:     /// @notice PanopticPool: there is not enough avaiable liquidity to buy an option
/// @audit accomodate
486:     /// @dev Uses reduced precision after tick 443636 in order to accomodate the full range of tick.s

/// @audit accomodate
503:     /// @dev Uses reduced precision after tick 443636 in order to accomodate the full range of ticks.

/// @audit accomodate
520:     /// @dev Uses reduced precision after tick 443636 in order to accomodate the full range of ticks.

/// @audit accomodate
543:     /// @dev Uses reduced precision after tick 443636 in order to accomodate the full range of ticks.

/// @audit consistentcy
663:                 // evaluate at TWAP price to keep consistentcy with solvency calculations

/// @audit commited
886:                         // The long premium is not commited to storage during the liquidation, so we add the entire adjusted amount
/// @audit explictily
153:             // adding leftRight packed uint128's is same as just adding the values explictily

/// @audit occured
159:             // then an overflow has occured

/// @audit explictily
176:             // subtracting leftRight packed uint128's is same as just subtracting the values explictily

/// @audit occured
182:             // then an underflow has occured
</details>

[N-37] Consider bounding input array length

The functions below take in an unbounded array, and make function calls for entries in the array. While the function will revert if it eventually runs out of gas, it may be a nicer user experience to require() that the length of the array is below some reasonable maximum, so that the user doesn't have to use up a full transaction's gas only to see that the transaction reverts.

<details> <summary>There are 9 instances (click to show):</summary>
  • CollateralTracker.sol ( 1208 ):
1208:         for (uint256 i = 0; i < totalIterations; ) {
441:         for (uint256 k = 0; k < pLength; ) {

459:             for (uint256 leg = 0; leg < numLegs; ) {

802:         for (uint256 i = 0; i < positionIdList.length; ) {

1382:         for (uint256 i = 0; i < pLength; ) {
  • SemiFungiblePositionManager.sol ( 575 ):
575:         for (uint256 i = 0; i < ids.length; ) {
  • Multicall.sol ( 14 ):
14:         for (uint256 i = 0; i < data.length; ) {
  • ERC1155Minimal.sol ( 143, 187 ):
143:         for (uint256 i = 0; i < ids.length; ) {

187:             for (uint256 i = 0; i < owners.length; ++i) {
</details>

[N-38] Unnecessary casting

Unnecessary castings can be removed.

<details> <summary>There are 15 instances (click to show):</summary>
/// @audit IUniswapV3Pool(v3Pool)
404:             IUniswapV3Pool(v3Pool).mint(
/// @audit IUniswapV3Pool(_univ3pool)
302:         s_univ3pool = IUniswapV3Pool(_univ3pool);

/// @audit IUniswapV3Pool(_univ3pool)
304:         (, int24 currentTick, , , , , ) = IUniswapV3Pool(_univ3pool).slot0();

/// @audit uint256(block.timestamp)
309:                 (uint256(block.timestamp) << 216) +
/// @audit address(decoded.poolFeatures.token0)
447:             ? address(decoded.poolFeatures.token0)

/// @audit address(decoded.poolFeatures.token1)
448:             : address(decoded.poolFeatures.token1);
/// @audit uint256((TokenId.unwrap(self) >> (64 + legIndex * 48)) % 2)
110:             return uint256((TokenId.unwrap(self) >> (64 + legIndex * 48)) % 2);

/// @audit uint256((TokenId.unwrap(self) >> (64 + legIndex * 48 + 1)) % 128)
120:             return uint256((TokenId.unwrap(self) >> (64 + legIndex * 48 + 1)) % 128);

/// @audit uint256((TokenId.unwrap(self) >> (64 + legIndex * 48 + 8)) % 2)
130:             return uint256((TokenId.unwrap(self) >> (64 + legIndex * 48 + 8)) % 2);

/// @audit uint256((TokenId.unwrap(self) >> (64 + legIndex * 48 + 9)) % 2)
140:             return uint256((TokenId.unwrap(self) >> (64 + legIndex * 48 + 9)) % 2);

/// @audit uint256((TokenId.unwrap(self) >> (64 + legIndex * 48 + 10)) % 4)
150:             return uint256((TokenId.unwrap(self) >> (64 + legIndex * 48 + 10)) % 4);

/// @audit uint256(_asset % 2)
212:                 TokenId.wrap(TokenId.unwrap(self) + (uint256(_asset % 2) << (64 + legIndex * 48)));

/// @audit uint256(_optionRatio % 128)
229:                     TokenId.unwrap(self) + (uint256(_optionRatio % 128) << (64 + legIndex * 48 + 1))

/// @audit uint256(_tokenType % 2)
263:                     TokenId.unwrap(self) + (uint256(_tokenType % 2) << (64 + legIndex * 48 + 9))

/// @audit uint256(_riskPartner % 4)
281:                     TokenId.unwrap(self) + (uint256(_riskPartner % 4) << (64 + legIndex * 48 + 10))
</details>

[N-39] Unused contract variables

The following state variables are defined but not used. It is recommended to check the code for logical omissions that cause them not to be used. If it's determined that they are not needed anywhere, it's best to remove them from the codebase to improve code clarity and minimize confusion.

There is 1 instance:

  • CollateralTracker.sol ( 131-131 ):
131:     uint256 immutable TICK_DEVIATION;

[N-40] Unused import

The identifier is imported but never used within the file.

<details> <summary>There are 29 instances (click to show):</summary>
/// @audit PanopticPool
5: import {PanopticPool} from "./PanopticPool.sol";

/// @audit Multicall
8: import {Multicall} from "@multicall/Multicall.sol";

/// @audit LiquidityChunk
18: import {LiquidityChunk} from "@types/LiquidityChunk.sol";
/// @audit SemiFungiblePositionManager
7: import {SemiFungiblePositionManager} from "@contracts/SemiFungiblePositionManager.sol";

/// @audit IDonorNFT
8: import {IDonorNFT} from "@contracts/tokens/interfaces/IDonorNFT.sol";

/// @audit IUniswapV3Factory
9: import {IUniswapV3Factory} from "univ3-core/interfaces/IUniswapV3Factory.sol";

/// @audit Multicall
12: import {Multicall} from "@multicall/Multicall.sol";
/// @audit CollateralTracker
5: import {CollateralTracker} from "@contracts/CollateralTracker.sol";

/// @audit SemiFungiblePositionManager
6: import {SemiFungiblePositionManager} from "@contracts/SemiFungiblePositionManager.sol";

/// @audit ERC1155Holder
9: import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol";

/// @audit Multicall
10: import {Multicall} from "@multicall/Multicall.sol";

/// @audit LiquidityChunk
20: import {LiquidityChunk} from "@types/LiquidityChunk.sol";
/// @audit IUniswapV3Factory
5: import {IUniswapV3Factory} from "univ3-core/interfaces/IUniswapV3Factory.sol";

/// @audit ERC1155
8: import {ERC1155} from "@tokens/ERC1155Minimal.sol";

/// @audit Multicall
9: import {Multicall} from "@multicall/Multicall.sol";

/// @audit LiquidityChunk
20: import {LiquidityChunk} from "@types/LiquidityChunk.sol";
  • CallbackLib.sol ( 5-5 ):
/// @audit IUniswapV3Factory
5: import {IUniswapV3Factory} from "univ3-core/interfaces/IUniswapV3Factory.sol";
/// @audit IUniswapV3Pool
5: import {IUniswapV3Pool} from "univ3-core/interfaces/IUniswapV3Pool.sol";

/// @audit LeftRightUnsigned
10: import {LeftRightUnsigned, LeftRightSigned} from "@types/LeftRight.sol";

/// @audit LiquidityChunk
11: import {LiquidityChunk} from "@types/LiquidityChunk.sol";

/// @audit TokenId
12: import {TokenId} from "@types/TokenId.sol";
  • InteractionHelper.sol ( 5-5, 8-8 ):
/// @audit CollateralTracker
5: import {CollateralTracker} from "@contracts/CollateralTracker.sol";

/// @audit SemiFungiblePositionManager
8: import {SemiFungiblePositionManager} from "@contracts/SemiFungiblePositionManager.sol";
  • Math.sol ( 8-8 ):
/// @audit LiquidityChunk
8: import {LiquidityChunk, LiquidityChunkLibrary} from "@types/LiquidityChunk.sol";
/// @audit CollateralTracker
5: import {CollateralTracker} from "@contracts/CollateralTracker.sol";

/// @audit LiquidityChunk
13: import {LiquidityChunk} from "@types/LiquidityChunk.sol";

/// @audit TokenId
14: import {TokenId} from "@types/TokenId.sol";
  • IDonorNFT.sol ( 4-4 ):
/// @audit PanopticPool
4: import {PanopticPool} from "@contracts/PanopticPool.sol";
  • LiquidityChunk.sol ( 5-5 ):
/// @audit TokenId
5: import {TokenId} from "@types/TokenId.sol";
</details>

[N-41] Unused named return

Declaring named returns, but not using them, is confusing to the reader. Consider either completely removing them (by declaring just the type without a name), or remove the return statement and do a variable assignment. This would improve the readability of the code, and it may also help reduce regressions during future code refactors.

<details> <summary>There are 18 instances (click to show):</summary>
/// @audit assetTokenAddress
361:     function asset() external view returns (address assetTokenAddress) {

/// @audit totalManagedAssets
370:     function totalAssets() public view returns (uint256 totalManagedAssets) {

/// @audit shares
379:     function convertToShares(uint256 assets) public view returns (uint256 shares) {

/// @audit assets
386:     function convertToAssets(uint256 shares) public view returns (uint256 assets) {

/// @audit maxAssets
392:     function maxDeposit(address) external pure returns (uint256 maxAssets) {

/// @audit maxShares
444:     function maxMint(address) external view returns (uint256 maxShares) {

/// @audit maxAssets
507:     function maxWithdraw(address owner) public view returns (uint256 maxAssets) {

/// @audit shares
518:     function previewWithdraw(uint256 assets) public view returns (uint256 shares) {

/// @audit maxShares
572:     function maxRedeem(address owner) public view returns (uint256 maxShares) {

/// @audit assets
581:     function previewRedeem(uint256 shares) public view returns (uint256 assets) {

/// @audit poolUtilization
741:     function _poolUtilization() internal view returns (int256 poolUtilization) {

/// @audit sellCollateralRatio
751:     function _sellCollateralRatio(
752:         int256 utilization
753:     ) internal view returns (uint256 sellCollateralRatio) {

/// @audit buyCollateralRatio
806:     function _buyCollateralRatio(
807:         uint256 utilization
808:     ) internal view returns (uint256 buyCollateralRatio) {

/// @audit required
1278:     function _getRequiredCollateralSingleLeg(
1279:         TokenId tokenId,
1280:         uint256 index,
1281:         uint128 positionSize,
1282:         int24 atTick,
1283:         uint128 poolUtilization
1284:     ) internal view returns (uint256 required) {
/// @audit premium0
/// @audit premium1
381:     function calculateAccumulatedFeesBatch(
382:         address user,
383:         bool includePendingPremium,
384:         TokenId[] calldata positionIdList
385:     ) external view returns (int128 premium0, int128 premium1, uint256[2][] memory) {

/// @audit collateralToken
1431:     function collateralToken0() external view returns (CollateralTracker collateralToken) {
  • SemiFungiblePositionManager.sol ( 1555-1557 ):
/// @audit UniswapV3Pool
1555:     function getUniswapV3PoolFromId(
1556:         uint64 poolId
1557:     ) external view returns (IUniswapV3Pool UniswapV3Pool) {
</details>

[N-42] Use delete instead of assigning values to false

The delete keyword more closely matches the semantics of what is being done, and draws more attention to the changing of state, which may lead to a more thorough audit of its associated logic.

There are 2 instances:

  • SemiFungiblePositionManager.sol ( 332-332 ):
332:         s_poolContext[poolId].locked = false;
220:                         below = false;

[N-43] Consider using delete rather than assigning zero to clear values

The delete keyword more closely matches the semantics of what is being done, and draws more attention to the changing of state, which may lead to a more thorough audit of its associated logic.

There are 3 instances:

850:                 collateralDelta0 = 0;

851:                 collateralDelta1 = 0;
377:                 optionRatios = 0;

[N-44] Use the latest Solidity version

Upgrading to the latest solidity version (0.8.19 for L2s) can optimize gas usage, take advantage of new features and improve overall contract efficiency. Where possible, based on compatibility requirements, it is recommended to use newer/latest solidity version to take advantage of the latest optimizations and features.

<details> <summary>There are 18 instances (click to show):</summary>
  • CollateralTracker.sol ( 2 ):
2: pragma solidity ^0.8.18;
  • PanopticFactory.sol ( 2 ):
2: pragma solidity ^0.8.18;
  • PanopticPool.sol ( 2 ):
2: pragma solidity ^0.8.18;
  • SemiFungiblePositionManager.sol ( 2 ):
2: pragma solidity ^0.8.18;
  • CallbackLib.sol ( 2 ):
2: pragma solidity ^0.8.0;
  • Constants.sol ( 2 ):
2: pragma solidity ^0.8.0;
  • Errors.sol ( 2 ):
2: pragma solidity ^0.8.0;
  • FeesCalc.sol ( 2 ):
2: pragma solidity ^0.8.0;
  • InteractionHelper.sol ( 2 ):
2: pragma solidity ^0.8.0;
  • Math.sol ( 2 ):
2: pragma solidity ^0.8.0;
  • PanopticMath.sol ( 2 ):
2: pragma solidity ^0.8.0;
  • SafeTransferLib.sol ( 2 ):
2: pragma solidity ^0.8.0;
  • Multicall.sol ( 2 ):
2: pragma solidity ^0.8.18;
  • ERC1155Minimal.sol ( 2 ):
2: pragma solidity ^0.8.0;
  • ERC20Minimal.sol ( 2 ):
2: pragma solidity ^0.8.0;
  • LeftRight.sol ( 2 ):
2: pragma solidity ^0.8.0;
  • LiquidityChunk.sol ( 2 ):
2: pragma solidity ^0.8.0;
  • TokenId.sol ( 2 ):
2: pragma solidity ^0.8.0;
</details>

[N-45] Use a struct to encapsulate multiple function parameters

If a function has too many parameters, replacing them with a struct can improve code readability and maintainability, increase reusability, and reduce the likelihood of errors when passing the parameters.

<details> <summary>There are 9 instances (click to show):</summary>
178:     constructor(
179:         uint256 _commissionFee,
180:         uint256 _sellerCollateralRatio,
181:         uint256 _buyerCollateralRatio,
182:         int256 _forceExerciseCost,
183:         uint256 _targetPoolUtilization,
184:         uint256 _saturatedPoolUtilization,
185:         uint256 _ITMSpreadMultiplier
186:     ) {

221:     function startToken(
222:         bool underlyingIsToken0,
223:         address token0,
224:         address token1,
225:         uint24 fee,
226:         PanopticPool panopticPool
227:     ) external {

650:     function exerciseCost(
651:         int24 currentTick,
652:         int24 oracleTick,
653:         TokenId positionId,
654:         uint128 positionBalance,
655:         LeftRightSigned longAmounts
656:     ) external view returns (LeftRightSigned exerciseFees) {

1043:     function exercise(
1044:         address optionOwner,
1045:         int128 longAmount,
1046:         int128 shortAmount,
1047:         int128 swappedAmount,
1048:         int128 realizedPremium
1049:     ) external onlyPanopticPool returns (int128) {

1278:     function _getRequiredCollateralSingleLeg(
1279:         TokenId tokenId,
1280:         uint256 index,
1281:         uint128 positionSize,
1282:         int24 atTick,
1283:         uint128 poolUtilization
1284:     ) internal view returns (uint256 required) {

1311:     function _getRequiredCollateralSingleLegNoPartner(
1312:         TokenId tokenId,
1313:         uint256 index,
1314:         uint128 positionSize,
1315:         int24 atTick,
1316:         uint128 poolUtilization
1317:     ) internal view returns (uint256 required) {

1439:     function _getRequiredCollateralSingleLegPartner(
1440:         TokenId tokenId,
1441:         uint256 index,
1442:         uint128 positionSize,
1443:         int24 atTick,
1444:         uint128 poolUtilization
1445:     ) internal view returns (uint256 required) {

1510:     function _computeSpread(
1511:         TokenId tokenId,
1512:         uint128 positionSize,
1513:         uint256 index,
1514:         uint256 partnerIndex,
1515:         uint128 poolUtilization
1516:     ) internal view returns (uint256 spreadRequirement) {

1600:     function _computeStrangle(
1601:         TokenId tokenId,
1602:         uint256 index,
1603:         uint128 positionSize,
1604:         int24 atTick,
1605:         uint128 poolUtilization
1606:     ) internal view returns (uint256 strangleRequired) {
</details>

[N-46] Returning a struct instead of a bunch of variables is better

If a function returns too many variables, replacing them with a struct can improve code readability, maintainability and reusability.

<details> <summary>There are 5 instances (click to show):</summary>
  • CollateralTracker.sol ( 277-280 ):
277:     function getPoolData()
278:         external
279:         view
280:         returns (uint256 poolAssets, uint256 insideAMM, int256 currentPoolUtilization)
290:     function minePoolAddress(
291:         bytes32 salt,
292:         uint256 loops,
293:         uint256 minTargetRarity
294:     ) external view returns (bytes32 bestSalt, uint256 highestRarity) {

335:     function _mintFullRange(
336:         IUniswapV3Pool v3Pool,
337:         address token0,
338:         address token1,
339:         uint24 fee
340:     ) internal returns (uint256, uint256) {
352:     function optionPositionBalance(
353:         address user,
354:         TokenId tokenId
355:     ) external view returns (uint128 balance, uint64 poolUtilization0, uint64 poolUtilization1) {

381:     function calculateAccumulatedFeesBatch(
382:         address user,
383:         bool includePendingPremium,
384:         TokenId[] calldata positionIdList
385:     ) external view returns (int128 premium0, int128 premium1, uint256[2][] memory) {
</details>

[N-47] Contract variables should have comments

Consider adding some comments on non-public contract variables to explain what they are supposed to do. This will help for future code reviews.

There is 1 instance:

  • SemiFungiblePositionManager.sol ( 289-289 ):
289:     mapping(bytes32 positionKey => LeftRightUnsigned accountPremium) private s_accountPremiumGross;

[N-48] Empty bytes check is missing

Passing empty bytes to a function can cause unexpected behavior, such as certain operations failing, producing incorrect results, or wasting gas. It is recommended to check that all byte parameters are not empty.

<details> <summary>There are 7 instances (click to show):</summary>
/// @audit data
172:     function uniswapV3MintCallback(
173:         uint256 amount0Owed,
174:         uint256 amount1Owed,
175:         bytes calldata data
176:     ) external {
/// @audit data
402:     function uniswapV3MintCallback(
403:         uint256 amount0Owed,
404:         uint256 amount1Owed,
405:         bytes calldata data
406:     ) external {

/// @audit data
435:     function uniswapV3SwapCallback(
436:         int256 amount0Delta,
437:         int256 amount1Delta,
438:         bytes calldata data
439:     ) external {

/// @audit data
540:     function safeTransferFrom(
541:         address from,
542:         address to,
543:         uint256 id,
544:         uint256 amount,
545:         bytes calldata data
546:     ) public override {

/// @audit data
566:     function safeBatchTransferFrom(
567:         address from,
568:         address to,
569:         uint256[] calldata ids,
570:         uint256[] calldata amounts,
571:         bytes calldata data
572:     ) public override {
/// @audit data
94:     function safeTransferFrom(
95:         address from,
96:         address to,
97:         uint256 id,
98:         uint256 amount,
99:         bytes calldata data
100:     ) public virtual {

/// @audit data
130:     function safeBatchTransferFrom(
131:         address from,
132:         address to,
133:         uint256[] calldata ids,
134:         uint256[] calldata amounts,
135:         bytes calldata data
136:     ) public virtual {
</details>

[N-49] Don't define functions with the same name in a contract

In Solidity, while function overriding allows for functions with the same name to coexist, it is advisable to avoid this practice to enhance code readability and maintainability. Having multiple functions with the same name, even with different parameters or in inherited contracts, can cause confusion and increase the likelihood of errors during development, testing, and debugging. Using distinct and descriptive function names not only clarifies the purpose and behavior of each function, but also helps prevent unintended function calls or incorrect overriding. By adopting a clear and consistent naming convention, developers can create more comprehensible and maintainable smart contracts.

<details> <summary>There are 16 instances (click to show):</summary>
  • CollateralTracker.sol ( 894, 975 ):
/// @audit Different function with same name found on line 864
894:     function delegate(address delegatee, uint256 assets) external onlyPanopticPool {

/// @audit Different function with same name found on line 903
975:     function refund(address refunder, address refundee, int256 assets) external onlyPanopticPool {
  • PanopticPool.sol ( 586 ):
/// @audit Different function with same name found on line 569
586:     function burnOptions(
/// @audit Different function with same name found on line 41
49:     function min(int256 a, int256 b) internal pure returns (int256) {

/// @audit Different function with same name found on line 57
65:     function max(int256 a, int256 b) internal pure returns (int256) {

/// @audit Different function with same name found on line 311
318:     function toInt128(int256 toCast) internal pure returns (int128 downcastedInt) {
/// @audit Different function with same name found on line 419
445:     function convertCollateralData(

/// @audit Different function with same name found on line 490
524:     function convert0to1(int256 amount, uint160 sqrtPriceX96) internal pure returns (int256) {

/// @audit Different function with same name found on line 507
547:     function convert1to0(int256 amount, uint160 sqrtPriceX96) internal pure returns (int256) {
/// @audit Different function with same name found on line 39
46:     function rightSlot(LeftRightSigned self) internal pure returns (int128) {

/// @audit Different function with same name found on line 59
78:     function toRightSlot(

/// @audit Different function with same name found on line 101
108:     function leftSlot(LeftRightSigned self) internal pure returns (int128) {

/// @audit Different function with same name found on line 121
134:     function toLeftSlot(LeftRightSigned self, int128 left) internal pure returns (LeftRightSigned) {

/// @audit Different function with same name found on line 148
194:     function add(LeftRightUnsigned x, LeftRightSigned y) internal pure returns (LeftRightSigned z) {

/// @audit Different function with same name found on line 148
214:     function add(LeftRightSigned x, LeftRightSigned y) internal pure returns (LeftRightSigned z) {

/// @audit Different function with same name found on line 171
232:     function sub(LeftRightSigned x, LeftRightSigned y) internal pure returns (LeftRightSigned z) {
</details>

[N-50] Assembly block creates dirty bits

Writing data to the free memory pointer without later updating the free memory pointer will cause there to be dirty bits at that memory location. Not updating the free memory pointer will make it harder for the optimizer to reason about whether the memory needs to be cleaned before use, which will lead to worse optimizations. Update the free memory pointer and annotate the block (assembly ("memory-safe") { ... }) to avoid this issue.

<details> <summary>There are 2 instances (click to show):</summary>
24:         assembly ("memory-safe") {
25:             // Get free memory pointer - we will store our calldata in scratch space starting at the offset specified here.
26:             let p := mload(0x40)
27: 
28:             // Write the abi-encoded calldata into memory, beginning with the function selector.
29:             mstore(p, 0x23b872dd00000000000000000000000000000000000000000000000000000000)
30:             mstore(add(4, p), from) // Append the "from" argument.
31:             mstore(add(36, p), to) // Append the "to" argument.
32:             mstore(add(68, p), amount) // Append the "amount" argument.
33: 
34:             success := and(
35:                 // Set success to whether the call reverted, if not we check it either
36:                 // returned exactly 1 (can't just be non-zero data), or had no return data.
37:                 or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())),
38:                 // We use 100 because that's the total length of our calldata (4 + 32 * 3)
39:                 // Counterintuitively, this call() must be positioned after the or() in the
40:                 // surrounding and() because and() evaluates its arguments from right to left.
41:                 call(gas(), token, 0, p, 100, 0, 32)
42:             )
43:         }

55:         assembly ("memory-safe") {
56:             // Get free memory pointer - we will store our calldata in scratch space starting at the offset specified here.
57:             let p := mload(0x40)
58: 
59:             // Write the abi-encoded calldata into memory, beginning with the function selector.
60:             mstore(p, 0xa9059cbb00000000000000000000000000000000000000000000000000000000)
61:             mstore(add(4, p), to) // Append the "to" argument.
62:             mstore(add(36, p), amount) // Append the "amount" argument.
63: 
64:             success := and(
65:                 // Set success to whether the call reverted, if not we check it either
66:                 // returned exactly 1 (can't just be non-zero data), or had no return data.
67:                 or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())),
68:                 // We use 68 because that's the total length of our calldata (4 + 32 * 2)
69:                 // Counterintuitively, this call() must be positioned after the or() in the
70:                 // surrounding and() because and() evaluates its arguments from right to left.
71:                 call(gas(), token, 0, p, 68, 0, 32)
72:             )
73:         }
</details>

[N-51] Multiple mappings with same keys can be combined into a single struct mapping for readability

Well-organized data structures make code reviews easier, which may lead to fewer bugs. Consider combining related mappings into mappings to structs, so it's clear what data is related.

<details> <summary>There are 13 instances (click to show):</summary>
238:     mapping(address account => mapping(TokenId tokenId => mapping(uint256 leg => LeftRightUnsigned premiaGrowth)))

245:     mapping(bytes32 chunkKey => LeftRightUnsigned lastGrossPremium) internal s_grossPremiumLast;

251:     mapping(bytes32 chunkKey => LeftRightUnsigned settledTokens) internal s_settledTokens;

258:     mapping(address account => mapping(TokenId tokenId => LeftRightUnsigned balanceAndUtilizations))

272:     mapping(address account => uint256 positionsHash) internal s_positionsHash;
177:     mapping(bytes32 positionKey => LeftRightUnsigned removedAndNetLiquidity)

287:     mapping(bytes32 positionKey => LeftRightUnsigned accountPremium) private s_accountPremiumOwed;

289:     mapping(bytes32 positionKey => LeftRightUnsigned accountPremium) private s_accountPremiumGross;

295:     mapping(bytes32 positionKey => LeftRightSigned baseFees0And1) internal s_accountFeesBase;
66:     mapping(address account => mapping(uint256 tokenId => uint256 balance)) public balanceOf;

71:     mapping(address owner => mapping(address operator => bool approvedForAll))
35:     mapping(address account => uint256 balance) public balanceOf;

39:     mapping(address owner => mapping(address spender => uint256 allowance)) public allowance;
</details>

[N-52] Do not cache immutable variables

Instead of caching immutable variables, using them directly can make code more consistent and less prone to errors.

There is 1 instance:

  • CollateralTracker.sol ( 773-773 ):
773:         uint256 min_sell_ratio = SELLER_COLLATERAL_RATIO;

[N-53] Function state mutability can be restricted to view

Function state mutability can be restricted to view

There is 1 instance:

  • SemiFungiblePositionManager.sol ( 1110-1114 ):
1110:     function _updateStoredPremia(
1111:         bytes32 positionKey,
1112:         LeftRightUnsigned currentLiquidity,
1113:         LeftRightUnsigned collectedAmounts
1114:     ) private {

[N-54] Missing event for critical changes

Events should be emitted when critical changes are made to the contracts.

<details> <summary>There are 9 instances (click to show):</summary>
221:     function startToken(
222:         bool underlyingIsToken0,
223:         address token0,
224:         address token1,
225:         uint24 fee,
226:         PanopticPool panopticPool
227:     ) external {

995:     function takeCommissionAddData(
996:         address optionOwner,
997:         int128 longAmount,
998:         int128 shortAmount,
999:         int128 swappedAmount
1000:     ) external onlyPanopticPool returns (int256 utilization) {
291:     function startPool(
292:         IUniswapV3Pool _univ3pool,
293:         address token0,
294:         address token1,
295:         CollateralTracker collateralTracker0,
296:         CollateralTracker collateralTracker1
297:     ) external {

739:     function _addUserOption(TokenId tokenId, uint64 effectiveLiquidityLimitX32) internal {

859:     function _updatePositionDataBurn(address owner, TokenId tokenId) internal {

1405:     function _updatePositionsHash(address account, TokenId tokenId, bool addFlag) internal {
1406:         // Get the current position hash value (fingerprint of all pre-existing positions created by '_account')
1407:         // Add the current tokenId to the positionsHash as XOR'd
1408:         // since 0 ^ x = x, no problem on first mint
1409:         // Store values back into the user option details with the updated hash (leaves the other parameters unchanged)
1410:         uint256 newHash = PanopticMath.updatePositionsHash(
1411:             s_positionsHash[account],
1412:             tokenId,
1413:             addFlag
1414:         );
1415:         if ((newHash >> 248) > MAX_POSITIONS) revert Errors.TooManyPositionsOpen();
1416:         s_positionsHash[account] = newHash;
1417:     }

1666:     function _updateSettlementPostMint(
1667:         TokenId tokenId,
1668:         LeftRightUnsigned[4] memory collectedByLeg,
1669:         uint128 positionSize
1670:     ) internal {

1833:     function _updateSettlementPostBurn(
1834:         address owner,
1835:         TokenId tokenId,
1836:         LeftRightUnsigned[4] memory collectedByLeg,
1837:         uint128 positionSize,
1838:         bool commitLongSettled
1839:     ) internal returns (LeftRightSigned realizedPremia, LeftRightSigned[4] memory premiaByLeg) {
  • SemiFungiblePositionManager.sol ( 1110-1114 ):
1110:     function _updateStoredPremia(
1111:         bytes32 positionKey,
1112:         LeftRightUnsigned currentLiquidity,
1113:         LeftRightUnsigned collectedAmounts
1114:     ) private {
</details>

[N-55] Non-assembly method available

There are some automated tools that will flag a project as having higher complexity if there is inline-assembly, so it's best to avoid using it where it's not necessary. In addition, most assembly methods can be replaced by non-assembly methods, for example:

  • assembly{ g := gas() } => uint256 g = gasleft()
  • assembly{ id := chainid() } => uint256 id = block.chainid
  • assembly { r := mulmod(a, b, d) } => uint256 m = mulmod(x, y, k)
  • assembly { size := extcodesize() } => uint256 size = address(a).code.length
  • etc.

There are 6 instances:

380:                 remainder := mulmod(a, b, denominator)

494:                 remainder := mulmod(a, b, 0x10000000000000000)

557:                 remainder := mulmod(a, b, 0x1000000000000000000000000)

634:                 remainder := mulmod(a, b, 0x100000000000000000000000000000000)

711:                 remainder := mulmod(a, b, 0x1000000000000000000000000000000000000000000000000)
  • Multicall.sol ( 26-26 ):
26:                     revert(add(result, 32), mload(result))

[N-56] Duplicated require()/revert() checks should be refactored

Refactoring duplicate require()/revert() checks into a modifier or function can make the code more concise, readable and maintainable, and less likely to make errors or omissions when modifying the require() or revert().

<details> <summary>There are 13 instances (click to show):</summary>
/// @audit Duplicated on line 350
331:         if (s_panopticPool.numberOfPositions(msg.sender) != 0) revert Errors.PositionCountNotZero();

/// @audit Duplicated on line 480
418:         if (assets > type(uint104).max) revert Errors.DepositTooLarge();

/// @audit Duplicated on line 596
536:         if (assets > maxWithdraw(owner)) revert Errors.ExceedsMaximumRedemption();
/// @audit Duplicated on line 945, 1163
940:         if (!solventAtFast) revert Errors.NotEnoughCollateral();

/// @audit Duplicated on line 1394
1188:         if (touchedId.length != 1) revert Errors.InputListFail();
/// @audit Duplicated on line 549, 576
322:         if (s_poolContext[poolId].locked) revert Errors.ReentrantCall();

/// @audit Duplicated on line 702
355:         if (univ3pool == address(0)) revert Errors.UniswapPoolNotInitialized();

/// @audit Duplicated on line 639
634:             ) revert Errors.TransferFailed();
/// @audit Duplicated on line 137
101:         if (!(msg.sender == from || isApprovedForAll[from][msg.sender])) revert NotAuthorized();

/// @audit Duplicated on line 168, 227
117:                 revert UnsafeRecipient();
</details>

[N-57] Consider adding emergency-stop functionality

Adding a way to quickly halt protocol functionality in an emergency, rather than having to pause individual contracts one-by-one, will make in-progress hack mitigation faster and much less stressful.

There is 1 instance:

  • SemiFungiblePositionManager.sol ( 72-72 ):
72: contract SemiFungiblePositionManager is ERC1155, Multicall {

[N-58] Missing checks for uint state variable assignments

Consider whether reasonable bounds checks for variables would be useful.

There are 4 instances:

251:         s_poolFee = _poolFee;

1028:             s_poolAssets = uint128(uint256(updatedAssets));
533:         if (medianData != 0) s_miniMedian = medianData;

664:         if (medianData != 0) s_miniMedian = medianData;

[N-59] Use the Modern Upgradeable Contract Paradigm

Modern smart contract development often employs upgradeable contract structures, utilizing proxy patterns like OpenZeppelin’s Upgradeable Contracts. This paradigm separates logic and state, allowing developers to amend and enhance the contract's functionality without altering its state or the deployed contract address. Transitioning to this approach enhances long-term maintainability. Resolution: Adopt a well-established proxy pattern for upgradeability, ensuring proper initialization and employing transparent proxies to mitigate potential risks. Embrace comprehensive testing and audit practices, particularly when updating contract logic, to ensure state consistency and security are preserved across upgrades. This ensures your contract remains robust and adaptable to future requirements.

There are 3 instances:

  • CollateralTracker.sol ( 36 ):
36: contract CollateralTracker is ERC20Minimal, Multicall {
  • PanopticPool.sol ( 27 ):
27: contract PanopticPool is ERC1155Holder, Multicall {
  • SemiFungiblePositionManager.sol ( 72 ):
72: contract SemiFungiblePositionManager is ERC1155, Multicall {

[N-60] Large or complicated code bases should implement invariant tests

This includes: large code bases, or code with lots of inline-assembly, complicated math, or complicated interactions between multiple contracts. Invariant fuzzers such as Echidna require the test writer to come up with invariants which should not be violated under any circumstances, and the fuzzer tests various inputs and function calls to ensure that the invariants always hold. Even code with 100% code coverage can still have bugs due to the order of the operations a user performs, and invariant fuzzers may help significantly.

There is 1 instance:

  • Global finding

[N-61] The default value is manually set when it is declared

In instances where a new variable is defined, there is no need to set it to it's default value.

<details> <summary>There are 31 instances (click to show):</summary>
662:             for (uint256 leg = 0; leg < positionId.countLegs(); ++leg) {

1208:         for (uint256 i = 0; i < totalIterations; ) {

1255:             for (uint256 index = 0; index < numLegs; ++index) {
441:         for (uint256 k = 0; k < pLength; ) {

459:             for (uint256 leg = 0; leg < numLegs; ) {

745:         for (uint256 leg = 0; leg < numLegs; ) {

802:         for (uint256 i = 0; i < positionIdList.length; ) {

864:         for (uint256 leg = 0; leg < numLegs; ) {

1382:         for (uint256 i = 0; i < pLength; ) {

1518:         for (uint256 leg = 0; leg < numLegs; ) {

1672:         for (uint256 leg = 0; leg < numLegs; ++leg) {

1852:         for (uint256 leg = 0; leg < numLegs; ) {
575:         for (uint256 i = 0; i < ids.length; ) {

601:         for (uint256 leg = 0; leg < numLegs; ) {

882:         for (uint256 leg = 0; leg < numLegs; ) {
51:         for (uint256 k = 0; k < positionIdList.length; ) {

55:             for (uint256 leg = 0; leg < numLegs; ) {
137:             for (uint256 i = 0; i < cardinality + 1; ++i) {

148:             for (uint256 i = 0; i < cardinality; ++i) {

248:             for (uint256 i = 0; i < 20; ++i) {

256:             for (uint256 i = 0; i < 19; ++i) {

395:         for (uint256 leg = 0; leg < numLegs; ) {

781:             for (uint256 i = 0; i < positionIdList.length; ++i) {

784:                 for (uint256 leg = 0; leg < numLegs; ++leg) {

860:             for (uint256 i = 0; i < positionIdList.length; i++) {

863:                 for (uint256 leg = 0; leg < tokenId.countLegs(); ++leg) {
  • Multicall.sol ( 14-14 ):
14:         for (uint256 i = 0; i < data.length; ) {
143:         for (uint256 i = 0; i < ids.length; ) {

187:             for (uint256 i = 0; i < owners.length; ++i) {
507:             for (uint256 i = 0; i < 4; ++i) {

581:             for (uint256 i = 0; i < numLegs; ++i) {
</details>

[N-62] Contracts should have all public/external functions exposed by interfaces

All external/public functions should extend an interface. This is useful to ensure that the whole API is extracted and can be more easily integrated by other projects.

There are 4 instances:

  • CollateralTracker.sol ( 36 ):
36: contract CollateralTracker is ERC20Minimal, Multicall {
  • PanopticFactory.sol ( 26 ):
26: contract PanopticFactory is Multicall {
  • PanopticPool.sol ( 27 ):
27: contract PanopticPool is ERC1155Holder, Multicall {
  • SemiFungiblePositionManager.sol ( 72 ):
72: contract SemiFungiblePositionManager is ERC1155, Multicall {

[N-63] Top-level declarations should be separated by at least two lines

There are 2 instances:

2: pragma solidity ^0.8.0;
3: 
4: import {PanopticPool} from "@contracts/PanopticPool.sol";

4: import {PanopticPool} from "@contracts/PanopticPool.sol";
5: 
6: interface IDonorNFT {

[N-64] Consider adding formal verification proofs

Formal verification is the act of proving or disproving the correctness of intended algorithms underlying a system with respect to a certain formal specification/property/invariant, using formal methods of mathematics.

Some tools that are currently available to perform these tests on smart contracts are SMTChecker and Certora Prover.

<details> <summary>There are 20 instances (click to show):</summary> </details>

[N-65] Use scopes sparingly

The use of scoped blocks, denoted by {} without a preceding control structure like if, for, etc., allows for the creation of isolated scopes within a function. While this can be useful for managing memory and preventing naming conflicts, it should be used sparingly. Excessive use of these scope blocks can obscure the code's logic flow and make it more difficult to understand, impeding code maintainability. As a best practice, only employ scoped blocks when necessary for memory management or to avoid clear naming conflicts. Otherwise, aim for clarity and simplicity in your code structure for optimal readability and maintainability.

<details> <summary>There are 21 instances (click to show):</summary>
666:                 {
667:                     int24 range = int24(

686:                 {
687:                     LiquidityChunk liquidityChunk = PanopticMath.getLiquidityChunk(
750:             {
751:                 (uint128 premiumAccumulator0, uint128 premiumAccumulator1) = SFPM.getAccountPremium(

984:         {
985:             int128 paid0 = s_collateralToken0.exercise(

995:         {
996:             int128 paid1 = s_collateralToken1.exercise(

1031:         {
1032:             (, int24 currentTick, , , , , ) = s_univ3pool.slot0();

1078:         {
1079:             LeftRightSigned netExchanged;

1204:         {
1205:             // add the premia to the delegated amounts to ensure the user has enough collateral to exercise

1601:         {
1602:             (int24 tickLower, int24 tickUpper) = tokenId.asTicks(legIndex);
887:             {
888:                 // cache the univ3pool, tokenId, isBurn, and _positionSize variables to get rid of stack too deep error

989:         {
990:             // did we have liquidity already deployed in Uniswap for this chunk range from some past mint?

1046:         {
1047:             /** if the position is NOT long (selling a put or a call), then _mintLiquidity to move liquidity

1345:             {
1346:                 uint128 collected0 = collectedAmounts.rightSlot();

1362:             {
1363:                 uint128 premium0X64_owed;

1365:                 {
1366:                     // compute the owed premium (from Eqn 3)

1383:             {
1384:                 uint128 premium0X64_gross;

1386:                 {
1387:                     // compute the gross premium (from Eqn 4)

1471:                 {
1472:                     IUniswapV3Pool _univ3pool = IUniswapV3Pool(univ3pool);
185:                 {
186:                     (uint256 timestamp_old, int56 tickCumulative_old, , ) = univ3pool.observations(

661:             {
662:                 // compute the ratio of token0 to total collateral requirements

854:             {
855:                 address _liquidatee = liquidatee;
</details>

[N-66] Prevent re-setting a state variable with the same value

This especially problematic when the setter also emits the same value, which may be confusing to offline parsers.

<details> <summary>There are 15 instances (click to show):</summary>
234:         totalSupply = 10 ** 6;

241:         s_underlyingToken = underlyingIsToken0 ? token0 : token1;

244:         s_panopticPool = panopticPool;

251:         s_poolFee = _poolFee;

254:         s_univ3token0 = token0;

255:         s_univ3token1 = token1;

258:         s_underlyingIsToken0 = underlyingIsToken0;

262:             s_ITMSpreadFee = uint128((ITM_SPREAD_MULTIPLIER * _poolFee) / DECIMALS);

1028:             s_poolAssets = uint128(uint256(updatedAssets));
302:         s_univ3pool = IUniswapV3Pool(_univ3pool);

308:             s_miniMedian =
309:                 (uint256(block.timestamp) << 216) +
310:                 // magic number which adds (7,5,3,1,0,2,4,6) order and minTick in positions 7, 5, 3 and maxTick in 6, 4, 2
311:                 // see comment on s_miniMedian initialization for format of this magic number
312:                 (uint256(0xF590A6F276170D89E9F276170D89E9F276170D89E9000000000000)) +
313:                 (uint256(uint24(currentTick)) << 24) + // add to slot 4
314:                 (uint256(uint24(currentTick))); // add to slot 3

318:         s_collateralToken0 = collateralTracker0;

319:         s_collateralToken1 = collateralTracker1;

533:         if (medianData != 0) s_miniMedian = medianData;

664:         if (medianData != 0) s_miniMedian = medianData;
</details>

[N-67] Whitespace in Expressions

See the whitespace section of the Solidity Style Guide.

<details> <summary>There are 98 instances (click to show):</summary>
  • CollateralTracker.sol ( 1208 ):
/// @audit Whitespace inside parenthesis
1208:         for (uint256 i = 0; i < totalIterations; ) {
  • PanopticFactory.sol ( 303, 341 ):
/// @audit Whitespace inside parenthesis
303:         for (; uint256(salt) < maxSalt; ) {

/// @audit Whitespace inside parenthesis
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
341:         (uint160 currentSqrtPriceX96, , , , , , ) = v3Pool.slot0();
/// @audit Whitespace inside parenthesis
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
304:         (, int24 currentTick, , , , , ) = IUniswapV3Pool(_univ3pool).slot0();

/// @audit Whitespace inside parenthesis
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
339:         (uint160 currentSqrtPriceX96, , , , , , ) = s_univ3pool.slot0();

/// @audit Whitespace inside parenthesis
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
387:         (, int24 currentTick, , , , , ) = s_univ3pool.slot0();

/// @audit Whitespace inside parenthesis
441:         for (uint256 k = 0; k < pLength; ) {

/// @audit Whitespace inside parenthesis
459:             for (uint256 leg = 0; leg < numLegs; ) {

/// @audit Whitespace inside parenthesis
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
523:         (, , uint16 observationIndex, uint16 observationCardinality, , , ) = s_univ3pool.slot0();

/// @audit Whitespace inside parenthesis
745:         for (uint256 leg = 0; leg < numLegs; ) {

/// @audit Whitespace inside parenthesis
802:         for (uint256 i = 0; i < positionIdList.length; ) {

/// @audit Whitespace inside parenthesis
864:         for (uint256 leg = 0; leg < numLegs; ) {

/// @audit Whitespace before a comma
897:             ,

/// @audit Whitespace before a comma
901:             ,

/// @audit Whitespace before a comma
902:             ,

/// @audit Whitespace inside parenthesis
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
1032:             (, int24 currentTick, , , , , ) = s_univ3pool.slot0();

/// @audit Whitespace inside parenthesis
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
1094:             (, finalTick, , , , , ) = s_univ3pool.slot0();

/// @audit Whitespace inside parenthesis
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
1202:         (, int24 currentTick, , , , , ) = s_univ3pool.slot0();

/// @audit Whitespace inside parenthesis
1206:             (LeftRightSigned positionPremia, ) = _calculateAccumulatedPremia(

/// @audit Whitespace inside parenthesis
1382:         for (uint256 i = 0; i < pLength; ) {

/// @audit Whitespace inside parenthesis
1518:         for (uint256 leg = 0; leg < numLegs; ) {

/// @audit Whitespace inside parenthesis
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
1598:         (, int24 currentTick, , , , , ) = s_univ3pool.slot0();

/// @audit Whitespace inside parenthesis
1852:         for (uint256 leg = 0; leg < numLegs; ) {
/// @audit Whitespace inside parenthesis
575:         for (uint256 i = 0; i < ids.length; ) {

/// @audit Whitespace inside parenthesis
601:         for (uint256 leg = 0; leg < numLegs; ) {

/// @audit Whitespace inside parenthesis
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
725:         (, int24 currentTick, , , , , ) = univ3pool.slot0();

/// @audit Whitespace inside parenthesis
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
788:                 (uint160 sqrtPriceX96, , , , , , ) = _univ3pool.slot0();

/// @audit Whitespace inside parenthesis
882:         for (uint256 leg = 0; leg < numLegs; ) {

/// @audit Whitespace inside parenthesis
/// @audit Whitespace before a comma
1148:         (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, , ) = univ3pool
/// @audit Whitespace inside parenthesis
51:         for (uint256 k = 0; k < positionIdList.length; ) {

/// @audit Whitespace inside parenthesis
55:             for (uint256 leg = 0; leg < numLegs; ) {

/// @audit Whitespace inside parenthesis
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
142:         (, , uint256 lowerOut0, uint256 lowerOut1, , , , ) = univ3pool.ticks(tickLower);

/// @audit Whitespace inside parenthesis
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
/// @audit Whitespace before a comma
143:         (, , uint256 upperOut0, uint256 upperOut1, , , , ) = univ3pool.ticks(tickUpper);
/// @audit Whitespace inside parenthesis
/// @audit Whitespace before a comma
138:                 (timestamps[i], tickCumulatives[i], , ) = univ3pool.observations(

/// @audit Whitespace inside parenthesis
/// @audit Whitespace before a comma
186:                     (uint256 timestamp_old, int56 tickCumulative_old, , ) = univ3pool.observations(

/// @audit Whitespace inside parenthesis
/// @audit Whitespace before a comma
192:                     (uint256 timestamp_last, int56 tickCumulative_last, , ) = univ3pool

/// @audit Whitespace inside parenthesis
253:             (int56[] memory tickCumulatives, ) = univ3pool.observe(secondsAgos);

/// @audit Whitespace inside parenthesis
395:         for (uint256 leg = 0; leg < numLegs; ) {
  • Multicall.sol ( 14 ):
/// @audit Whitespace inside parenthesis
14:         for (uint256 i = 0; i < data.length; ) {
  • ERC1155Minimal.sol ( 143 ):
/// @audit Whitespace inside parenthesis
143:         for (uint256 i = 0; i < ids.length; ) {
</details>

#0 - c4-judge

2024-04-26T10:34:32Z

Picodes marked the issue as grade-b

#1 - c4-judge

2024-04-26T17:17:10Z

Picodes marked the issue as grade-a

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