ERC-20
Overview
Max Total Supply
703,356.00083062 ERC20 ***
Holders
9
Market
Onchain Market Cap
$0.00
Circulating Supply Market Cap
-
Other Info
Token Contract (WITH 8 Decimals)
Balance
591,935.83965738 ERC20 ***Value
$0.00Loading...
Loading
Loading...
Loading
Loading...
Loading
# | Exchange | Pair | Price | 24H Volume | % Volume |
---|
Minimal Proxy Contract for 0x000000000001931ac40ff8b16f08e47d2a7cd650
Contract Name:
CollateralTracker
Compiler Version
v0.8.28+commit.7893614a
Optimization Enabled:
Yes with 9999999 runs
Other Settings:
cancun EvmVersion
Contract Source Code (Solidity Standard Json-Input format)
// SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.24; // Interfaces import {PanopticPool} from "./PanopticPool.sol"; // Inherited implementations import {ERC20Minimal} from "@tokens/ERC20Minimal.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 {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 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 USDC LP on ETH/USDC 30bps). string internal constant NAME_PREFIX = "POPT-V1"; /// @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; /*////////////////////////////////////////////////////////////// UNISWAP POOL DATA //////////////////////////////////////////////////////////////*/ /// @notice The address of underlying token0 or token1 from the Uniswap Pool. /// @dev Whether this is token0 or token1 depends on which collateral token is being tracked in this CollateralTracker instance. address internal s_underlyingToken; /// @notice Boolean which tracks whether this CollateralTracker has been initialized. bool internal s_initialized; /// @notice Stores address of token0 from the underlying Uniswap V3 pool. address internal s_univ3token0; /// @notice Stores address of token1 from the underlying Uniswap V3 pool. address internal s_univ3token1; /// @notice Store whether the current collateral token is token0 of the AMM (true) or token1 (false). bool internal s_underlyingIsToken0; /*////////////////////////////////////////////////////////////// PANOPTIC POOL DATA //////////////////////////////////////////////////////////////*/ /// @notice The Collateral Tracker keeps a reference to the Panoptic Pool using it. PanopticPool internal s_panopticPool; /// @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; /// @notice Additional risk premium charged on intrinsic value of ITM positions, /// denominated in hundredths of basis points. uint128 internal s_ITMSpreadFee; /// @notice The fee of the Uniswap pool in hundredths of basis points. uint24 internal s_poolFee; /*////////////////////////////////////////////////////////////// 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 Multiplier, in basis points, to the pool fee that is charged on the intrinsic value of ITM positions. /// @dev e.g. ITM_SPREAD_MULTIPLIER = 20_000, s_ITMSpreadFee = 2 * s_poolFee. uint256 immutable ITM_SPREAD_MULTIPLIER; /*////////////////////////////////////////////////////////////// ACCESS CONTROL //////////////////////////////////////////////////////////////*/ /// @notice Reverts if the associated Panoptic Pool is not the caller. modifier onlyPanopticPool() { if (msg.sender != address(s_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 _ITMSpreadMultiplier Multiplier, in basis points, to the pool fee that is charged on the intrinsic value of ITM positions constructor( uint256 _commissionFee, uint256 _sellerCollateralRatio, uint256 _buyerCollateralRatio, int256 _forceExerciseCost, uint256 _targetPoolUtilization, uint256 _saturatedPoolUtilization, uint256 _ITMSpreadMultiplier ) { 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_MULTIPLIER = _ITMSpreadMultiplier; } /// @notice Initialize a new collateral tracker for a specific token corresponding to the Panoptic Pool being created by the factory that called it. /// @dev The factory calls this function to start a new collateral tracking system for the incoming token at `underlyingToken`. /// @dev The factory will do this for each of the two tokens being tracked. Thus, the collateral tracking system does not track *both* tokens at once. /// @param underlyingIsToken0 Whether this collateral tracker is for token0 (true) or token1 (false) /// @param token0 Token 0 of the Uniswap pool /// @param token1 Token 1 of the Uniswap pool /// @param fee The fee of the Uniswap pool /// @param panopticPool The address of the Panoptic Pool being created and linked to this Collateral Tracker function startToken( bool underlyingIsToken0, address token0, address token1, uint24 fee, PanopticPool panopticPool ) external { // fails if already initialized 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; // store the address of the underlying ERC20 token s_underlyingToken = underlyingIsToken0 ? token0 : token1; // store the Panoptic pool for this collateral token s_panopticPool = panopticPool; // cache the pool fee in hundredths of basis points s_poolFee = fee; // Stores the addresses of the underlying tracked tokens. s_univ3token0 = token0; s_univ3token1 = token1; // store whether the current collateral token is token0 (true) or token1 (false) s_underlyingIsToken0 = underlyingIsToken0; // Additional risk premium charged on intrinsic value of ITM positions unchecked { s_ITMSpreadFee = uint128((ITM_SPREAD_MULTIPLIER * fee) / DECIMALS); } } /*////////////////////////////////////////////////////////////// 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 token 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 token 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( s_univ3token0, s_univ3token1, s_underlyingIsToken0, s_poolFee, NAME_PREFIX ); } /// @notice Returns symbol as prefixed symbol of underlying token. /// @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(s_underlyingToken, TICKER_PREFIX); } /// @notice Returns decimals of underlying token (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(s_underlyingToken); } /*////////////////////////////////////////////////////////////// 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 (s_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 (s_panopticPool.numberOfLegs(from) != 0) revert Errors.PositionCountNotZero(); return ERC20Minimal.transferFrom(from, to, amount); } /*////////////////////////////////////////////////////////////// STANDARD ERC4626 INTERFACE //////////////////////////////////////////////////////////////*/ /// @notice Get the token contract address of the underlying asset being managed. /// @return assetTokenAddress The address of the underlying asset function asset() external view returns (address assetTokenAddress) { return s_underlyingToken; } /// @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 tokens (assets) to the Panoptic pool from the LP and mint corresponding amount of shares. /// @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 returns (uint256 shares) { if (assets > type(uint104).max) revert Errors.DepositTooLarge(); shares = previewDeposit(assets); // transfer assets (underlying token funds) from the user/the LP to the PanopticPool // in return for the shares to be minted SafeTransferLib.safeTransferFrom( s_underlyingToken, msg.sender, address(s_panopticPool), assets ); // mint collateral shares of the Panoptic Pool funds (this ERC20 token) _mint(receiver, shares); // update tracked asset balance s_poolAssets += uint128(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 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 returns (uint256 assets) { assets = previewMint(shares); if (assets > type(uint104).max) revert Errors.DepositTooLarge(); // transfer assets (underlying token funds) from the user/the LP to the PanopticPool // in return for the shares to be minted SafeTransferLib.safeTransferFrom( s_underlyingToken, msg.sender, address(s_panopticPool), assets ); // mint collateral shares of the Panoptic Pool funds (this ERC20 token) _mint(receiver, shares); // update tracked asset balance s_poolAssets += uint128(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 s_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 collateral shares of the Panoptic Pool funds (this ERC20 token) _burn(owner, shares); // update tracked asset balance unchecked { s_poolAssets -= uint128(assets); } // transfer assets (underlying token funds) from the PanopticPool to the LP SafeTransferLib.safeTransferFrom( s_underlyingToken, address(s_panopticPool), receiver, 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 function withdraw( uint256 assets, address receiver, address owner, TokenId[] calldata positionIdList ) 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 collateral shares of the Panoptic Pool funds (this ERC20 token) _burn(owner, shares); // update tracked asset balance s_poolAssets -= uint128(assets); // reverts if account is not solvent/eligible to withdraw s_panopticPool.validateCollateralWithdrawable(owner, positionIdList); // transfer assets (underlying token funds) from the PanopticPool to the LP SafeTransferLib.safeTransferFrom( s_underlyingToken, address(s_panopticPool), receiver, 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 s_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 collateral shares of the Panoptic Pool funds (this ERC20 token) _burn(owner, shares); // update tracked asset balance unchecked { s_poolAssets -= uint128(assets); } // transfer assets (underlying token funds) from the PanopticPool to the LP SafeTransferLib.safeTransferFrom( s_underlyingToken, address(s_panopticPool), receiver, 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 token 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 token1 for token0 at a price (token0/token1) more favorable than market (token1/token0), // forcing the exercisee to provide more value in token0 than they would have provided in token1 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 onlyPanopticPool { if (bonus < 0) { uint256 bonusAbs; unchecked { bonusAbs = uint256(-bonus); } SafeTransferLib.safeTransferFrom(s_underlyingToken, liquidator, msg.sender, bonusAbs); _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; } } } 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); } } } /// @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( s_ITMSpreadFee * uint256(Math.abs(intrinsicValue)), DECIMALS * 100 ) ); 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 underlyingIsToken0 = s_underlyingIsToken0; int16 poolUtilization = underlyingIsToken0 ? int16(PositionBalance.wrap(positionBalanceArray[i][1]).utilization0()) : int16(PositionBalance.wrap(positionBalanceArray[i][1]).utilization1()); uint256 _tokenRequired = _getRequiredCollateralAtTickSinglePosition( tokenId, positionSize, atTick, poolUtilization, underlyingIsToken0 ); 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 underlyingIsToken0 Cached `s_underlyingIsToken0` 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 underlyingIsToken0 ) internal view returns (uint256 tokenRequired) { uint256 numLegs = tokenId.countLegs(); unchecked { for (uint256 index = 0; index < numLegs; ++index) { if (tokenId.tokenType(index) != (underlyingIsToken0 ? 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 (token0 or token1) 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_V3POOL_TICK) ) // puts -> price/strike : Math.getSqrtRatioAtTick( Math.max24(2 * (strike - atTick), Constants.MIN_V3POOL_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 {CollateralTracker} from "@contracts/CollateralTracker.sol"; import {SemiFungiblePositionManager} from "@contracts/SemiFungiblePositionManager.sol"; import {IUniswapV3Pool} from "univ3-core/interfaces/IUniswapV3Pool.sol"; // Inherited implementations import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.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"; // Custom types import {LeftRightUnsigned, LeftRightSigned} from "@types/LeftRight.sol"; import {LiquidityChunk} from "@types/LiquidityChunk.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 ERC1155Holder, 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 token 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 token 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 token0 (right slot) and token1 (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 settled for token0 (right) and token1 (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_V3POOL_TICK - 1; /// @notice Upper price bound used when no slippage check is required. int24 internal constant MAX_SWAP_TICK = Constants.MAX_V3POOL_TICK + 1; /// @notice Flag that signals to compute premia for both the short and long legs of a position. bool internal constant COMPUTE_ALL_PREMIA = 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 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). uint64 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; /*////////////////////////////////////////////////////////////// STORAGE //////////////////////////////////////////////////////////////*/ /// @notice The Uniswap V3 pool that this instance of Panoptic is deployed on. IUniswapV3Pool internal s_univ3pool; /// @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_V3POOL_TICK-1] [7] // 111100100111011000010111 // // [Constants.MAX_V3POOL_TICK+1] [0] // 000011011000100111101001 // // [Constants.MIN_V3POOL_TICK-1] [6] // 111100100111011000010111 // // [Constants.MAX_V3POOL_TICK+1] [1] // 000011011000100111101001 // // [Constants.MIN_V3POOL_TICK-1] [5] // 111100100111011000010111 // // [Constants.MAX_V3POOL_TICK+1] [2] // 000011011000100111101001 // // [CURRENT TICK] [4] // (000000000000000000000000) // dynamic // // [CURRENT TICK] [3] // (000000000000000000000000) // dynamic uint256 internal s_miniMedian; // ERC4626 vaults that users collateralize their positions with // Each token has its own vault, listed in the same order as the tokens in the pool // In addition to collateral deposits, these vaults also handle various collateral/bonus/exercise computations /// @notice Collateral vault for token0 in the Uniswap pool. CollateralTracker internal s_collateralToken0; /// @notice Collateral vault for token1 in the Uniswap pool. CollateralTracker internal s_collateralToken1; /// @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 token0, left slot is token1. 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 token0, left slot is token1. 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; /*////////////////////////////////////////////////////////////// INITIALIZATION //////////////////////////////////////////////////////////////*/ /// @notice Store the address of the canonical SemiFungiblePositionManager (SFPM) contract. /// @param _sfpm The address of the SFPM constructor(SemiFungiblePositionManager _sfpm) { SFPM = _sfpm; } /// @notice Initializes a Panoptic Pool on top of an existing Uniswap V3 + collateral vault pair. /// @dev Must be called first (by a factory contract) before any transaction can occur. /// @param _univ3pool Address of the target Uniswap V3 pool /// @param token0 Address of the pool's token0 /// @param token1 Address of the pool's token1 /// @param collateralTracker0 Address of the collateral vault for token0 /// @param collateralTracker1 Address of the collateral vault for token1 function startPool( IUniswapV3Pool _univ3pool, address token0, address token1, CollateralTracker collateralTracker0, CollateralTracker collateralTracker1 ) external { // reverts if the Uniswap pool has already been initialized if (address(s_univ3pool) != address(0)) revert Errors.PoolAlreadyInitialized(); // Store the univ3Pool variable s_univ3pool = IUniswapV3Pool(_univ3pool); (, int24 currentTick, , , , , ) = IUniswapV3Pool(_univ3pool).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) } // Store the collateral token0 s_collateralToken0 = collateralTracker0; s_collateralToken1 = collateralTracker1; // consolidate all 4 approval calls to one library delegatecall in order to reduce bytecode size // approves: // SFPM: token0, token1 // CollateralTracker0 - token0 // CollateralTracker1 - token1 InteractionHelper.doApprovals(SFPM, collateralTracker0, collateralTracker1, token0, token1); } /*////////////////////////////////////////////////////////////// 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 `token0` value of collateral /// @param minValue1 The minimum acceptable `token1` value of collateral function assertMinCollateralValues(uint256 minValue0, uint256 minValue1) external view { CollateralTracker ct0 = s_collateralToken0; CollateralTracker ct1 = s_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` function validateCollateralWithdrawable( address user, TokenId[] calldata positionIdList ) external view { _validateSolvency(user, positionIdList, BP_DECREASE_BUFFER); } /// @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` (token0: right slot, token1: left slot) /// @return The total amount of premium owed by the long legs in `positionIdList` (token0: right slot, token1: 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) { // Get the current tick of the Uniswap pool (, int24 currentTick, , , , , ) = s_univ3pool.slot0(); // Compute the accumulated premia for all tokenId in positionIdList (includes short+long premium) return _calculateAccumulatedPremia( user, positionIdList, COMPUTE_ALL_PREMIA, includePendingPremium, currentTick ); } /// @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 computeAllPremia Whether to compute accumulated premia for all legs held by the user (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` (token0: right slot, token1: left slot) /// @return longPremium The total amount of premium owed by the long legs in `positionIdList` (token0: right slot, token1: 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 computeAllPremia, bool includePendingPremium, int24 atTick ) internal view returns ( LeftRightUnsigned shortPremium, LeftRightUnsigned longPremium, uint256[2][] memory balances ) { uint256 pLength = positionIdList.length; balances = new uint256[2][](pLength); address c_user = user; // loop through each option position/tokenId for (uint256 k = 0; k < pLength; ) { TokenId tokenId = positionIdList[k]; balances[k][0] = TokenId.unwrap(tokenId); balances[k][1] = PositionBalance.unwrap(s_positionBalance[c_user][tokenId]); ( LeftRightSigned[4] memory premiaByLeg, uint256[2][4] memory premiumAccumulatorsByLeg ) = _getPremia( tokenId, LeftRightUnsigned.wrap(balances[k][1]).rightSlot(), c_user, computeAllPremia, 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); LeftRightUnsigned availablePremium = _getAvailablePremium( totalLiquidity, s_settledTokens[chunkKey], s_grossPremiumLast[chunkKey], LeftRightUnsigned.wrap( uint256(LeftRightSigned.unwrap(premiaByLeg[leg])) ), premiumAccumulatorsByLeg[leg] ); shortPremium = shortPremium.add(availablePremium); } 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 Uniswap observation if the `MEDIAN_PERIOD` has elapsed. function pokeMedian() external { (, , uint16 observationIndex, uint16 observationCardinality, , , ) = s_univ3pool.slot0(); (, uint256 medianData) = PanopticMath.computeInternalMedian( observationIndex, observationCardinality, Constants.MEDIAN_PERIOD, s_miniMedian, s_univ3pool ); 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 function mintOptions( TokenId[] calldata positionIdList, uint128 positionSize, uint64 effectiveLiquidityLimitX32, int24 tickLimitLow, int24 tickLimitHigh ) external { _mintOptions( positionIdList, positionSize, effectiveLiquidityLimitX32, tickLimitLow, tickLimitHigh ); } /// @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 function burnOptions( TokenId tokenId, TokenId[] calldata newPositionIdList, int24 tickLimitLow, int24 tickLimitHigh ) external { _burnOptions(COMMIT_LONG_SETTLED, tokenId, msg.sender, tickLimitLow, tickLimitHigh); uint256 medianData = _validateSolvency(msg.sender, newPositionIdList, NO_BUFFER); // 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 function burnOptions( TokenId[] calldata positionIdList, TokenId[] calldata newPositionIdList, int24 tickLimitLow, int24 tickLimitHigh ) external { _burnAllOptionsFrom( msg.sender, tickLimitLow, tickLimitHigh, COMMIT_LONG_SETTLED, positionIdList ); uint256 medianData = _validateSolvency(msg.sender, newPositionIdList, NO_BUFFER); // 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 function _mintOptions( TokenId[] calldata positionIdList, uint128 positionSize, uint64 effectiveLiquidityLimitX32, int24 tickLimitLow, int24 tickLimitHigh ) 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(address(s_univ3pool))) 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 currentTick, int24 fastOracleTick, int24 slowOracleTick, int24 lastObservedTick, uint256 medianData ) = PanopticMath.getOracleTicks(s_univ3pool, s_miniMedian); tickData = PositionBalanceLibrary.packTickData( currentTick, 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); 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 token0 and left 64bits for token1. 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 token0 (right) and token1 (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(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 token0 and left 64bits for token1, 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) = s_collateralToken0.takeCommissionAddData( msg.sender, longAmounts.rightSlot(), shortAmounts.rightSlot(), totalSwapped.rightSlot(), isCovered ); (uint32 utilization1, uint128 commission1) = s_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` /// @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 ) internal view returns (uint256) { ( int24 currentTick, int24 fastOracleTick, int24 slowOracleTick, int24 lastObservedTick, uint256 medianData ) = PanopticMath.getOracleTicks(s_univ3pool, s_miniMedian); uint96 tickData = PositionBalanceLibrary.packTickData( currentTick, fastOracleTick, slowOracleTick, lastObservedTick ); _checkSolvency(user, positionIdList, tickData, buffer); 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` function _checkSolvency( address user, TokenId[] calldata positionIdList, uint96 tickData, uint256 buffer ) 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. 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); } /// @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(tokenId, positionSize, tickLimitLow, tickLimitHigh); LeftRightSigned realizedPremia; (realizedPremia, premiaByLeg) = _updateSettlementPostBurn( owner, tokenId, collectedByLeg, positionSize, commitLongSettled ); (LeftRightSigned longAmounts, LeftRightSigned shortAmounts) = PanopticMath .computeExercisedAmounts(tokenId, positionSize); { int128 paid0 = s_collateralToken0.exercise( owner, longAmounts.rightSlot(), shortAmounts.rightSlot(), totalSwapped.rightSlot(), realizedPremia.rightSlot() ); paidAmounts = paidAmounts.toRightSlot(paid0); } { int128 paid1 = s_collateralToken1.exercise( owner, longAmounts.leftSlot(), shortAmounts.leftSlot(), totalSwapped.leftSlot(), realizedPremia.leftSlot() ); paidAmounts = paidAmounts.toLeftSlot(paid1); } } /*////////////////////////////////////////////////////////////// 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. /// @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 { _validatePositionList(liquidatee, positionIdList, 0); // Assert the account we are liquidating is actually insolvent int24 twapTick = getUniV3TWAP(); int24 currentTick; { // Enforce maximum delta between TWAP and currentTick to prevent extreme price manipulation int24 fastOracleTick; int24 lastObservedTick; (currentTick, fastOracleTick, , lastObservedTick, ) = PanopticMath.getOracleTicks( s_univ3pool, s_miniMedian ); unchecked { if (Math.abs(currentTick - twapTick) > MAX_TWAP_DELTA_LIQUIDATION) revert Errors.StaleTWAP(); } // 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 ); } LeftRightUnsigned tokenData0; LeftRightUnsigned tokenData1; LeftRightUnsigned shortPremium; { uint256[2][] memory positionBalanceArray = new uint256[2][](positionIdList.length); LeftRightUnsigned longPremium; (shortPremium, longPremium, positionBalanceArray) = _calculateAccumulatedPremia( liquidatee, positionIdList, COMPUTE_ALL_PREMIA, ONLY_AVAILABLE_PREMIUM, currentTick ); tokenData0 = s_collateralToken0.getAccountMarginDetails( liquidatee, twapTick, positionBalanceArray, shortPremium.rightSlot(), longPremium.rightSlot() ); tokenData1 = s_collateralToken1.getAccountMarginDetails( liquidatee, twapTick, positionBalanceArray, shortPremium.leftSlot(), longPremium.leftSlot() ); } // The protocol delegates some virtual shares to ensure the burn can be settled. s_collateralToken0.delegate(liquidatee); s_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, s_collateralToken0, s_collateralToken1, Math.getSqrtRatioAtTick(_twapTick), s_settledTokens ); bonusAmounts = bonusAmounts.add(bonusDeltas); } // revoke delegated virtual shares and settle any bonus deltas with the liquidator s_collateralToken0.settleLiquidation(msg.sender, liquidatee, bonusAmounts.rightSlot()); s_collateralToken1.settleLiquidation(msg.sender, liquidatee, bonusAmounts.leftSlot()); // ensure the liquidator is still solvent after the liquidation _validateSolvency(msg.sender, positionIdListLiquidator, NO_BUFFER); 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 function forceExercise( address account, TokenId tokenId, TokenId[] calldata positionIdListExercisee, TokenId[] calldata positionIdListExercisor ) 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 twapTick = getUniV3TWAP(); // to be eligible for force exercise, the price *must* be outside the position's range for at least 1 leg tokenId.validateIsExercisable(twapTick); LeftRightSigned exerciseFees; { (, int24 currentTick, , , , , ) = s_univ3pool.slot0(); 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 = s_collateralToken0.exerciseCost( currentTick, twapTick, tokenId, positionSize, longAmounts ); } // The protocol delegates some virtual shares to ensure the burn can be settled. s_collateralToken0.delegate(account); s_collateralToken1.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.getExerciseDeltas( account, exerciseFees, twapTick, s_collateralToken0, s_collateralToken1 ); // settle difference between delegated amounts (from the protocol) and exercise fees/substituted tokens s_collateralToken0.refund(account, msg.sender, refundAmounts.rightSlot()); s_collateralToken1.refund(account, msg.sender, refundAmounts.leftSlot()); // revoke the virtual shares that were delegated after settling the difference with the exercisor s_collateralToken0.revoke(account); s_collateralToken1.revoke(account); _validateSolvency(account, positionIdListExercisee, NO_BUFFER); // 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); 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` function _checkSolvencyAtTicks( address account, TokenId[] calldata positionIdList, int24 currentTick, int24[] memory atTicks, uint256 buffer, bool expectedSolvent ) internal view { ( LeftRightUnsigned shortPremium, LeftRightUnsigned longPremium, uint256[2][] memory positionBalanceArray ) = _calculateAccumulatedPremia( account, positionIdList, COMPUTE_ALL_PREMIA, ONLY_AVAILABLE_PREMIUM, currentTick ); uint256 numberOfTicks = atTicks.length; uint8 solvent; for (uint256 i; i < numberOfTicks; ) { unchecked { solvent += ( _isAccountSolvent( account, atTicks[i], positionBalanceArray, shortPremium, longPremium, buffer ) ? uint8(1) : uint8(0) ); ++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) { LeftRightUnsigned tokenData0 = s_collateralToken0.getAccountMarginDetails( account, atTick, positionBalanceArray, shortPremium.rightSlot(), longPremium.rightSlot() ); LeftRightUnsigned tokenData1 = s_collateralToken1.getAccountMarginDetails( account, atTick, positionBalanceArray, shortPremium.leftSlot(), longPremium.leftSlot() ); (uint256 balanceCross, uint256 thresholdCross) = PanopticMath.getCrossBalances( tokenData0, tokenData1, 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) { (, int24 currentTick, , , , , ) = s_univ3pool.slot0(); uint256 medianData = s_miniMedian; unchecked { int24 medianTick = (int24( uint24(medianData >> ((uint24(medianData >> (192 + 3 * 3)) % 8) * 24)) ) + int24(uint24(medianData >> ((uint24(medianData >> (192 + 3 * 4)) % 8) * 24)))) / 2; // If ticks have recently deviated more than +/- ~10%, enforce covered mints return Math.abs(currentTick - medianTick) > 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; uint256 currentHash = s_positionsHash[account]; 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 != currentHash) 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 Get the address of the AMM pool connected to this Panoptic pool. /// @return AMM pool corresponding to this Panoptic pool function univ3pool() external view returns (IUniswapV3Pool) { return s_univ3pool; } /// @notice Get the collateral token corresponding to token0 of the AMM pool. /// @return Collateral token corresponding to token0 in the AMM function collateralToken0() external view returns (CollateralTracker) { return s_collateralToken0; } /// @notice Get the collateral token corresponding to token1 of the AMM pool. /// @return Collateral token corresponding to token1 in the AMM function collateralToken1() external view returns (CollateralTracker) { return s_collateralToken1; } /// @notice Computes and returns all oracle ticks. /// @return currentTick The current tick in the Uniswap pool /// @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 (either composed of observations retrieved from Uniswap or observations stored in `s_miniMedian`) /// @return latestObservation The latest observation from the Uniswap pool /// @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, fastOracleTick, slowOracleTick, latestObservation, ) = PanopticMath .getOracleTicks(s_univ3pool, 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 token0 at mint /// @return Utilization of token1 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 getUniV3TWAP() internal view returns (int24 twapTick) { twapTick = PanopticMath.twapFilter(s_univ3pool, 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, uint64 effectiveLiquidityLimitX32 ) internal view returns (uint256 totalLiquidity) { uint128 netLiquidity; uint128 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 = (uint256(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 > uint256(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 computeAllPremia Whether to compute accumulated premia for all legs held by the user (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 computeAllPremia, 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) || computeAllPremia) { LiquidityChunk liquidityChunk = PanopticMath.getLiquidityChunk( tokenId, leg, positionSize ); uint256 tokenType = tokenId.tokenType(leg); (premiumAccumulatorsByLeg[leg][0], premiumAccumulatorsByLeg[leg][1]) = SFPM .getAccountPremium( address(s_univ3pool), 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) function settleLongPremium( TokenId[] calldata positionIdList, address owner, uint256 legIndex ) external { _validatePositionList(owner, positionIdList, 0); TokenId tokenId = positionIdList[positionIdList.length - 1]; if (tokenId.isLong(legIndex) == 0 || legIndex > 3) revert Errors.NotALongLeg(); LiquidityChunk liquidityChunk = PanopticMath.getLiquidityChunk( tokenId, legIndex, s_positionBalance[owner][tokenId].positionSize() ); (, int24 currentTick, , , , , ) = s_univ3pool.slot0(); LeftRightUnsigned accumulatedPremium; { uint256 tokenType = tokenId.tokenType(legIndex); (uint128 premiumAccumulator0, uint128 premiumAccumulator1) = SFPM.getAccountPremium( address(s_univ3pool), address(this), tokenType, 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 s_collateralToken0.exercise(owner, 0, 0, 0, -realizedPremia.rightSlot()); s_collateralToken1.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); } // ensure the owner is solvent (insolvent accounts are not permitted to pay premium unless they are being liquidated) _validateSolvency(owner, positionIdList, NO_BUFFER); } /// @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( address(s_univ3pool), 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 : uint64(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 token0/token1 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( address(s_univ3pool), 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_ALL_PREMIA, 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); } }
// 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: 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 payable 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; } } } }
// 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 V3 pool int24 internal constant MIN_V3POOL_TICK = -887272; /// @notice Maximum possible price tick in a Uniswap V3 pool int24 internal constant MAX_V3POOL_TICK = 887272; /// @notice Minimum possible sqrtPriceX96 in a Uniswap V3 pool uint160 internal constant MIN_V3POOL_SQRT_RATIO = 4295128739; /// @notice Maximum possible sqrtPriceX96 in a Uniswap V3 pool uint160 internal constant MAX_V3POOL_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 Uniswap 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 Uniswap 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 Uniswap 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 Uniswap 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 Uniswap 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; }
// 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 A mint or swap callback was attempted from an address that did not match the canonical Uniswap V3 pool with the claimed features error InvalidUniswapCallback(); /// @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 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, and liquidations error StaleTWAP(); /// @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(); }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.24; // Interfaces import {CollateralTracker} from "@contracts/CollateralTracker.sol"; import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import {IERC20Partial} from "@tokens/interfaces/IERC20Partial.sol"; import {SemiFungiblePositionManager} from "@contracts/SemiFungiblePositionManager.sol"; // Libraries import {PanopticMath} from "@libraries/PanopticMath.sol"; /// @title InteractionHelper - contains helper functions for external interactions such as approvals. /// @notice Used to delegate logic with multiple external calls. /// @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 Function that performs approvals on behalf of the PanopticPool for CollateralTracker and SemiFungiblePositionManager. /// @param sfpm The SemiFungiblePositionManager being approved for both token0 and token1 /// @param ct0 The CollateralTracker (token0) being approved for token0 /// @param ct1 The CollateralTracker (token1) being approved for token1 /// @param token0 The token0 (in Uniswap) being approved for /// @param token1 The token1 (in Uniswap) being approved for function doApprovals( SemiFungiblePositionManager sfpm, CollateralTracker ct0, CollateralTracker ct1, address token0, address token1 ) external { // Approve transfers of Panoptic Pool funds by SFPM IERC20Partial(token0).approve(address(sfpm), type(uint256).max); IERC20Partial(token1).approve(address(sfpm), type(uint256).max); // Approve transfers of Panoptic Pool funds by Collateral token IERC20Partial(token0).approve(address(ct0), type(uint256).max); IERC20Partial(token1).approve(address(ct1), type(uint256).max); } /// @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 token0 The token0 of the Uniswap Pool /// @param token1 The token1 of the Uniswap Pool /// @param isToken0 Whether the collateral token computing the name is for token0 or token1 /// @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 token0, address token1, bool isToken0, uint24 fee, string memory prefix ) external view returns (string memory) { string memory symbol0 = PanopticMath.safeERC20Symbol(token0); string memory symbol1 = PanopticMath.safeERC20Symbol(token1); unchecked { return string.concat( prefix, " ", isToken0 ? symbol0 : symbol1, " LP on ", symbol0, "/", symbol1, " ", PanopticMath.uniswapFeeToString(fee) ); } } /// @notice Returns collateral token symbol as `prefix` + `underlying token symbol`. /// @param token The address of the underlying token used to compute the symbol /// @param prefix A constant string prepended to the symbol of the underlying token 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 token (0 if not present). /// @param token The address of the underlying token used to compute the decimals /// @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"; 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; /*////////////////////////////////////////////////////////////// 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 token0 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 token0 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 token0 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 V3 pool with `tickSpacing`. /// @param tickSpacing The spacing between initializable ticks in the Uniswap V3 pool /// @return The maximum liquidity that can reference any given tick in the Uniswap V3 pool function getMaxLiquidityPerTick(int24 tickSpacing) internal pure returns (uint128) { unchecked { return type(uint128).max / uint24((Constants.MAX_V3POOL_TICK / tickSpacing) * 2 + 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_V3POOL_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 token0 received for a given LiquidityChunk. /// @param liquidityChunk A specification for a liquidity chunk in Uniswap containing `liquidity`, `tickLower`, and `tickUpper` /// @return The amount of token0 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 token1 received for a given LiquidityChunk. /// @param liquidityChunk A specification for a liquidity chunk in Uniswap containing `liquidity`, `tickLower`, and `tickUpper` /// @return The amount of token1 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 token0 and token1 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 token0 represented by `liquidityChunk` at `currentTick` /// @return amount1 The amount of token1 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 token0 /// @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 token1 /// @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 {IUniswapV3Pool} from "univ3-core/interfaces/IUniswapV3Pool.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 {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 an address to a Uniswap V3 pool, return its 64-bit ID as used in the `TokenId` of Panoptic. // Example: // the 64 bits are the 48 *last* (most significant) bits - and thus corresponds to the *first* 12 hex characters (reading left to right) // of the Uniswap V3 pool address, with the tickSpacing written in the highest 16 bits (i.e, max tickSpacing is 32767) // e.g.: // univ3pool = 0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8 // tickSpacing = 60 // the returned id is then: // poolPattern = 0x00008ad599c3A0ff // tickSpacing = 0x003c000000000000 + // -------------------------------------------- // poolId = 0x003c8ad599c3A0ff // /// @param univ3pool The address of the Uniswap V3 pool to get the ID of /// @param tickSpacing The tick spacing of `univ3pool` /// @return A uint64 representing a fingerprint of the Uniswap V3 pool address function getPoolId(address univ3pool, int24 tickSpacing) internal pure returns (uint64) { unchecked { uint64 poolId = uint64(uint160(univ3pool) >> 112); poolId += uint64(uint24(tickSpacing)) << 48; return poolId; } } /// @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 `token`. /// @param token The address of the token to get the symbol of /// @return The symbol of `token` or "???" if not supported function safeERC20Symbol(address token) external view returns (string memory) { // not guaranteed that token supports metadata extension // so we need to let call fail and return placeholder if not try IERC20Metadata(token).symbol() returns (string memory symbol) { return symbol; } catch { return "???"; } } /// @notice Converts `fee` to a string with "bps" appended. /// @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 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))))); // increment the upper 8 bits (leg counter) if addFlag=true, decrement otherwise uint256 newLegCount = addFlag ? uint8(existingHash >> 248) + uint8(tokenId.countLegs()) : uint8(existingHash >> 248) - tokenId.countLegs(); unchecked { return uint256(updatedHash) + (newLegCount << 248); } } /*////////////////////////////////////////////////////////////// ORACLE CALCULATIONS //////////////////////////////////////////////////////////////*/ /// @notice Computes various oracle prices corresponding to a Uniswap pool. /// @param univ3pool The Uniswap pool to get the observations from /// @param miniMedian The packed structure representing the sorted 8-slot queue of internal median observations /// @return currentTick The current tick in the Uniswap pool /// @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( IUniswapV3Pool univ3pool, uint256 miniMedian ) external view returns ( int24 currentTick, int24 fastOracleTick, int24 slowOracleTick, int24 latestObservation, uint256 medianData ) { uint16 observationIndex; uint16 observationCardinality; IUniswapV3Pool _univ3pool = univ3pool; (, currentTick, observationIndex, observationCardinality, , , ) = univ3pool.slot0(); (fastOracleTick, latestObservation) = computeMedianObservedPrice( _univ3pool, observationIndex, observationCardinality, Constants.FAST_ORACLE_CARDINALITY, Constants.FAST_ORACLE_PERIOD ); if (Constants.SLOW_ORACLE_UNISWAP_MODE) { (slowOracleTick, ) = computeMedianObservedPrice( _univ3pool, observationIndex, observationCardinality, Constants.SLOW_ORACLE_CARDINALITY, Constants.SLOW_ORACLE_PERIOD ); } else { (slowOracleTick, medianData) = computeInternalMedian( observationIndex, observationCardinality, Constants.MEDIAN_PERIOD, miniMedian, _univ3pool ); } } /// @notice Returns the median of the last `cardinality` average prices over `period` observations from `univ3pool`. /// @dev Used when we need a manipulation-resistant TWAP price. /// @dev Uniswap 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 univ3pool The Uniswap pool to get the median observation 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( IUniswapV3Pool univ3pool, 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], , ) = univ3pool.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]); } // get the median of the `ticks` array (assuming `cardinality` is odd) return (int24(Math.sort(ticks)[cardinality / 2]), int24(ticks[0])); } } /// @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 Uniswap 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 univ3pool The Uniswap pool 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, IUniswapV3Pool univ3pool ) 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, , ) = univ3pool.observations( uint256( int256(observationIndex) - int256(1) + int256(observationCardinality) ) % observationCardinality ); (uint256 timestamp_last, int56 tickCumulative_last, , ) = univ3pool .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 the TWAP of a Uniswap V3 pool using data from its 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 univ3pool The Uniswap pool from which to compute the TWAP /// @param twapWindow The time window to compute the TWAP over /// @return The final calculated TWAP tick function twapFilter(IUniswapV3Pool univ3pool, 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, ) = univ3pool.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 V3 pool; this is a liquidity chunk. // Liquidity chunk (defined by tick upper, tick lower, and its size/amount: the liquidity) // liquidity │ // ▲ │ // │ ┌▼┐ // │ ┌──┴─┴──┐ // │ │ │ // │ │ │ // └──┴───────┴────► price // Uniswap V3 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 V3 pool in the above tick range // Background: // // In Uniswap V3, the amount of liquidity received for a given amount of token0 when the price is // not in range is given by: // Liquidity = amount0 * (sqrt(upper) * sqrt(lower)) / (sqrt(upper) - sqrt(lower)) // For token1, 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 V3 chooses token0 and token1 from the alphanumeric order, there is no consistency as to whether token0 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 token0 or token1 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 = uint256(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 V3 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 = token0 and leftSlot = token1 held against borrowed Uniswap liquidity for long legs /// @return shortAmounts Left-right packed word where where rightSlot = token0 and leftSlot = token1 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 token0 into an amount of token1 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 token0 to convert into token1 /// @param sqrtPriceX96 The square root of the price at which to convert `amount` of token0 into token1 /// @return The converted `amount` of token0 represented in terms of token1 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 token0 into an amount of token1 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 token0 to convert into token1 /// @param sqrtPriceX96 The square root of the price at which to convert `amount` of token0 into token1 /// @return The converted `amount` of token0 represented in terms of token1 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 token1 into an amount of token0 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 token1 to convert into token0 /// @param sqrtPriceX96 The square root of the price at which to convert `amount` of token1 into token0 /// @return The converted `amount` of token1 represented in terms of token0 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 token1 into an amount of token0 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 token1 to convert into token0 /// @param sqrtPriceX96 The square root of the price at which to convert `amount` of token1 into token0 /// @return The converted `amount` of token1 represented in terms of token0 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 token0 into an amount of token1 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 token0 to convert into token1 /// @param sqrtPriceX96 The square root of the price at which to convert `amount` of token0 into token1 /// @return The converted `amount` of token0 represented in terms of token1 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 token1 into an amount of token0 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 token1 to convert into token0 /// @param sqrtPriceX96 The square root of the price at which to convert `amount` of token1 into token0 /// @return The converted `amount` of token1 represented in terms of token0 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 (token0/token1) collateral balances and requirements. /// @param tokenData0 LeftRight encoded word with balance of token0 in the right slot, and required balance in left slot /// @param tokenData1 LeftRight encoded word with balance of token1 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 (token0 if `price(token1/token0) < 1` and vice versa) /// @return The combined required collateral threshold of `tokenData0` and `tokenData1` in terms of (token0 if `price(token1/token0) < 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 (token0 if price token1/token0 < 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 = shorts.toRightSlot(Math.toInt128(amountsMoved.rightSlot())); } else { // is option is long, increment longs by contracts longs = longs.toRightSlot(Math.toInt128(amountsMoved.rightSlot())); } } else { if (isShort) { // if option is short, increment shorts by notional shorts = shorts.toLeftSlot(Math.toInt128(amountsMoved.leftSlot())); } else { // if option is long, increment longs by notional longs = longs.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 token0 in the right slot, and required balance in left slot /// @param tokenData1 LeftRight encoded word with balance of token1 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 token0 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 + token0(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 / (token1(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 token0 but some token1 left over, so we use what they have left to mitigate token0 losses // we do this by substituting an equivalent value of token1 in our refund to the liquidator, plus a bonus, for the token0 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 token1 balance: balance1 - paid1 // and paid0 - balance0 is the amount of token0 that the liquidatee is missing, i.e the protocol loss // if the protocol loss is lower than the excess token1 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 token1 balance, we can only mitigate part of the loss, so we should convert only the excess token1 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 token1 but some token0 left over, so we use what they have left to mitigate token1 losses // we do this by substituting an equivalent value of token0 in our refund to the liquidator, plus a bonus, for the token1 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 token0 balance: balance0 - paid0 // and paid1 - balance1 is the amount of token1 that the liquidatee is missing, i.e the protocol loss // if the protocol loss is lower than the excess token0 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 token0 balance, we can only mitigate part of the loss, so we should convert only the excess token0 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 token0 /// @param collateral1 The collateral tracker for token1 /// @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 Redistribute the final exercise fee deltas between tokens if necessary according to the available collateral from the exercised user. /// @param exercisee The address of the user being exercised /// @param exerciseFees Pre-adjustment exercise fees to debit from exercisor (rightSlot = token0 left = token1) /// @param atTick The tick at which to convert between token0/token1 when redistributing the exercise fees /// @param ct0 The collateral tracker for token0 /// @param ct1 The collateral tracker for token1 /// @return The LeftRight-packed deltas for token0/token1 to move from the exercisor to the exercisee function getExerciseDeltas( address exercisee, LeftRightSigned exerciseFees, int24 atTick, CollateralTracker ct0, CollateralTracker ct1 ) external view returns (LeftRightSigned) { uint160 sqrtPriceX96 = Math.getSqrtRatioAtTick(atTick); unchecked { // if the refunder lacks sufficient token0 to pay back the virtual shares, have the exercisor cover the difference in exchange for token1 (and vice versa) int256 balanceShortage = int256(uint256(type(uint248).max)) - int256(ct0.balanceOf(exercisee)) - int256(ct0.convertToShares(uint128(-exerciseFees.rightSlot()))); if (balanceShortage > 0) { return LeftRightSigned .wrap(0) .toRightSlot( int128( exerciseFees.rightSlot() - int256( Math.mulDivRoundingUp( uint256(balanceShortage), ct0.totalAssets(), ct0.totalSupply() ) ) ) ) .toLeftSlot( int128( int256( PanopticMath.convert0to1( ct0.convertToAssets(uint256(balanceShortage)), sqrtPriceX96 ) ) + exerciseFees.leftSlot() ) ); } balanceShortage = int256(uint256(type(uint248).max)) - int256(ct1.balanceOf(exercisee)) - int256(ct1.convertToShares(uint128(-exerciseFees.leftSlot()))); if (balanceShortage > 0) { return LeftRightSigned .wrap(0) .toRightSlot( int128( int256( PanopticMath.convert1to0( ct1.convertToAssets(uint256(balanceShortage)), sqrtPriceX96 ) ) + exerciseFees.rightSlot() ) ) .toLeftSlot( int128( exerciseFees.leftSlot() - int256( Math.mulDivRoundingUp( uint256(balanceShortage), ct1.totalAssets(), ct1.totalSupply() ) ) ) ); } } // otherwise, no need to deviate from the original exercise fee deltas return exerciseFees; } }
// 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 { /*////////////////////////////////////////////////////////////// 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: 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. /// @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)); } } }
// 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 token0, stored as (10000 * inAMM0)/totalAssets0 (uint16). // (3) poolUtilization1 16bits : The pool utilization of token1, 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 ( self.currentTick(), self.fastOracleTick(), self.slowOracleTick(), self.lastObservedTick() ); } /// @notice Get token0 utilization of `self`. /// @param self The PositionBalance to retrieve the token0 utilization from /// @return The token0 utilization in basis points function utilization0(PositionBalance self) internal pure returns (int256) { unchecked { return int256((PositionBalance.unwrap(self) >> 128) % 2 ** 16); } } /// @notice Get token1 utilization of `self`. /// @param self The PositionBalance to retrieve the token1 utilization from /// @return The token1 utilization in basis points function utilization1(PositionBalance self) internal pure returns (int256) { unchecked { return int256((PositionBalance.unwrap(self) >> 144) % 2 ** 16); } } /// @notice Get both token0 and token1 utilizations of `self`. /// @param self The PositionBalance to retrieve the utilizations from /// @return The packed utilizations for token0 and token1 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 token0 at mint /// @return utilization1AtMint Utilization of token1 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) univ3pool 48bits 0bits : first 6 bytes of the Uniswap V3 pool address (first 48 bits; little-endian), plus a pseudorandom number in the event of a collision // (1) tickSpacing 16bits 48bits : tickSpacing for the univ3pool. Up to 16 bits // ===== 4 times (one for each leg) ============================================================== // (2) asset 1bit 0bits : Specifies the asset (0: token0, 1: token1) // (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 -> token0, 1 -> token1) // (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 V3 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 V3 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 V3 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 token0 (return 0) or token1 (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 token0, 1 if asset is token1 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 token moved for a given leg (implies a call or put). Either Token0 or Token1. /// @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 token moved is token1 or 0 if the token moved is token0 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 token moved for a given leg (implies a call or put). Either Token0 or Token1. /// @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 { // NOTE: This is a hack to avoid blowing up the contract size. // We copy the logic from the countLegs function, using it here adds 5K to the contract size with IR for some reason // Strip all bits except for the option ratios uint256 optionRatios = TokenId.unwrap(self) & OPTION_RATIO_MASK; // The legs are filled in from least to most significant // Each comparison here is to the start of the next leg's option ratio // Since only the option ratios remain, we can be sure that no bits above the start of the inactive legs will be 1 if (optionRatios < 2 ** 64) { optionRatios = 0; } else if (optionRatios < 2 ** 112) { optionRatios = 1; } else if (optionRatios < 2 ** 160) { optionRatios = 2; } else if (optionRatios < 2 ** 208) { optionRatios = 3; } else { optionRatios = 4; } // 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 - optionRatios))) & 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 The number of active legs in `self` (in the range {0,...,4}) function countLegs(TokenId self) internal pure returns (uint256) { // Strip all bits except for the option ratios uint256 optionRatios = TokenId.unwrap(self) & OPTION_RATIO_MASK; // The legs are filled in from least to most significant // Each comparison here is to the start of the next leg's option ratio section // Since only the option ratios remain, we can be sure that no bits above the start of the inactive legs will be 1 if (optionRatios < 2 ** 64) { return 0; } else if (optionRatios < 2 ** 112) { return 1; } else if (optionRatios < 2 ** 160) { return 2; } else if (optionRatios < 2 ** 208) { return 3; } return 4; } /// @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_V3POOL_TICK) || (self.strike(i) == Constants.MAX_V3POOL_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 V3 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: BUSL-1.1 pragma solidity ^0.8.24; // Interfaces import {IERC20Partial} from "@tokens/interfaces/IERC20Partial.sol"; import {IUniswapV3Factory} from "univ3-core/interfaces/IUniswapV3Factory.sol"; import {IUniswapV3Pool} from "univ3-core/interfaces/IUniswapV3Pool.sol"; // Inherited implementations import {ERC1155} from "@tokens/ERC1155Minimal.sol"; import {Multicall} from "@base/Multicall.sol"; import {TransientReentrancyGuard} from "solmate/utils/TransientReentrancyGuard.sol"; // Libraries import {CallbackLib} from "@libraries/CallbackLib.sol"; import {Constants} from "@libraries/Constants.sol"; import {Errors} from "@libraries/Errors.sol"; import {FeesCalc} from "@libraries/FeesCalc.sol"; import {Math} from "@libraries/Math.sol"; import {PanopticMath} from "@libraries/PanopticMath.sol"; import {SafeTransferLib} from "@libraries/SafeTransferLib.sol"; // Custom types import {LeftRightUnsigned, LeftRightSigned, LeftRightLibrary} from "@types/LeftRight.sol"; import {LiquidityChunk} from "@types/LiquidityChunk.sol"; import {TokenId} from "@types/TokenId.sol"; // .......... // ,. .,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,. ,, // ,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,, // .,,,,,,,,,,. ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,, // .,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,. ,,,,,,,,,,,,,,, // ,,,,,,,,,,,,,,. ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,. ,,,,,,,,,,,,,,, // ,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,, // ,,,,,,,,,,,,,. ,,,,,,,,,,,,,,,,,, .,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,, // ,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,. ,,,,,,,,,,,,,,,,, .,,,,,,,,,,,,, // ,,,,,,,,,,,,,. .,,,,,,,,,,,,,,,. ,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,. // ,,,,,,,,,,,,, ,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,, ,,,,,,,,,,,,, // ,,,,,,,,,,,,, ,,,,,,,,,,,,,,. ,,,,,,,,,,,,,, ,,,,,,,,,,,,, // ,,,,,,,,,,,,, ,,,,,,,,,,,,,, ,,,,,,,,,,,,,, ,,,,,,,,,,,,, // ,,,,,,,,,,,,, ,,,,,,,,,,,,, ,,,,,,,,,,,,,, ,,,,,,,,,,,,. // .,,,,,,,,,,,, .,,,,,,,,,,,,, ,,,,,,,,,,,,, ,,,,,,,,,,,, // ,,,,,,,,,,,, ,,,,,,,,,,,, ,,,,,,,,,,,,, .,,,,,,,,,,,, // ,,,,,,,,,,,, ,,,,,,,,,,,, ,,,,,,,,,,,,. ,,,,,,,,,,,, // ,,,,,,,,,,,, ,,,,,,,,,,,,. █████████ ███████████ ███████████ ██████ ██████ ,,,,,,,,,,,, ,,,,,,,,,,,, // .,,,,,,,,,,,, ,,,,,,,,,,,, ███░░░░░███░░███░░░░░░█░░███░░░░░███░░██████ ██████ .,,,,,,,,,,,, ,,,,,,,,,,,, // ,,,,,,,,,,,, ,,,,,,,,,,,, ░███ ░░░ ░███ █ ░ ░███ ░███ ░███░█████░███ ,,,,,,,,,,,, ,,,,,,,,,,,,. // ,,,,,,,,,,,, ,,,,,,,,,,,, ░░█████████ ░███████ ░██████████ ░███░░███ ░███ .,,,,,,,,,,, ,,,,,,,,,,,. // ,,,,,,,,,,,, ,,,,,,,,,,,, ░░░░░░░░███ ░███░░░█ ░███░░░░░░ ░███ ░░░ ░███ ,,,,,,,,,,,. ,,,,,,,,,,,, // ,,,,,,,,,,,, ,,,,,,,,,,,, ███ ░███ ░███ ░ ░███ ░███ ░███ ,,,,,,,,,,,, ,,,,,,,,,,,, // ,,,,,,,,,,,, ,,,,,,,,,,,, ░░█████████ █████ █████ █████ █████ ,,,,,,,,,,, ,,,,,,,,,,,, // ,,,,,,,,,,,, ,,,,,,,,,,,, ░░░░░░░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░ ,,,,,,,,,,,, ,,,,,,,,,,,. // ,,,,,,,,,,,, .,,,,,,,,,,,. ,,,,,,,,,,,, ,,,,,,,,,,,, // .,,,,,,,,,,,, ,,,,,,,,,,,, .,,,,,,,,,,,, ,,,,,,,,,,,, // ,,,,,,,,,,,, ,,,,,,,,,,,,, ,,,,,,,,,,,, ,,,,,,,,,,,, // ,,,,,,,,,,,,. ,,,,,,,,,,,,. ,,,,,,,,,,,,. ,,,,,,,,,,,, // ,,,,,,,,,,,, ,,,,,,,,,,,,, ,,,,,,,,,,,,, .,,,,,,,,,,,, // ,,,,,,,,,,,, ,,,,,,,,,,,,, ,,,,,,,,,,,,, .,,,,,,,,,,,, // .,,,,,,,,,,,, ,,,,,,,,,,,,, ,,,,,,,,,,,,,. ,,,,,,,,,,,, // ,,,,,,,,,,,,, ,,,,,,,,,,,,,, .,,,,,,,,,,,,,. ,,,,,,,,,,,, // ,,,,,,,,,,,,, .,,,,,,,,,,,,,, .,,,,,,,,,,,,,, .,,,,,,,,,,,, // ,,,,,,,,,,,,, ,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,. ,,,,,,,,,,,,,. // ,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,, .,,,,,,,,,,,,,,,, ,,,,,,,,,,,,, // .,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,, .,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,, // ,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,. ,,,,,,,,,,,,,,,,,,,. ,,,,,,,,,,,,,, // ,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,, .,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,, // ,,,,,,,,,,,,,,, .,,,,,,,,,,,,,,,,,,,,,,,,,,,,,. ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, .,,,,,,,,,,,,,,. // ,,,,,,,,,,,,,,. ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,, // ,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, .,,,,,,,,,, // ,,,,,. ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,, // , ..,,,,,,,,,,,,,,,,,,,,,,,,,,,,. /// @author Axicon Labs Limited /// @title Semi-Fungible Position Manager (ERC1155) - a gas-efficient Uniswap V3 position manager. /// @notice Wraps Uniswap V3 positions with up to 4 legs behind an ERC1155 token. /// @dev Replaces the NonfungiblePositionManager.sol (ERC721) from Uniswap Labs. contract SemiFungiblePositionManager is ERC1155, Multicall, TransientReentrancyGuard { /*////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////*/ /// @notice Emitted when a UniswapV3Pool is initialized in the SFPM. /// @param uniswapPool Address of the underlying Uniswap V3 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 initial minimum enforced tick for the pool /// @param maxEnforcedTick The initial maximum enforced tick for the pool event PoolInitialized( address indexed uniswapPool, uint64 poolId, int24 minEnforcedTick, int24 maxEnforcedTick ); /// @notice Emitted when the enforced tick range is expanded for a given `poolId`. /// @dev Will be emitted on any `expandEnforcedTickRange` call, even if the enforced ticks are not actually changed. /// @param uniswapPool Address of the underlying Uniswap V3 pool /// @param minEnforcedTick The new minimum enforced tick for the pool /// @param maxEnforcedTick The new maximum enforced tick for the pool event EnforcedTicksUpdated( address indexed uniswapPool, 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 a given Uniswap `pool`. /// @param pool A canonical Uniswap V3 pool initialized in the SFPM /// @param minEnforcedTick The current minimum enforced tick for the pool in the SFPM /// @param maxEnforcedTick The current maximum enforced tick for the pool in the SFPM struct PoolData { IUniswapV3Pool pool; int24 minEnforcedTick; int24 maxEnforcedTick; } /*////////////////////////////////////////////////////////////// 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 Canonical Uniswap V3 Factory address. /// @dev Used to verify callbacks and initialize pools. IUniswapV3Factory internal immutable FACTORY; /// @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 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 corresponding poolId for a given Uniswap V3 pool address. /// @dev pool address => pool id + 2 ** 255 (initialization bit for `poolId == 0`, set if the pool exists) mapping(address univ3pool => uint256 poolIdData) internal s_AddrToPoolIdData; /// @notice Retrieve the PoolData struct corresponding to a given poolId. mapping(uint64 poolId => PoolData poolData) internal s_poolIdToPoolData; /* 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 UniswapV3Pool │ │ │ mints │ │ │ ┌────┐-(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; /// @notice Per-liquidity accumulator for the fees collected on an account for a given chunk. /// @dev Base fees are stored as `int128((feeGrowthInsideLastX128 * liquidity) / 2**128)`, which allows us to store the accumulated fees as int128 instead of uint256. /// @dev Right slot: int128 token0 base fees, Left slot: int128 token1 base fees. /// @dev feesBase represents the baseline fees collected by the position last time it was updated - this is recalculated every time the position is collected from with the new value. mapping(bytes32 positionKey => LeftRightSigned baseFees0And1) internal s_accountFeesBase; /*////////////////////////////////////////////////////////////// INITIALIZATION //////////////////////////////////////////////////////////////*/ /// @notice Set the canonical Uniswap V3 Factory address. /// @param _factory The canonical Uniswap V3 Factory address /// @param _minEnforcedTickFillCost The minimum amount of tokens it should require to fill `maxLiquidityPerTick` at the minimum and maximum enforced ticks /// @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( IUniswapV3Factory _factory, uint256 _minEnforcedTickFillCost, uint256 _supplyMultiplierTickFill ) { FACTORY = _factory; MIN_ENFORCED_TICKFILL_COST = _minEnforcedTickFillCost; SUPPLY_MULTIPLIER_TICKFILL = _supplyMultiplierTickFill; } /// @notice Initialize a Uniswap V3 pool in the SFPM. /// @dev Revert if already initialized. /// @param token0 The contract address of token0 of the pool /// @param token1 The contract address of token1 of the pool /// @param fee The fee level of the of the underlying Uniswap V3 pool, denominated in hundredths of bips function initializeAMMPool(address token0, address token1, uint24 fee) external { // sort the tokens, if necessary: (token0, token1) = token0 < token1 ? (token0, token1) : (token1, token0); // compute the address of the Uniswap V3 pool for the given token0, token1, and fee tier address univ3pool = FACTORY.getPool(token0, token1, fee); // reverts if the Uniswap V3 pool has not been initialized if (univ3pool == address(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 poolId == 0, we have a bit on the left set if it was initialized, so this will still return properly if (s_AddrToPoolIdData[univ3pool] != 0) return; int24 tickSpacing = IUniswapV3Pool(univ3pool).tickSpacing(); // The base poolId is composed as follows: // [tickSpacing][pool pattern] // [16 bit tickSpacing][most significant 48 bits of the pool address] uint64 poolId = PanopticMath.getPoolId(univ3pool, 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 (address(s_poolIdToPoolData[poolId].pool) != address(0)) { poolId = PanopticMath.incrementPoolPattern(poolId); } uint256 maxLiquidityPerTick = Math.getMaxLiquidityPerTick(tickSpacing); int24 minEnforcedTick; int24 maxEnforcedTick; unchecked { minEnforcedTick = int24( -Math.getApproxTickWithMaxAmount( Math.max( MIN_ENFORCED_TICKFILL_COST, (IERC20Partial(token1).totalSupply() * SUPPLY_MULTIPLIER_TICKFILL) / 10_000 ), tickSpacing, maxLiquidityPerTick ) ); maxEnforcedTick = int24( Math.getApproxTickWithMaxAmount( Math.max( MIN_ENFORCED_TICKFILL_COST, (IERC20Partial(token0).totalSupply() * SUPPLY_MULTIPLIER_TICKFILL) / 10_000 ), tickSpacing, maxLiquidityPerTick ) ); } s_poolIdToPoolData[poolId] = PoolData( IUniswapV3Pool(univ3pool), minEnforcedTick, maxEnforcedTick ); // add a bit on the end to indicate that the pool is initialized // (this is for the case that poolId == 0, so we can make a distinction between zero and uninitialized) unchecked { s_AddrToPoolIdData[univ3pool] = uint256(poolId) + 2 ** 255; } emit PoolInitialized(univ3pool, poolId, minEnforcedTick, maxEnforcedTick); } /// @notice Recomputes and decreases `minEnforcedTick` and/or increases `maxEnforcedTick` for a given `poolId` 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 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 poolId The poolId on which to expand the enforced tick range function expandEnforcedTickRange(uint64 poolId) external { PoolData memory dataOld = s_poolIdToPoolData[poolId]; // tick spacing is stored in the highest 16 bits of the poolId int24 tickSpacing = int24(uint24(poolId >> 48)); uint128 maxLiquidityPerTick = Math.getMaxLiquidityPerTick(tickSpacing); int24 minEnforcedTick; int24 maxEnforcedTick; unchecked { minEnforcedTick = int24( Math.min( dataOld.minEnforcedTick, -Math.getApproxTickWithMaxAmount( Math.max( MIN_ENFORCED_TICKFILL_COST, (IERC20Partial(dataOld.pool.token1()).totalSupply() * SUPPLY_MULTIPLIER_TICKFILL) / 10_000 ), tickSpacing, maxLiquidityPerTick ) ) ); maxEnforcedTick = int24( Math.max( dataOld.maxEnforcedTick, Math.getApproxTickWithMaxAmount( Math.max( MIN_ENFORCED_TICKFILL_COST, (IERC20Partial(dataOld.pool.token0()).totalSupply() * SUPPLY_MULTIPLIER_TICKFILL) / 10_000 ), tickSpacing, maxLiquidityPerTick ) ) ); } s_poolIdToPoolData[poolId] = PoolData(dataOld.pool, minEnforcedTick, maxEnforcedTick); emit EnforcedTicksUpdated(address(dataOld.pool), minEnforcedTick, maxEnforcedTick); } /*////////////////////////////////////////////////////////////// CALLBACK HANDLERS //////////////////////////////////////////////////////////////*/ /// @notice Called after minting liquidity to a position. /// @dev Pays the pool tokens owed for the minted liquidity from the payer (always the caller). /// @param amount0Owed The amount of token0 due to the pool for the minted liquidity /// @param amount1Owed The amount of token1 due to the pool for the minted liquidity /// @param data Contains the payer address and the pool features required to validate the callback function uniswapV3MintCallback( uint256 amount0Owed, uint256 amount1Owed, bytes calldata data ) external { // Decode the mint callback data CallbackLib.CallbackData memory decoded = abi.decode(data, (CallbackLib.CallbackData)); // Validate caller to ensure we got called from the AMM pool CallbackLib.validateCallback(msg.sender, FACTORY, decoded.poolFeatures); // Sends the amount0Owed and amount1Owed quantities provided if (amount0Owed > 0) SafeTransferLib.safeTransferFrom( decoded.poolFeatures.token0, decoded.payer, msg.sender, amount0Owed ); if (amount1Owed > 0) SafeTransferLib.safeTransferFrom( decoded.poolFeatures.token1, decoded.payer, msg.sender, amount1Owed ); } /// @notice Called by the pool after executing a swap during an ITM option mint/burn. /// @dev Pays the pool tokens owed for the swap from the payer (always the caller). /// @param amount0Delta The amount of token0 that was sent (negative) or must be received (positive) by the pool by /// the end of the swap. If positive, the callback must send that amount of token0 to the pool /// @param amount1Delta The amount of token1 that was sent (negative) or must be received (positive) by the pool by /// the end of the swap. If positive, the callback must send that amount of token1 to the pool /// @param data Contains the payer address and the pool features required to validate the callback function uniswapV3SwapCallback( int256 amount0Delta, int256 amount1Delta, bytes calldata data ) external { // Decode the swap callback data, checks that the UniswapV3Pool has the correct address. CallbackLib.CallbackData memory decoded = abi.decode(data, (CallbackLib.CallbackData)); // Validate caller to ensure we got called from the AMM pool CallbackLib.validateCallback(msg.sender, FACTORY, decoded.poolFeatures); // Extract the address of the token to be sent (amount0 -> token0, amount1 -> token1) address token = amount0Delta > 0 ? address(decoded.poolFeatures.token0) : address(decoded.poolFeatures.token1); // Transform the amount to pay to uint256 (take positive one from amount0 and amount1) // the pool will always pass one delta with a positive sign and one with a negative sign or zero, // so this logic always picks the correct delta to pay uint256 amountToPay = amount0Delta > 0 ? uint256(amount0Delta) : uint256(amount1Delta); // Pay the required token from the payer to the caller of this contract SafeTransferLib.safeTransferFrom(token, decoded.payer, msg.sender, amountToPay); } /*////////////////////////////////////////////////////////////// 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 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 token0 and token1 collected as fees for each leg /// @return The net amount of token0 and token1 moved to/from the Uniswap V3 pool function burnTokenizedPosition( 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 _createPositionInAMM( slippageTickLimitLow, slippageTickLimitHigh, positionSize, tokenId.flipToBurnToken(), BURN ); } /// @notice Create a new position `tokenId` containing up to 4 legs. /// @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 token0 and token1 collected as fees for each leg /// @return The net amount of token0 and token1 moved to/from the Uniswap V3 pool function mintTokenizedPosition( 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 _createPositionInAMM( slippageTickLimitLow, slippageTickLimitHigh, positionSize, tokenId, MINT ); } /*////////////////////////////////////////////////////////////// TRANSFER HOOK IMPLEMENTATIONS //////////////////////////////////////////////////////////////*/ /// @notice Transfer a single token 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 id The ERC1155 token id to transfer /// @param amount The amount of tokens to transfer /// @param data Optional data to include in the receive hook function safeTransferFrom( address from, address to, uint256 id, uint256 amount, bytes calldata data ) public override nonReentrant { registerTokenTransfer(from, to, TokenId.wrap(id), amount); super.safeTransferFrom(from, to, id, amount, data); } /// @notice Transfer multiple tokens from one user to another. /// @dev Supports token approvals. /// @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 receive hook function safeBatchTransferFrom( address from, address to, uint256[] calldata ids, uint256[] calldata amounts, bytes calldata data ) public override nonReentrant { for (uint256 i = 0; i < ids.length; ) { registerTokenTransfer(from, to, TokenId.wrap(ids[i]), amounts[i]); unchecked { ++i; } } super.safeBatchTransferFrom(from, to, ids, amounts, data); } /// @notice Update user position data following a token transfer. /// @dev All liquidity for `from` in the chunk for each leg of `id` must be transferred. /// @dev `from` must not have long liquidity in any of the chunks being transferred. /// @dev `to` must not have (long or short) liquidity in any of the chunks being transferred. /// @param from The address of the sender /// @param to The address of the recipient /// @param id The tokenId being transferred /// @param amount The amount of the token being transferred function registerTokenTransfer(address from, address to, TokenId id, uint256 amount) internal { IUniswapV3Pool univ3pool = s_poolIdToPoolData[id.poolId()].pool; uint256 numLegs = id.countLegs(); for (uint256 leg = 0; leg < numLegs; ) { LiquidityChunk liquidityChunk = PanopticMath.getLiquidityChunk( id, leg, uint128(amount) ); bytes32 positionKey_from = keccak256( abi.encodePacked( address(univ3pool), from, id.tokenType(leg), liquidityChunk.tickLower(), liquidityChunk.tickUpper() ) ); bytes32 positionKey_to = keccak256( abi.encodePacked( address(univ3pool), to, id.tokenType(leg), liquidityChunk.tickLower(), liquidityChunk.tickUpper() ) ); // Revert if recipient already has liquidity in `liquidityChunk` // Revert if sender has long liquidity in `liquidityChunk` or they are attempting to transfer less than their `netLiquidity` LeftRightUnsigned fromLiq = s_accountLiquidity[positionKey_from]; if ( LeftRightUnsigned.unwrap(s_accountLiquidity[positionKey_to]) != 0 || LeftRightUnsigned.unwrap(fromLiq) != liquidityChunk.liquidity() ) revert Errors.TransferFailed(); s_accountLiquidity[positionKey_to] = fromLiq; s_accountLiquidity[positionKey_from] = LeftRightUnsigned.wrap(0); s_accountFeesBase[positionKey_to] = s_accountFeesBase[positionKey_from]; s_accountFeesBase[positionKey_from] = LeftRightSigned.wrap(0); unchecked { ++leg; } } } /*////////////////////////////////////////////////////////////// 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% token0 or 100% token1: 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 token0 as an example, we deploy it to the AMM pool and *then* swap to get the right mix of token0 and token1 // 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 univ3pool The Uniswap pool in which to swap. /// @param itmAmounts How much to swap (i.e. how many tokens are ITM) /// @return totalSwapped The token deltas swapped in the AMM function swapInAMM( IUniswapV3Pool univ3pool, LeftRightSigned itmAmounts ) internal returns (LeftRightSigned totalSwapped) { bool zeroForOne; // The direction of the swap, true for token0 to token1, false for token1 to token0 int256 swapAmount; // The amount of token0 or token1 to swap bytes memory data; IUniswapV3Pool _univ3pool = univ3pool; unchecked { // unpack the in-the-money amounts int128 itm0 = itmAmounts.rightSlot(); int128 itm1 = itmAmounts.leftSlot(); // construct the swap callback struct data = abi.encode( CallbackLib.CallbackData({ poolFeatures: CallbackLib.PoolFeatures({ token0: _univ3pool.token0(), token1: _univ3pool.token1(), fee: _univ3pool.fee() }), payer: msg.sender }) ); // 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)) { (uint160 sqrtPriceX96, , , , , , ) = _univ3pool.slot0(); // 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 token0 that should be resolved in the swap at the current tick // we do this by flipping the signs on the token1 ITM amount converting+deducting it against the token0 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 token0 // + = Net shortage of token0 int256 net0 = itm0 - PanopticMath.convert1to0(itm1, sqrtPriceX96); 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); // swap tokens in the Uniswap pool // NOTE: this triggers our swap callback function (int256 swap0, int256 swap1) = _univ3pool.swap( msg.sender, zeroForOne, swapAmount, zeroForOne ? Constants.MIN_V3POOL_SQRT_RATIO + 1 : Constants.MAX_V3POOL_SQRT_RATIO - 1, data ); // Add amounts swapped to totalSwapped variable totalSwapped = LeftRightSigned.wrap(0).toRightSlot(swap0.toInt128()).toLeftSlot( swap1.toInt128() ); } } /// @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 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 token0 and token1 collected as fees for each leg /// @return totalMoved The net amount of funds moved to/from Uniswap function _createPositionInAMM( int24 tickLimitLow, int24 tickLimitHigh, uint128 positionSize, TokenId tokenId, bool isBurn ) internal returns (LeftRightUnsigned[4] memory collectedByLeg, LeftRightSigned totalMoved) { // Extract univ3pool from the poolId map to Uniswap Pool PoolData memory poolData = s_poolIdToPoolData[tokenId.poolId()]; // Revert if the pool not been previously initialized if (poolData.pool == IUniswapV3Pool(address(0))) revert Errors.UniswapPoolNotInitialized(); // upper bound on amount of tokens contained across all legs of the position at any given tick uint256 amount0; uint256 amount1; LeftRightSigned itmAmounts; uint256 numLegs = tokenId.countLegs(); for (uint256 leg = 0; leg < numLegs; ) { LiquidityChunk 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(); // Revert if the upper/lower ticks are not multiples of tickSpacing // This is an invalid state, and would revert silently later in `univ3Pool.mint` // Revert if the tick range extends from the strike outside of the enforced tick range if ( tickLower % tickSpacing != 0 || tickUpper % tickSpacing != 0 || tickLower < poolData.minEnforcedTick || tickUpper > poolData.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; (movedLeg, collectedByLeg[leg]) = _createLegInAMM( poolData.pool, tokenId, leg, liquidityChunk, isBurn ); totalMoved = totalMoved.add(movedLeg); // if tokenType is 1, and we transacted some token0: then this leg is ITM // if tokenType is 0, and we transacted some token1: 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 = swapInAMM(poolData.pool, itmAmounts).add(totalMoved); } (tickLimitLow, tickLimitHigh) = (tickLimitHigh, tickLimitLow); } // Get the current tick of the Uniswap pool, check slippage (, int24 currentTick, , , , , ) = poolData.pool.slot0(); 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 univ3pool The Uniswap pool /// @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 token0 and token1 collected as fees function _createLegInAMM( IUniswapV3Pool univ3pool, 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( address(univ3pool), msg.sender, 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 msg.sender *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 msg.sender) // so we seek to move the incoming liquidity chunk *out* of uniswap - but was there sufficient liquidity sitting in uniswap // in the first place? if (startingLiquidity < chunkLiquidity) { // the amount we want to move (liquidityChunk.legLiquidity()) out of uniswap is greater than // what the account that owns the liquidity in uniswap has (startingLiquidity) // we must ensure that an account can only move its own liquidity out of uniswap // so we revert in this case revert Errors.NotEnoughLiquidity(); } else { // 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 V3 pool: Selling(isLong=0): Mint chunk of liquidity in Uniswap (defined by upper tick, lower tick, and amount) ┌─────────────────────────────────┐ ▲ ┌▼┐ liquidityChunk │ │ ┌──┴─┴──┐ ┌───┴──┐ │ │ │ │ │ └──┴───────┴──► └──────┘ Uniswap V3 msg.sender else: the position is long (buying a put or a call), then _burnLiquidity to remove liquidity from Uniswap V3 Buying(isLong=1): Burn in Uniswap ┌─────────────────┐ ▲ ┌┼┐ │ │ ┌──┴─┴──┐ ┌───▼──┐ │ │ │ │ │ └──┴───────┴──► └──────┘ Uniswap V3 msg.sender */ moved = isLong == 0 ? _mintLiquidity(liquidityChunk, univ3pool) : _burnLiquidity(liquidityChunk, univ3pool); // if there was liquidity at that tick before the transaction, collect any accumulated fees if (currentLiquidity.rightSlot() > 0) { collectedSingleLeg = _collectAndWritePositionData( liquidityChunk, univ3pool, currentLiquidity, positionKey, moved, isLong ); } // position has been touched, update s_accountFeesBase with the latest values from the pool.positions // round up the stored feesbase to minimize Δfeesbase when we next calculate it s_accountFeesBase[positionKey] = _getFeesBase( univ3pool, updatedLiquidity, liquidityChunk, true ); } /// @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 (token0 and token1) 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 token0 (right slot) of the owed premium overflows, then stop accumulating both token0 owed premium and token0 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 an up-to-date feeGrowth value without a poke. /// @dev Stored fees base is rounded up and the current fees base is rounded down to minimize the amount of fees collected (Δfeesbase) in favor of the protocol. /// @param univ3pool The Uniswap pool /// @param liquidity The total amount of liquidity in the AMM for the specific position /// @param liquidityChunk The liquidity chunk in Uniswap to compute the feesBase for /// @param roundUp If true, round up the feesBase, otherwise round down function _getFeesBase( IUniswapV3Pool univ3pool, uint128 liquidity, LiquidityChunk liquidityChunk, bool roundUp ) private view returns (LeftRightSigned feesBase) { // read the latest feeGrowth directly from the Uniswap pool (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, , ) = univ3pool .positions( keccak256( abi.encodePacked( address(this), liquidityChunk.tickLower(), liquidityChunk.tickUpper() ) ) ); // (feegrowth * liquidity) / 2 ** 128 // here we're converting the value to an int128 even though all values (feeGrowth, liquidity, Q128) are strictly positive. // That's because of the way feeGrowthInside works in Uniswap V3, where it can underflow when stored for the first time. // This is not a problem in Uniswap V3 because the fees are always calculated by taking the difference of the feeGrowths, // so that the net different is always positive. // So by using int128 instead of uint128, we remove the need to handle extremely large underflows and simply allow it to be negative feesBase = roundUp ? LeftRightSigned .wrap(0) .toRightSlot( int128(int256(Math.mulDiv128RoundingUp(feeGrowthInside0LastX128, liquidity))) ) .toLeftSlot( int128(int256(Math.mulDiv128RoundingUp(feeGrowthInside1LastX128, liquidity))) ) : LeftRightSigned .wrap(0) .toRightSlot(int128(int256(Math.mulDiv128(feeGrowthInside0LastX128, liquidity)))) .toLeftSlot(int128(int256(Math.mulDiv128(feeGrowthInside1LastX128, liquidity)))); } /// @notice Mint a chunk of liquidity (`liquidityChunk`) in the Uniswap V3 pool; return the amount moved. /// @param liquidityChunk The liquidity chunk in Uniswap to mint /// @param univ3pool The Uniswap V3 pool to mint liquidity in/to /// @return movedAmounts How many tokens were moved from `msg.sender` to Uniswap function _mintLiquidity( LiquidityChunk liquidityChunk, IUniswapV3Pool univ3pool ) internal returns (LeftRightSigned movedAmounts) { // build callback data bytes memory mintdata = abi.encode( CallbackLib.CallbackData({ poolFeatures: CallbackLib.PoolFeatures({ token0: univ3pool.token0(), token1: univ3pool.token1(), fee: univ3pool.fee() }), payer: msg.sender }) ); // mint the required amount in the Uniswap pool // this triggers the uniswap mint callback function (uint256 amount0, uint256 amount1) = univ3pool.mint( address(this), liquidityChunk.tickLower(), liquidityChunk.tickUpper(), liquidityChunk.liquidity(), mintdata ); // amount0 The amount of token0 that was paid to mint the given amount of liquidity // amount1 The amount of token1 that was paid to mint the given amount of liquidity // no need to safecast to int from uint here as the max position size is int128 movedAmounts = LeftRightSigned.wrap(0).toRightSlot(int128(int256(amount0))).toLeftSlot( int128(int256(amount1)) ); } /// @notice Burn a chunk of liquidity (`liquidityChunk`) in the Uniswap V3 pool and send to msg.sender; return the amount moved. /// @param liquidityChunk The liquidity chunk in Uniswap to burn /// @param univ3pool The Uniswap V3 pool to burn liquidity in/from /// @return movedAmounts How many tokens were moved from Uniswap to `msg.sender` function _burnLiquidity( LiquidityChunk liquidityChunk, IUniswapV3Pool univ3pool ) internal returns (LeftRightSigned movedAmounts) { // burn that option's liquidity in the Uniswap Pool. // This will send the underlying tokens back to the Panoptic Pool (msg.sender) (uint256 amount0, uint256 amount1) = univ3pool.burn( liquidityChunk.tickLower(), liquidityChunk.tickUpper(), liquidityChunk.liquidity() ); // amount0 The amount of token0 that was sent back to the Panoptic Pool // amount1 The amount of token1 that was sent back to the Panoptic Pool // no need to safecast to int from uint here as the max position size is int128 // decrement the amountsOut with burnt amounts. amountsOut = notional value of tokens moved unchecked { movedAmounts = LeftRightSigned.wrap(0).toRightSlot(-int128(int256(amount0))).toLeftSlot( -int128(int256(amount1)) ); } } /// @notice Helper to collect amounts between msg.sender and Uniswap and also to update the Uniswap fees collected to date from the AMM. /// @param liquidityChunk The liquidity chunk in Uniswap to collect from /// @param univ3pool The Uniswap pool where the position is deployed /// @param currentLiquidity The existing liquidity msg.sender owns in the AMM for this chunk before the SFPM was called /// @param positionKey The unique key to identify the liquidity chunk/tokenType pairing in this Uniswap pool /// @param movedInLeg How many tokens have been moved between msg.sender and Uniswap before this function call /// @param isLong Whether the leg in question is long (=1) or short (=0) /// @return collectedChunk The amount of tokens collected from Uniswap function _collectAndWritePositionData( LiquidityChunk liquidityChunk, IUniswapV3Pool univ3pool, LeftRightUnsigned currentLiquidity, bytes32 positionKey, LeftRightSigned movedInLeg, uint256 isLong ) internal returns (LeftRightUnsigned collectedChunk) { uint128 startingLiquidity = currentLiquidity.rightSlot(); // round down current fees base to minimize Δfeesbase // If the current feesBase is close or identical to the stored one, the amountToCollect can be negative. // This is because the stored feesBase is rounded up, and the current feesBase is rounded down. // When this is the case, we want to behave as if there are 0 fees, so we just rectify the values. LeftRightUnsigned amountToCollect = _getFeesBase( univ3pool, startingLiquidity, liquidityChunk, false ).subRect(s_accountFeesBase[positionKey]); if (isLong == 1) { // movedInLeg deltas are always represented as negative during liquidity burns (for long positions), so the result here will always be positive amountToCollect = LeftRightUnsigned.wrap( uint256( LeftRightSigned.unwrap( LeftRightSigned.wrap(int256(LeftRightUnsigned.unwrap(amountToCollect))).sub( movedInLeg ) ) ) ); } if (LeftRightUnsigned.unwrap(amountToCollect) != 0) { // Collects tokens owed to a liquidity chunk (uint128 receivedAmount0, uint128 receivedAmount1) = univ3pool.collect( msg.sender, liquidityChunk.tickLower(), liquidityChunk.tickUpper(), amountToCollect.rightSlot(), amountToCollect.leftSlot() ); // moved will be negative if the leg was long (funds left the caller, don't count it in collected fees) uint128 collected0; uint128 collected1; unchecked { collected0 = movedInLeg.rightSlot() < 0 ? receivedAmount0 - uint128(-movedInLeg.rightSlot()) : receivedAmount0; collected1 = movedInLeg.leftSlot() < 0 ? receivedAmount1 - uint128(-movedInLeg.leftSlot()) : receivedAmount1; } // CollectedOut is the amount of fees accumulated+collected (received - burnt) // That's because receivedAmount contains the burnt tokens and whatever amount of fees collected collectedChunk = LeftRightUnsigned.wrap(collected0).toLeftSlot(collected1); // record the collected amounts in the s_accountPremiumOwed and s_accountPremiumGross accumulators _updateStoredPremia(positionKey, currentLiquidity, collectedChunk); } } /// @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 (token0 and token1) collected from Uniswap /// @return deltaPremiumOwed The extra premium (per liquidity X64) to be added to the owed accumulator for token0 (right) and token1 (left) /// @return deltaPremiumGross The extra premium (per liquidity X64) to be added to the gross accumulator for token0 (right) and token1 (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 univ3pool The address of the Uniswap V3 Pool /// @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( address univ3pool, 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(univ3pool, 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 V3 pool has been touched, /// so no need to read the feeGrowths from the Uniswap V3 pool. /// @param univ3pool The address of the Uniswap V3 Pool /// @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 token0 = `sum(feeGrowthLast0X128)` over every block where the position has been touched /// @return The amount of premium (per liquidity X64) for token1 = `sum(feeGrowthLast0X128)` over every block where the position has been touched function getAccountPremium( address univ3pool, address owner, uint256 tokenType, int24 tickLower, int24 tickUpper, int24 atTick, uint256 isLong ) external view returns (uint128, uint128) { bytes32 positionKey = keccak256( abi.encodePacked(univ3pool, 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; { IUniswapV3Pool _univ3pool = IUniswapV3Pool(univ3pool); int24 _tickLower = tickLower; int24 _tickUpper = tickUpper; // how much fees have been accumulated within the liquidity chunk since last time we updated this chunk? // Compute (currentFeesGrowth - oldFeesGrowth), the amount to collect // currentFeesGrowth (calculated from FeesCalc.calculateAMMSwapFeesLiquidityChunk) is (ammFeesCollectedPerLiquidity * liquidityChunk.liquidity()) // oldFeesGrowth is the last stored update of fee growth within the position range in the past (feeGrowthRange*liquidityChunk.liquidity()) (s_accountFeesBase[positionKey]) LeftRightSigned feesBase = FeesCalc.calculateAMMSwapFees( _univ3pool, atTick, _tickLower, _tickUpper, netLiquidity ); // If the current feesBase is close or identical to the stored one, the amountToCollect can be negative. // This is because the stored feesBase is rounded up, and the current feesBase is rounded down. // When this is the case, we want to behave as if there are 0 fees, so we just rectify the values. // Guaranteed to be positive, so swap to unsigned type amountToCollect = feesBase.subRect(s_accountFeesBase[positionKey]); } (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 token0 (right slot) of the owed premium overflows, then stop accumulating both token0 owed premium and token0 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 Return the feesBase associated with a given liquidity chunk. /// @param univ3pool The address of the Uniswap V3 Pool /// @param owner The address of the account that is queried /// @param tokenType The tokenType of the position (the token it started as) /// @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 feesBase0 The feesBase of the position for token0 /// @return feesBase1 The feesBase of the position for token1 function getAccountFeesBase( address univ3pool, address owner, uint256 tokenType, int24 tickLower, int24 tickUpper ) external view returns (int128 feesBase0, int128 feesBase1) { // Get accumulated fees for token0 (rightSlot) and token1 (leftSlot) LeftRightSigned feesBase = s_accountFeesBase[ keccak256(abi.encodePacked(univ3pool, owner, tokenType, tickLower, tickUpper)) ]; feesBase0 = feesBase.rightSlot(); feesBase1 = feesBase.leftSlot(); } /// @notice Returns the Uniswap pool for a given `poolId`. /// @param poolId The unique pool identifier for a Uniswap V3 pool /// @return uniswapV3Pool The Uniswap pool corresponding to `poolId` function getUniswapV3PoolFromId( uint64 poolId ) external view returns (IUniswapV3Pool uniswapV3Pool) { return s_poolIdToPoolData[poolId].pool; } /// @notice Returns the current enforced tick limits for a given `poolId`. /// @param poolId The unique pool identifier for a Uniswap V3 pool /// @return The minimum enforced tick for chunks created in the pool corresponding to `poolId` /// @return The maximum enforced tick for chunks created in the pool corresponding to `poolId` function getEnforcedTickLimits(uint64 poolId) external view returns (int24, int24) { PoolData memory poolData = s_poolIdToPoolData[poolId]; return (poolData.minEnforcedTick, poolData.maxEnforcedTick); } /// @notice Returns the `poolId` for a given Uniswap pool. /// @param univ3pool The address of the Uniswap Pool /// @return poolId The unique pool identifier corresponding to `univ3pool` function getPoolId(address univ3pool) external view returns (uint64 poolId) { poolId = uint64(s_AddrToPoolIdData[univ3pool]); } }
// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity >=0.5.0; import {IUniswapV3PoolImmutables} from './pool/IUniswapV3PoolImmutables.sol'; import {IUniswapV3PoolState} from './pool/IUniswapV3PoolState.sol'; import {IUniswapV3PoolDerivedState} from './pool/IUniswapV3PoolDerivedState.sol'; import {IUniswapV3PoolActions} from './pool/IUniswapV3PoolActions.sol'; import {IUniswapV3PoolOwnerActions} from './pool/IUniswapV3PoolOwnerActions.sol'; import {IUniswapV3PoolErrors} from './pool/IUniswapV3PoolErrors.sol'; import {IUniswapV3PoolEvents} from './pool/IUniswapV3PoolEvents.sol'; /// @title The interface for a Uniswap V3 Pool /// @notice A Uniswap pool facilitates swapping and automated market making between any two assets that strictly conform /// to the ERC20 specification /// @dev The pool interface is broken up into many smaller pieces interface IUniswapV3Pool is IUniswapV3PoolImmutables, IUniswapV3PoolState, IUniswapV3PoolDerivedState, IUniswapV3PoolActions, IUniswapV3PoolOwnerActions, IUniswapV3PoolErrors, IUniswapV3PoolEvents { }
// SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.5.0) (token/ERC1155/utils/ERC1155Holder.sol) pragma solidity ^0.8.0; import "./ERC1155Receiver.sol"; /** * Simple implementation of `ERC1155Receiver` 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. * * @dev _Available since v3.1._ */ contract ERC1155Holder is ERC1155Receiver { 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 // OpenZeppelin Contracts v4.4.1 (token/ERC20/extensions/IERC20Metadata.sol) pragma solidity ^0.8.0; import "../IERC20.sol"; /** * @dev Interface for the optional metadata functions from the ERC20 standard. * * _Available since v4.1._ */ 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: 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: 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 v4.8.0) (utils/Strings.sol) pragma solidity ^0.8.0; import "./math/Math.sol"; /** * @dev String operations. */ library Strings { bytes16 private constant _SYMBOLS = "0123456789abcdef"; uint8 private constant _ADDRESS_LENGTH = 20; /** * @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), _SYMBOLS)) } value /= 10; if (value == 0) break; } return buffer; } } /** * @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) { 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] = _SYMBOLS[value & 0xf]; value >>= 4; } require(value == 0, "Strings: hex length insufficient"); 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); } }
// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity >=0.5.0; /// @title The interface for the Uniswap V3 Factory /// @notice The Uniswap V3 Factory facilitates creation of Uniswap V3 pools and control over the protocol fees interface IUniswapV3Factory { /// @notice Emitted when the owner of the factory is changed /// @param oldOwner The owner before the owner was changed /// @param newOwner The owner after the owner was changed event OwnerChanged(address indexed oldOwner, address indexed newOwner); /// @notice Emitted when a pool is created /// @param token0 The first token of the pool by address sort order /// @param token1 The second token 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 pool The address of the created pool event PoolCreated( address indexed token0, address indexed token1, uint24 indexed fee, int24 tickSpacing, address pool ); /// @notice Emitted when a new fee amount is enabled for pool creation via the factory /// @param fee The enabled fee, denominated in hundredths of a bip /// @param tickSpacing The minimum number of ticks between initialized ticks for pools created with the given fee event FeeAmountEnabled(uint24 indexed fee, int24 indexed tickSpacing); /// @notice Returns the current owner of the factory /// @dev Can be changed by the current owner via setOwner /// @return The address of the factory owner function owner() external view returns (address); /// @notice Returns the tick spacing for a given fee amount, if enabled, or 0 if not enabled /// @dev A fee amount can never be removed, so this value should be hard coded or cached in the calling context /// @param fee The enabled fee, denominated in hundredths of a bip. Returns 0 in case of unenabled fee /// @return The tick spacing function feeAmountTickSpacing(uint24 fee) external view returns (int24); /// @notice Returns the pool address for a given pair of tokens and a fee, or address 0 if it does not exist /// @dev tokenA and tokenB may be passed in either token0/token1 or token1/token0 order /// @param tokenA The contract address of either token0 or token1 /// @param tokenB The contract address of the other token /// @param fee The fee collected upon every swap in the pool, denominated in hundredths of a bip /// @return pool The pool address function getPool( address tokenA, address tokenB, uint24 fee ) external view returns (address pool); /// @notice Creates a pool for the given two tokens and fee /// @param tokenA One of the two tokens in the desired pool /// @param tokenB The other of the two tokens in the desired pool /// @param fee The desired fee for the pool /// @dev tokenA and tokenB may be passed in either order: token0/token1 or token1/token0. tickSpacing is retrieved /// from the fee. The call will revert if the pool already exists, the fee is invalid, or the token arguments /// are invalid. /// @return pool The address of the newly created pool function createPool( address tokenA, address tokenB, uint24 fee ) external returns (address pool); /// @notice Updates the owner of the factory /// @dev Must be called by the current owner /// @param _owner The new owner of the factory function setOwner(address _owner) external; /// @notice Enables a fee amount with the given tickSpacing /// @dev Fee amounts may never be removed once enabled /// @param fee The fee amount to enable, denominated in hundredths of a bip (i.e. 1e-6) /// @param tickSpacing The spacing between ticks to be enforced for all pools created with the given fee amount function enableFeeAmount(uint24 fee, int24 tickSpacing) external; }
// 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); } }
// 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: GPL-2.0-or-later pragma solidity ^0.8.24; // Interfaces import {IUniswapV3Factory} from "univ3-core/interfaces/IUniswapV3Factory.sol"; // Libraries import {Errors} from "@libraries/Errors.sol"; /// @title Library for verifying and decoding Uniswap callbacks. /// @author Axicon Labs Limited /// @notice This library provides functions to verify that a callback came from a canonical Uniswap V3 pool with a claimed set of features. library CallbackLib { /// @notice Type defining characteristics of a Uniswap V3 pool. /// @param token0 The address of `token0` for the Uniswap pool /// @param token1 The address of `token1` for the Uniswap pool /// @param fee The fee tier of the Uniswap pool (in hundredths of basis points) struct PoolFeatures { address token0; address token1; uint24 fee; } /// @notice Type for data sent by pool in mint/swap callbacks used to validate the pool and send back requisite tokens. /// @param poolFeatures The features of the pool that sent the callback (used to validate that the pool is canonical) /// @param payer The address from which the requested tokens should be transferred struct CallbackData { PoolFeatures poolFeatures; address payer; } /// @notice Verifies that a callback came from the canonical Uniswap pool with a claimed set of features. /// @param sender The address initiating the callback and claiming to be a Uniswap pool /// @param factory The address of the canonical Uniswap V3 factory /// @param features The features `sender` claims to contain (tokens and fee) function validateCallback( address sender, IUniswapV3Factory factory, PoolFeatures memory features ) internal view { // Call getPool on the factory to verify that the sender corresponds to the canonical pool with the claimed features if (factory.getPool(features.token0, features.token1, features.fee) != sender) revert Errors.InvalidUniswapCallback(); } }
// SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.24; // Interfaces import {IUniswapV3Pool} from "univ3-core/interfaces/IUniswapV3Pool.sol"; // Libraries import {Math} from "@libraries/Math.sol"; // Custom types import {LeftRightUnsigned, LeftRightSigned} from "@types/LeftRight.sol"; /// @title Library for Fee Calculations. /// @author Axicon Labs Limited /// @notice Compute fees accumulated within option position legs (a leg is a liquidity chunk). /// @dev Some options positions involve moving liquidity chunks to the AMM/Uniswap. Those chunks can then earn AMM swap fees. // // When price tick moves within // this liquidity chunk == an option leg within a `tokenId` option position: // Fees accumulate. // ◄────────────► // liquidity ┌───┼────────┐ // ▲ │ │ │ // │ │ : ◄──────Liquidity chunk // │ │ │ │ (an option position leg) // │ ┌─┴───┼────────┴─┐ // │ │ │ │ // │ │ : │ // │ │ │ │ // │ │ : │ // │ │ │ │ // └───┴─────┴──────────┴────► price // ▲ // │ // Current price tick // of the AMM // library FeesCalc { /// @notice Calculate the AMM swap fees accumulated by the `liquidityChunk` in each token of the pool. /// @dev Read from the Uniswap pool and compute the accumulated fees from swapping activity. /// @param univ3pool The AMM/Uniswap pool where fees are collected from /// @param currentTick The current price tick /// @param tickLower The lower tick of the chunk to calculate fees for /// @param tickUpper The upper tick of the chunk to calculate fees for /// @param liquidity The liquidity amount of the chunk to calculate fees for /// @return The fees collected from the AMM for each token (LeftRight-packed) with token0 in the right slot and token1 in the left slot function calculateAMMSwapFees( IUniswapV3Pool univ3pool, int24 currentTick, int24 tickLower, int24 tickUpper, uint128 liquidity ) public view returns (LeftRightSigned) { // extract the amount of AMM fees collected within the liquidity chunk // NOTE: the fee variables are *per unit of liquidity*; so more "rate" variables ( uint256 ammFeesPerLiqToken0X128, uint256 ammFeesPerLiqToken1X128 ) = _getAMMSwapFeesPerLiquidityCollected(univ3pool, currentTick, tickLower, tickUpper); // Use the fee growth (rate) variable to compute the absolute fees accumulated within the chunk: // ammFeesToken0X128 * liquidity / (2**128) // to store the (absolute) fees as int128: return LeftRightSigned .wrap(0) .toRightSlot(int128(int256(Math.mulDiv128(ammFeesPerLiqToken0X128, liquidity)))) .toLeftSlot(int128(int256(Math.mulDiv128(ammFeesPerLiqToken1X128, liquidity)))); } /// @notice Calculates the fee growth that has occurred (per unit of liquidity) in the AMM/Uniswap for an /// option position's tick range. /// @dev Extracts the feeGrowth from the Uniswap V3 pool. /// @param univ3pool The AMM pool where the leg is deployed /// @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 of token0 /// @return feeGrowthInside1X128 The fee growth in the AMM of token1 function _getAMMSwapFeesPerLiquidityCollected( IUniswapV3Pool univ3pool, 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 token0: 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 token1: 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, , , , ) = univ3pool.ticks(tickLower); (, , uint256 upperOut0, uint256 upperOut1, , , , ) = univ3pool.ticks(tickUpper); // compute the effective feeGrowth, depending on whether price is above/below/within range unchecked { if (currentTick < tickLower) { /** L = lowerTick, U = upperTick liquidity lowerOut (all fees collected in this price tick range) ▲ ◄──────────────^v───► (to MAX_TICK) │ │ upperOut │ ◄─────^v───► │ ┌────────┐ │ │ chunk │ │ │ │ └─────▲─────┴────────┴────────► price tick │ L U │ current tick */ feeGrowthInside0X128 = lowerOut0 - upperOut0; // fee growth inside the chunk feeGrowthInside1X128 = lowerOut1 - upperOut1; } else if (currentTick >= tickUpper) { /** liquidity ▲ upperOut │◄─^v─────────────────────► │ │ lowerOut ┌────────┐ │◄─^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 ▲ feeGrowthGlobalX128 = global fee growth │ = (all fees collected for the entire price range) │ │ │ lowerOut ┌──────────────┐ upperOut │◄─^v───────────►│ │◄─────^v───► │ │ chunk │ │ │ │ └────────────────┴───────▲──────┴─────► price tick L │ U │ current tick */ feeGrowthInside0X128 = univ3pool.feeGrowthGlobal0X128() - lowerOut0 - upperOut0; feeGrowthInside1X128 = univ3pool.feeGrowthGlobal1X128() - lowerOut1 - upperOut1; } } } }
// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity >=0.5.0; /// @title Pool state that never changes /// @notice These parameters are fixed for a pool forever, i.e., the methods will always return the same values interface IUniswapV3PoolImmutables { /// @notice The contract that deployed the pool, which must adhere to the IUniswapV3Factory interface /// @return The contract address function factory() external view returns (address); /// @notice The first of the two tokens of the pool, sorted by address /// @return The token contract address function token0() external view returns (address); /// @notice The second of the two tokens of the pool, sorted by address /// @return The token contract address function token1() external view returns (address); /// @notice The pool's fee in hundredths of a bip, i.e. 1e-6 /// @return The fee function fee() external view returns (uint24); /// @notice The pool tick spacing /// @dev Ticks can only be used at multiples of this value, minimum of 1 and always positive /// e.g.: a tickSpacing of 3 means ticks can be initialized every 3rd tick, i.e., ..., -6, -3, 0, 3, 6, ... /// This value is an int24 to avoid casting even though it is always positive. /// @return The tick spacing function tickSpacing() external view returns (int24); /// @notice The maximum amount of position liquidity that can use any tick in the range /// @dev This parameter is enforced per tick to prevent liquidity from overflowing a uint128 at any point, and /// also prevents out-of-range liquidity from being used to prevent adding in-range liquidity to a pool /// @return The max amount of liquidity per tick function maxLiquidityPerTick() external view returns (uint128); }
// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity >=0.5.0; /// @title Pool state that can change /// @notice These methods compose the pool's state, and can change with any frequency including multiple times /// per transaction interface IUniswapV3PoolState { /// @notice The 0th storage slot in the pool stores many values, and is exposed as a single method to save gas /// when accessed externally. /// @return sqrtPriceX96 The current price of the pool as a sqrt(token1/token0) Q64.96 value /// @return tick The current tick of the pool, 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 pool, /// @return observationCardinalityNext The next maximum number of observations, to be updated when the observation. /// @return feeProtocol The protocol fee for both tokens of the pool. /// Encoded as two 4 bit values, where the protocol fee of token1 is shifted 4 bits and the protocol fee of token0 /// is the lower 4 bits. Used as the denominator of a fraction of the swap fee, e.g. 4 means 1/4th of the swap fee. /// unlocked Whether the pool is currently locked to reentrancy function slot0() external view returns ( uint160 sqrtPriceX96, int24 tick, uint16 observationIndex, uint16 observationCardinality, uint16 observationCardinalityNext, uint8 feeProtocol, bool unlocked ); /// @notice The fee growth as a Q128.128 fees of token0 collected per unit of liquidity for the entire life of the pool /// @dev This value can overflow the uint256 function feeGrowthGlobal0X128() external view returns (uint256); /// @notice The fee growth as a Q128.128 fees of token1 collected per unit of liquidity for the entire life of the pool /// @dev This value can overflow the uint256 function feeGrowthGlobal1X128() external view returns (uint256); /// @notice The amounts of token0 and token1 that are owed to the protocol /// @dev Protocol fees will never exceed uint128 max in either token function protocolFees() external view returns (uint128 token0, uint128 token1); /// @notice The currently in range liquidity available to the pool /// @dev This value has no relationship to the total liquidity across all ticks /// @return The liquidity at the current price of the pool function liquidity() external view returns (uint128); /// @notice Look up information about a specific tick in the pool /// @param tick The tick to look up /// @return liquidityGross the total amount of position liquidity that uses the pool either as tick lower or /// tick upper /// @return liquidityNet how much liquidity changes when the pool price crosses the tick, /// @return feeGrowthOutside0X128 the fee growth on the other side of the tick from the current tick in token0, /// @return feeGrowthOutside1X128 the fee growth on the other side of the tick from the current tick in token1, /// @return tickCumulativeOutside the cumulative tick value on the other side of the tick from the current tick /// @return secondsPerLiquidityOutsideX128 the seconds spent per liquidity on the other side of the tick from the current tick, /// @return secondsOutside the seconds spent on the other side of the tick from the current tick, /// @return initialized Set to true if the tick is initialized, i.e. liquidityGross is greater than 0, otherwise equal to false. /// Outside values can only be used if the tick is initialized, i.e. if liquidityGross is greater than 0. /// In addition, these values are only relative and must be used only in comparison to previous snapshots for /// a specific position. function ticks(int24 tick) external view returns ( uint128 liquidityGross, int128 liquidityNet, uint256 feeGrowthOutside0X128, uint256 feeGrowthOutside1X128, int56 tickCumulativeOutside, uint160 secondsPerLiquidityOutsideX128, uint32 secondsOutside, bool initialized ); /// @notice Returns 256 packed tick initialized boolean values. See TickBitmap for more information function tickBitmap(int16 wordPosition) external view returns (uint256); /// @notice Returns the information about a position by the position's key /// @param key The position's key is a hash of a preimage composed by the owner, tickLower and tickUpper /// @return liquidity The amount of liquidity in the position, /// @return feeGrowthInside0LastX128 fee growth of token0 inside the tick range as of the last mint/burn/poke, /// @return feeGrowthInside1LastX128 fee growth of token1 inside the tick range as of the last mint/burn/poke, /// @return tokensOwed0 the computed amount of token0 owed to the position as of the last mint/burn/poke, /// @return tokensOwed1 the computed amount of token1 owed to the position as of the last mint/burn/poke function positions(bytes32 key) external view returns ( uint128 liquidity, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, uint128 tokensOwed0, uint128 tokensOwed1 ); /// @notice Returns data about a specific observation index /// @param index The element of the observations array to fetch /// @dev You most likely want to use #observe() instead of this method to get an observation as of some amount of time /// ago, rather than at a specific index in the array. /// @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 ); }
// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity >=0.5.0; /// @title Pool state that is not stored /// @notice Contains view functions to provide information about the pool that is computed rather than stored on the /// blockchain. The functions here may have variable gas costs. interface IUniswapV3PoolDerivedState { /// @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 token1 / token0. 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 Returns a snapshot of the tick cumulative, seconds per liquidity and seconds inside a tick range /// @dev Snapshots must only be compared to other snapshots, taken over a period for which a position existed. /// I.e., snapshots cannot be compared if a position is not held for the entire period between when the first /// snapshot is taken and the second snapshot is taken. /// @param tickLower The lower tick of the range /// @param tickUpper The upper tick of the range /// @return tickCumulativeInside The snapshot of the tick accumulator for the range /// @return secondsPerLiquidityInsideX128 The snapshot of seconds per liquidity for the range /// @return secondsInside The snapshot of seconds per liquidity for the range function snapshotCumulativesInside(int24 tickLower, int24 tickUpper) external view returns ( int56 tickCumulativeInside, uint160 secondsPerLiquidityInsideX128, uint32 secondsInside ); }
// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity >=0.5.0; /// @title Permissionless pool actions /// @notice Contains pool methods that can be called by anyone interface IUniswapV3PoolActions { /// @notice Sets the initial price for the pool /// @dev Price is represented as a sqrt(amountToken1/amountToken0) Q64.96 value /// @param sqrtPriceX96 the initial sqrt price of the pool as a Q64.96 function initialize(uint160 sqrtPriceX96) external; /// @notice Adds liquidity for the given recipient/tickLower/tickUpper position /// @dev The caller of this method receives a callback in the form of IUniswapV3MintCallback#uniswapV3MintCallback /// in which they must pay any token0 or token1 owed for the liquidity. The amount of token0/token1 due depends /// on tickLower, tickUpper, the amount of liquidity, and the current price. /// @param recipient The address for which the liquidity will be created /// @param tickLower The lower tick of the position in which to add liquidity /// @param tickUpper The upper tick of the position in which to add liquidity /// @param amount The amount of liquidity to mint /// @param data Any data that should be passed through to the callback /// @return amount0 The amount of token0 that was paid to mint the given amount of liquidity. Matches the value in the callback /// @return amount1 The amount of token1 that was paid to mint the given amount of liquidity. Matches the value in the callback function mint( address recipient, int24 tickLower, int24 tickUpper, uint128 amount, bytes calldata data ) external returns (uint256 amount0, uint256 amount1); /// @notice Collects tokens owed to a position /// @dev Does not recompute fees earned, which must be done either via mint or burn of any amount of liquidity. /// Collect must be called by the position owner. To withdraw only token0 or only token1, amount0Requested or /// amount1Requested may be set to zero. To withdraw all tokens owed, caller may pass any value greater than the /// actual tokens owed, e.g. type(uint128).max. Tokens owed may be from accumulated swap fees or burned liquidity. /// @param recipient The address which should receive the fees collected /// @param tickLower The lower tick of the position for which to collect fees /// @param tickUpper The upper tick of the position for which to collect fees /// @param amount0Requested How much token0 should be withdrawn from the fees owed /// @param amount1Requested How much token1 should be withdrawn from the fees owed /// @return amount0 The amount of fees collected in token0 /// @return amount1 The amount of fees collected in token1 function collect( address recipient, int24 tickLower, int24 tickUpper, uint128 amount0Requested, uint128 amount1Requested ) external returns (uint128 amount0, uint128 amount1); /// @notice Burn liquidity from the sender and account tokens owed for the liquidity to the position /// @dev Can be used to trigger a recalculation of fees owed to a position by calling with an amount of 0 /// @dev Fees must be collected separately via a call to #collect /// @param tickLower The lower tick of the position for which to burn liquidity /// @param tickUpper The upper tick of the position for which to burn liquidity /// @param amount How much liquidity to burn /// @return amount0 The amount of token0 sent to the recipient /// @return amount1 The amount of token1 sent to the recipient function burn( int24 tickLower, int24 tickUpper, uint128 amount ) external returns (uint256 amount0, uint256 amount1); /// @notice Swap token0 for token1, or token1 for token0 /// @dev The caller of this method receives a callback in the form of IUniswapV3SwapCallback#uniswapV3SwapCallback /// @param recipient The address to receive the output of the swap /// @param zeroForOne The direction of the swap, true for token0 to token1, false for token1 to token0 /// @param amountSpecified The amount of the swap, which implicitly configures the swap as exact input (positive), or exact output (negative) /// @param sqrtPriceLimitX96 The Q64.96 sqrt price limit. If zero for one, the price cannot be less than this /// value after the swap. If one for zero, the price cannot be greater than this value after the swap /// @param data Any data to be passed through to the callback /// @return amount0 The delta of the balance of token0 of the pool, exact when negative, minimum when positive /// @return amount1 The delta of the balance of token1 of the pool, exact when negative, minimum when positive function swap( address recipient, bool zeroForOne, int256 amountSpecified, uint160 sqrtPriceLimitX96, bytes calldata data ) external returns (int256 amount0, int256 amount1); /// @notice Receive token0 and/or token1 and pay it back, plus a fee, in the callback /// @dev The caller of this method receives a callback in the form of IUniswapV3FlashCallback#uniswapV3FlashCallback /// @dev Can be used to donate underlying tokens pro-rata to currently in-range liquidity providers by calling /// with 0 amount{0,1} and sending the donation amount(s) from the callback /// @param recipient The address which will receive the token0 and token1 amounts /// @param amount0 The amount of token0 to send /// @param amount1 The amount of token1 to send /// @param data Any data to be passed through to the callback function flash( address recipient, uint256 amount0, uint256 amount1, bytes calldata data ) external; /// @notice Increase the maximum number of price and liquidity observations that this pool will store /// @dev This method is no-op if the pool already has an observationCardinalityNext greater than or equal to /// the input observationCardinalityNext. /// @param observationCardinalityNext The desired minimum number of observations for the pool to store function increaseObservationCardinalityNext(uint16 observationCardinalityNext) external; }
// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity >=0.5.0; /// @title Permissioned pool actions /// @notice Contains pool methods that may only be called by the factory owner interface IUniswapV3PoolOwnerActions { /// @notice Set the denominator of the protocol's % share of the fees /// @param feeProtocol0 new protocol fee for token0 of the pool /// @param feeProtocol1 new protocol fee for token1 of the pool function setFeeProtocol(uint8 feeProtocol0, uint8 feeProtocol1) external; /// @notice Collect the protocol fee accrued to the pool /// @param recipient The address to which collected protocol fees should be sent /// @param amount0Requested The maximum amount of token0 to send, can be 0 to collect fees in only token1 /// @param amount1Requested The maximum amount of token1 to send, can be 0 to collect fees in only token0 /// @return amount0 The protocol fee collected in token0 /// @return amount1 The protocol fee collected in token1 function collectProtocol( address recipient, uint128 amount0Requested, uint128 amount1Requested ) external returns (uint128 amount0, uint128 amount1); }
// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity >=0.5.0; /// @title Errors emitted by a pool /// @notice Contains all events emitted by the pool interface IUniswapV3PoolErrors { error LOK(); error TLU(); error TLM(); error TUM(); error AI(); error M0(); error M1(); error AS(); error IIA(); error L(); error F0(); error F1(); }
// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity >=0.5.0; /// @title Events emitted by a pool /// @notice Contains all events emitted by the pool interface IUniswapV3PoolEvents { /// @notice Emitted exactly once by a pool when #initialize is first called on the pool /// @dev Mint/Burn/Swap cannot be emitted by the pool before Initialize /// @param sqrtPriceX96 The initial sqrt price of the pool, as a Q64.96 /// @param tick The initial tick of the pool, i.e. log base 1.0001 of the starting price of the pool event Initialize(uint160 sqrtPriceX96, int24 tick); /// @notice Emitted when liquidity is minted for a given position /// @param sender The address that minted the liquidity /// @param owner The owner of the position and recipient of any minted liquidity /// @param tickLower The lower tick of the position /// @param tickUpper The upper tick of the position /// @param amount The amount of liquidity minted to the position range /// @param amount0 How much token0 was required for the minted liquidity /// @param amount1 How much token1 was required for the minted liquidity event Mint( address sender, address indexed owner, int24 indexed tickLower, int24 indexed tickUpper, uint128 amount, uint256 amount0, uint256 amount1 ); /// @notice Emitted when fees are collected by the owner of a position /// @dev Collect events may be emitted with zero amount0 and amount1 when the caller chooses not to collect fees /// @param owner The owner of the position for which fees are collected /// @param tickLower The lower tick of the position /// @param tickUpper The upper tick of the position /// @param amount0 The amount of token0 fees collected /// @param amount1 The amount of token1 fees collected event Collect( address indexed owner, address recipient, int24 indexed tickLower, int24 indexed tickUpper, uint128 amount0, uint128 amount1 ); /// @notice Emitted when a position's liquidity is removed /// @dev Does not withdraw any fees earned by the liquidity position, which must be withdrawn via #collect /// @param owner The owner of the position for which liquidity is removed /// @param tickLower The lower tick of the position /// @param tickUpper The upper tick of the position /// @param amount The amount of liquidity to remove /// @param amount0 The amount of token0 withdrawn /// @param amount1 The amount of token1 withdrawn event Burn( address indexed owner, int24 indexed tickLower, int24 indexed tickUpper, uint128 amount, uint256 amount0, uint256 amount1 ); /// @notice Emitted by the pool for any swaps between token0 and token1 /// @param sender The address that initiated the swap call, and that received the callback /// @param recipient The address that received the output of the swap /// @param amount0 The delta of the token0 balance of the pool /// @param amount1 The delta of the token1 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 price of the pool after the swap event Swap( address indexed sender, address indexed recipient, int256 amount0, int256 amount1, uint160 sqrtPriceX96, uint128 liquidity, int24 tick ); /// @notice Emitted by the pool for any flashes of token0/token1 /// @param sender The address that initiated the swap call, and that received the callback /// @param recipient The address that received the tokens from flash /// @param amount0 The amount of token0 that was flashed /// @param amount1 The amount of token1 that was flashed /// @param paid0 The amount of token0 paid for the flash, which can exceed the amount0 plus the fee /// @param paid1 The amount of token1 paid for the flash, which can exceed the amount1 plus the fee event Flash( address indexed sender, address indexed recipient, uint256 amount0, uint256 amount1, uint256 paid0, uint256 paid1 ); /// @notice Emitted by the pool for increases to the number of observations that can be stored /// @dev observationCardinalityNext is not the observation cardinality until an observation is written at the index /// just before a mint/swap/burn. /// @param observationCardinalityNextOld The previous value of the next observation cardinality /// @param observationCardinalityNextNew The updated value of the next observation cardinality event IncreaseObservationCardinalityNext( uint16 observationCardinalityNextOld, uint16 observationCardinalityNextNew ); /// @notice Emitted when the protocol fee is changed by the pool /// @param feeProtocol0Old The previous value of the token0 protocol fee /// @param feeProtocol1Old The previous value of the token1 protocol fee /// @param feeProtocol0New The updated value of the token0 protocol fee /// @param feeProtocol1New The updated value of the token1 protocol fee event SetFeeProtocol(uint8 feeProtocol0Old, uint8 feeProtocol1Old, uint8 feeProtocol0New, uint8 feeProtocol1New); /// @notice Emitted when the collected protocol fees are withdrawn by the factory owner /// @param sender The address that collects the protocol fees /// @param recipient The address that receives the collected protocol fees /// @param amount0 The amount of token0 protocol fees that is withdrawn /// @param amount0 The amount of token1 protocol fees that is withdrawn event CollectProtocol(address indexed sender, address indexed recipient, uint128 amount0, uint128 amount1); }
// SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (token/ERC1155/utils/ERC1155Receiver.sol) pragma solidity ^0.8.0; import "../IERC1155Receiver.sol"; import "../../../utils/introspection/ERC165.sol"; /** * @dev _Available since v3.1._ */ abstract contract ERC1155Receiver 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); } }
// SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/IERC20.sol) pragma solidity ^0.8.0; /** * @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 amount of tokens in existence. */ function totalSupply() external view returns (uint256); /** * @dev Returns the amount of tokens owned by `account`. */ function balanceOf(address account) external view returns (uint256); /** * @dev Moves `amount` 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 amount) 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 `amount` 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 amount) external returns (bool); /** * @dev Moves `amount` tokens from `from` to `to` using the * allowance mechanism. `amount` 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 amount ) external returns (bool); }
// SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.8.0) (utils/math/Math.sol) pragma solidity ^0.8.0; /** * @dev Standard math utilities missing in the Solidity language. */ library Math { enum Rounding { Down, // Toward negative infinity Up, // Toward infinity Zero // Toward zero } /** * @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 up instead * of rounding down. */ function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) { // (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; // Least significant 256 bits of the product uint256 prod1; // Most significant 256 bits of the product assembly { let mm := mulmod(x, y, not(0)) prod0 := mul(x, y) prod1 := sub(sub(mm, prod0), lt(mm, prod0)) } // Handle non-overflow cases, 256 by 256 division. if (prod1 == 0) { return prod0 / denominator; } // 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]. 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. // Does not overflow because the denominator cannot be zero at this stage in the function. uint256 twos = denominator & (~denominator + 1); 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 (rounding == Rounding.Up && 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 down. * * 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 + (rounding == Rounding.Up && result * result < a ? 1 : 0); } } /** * @dev Return the log in base 2, rounded down, of a positive value. * 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 + (rounding == Rounding.Up && 1 << result < value ? 1 : 0); } } /** * @dev Return the log in base 10, rounded down, of a positive value. * 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 + (rounding == Rounding.Up && 10**result < value ? 1 : 0); } } /** * @dev Return the log in base 256, rounded down, of a positive value. * 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 10, 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 + (rounding == Rounding.Up && 1 << (result * 8) < value ? 1 : 0); } } }
// SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.5.0) (token/ERC1155/IERC1155Receiver.sol) pragma solidity ^0.8.0; import "../../utils/introspection/IERC165.sol"; /** * @dev _Available since v3.1._ */ 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 // OpenZeppelin Contracts v4.4.1 (utils/introspection/ERC165.sol) pragma solidity ^0.8.0; import "./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); * } * ``` * * Alternatively, {ERC165Storage} provides an easier to use but more expensive implementation. */ abstract contract ERC165 is IERC165 { /** * @dev See {IERC165-supportsInterface}. */ function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { return interfaceId == type(IERC165).interfaceId; } }
// SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (utils/introspection/IERC165.sol) pragma solidity ^0.8.0; /** * @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); }
{ "remappings": [ "forge-std/=lib/forge-std/src/", "@openzeppelin/=lib/openzeppelin-contracts/", "solmate/=lib/solmate/src/", "univ3-core/=lib/v3-core/contracts/", "univ3-periphery/=lib/v3-periphery/contracts/", "@contracts/=contracts/", "@libraries/=contracts/libraries/", "@base/=contracts/base/", "@test_periphery/=test/foundry/test_periphery/", "@tokens/=contracts/tokens/", "@types/=contracts/types/", "@scripts/=scripts/", "@uniswap/=lib/", "ds-test/=lib/solmate/lib/ds-test/src/", "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": 9999999 }, "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": "0x000000000001a911bE5b2C386CC5807219f10eBe" } } }
[{"inputs":[{"internalType":"uint256","name":"_commissionFee","type":"uint256"},{"internalType":"uint256","name":"_sellerCollateralRatio","type":"uint256"},{"internalType":"uint256","name":"_buyerCollateralRatio","type":"uint256"},{"internalType":"int256","name":"_forceExerciseCost","type":"int256"},{"internalType":"uint256","name":"_targetPoolUtilization","type":"uint256"},{"internalType":"uint256","name":"_saturatedPoolUtilization","type":"uint256"},{"internalType":"uint256","name":"_ITMSpreadMultiplier","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"CastingError","type":"error"},{"inputs":[],"name":"CollateralTokenAlreadyInitialized","type":"error"},{"inputs":[],"name":"DepositTooLarge","type":"error"},{"inputs":[],"name":"ExceedsMaximumRedemption","type":"error"},{"inputs":[],"name":"InvalidTick","type":"error"},{"inputs":[],"name":"NotPanopticPool","type":"error"},{"inputs":[],"name":"PositionCountNotZero","type":"error"},{"inputs":[],"name":"TransferFailed","type":"error"},{"inputs":[],"name":"UnderOverFlow","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":false,"internalType":"uint256","name":"assets","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"shares","type":"uint256"}],"name":"Deposit","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":true,"internalType":"address","name":"receiver","type":"address"},{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":false,"internalType":"uint256","name":"assets","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"shares","type":"uint256"}],"name":"Withdraw","type":"event"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"allowance","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"asset","outputs":[{"internalType":"address","name":"assetTokenAddress","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"balance","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"shares","type":"uint256"}],"name":"convertToAssets","outputs":[{"internalType":"uint256","name":"assets","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"assets","type":"uint256"}],"name":"convertToShares","outputs":[{"internalType":"uint256","name":"shares","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"delegatee","type":"address"}],"name":"delegate","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"assets","type":"uint256"},{"internalType":"address","name":"receiver","type":"address"}],"name":"deposit","outputs":[{"internalType":"uint256","name":"shares","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"optionOwner","type":"address"},{"internalType":"int128","name":"longAmount","type":"int128"},{"internalType":"int128","name":"shortAmount","type":"int128"},{"internalType":"int128","name":"swappedAmount","type":"int128"},{"internalType":"int128","name":"realizedPremium","type":"int128"}],"name":"exercise","outputs":[{"internalType":"int128","name":"","type":"int128"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"int24","name":"currentTick","type":"int24"},{"internalType":"int24","name":"oracleTick","type":"int24"},{"internalType":"TokenId","name":"positionId","type":"uint256"},{"internalType":"uint128","name":"positionSize","type":"uint128"},{"internalType":"LeftRightSigned","name":"longAmounts","type":"int256"}],"name":"exerciseCost","outputs":[{"internalType":"LeftRightSigned","name":"exerciseFees","type":"int256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"user","type":"address"},{"internalType":"int24","name":"atTick","type":"int24"},{"internalType":"uint256[2][]","name":"positionBalanceArray","type":"uint256[2][]"},{"internalType":"uint128","name":"shortPremium","type":"uint128"},{"internalType":"uint128","name":"longPremium","type":"uint128"}],"name":"getAccountMarginDetails","outputs":[{"internalType":"LeftRightUnsigned","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getPoolData","outputs":[{"internalType":"uint256","name":"poolAssets","type":"uint256"},{"internalType":"uint256","name":"insideAMM","type":"uint256"},{"internalType":"uint256","name":"currentPoolUtilization","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"maxDeposit","outputs":[{"internalType":"uint256","name":"maxAssets","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"maxMint","outputs":[{"internalType":"uint256","name":"maxShares","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"maxRedeem","outputs":[{"internalType":"uint256","name":"maxShares","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"maxWithdraw","outputs":[{"internalType":"uint256","name":"maxAssets","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"shares","type":"uint256"},{"internalType":"address","name":"receiver","type":"address"}],"name":"mint","outputs":[{"internalType":"uint256","name":"assets","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes[]","name":"data","type":"bytes[]"}],"name":"multicall","outputs":[{"internalType":"bytes[]","name":"results","type":"bytes[]"}],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"assets","type":"uint256"}],"name":"previewDeposit","outputs":[{"internalType":"uint256","name":"shares","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"shares","type":"uint256"}],"name":"previewMint","outputs":[{"internalType":"uint256","name":"assets","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"shares","type":"uint256"}],"name":"previewRedeem","outputs":[{"internalType":"uint256","name":"assets","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"assets","type":"uint256"}],"name":"previewWithdraw","outputs":[{"internalType":"uint256","name":"shares","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"shares","type":"uint256"},{"internalType":"address","name":"receiver","type":"address"},{"internalType":"address","name":"owner","type":"address"}],"name":"redeem","outputs":[{"internalType":"uint256","name":"assets","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"refunder","type":"address"},{"internalType":"address","name":"refundee","type":"address"},{"internalType":"int256","name":"assets","type":"int256"}],"name":"refund","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"delegatee","type":"address"}],"name":"revoke","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"liquidator","type":"address"},{"internalType":"address","name":"liquidatee","type":"address"},{"internalType":"int256","name":"bonus","type":"int256"}],"name":"settleLiquidation","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bool","name":"underlyingIsToken0","type":"bool"},{"internalType":"address","name":"token0","type":"address"},{"internalType":"address","name":"token1","type":"address"},{"internalType":"uint24","name":"fee","type":"uint24"},{"internalType":"contract PanopticPool","name":"panopticPool","type":"address"}],"name":"startToken","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"optionOwner","type":"address"},{"internalType":"int128","name":"longAmount","type":"int128"},{"internalType":"int128","name":"shortAmount","type":"int128"},{"internalType":"int128","name":"swappedAmount","type":"int128"},{"internalType":"bool","name":"isCovered","type":"bool"}],"name":"takeCommissionAddData","outputs":[{"internalType":"uint32","name":"","type":"uint32"},{"internalType":"uint128","name":"","type":"uint128"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"totalAssets","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"assets","type":"uint256"},{"internalType":"address","name":"receiver","type":"address"},{"internalType":"address","name":"owner","type":"address"},{"internalType":"TokenId[]","name":"positionIdList","type":"uint256[]"}],"name":"withdraw","outputs":[{"internalType":"uint256","name":"shares","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"assets","type":"uint256"},{"internalType":"address","name":"receiver","type":"address"},{"internalType":"address","name":"owner","type":"address"}],"name":"withdraw","outputs":[{"internalType":"uint256","name":"shares","type":"uint256"}],"stateMutability":"nonpayable","type":"function"}]
Loading...
Loading
Loading...
Loading
[ Download: CSV Export ]
[ Download: CSV Export ]
A token is a representation of an on-chain or off-chain asset. The token page shows information such as price, total supply, holders, transfers and social links. Learn more about this page in our Knowledge Base.