ETH Price: $3,140.83 (+0.07%)
 

More Info

Private Name Tags

Multichain Info

No addresses found
Transaction Hash
Method
Block
From
To
Burn Options348022372025-08-28 15:10:2198 days ago1756393821IN
0x36a3088B...5C6354f27
0 ETH0.00000170.0056492
Burn Options347999272025-08-28 13:53:2198 days ago1756389201IN
0x36a3088B...5C6354f27
0 ETH0.000002910.00779936
Burn Options347997932025-08-28 13:48:5398 days ago1756388933IN
0x36a3088B...5C6354f27
0 ETH0.000002540.00626198
Burn Options347982712025-08-28 12:58:0998 days ago1756385889IN
0x36a3088B...5C6354f27
0 ETH0.000004240.01335292
Burn Options347966452025-08-28 12:03:5798 days ago1756382637IN
0x36a3088B...5C6354f27
0 ETH0.000002840.00932473
Burn Options347666522025-08-27 19:24:1199 days ago1756322651IN
0x36a3088B...5C6354f27
0 ETH0.000007990.02553234
Burn Options347665522025-08-27 19:20:5199 days ago1756322451IN
0x36a3088B...5C6354f27
0 ETH0.000010610.02515267
Burn Options347665412025-08-27 19:20:2999 days ago1756322429IN
0x36a3088B...5C6354f27
0 ETH0.000011170.02389294
Burn Options347664742025-08-27 19:18:1599 days ago1756322295IN
0x36a3088B...5C6354f27
0 ETH0.000005830.0275279
Burn Options347664622025-08-27 19:17:5199 days ago1756322271IN
0x36a3088B...5C6354f27
0 ETH0.000015160.02468119
Burn Options347664492025-08-27 19:17:2599 days ago1756322245IN
0x36a3088B...5C6354f27
0 ETH0.00001090.02383231
Multicall347600432025-08-27 15:43:5399 days ago1756309433IN
0x36a3088B...5C6354f27
0 ETH0.000018030.02133206
Mint Options347486232025-08-27 9:23:1399 days ago1756286593IN
0x36a3088B...5C6354f27
0 ETH0.000001960.00311354
Mint Options347475552025-08-27 8:47:3799 days ago1756284457IN
0x36a3088B...5C6354f27
0 ETH0.000000720.00181011
Force Exercise347475322025-08-27 8:46:5199 days ago1756284411IN
0x36a3088B...5C6354f27
0 ETH0.000000640.00163844
Force Exercise347474872025-08-27 8:45:2199 days ago1756284321IN
0x36a3088B...5C6354f27
0 ETH0.000000760.00158549
Force Exercise347474792025-08-27 8:45:0599 days ago1756284305IN
0x36a3088B...5C6354f27
0 ETH0.000000880.00158299
Force Exercise347474702025-08-27 8:44:4799 days ago1756284287IN
0x36a3088B...5C6354f27
0 ETH0.000000720.00161853
Mint Options347429242025-08-27 6:13:1599 days ago1756275195IN
0x36a3088B...5C6354f27
0 ETH0.000000830.0014854
Burn Options347242812025-08-26 19:51:49100 days ago1756237909IN
0x36a3088B...5C6354f27
0 ETH0.000000880.00236589
Multicall347189352025-08-26 16:53:37100 days ago1756227217IN
0x36a3088B...5C6354f27
0 ETH0.000003070.00433196
Multicall347189042025-08-26 16:52:35100 days ago1756227155IN
0x36a3088B...5C6354f27
0 ETH0.000003580.00427391
Multicall347188482025-08-26 16:50:43100 days ago1756227043IN
0x36a3088B...5C6354f27
0 ETH0.000003180.00378291
Burn Options347133192025-08-26 13:46:25100 days ago1756215985IN
0x36a3088B...5C6354f27
0 ETH0.000007130.01819577
Burn Options347133122025-08-26 13:46:11100 days ago1756215971IN
0x36a3088B...5C6354f27
0 ETH0.000006880.01831439
View all transactions

Latest 10 internal transactions

Parent Transaction Hash Block From To
341642772025-08-13 20:45:01113 days ago1755117901
0x36a3088B...5C6354f27
0.42117205 ETH
341639092025-08-13 20:32:45113 days ago1755117165
0x36a3088B...5C6354f27
0.29357386 ETH
341638532025-08-13 20:30:53113 days ago1755117053
0x36a3088B...5C6354f27
0.09699864 ETH
341638202025-08-13 20:29:47113 days ago1755116987
0x36a3088B...5C6354f27
0.25694493 ETH
339759832025-08-09 12:08:33117 days ago1754741313
0x36a3088B...5C6354f27
0.20530226 ETH
332563512025-07-23 20:20:49134 days ago1753302049
0x36a3088B...5C6354f27
2.56118547 ETH
332529102025-07-23 18:26:07134 days ago1753295167
0x36a3088B...5C6354f27
0.21185041 ETH
332478262025-07-23 15:36:39134 days ago1753284999
0x36a3088B...5C6354f27
0.23953167 ETH
316529932025-06-16 17:35:33171 days ago1750095333
0x36a3088B...5C6354f27
0.04 ETH
295483732025-04-29 0:21:33219 days ago1745886093  Contract Creation0 ETH

Cross-Chain Transactions
Loading...
Loading

Minimal Proxy Contract for 0x0000000000035d9945bf4d24393828e920376bae

Contract Name:
PanopticPool

Compiler Version
v0.8.28+commit.7893614a

Optimization Enabled:
Yes with 1 runs

Other Settings:
cancun EvmVersion

Contract Source Code (Solidity Standard Json-Input format)

File 1 of 50 : PanopticPool.sol
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.24;

// Interfaces
import {CollateralTracker} from "@contracts/CollateralTracker.sol";
import {SemiFungiblePositionManager} from "@contracts/SemiFungiblePositionManager.sol";
import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol";
import {IV3CompatibleOracle} from "@interfaces/IV3CompatibleOracle.sol";
// Inherited implementations
import {Clone} from "clones-with-immutable-args/Clone.sol";
import {Multicall} from "@base/Multicall.sol";
// Libraries
import {Constants} from "@libraries/Constants.sol";
import {Errors} from "@libraries/Errors.sol";
import {Math} from "@libraries/Math.sol";
import {PanopticMath} from "@libraries/PanopticMath.sol";
import {V4StateReader} from "@libraries/V4StateReader.sol";
// Custom types
import {LeftRightUnsigned, LeftRightSigned} from "@types/LeftRight.sol";
import {LiquidityChunk} from "@types/LiquidityChunk.sol";
import {PoolKey} from "v4-core/types/PoolKey.sol";
import {PoolId} from "v4-core/types/PoolId.sol";
import {PositionBalance, PositionBalanceLibrary} from "@types/PositionBalance.sol";
import {TokenId} from "@types/TokenId.sol";

/// @title The Panoptic Pool: Create permissionless options on a CLAMM.
/// @author Axicon Labs Limited
/// @notice Manages positions, collateral, liquidations and forced exercises.
contract PanopticPool is Clone, Multicall {
    /*//////////////////////////////////////////////////////////////
                                EVENTS
    //////////////////////////////////////////////////////////////*/

    /// @notice Emitted when an account is liquidated.
    /// @param liquidator Address of the caller liquidating the distressed account
    /// @param liquidatee Address of the distressed/liquidatable account
    /// @param bonusAmounts LeftRight encoding for the the bonus paid for currency 0 (right slot) and 1 (left slot) to the liquidator
    event AccountLiquidated(
        address indexed liquidator,
        address indexed liquidatee,
        LeftRightSigned bonusAmounts
    );

    /// @notice Emitted when a position is force exercised.
    /// @param exercisor Address of the account that forces the exercise of the position
    /// @param user Address of the owner of the liquidated position
    /// @param tokenId TokenId of the liquidated position
    /// @param exerciseFee LeftRight encoding for the cost paid by the exercisor to force the exercise of the token;
    /// the cost for currency 0 (right slot) and 1 (left slot) is represented as negative
    event ForcedExercised(
        address indexed exercisor,
        address indexed user,
        TokenId indexed tokenId,
        LeftRightSigned exerciseFee
    );

    /// @notice Emitted when premium is settled independent of a mint/burn (e.g. during `settleLongPremium`).
    /// @param user Address of the owner of the settled position
    /// @param tokenId TokenId of the settled position
    /// @param legIndex The leg index of `tokenId` that the premium was settled for
    /// @param settledAmounts LeftRight encoding for the amount of premium settled for currency0 (right slot) and currency1 (left slot)
    event PremiumSettled(
        address indexed user,
        TokenId indexed tokenId,
        uint256 legIndex,
        LeftRightSigned settledAmounts
    );

    /// @notice Emitted when an option is burned.
    /// @param recipient User that burnt the option
    /// @param positionSize The number of contracts burnt, expressed in terms of the asset
    /// @param tokenId TokenId of the burnt option
    /// @param premiaByLeg LeftRight packing for the amount of premia collected for currency0 (right) and currency1 (left) for each leg of `tokenId`
    event OptionBurnt(
        address indexed recipient,
        uint128 positionSize,
        TokenId indexed tokenId,
        LeftRightSigned[4] premiaByLeg
    );

    /// @notice Emitted when an option is minted.
    /// @param recipient User that minted the option
    /// @param tokenId TokenId of the created option
    /// @param balanceData The `PositionBalance` data for `tokenId` containing the number of contracts, pool utilizations, and ticks at mint
    /// @param commissions The total amount of commissions (base rate + ITM spread) paid for token0 (right) and token1 (left)
    event OptionMinted(
        address indexed recipient,
        TokenId indexed tokenId,
        PositionBalance balanceData,
        LeftRightUnsigned commissions
    );

    /*//////////////////////////////////////////////////////////////
                         IMMUTABLES & CONSTANTS
    //////////////////////////////////////////////////////////////*/

    /// @notice Lower price bound used when no slippage check is required.
    int24 internal constant MIN_SWAP_TICK = Constants.MIN_V4POOL_TICK - 1;

    /// @notice Upper price bound used when no slippage check is required.
    int24 internal constant MAX_SWAP_TICK = Constants.MAX_V4POOL_TICK + 1;

    /// @notice Flag that signals to compute premia for both the short and long legs of a position.
    bool internal constant COMPUTE_PREMIA_AS_COLLATERAL = true;

    /// @notice Flag that indicates only to include the share of (settled) premium that is available to collect when calling `_calculateAccumulatedPremia`.
    bool internal constant ONLY_AVAILABLE_PREMIUM = false;

    /// @notice Flag that signals to commit both collected Uniswap fees and settled long premium to `s_settledTokens`.
    bool internal constant COMMIT_LONG_SETTLED = true;
    /// @notice Flag that signals to only commit collected Uniswap fees to `s_settledTokens`.
    bool internal constant DONOT_COMMIT_LONG_SETTLED = false;

    /// @notice Flag for `_checkSolvency` to indicate that an account should be solvent at all input ticks.
    bool internal constant ASSERT_SOLVENCY = true;

    /// @notice Flag for `_checkSolvency` to indicate that an account should be insolvent at all input ticks.
    bool internal constant ASSERT_INSOLVENCY = false;

    /// @notice Flag that signals to add a new position to the user's positions hash (as opposed to removing an existing position).
    bool internal constant ADD = true;

    /// @notice The minimum window (in seconds) used to calculate the TWAP price for solvency checks during liquidations.
    uint32 internal constant TWAP_WINDOW = 600;

    /// @notice The maximum allowed delta between the currentTick and the Uniswap TWAP tick during a liquidation (~5% down, ~5.26% up).
    /// @dev Mitigates manipulation of the currentTick that causes positions to be liquidated at a less favorable price.
    int256 internal constant MAX_TWAP_DELTA_LIQUIDATION = 513;

    /// @notice The maximum allowed delta (~2%) between the lastObservedTick and the slowOracleTick/fastOracleTick during forceExercise/settleLongPremium.
    /// @dev Ensures token substitution between two accounts is settled safely at a price close to market.
    int256 internal constant MAX_TICK_DELTA_SUBSTITUTION = 203;

    /// @notice The maximum allowed cumulative delta between the fast & slow oracle tick, the current & slow oracle tick, and the last-observed & slow oracle tick.
    /// @dev Falls back on the more conservative (less solvent) tick during times of extreme volatility, where the price moves ~10% in <4 minutes.
    int256 internal constant MAX_TICKS_DELTA = 953;

    /// @notice The maximum allowed ratio for a single chunk, defined as `removedLiquidity / netLiquidity`.
    /// @dev The long premium spread multiplier that corresponds with the MAX_SPREAD value depends on VEGOID,
    /// which can be explored in this calculator: [https://www.desmos.com/calculator/mdeqob2m04](https://www.desmos.com/calculator/mdeqob2m04).
    uint256 internal constant MAX_SPREAD = 9 * (2 ** 32);

    /// @notice The maximum allowed number of legs across all open positions for a user.
    uint64 internal constant MAX_OPEN_LEGS = 25;

    /// @notice Multiplier in basis points for the collateral requirement in the event of a buying power decrease, such as minting or force exercising another user.
    uint256 internal constant BP_DECREASE_BUFFER = 13_333;

    /// @notice Multiplier for the collateral requirement in the general case.
    uint256 internal constant NO_BUFFER = 10_000;

    /// @notice The "engine" of Panoptic - manages AMM liquidity and executes all mints/burns/exercises.
    SemiFungiblePositionManager internal immutable SFPM;

    /// @notice The canonical Uniswap V4 Pool Manager address.
    IPoolManager internal immutable POOL_MANAGER_V4;

    /*//////////////////////////////////////////////////////////////
                                STORAGE
    //////////////////////////////////////////////////////////////*/
    /// @notice Stores a sorted set of 8 price observations used to compute the internal median oracle price.
    // The data for the last 8 interactions is stored as such:
    // LAST UPDATED BLOCK TIMESTAMP (40 bits)
    // [BLOCK.TIMESTAMP]
    // (00000000000000000000000000000000) // dynamic
    //
    // ORDERING of tick indices least --> greatest (24 bits)
    // The value of the bit codon ([#]) is a pointer to a tick index in the tick array.
    // The position of the bit codon from most to least significant is the ordering of the
    // tick index it points to from least to greatest.
    //
    // rank:  0   1   2   3   4   5   6   7
    // slot: [7] [5] [3] [1] [0] [2] [4] [6]
    //       111 101 011 001 000 010 100 110
    //
    // [Constants.MIN_V4POOL_TICK-1] [7]
    // 111100100111011000010111
    //
    // [Constants.MAX_V4POOL_TICK+1] [0]
    // 000011011000100111101001
    //
    // [Constants.MIN_V4POOL_TICK-1] [6]
    // 111100100111011000010111
    //
    // [Constants.MAX_V4POOL_TICK+1] [1]
    // 000011011000100111101001
    //
    // [Constants.MIN_V4POOL_TICK-1] [5]
    // 111100100111011000010111
    //
    // [Constants.MAX_V4POOL_TICK+1] [2]
    // 000011011000100111101001
    //
    // [CURRENT TICK] [4]
    // (000000000000000000000000) // dynamic
    //
    // [CURRENT TICK] [3]
    // (000000000000000000000000) // dynamic
    uint256 internal s_miniMedian;

    /// @notice Nested mapping that tracks the option formation: address => tokenId => leg => premiaGrowth.
    /// @dev Premia growth is taking a snapshot of the chunk premium in SFPM, which is measuring the amount of fees
    /// collected for every chunk per unit of liquidity (net or short, depending on the isLong value of the specific leg index).
    mapping(address account => mapping(TokenId tokenId => mapping(uint256 leg => LeftRightUnsigned premiaGrowth)))
        internal s_options;

    /// @notice Per-chunk `last` value that gives the aggregate amount of premium owed to all sellers when multiplied by the total amount of liquidity `totalLiquidity`.
    /// @dev `totalGrossPremium = totalLiquidity * (grossPremium(perLiquidityX64) - lastGrossPremium(perLiquidityX64)) / 2**64`
    /// @dev Used to compute the denominator for the fraction of premium available to sellers to collect.
    /// @dev LeftRight - right slot is currency0, left slot is currency1.
    mapping(bytes32 chunkKey => LeftRightUnsigned lastGrossPremium) internal s_grossPremiumLast;

    /// @notice Per-chunk accumulator for tokens owed to sellers that have been settled and are now available.
    /// @dev This number increases when buyers pay long premium and when tokens are collected from Uniswap.
    /// @dev It decreases when sellers close positions and collect the premium they are owed.
    /// @dev LeftRight - right slot is currency0, left slot is currency1.
    mapping(bytes32 chunkKey => LeftRightUnsigned settledTokens) internal s_settledTokens;

    /// @notice Tracks the position size of a tokenId for a given user, and the pool utilizations and oracle tick values at the time of last mint.
    //    <-- 24 bits --> <-- 24 bits --> <-- 24 bits --> <-- 24 bits --> <-- 16 bits --> <-- 16 bits --> <-- 128 bits -->
    //   lastObservedTick  slowOracleTick  fastOracleTick   currentTick     utilization1    utilization0    positionSize
    mapping(address account => mapping(TokenId tokenId => PositionBalance positionBalance))
        internal s_positionBalance;

    /// @notice Tracks the position list hash (i.e `keccak256(XORs of abi.encodePacked(positionIdList))`).
    /// @dev A component of this hash also tracks the total number of legs across all positions (i.e. makes sure the length of the provided positionIdList matches).
    /// @dev The purpose of this system is to reduce storage usage when a user has more than one active position.
    /// @dev Instead of having to manage an unwieldy storage array and do lots of loads, we just store a hash of the array.
    /// @dev This hash can be cheaply verified on every operation with a user provided positionIdList - which can then be used for operations
    /// without having to every load any other data from storage.
    //      numLegs                   user positions hash
    //  |<-- 8 bits -->|<------------------ 248 bits ------------------->|
    //  |<---------------------- 256 bits ------------------------------>|
    mapping(address account => uint256 positionsHash) internal s_positionsHash;

    /*//////////////////////////////////////////////////////////////
                   POOL-SPECIFIC IMMUTABLE PARAMETERS
    //////////////////////////////////////////////////////////////*/

    // The parameters will be encoded in calldata at `_getImmutableArgsOffset()` as follows:
    // abi.encodePacked(address collateralToken0, address collateralToken1, address oracleContract, uint256 poolId, abi.encode(PoolKey poolKey))
    // bytes: 0                    20                   40                   60                   92
    //        |<---- 160 bits ---->|<---- 160 bits ---->|<---- 160 bits ---->|<---- 256 bits ---->|<---- 1280 bits ---->|
    //           collateralToken0     collateralToken1      oracleContract           poolId               poolKey

    /// @notice Get the collateral token corresponding to currency0 of the Uniswap pool.
    /// @return Collateral token corresponding to currency0 in Uniswap
    function collateralToken0() public pure returns (CollateralTracker) {
        return CollateralTracker(_getArgAddress(0));
    }

    /// @notice Get the collateral token corresponding to currency1 of the Uniswap pool.
    /// @return Collateral token corresponding to currency1 in Uniswap
    function collateralToken1() public pure returns (CollateralTracker) {
        return CollateralTracker(_getArgAddress(20));
    }

    /// @notice Get the address of the external oracle contract used by this Panoptic Pool.
    /// @return The external oracle contract used by this Panoptic Pool
    function oracleContract() public pure returns (IV3CompatibleOracle) {
        return IV3CompatibleOracle(_getArgAddress(40));
    }

    /// @notice Get the Uniswap Pool ID for the V4 pool used by this Panoptic Pool (hash of `poolKey`).
    /// @return The Uniswap V4 Pool ID for this Panoptic Pool
    function _V4PoolId() internal pure returns (PoolId) {
        return PoolId.wrap(bytes32(_getArgUint256(60)));
    }

    /// @notice Get the pool key for the Uniswap V4 pool used by this Panoptic Pool.
    /// @return key The Uniswap V4 Pool Key for this Panoptic Pool
    function poolKey() public pure returns (PoolKey calldata key) {
        uint256 offset = _getImmutableArgsOffset();

        assembly {
            key := add(offset, 92)
        }
    }

    /*//////////////////////////////////////////////////////////////
                             INITIALIZATION
    //////////////////////////////////////////////////////////////*/

    /// @notice Store the address of the canonical SemiFungiblePositionManager (SFPM) and Uniswap V4 pool manager contracts.
    /// @param _sfpm The address of the SFPM
    /// @param _poolManager The address of the canonical Uniswap V4 pool manager
    constructor(SemiFungiblePositionManager _sfpm, IPoolManager _poolManager) {
        SFPM = _sfpm;
        POOL_MANAGER_V4 = _poolManager;
    }

    /// @notice Initializes the median oracle of a new `PanopticPool` instance with median oracle state and performs initial token approvals.
    /// @dev Must be called first (by the factory contract) before any transaction can occur.
    function initialize() external {
        // reverts if this contract has already been initialized (assuming block.timestamp > 0)
        if (s_miniMedian != 0) revert Errors.PoolAlreadyInitialized();

        (, int24 currentTick, , , , , ) = oracleContract().slot0();

        // Store the median data
        unchecked {
            s_miniMedian =
                (uint256(block.timestamp) << 216) +
                // 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
                // see comment on s_miniMedian initialization for format of this magic number
                (uint256(0xF590A6F276170D89E9F276170D89E9F276170D89E9000000000000)) +
                (uint256(uint24(currentTick)) << 24) + // add to slot 1 (rank 3)
                (uint256(uint24(currentTick))); // add to slot 0 (rank 4)
        }

        POOL_MANAGER_V4.setOperator(address(SFPM), true);
        POOL_MANAGER_V4.setOperator(address(collateralToken0()), true);
        POOL_MANAGER_V4.setOperator(address(collateralToken1()), true);
    }

    /*//////////////////////////////////////////////////////////////
                              EIP SUPPORT
    //////////////////////////////////////////////////////////////*/

    // note: this contract does not need to accept batch ERC1155 transfers from the SFPM or supply ERC-165 calls
    // thus, `supportsInterface` and `onERC1155BatchReceived` are left unimplemented to reduce contract size

    /// @notice Returns magic value when called by the `SemiFungiblePositionManager` contract to indicate that this contract supports ERC1155.
    function onERC1155Received(
        address,
        address,
        uint256,
        uint256,
        bytes memory
    ) external pure returns (bytes4) {
        return this.onERC1155Received.selector;
    }

    /*//////////////////////////////////////////////////////////////
                             QUERY HELPERS
    //////////////////////////////////////////////////////////////*/

    /// @notice Reverts if the caller has a lower collateral balance than required to meet the provided `minValue0` and `minValue1`.
    /// @dev Can be used for composable slippage checks with `multicall` (such as for a force exercise or liquidation).
    /// @param minValue0 The minimum acceptable `currency0` value of collateral
    /// @param minValue1 The minimum acceptable `currency1` value of collateral
    function assertMinCollateralValues(uint256 minValue0, uint256 minValue1) external view {
        CollateralTracker ct0 = collateralToken0();
        CollateralTracker ct1 = collateralToken1();
        if (
            ct0.convertToAssets(ct0.balanceOf(msg.sender)) < minValue0 ||
            ct1.convertToAssets(ct1.balanceOf(msg.sender)) < minValue1
        ) revert Errors.AccountInsolvent();
    }

    /// @notice Determines if account is eligible to withdraw or transfer collateral.
    /// @dev Checks whether account is solvent with `BP_DECREASE_BUFFER` according to `_validateSolvency`.
    /// @dev Prevents insolvent and near-insolvent accounts from withdrawing collateral before they are liquidated.
    /// @dev Reverts if account is not solvent with `BP_DECREASE_BUFFER`.
    /// @param user The account to check for collateral withdrawal eligibility
    /// @param positionIdList The list of all option positions held by `user`
    /// @param usePremiaAsCollateral Whether to compute accumulated premia for all legs held by the user for collateral (true), or just owed premia for long legs (false)
    function validateCollateralWithdrawable(
        address user,
        TokenId[] calldata positionIdList,
        bool usePremiaAsCollateral
    ) external view {
        _validateSolvency(user, positionIdList, BP_DECREASE_BUFFER, usePremiaAsCollateral);
    }

    /// @notice Returns the total amount of premium accumulated for a list of positions and a list containing the corresponding `PositionBalance` information for each position.
    /// @param user Address of the user that owns the positions
    /// @param positionIdList List of positions. Written as `[tokenId1, tokenId2, ...]`
    /// @param includePendingPremium If true, include premium that is owed to the user but has not yet settled; if false, only include premium that is available to collect
    /// @return The total amount of premium owed (which may `includePendingPremium`) to the short legs in `positionIdList` (currency0: right slot, currency1: left slot)
    /// @return The total amount of premium owed by the long legs in `positionIdList` (currency0: right slot, currency1: left slot)
    /// @return A list of `PositionBalance` data (balance and pool utilization/oracle ticks at last mint) for each position, of the form `[[tokenId0, PositionBalance_0], [tokenId1, PositionBalance_1], ...]`
    function getAccumulatedFeesAndPositionsData(
        address user,
        bool includePendingPremium,
        TokenId[] calldata positionIdList
    ) external view returns (LeftRightUnsigned, LeftRightUnsigned, uint256[2][] memory) {
        // Compute the accumulated premia for all tokenId in positionIdList (includes short+long premium)
        return
            _calculateAccumulatedPremia(
                user,
                positionIdList,
                COMPUTE_PREMIA_AS_COLLATERAL,
                includePendingPremium,
                V4StateReader.getTick(POOL_MANAGER_V4, _V4PoolId())
            );
    }

    /// @notice Calculate the accumulated premia owed from the option buyer to the option seller.
    /// @param user The holder of options
    /// @param positionIdList The list of all option positions held by user
    /// @param usePremiaAsCollateral Whether to compute accumulated premia for all legs held by the user for collateral (true), or just owed premia for long legs (false)
    /// @param includePendingPremium If true, include premium that is owed to the user but has not yet settled; if false, only include premium that is available to collect
    /// @param atTick The current tick of the Uniswap pool
    /// @return shortPremium The total amount of premium owed (which may `includePendingPremium`) to the short legs in `positionIdList` (currency0: right slot, currency1: left slot)
    /// @return longPremium The total amount of premium owed by the long legs in `positionIdList` (currency0: right slot, currency1: left slot)
    /// @return balances A list of balances and pool utilization for each position, of the form `[[tokenId0, balances0], [tokenId1, balances1], ...]`
    function _calculateAccumulatedPremia(
        address user,
        TokenId[] calldata positionIdList,
        bool usePremiaAsCollateral,
        bool includePendingPremium,
        int24 atTick
    )
        internal
        view
        returns (
            LeftRightUnsigned shortPremium,
            LeftRightUnsigned longPremium,
            uint256[2][] memory balances
        )
    {
        balances = new uint256[2][](positionIdList.length);

        // loop through each option position/tokenId
        for (uint256 k = 0; k < positionIdList.length; ) {
            TokenId tokenId = positionIdList[k];

            balances[k] = [
                TokenId.unwrap(tokenId),
                PositionBalance.unwrap(s_positionBalance[user][tokenId])
            ];

            (
                LeftRightSigned[4] memory premiaByLeg,
                uint256[2][4] memory premiumAccumulatorsByLeg
            ) = _getPremia(
                    tokenId,
                    LeftRightUnsigned.wrap(balances[k][1]).rightSlot(),
                    user,
                    usePremiaAsCollateral,
                    atTick
                );

            uint256 numLegs = tokenId.countLegs();
            for (uint256 leg = 0; leg < numLegs; ) {
                if (tokenId.isLong(leg) == 0) {
                    if (!includePendingPremium) {
                        bytes32 chunkKey = keccak256(
                            abi.encodePacked(
                                tokenId.strike(leg),
                                tokenId.width(leg),
                                tokenId.tokenType(leg)
                            )
                        );

                        (uint256 totalLiquidity, , ) = _getLiquidities(tokenId, leg);
                        shortPremium = shortPremium.add(
                            _getAvailablePremium(
                                totalLiquidity,
                                s_settledTokens[chunkKey],
                                s_grossPremiumLast[chunkKey],
                                LeftRightUnsigned.wrap(
                                    uint256(LeftRightSigned.unwrap(premiaByLeg[leg]))
                                ),
                                premiumAccumulatorsByLeg[leg]
                            )
                        );
                    } else {
                        shortPremium = shortPremium.add(
                            LeftRightUnsigned.wrap(
                                uint256(LeftRightSigned.unwrap(premiaByLeg[leg]))
                            )
                        );
                    }
                } else {
                    longPremium = LeftRightUnsigned.wrap(
                        uint256(
                            LeftRightSigned.unwrap(
                                LeftRightSigned
                                    .wrap(int256(LeftRightUnsigned.unwrap(longPremium)))
                                    .sub(premiaByLeg[leg])
                            )
                        )
                    );
                }
                unchecked {
                    ++leg;
                }
            }

            unchecked {
                ++k;
            }
        }
    }

    /*//////////////////////////////////////////////////////////////
                          ONBOARD MEDIAN TWAP
    //////////////////////////////////////////////////////////////*/

    /// @notice Updates the internal median with the last oracle observation if the `MEDIAN_PERIOD` has elapsed.
    function pokeMedian() external {
        IV3CompatibleOracle oracle = oracleContract();

        (, , uint16 observationIndex, uint16 observationCardinality, , , ) = oracle.slot0();

        (, uint256 medianData) = PanopticMath.computeInternalMedian(
            observationIndex,
            observationCardinality,
            Constants.MEDIAN_PERIOD,
            s_miniMedian,
            oracle
        );

        if (medianData != 0) s_miniMedian = medianData;
    }

    /*//////////////////////////////////////////////////////////////
                          MINT/BURN INTERFACE
    //////////////////////////////////////////////////////////////*/

    /// @notice Validates the current options of the user, and mints a new position.
    /// @param positionIdList The list of currently held positions by the user, where the newly minted position(token) will be the last element in `positionIdList`
    /// @param positionSize The size of the position to be minted, expressed in terms of the asset
    /// @param effectiveLiquidityLimitX32 Maximum amount of "spread" defined as `removedLiquidity/netLiquidity` for a new position and
    /// denominated as X32 = (`ratioLimit * 2^32`)
    /// @param tickLimitLow The lower bound of an acceptable open interval for the ending price
    /// @param tickLimitHigh The upper bound of an acceptable open interval for the ending price
    /// @param usePremiaAsCollateral Whether to compute accumulated premia for all legs held by the user for collateral (true), or just owed premia for long legs (false)
    function mintOptions(
        TokenId[] calldata positionIdList,
        uint128 positionSize,
        uint64 effectiveLiquidityLimitX32,
        int24 tickLimitLow,
        int24 tickLimitHigh,
        bool usePremiaAsCollateral
    ) external {
        _mintOptions(
            positionIdList,
            positionSize,
            effectiveLiquidityLimitX32,
            tickLimitLow,
            tickLimitHigh,
            usePremiaAsCollateral
        );
    }

    /// @notice Closes and burns the caller's entire balance of `tokenId`.
    /// @param tokenId The tokenId of the option position to be burnt
    /// @param newPositionIdList The new positionIdList without the token being burnt
    /// @param tickLimitLow The lower bound of an acceptable open interval for the ending price
    /// @param tickLimitHigh The upper bound of an acceptable open interval for the ending price
    /// @param usePremiaAsCollateral Whether to compute accumulated premia for all legs held by the user for collateral (true), or just owed premia for long legs (false)
    function burnOptions(
        TokenId tokenId,
        TokenId[] calldata newPositionIdList,
        int24 tickLimitLow,
        int24 tickLimitHigh,
        bool usePremiaAsCollateral
    ) external {
        _burnOptions(COMMIT_LONG_SETTLED, tokenId, msg.sender, tickLimitLow, tickLimitHigh);

        uint256 medianData = _validateSolvency(
            msg.sender,
            newPositionIdList,
            NO_BUFFER,
            usePremiaAsCollateral
        );

        // Update `s_miniMedian` with a new observation if the last observation is old enough (returned medianData is nonzero)
        if (medianData != 0) s_miniMedian = medianData;
    }

    /// @notice Closes and burns the caller's entire balance of each `tokenId` in `positionIdList.
    /// @param positionIdList The list of tokenIds for the option positions to be burnt
    /// @param newPositionIdList The new positionIdList without the token(s) being burnt
    /// @param tickLimitLow The lower bound of an acceptable open interval for the ending price
    /// @param tickLimitHigh The upper bound of an acceptable open interval for the ending price
    /// @param usePremiaAsCollateral Whether to compute accumulated premia for all legs held by the user for collateral (true), or just owed premia for long legs (false)
    function burnOptions(
        TokenId[] calldata positionIdList,
        TokenId[] calldata newPositionIdList,
        int24 tickLimitLow,
        int24 tickLimitHigh,
        bool usePremiaAsCollateral
    ) external {
        _burnAllOptionsFrom(
            msg.sender,
            tickLimitLow,
            tickLimitHigh,
            COMMIT_LONG_SETTLED,
            positionIdList
        );

        uint256 medianData = _validateSolvency(
            msg.sender,
            newPositionIdList,
            NO_BUFFER,
            usePremiaAsCollateral
        );

        // Update `s_miniMedian` with a new observation if the last observation is old enough (returned medianData is nonzero)
        if (medianData != 0) s_miniMedian = medianData;
    }

    /*//////////////////////////////////////////////////////////////
                         POSITION MINTING LOGIC
    //////////////////////////////////////////////////////////////*/

    /// @notice Validates the current options of the user, and mints a new position.
    /// @param positionIdList The list of currently held positions by the user, where the newly minted position(token) will be the last element in `positionIdList`
    /// @param positionSize The size of the position to be minted, expressed in terms of the asset
    /// @param effectiveLiquidityLimitX32 Maximum amount of "spread" defined as `removedLiquidity/netLiquidity` for a new position and
    /// denominated as X32 = (`ratioLimit * 2^32`)
    /// @param tickLimitLow The lower bound of an acceptable open interval for the ending price
    /// @param tickLimitHigh The upper bound of an acceptable open interval for the ending price
    /// @param usePremiaAsCollateral Whether to compute accumulated premia for all legs held by the user for collateral (true), or just owed premia for long legs (false)
    function _mintOptions(
        TokenId[] calldata positionIdList,
        uint128 positionSize,
        uint64 effectiveLiquidityLimitX32,
        int24 tickLimitLow,
        int24 tickLimitHigh,
        bool usePremiaAsCollateral
    ) internal {
        // the new tokenId will be the last element in `positionIdList`
        TokenId tokenId;
        unchecked {
            tokenId = positionIdList[positionIdList.length - 1];
        }

        // do duplicate checks and the checks related to minting and positions
        _validatePositionList(msg.sender, positionIdList, 1);

        // make sure the tokenId is for this Panoptic pool
        if (tokenId.poolId() != SFPM.getPoolId(_V4PoolId()))
            revert Errors.InvalidTokenIdParameter(0);

        // disallow user to mint exact same position
        // in order to do it, user should burn it first and then mint
        if (PositionBalance.unwrap(s_positionBalance[msg.sender][tokenId]) != 0)
            revert Errors.PositionAlreadyMinted();

        // Mint in the SFPM and update state of collateral
        (uint32 poolUtilizations, LeftRightUnsigned commissions) = _mintInSFPMAndUpdateCollateral(
            tokenId,
            positionSize,
            effectiveLiquidityLimitX32,
            tickLimitLow,
            tickLimitHigh
        );

        uint96 tickData;
        {
            (
                int24 fastOracleTick,
                int24 slowOracleTick,
                int24 lastObservedTick,
                uint256 medianData
            ) = PanopticMath.getOracleTicks(oracleContract(), s_miniMedian);

            tickData = PositionBalanceLibrary.packTickData(
                V4StateReader.getTick(POOL_MANAGER_V4, _V4PoolId()),
                fastOracleTick,
                slowOracleTick,
                lastObservedTick
            );

            // Update `s_miniMedian` with a new observation if the last observation is old enough (returned medianData is nonzero)
            if (medianData != 0) s_miniMedian = medianData;
        }

        PositionBalance balanceData = PositionBalanceLibrary.storeBalanceData(
            positionSize,
            poolUtilizations,
            tickData
        );

        // update the users options balance of position `tokenId`
        // NOTE: user can't mint same position multiple times, so set the positionSize instead of adding
        s_positionBalance[msg.sender][tokenId] = balanceData;

        // Perform solvency check on user's account to ensure they had enough buying power to mint the option
        // Add an initial buffer to the collateral requirement to prevent users from minting their account close to insolvency
        _checkSolvency(
            msg.sender,
            positionIdList,
            tickData,
            BP_DECREASE_BUFFER,
            usePremiaAsCollateral
        );

        emit OptionMinted(msg.sender, tokenId, balanceData, commissions);
    }

    /// @notice Move all the required liquidity to/from the AMM and settle any required collateral deltas.
    /// @param tokenId The option position to be minted
    /// @param positionSize The size of the position, expressed in terms of the asset
    /// @param effectiveLiquidityLimitX32 Maximum amount of "spread" defined as `removedLiquidity/netLiquidity`
    /// @param tickLimitLow The lower bound of an acceptable open interval for the ending price
    /// @param tickLimitHigh The upper bound of an acceptable open interval for the ending price
    /// @return poolUtilizations Packing of the pool utilization (how much funds are in the Panoptic pool versus the AMM pool) at the time of minting,
    /// right 64bits for currency0 and left 64bits for currency1. When safeMode is active, it returns 100% pool utilization for both tokens
    /// @return commissions The total amount of commissions (base rate + ITM spread) paid for currency0 (right) and currency1 (left)
    function _mintInSFPMAndUpdateCollateral(
        TokenId tokenId,
        uint128 positionSize,
        uint64 effectiveLiquidityLimitX32,
        int24 tickLimitLow,
        int24 tickLimitHigh
    ) internal returns (uint32 poolUtilizations, LeftRightUnsigned commissions) {
        bool safeMode = isSafeMode();

        // if safeMode, enforce covered deployment
        if (safeMode) {
            if (tickLimitLow > tickLimitHigh) {
                (tickLimitLow, tickLimitHigh) = (tickLimitHigh, tickLimitLow);
            }
        }

        (LeftRightUnsigned[4] memory collectedByLeg, LeftRightSigned totalSwapped) = SFPM
            .mintTokenizedPosition(poolKey(), tokenId, positionSize, tickLimitLow, tickLimitHigh);

        _updateSettlementPostMint(
            tokenId,
            collectedByLeg,
            positionSize,
            effectiveLiquidityLimitX32
        );

        (poolUtilizations, commissions) = _payCommissionAndWriteData(
            tokenId,
            positionSize,
            totalSwapped,
            tickLimitLow < tickLimitHigh
        );

        if (safeMode) poolUtilizations = uint32(10_000 + (10_000 << 16));
    }

    /// @notice Take the commission fees for minting `tokenId` and settle any other required collateral deltas.
    /// @param tokenId The option position
    /// @param positionSize The size of the position, expressed in terms of the asset
    /// @param totalSwapped The amount of tokens moved during creation of the option position
    /// @param isCovered Whether the option was minted as covered (no swap occurred if ITM)
    /// @return Packing of the pool utilization (how much funds are in the Panoptic pool versus the AMM pool at the time of minting),
    /// right 64bits for currency0 and left 64bits for currency1, defined as `(inAMM * 10_000) / totalAssets()`
    /// where totalAssets is the total tracked assets in the AMM and PanopticPool minus fees and donations to the Panoptic pool
    /// @return The total amount of commissions (base rate + ITM spread) paid for token0 (right) and token1 (left)
    function _payCommissionAndWriteData(
        TokenId tokenId,
        uint128 positionSize,
        LeftRightSigned totalSwapped,
        bool isCovered
    ) internal returns (uint32, LeftRightUnsigned) {
        // compute how much of tokenId is long and short positions
        (LeftRightSigned longAmounts, LeftRightSigned shortAmounts) = PanopticMath
            .computeExercisedAmounts(tokenId, positionSize);

        (uint32 utilization0, uint128 commission0) = collateralToken0().takeCommissionAddData(
            msg.sender,
            longAmounts.rightSlot(),
            shortAmounts.rightSlot(),
            totalSwapped.rightSlot(),
            isCovered
        );
        (uint32 utilization1, uint128 commission1) = collateralToken1().takeCommissionAddData(
            msg.sender,
            longAmounts.leftSlot(),
            shortAmounts.leftSlot(),
            totalSwapped.leftSlot(),
            isCovered
        );

        // return pool utilizations as two uint16 (pool Utilization is always <= 10000)
        unchecked {
            return (
                utilization0 + (utilization1 << 16),
                LeftRightUnsigned.wrap(commission0).toLeftSlot(commission1)
            );
        }
    }

    /*//////////////////////////////////////////////////////////////
                         POSITION BURNING LOGIC
    //////////////////////////////////////////////////////////////*/

    /// @notice Close all options in `positionIdList`.
    /// @param owner The owner of the option position to be closed
    /// @param tickLimitLow The lower bound of an acceptable open interval for the ending price on each option close
    /// @param tickLimitHigh The upper bound of an acceptable open interval for the ending price on each option close
    /// @param commitLongSettled Whether to commit the long premium that will be settled to storage (disabled during liquidations)
    /// @param positionIdList The list of option positions to close
    /// @return netPaid The net amount of tokens paid after closing the positions
    /// @return premiasByLeg The amount of premia settled by the user for each leg of the position
    function _burnAllOptionsFrom(
        address owner,
        int24 tickLimitLow,
        int24 tickLimitHigh,
        bool commitLongSettled,
        TokenId[] calldata positionIdList
    ) internal returns (LeftRightSigned netPaid, LeftRightSigned[4][] memory premiasByLeg) {
        premiasByLeg = new LeftRightSigned[4][](positionIdList.length);
        for (uint256 i = 0; i < positionIdList.length; ) {
            LeftRightSigned paidAmounts;
            (paidAmounts, premiasByLeg[i]) = _burnOptions(
                commitLongSettled,
                positionIdList[i],
                owner,
                tickLimitLow,
                tickLimitHigh
            );
            netPaid = netPaid.add(paidAmounts);
            unchecked {
                ++i;
            }
        }
    }

    /// @notice Close a single option position.
    /// @param tokenId The option position to burn
    /// @param owner The owner of the option position to be burned
    /// @param tickLimitLow The lower bound of an acceptable open interval for the ending price on each option close
    /// @param tickLimitHigh The upper bound of an acceptable open interval for the ending price on each option close
    /// @param commitLongSettled Whether to commit the long premium that will be settled to storage (disabled during liquidations)
    /// @return paidAmounts The net amount of tokens paid after closing the position
    /// @return premiaByLeg The amount of premia settled by the user for each leg of the position
    function _burnOptions(
        bool commitLongSettled,
        TokenId tokenId,
        address owner,
        int24 tickLimitLow,
        int24 tickLimitHigh
    ) internal returns (LeftRightSigned paidAmounts, LeftRightSigned[4] memory premiaByLeg) {
        uint128 positionSize = s_positionBalance[owner][tokenId].positionSize();

        // burn position and do exercise checks
        (premiaByLeg, paidAmounts) = _burnAndHandleExercise(
            commitLongSettled,
            tickLimitLow,
            tickLimitHigh,
            tokenId,
            positionSize,
            owner
        );

        emit OptionBurnt(owner, positionSize, tokenId, premiaByLeg);
    }

    /// @notice Validates the solvency of `user`.
    /// @dev Falls back to the most conservative (least solvent) oracle tick if the sum of the squares of the deltas between all oracle ticks exceeds `MAX_TICKS_DELTA^2`.
    /// @dev Effectively, this means that the users must be solvent at all oracle ticks if the at least one of the ticks is sufficiently stale.
    /// @param user The account to validate
    /// @param positionIdList The list of positions to validate solvency for
    /// @param buffer The buffer to apply to the collateral requirement for `user`
    /// @param usePremiaAsCollateral Whether to compute accumulated premia for all legs held by the user for collateral (true), or just owed premia for long legs (false)
    /// @return If nonzero (enough time has passed since last observation), the updated value for `s_miniMedian` with a new observation
    function _validateSolvency(
        address user,
        TokenId[] calldata positionIdList,
        uint256 buffer,
        bool usePremiaAsCollateral
    ) internal view returns (uint256) {
        (
            int24 fastOracleTick,
            int24 slowOracleTick,
            int24 lastObservedTick,
            uint256 medianData
        ) = PanopticMath.getOracleTicks(oracleContract(), s_miniMedian);

        _checkSolvency(
            user,
            positionIdList,
            PositionBalanceLibrary.packTickData(
                V4StateReader.getTick(POOL_MANAGER_V4, _V4PoolId()),
                fastOracleTick,
                slowOracleTick,
                lastObservedTick
            ),
            buffer,
            usePremiaAsCollateral
        );

        return medianData;
    }

    /// @notice Validates the solvency of `user` from tickData.
    /// @param user The account to validate
    /// @param positionIdList The list of positions to validate solvency for
    /// @param tickData The packed tick data to check solvency at
    /// @param buffer The buffer to apply to the collateral requirement for `user`
    /// @param usePremiaAsCollateral Whether to compute accumulated premia for all legs held by the user for collateral (true), or just owed premia for long legs (false)
    function _checkSolvency(
        address user,
        TokenId[] calldata positionIdList,
        uint96 tickData,
        uint256 buffer,
        bool usePremiaAsCollateral
    ) internal view {
        // check that the provided positionIdList matches the positions in memory
        _validatePositionList(user, positionIdList, 0);

        (
            int24 currentTick,
            int24 fastOracleTick,
            int24 slowOracleTick,
            int24 lastObservedTick
        ) = PositionBalanceLibrary.unpackTickData(tickData);

        int24[] memory atTicks;
        // Fall back to a conservative approach if there's high deviation between internal ticks:
        // Check solvency at the slowOracleTick, currentTick, and lastObservedTick instead of just the fastOracleTick.
        // Deviation is measured as the magnitude of a 3D vector:
        // (fastOracleTick - slowOracleTick, lastObservedTick - slowOracleTick, currentTick - slowOracleTick)
        // This approach is more conservative than checking each tick difference individually,
        // as the Euclidean norm is always greater than or equal to the maximum of the individual differences.
        unchecked {
            if (
                int256(fastOracleTick - slowOracleTick) ** 2 +
                    int256(lastObservedTick - slowOracleTick) ** 2 +
                    int256(currentTick - slowOracleTick) ** 2 >
                MAX_TICKS_DELTA ** 2
            ) {
                atTicks = new int24[](4);
                atTicks[0] = fastOracleTick;
                atTicks[1] = slowOracleTick;
                atTicks[2] = lastObservedTick;
                atTicks[3] = currentTick;
            } else {
                atTicks = new int24[](1);
                atTicks[0] = fastOracleTick;
            }
        }

        _checkSolvencyAtTicks(
            user,
            positionIdList,
            currentTick,
            atTicks,
            buffer,
            ASSERT_SOLVENCY,
            usePremiaAsCollateral
        );
    }

    /// @notice Burns and handles the exercise of options.
    /// @param commitLongSettled Whether to commit the long premium that will be settled to storage (disabled during liquidations)
    /// @param tickLimitLow The lower bound of an acceptable open interval for the ending price
    /// @param tickLimitHigh The upper bound of an acceptable open interval for the ending price
    /// @param tokenId The option position to burn
    /// @param positionSize The size of the option position, expressed in terms of the asset
    /// @param owner The owner of the option position
    /// @return premiaByLeg The premia settled by the user for each leg of the option position
    /// @return paidAmounts The net amount of tokens paid after closing the position
    function _burnAndHandleExercise(
        bool commitLongSettled,
        int24 tickLimitLow,
        int24 tickLimitHigh,
        TokenId tokenId,
        uint128 positionSize,
        address owner
    ) internal returns (LeftRightSigned[4] memory premiaByLeg, LeftRightSigned paidAmounts) {
        // if safeMode, enforce covered at assignment
        if (isSafeMode()) {
            if (tickLimitLow > tickLimitHigh) {
                (tickLimitLow, tickLimitHigh) = (tickLimitHigh, tickLimitLow);
            }
        }

        (LeftRightUnsigned[4] memory collectedByLeg, LeftRightSigned totalSwapped) = SFPM
            .burnTokenizedPosition(poolKey(), tokenId, positionSize, tickLimitLow, tickLimitHigh);

        LeftRightSigned realizedPremia;
        (realizedPremia, premiaByLeg) = _updateSettlementPostBurn(
            owner,
            tokenId,
            collectedByLeg,
            positionSize,
            commitLongSettled
        );

        (LeftRightSigned longAmounts, LeftRightSigned shortAmounts) = PanopticMath
            .computeExercisedAmounts(tokenId, positionSize);

        paidAmounts = paidAmounts
            .toRightSlot(
                collateralToken0().exercise(
                    owner,
                    longAmounts.rightSlot(),
                    shortAmounts.rightSlot(),
                    totalSwapped.rightSlot(),
                    realizedPremia.rightSlot()
                )
            )
            .toLeftSlot(
                collateralToken1().exercise(
                    owner,
                    longAmounts.leftSlot(),
                    shortAmounts.leftSlot(),
                    totalSwapped.leftSlot(),
                    realizedPremia.leftSlot()
                )
            );
    }

    /*//////////////////////////////////////////////////////////////
                    LIQUIDATIONS & FORCED EXERCISES
    //////////////////////////////////////////////////////////////*/

    /// @notice Liquidates a distressed account. Will burn all positions and issue a bonus to the liquidator.
    /// @dev Will revert if liquidated account is solvent at one of the oracle ticks or if TWAP tick is too far away from the current tick.
    /// @dev If native currency is attached, non-EOA callers *must* accept empty calls with value up to the amount attached.
    /// @param positionIdListLiquidator List of positions owned by the liquidator
    /// @param liquidatee Address of the distressed account
    /// @param positionIdList List of positions owned by the user. Written as `[tokenId1, tokenId2, ...]`
    function liquidate(
        TokenId[] calldata positionIdListLiquidator,
        address liquidatee,
        TokenId[] calldata positionIdList
    ) external payable {
        _validatePositionList(liquidatee, positionIdList, 0);

        // Assert the account we are liquidating is actually insolvent
        int24 twapTick = getOracleTWAP();

        int24 currentTick = V4StateReader.getTick(POOL_MANAGER_V4, _V4PoolId());
        {
            // Enforce maximum delta between TWAP and currentTick to prevent extreme price manipulation
            int24 fastOracleTick;
            int24 lastObservedTick;
            (fastOracleTick, , lastObservedTick, ) = PanopticMath.getOracleTicks(
                oracleContract(),
                s_miniMedian
            );

            unchecked {
                if (Math.abs(currentTick - twapTick) > MAX_TWAP_DELTA_LIQUIDATION)
                    revert Errors.StaleOracle();
            }

            // Ensure the account is insolvent at twapTick (in place of slowOracleTick), currentTick, fastOracleTick, and lastObservedTick
            int24[] memory atTicks = new int24[](4);
            atTicks[0] = fastOracleTick;
            atTicks[1] = twapTick;
            atTicks[2] = lastObservedTick;
            atTicks[3] = currentTick;

            _checkSolvencyAtTicks(
                liquidatee,
                positionIdList,
                currentTick,
                atTicks,
                NO_BUFFER,
                ASSERT_INSOLVENCY,
                COMPUTE_PREMIA_AS_COLLATERAL
            );
        }

        LeftRightUnsigned tokenData0;
        LeftRightUnsigned tokenData1;
        LeftRightUnsigned shortPremium;
        {
            uint256[2][] memory positionBalanceArray = new uint256[2][](positionIdList.length);
            LeftRightUnsigned longPremium;
            (shortPremium, longPremium, positionBalanceArray) = _calculateAccumulatedPremia(
                liquidatee,
                positionIdList,
                COMPUTE_PREMIA_AS_COLLATERAL,
                ONLY_AVAILABLE_PREMIUM,
                currentTick
            );

            tokenData0 = collateralToken0().getAccountMarginDetails(
                liquidatee,
                twapTick,
                positionBalanceArray,
                shortPremium.rightSlot(),
                longPremium.rightSlot()
            );

            tokenData1 = collateralToken1().getAccountMarginDetails(
                liquidatee,
                twapTick,
                positionBalanceArray,
                shortPremium.leftSlot(),
                longPremium.leftSlot()
            );
        }

        // The protocol delegates some virtual shares to ensure the burn can be settled.
        collateralToken0().delegate(liquidatee);
        collateralToken1().delegate(liquidatee);

        LeftRightSigned bonusAmounts;
        {
            LeftRightSigned netPaid;
            LeftRightSigned[4][] memory premiasByLeg;
            // burn all options from the liquidatee

            // Do not commit any settled long premium to storage - we will do this after we determine if any long premium must be revoked
            // This is to prevent any short positions the liquidatee has being settled with tokens that will later be revoked
            // NOTE: tick limits are not applied here since it is not the liquidator's position being liquidated
            (netPaid, premiasByLeg) = _burnAllOptionsFrom(
                liquidatee,
                MIN_SWAP_TICK,
                MAX_SWAP_TICK,
                DONOT_COMMIT_LONG_SETTLED,
                positionIdList
            );

            LeftRightSigned collateralRemaining;
            // compute bonus amounts using latest tick data
            (bonusAmounts, collateralRemaining) = PanopticMath.getLiquidationBonus(
                tokenData0,
                tokenData1,
                Math.getSqrtRatioAtTick(twapTick),
                netPaid,
                shortPremium
            );

            // premia cannot be paid if there is protocol loss associated with the liquidatee
            // otherwise, an economic exploit could occur if the liquidator and liquidatee collude to
            // manipulate the fees in a liquidity area they control past the protocol loss threshold
            // such that the PLPs are forced to pay out premia to the liquidator
            // thus, we haircut any premium paid by the liquidatee (converting tokens as necessary) until the protocol loss is covered or the premium is exhausted
            // note that the haircutPremia function also commits the settled amounts (adjusted for the haircut) to storage, so it will be called even if there is no haircut

            // if premium is haircut from a token that is not in protocol loss, some of the liquidation bonus will be converted into that token
            address _liquidatee = liquidatee;
            int24 _twapTick = twapTick;
            TokenId[] memory _positionIdList = positionIdList;

            LeftRightSigned bonusDeltas = PanopticMath.haircutPremia(
                _liquidatee,
                _positionIdList,
                premiasByLeg,
                collateralRemaining,
                collateralToken0(),
                collateralToken1(),
                Math.getSqrtRatioAtTick(_twapTick),
                s_settledTokens
            );

            bonusAmounts = bonusAmounts.add(bonusDeltas);
        }

        // revoke delegated virtual shares and settle any bonus deltas with the liquidator
        collateralToken1().settleLiquidation(msg.sender, liquidatee, bonusAmounts.leftSlot());
        // native currency is represented as address(0), so it will always be currency0 alphanumerically
        collateralToken0().settleLiquidation{value: msg.value}(
            msg.sender,
            liquidatee,
            bonusAmounts.rightSlot()
        );

        // ensure the liquidator is still solvent after the liquidation
        _validateSolvency(
            msg.sender,
            positionIdListLiquidator,
            NO_BUFFER,
            COMPUTE_PREMIA_AS_COLLATERAL
        );

        emit AccountLiquidated(msg.sender, liquidatee, bonusAmounts);
    }

    /// @notice Force the exercise of a single position. Exercisor will have to pay a fee to the force exercisee.
    /// @param account Address of the distressed account
    /// @param tokenId The position to be force exercised; this position must contain at least one out-of-range long leg
    /// @param positionIdListExercisee Post-burn list of open positions in the exercisee's (`account`) account
    /// @param positionIdListExercisor List of open positions in the exercisor's (`msg.sender`) account
    /// @param usePremiaAsCollateral Whether to compute accumulated premia for all legs held by the exercisee(right slot)/exercisor(left slot) for collateral (>0), or just owed premia for long legs (0)
    function forceExercise(
        address account,
        TokenId tokenId,
        TokenId[] calldata positionIdListExercisee,
        TokenId[] calldata positionIdListExercisor,
        LeftRightUnsigned usePremiaAsCollateral
    ) external {
        // validate the exercisor's position list (the exercisee's list will be evaluated after their position is force exercised)
        _validatePositionList(msg.sender, positionIdListExercisor, 0);

        int24 lastObservedTick;
        {
            int24 fastOracleTick;
            int24 slowOracleTick;
            (fastOracleTick, slowOracleTick, lastObservedTick, ) = PanopticMath.getOracleTicks(
                oracleContract(),
                s_miniMedian
            );

            if (
                Math.abs(lastObservedTick - fastOracleTick) > MAX_TICK_DELTA_SUBSTITUTION ||
                Math.abs(lastObservedTick - slowOracleTick) > MAX_TICK_DELTA_SUBSTITUTION
            ) revert Errors.StaleOracle();
        }

        // to be eligible for force exercise, the price *must* be outside the position's range for at least 1 leg
        tokenId.validateIsExercisable(lastObservedTick);

        CollateralTracker ct0 = collateralToken0();
        CollateralTracker ct1 = collateralToken1();

        LeftRightSigned exerciseFees;
        {
            uint128 positionSize = s_positionBalance[account][tokenId].positionSize();

            (LeftRightSigned longAmounts, ) = PanopticMath.computeExercisedAmounts(
                tokenId,
                positionSize
            );

            // Compute the exerciseFee, this will decrease the further away the price is from the exercised position
            // Include any deltas in long legs between the current and oracle tick in the exercise fee
            exerciseFees = ct0.exerciseCost(
                V4StateReader.getTick(POOL_MANAGER_V4, _V4PoolId()),
                lastObservedTick,
                tokenId,
                positionSize,
                longAmounts
            );
        }

        // The protocol delegates some virtual shares to ensure the burn can be settled.
        ct0.delegate(account);
        ct1.delegate(account);

        // Exercise the option
        // Turn off ITM swapping to prevent swap at potentially unfavorable price
        _burnOptions(COMMIT_LONG_SETTLED, tokenId, account, MIN_SWAP_TICK, MAX_SWAP_TICK);

        // redistribute token composition of refund amounts if user doesn't have enough of one token to pay
        LeftRightSigned refundAmounts = PanopticMath.getRefundAmounts(
            account,
            exerciseFees,
            lastObservedTick,
            ct0,
            ct1
        );

        // settle difference between delegated amounts (from the protocol) and exercise fees/substituted tokens
        ct0.refund(account, msg.sender, refundAmounts.rightSlot());
        ct1.refund(account, msg.sender, refundAmounts.leftSlot());

        // revoke the virtual shares that were delegated after settling the difference with the exercisor
        ct0.revoke(account);
        ct1.revoke(account);

        _validateSolvency(
            account,
            positionIdListExercisee,
            NO_BUFFER,
            usePremiaAsCollateral.rightSlot() > 0
        );

        // the exercisor's position list is validated above
        // we need to assert their solvency against their collateral requirement plus a buffer
        // force exercises involve a collateral decrease with open positions, so there is a higher standard for solvency
        // a similar buffer is also invoked when minting options, which also decreases the available collateral
        if (positionIdListExercisor.length > 0)
            _validateSolvency(
                msg.sender,
                positionIdListExercisor,
                BP_DECREASE_BUFFER,
                usePremiaAsCollateral.leftSlot() > 0
            );

        emit ForcedExercised(msg.sender, account, tokenId, exerciseFees);
    }

    /*//////////////////////////////////////////////////////////////
                            SOLVENCY CHECKS
    //////////////////////////////////////////////////////////////*/

    /// @notice Check whether an account is solvent at a given `atTick` with a collateral requirement of `buffer/10_000` multiplied by the requirement of `positionIdList`.
    /// @dev Reverts if `account` is not solvent at all provided ticks and `expectedSolvent == true`, or if `account` is solvent at all ticks and `!expectedSolvent`.
    /// @param account The account to check solvency for
    /// @param positionIdList The list of positions to check solvency for
    /// @param currentTick The current tick of the Uniswap pool (needed for fee calculations)
    /// @param atTicks An array of ticks to check solvency at
    /// @param buffer The buffer to apply to the collateral requirement
    /// @param expectedSolvent Whether the account is expected to be solvent (true) or insolvent (false) at all provided `atTicks`
    /// @param usePremiaAsCollateral Whether to compute accumulated premia for all legs held by the user for collateral (true), or just owed premia for long legs (false)
    function _checkSolvencyAtTicks(
        address account,
        TokenId[] calldata positionIdList,
        int24 currentTick,
        int24[] memory atTicks,
        uint256 buffer,
        bool expectedSolvent,
        bool usePremiaAsCollateral
    ) internal view {
        (
            LeftRightUnsigned shortPremium,
            LeftRightUnsigned longPremium,
            uint256[2][] memory positionBalanceArray
        ) = _calculateAccumulatedPremia(
                account,
                positionIdList,
                usePremiaAsCollateral,
                ONLY_AVAILABLE_PREMIUM,
                currentTick
            );

        uint256 numberOfTicks = atTicks.length;

        uint256 solvent;
        for (uint256 i; i < numberOfTicks; ) {
            unchecked {
                if (
                    _isAccountSolvent(
                        account,
                        atTicks[i],
                        positionBalanceArray,
                        shortPremium,
                        longPremium,
                        buffer
                    )
                ) solvent++;
                ++i;
            }
        }

        if (expectedSolvent && solvent != numberOfTicks) revert Errors.AccountInsolvent();
        if (!expectedSolvent && solvent != 0) revert Errors.NotMarginCalled();
    }

    /// @notice Check whether an account is solvent at a given `atTick` with a collateral requirement of `buffer/10_000` multiplied by the requirement of `positionBalanceArray`.
    /// @param account The account to check solvency for
    /// @param atTick The tick to check solvency at
    /// @param positionBalanceArray A list of balances and pool utilization for each position, of the form `[[tokenId0, balances0], [tokenId1, balances1], ...]`
    /// @param shortPremium The total amount of premium (prorated by available settled tokens) owed to the short legs of `account`
    /// @param longPremium The total amount of premium owed by the long legs of `account`
    /// @param buffer The buffer to apply to the collateral requirement
    /// @return Whether the account is solvent at the given tick
    function _isAccountSolvent(
        address account,
        int24 atTick,
        uint256[2][] memory positionBalanceArray,
        LeftRightUnsigned shortPremium,
        LeftRightUnsigned longPremium,
        uint256 buffer
    ) internal view returns (bool) {
        (uint256 balanceCross, uint256 thresholdCross) = PanopticMath.getCrossBalances(
            collateralToken0().getAccountMarginDetails(
                account,
                atTick,
                positionBalanceArray,
                shortPremium.rightSlot(),
                longPremium.rightSlot()
            ),
            collateralToken1().getAccountMarginDetails(
                account,
                atTick,
                positionBalanceArray,
                shortPremium.leftSlot(),
                longPremium.leftSlot()
            ),
            Math.getSqrtRatioAtTick(atTick)
        );

        // compare balance and required tokens, can use unsafe div because denominator is always nonzero
        return balanceCross >= Math.mulDivRoundingUp(thresholdCross, buffer, 10_000);
    }

    /// @notice Checks whether the current tick has deviated by `> MAX_TICKS_DELTA` from the slow oracle median tick.
    /// @return Whether the current tick has deviated from the median by `> MAX_TICKS_DELTA`
    function isSafeMode() public view returns (bool) {
        uint256 medianData = s_miniMedian;
        unchecked {
            // If ticks have recently deviated more than +/- ~10%, enforce covered mints
            return
                Math.abs(
                    V4StateReader.getTick(POOL_MANAGER_V4, _V4PoolId()) -
                        ((int24(
                            uint24(medianData >> ((uint24(medianData >> (192 + 3 * 3)) % 8) * 24))
                        ) +
                            int24(
                                uint24(
                                    medianData >> ((uint24(medianData >> (192 + 3 * 4)) % 8) * 24)
                                )
                            )) / 2)
                ) > MAX_TICKS_DELTA;
        }
    }

    /*//////////////////////////////////////////////////////////////
                 POSITIONS HASH GENERATION & VALIDATION
    //////////////////////////////////////////////////////////////*/

    /// @notice Makes sure that the positions in the incoming user's list match the existing active option positions.
    /// @param account The owner of the incoming list of positions
    /// @param positionIdList The existing list of active options for the owner
    /// @param offset The amount of positions from the end of the list to exclude from validation
    function _validatePositionList(
        address account,
        TokenId[] calldata positionIdList,
        uint256 offset
    ) internal view {
        uint256 pLength;

        unchecked {
            pLength = positionIdList.length - offset;
        }

        uint256 fingerprintIncomingList;

        for (uint256 i = 0; i < pLength; ) {
            fingerprintIncomingList = PanopticMath.updatePositionsHash(
                fingerprintIncomingList,
                positionIdList[i],
                ADD
            );
            unchecked {
                ++i;
            }
        }

        // revert if fingerprint for provided `_positionIdList` does not match the one stored for the `_account`
        if (fingerprintIncomingList != s_positionsHash[account]) revert Errors.InputListFail();
    }

    /// @notice Updates the hash for all positions owned by an account. This fingerprints the list of all incoming options with a single hash.
    /// @dev The outcome of this function will be to update the hash of positions.
    /// This is done as a duplicate/validation check of the incoming list O(N).
    /// @dev The positions hash is stored as the XOR of the keccak256 of each tokenId. Updating will XOR the existing hash with the new tokenId.
    /// The same update can either add a new tokenId (when minting an option), or remove an existing one (when burning it).
    /// @param account The owner of `tokenId`
    /// @param tokenId The option position
    /// @param addFlag Whether to add `tokenId` to the hash (true) or remove it (false)
    function _updatePositionsHash(address account, TokenId tokenId, bool addFlag) internal {
        // Get the current position hash value (fingerprint of all pre-existing positions created by `_account`)
        // Add the current tokenId to the positionsHash as XOR'd
        // since 0 ^ x = x, no problem on first mint
        // Store values back into the user option details with the updated hash (leaves the other parameters unchanged)
        uint256 newHash = PanopticMath.updatePositionsHash(
            s_positionsHash[account],
            tokenId,
            addFlag
        );
        if ((newHash >> 248) > MAX_OPEN_LEGS) revert Errors.TooManyLegsOpen();
        s_positionsHash[account] = newHash;
    }

    /*//////////////////////////////////////////////////////////////
                                QUERIES
    //////////////////////////////////////////////////////////////*/

    /// @notice Computes and returns all ticks used for collateral checks at mint/burn.
    /// @return currentTick The current tick of the Uniswap V4 pool
    /// @return fastOracleTick The fast oracle tick computed as the median of the past N observations in the oracle contract
    /// @return slowOracleTick The slow oracle tick (either composed of observations retrieved from Uniswap or observations stored in `s_miniMedian`)
    /// @return latestObservation The latest observation from the oracle contract
    /// @return medianData The current value of the 8-slot internal observation queue (`s_miniMedian`)
    function getOracleTicks()
        external
        view
        returns (
            int24 currentTick,
            int24 fastOracleTick,
            int24 slowOracleTick,
            int24 latestObservation,
            uint256 medianData
        )
    {
        currentTick = V4StateReader.getTick(POOL_MANAGER_V4, _V4PoolId());

        (fastOracleTick, slowOracleTick, latestObservation, ) = PanopticMath.getOracleTicks(
            oracleContract(),
            s_miniMedian
        );
        medianData = s_miniMedian;
    }

    /// @notice Get the current number of legs across all open positions for an account.
    /// @param user The account to query
    /// @return Number of legs across the open positions of `user`
    function numberOfLegs(address user) external view returns (uint256) {
        return s_positionsHash[user] >> 248;
    }

    /// @notice Get the `tokenId` position data for `user`.
    /// @param user The account that owns `tokenId`
    /// @param tokenId The position to query
    /// @return `currentTick` at mint
    /// @return Fast oracle tick at mint
    /// @return Slow oracle tick at mint
    /// @return Last observed tick at mint
    /// @return Utilization of currency0 at mint
    /// @return Utilization of currency1 at mint
    /// @return Size of the position
    function positionData(
        address user,
        TokenId tokenId
    ) external view returns (int24, int24, int24, int24, int256, int256, uint128) {
        return s_positionBalance[user][tokenId].unpackAll();
    }

    /// @notice Get the oracle price used to check solvency in liquidations.
    /// @return twapTick The current oracle price used to check solvency in liquidations
    function getOracleTWAP() internal view returns (int24 twapTick) {
        twapTick = PanopticMath.twapFilter(oracleContract(), TWAP_WINDOW);
    }

    /*//////////////////////////////////////////////////////////////
                  PREMIA & PREMIA SPREAD CALCULATIONS
    //////////////////////////////////////////////////////////////*/

    /// @notice Ensure the effective liquidity in a given chunk is above a certain threshold.
    /// @param tokenId An option position
    /// @param leg A leg index of `tokenId` corresponding to a tickLower-tickUpper chunk
    /// @param effectiveLiquidityLimitX32 Maximum amount of "spread" defined as removedLiquidity/netLiquidity for a new position
    /// denominated as X32 = (`ratioLimit * 2^32`)
    /// @return totalLiquidity The total liquidity deposited in that chunk: `totalLiquidity = netLiquidity + removedLiquidity`
    function _checkLiquiditySpread(
        TokenId tokenId,
        uint256 leg,
        uint256 effectiveLiquidityLimitX32
    ) internal view returns (uint256 totalLiquidity) {
        uint256 netLiquidity;
        uint256 removedLiquidity;
        (totalLiquidity, netLiquidity, removedLiquidity) = _getLiquidities(tokenId, leg);

        // compute and return effective liquidity. Return if short=net=0, which is closing short position
        if (netLiquidity == 0 && removedLiquidity == 0) return totalLiquidity;

        uint256 effectiveLiquidityFactorX32;
        unchecked {
            effectiveLiquidityFactorX32 = (removedLiquidity * 2 ** 32) / netLiquidity;
        }

        // put a limit on how much new liquidity in one transaction can be deployed into this leg
        // the effective liquidity measures how many times more the newly added liquidity is compared to the existing/base liquidity
        if (effectiveLiquidityFactorX32 > effectiveLiquidityLimitX32)
            revert Errors.EffectiveLiquidityAboveThreshold();
    }

    /// @notice Compute the premia collected for a single option position `tokenId`.
    /// @param tokenId The option position
    /// @param positionSize The number of contracts (size) of the option position
    /// @param owner The holder of the tokenId option
    /// @param usePremiaAsCollateral Whether to compute accumulated premia for all legs held by the user for collateral (true), or just owed premia for long legs (false)
    /// @param atTick The tick at which the premia is calculated -> use (`atTick < type(int24).max`) to compute it
    /// up to current block. `atTick = type(int24).max` will only consider fees as of the last on-chain transaction
    /// @return premiaByLeg The amount of premia owed to the user for each leg of the position
    /// @return premiumAccumulatorsByLeg The amount of premia accumulated for each leg of the position
    function _getPremia(
        TokenId tokenId,
        uint128 positionSize,
        address owner,
        bool usePremiaAsCollateral,
        int24 atTick
    )
        internal
        view
        returns (
            LeftRightSigned[4] memory premiaByLeg,
            uint256[2][4] memory premiumAccumulatorsByLeg
        )
    {
        uint256 numLegs = tokenId.countLegs();
        for (uint256 leg = 0; leg < numLegs; ) {
            uint256 isLong = tokenId.isLong(leg);
            if ((isLong == 1) || usePremiaAsCollateral) {
                LiquidityChunk liquidityChunk = PanopticMath.getLiquidityChunk(
                    tokenId,
                    leg,
                    positionSize
                );
                uint256 tokenType = tokenId.tokenType(leg);

                (premiumAccumulatorsByLeg[leg][0], premiumAccumulatorsByLeg[leg][1]) = SFPM
                    .getAccountPremium(
                        _V4PoolId(),
                        address(this),
                        tokenType,
                        liquidityChunk.tickLower(),
                        liquidityChunk.tickUpper(),
                        atTick,
                        isLong
                    );

                unchecked {
                    LeftRightUnsigned premiumAccumulatorLast = s_options[owner][tokenId][leg];

                    premiaByLeg[leg] = LeftRightSigned
                        .wrap(0)
                        .toRightSlot(
                            int128(
                                int256(
                                    ((premiumAccumulatorsByLeg[leg][0] -
                                        premiumAccumulatorLast.rightSlot()) *
                                        (liquidityChunk.liquidity())) / 2 ** 64
                                )
                            )
                        )
                        .toLeftSlot(
                            int128(
                                int256(
                                    ((premiumAccumulatorsByLeg[leg][1] -
                                        premiumAccumulatorLast.leftSlot()) *
                                        (liquidityChunk.liquidity())) / 2 ** 64
                                )
                            )
                        );
                }

                if (isLong == 1) {
                    premiaByLeg[leg] = LeftRightSigned.wrap(0).sub(premiaByLeg[leg]);
                }
            }
            unchecked {
                ++leg;
            }
        }
    }

    /*//////////////////////////////////////////////////////////////
                        AVAILABLE PREMIUM LOGIC
    //////////////////////////////////////////////////////////////*/

    /// @notice Settle unpaid premium for one `legIndex` on a position owned by `owner`.
    /// @dev Called by sellers on buyers of their chunk to increase the available premium for withdrawal (before closing their position).
    /// @dev This feature is only available when `owner` is solvent and has the requisite tokens to settle the premium.
    /// @param positionIdList Exhaustive list of open positions for `owner` used for solvency checks where the tokenId to settle is placed at the last index
    /// @param owner The owner of the option position to make premium payments on
    /// @param legIndex the index of the leg in tokenId that is to be collected on (must be isLong=1)
    /// @param usePremiaAsCollateral Whether to compute accumulated premia for all legs held by the settlee for collateral (true), or just owed premia for long legs (false)
    function settleLongPremium(
        TokenId[] calldata positionIdList,
        address owner,
        uint256 legIndex,
        bool usePremiaAsCollateral
    ) external {
        _validatePositionList(owner, positionIdList, 0);

        TokenId tokenId;
        unchecked {
            tokenId = positionIdList[positionIdList.length - 1];
        }

        if (tokenId.isLong(legIndex) == 0 || legIndex > 3) revert Errors.NotALongLeg();

        CollateralTracker ct0 = collateralToken0();
        CollateralTracker ct1 = collateralToken1();

        // The protocol delegates some virtual shares to ensure the premia can be settled.
        ct0.delegate(owner);
        ct1.delegate(owner);

        LiquidityChunk liquidityChunk = PanopticMath.getLiquidityChunk(
            tokenId,
            legIndex,
            s_positionBalance[owner][tokenId].positionSize()
        );

        int24 currentTick = V4StateReader.getTick(POOL_MANAGER_V4, _V4PoolId());

        LeftRightUnsigned accumulatedPremium;
        {
            (uint128 premiumAccumulator0, uint128 premiumAccumulator1) = SFPM.getAccountPremium(
                _V4PoolId(),
                address(this),
                tokenId.tokenType(legIndex),
                liquidityChunk.tickLower(),
                liquidityChunk.tickUpper(),
                currentTick,
                1
            );
            accumulatedPremium = LeftRightUnsigned.wrap(premiumAccumulator0).toLeftSlot(
                premiumAccumulator1
            );

            // update the premium accumulator for the long position to the latest value
            // (the entire premia delta will be settled)
            LeftRightUnsigned premiumAccumulatorsLast = s_options[owner][tokenId][legIndex];
            s_options[owner][tokenId][legIndex] = accumulatedPremium;

            accumulatedPremium = accumulatedPremium.sub(premiumAccumulatorsLast);
        }

        unchecked {
            uint256 liquidity = liquidityChunk.liquidity();

            // update the realized premia
            LeftRightSigned realizedPremia = LeftRightSigned
                .wrap(0)
                .toRightSlot(int128(int256((accumulatedPremium.rightSlot() * liquidity) / 2 ** 64)))
                .toLeftSlot(int128(int256((accumulatedPremium.leftSlot() * liquidity) / 2 ** 64)));

            // deduct the paid premium tokens from the owner's balance and add them to the cumulative settled token delta
            ct0.exercise(owner, 0, 0, 0, -realizedPremia.rightSlot());
            ct1.exercise(owner, 0, 0, 0, -realizedPremia.leftSlot());

            bytes32 chunkKey = keccak256(
                abi.encodePacked(
                    tokenId.strike(legIndex),
                    tokenId.width(legIndex),
                    tokenId.tokenType(legIndex)
                )
            );
            // commit the delta in settled tokens (all of the premium paid by long chunks in the tokenIds list) to storage
            s_settledTokens[chunkKey] = s_settledTokens[chunkKey].add(
                LeftRightUnsigned.wrap(uint256(LeftRightSigned.unwrap(realizedPremia)))
            );

            emit PremiumSettled(owner, tokenId, legIndex, realizedPremia);
        }

        int24 lastObservedTick;
        uint96 tickData;
        {
            int24 fastOracleTick;
            int24 slowOracleTick;
            (fastOracleTick, slowOracleTick, lastObservedTick, ) = PanopticMath.getOracleTicks(
                oracleContract(),
                s_miniMedian
            );

            if (
                Math.abs(lastObservedTick - fastOracleTick) > MAX_TICK_DELTA_SUBSTITUTION ||
                Math.abs(lastObservedTick - slowOracleTick) > MAX_TICK_DELTA_SUBSTITUTION
            ) revert Errors.StaleOracle();

            tickData = PositionBalanceLibrary.packTickData(
                V4StateReader.getTick(POOL_MANAGER_V4, _V4PoolId()),
                fastOracleTick,
                slowOracleTick,
                lastObservedTick
            );
        }

        LeftRightSigned refundAmounts = PanopticMath.getRefundAmounts(
            owner,
            LeftRightSigned.wrap(0),
            lastObservedTick,
            ct0,
            ct1
        );

        // allow the caller to settle tokens owed to the protocol by the settlee in exchange for the surplus token
        ct0.refund(owner, msg.sender, refundAmounts.rightSlot());
        ct1.refund(owner, msg.sender, refundAmounts.leftSlot());

        ct0.revoke(owner);
        ct1.revoke(owner);

        // ensure the owner is solvent (insolvent accounts are not permitted to pay premium unless they are being liquidated)
        _checkSolvency(owner, positionIdList, tickData, NO_BUFFER, usePremiaAsCollateral);
    }

    /// @notice Adds collected tokens to `s_settledTokens` and adjusts `s_grossPremiumLast` for any liquidity added.
    /// @dev Always called after `mintTokenizedPosition`.
    /// @param tokenId The option position that was minted
    /// @param collectedByLeg The amount of tokens collected in the corresponding chunk for each leg of the position
    /// @param positionSize The size of the position, expressed in terms of the asset
    /// @param effectiveLiquidityLimitX32 Maximum amount of "spread" defined as `removedLiquidity/netLiquidity`
    function _updateSettlementPostMint(
        TokenId tokenId,
        LeftRightUnsigned[4] memory collectedByLeg,
        uint128 positionSize,
        uint64 effectiveLiquidityLimitX32
    ) internal {
        // ADD the current tokenId to the position list hash (hash = XOR of all keccak256(tokenId))
        // and increase the number of positions counter by 1.
        _updatePositionsHash(msg.sender, tokenId, ADD);

        uint256 numLegs = tokenId.countLegs();
        for (uint256 leg = 0; leg < numLegs; ++leg) {
            uint256 isLong = tokenId.isLong(leg);

            bytes32 chunkKey = keccak256(
                abi.encodePacked(tokenId.strike(leg), tokenId.width(leg), tokenId.tokenType(leg))
            );

            // add any tokens collected from Uniswap in a given chunk to the settled tokens available for withdrawal by sellers
            s_settledTokens[chunkKey] = s_settledTokens[chunkKey].add(collectedByLeg[leg]);

            LiquidityChunk liquidityChunk = PanopticMath.getLiquidityChunk(
                tokenId,
                leg,
                positionSize
            );

            uint256 grossCurrent0;
            uint256 grossCurrent1;
            {
                uint256 tokenType = tokenId.tokenType(leg);
                // can use (type(int24).max flag because premia accumulators were updated during the mintTokenizedPosition step.
                (grossCurrent0, grossCurrent1) = SFPM.getAccountPremium(
                    _V4PoolId(),
                    address(this),
                    tokenType,
                    liquidityChunk.tickLower(),
                    liquidityChunk.tickUpper(),
                    type(int24).max,
                    isLong
                );

                s_options[msg.sender][tokenId][leg] = LeftRightUnsigned
                    .wrap(uint128(grossCurrent0))
                    .toLeftSlot(uint128(grossCurrent1));
            }

            // if position is long, ensure that removed liquidity does not deplete strike beyond min(MAX_SPREAD, user-provided effectiveLiquidityLimit)
            // new totalLiquidity (total sold) = removedLiquidity + netLiquidity (R + N)
            uint256 totalLiquidity = _checkLiquiditySpread(
                tokenId,
                leg,
                isLong == 0 ? MAX_SPREAD : Math.min(effectiveLiquidityLimitX32, MAX_SPREAD)
            );

            // if position is short, adjust `grossPremiumLast` upward to account for the increase in short liquidity
            if (isLong == 0) {
                unchecked {
                    // L
                    LeftRightUnsigned grossPremiumLast = s_grossPremiumLast[chunkKey];
                    // R
                    uint256 positionLiquidity = liquidityChunk.liquidity();
                    // T (totalLiquidity is (T + R) after minting)
                    uint256 totalLiquidityBefore = totalLiquidity - positionLiquidity;

                    // We need to adjust the grossPremiumLast value such that the result of
                    // (grossPremium - adjustedGrossPremiumLast) * updatedTotalLiquidityPostMint / 2**64 is equal to (grossPremium - grossPremiumLast) * totalLiquidityBeforeMint / 2**64
                    // G: total gross premium
                    // T: totalLiquidityBeforeMint
                    // R: positionLiquidity
                    // C: current grossPremium value
                    // L: current grossPremiumLast value
                    // Ln: updated grossPremiumLast value
                    // T * (C - L) = G
                    // (T + R) * (C - Ln) = G
                    //
                    // T * (C - L) = (T + R) * (C - Ln)
                    // (TC - TL) / (T + R) = C - Ln
                    // Ln = C - (TC - TL)/(T + R)
                    // Ln = (CT + CR - TC + TL)/(T+R)
                    // Ln = (CR + TL)/(T+R)

                    s_grossPremiumLast[chunkKey] = LeftRightUnsigned
                        .wrap(
                            uint128(
                                (grossCurrent0 *
                                    positionLiquidity +
                                    grossPremiumLast.rightSlot() *
                                    totalLiquidityBefore) / totalLiquidity
                            )
                        )
                        .toLeftSlot(
                            uint128(
                                (grossCurrent1 *
                                    positionLiquidity +
                                    grossPremiumLast.leftSlot() *
                                    totalLiquidityBefore) / totalLiquidity
                            )
                        );
                }
            }
        }
    }

    /// @notice Query the amount of premium available for withdrawal given a certain `premiumOwed` for a chunk.
    /// @dev Based on the ratio between `settledTokens` and the total premium owed to sellers in a chunk.
    /// @dev The ratio is capped at 1 (as the base ratio can be greater than one if some seller forfeits enough premium).
    /// @param totalLiquidity The updated total liquidity amount for the chunk
    /// @param settledTokens LeftRight accumulator for the amount of tokens that have been settled (collected or paid)
    /// @param grossPremiumLast The `last` values used with `premiumAccumulators` to compute the total premium owed to sellers
    /// @param premiumOwed The amount of premium owed to sellers in the chunk
    /// @param premiumAccumulators The current values of the premium accumulators for the chunk
    /// @return The amount of currency0/currency1 premium available for withdrawal
    function _getAvailablePremium(
        uint256 totalLiquidity,
        LeftRightUnsigned settledTokens,
        LeftRightUnsigned grossPremiumLast,
        LeftRightUnsigned premiumOwed,
        uint256[2] memory premiumAccumulators
    ) internal pure returns (LeftRightUnsigned) {
        unchecked {
            // long premium only accumulates as it is settled, so compute the ratio
            // of total settled tokens in a chunk to total premium owed to sellers and multiply
            // cap the ratio at 1 (it can be greater than one if some seller forfeits enough premium)
            uint256 accumulated0 = ((premiumAccumulators[0] - grossPremiumLast.rightSlot()) *
                totalLiquidity) / 2 ** 64;
            uint256 accumulated1 = ((premiumAccumulators[1] - grossPremiumLast.leftSlot()) *
                totalLiquidity) / 2 ** 64;

            return (
                LeftRightUnsigned
                    .wrap(
                        uint128(
                            Math.min(
                                (uint256(premiumOwed.rightSlot()) * settledTokens.rightSlot()) /
                                    (accumulated0 == 0 ? type(uint256).max : accumulated0),
                                premiumOwed.rightSlot()
                            )
                        )
                    )
                    .toLeftSlot(
                        uint128(
                            Math.min(
                                (uint256(premiumOwed.leftSlot()) * settledTokens.leftSlot()) /
                                    (accumulated1 == 0 ? type(uint256).max : accumulated1),
                                premiumOwed.leftSlot()
                            )
                        )
                    )
            );
        }
    }

    /// @notice Query the total amount of liquidity sold in the corresponding chunk for a position leg.
    /// @dev totalLiquidity (total sold) = removedLiquidity + netLiquidity (in AMM).
    /// @param tokenId The option position
    /// @param leg The leg of the option position to get `totalLiquidity` for
    /// @return totalLiquidity The total amount of liquidity sold in the corresponding chunk for a position leg
    /// @return netLiquidity The amount of liquidity available in the corresponding chunk for a position leg
    /// @return removedLiquidity The amount of liquidity removed through buying in the corresponding chunk for a position leg
    function _getLiquidities(
        TokenId tokenId,
        uint256 leg
    )
        internal
        view
        returns (uint256 totalLiquidity, uint128 netLiquidity, uint128 removedLiquidity)
    {
        (int24 tickLower, int24 tickUpper) = tokenId.asTicks(leg);

        LeftRightUnsigned accountLiquidities = SFPM.getAccountLiquidity(
            _V4PoolId(),
            address(this),
            tokenId.tokenType(leg),
            tickLower,
            tickUpper
        );

        netLiquidity = accountLiquidities.rightSlot();
        removedLiquidity = accountLiquidities.leftSlot();

        unchecked {
            totalLiquidity = netLiquidity + removedLiquidity;
        }
    }

    /// @notice Updates settled tokens and grossPremiumLast for a chunk after a burn and returns premium info.
    /// @param owner The owner of the option position that was burnt
    /// @param tokenId The option position that was burnt
    /// @param collectedByLeg The amount of tokens collected in the corresponding chunk for each leg of the position
    /// @param positionSize The size of the position, expressed in terms of the asset
    /// @param commitLongSettled Whether to commit the long premium that will be settled to storage
    /// @return realizedPremia The amount of premia settled by the user
    /// @return premiaByLeg The amount of premia settled by the user for each leg of the position
    function _updateSettlementPostBurn(
        address owner,
        TokenId tokenId,
        LeftRightUnsigned[4] memory collectedByLeg,
        uint128 positionSize,
        bool commitLongSettled
    ) internal returns (LeftRightSigned realizedPremia, LeftRightSigned[4] memory premiaByLeg) {
        uint256 numLegs = tokenId.countLegs();
        uint256[2][4] memory premiumAccumulatorsByLeg;

        // compute accumulated fees
        (premiaByLeg, premiumAccumulatorsByLeg) = _getPremia(
            tokenId,
            positionSize,
            owner,
            COMPUTE_PREMIA_AS_COLLATERAL,
            type(int24).max
        );

        for (uint256 leg = 0; leg < numLegs; ) {
            LeftRightSigned legPremia = premiaByLeg[leg];

            bytes32 chunkKey = keccak256(
                abi.encodePacked(tokenId.strike(leg), tokenId.width(leg), tokenId.tokenType(leg))
            );

            // collected from Uniswap
            LeftRightUnsigned settledTokens = s_settledTokens[chunkKey].add(collectedByLeg[leg]);

            // (will be) paid by long legs
            if (tokenId.isLong(leg) == 1) {
                if (commitLongSettled)
                    settledTokens = LeftRightUnsigned.wrap(
                        uint256(
                            LeftRightSigned.unwrap(
                                LeftRightSigned
                                    .wrap(int256(LeftRightUnsigned.unwrap(settledTokens)))
                                    .sub(legPremia)
                            )
                        )
                    );
                realizedPremia = realizedPremia.add(legPremia);
            } else {
                uint256 positionLiquidity;
                uint256 totalLiquidity;
                {
                    LiquidityChunk liquidityChunk = PanopticMath.getLiquidityChunk(
                        tokenId,
                        leg,
                        positionSize
                    );
                    positionLiquidity = liquidityChunk.liquidity();

                    // if position is short, ensure that removed liquidity does not deplete strike beyond MAX_SPREAD when closed
                    // new totalLiquidity (total sold) = removedLiquidity + netLiquidity (T - R)
                    totalLiquidity = _checkLiquiditySpread(tokenId, leg, MAX_SPREAD);
                }
                // T (totalLiquidity is (T - R) after burning)
                uint256 totalLiquidityBefore = totalLiquidity + positionLiquidity;

                LeftRightUnsigned grossPremiumLast = s_grossPremiumLast[chunkKey];

                LeftRightUnsigned availablePremium = _getAvailablePremium(
                    totalLiquidity + positionLiquidity,
                    settledTokens,
                    grossPremiumLast,
                    LeftRightUnsigned.wrap(uint256(LeftRightSigned.unwrap(legPremia))),
                    premiumAccumulatorsByLeg[leg]
                );

                // subtract settled tokens sent to seller
                settledTokens = settledTokens.sub(availablePremium);

                // add available premium to amount that should be settled
                realizedPremia = realizedPremia.add(
                    LeftRightSigned.wrap(int256(LeftRightUnsigned.unwrap(availablePremium)))
                );

                // update the base `premiaByLeg` value to reflect the amount of premium that will actually be settled
                premiaByLeg[leg] = LeftRightSigned.wrap(
                    int256(LeftRightUnsigned.unwrap(availablePremium))
                );

                // We need to adjust the grossPremiumLast value such that the result of
                // (grossPremium - adjustedGrossPremiumLast) * updatedTotalLiquidityPostBurn / 2**64 is equal to
                // (grossPremium - grossPremiumLast) * totalLiquidityBeforeBurn / 2**64 - premiumOwedToPosition
                // G: total gross premium (- premiumOwedToPosition)
                // T: totalLiquidityBeforeMint
                // R: positionLiquidity
                // C: current grossPremium value
                // L: current grossPremiumLast value
                // Ln: updated grossPremiumLast value
                // T * (C - L) = G
                // (T - R) * (C - Ln) = G - P
                //
                // T * (C - L) = (T - R) * (C - Ln) + P
                // (TC - TL - P) / (T - R) = C - Ln
                // Ln = C - (TC - TL - P) / (T - R)
                // Ln = (TC - CR - TC + LT + P) / (T-R)
                // Ln = (LT - CR + P) / (T-R)

                unchecked {
                    uint256[2][4] memory _premiumAccumulatorsByLeg = premiumAccumulatorsByLeg;
                    uint256 _leg = leg;

                    // if there's still liquidity, compute the new grossPremiumLast
                    // otherwise, we just reset grossPremiumLast to the current grossPremium
                    s_grossPremiumLast[chunkKey] = totalLiquidity != 0
                        ? LeftRightUnsigned
                            .wrap(
                                uint128(
                                    uint256(
                                        Math.max(
                                            (int256(
                                                grossPremiumLast.rightSlot() * totalLiquidityBefore
                                            ) -
                                                int256(
                                                    _premiumAccumulatorsByLeg[_leg][0] *
                                                        positionLiquidity
                                                )) + int256(legPremia.rightSlot() * 2 ** 64),
                                            0
                                        )
                                    ) / totalLiquidity
                                )
                            )
                            .toLeftSlot(
                                uint128(
                                    uint256(
                                        Math.max(
                                            (int256(
                                                grossPremiumLast.leftSlot() * totalLiquidityBefore
                                            ) -
                                                int256(
                                                    _premiumAccumulatorsByLeg[_leg][1] *
                                                        positionLiquidity
                                                )) + int256(legPremia.leftSlot()) * 2 ** 64,
                                            0
                                        )
                                    ) / totalLiquidity
                                )
                            )
                        : LeftRightUnsigned
                            .wrap(uint128(premiumAccumulatorsByLeg[_leg][0]))
                            .toLeftSlot(uint128(premiumAccumulatorsByLeg[_leg][1]));
                }
            }
            // update settled tokens in storage with all local deltas
            s_settledTokens[chunkKey] = settledTokens;

            // erase the s_options entry for that leg
            s_options[owner][tokenId][leg] = LeftRightUnsigned.wrap(0);

            unchecked {
                ++leg;
            }
        }

        // reset balances and delete stored option data
        s_positionBalance[owner][tokenId] = PositionBalance.wrap(0);

        // REMOVE the current tokenId from the position list hash (hash = XOR of all keccak256(tokenId), remove by XOR'ing again)
        // and decrease the number of positions counter by 1.
        _updatePositionsHash(owner, tokenId, !ADD);
    }
}

File 2 of 50 : CollateralTracker.sol
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.24;

// Interfaces
import {PanopticPool} from "./PanopticPool.sol";
// Inherited implementations
import {Clone} from "clones-with-immutable-args/Clone.sol";
import {ERC20Minimal} from "@tokens/ERC20Minimal.sol";
import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol";
import {Multicall} from "@base/Multicall.sol";
// Libraries
import {Constants} from "@libraries/Constants.sol";
import {Errors} from "@libraries/Errors.sol";
import {InteractionHelper} from "@libraries/InteractionHelper.sol";
import {Math} from "@libraries/Math.sol";
import {PanopticMath} from "@libraries/PanopticMath.sol";
import {SafeTransferLib} from "@libraries/SafeTransferLib.sol";
// Custom types
import {Currency} from "v4-core/types/Currency.sol";
import {LeftRightUnsigned, LeftRightSigned} from "@types/LeftRight.sol";
import {LiquidityChunk} from "@types/LiquidityChunk.sol";
import {PositionBalance} from "@types/PositionBalance.sol";
import {TokenId} from "@types/TokenId.sol";

/// @title Collateral Tracking System / Margin Accounting used in conjunction with a Panoptic Pool.
/// @author Axicon Labs Limited
//
/// @notice Tracks collateral of users which is key to ensure the correct level of collateralization is achieved.
/// This is represented as an ERC20 share token. A Panoptic pool has 2 tokens, each issued by its own instance of a CollateralTracker.
/// All math within this contract pertains to a single token.
//
/// @notice This contract uses the ERC4626 standard allowing the minting and burning of "shares" (represented using ERC20 inheritance) in exchange for underlying "assets".
/// Panoptic uses a collateral tracking system that is similar to TradFi margin accounts. While users can borrow and
/// effectively control funds several times larger than the collateral they deposited, they cannot withdraw those funds
/// from the Panoptic-Uniswap ecosystem. All funds are always owned by the Panoptic protocol, but users will:
//
/// @notice 1) collect any fees generated by selling an option.
//
/// @notice 2) get any gain in capital that results from buying an option that becomes in-the-money.
contract CollateralTracker is Clone, ERC20Minimal, Multicall {
    using Math for uint256;

    /*//////////////////////////////////////////////////////////////
                                EVENTS
    //////////////////////////////////////////////////////////////*/

    /// @notice Emitted when assets are deposited into the Collateral Tracker.
    /// @param sender The address of the caller
    /// @param owner The address of the recipient of the newly minted shares
    /// @param assets The amount of assets deposited by `sender` in exchange for `shares`
    /// @param shares The amount of shares minted to `owner`
    event Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares);

    /// @notice Emitted when assets are withdrawn from the Collateral Tracker.
    /// @param sender The address of the caller
    /// @param receiver The address of the recipient of the withdrawn assets
    /// @param owner The address of the owner of the shares being burned
    /// @param assets The amount of assets withdrawn to `receiver`
    /// @param shares The amount of shares burned by `owner` in exchange for `assets`
    event Withdraw(
        address indexed sender,
        address indexed receiver,
        address indexed owner,
        uint256 assets,
        uint256 shares
    );

    /*//////////////////////////////////////////////////////////////
                               CONSTANTS
    //////////////////////////////////////////////////////////////*/

    /// @notice Prefix for the token symbol (i.e. poUSDC).
    string internal constant TICKER_PREFIX = "po";

    /// @notice Prefix for the token name (i.e POPT-V1.1 USDC LP on ETH/USDC 30bps).
    string internal constant NAME_PREFIX = "POPT-V1.1";

    /// @notice Decimals for computation (1 bps (basis point) precision: 0.01%).
    /// @dev uint type for composability with unsigned integer based mathematical operations.
    uint256 internal constant DECIMALS = 10_000;

    /// @notice Decimals for computation (1 bps (basis point) precision: 0.01%).
    /// @dev int type for composability with signed integer based mathematical operations.
    int128 internal constant DECIMALS_128 = 10_000;

    /*//////////////////////////////////////////////////////////////
                            RISK PARAMETERS
    //////////////////////////////////////////////////////////////*/

    /// @notice The commission fee, in basis points, collected from PLPs at option mint.
    /// @dev In Panoptic, options never expire, commissions are only paid when a new position is minted.
    /// @dev We believe that this will eliminate the impact of the commission fee on the user's decision-making process when closing a position.
    uint256 immutable COMMISSION_FEE;

    /// @notice Required collateral ratios for selling options, represented as percentage * 10_000.
    /// @dev i.e 20% -> 0.2 * 10_000 = 2_000.
    uint256 immutable SELLER_COLLATERAL_RATIO;

    /// @notice Required collateral ratios for buying options, represented as percentage * 10_000.
    /// @dev i.e 10% -> 0.1 * 10_000 = 1_000.
    uint256 immutable BUYER_COLLATERAL_RATIO;

    /// @notice Basal cost (in bps of notional) to force exercise an out-of-range position.
    int256 immutable FORCE_EXERCISE_COST;

    // Targets a pool utilization (balance between buying and selling)
    /// @notice Target pool utilization below which buying+selling is optimal, represented as percentage * 10_000.
    /// @dev i.e 50% -> 0.5 * 10_000 = 5_000.
    uint256 immutable TARGET_POOL_UTIL;

    /// @notice Pool utilization above which selling is 100% collateral backed, represented as percentage * 10_000.
    /// @dev i.e 90% -> 0.9 * 10_000 = 9_000.
    uint256 immutable SATURATED_POOL_UTIL;

    /// @notice Fee, in basis points, that is charged on the intrinsic value of ITM positions.
    uint256 immutable ITM_SPREAD_FEE;

    /// @notice The canonical Uniswap V4 Pool Manager address.
    IPoolManager internal immutable POOL_MANAGER_V4;

    /*//////////////////////////////////////////////////////////////
                         POOL UTILIZATION DATA
    //////////////////////////////////////////////////////////////*/

    /// @notice Cached amount of assets accounted to be held by the Panoptic Pool — ignores donations, pending fee payouts, and other untracked balance changes.
    uint128 internal s_poolAssets;

    /// @notice Amount of assets moved from the Panoptic Pool to the AMM.
    uint128 internal s_inAMM;

    /*//////////////////////////////////////////////////////////////
                          INITIALIZATION STATE
    //////////////////////////////////////////////////////////////*/

    /// @notice Boolean tracking whether this CollateralTracker has been initialized.
    bool internal s_initialized;

    /*//////////////////////////////////////////////////////////////
                   POOL-SPECIFIC IMMUTABLE PARAMETERS
    //////////////////////////////////////////////////////////////*/

    // The parameters will be encoded at `_getImmutableArgsOffset()` in calldata as follows:
    // abi.encodePacked(address panopticPool, bool underlyingIsCurrency0, address underlyingAsset, address currency0, address currency1, uint24 poolFee)
    // bytes: 0                    20                 21                   41                   61                   81
    //        |<----- 160 bits ---->|<---- 8 bits ---->|<---- 160 bits ---->|<---- 160 bits ---->|<---- 160 bits ---->|<---- 24 bits ---->|
    //             panopticPool    underlyingIsCurrency0   underlyingAsset         currency0            currency1            poolFee

    /// @notice Retrieve the Panoptic Pool that this collateral token belongs to.
    /// @return The Panoptic Pool associated with this collateral token
    function _panopticPool() internal pure returns (PanopticPool) {
        return PanopticPool(_getArgAddress(0));
    }

    /// @notice Retrieve a boolean indicating whether the underlying asset is currency0 or currency1 in the Uniswap V4 pool.
    /// @return underlyingIsCurrency0 True if the underlying asset is currency0, false if it is currency1
    function _underlyingIsCurrency0() internal pure returns (bool underlyingIsCurrency0) {
        uint256 offset = _getImmutableArgsOffset();

        assembly ("memory-safe") {
            underlyingIsCurrency0 := shr(0xf8, calldataload(add(offset, 20)))
        }
    }

    /// @notice Retrieve the address of the underlying asset.
    /// @return The address of the underlying asset
    function _underlyingAsset() internal pure returns (address) {
        return _getArgAddress(21);
    }

    /// @notice Retrieve the address of currency0 in the Uniswap V4 pool.
    /// @return The address of currency0 in the Uniswap V4 pool
    function _currency0() internal pure returns (address) {
        return _getArgAddress(41);
    }

    /// @notice Retrieve the address of currency1 in the Uniswap V4 pool.
    /// @return The address of currency1 in the Uniswap V4 pool
    function _currency1() internal pure returns (address) {
        return _getArgAddress(61);
    }

    /// @notice Retrieve the fee of the Uniswap V4 pool.
    /// @return poolFee The fee of the Uniswap V4 pool
    function _poolFee() internal pure returns (uint24 poolFee) {
        uint256 offset = _getImmutableArgsOffset();

        assembly ("memory-safe") {
            poolFee := shr(0xe8, calldataload(add(offset, 81)))
        }
    }

    /*//////////////////////////////////////////////////////////////
                            ACCESS CONTROL
    //////////////////////////////////////////////////////////////*/

    /// @notice Reverts if the associated Panoptic Pool is not the caller.
    modifier onlyPanopticPool() {
        if (msg.sender != address(_panopticPool())) revert Errors.NotPanopticPool();
        _;
    }

    /*//////////////////////////////////////////////////////////////
                  INITIALIZATION & PARAMETER SETTINGS
    //////////////////////////////////////////////////////////////*/

    /// @notice Set immutable parameters for the Collateral Tracker.
    /// @param _commissionFee The commission fee, in basis points, collected from PLPs at option mint
    /// @param _sellerCollateralRatio Required collateral ratio for selling options, represented as percentage * 10_000
    /// @param _buyerCollateralRatio Required collateral ratio for buying options, represented as percentage * 10_000
    /// @param _forceExerciseCost Basal cost (in bps of notional) to force exercise an out-of-range position
    /// @param _targetPoolUtilization Target pool utilization below which buying+selling is optimal, represented as percentage * 10_000
    /// @param _saturatedPoolUtilization Pool utilization above which selling is 100% collateral backed, represented as percentage * 10_000
    /// @param _ITMSpreadFee Fee, in basis points, that is charged on the intrinsic value of ITM positions
    /// @param _manager The canonical Uniswap V4 pool manager
    constructor(
        uint256 _commissionFee,
        uint256 _sellerCollateralRatio,
        uint256 _buyerCollateralRatio,
        int256 _forceExerciseCost,
        uint256 _targetPoolUtilization,
        uint256 _saturatedPoolUtilization,
        uint256 _ITMSpreadFee,
        IPoolManager _manager
    ) {
        COMMISSION_FEE = _commissionFee;
        SELLER_COLLATERAL_RATIO = _sellerCollateralRatio;
        BUYER_COLLATERAL_RATIO = _buyerCollateralRatio;
        FORCE_EXERCISE_COST = _forceExerciseCost;
        TARGET_POOL_UTIL = _targetPoolUtilization;
        SATURATED_POOL_UTIL = _saturatedPoolUtilization;
        ITM_SPREAD_FEE = _ITMSpreadFee;
        POOL_MANAGER_V4 = _manager;
    }

    /// @notice Initializes a new `CollateralTracker` instance with 1 virtual asset and 10^6 virtual shares.
    function initialize() external {
        if (s_initialized) revert Errors.CollateralTokenAlreadyInitialized();
        s_initialized = true;

        // these virtual shares function as a multiplier for the capital requirement to manipulate the pool price
        // e.g. if the virtual shares are 10**6, then the capital requirement to manipulate the price to 10**12 is 10**18
        totalSupply = 10 ** 6;

        // set total assets to 1
        // the initial share price is defined by 1/virtualShares
        s_poolAssets = 1;
    }

    /*//////////////////////////////////////////////////////////////
                      COLLATERAL TOKEN INFORMATION
    //////////////////////////////////////////////////////////////*/

    /// @notice Get information about the utilization of this collateral vault.
    /// @return poolAssets Cached amount of assets accounted to be held by the Panoptic Pool — ignores donations, pending fee payouts, and other untracked balance changes
    /// @return insideAMM The underlying asset amount held in the AMM
    /// @return currentPoolUtilization The pool utilization defined as`s_inAMM * 10_000 / totalAssets()`,
    /// where totalAssets is the total tracked assets in the AMM and PanopticPool minus fees and donations to the Panoptic pool
    function getPoolData()
        external
        view
        returns (uint256 poolAssets, uint256 insideAMM, uint256 currentPoolUtilization)
    {
        poolAssets = s_poolAssets;
        insideAMM = s_inAMM;
        currentPoolUtilization = _poolUtilization();
    }

    /// @notice Returns name of token composed of underlying asset symbol and pool data.
    /// @return The name of the token
    function name() external view returns (string memory) {
        // this logic requires multiple external calls and error handling, so we do it in a delegatecall to a library to save bytecode size
        return
            InteractionHelper.computeName(
                _currency0(),
                _currency1(),
                _underlyingIsCurrency0(),
                _poolFee(),
                NAME_PREFIX
            );
    }

    /// @notice Returns symbol as prefixed symbol of underlying asset.
    /// @return The symbol of the token
    function symbol() external view returns (string memory) {
        // this logic requires multiple external calls and error handling, so we do it in a delegatecall to a library to save bytecode size
        return InteractionHelper.computeSymbol(_underlyingAsset(), TICKER_PREFIX);
    }

    /// @notice Returns decimals of underlying asset (0 if not present).
    /// @return The decimals of the token
    function decimals() external view returns (uint8) {
        // this logic requires multiple external calls and error handling, so we do it in a delegatecall to a library to save bytecode size
        return InteractionHelper.computeDecimals(_underlyingAsset());
    }

    /*//////////////////////////////////////////////////////////////
                     LIMITED TRANSFER FUNCTIONS
    //////////////////////////////////////////////////////////////*/

    /// @dev See {IERC20-transfer}.
    /// @dev Requirements:
    /// - the caller must have a balance of at least `amount`.
    /// - the caller must not have any open positions on the Panoptic Pool.
    function transfer(
        address recipient,
        uint256 amount
    ) public override(ERC20Minimal) returns (bool) {
        // make sure the caller does not have any open option positions
        // if they do: we don't want them sending panoptic pool shares to others
        // as this would reduce their amount of collateral against the opened positions

        if (_panopticPool().numberOfLegs(msg.sender) != 0) revert Errors.PositionCountNotZero();

        return ERC20Minimal.transfer(recipient, amount);
    }

    /// @dev See {IERC20-transferFrom}.
    /// @dev Requirements:
    /// - the `from` must have a balance of at least `amount`.
    /// - the caller must have allowance for `from` of at least `amount` tokens.
    /// - `from` must not have any open positions on the Panoptic Pool.
    function transferFrom(
        address from,
        address to,
        uint256 amount
    ) public override(ERC20Minimal) returns (bool) {
        // make sure the sender does not have any open option positions
        // if they do: we don't want them sending panoptic pool shares to others
        // as this would reduce their amount of collateral against the opened positions

        if (_panopticPool().numberOfLegs(from) != 0) revert Errors.PositionCountNotZero();

        return ERC20Minimal.transferFrom(from, to, amount);
    }

    /*//////////////////////////////////////////////////////////////
                        UNISWAP V4 LOCK CALLBACK
    //////////////////////////////////////////////////////////////*/

    /// @notice Initiates the unlock callback to wrap/unwrap `delta` amount of the underlying asset and transfer to/from the Panoptic Pool.
    /// @param account The address of the account to transfer the underlying asset to/from
    /// @param delta The amount of the underlying asset to wrap/unwrap and transfer
    function _settleCurrencyDelta(address account, int256 delta) internal {
        POOL_MANAGER_V4.unlock(abi.encode(account, delta, msg.value));
    }

    /// @notice Uniswap V4 unlock callback implementation.
    /// @dev Parameters are `(address account, int256 delta, uint256 valueOrigin)`.
    /// @dev Wraps/unwraps `delta` amount of the underlying asset and transfers to/from the Panoptic Pool.
    /// @param data The encoded data containing the account and delta
    /// @return This function returns no data
    function unlockCallback(bytes calldata data) external returns (bytes memory) {
        if (msg.sender != address(POOL_MANAGER_V4)) revert Errors.UnauthorizedUniswapCallback();

        (address account, int256 delta, uint256 valueOrigin) = abi.decode(
            data,
            (address, int256, uint256)
        );

        address underlyingAsset = _underlyingAsset();
        if (delta > 0) {
            if (Currency.wrap(underlyingAsset).isAddressZero()) {
                POOL_MANAGER_V4.settle{value: uint256(delta)}();

                uint256 surplus = valueOrigin - uint256(delta);
                if (surplus > 0) SafeTransferLib.safeTransferETH(account, surplus);
            } else {
                POOL_MANAGER_V4.sync(Currency.wrap(underlyingAsset));
                SafeTransferLib.safeTransferFrom(
                    underlyingAsset,
                    account,
                    address(POOL_MANAGER_V4),
                    uint256(delta)
                );
                POOL_MANAGER_V4.settle();
            }

            POOL_MANAGER_V4.mint(
                address(_panopticPool()),
                uint160(underlyingAsset),
                uint256(delta)
            );
        } else if (delta < 0) {
            unchecked {
                delta = -delta;
            }
            POOL_MANAGER_V4.burn(
                address(_panopticPool()),
                uint160(underlyingAsset),
                uint256(delta)
            );
            POOL_MANAGER_V4.take(Currency.wrap(underlyingAsset), account, uint256(delta));
        }

        return "";
    }

    /*//////////////////////////////////////////////////////////////
                     STANDARD ERC4626 INTERFACE
    //////////////////////////////////////////////////////////////*/

    /// @notice Get the address of the underlying asset being managed (`address(0)` = native asset).
    /// @return assetTokenAddress The address of the underlying asset
    function asset() external pure returns (address assetTokenAddress) {
        return _underlyingAsset();
    }

    /// @notice Get the total amount of assets managed by the CollateralTracker vault.
    /// @dev This returns the total tracked assets in the AMM and PanopticPool,
    /// @dev - EXCLUDING the amount of collected fees (because they are reserved for short options)
    /// @dev - EXCLUDING any donations that have been made to the pool
    /// @return The total amount of assets managed by the CollateralTracker vault
    function totalAssets() public view returns (uint256) {
        unchecked {
            return uint256(s_poolAssets) + s_inAMM;
        }
    }

    /// @notice Returns the amount of shares that can be minted for the given amount of assets.
    /// @param assets The amount of assets to be deposited
    /// @return shares The amount of shares that can be minted
    function convertToShares(uint256 assets) public view returns (uint256 shares) {
        return Math.mulDiv(assets, totalSupply, totalAssets());
    }

    /// @notice Returns the amount of assets that can be redeemed for the given amount of shares.
    /// @param shares The amount of shares to be redeemed
    /// @return assets The amount of assets that can be redeemed
    function convertToAssets(uint256 shares) public view returns (uint256 assets) {
        return Math.mulDiv(shares, totalAssets(), totalSupply);
    }

    /// @notice Returns the maximum deposit amount.
    /// @return maxAssets The maximum amount of assets that can be deposited
    function maxDeposit(address) external pure returns (uint256 maxAssets) {
        return type(uint104).max;
    }

    /// @notice Returns shares received for depositing given amount of assets.
    /// @param assets The amount of assets to be deposited
    /// @return shares The amount of shares that can be minted
    function previewDeposit(uint256 assets) public view returns (uint256 shares) {
        // compute the MEV tax, which is equal to a single payment of the commissionRate on the FINAL (post mev-tax) assets paid
        unchecked {
            shares = Math.mulDiv(
                assets * (DECIMALS - COMMISSION_FEE),
                totalSupply,
                totalAssets() * DECIMALS
            );
        }
    }

    /// @notice Deposit underlying assets (assets) to the Panoptic pool from the LP and mint corresponding amount of shares.
    /// @dev If depositing native currency (`asset() == address(0)`), non-EOA callers *must* accept empty calls with value up to the amount attached.
    /// @dev There is a maximum asset deposit limit of `2^104 - 1`.
    /// @dev An "MEV tax" is levied, which is equal to a single payment of the commissionRate BEFORE adding the funds.
    /// @dev Shares are minted and sent to the LP (`receiver`).
    /// @param assets Amount of assets deposited
    /// @param receiver User to receive the shares
    /// @return shares The amount of Panoptic pool shares that were minted to the recipient
    function deposit(uint256 assets, address receiver) external payable returns (uint256 shares) {
        if (assets > type(uint104).max) revert Errors.DepositTooLarge();

        shares = previewDeposit(assets);

        _mint(receiver, shares);

        // update tracked asset balance
        s_poolAssets += uint128(assets);

        // transfer assets from the user/the LP to the PanopticPool
        // in return for the shares to be minted
        _settleCurrencyDelta(msg.sender, int256(assets));

        emit Deposit(msg.sender, receiver, assets, shares);
    }

    /// @notice Returns the maximum shares received for a deposit.
    /// @return maxShares The maximum amount of shares that can be minted
    function maxMint(address) external view returns (uint256 maxShares) {
        unchecked {
            return (convertToShares(type(uint104).max) * (DECIMALS - COMMISSION_FEE)) / DECIMALS;
        }
    }

    /// @notice Returns the amount of assets that would be deposited to mint a given amount of shares.
    /// @param shares The amount of shares to be minted
    /// @return assets The amount of assets required to mint `shares`
    function previewMint(uint256 shares) public view returns (uint256 assets) {
        // round up depositing assets to avoid protocol loss
        // This prevents minting of shares where the assets provided is rounded down to zero
        // compute the MEV tax, which is equal to a single payment of the commissionRate on the FINAL (post mev-tax) assets paid
        // finalAssets - convertedAssets = commissionRate * finalAssets
        // finalAssets - commissionRate * finalAssets = convertedAssets
        // finalAssets * (1 - commissionRate) = convertedAssets
        // finalAssets = convertedAssets / (1 - commissionRate)
        assets = Math.mulDivRoundingUp(
            shares * DECIMALS,
            totalAssets(),
            totalSupply * (DECIMALS - COMMISSION_FEE)
        );
    }

    /// @notice Deposit required amount of assets to receive specified amount of shares.
    /// @dev If depositing native currency (`asset() == address(0)`), non-EOA callers *must* accept empty calls with value up to the amount attached.
    /// @dev There is a maximum asset deposit limit of `2^104 - 1`.
    /// @dev An "MEV tax" is levied, which is equal to a single payment of the commissionRate BEFORE adding the funds.
    /// @dev Shares are minted and sent to the LP (`receiver`).
    /// @param shares Amount of shares to be minted
    /// @param receiver User to receive the shares
    /// @return assets The amount of assets deposited to mint the desired amount of shares
    function mint(uint256 shares, address receiver) external payable returns (uint256 assets) {
        assets = previewMint(shares);

        if (assets > type(uint104).max) revert Errors.DepositTooLarge();

        _mint(receiver, shares);

        // update tracked asset balance
        s_poolAssets += uint128(assets);

        // transfer assets from the user/the LP to the PanopticPool
        // in return for the shares to be minted
        _settleCurrencyDelta(msg.sender, int256(assets));

        emit Deposit(msg.sender, receiver, assets, shares);
    }

    /// @notice Returns The maximum amount of assets that can be withdrawn for a given user.
    /// If the user has any open positions, the max withdrawable balance is zero.
    /// @dev Calculated from the balance of the user; limited by the assets the pool has available.
    /// @param owner The address being withdrawn for
    /// @return maxAssets The maximum amount of assets that can be withdrawn
    function maxWithdraw(address owner) public view returns (uint256 maxAssets) {
        uint256 poolAssets = s_poolAssets;
        unchecked {
            uint256 available = poolAssets > 0 ? poolAssets - 1 : 0;
            uint256 balance = convertToAssets(balanceOf[owner]);
            return _panopticPool().numberOfLegs(owner) == 0 ? Math.min(available, balance) : 0;
        }
    }

    /// @notice Returns the amount of shares that would be burned to withdraw a given amount of assets.
    /// @param assets The amount of assets to be withdrawn
    /// @return shares The amount of shares that would be burned
    function previewWithdraw(uint256 assets) public view returns (uint256 shares) {
        uint256 supply = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero.

        return Math.mulDivRoundingUp(assets, supply, totalAssets());
    }

    /// @notice Redeem the amount of shares required to withdraw the specified amount of assets.
    /// @dev We can only use this standard 4626 function if the user has no open positions.
    /// @dev Shares are burned and assets are sent to the LP (`receiver`).
    /// @param assets Amount of assets to be withdrawn
    /// @param receiver User to receive the assets
    /// @param owner User to burn the shares from
    /// @return shares The amount of shares burned to withdraw the desired amount of assets
    function withdraw(
        uint256 assets,
        address receiver,
        address owner
    ) external returns (uint256 shares) {
        if (assets > maxWithdraw(owner)) revert Errors.ExceedsMaximumRedemption();

        shares = previewWithdraw(assets);

        // check/update allowance for approved withdraw
        if (msg.sender != owner) {
            uint256 allowed = allowance[owner][msg.sender];

            if (allowed != type(uint256).max) allowance[owner][msg.sender] = allowed - shares; // Saves gas for unlimited approvals.
        }

        _burn(owner, shares);

        // update tracked asset balance
        unchecked {
            s_poolAssets -= uint128(assets);
        }

        // transfer assets from the PanopticPool to the LP
        unchecked {
            _settleCurrencyDelta(receiver, -int256(assets));
        }

        emit Withdraw(msg.sender, receiver, owner, assets, shares);
    }

    /// @notice Redeem the amount of shares required to withdraw the specified amount of assets.
    /// @dev Reverts if the account is not solvent with the given `positionIdList`.
    /// @dev Shares are burned and assets are sent to the LP (`receiver`).
    /// @param assets Amount of assets to be withdrawn
    /// @param receiver User to receive the assets
    /// @param owner User to burn the shares from
    /// @param positionIdList The list of all option positions held by `owner`
    /// @return shares The amount of shares burned to withdraw the desired amount of assets
    /// @param usePremiaAsCollateral Whether to compute accumulated premia for all legs held by the user for collateral (true), or just owed premia for long legs (false)
    function withdraw(
        uint256 assets,
        address receiver,
        address owner,
        TokenId[] calldata positionIdList,
        bool usePremiaAsCollateral
    ) external returns (uint256 shares) {
        shares = previewWithdraw(assets);

        // check/update allowance for approved withdraw
        if (msg.sender != owner) {
            uint256 allowed = allowance[owner][msg.sender];

            if (allowed != type(uint256).max) allowance[owner][msg.sender] = allowed - shares; // Saves gas for unlimited approvals.
        }

        _burn(owner, shares);

        // update tracked asset balance
        s_poolAssets -= uint128(assets);

        // reverts if account is not solvent/eligible to withdraw
        _panopticPool().validateCollateralWithdrawable(
            owner,
            positionIdList,
            usePremiaAsCollateral
        );

        // transfer assets from the PanopticPool to the LP
        unchecked {
            _settleCurrencyDelta(receiver, -int256(assets));
        }

        emit Withdraw(msg.sender, receiver, owner, assets, shares);
    }

    /// @notice Returns the maximum amount of shares that can be redeemed for a given user.
    /// @dev If the user has any open positions, the max redeemable balance is zero.
    /// @param owner The redeeming address
    /// @return maxShares The maximum amount of shares that can be redeemed by `owner`
    function maxRedeem(address owner) public view returns (uint256 maxShares) {
        uint256 poolAssets = s_poolAssets;
        unchecked {
            uint256 available = convertToShares(poolAssets > 0 ? poolAssets - 1 : 0);
            uint256 balance = balanceOf[owner];
            return _panopticPool().numberOfLegs(owner) == 0 ? Math.min(available, balance) : 0;
        }
    }

    /// @notice Returns the amount of assets resulting from a given amount of shares being redeemed.
    /// @param shares The amount of shares to be redeemed
    /// @return assets The amount of assets resulting from the redemption
    function previewRedeem(uint256 shares) public view returns (uint256 assets) {
        return convertToAssets(shares);
    }

    /// @notice Redeem exact shares for underlying assets.
    /// @dev We can only use this standard 4626 function if the user has no open positions.
    /// @param shares Amount of shares to be redeemed
    /// @param receiver User to receive the assets
    /// @param owner User to burn the shares from
    /// @return assets The amount of assets resulting from the redemption
    function redeem(
        uint256 shares,
        address receiver,
        address owner
    ) external returns (uint256 assets) {
        if (shares > maxRedeem(owner)) revert Errors.ExceedsMaximumRedemption();

        // check/update allowance for approved redeem
        if (msg.sender != owner) {
            uint256 allowed = allowance[owner][msg.sender];

            if (allowed != type(uint256).max) allowance[owner][msg.sender] = allowed - shares; // Saves gas for unlimited approvals.
        }

        assets = previewRedeem(shares);

        _burn(owner, shares);

        // update tracked asset balance
        unchecked {
            s_poolAssets -= uint128(assets);
        }

        // transfer assets from the PanopticPool to the LP
        unchecked {
            _settleCurrencyDelta(receiver, -int256(assets));
        }

        emit Withdraw(msg.sender, receiver, owner, assets, shares);
    }

    /*//////////////////////////////////////////////////////////////
                            ACCOUNTING LOGIC
    //////////////////////////////////////////////////////////////*/

    /// @notice Get the cost of exercising an option. Used during a forced exercise.
    /// @notice This one computes the cost of calling the forceExercise function on a position:
    /// - The forceExercisor will have to *pay* the exercisee because their position will be closed "against their will"
    /// - The cost must be larger when the position is close to being in-range, and should be minimal when it is far from being in range. eg. Exercising a (1000, 1050)
    ///   position will cost more if the price is 999 than if it is 100
    /// - The cost is an exponentially decaying function of the distance between the position's strike and the current price
    /// - The cost decreases by a factor of 2 for every "position's width"
    /// - Note that the cost is the largest among all active legs, not the sum
    /// @notice Example exercise cost progression:
    /// - 10% if the position is liquidated when the price is between 950 and 1000, or if it is between 1050 and 1100
    /// - 5% if the price is between 900 and 950 or (1100, 1150)
    /// - 2.5% if between (850, 900) or (1150, 1200)
    /// @param currentTick The current price tick
    /// @param oracleTick The price oracle tick
    /// @param positionId The position to be exercised
    /// @param positionSize The size of the position to be exercised
    /// @param longAmounts The amount of longs in the position
    /// @return exerciseFees The fees for exercising the option position
    function exerciseCost(
        int24 currentTick,
        int24 oracleTick,
        TokenId positionId,
        uint128 positionSize,
        LeftRightSigned longAmounts
    ) external view returns (LeftRightSigned exerciseFees) {
        // find the leg furthest to the strike price `currentTick`; this will have the lowest exercise cost
        // we don't need the leg information itself, really just "the number of half ranges" from the strike price:
        uint256 maxNumRangesFromStrike = 1; // technically "maxNum(Half)RangesFromStrike" but the name is long

        unchecked {
            for (uint256 leg = 0; leg < positionId.countLegs(); ++leg) {
                // short legs are not counted - exercise is intended to be based on long legs
                if (positionId.isLong(leg) == 0) continue;

                {
                    int24 range = int24(
                        int256(
                            Math.unsafeDivRoundingUp(
                                uint24(positionId.width(leg) * positionId.tickSpacing()),
                                2
                            )
                        )
                    );
                    maxNumRangesFromStrike = Math.max(
                        maxNumRangesFromStrike,
                        uint256(Math.abs(currentTick - positionId.strike(leg)) / range)
                    );
                }

                uint256 currentValue0;
                uint256 currentValue1;
                uint256 oracleValue0;
                uint256 oracleValue1;

                {
                    LiquidityChunk liquidityChunk = PanopticMath.getLiquidityChunk(
                        positionId,
                        leg,
                        positionSize
                    );

                    (currentValue0, currentValue1) = Math.getAmountsForLiquidity(
                        currentTick,
                        liquidityChunk
                    );

                    (oracleValue0, oracleValue1) = Math.getAmountsForLiquidity(
                        oracleTick,
                        liquidityChunk
                    );
                }

                // reverse any deltas between the current and oracle prices for the chunk the exercisee had to mint in Uniswap
                // the outcome of current price crossing a long chunk will always be less favorable than the status quo, i.e.,
                // if the current price is moved downward such that some part of the chunk is between the current and market prices,
                // the chunk composition will swap currency1 for currency0 at a price (currency0/currency1) more favorable than market (currency1/currency0),
                // forcing the exercisee to provide more value in currency0 than they would have provided in currency1 at market, and vice versa.
                // (the excess value provided by the exercisee could then be captured in a return swap across their newly added liquidity)
                exerciseFees = exerciseFees.sub(
                    LeftRightSigned
                        .wrap(0)
                        .toRightSlot(int128(uint128(currentValue0)) - int128(uint128(oracleValue0)))
                        .toLeftSlot(int128(uint128(currentValue1)) - int128(uint128(oracleValue1)))
                );
            }

            // NOTE: we HAVE to start with a negative number as the base exercise cost because when shifting a negative number right by n bits,
            // the result is rounded DOWN and NOT toward zero
            // this divergence is observed when n (the number of half ranges) is > 10 (ensuring the floor is not zero, but -1 = 1bps at that point)
            // subtract 1 from max half ranges from strike so fee starts at FORCE_EXERCISE_COST when moving OTM
            int256 fee = (FORCE_EXERCISE_COST >> (maxNumRangesFromStrike - 1)); // exponential decay of fee based on number of half ranges away from the price

            // store the exercise fees in the exerciseFees variable
            exerciseFees = exerciseFees
                .toRightSlot(int128((longAmounts.rightSlot() * fee) / DECIMALS_128))
                .toLeftSlot(int128((longAmounts.leftSlot() * fee) / DECIMALS_128));
        }
    }

    /// @notice Get the pool utilization defined by the ratio of assets in the AMM to total assets.
    /// @return poolUtilization The pool utilization in basis points
    function _poolUtilization() internal view returns (uint256 poolUtilization) {
        unchecked {
            return (s_inAMM * DECIMALS) / totalAssets();
        }
    }

    /// @notice Get the base collateral requirement for a short leg at a given pool utilization.
    /// @dev This is computed at the time the position is minted.
    /// @param utilization The pool utilization of this collateral vault at the time the position is minted
    /// @return sellCollateralRatio The sell collateral ratio at `utilization`
    function _sellCollateralRatio(
        int256 utilization
    ) internal view returns (uint256 sellCollateralRatio) {
        // the sell ratio is on a straight line defined between two points (x0,y0) and (x1,y1):
        //   (x0,y0) = (targetPoolUtilization,min_sell_ratio) and
        //   (x1,y1) = (saturatedPoolUtilization,max_sell_ratio)
        // the line's formula: y = a * (x - x0) + y0, where a = (y1 - y0) / (x1 - x0)
        /*
            SELL
            COLLATERAL
            RATIO
                          ^
                          |                  max ratio = 100%
                   100% - |                _------
                          |             _-¯
                          |          _-¯
                    20% - |---------¯
                          |         .       . .
                          +---------+-------+-+--->   POOL_
                                   50%    90% 100%     UTILIZATION
        */

        uint256 min_sell_ratio = SELLER_COLLATERAL_RATIO;
        /// if utilization is less than zero, this is the calculation for a strangle, which gets 2x the capital efficiency at low pool utilization
        if (utilization < 0) {
            unchecked {
                min_sell_ratio /= 2;
                utilization = -utilization;
            }
        }

        // return the basal sell ratio if pool utilization is lower than target
        if (uint256(utilization) < TARGET_POOL_UTIL) {
            return min_sell_ratio;
        }

        // return 100% collateral ratio if utilization is above saturated pool utilization
        if (uint256(utilization) > SATURATED_POOL_UTIL) {
            return DECIMALS;
        }

        unchecked {
            return
                min_sell_ratio +
                ((DECIMALS - min_sell_ratio) * (uint256(utilization) - TARGET_POOL_UTIL)) /
                (SATURATED_POOL_UTIL - TARGET_POOL_UTIL);
        }
    }

    /// @notice Get the base collateral requirement for a long leg at a given pool utilization.
    /// @dev This is computed at the time the position is minted.
    /// @param utilization The pool utilization of this collateral vault at the time the position is minted
    /// @return buyCollateralRatio The buy collateral ratio at `utilization`
    function _buyCollateralRatio(
        uint16 utilization
    ) internal view returns (uint256 buyCollateralRatio) {
        // linear from BUY to BUY/2 between 50% and 90%
        // the buy ratio is on a straight line defined between two points (x0,y0) and (x1,y1):
        //   (x0,y0) = (targetPoolUtilization,buyCollateralRatio) and
        //   (x1,y1) = (saturatedPoolUtilization,buyCollateralRatio / 2)
        // note that y1<y0 so the slope is negative:
        // aka the buy ratio starts high and drops to a lower value with increased utilization; the sell ratio does the opposite (slope is positive)
        // the line's formula: y = a * (x - x0) + y0, where a = (y1 - y0) / (x1 - x0)
        // but since a<0, we rewrite as:
        // y = a' * (x0 - x) + y0, where a' = (y0 - y1) / (x1 - x0)

        /*
          BUY
          COLLATERAL
          RATIO
                 ^
                 |   buy_ratio = 10%
           10% - |----------__       min_ratio = 5%
           5%  - | . . . . .  ¯¯¯--______
                 |         .       . .
                 +---------+-------+-+--->   POOL_
                          50%    90% 100%      UTILIZATION
         */

        // return the basal buy ratio if pool utilization is lower than target
        if (utilization < TARGET_POOL_UTIL) {
            return BUYER_COLLATERAL_RATIO;
        }

        // return the basal ratio divided by 2 if pool utilization is above saturated pool utilization
        /// this incentivizes option buying, which returns funds to the Panoptic pool
        if (utilization > SATURATED_POOL_UTIL) {
            unchecked {
                return BUYER_COLLATERAL_RATIO / 2;
            }
        }

        unchecked {
            return
                (BUYER_COLLATERAL_RATIO +
                    (BUYER_COLLATERAL_RATIO * (SATURATED_POOL_UTIL - utilization)) /
                    (SATURATED_POOL_UTIL - TARGET_POOL_UTIL)) / 2; // do the division by 2 at the end after all addition and multiplication; b/c y1 = buyCollateralRatio / 2
        }
    }

    /*////////////////////////////////////////////////////////////////////
          LIFECYCLE OF A COLLATERAL TOKEN AND DELEGATE/REVOKE LOGIC
    ////////////////////////////////////////////////////////////////////*/

    /// @notice Increase the share balance of a user by `2^248 - 1` without updating the total supply.
    /// @dev This is controlled by the Panoptic Pool - not individual users.
    /// @param delegatee The account to increase the balance of
    function delegate(address delegatee) external onlyPanopticPool {
        balanceOf[delegatee] += type(uint248).max;
    }

    /// @notice Decrease the share balance of a user by `2^248 - 1` without updating the total supply.
    /// @dev Assumes that `delegatee` has `>=(2^248 - 1)` tokens, will revert otherwise.
    /// @dev This is controlled by the Panoptic Pool - not individual users.
    /// @param delegatee The account to decrease the balance of
    function revoke(address delegatee) external onlyPanopticPool {
        balanceOf[delegatee] -= type(uint248).max;
    }

    /// @notice Settles liquidation bonus and returns remaining virtual shares to the protocol.
    /// @dev This function is where protocol loss is realized, if it exists.
    /// @param liquidator The account performing the liquidation of `liquidatee`
    /// @param liquidatee The liquidated account to settle
    /// @param bonus The liquidation bonus, in assets, to be paid to `liquidator`. May be negative
    function settleLiquidation(
        address liquidator,
        address liquidatee,
        int256 bonus
    ) external payable onlyPanopticPool {
        if (bonus < 0) {
            uint256 bonusAbs;

            unchecked {
                bonusAbs = uint256(-bonus);
            }

            _mint(liquidatee, convertToShares(bonusAbs));

            s_poolAssets += uint128(bonusAbs);

            uint256 liquidateeBalance = balanceOf[liquidatee];

            if (type(uint248).max > liquidateeBalance) {
                balanceOf[liquidatee] = 0;
                unchecked {
                    totalSupply += type(uint248).max - liquidateeBalance;
                }
            } else {
                unchecked {
                    balanceOf[liquidatee] = liquidateeBalance - type(uint248).max;
                }
            }

            _settleCurrencyDelta(liquidator, int256(bonusAbs));
        } else {
            uint256 liquidateeBalance = balanceOf[liquidatee];

            if (type(uint248).max > liquidateeBalance) {
                unchecked {
                    totalSupply += type(uint248).max - liquidateeBalance;
                }
                liquidateeBalance = 0;
            } else {
                unchecked {
                    liquidateeBalance -= type(uint248).max;
                }
            }

            balanceOf[liquidatee] = liquidateeBalance;

            uint256 bonusShares = convertToShares(uint256(bonus));

            // if requested amount is larger than user balance, transfer their balance and mint the remaining shares
            if (bonusShares > liquidateeBalance) {
                _transferFrom(liquidatee, liquidator, liquidateeBalance);

                // this is paying out protocol loss, so correct for that in the amount of shares to be minted
                // X: total assets in vault
                // Y: total supply of shares
                // Z: desired value (assets) of shares to be minted
                // N: total shares corresponding to Z
                // T: transferred shares from liquidatee which are a component of N but do not contribute toward protocol loss
                // Z = N * X / (Y + N - T)
                // Z * (Y + N - T) = N * X
                // ZY + ZN - ZT = NX
                // ZY - ZT = N(X - Z)
                // N = (ZY - ZT) / (X - Z)
                // N = Z(Y - T) / (X - Z)
                // subtract delegatee balance from N since it was already transferred to the delegator
                uint256 _totalSupply = totalSupply;
                unchecked {
                    _mint(
                        liquidator,
                        Math.min(
                            Math.mulDivCapped(
                                uint256(bonus),
                                _totalSupply - liquidateeBalance,
                                uint256(Math.max(1, int256(totalAssets()) - bonus))
                            ) - liquidateeBalance,
                            _totalSupply * DECIMALS
                        )
                    );
                }
            } else {
                _transferFrom(liquidatee, liquidator, bonusShares);
            }

            // refund liquidator if they attached value expecting to settle a negative bonus in the native currency
            if (msg.value > 0) SafeTransferLib.safeTransferETH(liquidator, msg.value);
        }
    }

    /// @notice Refunds tokens to `refunder` from `refundee`.
    /// @dev Assumes that the refunder has enough money to pay for the refund.
    /// @param refunder The account refunding tokens to `refundee`
    /// @param refundee The account being refunded to
    /// @param assets The amount of assets to refund. Positive means a transfer from refunder to refundee, vice versa for negative
    function refund(address refunder, address refundee, int256 assets) external onlyPanopticPool {
        if (assets > 0) {
            _transferFrom(refunder, refundee, convertToShares(uint256(assets)));
        } else {
            unchecked {
                _transferFrom(refundee, refunder, convertToShares(uint256(-assets)));
            }
        }
    }

    /*//////////////////////////////////////////////////////////////
                     OPTION EXERCISE AND COMMISSION
    //////////////////////////////////////////////////////////////*/

    /// @notice Take commission and settle ITM amounts on option creation.
    /// @param optionOwner The user minting the option
    /// @param longAmount The amount of longs
    /// @param shortAmount The amount of shorts
    /// @param swappedAmount The amount of tokens moved during creation of the option position
    /// @param isCovered Whether the option was minted as covered (no swap occurred if ITM)
    /// @return The final utilization of the collateral vault
    /// @return The total amount of commission (base rate + ITM spread) paid
    function takeCommissionAddData(
        address optionOwner,
        int128 longAmount,
        int128 shortAmount,
        int128 swappedAmount,
        bool isCovered
    ) external onlyPanopticPool returns (uint32, uint128) {
        unchecked {
            // current available assets belonging to PLPs (updated after settlement) excluding any premium paid
            int256 updatedAssets = int256(uint256(s_poolAssets)) - swappedAmount;

            (int256 tokenToPay, uint128 commission) = _getExchangedAmount(
                longAmount,
                shortAmount,
                swappedAmount,
                isCovered
            );

            // compute tokens to be paid due to swap
            // mint or burn tokens due to minting in-the-money
            if (tokenToPay > 0) {
                // if user must pay tokens, burn them from user balance
                uint256 sharesToBurn = Math.mulDivRoundingUp(
                    uint256(tokenToPay),
                    totalSupply,
                    totalAssets()
                );
                _burn(optionOwner, sharesToBurn);
            } else if (tokenToPay < 0) {
                // if user must receive tokens, mint them
                uint256 sharesToMint = convertToShares(uint256(-tokenToPay));
                _mint(optionOwner, sharesToMint);
            }

            // update stored asset balances with net moved amounts
            // the inflow or outflow of pool assets is defined by the swappedAmount: it includes both the ITM swap amounts and the short/long amounts used to create the position
            // however, any intrinsic value is paid for by the users, so we only add the portion that comes from PLPs: the short/long amounts
            // premia is not included in the balance since it is the property of options buyers and sellers, not PLPs
            s_poolAssets = uint256(updatedAssets).toUint128();
            s_inAMM = uint256(int256(uint256(s_inAMM)) + (shortAmount - longAmount)).toUint128();

            return (uint32(_poolUtilization()), commission);
        }
    }

    /// @notice Exercise an option and pay to the seller what is owed from the buyer.
    /// @dev Called when a position is burnt because it may need to be exercised.
    /// @param optionOwner The owner of the option being burned
    /// @param longAmount The notional value of the long legs of the position (if any)
    /// @param shortAmount The notional value of the short legs of the position (if any)
    /// @param swappedAmount The amount of tokens moved during the option close
    /// @param realizedPremium Premium to settle on the current positions
    /// @return The amount of tokens paid when closing that position
    function exercise(
        address optionOwner,
        int128 longAmount,
        int128 shortAmount,
        int128 swappedAmount,
        int128 realizedPremium
    ) external onlyPanopticPool returns (int128) {
        unchecked {
            // current available assets belonging to PLPs (updated after settlement) excluding any premium paid
            int256 updatedAssets = int256(uint256(s_poolAssets)) - swappedAmount;

            // add premium and token deltas not covered by swap to be paid/collected on position close
            int256 tokenToPay = int256(swappedAmount) -
                (longAmount - shortAmount) -
                realizedPremium;

            if (tokenToPay > 0) {
                // if user must pay tokens, burn them from user balance (revert if balance too small)
                uint256 sharesToBurn = Math.mulDivRoundingUp(
                    uint256(tokenToPay),
                    totalSupply,
                    totalAssets()
                );
                _burn(optionOwner, sharesToBurn);
            } else if (tokenToPay < 0) {
                // if user must receive tokens, mint them
                uint256 sharesToMint = convertToShares(uint256(-tokenToPay));
                _mint(optionOwner, sharesToMint);
            }

            // update stored asset balances with net moved amounts
            // any intrinsic value is paid for by the users, so we do not add it to s_inAMM
            // premia is not included in the balance since it is the property of options buyers and sellers, not PLPs
            s_poolAssets = uint256(updatedAssets + realizedPremium).toUint128();
            s_inAMM = uint256(int256(uint256(s_inAMM)) - (shortAmount - longAmount)).toUint128();

            return (int128(tokenToPay));
        }
    }

    /// @notice Get the amount exchanged to mint an option, including any fees.
    /// @param longAmount The amount of long options held
    /// @param shortAmount The amount of short options held
    /// @param swappedAmount The amount of tokens moved during creation of the option position
    /// @param isCovered Whether the option was minted as covered (no swap occurred if ITM)
    /// @return The amount of funds to be exchanged for minting an option (includes commission, swapFee, and intrinsic value)
    /// @return The total commission (base rate + ITM spread) paid for minting the option
    function _getExchangedAmount(
        int128 longAmount,
        int128 shortAmount,
        int128 swappedAmount,
        bool isCovered
    ) internal view returns (int256, uint128) {
        unchecked {
            int256 intrinsicValue = int256(swappedAmount) - (shortAmount - longAmount);

            // the swap commission is paid on the intrinsic value (if a swap occurred; users who mint covered options with their own collateral do not pay this fee)
            uint256 commission = Math.unsafeDivRoundingUp(
                uint256(uint128(shortAmount + longAmount)) * COMMISSION_FEE,
                DECIMALS
            ) +
                (
                    intrinsicValue == 0 || isCovered
                        ? 0
                        : Math.unsafeDivRoundingUp(
                            ITM_SPREAD_FEE * uint256(Math.abs(intrinsicValue)),
                            DECIMALS
                        )
                );

            return (intrinsicValue + int256(commission), uint128(commission));
        }
    }

    /*//////////////////////////////////////////////////////////////
                     HEALTH AND COLLATERAL TRACKING
    //////////////////////////////////////////////////////////////*/

    /// @notice Get the collateral status/margin details of an account/user.
    /// @dev NOTE: It's up to the caller to confirm from the returned result that the account has enough collateral.
    /// @dev This can be used to check the health: how many tokens a user has compared to the margin threshold.
    /// @param user The account to check collateral/margin health for
    /// @param atTick The tick at which to evaluate the account's positions
    /// @param positionBalanceArray The list of all open positions held by the `optionOwner`, stored as `[[tokenId, balance/poolUtilizationAtMint], ...]`
    /// @param shortPremium The total amount of premium (prorated by available settled tokens) owed to the short legs of `user`
    /// @param longPremium The total amount of premium owed by the long legs of `user`
    /// @return Information collected for the tokens about the health of the account
    /// The collateral balance of the user is in the right slot and the threshold for margin call is in the left slot.
    function getAccountMarginDetails(
        address user,
        int24 atTick,
        uint256[2][] memory positionBalanceArray,
        uint128 shortPremium,
        uint128 longPremium
    ) public view returns (LeftRightUnsigned) {
        unchecked {
            return
                LeftRightUnsigned
                    .wrap((convertToAssets(balanceOf[user]) + shortPremium).toUint128())
                    .toLeftSlot(
                        positionBalanceArray.length > 0
                            ? (_getTotalRequiredCollateral(atTick, positionBalanceArray) +
                                longPremium).toUint128()
                            : 0
                    );
        }
    }

    /// @notice Get the total required amount of collateral tokens of a user/account across all active positions to stay above the margin requirement.
    /// @dev Returns the token amounts required for the entire account with active positions in `positionIdList` (list of tokenIds).
    /// @param atTick The tick at which to evaluate the account's positions
    /// @param positionBalanceArray The list of all open positions held by the `optionOwner`, stored as `[[tokenId, balance/poolUtilizationAtMint], ...]`
    /// @return tokenRequired The amount of tokens required to stay above the margin threshold for all active positions of user
    function _getTotalRequiredCollateral(
        int24 atTick,
        uint256[2][] memory positionBalanceArray
    ) internal view returns (uint256 tokenRequired) {
        uint256 totalIterations = positionBalanceArray.length;
        for (uint256 i = 0; i < totalIterations; ) {
            TokenId tokenId = TokenId.wrap(positionBalanceArray[i][0]);

            uint128 positionSize = PositionBalance.wrap(positionBalanceArray[i][1]).positionSize();

            bool underlyingIsCurrency0 = _underlyingIsCurrency0();

            int16 poolUtilization = underlyingIsCurrency0
                ? int16(PositionBalance.wrap(positionBalanceArray[i][1]).utilization0())
                : int16(PositionBalance.wrap(positionBalanceArray[i][1]).utilization1());

            uint256 _tokenRequired = _getRequiredCollateralAtTickSinglePosition(
                tokenId,
                positionSize,
                atTick,
                poolUtilization,
                underlyingIsCurrency0
            );

            unchecked {
                tokenRequired += _tokenRequired;
            }
            unchecked {
                ++i;
            }
        }
    }

    /// @notice Get the required amount of collateral tokens corresponding to a specific single position `tokenId` at a price `atTick`.
    /// @param tokenId The option position
    /// @param positionSize The size of the option position
    /// @param atTick The tick at which to evaluate the account's positions
    /// @param poolUtilization The utilization of the collateral vault (balance of buying and selling)
    /// @param underlyingIsCurrency0 Cached `_underlyingIsCurrency0()` value for this CollateralTracker instance
    /// @return tokenRequired Total required tokens for all legs of the specified tokenId.
    function _getRequiredCollateralAtTickSinglePosition(
        TokenId tokenId,
        uint128 positionSize,
        int24 atTick,
        int16 poolUtilization,
        bool underlyingIsCurrency0
    ) internal view returns (uint256 tokenRequired) {
        uint256 numLegs = tokenId.countLegs();

        unchecked {
            for (uint256 index = 0; index < numLegs; ++index) {
                if (tokenId.tokenType(index) != (underlyingIsCurrency0 ? 0 : 1)) continue;

                // Increment the tokenRequired accumulator
                tokenRequired += _getRequiredCollateralSingleLeg(
                    tokenId,
                    index,
                    positionSize,
                    atTick,
                    poolUtilization
                );
            }
        }
    }

    /// @notice Calculate the required amount of collateral for a single leg `index` of position `tokenId`.
    /// @param tokenId The option position
    /// @param index The leg index (associated with a liquidity chunk) to compute the required collateral for
    /// @param positionSize The size of the position
    /// @param atTick The tick at which to evaluate the account's positions
    /// @param poolUtilization The pool utilization: how much funds are in the Panoptic pool versus the AMM pool
    /// @return required The required amount collateral needed for this leg `index`
    function _getRequiredCollateralSingleLeg(
        TokenId tokenId,
        uint256 index,
        uint128 positionSize,
        int24 atTick,
        int16 poolUtilization
    ) internal view returns (uint256 required) {
        return
            tokenId.riskPartner(index) == index // does this leg have a risk partner? Affects required collateral
                ? _getRequiredCollateralSingleLegNoPartner(
                    tokenId,
                    index,
                    positionSize,
                    atTick,
                    poolUtilization
                )
                : _getRequiredCollateralSingleLegPartner(
                    tokenId,
                    index,
                    positionSize,
                    atTick,
                    poolUtilization
                );
    }

    /// @notice Calculate the required amount of collateral for leg `index` of position `tokenId` when the leg does not have a risk partner.
    /// @param tokenId The option position
    /// @param index The leg index (associated with a liquidity chunk) to consider a partner for
    /// @param positionSize The size of the position
    /// @param atTick The tick at which to evaluate the account's positions
    /// @param poolUtilization The pool utilization: ratio of how much funds are in the Panoptic pool versus the AMM pool
    /// @return required The required amount collateral needed for this leg `index`
    function _getRequiredCollateralSingleLegNoPartner(
        TokenId tokenId,
        uint256 index,
        uint128 positionSize,
        int24 atTick,
        int16 poolUtilization
    ) internal view returns (uint256 required) {
        // extract the tokenType (currency0 or currency1)
        uint256 tokenType = tokenId.tokenType(index);

        // compute the total amount of funds moved for that position
        LeftRightUnsigned amountsMoved = PanopticMath.getAmountsMoved(tokenId, positionSize, index);

        // amount moved is right slot if tokenType=0, left slot otherwise
        uint128 amountMoved = tokenType == 0 ? amountsMoved.rightSlot() : amountsMoved.leftSlot();

        uint256 isLong = tokenId.isLong(index);

        // start with base requirement, which is based on isLong value
        required = _getRequiredCollateralAtUtilization(amountMoved, isLong, poolUtilization);

        // if the position is long, required tokens do not depend on price
        unchecked {
            if (isLong == 0) {
                // if position is short, check whether the position is out-the-money

                (int24 tickLower, int24 tickUpper) = tokenId.asTicks(index);

                // compute the collateral requirement as a fixed amount that doesn't depend on price
                if (
                    ((atTick >= tickUpper) && (tokenType == 1)) || // strike OTM when price >= upperTick for tokenType=1
                    ((atTick < tickLower) && (tokenType == 0)) // strike OTM when price < lowerTick for tokenType=0
                ) {
                    // position is out-of-the-money, collateral requirement = SCR * amountMoved
                    required;
                } else {
                    int24 strike = tokenId.strike(index);
                    // if position is ITM or ATM, then the collateral requirement depends on price:

                    // compute the ratio of strike to price for calls (or price to strike for puts)
                    // (- and * 2 in tick space are / and ^ 2 in price space so sqrtRatioAtTick(2 *(a - b)) = a/b (*2^96)
                    // both of these ratios decrease as the position becomes deeper ITM, and it is possible
                    // for the ratio of the prices to go under the minimum price
                    // (which is the limit of what getSqrtRatioAtTick supports)
                    // so instead we cap it at the minimum price, which is acceptable because
                    // a higher ratio will result in an increased slope for the collateral requirement
                    uint160 ratio = tokenType == 1 // tokenType
                        ? Math.getSqrtRatioAtTick(
                            Math.max24(2 * (atTick - strike), Constants.MIN_V4POOL_TICK)
                        ) // puts ->  price/strike
                        : Math.getSqrtRatioAtTick(
                            Math.max24(2 * (strike - atTick), Constants.MIN_V4POOL_TICK)
                        ); // calls -> strike/price

                    // compute the collateral requirement depending on whether the position is ITM & out-of-range or ITM and in-range:

                    /// ITM and out-of-range
                    if (
                        ((atTick < tickLower) && (tokenType == 1)) || // strike ITM but out of range price < lowerTick for tokenType=1
                        ((atTick >= tickUpper) && (tokenType == 0)) // strike ITM but out of range when price >= upperTick for tokenType=0
                    ) {
                        /*
                                    Short put BPR = 100% - (price/strike) + SCR

                           BUYING
                           POWER
                           REQUIREMENT
                         
                                         ^               .         .
                                         |        <- ITM . <-ATM-> . OTM ->
                           100% + SCR% - |--__           .    .    .
                                  100% - | . .¯¯--__     .    .    .
                                         |    .     ¯¯--__    .    .
                                   SCR - |    .          .¯¯--__________
                                         |    .          .    .    .
                                         +----+----------+----+----+--->   current
                                         0   Liqui-     Pa  strike Pb       price
                                             dation
                                             price = SCR*strike                                         
                         */

                        uint256 c2 = Constants.FP96 - ratio;

                        // compute the tokens required
                        // position is in-the-money, collateral requirement = amountMoved*(1-ratio) + SCR*amountMoved
                        required += Math.mulDiv96RoundingUp(amountMoved, c2);
                    } else {
                        // position is in-range (ie. current tick is between upper+lower tick): we draw a line between the
                        // collateral requirement at the lowerTick and the one at the upperTick. We use that interpolation as
                        // the collateral requirement when in-range, which always over-estimates the amount of token required
                        // Specifically:
                        //  required = amountMoved * (scaleFactor - ratio) / (scaleFactor + 1) + sellCollateralRatio*amountMoved
                        uint160 scaleFactor = Math.getSqrtRatioAtTick(
                            (tickUpper - strike) + (strike - tickLower)
                        );
                        uint256 c3 = Math.mulDivRoundingUp(
                            amountMoved,
                            scaleFactor - ratio,
                            scaleFactor + Constants.FP96
                        );

                        required += c3;
                    }
                }
            }
        }
    }

    /// @notice Calculate the required amount of collateral for leg `index` for position `tokenId` accounting for its partner leg.
    /// @dev If the two `isLong` fields are different (i.e., a short leg and a long leg are partnered) but the tokenTypes are the same, this is a spread.
    /// @dev A spread is a defined risk position which has a max loss given by difference between the long and short strikes.
    /// @dev If the two `isLong` fields are the same but the tokenTypes are different (one is a call, the other a put, e.g.), this is a strangle -
    /// a strangle benefits from enhanced capital efficiency because only one side can be ITM at any given time.
    /// @param tokenId The option position
    /// @param index The leg index (associated with a liquidity chunk) to consider a partner for
    /// @param positionSize The size of the position
    /// @param atTick The tick at which to evaluate the account's positions
    /// @param poolUtilization The pool utilization: how much funds are in the Panoptic pool versus the AMM pool
    /// @return required The required amount of collateral needed for this leg `index`
    function _getRequiredCollateralSingleLegPartner(
        TokenId tokenId,
        uint256 index,
        uint128 positionSize,
        int24 atTick,
        int16 poolUtilization
    ) internal view returns (uint256 required) {
        // extract partner index (associated with another liquidity chunk)
        uint256 partnerIndex = tokenId.riskPartner(index);

        uint256 isLong = tokenId.isLong(index);
        if (isLong != tokenId.isLong(partnerIndex)) {
            if (isLong == 1) {
                required = _computeSpread(
                    tokenId,
                    positionSize,
                    index,
                    partnerIndex,
                    poolUtilization
                );
            }
        } else {
            required = _computeStrangle(tokenId, index, positionSize, atTick, poolUtilization);
        }
    }

    /// @notice Get the base collateral requirement for a position of notional value `amount` at the current Panoptic pool `utilization` level.
    /// @param amount The amount to multiply by the base collateral ratio
    /// @param isLong Whether the position is long (=1) or short (=0)
    /// @param utilization The utilization of the Panoptic pool (balance between sellers and buyers)
    /// @return required The base collateral requirement corresponding to the incoming `amount`
    function _getRequiredCollateralAtUtilization(
        uint128 amount,
        uint256 isLong,
        int16 utilization
    ) internal view returns (uint256 required) {
        // if position is short, use sell collateral ratio
        if (isLong == 0) {
            // compute the sell collateral ratio, which depends on the pool utilization
            uint256 sellCollateral = _sellCollateralRatio(utilization);

            // compute required as amount*collateralRatio
            // can use unsafe because denominator is always nonzero
            unchecked {
                required = Math.unsafeDivRoundingUp(amount * sellCollateral, DECIMALS);
            }
        } else if (isLong == 1) {
            // if options is long, use buy collateral ratio
            // compute the buy collateral ratio, which depends on the pool utilization
            uint256 buyCollateral = _buyCollateralRatio(uint16(utilization));

            // compute required as amount*collateralRatio
            // can use unsafe because denominator is always nonzero
            unchecked {
                required = Math.unsafeDivRoundingUp(amount * buyCollateral, DECIMALS);
            }
        }
    }

    /// @notice Calculate the required amount of collateral for the spread portion of the spread position.
    /// @dev `max(long leg requirement, 100% collateralized risk)`
    /// @dev May be higher than the requirement of an equivalent pair of non-risk-partnered legs if the spread is very wide (risky).
    /// @param tokenId The option position
    /// @param positionSize The size of the position
    /// @param index The leg index of the LONG leg in the spread position
    /// @param partnerIndex The index of the partnered SHORT leg in the spread position
    /// @param poolUtilization The pool utilization: how much funds are in the Panoptic pool versus the AMM pool
    /// @return spreadRequirement The required amount of collateral needed for the spread
    function _computeSpread(
        TokenId tokenId,
        uint128 positionSize,
        uint256 index,
        uint256 partnerIndex,
        int16 poolUtilization
    ) internal view returns (uint256 spreadRequirement) {
        // compute the total amount of funds moved for the position's current leg
        LeftRightUnsigned amountsMoved = PanopticMath.getAmountsMoved(tokenId, positionSize, index);

        // compute the total amount of funds moved for the position's partner leg
        LeftRightUnsigned amountsMovedPartner = PanopticMath.getAmountsMoved(
            tokenId,
            positionSize,
            partnerIndex
        );

        uint128 movedRight = amountsMoved.rightSlot();
        uint128 movedLeft = amountsMoved.leftSlot();

        uint128 movedPartnerRight = amountsMovedPartner.rightSlot();
        uint128 movedPartnerLeft = amountsMovedPartner.leftSlot();

        uint256 tokenType = tokenId.tokenType(index);

        // compute the max loss of the spread

        // if asset is NOT the same as the tokenType, the required amount is simply the difference in notional values
        // ie. asset = 1, tokenType = 0:
        if (tokenId.asset(index) != tokenType) {
            unchecked {
                // always take the absolute values of the difference of amounts moved
                if (tokenType == 0) {
                    spreadRequirement = movedRight < movedPartnerRight
                        ? movedPartnerRight - movedRight
                        : movedRight - movedPartnerRight;
                } else {
                    spreadRequirement = movedLeft < movedPartnerLeft
                        ? movedPartnerLeft - movedLeft
                        : movedLeft - movedPartnerLeft;
                }
            }
        } else {
            unchecked {
                uint256 notional;
                uint256 notionalP;
                uint128 contracts;
                if (tokenType == 1) {
                    notional = movedRight;
                    notionalP = movedPartnerRight;
                    contracts = movedLeft;
                } else {
                    notional = movedLeft;
                    notionalP = movedPartnerLeft;
                    contracts = movedRight;
                }
                // the required amount is the amount of contracts multiplied by (notional1 - notional2)/min(notional1, notional2)
                // can use unsafe because denominator is always nonzero
                spreadRequirement = (notional < notionalP)
                    ? Math.unsafeDivRoundingUp((notionalP - notional) * contracts, notional)
                    : Math.unsafeDivRoundingUp((notional - notionalP) * contracts, notionalP);
            }
        }

        // calculate the spread requirement as max(max_loss, long_leg_col_req)
        // narrower spreads will be very capital efficient (up to only ~5% of non-partnered CR!), but
        // wider spreads (an uncommon position w/ high max loss) may not benefit from risk partnering
        spreadRequirement = Math.max(
            spreadRequirement,
            _getRequiredCollateralAtUtilization(
                tokenType == 0 ? movedRight : movedLeft,
                1,
                poolUtilization
            )
        );
    }

    /// @notice Calculate the required amount of collateral for a strangle leg.
    /// @dev The base collateral requirement is halved for short strangles.
    /// @dev A strangle can only have only one of its legs ITM at any given time, so this reduces the total risk and collateral requirement.
    /// @param tokenId The option position
    /// @param positionSize The size of the position
    /// @param index The leg index (associated with a liquidity chunk) to consider a partner for
    /// @param atTick The tick at which to evaluate the account's positions
    /// @param poolUtilization The pool utilization: how much funds are in the Panoptic pool versus the AMM pool
    /// @return strangleRequired The required amount of collateral needed for the strangle leg
    function _computeStrangle(
        TokenId tokenId,
        uint256 index,
        uint128 positionSize,
        int24 atTick,
        int16 poolUtilization
    ) internal view returns (uint256 strangleRequired) {
        // If both tokenTypes are the same, then this is a short strangle.
        // A strangle is an options strategy in which the investor holds a position
        // in both a call and a put option with different strike prices,
        // but with the same expiration date and underlying asset.

        /// collateral requirement is for short strangles depicted:
        /**
                    Put side of a short strangle, BPR = 100% - (100% - SCR/2)*(price/strike)
           BUYING
           POWER
           REQUIREMENT
                         ^                    .
                         |           <- ITM   .  OTM ->
                  100% - |--__                .
                         |    ¯¯--__          .
                         |          ¯¯--__    .
                 SCR/2 - |                ¯¯--______ <------ base collateral is half that of a single-leg
                         +--------------------+--->   current
                         0                  strike     price
         */
        unchecked {
            // A negative pool utilization is used to denote a position which is a strangle
            // add 1 to handle poolUtilization = 0
            poolUtilization = -(poolUtilization == 0 ? int16(1) : poolUtilization);

            return
                strangleRequired = _getRequiredCollateralSingleLegNoPartner(
                    tokenId,
                    index,
                    positionSize,
                    atTick,
                    poolUtilization
                );
        }
    }
}

// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.24;

// Interfaces
import {IERC20Partial} from "@tokens/interfaces/IERC20Partial.sol";
import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol";
// Inherited implementations
import {ERC1155} from "@tokens/ERC1155Minimal.sol";
import {Multicall} from "@base/Multicall.sol";
import {TransientReentrancyGuard} from "solmate/src/utils/TransientReentrancyGuard.sol";
// Libraries
import {Constants} from "@libraries/Constants.sol";
import {Errors} from "@libraries/Errors.sol";
import {Math} from "@libraries/Math.sol";
import {PanopticMath} from "@libraries/PanopticMath.sol";
import {V4StateReader} from "@libraries/V4StateReader.sol";
// Custom types
import {BalanceDelta} from "v4-core/types/BalanceDelta.sol";
import {Currency} from "v4-core/types/Currency.sol";
import {LeftRightUnsigned, LeftRightSigned, LeftRightLibrary} from "@types/LeftRight.sol";
import {LiquidityChunk} from "@types/LiquidityChunk.sol";
import {PoolId} from "v4-core/types/PoolId.sol";
import {PoolKey} from "v4-core/types/PoolKey.sol";
import {TokenId} from "@types/TokenId.sol";

//                                                                        ..........
//                       ,.                                   .,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,.                                    ,,
//                    ,,,,,,,                           ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,                            ,,,,,,
//                  .,,,,,,,,,,.                   ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,                     ,,,,,,,,,,,
//                .,,,,,,,,,,,,,,,             ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,.              ,,,,,,,,,,,,,,,
//               ,,,,,,,,,,,,,,.            ,,,,,,,,,,,,,,,,,,,,,,,,,,,                ,,,,,,,,,,,,,,,,,,,,,,,,,,.             ,,,,,,,,,,,,,,,
//             ,,,,,,,,,,,,,,,           ,,,,,,,,,,,,,,,,,,,,,,                                ,,,,,,,,,,,,,,,,,,,,,,            ,,,,,,,,,,,,,,,
//            ,,,,,,,,,,,,,.           ,,,,,,,,,,,,,,,,,,                                           .,,,,,,,,,,,,,,,,,,            ,,,,,,,,,,,,,,
//          ,,,,,,,,,,,,,,          ,,,,,,,,,,,,,,,,,.                                                  ,,,,,,,,,,,,,,,,,           .,,,,,,,,,,,,,
//         ,,,,,,,,,,,,,.         .,,,,,,,,,,,,,,,.                                                        ,,,,,,,,,,,,,,,,           ,,,,,,,,,,,,,.
//        ,,,,,,,,,,,,,          ,,,,,,,,,,,,,,,                                                              ,,,,,,,,,,,,,,,           ,,,,,,,,,,,,,
//       ,,,,,,,,,,,,,         ,,,,,,,,,,,,,,.                                                                  ,,,,,,,,,,,,,,           ,,,,,,,,,,,,,
//      ,,,,,,,,,,,,,         ,,,,,,,,,,,,,,                                                                      ,,,,,,,,,,,,,,          ,,,,,,,,,,,,,
//     ,,,,,,,,,,,,,         ,,,,,,,,,,,,,                                                                         ,,,,,,,,,,,,,,          ,,,,,,,,,,,,.
//    .,,,,,,,,,,,,        .,,,,,,,,,,,,,                                                                            ,,,,,,,,,,,,,          ,,,,,,,,,,,,
//    ,,,,,,,,,,,,         ,,,,,,,,,,,,                                                                               ,,,,,,,,,,,,,         .,,,,,,,,,,,,
//   ,,,,,,,,,,,,         ,,,,,,,,,,,,                                                                                 ,,,,,,,,,,,,.         ,,,,,,,,,,,,
//   ,,,,,,,,,,,,        ,,,,,,,,,,,,.                █████████  ███████████ ███████████  ██████   ██████               ,,,,,,,,,,,,          ,,,,,,,,,,,,
//  .,,,,,,,,,,,,        ,,,,,,,,,,,,                ███░░░░░███░░███░░░░░░█░░███░░░░░███░░██████ ██████                .,,,,,,,,,,,,         ,,,,,,,,,,,,
//  ,,,,,,,,,,,,        ,,,,,,,,,,,,                ░███    ░░░  ░███   █ ░  ░███    ░███ ░███░█████░███                 ,,,,,,,,,,,,         ,,,,,,,,,,,,.
//  ,,,,,,,,,,,,        ,,,,,,,,,,,,                ░░█████████  ░███████    ░██████████  ░███░░███ ░███                 .,,,,,,,,,,,          ,,,,,,,,,,,.
//  ,,,,,,,,,,,,        ,,,,,,,,,,,,                 ░░░░░░░░███ ░███░░░█    ░███░░░░░░   ░███ ░░░  ░███                  ,,,,,,,,,,,.         ,,,,,,,,,,,,
//  ,,,,,,,,,,,,        ,,,,,,,,,,,,                 ███    ░███ ░███  ░     ░███         ░███      ░███                  ,,,,,,,,,,,,         ,,,,,,,,,,,,
//  ,,,,,,,,,,,,        ,,,,,,,,,,,,                ░░█████████  █████       █████        █████     █████                 ,,,,,,,,,,,          ,,,,,,,,,,,,
//  ,,,,,,,,,,,,        ,,,,,,,,,,,,                 ░░░░░░░░░  ░░░░░       ░░░░░        ░░░░░     ░░░░░                 ,,,,,,,,,,,,          ,,,,,,,,,,,.
//  ,,,,,,,,,,,,        .,,,,,,,,,,,.                                                                                    ,,,,,,,,,,,,         ,,,,,,,,,,,,
//  .,,,,,,,,,,,,        ,,,,,,,,,,,,                                                                                   .,,,,,,,,,,,,         ,,,,,,,,,,,,
//   ,,,,,,,,,,,,        ,,,,,,,,,,,,,                                                                                  ,,,,,,,,,,,,          ,,,,,,,,,,,,
//   ,,,,,,,,,,,,.        ,,,,,,,,,,,,.                                                                                ,,,,,,,,,,,,.         ,,,,,,,,,,,,
//    ,,,,,,,,,,,,         ,,,,,,,,,,,,,                                                                              ,,,,,,,,,,,,,         .,,,,,,,,,,,,
//     ,,,,,,,,,,,,         ,,,,,,,,,,,,,                                                                            ,,,,,,,,,,,,,         .,,,,,,,,,,,,
//     .,,,,,,,,,,,,         ,,,,,,,,,,,,,                                                                         ,,,,,,,,,,,,,.          ,,,,,,,,,,,,
//      ,,,,,,,,,,,,,         ,,,,,,,,,,,,,,                                                                     .,,,,,,,,,,,,,.          ,,,,,,,,,,,,
//       ,,,,,,,,,,,,,         .,,,,,,,,,,,,,,                                                                 .,,,,,,,,,,,,,,          .,,,,,,,,,,,,
//        ,,,,,,,,,,,,,          ,,,,,,,,,,,,,,,                                                             ,,,,,,,,,,,,,,,.          ,,,,,,,,,,,,,.
//         ,,,,,,,,,,,,,,          ,,,,,,,,,,,,,,,,                                                       .,,,,,,,,,,,,,,,,           ,,,,,,,,,,,,,
//          .,,,,,,,,,,,,,           ,,,,,,,,,,,,,,,,,                                                 .,,,,,,,,,,,,,,,,,           ,,,,,,,,,,,,,,
//            ,,,,,,,,,,,,,,           ,,,,,,,,,,,,,,,,,,,.                                        ,,,,,,,,,,,,,,,,,,,.            ,,,,,,,,,,,,,,
//             ,,,,,,,,,,,,,,,            ,,,,,,,,,,,,,,,,,,,,,,                             .,,,,,,,,,,,,,,,,,,,,,,             ,,,,,,,,,,,,,,
//               ,,,,,,,,,,,,,,,            .,,,,,,,,,,,,,,,,,,,,,,,,,,,,,.        ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,             .,,,,,,,,,,,,,,.
//                 ,,,,,,,,,,,,,,.              ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,               ,,,,,,,,,,,,,,,
//                   ,,,,,,,,,,                     ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,                     .,,,,,,,,,,
//                     ,,,,,.                            ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,                             ,,,,,,
//                       ,                                     ..,,,,,,,,,,,,,,,,,,,,,,,,,,,,.

/// @author Axicon Labs Limited
/// @title Semi-Fungible Position Manager (ERC1155) - a gas-efficient Uniswap V4 position manager.
/// @notice Wraps Uniswap V4 positions with up to 4 legs behind an ERC1155 token.
contract SemiFungiblePositionManager is ERC1155, Multicall, TransientReentrancyGuard {
    /*//////////////////////////////////////////////////////////////
                                 EVENTS
    //////////////////////////////////////////////////////////////*/

    /// @notice Emitted when a Uniswap V4 pool is initialized in the SFPM.
    /// @param idV4 The Uniswap V4 pool identifier (hash of `poolKey`)
    /// @param poolId The SFPM's pool identifier for the pool, including the 16-bit tick spacing and 48-bit pool pattern
    /// @param minEnforcedTick The initial minimum enforced tick for the pool
    /// @param maxEnforcedTick The initial maximum enforced tick for the pool
    event PoolInitialized(
        PoolId indexed idV4,
        uint64 poolId,
        int24 minEnforcedTick,
        int24 maxEnforcedTick
    );

    /// @notice Emitted when the enforced tick range is expanded for a given Uniswap `idV4`.
    /// @dev Will be emitted on any `expandEnforcedTickRange` call, even if the enforced ticks are not actually changed.
    /// @param idV4 The Uniswap V4 pool identifier (hash of `poolKey`)
    /// @param minEnforcedTick The new minimum enforced tick for the pool
    /// @param maxEnforcedTick The new maximum enforced tick for the pool
    event EnforcedTicksUpdated(PoolId indexed idV4, int24 minEnforcedTick, int24 maxEnforcedTick);

    /// @notice Emitted when a position is destroyed/burned.
    /// @param recipient The address of the user who burned the position
    /// @param tokenId The tokenId of the burned position
    /// @param positionSize The number of contracts burnt, expressed in terms of the asset
    event TokenizedPositionBurnt(
        address indexed recipient,
        TokenId indexed tokenId,
        uint128 positionSize
    );

    /// @notice Emitted when a position is created/minted.
    /// @param caller The address of the user who minted the position
    /// @param tokenId The tokenId of the minted position
    /// @param positionSize The number of contracts minted, expressed in terms of the asset
    event TokenizedPositionMinted(
        address indexed caller,
        TokenId indexed tokenId,
        uint128 positionSize
    );

    /*//////////////////////////////////////////////////////////////
                                 TYPES
    //////////////////////////////////////////////////////////////*/

    using Math for uint256;
    using Math for int256;

    /// @notice Type for data associated with an initialized `poolId` in the SFPM.
    /// @param maxLiquidityPerTick The maximum liquidity that can reference any given tick in the Uniswap pool
    /// @param poolId The SFPM's pool identifier for the pool, including the 16-bit tick spacing and 48-bit pool pattern
    /// @param minEnforcedTick The current minimum enforced tick for the pool
    /// @param maxEnforcedTick The current maximum enforced tick for the pool
    /// @param initialized Whether the pool has been initialized in the SFPM
    struct PoolIdData {
        uint128 maxLiquidityPerTick;
        uint64 poolId;
        int24 minEnforcedTick;
        int24 maxEnforcedTick;
        bool initialized;
    }

    /*//////////////////////////////////////////////////////////////
                            IMMUTABLES 
    //////////////////////////////////////////////////////////////*/

    /// @notice Flag used to indicate a regular position mint.
    bool internal constant MINT = false;

    /// @notice Flag used to indicate that a position burn (with a burnTokenId) is occurring.
    bool internal constant BURN = true;

    /// @notice Parameter used to modify the [equation](https://www.desmos.com/calculator/mdeqob2m04) of the utilization-based multiplier for long premium.
    // ν = 1/2**VEGOID = multiplicative factor for long premium (Eqns 1-5)
    // Similar to vega in options because the liquidity utilization is somewhat reflective of the implied volatility (IV),
    // and vegoid modifies the sensitivity of the streamia to changes in that utilization,
    // much like vega measures the sensitivity of traditional option prices to IV.
    // The effect of vegoid on the long premium multiplier can be explored here: https://www.desmos.com/calculator/mdeqob2m04
    uint128 private constant VEGOID = 2;

    /// @notice The canonical Uniswap V4 Pool Manager address.
    IPoolManager internal immutable POOL_MANAGER_V4;

    /// @notice The approximate minimum amount of tokens it should require to fill `maxLiquidityPerTick` at the minimum and maximum enforced ticks.
    uint256 internal immutable MIN_ENFORCED_TICKFILL_COST;

    /// @notice The approximate minimum amount of tokens it should require to fill `maxLiquidityPerTick` at the minimum and maximum enforced ticks for native-token pools.
    uint256 internal immutable NATIVE_ENFORCED_TICKFILL_COST;

    /// @notice The multiplier, in basis points, to apply to the token supply and set as the minimum enforced tick fill cost if greater than `MIN_ENFORCED_TICKFILL_COST`.
    uint256 internal immutable SUPPLY_MULTIPLIER_TICKFILL;

    /*//////////////////////////////////////////////////////////////
                            STORAGE 
    //////////////////////////////////////////////////////////////*/

    /// @notice Retrieve the SFPM PoolIdData struct associated with a given Uniswap V4 poolId.
    mapping(PoolId idV4 => PoolIdData poolIdData) internal s_V4toSFPMIdData;

    /// @notice Retrieve the Uniswap V4 pool key corresponding to a given poolId.
    mapping(uint64 poolId => PoolKey key) internal s_poolIdToKey;

    /*
        We're tracking the amount of net and removed liquidity for the specific region:

             net amount    
           received minted  
          ▲ for isLong=0     amount           
          │                 moved out      actual amount 
          │  ┌────┐-T      due isLong=1   in the Uniswap V4 
          │  │    │          mints          pool 
          │  │    │      
          │  │    │                        ┌────┐-(T-R)  
          │  │    │         ┌────┐-R       │    │          
          │  │    │         │    │         │    │     
          └──┴────┴─────────┴────┴─────────┴────┴──────►                     
             total=T       removed=R      net=(T-R)


     *       removed liquidity r          net liquidity N=(T-R)
     * |<------- 128 bits ------->|<------- 128 bits ------->|
     * |<---------------------- 256 bits ------------------->|
     */

    /// @notice Retrieve the current liquidity state in a chunk for a given user.
    /// @dev `removedAndNetLiquidity` is a LeftRight. The right slot represents the liquidity currently sold (added) in the AMM owned by the user and
    // the left slot represents the amount of liquidity currently bought (removed) that has been removed from the AMM - the user owes it to a seller.
    // The reason why it is called "removedLiquidity" is because long options are created by removed liquidity - ie. short selling LP positions.
    mapping(bytes32 positionKey => LeftRightUnsigned removedAndNetLiquidity)
        internal s_accountLiquidity;

    /*
        Any liquidity that has been deposited in the AMM using the SFPM will collect fees over 
        time, we call this the gross premia. If that liquidity has been removed, we also need to
        keep track of the amount of fees that *would have been collected*, we call this the owed
        premia. The gross and owed premia are tracked per unit of liquidity by the 
        s_accountPremiumGross and s_accountPremiumOwed accumulators.
        
        Here is how we can use the accumulators to compute the Gross, Net, and Owed fees collected
        by any position.

        Let`s say Charlie the smart contract deposited T into the AMM and later removed R from that
        same tick using a tokenId with a isLong=1 parameter. Because the netLiquidity is only (T-R),
        the AMM will collect fees equal to:

              net_feesCollectedX128 = feeGrowthX128 * (T - R)
                                    = feeGrowthX128 * N                                     
        
        where N = netLiquidity = T-R. Had that liquidity never been removed, we want the gross
        premia to be given by:

              gross_feesCollectedX128 = feeGrowthX128 * T

        So we must keep track of fees for the removed liquidity R so that the long premia exactly
        compensates for the fees that would have been collected from the initial liquidity.

        In addition to tracking, we also want to track those fees plus a small spread. Specifically,
        we want:

              gross_feesCollectedX128 = net_feesCollectedX128 + owed_feesCollectedX128

       where 

              owed_feesCollectedX128 = feeGrowthX128 * R * (1 + spread)                      (Eqn 1)

        A very opinionated definition for the spread is: 
              
              spread = ν*(liquidity removed from that strike)/(netLiquidity remaining at that strike)
                     = ν*R/N

        For an arbitrary parameter 0 <= ν <= 1 (ν = 1/2^VEGOID). This way, the gross_feesCollectedX128 will be given by: 

              gross_feesCollectedX128 = feeGrowthX128 * N + feeGrowthX128*R*(1 + ν*R/N) 
                                      = feeGrowthX128 * T + feesGrowthX128*ν*R^2/N         
                                      = feeGrowthX128 * T * (1 + ν*R^2/(N*T))                (Eqn 2)
        
        The s_accountPremiumOwed accumulator tracks the feeGrowthX128 * R * (1 + spread) term
        per unit of removed liquidity R every time the position touched:

              s_accountPremiumOwed += feeGrowthX128 * R * (1 + ν*R/N) / R
                                   += feeGrowthX128 * (T - R + ν*R)/N
                                   += feeGrowthX128 * T/N * (1 - R/T + ν*R/T)
         
        Note that the value of feeGrowthX128 can be extracted from the amount of fees collected by
        the smart contract since the amount of feesCollected is related to feeGrowthX128 according
        to:

             feesCollected = feesGrowthX128 * (T-R)

        So that we get:
             
             feesGrowthX128 = feesCollected/N

        And the accumulator is computed from the amount of collected fees according to:
             
             s_accountPremiumOwed += feesCollected * T/N^2 * (1 - R/T + ν*R/T)          (Eqn 3)     

        So, the amount of owed premia for a position of size r minted at time t1 and burnt at 
        time t2 is:

             owedPremia(t1, t2) = (s_accountPremiumOwed_t2-s_accountPremiumOwed_t1) * r
                                = ∆feesGrowthX128 * r * T/N * (1 - R/T + ν*R/T)
                                = ∆feesGrowthX128 * r * (T - R + ν*R)/N
                                = ∆feesGrowthX128 * r * (N + ν*R)/N
                                = ∆feesGrowthX128 * r * (1 + ν*R/N)             (same as Eqn 1)

        This way, the amount of premia owed for a position will match Eqn 1 exactly.

        Similarly, the amount of gross fees for the total liquidity is tracked in a similar manner
        by the s_accountPremiumGross accumulator. 

        However, since we require that Eqn 2 holds up-- ie. the gross fees collected should be equal
        to the net fees collected plus the ower fees plus the small spread, the expression for the
        s_accountPremiumGross accumulator has to be given by (you`ll see why in a minute): 

            s_accountPremiumGross += feesCollected * T/N^2 * (1 - R/T + ν*R^2/T^2)       (Eqn 4) 

        This expression can be used to calculate the fees collected by a position of size t between times
        t1 and t2 according to:
             
            grossPremia(t1, t2) = ∆(s_accountPremiumGross) * t
                                = ∆feeGrowthX128 * t * T/N * (1 - R/T + ν*R^2/T^2) 
                                = ∆feeGrowthX128 * t * (T - R + ν*R^2/T) / N 
                                = ∆feeGrowthX128 * t * (N + ν*R^2/T) / N
                                = ∆feeGrowthX128 * t * (1  + ν*R^2/(N*T))   (same as Eqn 2)
            
        where the last expression matches Eqn 2 exactly.

        In summary, the s_accountPremium accumulators allow smart contracts that need to handle 
        long+short liquidity to guarantee that liquidity deposited always receives the correct
        premia, whether that liquidity has been removed from the AMM or not.

        Note that the expression for the spread is extremely opinionated, and may not fit the
        specific risk management profile of every smart contract. And simply setting the ν parameter
        to zero would get rid of the "spread logic".
    */

    /// @notice Per-liquidity accumulator for the premium owed by buyers on a given chunk, tokenType and account.
    mapping(bytes32 positionKey => LeftRightUnsigned accountPremium) private s_accountPremiumOwed;

    /// @notice Per-liquidity accumulator for the premium earned by sellers on a given chunk, tokenType and account.
    mapping(bytes32 positionKey => LeftRightUnsigned accountPremium) private s_accountPremiumGross;

    /*//////////////////////////////////////////////////////////////
                             INITIALIZATION
    //////////////////////////////////////////////////////////////*/

    /// @notice Set the canonical Uniswap V4 pool manager address and tick fill parameters.
    /// @param poolManager The canonical Uniswap V4 pool manager address
    /// @param _minEnforcedTickFillCost The minimum amount of tokens it should require to fill `maxLiquidityPerTick` at the minimum and maximum enforced ticks
    /// @param _nativeEnforcedTickFillCost The minimum amount of tokens it should require to fill `maxLiquidityPerTick` at the minimum and maximum enforced ticks for native-token pools
    /// @param _supplyMultiplierTickFill The multiplier, in basis points, to apply to the token supply and set as the minimum enforced tick fill cost if greater than `MIN_ENFORCED_TICKFILL_COST`
    constructor(
        IPoolManager poolManager,
        uint256 _minEnforcedTickFillCost,
        uint256 _nativeEnforcedTickFillCost,
        uint256 _supplyMultiplierTickFill
    ) {
        POOL_MANAGER_V4 = poolManager;
        MIN_ENFORCED_TICKFILL_COST = _minEnforcedTickFillCost;
        NATIVE_ENFORCED_TICKFILL_COST = _nativeEnforcedTickFillCost;
        SUPPLY_MULTIPLIER_TICKFILL = _supplyMultiplierTickFill;
    }

    /// @notice Initialize a Uniswap V4 pool in the SFPM.
    /// @dev Revert if already initialized.
    /// @param key An identifying key for a Uniswap V4 pool
    function initializeAMMPool(PoolKey calldata key) external {
        PoolId idV4 = key.toId();

        if (V4StateReader.getSqrtPriceX96(POOL_MANAGER_V4, idV4) == 0)
            revert Errors.UniswapPoolNotInitialized();

        // return if the pool has already been initialized in SFPM
        // pools can be initialized from the Panoptic Factory or by calling initializeAMMPool directly, so reverting
        // could prevent a PanopticPool from being deployed on a previously initialized but otherwise valid pool
        if (s_V4toSFPMIdData[idV4].initialized) return;

        // The base poolId is composed as follows:
        // [tickSpacing][pool pattern]
        // [16 bit tickSpacing][most significant 48 bits of the V4 poolId]
        uint64 poolId = PanopticMath.getPoolId(idV4, key.tickSpacing);

        // There are 281,474,976,710,655 possible pool patterns.
        // A modern GPU can generate a collision in such a space relatively quickly,
        // so if a collision is detected increment the pool pattern until a unique poolId is found
        while (s_poolIdToKey[poolId].tickSpacing != 0) {
            poolId = PanopticMath.incrementPoolPattern(poolId);
        }

        s_poolIdToKey[poolId] = key;

        uint128 maxLiquidityPerTick = Math.getMaxLiquidityPerTick(key.tickSpacing);

        int24 minEnforcedTick;
        int24 maxEnforcedTick;
        unchecked {
            minEnforcedTick = int24(
                -Math.getApproxTickWithMaxAmount(
                    Math.max(
                        MIN_ENFORCED_TICKFILL_COST,
                        (IERC20Partial(Currency.unwrap(key.currency1)).totalSupply() *
                            SUPPLY_MULTIPLIER_TICKFILL) / 10_000
                    ),
                    key.tickSpacing,
                    maxLiquidityPerTick
                )
            );
            maxEnforcedTick = int24(
                Math.getApproxTickWithMaxAmount(
                    Currency.unwrap(key.currency0) == address(0)
                        ? NATIVE_ENFORCED_TICKFILL_COST
                        : Math.max(
                            MIN_ENFORCED_TICKFILL_COST,
                            (IERC20Partial(Currency.unwrap(key.currency0)).totalSupply() *
                                SUPPLY_MULTIPLIER_TICKFILL) / 10_000
                        ),
                    key.tickSpacing,
                    maxLiquidityPerTick
                )
            );
        }

        s_V4toSFPMIdData[idV4] = PoolIdData(
            maxLiquidityPerTick,
            poolId,
            minEnforcedTick,
            maxEnforcedTick,
            true
        );

        emit PoolInitialized(idV4, poolId, minEnforcedTick, maxEnforcedTick);
    }

    /// @notice Recomputes and decreases `minEnforcedTick` and/or increases `maxEnforcedTick` for a given V4 pool `key` if certain conditions are met.
    /// @dev This function will only have an effect if both conditions are met:
    /// - The token supply for one of the (non-native) tokens was greater than MIN_ENFORCED_TICKFILL_COST at the last `initializeAMMPool` or `expandEnforcedTickRangeForPool` call for `poolId`
    /// - The token supply for one of the tokens meeting the first condition has *decreased* significantly since the last call
    /// @dev This function *cannot* decrease the absolute value of either enforced tick, i.e., it can only widen the range of possible ticks.
    /// @dev The purpose of this function is to prevent pools created while a large amount of one of the tokens was flash-minted from being stuck in a narrow tick range.
    /// @param key The key for the Uniswap V4 pool on which to expand the enforced tick range
    function expandEnforcedTickRange(PoolKey calldata key) external {
        PoolId idV4 = key.toId();

        PoolIdData memory dataOld = s_V4toSFPMIdData[idV4];

        if (!dataOld.initialized) revert Errors.PoolNotInitialized();

        // tick spacing is stored in the highest 16 bits of the poolId
        int24 tickSpacing = int24(uint24(dataOld.poolId >> 48));

        uint128 maxLiquidityPerTick = dataOld.maxLiquidityPerTick;

        int24 minEnforcedTick;
        int24 maxEnforcedTick;
        unchecked {
            minEnforcedTick = int24(
                Math.min(
                    dataOld.minEnforcedTick,
                    -Math.getApproxTickWithMaxAmount(
                        Math.max(
                            MIN_ENFORCED_TICKFILL_COST,
                            (IERC20Partial(Currency.unwrap(key.currency1)).totalSupply() *
                                SUPPLY_MULTIPLIER_TICKFILL) / 10_000
                        ),
                        tickSpacing,
                        maxLiquidityPerTick
                    )
                )
            );
            maxEnforcedTick = int24(
                Math.max(
                    dataOld.maxEnforcedTick,
                    Math.getApproxTickWithMaxAmount(
                        Currency.unwrap(key.currency0) == address(0)
                            ? NATIVE_ENFORCED_TICKFILL_COST
                            : Math.max(
                                MIN_ENFORCED_TICKFILL_COST,
                                (IERC20Partial(Currency.unwrap(key.currency0)).totalSupply() *
                                    SUPPLY_MULTIPLIER_TICKFILL) / 10_000
                            ),
                        tickSpacing,
                        maxLiquidityPerTick
                    )
                )
            );
        }

        s_V4toSFPMIdData[idV4] = PoolIdData(
            maxLiquidityPerTick,
            dataOld.poolId,
            minEnforcedTick,
            maxEnforcedTick,
            dataOld.initialized
        );

        emit EnforcedTicksUpdated(idV4, minEnforcedTick, maxEnforcedTick);
    }

    /*//////////////////////////////////////////////////////////////
                        UNISWAP V4 LOCK CALLBACK
    //////////////////////////////////////////////////////////////*/

    /// @notice Executes the corresponding operations and state updates required to mint `tokenId` of `positionSize` in `key`
    /// @param key The Uniswap V4 pool key in which to mint `tokenId`
    /// @param tickLimitLow The lower bound of an acceptable open interval for the ending price
    /// @param tickLimitHigh The upper bound of an acceptable open interval for the ending price
    /// @param positionSize The number of contracts minted, expressed in terms of the asset
    /// @param tokenId The tokenId of the minted position, which encodes information about up to 4 legs
    /// @param isBurn Flag indicating if the position is being burnt
    /// @return An array of LeftRight encoded words containing the amount of currency0 and currency1 collected as fees for each leg
    /// @return The net amount of currency0 and currency1 moved to/from the Uniswap V4 pool
    function _unlockAndCreatePositionInAMM(
        PoolKey calldata key,
        int24 tickLimitLow,
        int24 tickLimitHigh,
        uint128 positionSize,
        TokenId tokenId,
        bool isBurn
    ) internal returns (LeftRightUnsigned[4] memory, LeftRightSigned) {
        return
            abi.decode(
                POOL_MANAGER_V4.unlock(
                    abi.encode(
                        msg.sender,
                        key,
                        tickLimitLow,
                        tickLimitHigh,
                        positionSize,
                        tokenId,
                        isBurn
                    )
                ),
                (LeftRightUnsigned[4], LeftRightSigned)
            );
    }

    /// @notice Uniswap V4 unlock callback implementation.
    /// @dev Parameters are `(address account, PoolKey key, int24 tickLimitLow, int24 tickLimitHigh, uint128 positionSize, TokenId tokenId, bool isBurn)`.
    /// @dev Executes the corresponding operations and state updates required to mint `tokenId` of `positionSize` in `key`
    /// @dev (shorts/longs are reversed before calling this function at burn)
    /// @param data The encoded data containing the input parameters
    /// @return `(LeftRightUnsigned[4] collectedByLeg, LeftRightSigned totalMoved)` An array of LeftRight encoded words containing the amount of currency0 and currency1 collected as fees for each leg and the net amount of currency0 and currency1 moved to/from the Uniswap V4 pool
    function unlockCallback(bytes calldata data) external returns (bytes memory) {
        if (msg.sender != address(POOL_MANAGER_V4)) revert Errors.UnauthorizedUniswapCallback();

        (
            address account,
            PoolKey memory key,
            int24 tickLimitLow,
            int24 tickLimitHigh,
            uint128 positionSize,
            TokenId tokenId,
            bool isBurn
        ) = abi.decode(data, (address, PoolKey, int24, int24, uint128, TokenId, bool));

        (
            LeftRightUnsigned[4] memory collectedByLeg,
            LeftRightSigned totalMoved
        ) = _createPositionInAMM(
                account,
                key,
                tickLimitLow,
                tickLimitHigh,
                positionSize,
                tokenId,
                isBurn
            );
        return abi.encode(collectedByLeg, totalMoved);
    }

    /*//////////////////////////////////////////////////////////////
                       PUBLIC MINT/BURN FUNCTIONS
    //////////////////////////////////////////////////////////////*/

    /// @notice Burn a new position containing up to 4 legs wrapped in a ERC1155 token.
    /// @dev Auto-collect all accumulated fees.
    /// @param key The Uniswap V4 pool key in which to burn `tokenId`
    /// @param tokenId The tokenId of the minted position, which encodes information about up to 4 legs
    /// @param positionSize The number of contracts minted, expressed in terms of the asset
    /// @param slippageTickLimitLow The lower bound of an acceptable open interval for the ending price
    /// @param slippageTickLimitHigh The upper bound of an acceptable open interval for the ending price
    /// @return An array of LeftRight encoded words containing the amount of currency0 and currency1 collected as fees for each leg
    /// @return The net amount of currency0 and currency1 moved to/from the Uniswap V4 pool
    function burnTokenizedPosition(
        PoolKey calldata key,
        TokenId tokenId,
        uint128 positionSize,
        int24 slippageTickLimitLow,
        int24 slippageTickLimitHigh
    ) external nonReentrant returns (LeftRightUnsigned[4] memory, LeftRightSigned) {
        _burn(msg.sender, TokenId.unwrap(tokenId), positionSize);

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

        return
            _unlockAndCreatePositionInAMM(
                key,
                slippageTickLimitLow,
                slippageTickLimitHigh,
                positionSize,
                tokenId.flipToBurnToken(),
                BURN
            );
    }

    /// @notice Create a new position `tokenId` containing up to 4 legs.
    /// @param key The Uniswap V4 pool key in which to mint `tokenId`
    /// @param tokenId The tokenId of the minted position, which encodes information for up to 4 legs
    /// @param positionSize The number of contracts minted, expressed in terms of the asset
    /// @param slippageTickLimitLow The lower bound of an acceptable open interval for the ending price
    /// @param slippageTickLimitHigh The upper bound of an acceptable open interval for the ending price
    /// @return An array of LeftRight encoded words containing the amount of currency0 and currency1 collected as fees for each leg
    /// @return The net amount of currency0 and currency1 moved to/from the Uniswap V4 pool
    function mintTokenizedPosition(
        PoolKey calldata key,
        TokenId tokenId,
        uint128 positionSize,
        int24 slippageTickLimitLow,
        int24 slippageTickLimitHigh
    ) external nonReentrant returns (LeftRightUnsigned[4] memory, LeftRightSigned) {
        _mint(msg.sender, TokenId.unwrap(tokenId), positionSize);

        emit TokenizedPositionMinted(msg.sender, tokenId, positionSize);

        // verify that the tokenId is correctly formatted and conforms to all enforced constraints
        tokenId.validate();

        return
            _unlockAndCreatePositionInAMM(
                key,
                slippageTickLimitLow,
                slippageTickLimitHigh,
                positionSize,
                tokenId,
                MINT
            );
    }

    /*//////////////////////////////////////////////////////////////
                     TRANSFER HOOK IMPLEMENTATIONS
    //////////////////////////////////////////////////////////////*/

    /// @notice All ERC1155 transfers are disabled.
    function safeTransferFrom(
        address,
        address,
        uint256,
        uint256,
        bytes calldata
    ) public pure override {
        revert();
    }

    /// @notice All ERC1155 transfers are disabled.
    function safeBatchTransferFrom(
        address,
        address,
        uint256[] calldata,
        uint256[] calldata,
        bytes calldata
    ) public pure override {
        revert();
    }

    /*//////////////////////////////////////////////////////////////
              AMM INTERACTION AND POSITION UPDATE HELPERS
    //////////////////////////////////////////////////////////////*/

    /// @notice Called to perform an ITM swap in the Uniswap pool to resolve any non-tokenType token deltas.
    /// @dev When a position is minted or burnt in-the-money (ITM) we are *not* 100% currency0 or 100% currency1: we have a mix of both tokens.
    /// @dev The swapping for ITM options is needed because only one of the tokens are "borrowed" by a user to create the position.
    // This is an ITM situation below (price within the range of the chunk):
    //
    //        AMM       strike
    //     liquidity   price tick
    //        ▲           │
    //        │       ┌───▼───┐
    //        │       │       │liquidity chunk
    //        │ ┌─────┴─▲─────┴─────┐
    //        │ │       │           │
    //        │ │       :           │
    //        │ │       :           │
    //        │ │       :           │
    //        └─┴───────▲───────────┴─► price
    //                  │
    //            current price
    //             in-the-money: mix of tokens 0 and 1 within the chunk
    //
    //   If we take currency0 as an example, we deploy it to the AMM pool and *then* swap to get the right mix of currency0 and currency1
    //   to be correctly in the money at that strike.
    //   It that position is burnt, then we remove a mix of the two tokens and swap one of them so that the user receives only one.
    /// @param key The Uniswap V4 pool key in which to perform the swap
    /// @param itmAmounts How much to swap (i.e. how many tokens are ITM)
    /// @return The token deltas swapped in the AMM
    function swapInAMM(
        PoolKey memory key,
        LeftRightSigned itmAmounts
    ) internal returns (LeftRightSigned) {
        unchecked {
            bool zeroForOne; // The direction of the swap, true for currency0 to currency1, false for currency1 to currency0
            int256 swapAmount; // The amount of currency0 or currency1 to swap

            // unpack the in-the-money amounts
            int128 itm0 = itmAmounts.rightSlot();
            int128 itm1 = itmAmounts.leftSlot();

            // NOTE: upstream users of this function such as the Panoptic Pool should ensure users always compensate for the ITM amount delta
            // the netting swap is not perfectly accurate, and it is possible for swaps to run out of liquidity, so we do not want to rely on it
            // this is simply a convenience feature, and should be treated as such
            if ((itm0 != 0) && (itm1 != 0)) {
                // implement a single "netting" swap. Thank you @danrobinson for this puzzle/idea
                // NOTE: negative ITM amounts denote a surplus of tokens (burning liquidity), while positive amounts denote a shortage of tokens (minting liquidity)
                // compute the approximate delta of currency0 that should be resolved in the swap at the current tick
                // we do this by flipping the signs on the currency1 ITM amount converting+deducting it against the currency0 ITM amount
                // couple examples (price = 2 1/0):
                //  - 100 surplus 0, 100 surplus 1 (itm0 = -100, itm1 = -100)
                //    normal swap 0: 100 0 => 200 1
                //    normal swap 1: 100 1 => 50 0
                //    final swap amounts: 50 0 => 100 1
                //    netting swap: net0 = -100 - (-100/2) = -50, ZF1 = true, 50 0 => 100 1
                // - 100 surplus 0, 100 shortage 1 (itm0 = -100, itm1 = 100)
                //    normal swap 0: 100 0 => 200 1
                //    normal swap 1: 50 0 => 100 1
                //    final swap amounts: 150 0 => 300 1
                //    netting swap: net0 = -100 - (100/2) = -150, ZF1 = true, 150 0 => 300 1
                // - 100 shortage 0, 100 surplus 1 (itm0 = 100, itm1 = -100)
                //    normal swap 0: 200 1 => 100 0
                //    normal swap 1: 100 1 => 50 0
                //    final swap amounts: 300 1 => 150 0
                //    netting swap: net0 = 100 - (-100/2) = 150, ZF1 = false, 300 1 => 150 0
                // - 100 shortage 0, 100 shortage 1 (itm0 = 100, itm1 = 100)
                //    normal swap 0: 200 1 => 100 0
                //    normal swap 1: 50 0 => 100 1
                //    final swap amounts: 100 1 => 50 0
                //    netting swap: net0 = 100 - (100/2) = 50, ZF1 = false, 100 1 => 50 0
                // - = Net surplus of currency0
                // + = Net shortage of currency0
                int256 net0 = itm0 -
                    PanopticMath.convert1to0(
                        itm1,
                        V4StateReader.getSqrtPriceX96(POOL_MANAGER_V4, key.toId())
                    );

                zeroForOne = net0 < 0;
                swapAmount = net0;
            } else if (itm0 != 0) {
                zeroForOne = itm0 < 0;
                swapAmount = itm0;
            } else {
                zeroForOne = itm1 > 0;
                swapAmount = itm1;
            }

            // NOTE: can occur if itm0 and itm1 have the same value
            // in that case, swapping would be pointless so skip
            if (swapAmount == 0) return LeftRightSigned.wrap(0);

            BalanceDelta swapDelta = POOL_MANAGER_V4.swap(
                key,
                IPoolManager.SwapParams(
                    zeroForOne,
                    swapAmount,
                    zeroForOne
                        ? Constants.MIN_V4POOL_SQRT_RATIO + 1
                        : Constants.MAX_V4POOL_SQRT_RATIO - 1
                ),
                ""
            );

            return
                LeftRightSigned.wrap(0).toRightSlot(-swapDelta.amount0()).toLeftSlot(
                    -swapDelta.amount1()
                );
        }
    }

    /// @notice Create the position in the AMM defined by `tokenId`.
    /// @dev Loops over each leg in the tokenId and calls _createLegInAMM for each, which does the mint/burn in the AMM.
    /// @param account The address of the user creating the position
    /// @param key The Uniswap V4 pool key in which to create the position
    /// @param tickLimitLow The lower bound of an acceptable open interval for the ending price
    /// @param tickLimitHigh The upper bound of an acceptable open interval for the ending price
    /// @param positionSize The size of the option position
    /// @param tokenId The option position
    /// @param isBurn Whether a position is being minted (false) or burned (true)
    /// @return collectedByLeg An array of LeftRight encoded words containing the amount of currency0 and currency1 collected as fees for each leg
    /// @return totalMoved The net amount of funds moved to/from Uniswap
    function _createPositionInAMM(
        address account,
        PoolKey memory key,
        int24 tickLimitLow,
        int24 tickLimitHigh,
        uint128 positionSize,
        TokenId tokenId,
        bool isBurn
    ) internal returns (LeftRightUnsigned[4] memory collectedByLeg, LeftRightSigned totalMoved) {
        uint256 amount0;
        uint256 amount1;

        LeftRightSigned itmAmounts;

        LeftRightUnsigned totalCollected;

        {
            PoolIdData memory poolIdData = s_V4toSFPMIdData[key.toId()];

            if (poolIdData.poolId != tokenId.poolId() || poolIdData.poolId == 0)
                revert Errors.InvalidTokenIdParameter(0);

            for (uint256 leg = 0; leg < tokenId.countLegs(); ) {
                address _account = account;
                PoolKey memory _key = key;

                LiquidityChunk liquidityChunk;
                {
                    uint128 _positionSize = positionSize;
                    liquidityChunk = PanopticMath.getLiquidityChunk(tokenId, leg, _positionSize);
                }

                // validate tick range for newly minted positions
                if (!isBurn) {
                    int24 tickSpacing = tokenId.tickSpacing();
                    int24 tickLower = liquidityChunk.tickLower();
                    int24 tickUpper = liquidityChunk.tickUpper();

                    if (
                        tickLower % tickSpacing != 0 ||
                        tickUpper % tickSpacing != 0 ||
                        tickLower < poolIdData.minEnforcedTick ||
                        tickUpper > poolIdData.maxEnforcedTick
                    ) revert Errors.InvalidTickBound();
                }

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

                    amount1 += Math.getAmount1ForLiquidity(liquidityChunk);
                }

                LeftRightSigned movedLeg;
                TokenId _tokenId = tokenId;
                bool _isBurn = isBurn;

                (movedLeg, collectedByLeg[leg]) = _createLegInAMM(
                    _account,
                    _key,
                    _tokenId,
                    leg,
                    liquidityChunk,
                    _isBurn
                );

                totalMoved = totalMoved.add(movedLeg);
                totalCollected = totalCollected.add(collectedByLeg[leg]);

                // if tokenType is 1, and we transacted some currency0: then this leg is ITM
                // if tokenType is 0, and we transacted some currency1: then this leg is ITM
                itmAmounts = itmAmounts.add(
                    _tokenId.tokenType(leg) == 0
                        ? LeftRightSigned.wrap(0).toLeftSlot(movedLeg.leftSlot())
                        : LeftRightSigned.wrap(0).toRightSlot(movedLeg.rightSlot())
                );

                unchecked {
                    ++leg;
                }
            }
        }

        // Ensure upper bound on amount of tokens contained across all legs of the position on any given tick does not exceed a maximum of (2**127-1).
        // This is the maximum value of the `int128` type we frequently use to hold token amounts, so a given position's size should be guaranteed to
        // fit within that limit at all times.
        if (amount0 > uint128(type(int128).max - 4) || amount1 > uint128(type(int128).max - 4))
            revert Errors.PositionTooLarge();

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

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

        LeftRightSigned cumulativeDelta = totalMoved.sub(totalCollected);

        if (cumulativeDelta.rightSlot() > 0) {
            POOL_MANAGER_V4.burn(
                account,
                uint160(Currency.unwrap(key.currency0)),
                uint128(cumulativeDelta.rightSlot())
            );
        } else if (cumulativeDelta.rightSlot() < 0) {
            POOL_MANAGER_V4.mint(
                account,
                uint160(Currency.unwrap(key.currency0)),
                uint128(-cumulativeDelta.rightSlot())
            );
        }

        if (cumulativeDelta.leftSlot() > 0) {
            POOL_MANAGER_V4.burn(
                account,
                uint160(Currency.unwrap(key.currency1)),
                uint128(cumulativeDelta.leftSlot())
            );
        } else if (cumulativeDelta.leftSlot() < 0) {
            POOL_MANAGER_V4.mint(
                account,
                uint160(Currency.unwrap(key.currency1)),
                uint128(-cumulativeDelta.leftSlot())
            );
        }

        PoolKey memory __key = key;

        // Get the current tick of the Uniswap pool, check slippage
        int24 currentTick = V4StateReader.getTick(POOL_MANAGER_V4, __key.toId());

        if ((currentTick >= tickLimitHigh) || (currentTick <= tickLimitLow))
            revert Errors.PriceBoundFail();
    }

    /// @notice Create the position in the AMM for a specific leg in the tokenId.
    /// @dev For the leg specified by the _leg input:
    /// @dev  - mints any new liquidity in the AMM needed (via _mintLiquidity)
    /// @dev  - burns any new liquidity in the AMM needed (via _burnLiquidity)
    /// @dev  - tracks all amounts minted and burned
    /// @dev To burn a position, the opposing position is "created" through this function,
    /// but we need to pass in a flag to indicate that so the removedLiquidity is updated.
    /// @param account The address of the user creating the position
    /// @param key The Uniswap V4 pool key in which to create the position
    /// @param tokenId The option position
    /// @param leg The leg index that needs to be modified
    /// @param liquidityChunk The liquidity chunk in Uniswap represented by the leg
    /// @param isBurn Whether a position is being minted (true) or burned (false)
    /// @return moved The net amount of funds moved to/from Uniswap
    /// @return collectedSingleLeg LeftRight encoded words containing the amount of currency0 and currency1 collected as fees
    function _createLegInAMM(
        address account,
        PoolKey memory key,
        TokenId tokenId,
        uint256 leg,
        LiquidityChunk liquidityChunk,
        bool isBurn
    ) internal returns (LeftRightSigned moved, LeftRightUnsigned collectedSingleLeg) {
        // unique key to identify the liquidity chunk in this Uniswap pool
        bytes32 positionKey = keccak256(
            abi.encodePacked(
                key.toId(),
                account,
                tokenId.tokenType(leg),
                liquidityChunk.tickLower(),
                liquidityChunk.tickUpper()
            )
        );

        // update our internal bookkeeping of how much liquidity we have deployed in the AMM
        // for example: if this leg is short, we add liquidity to the amm, make sure to add that to our tracking
        uint128 updatedLiquidity;
        uint256 isLong = tokenId.isLong(leg);
        LeftRightUnsigned currentLiquidity = s_accountLiquidity[positionKey];
        {
            // s_accountLiquidity is a LeftRight. The right slot represents the liquidity currently sold (added) in the AMM owned by the user
            // the left slot represents the amount of liquidity currently bought (removed) that has been removed from the AMM - the user owes it to a seller
            // the reason why it is called "removedLiquidity" is because long options are created by removing - ie. short selling LP positions
            uint128 startingLiquidity = currentLiquidity.rightSlot();
            uint128 removedLiquidity = currentLiquidity.leftSlot();
            uint128 chunkLiquidity = liquidityChunk.liquidity();

            // 0-liquidity interactions are asymmetrical in Uniswap (burning 0 liquidity is permitted and functions as a poke, but minting is prohibited)
            // thus, we prohibit all 0-liquidity chunks to prevent users from creating positions that cannot be closed
            if (chunkLiquidity == 0) revert Errors.ZeroLiquidity();

            if (isLong == 0) {
                // selling/short: so move from account *to* uniswap
                // we're minting more liquidity in uniswap: so add the incoming liquidity chunk to the existing liquidity chunk
                updatedLiquidity = startingLiquidity + chunkLiquidity;

                /// @dev If the isLong flag is 0=short but the position was burnt, then this is closing a long position
                /// @dev so the amount of removed liquidity should decrease.
                if (isBurn) {
                    removedLiquidity -= chunkLiquidity;
                }
            } else {
                // the _leg is long (buying: moving *from* uniswap to account)
                // so we seek to move the incoming liquidity chunk *out* of uniswap - but was there sufficient liquidity sitting in uniswap
                // in the first place?
                if (startingLiquidity < chunkLiquidity) {
                    // the amount we want to move (liquidityChunk.legLiquidity()) out of uniswap is greater than
                    // what the account that owns the liquidity in uniswap has (startingLiquidity)
                    // we must ensure that an account can only move its own liquidity out of uniswap
                    // so we revert in this case
                    revert Errors.NotEnoughLiquidity();
                } else {
                    // startingLiquidity is >= chunkLiquidity, so no possible underflow
                    unchecked {
                        // we want to move less than what already sits in uniswap, no problem:
                        updatedLiquidity = startingLiquidity - chunkLiquidity;
                    }
                }

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

            // update the starting liquidity for this position for next time around
            s_accountLiquidity[positionKey] = LeftRightUnsigned.wrap(updatedLiquidity).toLeftSlot(
                removedLiquidity
            );
        }

        // track how much liquidity we need to collect from uniswap
        // add the fees that accumulated in uniswap within the liquidityChunk:

        /* if the position is NOT long (selling a put or a call), then _mintLiquidity to move liquidity
            from the msg.sender to the Uniswap V4 pool:
            Selling(isLong=0): Mint chunk of liquidity in Uniswap (defined by upper tick, lower tick, and amount)
                   ┌─────────────────────────────────┐
            ▲     ┌▼┐ liquidityChunk                 │
            │  ┌──┴─┴──┐                         ┌───┴──┐
            │  │       │                         │      │
            └──┴───────┴──►                      └──────┘
              Uniswap V4                        msg.sender
        
            else: the position is long (buying a put or a call), then _burnLiquidity to remove liquidity from Uniswap V4
            Buying(isLong=1): Burn in Uniswap
                   ┌─────────────────┐
            ▲     ┌┼┐                │
            │  ┌──┴─┴──┐         ┌───▼──┐
            │  │       │         │      │
            └──┴───────┴──►      └──────┘
              Uniswap V4        msg.sender 
        */

        LiquidityChunk _liquidityChunk = liquidityChunk;

        PoolKey memory _key = key;

        (BalanceDelta delta, BalanceDelta feesAccrued) = POOL_MANAGER_V4.modifyLiquidity(
            _key,
            IPoolManager.ModifyLiquidityParams(
                _liquidityChunk.tickLower(),
                _liquidityChunk.tickUpper(),
                isLong == 0
                    ? int256(uint256(_liquidityChunk.liquidity()))
                    : -int256(uint256(_liquidityChunk.liquidity())),
                positionKey
            ),
            ""
        );

        unchecked {
            moved = LeftRightSigned
                .wrap(0)
                .toRightSlot(feesAccrued.amount0() - delta.amount0())
                .toLeftSlot(feesAccrued.amount1() - delta.amount1());
        }

        // (premium can only be collected if liquidity existed in the chunk prior to this mint)
        if (currentLiquidity.rightSlot() > 0) {
            collectedSingleLeg = LeftRightUnsigned
                .wrap(0)
                .toRightSlot(uint128(feesAccrued.amount0()))
                .toLeftSlot(uint128(feesAccrued.amount1()));

            _updateStoredPremia(positionKey, currentLiquidity, collectedSingleLeg);
        }
    }

    /// @notice Updates the premium accumulators for a chunk with the latest collected tokens.
    /// @param positionKey A key representing a liquidity chunk/range in Uniswap
    /// @param currentLiquidity The total amount of liquidity in the AMM for the specified chunk
    /// @param collectedAmounts The amount of tokens (currency0 and currency1) collected from Uniswap
    function _updateStoredPremia(
        bytes32 positionKey,
        LeftRightUnsigned currentLiquidity,
        LeftRightUnsigned collectedAmounts
    ) private {
        (
            LeftRightUnsigned deltaPremiumOwed,
            LeftRightUnsigned deltaPremiumGross
        ) = _getPremiaDeltas(currentLiquidity, collectedAmounts);

        // add deltas to accumulators and freeze both accumulators (for a token) if one of them overflows
        // (i.e if only currency0 (right slot) of the owed premium overflows, then stop accumulating  both currency0 owed premium and currency0 gross premium for the chunk)
        // this prevents situations where the owed premium gets out of sync with the gross premium due to one of them overflowing
        (s_accountPremiumOwed[positionKey], s_accountPremiumGross[positionKey]) = LeftRightLibrary
            .addCapped(
                s_accountPremiumOwed[positionKey],
                deltaPremiumOwed,
                s_accountPremiumGross[positionKey],
                deltaPremiumGross
            );
    }

    /// @notice Compute deltas for Owed/Gross premium given quantities of tokens collected from Uniswap.
    /// @dev Returned accumulators are capped at the max value (`2^128 - 1`) for each token if they overflow.
    /// @param currentLiquidity NetLiquidity (right) and removedLiquidity (left) at the start of the transaction
    /// @param collectedAmounts Total amount of tokens (currency0 and currency1) collected from Uniswap
    /// @return deltaPremiumOwed The extra premium (per liquidity X64) to be added to the owed accumulator for currency0 (right) and currency1 (left)
    /// @return deltaPremiumGross The extra premium (per liquidity X64) to be added to the gross accumulator for currency0 (right) and currency1 (left)
    function _getPremiaDeltas(
        LeftRightUnsigned currentLiquidity,
        LeftRightUnsigned collectedAmounts
    )
        private
        pure
        returns (LeftRightUnsigned deltaPremiumOwed, LeftRightUnsigned deltaPremiumGross)
    {
        // extract liquidity values
        uint256 removedLiquidity = currentLiquidity.leftSlot();
        uint256 netLiquidity = currentLiquidity.rightSlot();

        // premia spread equations are graphed and documented here: https://www.desmos.com/calculator/mdeqob2m04
        // explains how we get from the premium per liquidity (calculated here) to the total premia collected and the multiplier
        // as well as how the value of VEGOID affects the premia
        // note that the "base" premium is just a common factor shared between the owed (long) and gross (short)
        // premia, and is only separated to simplify the calculation
        // (the graphed equations include this factor without separating it)
        unchecked {
            uint256 totalLiquidity = netLiquidity + removedLiquidity;

            uint256 premium0X64_base;
            uint256 premium1X64_base;

            {
                uint128 collected0 = collectedAmounts.rightSlot();
                uint128 collected1 = collectedAmounts.leftSlot();

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

            {
                uint128 premium0X64_owed;
                uint128 premium1X64_owed;
                {
                    // compute the owed premium (from Eqn 3)
                    uint256 numerator = netLiquidity + (removedLiquidity / 2 ** VEGOID);

                    premium0X64_owed = Math
                        .mulDiv(premium0X64_base, numerator, totalLiquidity)
                        .toUint128Capped();
                    premium1X64_owed = Math
                        .mulDiv(premium1X64_base, numerator, totalLiquidity)
                        .toUint128Capped();

                    deltaPremiumOwed = LeftRightUnsigned.wrap(premium0X64_owed).toLeftSlot(
                        premium1X64_owed
                    );
                }
            }

            {
                uint128 premium0X64_gross;
                uint128 premium1X64_gross;
                {
                    // compute the gross premium (from Eqn 4)
                    uint256 numerator = totalLiquidity ** 2 -
                        totalLiquidity *
                        removedLiquidity +
                        ((removedLiquidity ** 2) / 2 ** (VEGOID));

                    premium0X64_gross = Math
                        .mulDiv(premium0X64_base, numerator, totalLiquidity ** 2)
                        .toUint128Capped();
                    premium1X64_gross = Math
                        .mulDiv(premium1X64_base, numerator, totalLiquidity ** 2)
                        .toUint128Capped();

                    deltaPremiumGross = LeftRightUnsigned.wrap(premium0X64_gross).toLeftSlot(
                        premium1X64_gross
                    );
                }
            }
        }
    }

    /*//////////////////////////////////////////////////////////////
                                QUERIES
    //////////////////////////////////////////////////////////////*/

    /// @notice Return the liquidity associated with a given liquidity chunk/tokenType for a user on a Uniswap pool.
    /// @param idV4 The Uniswap V4 pool id to query
    /// @param owner The address of the account that is queried
    /// @param tokenType The tokenType of the position
    /// @param tickLower The lower end of the tick range for the position
    /// @param tickUpper The upper end of the tick range for the position
    /// @return accountLiquidities The amount of liquidity that held in and removed from Uniswap for that chunk (netLiquidity:removedLiquidity -> rightSlot:leftSlot)
    function getAccountLiquidity(
        PoolId idV4,
        address owner,
        uint256 tokenType,
        int24 tickLower,
        int24 tickUpper
    ) external view returns (LeftRightUnsigned accountLiquidities) {
        // Extract the account liquidity for a given Uniswap pool, owner, token type, and ticks
        // tokenType input here is the asset of the positions minted, this avoids put liquidity to be used for call, and vice-versa
        accountLiquidities = s_accountLiquidity[
            keccak256(abi.encodePacked(idV4, owner, tokenType, tickLower, tickUpper))
        ];
    }

    /// @notice Return the premium associated with a given position, where premium is an accumulator of feeGrowth for the touched position.
    /// @dev If an atTick parameter is provided that is different from `type(int24).max`, then it will update the premium up to the current
    /// block at the provided atTick value. We do this because this may be called immediately after the Uniswap V4 pool has been touched,
    /// so no need to read the feeGrowths from the Uniswap V4 pool.
    /// @param idV4 The Uniswap V4 pool id to query
    /// @param owner The address of the account that is queried
    /// @param tokenType The tokenType of the position
    /// @param tickLower The lower end of the tick range for the position
    /// @param tickUpper The upper end of the tick range for the position
    /// @param atTick The current tick. Set `atTick < (type(int24).max = 8388608)` to get latest premium up to the current block
    /// @param isLong Whether the position is long (=1) or short (=0)
    /// @return The amount of premium (per liquidity X64) for currency0 = `sum(feeGrowthLast0X128)` over every block where the position has been touched
    /// @return The amount of premium (per liquidity X64) for currency1 = `sum(feeGrowthLast0X128)` over every block where the position has been touched
    function getAccountPremium(
        PoolId idV4,
        address owner,
        uint256 tokenType,
        int24 tickLower,
        int24 tickUpper,
        int24 atTick,
        uint256 isLong
    ) external view returns (uint128, uint128) {
        bytes32 positionKey = keccak256(
            abi.encodePacked(idV4, owner, tokenType, tickLower, tickUpper)
        );

        LeftRightUnsigned acctPremia;

        LeftRightUnsigned accountLiquidities = s_accountLiquidity[positionKey];
        uint128 netLiquidity = accountLiquidities.rightSlot();

        // Compute the premium up to the current block (ie. after last touch until now). Do not proceed if `atTick == (type(int24).max = 8388608)`
        if (atTick < type(int24).max && netLiquidity != 0) {
            // unique key to identify the liquidity chunk in this Uniswap pool
            LeftRightUnsigned amountToCollect;
            {
                PoolId _idV4 = idV4;
                int24 _tickLower = tickLower;
                int24 _tickUpper = tickUpper;
                int24 _atTick = atTick;
                bytes32 _positionKey = positionKey;

                (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = V4StateReader
                    .getFeeGrowthInside(POOL_MANAGER_V4, _idV4, _atTick, _tickLower, _tickUpper);

                (uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128) = V4StateReader
                    .getFeeGrowthInsideLast(
                        POOL_MANAGER_V4,
                        _idV4,
                        keccak256(
                            abi.encodePacked(address(this), _tickLower, _tickUpper, _positionKey)
                        )
                    );

                unchecked {
                    amountToCollect = LeftRightUnsigned
                        .wrap(
                            uint128(
                                Math.mulDiv128(
                                    feeGrowthInside0X128 - feeGrowthInside0LastX128,
                                    netLiquidity
                                )
                            )
                        )
                        .toLeftSlot(
                            uint128(
                                Math.mulDiv128(
                                    feeGrowthInside1X128 - feeGrowthInside1LastX128,
                                    netLiquidity
                                )
                            )
                        );
                }
            }

            (LeftRightUnsigned premiumOwed, LeftRightUnsigned premiumGross) = _getPremiaDeltas(
                accountLiquidities,
                amountToCollect
            );

            // add deltas to accumulators and freeze both accumulators (for a token) if one of them overflows
            // (i.e if only currency0 (right slot) of the owed premium overflows, then stop accumulating both currency0 owed premium and currency0 gross premium for the chunk)
            // this prevents situations where the owed premium gets out of sync with the gross premium due to one of them overflowing
            (premiumOwed, premiumGross) = LeftRightLibrary.addCapped(
                s_accountPremiumOwed[positionKey],
                premiumOwed,
                s_accountPremiumGross[positionKey],
                premiumGross
            );

            acctPremia = isLong == 1 ? premiumOwed : premiumGross;
        } else {
            // Extract the account liquidity for a given Uniswap pool, owner, token type, and ticks
            acctPremia = isLong == 1
                ? s_accountPremiumOwed[positionKey]
                : s_accountPremiumGross[positionKey];
        }
        return (acctPremia.rightSlot(), acctPremia.leftSlot());
    }

    /// @notice Returns the Uniswap V4 poolkey  for a given `poolId`.
    /// @param poolId The unique pool identifier for a Uni V4 pool in the SFPM
    /// @return The Uniswap V4 pool key corresponding to `poolId`
    function getUniswapV4PoolKeyFromId(uint64 poolId) external view returns (PoolKey memory) {
        return s_poolIdToKey[poolId];
    }

    /// @notice Returns the current enforced tick limits for a given Uniswap V4 `PoolId`.
    /// @param idV4 The Uniswap V4 pool identifier
    /// @return The minimum enforced tick for chunks created in the pool corresponding to `idV4`
    /// @return The maximum enforced tick for chunks created in the pool corresponding to `idV4`
    function getEnforcedTickLimits(PoolId idV4) external view returns (int24, int24) {
        PoolIdData memory poolIdData = s_V4toSFPMIdData[idV4];
        return (poolIdData.minEnforcedTick, poolIdData.maxEnforcedTick);
    }

    /// @notice Returns the SFPM `poolId` for a given Uniswap V4 `PoolId`.
    /// @param idV4 The Uniswap V4 pool identifier
    /// @return The unique pool identifier in the SFPM corresponding to `idV4`
    function getPoolId(PoolId idV4) external view returns (uint64) {
        return s_V4toSFPMIdData[idV4].poolId;
    }

    /// @notice Returns the SFPM `poolId` for a given Uniswap V4 `PoolKey`.
    /// @param key The Uniswap V4 pool key
    /// @return The unique pool identifier in the SFPM corresponding to `key`
    function getPoolId(PoolKey calldata key) external view returns (uint64) {
        return s_V4toSFPMIdData[key.toId()].poolId;
    }
}

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {Currency} from "../types/Currency.sol";
import {PoolKey} from "../types/PoolKey.sol";
import {IHooks} from "./IHooks.sol";
import {IERC6909Claims} from "./external/IERC6909Claims.sol";
import {IProtocolFees} from "./IProtocolFees.sol";
import {BalanceDelta} from "../types/BalanceDelta.sol";
import {PoolId} from "../types/PoolId.sol";
import {IExtsload} from "./IExtsload.sol";
import {IExttload} from "./IExttload.sol";

/// @notice Interface for the PoolManager
interface IPoolManager is IProtocolFees, IERC6909Claims, IExtsload, IExttload {
    /// @notice Thrown when a currency is not netted out after the contract is unlocked
    error CurrencyNotSettled();

    /// @notice Thrown when trying to interact with a non-initialized pool
    error PoolNotInitialized();

    /// @notice Thrown when unlock is called, but the contract is already unlocked
    error AlreadyUnlocked();

    /// @notice Thrown when a function is called that requires the contract to be unlocked, but it is not
    error ManagerLocked();

    /// @notice Pools are limited to type(int16).max tickSpacing in #initialize, to prevent overflow
    error TickSpacingTooLarge(int24 tickSpacing);

    /// @notice Pools must have a positive non-zero tickSpacing passed to #initialize
    error TickSpacingTooSmall(int24 tickSpacing);

    /// @notice PoolKey must have currencies where address(currency0) < address(currency1)
    error CurrenciesOutOfOrderOrEqual(address currency0, address currency1);

    /// @notice Thrown when a call to updateDynamicLPFee is made by an address that is not the hook,
    /// or on a pool that does not have a dynamic swap fee.
    error UnauthorizedDynamicLPFeeUpdate();

    /// @notice Thrown when trying to swap amount of 0
    error SwapAmountCannotBeZero();

    ///@notice Thrown when native currency is passed to a non native settlement
    error NonzeroNativeValue();

    /// @notice Thrown when `clear` is called with an amount that is not exactly equal to the open currency delta.
    error MustClearExactPositiveDelta();

    /// @notice Emitted when a new pool is initialized
    /// @param id The abi encoded hash of the pool key struct for the new pool
    /// @param currency0 The first currency of the pool by address sort order
    /// @param currency1 The second currency of the pool by address sort order
    /// @param fee The fee collected upon every swap in the pool, denominated in hundredths of a bip
    /// @param tickSpacing The minimum number of ticks between initialized ticks
    /// @param hooks The hooks contract address for the pool, or address(0) if none
    /// @param sqrtPriceX96 The price of the pool on initialization
    /// @param tick The initial tick of the pool corresponding to the initialized price
    event Initialize(
        PoolId indexed id,
        Currency indexed currency0,
        Currency indexed currency1,
        uint24 fee,
        int24 tickSpacing,
        IHooks hooks,
        uint160 sqrtPriceX96,
        int24 tick
    );

    /// @notice Emitted when a liquidity position is modified
    /// @param id The abi encoded hash of the pool key struct for the pool that was modified
    /// @param sender The address that modified the pool
    /// @param tickLower The lower tick of the position
    /// @param tickUpper The upper tick of the position
    /// @param liquidityDelta The amount of liquidity that was added or removed
    /// @param salt The extra data to make positions unique
    event ModifyLiquidity(
        PoolId indexed id, address indexed sender, int24 tickLower, int24 tickUpper, int256 liquidityDelta, bytes32 salt
    );

    /// @notice Emitted for swaps between currency0 and currency1
    /// @param id The abi encoded hash of the pool key struct for the pool that was modified
    /// @param sender The address that initiated the swap call, and that received the callback
    /// @param amount0 The delta of the currency0 balance of the pool
    /// @param amount1 The delta of the currency1 balance of the pool
    /// @param sqrtPriceX96 The sqrt(price) of the pool after the swap, as a Q64.96
    /// @param liquidity The liquidity of the pool after the swap
    /// @param tick The log base 1.0001 of the price of the pool after the swap
    /// @param fee The swap fee in hundredths of a bip
    event Swap(
        PoolId indexed id,
        address indexed sender,
        int128 amount0,
        int128 amount1,
        uint160 sqrtPriceX96,
        uint128 liquidity,
        int24 tick,
        uint24 fee
    );

    /// @notice Emitted for donations
    /// @param id The abi encoded hash of the pool key struct for the pool that was donated to
    /// @param sender The address that initiated the donate call
    /// @param amount0 The amount donated in currency0
    /// @param amount1 The amount donated in currency1
    event Donate(PoolId indexed id, address indexed sender, uint256 amount0, uint256 amount1);

    /// @notice All interactions on the contract that account deltas require unlocking. A caller that calls `unlock` must implement
    /// `IUnlockCallback(msg.sender).unlockCallback(data)`, where they interact with the remaining functions on this contract.
    /// @dev The only functions callable without an unlocking are `initialize` and `updateDynamicLPFee`
    /// @param data Any data to pass to the callback, via `IUnlockCallback(msg.sender).unlockCallback(data)`
    /// @return The data returned by the call to `IUnlockCallback(msg.sender).unlockCallback(data)`
    function unlock(bytes calldata data) external returns (bytes memory);

    /// @notice Initialize the state for a given pool ID
    /// @dev A swap fee totaling MAX_SWAP_FEE (100%) makes exact output swaps impossible since the input is entirely consumed by the fee
    /// @param key The pool key for the pool to initialize
    /// @param sqrtPriceX96 The initial square root price
    /// @return tick The initial tick of the pool
    function initialize(PoolKey memory key, uint160 sqrtPriceX96) external returns (int24 tick);

    struct ModifyLiquidityParams {
        // the lower and upper tick of the position
        int24 tickLower;
        int24 tickUpper;
        // how to modify the liquidity
        int256 liquidityDelta;
        // a value to set if you want unique liquidity positions at the same range
        bytes32 salt;
    }

    /// @notice Modify the liquidity for the given pool
    /// @dev Poke by calling with a zero liquidityDelta
    /// @param key The pool to modify liquidity in
    /// @param params The parameters for modifying the liquidity
    /// @param hookData The data to pass through to the add/removeLiquidity hooks
    /// @return callerDelta The balance delta of the caller of modifyLiquidity. This is the total of both principal, fee deltas, and hook deltas if applicable
    /// @return feesAccrued The balance delta of the fees generated in the liquidity range. Returned for informational purposes
    function modifyLiquidity(PoolKey memory key, ModifyLiquidityParams memory params, bytes calldata hookData)
        external
        returns (BalanceDelta callerDelta, BalanceDelta feesAccrued);

    struct SwapParams {
        /// Whether to swap token0 for token1 or vice versa
        bool zeroForOne;
        /// The desired input amount if negative (exactIn), or the desired output amount if positive (exactOut)
        int256 amountSpecified;
        /// The sqrt price at which, if reached, the swap will stop executing
        uint160 sqrtPriceLimitX96;
    }

    /// @notice Swap against the given pool
    /// @param key The pool to swap in
    /// @param params The parameters for swapping
    /// @param hookData The data to pass through to the swap hooks
    /// @return swapDelta The balance delta of the address swapping
    /// @dev Swapping on low liquidity pools may cause unexpected swap amounts when liquidity available is less than amountSpecified.
    /// Additionally note that if interacting with hooks that have the BEFORE_SWAP_RETURNS_DELTA_FLAG or AFTER_SWAP_RETURNS_DELTA_FLAG
    /// the hook may alter the swap input/output. Integrators should perform checks on the returned swapDelta.
    function swap(PoolKey memory key, SwapParams memory params, bytes calldata hookData)
        external
        returns (BalanceDelta swapDelta);

    /// @notice Donate the given currency amounts to the in-range liquidity providers of a pool
    /// @dev Calls to donate can be frontrun adding just-in-time liquidity, with the aim of receiving a portion donated funds.
    /// Donors should keep this in mind when designing donation mechanisms.
    /// @dev This function donates to in-range LPs at slot0.tick. In certain edge-cases of the swap algorithm, the `sqrtPrice` of
    /// a pool can be at the lower boundary of tick `n`, but the `slot0.tick` of the pool is already `n - 1`. In this case a call to
    /// `donate` would donate to tick `n - 1` (slot0.tick) not tick `n` (getTickAtSqrtPrice(slot0.sqrtPriceX96)).
    /// Read the comments in `Pool.swap()` for more information about this.
    /// @param key The key of the pool to donate to
    /// @param amount0 The amount of currency0 to donate
    /// @param amount1 The amount of currency1 to donate
    /// @param hookData The data to pass through to the donate hooks
    /// @return BalanceDelta The delta of the caller after the donate
    function donate(PoolKey memory key, uint256 amount0, uint256 amount1, bytes calldata hookData)
        external
        returns (BalanceDelta);

    /// @notice Writes the current ERC20 balance of the specified currency to transient storage
    /// This is used to checkpoint balances for the manager and derive deltas for the caller.
    /// @dev This MUST be called before any ERC20 tokens are sent into the contract, but can be skipped
    /// for native tokens because the amount to settle is determined by the sent value.
    /// However, if an ERC20 token has been synced and not settled, and the caller instead wants to settle
    /// native funds, this function can be called with the native currency to then be able to settle the native currency
    function sync(Currency currency) external;

    /// @notice Called by the user to net out some value owed to the user
    /// @dev Will revert if the requested amount is not available, consider using `mint` instead
    /// @dev Can also be used as a mechanism for free flash loans
    /// @param currency The currency to withdraw from the pool manager
    /// @param to The address to withdraw to
    /// @param amount The amount of currency to withdraw
    function take(Currency currency, address to, uint256 amount) external;

    /// @notice Called by the user to pay what is owed
    /// @return paid The amount of currency settled
    function settle() external payable returns (uint256 paid);

    /// @notice Called by the user to pay on behalf of another address
    /// @param recipient The address to credit for the payment
    /// @return paid The amount of currency settled
    function settleFor(address recipient) external payable returns (uint256 paid);

    /// @notice WARNING - Any currency that is cleared, will be non-retrievable, and locked in the contract permanently.
    /// A call to clear will zero out a positive balance WITHOUT a corresponding transfer.
    /// @dev This could be used to clear a balance that is considered dust.
    /// Additionally, the amount must be the exact positive balance. This is to enforce that the caller is aware of the amount being cleared.
    function clear(Currency currency, uint256 amount) external;

    /// @notice Called by the user to move value into ERC6909 balance
    /// @param to The address to mint the tokens to
    /// @param id The currency address to mint to ERC6909s, as a uint256
    /// @param amount The amount of currency to mint
    /// @dev The id is converted to a uint160 to correspond to a currency address
    /// If the upper 12 bytes are not 0, they will be 0-ed out
    function mint(address to, uint256 id, uint256 amount) external;

    /// @notice Called by the user to move value from ERC6909 balance
    /// @param from The address to burn the tokens from
    /// @param id The currency address to burn from ERC6909s, as a uint256
    /// @param amount The amount of currency to burn
    /// @dev The id is converted to a uint160 to correspond to a currency address
    /// If the upper 12 bytes are not 0, they will be 0-ed out
    function burn(address from, uint256 id, uint256 amount) external;

    /// @notice Updates the pools lp fees for the a pool that has enabled dynamic lp fees.
    /// @dev A swap fee totaling MAX_SWAP_FEE (100%) makes exact output swaps impossible since the input is entirely consumed by the fee
    /// @param key The key of the pool to update dynamic LP fees for
    /// @param newDynamicLPFee The new dynamic pool LP fee
    function updateDynamicLPFee(PoolKey memory key, uint24 newDynamicLPFee) external;
}

// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity >=0.5.0;

/// @title Interface defining the oracle contract used by Panoptic, which may be a Uniswap V3 pool or custom implementation
/// @author Axicon Labs Inc, credit to Uniswap Labs [https://github.com/Uniswap/v3-core](https://github.com/Uniswap/v3-core) under GPL-2.0 license
/// @notice This interface defines the set of functions called by Panoptic on its external oracle contract.
/// @dev The interface is compatible with Uniswap V3 pools, but can also be implemented by a custom oracle contract.
interface IV3CompatibleOracle {
    /// @notice The 0th storage slot in the oracle stores many values, and is exposed as a single method to save gas
    /// when accessed externally.
    /// @return sqrtPriceX96 The current price of the oracle as a sqrt(currency1/currency0) Q64.96 value
    /// @return tick The current tick of the oracle, i.e. according to the last tick transition that was run.
    /// This value may not always be equal to SqrtTickMath.getTickAtSqrtRatio(sqrtPriceX96) if the price is on a tick
    /// boundary
    /// @return observationIndex The index of the last oracle observation that was written
    /// @return observationCardinality The current maximum number of observations stored in the oracle
    function slot0()
        external
        view
        returns (
            uint160 sqrtPriceX96,
            int24 tick,
            uint16 observationIndex,
            uint16 observationCardinality,
            uint16,
            uint8,
            bool
        );

    /// @notice Returns data about a specific observation index.
    /// @param index The element of the observations array to fetch
    /// @return blockTimestamp The timestamp of the observation
    /// @return tickCumulative The tick multiplied by seconds elapsed for the life of the pool as of the observation timestamp
    /// @return secondsPerLiquidityCumulativeX128 The seconds per in range liquidity for the life of the pool as of the observation timestamp
    /// @return initialized Whether the observation has been initialized and the values are safe to use
    function observations(
        uint256 index
    )
        external
        view
        returns (
            uint32 blockTimestamp,
            int56 tickCumulative,
            uint160 secondsPerLiquidityCumulativeX128,
            bool initialized
        );

    /// @notice Returns the cumulative tick and liquidity as of each timestamp `secondsAgo` from the current block timestamp.
    /// @dev To get a time weighted average tick or liquidity-in-range, you must call this with two values, one representing
    /// the beginning of the period and another for the end of the period. E.g., to get the last hour time-weighted average tick,
    /// you must call it with secondsAgos = [3600, 0].
    /// @dev The time weighted average tick represents the geometric time weighted average price of the pool, in
    /// log base sqrt(1.0001) of currency1 / currency0. The TickMath library can be used to go from a tick value to a ratio.
    /// @param secondsAgos From how long ago each cumulative tick and liquidity value should be returned
    /// @return tickCumulatives Cumulative tick values as of each `secondsAgos` from the current block timestamp
    /// @return secondsPerLiquidityCumulativeX128s Cumulative seconds per liquidity-in-range value as of each `secondsAgos` from the current block
    /// timestamp
    function observe(
        uint32[] calldata secondsAgos
    )
        external
        view
        returns (
            int56[] memory tickCumulatives,
            uint160[] memory secondsPerLiquidityCumulativeX128s
        );

    /// @notice Increase the maximum number of price and liquidity observations that this oracle will store.
    /// @param observationCardinalityNext The desired minimum number of observations for the oracle to store
    function increaseObservationCardinalityNext(uint16 observationCardinalityNext) external;
}

// SPDX-License-Identifier: BSD
pragma solidity ^0.8.4;

/// @title Clone
/// @author zefram.eth
/// @notice Provides helper functions for reading immutable args from calldata
contract Clone {
    /// @notice Reads an immutable arg with type address
    /// @param argOffset The offset of the arg in the packed data
    /// @return arg The arg value
    function _getArgAddress(
        uint256 argOffset
    ) internal pure returns (address arg) {
        uint256 offset = _getImmutableArgsOffset();
        // solhint-disable-next-line no-inline-assembly
        assembly {
            arg := shr(0x60, calldataload(add(offset, argOffset)))
        }
    }

    /// @notice Reads an immutable arg with type uint256
    /// @param argOffset The offset of the arg in the packed data
    /// @return arg The arg value
    function _getArgUint256(
        uint256 argOffset
    ) internal pure returns (uint256 arg) {
        uint256 offset = _getImmutableArgsOffset();
        // solhint-disable-next-line no-inline-assembly
        assembly {
            arg := calldataload(add(offset, argOffset))
        }
    }

    /// @notice Reads a uint256 array stored in the immutable args.
    /// @param argOffset The offset of the arg in the packed data
    /// @param arrLen Number of elements in the array
    /// @return arr The array
    function _getArgUint256Array(
        uint256 argOffset,
        uint64 arrLen
    ) internal pure returns (uint256[] memory arr) {
        uint256 offset = _getImmutableArgsOffset();
        uint256 el;
        arr = new uint256[](arrLen);
        for (uint64 i = 0; i < arrLen; i++) {
            // solhint-disable-next-line no-inline-assembly
            assembly {
                el := calldataload(add(add(offset, argOffset), mul(i, 32)))
            }
            arr[i] = el;
        }
        return arr;
    }

    /// @notice Reads an immutable arg with type uint64
    /// @param argOffset The offset of the arg in the packed data
    /// @return arg The arg value
    function _getArgUint64(
        uint256 argOffset
    ) internal pure returns (uint64 arg) {
        uint256 offset = _getImmutableArgsOffset();
        // solhint-disable-next-line no-inline-assembly
        assembly {
            arg := shr(0xc0, calldataload(add(offset, argOffset)))
        }
    }

    /// @notice Reads an immutable arg with type uint8
    /// @param argOffset The offset of the arg in the packed data
    /// @return arg The arg value
    function _getArgUint8(uint256 argOffset) internal pure returns (uint8 arg) {
        uint256 offset = _getImmutableArgsOffset();
        // solhint-disable-next-line no-inline-assembly
        assembly {
            arg := shr(0xf8, calldataload(add(offset, argOffset)))
        }
    }

    /// @return offset The offset of the packed immutable args in calldata
    function _getImmutableArgsOffset() internal pure returns (uint256 offset) {
        // solhint-disable-next-line no-inline-assembly
        assembly {
            offset := sub(
                calldatasize(),
                add(shr(240, calldataload(sub(calldatasize(), 2))), 2)
            )
        }
    }
}

File 7 of 50 : Multicall.sol
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.24;

/// @title Multicall
/// @notice Enables calling multiple methods in a single call to the contract.
/// @dev Helpful for performing batch operations such as an "emergency exit", or simply creating advanced positions.
/// @author Axicon Labs Limited
abstract contract Multicall {
    /// @notice Performs multiple calls on the inheritor in a single transaction, and returns the data from each call.
    /// @param data The calldata for each call
    /// @return results The data returned by each call
    function multicall(bytes[] calldata data) public returns (bytes[] memory results) {
        results = new bytes[](data.length);
        for (uint256 i = 0; i < data.length; ) {
            (bool success, bytes memory result) = address(this).delegatecall(data[i]);

            if (!success) {
                // Bubble up the revert reason
                // The bytes type is ABI encoded as a length-prefixed byte array
                // So we simply need to add 32 to the pointer to get the start of the data
                // And then revert with the size loaded from the first 32 bytes
                // Other solutions will do work to differentiate the revert reasons and provide parenthetical information
                // However, we have chosen to simply replicate the the normal behavior of the call
                // NOTE: memory-safe because it reads from memory already allocated by solidity (the bytes memory result)
                assembly ("memory-safe") {
                    revert(add(result, 32), mload(result))
                }
            }

            results[i] = result;

            unchecked {
                ++i;
            }
        }
    }
}

File 8 of 50 : Constants.sol
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.24;

/// @title Library of Constants used in Panoptic.
/// @author Axicon Labs Limited
/// @notice This library provides constants used in Panoptic.
library Constants {
    /// @notice Fixed point multiplier: 2**96
    uint256 internal constant FP96 = 0x1000000000000000000000000;

    /// @notice Minimum possible price tick in a Uniswap V4 pool
    int24 internal constant MIN_V4POOL_TICK = -887272;

    /// @notice Maximum possible price tick in a Uniswap V4 pool
    int24 internal constant MAX_V4POOL_TICK = 887272;

    /// @notice Minimum possible sqrtPriceX96 in a Uniswap V4 pool
    uint160 internal constant MIN_V4POOL_SQRT_RATIO = 4295128739;

    /// @notice Maximum possible sqrtPriceX96 in a Uniswap V4 pool
    uint160 internal constant MAX_V4POOL_SQRT_RATIO =
        1461446703485210103287273052203988822378723970342;

    /// @notice Parameter that determines which oracle type to use for the "slow" oracle price on non-liquidation solvency checks.
    /// @dev If false, an 8-slot internal median array is used to compute the "slow" oracle price.
    /// @dev This oracle is updated with the last oracle observation during `mintOptions` if MEDIAN_PERIOD has elapsed past the last observation.
    /// @dev If true, the "slow" oracle price is instead computed on-the-fly from 9 oracle observations (spaced 5 observations apart) irrespective of the frequency of `mintOptions` calls.
    bool internal constant SLOW_ORACLE_UNISWAP_MODE = false;

    /// @notice The minimum amount of time, in seconds, permitted between internal TWAP updates.
    uint256 internal constant MEDIAN_PERIOD = 60;

    /// @notice Amount of oracle observations to include in the "fast" oracle price.
    uint256 internal constant FAST_ORACLE_CARDINALITY = 3;

    /// @dev Amount of observation indices to skip in between each observation for the "fast" oracle price.
    /// @dev Note that the *minimum* total observation time is determined by the blocktime and may need to be adjusted by chain.
    /// @dev oracle observations snapshot the last block's closing price at the first interaction with the pool in a block.
    /// @dev In this case, if there is an interaction every block, the "fast" oracle can consider 3 consecutive block end prices (min=36 seconds on Ethereum).
    uint256 internal constant FAST_ORACLE_PERIOD = 1;

    /// @notice Amount of oracle observations to include in the "slow" oracle price (in Uniswap mode).
    uint256 internal constant SLOW_ORACLE_CARDINALITY = 9;

    /// @notice Amount of observation indices to skip in between each observation for the "slow" oracle price.
    /// @dev Structured such that the minimum total observation time is 9 minutes on Ethereum.
    uint256 internal constant SLOW_ORACLE_PERIOD = 5;
}

File 9 of 50 : Errors.sol
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.24;

/// @title Custom Errors library.
/// @author Axicon Labs Limited
/// @notice Contains all custom error messages used in Panoptic.
library Errors {
    /// @notice PanopticPool: The account is not solvent enough to perform the desired action
    error AccountInsolvent();

    /// @notice Casting error
    /// @dev e.g. uint128(uint256(a)) fails
    error CastingError();

    /// @notice CollateralTracker: Collateral token has already been initialized
    error CollateralTokenAlreadyInitialized();

    /// @notice CollateralTracker: The amount of shares (or assets) deposited is larger than the maximum permitted
    error DepositTooLarge();

    /// @notice PanopticPool: The effective liquidity (X32) is greater than min(`MAX_SPREAD`, `USER_PROVIDED_THRESHOLD`) during a long mint or short burn
    /// @dev Effective liquidity measures how much new liquidity is minted relative to how much is already in the pool
    error EffectiveLiquidityAboveThreshold();

    /// @notice CollateralTracker: Attempted to withdraw/redeem more than available liquidity, owned shares, or open positions would allow for
    error ExceedsMaximumRedemption();

    /// @notice PanopticPool: The provided list of option positions is incorrect or invalid
    error InputListFail();

    /// @notice Tick is not between `MIN_TICK` and `MAX_TICK`
    error InvalidTick();

    /// @notice The TokenId provided by the user is malformed or invalid
    /// @param parameterType poolId=0, ratio=1, tokenType=2, risk_partner=3, strike=4, width=5, two identical strike/width/tokenType chunks=6
    error InvalidTokenIdParameter(uint256 parameterType);

    /// @notice An unlock callback was attempted from an address other than the canonical Uniswap V4 pool manager
    error UnauthorizedUniswapCallback();

    /// @notice PanopticPool: None of the legs in a position are force-exercisable (they are all either short or ATM long)
    error NoLegsExercisable();

    /// @notice PanopticPool: The leg is not long, so premium cannot be settled through `settleLongPremium`
    error NotALongLeg();

    /// @notice PanopticPool: There is not enough available liquidity in the chunk for one of the long legs to be created (or for one of the short legs to be closed)
    error NotEnoughLiquidity();

    /// @notice PanopticPool: Position is still solvent and cannot be liquidated
    error NotMarginCalled();

    /// @notice CollateralTracker: The caller for a permissioned function is not the Panoptic Pool
    error NotPanopticPool();

    /// @notice Uniswap pool has already been initialized in the SFPM or created in the factory
    error PoolAlreadyInitialized();

    /// @notice SemiFungiblePositionManager: Tick range cannot be expanded on an an uninitialized pool
    error PoolNotInitialized();

    /// @notice PanopticPool: A position with the given token ID has already been minted by the caller and is still open
    error PositionAlreadyMinted();

    /// @notice CollateralTracker: The user has open/active option positions, so they cannot transfer collateral shares
    error PositionCountNotZero();

    /// @notice SFPM: The maximum token deltas (excluding swaps) for a position exceed (2^127 - 5) at some valid price
    error PositionTooLarge();

    /// @notice The current tick in the pool (post-ITM-swap) has fallen outside a user-defined open interval slippage range
    error PriceBoundFail();

    /// @notice An oracle price is too far away from another oracle price or the current tick
    /// @dev This is a safeguard against price manipulation during option mints, burns, liquidations, force exercises, and premium settlements
    error StaleOracle();

    /// @notice PanopticPool: The position being minted would increase the total amount of legs open for the account above the maximum
    error TooManyLegsOpen();

    /// @notice ERC20 or SFPM (ERC1155) token transfer did not complete successfully
    error TransferFailed();

    /// @notice The tick range given by the strike price and width is invalid
    /// because the upper and lower ticks are not initializable multiples of `tickSpacing`
    /// or one of the ticks exceeds the `MIN_TICK` or `MAX_TICK` bounds
    error InvalidTickBound();

    /// @notice An operation in a library has failed due to an underflow or overflow
    error UnderOverFlow();

    /// @notice The Uniswap Pool has not been created, so it cannot be used in the SFPM or have a PanopticPool created for it by the factory
    error UniswapPoolNotInitialized();

    /// @notice SFPM: Mints/burns of zero-liquidity chunks in Uniswap are not supported
    error ZeroLiquidity();
}

File 10 of 50 : Math.sol
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.24;

// Libraries
import {Errors} from "@libraries/Errors.sol";
import {Constants} from "@libraries/Constants.sol";
import {FixedPointMathLib} from "solady/utils/FixedPointMathLib.sol";
// Custom types
import {LiquidityChunk, LiquidityChunkLibrary} from "@types/LiquidityChunk.sol";

/// @title Core math library.
/// @author Axicon Labs Limited
/// @notice Contains general math helpers and functions
library Math {
    /// @notice This is equivalent to `type(uint256).max` — used in assembly blocks as a replacement.
    uint256 internal constant MAX_UINT256 = 2 ** 256 - 1;

    /// @notice This is equivalent to `type(uint128).max` — used in assembly blocks as a replacement.
    uint256 internal constant MAX_UINT128 = 2 ** 128 - 1;

    /*//////////////////////////////////////////////////////////////
                          GENERAL MATH HELPERS
    //////////////////////////////////////////////////////////////*/

    /// @notice Compute the min of the incoming int24s `a` and `b`.
    /// @param a The first number
    /// @param b The second number
    /// @return The min of `a` and `b`: min(a, b), e.g.: min(4, 1) = 1
    function min24(int24 a, int24 b) internal pure returns (int24) {
        return a < b ? a : b;
    }

    /// @notice Compute the max of the incoming int24s `a` and `b`.
    /// @param a The first number
    /// @param b The second number
    /// @return The max of `a` and `b`: max(a, b), e.g.: max(4, 1) = 4
    function max24(int24 a, int24 b) internal pure returns (int24) {
        return a > b ? a : b;
    }

    /// @notice Compute the min of the incoming `a` and `b`.
    /// @param a The first number
    /// @param b The second number
    /// @return The min of `a` and `b`: min(a, b), e.g.: min(4, 1) = 1
    function min(uint256 a, uint256 b) internal pure returns (uint256) {
        return a < b ? a : b;
    }

    /// @notice Compute the min of the incoming `a` and `b`.
    /// @param a The first number
    /// @param b The second number
    /// @return The min of `a` and `b`: min(a, b), e.g.: min(4, 1) = 1
    function min(int256 a, int256 b) internal pure returns (int256) {
        return a < b ? a : b;
    }

    /// @notice Compute the max of the incoming `a` and `b`.
    /// @param a The first number
    /// @param b The second number
    /// @return The max of `a` and `b`: max(a, b), e.g.: max(4, 1) = 4
    function max(uint256 a, uint256 b) internal pure returns (uint256) {
        return a > b ? a : b;
    }

    /// @notice Compute the max of the incoming `a` and `b`.
    /// @param a The first number
    /// @param b The second number
    /// @return The max of `a` and `b`: max(a, b), e.g.: max(4, 1) = 4
    function max(int256 a, int256 b) internal pure returns (int256) {
        return a > b ? a : b;
    }

    /// @notice Compute the absolute value of an integer.
    /// @param x The incoming *signed* integer to take the absolute value of
    /// @dev Does not support `type(int256).min` and will revert (`type(int256).max = abs(type(int256).min) - 1`).
    /// @return The absolute value of `x`, e.g. abs(-4) = 4
    function abs(int256 x) internal pure returns (int256) {
        return x > 0 ? x : -x;
    }

    /// @notice Compute the absolute value of an integer.
    /// @param x The incoming *signed* integer to take the absolute value of
    /// @dev Supports `type(int256).min` because the corresponding value can fit in a uint (unlike `type(int256).max`).
    /// @return The absolute value of `x`, e.g. abs(-4) = 4
    function absUint(int256 x) internal pure returns (uint256) {
        unchecked {
            return x > 0 ? uint256(x) : uint256(-x);
        }
    }

    /// @notice Returns the index of the most significant nibble of the 160-bit number,
    /// where the least significant nibble is at index 0 and the most significant nibble is at index 39.
    /// @param x The value for which to compute the most significant nibble
    /// @return r The index of the most significant nibble (default: 0)
    function mostSignificantNibble(uint160 x) internal pure returns (uint256 r) {
        unchecked {
            if (x >= 0x100000000000000000000000000000000) {
                x >>= 128;
                r += 32;
            }
            if (x >= 0x10000000000000000) {
                x >>= 64;
                r += 16;
            }
            if (x >= 0x100000000) {
                x >>= 32;
                r += 8;
            }
            if (x >= 0x10000) {
                x >>= 16;
                r += 4;
            }
            if (x >= 0x100) {
                x >>= 8;
                r += 2;
            }
            if (x >= 0x10) {
                r += 1;
            }
        }
    }

    /*//////////////////////////////////////////////////////////////
                               TICK MATH
    //////////////////////////////////////////////////////////////*/

    /// @notice Computes a tick that will require approximately `amount` of currency0 to create a `tickSpacing`-wide position with `maxLiquidityPerTick` at `tickUpper = tick` in Uniswap.
    /// @dev This function can have a maximum of two ticks of error from one of the ticks with `amount(tickRes + 2) < amount < amount(tickRes - 2)`.
    /// @dev `tickSpacing` is assumed to be within the range (0, 32768)
    /// @dev `maxLiquidityPerTick` for `s=tickSpacing` should be defined by `(2^128 - 1) / ((887272/s) - (-887272/s) + 1)`
    /// @param amount The desired amount of currency0 required to fill the returned tick
    /// @param tickSpacing The spacing between initializable ticks in the Uniswap pool
    /// @param maxLiquidityPerTick The maximum liquidity that can reference any given tick in the Uniswap pool
    /// @return A tick that will require approximately `amount` of currency0 to create a `tickSpacing`-wide position with `maxLiquidityPerTick` at `tickUpper = tick`
    function getApproxTickWithMaxAmount(
        uint256 amount,
        int24 tickSpacing,
        uint256 maxLiquidityPerTick
    ) internal pure returns (int24) {
        unchecked {
            // abs(max_error) ≈ 2^-13 * log₂(√1.0001)⁻¹ ≈ -1.70234
            return
                int24(
                    int256(
                        Math.log_Sqrt1p0001MantissaRect(
                            Math.mulDivCapped(
                                amount,
                                2 ** 224,
                                (maxLiquidityPerTick *
                                    (Math.getSqrtRatioAtTick(tickSpacing) - 2 ** 96)),
                                128
                            ),
                            13
                        )
                    )
                );
        }
    }

    /// @notice Computes the maximum liquidity that is allowed to reference any given tick in a Uniswap V4 pool with `tickSpacing`.
    /// @param tickSpacing The spacing between initializable ticks in the Uniswap V4 pool
    /// @return maxLiquidityPerTick The maximum liquidity that can reference any given tick in the Uniswap V4 pool
    function getMaxLiquidityPerTick(
        int24 tickSpacing
    ) internal pure returns (uint128 maxLiquidityPerTick) {
        int24 MAX_TICK = Constants.MAX_V4POOL_TICK;
        assembly {
            // Uniswap V4 adds an unnecessary round toward negative infinity to match tick compression behavior
            // Equivalent to type(uint128).max/(floor(MAX_TICK/tickSpacing) - floor(MIN_TICK/tickSpacing) + 1)
            maxLiquidityPerTick := div(
                MAX_UINT128,
                add(add(mul(div(MAX_TICK, tickSpacing), 2), gt(mod(MAX_TICK, tickSpacing), 0)), 1)
            )
        }
    }

    /// @notice Calculates `1.0001^(tick/2)` as an X96 number.
    /// @dev Will revert if `abs(tick) > 887272`.
    /// @param tick Value of the tick for which `sqrt(1.0001^tick)` is calculated
    /// @return A Q64.96 number representing the sqrt price at the provided tick
    function getSqrtRatioAtTick(int24 tick) internal pure returns (uint160) {
        unchecked {
            uint256 absTick = tick < 0 ? uint256(-int256(tick)) : uint256(int256(tick));
            if (absTick > uint256(int256(Constants.MAX_V4POOL_TICK))) revert Errors.InvalidTick();

            // sqrt(1.0001^(-absTick)) = ∏ sqrt(1.0001^(-bit_i))
            // ex: absTick = 100 = binary 1100100, so sqrt(1.0001^-100) = sqrt(1.0001^-64) * sqrt(1.0001^-32) * sqrt(1.0001^-4)
            // constants are 2^128/(sqrt(1.0001)^bit_i) rounded half-up

            // if the first bit is 0, initialize sqrtR to 1 (2^128)
            uint256 sqrtR = absTick & 0x1 != 0
                ? 0xfffcb933bd6fad37aa2d162d1a594001
                : 0x100000000000000000000000000000000;

            if (absTick & 0x2 != 0) sqrtR = (sqrtR * 0xfff97272373d413259a46990580e213a) >> 128;

            if (absTick & 0x4 != 0) sqrtR = (sqrtR * 0xfff2e50f5f656932ef12357cf3c7fdcc) >> 128;

            if (absTick & 0x8 != 0) sqrtR = (sqrtR * 0xffe5caca7e10e4e61c3624eaa0941cd0) >> 128;

            if (absTick & 0x10 != 0) sqrtR = (sqrtR * 0xffcb9843d60f6159c9db58835c926644) >> 128;

            if (absTick & 0x20 != 0) sqrtR = (sqrtR * 0xff973b41fa98c081472e6896dfb254c0) >> 128;

            if (absTick & 0x40 != 0) sqrtR = (sqrtR * 0xff2ea16466c96a3843ec78b326b52861) >> 128;

            if (absTick & 0x80 != 0) sqrtR = (sqrtR * 0xfe5dee046a99a2a811c461f1969c3053) >> 128;

            if (absTick & 0x100 != 0) sqrtR = (sqrtR * 0xfcbe86c7900a88aedcffc83b479aa3a4) >> 128;

            if (absTick & 0x200 != 0) sqrtR = (sqrtR * 0xf987a7253ac413176f2b074cf7815e54) >> 128;

            if (absTick & 0x400 != 0) sqrtR = (sqrtR * 0xf3392b0822b70005940c7a398e4b70f3) >> 128;

            if (absTick & 0x800 != 0) sqrtR = (sqrtR * 0xe7159475a2c29b7443b29c7fa6e889d9) >> 128;

            if (absTick & 0x1000 != 0) sqrtR = (sqrtR * 0xd097f3bdfd2022b8845ad8f792aa5825) >> 128;

            if (absTick & 0x2000 != 0) sqrtR = (sqrtR * 0xa9f746462d870fdf8a65dc1f90e061e5) >> 128;

            if (absTick & 0x4000 != 0) sqrtR = (sqrtR * 0x70d869a156d2a1b890bb3df62baf32f7) >> 128;

            if (absTick & 0x8000 != 0) sqrtR = (sqrtR * 0x31be135f97d08fd981231505542fcfa6) >> 128;

            if (absTick & 0x10000 != 0) sqrtR = (sqrtR * 0x9aa508b5b7a84e1c677de54f3e99bc9) >> 128;

            if (absTick & 0x20000 != 0) sqrtR = (sqrtR * 0x5d6af8dedb81196699c329225ee604) >> 128;

            if (absTick & 0x40000 != 0) sqrtR = (sqrtR * 0x2216e584f5fa1ea926041bedfe98) >> 128;

            if (absTick & 0x80000 != 0) sqrtR = (sqrtR * 0x48a170391f7dc42444e8fa2) >> 128;

            // 2^128 * sqrt(1.0001^x) = 2^128 / sqrt(1.0001^-x)
            if (tick > 0) sqrtR = type(uint256).max / sqrtR;

            // Downcast + rounding up to keep is consistent with Uniswap's
            return uint160((sqrtR >> 32) + (sqrtR % (1 << 32) == 0 ? 0 : 1));
        }
    }

    /// @notice Approximates the absolute value of log base `sqrt(1.0001)` for a number in (0, 1) (`argX128/2^128`) with `precision` bits of precision.
    /// @param argX128 The Q128.128 fixed-point number in the range (0, 1) to calculate the log of
    /// @param precision The bits of precision with which to compute the result, max 63 (`err <≈ 2^-precision * log₂(√1.0001)⁻¹`)
    /// @return The absolute value of log with base `sqrt(1.0001)` for `argX128/2^128`
    function log_Sqrt1p0001MantissaRect(
        uint256 argX128,
        uint256 precision
    ) internal pure returns (uint256) {
        unchecked {
            // =[log₂(x)] =MSB(x)
            uint256 log2_res = FixedPointMathLib.log2(argX128);

            // Normalize argX128 to [1, 2)
            // x_normal = x / 2^[log₂(x)]
            // = 1.a₁a₂a₃... = 2^(0.b₁b₂b₃...)
            // log₂(x_normal) = log₂(x / 2^⌊log₂(x)⌋)
            // log₂(x_normal) = log₂(x) - log₂(2^⌊log₂(x)⌋)
            // log₂(x_normal) = log₂(x) - ⌊log₂(x)⌋
            // log₂(x) = log₂(x_normal) + ⌊log₂(x)⌋
            argX128 <<= (127 - log2_res);

            // =[log₂(x)] * 2^64
            log2_res = (128 - log2_res) << 64;

            // log₂(x_normal) = 0.b₁b₂b₃...
            // x_normal = (1.a₁a₂a₃...) = 2^(0.b₁b₂b₃...)
            // x_normal² = (1.a₁a₂a₃...)² = (2^(0.b₁b₂b₃...))²
            // = 2^(0.b₁b₂b₃... * 2)
            // = 2^(b₁ + 0.b₂b₃...)
            // if bᵢ = 1, renormalize x_normal² to [1, 2):
            // 2^(b₁ + 0.b₂b₃...) / 2^b₁ = 2^((b₁ - 1).b₂b₃...)
            // = 2^(0.b₂b₃...)
            // error = [0, 2⁻ⁿ)
            uint256 iterBound = 63 - precision;
            for (uint256 i = 63; i > iterBound; i--) {
                argX128 = (argX128 ** 2) >> 127;
                uint256 bit = argX128 >> 128;
                log2_res -= bit << i;
                argX128 >>= bit;
            }

            // log₍√₁.₀₀₀₁₎(x) = log₂(x) / log₂(√1.0001)
            // 2^64 / log₂(√1.0001) ≈ 255738959000112593413423
            return (log2_res * 255738959000112593413423) / 2 ** 128;
        }
    }

    /*//////////////////////////////////////////////////////////////
                           LIQUIDITY AMOUNTS
    //////////////////////////////////////////////////////////////*/

    /// @notice Calculates the amount of currency0 received for a given LiquidityChunk.
    /// @param liquidityChunk A specification for a liquidity chunk in Uniswap containing `liquidity`, `tickLower`, and `tickUpper`
    /// @return The amount of currency0 represented by `liquidityChunk` when `currentTick < tickLower`
    function getAmount0ForLiquidity(LiquidityChunk liquidityChunk) internal pure returns (uint256) {
        uint160 lowPriceX96 = getSqrtRatioAtTick(liquidityChunk.tickLower());
        uint160 highPriceX96 = getSqrtRatioAtTick(liquidityChunk.tickUpper());
        unchecked {
            return
                mulDiv(
                    uint256(liquidityChunk.liquidity()) << 96,
                    highPriceX96 - lowPriceX96,
                    highPriceX96
                ) / lowPriceX96;
        }
    }

    /// @notice Calculates the amount of currency1 received for a given LiquidityChunk.
    /// @param liquidityChunk A specification for a liquidity chunk in Uniswap containing `liquidity`, `tickLower`, and `tickUpper`
    /// @return The amount of currency1 represented by `liquidityChunk` when `currentTick > tickUpper`
    function getAmount1ForLiquidity(LiquidityChunk liquidityChunk) internal pure returns (uint256) {
        uint160 lowPriceX96 = getSqrtRatioAtTick(liquidityChunk.tickLower());
        uint160 highPriceX96 = getSqrtRatioAtTick(liquidityChunk.tickUpper());

        unchecked {
            return mulDiv96(liquidityChunk.liquidity(), highPriceX96 - lowPriceX96);
        }
    }

    /// @notice Calculates the amount of currency0 and currency1 received for a given LiquidityChunk at the provided `currentTick`.
    /// @param currentTick The tick at which to evaluate `liquidityChunk`
    /// @param liquidityChunk A specification for a liquidity chunk in Uniswap containing `liquidity`, `tickLower`, and `tickUpper`
    /// @return amount0 The amount of currency0 represented by `liquidityChunk` at `currentTick`
    /// @return amount1 The amount of currency1 represented by `liquidityChunk` at `currentTick`
    function getAmountsForLiquidity(
        int24 currentTick,
        LiquidityChunk liquidityChunk
    ) internal pure returns (uint256 amount0, uint256 amount1) {
        if (currentTick <= liquidityChunk.tickLower()) {
            amount0 = getAmount0ForLiquidity(liquidityChunk);
        } else if (currentTick >= liquidityChunk.tickUpper()) {
            amount1 = getAmount1ForLiquidity(liquidityChunk);
        } else {
            amount0 = getAmount0ForLiquidity(liquidityChunk.updateTickLower(currentTick));
            amount1 = getAmount1ForLiquidity(liquidityChunk.updateTickUpper(currentTick));
        }
    }

    /// @notice Returns a LiquidityChunk at the provided tick range with `liquidity` corresponding to `amount0`.
    /// @param tickLower The lower tick of the chunk
    /// @param tickUpper The upper tick of the chunk
    /// @param amount0 The amount of currency0
    /// @return A LiquidityChunk with `tickLower`, `tickUpper`, and the calculated amount of liquidity for `amount0`
    function getLiquidityForAmount0(
        int24 tickLower,
        int24 tickUpper,
        uint256 amount0
    ) internal pure returns (LiquidityChunk) {
        uint160 lowPriceX96 = getSqrtRatioAtTick(tickLower);
        uint160 highPriceX96 = getSqrtRatioAtTick(tickUpper);

        unchecked {
            return
                LiquidityChunkLibrary.createChunk(
                    tickLower,
                    tickUpper,
                    toUint128(
                        mulDiv(
                            amount0,
                            mulDiv96(highPriceX96, lowPriceX96),
                            highPriceX96 - lowPriceX96
                        )
                    )
                );
        }
    }

    /// @notice Returns a LiquidityChunk at the provided tick range with `liquidity` corresponding to `amount1`.
    /// @param tickLower The lower tick of the chunk
    /// @param tickUpper The upper tick of the chunk
    /// @param amount1 The amount of currency1
    /// @return A LiquidityChunk with `tickLower`, `tickUpper`, and the calculated amount of liquidity for `amount1`
    function getLiquidityForAmount1(
        int24 tickLower,
        int24 tickUpper,
        uint256 amount1
    ) internal pure returns (LiquidityChunk) {
        uint160 lowPriceX96 = getSqrtRatioAtTick(tickLower);
        uint160 highPriceX96 = getSqrtRatioAtTick(tickUpper);

        unchecked {
            return
                LiquidityChunkLibrary.createChunk(
                    tickLower,
                    tickUpper,
                    toUint128(mulDiv(amount1, Constants.FP96, highPriceX96 - lowPriceX96))
                );
        }
    }

    /*//////////////////////////////////////////////////////////////
                                CASTING
    //////////////////////////////////////////////////////////////*/

    /// @notice Downcast uint256 to uint128. Revert on overflow or underflow.
    /// @param toDowncast The uint256 to be downcasted
    /// @return downcastedInt `toDowncast` downcasted to uint128
    function toUint128(uint256 toDowncast) internal pure returns (uint128 downcastedInt) {
        if ((downcastedInt = uint128(toDowncast)) != toDowncast) revert Errors.CastingError();
    }

    /// @notice Downcast uint256 to uint128, but cap at type(uint128).max on overflow.
    /// @param toDowncast The uint256 to be downcasted
    /// @return downcastedInt `toDowncast` downcasted to uint128
    function toUint128Capped(uint256 toDowncast) internal pure returns (uint128 downcastedInt) {
        if ((downcastedInt = uint128(toDowncast)) != toDowncast) {
            downcastedInt = type(uint128).max;
        }
    }

    /// @notice Downcast uint128 to int128.
    /// @param toCast The uint256 to be downcasted
    /// @return downcastedInt `toDowncast` downcasted to int128
    function toInt128(uint128 toCast) internal pure returns (int128 downcastedInt) {
        if ((downcastedInt = int128(toCast)) < 0) revert Errors.CastingError();
    }

    /// @notice Cast an int256 to an int128, revert on overflow or underflow.
    /// @param toCast The int256 to be downcasted
    /// @return downcastedInt `toCast` downcasted to int128
    function toInt128(int256 toCast) internal pure returns (int128 downcastedInt) {
        if (!((downcastedInt = int128(toCast)) == toCast)) revert Errors.CastingError();
    }

    /// @notice Cast a uint256 to an int256, revert on overflow.
    /// @param toCast The uint256 to be downcasted
    /// @return `toCast` downcasted to int256
    function toInt256(uint256 toCast) internal pure returns (int256) {
        if (toCast > uint256(type(int256).max)) revert Errors.CastingError();
        return int256(toCast);
    }

    /*//////////////////////////////////////////////////////////////
                                 MULDIV
    //////////////////////////////////////////////////////////////*/

    /// @notice Calculates `floor(a×b÷denominator)` with full precision. Throws if result overflows a uint256 or `denominator == 0`.
    /// @param a The multiplicand
    /// @param b The multiplier
    /// @param denominator The divisor
    /// @return result The 256-bit result
    /// @dev Credit to Remco Bloemen under MIT license https://xn--2-umb.com/21/muldiv for this and all following `mulDiv` functions.
    function mulDiv(
        uint256 a,
        uint256 b,
        uint256 denominator
    ) internal pure returns (uint256 result) {
        unchecked {
            // 512-bit multiply [prod1 prod0] = a * b
            // Compute the product mod 2**256 and mod 2**256 - 1
            // then use the Chinese Remainder Theorem to reconstruct
            // the 512 bit result. The result is stored in two 256
            // variables such that product = prod1 * 2**256 + prod0
            uint256 prod0; // Least significant 256 bits of the product
            uint256 prod1; // Most significant 256 bits of the product
            assembly ("memory-safe") {
                let mm := mulmod(a, b, not(0))
                prod0 := mul(a, b)
                prod1 := sub(sub(mm, prod0), lt(mm, prod0))
            }

            // Handle non-overflow cases, 256 by 256 division
            if (prod1 == 0) {
                require(denominator > 0);
                assembly ("memory-safe") {
                    result := div(prod0, denominator)
                }
                return result;
            }

            // Make sure the result is less than 2**256.
            // Also prevents denominator == 0
            require(denominator > prod1);

            ///////////////////////////////////////////////
            // 512 by 256 division.
            ///////////////////////////////////////////////

            // Make division exact by subtracting the remainder from [prod1 prod0]
            // Compute remainder using mulmod
            uint256 remainder;
            assembly ("memory-safe") {
                remainder := mulmod(a, b, denominator)
            }
            // Subtract 256 bit number from 512 bit number
            assembly ("memory-safe") {
                prod1 := sub(prod1, gt(remainder, prod0))
                prod0 := sub(prod0, remainder)
            }

            // Factor powers of two out of denominator
            // Compute largest power of two divisor of denominator.
            // Always >= 1.
            uint256 twos = (0 - denominator) & denominator;
            // Divide denominator by power of two
            assembly ("memory-safe") {
                denominator := div(denominator, twos)
            }

            // Divide [prod1 prod0] by the factors of two
            assembly ("memory-safe") {
                prod0 := div(prod0, twos)
            }
            // Shift in bits from prod1 into prod0. For this we need
            // to flip `twos` such that it is 2**256 / twos.
            // If twos is zero, then it becomes one
            assembly ("memory-safe") {
                twos := add(div(sub(0, twos), twos), 1)
            }
            prod0 |= prod1 * twos;

            // Invert denominator mod 2**256
            // Now that denominator is an odd number, it has an inverse
            // modulo 2**256 such that denominator * inv = 1 mod 2**256.
            // Compute the inverse by starting with a seed that is correct
            // correct for four bits. That is, denominator * inv = 1 mod 2**4
            uint256 inv = (3 * denominator) ^ 2;
            // Now use Newton-Raphson iteration to improve the precision.
            // Thanks to Hensel's lifting lemma, this also works in modular
            // arithmetic, doubling the correct bits in each step.
            inv *= 2 - denominator * inv; // inverse mod 2**8
            inv *= 2 - denominator * inv; // inverse mod 2**16
            inv *= 2 - denominator * inv; // inverse mod 2**32
            inv *= 2 - denominator * inv; // inverse mod 2**64
            inv *= 2 - denominator * inv; // inverse mod 2**128
            inv *= 2 - denominator * inv; // inverse mod 2**256

            // Because the division is now exact we can divide by multiplying
            // with the modular inverse of denominator. This will give us the
            // correct result modulo 2**256. Since the preconditions guarantee
            // that the outcome is less than 2**256, this is the final result.
            // We don't need to compute the high bits of the result and prod1
            // is no longer required.
            result = prod0 * inv;
        }
    }

    /// @notice Calculates `min(floor(a×b÷denominator), 2^256-1)` with full precision.
    /// @param a The multiplicand
    /// @param b The multiplier
    /// @param denominator The divisor
    /// @return result The 256-bit result
    function mulDivCapped(
        uint256 a,
        uint256 b,
        uint256 denominator
    ) internal pure returns (uint256 result) {
        unchecked {
            // 512-bit multiply [prod1 prod0] = a * b
            // Compute the product mod 2**256 and mod 2**256 - 1
            // then use the Chinese Remainder Theorem to reconstruct
            // the 512 bit result. The result is stored in two 256
            // variables such that product = prod1 * 2**256 + prod0
            uint256 prod0; // Least significant 256 bits of the product
            uint256 prod1; // Most significant 256 bits of the product
            assembly ("memory-safe") {
                let mm := mulmod(a, b, not(0))
                prod0 := mul(a, b)
                prod1 := sub(sub(mm, prod0), lt(mm, prod0))
            }

            // Handle non-overflow cases, 256 by 256 division
            if (prod1 == 0) {
                require(denominator > 0);
                assembly ("memory-safe") {
                    result := div(prod0, denominator)
                }
                return result;
            }

            if (denominator <= prod1) return type(uint256).max;

            ///////////////////////////////////////////////
            // 512 by 256 division.
            ///////////////////////////////////////////////

            // Make division exact by subtracting the remainder from [prod1 prod0]
            // Compute remainder using mulmod
            uint256 remainder;
            assembly ("memory-safe") {
                remainder := mulmod(a, b, denominator)
            }
            // Subtract 256 bit number from 512 bit number
            assembly ("memory-safe") {
                prod1 := sub(prod1, gt(remainder, prod0))
                prod0 := sub(prod0, remainder)
            }

            // Factor powers of two out of denominator
            // Compute largest power of two divisor of denominator.
            // Always >= 1.
            uint256 twos = (0 - denominator) & denominator;
            // Divide denominator by power of two
            assembly ("memory-safe") {
                denominator := div(denominator, twos)
            }

            // Divide [prod1 prod0] by the factors of two
            assembly ("memory-safe") {
                prod0 := div(prod0, twos)
            }
            // Shift in bits from prod1 into prod0. For this we need
            // to flip `twos` such that it is 2**256 / twos.
            // If twos is zero, then it becomes one
            assembly ("memory-safe") {
                twos := add(div(sub(0, twos), twos), 1)
            }
            prod0 |= prod1 * twos;

            // Invert denominator mod 2**256
            // Now that denominator is an odd number, it has an inverse
            // modulo 2**256 such that denominator * inv = 1 mod 2**256.
            // Compute the inverse by starting with a seed that is correct
            // correct for four bits. That is, denominator * inv = 1 mod 2**4
            uint256 inv = (3 * denominator) ^ 2;
            // Now use Newton-Raphson iteration to improve the precision.
            // Thanks to Hensel's lifting lemma, this also works in modular
            // arithmetic, doubling the correct bits in each step.
            inv *= 2 - denominator * inv; // inverse mod 2**8
            inv *= 2 - denominator * inv; // inverse mod 2**16
            inv *= 2 - denominator * inv; // inverse mod 2**32
            inv *= 2 - denominator * inv; // inverse mod 2**64
            inv *= 2 - denominator * inv; // inverse mod 2**128
            inv *= 2 - denominator * inv; // inverse mod 2**256

            // Because the division is now exact we can divide by multiplying
            // with the modular inverse of denominator. This will give us the
            // correct result modulo 2**256. Since the preconditions guarantee
            // that the outcome is less than 2**256, this is the final result.
            // We don't need to compute the high bits of the result and prod1
            // is no longer required.
            result = prod0 * inv;
        }
    }

    /// @notice Calculates `min(floor(a×b÷denominator), 2^power-1)` with full precision.
    /// @param a The multiplicand
    /// @param b The multiplier
    /// @param denominator The divisor
    /// @param power The upper bound of the open interval representing the range of this function, given by `2^power`
    /// @return result The 256-bit result
    function mulDivCapped(
        uint256 a,
        uint256 b,
        uint256 denominator,
        uint256 power
    ) internal pure returns (uint256 result) {
        unchecked {
            // 512-bit multiply [prod1 prod0] = a * b
            // Compute the product mod 2**256 and mod 2**256 - 1
            // then use the Chinese Remainder Theorem to reconstruct
            // the 512 bit result. The result is stored in two 256
            // variables such that product = prod1 * 2**256 + prod0
            uint256 prod0; // Least significant 256 bits of the product
            uint256 prod1; // Most significant 256 bits of the product
            assembly ("memory-safe") {
                let mm := mulmod(a, b, not(0))
                prod0 := mul(a, b)
                prod1 := sub(sub(mm, prod0), lt(mm, prod0))
            }
            // Handle non-overflow cases, 256 by 256 division
            if (prod1 == 0) {
                require(denominator > 0);
                assembly ("memory-safe") {
                    result := div(prod0, denominator)
                }
                return Math.min(result, 2 ** power - 1);
            }

            if (denominator >> (256 - power) <= prod1) return 2 ** power - 1;

            ///////////////////////////////////////////////
            // 512 by 256 division.
            ///////////////////////////////////////////////

            // Make division exact by subtracting the remainder from [prod1 prod0]
            // Compute remainder using mulmod
            uint256 remainder;
            assembly ("memory-safe") {
                remainder := mulmod(a, b, denominator)
            }
            // Subtract 256 bit number from 512 bit number
            assembly ("memory-safe") {
                prod1 := sub(prod1, gt(remainder, prod0))
                prod0 := sub(prod0, remainder)
            }

            // Factor powers of two out of denominator
            // Compute largest power of two divisor of denominator.
            // Always >= 1.
            uint256 twos = (0 - denominator) & denominator;
            // Divide denominator by power of two
            assembly ("memory-safe") {
                denominator := div(denominator, twos)
            }

            // Divide [prod1 prod0] by the factors of two
            assembly ("memory-safe") {
                prod0 := div(prod0, twos)
            }
            // Shift in bits from prod1 into prod0. For this we need
            // to flip `twos` such that it is 2**256 / twos.
            // If twos is zero, then it becomes one
            assembly ("memory-safe") {
                twos := add(div(sub(0, twos), twos), 1)
            }
            prod0 |= prod1 * twos;

            // Invert denominator mod 2**256
            // Now that denominator is an odd number, it has an inverse
            // modulo 2**256 such that denominator * inv = 1 mod 2**256.
            // Compute the inverse by starting with a seed that is correct
            // correct for four bits. That is, denominator * inv = 1 mod 2**4
            uint256 inv = (3 * denominator) ^ 2;
            // Now use Newton-Raphson iteration to improve the precision.
            // Thanks to Hensel's lifting lemma, this also works in modular
            // arithmetic, doubling the correct bits in each step.
            inv *= 2 - denominator * inv; // inverse mod 2**8
            inv *= 2 - denominator * inv; // inverse mod 2**16
            inv *= 2 - denominator * inv; // inverse mod 2**32
            inv *= 2 - denominator * inv; // inverse mod 2**64
            inv *= 2 - denominator * inv; // inverse mod 2**128
            inv *= 2 - denominator * inv; // inverse mod 2**256

            // Because the division is now exact we can divide by multiplying
            // with the modular inverse of denominator. This will give us the
            // correct result modulo 2**256. Since the preconditions guarantee
            // that the outcome is less than 2**256, this is the final result.
            // We don't need to compute the high bits of the result and prod1
            // is no longer required.
            result = prod0 * inv;
        }
    }

    /// @notice Calculates `ceil(a×b÷denominator)` with full precision. Throws if result overflows a uint256 or `denominator == 0`.
    /// @param a The multiplicand
    /// @param b The multiplier
    /// @param denominator The divisor
    /// @return result The 256-bit result
    function mulDivRoundingUp(
        uint256 a,
        uint256 b,
        uint256 denominator
    ) internal pure returns (uint256 result) {
        unchecked {
            result = mulDiv(a, b, denominator);
            if (mulmod(a, b, denominator) > 0) {
                require(result < type(uint256).max);
                result++;
            }
        }
    }

    /// @notice Calculates `floor(a×b÷2^64)` with full precision. Throws if result overflows a uint256.
    /// @param a The multiplicand
    /// @param b The multiplier
    /// @return The 256-bit result
    function mulDiv64(uint256 a, uint256 b) internal pure returns (uint256) {
        unchecked {
            // 512-bit multiply [prod1 prod0] = a * b
            // Compute the product mod 2**256 and mod 2**256 - 1
            // then use the Chinese Remainder Theorem to reconstruct
            // the 512 bit result. The result is stored in two 256
            // variables such that product = prod1 * 2**256 + prod0
            uint256 prod0; // Least significant 256 bits of the product
            uint256 prod1; // Most significant 256 bits of the product
            assembly ("memory-safe") {
                let mm := mulmod(a, b, not(0))
                prod0 := mul(a, b)
                prod1 := sub(sub(mm, prod0), lt(mm, prod0))
            }

            // Handle non-overflow cases, 256 by 256 division
            if (prod1 == 0) {
                uint256 res;
                assembly ("memory-safe") {
                    // Right shift by n is equivalent and 2 gas cheaper than division by 2^n
                    res := shr(64, prod0)
                }
                return res;
            }

            // Make sure the result is less than 2**256.
            require(2 ** 64 > prod1);

            ///////////////////////////////////////////////
            // 512 by 256 division.
            ///////////////////////////////////////////////

            // Make division exact by subtracting the remainder from [prod1 prod0]
            // Compute remainder using mulmod
            uint256 remainder;
            assembly ("memory-safe") {
                remainder := mulmod(a, b, 0x10000000000000000)
            }
            // Subtract 256 bit number from 512 bit number
            assembly ("memory-safe") {
                prod1 := sub(prod1, gt(remainder, prod0))
                prod0 := sub(prod0, remainder)
            }

            // Divide [prod1 prod0] by the factors of two (note that this is just 2**96 since the denominator is a power of 2 itself)
            assembly ("memory-safe") {
                // Right shift by n is equivalent and 2 gas cheaper than division by 2^n
                prod0 := shr(64, prod0)
            }
            // Shift in bits from prod1 into prod0. For this we need
            // to flip `twos` such that it is 2**256 / twos.
            // If twos is zero, then it becomes one
            // Note that this is just 2**192 since 2**256 over the fixed denominator (2**64) equals 2**192
            prod0 |= prod1 * 2 ** 192;

            return prod0;
        }
    }

    /// @notice Calculates `floor(a×b÷2^96)` with full precision. Throws if result overflows a uint256.
    /// @param a The multiplicand
    /// @param b The multiplier
    /// @return The 256-bit result
    function mulDiv96(uint256 a, uint256 b) internal pure returns (uint256) {
        unchecked {
            // 512-bit multiply [prod1 prod0] = a * b
            // Compute the product mod 2**256 and mod 2**256 - 1
            // then use the Chinese Remainder Theorem to reconstruct
            // the 512 bit result. The result is stored in two 256
            // variables such that product = prod1 * 2**256 + prod0
            uint256 prod0; // Least significant 256 bits of the product
            uint256 prod1; // Most significant 256 bits of the product
            assembly ("memory-safe") {
                let mm := mulmod(a, b, not(0))
                prod0 := mul(a, b)
                prod1 := sub(sub(mm, prod0), lt(mm, prod0))
            }

            // Handle non-overflow cases, 256 by 256 division
            if (prod1 == 0) {
                uint256 res;
                assembly ("memory-safe") {
                    // Right shift by n is equivalent and 2 gas cheaper than division by 2^n
                    res := shr(96, prod0)
                }
                return res;
            }

            // Make sure the result is less than 2**256.
            require(2 ** 96 > prod1);

            ///////////////////////////////////////////////
            // 512 by 256 division.
            ///////////////////////////////////////////////

            // Make division exact by subtracting the remainder from [prod1 prod0]
            // Compute remainder using mulmod
            uint256 remainder;
            assembly ("memory-safe") {
                remainder := mulmod(a, b, 0x1000000000000000000000000)
            }
            // Subtract 256 bit number from 512 bit number
            assembly ("memory-safe") {
                prod1 := sub(prod1, gt(remainder, prod0))
                prod0 := sub(prod0, remainder)
            }

            // Divide [prod1 prod0] by the factors of two (note that this is just 2**96 since the denominator is a power of 2 itself)
            assembly ("memory-safe") {
                // Right shift by n is equivalent and 2 gas cheaper than division by 2^n
                prod0 := shr(96, prod0)
            }
            // Shift in bits from prod1 into prod0. For this we need
            // to flip `twos` such that it is 2**256 / twos.
            // If twos is zero, then it becomes one
            // Note that this is just 2**160 since 2**256 over the fixed denominator (2**96) equals 2**160
            prod0 |= prod1 * 2 ** 160;

            return prod0;
        }
    }

    /// @notice Calculates `ceil(a×b÷2^96)` with full precision. Throws if result overflows a uint256.
    /// @param a The multiplicand
    /// @param b The multiplier
    /// @return result The 256-bit result
    function mulDiv96RoundingUp(uint256 a, uint256 b) internal pure returns (uint256 result) {
        unchecked {
            result = mulDiv96(a, b);
            if (mulmod(a, b, 2 ** 96) > 0) {
                require(result < type(uint256).max);
                result++;
            }
        }
    }

    /// @notice Calculates `floor(a×b÷2^128)` with full precision. Throws if result overflows a uint256.
    /// @param a The multiplicand
    /// @param b The multiplier
    /// @return The 256-bit result
    function mulDiv128(uint256 a, uint256 b) internal pure returns (uint256) {
        unchecked {
            // 512-bit multiply [prod1 prod0] = a * b
            // Compute the product mod 2**256 and mod 2**256 - 1
            // then use the Chinese Remainder Theorem to reconstruct
            // the 512 bit result. The result is stored in two 256
            // variables such that product = prod1 * 2**256 + prod0
            uint256 prod0; // Least significant 256 bits of the product
            uint256 prod1; // Most significant 256 bits of the product
            assembly ("memory-safe") {
                let mm := mulmod(a, b, not(0))
                prod0 := mul(a, b)
                prod1 := sub(sub(mm, prod0), lt(mm, prod0))
            }

            // Handle non-overflow cases, 256 by 256 division
            if (prod1 == 0) {
                uint256 res;
                assembly ("memory-safe") {
                    // Right shift by n is equivalent and 2 gas cheaper than division by 2^n
                    res := shr(128, prod0)
                }
                return res;
            }

            // Make sure the result is less than 2**256.
            require(2 ** 128 > prod1);

            ///////////////////////////////////////////////
            // 512 by 256 division.
            ///////////////////////////////////////////////

            // Make division exact by subtracting the remainder from [prod1 prod0]
            // Compute remainder using mulmod
            uint256 remainder;
            assembly ("memory-safe") {
                remainder := mulmod(a, b, 0x100000000000000000000000000000000)
            }
            // Subtract 256 bit number from 512 bit number
            assembly ("memory-safe") {
                prod1 := sub(prod1, gt(remainder, prod0))
                prod0 := sub(prod0, remainder)
            }

            // Divide [prod1 prod0] by the factors of two (note that this is just 2**128 since the denominator is a power of 2 itself)
            assembly ("memory-safe") {
                // Right shift by n is equivalent and 2 gas cheaper than division by 2^n
                prod0 := shr(128, prod0)
            }
            // Shift in bits from prod1 into prod0. For this we need
            // to flip `twos` such that it is 2**256 / twos.
            // If twos is zero, then it becomes one
            // Note that this is just 2**160 since 2**256 over the fixed denominator (2**128) equals 2**128
            prod0 |= prod1 * 2 ** 128;

            return prod0;
        }
    }

    /// @notice Calculates `ceil(a×b÷2^128)` with full precision. Throws if result overflows a uint256.
    /// @param a The multiplicand
    /// @param b The multiplier
    /// @return result The 256-bit result
    function mulDiv128RoundingUp(uint256 a, uint256 b) internal pure returns (uint256 result) {
        unchecked {
            result = mulDiv128(a, b);
            if (mulmod(a, b, 2 ** 128) > 0) {
                require(result < type(uint256).max);
                result++;
            }
        }
    }

    /// @notice Calculates `floor(a×b÷2^192)` with full precision. Throws if result overflows a uint256.
    /// @param a The multiplicand
    /// @param b The multiplier
    /// @return The 256-bit result
    function mulDiv192(uint256 a, uint256 b) internal pure returns (uint256) {
        unchecked {
            // 512-bit multiply [prod1 prod0] = a * b
            // Compute the product mod 2**256 and mod 2**256 - 1
            // then use the Chinese Remainder Theorem to reconstruct
            // the 512 bit result. The result is stored in two 256
            // variables such that product = prod1 * 2**256 + prod0
            uint256 prod0; // Least significant 256 bits of the product
            uint256 prod1; // Most significant 256 bits of the product
            assembly ("memory-safe") {
                let mm := mulmod(a, b, not(0))
                prod0 := mul(a, b)
                prod1 := sub(sub(mm, prod0), lt(mm, prod0))
            }

            // Handle non-overflow cases, 256 by 256 division
            if (prod1 == 0) {
                uint256 res;
                assembly ("memory-safe") {
                    // Right shift by n is equivalent and 2 gas cheaper than division by 2^n
                    res := shr(192, prod0)
                }
                return res;
            }

            // Make sure the result is less than 2**256.
            require(2 ** 192 > prod1);

            ///////////////////////////////////////////////
            // 512 by 256 division.
            ///////////////////////////////////////////////

            // Make division exact by subtracting the remainder from [prod1 prod0]
            // Compute remainder using mulmod
            uint256 remainder;
            assembly ("memory-safe") {
                remainder := mulmod(a, b, 0x1000000000000000000000000000000000000000000000000)
            }
            // Subtract 256 bit number from 512 bit number
            assembly ("memory-safe") {
                prod1 := sub(prod1, gt(remainder, prod0))
                prod0 := sub(prod0, remainder)
            }

            // Divide [prod1 prod0] by the factors of two (note that this is just 2**96 since the denominator is a power of 2 itself)
            assembly ("memory-safe") {
                // Right shift by n is equivalent and 2 gas cheaper than division by 2^n
                prod0 := shr(192, prod0)
            }
            // Shift in bits from prod1 into prod0. For this we need
            // to flip `twos` such that it is 2**256 / twos.
            // If twos is zero, then it becomes one
            // Note that this is just 2**64 since 2**256 over the fixed denominator (2**192) equals 2**64
            prod0 |= prod1 * 2 ** 64;

            return prod0;
        }
    }

    /// @notice Calculates `ceil(a×b÷2^192)` with full precision.
    /// @param a The multiplicand
    /// @param b The multiplier
    /// @return result The 256-bit result
    function mulDiv192RoundingUp(uint256 a, uint256 b) internal pure returns (uint256 result) {
        unchecked {
            result = mulDiv192(a, b);
            if (mulmod(a, b, 2 ** 192) > 0) {
                require(result < type(uint256).max);
                result++;
            }
        }
    }

    /// @notice Calculates `ceil(a÷b)`, returning 0 if `b == 0`.
    /// @param a The numerator
    /// @param b The denominator
    /// @return result The 256-bit result
    function unsafeDivRoundingUp(uint256 a, uint256 b) internal pure returns (uint256 result) {
        assembly ("memory-safe") {
            result := add(div(a, b), gt(mod(a, b), 0))
        }
    }

    /*//////////////////////////////////////////////////////////////
                                SORTING
    //////////////////////////////////////////////////////////////*/

    /// @notice QuickSort is a sorting algorithm that employs the Divide and Conquer strategy. It selects a pivot element and arranges the given array around
    /// this pivot by correctly positioning it within the sorted array.
    /// @param arr The elements that must be sorted
    /// @param left The starting index
    /// @param right The ending index
    function quickSort(int256[] memory arr, int256 left, int256 right) internal pure {
        unchecked {
            int256 i = left;
            int256 j = right;
            if (i == j) return;
            int256 pivot = arr[uint256(left + (right - left) / 2)];
            while (i < j) {
                while (arr[uint256(i)] < pivot) i++;
                while (pivot < arr[uint256(j)]) j--;
                if (i <= j) {
                    (arr[uint256(i)], arr[uint256(j)]) = (arr[uint256(j)], arr[uint256(i)]);
                    i++;
                    j--;
                }
            }
            if (left < j) quickSort(arr, left, j);
            if (i < right) quickSort(arr, i, right);
        }
    }

    /// @notice Calls `quickSort` with default starting index of 0 and ending index of the last element in the array.
    /// @param data The elements that must be sorted
    /// @return The sorted array
    function sort(int256[] memory data) internal pure returns (int256[] memory) {
        unchecked {
            quickSort(data, int256(0), int256(data.length - 1));
        }
        return data;
    }
}

// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.24;

// Interfaces
import {CollateralTracker} from "@contracts/CollateralTracker.sol";
import {PanopticPool} from "@contracts/PanopticPool.sol";
import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import {IV3CompatibleOracle} from "@interfaces/IV3CompatibleOracle.sol";
// Libraries
import {Constants} from "@libraries/Constants.sol";
import {Math} from "@libraries/Math.sol";
// OpenZeppelin libraries
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
// Custom types
import {LeftRightUnsigned, LeftRightSigned} from "@types/LeftRight.sol";
import {LiquidityChunk} from "@types/LiquidityChunk.sol";
import {PoolId} from "v4-core/types/PoolId.sol";
import {TokenId} from "@types/TokenId.sol";

/// @title Compute general math quantities relevant to Panoptic and AMM pool management.
/// @notice Contains Panoptic-specific helpers and math functions.
/// @author Axicon Labs Limited
library PanopticMath {
    using Math for uint256;

    /// @notice This is equivalent to `type(uint256).max` — used in assembly blocks as a replacement.
    uint256 internal constant MAX_UINT256 = 2 ** 256 - 1;

    /// @notice Masks 16-bit tickSpacing out of 64-bit `[16-bit tickspacing][48-bit poolPattern]` format poolId.
    uint64 internal constant TICKSPACING_MASK = 0xFFFF000000000000;

    /*//////////////////////////////////////////////////////////////
                              UTILITIES
    //////////////////////////////////////////////////////////////*/

    /// @notice Given a 256-bit Uniswap V4 pool ID (hash) and the corresponding `tickSpacing`, return its 64-bit ID as used in the `TokenId` of Panoptic.
    // Example:
    //      [16-bit tickSpacing][last 48 bits of Uniswap V4 pool ID] = poolId
    //      e.g.:
    //        idV4        = 0x9c33e1937fe23c3ff82d7725f2bb5af696db1c89a9b8cae141cb0e986847638a
    //        tickSpacing = 60
    //      the returned id is then:
    //        poolPattern = 0x0000e986847638a
    //        tickSpacing = 0x003c000000000000    +
    //        --------------------------------------------
    //        poolId      = 0x003ce986847638a
    /// @param idV4 The 256-bit Uniswap V4 pool ID
    /// @param tickSpacing The tick spacing of the Uniswap V4 pool identified by `idV4`
    /// @return A fingerprint representing the Uniswap V4 pool
    function getPoolId(PoolId idV4, int24 tickSpacing) internal pure returns (uint64) {
        unchecked {
            return uint48(uint256(PoolId.unwrap(idV4))) + (uint64(uint24(tickSpacing)) << 48);
        }
    }

    /// @notice Increments the pool pattern (first 48 bits) of a poolId by 1.
    /// @param poolId The 64-bit pool ID
    /// @return The provided `poolId` with its pool pattern slot incremented by 1
    function incrementPoolPattern(uint64 poolId) internal pure returns (uint64) {
        unchecked {
            return (poolId & TICKSPACING_MASK) + (uint48(poolId) + 1);
        }
    }

    /// @notice Get the number of leading hex characters in an address.
    //     0x0000bababaab...     0xababababab...
    //          ▲                 ▲
    //          │                 │
    //     4 leading hex      0 leading hex
    //    character zeros    character zeros
    //
    /// @param addr The address to get the number of leading zero hex characters for
    /// @return The number of leading zero hex characters in the address
    function numberOfLeadingHexZeros(address addr) external pure returns (uint256) {
        unchecked {
            return addr == address(0) ? 40 : 39 - Math.mostSignificantNibble(uint160(addr));
        }
    }

    /// @notice Returns ERC20 symbol of `asset`.
    /// @param asset The address of the asset to get the symbol of (`address(0)` = native asset)
    /// @return The symbol of `asset` or "???" if not supported
    function safeERC20Symbol(address asset) external view returns (string memory) {
        if (asset == address(0)) return "ETH";
        // not guaranteed that token supports metadata extension
        // so we need to let call fail and return placeholder if not
        try IERC20Metadata(asset).symbol() returns (string memory symbol) {
            return symbol;
        } catch {
            return "???";
        }
    }

    /// @notice Converts `fee` to a string with "bps" appended, or DYNAMIC if "fee" is equivalent to `0x800000`.
    /// @dev The lowest supported value of `fee` is 1 (`="0.01bps"`).
    /// @param fee The fee to convert to a string (in hundredths of basis points)
    /// @return Stringified version of `fee` with "bps" appended
    function uniswapFeeToString(uint24 fee) internal pure returns (string memory) {
        return
            fee == 0x800000
                ? "DYNAMIC"
                : string.concat(
                    Strings.toString(fee / 100),
                    fee % 100 == 0
                        ? ""
                        : string.concat(
                            ".",
                            Strings.toString((fee / 10) % 10),
                            Strings.toString(fee % 10)
                        ),
                    "bps"
                );
    }

    /// @notice Update an existing account's "positions hash" with a new `tokenId`.
    /// @notice The positions hash contains a fingerprint of all open positions created by an account/user and a count of the legs across those positions.
    /// @dev The "fingerprint" portion of the hash is given by XORing the hashed `tokenId` of each position the user has open together.
    /// @param existingHash The existing position hash representing a list of positions and the count of the legs across those positions
    /// @param tokenId The new position to modify the existing hash with: `existingHash = uint248(existingHash) ^ uint248(hashOf(tokenId))`
    /// @param addFlag Whether to mint (add) the tokenId to the count of positions or burn (subtract) it from the count `(existingHash >> 248) +/- tokenId.countLegs()`
    /// @return newHash The updated position hash with the new tokenId XORed in and the leg count incremented/decremented
    function updatePositionsHash(
        uint256 existingHash,
        TokenId tokenId,
        bool addFlag
    ) internal pure returns (uint256) {
        // update hash by taking the XOR of the existing hash with the new tokenId
        uint256 updatedHash = uint248(existingHash) ^
            (uint248(uint256(keccak256(abi.encode(tokenId)))));

        uint256 positionLegs = tokenId.countLegs();

        // increment the upper 8 bits (leg counter) if addFlag=true, decrement otherwise
        uint256 newLegCount;
        if (addFlag) {
            newLegCount = uint8(existingHash >> 248) + uint8(positionLegs);
        } else {
            unchecked {
                newLegCount = (existingHash >> 248) - positionLegs;
            }
        }

        unchecked {
            return uint256(updatedHash) + (newLegCount << 248);
        }
    }

    /*//////////////////////////////////////////////////////////////
                          ORACLE CALCULATIONS
    //////////////////////////////////////////////////////////////*/

    /// @notice Computes various oracle prices corresponding to a Uniswap pool.
    /// @param oracleContract The external oracle contract to retrieve observations from
    /// @param miniMedian The packed structure representing the sorted 8-slot queue of internal median observations
    /// @return fastOracleTick The fast oracle tick computed as the median of the past N observations in the Uniswap Pool
    /// @return slowOracleTick The slow oracle tick computed with the method specified in `SLOW_ORACLE_UNISWAP_MODE`
    /// @return latestObservation The latest observation from the Uniswap pool (price at the end of the last block)
    /// @return medianData The updated value for `s_miniMedian` (0 if not enough time has passed since last observation or if `SLOW_ORACLE_UNISWAP_MODE` is true)
    function getOracleTicks(
        IV3CompatibleOracle oracleContract,
        uint256 miniMedian
    )
        external
        view
        returns (
            int24 fastOracleTick,
            int24 slowOracleTick,
            int24 latestObservation,
            uint256 medianData
        )
    {
        uint16 observationIndex;
        uint16 observationCardinality;

        (, , observationIndex, observationCardinality, , , ) = oracleContract.slot0();

        (fastOracleTick, latestObservation) = computeMedianObservedPrice(
            oracleContract,
            observationIndex,
            observationCardinality,
            Constants.FAST_ORACLE_CARDINALITY,
            Constants.FAST_ORACLE_PERIOD
        );

        if (Constants.SLOW_ORACLE_UNISWAP_MODE) {
            (slowOracleTick, ) = computeMedianObservedPrice(
                oracleContract,
                observationIndex,
                observationCardinality,
                Constants.SLOW_ORACLE_CARDINALITY,
                Constants.SLOW_ORACLE_PERIOD
            );
        } else {
            (slowOracleTick, medianData) = computeInternalMedian(
                observationIndex,
                observationCardinality,
                Constants.MEDIAN_PERIOD,
                miniMedian,
                oracleContract
            );
        }
    }

    /// @notice Returns the median of the last `cardinality` average prices over `period` observations from `oracleContract`.
    /// @dev Used when we need a manipulation-resistant TWAP price.
    /// @dev Oracle observations snapshot the closing price of the last block before the first interaction of a given block.
    /// @dev The maximum frequency of observations is 1 per block, but there is no guarantee that the pool will be observed at every block.
    /// @dev Each period has a minimum length of `blocktime * period`, but may be longer if the Uniswap pool is relatively inactive.
    /// @dev The final price used in the array (of length `cardinality`) is the average of `cardinality` observations spaced by `period` (which is itself a number of observations).
    /// @dev Thus, the minimum total time window is `cardinality * period * blocktime`.
    /// @param oracleContract The external oracle contract to retrieve observations from
    /// @param observationIndex The index of the last observation in the pool
    /// @param observationCardinality The number of observations in the pool
    /// @param cardinality The number of `periods` to in the median price array, should be odd
    /// @param period The number of observations to average to compute one entry in the median price array
    /// @return The median of `cardinality` observations spaced by `period` in the Uniswap pool
    /// @return The latest observation in the Uniswap pool
    function computeMedianObservedPrice(
        IV3CompatibleOracle oracleContract,
        uint256 observationIndex,
        uint256 observationCardinality,
        uint256 cardinality,
        uint256 period
    ) internal view returns (int24, int24) {
        unchecked {
            int256[] memory tickCumulatives = new int256[](cardinality + 1);

            uint256[] memory timestamps = new uint256[](cardinality + 1);
            // get the last "cardinality" timestamps/tickCumulatives (if observationIndex < cardinality, the index will wrap back from observationCardinality)
            for (uint256 i = 0; i < cardinality + 1; ++i) {
                (timestamps[i], tickCumulatives[i], , ) = oracleContract.observations(
                    uint256(
                        (int256(observationIndex) - int256(i * period)) +
                            int256(observationCardinality)
                    ) % observationCardinality
                );
            }

            int256[] memory ticks = new int256[](cardinality);
            // use cardinality periods given by cardinality + 1 accumulator observations to compute the last cardinality observed ticks spaced by period
            for (uint256 i = 0; i < cardinality; ++i) {
                ticks[i] =
                    (tickCumulatives[i] - tickCumulatives[i + 1]) /
                    int256(timestamps[i] - timestamps[i + 1]);
            }

            // the `ticks` array descends from the most recent Uniswap observation prior to the sort
            int24 latestObservation = int24(ticks[0]);

            // get the median of the `ticks` array (assuming `cardinality` is odd)
            return (int24(Math.sort(ticks)[cardinality / 2]), latestObservation);
        }
    }

    /// @notice Takes a packed structure representing a sorted 8-slot queue of ticks and returns the median of those values and an updated queue if another observation is warranted.
    /// @dev Also inserts the latest oracle observation into the buffer, resorts, and returns if the last entry is at least `period` seconds old.
    /// @param observationIndex The index of the last observation in the Uniswap pool
    /// @param observationCardinality The number of observations in the Uniswap pool
    /// @param period The minimum time in seconds that must have passed since the last observation was inserted into the buffer
    /// @param medianData The packed structure representing the sorted 8-slot queue of ticks
    /// @param oracleContract The external oracle contract to retrieve observations from
    /// @return medianTick The median of the provided 8-slot queue of ticks in `medianData`
    /// @return updatedMedianData The updated 8-slot queue of ticks with the latest observation inserted if the last entry is at least `period` seconds old (returns 0 otherwise)
    function computeInternalMedian(
        uint256 observationIndex,
        uint256 observationCardinality,
        uint256 period,
        uint256 medianData,
        IV3CompatibleOracle oracleContract
    ) public view returns (int24 medianTick, uint256 updatedMedianData) {
        unchecked {
            // return the average of the rank 3 and 4 values
            medianTick =
                (int24(uint24(medianData >> ((uint24(medianData >> (192 + 3 * 3)) % 8) * 24))) +
                    int24(uint24(medianData >> ((uint24(medianData >> (192 + 3 * 4)) % 8) * 24)))) /
                2;

            // only proceed if last entry is at least MEDIAN_PERIOD seconds old
            if (block.timestamp >= uint256(uint40(medianData >> 216)) + period) {
                int24 lastObservedTick;
                {
                    (uint256 timestamp_old, int56 tickCumulative_old, , ) = oracleContract
                        .observations(
                            uint256(
                                int256(observationIndex) -
                                    int256(1) +
                                    int256(observationCardinality)
                            ) % observationCardinality
                        );

                    (uint256 timestamp_last, int56 tickCumulative_last, , ) = oracleContract
                        .observations(observationIndex);
                    lastObservedTick = int24(
                        (tickCumulative_last - tickCumulative_old) /
                            int256(timestamp_last - timestamp_old)
                    );
                }

                uint24 orderMap = uint24(medianData >> 192);

                uint24 newOrderMap;
                uint24 shift = 1;
                bool below = true;
                uint24 rank;
                int24 entry;
                for (uint8 i; i < 8; ++i) {
                    // read the rank from the existing ordering
                    rank = (orderMap >> (3 * i)) % 8;

                    if (rank == 7) {
                        shift -= 1;
                        continue;
                    }

                    // read the corresponding entry
                    entry = int24(uint24(medianData >> (rank * 24)));
                    if ((below) && (lastObservedTick > entry)) {
                        shift += 1;
                        below = false;
                    }

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

                updatedMedianData =
                    (block.timestamp << 216) +
                    (uint256(newOrderMap) << 192) +
                    uint256(uint192(medianData << 24)) +
                    uint256(uint24(lastObservedTick));
            }
        }
    }

    /// @notice Computes a TWAP price over `twapWindow` on a Uniswap V3-style observation oracle.
    /// @dev Note that our definition of TWAP differs from a typical mean of prices over a time window.
    /// @dev We instead observe the average price over a series of time intervals, and define the TWAP as the median of those averages.
    /// @param oracleContract The external oracle contract to retrieve observations from
    /// @param twapWindow The time window to compute the TWAP over
    /// @return The final calculated TWAP tick
    function twapFilter(
        IV3CompatibleOracle oracleContract,
        uint32 twapWindow
    ) external view returns (int24) {
        uint32[] memory secondsAgos = new uint32[](20);

        int256[] memory twapMeasurement = new int256[](19);

        unchecked {
            // construct the time slots
            for (uint256 i = 0; i < 20; ++i) {
                secondsAgos[i] = uint32(((i + 1) * twapWindow) / 20);
            }

            // observe the tickCumulative at the 20 pre-defined time slots
            (int56[] memory tickCumulatives, ) = oracleContract.observe(secondsAgos);

            // compute the average tick per 30s window
            for (uint256 i = 0; i < 19; ++i) {
                twapMeasurement[i] = int24(
                    (tickCumulatives[i] - tickCumulatives[i + 1]) / int56(uint56(twapWindow / 20))
                );
            }

            // sort the tick measurements
            int256[] memory sortedTicks = Math.sort(twapMeasurement);

            // Get the median value
            return int24(sortedTicks[9]);
        }
    }

    /*//////////////////////////////////////////////////////////////
                          LIQUIDITY CHUNK MATH
    //////////////////////////////////////////////////////////////*/

    /// @notice For a given option position (`tokenId`), leg index within that position (`legIndex`), and `positionSize` get the tick range spanned and its
    /// liquidity (share ownership) in the Uniswap V4 pool; this is a liquidity chunk.
    //          Liquidity chunk  (defined by tick upper, tick lower, and its size/amount: the liquidity)
    //   liquidity    │
    //         ▲      │
    //         │     ┌▼┐
    //         │  ┌──┴─┴──┐
    //         │  │       │
    //         │  │       │
    //         └──┴───────┴────► price
    //         Uniswap V4 Pool
    /// @param tokenId The option position id
    /// @param legIndex The leg index of the option position, can be {0,1,2,3}
    /// @param positionSize The number of contracts held by this leg
    /// @return A LiquidityChunk with `tickLower`, `tickUpper`, and `liquidity`
    function getLiquidityChunk(
        TokenId tokenId,
        uint256 legIndex,
        uint128 positionSize
    ) internal pure returns (LiquidityChunk) {
        // get the tick range for this leg
        (int24 tickLower, int24 tickUpper) = tokenId.asTicks(legIndex);

        // Get the amount of liquidity owned by this leg in the Uniswap V4 pool in the above tick range
        // Background:
        //
        //  In Uniswap V4, the amount of liquidity received for a given amount of currency0 when the price is
        //  not in range is given by:
        //     Liquidity = amount0 * (sqrt(upper) * sqrt(lower)) / (sqrt(upper) - sqrt(lower))
        //  For currency1, it is given by:
        //     Liquidity = amount1 / (sqrt(upper) - sqrt(lower))
        //
        //  However, in Panoptic, each position has a asset parameter. The asset is the "basis" of the position.
        //  In TradFi, the asset is always cash and selling a $1000 put requires the user to lock $1000, and selling
        //  a call requires the user to lock 1 unit of asset.
        //
        //  Because Uniswap V4 chooses currency0 and currency1 from the alphanumeric order, there is no consistency as to whether currency0 is
        //  stablecoin, ETH, or an ERC20. Some pools may want ETH to be the asset (e.g. ETH-DAI) and some may wish the stablecoin to
        //  be the asset (e.g. DAI-ETH) so that K asset is moved for puts and 1 asset is moved for calls.
        //  But since the convention is to force the order always we have no say in this.
        //
        //  To solve this, we encode the asset value in tokenId. This parameter specifies which of currency0 or currency1 is the
        //  asset, such that:
        //     when asset=0, then amount0 moved at strike K =1.0001**currentTick is 1, amount1 moved to strike K is K
        //     when asset=1, then amount1 moved at strike K =1.0001**currentTick is K, amount0 moved to strike K is 1/K
        //
        //  The following function takes this into account when computing the liquidity of the leg and switches between
        //  the definition for getLiquidityForAmount0 or getLiquidityForAmount1 when relevant.

        uint256 amount = positionSize * tokenId.optionRatio(legIndex);
        if (tokenId.asset(legIndex) == 0) {
            return Math.getLiquidityForAmount0(tickLower, tickUpper, amount);
        } else {
            return Math.getLiquidityForAmount1(tickLower, tickUpper, amount);
        }
    }

    /// @notice Extract the tick range specified by `strike` and `width` for the given `tickSpacing`.
    /// @param strike The strike price of the option
    /// @param width The width of the option
    /// @param tickSpacing The tick spacing of the underlying Uniswap V4 pool
    /// @return The lower tick of the liquidity chunk
    /// @return The upper tick of the liquidity chunk
    function getTicks(
        int24 strike,
        int24 width,
        int24 tickSpacing
    ) internal pure returns (int24, int24) {
        (int24 rangeDown, int24 rangeUp) = PanopticMath.getRangesFromStrike(width, tickSpacing);

        unchecked {
            return (strike - rangeDown, strike + rangeUp);
        }
    }

    /// @notice Returns the distances of the upper and lower ticks from the strike for a position with the given width and tickSpacing.
    /// @dev Given `r = (width * tickSpacing) / 2`, `tickLower = strike - floor(r)` and `tickUpper = strike + ceil(r)`.
    /// @param width The width of the leg
    /// @param tickSpacing The tick spacing of the underlying pool
    /// @return The distance of the lower tick from the strike
    /// @return The distance of the upper tick from the strike
    function getRangesFromStrike(
        int24 width,
        int24 tickSpacing
    ) internal pure returns (int24, int24) {
        return (
            (width * tickSpacing) / 2,
            int24(int256(Math.unsafeDivRoundingUp(uint24(width) * uint24(tickSpacing), 2)))
        );
    }

    /*//////////////////////////////////////////////////////////////
                         TOKEN CONVERSION LOGIC
    //////////////////////////////////////////////////////////////*/

    /// @notice Compute the amount of notional value underlying an option position.
    /// @param tokenId The option position id
    /// @param positionSize The number of contracts of the option
    /// @return longAmounts Left-right packed word where rightSlot = currency0 and leftSlot = currency1 held against borrowed Uniswap liquidity for long legs
    /// @return shortAmounts Left-right packed word where where rightSlot = currency0 and leftSlot = currency1 borrowed to create short legs
    function computeExercisedAmounts(
        TokenId tokenId,
        uint128 positionSize
    ) internal pure returns (LeftRightSigned longAmounts, LeftRightSigned shortAmounts) {
        uint256 numLegs = tokenId.countLegs();
        for (uint256 leg = 0; leg < numLegs; ) {
            (LeftRightSigned longs, LeftRightSigned shorts) = _calculateIOAmounts(
                tokenId,
                positionSize,
                leg
            );

            longAmounts = longAmounts.add(longs);
            shortAmounts = shortAmounts.add(shorts);

            unchecked {
                ++leg;
            }
        }
    }

    /// @notice Convert an amount of currency0 into an amount of currency1 given the sqrtPriceX96 in a Uniswap pool defined as `sqrt(1/0)*2^96`.
    /// @dev Uses reduced precision after tick 443636 in order to accommodate the full range of ticks
    /// @param amount The amount of currency0 to convert into currency1
    /// @param sqrtPriceX96 The square root of the price at which to convert `amount` of currency0 into currency1
    /// @return The converted `amount` of currency0 represented in terms of currency1
    function convert0to1(uint256 amount, uint160 sqrtPriceX96) internal pure returns (uint256) {
        unchecked {
            // the tick 443636 is the maximum price where (price) * 2**192 fits into a uint256 (< 2**256-1)
            // above that tick, we are forced to reduce the amount of decimals in the final price by 2**64 to 2**128
            if (sqrtPriceX96 < type(uint128).max) {
                return Math.mulDiv192(amount, uint256(sqrtPriceX96) ** 2);
            } else {
                return Math.mulDiv128(amount, Math.mulDiv64(sqrtPriceX96, sqrtPriceX96));
            }
        }
    }

    /// @notice Convert an amount of currency0 into an amount of currency1 given the sqrtPriceX96 in a Uniswap pool defined as `sqrt(1/0)*2^96`.
    /// @dev Uses reduced precision after tick 443636 in order to accommodate the full range of ticks
    /// @param amount The amount of currency0 to convert into currency1
    /// @param sqrtPriceX96 The square root of the price at which to convert `amount` of currency0 into currency1
    /// @return The converted `amount` of currency0 represented in terms of currency1
    function convert0to1RoundingUp(
        uint256 amount,
        uint160 sqrtPriceX96
    ) internal pure returns (uint256) {
        unchecked {
            // the tick 443636 is the maximum price where (price) * 2**192 fits into a uint256 (< 2**256-1)
            // above that tick, we are forced to reduce the amount of decimals in the final price by 2**64 to 2**128
            if (sqrtPriceX96 < type(uint128).max) {
                return Math.mulDiv192RoundingUp(amount, uint256(sqrtPriceX96) ** 2);
            } else {
                return Math.mulDiv128RoundingUp(amount, Math.mulDiv64(sqrtPriceX96, sqrtPriceX96));
            }
        }
    }

    /// @notice Convert an amount of currency1 into an amount of currency0 given the sqrtPriceX96 in a Uniswap pool defined as `sqrt(1/0)*2^96`.
    /// @dev Uses reduced precision after tick 443636 in order to accommodate the full range of ticks.
    /// @param amount The amount of currency1 to convert into currency0
    /// @param sqrtPriceX96 The square root of the price at which to convert `amount` of currency1 into currency0
    /// @return The converted `amount` of currency1 represented in terms of currency0
    function convert1to0(uint256 amount, uint160 sqrtPriceX96) internal pure returns (uint256) {
        unchecked {
            // the tick 443636 is the maximum price where (price) * 2**192 fits into a uint256 (< 2**256-1)
            // above that tick, we are forced to reduce the amount of decimals in the final price by 2**64 to 2**128
            if (sqrtPriceX96 < type(uint128).max) {
                return Math.mulDiv(amount, 2 ** 192, uint256(sqrtPriceX96) ** 2);
            } else {
                return Math.mulDiv(amount, 2 ** 128, Math.mulDiv64(sqrtPriceX96, sqrtPriceX96));
            }
        }
    }

    /// @notice Convert an amount of currency1 into an amount of currency0 given the sqrtPriceX96 in a Uniswap pool defined as `sqrt(1/0)*2^96`.
    /// @dev Uses reduced precision after tick 443636 in order to accommodate the full range of ticks.
    /// @param amount The amount of currency1 to convert into currency0
    /// @param sqrtPriceX96 The square root of the price at which to convert `amount` of currency1 into currency0
    /// @return The converted `amount` of currency1 represented in terms of currency0
    function convert1to0RoundingUp(
        uint256 amount,
        uint160 sqrtPriceX96
    ) internal pure returns (uint256) {
        unchecked {
            // the tick 443636 is the maximum price where (price) * 2**192 fits into a uint256 (< 2**256-1)
            // above that tick, we are forced to reduce the amount of decimals in the final price by 2**64 to 2**128
            if (sqrtPriceX96 < type(uint128).max) {
                return Math.mulDivRoundingUp(amount, 2 ** 192, uint256(sqrtPriceX96) ** 2);
            } else {
                return
                    Math.mulDivRoundingUp(
                        amount,
                        2 ** 128,
                        Math.mulDiv64(sqrtPriceX96, sqrtPriceX96)
                    );
            }
        }
    }

    /// @notice Convert an amount of currency0 into an amount of currency1 given the sqrtPriceX96 in a Uniswap pool defined as `sqrt(1/0)*2^96`.
    /// @dev Uses reduced precision after tick 443636 in order to accommodate the full range of ticks.
    /// @param amount The amount of currency0 to convert into currency1
    /// @param sqrtPriceX96 The square root of the price at which to convert `amount` of currency0 into currency1
    /// @return The converted `amount` of currency0 represented in terms of currency1
    function convert0to1(int256 amount, uint160 sqrtPriceX96) internal pure returns (int256) {
        unchecked {
            // the tick 443636 is the maximum price where (price) * 2**192 fits into a uint256 (< 2**256-1)
            // above that tick, we are forced to reduce the amount of decimals in the final price by 2**64 to 2**128
            if (sqrtPriceX96 < type(uint128).max) {
                int256 absResult = Math
                    .mulDiv192(Math.absUint(amount), uint256(sqrtPriceX96) ** 2)
                    .toInt256();
                return amount < 0 ? -absResult : absResult;
            } else {
                int256 absResult = Math
                    .mulDiv128(Math.absUint(amount), Math.mulDiv64(sqrtPriceX96, sqrtPriceX96))
                    .toInt256();
                return amount < 0 ? -absResult : absResult;
            }
        }
    }

    /// @notice Convert an amount of currency1 into an amount of currency0 given the sqrtPriceX96 in a Uniswap pool defined as `sqrt(1/0)*2^96`.
    /// @dev Uses reduced precision after tick 443636 in order to accommodate the full range of ticks.
    /// @param amount The amount of currency1 to convert into currency0
    /// @param sqrtPriceX96 The square root of the price at which to convert `amount` of currency1 into currency0
    /// @return The converted `amount` of currency1 represented in terms of currency0
    function convert1to0(int256 amount, uint160 sqrtPriceX96) internal pure returns (int256) {
        unchecked {
            // the tick 443636 is the maximum price where (price) * 2**192 fits into a uint256 (< 2**256-1)
            // above that tick, we are forced to reduce the amount of decimals in the final price by 2**64 to 2**128
            if (sqrtPriceX96 < type(uint128).max) {
                int256 absResult = Math
                    .mulDiv(Math.absUint(amount), 2 ** 192, uint256(sqrtPriceX96) ** 2)
                    .toInt256();
                return amount < 0 ? -absResult : absResult;
            } else {
                int256 absResult = Math
                    .mulDiv(
                        Math.absUint(amount),
                        2 ** 128,
                        Math.mulDiv64(sqrtPriceX96, sqrtPriceX96)
                    )
                    .toInt256();
                return amount < 0 ? -absResult : absResult;
            }
        }
    }

    /// @notice Get a single collateral balance and requirement in terms of the lowest-priced token for a given set of (currency0/currency1) collateral balances and requirements.
    /// @param tokenData0 LeftRight encoded word with balance of currency0 in the right slot, and required balance in left slot
    /// @param tokenData1 LeftRight encoded word with balance of currency1 in the right slot, and required balance in left slot
    /// @param sqrtPriceX96 The price at which to compute the collateral value and requirements
    /// @return The combined collateral balance of `tokenData0` and `tokenData1` in terms of (currency0 if `price(currency1/currency0) < 1` and vice versa)
    /// @return The combined required collateral threshold of `tokenData0` and `tokenData1` in terms of (currency0 if `price(currency1/currency0) < 1` and vice versa)
    function getCrossBalances(
        LeftRightUnsigned tokenData0,
        LeftRightUnsigned tokenData1,
        uint160 sqrtPriceX96
    ) internal pure returns (uint256, uint256) {
        // convert values to the highest precision (lowest price) of the two tokens (currency0 if price currency1/currency0 < 1 and vice versa)
        if (sqrtPriceX96 < Constants.FP96) {
            return (
                tokenData0.rightSlot() +
                    PanopticMath.convert1to0(tokenData1.rightSlot(), sqrtPriceX96),
                tokenData0.leftSlot() +
                    PanopticMath.convert1to0RoundingUp(tokenData1.leftSlot(), sqrtPriceX96)
            );
        }

        return (
            PanopticMath.convert0to1(tokenData0.rightSlot(), sqrtPriceX96) + tokenData1.rightSlot(),
            PanopticMath.convert0to1RoundingUp(tokenData0.leftSlot(), sqrtPriceX96) +
                tokenData1.leftSlot()
        );
    }

    /// @notice Compute the notional value (for `tokenType = 0` and `tokenType = 1`) represented by a given leg in an option position.
    /// @param tokenId The option position identifier
    /// @param positionSize The number of option contracts held in this position (each contract can control multiple tokens)
    /// @param legIndex The leg index of the option contract, can be {0,1,2,3}
    /// @return A LeftRight encoded variable containing the amount0 and the amount1 value controlled by this option position's leg
    function getAmountsMoved(
        TokenId tokenId,
        uint128 positionSize,
        uint256 legIndex
    ) internal pure returns (LeftRightUnsigned) {
        uint128 amount0;
        uint128 amount1;

        (int24 tickLower, int24 tickUpper) = tokenId.asTicks(legIndex);

        // effective strike price of the option (avg. price over LP range)
        // geometric mean of two numbers = √(x1 * x2) = √x1 * √x2
        uint256 geometricMeanPriceX96 = Math.mulDiv96(
            Math.getSqrtRatioAtTick(tickLower),
            Math.getSqrtRatioAtTick(tickUpper)
        );

        if (tokenId.asset(legIndex) == 0) {
            amount0 = positionSize * uint128(tokenId.optionRatio(legIndex));
            amount1 = Math.mulDiv96RoundingUp(amount0, geometricMeanPriceX96).toUint128();
        } else {
            amount1 = positionSize * uint128(tokenId.optionRatio(legIndex));
            amount0 = Math.mulDivRoundingUp(amount1, 2 ** 96, geometricMeanPriceX96).toUint128();
        }

        return LeftRightUnsigned.wrap(amount0).toLeftSlot(amount1);
    }

    /// @notice Compute the amount of funds that are moved to or removed from the Panoptic Pool when `tokenId` is created.
    /// @param tokenId The option position identifier
    /// @param positionSize The number of positions minted
    /// @param legIndex The leg index minted in this position, can be {0,1,2,3}
    /// @return longs A LeftRight-packed word containing the total amount of long positions
    /// @return shorts A LeftRight-packed word containing the amount of short positions
    function _calculateIOAmounts(
        TokenId tokenId,
        uint128 positionSize,
        uint256 legIndex
    ) internal pure returns (LeftRightSigned longs, LeftRightSigned shorts) {
        LeftRightUnsigned amountsMoved = getAmountsMoved(tokenId, positionSize, legIndex);

        bool isShort = tokenId.isLong(legIndex) == 0;

        if (tokenId.tokenType(legIndex) == 0) {
            if (isShort) {
                // if option is short, increment shorts by contracts
                shorts = LeftRightSigned.wrap(0).toRightSlot(
                    Math.toInt128(amountsMoved.rightSlot())
                );
            } else {
                // is option is long, increment longs by contracts
                longs = LeftRightSigned.wrap(0).toRightSlot(
                    Math.toInt128(amountsMoved.rightSlot())
                );
            }
        } else {
            if (isShort) {
                // if option is short, increment shorts by notional
                shorts = LeftRightSigned.wrap(0).toLeftSlot(Math.toInt128(amountsMoved.leftSlot()));
            } else {
                // if option is long, increment longs by notional
                longs = LeftRightSigned.wrap(0).toLeftSlot(Math.toInt128(amountsMoved.leftSlot()));
            }
        }
    }

    /*//////////////////////////////////////////////////////////////
                LIQUIDATION/FORCE EXERCISE CALCULATIONS
    //////////////////////////////////////////////////////////////*/

    /// @notice Compute the pre-haircut liquidation bonuses to be paid to the liquidator and the protocol loss caused by the liquidation (pre-haircut).
    /// @param tokenData0 LeftRight encoded word with balance of currency0 in the right slot, and required balance in left slot
    /// @param tokenData1 LeftRight encoded word with balance of currency1 in the right slot, and required balance in left slot
    /// @param atSqrtPriceX96 The oracle price used to swap tokens between the liquidator/liquidatee and determine solvency for the liquidatee
    /// @param netPaid The net amount of tokens paid/received by the liquidatee to close their portfolio of positions
    /// @param shortPremium Total owed premium (prorated by available settled tokens) across all short legs being liquidated
    /// @return The LeftRight-packed bonus amounts to be paid to the liquidator for both tokens (may be negative)
    /// @return The LeftRight-packed protocol loss (pre-haircut) for both tokens, i.e., the delta between the user's starting balance and expended tokens
    function getLiquidationBonus(
        LeftRightUnsigned tokenData0,
        LeftRightUnsigned tokenData1,
        uint160 atSqrtPriceX96,
        LeftRightSigned netPaid,
        LeftRightUnsigned shortPremium
    ) external pure returns (LeftRightSigned, LeftRightSigned) {
        int256 bonus0;
        int256 bonus1;
        unchecked {
            // compute bonus as min(collateralBalance/2, required-collateralBalance)
            {
                // compute the ratio of currency0 to total collateral requirements
                // evaluate at TWAP price to maintain consistency with solvency calculations
                (uint256 balanceCross, uint256 thresholdCross) = PanopticMath.getCrossBalances(
                    tokenData0,
                    tokenData1,
                    atSqrtPriceX96
                );

                uint256 bonusCross = Math.min(balanceCross / 2, thresholdCross - balanceCross);

                // `bonusCross` and `thresholdCross` are returned in terms of the lowest-priced token
                if (atSqrtPriceX96 < Constants.FP96) {
                    // required0 / (required0 + currency0(required1))
                    uint256 requiredRatioX128 = Math.mulDiv(
                        tokenData0.leftSlot(),
                        2 ** 128,
                        thresholdCross
                    );

                    bonus0 = int256(Math.mulDiv128(bonusCross, requiredRatioX128));

                    bonus1 = int256(
                        PanopticMath.convert0to1(
                            Math.mulDiv128(bonusCross, 2 ** 128 - requiredRatioX128),
                            atSqrtPriceX96
                        )
                    );
                } else {
                    // required1 / (currency1(required0) + required1)
                    uint256 requiredRatioX128 = Math.mulDiv(
                        tokenData1.leftSlot(),
                        2 ** 128,
                        thresholdCross
                    );

                    bonus1 = int256(Math.mulDiv128(bonusCross, requiredRatioX128));

                    bonus0 = int256(
                        PanopticMath.convert1to0(
                            Math.mulDiv128(bonusCross, 2 ** 128 - requiredRatioX128),
                            atSqrtPriceX96
                        )
                    );
                }
            }

            // negative premium (owed to the liquidatee) is credited to the collateral balance
            // this is already present in the netPaid amount, so to avoid double-counting we remove it from the balance
            int256 balance0 = int256(uint256(tokenData0.rightSlot())) -
                int256(uint256(shortPremium.rightSlot()));
            int256 balance1 = int256(uint256(tokenData1.rightSlot())) -
                int256(uint256(shortPremium.leftSlot()));

            int256 paid0 = bonus0 + int256(netPaid.rightSlot());
            int256 paid1 = bonus1 + int256(netPaid.leftSlot());

            // note that "balance0" and "balance1" are the liquidatee's original balances before token delegation by a liquidator
            // their actual balances at the time of computation may be higher, but these are a buffer representing the amount of tokens we
            // have to work with before cutting into the liquidator's funds
            if (!(paid0 > balance0 && paid1 > balance1)) {
                // liquidatee cannot pay back the liquidator fully in either token, so no protocol loss can be avoided
                if ((paid0 > balance0)) {
                    // liquidatee has insufficient currency0 but some currency1 left over, so we use what they have left to mitigate currency0 losses
                    // we do this by substituting an equivalent value of currency1 in our refund to the liquidator, plus a bonus, for the currency0 we convert
                    // we want to convert the minimum amount of tokens required to achieve the lowest possible protocol loss (to avoid overpaying on the conversion bonus)
                    // the maximum level of protocol loss mitigation that can be achieved is the liquidatee's excess currency1 balance: balance1 - paid1
                    // and paid0 - balance0 is the amount of currency0 that the liquidatee is missing, i.e the protocol loss
                    // if the protocol loss is lower than the excess currency1 balance, then we can fully mitigate the loss and we should only convert the loss amount
                    // if the protocol loss is higher than the excess currency1 balance, we can only mitigate part of the loss, so we should convert only the excess currency1 balance
                    // thus, the value converted should be min(balance1 - paid1, paid0 - balance0)
                    bonus1 += Math.min(
                        balance1 - paid1,
                        PanopticMath.convert0to1(paid0 - balance0, atSqrtPriceX96)
                    );
                    bonus0 -= Math.min(
                        PanopticMath.convert1to0(balance1 - paid1, atSqrtPriceX96),
                        paid0 - balance0
                    );
                }
                if ((paid1 > balance1)) {
                    // liquidatee has insufficient currency1 but some currency0 left over, so we use what they have left to mitigate currency1 losses
                    // we do this by substituting an equivalent value of currency0 in our refund to the liquidator, plus a bonus, for the currency1 we convert
                    // we want to convert the minimum amount of tokens required to achieve the lowest possible protocol loss (to avoid overpaying on the conversion bonus)
                    // the maximum level of protocol loss mitigation that can be achieved is the liquidatee's excess currency0 balance: balance0 - paid0
                    // and paid1 - balance1 is the amount of currency1 that the liquidatee is missing, i.e the protocol loss
                    // if the protocol loss is lower than the excess currency0 balance, then we can fully mitigate the loss and we should only convert the loss amount
                    // if the protocol loss is higher than the excess currency0 balance, we can only mitigate part of the loss, so we should convert only the excess currency0 balance
                    // thus, the value converted should be min(balance0 - paid0, paid1 - balance1)
                    bonus0 += Math.min(
                        balance0 - paid0,
                        PanopticMath.convert1to0(paid1 - balance1, atSqrtPriceX96)
                    );
                    bonus1 -= Math.min(
                        PanopticMath.convert0to1(balance0 - paid0, atSqrtPriceX96),
                        paid1 - balance1
                    );
                }
            }

            paid0 = bonus0 + int256(netPaid.rightSlot());
            paid1 = bonus1 + int256(netPaid.leftSlot());
            return (
                LeftRightSigned.wrap(0).toRightSlot(int128(bonus0)).toLeftSlot(int128(bonus1)),
                LeftRightSigned.wrap(0).toRightSlot(int128(balance0 - paid0)).toLeftSlot(
                    int128(balance1 - paid1)
                )
            );
        }
    }

    /// @notice Haircut/clawback any premium paid by `liquidatee` on `positionIdList` over the protocol loss threshold during a liquidation.
    /// @dev Note that the storage mapping provided as the `settledTokens` parameter WILL be modified on the caller by this function.
    /// @param liquidatee The address of the user being liquidated
    /// @param positionIdList The list of position ids being liquidated
    /// @param premiasByLeg The premium paid (or received) by the liquidatee for each leg of each position
    /// @param collateralRemaining The remaining collateral after the liquidation (negative if protocol loss)
    /// @param atSqrtPriceX96 The oracle price used to swap tokens between the liquidator/liquidatee and determine solvency for the liquidatee
    /// @param collateral0 The collateral tracker for currency0
    /// @param collateral1 The collateral tracker for currency1
    /// @param settledTokens The per-chunk accumulator of settled tokens in storage from which to subtract the haircut premium
    /// @return The delta, if any, to apply to the existing liquidation bonus
    function haircutPremia(
        address liquidatee,
        TokenId[] memory positionIdList,
        LeftRightSigned[4][] memory premiasByLeg,
        LeftRightSigned collateralRemaining,
        CollateralTracker collateral0,
        CollateralTracker collateral1,
        uint160 atSqrtPriceX96,
        mapping(bytes32 chunkKey => LeftRightUnsigned settledTokens) storage settledTokens
    ) external returns (LeftRightSigned) {
        unchecked {
            // get the amount of premium paid by the liquidatee
            LeftRightSigned longPremium;
            for (uint256 i = 0; i < positionIdList.length; ++i) {
                TokenId tokenId = positionIdList[i];
                uint256 numLegs = tokenId.countLegs();
                for (uint256 leg = 0; leg < numLegs; ++leg) {
                    if (tokenId.isLong(leg) == 1) {
                        longPremium = longPremium.sub(premiasByLeg[i][leg]);
                    }
                }
            }
            // Ignore any surplus collateral - the liquidatee is either solvent or it converts to <1 unit of the other token
            int256 collateralDelta0 = -Math.min(collateralRemaining.rightSlot(), 0);
            int256 collateralDelta1 = -Math.min(collateralRemaining.leftSlot(), 0);
            LeftRightSigned haircutBase;

            // if the premium in the same token is not enough to cover the loss and there is a surplus of the other token,
            // the liquidator will provide the tokens (reflected in the bonus amount) & receive compensation in the other token
            if (
                longPremium.rightSlot() < collateralDelta0 &&
                longPremium.leftSlot() > collateralDelta1
            ) {
                int256 protocolLoss1 = collateralDelta1;
                (collateralDelta0, collateralDelta1) = (
                    -Math.min(
                        collateralDelta0 - longPremium.rightSlot(),
                        PanopticMath.convert1to0(
                            longPremium.leftSlot() - collateralDelta1,
                            atSqrtPriceX96
                        )
                    ),
                    Math.min(
                        longPremium.leftSlot() - collateralDelta1,
                        PanopticMath.convert0to1(
                            collateralDelta0 - longPremium.rightSlot(),
                            atSqrtPriceX96
                        )
                    )
                );

                // It is assumed the sum of `protocolLoss1` and `collateralDelta1` does not exceed `2^127 - 1` given practical constraints
                // on token supplies and deposit limits
                haircutBase = LeftRightSigned.wrap(longPremium.rightSlot()).toLeftSlot(
                    int128(protocolLoss1 + collateralDelta1)
                );
            } else if (
                longPremium.leftSlot() < collateralDelta1 &&
                longPremium.rightSlot() > collateralDelta0
            ) {
                int256 protocolLoss0 = collateralDelta0;
                (collateralDelta0, collateralDelta1) = (
                    Math.min(
                        longPremium.rightSlot() - collateralDelta0,
                        PanopticMath.convert1to0(
                            collateralDelta1 - longPremium.leftSlot(),
                            atSqrtPriceX96
                        )
                    ),
                    -Math.min(
                        collateralDelta1 - longPremium.leftSlot(),
                        PanopticMath.convert0to1(
                            longPremium.rightSlot() - collateralDelta0,
                            atSqrtPriceX96
                        )
                    )
                );

                // It is assumed the sum of `protocolLoss0` and `collateralDelta0` does not exceed `2^127 - 1` given practical constraints
                // on token supplies and deposit limits
                haircutBase = LeftRightSigned
                    .wrap(int128(protocolLoss0 + collateralDelta0))
                    .toLeftSlot(longPremium.leftSlot());
            } else {
                // for each token, haircut until the protocol loss is mitigated or the premium paid is exhausted
                // the size of `collateralDelta0/1` and `longPremium.rightSlot()/leftSlot()` is limited to `2^127 - 1` given that they originate from LeftRightSigned types
                haircutBase = LeftRightSigned
                    .wrap(int128(Math.min(collateralDelta0, longPremium.rightSlot())))
                    .toLeftSlot(int128(Math.min(collateralDelta1, longPremium.leftSlot())));

                collateralDelta0 = 0;
                collateralDelta1 = 0;
            }

            // total haircut after rounding up prorated haircut amounts for each leg
            LeftRightUnsigned haircutTotal;
            address _liquidatee = liquidatee;
            for (uint256 i = 0; i < positionIdList.length; i++) {
                TokenId tokenId = positionIdList[i];
                LeftRightSigned[4][] memory _premiasByLeg = premiasByLeg;
                for (uint256 leg = 0; leg < tokenId.countLegs(); ++leg) {
                    if (
                        tokenId.isLong(leg) == 1 &&
                        LeftRightSigned.unwrap(_premiasByLeg[i][leg]) != 0
                    ) {
                        // calculate prorated (by target/liquidity) haircut amounts to revoke from settled for each leg
                        // `-premiasByLeg[i][leg]` (and `longPremium` which is the sum of all -premiasByLeg[i][leg]`) is always positive because long premium is represented as a negative delta
                        // `haircutBase` is always positive because all of its possible constituent values (`collateralDelta`, `longPremium`) are guaranteed to be positive
                        // the sum of all prorated haircut amounts for each token is assumed to be less than `2^127 - 1` given practical constraints on token supplies and deposit limits

                        LeftRightSigned haircutAmounts = LeftRightSigned
                            .wrap(
                                int128(
                                    uint128(
                                        Math.unsafeDivRoundingUp(
                                            uint128(-_premiasByLeg[i][leg].rightSlot()) *
                                                uint256(uint128(haircutBase.rightSlot())),
                                            uint128(longPremium.rightSlot())
                                        )
                                    )
                                )
                            )
                            .toLeftSlot(
                                int128(
                                    uint128(
                                        Math.unsafeDivRoundingUp(
                                            uint128(-_premiasByLeg[i][leg].leftSlot()) *
                                                uint256(uint128(haircutBase.leftSlot())),
                                            uint128(longPremium.leftSlot())
                                        )
                                    )
                                )
                            );

                        haircutTotal = haircutTotal.add(
                            LeftRightUnsigned.wrap(uint256(LeftRightSigned.unwrap(haircutAmounts)))
                        );

                        emit PanopticPool.PremiumSettled(
                            _liquidatee,
                            tokenId,
                            leg,
                            LeftRightSigned.wrap(0).sub(haircutAmounts)
                        );

                        bytes32 chunkKey = keccak256(
                            abi.encodePacked(
                                tokenId.strike(leg),
                                tokenId.width(leg),
                                tokenId.tokenType(leg)
                            )
                        );

                        // The long premium is not committed to storage during the liquidation, so we add the entire adjusted amount
                        // for the haircut directly to the accumulator
                        settledTokens[chunkKey] = settledTokens[chunkKey].add(
                            (LeftRightSigned.wrap(0).sub(_premiasByLeg[i][leg])).subRect(
                                haircutAmounts
                            )
                        );
                    }
                }
            }

            if (haircutTotal.rightSlot() != 0)
                collateral0.exercise(_liquidatee, 0, 0, 0, int128(haircutTotal.rightSlot()));
            if (haircutTotal.leftSlot() != 0)
                collateral1.exercise(_liquidatee, 0, 0, 0, int128(haircutTotal.leftSlot()));

            return
                LeftRightSigned.wrap(0).toRightSlot(int128(collateralDelta0)).toLeftSlot(
                    int128(collateralDelta1)
                );
        }
    }

    /// @notice Substitutes surplus tokens to a caller in exchange for any potential token shortages prior to revoking virtual shares from a payor.
    /// @param payor The address of the user being exercised/settled
    /// @param fees If applicable, fees to debit from caller (rightSlot = currency0 left = currency1), 0 for `settleLongPremium`
    /// @param atTick The tick at which to convert between currency0/currency1 when redistributing the surplus tokens
    /// @param ct0 The collateral tracker for currency0
    /// @param ct1 The collateral tracker for currency1
    /// @return The LeftRight-packed deltas for currency0/currency1 to move from the caller to the payor
    function getRefundAmounts(
        address payor,
        LeftRightSigned fees,
        int24 atTick,
        CollateralTracker ct0,
        CollateralTracker ct1
    ) external view returns (LeftRightSigned) {
        uint160 sqrtPriceX96 = Math.getSqrtRatioAtTick(atTick);
        unchecked {
            // if the refunder lacks sufficient currency0 to pay back the virtual shares, have the caller cover the difference in exchange for currency1 (and vice versa)

            int256 balanceShortage = int256(uint256(type(uint248).max)) -
                int256(ct0.balanceOf(payor)) -
                int256(ct0.convertToShares(uint128(-fees.rightSlot())));

            if (balanceShortage > 0) {
                return
                    LeftRightSigned
                        .wrap(0)
                        .toRightSlot(
                            int128(
                                fees.rightSlot() -
                                    int256(
                                        Math.mulDivRoundingUp(
                                            uint256(balanceShortage),
                                            ct0.totalAssets(),
                                            ct0.totalSupply()
                                        )
                                    )
                            )
                        )
                        .toLeftSlot(
                            int128(
                                int256(
                                    PanopticMath.convert0to1RoundingUp(
                                        ct0.convertToAssets(uint256(balanceShortage)),
                                        sqrtPriceX96
                                    )
                                ) + fees.leftSlot()
                            )
                        );
            }

            balanceShortage =
                int256(uint256(type(uint248).max)) -
                int256(ct1.balanceOf(payor)) -
                int256(ct1.convertToShares(uint128(-fees.leftSlot())));
            if (balanceShortage > 0) {
                return
                    LeftRightSigned
                        .wrap(0)
                        .toRightSlot(
                            int128(
                                int256(
                                    PanopticMath.convert1to0RoundingUp(
                                        ct1.convertToAssets(uint256(balanceShortage)),
                                        sqrtPriceX96
                                    )
                                ) + fees.rightSlot()
                            )
                        )
                        .toLeftSlot(
                            int128(
                                fees.leftSlot() -
                                    int256(
                                        Math.mulDivRoundingUp(
                                            uint256(balanceShortage),
                                            ct1.totalAssets(),
                                            ct1.totalSupply()
                                        )
                                    )
                            )
                        );
            }
        }

        // otherwise, no need to deviate from the original deltas
        return fees;
    }
}

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// Uniswap V4 interfaces
import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol";
// Uniswap V4 libraries
import {StateLibrary} from "v4-core/libraries/StateLibrary.sol";
// Uniswap V4 types
import {PoolId} from "v4-core/types/PoolId.sol";

/// @notice A library to retrieve state information from Uniswap V4 pools via `extsload`.
/// @author Axicon Labs Limited, credit to Uniswap Labs under MIT License
library V4StateReader {
    /// @notice Retrieves the current `sqrtPriceX96` from a Uniswap V4 pool.
    /// @param manager The Uniswap V4 pool manager contract
    /// @param poolId The pool ID of the Uniswap V4 pool
    /// @return sqrtPriceX96 The current `sqrtPriceX96` of the Uniswap V4 pool
    function getSqrtPriceX96(
        IPoolManager manager,
        PoolId poolId
    ) internal view returns (uint160 sqrtPriceX96) {
        bytes32 stateSlot = StateLibrary._getPoolStateSlot(poolId);
        bytes32 data = manager.extsload(stateSlot);

        assembly ("memory-safe") {
            sqrtPriceX96 := and(data, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)
        }
    }

    /// @notice Retrieves the current tick from a Uniswap V4 pool.
    /// @param manager The Uniswap V4 pool manager contract
    /// @param poolId The pool ID of the Uniswap V4 pool
    /// @return tick The current tick of the Uniswap V4 pool
    function getTick(IPoolManager manager, PoolId poolId) internal view returns (int24 tick) {
        bytes32 stateSlot = StateLibrary._getPoolStateSlot(poolId);
        bytes32 data = manager.extsload(stateSlot);

        assembly ("memory-safe") {
            tick := signextend(2, shr(160, data))
        }
    }

    /// @notice Calculates the fee growth that has occurred (per unit of liquidity) in the AMM/Uniswap for an
    /// option position's tick range.
    /// @param manager The Uniswap V4 pool manager contract
    /// @param idV4 The pool ID of the Uniswap V4 pool
    /// @param currentTick The current price tick in the AMM
    /// @param tickLower The lower tick of the option position leg (a liquidity chunk)
    /// @param tickUpper The upper tick of the option position leg (a liquidity chunk)
    /// @return feeGrowthInside0X128 The fee growth in the AMM for currency0
    /// @return feeGrowthInside1X128 The fee growth in the AMM for currency1
    function getFeeGrowthInside(
        IPoolManager manager,
        PoolId idV4,
        int24 currentTick,
        int24 tickLower,
        int24 tickUpper
    ) internal view returns (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) {
        // Get feesGrowths from the option position's lower+upper ticks
        // lowerOut0: For currency0: fee growth per unit of liquidity on the _other_ side of tickLower (relative to currentTick)
        // only has relative meaning, not absolute — the value depends on when the tick is initialized
        // (...)
        // upperOut1: For currency1: fee growth on the _other_ side of tickUpper (again: relative to currentTick)
        // the point is: the range covered by lowerOut0 changes depending on where currentTick is.
        (uint256 lowerOut0, uint256 lowerOut1) = StateLibrary.getTickFeeGrowthOutside(
            manager,
            idV4,
            tickLower
        );
        (uint256 upperOut0, uint256 upperOut1) = StateLibrary.getTickFeeGrowthOutside(
            manager,
            idV4,
            tickUpper
        );

        // compute the effective feeGrowth, depending on whether price is above/below/within range
        unchecked {
            if (currentTick < tickLower) {
                /**
                  Diagrams shown for currency0, and applies for currency1 the same
                  L = lowerTick, U = upperTick

                    liquidity         lowerOut0 (all fees collected in this price tick range for currency0)
                        ▲            ◄──────────────^v───► (to MAX_TICK)
                        │
                        │                      upperOut0
                        │                     ◄─────^v───►
                        │           ┌────────┐
                        │           │ chunk  │
                        │           │        │
                        └─────▲─────┴────────┴────────► price tick
                              │     L        U
                              │
                           current
                            tick
                */
                feeGrowthInside0X128 = lowerOut0 - upperOut0; // fee growth inside the chunk
                feeGrowthInside1X128 = lowerOut1 - upperOut1;
            } else if (currentTick >= tickUpper) {
                /**
                    liquidity
                        ▲           upperOut0
                        │◄─^v─────────────────────►
                        │
                        │     lowerOut0  ┌────────┐
                        │◄─^v───────────►│ chunk  │
                        │                │        │
                        └────────────────┴────────┴─▲─────► price tick
                                         L        U │
                                                    │
                                                 current
                                                  tick
                 */
                feeGrowthInside0X128 = upperOut0 - lowerOut0;
                feeGrowthInside1X128 = upperOut1 - lowerOut1;
            } else {
                /**
                  current AMM tick is within the option position range (within the chunk)

                     liquidity
                        ▲        feeGrowthGlobal0X128 = global fee growth
                        │                             = (all fees collected for the entire price range for currency0)
                        │
                        │
                        │     lowerOut0  ┌──────────────┐ upperOut0
                        │◄─^v───────────►│              │◄─────^v───►
                        │                │     chunk    │
                        │                │              │
                        └────────────────┴───────▲──────┴─────► price tick
                                         L       │      U
                                                 │
                                              current
                                               tick
                */

                (uint256 feeGrowthGlobal0X128, uint256 feeGrowthGlobal1X128) = StateLibrary
                    .getFeeGrowthGlobals(manager, idV4);
                feeGrowthInside0X128 = feeGrowthGlobal0X128 - lowerOut0 - upperOut0;
                feeGrowthInside1X128 = feeGrowthGlobal1X128 - lowerOut1 - upperOut1;
            }
        }
    }

    /// @notice Retrieves the last stored `feeGrowthInsideLast` values for a unique Uniswap V4 position.
    /// @dev Corresponds to pools[poolId].positions[positionId] in `manager`.
    /// @param manager The Uniswap V4 pool manager contract
    /// @param poolId The ID of the Uniswap V4 pool
    /// @param positionId The ID of the position, which is a hash of the owner, tickLower, tickUpper, and salt
    /// @return feeGrowthInside0LastX128 The fee growth inside the position for currency0
    /// @return feeGrowthInside1LastX128 The fee growth inside the position for currency1
    function getFeeGrowthInsideLast(
        IPoolManager manager,
        PoolId poolId,
        bytes32 positionId
    ) internal view returns (uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128) {
        bytes32 slot = StateLibrary._getPositionInfoSlot(poolId, positionId);

        // read all 3 words of the Position.State struct
        bytes32[] memory data = manager.extsload(slot, 3);

        assembly ("memory-safe") {
            feeGrowthInside0LastX128 := mload(add(data, 64))
            feeGrowthInside1LastX128 := mload(add(data, 96))
        }
    }
}

// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.24;

// Libraries
import {Errors} from "@libraries/Errors.sol";
import {Math} from "@libraries/Math.sol";

type LeftRightUnsigned is uint256;
using LeftRightLibrary for LeftRightUnsigned global;

type LeftRightSigned is int256;
using LeftRightLibrary for LeftRightSigned global;

/// @title Pack two separate data (each of 128bit) into a single 256-bit slot; 256bit-to-128bit packing methods.
/// @author Axicon Labs Limited
/// @notice Simple data type that divides a 256-bit word into two 128-bit slots.
library LeftRightLibrary {
    using Math for uint256;

    /// @notice AND bitmask to isolate the left half of a uint256.
    uint256 internal constant LEFT_HALF_BIT_MASK =
        0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000;

    /// @notice AND bitmask to isolate the left half of an int256.
    int256 internal constant LEFT_HALF_BIT_MASK_INT =
        int256(uint256(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000));

    /// @notice AND bitmask to isolate the right half of an int256.
    int256 internal constant RIGHT_HALF_BIT_MASK = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF;

    /*//////////////////////////////////////////////////////////////
                               RIGHT SLOT
    //////////////////////////////////////////////////////////////*/

    /// @notice Get the "right" slot from a bit pattern.
    /// @param self The 256 bit value to extract the right half from
    /// @return The right half of `self`
    function rightSlot(LeftRightUnsigned self) internal pure returns (uint128) {
        return uint128(LeftRightUnsigned.unwrap(self));
    }

    /// @notice Get the "right" slot from a bit pattern.
    /// @param self The 256 bit value to extract the right half from
    /// @return The right half of `self`
    function rightSlot(LeftRightSigned self) internal pure returns (int128) {
        return int128(LeftRightSigned.unwrap(self));
    }

    // All toRightSlot functions add bits to the right slot without clearing it first
    // Typically, the slot is already clear when writing to it, but if it is not, the bits will be added to the existing bits
    // Therefore, the assumption must not be made that the bits will be cleared while using these helpers
    // Note that the values *within* the slots are allowed to overflow, but overflows are contained and will not leak into the other slot

    /// @notice Add to the "right" slot in a 256-bit pattern.
    /// @param self The 256-bit pattern to be written to
    /// @param right The value to be added to the right slot
    /// @return `self` with `right` added (not overwritten, but added) to the value in its right 128 bits
    function toRightSlot(
        LeftRightUnsigned self,
        uint128 right
    ) internal pure returns (LeftRightUnsigned) {
        unchecked {
            // prevent the right slot from leaking into the left one in the case of an overflow
            // ff + 1 = (1)00, but we want just ff + 1 = 00
            return
                LeftRightUnsigned.wrap(
                    (LeftRightUnsigned.unwrap(self) & LEFT_HALF_BIT_MASK) +
                        uint256(uint128(LeftRightUnsigned.unwrap(self)) + right)
                );
        }
    }

    /// @notice Add to the "right" slot in a 256-bit pattern.
    /// @param self The 256-bit pattern to be written to
    /// @param right The value to be added to the right slot
    /// @return `self` with `right` added (not overwritten, but added) to the value in its right 128 bits
    function toRightSlot(
        LeftRightSigned self,
        int128 right
    ) internal pure returns (LeftRightSigned) {
        // bit mask needed in case rightHalfBitPattern < 0 due to 2's complement
        unchecked {
            // prevent the right slot from leaking into the left one in the case of a positive sign change
            // ff + 1 = (1)00, but we want just ff + 1 = 00
            return
                LeftRightSigned.wrap(
                    (LeftRightSigned.unwrap(self) & LEFT_HALF_BIT_MASK_INT) +
                        (int256(int128(LeftRightSigned.unwrap(self)) + right) & RIGHT_HALF_BIT_MASK)
                );
        }
    }

    /*//////////////////////////////////////////////////////////////
                               LEFT SLOT
    //////////////////////////////////////////////////////////////*/

    /// @notice Get the "left" slot from a bit pattern.
    /// @param self The 256 bit value to extract the left half from
    /// @return The left half of `self`
    function leftSlot(LeftRightUnsigned self) internal pure returns (uint128) {
        return uint128(LeftRightUnsigned.unwrap(self) >> 128);
    }

    /// @notice Get the "left" slot from a bit pattern.
    /// @param self The 256 bit value to extract the left half from
    /// @return The left half of `self`
    function leftSlot(LeftRightSigned self) internal pure returns (int128) {
        return int128(LeftRightSigned.unwrap(self) >> 128);
    }

    /// All toLeftSlot functions add bits to the left slot without clearing it first
    // Typically, the slot is already clear when writing to it, but if it is not, the bits will be added to the existing bits
    // Therefore, the assumption must not be made that the bits will be cleared while using these helpers
    // Note that the values *within* the slots are allowed to overflow, but overflows are contained and will not leak into the other slot

    /// @notice Add to the "left" slot in a 256-bit pattern.
    /// @param self The 256-bit pattern to be written to
    /// @param left The value to be added to the left slot
    /// @return `self` with `left` added (not overwritten, but added) to the value in its left 128 bits
    function toLeftSlot(
        LeftRightUnsigned self,
        uint128 left
    ) internal pure returns (LeftRightUnsigned) {
        unchecked {
            return LeftRightUnsigned.wrap(LeftRightUnsigned.unwrap(self) + (uint256(left) << 128));
        }
    }

    /// @notice Add to the "left" slot in a 256-bit pattern.
    /// @param self The 256-bit pattern to be written to
    /// @param left The value to be added to the left slot
    /// @return `self` with `left` added (not overwritten, but added) to the value in its left 128 bits
    function toLeftSlot(LeftRightSigned self, int128 left) internal pure returns (LeftRightSigned) {
        unchecked {
            return LeftRightSigned.wrap(LeftRightSigned.unwrap(self) + (int256(left) << 128));
        }
    }

    /*//////////////////////////////////////////////////////////////
                             MATH FUNCTIONS
    //////////////////////////////////////////////////////////////*/

    /// @notice Add two LeftRight-encoded words; revert on overflow or underflow.
    /// @param x The augend
    /// @param y The addend
    /// @return z The sum `x + y`
    function add(
        LeftRightUnsigned x,
        LeftRightUnsigned y
    ) internal pure returns (LeftRightUnsigned z) {
        unchecked {
            // adding leftRight packed uint128's is same as just adding the values explicitly
            // given that we check for overflows of the left and right values
            z = LeftRightUnsigned.wrap(LeftRightUnsigned.unwrap(x) + LeftRightUnsigned.unwrap(y));

            // on overflow z will be less than either x or y
            // type cast z to uint128 to isolate the right slot and if it's lower than a value it's comprised of (x)
            // then an overflow has occurred
            if (
                LeftRightUnsigned.unwrap(z) < LeftRightUnsigned.unwrap(x) ||
                (uint128(LeftRightUnsigned.unwrap(z)) < uint128(LeftRightUnsigned.unwrap(x)))
            ) revert Errors.UnderOverFlow();
        }
    }

    /// @notice Subtract two LeftRight-encoded words; revert on overflow or underflow.
    /// @param x The minuend
    /// @param y The subtrahend
    /// @return z The difference `x - y`
    function sub(
        LeftRightUnsigned x,
        LeftRightUnsigned y
    ) internal pure returns (LeftRightUnsigned z) {
        unchecked {
            // subtracting leftRight packed uint128's is same as just subtracting the values explicitly
            // given that we check for underflows of the left and right values
            z = LeftRightUnsigned.wrap(LeftRightUnsigned.unwrap(x) - LeftRightUnsigned.unwrap(y));

            // on underflow z will be greater than either x or y
            // type cast z to uint128 to isolate the right slot and if it's higher than a value that was subtracted from (x)
            // then an underflow has occurred
            if (
                LeftRightUnsigned.unwrap(z) > LeftRightUnsigned.unwrap(x) ||
                (uint128(LeftRightUnsigned.unwrap(z)) > uint128(LeftRightUnsigned.unwrap(x)))
            ) revert Errors.UnderOverFlow();
        }
    }

    /// @notice Add two LeftRight-encoded words; revert on overflow or underflow.
    /// @param x The augend
    /// @param y The addend
    /// @return z The sum `x + y`
    function add(LeftRightUnsigned x, LeftRightSigned y) internal pure returns (LeftRightSigned z) {
        unchecked {
            int256 left = int256(uint256(x.leftSlot())) + y.leftSlot();
            int128 left128 = int128(left);

            if (left128 != left) revert Errors.UnderOverFlow();

            int256 right = int256(uint256(x.rightSlot())) + y.rightSlot();
            int128 right128 = int128(right);

            if (right128 != right) revert Errors.UnderOverFlow();

            return z.toRightSlot(right128).toLeftSlot(left128);
        }
    }

    /// @notice Add two LeftRight-encoded words; revert on overflow or underflow.
    /// @param x The augend
    /// @param y The addend
    /// @return z The sum `x + y`
    function add(LeftRightSigned x, LeftRightSigned y) internal pure returns (LeftRightSigned z) {
        unchecked {
            int256 left256 = int256(x.leftSlot()) + y.leftSlot();
            int128 left128 = int128(left256);

            int256 right256 = int256(x.rightSlot()) + y.rightSlot();
            int128 right128 = int128(right256);

            if (left128 != left256 || right128 != right256) revert Errors.UnderOverFlow();

            return z.toRightSlot(right128).toLeftSlot(left128);
        }
    }

    /// @notice Subtract two LeftRight-encoded words; revert on overflow or underflow.
    /// @param x The minuend
    /// @param y The subtrahend
    /// @return z The difference `x - y`
    function sub(LeftRightSigned x, LeftRightSigned y) internal pure returns (LeftRightSigned z) {
        unchecked {
            int256 left256 = int256(x.leftSlot()) - y.leftSlot();
            int128 left128 = int128(left256);

            int256 right256 = int256(x.rightSlot()) - y.rightSlot();
            int128 right128 = int128(right256);

            if (left128 != left256 || right128 != right256) revert Errors.UnderOverFlow();

            return z.toRightSlot(right128).toLeftSlot(left128);
        }
    }

    /// @notice Subtract two LeftRight-encoded words; revert on overflow or underflow.
    /// @param x The minuend
    /// @param y The subtrahend
    /// @return z The difference `x - y`
    function sub(LeftRightSigned x, LeftRightUnsigned y) internal pure returns (LeftRightSigned z) {
        unchecked {
            int256 left256 = int256(x.leftSlot()) - int256(uint256(y.leftSlot()));
            int128 left128 = int128(left256);

            int256 right256 = int256(x.rightSlot()) - int256(uint256(y.rightSlot()));
            int128 right128 = int128(right256);

            if (left128 != left256 || right128 != right256) revert Errors.UnderOverFlow();

            return z.toRightSlot(right128).toLeftSlot(left128);
        }
    }

    /// @notice Subtract two LeftRight-encoded words; revert on overflow or underflow.
    /// @notice For each slot, rectify difference `x - y` to 0 if negative.
    /// @param x The minuend
    /// @param y The subtrahend
    /// @return z The difference `x - y`
    function subRect(
        LeftRightSigned x,
        LeftRightSigned y
    ) internal pure returns (LeftRightUnsigned z) {
        unchecked {
            int256 left256 = int256(x.leftSlot()) - y.leftSlot();
            int128 left128 = int128(left256);

            int256 right256 = int256(x.rightSlot()) - y.rightSlot();
            int128 right128 = int128(right256);

            if (left128 != left256 || right128 != right256) revert Errors.UnderOverFlow();

            return
                z.toRightSlot(uint128(uint256((Math.max(right128, 0))))).toLeftSlot(
                    uint128(uint256((Math.max(left128, 0))))
                );
        }
    }

    /// @notice Adds two sets of LeftRight-encoded words, freezing both right slots if either overflows, and vice versa.
    /// @dev Used for linked accumulators, so if the accumulator for one side overflows for a token, both cease to accumulate.
    /// @param x The first augend
    /// @param dx The addend for `x`
    /// @param y The second augend
    /// @param dy The addend for `y`
    /// @return The sum `x + dx`
    /// @return The sum `y + dy`
    function addCapped(
        LeftRightUnsigned x,
        LeftRightUnsigned dx,
        LeftRightUnsigned y,
        LeftRightUnsigned dy
    ) internal pure returns (LeftRightUnsigned, LeftRightUnsigned) {
        uint128 z_xR = (uint256(x.rightSlot()) + dx.rightSlot()).toUint128Capped();
        uint128 z_xL = (uint256(x.leftSlot()) + dx.leftSlot()).toUint128Capped();
        uint128 z_yR = (uint256(y.rightSlot()) + dy.rightSlot()).toUint128Capped();
        uint128 z_yL = (uint256(y.leftSlot()) + dy.leftSlot()).toUint128Capped();

        bool r_Enabled = !(z_xR == type(uint128).max || z_yR == type(uint128).max);
        bool l_Enabled = !(z_xL == type(uint128).max || z_yL == type(uint128).max);

        return (
            LeftRightUnsigned.wrap(r_Enabled ? z_xR : x.rightSlot()).toLeftSlot(
                l_Enabled ? z_xL : x.leftSlot()
            ),
            LeftRightUnsigned.wrap(r_Enabled ? z_yR : y.rightSlot()).toLeftSlot(
                l_Enabled ? z_yL : y.leftSlot()
            )
        );
    }
}

// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.24;

type LiquidityChunk is uint256;
using LiquidityChunkLibrary for LiquidityChunk global;

/// @title A Panoptic Liquidity Chunk. Tracks Tick Range and Liquidity Information for a "chunk." Used to track movement of chunks.
/// @author Axicon Labs Limited
///
/// @notice A liquidity chunk is an amount of `liquidity` deployed between two ticks: `tickLower` and `tickUpper`
/// into a concentrated liquidity AMM.
//
//                liquidity
//                    ▲      liquidity chunk
//                    │        │
//                    │    ┌───▼────┐   ▲
//                    │    │        │   │ liquidity/size
//      Other AMM     │  ┌─┴────────┴─┐ ▼ of chunk
//      liquidity  ───┼──┼─►          │
//                    │  │            │
//                    └──┴─▲────────▲─┴──► price ticks
//                         │        │
//                         │        │
//                    tickLower     │
//                              tickUpper
//
// PACKING RULES FOR A LIQUIDITYCHUNK:
// =================================================================================================
//  From the LSB to the MSB:
// (1) Liquidity        128bits  : The liquidity within the chunk (uint128).
// ( ) (Zero-bits)       80bits  : Zero-bits to match a total uint256.
// (2) tick Upper        24bits  : The upper tick of the chunk (int24).
// (3) tick Lower        24bits  : The lower tick of the chunk (int24).
// Total                256bits  : Total bits used by a chunk.
// ===============================================================================================
//
// The bit pattern is therefore:
//
//           (3)             (2)             ( )                (1)
//    <-- 24 bits -->  <-- 24 bits -->  <-- 80 bits -->   <-- 128 bits -->
//        tickLower       tickUpper         Zeros             Liquidity
//
//        <--- most significant bit        least significant bit --->
//
library LiquidityChunkLibrary {
    /// @notice AND mask to strip the `tickLower` value from a packed LiquidityChunk.
    uint256 internal constant CLEAR_TL_MASK =
        0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF;

    /// @notice AND mask to strip the `tickUpper` value from a packed LiquidityChunk.
    uint256 internal constant CLEAR_TU_MASK =
        0xFFFFFF000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF;

    /*//////////////////////////////////////////////////////////////
                                ENCODING
    //////////////////////////////////////////////////////////////*/

    /// @notice Create a new `LiquidityChunk` given by its bounding ticks and its liquidity.
    /// @param _tickLower The lower tick of the chunk
    /// @param _tickUpper The upper tick of the chunk
    /// @param amount The amount of liquidity to add to the chunk
    /// @return The new chunk with the given liquidity and tick range
    function createChunk(
        int24 _tickLower,
        int24 _tickUpper,
        uint128 amount
    ) internal pure returns (LiquidityChunk) {
        unchecked {
            return
                LiquidityChunk.wrap(
                    (uint256(uint24(_tickLower)) << 232) +
                        (uint256(uint24(_tickUpper)) << 208) +
                        uint256(amount)
                );
        }
    }

    /// @notice Add liquidity to `self`.
    /// @param self The LiquidityChunk to add liquidity to
    /// @param amount The amount of liquidity to add to `self`
    /// @return `self` with added liquidity `amount`
    function addLiquidity(
        LiquidityChunk self,
        uint128 amount
    ) internal pure returns (LiquidityChunk) {
        unchecked {
            return LiquidityChunk.wrap(LiquidityChunk.unwrap(self) + amount);
        }
    }

    /// @notice Add the lower tick to `self`.
    /// @param self The LiquidityChunk to add the lower tick to
    /// @param _tickLower The lower tick to add to `self`
    /// @return `self` with added lower tick `_tickLower`
    function addTickLower(
        LiquidityChunk self,
        int24 _tickLower
    ) internal pure returns (LiquidityChunk) {
        unchecked {
            return
                LiquidityChunk.wrap(
                    LiquidityChunk.unwrap(self) + (uint256(uint24(_tickLower)) << 232)
                );
        }
    }

    /// @notice Add the upper tick to `self`.
    /// @param self The LiquidityChunk to add the upper tick to
    /// @param _tickUpper The upper tick to add to `self`
    /// @return `self` with added upper tick `_tickUpper`
    function addTickUpper(
        LiquidityChunk self,
        int24 _tickUpper
    ) internal pure returns (LiquidityChunk) {
        unchecked {
            return
                LiquidityChunk.wrap(
                    LiquidityChunk.unwrap(self) + ((uint256(uint24(_tickUpper))) << 208)
                );
        }
    }

    /// @notice Overwrites the lower tick on `self`.
    /// @param self The LiquidityChunk to overwrite the lower tick on
    /// @param _tickLower The lower tick to overwrite `self` with
    /// @return `self` with `_tickLower` as the new lower tick
    function updateTickLower(
        LiquidityChunk self,
        int24 _tickLower
    ) internal pure returns (LiquidityChunk) {
        unchecked {
            return
                LiquidityChunk.wrap(LiquidityChunk.unwrap(self) & CLEAR_TL_MASK).addTickLower(
                    _tickLower
                );
        }
    }

    /// @notice Overwrites the upper tick on `self`.
    /// @param self The LiquidityChunk to overwrite the upper tick on
    /// @param _tickUpper The upper tick to overwrite `self` with
    /// @return `self` with `_tickUpper` as the new upper tick
    function updateTickUpper(
        LiquidityChunk self,
        int24 _tickUpper
    ) internal pure returns (LiquidityChunk) {
        unchecked {
            return
                LiquidityChunk.wrap(LiquidityChunk.unwrap(self) & CLEAR_TU_MASK).addTickUpper(
                    _tickUpper
                );
        }
    }

    /*//////////////////////////////////////////////////////////////
                                DECODING
    //////////////////////////////////////////////////////////////*/

    /// @notice Get the lower tick of `self`.
    /// @param self The LiquidityChunk to get the lower tick from
    /// @return The lower tick of `self`
    function tickLower(LiquidityChunk self) internal pure returns (int24) {
        unchecked {
            return int24(int256(LiquidityChunk.unwrap(self) >> 232));
        }
    }

    /// @notice Get the upper tick of `self`.
    /// @param self The LiquidityChunk to get the upper tick from
    /// @return The upper tick of `self`
    function tickUpper(LiquidityChunk self) internal pure returns (int24) {
        unchecked {
            return int24(int256(LiquidityChunk.unwrap(self) >> 208));
        }
    }

    /// @notice Get the amount of liquidity/size of `self`.
    /// @param self The LiquidityChunk to get the liquidity from
    /// @return The liquidity of `self`
    function liquidity(LiquidityChunk self) internal pure returns (uint128) {
        unchecked {
            return uint128(LiquidityChunk.unwrap(self));
        }
    }
}

File 15 of 50 : PoolKey.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {Currency} from "./Currency.sol";
import {IHooks} from "../interfaces/IHooks.sol";
import {PoolIdLibrary} from "./PoolId.sol";

using PoolIdLibrary for PoolKey global;

/// @notice Returns the key for identifying a pool
struct PoolKey {
    /// @notice The lower currency of the pool, sorted numerically
    Currency currency0;
    /// @notice The higher currency of the pool, sorted numerically
    Currency currency1;
    /// @notice The pool LP fee, capped at 1_000_000. If the highest bit is 1, the pool has a dynamic fee and must be exactly equal to 0x800000
    uint24 fee;
    /// @notice Ticks that involve positions must be a multiple of tick spacing
    int24 tickSpacing;
    /// @notice The hooks of the pool
    IHooks hooks;
}

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {PoolKey} from "./PoolKey.sol";

type PoolId is bytes32;

/// @notice Library for computing the ID of a pool
library PoolIdLibrary {
    /// @notice Returns value equal to keccak256(abi.encode(poolKey))
    function toId(PoolKey memory poolKey) internal pure returns (PoolId poolId) {
        assembly ("memory-safe") {
            // 0xa0 represents the total size of the poolKey struct (5 slots of 32 bytes)
            poolId := keccak256(poolKey, 0xa0)
        }
    }
}

// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.24;

type PositionBalance is uint256;
using PositionBalanceLibrary for PositionBalance global;

/// @title A Panoptic Position Balance. Tracks the Position Size, the Pool Utilizations at mint, and the current/fastOracle/slowOracle/latestObserved ticks at mint.
/// @author Axicon Labs Limited
//
//
// PACKING RULES FOR A POSITIONBALANCE:
// =================================================================================================
//  From the LSB to the MSB:
// (1) positionSize     128bits : The size of this position (uint128).
// (2) poolUtilization0 16bits  : The pool utilization of currency0, stored as (10000 * inAMM0)/totalAssets0 (uint16).
// (3) poolUtilization1 16bits  : The pool utilization of currency1, stored as (10000 * inAMM1)/totalAssets1 (uint16).
// (4) currentTick      24bits  : The currentTick at mint (int24).
// (5) fastOracleTick   24bits  : The fastOracleTick at mint (int24).
// (6) slowOracleTick   24bits  : The slowOracleTick at mint (int24).
// (7) lastObservedTick 24bits  : The lastObservedTick at mint (int24).
// Total                256bits : Total bits used by a PositionBalance.
// ===============================================================================================
//
// The bit pattern is therefore:
//
//           (7)             (6)            (5)             (4)             (3)             (2)             (1)
//    <-- 24 bits --> <-- 24 bits --> <-- 24 bits --> <-- 24 bits --> <-- 16 bits --> <-- 16 bits --> <-- 128 bits -->
//   lastObservedTick  slowOracleTick  fastOracleTick   currentTick     utilization1    utilization0    positionSize
//
//    <--- most significant bit                                                             least significant bit --->
//
library PositionBalanceLibrary {
    /*//////////////////////////////////////////////////////////////
                                ENCODING
    //////////////////////////////////////////////////////////////*/

    /// @notice Create a new `PositionBalance` given by positionSize, utilizations, and its tickData.
    /// @param _positionSize The amount of option minted
    /// @param _utilizations Packed data containing pool utilizations for token0 and token1 at mint
    /// @param _tickData Packed data containing ticks at mint (currentTick, fastOracleTick, slowOracleTick, lastObservedTick)
    /// @return The new PositionBalance with the given positionSize, utilization, and tickData
    function storeBalanceData(
        uint128 _positionSize,
        uint32 _utilizations,
        uint96 _tickData
    ) internal pure returns (PositionBalance) {
        unchecked {
            return
                PositionBalance.wrap(
                    (uint256(_tickData) << 160) +
                        (uint256(_utilizations) << 128) +
                        uint256(_positionSize)
                );
        }
    }

    /// @notice Concatenate all oracle ticks into a single uint96.
    /// @param _currentTick The current tick
    /// @param _fastOracleTick The fast oracle tick
    /// @param _slowOracleTick The slow oracle tick
    /// @param _lastObservedTick The last observed tick
    /// @return A 96bit word concatenating all 4 input ticks
    function packTickData(
        int24 _currentTick,
        int24 _fastOracleTick,
        int24 _slowOracleTick,
        int24 _lastObservedTick
    ) internal pure returns (uint96) {
        unchecked {
            return
                uint96(uint24(_currentTick)) +
                (uint96(uint24(_fastOracleTick)) << 24) +
                (uint96(uint24(_slowOracleTick)) << 48) +
                (uint96(uint24(_lastObservedTick)) << 72);
        }
    }

    /*//////////////////////////////////////////////////////////////
                                DECODING
    //////////////////////////////////////////////////////////////*/

    /// @notice Get the last observed tick of `self`.
    /// @param self The PositionBalance to retrieve the last observed tick from
    /// @return The last observed tick of `self`
    function lastObservedTick(PositionBalance self) internal pure returns (int24) {
        unchecked {
            return int24(int256(PositionBalance.unwrap(self) >> 232));
        }
    }

    /// @notice Get the slow oracle tick of `self`.
    /// @param self The PositionBalance to retrieve the slow oracle tick from
    /// @return The slow oracle tick of `self`
    function slowOracleTick(PositionBalance self) internal pure returns (int24) {
        unchecked {
            return int24(int256(PositionBalance.unwrap(self) >> 208));
        }
    }

    /// @notice Get the fast oracle tick of `self`.
    /// @param self The PositionBalance to retrieve the fast oracle tick from
    /// @return The fast oracle tick of `self`
    function fastOracleTick(PositionBalance self) internal pure returns (int24) {
        unchecked {
            return int24(int256(PositionBalance.unwrap(self) >> 184));
        }
    }

    /// @notice Get the current tick of `self`.
    /// @param self The PositionBalance to retrieve the current tick from
    /// @return The current tick of `self`
    function currentTick(PositionBalance self) internal pure returns (int24) {
        unchecked {
            return int24(int256(PositionBalance.unwrap(self) >> 160));
        }
    }

    /// @notice Get the tickData of `self`.
    /// @param self The PositionBalance to retrieve the tickData from
    /// @return The packed tickData (currentTick, fastOracleTick, slowOracleTick, lastObservedTick)
    function tickData(PositionBalance self) internal pure returns (uint96) {
        unchecked {
            return uint96(PositionBalance.unwrap(self) >> 160);
        }
    }

    /// @notice Unpack the current, last observed, and fast/slow oracle ticks from a 96-bit tickData encoding.
    /// @param _tickData The packed tickData to unpack ticks from
    /// @return The current tick contained in `_tickData`
    /// @return The fast oracle tick contained in `_tickData`
    /// @return The slow oracle tick contained in `_tickData`
    /// @return The last observed tick contained in `_tickData`
    function unpackTickData(uint96 _tickData) internal pure returns (int24, int24, int24, int24) {
        PositionBalance self = PositionBalance.wrap(uint256(_tickData) << 160);
        return (
            int24(int256(PositionBalance.unwrap(self) >> 160)),
            int24(int256(PositionBalance.unwrap(self) >> 184)),
            int24(int256(PositionBalance.unwrap(self) >> 208)),
            int24(int256(PositionBalance.unwrap(self) >> 232))
        );
    }

    /// @notice Get currency0 utilization of `self`.
    /// @param self The PositionBalance to retrieve the currency0 utilization from
    /// @return The currency0 utilization in basis points
    function utilization0(PositionBalance self) internal pure returns (int256) {
        unchecked {
            return int256((PositionBalance.unwrap(self) >> 128) % 2 ** 16);
        }
    }

    /// @notice Get currency1 utilization of `self`.
    /// @param self The PositionBalance to retrieve the currency1 utilization from
    /// @return The currency1 utilization in basis points
    function utilization1(PositionBalance self) internal pure returns (int256) {
        unchecked {
            return int256((PositionBalance.unwrap(self) >> 144) % 2 ** 16);
        }
    }

    /// @notice Get both currency0 and currency1 utilizations of `self`.
    /// @param self The PositionBalance to retrieve the utilizations from
    /// @return The packed currency utilizations in basis points
    function utilizations(PositionBalance self) internal pure returns (uint32) {
        unchecked {
            return uint32(PositionBalance.unwrap(self) >> 128);
        }
    }

    /// @notice Get the positionSize of `self`.
    /// @param self The PositionBalance to retrieve the positionSize from
    /// @return The positionSize of `self`
    function positionSize(PositionBalance self) internal pure returns (uint128) {
        unchecked {
            return uint128(PositionBalance.unwrap(self));
        }
    }

    /// @notice Unpack all data from `self`.
    /// @param self The PositionBalance to get all data from
    /// @return currentTickAtMint `currentTick` at mint
    /// @return fastOracleTickAtMint Fast oracle tick at mint
    /// @return slowOracleTickAtMint Slow oracle tick at mint
    /// @return lastObservedTickAtMint Last observed tick at mint
    /// @return utilization0AtMint Utilization of currency0 at mint
    /// @return utilization1AtMint Utilization of currency1 at mint
    /// @return _positionSize Size of the position
    function unpackAll(
        PositionBalance self
    )
        external
        pure
        returns (
            int24 currentTickAtMint,
            int24 fastOracleTickAtMint,
            int24 slowOracleTickAtMint,
            int24 lastObservedTickAtMint,
            int256 utilization0AtMint,
            int256 utilization1AtMint,
            uint128 _positionSize
        )
    {
        (
            currentTickAtMint,
            fastOracleTickAtMint,
            slowOracleTickAtMint,
            lastObservedTickAtMint
        ) = unpackTickData(self.tickData());

        utilization0AtMint = self.utilization0();
        utilization1AtMint = self.utilization1();

        _positionSize = self.positionSize();
    }
}

// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.24;

// Libraries
import {Constants} from "@libraries/Constants.sol";
import {Errors} from "@libraries/Errors.sol";
import {PanopticMath} from "@libraries/PanopticMath.sol";

type TokenId is uint256;
using TokenIdLibrary for TokenId global;

/// @title Panoptic's tokenId: the fundamental options position.
/// @author Axicon Labs Limited
/// @notice This is the token ID used in the ERC1155 representation of the option position in the SFPM.
/// @notice The SFPM "overloads" the ERC1155 `id` by storing all option information in said `id`.
/// @notice Contains methods for packing and unpacking a Panoptic options position into a uint256 bit pattern.
// PACKING RULES FOR A TOKENID:
// this is how the token Id is packed into its bit-constituents containing position information.
// the following is a diagram to be read top-down in a little endian format
// (so (1) below occupies the first 64 least significant bits, e.g.):
// From the LSB to the MSB:
// ===== 1 time (same for all legs) ==============================================================
//      Property         Size      Offset      Comment
// (0) idV4             48bits     0bits      : least significant 48 bits of the Uniswap V4 pool ID, plus a pseudorandom number in the event of a collision
// (1) tickSpacing      16bits     48bits     : tickSpacing for the pool corresponding to `idV4`. Up to 16 bits
// ===== 4 times (one for each leg) ==============================================================
// (2) asset             1bit      0bits      : Specifies the asset (0: currency0, 1: currency1)
// (3) optionRatio       7bits     1bits      : number of contracts per leg
// (4) isLong            1bit      8bits      : long==1 means liquidity is removed, long==0 -> liquidity is added
// (5) tokenType         1bit      9bits      : put/call: which token is moved when deployed (0 -> currency0, 1 -> currency1)
// (6) riskPartner       2bits     10bits     : normally its own index. Partner in defined risk position otherwise
// (7) strike           24bits     12bits     : strike price; defined as (tickUpper + tickLower) / 2
// (8) width            12bits     36bits     : width; defined as (tickUpper - tickLower) / tickSpacing
// Total                48bits                : Each leg takes up this many bits
// ===============================================================================================
//
// The bit pattern is therefore, in general:
//
//                        (strike price tick of the 3rd leg)
//                            |             (width of the 2nd leg)
//                            |                   |
// (8)(7)(6)(5)(4)(3)(2)  (8)(7)(6)(5)(4)(3)(2)  (8)(7)(6)(5)(4)(3)(2)   (8)(7)(6)(5)(4)(3)(2)        (1)           (0)
//  <---- 48 bits ---->    <---- 48 bits ---->    <---- 48 bits ---->     <---- 48 bits ---->   <- 16 bits ->   <- 48 bits ->
//         Leg 4                  Leg 3                  Leg 2                   Leg 1           tickSpacing Uniswap Pool Pattern
//
//  <--- most significant bit                                                                             least significant bit --->
//
// Some rules of how legs behave (we enforce these in a `validate()` function):
//   - a leg is inactive if it's not part of the position. Technically it means that all bits are zero.
//   - a leg is active if it has an optionRatio > 0 since this must always be set for an active leg.
//   - if a leg is active (e.g. leg 1) there can be no gaps in other legs meaning: if leg 1 is active then leg 3 cannot be active if leg 2 is inactive.
//
// Examples:
//  We can think of the bit pattern as an array starting at bit index 0 going to bit index 255 (so 256 total bits)
//  We also refer to the legs via their index, so leg number 2 has leg index 1 (legIndex) (counting from zero), and in general leg number N has leg index N-1.
//  - the underlying strike price of the 2nd leg (leg index = 1) in this option position starts at bit index  (64 + 12 + 48 * (leg index=1))=123
//  - the tokenType of the 4th leg in this option position starts at bit index 64+9+48*3=217
//  - the Uniswap V4 pool id starts at bit index 0 and ends at bit index 63 (and thus takes up 64 bits).
//  - the width of the 3rd leg in this option position starts at bit index 64+36+48*2=196
library TokenIdLibrary {
    /// @notice AND mask to extract all `isLong` bits for each leg from a TokenId.
    uint256 internal constant LONG_MASK =
        0x100_000000000100_000000000100_000000000100_0000000000000000;

    /// @notice AND mask to clear `poolId` from a TokenId.
    uint256 internal constant CLEAR_POOLID_MASK =
        0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF_0000000000000000;

    /// @notice AND mask to clear all bits except for the option ratios of the legs.
    uint256 internal constant OPTION_RATIO_MASK =
        0x0000000000FE_0000000000FE_0000000000FE_0000000000FE_0000000000000000;

    /// @notice AND mask to clear all bits except for the components of the chunk key (strike, width, tokenType) for each leg.
    uint256 internal constant CHUNK_MASK =
        0xFFFFFFFFF200_FFFFFFFFF200_FFFFFFFFF200_FFFFFFFFF200_0000000000000000;

    /// @notice AND mask to cut a sign-extended int256 back to an int24.
    int256 internal constant BITMASK_INT24 = 0xFFFFFF;

    /*//////////////////////////////////////////////////////////////
                                DECODING
    //////////////////////////////////////////////////////////////*/

    /// @notice The full poolId (Uniswap pool identifier + pool pattern) of this option position.
    /// @param self The TokenId to extract `poolId` from
    /// @return The `poolId` (Panoptic's pool fingerprint, contains the whole 64 bit sequence with the tickSpacing) of the Uniswap V4 pool
    function poolId(TokenId self) internal pure returns (uint64) {
        unchecked {
            return uint64(TokenId.unwrap(self));
        }
    }

    /// @notice The tickSpacing of this option position.
    /// @param self The TokenId to extract `tickSpacing` from
    /// @return The `tickSpacing` of the Uniswap V4 pool
    function tickSpacing(TokenId self) internal pure returns (int24) {
        unchecked {
            return int24(uint24((TokenId.unwrap(self) >> 48) % 2 ** 16));
        }
    }

    /// @notice Get the asset basis for this TokenId.
    /// @dev Which token is the asset - can be currency0 (return 0) or currency1 (return 1).
    /// @param self The TokenId to extract `asset` from
    /// @param legIndex The leg index of this position (in {0,1,2,3}) to extract `asset` from
    /// @return 0 if asset is currency0, 1 if asset is currency1
    function asset(TokenId self, uint256 legIndex) internal pure returns (uint256) {
        unchecked {
            return uint256((TokenId.unwrap(self) >> (64 + legIndex * 48)) % 2);
        }
    }

    /// @notice Get the number of contracts multiplier for leg `legIndex`.
    /// @param self The TokenId to extract `optionRatio` at `legIndex` from
    /// @param legIndex The leg index of this position (in {0,1,2,3})
    /// @return The number of contracts multiplier for leg `legIndex`
    function optionRatio(TokenId self, uint256 legIndex) internal pure returns (uint256) {
        unchecked {
            return uint256((TokenId.unwrap(self) >> (64 + legIndex * 48 + 1)) % 128);
        }
    }

    /// @notice Return 1 if the nth leg (leg index `legIndex`) is a long position.
    /// @param self The TokenId to extract `isLong` at `legIndex` from
    /// @param legIndex The leg index of this position (in {0,1,2,3})
    /// @return 1 if long; 0 if not long
    function isLong(TokenId self, uint256 legIndex) internal pure returns (uint256) {
        unchecked {
            return uint256((TokenId.unwrap(self) >> (64 + legIndex * 48 + 8)) % 2);
        }
    }

    /// @notice Get the type of currency moved for a given leg (implies a call or put). Either currency0 or currency1.
    /// @param self The TokenId to extract `tokenType` at `legIndex` from
    /// @param legIndex The leg index of this position (in {0,1,2,3})
    /// @return 1 if the currency moved is currency1 or 0 if the currency moved is currency0
    function tokenType(TokenId self, uint256 legIndex) internal pure returns (uint256) {
        unchecked {
            return uint256((TokenId.unwrap(self) >> (64 + legIndex * 48 + 9)) % 2);
        }
    }

    /// @notice Get the associated risk partner of the leg index (generally another leg index in the position if enabled or the same leg index if no partner).
    /// @param self The TokenId to extract `riskPartner` at `legIndex` from
    /// @param legIndex The leg index of this position (in {0,1,2,3})
    /// @return The leg index of `legIndex`'s risk partner
    function riskPartner(TokenId self, uint256 legIndex) internal pure returns (uint256) {
        unchecked {
            return uint256((TokenId.unwrap(self) >> (64 + legIndex * 48 + 10)) % 4);
        }
    }

    /// @notice Get the strike price tick of the nth leg (with index `legIndex`).
    /// @param self The TokenId to extract `strike` at `legIndex` from
    /// @param legIndex The leg index of this position (in {0,1,2,3})
    /// @return The strike price (the underlying price of the leg)
    function strike(TokenId self, uint256 legIndex) internal pure returns (int24) {
        unchecked {
            return int24(int256(TokenId.unwrap(self) >> (64 + legIndex * 48 + 12)));
        }
    }

    /// @notice Get the width (distance between upper and lower ticks) of the nth leg (index `legIndex`).
    /// @dev The width is always positive; it is returned as an int24 for internal consistency with strike operations.
    /// @param self The TokenId to extract `width` at `legIndex` from
    /// @param legIndex The leg index of this position (in {0,1,2,3})
    /// @return The width of the position
    function width(TokenId self, uint256 legIndex) internal pure returns (int24) {
        unchecked {
            return int24(int256((TokenId.unwrap(self) >> (64 + legIndex * 48 + 36)) % 4096));
        } // "% 4096" = take last (2 ** 12 = 4096) 12 bits
    }

    /*//////////////////////////////////////////////////////////////
                                ENCODING
    //////////////////////////////////////////////////////////////*/

    /// @notice Add the Uniswap pool identifier corresponding to this option position (contains the entropy and tickSpacing).
    /// @param self The TokenId to add `_poolId` to
    /// @param _poolId The PoolID to add to `self`
    /// @return `self` with `_poolId` added to the PoolID slot
    function addPoolId(TokenId self, uint64 _poolId) internal pure returns (TokenId) {
        unchecked {
            return TokenId.wrap(TokenId.unwrap(self) + _poolId);
        }
    }

    /// @notice Add the `tickSpacing` to the PoolID for `self`.
    /// @param self The TokenId to add `_tickSpacing` to
    /// @param _tickSpacing The tickSpacing to add to `self`
    /// @return `self` with `_tickSpacing` added to the TickSpacing slot in the PoolID
    function addTickSpacing(TokenId self, int24 _tickSpacing) internal pure returns (TokenId) {
        unchecked {
            return TokenId.wrap(TokenId.unwrap(self) + (uint256(uint24(_tickSpacing)) << 48));
        }
    }

    /// @notice Add the asset basis for this position.
    /// @param self The TokenId to add `_asset` to
    /// @param _asset The asset to add to the Asset slot in `self` for `legIndex`
    /// @param legIndex The leg index of this position (in {0,1,2,3})
    /// @return `self` with `_asset` added to the Asset slot
    function addAsset(
        TokenId self,
        uint256 _asset,
        uint256 legIndex
    ) internal pure returns (TokenId) {
        unchecked {
            return
                TokenId.wrap(TokenId.unwrap(self) + (uint256(_asset % 2) << (64 + legIndex * 48)));
        }
    }

    /// @notice Add the number of contracts multiplier to leg index `legIndex`.
    /// @param self The TokenId to add `_optionRatio` to
    /// @param _optionRatio The number of contracts multiplier to add to the OptionRatio slot in `self` for LegIndex
    /// @param legIndex The leg index of the position (in {0,1,2,3})
    /// @return `self` with `_optionRatio` added to the OptionRatio slot for `legIndex`
    function addOptionRatio(
        TokenId self,
        uint256 _optionRatio,
        uint256 legIndex
    ) internal pure returns (TokenId) {
        unchecked {
            return
                TokenId.wrap(
                    TokenId.unwrap(self) + (uint256(_optionRatio % 128) << (64 + legIndex * 48 + 1))
                );
        }
    }

    /// @notice Add "isLong" parameter indicating whether a leg is long (isLong=1) or short (isLong=0).
    /// @param self The TokenId to add `_isLong` to
    /// @param _isLong The isLong parameter to add to the IsLong slot in `self` for `legIndex`
    /// @param legIndex The leg index of this position (in {0,1,2,3})
    /// @return `self` with `_isLong` added to the IsLong slot for `legIndex`
    function addIsLong(
        TokenId self,
        uint256 _isLong,
        uint256 legIndex
    ) internal pure returns (TokenId) {
        unchecked {
            return TokenId.wrap(TokenId.unwrap(self) + ((_isLong % 2) << (64 + legIndex * 48 + 8)));
        }
    }

    /// @notice Add the type of currency moved for a given leg (implies a call or put). Either currency0 or currency1.
    /// @param self The TokenId to add `_tokenType` to
    /// @param _tokenType The tokenType to add to the TokenType slot in `self` for `legIndex`
    /// @param legIndex The leg index of this position (in {0,1,2,3})
    /// @return `self` with `_tokenType` added to the TokenType slot for `legIndex`
    function addTokenType(
        TokenId self,
        uint256 _tokenType,
        uint256 legIndex
    ) internal pure returns (TokenId) {
        unchecked {
            return
                TokenId.wrap(
                    TokenId.unwrap(self) + (uint256(_tokenType % 2) << (64 + legIndex * 48 + 9))
                );
        }
    }

    /// @notice Add the associated risk partner of the leg index.
    /// @param self The TokenId to add `_riskPartner` to
    /// @param _riskPartner The riskPartner to add to the RiskPartner slot in `self` for `legIndex`
    /// @param legIndex The leg index of this position (in {0,1,2,3})
    /// @return `self` with `_riskPartner` added to the RiskPartner slot for `legIndex`
    function addRiskPartner(
        TokenId self,
        uint256 _riskPartner,
        uint256 legIndex
    ) internal pure returns (TokenId) {
        unchecked {
            return
                TokenId.wrap(
                    TokenId.unwrap(self) + (uint256(_riskPartner % 4) << (64 + legIndex * 48 + 10))
                );
        }
    }

    /// @notice Add the strike price tick of the nth leg (index `legIndex`).
    /// @param self The TokenId to add `_strike` to
    /// @param _strike The strike price tick to add to the Strike slot in `self` for `legIndex`
    /// @param legIndex The leg index of this position (in {0,1,2,3})
    /// @return `self` with `_strike` added to the Strike slot for `legIndex`
    function addStrike(
        TokenId self,
        int24 _strike,
        uint256 legIndex
    ) internal pure returns (TokenId) {
        unchecked {
            return
                TokenId.wrap(
                    TokenId.unwrap(self) +
                        uint256((int256(_strike) & BITMASK_INT24) << (64 + legIndex * 48 + 12))
                );
        }
    }

    /// @notice Add the width of the nth leg (index `legIndex`).
    /// @param self The TokenId to add `_width` to
    /// @param _width The width to add to the Width slot in `self` for `legIndex`
    /// @param legIndex The leg index of this position (in {0,1,2,3})
    /// @return `self` with `_width` added to the Width slot for `legIndex`
    function addWidth(
        TokenId self,
        int24 _width,
        uint256 legIndex
    ) internal pure returns (TokenId) {
        // % 4096 -> take 12 bits from the incoming 24 bits (there's no uint12)
        unchecked {
            return
                TokenId.wrap(
                    TokenId.unwrap(self) +
                        (uint256(uint24(_width) % 4096) << (64 + legIndex * 48 + 36))
                );
        }
    }

    /// @notice Add a leg to a TokenId.
    /// @param self The tokenId in the SFPM representing an option position
    /// @param legIndex The leg index of this position (in {0,1,2,3}) to add
    /// @param _optionRatio The relative size of the leg
    /// @param _asset The asset of the leg
    /// @param _isLong Whether the leg is long
    /// @param _tokenType The type of token moved for the leg
    /// @param _riskPartner The associated risk partner of the leg
    /// @param _strike The strike price tick of the leg
    /// @param _width The width of the leg
    /// @return tokenId The tokenId with the leg added
    function addLeg(
        TokenId self,
        uint256 legIndex,
        uint256 _optionRatio,
        uint256 _asset,
        uint256 _isLong,
        uint256 _tokenType,
        uint256 _riskPartner,
        int24 _strike,
        int24 _width
    ) internal pure returns (TokenId tokenId) {
        tokenId = addOptionRatio(self, _optionRatio, legIndex);
        tokenId = addAsset(tokenId, _asset, legIndex);
        tokenId = addIsLong(tokenId, _isLong, legIndex);
        tokenId = addTokenType(tokenId, _tokenType, legIndex);
        tokenId = addRiskPartner(tokenId, _riskPartner, legIndex);
        tokenId = addStrike(tokenId, _strike, legIndex);
        tokenId = addWidth(tokenId, _width, legIndex);
    }

    /*//////////////////////////////////////////////////////////////
                                HELPERS
    //////////////////////////////////////////////////////////////*/

    /// @notice Flip all the `isLong` positions in the legs in the `tokenId` option position.
    /// @param self The TokenId to flip isLong for on all active legs
    /// @return tokenId `self` with all `isLong` bits flipped
    function flipToBurnToken(TokenId self) internal pure returns (TokenId) {
        unchecked {
            // We need to ensure that only active legs are flipped
            // In order to achieve this, we shift our long bit mask to the right by (4-# active legs)
            // i.e the whole mask is used to flip all legs with 4 legs, but only the first leg is flipped with 1 leg so we shift by 3 legs
            // We also clear the poolId area of the mask to ensure the bits that are shifted right into the area don't flip and cause issues
            return
                TokenId.wrap(
                    TokenId.unwrap(self) ^
                        ((LONG_MASK >> (48 * (4 - self.countLegs()))) & CLEAR_POOLID_MASK)
                );
        }
    }

    /// @notice Count the number of legs (out of a maximum of 4) that are long positions.
    /// @param self The TokenId to count longs for
    /// @return The number of long positions in `self` (in the range {0,...,4})
    function countLongs(TokenId self) internal pure returns (uint256) {
        unchecked {
            return self.isLong(0) + self.isLong(1) + self.isLong(2) + self.isLong(3);
        }
    }

    /// @notice Get the option position's nth leg's (index `legIndex`) tick ranges (lower, upper).
    /// @param self The TokenId to extract the tick range from
    /// @param legIndex The leg index of the position (in {0,1,2,3})
    /// @return legLowerTick The lower tick of the leg/liquidity chunk
    /// @return legUpperTick The upper tick of the leg/liquidity chunk
    function asTicks(
        TokenId self,
        uint256 legIndex
    ) internal pure returns (int24 legLowerTick, int24 legUpperTick) {
        (legLowerTick, legUpperTick) = PanopticMath.getTicks(
            self.strike(legIndex),
            self.width(legIndex),
            self.tickSpacing()
        );
    }

    /// @notice Return the number of active legs in the option position.
    /// @dev ASSUMPTION: For any leg, the option ratio is always > 0 (the leg always has a number of contracts associated with it).
    /// @param self The TokenId to count active legs for
    /// @return numLegs The number of active legs in `self` (in the range {0,...,4})
    function countLegs(TokenId self) internal pure returns (uint256 numLegs) {
        // Strip all bits except for the option ratios
        uint256 optionRatios = (TokenId.unwrap(self) & OPTION_RATIO_MASK) >> 64;

        unchecked {
            while (optionRatios >= 1 << (48 * numLegs)) {
                ++numLegs;
            }
        }
    }

    /// @notice Clear a leg in an option position at `legIndex`.
    /// @dev NOTE: it's important that the caller fills in the leg details after.
    //  - optionRatio is zeroed
    //  - asset is zeroed
    //  - width is zeroed
    //  - strike is zeroed
    //  - tokenType is zeroed
    //  - isLong is zeroed
    //  - riskPartner is zeroed
    /// @param self The TokenId to clear the leg from
    /// @param legIndex The leg index to reset, in {0,1,2,3}
    /// @return `self` with the `legIndex`th leg zeroed
    function clearLeg(TokenId self, uint256 legIndex) internal pure returns (TokenId) {
        if (legIndex == 0)
            return
                TokenId.wrap(
                    TokenId.unwrap(self) &
                        0xFFFFFFFFFFFF_FFFFFFFFFFFF_FFFFFFFFFFFF_000000000000_FFFFFFFFFFFFFFFF
                );
        if (legIndex == 1)
            return
                TokenId.wrap(
                    TokenId.unwrap(self) &
                        0xFFFFFFFFFFFF_FFFFFFFFFFFF_000000000000_FFFFFFFFFFFF_FFFFFFFFFFFFFFFF
                );
        if (legIndex == 2)
            return
                TokenId.wrap(
                    TokenId.unwrap(self) &
                        0xFFFFFFFFFFFF_000000000000_FFFFFFFFFFFF_FFFFFFFFFFFF_FFFFFFFFFFFFFFFF
                );
        if (legIndex == 3)
            return
                TokenId.wrap(
                    TokenId.unwrap(self) &
                        0x000000000000_FFFFFFFFFFFF_FFFFFFFFFFFF_FFFFFFFFFFFF_FFFFFFFFFFFFFFFF
                );

        return self;
    }

    /*//////////////////////////////////////////////////////////////
                               VALIDATION
    //////////////////////////////////////////////////////////////*/

    /// @notice Checks if a TokenId is valid and reverts with an error reflecting the incorrect parameter for invalid positions.
    /// @param self The TokenId to validate
    function validate(TokenId self) internal pure {
        if (self.optionRatio(0) == 0) revert Errors.InvalidTokenIdParameter(1);

        // loop through the 4 (possible) legs in the tokenId `self`
        unchecked {
            // extract strike, width, and tokenType
            uint256 chunkData = (TokenId.unwrap(self) & CHUNK_MASK) >> 64;
            for (uint256 i = 0; i < 4; ++i) {
                if (self.optionRatio(i) == 0) {
                    // final leg in this position identified;
                    // make sure any leg above this are zero as well
                    // (we don't allow gaps eg having legs 1 and 4 active without 2 and 3 is not allowed)
                    if ((TokenId.unwrap(self) >> (64 + 48 * i)) != 0)
                        revert Errors.InvalidTokenIdParameter(1);

                    break; // we are done iterating over potential legs
                }

                // prevent legs touching the same chunks - all chunks in the position must be discrete
                uint256 numLegs = self.countLegs();
                for (uint256 j = i + 1; j < numLegs; ++j) {
                    if (uint48(chunkData >> (48 * i)) == uint48(chunkData >> (48 * j))) {
                        revert Errors.InvalidTokenIdParameter(6);
                    }
                }

                // The width cannot be 0; the minimum is 1
                if ((self.width(i) == 0)) revert Errors.InvalidTokenIdParameter(5);
                // Strike cannot be MIN_TICK or MAX_TICK
                if (
                    (self.strike(i) == Constants.MIN_V4POOL_TICK) ||
                    (self.strike(i) == Constants.MAX_V4POOL_TICK)
                ) revert Errors.InvalidTokenIdParameter(4);

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

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

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

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

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

                    // if the two token long-types and the tokenTypes are both different (one is a short call, the other a long put, e.g.), this is a synthetic position
                    // A synthetic long or short is more capital efficient than each leg separated because the long+short premia accumulate proportionally
                    // unlike short strangles, long strangles also cannot be partnered, because there is no reduction in risk (both legs can earn premia simultaneously)
                    if (((_isLong != isLongP) || _isLong == 1) && (_tokenType != tokenTypeP))
                        revert Errors.InvalidTokenIdParameter(5);
                }
            }
        }
    }

    /// @notice Validate that a position `self` and its legs/chunks are exercisable.
    /// @dev At least one long leg must be far-out-of-the-money (i.e. price is outside its range).
    /// @dev Reverts if the position is not exercisable.
    /// @param self The TokenId to validate for exercisability
    /// @param currentTick The current tick corresponding to the current price in the Uniswap V4 pool
    function validateIsExercisable(TokenId self, int24 currentTick) internal pure {
        unchecked {
            uint256 numLegs = self.countLegs();
            for (uint256 i = 0; i < numLegs; ++i) {
                (int24 rangeDown, int24 rangeUp) = PanopticMath.getRangesFromStrike(
                    self.width(i),
                    self.tickSpacing()
                );

                int24 _strike = self.strike(i);
                // check if the price is outside this chunk
                if ((currentTick >= _strike + rangeUp) || (currentTick < _strike - rangeDown)) {
                    // if this leg is long and the price beyond the leg's range:
                    // this exercised ID, `self`, appears valid
                    if (self.isLong(i) == 1) return; // validated
                }
            }
        }

        // Fail if position has no legs that is far-out-of-the-money
        revert Errors.NoLegsExercisable();
    }
}

// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.24;

/// @title Minimal efficient ERC20 implementation without metadata
/// @author Axicon Labs Limited
/// @author Modified from Solmate (https://github.com/transmissions11/solmate/blob/v7/src/tokens/ERC20.sol)
/// @dev The metadata must be set in the inheriting contract.
/// @dev Do not manually set balances without updating totalSupply, as the sum of all user balances must not exceed it.
abstract contract ERC20Minimal {
    /*//////////////////////////////////////////////////////////////
                                 EVENTS
    //////////////////////////////////////////////////////////////*/

    /// @notice Emitted when tokens are transferred.
    /// @param from The sender of the tokens
    /// @param to The recipient of the tokens
    /// @param amount The amount of tokens transferred
    event Transfer(address indexed from, address indexed to, uint256 amount);

    /// @notice Emitted when a user approves another user to spend tokens on their behalf.
    /// @param owner The user who approved the spender
    /// @param spender The user who was approved to spend tokens
    /// @param amount The amount of tokens approved to spend
    event Approval(address indexed owner, address indexed spender, uint256 amount);

    /*//////////////////////////////////////////////////////////////
                              ERC20 STORAGE
    //////////////////////////////////////////////////////////////*/

    /// @notice The total supply of tokens.
    /// @dev This cannot exceed the max uint256 value.
    uint256 public totalSupply;

    /// @notice Token balances for each user.
    mapping(address account => uint256 balance) public balanceOf;

    /// @notice Stored allowances for each user.
    /// @dev Indexed by owner, then by spender.
    mapping(address owner => mapping(address spender => uint256 allowance)) public allowance;

    /*//////////////////////////////////////////////////////////////
                               ERC20 LOGIC
    //////////////////////////////////////////////////////////////*/

    /// @notice Approves a user to spend tokens on the caller's behalf.
    /// @param spender The user to approve
    /// @param amount The amount of tokens to approve
    /// @return Whether the approval succeeded
    function approve(address spender, uint256 amount) public returns (bool) {
        allowance[msg.sender][spender] = amount;

        emit Approval(msg.sender, spender, amount);

        return true;
    }

    /// @notice Transfers tokens from the caller to another user.
    /// @param to The user to transfer tokens to
    /// @param amount The amount of tokens to transfer
    /// @return Whether the transfer succeeded
    function transfer(address to, uint256 amount) public virtual returns (bool) {
        balanceOf[msg.sender] -= amount;

        // Cannot overflow because the sum of all user
        // balances can't exceed the max uint256 value.
        unchecked {
            balanceOf[to] += amount;
        }

        emit Transfer(msg.sender, to, amount);

        return true;
    }

    /// @notice Transfers tokens from one user to another.
    /// @dev Supports token approvals.
    /// @param from The user to transfer tokens from
    /// @param to The user to transfer tokens to
    /// @param amount The amount of tokens to transfer
    /// @return Whether the transfer succeeded
    function transferFrom(address from, address to, uint256 amount) public virtual returns (bool) {
        uint256 allowed = allowance[from][msg.sender]; // Saves gas for limited approvals.

        if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount;

        balanceOf[from] -= amount;

        // Cannot overflow because the sum of all user
        // balances can't exceed the max uint256 value.
        unchecked {
            balanceOf[to] += amount;
        }

        emit Transfer(from, to, amount);

        return true;
    }

    /// @notice Internal utility to transfer tokens from one user to another.
    /// @param from The user to transfer tokens from
    /// @param to The user to transfer tokens to
    /// @param amount The amount of tokens to transfer
    function _transferFrom(address from, address to, uint256 amount) internal {
        balanceOf[from] -= amount;

        // Cannot overflow because the sum of all user
        // balances can't exceed the max uint256 value.
        unchecked {
            balanceOf[to] += amount;
        }

        emit Transfer(from, to, amount);
    }

    /*//////////////////////////////////////////////////////////////
                        INTERNAL MINT/BURN LOGIC
    //////////////////////////////////////////////////////////////*/

    /// @notice Internal utility to mint tokens to a user's account.
    /// @param to The user to mint tokens to
    /// @param amount The amount of tokens to mint
    function _mint(address to, uint256 amount) internal {
        // Cannot overflow because the sum of all user
        // balances can't exceed the max uint256 value.
        unchecked {
            balanceOf[to] += amount;
        }
        totalSupply += amount;

        emit Transfer(address(0), to, amount);
    }

    /// @notice Internal utility to burn tokens from a user's account.
    /// @param from The user to burn tokens from
    /// @param amount The amount of tokens to burn
    function _burn(address from, uint256 amount) internal {
        balanceOf[from] -= amount;

        // Cannot underflow because a user's balance
        // will never be larger than the total supply.
        unchecked {
            totalSupply -= amount;
        }

        emit Transfer(from, address(0), amount);
    }
}

// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.24;

// Interfaces
import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
// Libraries
import {PanopticMath} from "@libraries/PanopticMath.sol";

/// @title InteractionHelper - contains helper functions for select external interactions.
/// @notice This library contains helper functions for select external interactions.
/// @dev Generally employed when there is a need to save or reuse bytecode size
/// on a core contract.
/// @author Axicon Labs Limited
library InteractionHelper {
    /// @notice Computes the name of a CollateralTracker based on the token composition and fee of the underlying Uniswap Pool.
    /// @dev Some tokens do not have proper symbols so error handling is required - this logic takes up significant bytecode size, which is why it is in a library.
    /// @param currency0 The currency0 of the Uniswap Pool
    /// @param currency1 The currency1 of the Uniswap Pool
    /// @param isToken0 Whether the collateral token computing the name is for currency0 or currency1
    /// @param fee The fee of the Uniswap pool in hundredths of basis points
    /// @param prefix A constant string appended to the start of the token name
    /// @return The complete name of the collateral token calling this function
    function computeName(
        address currency0,
        address currency1,
        bool isToken0,
        uint24 fee,
        string memory prefix
    ) external view returns (string memory) {
        string memory symbol0 = PanopticMath.safeERC20Symbol(currency0);
        string memory symbol1 = PanopticMath.safeERC20Symbol(currency1);

        unchecked {
            return
                string.concat(
                    prefix,
                    " ",
                    isToken0 ? symbol0 : symbol1,
                    " LP on ",
                    symbol0,
                    "/",
                    symbol1,
                    " ",
                    PanopticMath.uniswapFeeToString(fee)
                );
        }
    }

    /// @notice Returns collateral token symbol as `prefix` + `underlying asset symbol`.
    /// @param token The address of the underlying asset used to compute the symbol (`address(0)` = native asset)
    /// @param prefix A constant string prepended to the symbol of the underlying asset to create the final symbol
    /// @return The symbol of the collateral token
    function computeSymbol(
        address token,
        string memory prefix
    ) external view returns (string memory) {
        return string.concat(prefix, PanopticMath.safeERC20Symbol(token));
    }

    /// @notice Returns decimals of underlying asset (0 if not present).
    /// @param token The address of the underlying asset used to compute the decimals (`address(0)` = native asset)
    /// @return The decimals of the token
    function computeDecimals(address token) external view returns (uint8) {
        // not guaranteed that token supports metadata extension
        // so we need to let call fail and return placeholder if not
        try IERC20Metadata(token).decimals() returns (uint8 _decimals) {
            return _decimals;
        } catch {
            return 0;
        }
    }
}

// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.24;

// Libraries
import {Errors} from "@libraries/Errors.sol";

/// @notice Safe ERC20 transfer library that gracefully handles missing return values.
/// @author Axicon Labs Limited
/// @author Modified from Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/utils/SafeTransferLib.sol)
/// @dev Caution! This library won't check that a token has code, responsibility is delegated to the caller.
library SafeTransferLib {
    /*//////////////////////////////////////////////////////////////
                             ETH OPERATIONS
    //////////////////////////////////////////////////////////////*/

    /// @notice Safely transfers ETH to a specified address.
    /// @param to The address to transfer ETH to
    /// @param amount The amount of ETH to transfer
    function safeTransferETH(address to, uint256 amount) internal {
        bool success;

        assembly {
            // Transfer the ETH and store if it succeeded or not.
            success := call(gas(), to, amount, 0, 0, 0, 0)
        }

        if (!success) revert Errors.TransferFailed();
    }

    /*//////////////////////////////////////////////////////////////
                            ERC20 OPERATIONS
    //////////////////////////////////////////////////////////////*/

    /// @notice Safely transfers ERC20 tokens from one address to another.
    /// @param token The address of the ERC20 token
    /// @param from The address to transfer tokens from
    /// @param to The address to transfer tokens to
    /// @param amount The amount of tokens to transfer
    function safeTransferFrom(address token, address from, address to, uint256 amount) internal {
        bool success;

        assembly ("memory-safe") {
            // Get free memory pointer - we will store our calldata in scratch space starting at the offset specified here.
            let p := mload(0x40)

            // Write the abi-encoded calldata into memory, beginning with the function selector.
            mstore(p, 0x23b872dd00000000000000000000000000000000000000000000000000000000)
            mstore(add(4, p), from) // Append the "from" argument.
            mstore(add(36, p), to) // Append the "to" argument.
            mstore(add(68, p), amount) // Append the "amount" argument.

            success := and(
                // Set success to whether the call reverted, if not we check it either
                // returned exactly 1 (can't just be non-zero data), or had no return data.
                or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())),
                // We use 100 because that's the total length of our calldata (4 + 32 * 3)
                // Counterintuitively, this call() must be positioned after the or() in the
                // surrounding and() because and() evaluates its arguments from right to left.
                call(gas(), token, 0, p, 100, 0, 32)
            )
        }

        if (!success) revert Errors.TransferFailed();
    }

    /// @notice Safely transfers ERC20 tokens to a specified address.
    /// @param token The address of the ERC20 token
    /// @param to The address to transfer tokens to
    /// @param amount The amount of tokens to transfer
    function safeTransfer(address token, address to, uint256 amount) internal {
        bool success;

        assembly ("memory-safe") {
            // Get free memory pointer - we will store our calldata in scratch space starting at the offset specified here.
            let p := mload(0x40)

            // Write the abi-encoded calldata into memory, beginning with the function selector.
            mstore(p, 0xa9059cbb00000000000000000000000000000000000000000000000000000000)
            mstore(add(4, p), to) // Append the "to" argument.
            mstore(add(36, p), amount) // Append the "amount" argument.

            success := and(
                // Set success to whether the call reverted, if not we check it either
                // returned exactly 1 (can't just be non-zero data), or had no return data.
                or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())),
                // We use 68 because that's the total length of our calldata (4 + 32 * 2)
                // Counterintuitively, this call() must be positioned after the or() in the
                // surrounding and() because and() evaluates its arguments from right to left.
                call(gas(), token, 0, p, 68, 0, 32)
            )
        }

        if (!success) revert Errors.TransferFailed();
    }
}

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {IERC20Minimal} from "../interfaces/external/IERC20Minimal.sol";
import {CustomRevert} from "../libraries/CustomRevert.sol";

type Currency is address;

using {greaterThan as >, lessThan as <, greaterThanOrEqualTo as >=, equals as ==} for Currency global;
using CurrencyLibrary for Currency global;

function equals(Currency currency, Currency other) pure returns (bool) {
    return Currency.unwrap(currency) == Currency.unwrap(other);
}

function greaterThan(Currency currency, Currency other) pure returns (bool) {
    return Currency.unwrap(currency) > Currency.unwrap(other);
}

function lessThan(Currency currency, Currency other) pure returns (bool) {
    return Currency.unwrap(currency) < Currency.unwrap(other);
}

function greaterThanOrEqualTo(Currency currency, Currency other) pure returns (bool) {
    return Currency.unwrap(currency) >= Currency.unwrap(other);
}

/// @title CurrencyLibrary
/// @dev This library allows for transferring and holding native tokens and ERC20 tokens
library CurrencyLibrary {
    /// @notice Additional context for ERC-7751 wrapped error when a native transfer fails
    error NativeTransferFailed();

    /// @notice Additional context for ERC-7751 wrapped error when an ERC20 transfer fails
    error ERC20TransferFailed();

    /// @notice A constant to represent the native currency
    Currency public constant ADDRESS_ZERO = Currency.wrap(address(0));

    function transfer(Currency currency, address to, uint256 amount) internal {
        // altered from https://github.com/transmissions11/solmate/blob/44a9963d4c78111f77caa0e65d677b8b46d6f2e6/src/utils/SafeTransferLib.sol
        // modified custom error selectors

        bool success;
        if (currency.isAddressZero()) {
            assembly ("memory-safe") {
                // Transfer the ETH and revert if it fails.
                success := call(gas(), to, amount, 0, 0, 0, 0)
            }
            // revert with NativeTransferFailed, containing the bubbled up error as an argument
            if (!success) {
                CustomRevert.bubbleUpAndRevertWith(to, bytes4(0), NativeTransferFailed.selector);
            }
        } else {
            assembly ("memory-safe") {
                // Get a pointer to some free memory.
                let fmp := mload(0x40)

                // Write the abi-encoded calldata into memory, beginning with the function selector.
                mstore(fmp, 0xa9059cbb00000000000000000000000000000000000000000000000000000000)
                mstore(add(fmp, 4), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // Append and mask the "to" argument.
                mstore(add(fmp, 36), amount) // Append the "amount" argument. Masking not required as it's a full 32 byte type.

                success :=
                    and(
                        // Set success to whether the call reverted, if not we check it either
                        // returned exactly 1 (can't just be non-zero data), or had no return data.
                        or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())),
                        // We use 68 because the length of our calldata totals up like so: 4 + 32 * 2.
                        // We use 0 and 32 to copy up to 32 bytes of return data into the scratch space.
                        // Counterintuitively, this call must be positioned second to the or() call in the
                        // surrounding and() call or else returndatasize() will be zero during the computation.
                        call(gas(), currency, 0, fmp, 68, 0, 32)
                    )

                // Now clean the memory we used
                mstore(fmp, 0) // 4 byte `selector` and 28 bytes of `to` were stored here
                mstore(add(fmp, 0x20), 0) // 4 bytes of `to` and 28 bytes of `amount` were stored here
                mstore(add(fmp, 0x40), 0) // 4 bytes of `amount` were stored here
            }
            // revert with ERC20TransferFailed, containing the bubbled up error as an argument
            if (!success) {
                CustomRevert.bubbleUpAndRevertWith(
                    Currency.unwrap(currency), IERC20Minimal.transfer.selector, ERC20TransferFailed.selector
                );
            }
        }
    }

    function balanceOfSelf(Currency currency) internal view returns (uint256) {
        if (currency.isAddressZero()) {
            return address(this).balance;
        } else {
            return IERC20Minimal(Currency.unwrap(currency)).balanceOf(address(this));
        }
    }

    function balanceOf(Currency currency, address owner) internal view returns (uint256) {
        if (currency.isAddressZero()) {
            return owner.balance;
        } else {
            return IERC20Minimal(Currency.unwrap(currency)).balanceOf(owner);
        }
    }

    function isAddressZero(Currency currency) internal pure returns (bool) {
        return Currency.unwrap(currency) == Currency.unwrap(ADDRESS_ZERO);
    }

    function toId(Currency currency) internal pure returns (uint256) {
        return uint160(Currency.unwrap(currency));
    }

    // If the upper 12 bytes are non-zero, they will be zero-ed out
    // Therefore, fromId() and toId() are not inverses of each other
    function fromId(uint256 id) internal pure returns (Currency) {
        return Currency.wrap(address(uint160(id)));
    }
}

// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.24;

/// @title Partial definition of the ERC20 interface as defined in the EIP
/// @dev Does not include return values as certain tokens such as USDT fail to implement them.
/// @dev Since the return value is not expected, this interface works with both compliant and non-compliant tokens.
/// @dev Clients are recommended to consume and handle the return of negative success values.
/// @dev However, we cannot productively handle a failed approval and such a situation would surely cause a revert later in execution.
/// @dev In addition, no notable instances exist of tokens that both i) contain a failure case for `approve` and ii) return `false` instead of reverting.
/// @author Axicon Labs Limited
interface IERC20Partial {
    /// @notice Returns the amount of tokens owned by `account`.
    /// @dev This function is unchanged from the EIP.
    /// @param account The address to query the balance of
    /// @return The amount of tokens owned by `account`
    function balanceOf(address account) external view returns (uint256);

    /// @notice Sets `amount` as the allowance of `spender` over the caller's tokens.
    /// @dev While this function is specified to return a boolean value in the EIP, this interface does not expect one.
    /// @param spender The address which will spend the funds
    /// @param amount The amount of tokens allowed to be spent
    function approve(address spender, uint256 amount) external;

    /// @notice Transfers tokens from the caller to another user.
    /// @param to The user to transfer tokens to
    /// @param amount The amount of tokens to transfer
    function transfer(address to, uint256 amount) external;

    /// @notice Returns the amount of tokens in existence.
    function totalSupply() external view returns (uint256);
}

// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.24;

// OpenZeppelin libraries
import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol";

/// @title Minimalist ERC1155 implementation without metadata.
/// @author Axicon Labs Limited
/// @author Modified from Solmate (https://github.com/transmissions11/solmate/blob/v7/src/tokens/ERC1155.sol)
/// @dev Not compliant to the letter, does not include any metadata functionality.
abstract contract ERC1155 {
    /*//////////////////////////////////////////////////////////////
                                 EVENTS
    //////////////////////////////////////////////////////////////*/

    /// @notice Emitted when only a single token is transferred.
    /// @param operator The user who initiated the transfer
    /// @param from The user who sent the tokens
    /// @param to The user who received the tokens
    /// @param id The ERC1155 token id
    /// @param amount The amount of tokens transferred
    event TransferSingle(
        address indexed operator,
        address indexed from,
        address indexed to,
        uint256 id,
        uint256 amount
    );

    /// @notice Emitted when multiple tokens are transferred from one user to another.
    /// @param operator The user who initiated the transfer
    /// @param from The user who sent the tokens
    /// @param to The user who received the tokens
    /// @param ids The ERC1155 token ids
    /// @param amounts The amounts of tokens transferred
    event TransferBatch(
        address indexed operator,
        address indexed from,
        address indexed to,
        uint256[] ids,
        uint256[] amounts
    );

    /// @notice Emitted when the approval status of an operator to transfer all tokens on behalf of a user is modified.
    /// @param owner The user who approved or disapproved `operator` to transfer their tokens
    /// @param operator The user who was approved or disapproved to transfer all tokens on behalf of `owner`
    /// @param approved Whether `operator` is approved or disapproved to transfer all tokens on behalf of `owner`
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);

    /*//////////////////////////////////////////////////////////////
                                 ERRORS
    //////////////////////////////////////////////////////////////*/

    /// @notice Emitted when a user attempts to transfer tokens they do not own nor are approved to transfer.
    error NotAuthorized();

    /// @notice Emitted when an attempt is made to initiate a transfer to a contract recipient that fails to signal support for ERC1155.
    error UnsafeRecipient();

    /*//////////////////////////////////////////////////////////////
                             ERC1155 STORAGE
    //////////////////////////////////////////////////////////////*/

    /// @notice Token balances for each user.
    /// @dev Indexed by user, then by token id.
    mapping(address account => mapping(uint256 tokenId => uint256 balance)) public balanceOf;

    /// @notice Approved addresses for each user.
    /// @dev Indexed by user, then by operator.
    /// @dev Operator is approved to transfer all tokens on behalf of user.
    mapping(address owner => mapping(address operator => bool approvedForAll))
        public isApprovedForAll;

    /*//////////////////////////////////////////////////////////////
                              ERC1155 LOGIC
    //////////////////////////////////////////////////////////////*/

    /// @notice Approve or revoke approval for `operator` to transfer all tokens on behalf of the caller.
    /// @param operator The address to approve or revoke approval for
    /// @param approved True to approve, false to revoke approval
    function setApprovalForAll(address operator, bool approved) public {
        isApprovedForAll[msg.sender][operator] = approved;

        emit ApprovalForAll(msg.sender, operator, approved);
    }

    /// @notice Transfer a single token from one user to another.
    /// @dev Supports approved token transfers.
    /// @param from The user to transfer tokens from
    /// @param to The user to transfer tokens to
    /// @param id The ERC1155 token id to transfer
    /// @param amount The amount of tokens to transfer
    /// @param data Optional data to include in the `onERC1155Received` hook
    function safeTransferFrom(
        address from,
        address to,
        uint256 id,
        uint256 amount,
        bytes calldata data
    ) public virtual {
        if (!(msg.sender == from || isApprovedForAll[from][msg.sender])) revert NotAuthorized();

        balanceOf[from][id] -= amount;

        // balance will never overflow
        unchecked {
            balanceOf[to][id] += amount;
        }

        emit TransferSingle(msg.sender, from, to, id, amount);

        if (to.code.length != 0) {
            if (
                ERC1155Holder(to).onERC1155Received(msg.sender, from, id, amount, data) !=
                ERC1155Holder.onERC1155Received.selector
            ) {
                revert UnsafeRecipient();
            }
        }
    }

    /// @notice Transfer multiple tokens from one user to another.
    /// @dev Supports approved token transfers.
    /// @dev `ids` and `amounts` must be of equal length.
    /// @param from The user to transfer tokens from
    /// @param to The user to transfer tokens to
    /// @param ids The ERC1155 token ids to transfer
    /// @param amounts The amounts of tokens to transfer
    /// @param data Optional data to include in the `onERC1155Received` hook
    function safeBatchTransferFrom(
        address from,
        address to,
        uint256[] calldata ids,
        uint256[] calldata amounts,
        bytes calldata data
    ) public virtual {
        if (!(msg.sender == from || isApprovedForAll[from][msg.sender])) revert NotAuthorized();

        // Storing these outside the loop saves ~15 gas per iteration.
        uint256 id;
        uint256 amount;

        for (uint256 i = 0; i < ids.length; ) {
            id = ids[i];
            amount = amounts[i];

            balanceOf[from][id] -= amount;

            // balance will never overflow
            unchecked {
                balanceOf[to][id] += amount;
            }

            // An array can't have a total length
            // larger than the max uint256 value.
            unchecked {
                ++i;
            }
        }

        emit TransferBatch(msg.sender, from, to, ids, amounts);

        if (to.code.length != 0) {
            if (
                ERC1155Holder(to).onERC1155BatchReceived(msg.sender, from, ids, amounts, data) !=
                ERC1155Holder.onERC1155BatchReceived.selector
            ) {
                revert UnsafeRecipient();
            }
        }
    }

    /// @notice Query balances for multiple users and tokens at once.
    /// @dev `owners` and `ids` should be of equal length.
    /// @param owners The list of users to query balances for
    /// @param ids The list of ERC1155 token ids to query
    /// @return balances The balances for each owner-id pair in the same order as the input arrays
    function balanceOfBatch(
        address[] calldata owners,
        uint256[] calldata ids
    ) public view returns (uint256[] memory balances) {
        balances = new uint256[](owners.length);

        // Unchecked because the only math done is incrementing
        // the array index counter which cannot possibly overflow.
        unchecked {
            for (uint256 i = 0; i < owners.length; ++i) {
                balances[i] = balanceOf[owners[i]][ids[i]];
            }
        }
    }

    /*//////////////////////////////////////////////////////////////
                              ERC165 LOGIC
    //////////////////////////////////////////////////////////////*/

    /// @notice Signal support for ERC165 and ERC1155.
    /// @param interfaceId The interface to check for support
    /// @return Whether the interface is supported
    function supportsInterface(bytes4 interfaceId) public pure returns (bool) {
        return
            interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165
            interfaceId == 0xd9b67a26; // ERC165 Interface ID for ERC1155
    }

    /*//////////////////////////////////////////////////////////////
                        INTERNAL MINT/BURN LOGIC
    //////////////////////////////////////////////////////////////*/

    /// @notice Internal utility to mint tokens to a user's account.
    /// @param to The user to mint tokens to
    /// @param id The ERC1155 token id to mint
    /// @param amount The amount of tokens to mint
    function _mint(address to, uint256 id, uint256 amount) internal {
        // balance will never overflow
        unchecked {
            balanceOf[to][id] += amount;
        }

        emit TransferSingle(msg.sender, address(0), to, id, amount);

        if (to.code.length != 0) {
            if (
                ERC1155Holder(to).onERC1155Received(msg.sender, address(0), id, amount, "") !=
                ERC1155Holder.onERC1155Received.selector
            ) {
                revert UnsafeRecipient();
            }
        }
    }

    /// @notice Internal utility to burn tokens from a user's account.
    /// @param from The user to burn tokens from
    /// @param id The ERC1155 token id to mint
    /// @param amount The amount of tokens to burn
    function _burn(address from, uint256 id, uint256 amount) internal {
        balanceOf[from][id] -= amount;

        emit TransferSingle(msg.sender, from, address(0), id, amount);
    }
}

File 25 of 50 : TransientReentrancyGuard.sol
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.24;

/// @notice Gas optimized reentrancy protection for smart contracts. Leverages Cancun transient storage.
/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/TransientReentrancyGuard.sol)
/// @author Modified from Soledge (https://github.com/Vectorized/soledge/blob/main/src/utils/ReentrancyGuard.sol)
abstract contract TransientReentrancyGuard {
    /// Warning: Be careful to avoid collisions with this hand picked slot!
    uint256 private constant REENTRANCY_GUARD_SLOT = 0x1FACE81BADDEADBEEF;


    modifier nonReentrant() virtual {
        bool noReentrancy;

        /// @solidity memory-safe-assembly
        assembly {
            noReentrancy := iszero(tload(REENTRANCY_GUARD_SLOT))

            // Any non-zero value would work, but
            // ADDRESS is cheap and certainly not 0.
            // Wastes a bit of gas doing this before
            // require in the revert path, but we're
            // only optimizing for the happy path here.
            tstore(REENTRANCY_GUARD_SLOT, address())
        }

        require(noReentrancy, "REENTRANCY");

        _;

        /// @solidity memory-safe-assembly
        assembly {
            // Need to set back to zero, as transient
            // storage is only cleared at the end of the
            // tx, not the end of the outermost call frame.
            tstore(REENTRANCY_GUARD_SLOT, 0)
        }
    }
}

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {SafeCast} from "../libraries/SafeCast.sol";

/// @dev Two `int128` values packed into a single `int256` where the upper 128 bits represent the amount0
/// and the lower 128 bits represent the amount1.
type BalanceDelta is int256;

using {add as +, sub as -, eq as ==, neq as !=} for BalanceDelta global;
using BalanceDeltaLibrary for BalanceDelta global;
using SafeCast for int256;

function toBalanceDelta(int128 _amount0, int128 _amount1) pure returns (BalanceDelta balanceDelta) {
    assembly ("memory-safe") {
        balanceDelta := or(shl(128, _amount0), and(sub(shl(128, 1), 1), _amount1))
    }
}

function add(BalanceDelta a, BalanceDelta b) pure returns (BalanceDelta) {
    int256 res0;
    int256 res1;
    assembly ("memory-safe") {
        let a0 := sar(128, a)
        let a1 := signextend(15, a)
        let b0 := sar(128, b)
        let b1 := signextend(15, b)
        res0 := add(a0, b0)
        res1 := add(a1, b1)
    }
    return toBalanceDelta(res0.toInt128(), res1.toInt128());
}

function sub(BalanceDelta a, BalanceDelta b) pure returns (BalanceDelta) {
    int256 res0;
    int256 res1;
    assembly ("memory-safe") {
        let a0 := sar(128, a)
        let a1 := signextend(15, a)
        let b0 := sar(128, b)
        let b1 := signextend(15, b)
        res0 := sub(a0, b0)
        res1 := sub(a1, b1)
    }
    return toBalanceDelta(res0.toInt128(), res1.toInt128());
}

function eq(BalanceDelta a, BalanceDelta b) pure returns (bool) {
    return BalanceDelta.unwrap(a) == BalanceDelta.unwrap(b);
}

function neq(BalanceDelta a, BalanceDelta b) pure returns (bool) {
    return BalanceDelta.unwrap(a) != BalanceDelta.unwrap(b);
}

/// @notice Library for getting the amount0 and amount1 deltas from the BalanceDelta type
library BalanceDeltaLibrary {
    /// @notice A BalanceDelta of 0
    BalanceDelta public constant ZERO_DELTA = BalanceDelta.wrap(0);

    function amount0(BalanceDelta balanceDelta) internal pure returns (int128 _amount0) {
        assembly ("memory-safe") {
            _amount0 := sar(128, balanceDelta)
        }
    }

    function amount1(BalanceDelta balanceDelta) internal pure returns (int128 _amount1) {
        assembly ("memory-safe") {
            _amount1 := signextend(15, balanceDelta)
        }
    }
}

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {PoolKey} from "../types/PoolKey.sol";
import {BalanceDelta} from "../types/BalanceDelta.sol";
import {IPoolManager} from "./IPoolManager.sol";
import {BeforeSwapDelta} from "../types/BeforeSwapDelta.sol";

/// @notice V4 decides whether to invoke specific hooks by inspecting the least significant bits
/// of the address that the hooks contract is deployed to.
/// For example, a hooks contract deployed to address: 0x0000000000000000000000000000000000002400
/// has the lowest bits '10 0100 0000 0000' which would cause the 'before initialize' and 'after add liquidity' hooks to be used.
/// See the Hooks library for the full spec.
/// @dev Should only be callable by the v4 PoolManager.
interface IHooks {
    /// @notice The hook called before the state of a pool is initialized
    /// @param sender The initial msg.sender for the initialize call
    /// @param key The key for the pool being initialized
    /// @param sqrtPriceX96 The sqrt(price) of the pool as a Q64.96
    /// @return bytes4 The function selector for the hook
    function beforeInitialize(address sender, PoolKey calldata key, uint160 sqrtPriceX96) external returns (bytes4);

    /// @notice The hook called after the state of a pool is initialized
    /// @param sender The initial msg.sender for the initialize call
    /// @param key The key for the pool being initialized
    /// @param sqrtPriceX96 The sqrt(price) of the pool as a Q64.96
    /// @param tick The current tick after the state of a pool is initialized
    /// @return bytes4 The function selector for the hook
    function afterInitialize(address sender, PoolKey calldata key, uint160 sqrtPriceX96, int24 tick)
        external
        returns (bytes4);

    /// @notice The hook called before liquidity is added
    /// @param sender The initial msg.sender for the add liquidity call
    /// @param key The key for the pool
    /// @param params The parameters for adding liquidity
    /// @param hookData Arbitrary data handed into the PoolManager by the liquidity provider to be passed on to the hook
    /// @return bytes4 The function selector for the hook
    function beforeAddLiquidity(
        address sender,
        PoolKey calldata key,
        IPoolManager.ModifyLiquidityParams calldata params,
        bytes calldata hookData
    ) external returns (bytes4);

    /// @notice The hook called after liquidity is added
    /// @param sender The initial msg.sender for the add liquidity call
    /// @param key The key for the pool
    /// @param params The parameters for adding liquidity
    /// @param delta The caller's balance delta after adding liquidity; the sum of principal delta, fees accrued, and hook delta
    /// @param feesAccrued The fees accrued since the last time fees were collected from this position
    /// @param hookData Arbitrary data handed into the PoolManager by the liquidity provider to be passed on to the hook
    /// @return bytes4 The function selector for the hook
    /// @return BalanceDelta The hook's delta in token0 and token1. Positive: the hook is owed/took currency, negative: the hook owes/sent currency
    function afterAddLiquidity(
        address sender,
        PoolKey calldata key,
        IPoolManager.ModifyLiquidityParams calldata params,
        BalanceDelta delta,
        BalanceDelta feesAccrued,
        bytes calldata hookData
    ) external returns (bytes4, BalanceDelta);

    /// @notice The hook called before liquidity is removed
    /// @param sender The initial msg.sender for the remove liquidity call
    /// @param key The key for the pool
    /// @param params The parameters for removing liquidity
    /// @param hookData Arbitrary data handed into the PoolManager by the liquidity provider to be be passed on to the hook
    /// @return bytes4 The function selector for the hook
    function beforeRemoveLiquidity(
        address sender,
        PoolKey calldata key,
        IPoolManager.ModifyLiquidityParams calldata params,
        bytes calldata hookData
    ) external returns (bytes4);

    /// @notice The hook called after liquidity is removed
    /// @param sender The initial msg.sender for the remove liquidity call
    /// @param key The key for the pool
    /// @param params The parameters for removing liquidity
    /// @param delta The caller's balance delta after removing liquidity; the sum of principal delta, fees accrued, and hook delta
    /// @param feesAccrued The fees accrued since the last time fees were collected from this position
    /// @param hookData Arbitrary data handed into the PoolManager by the liquidity provider to be be passed on to the hook
    /// @return bytes4 The function selector for the hook
    /// @return BalanceDelta The hook's delta in token0 and token1. Positive: the hook is owed/took currency, negative: the hook owes/sent currency
    function afterRemoveLiquidity(
        address sender,
        PoolKey calldata key,
        IPoolManager.ModifyLiquidityParams calldata params,
        BalanceDelta delta,
        BalanceDelta feesAccrued,
        bytes calldata hookData
    ) external returns (bytes4, BalanceDelta);

    /// @notice The hook called before a swap
    /// @param sender The initial msg.sender for the swap call
    /// @param key The key for the pool
    /// @param params The parameters for the swap
    /// @param hookData Arbitrary data handed into the PoolManager by the swapper to be be passed on to the hook
    /// @return bytes4 The function selector for the hook
    /// @return BeforeSwapDelta The hook's delta in specified and unspecified currencies. Positive: the hook is owed/took currency, negative: the hook owes/sent currency
    /// @return uint24 Optionally override the lp fee, only used if three conditions are met: 1. the Pool has a dynamic fee, 2. the value's 2nd highest bit is set (23rd bit, 0x400000), and 3. the value is less than or equal to the maximum fee (1 million)
    function beforeSwap(
        address sender,
        PoolKey calldata key,
        IPoolManager.SwapParams calldata params,
        bytes calldata hookData
    ) external returns (bytes4, BeforeSwapDelta, uint24);

    /// @notice The hook called after a swap
    /// @param sender The initial msg.sender for the swap call
    /// @param key The key for the pool
    /// @param params The parameters for the swap
    /// @param delta The amount owed to the caller (positive) or owed to the pool (negative)
    /// @param hookData Arbitrary data handed into the PoolManager by the swapper to be be passed on to the hook
    /// @return bytes4 The function selector for the hook
    /// @return int128 The hook's delta in unspecified currency. Positive: the hook is owed/took currency, negative: the hook owes/sent currency
    function afterSwap(
        address sender,
        PoolKey calldata key,
        IPoolManager.SwapParams calldata params,
        BalanceDelta delta,
        bytes calldata hookData
    ) external returns (bytes4, int128);

    /// @notice The hook called before donate
    /// @param sender The initial msg.sender for the donate call
    /// @param key The key for the pool
    /// @param amount0 The amount of token0 being donated
    /// @param amount1 The amount of token1 being donated
    /// @param hookData Arbitrary data handed into the PoolManager by the donor to be be passed on to the hook
    /// @return bytes4 The function selector for the hook
    function beforeDonate(
        address sender,
        PoolKey calldata key,
        uint256 amount0,
        uint256 amount1,
        bytes calldata hookData
    ) external returns (bytes4);

    /// @notice The hook called after donate
    /// @param sender The initial msg.sender for the donate call
    /// @param key The key for the pool
    /// @param amount0 The amount of token0 being donated
    /// @param amount1 The amount of token1 being donated
    /// @param hookData Arbitrary data handed into the PoolManager by the donor to be be passed on to the hook
    /// @return bytes4 The function selector for the hook
    function afterDonate(
        address sender,
        PoolKey calldata key,
        uint256 amount0,
        uint256 amount1,
        bytes calldata hookData
    ) external returns (bytes4);
}

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/// @notice Interface for claims over a contract balance, wrapped as a ERC6909
interface IERC6909Claims {
    /*//////////////////////////////////////////////////////////////
                                 EVENTS
    //////////////////////////////////////////////////////////////*/

    event OperatorSet(address indexed owner, address indexed operator, bool approved);

    event Approval(address indexed owner, address indexed spender, uint256 indexed id, uint256 amount);

    event Transfer(address caller, address indexed from, address indexed to, uint256 indexed id, uint256 amount);

    /*//////////////////////////////////////////////////////////////
                                 FUNCTIONS
    //////////////////////////////////////////////////////////////*/

    /// @notice Owner balance of an id.
    /// @param owner The address of the owner.
    /// @param id The id of the token.
    /// @return amount The balance of the token.
    function balanceOf(address owner, uint256 id) external view returns (uint256 amount);

    /// @notice Spender allowance of an id.
    /// @param owner The address of the owner.
    /// @param spender The address of the spender.
    /// @param id The id of the token.
    /// @return amount The allowance of the token.
    function allowance(address owner, address spender, uint256 id) external view returns (uint256 amount);

    /// @notice Checks if a spender is approved by an owner as an operator
    /// @param owner The address of the owner.
    /// @param spender The address of the spender.
    /// @return approved The approval status.
    function isOperator(address owner, address spender) external view returns (bool approved);

    /// @notice Transfers an amount of an id from the caller to a receiver.
    /// @param receiver The address of the receiver.
    /// @param id The id of the token.
    /// @param amount The amount of the token.
    /// @return bool True, always, unless the function reverts
    function transfer(address receiver, uint256 id, uint256 amount) external returns (bool);

    /// @notice Transfers an amount of an id from a sender to a receiver.
    /// @param sender The address of the sender.
    /// @param receiver The address of the receiver.
    /// @param id The id of the token.
    /// @param amount The amount of the token.
    /// @return bool True, always, unless the function reverts
    function transferFrom(address sender, address receiver, uint256 id, uint256 amount) external returns (bool);

    /// @notice Approves an amount of an id to a spender.
    /// @param spender The address of the spender.
    /// @param id The id of the token.
    /// @param amount The amount of the token.
    /// @return bool True, always
    function approve(address spender, uint256 id, uint256 amount) external returns (bool);

    /// @notice Sets or removes an operator for the caller.
    /// @param operator The address of the operator.
    /// @param approved The approval status.
    /// @return bool True, always
    function setOperator(address operator, bool approved) external returns (bool);
}

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {Currency} from "../types/Currency.sol";
import {PoolId} from "../types/PoolId.sol";
import {PoolKey} from "../types/PoolKey.sol";

/// @notice Interface for all protocol-fee related functions in the pool manager
interface IProtocolFees {
    /// @notice Thrown when protocol fee is set too high
    error ProtocolFeeTooLarge(uint24 fee);

    /// @notice Thrown when collectProtocolFees or setProtocolFee is not called by the controller.
    error InvalidCaller();

    /// @notice Thrown when collectProtocolFees is attempted on a token that is synced.
    error ProtocolFeeCurrencySynced();

    /// @notice Emitted when the protocol fee controller address is updated in setProtocolFeeController.
    event ProtocolFeeControllerUpdated(address indexed protocolFeeController);

    /// @notice Emitted when the protocol fee is updated for a pool.
    event ProtocolFeeUpdated(PoolId indexed id, uint24 protocolFee);

    /// @notice Given a currency address, returns the protocol fees accrued in that currency
    /// @param currency The currency to check
    /// @return amount The amount of protocol fees accrued in the currency
    function protocolFeesAccrued(Currency currency) external view returns (uint256 amount);

    /// @notice Sets the protocol fee for the given pool
    /// @param key The key of the pool to set a protocol fee for
    /// @param newProtocolFee The fee to set
    function setProtocolFee(PoolKey memory key, uint24 newProtocolFee) external;

    /// @notice Sets the protocol fee controller
    /// @param controller The new protocol fee controller
    function setProtocolFeeController(address controller) external;

    /// @notice Collects the protocol fees for a given recipient and currency, returning the amount collected
    /// @dev This will revert if the contract is unlocked
    /// @param recipient The address to receive the protocol fees
    /// @param currency The currency to withdraw
    /// @param amount The amount of currency to withdraw
    /// @return amountCollected The amount of currency successfully withdrawn
    function collectProtocolFees(address recipient, Currency currency, uint256 amount)
        external
        returns (uint256 amountCollected);

    /// @notice Returns the current protocol fee controller address
    /// @return address The current protocol fee controller address
    function protocolFeeController() external view returns (address);
}

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/// @notice Interface for functions to access any storage slot in a contract
interface IExtsload {
    /// @notice Called by external contracts to access granular pool state
    /// @param slot Key of slot to sload
    /// @return value The value of the slot as bytes32
    function extsload(bytes32 slot) external view returns (bytes32 value);

    /// @notice Called by external contracts to access granular pool state
    /// @param startSlot Key of slot to start sloading from
    /// @param nSlots Number of slots to load into return value
    /// @return values List of loaded values.
    function extsload(bytes32 startSlot, uint256 nSlots) external view returns (bytes32[] memory values);

    /// @notice Called by external contracts to access sparse pool state
    /// @param slots List of slots to SLOAD from.
    /// @return values List of loaded values.
    function extsload(bytes32[] calldata slots) external view returns (bytes32[] memory values);
}

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/// @notice Interface for functions to access any transient storage slot in a contract
interface IExttload {
    /// @notice Called by external contracts to access transient storage of the contract
    /// @param slot Key of slot to tload
    /// @return value The value of the slot as bytes32
    function exttload(bytes32 slot) external view returns (bytes32 value);

    /// @notice Called by external contracts to access sparse transient pool state
    /// @param slots List of slots to tload
    /// @return values List of loaded values
    function exttload(bytes32[] calldata slots) external view returns (bytes32[] memory values);
}

File 32 of 50 : FixedPointMathLib.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

/// @notice Arithmetic library with operations for fixed-point numbers.
/// @author Solady (https://github.com/vectorized/solady/blob/main/src/utils/FixedPointMathLib.sol)
/// @author Modified from Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/FixedPointMathLib.sol)
library FixedPointMathLib {
    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                       CUSTOM ERRORS                        */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev The operation failed, as the output exceeds the maximum value of uint256.
    error ExpOverflow();

    /// @dev The operation failed, as the output exceeds the maximum value of uint256.
    error FactorialOverflow();

    /// @dev The operation failed, due to an overflow.
    error RPowOverflow();

    /// @dev The mantissa is too big to fit.
    error MantissaOverflow();

    /// @dev The operation failed, due to an multiplication overflow.
    error MulWadFailed();

    /// @dev The operation failed, due to an multiplication overflow.
    error SMulWadFailed();

    /// @dev The operation failed, either due to a multiplication overflow, or a division by a zero.
    error DivWadFailed();

    /// @dev The operation failed, either due to a multiplication overflow, or a division by a zero.
    error SDivWadFailed();

    /// @dev The operation failed, either due to a multiplication overflow, or a division by a zero.
    error MulDivFailed();

    /// @dev The division failed, as the denominator is zero.
    error DivFailed();

    /// @dev The full precision multiply-divide operation failed, either due
    /// to the result being larger than 256 bits, or a division by a zero.
    error FullMulDivFailed();

    /// @dev The output is undefined, as the input is less-than-or-equal to zero.
    error LnWadUndefined();

    /// @dev The input outside the acceptable domain.
    error OutOfDomain();

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                         CONSTANTS                          */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev The scalar of ETH and most ERC20s.
    uint256 internal constant WAD = 1e18;

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*              SIMPLIFIED FIXED POINT OPERATIONS             */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev Equivalent to `(x * y) / WAD` rounded down.
    function mulWad(uint256 x, uint256 y) internal pure returns (uint256 z) {
        /// @solidity memory-safe-assembly
        assembly {
            // Equivalent to `require(y == 0 || x <= type(uint256).max / y)`.
            if mul(y, gt(x, div(not(0), y))) {
                mstore(0x00, 0xbac65e5b) // `MulWadFailed()`.
                revert(0x1c, 0x04)
            }
            z := div(mul(x, y), WAD)
        }
    }

    /// @dev Equivalent to `(x * y) / WAD` rounded down.
    function sMulWad(int256 x, int256 y) internal pure returns (int256 z) {
        /// @solidity memory-safe-assembly
        assembly {
            z := mul(x, y)
            // Equivalent to `require((x == 0 || z / x == y) && !(x == -1 && y == type(int256).min))`.
            if iszero(gt(or(iszero(x), eq(sdiv(z, x), y)), lt(not(x), eq(y, shl(255, 1))))) {
                mstore(0x00, 0xedcd4dd4) // `SMulWadFailed()`.
                revert(0x1c, 0x04)
            }
            z := sdiv(z, WAD)
        }
    }

    /// @dev Equivalent to `(x * y) / WAD` rounded down, but without overflow checks.
    function rawMulWad(uint256 x, uint256 y) internal pure returns (uint256 z) {
        /// @solidity memory-safe-assembly
        assembly {
            z := div(mul(x, y), WAD)
        }
    }

    /// @dev Equivalent to `(x * y) / WAD` rounded down, but without overflow checks.
    function rawSMulWad(int256 x, int256 y) internal pure returns (int256 z) {
        /// @solidity memory-safe-assembly
        assembly {
            z := sdiv(mul(x, y), WAD)
        }
    }

    /// @dev Equivalent to `(x * y) / WAD` rounded up.
    function mulWadUp(uint256 x, uint256 y) internal pure returns (uint256 z) {
        /// @solidity memory-safe-assembly
        assembly {
            // Equivalent to `require(y == 0 || x <= type(uint256).max / y)`.
            if mul(y, gt(x, div(not(0), y))) {
                mstore(0x00, 0xbac65e5b) // `MulWadFailed()`.
                revert(0x1c, 0x04)
            }
            z := add(iszero(iszero(mod(mul(x, y), WAD))), div(mul(x, y), WAD))
        }
    }

    /// @dev Equivalent to `(x * y) / WAD` rounded up, but without overflow checks.
    function rawMulWadUp(uint256 x, uint256 y) internal pure returns (uint256 z) {
        /// @solidity memory-safe-assembly
        assembly {
            z := add(iszero(iszero(mod(mul(x, y), WAD))), div(mul(x, y), WAD))
        }
    }

    /// @dev Equivalent to `(x * WAD) / y` rounded down.
    function divWad(uint256 x, uint256 y) internal pure returns (uint256 z) {
        /// @solidity memory-safe-assembly
        assembly {
            // Equivalent to `require(y != 0 && (WAD == 0 || x <= type(uint256).max / WAD))`.
            if iszero(mul(y, iszero(mul(WAD, gt(x, div(not(0), WAD)))))) {
                mstore(0x00, 0x7c5f487d) // `DivWadFailed()`.
                revert(0x1c, 0x04)
            }
            z := div(mul(x, WAD), y)
        }
    }

    /// @dev Equivalent to `(x * WAD) / y` rounded down.
    function sDivWad(int256 x, int256 y) internal pure returns (int256 z) {
        /// @solidity memory-safe-assembly
        assembly {
            z := mul(x, WAD)
            // Equivalent to `require(y != 0 && ((x * WAD) / WAD == x))`.
            if iszero(and(iszero(iszero(y)), eq(sdiv(z, WAD), x))) {
                mstore(0x00, 0x5c43740d) // `SDivWadFailed()`.
                revert(0x1c, 0x04)
            }
            z := sdiv(mul(x, WAD), y)
        }
    }

    /// @dev Equivalent to `(x * WAD) / y` rounded down, but without overflow and divide by zero checks.
    function rawDivWad(uint256 x, uint256 y) internal pure returns (uint256 z) {
        /// @solidity memory-safe-assembly
        assembly {
            z := div(mul(x, WAD), y)
        }
    }

    /// @dev Equivalent to `(x * WAD) / y` rounded down, but without overflow and divide by zero checks.
    function rawSDivWad(int256 x, int256 y) internal pure returns (int256 z) {
        /// @solidity memory-safe-assembly
        assembly {
            z := sdiv(mul(x, WAD), y)
        }
    }

    /// @dev Equivalent to `(x * WAD) / y` rounded up.
    function divWadUp(uint256 x, uint256 y) internal pure returns (uint256 z) {
        /// @solidity memory-safe-assembly
        assembly {
            // Equivalent to `require(y != 0 && (WAD == 0 || x <= type(uint256).max / WAD))`.
            if iszero(mul(y, iszero(mul(WAD, gt(x, div(not(0), WAD)))))) {
                mstore(0x00, 0x7c5f487d) // `DivWadFailed()`.
                revert(0x1c, 0x04)
            }
            z := add(iszero(iszero(mod(mul(x, WAD), y))), div(mul(x, WAD), y))
        }
    }

    /// @dev Equivalent to `(x * WAD) / y` rounded up, but without overflow and divide by zero checks.
    function rawDivWadUp(uint256 x, uint256 y) internal pure returns (uint256 z) {
        /// @solidity memory-safe-assembly
        assembly {
            z := add(iszero(iszero(mod(mul(x, WAD), y))), div(mul(x, WAD), y))
        }
    }

    /// @dev Equivalent to `x` to the power of `y`.
    /// because `x ** y = (e ** ln(x)) ** y = e ** (ln(x) * y)`.
    function powWad(int256 x, int256 y) internal pure returns (int256) {
        // Using `ln(x)` means `x` must be greater than 0.
        return expWad((lnWad(x) * y) / int256(WAD));
    }

    /// @dev Returns `exp(x)`, denominated in `WAD`.
    /// Credit to Remco Bloemen under MIT license: https://2π.com/22/exp-ln
    function expWad(int256 x) internal pure returns (int256 r) {
        unchecked {
            // When the result is less than 0.5 we return zero.
            // This happens when `x <= (log(1e-18) * 1e18) ~ -4.15e19`.
            if (x <= -41446531673892822313) return r;

            /// @solidity memory-safe-assembly
            assembly {
                // When the result is greater than `(2**255 - 1) / 1e18` we can not represent it as
                // an int. This happens when `x >= floor(log((2**255 - 1) / 1e18) * 1e18) ≈ 135`.
                if iszero(slt(x, 135305999368893231589)) {
                    mstore(0x00, 0xa37bfec9) // `ExpOverflow()`.
                    revert(0x1c, 0x04)
                }
            }

            // `x` is now in the range `(-42, 136) * 1e18`. Convert to `(-42, 136) * 2**96`
            // for more intermediate precision and a binary basis. This base conversion
            // is a multiplication by 1e18 / 2**96 = 5**18 / 2**78.
            x = (x << 78) / 5 ** 18;

            // Reduce range of x to (-½ ln 2, ½ ln 2) * 2**96 by factoring out powers
            // of two such that exp(x) = exp(x') * 2**k, where k is an integer.
            // Solving this gives k = round(x / log(2)) and x' = x - k * log(2).
            int256 k = ((x << 96) / 54916777467707473351141471128 + 2 ** 95) >> 96;
            x = x - k * 54916777467707473351141471128;

            // `k` is in the range `[-61, 195]`.

            // Evaluate using a (6, 7)-term rational approximation.
            // `p` is made monic, we'll multiply by a scale factor later.
            int256 y = x + 1346386616545796478920950773328;
            y = ((y * x) >> 96) + 57155421227552351082224309758442;
            int256 p = y + x - 94201549194550492254356042504812;
            p = ((p * y) >> 96) + 28719021644029726153956944680412240;
            p = p * x + (4385272521454847904659076985693276 << 96);

            // We leave `p` in `2**192` basis so we don't need to scale it back up for the division.
            int256 q = x - 2855989394907223263936484059900;
            q = ((q * x) >> 96) + 50020603652535783019961831881945;
            q = ((q * x) >> 96) - 533845033583426703283633433725380;
            q = ((q * x) >> 96) + 3604857256930695427073651918091429;
            q = ((q * x) >> 96) - 14423608567350463180887372962807573;
            q = ((q * x) >> 96) + 26449188498355588339934803723976023;

            /// @solidity memory-safe-assembly
            assembly {
                // Div in assembly because solidity adds a zero check despite the unchecked.
                // The q polynomial won't have zeros in the domain as all its roots are complex.
                // No scaling is necessary because p is already `2**96` too large.
                r := sdiv(p, q)
            }

            // r should be in the range `(0.09, 0.25) * 2**96`.

            // We now need to multiply r by:
            // - The scale factor `s ≈ 6.031367120`.
            // - The `2**k` factor from the range reduction.
            // - The `1e18 / 2**96` factor for base conversion.
            // We do this all at once, with an intermediate result in `2**213`
            // basis, so the final right shift is always by a positive amount.
            r = int256(
                (uint256(r) * 3822833074963236453042738258902158003155416615667) >> uint256(195 - k)
            );
        }
    }

    /// @dev Returns `ln(x)`, denominated in `WAD`.
    /// Credit to Remco Bloemen under MIT license: https://2π.com/22/exp-ln
    function lnWad(int256 x) internal pure returns (int256 r) {
        /// @solidity memory-safe-assembly
        assembly {
            // We want to convert `x` from `10**18` fixed point to `2**96` fixed point.
            // We do this by multiplying by `2**96 / 10**18`. But since
            // `ln(x * C) = ln(x) + ln(C)`, we can simply do nothing here
            // and add `ln(2**96 / 10**18)` at the end.

            // Compute `k = log2(x) - 96`, `r = 159 - k = 255 - log2(x) = 255 ^ log2(x)`.
            r := shl(7, lt(0xffffffffffffffffffffffffffffffff, x))
            r := or(r, shl(6, lt(0xffffffffffffffff, shr(r, x))))
            r := or(r, shl(5, lt(0xffffffff, shr(r, x))))
            r := or(r, shl(4, lt(0xffff, shr(r, x))))
            r := or(r, shl(3, lt(0xff, shr(r, x))))
            // We place the check here for more optimal stack operations.
            if iszero(sgt(x, 0)) {
                mstore(0x00, 0x1615e638) // `LnWadUndefined()`.
                revert(0x1c, 0x04)
            }
            // forgefmt: disable-next-item
            r := xor(r, byte(and(0x1f, shr(shr(r, x), 0x8421084210842108cc6318c6db6d54be)),
                0xf8f9f9faf9fdfafbf9fdfcfdfafbfcfef9fafdfafcfcfbfefafafcfbffffffff))

            // Reduce range of x to (1, 2) * 2**96
            // ln(2^k * x) = k * ln(2) + ln(x)
            x := shr(159, shl(r, x))

            // Evaluate using a (8, 8)-term rational approximation.
            // `p` is made monic, we will multiply by a scale factor later.
            // forgefmt: disable-next-item
            let p := sub( // This heavily nested expression is to avoid stack-too-deep for via-ir.
                sar(96, mul(add(43456485725739037958740375743393,
                sar(96, mul(add(24828157081833163892658089445524,
                sar(96, mul(add(3273285459638523848632254066296,
                    x), x))), x))), x)), 11111509109440967052023855526967)
            p := sub(sar(96, mul(p, x)), 45023709667254063763336534515857)
            p := sub(sar(96, mul(p, x)), 14706773417378608786704636184526)
            p := sub(mul(p, x), shl(96, 795164235651350426258249787498))
            // We leave `p` in `2**192` basis so we don't need to scale it back up for the division.

            // `q` is monic by convention.
            let q := add(5573035233440673466300451813936, x)
            q := add(71694874799317883764090561454958, sar(96, mul(x, q)))
            q := add(283447036172924575727196451306956, sar(96, mul(x, q)))
            q := add(401686690394027663651624208769553, sar(96, mul(x, q)))
            q := add(204048457590392012362485061816622, sar(96, mul(x, q)))
            q := add(31853899698501571402653359427138, sar(96, mul(x, q)))
            q := add(909429971244387300277376558375, sar(96, mul(x, q)))

            // `p / q` is in the range `(0, 0.125) * 2**96`.

            // Finalization, we need to:
            // - Multiply by the scale factor `s = 5.549…`.
            // - Add `ln(2**96 / 10**18)`.
            // - Add `k * ln(2)`.
            // - Multiply by `10**18 / 2**96 = 5**18 >> 78`.

            // The q polynomial is known not to have zeros in the domain.
            // No scaling required because p is already `2**96` too large.
            p := sdiv(p, q)
            // Multiply by the scaling factor: `s * 5**18 * 2**96`, base is now `5**18 * 2**192`.
            p := mul(1677202110996718588342820967067443963516166, p)
            // Add `ln(2) * k * 5**18 * 2**192`.
            // forgefmt: disable-next-item
            p := add(mul(16597577552685614221487285958193947469193820559219878177908093499208371, sub(159, r)), p)
            // Add `ln(2**96 / 10**18) * 5**18 * 2**192`.
            p := add(600920179829731861736702779321621459595472258049074101567377883020018308, p)
            // Base conversion: mul `2**18 / 2**192`.
            r := sar(174, p)
        }
    }

    /// @dev Returns `W_0(x)`, denominated in `WAD`.
    /// See: https://en.wikipedia.org/wiki/Lambert_W_function
    /// a.k.a. Product log function. This is an approximation of the principal branch.
    function lambertW0Wad(int256 x) internal pure returns (int256 w) {
        // forgefmt: disable-next-item
        unchecked {
            if ((w = x) <= -367879441171442322) revert OutOfDomain(); // `x` less than `-1/e`.
            int256 wad = int256(WAD);
            int256 p = x;
            uint256 c; // Whether we need to avoid catastrophic cancellation.
            uint256 i = 4; // Number of iterations.
            if (w <= 0x1ffffffffffff) {
                if (-0x4000000000000 <= w) {
                    i = 1; // Inputs near zero only take one step to converge.
                } else if (w <= -0x3ffffffffffffff) {
                    i = 32; // Inputs near `-1/e` take very long to converge.
                }
            } else if (w >> 63 == 0) {
                /// @solidity memory-safe-assembly
                assembly {
                    // Inline log2 for more performance, since the range is small.
                    let v := shr(49, w)
                    let l := shl(3, lt(0xff, v))
                    l := add(or(l, byte(and(0x1f, shr(shr(l, v), 0x8421084210842108cc6318c6db6d54be)),
                        0x0706060506020504060203020504030106050205030304010505030400000000)), 49)
                    w := sdiv(shl(l, 7), byte(sub(l, 31), 0x0303030303030303040506080c13))
                    c := gt(l, 60)
                    i := add(2, add(gt(l, 53), c))
                }
            } else {
                int256 ll = lnWad(w = lnWad(w));
                /// @solidity memory-safe-assembly
                assembly {
                    // `w = ln(x) - ln(ln(x)) + b * ln(ln(x)) / ln(x)`.
                    w := add(sdiv(mul(ll, 1023715080943847266), w), sub(w, ll))
                    i := add(3, iszero(shr(68, x)))
                    c := iszero(shr(143, x))
                }
                if (c == 0) {
                    do { // If `x` is big, use Newton's so that intermediate values won't overflow.
                        int256 e = expWad(w);
                        /// @solidity memory-safe-assembly
                        assembly {
                            let t := mul(w, div(e, wad))
                            w := sub(w, sdiv(sub(t, x), div(add(e, t), wad)))
                        }
                        if (p <= w) break;
                        p = w;
                    } while (--i != 0);
                    /// @solidity memory-safe-assembly
                    assembly {
                        w := sub(w, sgt(w, 2))
                    }
                    return w;
                }
            }
            do { // Otherwise, use Halley's for faster convergence.
                int256 e = expWad(w);
                /// @solidity memory-safe-assembly
                assembly {
                    let t := add(w, wad)
                    let s := sub(mul(w, e), mul(x, wad))
                    w := sub(w, sdiv(mul(s, wad), sub(mul(e, t), sdiv(mul(add(t, wad), s), add(t, t)))))
                }
                if (p <= w) break;
                p = w;
            } while (--i != c);
            /// @solidity memory-safe-assembly
            assembly {
                w := sub(w, sgt(w, 2))
            }
            // For certain ranges of `x`, we'll use the quadratic-rate recursive formula of
            // R. Iacono and J.P. Boyd for the last iteration, to avoid catastrophic cancellation.
            if (c != 0) {
                int256 t = w | 1;
                /// @solidity memory-safe-assembly
                assembly {
                    x := sdiv(mul(x, wad), t)
                }
                x = (t * (wad + lnWad(x)));
                /// @solidity memory-safe-assembly
                assembly {
                    w := sdiv(x, add(wad, t))
                }
            }
        }
    }

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                  GENERAL NUMBER UTILITIES                  */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev Calculates `floor(x * y / d)` with full precision.
    /// Throws if result overflows a uint256 or when `d` is zero.
    /// Credit to Remco Bloemen under MIT license: https://2π.com/21/muldiv
    function fullMulDiv(uint256 x, uint256 y, uint256 d) internal pure returns (uint256 result) {
        /// @solidity memory-safe-assembly
        assembly {
            for {} 1 {} {
                // 512-bit multiply `[p1 p0] = x * y`.
                // Compute the product mod `2**256` and mod `2**256 - 1`
                // then use the Chinese Remainder Theorem to reconstruct
                // the 512 bit result. The result is stored in two 256
                // variables such that `product = p1 * 2**256 + p0`.

                // Least significant 256 bits of the product.
                result := mul(x, y) // Temporarily use `result` as `p0` to save gas.
                let mm := mulmod(x, y, not(0))
                // Most significant 256 bits of the product.
                let p1 := sub(mm, add(result, lt(mm, result)))

                // Handle non-overflow cases, 256 by 256 division.
                if iszero(p1) {
                    if iszero(d) {
                        mstore(0x00, 0xae47f702) // `FullMulDivFailed()`.
                        revert(0x1c, 0x04)
                    }
                    result := div(result, d)
                    break
                }

                // Make sure the result is less than `2**256`. Also prevents `d == 0`.
                if iszero(gt(d, p1)) {
                    mstore(0x00, 0xae47f702) // `FullMulDivFailed()`.
                    revert(0x1c, 0x04)
                }

                /*------------------- 512 by 256 division --------------------*/

                // Make division exact by subtracting the remainder from `[p1 p0]`.
                // Compute remainder using mulmod.
                let r := mulmod(x, y, d)
                // `t` is the least significant bit of `d`.
                // Always greater or equal to 1.
                let t := and(d, sub(0, d))
                // Divide `d` by `t`, which is a power of two.
                d := div(d, t)
                // Invert `d mod 2**256`
                // Now that `d` is an odd number, it has an inverse
                // modulo `2**256` such that `d * inv = 1 mod 2**256`.
                // Compute the inverse by starting with a seed that is correct
                // correct for four bits. That is, `d * inv = 1 mod 2**4`.
                let inv := xor(2, mul(3, d))
                // Now use Newton-Raphson iteration to improve the precision.
                // Thanks to Hensel's lifting lemma, this also works in modular
                // arithmetic, doubling the correct bits in each step.
                inv := mul(inv, sub(2, mul(d, inv))) // inverse mod 2**8
                inv := mul(inv, sub(2, mul(d, inv))) // inverse mod 2**16
                inv := mul(inv, sub(2, mul(d, inv))) // inverse mod 2**32
                inv := mul(inv, sub(2, mul(d, inv))) // inverse mod 2**64
                inv := mul(inv, sub(2, mul(d, inv))) // inverse mod 2**128
                result :=
                    mul(
                        // Divide [p1 p0] by the factors of two.
                        // Shift in bits from `p1` into `p0`. For this we need
                        // to flip `t` such that it is `2**256 / t`.
                        or(
                            mul(sub(p1, gt(r, result)), add(div(sub(0, t), t), 1)),
                            div(sub(result, r), t)
                        ),
                        // inverse mod 2**256
                        mul(inv, sub(2, mul(d, inv)))
                    )
                break
            }
        }
    }

    /// @dev Calculates `floor(x * y / d)` with full precision, rounded up.
    /// Throws if result overflows a uint256 or when `d` is zero.
    /// Credit to Uniswap-v3-core under MIT license:
    /// https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/FullMath.sol
    function fullMulDivUp(uint256 x, uint256 y, uint256 d) internal pure returns (uint256 result) {
        result = fullMulDiv(x, y, d);
        /// @solidity memory-safe-assembly
        assembly {
            if mulmod(x, y, d) {
                result := add(result, 1)
                if iszero(result) {
                    mstore(0x00, 0xae47f702) // `FullMulDivFailed()`.
                    revert(0x1c, 0x04)
                }
            }
        }
    }

    /// @dev Returns `floor(x * y / d)`.
    /// Reverts if `x * y` overflows, or `d` is zero.
    function mulDiv(uint256 x, uint256 y, uint256 d) internal pure returns (uint256 z) {
        /// @solidity memory-safe-assembly
        assembly {
            // Equivalent to require(d != 0 && (y == 0 || x <= type(uint256).max / y))
            if iszero(mul(d, iszero(mul(y, gt(x, div(not(0), y)))))) {
                mstore(0x00, 0xad251c27) // `MulDivFailed()`.
                revert(0x1c, 0x04)
            }
            z := div(mul(x, y), d)
        }
    }

    /// @dev Returns `ceil(x * y / d)`.
    /// Reverts if `x * y` overflows, or `d` is zero.
    function mulDivUp(uint256 x, uint256 y, uint256 d) internal pure returns (uint256 z) {
        /// @solidity memory-safe-assembly
        assembly {
            // Equivalent to require(d != 0 && (y == 0 || x <= type(uint256).max / y))
            if iszero(mul(d, iszero(mul(y, gt(x, div(not(0), y)))))) {
                mstore(0x00, 0xad251c27) // `MulDivFailed()`.
                revert(0x1c, 0x04)
            }
            z := add(iszero(iszero(mod(mul(x, y), d))), div(mul(x, y), d))
        }
    }

    /// @dev Returns `ceil(x / d)`.
    /// Reverts if `d` is zero.
    function divUp(uint256 x, uint256 d) internal pure returns (uint256 z) {
        /// @solidity memory-safe-assembly
        assembly {
            if iszero(d) {
                mstore(0x00, 0x65244e4e) // `DivFailed()`.
                revert(0x1c, 0x04)
            }
            z := add(iszero(iszero(mod(x, d))), div(x, d))
        }
    }

    /// @dev Returns `max(0, x - y)`.
    function zeroFloorSub(uint256 x, uint256 y) internal pure returns (uint256 z) {
        /// @solidity memory-safe-assembly
        assembly {
            z := mul(gt(x, y), sub(x, y))
        }
    }

    /// @dev Exponentiate `x` to `y` by squaring, denominated in base `b`.
    /// Reverts if the computation overflows.
    function rpow(uint256 x, uint256 y, uint256 b) internal pure returns (uint256 z) {
        /// @solidity memory-safe-assembly
        assembly {
            z := mul(b, iszero(y)) // `0 ** 0 = 1`. Otherwise, `0 ** n = 0`.
            if x {
                z := xor(b, mul(xor(b, x), and(y, 1))) // `z = isEven(y) ? scale : x`
                let half := shr(1, b) // Divide `b` by 2.
                // Divide `y` by 2 every iteration.
                for { y := shr(1, y) } y { y := shr(1, y) } {
                    let xx := mul(x, x) // Store x squared.
                    let xxRound := add(xx, half) // Round to the nearest number.
                    // Revert if `xx + half` overflowed, or if `x ** 2` overflows.
                    if or(lt(xxRound, xx), shr(128, x)) {
                        mstore(0x00, 0x49f7642b) // `RPowOverflow()`.
                        revert(0x1c, 0x04)
                    }
                    x := div(xxRound, b) // Set `x` to scaled `xxRound`.
                    // If `y` is odd:
                    if and(y, 1) {
                        let zx := mul(z, x) // Compute `z * x`.
                        let zxRound := add(zx, half) // Round to the nearest number.
                        // If `z * x` overflowed or `zx + half` overflowed:
                        if or(xor(div(zx, x), z), lt(zxRound, zx)) {
                            // Revert if `x` is non-zero.
                            if iszero(iszero(x)) {
                                mstore(0x00, 0x49f7642b) // `RPowOverflow()`.
                                revert(0x1c, 0x04)
                            }
                        }
                        z := div(zxRound, b) // Return properly scaled `zxRound`.
                    }
                }
            }
        }
    }

    /// @dev Returns the square root of `x`.
    function sqrt(uint256 x) internal pure returns (uint256 z) {
        /// @solidity memory-safe-assembly
        assembly {
            // `floor(sqrt(2**15)) = 181`. `sqrt(2**15) - 181 = 2.84`.
            z := 181 // The "correct" value is 1, but this saves a multiplication later.

            // This segment is to get a reasonable initial estimate for the Babylonian method. With a bad
            // start, the correct # of bits increases ~linearly each iteration instead of ~quadratically.

            // Let `y = x / 2**r`. We check `y >= 2**(k + 8)`
            // but shift right by `k` bits to ensure that if `x >= 256`, then `y >= 256`.
            let r := shl(7, lt(0xffffffffffffffffffffffffffffffffff, x))
            r := or(r, shl(6, lt(0xffffffffffffffffff, shr(r, x))))
            r := or(r, shl(5, lt(0xffffffffff, shr(r, x))))
            r := or(r, shl(4, lt(0xffffff, shr(r, x))))
            z := shl(shr(1, r), z)

            // Goal was to get `z*z*y` within a small factor of `x`. More iterations could
            // get y in a tighter range. Currently, we will have y in `[256, 256*(2**16))`.
            // We ensured `y >= 256` so that the relative difference between `y` and `y+1` is small.
            // That's not possible if `x < 256` but we can just verify those cases exhaustively.

            // Now, `z*z*y <= x < z*z*(y+1)`, and `y <= 2**(16+8)`, and either `y >= 256`, or `x < 256`.
            // Correctness can be checked exhaustively for `x < 256`, so we assume `y >= 256`.
            // Then `z*sqrt(y)` is within `sqrt(257)/sqrt(256)` of `sqrt(x)`, or about 20bps.

            // For `s` in the range `[1/256, 256]`, the estimate `f(s) = (181/1024) * (s+1)`
            // is in the range `(1/2.84 * sqrt(s), 2.84 * sqrt(s))`,
            // with largest error when `s = 1` and when `s = 256` or `1/256`.

            // Since `y` is in `[256, 256*(2**16))`, let `a = y/65536`, so that `a` is in `[1/256, 256)`.
            // Then we can estimate `sqrt(y)` using
            // `sqrt(65536) * 181/1024 * (a + 1) = 181/4 * (y + 65536)/65536 = 181 * (y + 65536)/2**18`.

            // There is no overflow risk here since `y < 2**136` after the first branch above.
            z := shr(18, mul(z, add(shr(r, x), 65536))) // A `mul()` is saved from starting `z` at 181.

            // Given the worst case multiplicative error of 2.84 above, 7 iterations should be enough.
            z := shr(1, add(z, div(x, z)))
            z := shr(1, add(z, div(x, z)))
            z := shr(1, add(z, div(x, z)))
            z := shr(1, add(z, div(x, z)))
            z := shr(1, add(z, div(x, z)))
            z := shr(1, add(z, div(x, z)))
            z := shr(1, add(z, div(x, z)))

            // If `x+1` is a perfect square, the Babylonian method cycles between
            // `floor(sqrt(x))` and `ceil(sqrt(x))`. This statement ensures we return floor.
            // See: https://en.wikipedia.org/wiki/Integer_square_root#Using_only_integer_division
            z := sub(z, lt(div(x, z), z))
        }
    }

    /// @dev Returns the cube root of `x`.
    /// Credit to bout3fiddy and pcaversaccio under AGPLv3 license:
    /// https://github.com/pcaversaccio/snekmate/blob/main/src/utils/Math.vy
    function cbrt(uint256 x) internal pure returns (uint256 z) {
        /// @solidity memory-safe-assembly
        assembly {
            let r := shl(7, lt(0xffffffffffffffffffffffffffffffff, x))
            r := or(r, shl(6, lt(0xffffffffffffffff, shr(r, x))))
            r := or(r, shl(5, lt(0xffffffff, shr(r, x))))
            r := or(r, shl(4, lt(0xffff, shr(r, x))))
            r := or(r, shl(3, lt(0xff, shr(r, x))))

            z := div(shl(div(r, 3), shl(lt(0xf, shr(r, x)), 0xf)), xor(7, mod(r, 3)))

            z := div(add(add(div(x, mul(z, z)), z), z), 3)
            z := div(add(add(div(x, mul(z, z)), z), z), 3)
            z := div(add(add(div(x, mul(z, z)), z), z), 3)
            z := div(add(add(div(x, mul(z, z)), z), z), 3)
            z := div(add(add(div(x, mul(z, z)), z), z), 3)
            z := div(add(add(div(x, mul(z, z)), z), z), 3)
            z := div(add(add(div(x, mul(z, z)), z), z), 3)

            z := sub(z, lt(div(x, mul(z, z)), z))
        }
    }

    /// @dev Returns the square root of `x`, denominated in `WAD`.
    function sqrtWad(uint256 x) internal pure returns (uint256 z) {
        unchecked {
            z = 10 ** 9;
            if (x <= type(uint256).max / 10 ** 36 - 1) {
                x *= 10 ** 18;
                z = 1;
            }
            z *= sqrt(x);
        }
    }

    /// @dev Returns the cube root of `x`, denominated in `WAD`.
    function cbrtWad(uint256 x) internal pure returns (uint256 z) {
        unchecked {
            z = 10 ** 12;
            if (x <= (type(uint256).max / 10 ** 36) * 10 ** 18 - 1) {
                if (x >= type(uint256).max / 10 ** 36) {
                    x *= 10 ** 18;
                    z = 10 ** 6;
                } else {
                    x *= 10 ** 36;
                    z = 1;
                }
            }
            z *= cbrt(x);
        }
    }

    /// @dev Returns the factorial of `x`.
    function factorial(uint256 x) internal pure returns (uint256 result) {
        /// @solidity memory-safe-assembly
        assembly {
            if iszero(lt(x, 58)) {
                mstore(0x00, 0xaba0f2a2) // `FactorialOverflow()`.
                revert(0x1c, 0x04)
            }
            for { result := 1 } x { x := sub(x, 1) } { result := mul(result, x) }
        }
    }

    /// @dev Returns the log2 of `x`.
    /// Equivalent to computing the index of the most significant bit (MSB) of `x`.
    /// Returns 0 if `x` is zero.
    function log2(uint256 x) internal pure returns (uint256 r) {
        /// @solidity memory-safe-assembly
        assembly {
            r := shl(7, lt(0xffffffffffffffffffffffffffffffff, x))
            r := or(r, shl(6, lt(0xffffffffffffffff, shr(r, x))))
            r := or(r, shl(5, lt(0xffffffff, shr(r, x))))
            r := or(r, shl(4, lt(0xffff, shr(r, x))))
            r := or(r, shl(3, lt(0xff, shr(r, x))))
            // forgefmt: disable-next-item
            r := or(r, byte(and(0x1f, shr(shr(r, x), 0x8421084210842108cc6318c6db6d54be)),
                0x0706060506020504060203020504030106050205030304010505030400000000))
        }
    }

    /// @dev Returns the log2 of `x`, rounded up.
    /// Returns 0 if `x` is zero.
    function log2Up(uint256 x) internal pure returns (uint256 r) {
        r = log2(x);
        /// @solidity memory-safe-assembly
        assembly {
            r := add(r, lt(shl(r, 1), x))
        }
    }

    /// @dev Returns the log10 of `x`.
    /// Returns 0 if `x` is zero.
    function log10(uint256 x) internal pure returns (uint256 r) {
        /// @solidity memory-safe-assembly
        assembly {
            if iszero(lt(x, 100000000000000000000000000000000000000)) {
                x := div(x, 100000000000000000000000000000000000000)
                r := 38
            }
            if iszero(lt(x, 100000000000000000000)) {
                x := div(x, 100000000000000000000)
                r := add(r, 20)
            }
            if iszero(lt(x, 10000000000)) {
                x := div(x, 10000000000)
                r := add(r, 10)
            }
            if iszero(lt(x, 100000)) {
                x := div(x, 100000)
                r := add(r, 5)
            }
            r := add(r, add(gt(x, 9), add(gt(x, 99), add(gt(x, 999), gt(x, 9999)))))
        }
    }

    /// @dev Returns the log10 of `x`, rounded up.
    /// Returns 0 if `x` is zero.
    function log10Up(uint256 x) internal pure returns (uint256 r) {
        r = log10(x);
        /// @solidity memory-safe-assembly
        assembly {
            r := add(r, lt(exp(10, r), x))
        }
    }

    /// @dev Returns the log256 of `x`.
    /// Returns 0 if `x` is zero.
    function log256(uint256 x) internal pure returns (uint256 r) {
        /// @solidity memory-safe-assembly
        assembly {
            r := shl(7, lt(0xffffffffffffffffffffffffffffffff, x))
            r := or(r, shl(6, lt(0xffffffffffffffff, shr(r, x))))
            r := or(r, shl(5, lt(0xffffffff, shr(r, x))))
            r := or(r, shl(4, lt(0xffff, shr(r, x))))
            r := or(shr(3, r), lt(0xff, shr(r, x)))
        }
    }

    /// @dev Returns the log256 of `x`, rounded up.
    /// Returns 0 if `x` is zero.
    function log256Up(uint256 x) internal pure returns (uint256 r) {
        r = log256(x);
        /// @solidity memory-safe-assembly
        assembly {
            r := add(r, lt(shl(shl(3, r), 1), x))
        }
    }

    /// @dev Returns the scientific notation format `mantissa * 10 ** exponent` of `x`.
    /// Useful for compressing prices (e.g. using 25 bit mantissa and 7 bit exponent).
    function sci(uint256 x) internal pure returns (uint256 mantissa, uint256 exponent) {
        /// @solidity memory-safe-assembly
        assembly {
            mantissa := x
            if mantissa {
                if iszero(mod(mantissa, 1000000000000000000000000000000000)) {
                    mantissa := div(mantissa, 1000000000000000000000000000000000)
                    exponent := 33
                }
                if iszero(mod(mantissa, 10000000000000000000)) {
                    mantissa := div(mantissa, 10000000000000000000)
                    exponent := add(exponent, 19)
                }
                if iszero(mod(mantissa, 1000000000000)) {
                    mantissa := div(mantissa, 1000000000000)
                    exponent := add(exponent, 12)
                }
                if iszero(mod(mantissa, 1000000)) {
                    mantissa := div(mantissa, 1000000)
                    exponent := add(exponent, 6)
                }
                if iszero(mod(mantissa, 10000)) {
                    mantissa := div(mantissa, 10000)
                    exponent := add(exponent, 4)
                }
                if iszero(mod(mantissa, 100)) {
                    mantissa := div(mantissa, 100)
                    exponent := add(exponent, 2)
                }
                if iszero(mod(mantissa, 10)) {
                    mantissa := div(mantissa, 10)
                    exponent := add(exponent, 1)
                }
            }
        }
    }

    /// @dev Convenience function for packing `x` into a smaller number using `sci`.
    /// The `mantissa` will be in bits [7..255] (the upper 249 bits).
    /// The `exponent` will be in bits [0..6] (the lower 7 bits).
    /// Use `SafeCastLib` to safely ensure that the `packed` number is small
    /// enough to fit in the desired unsigned integer type:
    /// ```
    ///     uint32 packed = SafeCastLib.toUint32(FixedPointMathLib.packSci(777 ether));
    /// ```
    function packSci(uint256 x) internal pure returns (uint256 packed) {
        (x, packed) = sci(x); // Reuse for `mantissa` and `exponent`.
        /// @solidity memory-safe-assembly
        assembly {
            if shr(249, x) {
                mstore(0x00, 0xce30380c) // `MantissaOverflow()`.
                revert(0x1c, 0x04)
            }
            packed := or(shl(7, x), packed)
        }
    }

    /// @dev Convenience function for unpacking a packed number from `packSci`.
    function unpackSci(uint256 packed) internal pure returns (uint256 unpacked) {
        unchecked {
            unpacked = (packed >> 7) * 10 ** (packed & 0x7f);
        }
    }

    /// @dev Returns the average of `x` and `y`.
    function avg(uint256 x, uint256 y) internal pure returns (uint256 z) {
        unchecked {
            z = (x & y) + ((x ^ y) >> 1);
        }
    }

    /// @dev Returns the average of `x` and `y`.
    function avg(int256 x, int256 y) internal pure returns (int256 z) {
        unchecked {
            z = (x >> 1) + (y >> 1) + (x & y & 1);
        }
    }

    /// @dev Returns the absolute value of `x`.
    function abs(int256 x) internal pure returns (uint256 z) {
        /// @solidity memory-safe-assembly
        assembly {
            z := xor(sar(255, x), add(sar(255, x), x))
        }
    }

    /// @dev Returns the absolute distance between `x` and `y`.
    function dist(int256 x, int256 y) internal pure returns (uint256 z) {
        /// @solidity memory-safe-assembly
        assembly {
            z := xor(mul(xor(sub(y, x), sub(x, y)), sgt(x, y)), sub(y, x))
        }
    }

    /// @dev Returns the minimum of `x` and `y`.
    function min(uint256 x, uint256 y) internal pure returns (uint256 z) {
        /// @solidity memory-safe-assembly
        assembly {
            z := xor(x, mul(xor(x, y), lt(y, x)))
        }
    }

    /// @dev Returns the minimum of `x` and `y`.
    function min(int256 x, int256 y) internal pure returns (int256 z) {
        /// @solidity memory-safe-assembly
        assembly {
            z := xor(x, mul(xor(x, y), slt(y, x)))
        }
    }

    /// @dev Returns the maximum of `x` and `y`.
    function max(uint256 x, uint256 y) internal pure returns (uint256 z) {
        /// @solidity memory-safe-assembly
        assembly {
            z := xor(x, mul(xor(x, y), gt(y, x)))
        }
    }

    /// @dev Returns the maximum of `x` and `y`.
    function max(int256 x, int256 y) internal pure returns (int256 z) {
        /// @solidity memory-safe-assembly
        assembly {
            z := xor(x, mul(xor(x, y), sgt(y, x)))
        }
    }

    /// @dev Returns `x`, bounded to `minValue` and `maxValue`.
    function clamp(uint256 x, uint256 minValue, uint256 maxValue)
        internal
        pure
        returns (uint256 z)
    {
        /// @solidity memory-safe-assembly
        assembly {
            z := xor(x, mul(xor(x, minValue), gt(minValue, x)))
            z := xor(z, mul(xor(z, maxValue), lt(maxValue, z)))
        }
    }

    /// @dev Returns `x`, bounded to `minValue` and `maxValue`.
    function clamp(int256 x, int256 minValue, int256 maxValue) internal pure returns (int256 z) {
        /// @solidity memory-safe-assembly
        assembly {
            z := xor(x, mul(xor(x, minValue), sgt(minValue, x)))
            z := xor(z, mul(xor(z, maxValue), slt(maxValue, z)))
        }
    }

    /// @dev Returns greatest common divisor of `x` and `y`.
    function gcd(uint256 x, uint256 y) internal pure returns (uint256 z) {
        /// @solidity memory-safe-assembly
        assembly {
            for { z := x } y {} {
                let t := y
                y := mod(z, y)
                z := t
            }
        }
    }

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                   RAW NUMBER OPERATIONS                    */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev Returns `x + y`, without checking for overflow.
    function rawAdd(uint256 x, uint256 y) internal pure returns (uint256 z) {
        unchecked {
            z = x + y;
        }
    }

    /// @dev Returns `x + y`, without checking for overflow.
    function rawAdd(int256 x, int256 y) internal pure returns (int256 z) {
        unchecked {
            z = x + y;
        }
    }

    /// @dev Returns `x - y`, without checking for underflow.
    function rawSub(uint256 x, uint256 y) internal pure returns (uint256 z) {
        unchecked {
            z = x - y;
        }
    }

    /// @dev Returns `x - y`, without checking for underflow.
    function rawSub(int256 x, int256 y) internal pure returns (int256 z) {
        unchecked {
            z = x - y;
        }
    }

    /// @dev Returns `x * y`, without checking for overflow.
    function rawMul(uint256 x, uint256 y) internal pure returns (uint256 z) {
        unchecked {
            z = x * y;
        }
    }

    /// @dev Returns `x * y`, without checking for overflow.
    function rawMul(int256 x, int256 y) internal pure returns (int256 z) {
        unchecked {
            z = x * y;
        }
    }

    /// @dev Returns `x / y`, returning 0 if `y` is zero.
    function rawDiv(uint256 x, uint256 y) internal pure returns (uint256 z) {
        /// @solidity memory-safe-assembly
        assembly {
            z := div(x, y)
        }
    }

    /// @dev Returns `x / y`, returning 0 if `y` is zero.
    function rawSDiv(int256 x, int256 y) internal pure returns (int256 z) {
        /// @solidity memory-safe-assembly
        assembly {
            z := sdiv(x, y)
        }
    }

    /// @dev Returns `x % y`, returning 0 if `y` is zero.
    function rawMod(uint256 x, uint256 y) internal pure returns (uint256 z) {
        /// @solidity memory-safe-assembly
        assembly {
            z := mod(x, y)
        }
    }

    /// @dev Returns `x % y`, returning 0 if `y` is zero.
    function rawSMod(int256 x, int256 y) internal pure returns (int256 z) {
        /// @solidity memory-safe-assembly
        assembly {
            z := smod(x, y)
        }
    }

    /// @dev Returns `(x + y) % d`, return 0 if `d` if zero.
    function rawAddMod(uint256 x, uint256 y, uint256 d) internal pure returns (uint256 z) {
        /// @solidity memory-safe-assembly
        assembly {
            z := addmod(x, y, d)
        }
    }

    /// @dev Returns `(x * y) % d`, return 0 if `d` if zero.
    function rawMulMod(uint256 x, uint256 y, uint256 d) internal pure returns (uint256 z) {
        /// @solidity memory-safe-assembly
        assembly {
            z := mulmod(x, y, d)
        }
    }
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/extensions/IERC20Metadata.sol)

pragma solidity ^0.8.20;

import {IERC20} from "../IERC20.sol";

/**
 * @dev Interface for the optional metadata functions from the ERC20 standard.
 */
interface IERC20Metadata is IERC20 {
    /**
     * @dev Returns the name of the token.
     */
    function name() external view returns (string memory);

    /**
     * @dev Returns the symbol of the token.
     */
    function symbol() external view returns (string memory);

    /**
     * @dev Returns the decimals places of the token.
     */
    function decimals() external view returns (uint8);
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (utils/Strings.sol)

pragma solidity ^0.8.20;

import {Math} from "./math/Math.sol";
import {SignedMath} from "./math/SignedMath.sol";

/**
 * @dev String operations.
 */
library Strings {
    bytes16 private constant HEX_DIGITS = "0123456789abcdef";
    uint8 private constant ADDRESS_LENGTH = 20;

    /**
     * @dev The `value` string doesn't fit in the specified `length`.
     */
    error StringsInsufficientHexLength(uint256 value, uint256 length);

    /**
     * @dev Converts a `uint256` to its ASCII `string` decimal representation.
     */
    function toString(uint256 value) internal pure returns (string memory) {
        unchecked {
            uint256 length = Math.log10(value) + 1;
            string memory buffer = new string(length);
            uint256 ptr;
            /// @solidity memory-safe-assembly
            assembly {
                ptr := add(buffer, add(32, length))
            }
            while (true) {
                ptr--;
                /// @solidity memory-safe-assembly
                assembly {
                    mstore8(ptr, byte(mod(value, 10), HEX_DIGITS))
                }
                value /= 10;
                if (value == 0) break;
            }
            return buffer;
        }
    }

    /**
     * @dev Converts a `int256` to its ASCII `string` decimal representation.
     */
    function toStringSigned(int256 value) internal pure returns (string memory) {
        return string.concat(value < 0 ? "-" : "", toString(SignedMath.abs(value)));
    }

    /**
     * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation.
     */
    function toHexString(uint256 value) internal pure returns (string memory) {
        unchecked {
            return toHexString(value, Math.log256(value) + 1);
        }
    }

    /**
     * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length.
     */
    function toHexString(uint256 value, uint256 length) internal pure returns (string memory) {
        uint256 localValue = value;
        bytes memory buffer = new bytes(2 * length + 2);
        buffer[0] = "0";
        buffer[1] = "x";
        for (uint256 i = 2 * length + 1; i > 1; --i) {
            buffer[i] = HEX_DIGITS[localValue & 0xf];
            localValue >>= 4;
        }
        if (localValue != 0) {
            revert StringsInsufficientHexLength(value, length);
        }
        return string(buffer);
    }

    /**
     * @dev Converts an `address` with fixed length of 20 bytes to its not checksummed ASCII `string` hexadecimal
     * representation.
     */
    function toHexString(address addr) internal pure returns (string memory) {
        return toHexString(uint256(uint160(addr)), ADDRESS_LENGTH);
    }

    /**
     * @dev Returns true if the two strings are equal.
     */
    function equal(string memory a, string memory b) internal pure returns (bool) {
        return bytes(a).length == bytes(b).length && keccak256(bytes(a)) == keccak256(bytes(b));
    }
}

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {PoolId} from "../types/PoolId.sol";
import {IPoolManager} from "../interfaces/IPoolManager.sol";
import {Position} from "./Position.sol";

/// @notice A helper library to provide state getters that use extsload
library StateLibrary {
    /// @notice index of pools mapping in the PoolManager
    bytes32 public constant POOLS_SLOT = bytes32(uint256(6));

    /// @notice index of feeGrowthGlobal0X128 in Pool.State
    uint256 public constant FEE_GROWTH_GLOBAL0_OFFSET = 1;

    // feeGrowthGlobal1X128 offset in Pool.State = 2

    /// @notice index of liquidity in Pool.State
    uint256 public constant LIQUIDITY_OFFSET = 3;

    /// @notice index of TicksInfo mapping in Pool.State: mapping(int24 => TickInfo) ticks;
    uint256 public constant TICKS_OFFSET = 4;

    /// @notice index of tickBitmap mapping in Pool.State
    uint256 public constant TICK_BITMAP_OFFSET = 5;

    /// @notice index of Position.State mapping in Pool.State: mapping(bytes32 => Position.State) positions;
    uint256 public constant POSITIONS_OFFSET = 6;

    /**
     * @notice Get Slot0 of the pool: sqrtPriceX96, tick, protocolFee, lpFee
     * @dev Corresponds to pools[poolId].slot0
     * @param manager The pool manager contract.
     * @param poolId The ID of the pool.
     * @return sqrtPriceX96 The square root of the price of the pool, in Q96 precision.
     * @return tick The current tick of the pool.
     * @return protocolFee The protocol fee of the pool.
     * @return lpFee The swap fee of the pool.
     */
    function getSlot0(IPoolManager manager, PoolId poolId)
        internal
        view
        returns (uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 lpFee)
    {
        // slot key of Pool.State value: `pools[poolId]`
        bytes32 stateSlot = _getPoolStateSlot(poolId);

        bytes32 data = manager.extsload(stateSlot);

        //   24 bits  |24bits|24bits      |24 bits|160 bits
        // 0x000000   |000bb8|000000      |ffff75 |0000000000000000fe3aa841ba359daa0ea9eff7
        // ---------- | fee  |protocolfee | tick  | sqrtPriceX96
        assembly ("memory-safe") {
            // bottom 160 bits of data
            sqrtPriceX96 := and(data, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)
            // next 24 bits of data
            tick := signextend(2, shr(160, data))
            // next 24 bits of data
            protocolFee := and(shr(184, data), 0xFFFFFF)
            // last 24 bits of data
            lpFee := and(shr(208, data), 0xFFFFFF)
        }
    }

    /**
     * @notice Retrieves the tick information of a pool at a specific tick.
     * @dev Corresponds to pools[poolId].ticks[tick]
     * @param manager The pool manager contract.
     * @param poolId The ID of the pool.
     * @param tick The tick to retrieve information for.
     * @return liquidityGross The total position liquidity that references this tick
     * @return liquidityNet The amount of net liquidity added (subtracted) when tick is crossed from left to right (right to left)
     * @return feeGrowthOutside0X128 fee growth per unit of liquidity on the _other_ side of this tick (relative to the current tick)
     * @return feeGrowthOutside1X128 fee growth per unit of liquidity on the _other_ side of this tick (relative to the current tick)
     */
    function getTickInfo(IPoolManager manager, PoolId poolId, int24 tick)
        internal
        view
        returns (
            uint128 liquidityGross,
            int128 liquidityNet,
            uint256 feeGrowthOutside0X128,
            uint256 feeGrowthOutside1X128
        )
    {
        bytes32 slot = _getTickInfoSlot(poolId, tick);

        // read all 3 words of the TickInfo struct
        bytes32[] memory data = manager.extsload(slot, 3);
        assembly ("memory-safe") {
            let firstWord := mload(add(data, 32))
            liquidityNet := sar(128, firstWord)
            liquidityGross := and(firstWord, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)
            feeGrowthOutside0X128 := mload(add(data, 64))
            feeGrowthOutside1X128 := mload(add(data, 96))
        }
    }

    /**
     * @notice Retrieves the liquidity information of a pool at a specific tick.
     * @dev Corresponds to pools[poolId].ticks[tick].liquidityGross and pools[poolId].ticks[tick].liquidityNet. A more gas efficient version of getTickInfo
     * @param manager The pool manager contract.
     * @param poolId The ID of the pool.
     * @param tick The tick to retrieve liquidity for.
     * @return liquidityGross The total position liquidity that references this tick
     * @return liquidityNet The amount of net liquidity added (subtracted) when tick is crossed from left to right (right to left)
     */
    function getTickLiquidity(IPoolManager manager, PoolId poolId, int24 tick)
        internal
        view
        returns (uint128 liquidityGross, int128 liquidityNet)
    {
        bytes32 slot = _getTickInfoSlot(poolId, tick);

        bytes32 value = manager.extsload(slot);
        assembly ("memory-safe") {
            liquidityNet := sar(128, value)
            liquidityGross := and(value, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)
        }
    }

    /**
     * @notice Retrieves the fee growth outside a tick range of a pool
     * @dev Corresponds to pools[poolId].ticks[tick].feeGrowthOutside0X128 and pools[poolId].ticks[tick].feeGrowthOutside1X128. A more gas efficient version of getTickInfo
     * @param manager The pool manager contract.
     * @param poolId The ID of the pool.
     * @param tick The tick to retrieve fee growth for.
     * @return feeGrowthOutside0X128 fee growth per unit of liquidity on the _other_ side of this tick (relative to the current tick)
     * @return feeGrowthOutside1X128 fee growth per unit of liquidity on the _other_ side of this tick (relative to the current tick)
     */
    function getTickFeeGrowthOutside(IPoolManager manager, PoolId poolId, int24 tick)
        internal
        view
        returns (uint256 feeGrowthOutside0X128, uint256 feeGrowthOutside1X128)
    {
        bytes32 slot = _getTickInfoSlot(poolId, tick);

        // offset by 1 word, since the first word is liquidityGross + liquidityNet
        bytes32[] memory data = manager.extsload(bytes32(uint256(slot) + 1), 2);
        assembly ("memory-safe") {
            feeGrowthOutside0X128 := mload(add(data, 32))
            feeGrowthOutside1X128 := mload(add(data, 64))
        }
    }

    /**
     * @notice Retrieves the global fee growth of a pool.
     * @dev Corresponds to pools[poolId].feeGrowthGlobal0X128 and pools[poolId].feeGrowthGlobal1X128
     * @param manager The pool manager contract.
     * @param poolId The ID of the pool.
     * @return feeGrowthGlobal0 The global fee growth for token0.
     * @return feeGrowthGlobal1 The global fee growth for token1.
     */
    function getFeeGrowthGlobals(IPoolManager manager, PoolId poolId)
        internal
        view
        returns (uint256 feeGrowthGlobal0, uint256 feeGrowthGlobal1)
    {
        // slot key of Pool.State value: `pools[poolId]`
        bytes32 stateSlot = _getPoolStateSlot(poolId);

        // Pool.State, `uint256 feeGrowthGlobal0X128`
        bytes32 slot_feeGrowthGlobal0X128 = bytes32(uint256(stateSlot) + FEE_GROWTH_GLOBAL0_OFFSET);

        // read the 2 words of feeGrowthGlobal
        bytes32[] memory data = manager.extsload(slot_feeGrowthGlobal0X128, 2);
        assembly ("memory-safe") {
            feeGrowthGlobal0 := mload(add(data, 32))
            feeGrowthGlobal1 := mload(add(data, 64))
        }
    }

    /**
     * @notice Retrieves total the liquidity of a pool.
     * @dev Corresponds to pools[poolId].liquidity
     * @param manager The pool manager contract.
     * @param poolId The ID of the pool.
     * @return liquidity The liquidity of the pool.
     */
    function getLiquidity(IPoolManager manager, PoolId poolId) internal view returns (uint128 liquidity) {
        // slot key of Pool.State value: `pools[poolId]`
        bytes32 stateSlot = _getPoolStateSlot(poolId);

        // Pool.State: `uint128 liquidity`
        bytes32 slot = bytes32(uint256(stateSlot) + LIQUIDITY_OFFSET);

        liquidity = uint128(uint256(manager.extsload(slot)));
    }

    /**
     * @notice Retrieves the tick bitmap of a pool at a specific tick.
     * @dev Corresponds to pools[poolId].tickBitmap[tick]
     * @param manager The pool manager contract.
     * @param poolId The ID of the pool.
     * @param tick The tick to retrieve the bitmap for.
     * @return tickBitmap The bitmap of the tick.
     */
    function getTickBitmap(IPoolManager manager, PoolId poolId, int16 tick)
        internal
        view
        returns (uint256 tickBitmap)
    {
        // slot key of Pool.State value: `pools[poolId]`
        bytes32 stateSlot = _getPoolStateSlot(poolId);

        // Pool.State: `mapping(int16 => uint256) tickBitmap;`
        bytes32 tickBitmapMapping = bytes32(uint256(stateSlot) + TICK_BITMAP_OFFSET);

        // slot id of the mapping key: `pools[poolId].tickBitmap[tick]
        bytes32 slot = keccak256(abi.encodePacked(int256(tick), tickBitmapMapping));

        tickBitmap = uint256(manager.extsload(slot));
    }

    /**
     * @notice Retrieves the position information of a pool without needing to calculate the `positionId`.
     * @dev Corresponds to pools[poolId].positions[positionId]
     * @param poolId The ID of the pool.
     * @param owner The owner of the liquidity position.
     * @param tickLower The lower tick of the liquidity range.
     * @param tickUpper The upper tick of the liquidity range.
     * @param salt The bytes32 randomness to further distinguish position state.
     * @return liquidity The liquidity of the position.
     * @return feeGrowthInside0LastX128 The fee growth inside the position for token0.
     * @return feeGrowthInside1LastX128 The fee growth inside the position for token1.
     */
    function getPositionInfo(
        IPoolManager manager,
        PoolId poolId,
        address owner,
        int24 tickLower,
        int24 tickUpper,
        bytes32 salt
    ) internal view returns (uint128 liquidity, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128) {
        // positionKey = keccak256(abi.encodePacked(owner, tickLower, tickUpper, salt))
        bytes32 positionKey = Position.calculatePositionKey(owner, tickLower, tickUpper, salt);

        (liquidity, feeGrowthInside0LastX128, feeGrowthInside1LastX128) = getPositionInfo(manager, poolId, positionKey);
    }

    /**
     * @notice Retrieves the position information of a pool at a specific position ID.
     * @dev Corresponds to pools[poolId].positions[positionId]
     * @param manager The pool manager contract.
     * @param poolId The ID of the pool.
     * @param positionId The ID of the position.
     * @return liquidity The liquidity of the position.
     * @return feeGrowthInside0LastX128 The fee growth inside the position for token0.
     * @return feeGrowthInside1LastX128 The fee growth inside the position for token1.
     */
    function getPositionInfo(IPoolManager manager, PoolId poolId, bytes32 positionId)
        internal
        view
        returns (uint128 liquidity, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128)
    {
        bytes32 slot = _getPositionInfoSlot(poolId, positionId);

        // read all 3 words of the Position.State struct
        bytes32[] memory data = manager.extsload(slot, 3);

        assembly ("memory-safe") {
            liquidity := mload(add(data, 32))
            feeGrowthInside0LastX128 := mload(add(data, 64))
            feeGrowthInside1LastX128 := mload(add(data, 96))
        }
    }

    /**
     * @notice Retrieves the liquidity of a position.
     * @dev Corresponds to pools[poolId].positions[positionId].liquidity. More gas efficient for just retrieiving liquidity as compared to getPositionInfo
     * @param manager The pool manager contract.
     * @param poolId The ID of the pool.
     * @param positionId The ID of the position.
     * @return liquidity The liquidity of the position.
     */
    function getPositionLiquidity(IPoolManager manager, PoolId poolId, bytes32 positionId)
        internal
        view
        returns (uint128 liquidity)
    {
        bytes32 slot = _getPositionInfoSlot(poolId, positionId);
        liquidity = uint128(uint256(manager.extsload(slot)));
    }

    /**
     * @notice Calculate the fee growth inside a tick range of a pool
     * @dev pools[poolId].feeGrowthInside0LastX128 in Position.State is cached and can become stale. This function will calculate the up to date feeGrowthInside
     * @param manager The pool manager contract.
     * @param poolId The ID of the pool.
     * @param tickLower The lower tick of the range.
     * @param tickUpper The upper tick of the range.
     * @return feeGrowthInside0X128 The fee growth inside the tick range for token0.
     * @return feeGrowthInside1X128 The fee growth inside the tick range for token1.
     */
    function getFeeGrowthInside(IPoolManager manager, PoolId poolId, int24 tickLower, int24 tickUpper)
        internal
        view
        returns (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128)
    {
        (uint256 feeGrowthGlobal0X128, uint256 feeGrowthGlobal1X128) = getFeeGrowthGlobals(manager, poolId);

        (uint256 lowerFeeGrowthOutside0X128, uint256 lowerFeeGrowthOutside1X128) =
            getTickFeeGrowthOutside(manager, poolId, tickLower);
        (uint256 upperFeeGrowthOutside0X128, uint256 upperFeeGrowthOutside1X128) =
            getTickFeeGrowthOutside(manager, poolId, tickUpper);
        (, int24 tickCurrent,,) = getSlot0(manager, poolId);
        unchecked {
            if (tickCurrent < tickLower) {
                feeGrowthInside0X128 = lowerFeeGrowthOutside0X128 - upperFeeGrowthOutside0X128;
                feeGrowthInside1X128 = lowerFeeGrowthOutside1X128 - upperFeeGrowthOutside1X128;
            } else if (tickCurrent >= tickUpper) {
                feeGrowthInside0X128 = upperFeeGrowthOutside0X128 - lowerFeeGrowthOutside0X128;
                feeGrowthInside1X128 = upperFeeGrowthOutside1X128 - lowerFeeGrowthOutside1X128;
            } else {
                feeGrowthInside0X128 = feeGrowthGlobal0X128 - lowerFeeGrowthOutside0X128 - upperFeeGrowthOutside0X128;
                feeGrowthInside1X128 = feeGrowthGlobal1X128 - lowerFeeGrowthOutside1X128 - upperFeeGrowthOutside1X128;
            }
        }
    }

    function _getPoolStateSlot(PoolId poolId) internal pure returns (bytes32) {
        return keccak256(abi.encodePacked(PoolId.unwrap(poolId), POOLS_SLOT));
    }

    function _getTickInfoSlot(PoolId poolId, int24 tick) internal pure returns (bytes32) {
        // slot key of Pool.State value: `pools[poolId]`
        bytes32 stateSlot = _getPoolStateSlot(poolId);

        // Pool.State: `mapping(int24 => TickInfo) ticks`
        bytes32 ticksMappingSlot = bytes32(uint256(stateSlot) + TICKS_OFFSET);

        // slot key of the tick key: `pools[poolId].ticks[tick]
        return keccak256(abi.encodePacked(int256(tick), ticksMappingSlot));
    }

    function _getPositionInfoSlot(PoolId poolId, bytes32 positionId) internal pure returns (bytes32) {
        // slot key of Pool.State value: `pools[poolId]`
        bytes32 stateSlot = _getPoolStateSlot(poolId);

        // Pool.State: `mapping(bytes32 => Position.State) positions;`
        bytes32 positionMapping = bytes32(uint256(stateSlot) + POSITIONS_OFFSET);

        // slot of the mapping key: `pools[poolId].positions[positionId]
        return keccak256(abi.encodePacked(positionId, positionMapping));
    }
}

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/// @title Minimal ERC20 interface for Uniswap
/// @notice Contains a subset of the full ERC20 interface that is used in Uniswap V3
interface IERC20Minimal {
    /// @notice Returns an account's balance in the token
    /// @param account The account for which to look up the number of tokens it has, i.e. its balance
    /// @return The number of tokens held by the account
    function balanceOf(address account) external view returns (uint256);

    /// @notice Transfers the amount of token from the `msg.sender` to the recipient
    /// @param recipient The account that will receive the amount transferred
    /// @param amount The number of tokens to send from the sender to the recipient
    /// @return Returns true for a successful transfer, false for an unsuccessful transfer
    function transfer(address recipient, uint256 amount) external returns (bool);

    /// @notice Returns the current allowance given to a spender by an owner
    /// @param owner The account of the token owner
    /// @param spender The account of the token spender
    /// @return The current allowance granted by `owner` to `spender`
    function allowance(address owner, address spender) external view returns (uint256);

    /// @notice Sets the allowance of a spender from the `msg.sender` to the value `amount`
    /// @param spender The account which will be allowed to spend a given amount of the owners tokens
    /// @param amount The amount of tokens allowed to be used by `spender`
    /// @return Returns true for a successful approval, false for unsuccessful
    function approve(address spender, uint256 amount) external returns (bool);

    /// @notice Transfers `amount` tokens from `sender` to `recipient` up to the allowance given to the `msg.sender`
    /// @param sender The account from which the transfer will be initiated
    /// @param recipient The recipient of the transfer
    /// @param amount The amount of the transfer
    /// @return Returns true for a successful transfer, false for unsuccessful
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);

    /// @notice Event emitted when tokens are transferred from one address to another, either via `#transfer` or `#transferFrom`.
    /// @param from The account from which the tokens were sent, i.e. the balance decreased
    /// @param to The account to which the tokens were sent, i.e. the balance increased
    /// @param value The amount of tokens that were transferred
    event Transfer(address indexed from, address indexed to, uint256 value);

    /// @notice Event emitted when the approval amount for the spender of a given owner's tokens changes.
    /// @param owner The account that approved spending of its tokens
    /// @param spender The account for which the spending allowance was modified
    /// @param value The new allowance from the owner to the spender
    event Approval(address indexed owner, address indexed spender, uint256 value);
}

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/// @title Library for reverting with custom errors efficiently
/// @notice Contains functions for reverting with custom errors with different argument types efficiently
/// @dev To use this library, declare `using CustomRevert for bytes4;` and replace `revert CustomError()` with
/// `CustomError.selector.revertWith()`
/// @dev The functions may tamper with the free memory pointer but it is fine since the call context is exited immediately
library CustomRevert {
    /// @dev ERC-7751 error for wrapping bubbled up reverts
    error WrappedError(address target, bytes4 selector, bytes reason, bytes details);

    /// @dev Reverts with the selector of a custom error in the scratch space
    function revertWith(bytes4 selector) internal pure {
        assembly ("memory-safe") {
            mstore(0, selector)
            revert(0, 0x04)
        }
    }

    /// @dev Reverts with a custom error with an address argument in the scratch space
    function revertWith(bytes4 selector, address addr) internal pure {
        assembly ("memory-safe") {
            mstore(0, selector)
            mstore(0x04, and(addr, 0xffffffffffffffffffffffffffffffffffffffff))
            revert(0, 0x24)
        }
    }

    /// @dev Reverts with a custom error with an int24 argument in the scratch space
    function revertWith(bytes4 selector, int24 value) internal pure {
        assembly ("memory-safe") {
            mstore(0, selector)
            mstore(0x04, signextend(2, value))
            revert(0, 0x24)
        }
    }

    /// @dev Reverts with a custom error with a uint160 argument in the scratch space
    function revertWith(bytes4 selector, uint160 value) internal pure {
        assembly ("memory-safe") {
            mstore(0, selector)
            mstore(0x04, and(value, 0xffffffffffffffffffffffffffffffffffffffff))
            revert(0, 0x24)
        }
    }

    /// @dev Reverts with a custom error with two int24 arguments
    function revertWith(bytes4 selector, int24 value1, int24 value2) internal pure {
        assembly ("memory-safe") {
            let fmp := mload(0x40)
            mstore(fmp, selector)
            mstore(add(fmp, 0x04), signextend(2, value1))
            mstore(add(fmp, 0x24), signextend(2, value2))
            revert(fmp, 0x44)
        }
    }

    /// @dev Reverts with a custom error with two uint160 arguments
    function revertWith(bytes4 selector, uint160 value1, uint160 value2) internal pure {
        assembly ("memory-safe") {
            let fmp := mload(0x40)
            mstore(fmp, selector)
            mstore(add(fmp, 0x04), and(value1, 0xffffffffffffffffffffffffffffffffffffffff))
            mstore(add(fmp, 0x24), and(value2, 0xffffffffffffffffffffffffffffffffffffffff))
            revert(fmp, 0x44)
        }
    }

    /// @dev Reverts with a custom error with two address arguments
    function revertWith(bytes4 selector, address value1, address value2) internal pure {
        assembly ("memory-safe") {
            let fmp := mload(0x40)
            mstore(fmp, selector)
            mstore(add(fmp, 0x04), and(value1, 0xffffffffffffffffffffffffffffffffffffffff))
            mstore(add(fmp, 0x24), and(value2, 0xffffffffffffffffffffffffffffffffffffffff))
            revert(fmp, 0x44)
        }
    }

    /// @notice bubble up the revert message returned by a call and revert with a wrapped ERC-7751 error
    /// @dev this method can be vulnerable to revert data bombs
    function bubbleUpAndRevertWith(
        address revertingContract,
        bytes4 revertingFunctionSelector,
        bytes4 additionalContext
    ) internal pure {
        bytes4 wrappedErrorSelector = WrappedError.selector;
        assembly ("memory-safe") {
            // Ensure the size of the revert data is a multiple of 32 bytes
            let encodedDataSize := mul(div(add(returndatasize(), 31), 32), 32)

            let fmp := mload(0x40)

            // Encode wrapped error selector, address, function selector, offset, additional context, size, revert reason
            mstore(fmp, wrappedErrorSelector)
            mstore(add(fmp, 0x04), and(revertingContract, 0xffffffffffffffffffffffffffffffffffffffff))
            mstore(
                add(fmp, 0x24),
                and(revertingFunctionSelector, 0xffffffff00000000000000000000000000000000000000000000000000000000)
            )
            // offset revert reason
            mstore(add(fmp, 0x44), 0x80)
            // offset additional context
            mstore(add(fmp, 0x64), add(0xa0, encodedDataSize))
            // size revert reason
            mstore(add(fmp, 0x84), returndatasize())
            // revert reason
            returndatacopy(add(fmp, 0xa4), 0, returndatasize())
            // size additional context
            mstore(add(fmp, add(0xa4, encodedDataSize)), 0x04)
            // additional context
            mstore(
                add(fmp, add(0xc4, encodedDataSize)),
                and(additionalContext, 0xffffffff00000000000000000000000000000000000000000000000000000000)
            )
            revert(fmp, add(0xe4, encodedDataSize))
        }
    }
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC1155/utils/ERC1155Holder.sol)

pragma solidity ^0.8.20;

import {IERC165, ERC165} from "../../../utils/introspection/ERC165.sol";
import {IERC1155Receiver} from "../IERC1155Receiver.sol";

/**
 * @dev Simple implementation of `IERC1155Receiver` that will allow a contract to hold ERC1155 tokens.
 *
 * IMPORTANT: When inheriting this contract, you must include a way to use the received tokens, otherwise they will be
 * stuck.
 */
abstract contract ERC1155Holder is ERC165, IERC1155Receiver {
    /**
     * @dev See {IERC165-supportsInterface}.
     */
    function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) {
        return interfaceId == type(IERC1155Receiver).interfaceId || super.supportsInterface(interfaceId);
    }

    function onERC1155Received(
        address,
        address,
        uint256,
        uint256,
        bytes memory
    ) public virtual override returns (bytes4) {
        return this.onERC1155Received.selector;
    }

    function onERC1155BatchReceived(
        address,
        address,
        uint256[] memory,
        uint256[] memory,
        bytes memory
    ) public virtual override returns (bytes4) {
        return this.onERC1155BatchReceived.selector;
    }
}

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {CustomRevert} from "./CustomRevert.sol";

/// @title Safe casting methods
/// @notice Contains methods for safely casting between types
library SafeCast {
    using CustomRevert for bytes4;

    error SafeCastOverflow();

    /// @notice Cast a uint256 to a uint160, revert on overflow
    /// @param x The uint256 to be downcasted
    /// @return y The downcasted integer, now type uint160
    function toUint160(uint256 x) internal pure returns (uint160 y) {
        y = uint160(x);
        if (y != x) SafeCastOverflow.selector.revertWith();
    }

    /// @notice Cast a uint256 to a uint128, revert on overflow
    /// @param x The uint256 to be downcasted
    /// @return y The downcasted integer, now type uint128
    function toUint128(uint256 x) internal pure returns (uint128 y) {
        y = uint128(x);
        if (x != y) SafeCastOverflow.selector.revertWith();
    }

    /// @notice Cast a int128 to a uint128, revert on overflow or underflow
    /// @param x The int128 to be casted
    /// @return y The casted integer, now type uint128
    function toUint128(int128 x) internal pure returns (uint128 y) {
        if (x < 0) SafeCastOverflow.selector.revertWith();
        y = uint128(x);
    }

    /// @notice Cast a int256 to a int128, revert on overflow or underflow
    /// @param x The int256 to be downcasted
    /// @return y The downcasted integer, now type int128
    function toInt128(int256 x) internal pure returns (int128 y) {
        y = int128(x);
        if (y != x) SafeCastOverflow.selector.revertWith();
    }

    /// @notice Cast a uint256 to a int256, revert on overflow
    /// @param x The uint256 to be casted
    /// @return y The casted integer, now type int256
    function toInt256(uint256 x) internal pure returns (int256 y) {
        y = int256(x);
        if (y < 0) SafeCastOverflow.selector.revertWith();
    }

    /// @notice Cast a uint256 to a int128, revert on overflow
    /// @param x The uint256 to be downcasted
    /// @return The downcasted integer, now type int128
    function toInt128(uint256 x) internal pure returns (int128) {
        if (x >= 1 << 127) SafeCastOverflow.selector.revertWith();
        return int128(int256(x));
    }
}

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// Return type of the beforeSwap hook.
// Upper 128 bits is the delta in specified tokens. Lower 128 bits is delta in unspecified tokens (to match the afterSwap hook)
type BeforeSwapDelta is int256;

// Creates a BeforeSwapDelta from specified and unspecified
function toBeforeSwapDelta(int128 deltaSpecified, int128 deltaUnspecified)
    pure
    returns (BeforeSwapDelta beforeSwapDelta)
{
    assembly ("memory-safe") {
        beforeSwapDelta := or(shl(128, deltaSpecified), and(sub(shl(128, 1), 1), deltaUnspecified))
    }
}

/// @notice Library for getting the specified and unspecified deltas from the BeforeSwapDelta type
library BeforeSwapDeltaLibrary {
    /// @notice A BeforeSwapDelta of 0
    BeforeSwapDelta public constant ZERO_DELTA = BeforeSwapDelta.wrap(0);

    /// extracts int128 from the upper 128 bits of the BeforeSwapDelta
    /// returned by beforeSwap
    function getSpecifiedDelta(BeforeSwapDelta delta) internal pure returns (int128 deltaSpecified) {
        assembly ("memory-safe") {
            deltaSpecified := sar(128, delta)
        }
    }

    /// extracts int128 from the lower 128 bits of the BeforeSwapDelta
    /// returned by beforeSwap and afterSwap
    function getUnspecifiedDelta(BeforeSwapDelta delta) internal pure returns (int128 deltaUnspecified) {
        assembly ("memory-safe") {
            deltaUnspecified := signextend(15, delta)
        }
    }
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/IERC20.sol)

pragma solidity ^0.8.20;

/**
 * @dev Interface of the ERC20 standard as defined in the EIP.
 */
interface IERC20 {
    /**
     * @dev Emitted when `value` tokens are moved from one account (`from`) to
     * another (`to`).
     *
     * Note that `value` may be zero.
     */
    event Transfer(address indexed from, address indexed to, uint256 value);

    /**
     * @dev Emitted when the allowance of a `spender` for an `owner` is set by
     * a call to {approve}. `value` is the new allowance.
     */
    event Approval(address indexed owner, address indexed spender, uint256 value);

    /**
     * @dev Returns the value of tokens in existence.
     */
    function totalSupply() external view returns (uint256);

    /**
     * @dev Returns the value of tokens owned by `account`.
     */
    function balanceOf(address account) external view returns (uint256);

    /**
     * @dev Moves a `value` amount of tokens from the caller's account to `to`.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transfer(address to, uint256 value) external returns (bool);

    /**
     * @dev Returns the remaining number of tokens that `spender` will be
     * allowed to spend on behalf of `owner` through {transferFrom}. This is
     * zero by default.
     *
     * This value changes when {approve} or {transferFrom} are called.
     */
    function allowance(address owner, address spender) external view returns (uint256);

    /**
     * @dev Sets a `value` amount of tokens as the allowance of `spender` over the
     * caller's tokens.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * IMPORTANT: Beware that changing an allowance with this method brings the risk
     * that someone may use both the old and the new allowance by unfortunate
     * transaction ordering. One possible solution to mitigate this race
     * condition is to first reduce the spender's allowance to 0 and set the
     * desired value afterwards:
     * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
     *
     * Emits an {Approval} event.
     */
    function approve(address spender, uint256 value) external returns (bool);

    /**
     * @dev Moves a `value` amount of tokens from `from` to `to` using the
     * allowance mechanism. `value` is then deducted from the caller's
     * allowance.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(address from, address to, uint256 value) external returns (bool);
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (utils/math/Math.sol)

pragma solidity ^0.8.20;

/**
 * @dev Standard math utilities missing in the Solidity language.
 */
library Math {
    /**
     * @dev Muldiv operation overflow.
     */
    error MathOverflowedMulDiv();

    enum Rounding {
        Floor, // Toward negative infinity
        Ceil, // Toward positive infinity
        Trunc, // Toward zero
        Expand // Away from zero
    }

    /**
     * @dev Returns the addition of two unsigned integers, with an overflow flag.
     */
    function tryAdd(uint256 a, uint256 b) internal pure returns (bool, uint256) {
        unchecked {
            uint256 c = a + b;
            if (c < a) return (false, 0);
            return (true, c);
        }
    }

    /**
     * @dev Returns the subtraction of two unsigned integers, with an overflow flag.
     */
    function trySub(uint256 a, uint256 b) internal pure returns (bool, uint256) {
        unchecked {
            if (b > a) return (false, 0);
            return (true, a - b);
        }
    }

    /**
     * @dev Returns the multiplication of two unsigned integers, with an overflow flag.
     */
    function tryMul(uint256 a, uint256 b) internal pure returns (bool, uint256) {
        unchecked {
            // Gas optimization: this is cheaper than requiring 'a' not being zero, but the
            // benefit is lost if 'b' is also tested.
            // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522
            if (a == 0) return (true, 0);
            uint256 c = a * b;
            if (c / a != b) return (false, 0);
            return (true, c);
        }
    }

    /**
     * @dev Returns the division of two unsigned integers, with a division by zero flag.
     */
    function tryDiv(uint256 a, uint256 b) internal pure returns (bool, uint256) {
        unchecked {
            if (b == 0) return (false, 0);
            return (true, a / b);
        }
    }

    /**
     * @dev Returns the remainder of dividing two unsigned integers, with a division by zero flag.
     */
    function tryMod(uint256 a, uint256 b) internal pure returns (bool, uint256) {
        unchecked {
            if (b == 0) return (false, 0);
            return (true, a % b);
        }
    }

    /**
     * @dev Returns the largest of two numbers.
     */
    function max(uint256 a, uint256 b) internal pure returns (uint256) {
        return a > b ? a : b;
    }

    /**
     * @dev Returns the smallest of two numbers.
     */
    function min(uint256 a, uint256 b) internal pure returns (uint256) {
        return a < b ? a : b;
    }

    /**
     * @dev Returns the average of two numbers. The result is rounded towards
     * zero.
     */
    function average(uint256 a, uint256 b) internal pure returns (uint256) {
        // (a + b) / 2 can overflow.
        return (a & b) + (a ^ b) / 2;
    }

    /**
     * @dev Returns the ceiling of the division of two numbers.
     *
     * This differs from standard division with `/` in that it rounds towards infinity instead
     * of rounding towards zero.
     */
    function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) {
        if (b == 0) {
            // Guarantee the same behavior as in a regular Solidity division.
            return a / b;
        }

        // (a + b - 1) / b can overflow on addition, so we distribute.
        return a == 0 ? 0 : (a - 1) / b + 1;
    }

    /**
     * @notice Calculates floor(x * y / denominator) with full precision. Throws if result overflows a uint256 or
     * denominator == 0.
     * @dev Original credit to Remco Bloemen under MIT license (https://xn--2-umb.com/21/muldiv) with further edits by
     * Uniswap Labs also under MIT license.
     */
    function mulDiv(uint256 x, uint256 y, uint256 denominator) internal pure returns (uint256 result) {
        unchecked {
            // 512-bit multiply [prod1 prod0] = x * y. Compute the product mod 2^256 and mod 2^256 - 1, then use
            // use the Chinese Remainder Theorem to reconstruct the 512 bit result. The result is stored in two 256
            // variables such that product = prod1 * 2^256 + prod0.
            uint256 prod0 = x * y; // Least significant 256 bits of the product
            uint256 prod1; // Most significant 256 bits of the product
            assembly {
                let mm := mulmod(x, y, not(0))
                prod1 := sub(sub(mm, prod0), lt(mm, prod0))
            }

            // Handle non-overflow cases, 256 by 256 division.
            if (prod1 == 0) {
                // Solidity will revert if denominator == 0, unlike the div opcode on its own.
                // The surrounding unchecked block does not change this fact.
                // See https://docs.soliditylang.org/en/latest/control-structures.html#checked-or-unchecked-arithmetic.
                return prod0 / denominator;
            }

            // Make sure the result is less than 2^256. Also prevents denominator == 0.
            if (denominator <= prod1) {
                revert MathOverflowedMulDiv();
            }

            ///////////////////////////////////////////////
            // 512 by 256 division.
            ///////////////////////////////////////////////

            // Make division exact by subtracting the remainder from [prod1 prod0].
            uint256 remainder;
            assembly {
                // Compute remainder using mulmod.
                remainder := mulmod(x, y, denominator)

                // Subtract 256 bit number from 512 bit number.
                prod1 := sub(prod1, gt(remainder, prod0))
                prod0 := sub(prod0, remainder)
            }

            // Factor powers of two out of denominator and compute largest power of two divisor of denominator.
            // Always >= 1. See https://cs.stackexchange.com/q/138556/92363.

            uint256 twos = denominator & (0 - denominator);
            assembly {
                // Divide denominator by twos.
                denominator := div(denominator, twos)

                // Divide [prod1 prod0] by twos.
                prod0 := div(prod0, twos)

                // Flip twos such that it is 2^256 / twos. If twos is zero, then it becomes one.
                twos := add(div(sub(0, twos), twos), 1)
            }

            // Shift in bits from prod1 into prod0.
            prod0 |= prod1 * twos;

            // Invert denominator mod 2^256. Now that denominator is an odd number, it has an inverse modulo 2^256 such
            // that denominator * inv = 1 mod 2^256. Compute the inverse by starting with a seed that is correct for
            // four bits. That is, denominator * inv = 1 mod 2^4.
            uint256 inverse = (3 * denominator) ^ 2;

            // Use the Newton-Raphson iteration to improve the precision. Thanks to Hensel's lifting lemma, this also
            // works in modular arithmetic, doubling the correct bits in each step.
            inverse *= 2 - denominator * inverse; // inverse mod 2^8
            inverse *= 2 - denominator * inverse; // inverse mod 2^16
            inverse *= 2 - denominator * inverse; // inverse mod 2^32
            inverse *= 2 - denominator * inverse; // inverse mod 2^64
            inverse *= 2 - denominator * inverse; // inverse mod 2^128
            inverse *= 2 - denominator * inverse; // inverse mod 2^256

            // Because the division is now exact we can divide by multiplying with the modular inverse of denominator.
            // This will give us the correct result modulo 2^256. Since the preconditions guarantee that the outcome is
            // less than 2^256, this is the final result. We don't need to compute the high bits of the result and prod1
            // is no longer required.
            result = prod0 * inverse;
            return result;
        }
    }

    /**
     * @notice Calculates x * y / denominator with full precision, following the selected rounding direction.
     */
    function mulDiv(uint256 x, uint256 y, uint256 denominator, Rounding rounding) internal pure returns (uint256) {
        uint256 result = mulDiv(x, y, denominator);
        if (unsignedRoundsUp(rounding) && mulmod(x, y, denominator) > 0) {
            result += 1;
        }
        return result;
    }

    /**
     * @dev Returns the square root of a number. If the number is not a perfect square, the value is rounded
     * towards zero.
     *
     * Inspired by Henry S. Warren, Jr.'s "Hacker's Delight" (Chapter 11).
     */
    function sqrt(uint256 a) internal pure returns (uint256) {
        if (a == 0) {
            return 0;
        }

        // For our first guess, we get the biggest power of 2 which is smaller than the square root of the target.
        //
        // We know that the "msb" (most significant bit) of our target number `a` is a power of 2 such that we have
        // `msb(a) <= a < 2*msb(a)`. This value can be written `msb(a)=2**k` with `k=log2(a)`.
        //
        // This can be rewritten `2**log2(a) <= a < 2**(log2(a) + 1)`
        // → `sqrt(2**k) <= sqrt(a) < sqrt(2**(k+1))`
        // → `2**(k/2) <= sqrt(a) < 2**((k+1)/2) <= 2**(k/2 + 1)`
        //
        // Consequently, `2**(log2(a) / 2)` is a good first approximation of `sqrt(a)` with at least 1 correct bit.
        uint256 result = 1 << (log2(a) >> 1);

        // At this point `result` is an estimation with one bit of precision. We know the true value is a uint128,
        // since it is the square root of a uint256. Newton's method converges quadratically (precision doubles at
        // every iteration). We thus need at most 7 iteration to turn our partial result with one bit of precision
        // into the expected uint128 result.
        unchecked {
            result = (result + a / result) >> 1;
            result = (result + a / result) >> 1;
            result = (result + a / result) >> 1;
            result = (result + a / result) >> 1;
            result = (result + a / result) >> 1;
            result = (result + a / result) >> 1;
            result = (result + a / result) >> 1;
            return min(result, a / result);
        }
    }

    /**
     * @notice Calculates sqrt(a), following the selected rounding direction.
     */
    function sqrt(uint256 a, Rounding rounding) internal pure returns (uint256) {
        unchecked {
            uint256 result = sqrt(a);
            return result + (unsignedRoundsUp(rounding) && result * result < a ? 1 : 0);
        }
    }

    /**
     * @dev Return the log in base 2 of a positive value rounded towards zero.
     * Returns 0 if given 0.
     */
    function log2(uint256 value) internal pure returns (uint256) {
        uint256 result = 0;
        unchecked {
            if (value >> 128 > 0) {
                value >>= 128;
                result += 128;
            }
            if (value >> 64 > 0) {
                value >>= 64;
                result += 64;
            }
            if (value >> 32 > 0) {
                value >>= 32;
                result += 32;
            }
            if (value >> 16 > 0) {
                value >>= 16;
                result += 16;
            }
            if (value >> 8 > 0) {
                value >>= 8;
                result += 8;
            }
            if (value >> 4 > 0) {
                value >>= 4;
                result += 4;
            }
            if (value >> 2 > 0) {
                value >>= 2;
                result += 2;
            }
            if (value >> 1 > 0) {
                result += 1;
            }
        }
        return result;
    }

    /**
     * @dev Return the log in base 2, following the selected rounding direction, of a positive value.
     * Returns 0 if given 0.
     */
    function log2(uint256 value, Rounding rounding) internal pure returns (uint256) {
        unchecked {
            uint256 result = log2(value);
            return result + (unsignedRoundsUp(rounding) && 1 << result < value ? 1 : 0);
        }
    }

    /**
     * @dev Return the log in base 10 of a positive value rounded towards zero.
     * Returns 0 if given 0.
     */
    function log10(uint256 value) internal pure returns (uint256) {
        uint256 result = 0;
        unchecked {
            if (value >= 10 ** 64) {
                value /= 10 ** 64;
                result += 64;
            }
            if (value >= 10 ** 32) {
                value /= 10 ** 32;
                result += 32;
            }
            if (value >= 10 ** 16) {
                value /= 10 ** 16;
                result += 16;
            }
            if (value >= 10 ** 8) {
                value /= 10 ** 8;
                result += 8;
            }
            if (value >= 10 ** 4) {
                value /= 10 ** 4;
                result += 4;
            }
            if (value >= 10 ** 2) {
                value /= 10 ** 2;
                result += 2;
            }
            if (value >= 10 ** 1) {
                result += 1;
            }
        }
        return result;
    }

    /**
     * @dev Return the log in base 10, following the selected rounding direction, of a positive value.
     * Returns 0 if given 0.
     */
    function log10(uint256 value, Rounding rounding) internal pure returns (uint256) {
        unchecked {
            uint256 result = log10(value);
            return result + (unsignedRoundsUp(rounding) && 10 ** result < value ? 1 : 0);
        }
    }

    /**
     * @dev Return the log in base 256 of a positive value rounded towards zero.
     * Returns 0 if given 0.
     *
     * Adding one to the result gives the number of pairs of hex symbols needed to represent `value` as a hex string.
     */
    function log256(uint256 value) internal pure returns (uint256) {
        uint256 result = 0;
        unchecked {
            if (value >> 128 > 0) {
                value >>= 128;
                result += 16;
            }
            if (value >> 64 > 0) {
                value >>= 64;
                result += 8;
            }
            if (value >> 32 > 0) {
                value >>= 32;
                result += 4;
            }
            if (value >> 16 > 0) {
                value >>= 16;
                result += 2;
            }
            if (value >> 8 > 0) {
                result += 1;
            }
        }
        return result;
    }

    /**
     * @dev Return the log in base 256, following the selected rounding direction, of a positive value.
     * Returns 0 if given 0.
     */
    function log256(uint256 value, Rounding rounding) internal pure returns (uint256) {
        unchecked {
            uint256 result = log256(value);
            return result + (unsignedRoundsUp(rounding) && 1 << (result << 3) < value ? 1 : 0);
        }
    }

    /**
     * @dev Returns whether a provided rounding mode is considered rounding up for unsigned integers.
     */
    function unsignedRoundsUp(Rounding rounding) internal pure returns (bool) {
        return uint8(rounding) % 2 == 1;
    }
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (utils/math/SignedMath.sol)

pragma solidity ^0.8.20;

/**
 * @dev Standard signed math utilities missing in the Solidity language.
 */
library SignedMath {
    /**
     * @dev Returns the largest of two signed numbers.
     */
    function max(int256 a, int256 b) internal pure returns (int256) {
        return a > b ? a : b;
    }

    /**
     * @dev Returns the smallest of two signed numbers.
     */
    function min(int256 a, int256 b) internal pure returns (int256) {
        return a < b ? a : b;
    }

    /**
     * @dev Returns the average of two signed numbers without overflow.
     * The result is rounded towards zero.
     */
    function average(int256 a, int256 b) internal pure returns (int256) {
        // Formula from the book "Hacker's Delight"
        int256 x = (a & b) + ((a ^ b) >> 1);
        return x + (int256(uint256(x) >> 255) & (a ^ b));
    }

    /**
     * @dev Returns the absolute unsigned value of a signed value.
     */
    function abs(int256 n) internal pure returns (uint256) {
        unchecked {
            // must be unchecked in order to support `n = type(int256).min`
            return uint256(n >= 0 ? n : -n);
        }
    }
}

// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

import {FullMath} from "./FullMath.sol";
import {FixedPoint128} from "./FixedPoint128.sol";
import {LiquidityMath} from "./LiquidityMath.sol";
import {CustomRevert} from "./CustomRevert.sol";

/// @title Position
/// @notice Positions represent an owner address' liquidity between a lower and upper tick boundary
/// @dev Positions store additional state for tracking fees owed to the position
library Position {
    using CustomRevert for bytes4;

    /// @notice Cannot update a position with no liquidity
    error CannotUpdateEmptyPosition();

    // info stored for each user's position
    struct State {
        // the amount of liquidity owned by this position
        uint128 liquidity;
        // fee growth per unit of liquidity as of the last update to liquidity or fees owed
        uint256 feeGrowthInside0LastX128;
        uint256 feeGrowthInside1LastX128;
    }

    /// @notice Returns the State struct of a position, given an owner and position boundaries
    /// @param self The mapping containing all user positions
    /// @param owner The address of the position owner
    /// @param tickLower The lower tick boundary of the position
    /// @param tickUpper The upper tick boundary of the position
    /// @param salt A unique value to differentiate between multiple positions in the same range
    /// @return position The position info struct of the given owners' position
    function get(mapping(bytes32 => State) storage self, address owner, int24 tickLower, int24 tickUpper, bytes32 salt)
        internal
        view
        returns (State storage position)
    {
        bytes32 positionKey = calculatePositionKey(owner, tickLower, tickUpper, salt);
        position = self[positionKey];
    }

    /// @notice A helper function to calculate the position key
    /// @param owner The address of the position owner
    /// @param tickLower the lower tick boundary of the position
    /// @param tickUpper the upper tick boundary of the position
    /// @param salt A unique value to differentiate between multiple positions in the same range, by the same owner. Passed in by the caller.
    function calculatePositionKey(address owner, int24 tickLower, int24 tickUpper, bytes32 salt)
        internal
        pure
        returns (bytes32 positionKey)
    {
        // positionKey = keccak256(abi.encodePacked(owner, tickLower, tickUpper, salt))
        assembly ("memory-safe") {
            let fmp := mload(0x40)
            mstore(add(fmp, 0x26), salt) // [0x26, 0x46)
            mstore(add(fmp, 0x06), tickUpper) // [0x23, 0x26)
            mstore(add(fmp, 0x03), tickLower) // [0x20, 0x23)
            mstore(fmp, owner) // [0x0c, 0x20)
            positionKey := keccak256(add(fmp, 0x0c), 0x3a) // len is 58 bytes

            // now clean the memory we used
            mstore(add(fmp, 0x40), 0) // fmp+0x40 held salt
            mstore(add(fmp, 0x20), 0) // fmp+0x20 held tickLower, tickUpper, salt
            mstore(fmp, 0) // fmp held owner
        }
    }

    /// @notice Credits accumulated fees to a user's position
    /// @param self The individual position to update
    /// @param liquidityDelta The change in pool liquidity as a result of the position update
    /// @param feeGrowthInside0X128 The all-time fee growth in currency0, per unit of liquidity, inside the position's tick boundaries
    /// @param feeGrowthInside1X128 The all-time fee growth in currency1, per unit of liquidity, inside the position's tick boundaries
    /// @return feesOwed0 The amount of currency0 owed to the position owner
    /// @return feesOwed1 The amount of currency1 owed to the position owner
    function update(
        State storage self,
        int128 liquidityDelta,
        uint256 feeGrowthInside0X128,
        uint256 feeGrowthInside1X128
    ) internal returns (uint256 feesOwed0, uint256 feesOwed1) {
        uint128 liquidity = self.liquidity;

        if (liquidityDelta == 0) {
            // disallow pokes for 0 liquidity positions
            if (liquidity == 0) CannotUpdateEmptyPosition.selector.revertWith();
        } else {
            self.liquidity = LiquidityMath.addDelta(liquidity, liquidityDelta);
        }

        // calculate accumulated fees. overflow in the subtraction of fee growth is expected
        unchecked {
            feesOwed0 =
                FullMath.mulDiv(feeGrowthInside0X128 - self.feeGrowthInside0LastX128, liquidity, FixedPoint128.Q128);
            feesOwed1 =
                FullMath.mulDiv(feeGrowthInside1X128 - self.feeGrowthInside1LastX128, liquidity, FixedPoint128.Q128);
        }

        // update the position
        self.feeGrowthInside0LastX128 = feeGrowthInside0X128;
        self.feeGrowthInside1LastX128 = feeGrowthInside1X128;
    }
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (utils/introspection/ERC165.sol)

pragma solidity ^0.8.20;

import {IERC165} from "./IERC165.sol";

/**
 * @dev Implementation of the {IERC165} interface.
 *
 * Contracts that want to implement ERC165 should inherit from this contract and override {supportsInterface} to check
 * for the additional interface id that will be supported. For example:
 *
 * ```solidity
 * function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
 *     return interfaceId == type(MyInterface).interfaceId || super.supportsInterface(interfaceId);
 * }
 * ```
 */
abstract contract ERC165 is IERC165 {
    /**
     * @dev See {IERC165-supportsInterface}.
     */
    function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) {
        return interfaceId == type(IERC165).interfaceId;
    }
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC1155/IERC1155Receiver.sol)

pragma solidity ^0.8.20;

import {IERC165} from "../../utils/introspection/IERC165.sol";

/**
 * @dev Interface that must be implemented by smart contracts in order to receive
 * ERC-1155 token transfers.
 */
interface IERC1155Receiver is IERC165 {
    /**
     * @dev Handles the receipt of a single ERC1155 token type. This function is
     * called at the end of a `safeTransferFrom` after the balance has been updated.
     *
     * NOTE: To accept the transfer, this must return
     * `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))`
     * (i.e. 0xf23a6e61, or its own function selector).
     *
     * @param operator The address which initiated the transfer (i.e. msg.sender)
     * @param from The address which previously owned the token
     * @param id The ID of the token being transferred
     * @param value The amount of tokens being transferred
     * @param data Additional data with no specified format
     * @return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` if transfer is allowed
     */
    function onERC1155Received(
        address operator,
        address from,
        uint256 id,
        uint256 value,
        bytes calldata data
    ) external returns (bytes4);

    /**
     * @dev Handles the receipt of a multiple ERC1155 token types. This function
     * is called at the end of a `safeBatchTransferFrom` after the balances have
     * been updated.
     *
     * NOTE: To accept the transfer(s), this must return
     * `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))`
     * (i.e. 0xbc197c81, or its own function selector).
     *
     * @param operator The address which initiated the batch transfer (i.e. msg.sender)
     * @param from The address which previously owned the token
     * @param ids An array containing ids of each token being transferred (order and length must match values array)
     * @param values An array containing amounts of each token being transferred (order and length must match ids array)
     * @param data Additional data with no specified format
     * @return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` if transfer is allowed
     */
    function onERC1155BatchReceived(
        address operator,
        address from,
        uint256[] calldata ids,
        uint256[] calldata values,
        bytes calldata data
    ) external returns (bytes4);
}

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/// @title Contains 512-bit math functions
/// @notice Facilitates multiplication and division that can have overflow of an intermediate value without any loss of precision
/// @dev Handles "phantom overflow" i.e., allows multiplication and division where an intermediate value overflows 256 bits
library FullMath {
    /// @notice Calculates floor(a×b÷denominator) with full precision. Throws if result overflows a uint256 or denominator == 0
    /// @param a The multiplicand
    /// @param b The multiplier
    /// @param denominator The divisor
    /// @return result The 256-bit result
    /// @dev Credit to Remco Bloemen under MIT license https://xn--2-umb.com/21/muldiv
    function mulDiv(uint256 a, uint256 b, uint256 denominator) internal pure returns (uint256 result) {
        unchecked {
            // 512-bit multiply [prod1 prod0] = a * b
            // Compute the product mod 2**256 and mod 2**256 - 1
            // then use the Chinese Remainder Theorem to reconstruct
            // the 512 bit result. The result is stored in two 256
            // variables such that product = prod1 * 2**256 + prod0
            uint256 prod0 = a * b; // Least significant 256 bits of the product
            uint256 prod1; // Most significant 256 bits of the product
            assembly ("memory-safe") {
                let mm := mulmod(a, b, not(0))
                prod1 := sub(sub(mm, prod0), lt(mm, prod0))
            }

            // Make sure the result is less than 2**256.
            // Also prevents denominator == 0
            require(denominator > prod1);

            // Handle non-overflow cases, 256 by 256 division
            if (prod1 == 0) {
                assembly ("memory-safe") {
                    result := div(prod0, denominator)
                }
                return result;
            }

            ///////////////////////////////////////////////
            // 512 by 256 division.
            ///////////////////////////////////////////////

            // Make division exact by subtracting the remainder from [prod1 prod0]
            // Compute remainder using mulmod
            uint256 remainder;
            assembly ("memory-safe") {
                remainder := mulmod(a, b, denominator)
            }
            // Subtract 256 bit number from 512 bit number
            assembly ("memory-safe") {
                prod1 := sub(prod1, gt(remainder, prod0))
                prod0 := sub(prod0, remainder)
            }

            // Factor powers of two out of denominator
            // Compute largest power of two divisor of denominator.
            // Always >= 1.
            uint256 twos = (0 - denominator) & denominator;
            // Divide denominator by power of two
            assembly ("memory-safe") {
                denominator := div(denominator, twos)
            }

            // Divide [prod1 prod0] by the factors of two
            assembly ("memory-safe") {
                prod0 := div(prod0, twos)
            }
            // Shift in bits from prod1 into prod0. For this we need
            // to flip `twos` such that it is 2**256 / twos.
            // If twos is zero, then it becomes one
            assembly ("memory-safe") {
                twos := add(div(sub(0, twos), twos), 1)
            }
            prod0 |= prod1 * twos;

            // Invert denominator mod 2**256
            // Now that denominator is an odd number, it has an inverse
            // modulo 2**256 such that denominator * inv = 1 mod 2**256.
            // Compute the inverse by starting with a seed that is correct
            // correct for four bits. That is, denominator * inv = 1 mod 2**4
            uint256 inv = (3 * denominator) ^ 2;
            // Now use Newton-Raphson iteration to improve the precision.
            // Thanks to Hensel's lifting lemma, this also works in modular
            // arithmetic, doubling the correct bits in each step.
            inv *= 2 - denominator * inv; // inverse mod 2**8
            inv *= 2 - denominator * inv; // inverse mod 2**16
            inv *= 2 - denominator * inv; // inverse mod 2**32
            inv *= 2 - denominator * inv; // inverse mod 2**64
            inv *= 2 - denominator * inv; // inverse mod 2**128
            inv *= 2 - denominator * inv; // inverse mod 2**256

            // Because the division is now exact we can divide by multiplying
            // with the modular inverse of denominator. This will give us the
            // correct result modulo 2**256. Since the preconditions guarantee
            // that the outcome is less than 2**256, this is the final result.
            // We don't need to compute the high bits of the result and prod1
            // is no longer required.
            result = prod0 * inv;
            return result;
        }
    }

    /// @notice Calculates ceil(a×b÷denominator) with full precision. Throws if result overflows a uint256 or denominator == 0
    /// @param a The multiplicand
    /// @param b The multiplier
    /// @param denominator The divisor
    /// @return result The 256-bit result
    function mulDivRoundingUp(uint256 a, uint256 b, uint256 denominator) internal pure returns (uint256 result) {
        unchecked {
            result = mulDiv(a, b, denominator);
            if (mulmod(a, b, denominator) != 0) {
                require(++result > 0);
            }
        }
    }
}

File 48 of 50 : FixedPoint128.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/// @title FixedPoint128
/// @notice A library for handling binary fixed point numbers, see https://en.wikipedia.org/wiki/Q_(number_format)
library FixedPoint128 {
    uint256 internal constant Q128 = 0x100000000000000000000000000000000;
}

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/// @title Math library for liquidity
library LiquidityMath {
    /// @notice Add a signed liquidity delta to liquidity and revert if it overflows or underflows
    /// @param x The liquidity before change
    /// @param y The delta by which liquidity should be changed
    /// @return z The liquidity delta
    function addDelta(uint128 x, int128 y) internal pure returns (uint128 z) {
        assembly ("memory-safe") {
            z := add(and(x, 0xffffffffffffffffffffffffffffffff), signextend(15, y))
            if shr(128, z) {
                // revert SafeCastOverflow()
                mstore(0, 0x93dafdf1)
                revert(0x1c, 0x04)
            }
        }
    }
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (utils/introspection/IERC165.sol)

pragma solidity ^0.8.20;

/**
 * @dev Interface of the ERC165 standard, as defined in the
 * https://eips.ethereum.org/EIPS/eip-165[EIP].
 *
 * Implementers can declare support of contract interfaces, which can then be
 * queried by others ({ERC165Checker}).
 *
 * For an implementation, see {ERC165}.
 */
interface IERC165 {
    /**
     * @dev Returns true if this contract implements the interface defined by
     * `interfaceId`. See the corresponding
     * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]
     * to learn more about how these ids are created.
     *
     * This function call must use less than 30 000 gas.
     */
    function supportsInterface(bytes4 interfaceId) external view returns (bool);
}

Settings
{
  "remappings": [
    "forge-std/=lib/forge-std/src/",
    "@openzeppelin/=lib/v4-core/lib/openzeppelin-contracts/",
    "solmate/=lib/solmate/",
    "clones-with-immutable-args/=lib/clones-with-immutable-args/src/",
    "univ3-core/=lib/v3-core/contracts/",
    "univ3-periphery/=lib/v3-periphery/contracts/",
    "v4-core/=lib/v4-core/src/",
    "@contracts/=contracts/",
    "@libraries/=contracts/libraries/",
    "@base/=contracts/base/",
    "@interfaces/=contracts/interfaces/",
    "@test_periphery/=test/foundry/test_periphery/",
    "@tokens/=contracts/tokens/",
    "@types/=contracts/types/",
    "@scripts/=scripts/",
    "@uniswap/=lib/",
    "@ensdomains/=lib/v4-core/node_modules/@ensdomains/",
    "ds-test/=lib/clones-with-immutable-args/lib/ds-test/src/",
    "erc4626-tests/=lib/v4-core/lib/openzeppelin-contracts/lib/erc4626-tests/",
    "hardhat/=lib/v4-core/node_modules/hardhat/",
    "openzeppelin-contracts/=lib/openzeppelin-contracts/",
    "solady/=lib/solady/src/",
    "v3-core/=lib/v3-core/contracts/",
    "v3-periphery/=lib/v3-periphery/contracts/"
  ],
  "optimizer": {
    "enabled": true,
    "runs": 1
  },
  "metadata": {
    "useLiteralContent": false,
    "bytecodeHash": "ipfs",
    "appendCBOR": true
  },
  "outputSelection": {
    "*": {
      "*": [
        "evm.bytecode",
        "evm.deployedBytecode",
        "devdoc",
        "userdoc",
        "metadata",
        "abi"
      ]
    }
  },
  "evmVersion": "cancun",
  "viaIR": false,
  "libraries": {
    "contracts/libraries/InteractionHelper.sol": {
      "InteractionHelper": "0x000000000002c18EaE97741d0E361cA88906Af98"
    },
    "contracts/libraries/PanopticMath.sol": {
      "PanopticMath": "0x000000000002a1aeE756F088F0ab4a98A9866F55"
    },
    "contracts/types/PositionBalance.sol": {
      "PositionBalanceLibrary": "0x000000000002Bf547771F919002b51231D9B411e"
    }
  }
}

Contract ABI

API
[{"inputs":[{"internalType":"contract SemiFungiblePositionManager","name":"_sfpm","type":"address"},{"internalType":"contract IPoolManager","name":"_poolManager","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"AccountInsolvent","type":"error"},{"inputs":[],"name":"CastingError","type":"error"},{"inputs":[],"name":"EffectiveLiquidityAboveThreshold","type":"error"},{"inputs":[],"name":"InputListFail","type":"error"},{"inputs":[],"name":"InvalidTick","type":"error"},{"inputs":[{"internalType":"uint256","name":"parameterType","type":"uint256"}],"name":"InvalidTokenIdParameter","type":"error"},{"inputs":[],"name":"NoLegsExercisable","type":"error"},{"inputs":[],"name":"NotALongLeg","type":"error"},{"inputs":[],"name":"NotMarginCalled","type":"error"},{"inputs":[],"name":"PoolAlreadyInitialized","type":"error"},{"inputs":[],"name":"PositionAlreadyMinted","type":"error"},{"inputs":[],"name":"StaleOracle","type":"error"},{"inputs":[],"name":"TooManyLegsOpen","type":"error"},{"inputs":[],"name":"UnderOverFlow","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"liquidator","type":"address"},{"indexed":true,"internalType":"address","name":"liquidatee","type":"address"},{"indexed":false,"internalType":"LeftRightSigned","name":"bonusAmounts","type":"int256"}],"name":"AccountLiquidated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"exercisor","type":"address"},{"indexed":true,"internalType":"address","name":"user","type":"address"},{"indexed":true,"internalType":"TokenId","name":"tokenId","type":"uint256"},{"indexed":false,"internalType":"LeftRightSigned","name":"exerciseFee","type":"int256"}],"name":"ForcedExercised","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"recipient","type":"address"},{"indexed":false,"internalType":"uint128","name":"positionSize","type":"uint128"},{"indexed":true,"internalType":"TokenId","name":"tokenId","type":"uint256"},{"indexed":false,"internalType":"LeftRightSigned[4]","name":"premiaByLeg","type":"int256[4]"}],"name":"OptionBurnt","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"recipient","type":"address"},{"indexed":true,"internalType":"TokenId","name":"tokenId","type":"uint256"},{"indexed":false,"internalType":"PositionBalance","name":"balanceData","type":"uint256"},{"indexed":false,"internalType":"LeftRightUnsigned","name":"commissions","type":"uint256"}],"name":"OptionMinted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"user","type":"address"},{"indexed":true,"internalType":"TokenId","name":"tokenId","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"legIndex","type":"uint256"},{"indexed":false,"internalType":"LeftRightSigned","name":"settledAmounts","type":"int256"}],"name":"PremiumSettled","type":"event"},{"inputs":[{"internalType":"uint256","name":"minValue0","type":"uint256"},{"internalType":"uint256","name":"minValue1","type":"uint256"}],"name":"assertMinCollateralValues","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"TokenId[]","name":"positionIdList","type":"uint256[]"},{"internalType":"TokenId[]","name":"newPositionIdList","type":"uint256[]"},{"internalType":"int24","name":"tickLimitLow","type":"int24"},{"internalType":"int24","name":"tickLimitHigh","type":"int24"},{"internalType":"bool","name":"usePremiaAsCollateral","type":"bool"}],"name":"burnOptions","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"TokenId","name":"tokenId","type":"uint256"},{"internalType":"TokenId[]","name":"newPositionIdList","type":"uint256[]"},{"internalType":"int24","name":"tickLimitLow","type":"int24"},{"internalType":"int24","name":"tickLimitHigh","type":"int24"},{"internalType":"bool","name":"usePremiaAsCollateral","type":"bool"}],"name":"burnOptions","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"collateralToken0","outputs":[{"internalType":"contract CollateralTracker","name":"","type":"address"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"collateralToken1","outputs":[{"internalType":"contract CollateralTracker","name":"","type":"address"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"},{"internalType":"TokenId","name":"tokenId","type":"uint256"},{"internalType":"TokenId[]","name":"positionIdListExercisee","type":"uint256[]"},{"internalType":"TokenId[]","name":"positionIdListExercisor","type":"uint256[]"},{"internalType":"LeftRightUnsigned","name":"usePremiaAsCollateral","type":"uint256"}],"name":"forceExercise","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"user","type":"address"},{"internalType":"bool","name":"includePendingPremium","type":"bool"},{"internalType":"TokenId[]","name":"positionIdList","type":"uint256[]"}],"name":"getAccumulatedFeesAndPositionsData","outputs":[{"internalType":"LeftRightUnsigned","name":"","type":"uint256"},{"internalType":"LeftRightUnsigned","name":"","type":"uint256"},{"internalType":"uint256[2][]","name":"","type":"uint256[2][]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getOracleTicks","outputs":[{"internalType":"int24","name":"currentTick","type":"int24"},{"internalType":"int24","name":"fastOracleTick","type":"int24"},{"internalType":"int24","name":"slowOracleTick","type":"int24"},{"internalType":"int24","name":"latestObservation","type":"int24"},{"internalType":"uint256","name":"medianData","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"initialize","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"isSafeMode","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"TokenId[]","name":"positionIdListLiquidator","type":"uint256[]"},{"internalType":"address","name":"liquidatee","type":"address"},{"internalType":"TokenId[]","name":"positionIdList","type":"uint256[]"}],"name":"liquidate","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"TokenId[]","name":"positionIdList","type":"uint256[]"},{"internalType":"uint128","name":"positionSize","type":"uint128"},{"internalType":"uint64","name":"effectiveLiquidityLimitX32","type":"uint64"},{"internalType":"int24","name":"tickLimitLow","type":"int24"},{"internalType":"int24","name":"tickLimitHigh","type":"int24"},{"internalType":"bool","name":"usePremiaAsCollateral","type":"bool"}],"name":"mintOptions","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes[]","name":"data","type":"bytes[]"}],"name":"multicall","outputs":[{"internalType":"bytes[]","name":"results","type":"bytes[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"user","type":"address"}],"name":"numberOfLegs","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bytes","name":"","type":"bytes"}],"name":"onERC1155Received","outputs":[{"internalType":"bytes4","name":"","type":"bytes4"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"oracleContract","outputs":[{"internalType":"contract IV3CompatibleOracle","name":"","type":"address"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"pokeMedian","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"poolKey","outputs":[{"components":[{"internalType":"Currency","name":"currency0","type":"address"},{"internalType":"Currency","name":"currency1","type":"address"},{"internalType":"uint24","name":"fee","type":"uint24"},{"internalType":"int24","name":"tickSpacing","type":"int24"},{"internalType":"contract IHooks","name":"hooks","type":"address"}],"internalType":"struct PoolKey","name":"key","type":"tuple"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"user","type":"address"},{"internalType":"TokenId","name":"tokenId","type":"uint256"}],"name":"positionData","outputs":[{"internalType":"int24","name":"","type":"int24"},{"internalType":"int24","name":"","type":"int24"},{"internalType":"int24","name":"","type":"int24"},{"internalType":"int24","name":"","type":"int24"},{"internalType":"int256","name":"","type":"int256"},{"internalType":"int256","name":"","type":"int256"},{"internalType":"uint128","name":"","type":"uint128"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"TokenId[]","name":"positionIdList","type":"uint256[]"},{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint256","name":"legIndex","type":"uint256"},{"internalType":"bool","name":"usePremiaAsCollateral","type":"bool"}],"name":"settleLongPremium","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"user","type":"address"},{"internalType":"TokenId[]","name":"positionIdList","type":"uint256[]"},{"internalType":"bool","name":"usePremiaAsCollateral","type":"bool"}],"name":"validateCollateralWithdrawable","outputs":[],"stateMutability":"view","type":"function"}]

Block Transaction Difficulty Gas Used Reward
View All Blocks Produced

Block Uncle Number Difficulty Gas Used Reward
View All Uncles
Loading...
Loading
Loading...
Loading
Loading...
Loading

Validator Index Block Amount
View All Withdrawals

Transaction Hash Block Value Eth2 PubKey Valid
View All Deposits
Loading...
Loading
[ Download: CSV Export  ]
[ Download: CSV Export  ]

A contract address hosts a smart contract, which is a set of code stored on the blockchain that runs when predetermined conditions are met. Learn more about addresses in our Knowledge Base.