From 58bc7f2a73a1c6df43daf043b2ad59dae776127b Mon Sep 17 00:00:00 2001 From: dyedm1 Date: Thu, 10 Oct 2024 19:10:17 -0400 Subject: [PATCH] feat: panoptic v1.1 --- .gitmodules | 6 + contracts/CollateralTracker.sol | 343 ++-- contracts/PanopticFactory.sol | 269 +-- contracts/PanopticPool.sol | 310 ++-- contracts/SemiFungiblePositionManager.sol | 851 ++++----- contracts/base/FactoryNFT.sol | 10 +- contracts/interfaces/IV3CompatibleOracle.sol | 71 + contracts/libraries/CallbackLib.sol | 39 - contracts/libraries/Constants.sol | 26 +- contracts/libraries/Errors.sol | 4 +- contracts/libraries/Math.sol | 2 +- contracts/libraries/PanopticMath.sol | 159 +- .../{FeesCalc.sol => V4StateReader.sol} | 155 +- contracts/types/LeftRight.sol | 19 +- contracts/types/TokenId.sol | 24 +- foundry.toml | 1 + lib/clones-with-immutable-args | 1 + lib/v4-core | 1 + remappings.txt | 7 +- script/DeployProtocol.s.sol | 16 +- test/foundry/core/CollateralTracker.t.sol | 562 +++--- test/foundry/core/Misc.t.sol | 1295 ++++++++------ test/foundry/core/PanopticFactory.t.sol | 237 ++- test/foundry/core/PanopticPool.t.sol | 908 +++++----- .../core/SemiFungiblePositionManager.t.sol | 1531 +++++++---------- test/foundry/libraries/CallbackLib.t.sol | 101 -- test/foundry/libraries/FeesCalc.t.sol | 209 --- test/foundry/libraries/PanopticMath.t.sol | 88 +- test/foundry/libraries/SafeTransferLib.t.sol | 24 +- .../harnesses/CallbackLibHarness.sol | 18 - .../libraries/harnesses/FeesCalcHarness.sol | 49 - .../harnesses/PanopticMathHarness.sol | 15 +- test/foundry/poc/POC.t.sol | 202 +++ test/foundry/testUtils/PositionUtils.sol | 620 ++----- test/foundry/testUtils/ReentrancyMocks.sol | 155 +- test/foundry/testUtils/V4RouterSimple.sol | 308 ++++ .../foundry/test_periphery/PanopticHelper.sol | 204 ++- test/foundry/types/LeftRight.t.sol | 29 - .../types/harnesses/LeftRightHarness.sol | 11 - 39 files changed, 4174 insertions(+), 4706 deletions(-) create mode 100644 contracts/interfaces/IV3CompatibleOracle.sol delete mode 100644 contracts/libraries/CallbackLib.sol rename contracts/libraries/{FeesCalc.sol => V4StateReader.sol} (53%) create mode 160000 lib/clones-with-immutable-args create mode 160000 lib/v4-core delete mode 100644 test/foundry/libraries/CallbackLib.t.sol delete mode 100644 test/foundry/libraries/FeesCalc.t.sol delete mode 100644 test/foundry/libraries/harnesses/CallbackLibHarness.sol delete mode 100644 test/foundry/libraries/harnesses/FeesCalcHarness.sol create mode 100644 test/foundry/poc/POC.t.sol create mode 100644 test/foundry/testUtils/V4RouterSimple.sol diff --git a/.gitmodules b/.gitmodules index f1fd122..bf07f2a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,3 +16,9 @@ [submodule "lib/solady"] path = lib/solady url = https://github.com/vectorized/solady +[submodule "lib/v4-core"] + path = lib/v4-core + url = https://github.com/Uniswap/v4-core.git +[submodule "lib/clones-with-immutable-args"] + path = lib/clones-with-immutable-args + url = https://github.com/wighawag/clones-with-immutable-args diff --git a/contracts/CollateralTracker.sol b/contracts/CollateralTracker.sol index 196f9ce..81691a8 100644 --- a/contracts/CollateralTracker.sol +++ b/contracts/CollateralTracker.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.24; // Interfaces import {PanopticPool} from "./PanopticPool.sol"; // Inherited implementations +import {Clone} from "clones-with-immutable-args/Clone.sol"; import {ERC20Minimal} from "@tokens/ERC20Minimal.sol"; import {Multicall} from "@base/Multicall.sol"; // Libraries @@ -18,6 +19,8 @@ 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"; +import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol"; +import {Currency} from "v4-core/types/Currency.sol"; /// @title Collateral Tracking System / Margin Accounting used in conjunction with a Panoptic Pool. /// @author Axicon Labs Limited @@ -34,7 +37,7 @@ import {TokenId} from "@types/TokenId.sol"; /// @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 { +contract CollateralTracker is Clone, ERC20Minimal, Multicall { using Math for uint256; /*////////////////////////////////////////////////////////////// @@ -69,8 +72,8 @@ contract CollateralTracker is ERC20Minimal, Multicall { /// @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 Prefix for the token name (i.e POPT-V1.1 USDC LP on ETH/USDC 30bps). + string internal constant NAME_PREFIX = "POPT-V1.1"; /// @notice Decimals for computation (1 bps (basis point) precision: 0.01%). /// @dev uint type for composability with unsigned integer based mathematical operations. @@ -80,49 +83,6 @@ contract CollateralTracker is ERC20Minimal, Multicall { /// @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. - /// @dev As each instance is deployed via proxy clone, initial parameters must only be initalized once via startToken(). - 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, - /// defined in hundredths of basis points. - /// @dev The result of the calculation is stored instead of the multiple to save gas during usage. - /// When the fee is set, the multiple is calculated and stored. - uint128 internal s_ITMSpreadFee; - - /// @notice The fee of the Uniswap pool in hundredths of basis points. - uint24 internal s_poolFee; - /*////////////////////////////////////////////////////////////// RISK PARAMETERS //////////////////////////////////////////////////////////////*/ @@ -156,9 +116,82 @@ contract CollateralTracker is ERC20Minimal, Multicall { /// @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; + /// @notice Fee, in basis points, that is charged on the intrinsic value of ITM positions. + uint256 immutable ITM_SPREAD_FEE; + + /// @notice The canonical Uniswap V4 Pool Manager address. + IPoolManager internal immutable POOL_MANAGER_V4; + + /*////////////////////////////////////////////////////////////// + POOL UTILIZATION DATA + //////////////////////////////////////////////////////////////*/ + + /// @notice Cached amount of assets accounted to be held by the Panoptic Pool — ignores donations, pending fee payouts, and other untracked balance changes. + uint128 internal s_poolAssets; + + /// @notice Amount of assets moved from the Panoptic Pool to the AMM. + uint128 internal s_inAMM; + + /*////////////////////////////////////////////////////////////// + INITIALIZATION STATE + //////////////////////////////////////////////////////////////*/ + + /// @notice Boolean tracking whether this CollateralTracker has been initialized. + bool internal s_initialized; + + /*////////////////////////////////////////////////////////////// + POOL-SPECIFIC IMMUTABLE PARAMETERS + //////////////////////////////////////////////////////////////*/ + + // The parameters will be encoded at `_getImmutableArgsOffset()` in calldata as follows: + // abi.encodePacked(address panopticPool, bool underlyingIsToken0, address underlyingToken, address token0, address token1, uint24 poolFee) + // bytes: 0 20 21 41 61 81 + // |<---- 160 bits ---->|<---- 8 bits ---->|<---- 160 bits ---->|<---- 160 bits ---->|<---- 160 bits ---->|<---- 24 bits ---->| + // panopticPool underlyingIsToken0 underlyingToken token0 token1 poolFee + + /// @notice Retrieve the Panoptic Pool that this collateral token belongs to. + /// @return The Panoptic Pool associated with this collateral token + function _panopticPool() internal pure returns (PanopticPool) { + return PanopticPool(_getArgAddress(0)); + } + + /// @notice Retrieve a boolean indicating whether the underlying token is token0 or token1 in the Uniswap V4 pool. + /// @return underlyingIsToken0 True if the underlying token is token0, false if it is token1 + function _underlyingIsToken0() internal pure returns (bool underlyingIsToken0) { + uint256 offset = _getImmutableArgsOffset(); + + assembly ("memory-safe") { + underlyingIsToken0 := shr(0xf8, calldataload(add(offset, 20))) + } + } + + /// @notice Retrieve the address of the underlying token. + /// @return The address of the underlying token + function _underlyingToken() internal pure returns (address) { + return _getArgAddress(21); + } + + /// @notice Retrieve the address of token0 in the Uniswap V4 pool. + /// @return The address of token0 in the Uniswap V4 pool + function _token0() internal pure returns (address) { + return _getArgAddress(41); + } + + /// @notice Retrieve the address of token1 in the Uniswap V4 pool. + /// @return The address of token1 in the Uniswap V4 pool + function _token1() internal pure returns (address) { + return _getArgAddress(61); + } + + /// @notice Retrieve the fee of the Uniswap V4 pool. + /// @return poolFee The fee of the Uniswap V4 pool + function _poolFee() internal pure returns (uint24 poolFee) { + uint256 offset = _getImmutableArgsOffset(); + + assembly ("memory-safe") { + poolFee := shr(0xe8, calldataload(add(offset, 81))) + } + } /*////////////////////////////////////////////////////////////// ACCESS CONTROL @@ -166,7 +199,7 @@ contract CollateralTracker is ERC20Minimal, Multicall { /// @notice Ensure that the associated Panoptic pool is the caller. Revert if not. modifier onlyPanopticPool() { - if (msg.sender != address(s_panopticPool)) revert Errors.NotPanopticPool(); + if (msg.sender != address(_panopticPool())) revert Errors.NotPanopticPool(); _; } @@ -181,7 +214,8 @@ contract CollateralTracker is ERC20Minimal, Multicall { /// @param _forceExerciseCost Basal cost (in bps of notional) to force exercise a position that is barely far-the-money (out-of-range) /// @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 + /// @param _ITMSpreadFee Fee, in basis points, that is charged on the intrinsic value of ITM positions + /// @param _manager The canonical Uniswap V4 pool manager constructor( uint256 _commissionFee, uint256 _sellerCollateralRatio, @@ -189,7 +223,8 @@ contract CollateralTracker is ERC20Minimal, Multicall { int256 _forceExerciseCost, uint256 _targetPoolUtilization, uint256 _saturatedPoolUtilization, - uint256 _ITMSpreadMultiplier + uint256 _ITMSpreadFee, + IPoolManager _manager ) { COMMISSION_FEE = _commissionFee; SELLER_COLLATERAL_RATIO = _sellerCollateralRatio; @@ -197,25 +232,12 @@ contract CollateralTracker is ERC20Minimal, Multicall { FORCE_EXERCISE_COST = _forceExerciseCost; TARGET_POOL_UTIL = _targetPoolUtilization; SATURATED_POOL_UTIL = _saturatedPoolUtilization; - ITM_SPREAD_MULTIPLIER = _ITMSpreadMultiplier; + ITM_SPREAD_FEE = _ITMSpreadFee; + POOL_MANAGER_V4 = _manager; } - /// @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 + /// @notice Initializes a new `CollateralTracker` instance with 1 virtual asset and 10^6 virtual shares. + function initialize() external { if (s_initialized) revert Errors.CollateralTokenAlreadyInitialized(); s_initialized = true; @@ -226,27 +248,6 @@ contract CollateralTracker is ERC20Minimal, Multicall { // 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; since there's always exactly two tokens it could be) - s_underlyingIsToken0 = underlyingIsToken0; - - // Additional risk premium charged on intrinsic value of ITM positions - unchecked { - s_ITMSpreadFee = uint128((ITM_SPREAD_MULTIPLIER * fee) / DECIMALS); - } } /*////////////////////////////////////////////////////////////// @@ -274,10 +275,10 @@ contract CollateralTracker is ERC20Minimal, Multicall { // 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, + _token0(), + _token1(), + _underlyingIsToken0(), + _poolFee(), NAME_PREFIX ); } @@ -286,14 +287,14 @@ contract CollateralTracker is ERC20Minimal, Multicall { /// @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); + return InteractionHelper.computeSymbol(_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); + return InteractionHelper.computeDecimals(_underlyingToken()); } /*////////////////////////////////////////////////////////////// @@ -312,7 +313,8 @@ contract CollateralTracker is ERC20Minimal, Multicall { // 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.numberOfPositions(msg.sender) != 0) revert Errors.PositionCountNotZero(); + if (_panopticPool().numberOfPositions(msg.sender) != 0) + revert Errors.PositionCountNotZero(); return ERC20Minimal.transfer(recipient, amount); } @@ -331,19 +333,71 @@ contract CollateralTracker is ERC20Minimal, Multicall { // 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.numberOfPositions(from) != 0) revert Errors.PositionCountNotZero(); + if (_panopticPool().numberOfPositions(from) != 0) revert Errors.PositionCountNotZero(); return ERC20Minimal.transferFrom(from, to, amount); } + /*////////////////////////////////////////////////////////////// + UNISWAP V4 LOCK CALLBACK + //////////////////////////////////////////////////////////////*/ + + /// @notice Initiates the unlock callback to wrap/unwrap `delta` amount of the underlying token and transfer to/from the Panoptic Pool. + /// @param account The address of the account to transfer the underlying token to/from + /// @param delta The amount of the underlying token to wrap/unwrap and transfer + function _settleTokenDelta(address account, int256 delta) internal { + POOL_MANAGER_V4.unlock(abi.encode(account, delta)); + } + + /// @notice Uniswap V4 unlock callback implementation. + /// @dev Parameters are `(address account, int256 delta)`. + /// @dev Wraps/unwraps `delta` amount of the underlying token and transfers to/from the Panoptic Pool. + /// @param data The encoded data containing the account and delta + /// @return This function returns no data + function unlockCallback(bytes calldata data) external returns (bytes memory) { + if (msg.sender != address(POOL_MANAGER_V4)) revert Errors.UnauthorizedUniswapCallback(); + + (address account, int256 delta) = abi.decode(data, (address, int256)); + + address underlyingToken = _underlyingToken(); + if (delta > 0) { + POOL_MANAGER_V4.sync(Currency.wrap(underlyingToken)); + SafeTransferLib.safeTransferFrom( + underlyingToken, + account, + address(POOL_MANAGER_V4), + uint256(delta) + ); + POOL_MANAGER_V4.settle(); + + POOL_MANAGER_V4.mint( + address(_panopticPool()), + uint160(underlyingToken), + uint256(delta) + ); + } else if (delta < 0) { + unchecked { + delta = -delta; + } + POOL_MANAGER_V4.burn( + address(_panopticPool()), + uint160(underlyingToken), + uint256(delta) + ); + POOL_MANAGER_V4.take(Currency.wrap(underlyingToken), account, uint256(delta)); + } + + return ""; + } + /*////////////////////////////////////////////////////////////// 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; + function asset() external pure returns (address assetTokenAddress) { + return _underlyingToken(); } /// @notice Get the total amount of assets managed by the CollateralTracker vault. @@ -405,12 +459,7 @@ contract CollateralTracker is ERC20Minimal, Multicall { // 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 - ); + _settleTokenDelta(msg.sender, int256(assets)); // mint collateral shares of the Panoptic Pool funds (this ERC20 token) _mint(receiver, shares); @@ -461,12 +510,7 @@ contract CollateralTracker is ERC20Minimal, Multicall { // 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 - ); + _settleTokenDelta(msg.sender, int256(assets)); // mint collateral shares of the Panoptic Pool funds (this ERC20 token) _mint(receiver, shares); @@ -483,13 +527,11 @@ contract CollateralTracker is ERC20Minimal, Multicall { /// @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) { - // We can only use the standard 4626 withdraw function if the user has no open positions - // For the sake of simplicity assets can only be withdrawn through the redeem function uint256 poolAssets = s_poolAssets; unchecked { uint256 available = poolAssets > 0 ? poolAssets - 1 : 0; uint256 balance = convertToAssets(balanceOf[owner]); - return s_panopticPool.numberOfPositions(owner) == 0 ? Math.min(available, balance) : 0; + return _panopticPool().numberOfPositions(owner) == 0 ? Math.min(available, balance) : 0; } } @@ -534,12 +576,9 @@ contract CollateralTracker is ERC20Minimal, Multicall { } // transfer assets (underlying token funds) from the PanopticPool to the LP - SafeTransferLib.safeTransferFrom( - s_underlyingToken, - address(s_panopticPool), - receiver, - assets - ); + unchecked { + _settleTokenDelta(receiver, -int256(assets)); + } emit Withdraw(msg.sender, receiver, owner, assets, shares); } @@ -574,15 +613,12 @@ contract CollateralTracker is ERC20Minimal, Multicall { s_poolAssets -= uint128(assets); // reverts if account is not solvent/eligible to withdraw - s_panopticPool.validateCollateralWithdrawable(owner, positionIdList); + _panopticPool().validateCollateralWithdrawable(owner, positionIdList); // transfer assets (underlying token funds) from the PanopticPool to the LP - SafeTransferLib.safeTransferFrom( - s_underlyingToken, - address(s_panopticPool), - receiver, - assets - ); + unchecked { + _settleTokenDelta(receiver, -int256(assets)); + } emit Withdraw(msg.sender, receiver, owner, assets, shares); } @@ -596,7 +632,7 @@ contract CollateralTracker is ERC20Minimal, Multicall { unchecked { uint256 available = convertToShares(poolAssets > 0 ? poolAssets - 1 : 0); uint256 balance = balanceOf[owner]; - return s_panopticPool.numberOfPositions(owner) == 0 ? Math.min(available, balance) : 0; + return _panopticPool().numberOfPositions(owner) == 0 ? Math.min(available, balance) : 0; } } @@ -638,12 +674,9 @@ contract CollateralTracker is ERC20Minimal, Multicall { } // transfer assets (underlying token funds) from the PanopticPool to the LP - SafeTransferLib.safeTransferFrom( - s_underlyingToken, - address(s_panopticPool), - receiver, - assets - ); + unchecked { + _settleTokenDelta(receiver, -int256(assets)); + } emit Withdraw(msg.sender, receiver, owner, assets, shares); } @@ -903,7 +936,7 @@ contract CollateralTracker is ERC20Minimal, Multicall { bonusAbs = uint256(-bonus); } - SafeTransferLib.safeTransferFrom(s_underlyingToken, liquidator, msg.sender, bonusAbs); + _settleTokenDelta(liquidator, int256(bonusAbs)); _mint(liquidatee, convertToShares(bonusAbs)); @@ -1109,26 +1142,20 @@ contract CollateralTracker is ERC20Minimal, Multicall { int128 swappedAmount, bool isCovered ) internal view returns (int256 exchangedAmount) { - // If amount swapped is positive, the amount of tokens to pay is the ITM amount - unchecked { - // intrinsic value is the amount that need to be exchanged due to minting in-the-money - int256 intrinsicValue = int256(swappedAmount) - (shortAmount - longAmount); - - if (intrinsicValue != 0) { - // the swap commission is paid on the intrinsic value (if a swap occured -- users who mint covered options with their own collateral do not pay this fee) - uint256 swapCommission = isCovered - ? 0 - : Math.unsafeDivRoundingUp( - s_ITMSpreadFee * uint256(Math.abs(intrinsicValue)), - DECIMALS * 100 - ); - - // set the exchanged amount to the sum of the intrinsic value and swapCommission - exchangedAmount = intrinsicValue + int256(swapCommission); - } + // add the intrinsic value (amount that needs to be exchanged due to minting in-the-money) + exchangedAmount = int256(swappedAmount) - (shortAmount - longAmount); + + // the swap commission is paid on the intrinsic value (if a swap occured -- users who mint covered options with their own collateral do not pay this fee) + if (!isCovered) + exchangedAmount += int256( + Math.unsafeDivRoundingUp( + ITM_SPREAD_FEE * uint256(Math.abs(exchangedAmount)), + DECIMALS + ) + ); - // compute total commission amount = commission rate + spread fee + // total commission rate = notional value * COMMISSION_FEE + intrinsic value (swapped) * ITM_SPREAD_FEE exchangedAmount += int256( Math.unsafeDivRoundingUp( uint256(uint128(shortAmount + longAmount)) * COMMISSION_FEE, @@ -1192,7 +1219,7 @@ contract CollateralTracker is ERC20Minimal, Multicall { // read the position size and the pool utilization at mint uint128 positionSize = PositionBalance.wrap(positionBalanceArray[i][1]).positionSize(); - bool underlyingIsToken0 = s_underlyingIsToken0; + bool underlyingIsToken0 = _underlyingIsToken0(); // read the pool utilization at mint int16 poolUtilization = underlyingIsToken0 @@ -1224,7 +1251,7 @@ contract CollateralTracker is ERC20Minimal, Multicall { /// @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 + /// @param underlyingIsToken0 Cached `_underlyingIsToken0()` value for this CollateralTracker instance /// @return tokenRequired Total required tokens for all legs of the specified tokenId. function _getRequiredCollateralAtTickSinglePosition( TokenId tokenId, @@ -1338,10 +1365,10 @@ contract CollateralTracker is ERC20Minimal, Multicall { // 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) + Math.max24(2 * (atTick - strike), Constants.MIN_V4POOL_TICK) ) // puts -> price/strike : Math.getSqrtRatioAtTick( - Math.max24(2 * (strike - atTick), Constants.MIN_V3POOL_TICK) + Math.max24(2 * (strike - atTick), Constants.MIN_V4POOL_TICK) ); // calls -> strike/price // compute the collateral requirement depending on whether the position is ITM & out-of-range or ITM and in-range: diff --git a/contracts/PanopticFactory.sol b/contracts/PanopticFactory.sol index a543478..f82ace1 100644 --- a/contracts/PanopticFactory.sol +++ b/contracts/PanopticFactory.sol @@ -5,22 +5,26 @@ pragma solidity ^0.8.24; import {CollateralTracker} from "@contracts/CollateralTracker.sol"; import {PanopticPool} from "@contracts/PanopticPool.sol"; import {SemiFungiblePositionManager} from "@contracts/SemiFungiblePositionManager.sol"; -import {IUniswapV3Factory} from "univ3-core/interfaces/IUniswapV3Factory.sol"; -import {IUniswapV3Pool} from "univ3-core/interfaces/IUniswapV3Pool.sol"; +import {IV3CompatibleOracle} from "@interfaces/IV3CompatibleOracle.sol"; // Inherited implementations import {Multicall} from "@base/Multicall.sol"; import {FactoryNFT} from "@base/FactoryNFT.sol"; -// OpenZeppelin libraries -import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; -// Libraries -import {CallbackLib} from "@libraries/CallbackLib.sol"; +// External libraries +import {ClonesWithImmutableArgs} from "clones-with-immutable-args/ClonesWithImmutableArgs.sol"; +// Internal libraries import {Constants} from "@libraries/Constants.sol"; import {Errors} from "@libraries/Errors.sol"; import {Math} from "@libraries/Math.sol"; import {PanopticMath} from "@libraries/PanopticMath.sol"; import {SafeTransferLib} from "@libraries/SafeTransferLib.sol"; +import {V4StateReader} from "@libraries/V4StateReader.sol"; // Custom types import {Pointer} from "@types/Pointer.sol"; +import {PoolKey} from "v4-core/types/PoolKey.sol"; +import {PoolId} from "v4-core/types/PoolId.sol"; +import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol"; +import {Currency} from "v4-core/types/Currency.sol"; +import {BalanceDelta} from "v4-core/types/BalanceDelta.sol"; /// @title Panoptic Factory which creates and registers Panoptic Pools. /// @author Axicon Labs Limited @@ -32,14 +36,16 @@ contract PanopticFactory is FactoryNFT, Multicall { /// @notice Emitted when a Panoptic Pool is created. /// @param poolAddress Address of the deployed Panoptic pool - /// @param uniswapPool Address of the underlying Uniswap V3 pool + /// @param oracleContract The external oracle contract used by the newly deployed Panoptic Pool + /// @param poolKey The Uniswap V4 pool key associated with the Panoptic Pool /// @param collateralTracker0 Address of the collateral tracker contract for token0 /// @param collateralTracker1 Address of the collateral tracker contract for token1 /// @param amount0 The amount of token0 deployed at full range /// @param amount1 The amount of token1 deployed at full range event PoolDeployed( PanopticPool indexed poolAddress, - IUniswapV3Pool indexed uniswapPool, + IV3CompatibleOracle indexed oracleContract, + PoolKey poolKey, CollateralTracker collateralTracker0, CollateralTracker collateralTracker1, uint256 amount0, @@ -50,14 +56,14 @@ contract PanopticFactory is FactoryNFT, Multicall { TYPES //////////////////////////////////////////////////////////////*/ - using Clones for address; + using ClonesWithImmutableArgs for address; /*////////////////////////////////////////////////////////////// CONSTANTS & IMMUTABLE //////////////////////////////////////////////////////////////*/ - /// @notice The Uniswap V3 factory contract to use. - IUniswapV3Factory internal immutable UNIV3_FACTORY; + /// @notice The canonical Uniswap V4 Pool Manager address. + IPoolManager internal immutable POOL_MANAGER_V4; /// @notice The Semi Fungible Position Manager (SFPM) which tracks option positions across Panoptic Pools. SemiFungiblePositionManager internal immutable SFPM; @@ -86,8 +92,8 @@ contract PanopticFactory is FactoryNFT, Multicall { STORAGE //////////////////////////////////////////////////////////////*/ - /// @notice Mapping from address(UniswapV3Pool) to address(PanopticPool) that stores the address of all deployed Panoptic Pools. - mapping(IUniswapV3Pool univ3pool => PanopticPool panopticPool) internal s_getPanopticPool; + /// @notice Mapping from keccak256(Uniswap V4 pool id, oracle contract address) to address(PanopticPool) that stores the address of all deployed Panoptic Pools. + mapping(bytes32 panopticPoolKey => PanopticPool panopticPool) internal s_getPanopticPool; /*////////////////////////////////////////////////////////////// INITIALIZATION @@ -96,7 +102,7 @@ contract PanopticFactory is FactoryNFT, Multicall { /// @notice Set immutable variables and store metadata pointers. /// @param _WETH9 Address of the Wrapped Ether (or other numeraire token) contract /// @param _SFPM The canonical `SemiFungiblePositionManager` deployment - /// @param _univ3Factory The canonical Uniswap V3 Factory deployment + /// @param manager The canonical Uniswap V4 pool manager /// @param _poolReference The reference implementation of the `PanopticPool` to clone /// @param _collateralReference The reference implementation of the `CollateralTracker` to clone /// @param properties An array of identifiers for different categories of metadata @@ -105,7 +111,7 @@ contract PanopticFactory is FactoryNFT, Multicall { constructor( address _WETH9, SemiFungiblePositionManager _SFPM, - IUniswapV3Factory _univ3Factory, + IPoolManager manager, address _poolReference, address _collateralReference, bytes32[] memory properties, @@ -114,42 +120,71 @@ contract PanopticFactory is FactoryNFT, Multicall { ) FactoryNFT(properties, indices, pointers) { WETH = _WETH9; SFPM = _SFPM; - UNIV3_FACTORY = _univ3Factory; + POOL_MANAGER_V4 = manager; POOL_REFERENCE = _poolReference; COLLATERAL_REFERENCE = _collateralReference; } /*////////////////////////////////////////////////////////////// - CALLBACK HANDLERS + UNISWAP V4 LOCK CALLBACK //////////////////////////////////////////////////////////////*/ - /// @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 { - CallbackLib.CallbackData memory decoded = abi.decode(data, (CallbackLib.CallbackData)); - - CallbackLib.validateCallback(msg.sender, UNIV3_FACTORY, decoded.poolFeatures); - - if (amount0Owed > 0) + /// @notice Uniswap V4 unlock callback implementation. + /// @dev Parameters are `(PoolKey key, int24 tickLower, int24 tickUpper, uint128 liquidity, address payer)`. + /// @dev Adds `liquidity` to the Uniswap V4 pool `key` at `tickLower-tickUpper` and transfers the tokens from `payer`. + /// @param data The encoded data containing the input parameters + /// @return `(uint256 token0Delta, uint256 token1Delta)` The amount of token0 and token1 used to create `liquidity` in the Uniswap pool + function unlockCallback(bytes calldata data) external returns (bytes memory) { + if (msg.sender != address(POOL_MANAGER_V4)) revert Errors.UnauthorizedUniswapCallback(); + + ( + PoolKey memory key, + int24 tickLower, + int24 tickUpper, + uint128 liquidity, + address payer + ) = abi.decode(data, (PoolKey, int24, int24, uint128, address)); + (BalanceDelta delta, BalanceDelta feesAccrued) = POOL_MANAGER_V4.modifyLiquidity( + key, + IPoolManager.ModifyLiquidityParams( + tickLower, + tickUpper, + int256(uint256(liquidity)), + bytes32(0) + ), + "" + ); + + if (delta.amount0() < 0) { + POOL_MANAGER_V4.sync(key.currency0); SafeTransferLib.safeTransferFrom( - decoded.poolFeatures.token0, - decoded.payer, - msg.sender, - amount0Owed + Currency.unwrap(key.currency0), + payer, + address(POOL_MANAGER_V4), + uint128(-delta.amount0()) ); - if (amount1Owed > 0) + POOL_MANAGER_V4.settle(); + } else if (delta.amount0() > 0) { + POOL_MANAGER_V4.clear(key.currency0, uint128(delta.amount0())); + } + + if (delta.amount1() < 0) { + POOL_MANAGER_V4.sync(key.currency1); SafeTransferLib.safeTransferFrom( - decoded.poolFeatures.token1, - decoded.payer, - msg.sender, - amount1Owed + Currency.unwrap(key.currency1), + payer, + address(POOL_MANAGER_V4), + uint128(-delta.amount1()) + ); + POOL_MANAGER_V4.settle(); + } else if (delta.amount1() > 0) { + POOL_MANAGER_V4.clear(key.currency1, uint128(delta.amount1())); + } + + return + abi.encode( + feesAccrued.amount0() - delta.amount0(), + feesAccrued.amount1() - delta.amount1() ); } @@ -160,71 +195,100 @@ contract PanopticFactory is FactoryNFT, Multicall { /// @notice Create a new Panoptic Pool linked to the given Uniswap pool identified uniquely by the incoming parameters. /// @dev There is a 1:1 mapping between a Panoptic Pool and a Uniswap Pool. /// @dev A Uniswap pool is uniquely identified by its tokens and the fee. - /// @dev Salt used in PanopticPool CREATE2 is `[leading 20 msg.sender chars][leading 20 pool address chars][salt]`. - /// @param token0 Address of token0 for the underlying Uniswap V3 pool - /// @param token1 Address of token1 for the underlying Uniswap V3 pool - /// @param fee The fee tier of the underlying Uniswap V3 pool, denominated in hundredths of bips - /// @param salt User-defined component of salt used in CREATE2 for the PanopticPool (must be a uint96 number) + /// @dev Salt used in PanopticPool creation is `[leading 20 msg.sender chars][uint80(uint256(keccak256(abi.encode(V4PoolKey, oracleContractAddress))))][salt]`. + /// @param oracleContract The external oracle contract to be used by the newly deployed Panoptic Pool + /// @param key The Uniswap V4 pool key + /// @param salt User-defined component of salt used in deployment process for the PanopticPool /// @param amount0Max The maximum amount of token0 to spend on the full-range deployment /// @param amount1Max The maximum amount of token1 to spend on the full-range deployment /// @return newPoolContract The address of the newly deployed Panoptic pool function deployNewPool( - address token0, - address token1, - uint24 fee, + IV3CompatibleOracle oracleContract, + PoolKey calldata key, uint96 salt, uint256 amount0Max, uint256 amount1Max ) external returns (PanopticPool newPoolContract) { - // sort the tokens, if necessary: - (token0, token1) = token0 < token1 ? (token0, token1) : (token1, token0); + bytes32 panopticPoolKey = keccak256(abi.encode(key, oracleContract)); - IUniswapV3Pool v3Pool = IUniswapV3Pool(UNIV3_FACTORY.getPool(token0, token1, fee)); - if (address(v3Pool) == address(0)) revert Errors.UniswapPoolNotInitialized(); + if (V4StateReader.getSqrtPriceX96(POOL_MANAGER_V4, key.toId()) == 0) + revert Errors.UniswapPoolNotInitialized(); - if (address(s_getPanopticPool[v3Pool]) != address(0)) + if (address(s_getPanopticPool[panopticPoolKey]) != address(0)) revert Errors.PoolAlreadyInitialized(); // initialize pool in SFPM if it has not already been initialized - SFPM.initializeAMMPool(token0, token1, fee); + SFPM.initializeAMMPool(key); // Users can specify a salt, the aim is to incentivize the mining of addresses with leading zeros - // salt format: (first 20 characters of deployer address) + (first 20 characters of UniswapV3Pool) + (uint96 user supplied salt) + // salt format: (first 20 characters of deployer address) + (hash of pool key and oracle contract address) + (uint96 user supplied salt) bytes32 salt32 = bytes32( abi.encodePacked( uint80(uint160(msg.sender) >> 80), - uint80(uint160(address(v3Pool)) >> 80), + uint80(uint256(panopticPoolKey)), salt ) ); - // This creates a new Panoptic Pool (proxy to the PanopticPool implementation) - newPoolContract = PanopticPool(POOL_REFERENCE.cloneDeterministic(salt32)); + // using CREATE3 for the PanopticPool given we don't know some of the immutable args (`CollateralTracker` addresses) + // this allows us to link the PanopticPool into the CollateralTrackers as an immutable arg without advance knowledge of their addresses + newPoolContract = PanopticPool(ClonesWithImmutableArgs.addressOfClone3(salt32)); // Deploy collateral token proxies CollateralTracker collateralTracker0 = CollateralTracker( - COLLATERAL_REFERENCE.cloneDeterministic(bytes32(uint256(salt32) + 1)) + COLLATERAL_REFERENCE.clone2( + abi.encodePacked( + newPoolContract, + true, + key.currency0, + key.currency0, + key.currency1, + key.fee + ) + ) ); + CollateralTracker collateralTracker1 = CollateralTracker( - COLLATERAL_REFERENCE.cloneDeterministic(bytes32(uint256(salt32) + 2)) + COLLATERAL_REFERENCE.clone2( + abi.encodePacked( + newPoolContract, + false, + key.currency1, + key.currency0, + key.currency1, + key.fee + ) + ) ); - // Run state initialization sequence for pool and collateral tokens - collateralTracker0.startToken(true, token0, token1, fee, newPoolContract); - collateralTracker1.startToken(false, token0, token1, fee, newPoolContract); + // This creates a new Panoptic Pool (proxy to the PanopticPool implementation) + newPoolContract = PanopticPool( + POOL_REFERENCE.clone3( + abi.encodePacked( + collateralTracker0, + collateralTracker1, + oracleContract, + key.toId(), + abi.encode(key) + ), + salt32 + ) + ); - newPoolContract.startPool(v3Pool, token0, token1, collateralTracker0, collateralTracker1); + newPoolContract.initialize(); + collateralTracker0.initialize(); + collateralTracker1.initialize(); - s_getPanopticPool[v3Pool] = newPoolContract; + s_getPanopticPool[panopticPoolKey] = newPoolContract; // The Panoptic pool won't be safe to use until the observation cardinality is at least CARDINALITY_INCREASE // If this is not the case, we increase the next cardinality during deployment so the cardinality can catch up over time // When that happens, there will be a period of time where the PanopticPool is deployed, but not (safely) usable - v3Pool.increaseObservationCardinalityNext(CARDINALITY_INCREASE); + oracleContract.increaseObservationCardinalityNext(CARDINALITY_INCREASE); // Mints the full-range initial deposit // which is why the deployer becomes also a "donor" of full-range liquidity - (uint256 amount0, uint256 amount1) = _mintFullRange(v3Pool, token0, token1, fee); + (uint256 amount0, uint256 amount1) = _mintFullRange(key, key.toId()); if (amount0 > amount0Max || amount1 > amount1Max) revert Errors.PriceBoundFail(); @@ -234,7 +298,8 @@ contract PanopticFactory is FactoryNFT, Multicall { emit PoolDeployed( newPoolContract, - v3Pool, + oracleContract, + key, collateralTracker0, collateralTracker1, amount0, @@ -250,7 +315,6 @@ contract PanopticFactory is FactoryNFT, Multicall { /// @dev The rarity is defined in terms of how many leading zeros the Panoptic pool address has. /// @dev Note that the final salt may overflow if too many loops are given relative to the amount in `salt`. /// @param deployerAddress Address of the account that deploys the new PanopticPool - /// @param v3Pool Address of the underlying UniswapV3Pool /// @param salt Salt value to start from, useful as a checkpoint across multiple calls /// @param loops The number of mining operations starting from `salt` in trying to find the highest rarity /// @param minTargetRarity The minimum target rarity to mine for. The internal loop stops when this is reached *or* when no more iterations @@ -258,7 +322,8 @@ contract PanopticFactory is FactoryNFT, Multicall { /// @return highestRarity The rarity of `bestSalt` function minePoolAddress( address deployerAddress, - address v3Pool, + address oracleContract, + PoolKey calldata key, uint96 salt, uint256 loops, uint256 minTargetRarity @@ -275,13 +340,13 @@ contract PanopticFactory is FactoryNFT, Multicall { bytes32 newSalt = bytes32( abi.encodePacked( uint80(uint160(deployerAddress) >> 80), - uint80(uint160(v3Pool) >> 80), + uint80(uint256(keccak256(abi.encode(key, oracleContract)))), salt ) ); uint256 rarity = PanopticMath.numberOfLeadingHexZeros( - POOL_REFERENCE.predictDeterministicAddress(newSalt) + ClonesWithImmutableArgs.addressOfClone3(newSalt) ); if (rarity > highestRarity) { @@ -304,20 +369,13 @@ contract PanopticFactory is FactoryNFT, Multicall { } } - /// @notice Seeds Uniswap V3 pool with a full-tick-range liquidity deployment using funds from caller. - /// @param v3Pool The address of the Uniswap V3 pool to deploy liquidity in - /// @param token0 The address of the first token in the Uniswap V3 pool - /// @param token1 The address of the second token in the Uniswap V3 pool - /// @param fee The fee level of the of the underlying Uniswap V3 pool, denominated in hundredths of bips + /// @notice Seeds Uniswap V4 pool with a full-tick-range liquidity deployment using funds from caller. + /// @param key The Uniswap V4 pool key + /// @param idV4 The Uniswap V4 pool id (hash of `key`) /// @return The amount of token0 deployed at full range /// @return The amount of token1 deployed at full range - function _mintFullRange( - IUniswapV3Pool v3Pool, - address token0, - address token1, - uint24 fee - ) internal returns (uint256, uint256) { - (uint160 currentSqrtPriceX96, , , , , , ) = v3Pool.slot0(); + function _mintFullRange(PoolKey memory key, PoolId idV4) internal returns (uint256, uint256) { + uint160 currentSqrtPriceX96 = V4StateReader.getSqrtPriceX96(POOL_MANAGER_V4, idV4); // For full range: L = Δx * sqrt(P) = Δy / sqrt(P) // We start with fixed token amounts and apply this equation to calculate the liquidity @@ -327,11 +385,11 @@ contract PanopticFactory is FactoryNFT, Multicall { uint128 fullRangeLiquidity; unchecked { // Since we know one of the tokens is WETH, we simply add 0.1 ETH + worth in tokens - if (token0 == WETH) { + if (Currency.unwrap(key.currency0) == WETH) { fullRangeLiquidity = uint128( Math.mulDiv96RoundingUp(FULL_RANGE_LIQUIDITY_AMOUNT_WETH, currentSqrtPriceX96) ); - } else if (token1 == WETH) { + } else if (Currency.unwrap(key.currency1) == WETH) { fullRangeLiquidity = uint128( Math.mulDivRoundingUp( FULL_RANGE_LIQUIDITY_AMOUNT_WETH, @@ -364,28 +422,21 @@ contract PanopticFactory is FactoryNFT, Multicall { // tickSpacing = 10: tU/L = +/-887270 // tickSpacing = 60: tU/L = +/-887220 // tickSpacing = 200: tU/L = +/-887200 + // etc... int24 tickLower; int24 tickUpper; unchecked { - int24 tickSpacing = v3Pool.tickSpacing(); - tickLower = (Constants.MIN_V3POOL_TICK / tickSpacing) * tickSpacing; + int24 tickSpacing = key.tickSpacing; + tickLower = (Constants.MIN_V4POOL_TICK / tickSpacing) * tickSpacing; tickUpper = -tickLower; } - bytes memory mintCallback = abi.encode( - CallbackLib.CallbackData({ - poolFeatures: CallbackLib.PoolFeatures({token0: token0, token1: token1, fee: fee}), - payer: msg.sender - }) - ); - return - IUniswapV3Pool(v3Pool).mint( - address(this), - tickLower, - tickUpper, - fullRangeLiquidity, - mintCallback + abi.decode( + POOL_MANAGER_V4.unlock( + abi.encode(key, tickLower, tickUpper, fullRangeLiquidity, msg.sender) + ), + (uint256, uint256) ); } @@ -393,10 +444,14 @@ contract PanopticFactory is FactoryNFT, Multicall { QUERIES //////////////////////////////////////////////////////////////*/ - /// @notice Return the address of the Panoptic Pool associated with `univ3pool`. - /// @param univ3pool The Uniswap V3 pool address to query - /// @return Address of the Panoptic Pool associated with `univ3pool` - function getPanopticPool(IUniswapV3Pool univ3pool) external view returns (PanopticPool) { - return s_getPanopticPool[univ3pool]; + /// @notice Return the address of the Panoptic Pool associated with the given Uniswap V4 pool key and oracle contract. + /// @param keyV4 The Uniswap V4 pool key + /// @param oracleContract The external oracle contract used by the Panoptic Pool + /// @return Address of the Panoptic Pool on `keyV4` using `oracleContract` + function getPanopticPool( + PoolKey calldata keyV4, + IV3CompatibleOracle oracleContract + ) external view returns (PanopticPool) { + return s_getPanopticPool[keccak256(abi.encode(keyV4, oracleContract))]; } } diff --git a/contracts/PanopticPool.sol b/contracts/PanopticPool.sol index 9b79ab5..5527fb4 100644 --- a/contracts/PanopticPool.sol +++ b/contracts/PanopticPool.sol @@ -4,26 +4,30 @@ 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"; +import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol"; +import {IV3CompatibleOracle} from "@interfaces/IV3CompatibleOracle.sol"; // Inherited implementations +import {Clone} from "clones-with-immutable-args/Clone.sol"; import {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"; +import {V4StateReader} from "@libraries/V4StateReader.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"; +import {PoolKey} from "v4-core/types/PoolKey.sol"; +import {PoolId} from "v4-core/types/PoolId.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 { +contract PanopticPool is Clone, ERC1155Holder, Multicall { /*////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////*/ @@ -93,10 +97,10 @@ contract PanopticPool is ERC1155Holder, Multicall { //////////////////////////////////////////////////////////////*/ /// @notice Lower price bound used when no slippage check is required. - int24 internal constant MIN_SWAP_TICK = Constants.MIN_V3POOL_TICK - 1; + int24 internal constant MIN_SWAP_TICK = Constants.MIN_V4POOL_TICK - 1; /// @notice Upper price bound used when no slippage check is required. - int24 internal constant MAX_SWAP_TICK = Constants.MAX_V3POOL_TICK + 1; + int24 internal constant MAX_SWAP_TICK = Constants.MAX_V4POOL_TICK + 1; /// @notice Flag that signals to compute premia for both the short and long legs of a position. bool internal constant COMPUTE_ALL_PREMIA = true; @@ -148,13 +152,12 @@ contract PanopticPool is ERC1155Holder, Multicall { /// @notice The "engine" of Panoptic - manages AMM liquidity and executes all mints/burns/exercises. SemiFungiblePositionManager internal immutable SFPM; + /// @notice The canonical Uniswap V4 Pool Manager address. + IPoolManager internal immutable POOL_MANAGER_V4; + /*////////////////////////////////////////////////////////////// STORAGE //////////////////////////////////////////////////////////////*/ - - /// @notice 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) @@ -170,22 +173,22 @@ contract PanopticPool is ERC1155Holder, Multicall { // slot: [7] [5] [3] [1] [0] [2] [4] [6] // 111 101 011 001 000 010 100 110 // - // [Constants.MIN_V3POOL_TICK-1] [7] + // [Constants.MIN_V4POOL_TICK-1] [7] // 111100100111011000010111 // - // [Constants.MAX_V3POOL_TICK+1] [0] + // [Constants.MAX_V4POOL_TICK+1] [0] // 000011011000100111101001 // - // [Constants.MIN_V3POOL_TICK-1] [6] + // [Constants.MIN_V4POOL_TICK-1] [6] // 111100100111011000010111 // - // [Constants.MAX_V3POOL_TICK+1] [1] + // [Constants.MAX_V4POOL_TICK+1] [1] // 000011011000100111101001 // - // [Constants.MIN_V3POOL_TICK-1] [5] + // [Constants.MIN_V4POOL_TICK-1] [5] // 111100100111011000010111 // - // [Constants.MAX_V3POOL_TICK+1] [2] + // [Constants.MAX_V4POOL_TICK+1] [2] // 000011011000100111101001 // // [CURRENT TICK] [4] @@ -195,15 +198,6 @@ contract PanopticPool is ERC1155Holder, Multicall { // (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). @@ -240,37 +234,69 @@ contract PanopticPool is ERC1155Holder, Multicall { // |<---------------------- 256 bits ------------------------------>| mapping(address account => uint256 positionsHash) internal s_positionsHash; + /*////////////////////////////////////////////////////////////// + POOL-SPECIFIC IMMUTABLE PARAMETERS + //////////////////////////////////////////////////////////////*/ + + // The parameters will be encoded in calldata at `_getImmutableArgsOffset()` as follows: + // abi.encodePacked(address collateralToken0, address collateralToken1, address oracleContract, uint256 poolId, abi.encode(PoolKey poolKey)) + // bytes: 0 20 40 60 92 + // |<---- 160 bits ---->|<---- 160 bits ---->|<---- 160 bits ---->|<---- 256 bits ---->|<---- 1280 bits ---->| + // collateralToken0 collateralToken1 oracleContract poolId poolKey + + /// @notice Get the collateral token corresponding to token0 of the Uniswap pool. + /// @return Collateral token corresponding to token0 in Uniswap + function collateralToken0() public pure returns (CollateralTracker) { + return CollateralTracker(_getArgAddress(0)); + } + + /// @notice Get the collateral token corresponding to token1 of the Uniswap pool. + /// @return Collateral token corresponding to token1 in Uniswap + function collateralToken1() public pure returns (CollateralTracker) { + return CollateralTracker(_getArgAddress(20)); + } + + /// @notice Get the address of the external oracle contract used by this Panoptic Pool. + /// @return The external oracle contract used by this Panoptic Pool + function oracleContract() public pure returns (IV3CompatibleOracle) { + return IV3CompatibleOracle(_getArgAddress(40)); + } + + /// @notice Get the Uniswap Pool ID for the V4 pool used by this Panoptic Pool (hash of `poolKey`). + /// @return The Uniswap V4 Pool ID for this Panoptic Pool + function _V4PoolId() internal pure returns (PoolId) { + return PoolId.wrap(bytes32(_getArgUint256(60))); + } + + /// @notice Get the pool key for the Uniswap V4 pool used by this Panoptic Pool. + /// @return key The Uniswap V4 Pool Key for this Panoptic Pool + function poolKey() public pure returns (PoolKey calldata key) { + uint256 offset = _getImmutableArgsOffset(); + + assembly { + key := add(offset, 92) + } + } + /*////////////////////////////////////////////////////////////// INITIALIZATION //////////////////////////////////////////////////////////////*/ - /// @notice Store the address of the canonical SemiFungiblePositionManager (SFPM) contract. + /// @notice Store the address of the canonical SemiFungiblePositionManager (SFPM) and Uniswap V4 pool manager contracts. /// @param _sfpm The address of the SFPM - constructor(SemiFungiblePositionManager _sfpm) { + /// @param _poolManager The address of the canonical Uniswap V4 pool manager + constructor(SemiFungiblePositionManager _sfpm, IPoolManager _poolManager) { SFPM = _sfpm; + POOL_MANAGER_V4 = _poolManager; } - /// @notice Initializes 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); + /// @notice Initializes the median oracle of a new `PanopticPool` instance with median oracle state and performs initial token approvals. + /// @dev Must be called first (by the factory contract) before any transaction can occur. + function initialize() external { + // reverts if this contract has already been initialized (assuming block.timestamp > 0) + if (s_miniMedian != 0) revert Errors.PoolAlreadyInitialized(); - (, int24 currentTick, , , , , ) = IUniswapV3Pool(_univ3pool).slot0(); + (, int24 currentTick, , , , , ) = oracleContract().slot0(); // Store the median data unchecked { @@ -283,16 +309,9 @@ contract PanopticPool is ERC1155Holder, Multicall { (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); + POOL_MANAGER_V4.setOperator(address(SFPM), true); + POOL_MANAGER_V4.setOperator(address(collateralToken0()), true); + POOL_MANAGER_V4.setOperator(address(collateralToken1()), true); } /*////////////////////////////////////////////////////////////// @@ -304,8 +323,8 @@ contract PanopticPool is ERC1155Holder, Multicall { /// @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; + CollateralTracker ct0 = collateralToken0(); + CollateralTracker ct1 = collateralToken1(); if ( ct0.convertToAssets(ct0.balanceOf(msg.sender)) < minValue0 || ct1.convertToAssets(ct1.balanceOf(msg.sender)) < minValue1 @@ -338,7 +357,7 @@ contract PanopticPool is ERC1155Holder, Multicall { TokenId[] calldata positionIdList ) external view returns (LeftRightUnsigned, LeftRightUnsigned, uint256[2][] memory) { // Get the current tick of the Uniswap pool - (, int24 currentTick, , , , , ) = s_univ3pool.slot0(); + int24 currentTick = V4StateReader.getTick(POOL_MANAGER_V4, _V4PoolId()); // Compute the accumulated premia for all tokenId in positionIdList (includes short+long premium) return @@ -453,16 +472,17 @@ contract PanopticPool is ERC1155Holder, Multicall { ONBOARD MEDIAN TWAP //////////////////////////////////////////////////////////////*/ - /// @notice Updates the internal median with the last Uniswap observation if the `MEDIAN_PERIOD` has elapsed. + /// @notice Updates the internal median with the last oracle observation if the `MEDIAN_PERIOD` has elapsed. function pokeMedian() external { - (, , uint16 observationIndex, uint16 observationCardinality, , , ) = s_univ3pool.slot0(); + (, , uint16 observationIndex, uint16 observationCardinality, , , ) = oracleContract() + .slot0(); (, uint256 medianData) = PanopticMath.computeInternalMedian( observationIndex, observationCardinality, Constants.MEDIAN_PERIOD, s_miniMedian, - s_univ3pool + oracleContract() ); if (medianData != 0) s_miniMedian = medianData; @@ -567,7 +587,7 @@ contract PanopticPool is ERC1155Holder, Multicall { _validatePositionList(msg.sender, positionIdList, 1); // make sure the tokenId is for this Panoptic pool - if (tokenId.poolId() != SFPM.getPoolId(address(s_univ3pool))) + if (tokenId.poolId() != SFPM.getPoolId(_V4PoolId())) revert Errors.InvalidTokenIdParameter(0); // disallow user to mint exact same position @@ -584,13 +604,14 @@ contract PanopticPool is ERC1155Holder, Multicall { tickLimitHigh ); + int24 currentTick = V4StateReader.getTick(POOL_MANAGER_V4, _V4PoolId()); + ( - int24 currentTick, int24 fastOracleTick, int24 slowOracleTick, int24 lastObservedTick, uint256 medianData - ) = PanopticMath.getOracleTicks(s_univ3pool, s_miniMedian); + ) = PanopticMath.getOracleTicks(oracleContract(), s_miniMedian); uint96 tickData = PositionBalanceLibrary.packTickData( currentTick, @@ -643,7 +664,7 @@ contract PanopticPool is ERC1155Holder, Multicall { } (LeftRightUnsigned[4] memory collectedByLeg, LeftRightSigned totalSwapped) = SFPM - .mintTokenizedPosition(tokenId, positionSize, tickLimitLow, tickLimitHigh); + .mintTokenizedPosition(poolKey(), tokenId, positionSize, tickLimitLow, tickLimitHigh); _updateSettlementPostMint( tokenId, @@ -684,14 +705,14 @@ contract PanopticPool is ERC1155Holder, Multicall { (LeftRightSigned longAmounts, LeftRightSigned shortAmounts) = PanopticMath .computeExercisedAmounts(tokenId, positionSize); - uint32 utilization0 = s_collateralToken0.takeCommissionAddData( + uint32 utilization0 = collateralToken0().takeCommissionAddData( msg.sender, longAmounts.rightSlot(), shortAmounts.rightSlot(), totalSwapped.rightSlot(), isCovered ); - uint32 utilization1 = s_collateralToken1.takeCommissionAddData( + uint32 utilization1 = collateralToken1().takeCommissionAddData( msg.sender, longAmounts.leftSlot(), shortAmounts.leftSlot(), @@ -785,15 +806,14 @@ contract PanopticPool is ERC1155Holder, Multicall { uint256 buffer ) internal view returns (uint256) { ( - int24 currentTick, int24 fastOracleTick, int24 slowOracleTick, int24 lastObservedTick, uint256 medianData - ) = PanopticMath.getOracleTicks(s_univ3pool, s_miniMedian); + ) = PanopticMath.getOracleTicks(oracleContract(), s_miniMedian); uint96 tickData = PositionBalanceLibrary.packTickData( - currentTick, + V4StateReader.getTick(POOL_MANAGER_V4, _V4PoolId()), fastOracleTick, slowOracleTick, lastObservedTick @@ -832,20 +852,22 @@ contract PanopticPool is ERC1155Holder, Multicall { // (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; + unchecked { + if ( + int256(fastOracleTick - slowOracleTick) ** 2 + + int256(lastObservedTick - slowOracleTick) ** 2 + + int256(currentTick - slowOracleTick) ** 2 > + MAX_TICKS_DELTA ** 2 + ) { + atTicks = new int24[](4); + atTicks[0] = fastOracleTick; + atTicks[1] = slowOracleTick; + atTicks[2] = lastObservedTick; + atTicks[3] = currentTick; + } else { + atTicks = new int24[](1); + atTicks[0] = fastOracleTick; + } } _checkSolvencyAtTicks(user, positionIdList, currentTick, atTicks, buffer, ASSERT_SOLVENCY); @@ -884,7 +906,7 @@ contract PanopticPool is ERC1155Holder, Multicall { } (LeftRightUnsigned[4] memory collectedByLeg, LeftRightSigned totalSwapped) = SFPM - .burnTokenizedPosition(tokenId, positionSize, tickLimitLow, tickLimitHigh); + .burnTokenizedPosition(poolKey(), tokenId, positionSize, tickLimitLow, tickLimitHigh); (realizedPremia, premiaByLeg) = _updateSettlementPostBurn( owner, @@ -898,7 +920,7 @@ contract PanopticPool is ERC1155Holder, Multicall { .computeExercisedAmounts(tokenId, positionSize); { - int128 paid0 = s_collateralToken0.exercise( + int128 paid0 = collateralToken0().exercise( owner, longAmounts.rightSlot(), shortAmounts.rightSlot(), @@ -909,7 +931,7 @@ contract PanopticPool is ERC1155Holder, Multicall { } { - int128 paid1 = s_collateralToken1.exercise( + int128 paid1 = collateralToken1().exercise( owner, longAmounts.leftSlot(), shortAmounts.leftSlot(), @@ -937,15 +959,15 @@ contract PanopticPool is ERC1155Holder, Multicall { _validatePositionList(liquidatee, positionIdList, 0); // Assert the account we are liquidating is actually insolvent - int24 twapTick = getUniV3TWAP(); + int24 twapTick = getOracleTWAP(); - int24 currentTick; + int24 currentTick = V4StateReader.getTick(POOL_MANAGER_V4, _V4PoolId()); { // Enforce maximum delta between TWAP and currentTick to prevent extreme price manipulation int24 fastOracleTick; int24 lastObservedTick; - (currentTick, fastOracleTick, , lastObservedTick, ) = PanopticMath.getOracleTicks( - s_univ3pool, + (fastOracleTick, , lastObservedTick, ) = PanopticMath.getOracleTicks( + oracleContract(), s_miniMedian ); @@ -985,7 +1007,7 @@ contract PanopticPool is ERC1155Holder, Multicall { currentTick ); - tokenData0 = s_collateralToken0.getAccountMarginDetails( + tokenData0 = collateralToken0().getAccountMarginDetails( liquidatee, twapTick, positionBalanceArray, @@ -993,7 +1015,7 @@ contract PanopticPool is ERC1155Holder, Multicall { longPremium.rightSlot() ); - tokenData1 = s_collateralToken1.getAccountMarginDetails( + tokenData1 = collateralToken1().getAccountMarginDetails( liquidatee, twapTick, positionBalanceArray, @@ -1003,8 +1025,8 @@ contract PanopticPool is ERC1155Holder, Multicall { } // The protocol delegates some virtual shares to ensure the burn can be settled. - s_collateralToken0.delegate(liquidatee); - s_collateralToken1.delegate(liquidatee); + collateralToken0().delegate(liquidatee); + collateralToken1().delegate(liquidatee); LeftRightSigned bonusAmounts; { @@ -1050,8 +1072,8 @@ contract PanopticPool is ERC1155Holder, Multicall { _positionIdList, premiasByLeg, collateralRemaining, - s_collateralToken0, - s_collateralToken1, + collateralToken0(), + collateralToken1(), Math.getSqrtRatioAtTick(_twapTick), s_settledTokens ); @@ -1060,8 +1082,8 @@ contract PanopticPool is ERC1155Holder, Multicall { } // 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()); + collateralToken0().settleLiquidation(msg.sender, liquidatee, bonusAmounts.rightSlot()); + collateralToken1().settleLiquidation(msg.sender, liquidatee, bonusAmounts.leftSlot()); // ensure the liquidator is still solvent after the liquidation _validateSolvency(msg.sender, positionIdListLiquidator, NO_BUFFER); @@ -1088,15 +1110,13 @@ contract PanopticPool is ERC1155Holder, Multicall { // 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(); + int24 twapTick = getOracleTWAP(); // to be eligible for force exercise, the price *must* be outside the position's range for at least 1 leg touchedId[0].validateIsExercisable(twapTick); LeftRightSigned exerciseFees; { - (, int24 currentTick, , , , , ) = s_univ3pool.slot0(); - uint128 positionSize = s_positionBalance[account][touchedId[0]].positionSize(); (LeftRightSigned longAmounts, ) = PanopticMath.computeExercisedAmounts( @@ -1106,8 +1126,8 @@ contract PanopticPool is ERC1155Holder, Multicall { // 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, + exerciseFees = collateralToken0().exerciseCost( + V4StateReader.getTick(POOL_MANAGER_V4, _V4PoolId()), twapTick, touchedId[0], positionSize, @@ -1116,8 +1136,8 @@ contract PanopticPool is ERC1155Holder, Multicall { } // The protocol delegates some virtual shares to ensure the burn can be settled. - s_collateralToken0.delegate(account); - s_collateralToken1.delegate(account); + collateralToken0().delegate(account); + collateralToken1().delegate(account); // Exercise the option // Turn off ITM swapping to prevent swap at potentially unfavorable price @@ -1128,17 +1148,17 @@ contract PanopticPool is ERC1155Holder, Multicall { account, exerciseFees, twapTick, - s_collateralToken0, - s_collateralToken1 + collateralToken0(), + 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()); + collateralToken0().refund(account, msg.sender, refundAmounts.rightSlot()); + 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); + collateralToken0().revoke(account); + collateralToken1().revoke(account); _validateSolvency(account, positionIdListExercisee, NO_BUFFER); @@ -1227,14 +1247,14 @@ contract PanopticPool is ERC1155Holder, Multicall { LeftRightUnsigned longPremium, uint256 buffer ) internal view returns (bool) { - LeftRightUnsigned tokenData0 = s_collateralToken0.getAccountMarginDetails( + LeftRightUnsigned tokenData0 = collateralToken0().getAccountMarginDetails( account, atTick, positionBalanceArray, shortPremium.rightSlot(), longPremium.rightSlot() ); - LeftRightUnsigned tokenData1 = s_collateralToken1.getAccountMarginDetails( + LeftRightUnsigned tokenData1 = collateralToken1().getAccountMarginDetails( account, atTick, positionBalanceArray, @@ -1255,8 +1275,6 @@ contract PanopticPool is ERC1155Holder, Multicall { /// @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( @@ -1264,7 +1282,9 @@ contract PanopticPool is ERC1155Holder, Multicall { ) + 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; + return + Math.abs(V4StateReader.getTick(POOL_MANAGER_V4, _V4PoolId()) - medianTick) > + MAX_TICKS_DELTA; } } @@ -1331,29 +1351,11 @@ contract PanopticPool is ERC1155Holder, Multicall { 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 Uniswap observations or tracked by `s_miniMedian`) - /// @return latestObservation The latest observation from the Uniswap pool + /// @notice Computes and returns all ticks used for collateral checks at mint/burn. + /// @return currentTick The current tick of the Uniswap V4 pool + /// @return fastOracleTick The fast oracle tick computed as the median of the past N observations in the oracle contract + /// @return slowOracleTick The slow oracle tick (either composed of oracle observations or tracked by `s_miniMedian`) + /// @return latestObservation The latest observation from the oracle contract /// @return medianData The updated value for `s_miniMedian` (0 if `MEDIAN_PERIOD` not elapsed) if `pokeMedian` is called at the current state function getOracleTicks() external @@ -1366,8 +1368,12 @@ contract PanopticPool is ERC1155Holder, Multicall { uint256 medianData ) { - (currentTick, fastOracleTick, slowOracleTick, latestObservation, ) = PanopticMath - .getOracleTicks(s_univ3pool, s_miniMedian); + currentTick = V4StateReader.getTick(POOL_MANAGER_V4, _V4PoolId()); + + (fastOracleTick, slowOracleTick, latestObservation, ) = PanopticMath.getOracleTicks( + oracleContract(), + s_miniMedian + ); medianData = s_miniMedian; } @@ -1397,8 +1403,8 @@ contract PanopticPool is ERC1155Holder, Multicall { /// @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); + function getOracleTWAP() internal view returns (int24 twapTick) { + twapTick = PanopticMath.twapFilter(oracleContract(), TWAP_WINDOW); } /*////////////////////////////////////////////////////////////// @@ -1470,7 +1476,7 @@ contract PanopticPool is ERC1155Holder, Multicall { (premiumAccumulatorsByLeg[leg][0], premiumAccumulatorsByLeg[leg][1]) = SFPM .getAccountPremium( - address(s_univ3pool), + _V4PoolId(), address(this), tokenType, liquidityChunk.tickLower(), @@ -1541,13 +1547,13 @@ contract PanopticPool is ERC1155Holder, Multicall { s_positionBalance[owner][tokenId].positionSize() ); - (, int24 currentTick, , , , , ) = s_univ3pool.slot0(); + int24 currentTick = V4StateReader.getTick(POOL_MANAGER_V4, _V4PoolId()); LeftRightUnsigned accumulatedPremium; { uint256 tokenType = tokenId.tokenType(legIndex); (uint128 premiumAccumulator0, uint128 premiumAccumulator1) = SFPM.getAccountPremium( - address(s_univ3pool), + _V4PoolId(), address(this), tokenType, liquidityChunk.tickLower(), @@ -1577,8 +1583,8 @@ contract PanopticPool is ERC1155Holder, Multicall { .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()); + collateralToken0().exercise(owner, 0, 0, 0, -realizedPremia.rightSlot()); + collateralToken1().exercise(owner, 0, 0, 0, -realizedPremia.leftSlot()); bytes32 chunkKey = keccak256( abi.encodePacked( @@ -1638,7 +1644,7 @@ contract PanopticPool is ERC1155Holder, Multicall { 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), + _V4PoolId(), address(this), tokenType, liquidityChunk.tickLower(), @@ -1776,7 +1782,7 @@ contract PanopticPool is ERC1155Holder, Multicall { (int24 tickLower, int24 tickUpper) = tokenId.asTicks(leg); LeftRightUnsigned accountLiquidities = SFPM.getAccountLiquidity( - address(s_univ3pool), + _V4PoolId(), address(this), tokenId.tokenType(leg), tickLower, diff --git a/contracts/SemiFungiblePositionManager.sol b/contracts/SemiFungiblePositionManager.sol index 86c303c..9f53a24 100644 --- a/contracts/SemiFungiblePositionManager.sol +++ b/contracts/SemiFungiblePositionManager.sol @@ -2,24 +2,26 @@ pragma solidity ^0.8.24; // Interfaces -import {IUniswapV3Factory} from "univ3-core/interfaces/IUniswapV3Factory.sol"; -import {IUniswapV3Pool} from "univ3-core/interfaces/IUniswapV3Pool.sol"; +import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol"; // Inherited implementations import {ERC1155} from "@tokens/ERC1155Minimal.sol"; import {Multicall} from "@base/Multicall.sol"; -import {TransientReentrancyGuard} from "solmate/utils/TransientReentrancyGuard.sol"; +import {TransientReentrancyGuard} from "solmate/src/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"; +import {V4StateReader} from "@libraries/V4StateReader.sol"; // Custom types import {LeftRightUnsigned, LeftRightSigned, LeftRightLibrary} from "@types/LeftRight.sol"; import {LiquidityChunk} from "@types/LiquidityChunk.sol"; import {TokenId} from "@types/TokenId.sol"; +// V4 types +import {PoolId} from "v4-core/types/PoolId.sol"; +import {PoolKey} from "v4-core/types/PoolKey.sol"; +import {BalanceDelta} from "v4-core/types/BalanceDelta.sol"; +import {Currency} from "v4-core/types/Currency.sol"; // .......... // ,. .,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,. ,, @@ -67,18 +69,18 @@ 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. +/// @title Semi-Fungible Position Manager (ERC1155) - a gas-efficient Uniswap V4 position manager. +/// @notice Wraps Uniswap V4 positions with up to 4 legs behind an ERC1155 token. /// @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 + /// @notice Emitted when a Uniswap V4 pool is initialized in the SFPM. + /// @param poolKeyV4 The Uniswap V4 pool key /// @param poolId The SFPM's pool identifier for the pool, including the 16-bit tick spacing and 48-bit pool pattern - event PoolInitialized(address indexed uniswapPool, uint64 poolId); + event PoolInitialized(PoolKey indexed poolKeyV4, uint64 poolId); /// @notice Emitted when a position is destroyed/burned. /// @param recipient The address of the user who burned the position @@ -125,20 +127,19 @@ contract SemiFungiblePositionManager is ERC1155, Multicall, TransientReentrancyG // 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 canonical Uniswap V4 Pool Manager address. + IPoolManager internal immutable POOL_MANAGER_V4; /*////////////////////////////////////////////////////////////// STORAGE //////////////////////////////////////////////////////////////*/ - /// @notice Retrieve the corresponding poolId for a given Uniswap V3 pool address. + /// @notice Retrieve the corresponding SFPM poolId for a given Uniswap V4 poolId. /// @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; + mapping(PoolId idV4 => uint256 poolIdData) internal s_V4toSFPMIdData; - /// @notice Retrieve the Uniswap V3 pool address corresponding to a given poolId. - mapping(uint64 poolId => IUniswapV3Pool pool) internal s_poolIdToAddr; + /// @notice Retrieve the Uniswap V4 pool key corresponding to a given poolId. + mapping(uint64 poolId => PoolKey key) internal s_poolIdToKey; /* We're tracking the amount of net and removed liquidity for the specific region: @@ -147,8 +148,9 @@ contract SemiFungiblePositionManager is ERC1155, Multicall, TransientReentrancyG received minted ▲ for isLong=0 amount │ moved out actual amount - │ ┌────┐-T due isLong=1 in the UniswapV3Pool - │ │ │ mints + │ ┌────┐-T due isLong=1 in the Uniswap V4 + │ │ │ mints pool + │ │ │ │ │ │ ┌────┐-(T-R) │ │ │ ┌────┐-R │ │ │ │ │ │ │ │ │ @@ -280,127 +282,124 @@ contract SemiFungiblePositionManager is ERC1155, Multicall, TransientReentrancyG /// @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 - constructor(IUniswapV3Factory _factory) { - FACTORY = _factory; + /// @notice Set the canonical Uniswap V4 pool manager address. + /// @param poolManager The canonical Uniswap V4 pool manager address + constructor(IPoolManager poolManager) { + POOL_MANAGER_V4 = poolManager; } - /// @notice Initialize a Uniswap V3 pool in the SFPM. + /// @notice Initialize a Uniswap V4 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 { - // compute the address of the Uniswap V3 pool for the given token0, token1, and fee tier - address univ3pool = FACTORY.getPool(token0, token1, fee); + /// @param key An identifying key for a Uniswap V4 pool + function initializeAMMPool(PoolKey calldata key) external { + PoolId idV4 = key.toId(); - // reverts if the Uniswap V3 pool has not been initialized - if (univ3pool == address(0)) revert Errors.UniswapPoolNotInitialized(); + if (V4StateReader.getSqrtPriceX96(POOL_MANAGER_V4, idV4) == 0) + revert Errors.UniswapPoolNotInitialized(); // return if the pool has already been initialized in SFPM // pools can be initialized from the Panoptic Factory or by calling initializeAMMPool directly, so reverting // could prevent a PanopticPool from being deployed on a previously initialized but otherwise valid pool // if 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; + if (s_V4toSFPMIdData[idV4] != 0) return; // 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); + // [16 bit tickSpacing][most significant 48 bits of the V4 poolId] + uint64 poolId = PanopticMath.getPoolId(idV4, key.tickSpacing); // There are 281,474,976,710,655 possible pool patterns. // A modern GPU can generate a collision in such a space relatively quickly, // so if a collision is detected increment the pool pattern until a unique poolId is found - while (address(s_poolIdToAddr[poolId]) != address(0)) { + while (s_poolIdToKey[poolId].tickSpacing != 0) { poolId = PanopticMath.incrementPoolPattern(poolId); } - s_poolIdToAddr[poolId] = IUniswapV3Pool(univ3pool); + s_poolIdToKey[poolId] = key; // 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; + s_V4toSFPMIdData[idV4] = uint256(poolId) + 2 ** 255; } - emit PoolInitialized(univ3pool, poolId); + emit PoolInitialized(key, poolId); } /*////////////////////////////////////////////////////////////// - CALLBACK HANDLERS + UNISWAP V4 LOCK CALLBACK //////////////////////////////////////////////////////////////*/ - /// @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 Executes the corresponding operations and state updates required to mint `tokenId` of `positionSize` in `key` + /// @param key The Uniswap V4 pool key in which to mint `tokenId` + /// @param tickLimitLow The lower bound of an acceptable open interval for the ending price + /// @param tickLimitHigh The upper bound of an acceptable open interval for the ending price + /// @param positionSize The number of contracts minted, expressed in terms of the asset + /// @param tokenId The tokenId of the minted position, which encodes information about up to 4 legs + /// @param isBurn Flag indicating if the position is being burnt + /// @return An array of LeftRight encoded words containing the amount of token0 and token1 collected as fees for each leg + /// @return The net amount of token0 and token1 moved to/from the Uniswap V4 pool + function _unlockAndCreatePositionInAMM( + PoolKey calldata key, + int24 tickLimitLow, + int24 tickLimitHigh, + uint128 positionSize, + TokenId tokenId, + bool isBurn + ) internal returns (LeftRightUnsigned[4] memory, LeftRightSigned) { + return + abi.decode( + POOL_MANAGER_V4.unlock( + abi.encode( + msg.sender, + key, + tickLimitLow, + tickLimitHigh, + positionSize, + tokenId, + isBurn + ) + ), + (LeftRightUnsigned[4], LeftRightSigned) ); } - /// @notice 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); + /// @notice Uniswap V4 unlock callback implementation. + /// @dev Parameters are `(PoolKey key, int24 tickLimitLow, int24 tickLimitHigh, uint128 positionSize, TokenId tokenId, bool isBurn)`. + /// @dev Executes the corresponding operations and state updates required to mint `tokenId` of `positionSize` in `key` + /// @dev (shorts/longs are reversed before calling this function at burn) + /// @param data The encoded data containing the input parameters + /// @return `(LeftRightUnsigned[4] collectedByLeg, LeftRightSigned totalMoved)` An array of LeftRight encoded words containing the amount of token0 and token1 collected as fees for each leg and the net amount of token0 and token1 moved to/from the Uniswap V4 pool + function unlockCallback(bytes calldata data) external returns (bytes memory) { + if (msg.sender != address(POOL_MANAGER_V4)) revert Errors.UnauthorizedUniswapCallback(); + + ( + address account, + PoolKey memory key, + int24 tickLimitLow, + int24 tickLimitHigh, + uint128 positionSize, + TokenId tokenId, + bool isBurn + ) = abi.decode(data, (address, PoolKey, int24, int24, uint128, TokenId, bool)); + + ( + LeftRightUnsigned[4] memory collectedByLeg, + LeftRightSigned totalMoved + ) = _createPositionInAMM( + account, + key, + tickLimitLow, + tickLimitHigh, + positionSize, + tokenId, + isBurn + ); + return abi.encode(collectedByLeg, totalMoved); } /*////////////////////////////////////////////////////////////// @@ -409,14 +408,15 @@ contract SemiFungiblePositionManager is ERC1155, Multicall, TransientReentrancyG /// @notice Burn a new position containing up to 4 legs wrapped in a ERC1155 token. /// @dev Auto-collect all accumulated fees. + /// @param key The Uniswap V4 pool key in which to burn `tokenId` /// @param tokenId The tokenId of the minted position, which encodes information about up to 4 legs /// @param positionSize The number of contracts minted, expressed in terms of the asset /// @param slippageTickLimitLow The lower bound of an acceptable open interval for the ending price /// @param slippageTickLimitHigh The upper bound of an acceptable open interval for the ending price /// @return An array of LeftRight encoded words containing the amount of token0 and token1 collected as fees for each leg - /// @return The net amount of token0 and token1 moved to/from the Uniswap V3 pool - + /// @return The net amount of token0 and token1 moved to/from the Uniswap V4 pool function burnTokenizedPosition( + PoolKey calldata key, TokenId tokenId, uint128 positionSize, int24 slippageTickLimitLow, @@ -424,10 +424,15 @@ contract SemiFungiblePositionManager is ERC1155, Multicall, TransientReentrancyG ) external nonReentrant returns (LeftRightUnsigned[4] memory, LeftRightSigned) { _burn(msg.sender, TokenId.unwrap(tokenId), positionSize); + uint256 sfpmId = s_V4toSFPMIdData[key.toId()]; + if (uint64(sfpmId) != tokenId.poolId() || sfpmId == 0) + revert Errors.InvalidTokenIdParameter(0); + emit TokenizedPositionBurnt(msg.sender, tokenId, positionSize); return - _createPositionInAMM( + _unlockAndCreatePositionInAMM( + key, slippageTickLimitLow, slippageTickLimitHigh, positionSize, @@ -437,14 +442,15 @@ contract SemiFungiblePositionManager is ERC1155, Multicall, TransientReentrancyG } /// @notice Create a new position `tokenId` containing up to 4 legs. + /// @param key The Uniswap V4 pool key in which to `tokenId` /// @param tokenId The tokenId of the minted position, which encodes information for up to 4 legs /// @param positionSize The number of contracts minted, expressed in terms of the asset /// @param slippageTickLimitLow The lower bound of an acceptable open interval for the ending price /// @param slippageTickLimitHigh The upper bound of an acceptable open interval for the ending price /// @return An array of LeftRight encoded words containing the amount of token0 and token1 collected as fees for each leg - /// @return The net amount of token0 and token1 moved to/from the Uniswap V3 pool - + /// @return The net amount of token0 and token1 moved to/from the Uniswap V4 pool function mintTokenizedPosition( + PoolKey calldata key, TokenId tokenId, uint128 positionSize, int24 slippageTickLimitLow, @@ -457,8 +463,13 @@ contract SemiFungiblePositionManager is ERC1155, Multicall, TransientReentrancyG // verify that the tokenId is correctly formatted and conforms to all enforced constraints tokenId.validate(); + uint256 sfpmId = s_V4toSFPMIdData[key.toId()]; + if (uint64(sfpmId) != tokenId.poolId() || sfpmId == 0) + revert Errors.InvalidTokenIdParameter(0); + return - _createPositionInAMM( + _unlockAndCreatePositionInAMM( + key, slippageTickLimitLow, slippageTickLimitHigh, positionSize, @@ -471,106 +482,26 @@ contract SemiFungiblePositionManager is ERC1155, Multicall, TransientReentrancyG 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 + /// @notice All ERC1155 transfers are disabled. 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); + address, + address, + uint256, + uint256, + bytes calldata + ) public pure override { + revert(); } - /// @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 + /// @notice All ERC1155 transfers are disabled. 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_poolIdToAddr[id.poolId()]; - - 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; - } - } + address, + address, + uint256[] calldata, + uint256[] calldata, + bytes calldata + ) public pure override { + revert(); } /*////////////////////////////////////////////////////////////// @@ -600,42 +531,25 @@ contract SemiFungiblePositionManager is ERC1155, Multicall, TransientReentrancyG // 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 key The Uniswap V4 pool key in which to perform the swap /// @param itmAmounts How much to swap (i.e. how many tokens are ITM) - /// @return totalSwapped The token deltas swapped in the AMM + /// @return The token deltas swapped in the AMM function swapInAMM( - IUniswapV3Pool univ3pool, + PoolKey memory key, 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; - + ) internal returns (LeftRightSigned) { unchecked { + 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 + // 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 @@ -663,45 +577,50 @@ contract SemiFungiblePositionManager is ERC1155, Multicall, TransientReentrancyG // 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); + int256 net0 = itm0 - + PanopticMath.convert1to0( + itm1, + V4StateReader.getSqrtPriceX96(POOL_MANAGER_V4, key.toId()) + ); zeroForOne = net0 < 0; - // compute the swap amount, set as positive (exact input) - swapAmount = -net0; + swapAmount = net0; } else if (itm0 != 0) { zeroForOne = itm0 < 0; - swapAmount = -itm0; + swapAmount = itm0; } else { zeroForOne = itm1 > 0; - swapAmount = -itm1; + 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 + BalanceDelta swapDelta = POOL_MANAGER_V4.swap( + key, + IPoolManager.SwapParams( + zeroForOne, + swapAmount, + zeroForOne + ? Constants.MIN_V4POOL_SQRT_RATIO + 1 + : Constants.MAX_V4POOL_SQRT_RATIO - 1 + ), + "" ); - // Add amounts swapped to totalSwapped variable - totalSwapped = LeftRightSigned.wrap(0).toRightSlot(swap0.toInt128()).toLeftSlot( - swap1.toInt128() - ); + return + LeftRightSigned.wrap(0).toRightSlot(-swapDelta.amount0()).toLeftSlot( + -swapDelta.amount1() + ); } } /// @notice Create the position in the AMM defined by `tokenId`. /// @dev Loops over each leg in the tokenId and calls _createLegInAMM for each, which does the mint/burn in the AMM. + /// @param account The address of the user creating the position + /// @param key The Uniswap V4 pool key in which to create the position /// @param tickLimitLow The lower bound of an acceptable open interval for the ending price /// @param tickLimitHigh The upper bound of an acceptable open interval for the ending price /// @param positionSize The size of the option position @@ -710,26 +629,23 @@ contract SemiFungiblePositionManager is ERC1155, Multicall, TransientReentrancyG /// @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( + address account, + PoolKey memory key, 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 - IUniswapV3Pool univ3pool = s_poolIdToAddr[tokenId.poolId()]; - - // Revert if the pool not been previously initialized - if (univ3pool == 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; ) { + LeftRightUnsigned totalCollected; + for (uint256 leg = 0; leg < tokenId.countLegs(); ) { + address _account = account; + LiquidityChunk liquidityChunk = PanopticMath.getLiquidityChunk( tokenId, leg, @@ -743,22 +659,27 @@ contract SemiFungiblePositionManager is ERC1155, Multicall, TransientReentrancyG amount1 += Math.getAmount1ForLiquidity(liquidityChunk); } + PoolKey memory _key = key; LeftRightSigned movedLeg; + TokenId _tokenId = tokenId; + bool _isBurn = isBurn; (movedLeg, collectedByLeg[leg]) = _createLegInAMM( - univ3pool, - tokenId, + _account, + _key, + _tokenId, leg, liquidityChunk, - isBurn + _isBurn ); totalMoved = totalMoved.add(movedLeg); + totalCollected = totalCollected.add(collectedByLeg[leg]); // 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 + _tokenId.tokenType(leg) == 0 ? LeftRightSigned.wrap(0).toLeftSlot(movedLeg.leftSlot()) : LeftRightSigned.wrap(0).toRightSlot(movedLeg.rightSlot()) ); @@ -777,14 +698,46 @@ contract SemiFungiblePositionManager is ERC1155, Multicall, TransientReentrancyG 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(univ3pool, itmAmounts).add(totalMoved); + totalMoved = totalMoved.add(swapInAMM(key, itmAmounts)); } (tickLimitLow, tickLimitHigh) = (tickLimitHigh, tickLimitLow); } + LeftRightSigned cumulativeDelta = totalMoved.sub(totalCollected); + + if (cumulativeDelta.rightSlot() > 0) { + POOL_MANAGER_V4.burn( + account, + uint160(Currency.unwrap(key.currency0)), + uint128(cumulativeDelta.rightSlot()) + ); + } else if (cumulativeDelta.rightSlot() < 0) { + POOL_MANAGER_V4.mint( + account, + uint160(Currency.unwrap(key.currency0)), + uint128(-cumulativeDelta.rightSlot()) + ); + } + + if (cumulativeDelta.leftSlot() > 0) { + POOL_MANAGER_V4.burn( + account, + uint160(Currency.unwrap(key.currency1)), + uint128(cumulativeDelta.leftSlot()) + ); + } else if (cumulativeDelta.leftSlot() < 0) { + POOL_MANAGER_V4.mint( + account, + uint160(Currency.unwrap(key.currency1)), + uint128(-cumulativeDelta.leftSlot()) + ); + } + + PoolKey memory __key = key; + // Get the current tick of the Uniswap pool, check slippage - (, int24 currentTick, , , , , ) = univ3pool.slot0(); + int24 currentTick = V4StateReader.getTick(POOL_MANAGER_V4, __key.toId()); if ((currentTick >= tickLimitHigh) || (currentTick <= tickLimitLow)) revert Errors.PriceBoundFail(); @@ -797,7 +750,8 @@ contract SemiFungiblePositionManager is ERC1155, Multicall, TransientReentrancyG /// @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 account The address of the user creating the position + /// @param key The Uniswap V4 pool key in which to create the position /// @param tokenId The option position /// @param leg The leg index that needs to be modified /// @param liquidityChunk The liquidity chunk in Uniswap represented by the leg @@ -805,7 +759,8 @@ contract SemiFungiblePositionManager is ERC1155, Multicall, TransientReentrancyG /// @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, + address account, + PoolKey memory key, TokenId tokenId, uint256 leg, LiquidityChunk liquidityChunk, @@ -814,8 +769,8 @@ contract SemiFungiblePositionManager is ERC1155, Multicall, TransientReentrancyG // unique key to identify the liquidity chunk in this Uniswap pool bytes32 positionKey = keccak256( abi.encodePacked( - address(univ3pool), - msg.sender, + key.toId(), + account, tokenId.tokenType(leg), liquidityChunk.tickLower(), liquidityChunk.tickUpper() @@ -842,7 +797,7 @@ contract SemiFungiblePositionManager is ERC1155, Multicall, TransientReentrancyG if (chunkLiquidity == 0) revert Errors.ZeroLiquidity(); if (isLong == 0) { - // selling/short: so move from msg.sender *to* uniswap + // selling/short: so move from account *to* uniswap // we're minting more liquidity in uniswap: so add the incoming liquidity chunk to the existing liquidity chunk updatedLiquidity = startingLiquidity + chunkLiquidity; @@ -852,7 +807,7 @@ contract SemiFungiblePositionManager is ERC1155, Multicall, TransientReentrancyG removedLiquidity -= chunkLiquidity; } } else { - // the _leg is long (buying: moving *from* uniswap to msg.sender) + // the _leg is long (buying: moving *from* uniswap to account) // so we seek to move the incoming liquidity chunk *out* of uniswap - but was there sufficient liquidity sitting in uniswap // in the first place? if (startingLiquidity < chunkLiquidity) { @@ -886,48 +841,58 @@ contract SemiFungiblePositionManager is ERC1155, Multicall, TransientReentrancyG // 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: + from the msg.sender to the Uniswap V4 pool: Selling(isLong=0): Mint chunk of liquidity in Uniswap (defined by upper tick, lower tick, and amount) ┌─────────────────────────────────┐ ▲ ┌▼┐ liquidityChunk │ │ ┌──┴─┴──┐ ┌───┴──┐ │ │ │ │ │ └──┴───────┴──► └──────┘ - Uniswap V3 msg.sender + Uniswap V4 msg.sender - else: the position is long (buying a put or a call), then _burnLiquidity to remove liquidity from Uniswap V3 + else: the position is long (buying a put or a call), then _burnLiquidity to remove liquidity from Uniswap V4 Buying(isLong=1): Burn in Uniswap ┌─────────────────┐ ▲ ┌┼┐ │ │ ┌──┴─┴──┐ ┌───▼──┐ │ │ │ │ │ └──┴───────┴──► └──────┘ - Uniswap V3 msg.sender + Uniswap V4 msg.sender */ - moved = isLong == 0 - ? _mintLiquidity(liquidityChunk, univ3pool) - : _burnLiquidity(liquidityChunk, univ3pool); // from msg.sender to Uniswap - // 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 - ); + LiquidityChunk _liquidityChunk = liquidityChunk; + + PoolKey memory _key = key; + + (BalanceDelta delta, BalanceDelta feesAccrued) = POOL_MANAGER_V4.modifyLiquidity( + _key, + IPoolManager.ModifyLiquidityParams( + _liquidityChunk.tickLower(), + _liquidityChunk.tickUpper(), + isLong == 0 + ? int256(uint256(_liquidityChunk.liquidity())) + : -int256(uint256(_liquidityChunk.liquidity())), + positionKey + ), + "" + ); + + unchecked { + moved = LeftRightSigned + .wrap(0) + .toRightSlot(feesAccrued.amount0() - delta.amount0()) + .toLeftSlot(feesAccrued.amount1() - delta.amount1()); } - // 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 - ); + // (premium can only be collected if liquidity existed in the chunk prior to this mint) + if (currentLiquidity.rightSlot() > 0) { + collectedSingleLeg = LeftRightUnsigned + .wrap(0) + .toRightSlot(uint128(feesAccrued.amount0())) + .toLeftSlot(uint128(feesAccrued.amount1())); + + _updateStoredPremia(positionKey, currentLiquidity, collectedSingleLeg); + } } /// @notice Updates the premium accumulators for a chunk with the latest collected tokens. @@ -956,182 +921,6 @@ contract SemiFungiblePositionManager is ERC1155, Multicall, TransientReentrancyG ); } - /// @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({ // compute by reading values from univ3pool every time - 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 much liquidity has 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. - LeftRightSigned amountToCollect = _getFeesBase( - univ3pool, - startingLiquidity, - liquidityChunk, - false - ).subRect(s_accountFeesBase[positionKey]); - - if (isLong == 1) { - amountToCollect = amountToCollect.sub(movedInLeg); - } - - if (LeftRightSigned.unwrap(amountToCollect) != 0) { - // first collect amounts from Uniswap corresponding to this position - // Collect only if there was existing startingLiquidity=liquidities.rightSlot() at that position: collect all fees - - // Collects tokens owed to a liquidity chunk - (uint128 receivedAmount0, uint128 receivedAmount1) = univ3pool.collect( - msg.sender, - liquidityChunk.tickLower(), - liquidityChunk.tickUpper(), - uint128(amountToCollect.rightSlot()), - uint128(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 @@ -1229,14 +1018,14 @@ contract SemiFungiblePositionManager is ERC1155, Multicall, TransientReentrancyG //////////////////////////////////////////////////////////////*/ /// @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 idV4 The Uniswap V4 pool id to query /// @param owner The address of the account that is queried /// @param tokenType The tokenType of the position /// @param tickLower The lower end of the tick range for the position /// @param tickUpper The upper end of the tick range for the position /// @return accountLiquidities The amount of liquidity that held in and removed from Uniswap for that chunk (netLiquidity:removedLiquidity -> rightSlot:leftSlot) function getAccountLiquidity( - address univ3pool, + PoolId idV4, address owner, uint256 tokenType, int24 tickLower, @@ -1245,15 +1034,15 @@ contract SemiFungiblePositionManager is ERC1155, Multicall, TransientReentrancyG // 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)) + keccak256(abi.encodePacked(idV4, owner, tokenType, tickLower, tickUpper)) ]; } /// @notice Return the premium associated with a given position, where premium is an accumulator of feeGrowth for the touched position. /// @dev If an atTick parameter is provided that is different from `type(int24).max`, then it will update the premium up to the current - /// block at the provided atTick value. We do this because this may be called immediately after the Uniswap 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 + /// block at the provided atTick value. We do this because this may be called immediately after the Uniswap V4 pool has been touched, + /// so no need to read the feeGrowths from the Uniswap V4 pool. + /// @param idV4 The Uniswap V4 pool id to query /// @param owner The address of the account that is queried /// @param tokenType The tokenType of the position /// @param tickLower The lower end of the tick range for the position @@ -1263,7 +1052,7 @@ contract SemiFungiblePositionManager is ERC1155, Multicall, TransientReentrancyG /// @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, + PoolId idV4, address owner, uint256 tokenType, int24 tickLower, @@ -1272,7 +1061,7 @@ contract SemiFungiblePositionManager is ERC1155, Multicall, TransientReentrancyG uint256 isLong ) external view returns (uint128, uint128) { bytes32 positionKey = keccak256( - abi.encodePacked(univ3pool, owner, tokenType, tickLower, tickUpper) + abi.encodePacked(idV4, owner, tokenType, tickLower, tickUpper) ); LeftRightUnsigned acctPremia; @@ -1285,31 +1074,43 @@ contract SemiFungiblePositionManager is ERC1155, Multicall, TransientReentrancyG // unique key to identify the liquidity chunk in this Uniswap pool LeftRightUnsigned amountToCollect; { - IUniswapV3Pool _univ3pool = IUniswapV3Pool(univ3pool); + PoolId _idV4 = idV4; int24 _tickLower = tickLower; int24 _tickUpper = tickUpper; + int24 _atTick = atTick; + bytes32 _positionKey = positionKey; + + (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = V4StateReader + .getFeeGrowthInside(POOL_MANAGER_V4, _idV4, _atTick, _tickLower, _tickUpper); + + (uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128) = V4StateReader + .getFeeGrowthInsideLast( + POOL_MANAGER_V4, + _idV4, + keccak256( + abi.encodePacked(address(this), _tickLower, _tickUpper, _positionKey) + ) + ); - // 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 = LeftRightUnsigned.wrap( - uint256( - LeftRightSigned.unwrap(feesBase.subRect(s_accountFeesBase[positionKey])) - ) - ); + unchecked { + amountToCollect = LeftRightUnsigned + .wrap( + uint128( + Math.mulDiv128( + feeGrowthInside0X128 - feeGrowthInside0LastX128, + netLiquidity + ) + ) + ) + .toLeftSlot( + uint128( + Math.mulDiv128( + feeGrowthInside1X128 - feeGrowthInside1LastX128, + netLiquidity + ) + ) + ); + } } (LeftRightUnsigned premiumOwed, LeftRightUnsigned premiumGross) = _getPremiaDeltas( @@ -1337,42 +1138,24 @@ contract SemiFungiblePositionManager is ERC1155, Multicall, TransientReentrancyG 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 V4 poolkey for a given `poolId`. + /// @param poolId The unique pool identifier for a Uni V4 pool in the SFPM + /// @return The Uniswap V4 pool key corresponding to `poolId` + function getUniswapV4PoolKeyFromId(uint64 poolId) external view returns (PoolKey memory) { + return s_poolIdToKey[poolId]; } - /// @notice Returns the 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_poolIdToAddr[poolId]; + /// @notice Returns the SFPM `poolId` for a given Uniswap V4 `PoolId`. + /// @param idV4 The Uniswap V4 pool identifier + /// @return The unique pool identifier in the SFPM corresponding to `idV4` + function getPoolId(PoolId idV4) external view returns (uint64) { + return uint64(s_V4toSFPMIdData[idV4]); } - /// @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]); + /// @notice Returns the SFPM `poolId` for a given Uniswap V4 `PoolKey`. + /// @param key The Uniswap V4 pool key + /// @return The unique pool identifier in the SFPM corresponding to `key` + function getPoolId(PoolKey calldata key) external view returns (uint64) { + return uint64(s_V4toSFPMIdData[key.toId()]); } } diff --git a/contracts/base/FactoryNFT.sol b/contracts/base/FactoryNFT.sol index 8880207..f5d5341 100644 --- a/contracts/base/FactoryNFT.sol +++ b/contracts/base/FactoryNFT.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.24; import {PanopticMath} from "@libraries/PanopticMath.sol"; import {PanopticPool} from "@contracts/PanopticPool.sol"; // Inherited implementations -import {ERC721} from "solmate/tokens/ERC721.sol"; +import {ERC721} from "solmate/src/tokens/ERC721.sol"; import {MetadataStore} from "@base/MetadataStore.sol"; // Custom types import {Pointer} from "@types/Pointer.sol"; @@ -30,7 +30,7 @@ contract FactoryNFT is MetadataStore, ERC721 { Pointer[][] memory pointers ) MetadataStore(properties, indices, pointers) - ERC721("Panoptic V1 Factory Deployer NFTs", "PANOPTIC-NFT") + ERC721("Panoptic V1.1 Factory Deployer NFTs", "PANOPTIC-NFT") {} /// @notice Returns the metadata URI for a given `tokenId`. @@ -44,9 +44,9 @@ contract FactoryNFT is MetadataStore, ERC721 { return constructMetadata( panopticPool, - PanopticMath.safeERC20Symbol(PanopticPool(panopticPool).univ3pool().token0()), - PanopticMath.safeERC20Symbol(PanopticPool(panopticPool).univ3pool().token1()), - PanopticPool(panopticPool).univ3pool().fee() + PanopticMath.safeERC20Symbol(PanopticPool(panopticPool).collateralToken0().asset()), + PanopticMath.safeERC20Symbol(PanopticPool(panopticPool).collateralToken1().asset()), + PanopticPool(panopticPool).poolKey().fee ); } diff --git a/contracts/interfaces/IV3CompatibleOracle.sol b/contracts/interfaces/IV3CompatibleOracle.sol new file mode 100644 index 0000000..a90e1a6 --- /dev/null +++ b/contracts/interfaces/IV3CompatibleOracle.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.5.0; + +/// @title Interface defining the oracle contract used by Panoptic, which may be a Uniswap V3 pool or custom implementation +/// @author Axicon Labs Inc, credit to Uniswap Labs [https://github.com/Uniswap/v3-core](https://github.com/Uniswap/v3-core) under GPL-2.0 license +/// @notice This interface defines the set of functions called by Panoptic on its external oracle contract. +/// @dev The interface is compatible with Uniswap V3 pools, but can also be implemented by a custom oracle contract. +interface IV3CompatibleOracle { + /// @notice The 0th storage slot in the oracle stores many values, and is exposed as a single method to save gas + /// when accessed externally. + /// @return sqrtPriceX96 The current price of the oracle as a sqrt(token1/token0) Q64.96 value + /// @return tick The current tick of the oracle, i.e. according to the last tick transition that was run. + /// This value may not always be equal to SqrtTickMath.getTickAtSqrtRatio(sqrtPriceX96) if the price is on a tick + /// boundary + /// @return observationIndex The index of the last oracle observation that was written + /// @return observationCardinality The current maximum number of observations stored in the oracle + function slot0() + external + view + returns ( + uint160 sqrtPriceX96, + int24 tick, + uint16 observationIndex, + uint16 observationCardinality, + uint16, + uint8, + bool + ); + + /// @notice Returns data about a specific observation index + /// @param index The element of the observations array to fetch + /// @return blockTimestamp The timestamp of the observation + /// @return tickCumulative The tick multiplied by seconds elapsed for the life of the pool as of the observation timestamp + /// @return secondsPerLiquidityCumulativeX128 The seconds per in range liquidity for the life of the pool as of the observation timestamp + /// @return initialized Whether the observation has been initialized and the values are safe to use + function observations( + uint256 index + ) + external + view + returns ( + uint32 blockTimestamp, + int56 tickCumulative, + uint160 secondsPerLiquidityCumulativeX128, + bool initialized + ); + + /// @notice Returns the cumulative tick and liquidity as of each timestamp `secondsAgo` from the current block timestamp + /// @dev To get a time weighted average tick or liquidity-in-range, you must call this with two values, one representing + /// the beginning of the period and another for the end of the period. E.g., to get the last hour time-weighted average tick, + /// you must call it with secondsAgos = [3600, 0]. + /// @dev The time weighted average tick represents the geometric time weighted average price of the pool, in + /// log base sqrt(1.0001) of 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 Increase the maximum number of price and liquidity observations that this oracle will store + /// @param observationCardinalityNext The desired minimum number of observations for the oracle to store + function increaseObservationCardinalityNext(uint16 observationCardinalityNext) external; +} diff --git a/contracts/libraries/CallbackLib.sol b/contracts/libraries/CallbackLib.sol deleted file mode 100644 index 76dfcdd..0000000 --- a/contracts/libraries/CallbackLib.sol +++ /dev/null @@ -1,39 +0,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 Defining characteristics of a Uniswap V3 pool - struct PoolFeatures { - address token0; - address token1; - uint24 fee; - } - - /// @notice Data sent by pool in mint/swap callbacks used to validate the pool and send back requisite tokens - 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(); - } -} diff --git a/contracts/libraries/Constants.sol b/contracts/libraries/Constants.sol index 6fbecba..614cf48 100644 --- a/contracts/libraries/Constants.sol +++ b/contracts/libraries/Constants.sol @@ -8,38 +8,38 @@ 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 Minimum possible price tick in a Uniswap V4 pool + int24 internal constant MIN_V4POOL_TICK = -887272; - /// @notice Maximum possible price tick in a Uniswap V3 pool - int24 internal constant MAX_V3POOL_TICK = 887272; + /// @notice Maximum possible price tick in a Uniswap V4 pool + int24 internal constant MAX_V4POOL_TICK = 887272; - /// @notice Minimum possible sqrtPriceX96 in a Uniswap V3 pool - uint160 internal constant MIN_V3POOL_SQRT_RATIO = 4295128739; + /// @notice Minimum possible sqrtPriceX96 in a Uniswap V4 pool + uint160 internal constant MIN_V4POOL_SQRT_RATIO = 4295128739; - /// @notice Maximum possible sqrtPriceX96 in a Uniswap V3 pool - uint160 internal constant MAX_V3POOL_SQRT_RATIO = + /// @notice Maximum possible sqrtPriceX96 in a Uniswap V4 pool + uint160 internal constant MAX_V4POOL_SQRT_RATIO = 1461446703485210103287273052203988822378723970342; /// @notice Parameter that determines which oracle type to use for the "slow" oracle price on non-liquidation solvency checks. /// @dev If false, an 8-slot internal median array is used to compute the "slow" oracle price. - /// @dev This oracle is updated with the last 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. + /// @dev This oracle is updated with the last oracle observation during `mintOptions` if MEDIAN_PERIOD has elapsed past the last observation. + /// @dev If true, the "slow" oracle price is instead computed on-the-fly from 9 oracle observations (spaced 5 observations apart) irrespective of the frequency of `mintOptions` calls. bool internal constant SLOW_ORACLE_UNISWAP_MODE = false; /// @notice The minimum amount of time, in seconds, permitted between internal TWAP updates. uint256 internal constant MEDIAN_PERIOD = 60; - /// @notice Amount of Uniswap observations to include in the "fast" oracle price. + /// @notice Amount of oracle observations to include in the "fast" oracle price. uint256 internal constant FAST_ORACLE_CARDINALITY = 3; /// @dev Amount of observation indices to skip in between each observation for the "fast" oracle price. /// @dev Note that the *minimum* total observation time is determined by the blocktime and may need to be adjusted by chain. - /// @dev Uniswap observations snapshot the last block's closing price at the first interaction with the pool in a block. + /// @dev oracle observations snapshot the last block's closing price at the first interaction with the pool in a block. /// @dev In this case, if there is an interaction every block, the "fast" oracle can consider 3 consecutive block end prices (min=36 seconds on Ethereum). uint256 internal constant FAST_ORACLE_PERIOD = 1; - /// @notice Amount of Uniswap observations to include in the "slow" oracle price (in Uniswap mode). + /// @notice Amount of oracle observations to include in the "slow" oracle price (in Uniswap mode). uint256 internal constant SLOW_ORACLE_CARDINALITY = 9; /// @notice Amount of observation indices to skip in between each observation for the "slow" oracle price. diff --git a/contracts/libraries/Errors.sol b/contracts/libraries/Errors.sol index c6f86ca..dc7a52f 100644 --- a/contracts/libraries/Errors.sol +++ b/contracts/libraries/Errors.sol @@ -38,8 +38,8 @@ library Errors { /// @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 An unlock callback was attempted from an address other than the canonical Uniswap V4 pool manager + error UnauthorizedUniswapCallback(); /// @notice PanopticPool: None of the legs in a position are force-exercisable (they are all either short or ATM long) error NoLegsExercisable(); diff --git a/contracts/libraries/Math.sol b/contracts/libraries/Math.sol index 9365d81..86031a8 100644 --- a/contracts/libraries/Math.sol +++ b/contracts/libraries/Math.sol @@ -127,7 +127,7 @@ library Math { 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(); + if (absTick > uint256(int256(Constants.MAX_V4POOL_TICK))) revert Errors.InvalidTick(); // sqrt(1.0001^(-absTick)) = ∏ sqrt(1.0001^(-bit_i)) // ex: absTick = 100 = binary 1100100, so sqrt(1.0001^-100) = sqrt(1.0001^-64) * sqrt(1.0001^-32) * sqrt(1.0001^-4) diff --git a/contracts/libraries/PanopticMath.sol b/contracts/libraries/PanopticMath.sol index d2cc0c9..5106cf5 100644 --- a/contracts/libraries/PanopticMath.sol +++ b/contracts/libraries/PanopticMath.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.24; // Interfaces import {CollateralTracker} from "@contracts/CollateralTracker.sol"; import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; -import {IUniswapV3Pool} from "univ3-core/interfaces/IUniswapV3Pool.sol"; +import {IV3CompatibleOracle} from "@interfaces/IV3CompatibleOracle.sol"; // Libraries import {Constants} from "@libraries/Constants.sol"; import {Errors} from "@libraries/Errors.sol"; @@ -15,6 +15,7 @@ import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; import {LeftRightUnsigned, LeftRightSigned} from "@types/LeftRight.sol"; import {LiquidityChunk} from "@types/LiquidityChunk.sol"; import {TokenId} from "@types/TokenId.sol"; +import {PoolId} from "v4-core/types/PoolId.sol"; /// @title Compute general math quantities relevant to Panoptic and AMM pool management. /// @notice Contains Panoptic-specific helpers and math functions. @@ -32,27 +33,23 @@ library PanopticMath { MATH HELPERS //////////////////////////////////////////////////////////////*/ - /// @notice Given an address to a Uniswap V3 pool, return its 64-bit ID as used in the `TokenId` of Panoptic. + /// @notice Given a 256-bit Uniswap V4 pool ID (hash) and the corresponding `tickSpacing`, return its 64-bit ID as used in the `TokenId` of Panoptic. // Example: - // 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 32768) + // [16-bit tickSpacing][last 48 bits of Uniswap V4 pool ID] = poolId // e.g.: - // univ3pool = 0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8 + // idV4 = 0x9c33e1937fe23c3ff82d7725f2bb5af696db1c89a9b8cae141cb0e986847638a // tickSpacing = 60 // the returned id is then: - // poolPattern = 0x00008ad599c3A0ff + // poolPattern = 0x0000e986847638a // tickSpacing = 0x003c000000000000 + // -------------------------------------------- - // poolId = 0x003c8ad599c3A0ff - // - /// @param univ3pool The address of the Uniswap V3 pool to get the ID of - /// @return A uint64 representing a fingerprint of the Uniswap V3 pool address - function getPoolId(address univ3pool) internal view returns (uint64) { + // poolId = 0x003ce986847638a + /// @param idV4 The 256-bit Uniswap V4 pool ID + /// @param tickSpacing The tick spacing of the Uniswap V4 pool identified by `idV4` + /// @return A fingerprint representing the Uniswap V4 pool + function getPoolId(PoolId idV4, int24 tickSpacing) internal pure returns (uint64) { unchecked { - int24 tickSpacing = IUniswapV3Pool(univ3pool).tickSpacing(); - uint64 poolId = uint64(uint160(univ3pool) >> 112); - poolId += uint64(uint24(tickSpacing)) << 48; - return poolId; + return uint48(uint256(PoolId.unwrap(idV4))) + (uint64(uint24(tickSpacing)) << 48); } } @@ -93,23 +90,25 @@ library PanopticMath { } } - /// @notice Converts `fee` to a string with "bps" appended. + /// @notice Converts `fee` to a string with "bps" appended, or DYNAMIC if "fee" is equivalent to `0x800000`. /// @dev The lowest supported value of `fee` is 1 (`="0.01bps"`). /// @param fee The fee to convert to a string (in hundredths of basis points) /// @return Stringified version of `fee` with "bps" appended function uniswapFeeToString(uint24 fee) internal pure returns (string memory) { return - string.concat( - Strings.toString(fee / 100), - fee % 100 == 0 - ? "" - : string.concat( - ".", - Strings.toString((fee / 10) % 10), - Strings.toString(fee % 10) - ), - "bps" - ); + fee == 0x800000 + ? "DYNAMIC" + : string.concat( + Strings.toString(fee / 100), + fee % 100 == 0 + ? "" + : string.concat( + ".", + Strings.toString((fee / 10) % 10), + Strings.toString(fee % 10) + ), + "bps" + ); } /*////////////////////////////////////////////////////////////// @@ -147,21 +146,19 @@ library PanopticMath { //////////////////////////////////////////////////////////////*/ /// @notice Computes various oracle prices corresponding to a Uniswap pool. - /// @param univ3pool The Uniswap pool to get the observations from + /// @param oracleContract The external oracle contract to retrieve observations from /// @param miniMedian The packed structure representing the sorted 8-slot queue of internal median observations - /// @return currentTick The current tick in the Uniswap pool (as returned in slot0) /// @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 as tracked by `s_miniMedian` /// @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` (returns 0 if not enough time has passed since last observation) + /// @return medianData The updated value for `s_miniMedian` (returns 0 if not enough time has passed since last observation) function getOracleTicks( - IUniswapV3Pool univ3pool, + IV3CompatibleOracle oracleContract, uint256 miniMedian ) external view returns ( - int24 currentTick, int24 fastOracleTick, int24 slowOracleTick, int24 latestObservation, @@ -171,11 +168,10 @@ library PanopticMath { uint16 observationIndex; uint16 observationCardinality; - IUniswapV3Pool _univ3pool = univ3pool; - (, currentTick, observationIndex, observationCardinality, , , ) = univ3pool.slot0(); + (, , observationIndex, observationCardinality, , , ) = oracleContract.slot0(); (fastOracleTick, latestObservation) = computeMedianObservedPrice( - _univ3pool, + oracleContract, observationIndex, observationCardinality, Constants.FAST_ORACLE_CARDINALITY, @@ -184,7 +180,7 @@ library PanopticMath { if (Constants.SLOW_ORACLE_UNISWAP_MODE) { (slowOracleTick, ) = computeMedianObservedPrice( - _univ3pool, + oracleContract, observationIndex, observationCardinality, Constants.SLOW_ORACLE_CARDINALITY, @@ -196,19 +192,19 @@ library PanopticMath { observationCardinality, Constants.MEDIAN_PERIOD, miniMedian, - _univ3pool + oracleContract ); } } - /// @notice Returns the median of the last `cardinality` average prices over `period` observations from `univ3pool`. + /// @notice Returns the median of the last `cardinality` average prices over `period` observations from `oracleContract`. /// @dev Used when we need a manipulation-resistant TWAP price. - /// @dev Uniswap observations snapshot the closing price of the last block before the first interaction of a given block. + /// @dev oracle observations snapshot the closing price of the last block before the first interaction of a given block. /// @dev The maximum frequency of observations is 1 per block, but there is no guarantee that the pool will be observed at every block. /// @dev Each period has a minimum length of blocktime * period, but may be longer if the Uniswap pool is relatively inactive. /// @dev The final price used in the array (of length `cardinality`) is the average of all observations comprising `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 oracleContract The external oracle contract to retrieve observations from /// @param observationIndex The index of the last observation in the pool /// @param observationCardinality The number of observations in the pool /// @param cardinality The number of `periods` to in the median price array, should be odd @@ -216,7 +212,7 @@ library PanopticMath { /// @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, + IV3CompatibleOracle oracleContract, uint256 observationIndex, uint256 observationCardinality, uint256 cardinality, @@ -228,7 +224,7 @@ library PanopticMath { 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( + (timestamps[i], tickCumulatives[i], , ) = oracleContract.observations( uint256( (int256(observationIndex) - int256(i * period)) + int256(observationCardinality) @@ -250,12 +246,12 @@ library PanopticMath { } /// @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. + /// @dev Also inserts the latest oracle observation into the buffer, resorts, and returns if the last entry is at least `period` seconds old. /// @param observationIndex The index of the last observation in the Uniswap pool /// @param observationCardinality The number of observations in the Uniswap pool /// @param period The minimum time in seconds that must have passed since the last observation was inserted into the buffer /// @param medianData The packed structure representing the sorted 8-slot queue of ticks - /// @param univ3pool The Uniswap pool to retrieve observations from + /// @param oracleContract The external oracle contract to retrieve observations from /// @return medianTick The median of the provided 8-slot queue of ticks in `medianData` /// @return updatedMedianData The updated 8-slot queue of ticks with the latest observation inserted if the last entry is at least `period` seconds old (returns 0 otherwise) function computeInternalMedian( @@ -263,7 +259,7 @@ library PanopticMath { uint256 observationCardinality, uint256 period, uint256 medianData, - IUniswapV3Pool univ3pool + IV3CompatibleOracle oracleContract ) public view returns (int24 medianTick, uint256 updatedMedianData) { unchecked { // return the average of the rank 3 and 4 values @@ -276,13 +272,16 @@ library PanopticMath { 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_old, int56 tickCumulative_old, , ) = oracleContract + .observations( + uint256( + int256(observationIndex) - + int256(1) + + int256(observationCardinality) + ) % observationCardinality + ); - (uint256 timestamp_last, int56 tickCumulative_last, , ) = univ3pool + (uint256 timestamp_last, int56 tickCumulative_last, , ) = oracleContract .observations(observationIndex); lastObservedTick = int24( (tickCumulative_last - tickCumulative_old) / @@ -325,25 +324,28 @@ library PanopticMath { } } - /// @notice Computes the twap of a Uniswap V3 pool using data from its oracle. + /// @notice Computes a TWAP price over `twapWindow` on a Uniswap V3-style observation oracle. /// @dev Note that our definition of TWAP differs from a typical mean of prices over a time window. /// @dev We instead observe the average price over a series of time intervals, and define the TWAP as the median of those averages. - /// @param univ3pool The Uniswap pool from which to compute the TWAP + /// @param oracleContract The external oracle contract to retrieve observations from /// @param twapWindow The time window to compute the TWAP over /// @return The final calculated TWAP tick - function twapFilter(IUniswapV3Pool univ3pool, uint32 twapWindow) external view returns (int24) { + function twapFilter( + IV3CompatibleOracle oracleContract, + uint32 twapWindow + ) external view returns (int24) { uint32[] memory secondsAgos = new uint32[](20); int256[] memory twapMeasurement = new int256[](19); unchecked { - // construct the time stots + // 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); + (int56[] memory tickCumulatives, ) = oracleContract.observe(secondsAgos); // compute the average tick per 30s window for (uint256 i = 0; i < 19; ++i) { @@ -365,7 +367,7 @@ library PanopticMath { //////////////////////////////////////////////////////////////*/ /// @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 (share ownership) in the Uniswap V4 pool; this is a liquidity chunk. // Liquidity chunk (defined by tick upper, tick lower, and its size/amount: the liquidity) // liquidity │ // ▲ │ @@ -374,7 +376,7 @@ library PanopticMath { // │ │ │ // │ │ │ // └──┴───────┴────► price - // Uniswap V3 Pool + // Uniswap V4 Pool /// @param tokenId The option position id /// @param legIndex The leg index of the option position, can be {0,1,2,3} /// @param positionSize The number of contracts held by this leg @@ -387,10 +389,10 @@ library PanopticMath { // 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 + // Get the amount of liquidity owned by this leg in the Uniswap V4 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 + // In Uniswap V4, 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: @@ -400,7 +402,7 @@ library PanopticMath { // 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 + // Because Uniswap V4 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. @@ -425,7 +427,7 @@ library PanopticMath { /// @notice Extract the tick range specified by `strike` and `width` for the given `tickSpacing`, if valid. /// @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 + /// @param tickSpacing The tick spacing of the underlying Uniswap V4 pool /// @return tickLower The lower tick of the liquidity chunk /// @return tickUpper The upper tick of the liquidity chunk function getTicks( @@ -433,26 +435,19 @@ library PanopticMath { int24 width, int24 tickSpacing ) internal pure returns (int24 tickLower, int24 tickUpper) { - unchecked { - // The max/min ticks that can be initialized are the closest multiple of tickSpacing to the actual max/min tick abs()=887272 - // Dividing and multiplying by tickSpacing rounds down and forces the tick to be a multiple of tickSpacing - int24 minTick = (Constants.MIN_V3POOL_TICK / tickSpacing) * tickSpacing; - int24 maxTick = (Constants.MAX_V3POOL_TICK / tickSpacing) * tickSpacing; - - (int24 rangeDown, int24 rangeUp) = PanopticMath.getRangesFromStrike(width, tickSpacing); - - (tickLower, tickUpper) = (strike - rangeDown, strike + rangeUp); - - // Revert if the upper/lower ticks are not multiples of tickSpacing - // Revert if the tick range extends from the strike outside of the valid tick range - // These are invalid states, and would revert silently later in `univ3Pool.mint` - if ( - tickLower % tickSpacing != 0 || - tickUpper % tickSpacing != 0 || - tickLower < minTick || - tickUpper > maxTick - ) revert Errors.TicksNotInitializable(); - } + (int24 rangeDown, int24 rangeUp) = PanopticMath.getRangesFromStrike(width, tickSpacing); + + (tickLower, tickUpper) = (strike - rangeDown, strike + rangeUp); + + // Revert if the upper/lower ticks are not multiples of tickSpacing + // Revert if the tick range extends from the strike outside of the valid tick range + // These are invalid states, and would revert later on in the Uniswap pool + if ( + tickLower % tickSpacing != 0 || + tickUpper % tickSpacing != 0 || + tickLower < Constants.MIN_V4POOL_TICK || + tickUpper > Constants.MAX_V4POOL_TICK + ) revert Errors.TicksNotInitializable(); } /// @notice Returns the distances of the upper and lower ticks from the strike for a position with the given width and tickSpacing. diff --git a/contracts/libraries/FeesCalc.sol b/contracts/libraries/V4StateReader.sol similarity index 53% rename from contracts/libraries/FeesCalc.sol rename to contracts/libraries/V4StateReader.sol index 47d1cab..7014ae7 100644 --- a/contracts/libraries/FeesCalc.sol +++ b/contracts/libraries/V4StateReader.sol @@ -1,82 +1,57 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.24; +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; -// 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"; +// Uniswap V4 interfaces +import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol"; +// Uniswap V4 libraries +import {StateLibrary} from "v4-core/libraries/StateLibrary.sol"; +// Uniswap V4 types +import {PoolId} from "v4-core/types/PoolId.sol"; -/// @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); +/// @notice A library to retrieve state information from Uniswap V4 pools via `extsload`. +/// @author Axicon Labs Limited, credit to Uniswap Labs under MIT License +library V4StateReader { + /// @notice Retrieves the current `sqrtPriceX96` from a Uniswap V4 pool. + /// @param manager The Uniswap V4 pool manager contract + /// @param poolId The pool ID of the Uniswap V4 pool + /// @return sqrtPriceX96 The current `sqrtPriceX96` of the Uniswap V4 pool + function getSqrtPriceX96( + IPoolManager manager, + PoolId poolId + ) internal view returns (uint160 sqrtPriceX96) { + bytes32 stateSlot = StateLibrary._getPoolStateSlot(poolId); + bytes32 data = manager.extsload(stateSlot); - // 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)))); + assembly ("memory-safe") { + sqrtPriceX96 := and(data, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) + } + } + + /// @notice Retrieves the current tick from a Uniswap V4 pool. + /// @param manager The Uniswap V4 pool manager contract + /// @param poolId The pool ID of the Uniswap V4 pool + /// @return tick The current tick of the Uniswap V4 pool + function getTick(IPoolManager manager, PoolId poolId) internal view returns (int24 tick) { + bytes32 stateSlot = StateLibrary._getPoolStateSlot(poolId); + bytes32 data = manager.extsload(stateSlot); + + assembly ("memory-safe") { + tick := signextend(2, shr(160, data)) + } } /// @notice Calculates the fee growth that has occurred (per unit of liquidity) in the AMM/Uniswap for an /// option position's tick range. - /// @dev Extracts the feeGrowth from the Uniswap V3 pool. - /// @param univ3pool The AMM pool where the leg is deployed + /// @param manager The Uniswap V4 pool manager contract + /// @param idV4 The pool ID of the Uniswap V4 pool /// @param currentTick The current price tick in the AMM /// @param tickLower The lower tick of the option position leg (a liquidity chunk) /// @param tickUpper The upper tick of the option position leg (a liquidity chunk) - /// @return feeGrowthInside0X128 The fee growth in the AMM of token0 - /// @return feeGrowthInside1X128 The fee growth in the AMM of token1 - function _getAMMSwapFeesPerLiquidityCollected( - IUniswapV3Pool univ3pool, + /// @return feeGrowthInside0X128 The fee growth in the AMM for token0 + /// @return feeGrowthInside1X128 The fee growth in the AMM for token1 + function getFeeGrowthInside( + IPoolManager manager, + PoolId idV4, int24 currentTick, int24 tickLower, int24 tickUpper @@ -87,8 +62,16 @@ library FeesCalc { // (...) // 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); + (uint256 lowerOut0, uint256 lowerOut1) = StateLibrary.getTickFeeGrowthOutside( + manager, + idV4, + tickLower + ); + (uint256 upperOut0, uint256 upperOut1) = StateLibrary.getTickFeeGrowthOutside( + manager, + idV4, + tickUpper + ); // compute the effective feeGrowth, depending on whether price is above/below/within range unchecked { @@ -149,9 +132,35 @@ library FeesCalc { current tick */ - feeGrowthInside0X128 = univ3pool.feeGrowthGlobal0X128() - lowerOut0 - upperOut0; - feeGrowthInside1X128 = univ3pool.feeGrowthGlobal1X128() - lowerOut1 - upperOut1; + + (uint256 feeGrowthGlobal0X128, uint256 feeGrowthGlobal1X128) = StateLibrary + .getFeeGrowthGlobals(manager, idV4); + feeGrowthInside0X128 = feeGrowthGlobal0X128 - lowerOut0 - upperOut0; + feeGrowthInside1X128 = feeGrowthGlobal1X128 - lowerOut1 - upperOut1; } } } + + /// @notice Retrieves the last stored `feeGrowthInsideLast` values for a unique Uniswap V4 position. + /// @dev Corresponds to pools[poolId].positions[positionId] in `manager`. + /// @param manager The Uniswap V4 pool manager contract + /// @param poolId The ID of the Uniswap V4 pool + /// @param positionId The ID of the position, which is a hash of the owner, tickLower, tickUpper, and salt. + /// @return feeGrowthInside0LastX128 The fee growth inside the position for token0 + /// @return feeGrowthInside1LastX128 The fee growth inside the position for token1 + function getFeeGrowthInsideLast( + IPoolManager manager, + PoolId poolId, + bytes32 positionId + ) internal view returns (uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128) { + bytes32 slot = StateLibrary._getPositionInfoSlot(poolId, positionId); + + // read all 3 words of the Position.State struct + bytes32[] memory data = manager.extsload(slot, 3); + + assembly ("memory-safe") { + feeGrowthInside0LastX128 := mload(add(data, 64)) + feeGrowthInside1LastX128 := mload(add(data, 96)) + } + } } diff --git a/contracts/types/LeftRight.sol b/contracts/types/LeftRight.sol index b75771e..cd91267 100644 --- a/contracts/types/LeftRight.sol +++ b/contracts/types/LeftRight.sol @@ -242,28 +242,17 @@ library LeftRightLibrary { } } - /// @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 (LeftRightSigned z) { + function sub(LeftRightSigned x, LeftRightUnsigned y) internal pure returns (LeftRightSigned z) { unchecked { - int256 left256 = int256(x.leftSlot()) - y.leftSlot(); + int256 left256 = int256(x.leftSlot()) - int256(uint256(y.leftSlot())); int128 left128 = int128(left256); - int256 right256 = int256(x.rightSlot()) - y.rightSlot(); + int256 right256 = int256(x.rightSlot()) - int256(uint256(y.rightSlot())); int128 right128 = int128(right256); if (left128 != left256 || right128 != right256) revert Errors.UnderOverFlow(); - return - z.toRightSlot(int128(Math.max(right128, 0))).toLeftSlot( - int128(Math.max(left128, 0)) - ); + return z.toRightSlot(right128).toLeftSlot(left128); } } diff --git a/contracts/types/TokenId.sol b/contracts/types/TokenId.sol index 1c64fa9..e62e2d8 100644 --- a/contracts/types/TokenId.sol +++ b/contracts/types/TokenId.sol @@ -21,8 +21,8 @@ using TokenIdLibrary for TokenId global; // 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 +// (0) idV4 48bits 0bits : least significant 48 bits of the Uniswap V4 pool ID, plus a pseudorandom number in the event of a collision +// (1) tickSpacing 16bits 48bits : tickSpacing for the pool corresponding to `idV4`. Up to 16 bits // ===== 4 times (one for each leg) ============================================================== // (2) asset 1bit 0bits : Specifies the asset (0: token0, 1: token1) // (3) optionRatio 7bits 1bits : number of contracts per leg @@ -39,9 +39,9 @@ using TokenIdLibrary for TokenId global; // (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 Address +// (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 idV4 // // <--- most significant bit least significant bit ---> // @@ -55,7 +55,7 @@ using TokenIdLibrary for TokenId global; // 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 Uniswap V4 pool id starts at bit index 0 and ends at bit index 63 (and thus takes up 64 bits). // - the width of the 3rd leg in this option position starts at bit index 64+36+48*2=196 library TokenIdLibrary { /// @notice AND mask to extract all `isLong` bits for each leg from a TokenId. @@ -83,7 +83,7 @@ library TokenIdLibrary { /// @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 + /// @return The `poolId` (Panoptic's pool fingerprint, contains the whole 64 bit sequence with the tickSpacing) of the Uniswap V4 pool function poolId(TokenId self) internal pure returns (uint64) { unchecked { return uint64(TokenId.unwrap(self)); @@ -92,7 +92,7 @@ library TokenIdLibrary { /// @notice The tickSpacing of this option position. /// @param self The TokenId to extract `tickSpacing` from - /// @return The `tickSpacing` of the Uniswap V3 pool + /// @return The `tickSpacing` of the Uniswap V4 pool function tickSpacing(TokenId self) internal pure returns (int24) { unchecked { return int24(uint24((TokenId.unwrap(self) >> 48) % 2 ** 16)); @@ -176,7 +176,7 @@ library TokenIdLibrary { ENCODING //////////////////////////////////////////////////////////////*/ - /// @notice Add the Uniswap V3 Pool pointed to by this option position (contains the entropy and tickSpacing). + /// @notice Add the Uniswap V4 Pool pointed to by 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 @@ -528,8 +528,8 @@ library TokenIdLibrary { 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) + (self.strike(i) == Constants.MIN_V4POOL_TICK) || + (self.strike(i) == Constants.MAX_V4POOL_TICK) ) revert Errors.InvalidTokenIdParameter(4); // In the following, we check whether the risk partner of this leg is itself @@ -574,7 +574,7 @@ library TokenIdLibrary { /// @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 + /// @param currentTick The current tick corresponding to the current price in the Uniswap V4 pool function validateIsExercisable(TokenId self, int24 currentTick) internal pure { unchecked { uint256 numLegs = self.countLegs(); diff --git a/foundry.toml b/foundry.toml index d48e0dd..90df58a 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,6 +1,7 @@ [profile.default] src = 'contracts' test = 'test/foundry' +script = 'script' out = 'out' libs = ['lib'] no_match_path = "contracts/*" diff --git a/lib/clones-with-immutable-args b/lib/clones-with-immutable-args new file mode 160000 index 0000000..196f1ec --- /dev/null +++ b/lib/clones-with-immutable-args @@ -0,0 +1 @@ +Subproject commit 196f1ecc6485c1bf2d41677fa01d3df4927ff9ce diff --git a/lib/v4-core b/lib/v4-core new file mode 160000 index 0000000..fc5b4cf --- /dev/null +++ b/lib/v4-core @@ -0,0 +1 @@ +Subproject commit fc5b4cf317f014ba96fa92f3ea964e03c58acbdd diff --git a/remappings.txt b/remappings.txt index 1bc605a..3f400c6 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,11 +1,14 @@ forge-std/=lib/forge-std/src/ -@openzeppelin/=lib/openzeppelin-contracts/ -solmate/=lib/solmate/src +@openzeppelin/=lib/v4-core/lib/openzeppelin-contracts/ +solmate/=lib/solmate +clones-with-immutable-args/=lib/clones-with-immutable-args/src/ univ3-core/=lib/v3-core/contracts univ3-periphery/=lib/v3-periphery/contracts +v4-core/=lib/v4-core/src @contracts/=contracts @libraries/=contracts/libraries @base/=contracts/base +@interfaces/=contracts/interfaces @test_periphery/=test/foundry/test_periphery @tokens/=contracts/tokens @types/=contracts/types diff --git a/script/DeployProtocol.s.sol b/script/DeployProtocol.s.sol index 8e18153..4b134cd 100644 --- a/script/DeployProtocol.s.sol +++ b/script/DeployProtocol.s.sol @@ -7,10 +7,9 @@ import {PanopticFactory} from "@contracts/PanopticFactory.sol"; import {CollateralTracker} from "@contracts/CollateralTracker.sol"; import {PanopticPool} from "@contracts/PanopticPool.sol"; import {SemiFungiblePositionManager} from "@contracts/SemiFungiblePositionManager.sol"; -import {IUniswapV3Factory} from "univ3-core/interfaces/IUniswapV3Factory.sol"; -import {IUniswapV3Pool} from "univ3-core/interfaces/IUniswapV3Pool.sol"; import {Pointer, PointerLibrary} from "@types/Pointer.sol"; import {PanopticHelper} from "@test_periphery/PanopticHelper.sol"; +import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol"; contract DeployProtocol is Script { struct PointerInfo { @@ -25,8 +24,7 @@ contract DeployProtocol is Script { // 0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14: sepolia address WETH9 = vm.envAddress("WETH9"); - // 0x0227628f3f023bb0b980b67d528571c95c6dac1c: sepolia - IUniswapV3Factory uniFactory = IUniswapV3Factory(vm.envAddress("UNIV3_FACTORY")); + IPoolManager manager = IPoolManager(vm.envAddress("POOL_MANAGER")); vm.startBroadcast(DEPLOYER_PRIVATE_KEY); @@ -80,13 +78,15 @@ contract DeployProtocol is Script { } } - SemiFungiblePositionManager sfpm = new SemiFungiblePositionManager(uniFactory); + IPoolManager _manager = manager; + + SemiFungiblePositionManager sfpm = new SemiFungiblePositionManager(_manager); new PanopticFactory( WETH9, sfpm, - uniFactory, - address(new PanopticPool(sfpm)), - address(new CollateralTracker(10, 2_000, 1_000, -128, 5_000, 9_000, 20_000)), + _manager, + address(new PanopticPool(sfpm, _manager)), + address(new CollateralTracker(10, 2_000, 1_000, -128, 5_000, 9_000, 20, _manager)), props, indices, pointers diff --git a/test/foundry/core/CollateralTracker.t.sol b/test/foundry/core/CollateralTracker.t.sol index 425cce6..e933ff7 100644 --- a/test/foundry/core/CollateralTracker.t.sol +++ b/test/foundry/core/CollateralTracker.t.sol @@ -34,14 +34,28 @@ import {IUniswapV3Factory} from "v3-core/interfaces/IUniswapV3Factory.sol"; import {ISwapRouter} from "v3-periphery/interfaces/ISwapRouter.sol"; import {PositionUtils, MiniPositionManager} from "../testUtils/PositionUtils.sol"; +import {ClonesWithImmutableArgs} from "clones-with-immutable-args/ClonesWithImmutableArgs.sol"; +// V4 types +import {PoolId} from "v4-core/types/PoolId.sol"; +import {PoolKey} from "v4-core/types/PoolKey.sol"; +import {StateLibrary} from "v4-core/libraries/StateLibrary.sol"; +import {V4StateReader} from "@libraries/V4StateReader.sol"; +import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol"; +import {BalanceDelta} from "v4-core/types/BalanceDelta.sol"; +import {Currency} from "v4-core/types/Currency.sol"; +import {PoolManager} from "v4-core/PoolManager.sol"; +import {IHooks} from "v4-core/interfaces/IHooks.sol"; +import {V4RouterSimple} from "../testUtils/V4RouterSimple.sol"; // CollateralTracker with extended functionality intended to expose internal data contract CollateralTrackerHarness is CollateralTracker, PositionUtils, MiniPositionManager { - constructor() CollateralTracker(10, 2_000, 1_000, -1_024, 5_000, 9_000, 20_000) {} + constructor( + IPoolManager _manager + ) CollateralTracker(10, 2_000, 1_000, -1_024, 5_000, 9_000, 20, _manager) {} // view deployer (panoptic pool) - function panopticPool() external view returns (PanopticPool) { - return s_panopticPool; + function panopticPool() external pure returns (PanopticPool) { + return _panopticPool(); } // whether the token has been initialized already or not @@ -50,8 +64,8 @@ contract CollateralTrackerHarness is CollateralTracker, PositionUtils, MiniPosit } // whether the current instance is token 0 - function underlyingIsToken0() external view returns (bool) { - return s_underlyingIsToken0; + function underlyingIsToken0() external pure returns (bool) { + return _underlyingIsToken0(); } function _inAMM() external view returns (uint256) { @@ -106,61 +120,11 @@ contract CollateralTrackerHarness is CollateralTracker, PositionUtils, MiniPosit } } -// Inherits all of PanopticPool's functionality, however uses a modified version of startPool -// which enables us to use our modified CollateralTracker harness that exposes internal data contract PanopticPoolHarness is PanopticPool { - constructor(SemiFungiblePositionManager _SFPM) PanopticPool(_SFPM) {} - - function modifiedStartPool( - address token0, - address token1, - IUniswapV3Pool uniswapPool - ) external { - // Store the univ3Pool variable - s_univ3pool = IUniswapV3Pool(uniswapPool); - - // store token0 and token1 - address s_token0 = uniswapPool.token0(); - address s_token1 = uniswapPool.token1(); - - // Start and store the collateral token0/1 - _initalizeCollateralPair(token0, token1, uniswapPool); - - (, int24 currentTick, , , , , ) = uniswapPool.slot0(); - - unchecked { - s_miniMedian = - (uint256(block.number) << 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 4 - (uint256(uint24(currentTick))); // add to slot 3 - } - - // Approve transfers of Panoptic Pool funds by SFPM - IERC20Partial(s_token0).approve(address(SFPM), type(uint256).max); - IERC20Partial(s_token1).approve(address(SFPM), type(uint256).max); - - // Approve transfers of Panoptic Pool funds by Collateral token - IERC20Partial(s_token0).approve(address(s_collateralToken0), type(uint256).max); - IERC20Partial(s_token1).approve(address(s_collateralToken1), type(uint256).max); - } - - // Generate a new pair of collateral tokens from a univ3 pool - function _initalizeCollateralPair( - address token0, - address token1, - IUniswapV3Pool uniswapPool - ) internal { - // Deploy collateral tokens - s_collateralToken0 = new CollateralTrackerHarness(); - s_collateralToken1 = new CollateralTrackerHarness(); - - // initialize the token - s_collateralToken0.startToken(true, token0, token1, uniswapPool.fee(), this); - s_collateralToken1.startToken(false, token0, token1, uniswapPool.fee(), this); - } + constructor( + SemiFungiblePositionManager _SFPM, + IPoolManager _manager + ) PanopticPool(_SFPM, _manager) {} function delegate(address delegatee, CollateralTracker collateralToken) external { collateralToken.delegate(delegatee); @@ -180,7 +144,7 @@ contract PanopticPoolHarness is PanopticPool { } function getTWAP() external view returns (int24 twapTick) { - return PanopticPool.getUniV3TWAP(); + return PanopticPool.getOracleTWAP(); } function positionBalance( @@ -192,7 +156,7 @@ contract PanopticPoolHarness is PanopticPool { } contract SemiFungiblePositionManagerHarness is SemiFungiblePositionManager { - constructor(IUniswapV3Factory _factory) SemiFungiblePositionManager(_factory) {} + constructor(IPoolManager _manager) SemiFungiblePositionManager(_manager) {} function accountLiquidity( bytes32 positionKey @@ -351,6 +315,12 @@ contract CollateralTrackerTest is Test, PositionUtils { CollateralTrackerHarness collateralToken0; CollateralTrackerHarness collateralToken1; + IPoolManager manager; + + V4RouterSimple routerV4; + + PoolKey poolKey; + /*////////////////////////////////////////////////////////////// POSITION DATA //////////////////////////////////////////////////////////////*/ @@ -415,12 +385,11 @@ contract CollateralTrackerTest is Test, PositionUtils { // Pick a pool from the seed and cache initial state _cacheWorldState(pools[bound(seed, 0, pools.length - 1)]); - _deployCustomPanopticPool(token0, token1, pool); + _deployCustomPanopticPool(); } function _cacheWorldState(IUniswapV3Pool _pool) internal { pool = _pool; - poolId = PanopticMath.getPoolId(address(_pool)); token0 = _pool.token0(); token1 = _pool.token1(); isWETH = token0 == address(WETH) ? 0 : 1; @@ -429,29 +398,110 @@ contract CollateralTrackerTest is Test, PositionUtils { (currentSqrtPriceX96, currentTick, , , , , ) = _pool.slot0(); feeGrowthGlobal0X128 = _pool.feeGrowthGlobal0X128(); feeGrowthGlobal1X128 = _pool.feeGrowthGlobal1X128(); + + poolKey = PoolKey( + Currency.wrap(token0), + Currency.wrap(token1), + fee, + tickSpacing, + IHooks(address(0)) + ); + poolId = PanopticMath.getPoolId(poolKey.toId(), poolKey.tickSpacing); } - function _deployCustomPanopticPool( - address _token0, - address _token1, - IUniswapV3Pool uniswapPool - ) internal { + function _deployCustomPanopticPool() internal { + manager = new PoolManager(); + routerV4 = new V4RouterSimple(manager); + + vm.startPrank(Swapper); + + deal(token0, Swapper, type(uint248).max); + deal(token1, Swapper, type(uint248).max); + + IERC20Partial(token0).approve(address(router), type(uint256).max); + IERC20Partial(token1).approve(address(router), type(uint256).max); + + IERC20Partial(token0).approve(address(routerV4), type(uint256).max); + IERC20Partial(token1).approve(address(routerV4), type(uint256).max); + + manager.initialize(poolKey, currentSqrtPriceX96); + + routerV4.modifyLiquidity( + address(0), + poolKey, + (TickMath.MIN_TICK / tickSpacing) * tickSpacing, + (TickMath.MAX_TICK / tickSpacing) * tickSpacing, + 1_000_000 ether + ); + // deploy the semiFungiblePositionManager - sfpm = new SemiFungiblePositionManagerHarness(V3FACTORY); + sfpm = new SemiFungiblePositionManagerHarness(manager); // Initialize the world pool - sfpm.initializeAMMPool(_token0, _token1, fee); + sfpm.initializeAMMPool(poolKey); panopticHelper = new PanopticHelper(SemiFungiblePositionManager(sfpm)); - // deploy modified Panoptic pool - panopticPool = new PanopticPoolHarness(sfpm); + vm.startPrank(address(this)); - // initalize Panoptic Pool - panopticPool.modifiedStartPool(_token0, _token1, uniswapPool); + panopticPool = PanopticPoolHarness( + ClonesWithImmutableArgs.addressOfClone3(PoolId.unwrap(poolKey.toId())) + ); - collateralToken0 = CollateralTrackerHarness(address(panopticPool.collateralToken0())); - collateralToken1 = CollateralTrackerHarness(address(panopticPool.collateralToken1())); + collateralToken0 = new CollateralTrackerHarness(manager); + collateralToken1 = new CollateralTrackerHarness(manager); + + collateralToken0 = CollateralTrackerHarness( + ClonesWithImmutableArgs.clone( + address(collateralToken0), + abi.encodePacked( + panopticPool, + true, + token0, + token0, + token1, + fee, + (fee * uint256(20_000)) / 10_000 + ) + ) + ); + collateralToken1 = CollateralTrackerHarness( + ClonesWithImmutableArgs.clone( + address(collateralToken1), + abi.encodePacked( + panopticPool, + false, + token1, + token0, + token1, + fee, + (fee * uint256(20_000)) / 10_000 + ) + ) + ); + + panopticPool = new PanopticPoolHarness(sfpm, manager); + + panopticPool = PanopticPoolHarness( + ClonesWithImmutableArgs.clone3( + address(panopticPool), + abi.encodePacked( + collateralToken0, + collateralToken1, + pool, + poolKey.toId(), + abi.encode(poolKey) + ), + PoolId.unwrap(poolKey.toId()) + ) + ); + + vm.startPrank(Swapper); + + panopticPool.initialize(); + + collateralToken0.initialize(); + collateralToken1.initialize(); // store panoptic pool address panopticPoolAddress = address(panopticPool); @@ -484,19 +534,20 @@ contract CollateralTrackerTest is Test, PositionUtils { // deposit to panoptic pool collateralToken0.setPoolAssets(collateralToken0._availableAssets() + initialMockTokens); collateralToken1.setPoolAssets(collateralToken1._availableAssets() + initialMockTokens); - deal( - token0, - address(panopticPool), - IERC20Partial(token0).balanceOf(panopticPoolAddress) + initialMockTokens - ); - deal( - token1, - address(panopticPool), - IERC20Partial(token1).balanceOf(panopticPoolAddress) + initialMockTokens - ); + + (, address pranked, ) = vm.readCallers(); + vm.startPrank(Swapper); + routerV4.mintCurrency(address(0), Currency.wrap(token0), initialMockTokens); + routerV4.mintCurrency(address(0), Currency.wrap(token1), initialMockTokens); + + manager.transfer(address(panopticPool), uint160(token0), initialMockTokens); + manager.transfer(address(panopticPool), uint160(token1), initialMockTokens); + + vm.startPrank(pranked); + + manager.transfer(address(0), 0, 0); } - //@note move this and panopticPool helper into position utils /*////////////////////////////////////////////////////////////// HELPERS //////////////////////////////////////////////////////////////*/ @@ -505,13 +556,9 @@ contract CollateralTrackerTest is Test, PositionUtils { function twoWaySwap(uint256 swapSize) public { vm.startPrank(Swapper); - deal(token0, Swapper, type(uint248).max); - deal(token1, Swapper, type(uint248).max); - - IERC20Partial(token0).approve(address(router), type(uint256).max); - IERC20Partial(token1).approve(address(router), type(uint256).max); - + uint160 originalSqrtPriceX96 = V4StateReader.getSqrtPriceX96(manager, poolKey.toId()); swapSize = bound(swapSize, 10 ** 18, 10 ** 22); + router.exactInputSingle( ISwapRouter.ExactInputSingleParams( isWETH == 0 ? token0 : token1, @@ -525,6 +572,10 @@ contract CollateralTrackerTest is Test, PositionUtils { ) ); + (uint160 swappedSqrtPriceX96, , , , , , ) = pool.slot0(); + + routerV4.swapTo(address(0), poolKey, swappedSqrtPriceX96); + router.exactOutputSingle( ISwapRouter.ExactOutputSingleParams( isWETH == 1 ? token0 : token1, @@ -538,7 +589,10 @@ contract CollateralTrackerTest is Test, PositionUtils { ) ); - (currentSqrtPriceX96, currentTick, , , , , ) = pool.slot0(); + routerV4.swapTo(address(0), poolKey, originalSqrtPriceX96); + + currentSqrtPriceX96 = V4StateReader.getSqrtPriceX96(manager, poolKey.toId()); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); } function setUp() public {} @@ -547,7 +601,7 @@ contract CollateralTrackerTest is Test, PositionUtils { START TOKEN TESTS //////////////////////////////////////////////////////////////*/ - function test_Success_StartToken_virtualShares() public { + function test_Success_initialize_virtualShares() public { _initWorld(0); CollateralTracker ct = new CollateralTracker( 10, @@ -556,26 +610,27 @@ contract CollateralTrackerTest is Test, PositionUtils { -1_024, 5_000, 9_000, - 20_000 + 20, + manager ); - ct.startToken(false, token0, token1, fee, panopticPool); + ct.initialize(); assertEq(ct.totalSupply(), 10 ** 6); assertEq(ct.totalAssets(), 1); } - function test_Fail_startToken_alreadyInitializedToken(uint256 x) public { + function test_Fail_initialize_alreadyInitializedToken(uint256 x) public { _initWorld(x); // Deploy collateral token - collateralToken0 = new CollateralTrackerHarness(); + collateralToken0 = new CollateralTrackerHarness(manager); // initialize the token - collateralToken0.startToken(false, token0, token1, fee, PanopticPool(address(0))); + collateralToken0.initialize(); // fails if already initialized vm.expectRevert(Errors.CollateralTokenAlreadyInitialized.selector); - collateralToken0.startToken(false, token0, token1, fee, PanopticPool(address(0))); + collateralToken0.initialize(); } /*////////////////////////////////////////////////////////////// @@ -625,8 +680,8 @@ contract CollateralTrackerTest is Test, PositionUtils { address underlyingToken1 = collateralToken1.asset(); // check if the panoptic pool got transferred the correct underlying assets - assertEq(assets, IERC20Partial(underlyingToken0).balanceOf(address(panopticPool))); - assertEq(assets, IERC20Partial(underlyingToken1).balanceOf(address(panopticPool))); + assertEq(assets, manager.balanceOf(address(panopticPool), uint160(underlyingToken0))); + assertEq(assets, manager.balanceOf(address(panopticPool), uint160(underlyingToken1))); } function test_Fail_deposit_DepositTooLarge(uint256 x, uint256 assets) public { @@ -862,8 +917,8 @@ contract CollateralTrackerTest is Test, PositionUtils { positionIdList, uint128(bound(positionSizeSeed, 501, 1000)), 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -898,8 +953,8 @@ contract CollateralTrackerTest is Test, PositionUtils { positionIdList, 750, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); tokenId = TokenId.wrap(0).addPoolId(poolId).addLeg(0, 1, 0, 1, 0, 0, strike, width); @@ -909,8 +964,8 @@ contract CollateralTrackerTest is Test, PositionUtils { positionIdList, 500, type(uint64).max, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); collateralToken0.setPoolAssets(collateralToken0._availableAssets() - 300); @@ -921,8 +976,8 @@ contract CollateralTrackerTest is Test, PositionUtils { panopticPool.burnOptions( tokenId, positionIdList, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -957,8 +1012,8 @@ contract CollateralTrackerTest is Test, PositionUtils { positionIdList, 750, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); tokenId = TokenId.wrap(0).addPoolId(poolId).addLeg(0, 1, 0, 1, 0, 0, strike, width); @@ -971,8 +1026,8 @@ contract CollateralTrackerTest is Test, PositionUtils { positionIdList, 500, type(uint64).max, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -1007,8 +1062,8 @@ contract CollateralTrackerTest is Test, PositionUtils { positionIdList, 750, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); collateralToken0.setInAMM(-250); @@ -1019,8 +1074,8 @@ contract CollateralTrackerTest is Test, PositionUtils { panopticPool.burnOptions( tokenId, positionIdList, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -1239,8 +1294,14 @@ contract CollateralTrackerTest is Test, PositionUtils { address underlyingToken1 = collateralToken1.asset(); // check if the panoptic pool got transferred the correct underlying assets - assertEq(returnedAssets0, IERC20Partial(underlyingToken0).balanceOf(address(panopticPool))); - assertEq(returnedAssets1, IERC20Partial(underlyingToken1).balanceOf(address(panopticPool))); + assertEq( + returnedAssets0, + manager.balanceOf(address(panopticPool), uint160(underlyingToken0)) + ); + assertEq( + returnedAssets1, + manager.balanceOf(address(panopticPool), uint160(underlyingToken1)) + ); } function test_Fail_mint_DepositTooLarge(uint256 x, uint256 shares) public { @@ -1355,8 +1416,8 @@ contract CollateralTrackerTest is Test, PositionUtils { positionIdList, positionSize0, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); // Attempt a transfer to Alice from Bob @@ -1447,8 +1508,8 @@ contract CollateralTrackerTest is Test, PositionUtils { positionIdList, positionSize0, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -1794,11 +1855,8 @@ contract CollateralTrackerTest is Test, PositionUtils { } // access control on delegate/revoke/settlement functions - function test_Fail_All_OnlyPanopticPool(uint256 x, address caller) public { + function test_Fail_All_OnlyPanopticPool(uint256 x) public { _initWorld(x); - vm.assume(caller != address(panopticPool)); - - vm.prank(caller); vm.expectRevert(Errors.NotPanopticPool.selector); collateralToken0.delegate(address(0)); @@ -1816,8 +1874,8 @@ contract CollateralTrackerTest is Test, PositionUtils { collateralToken0.takeCommissionAddData( address(0), 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK, + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK, false ); @@ -1826,8 +1884,8 @@ contract CollateralTrackerTest is Test, PositionUtils { address(0), 0, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -1850,9 +1908,6 @@ contract CollateralTrackerTest is Test, PositionUtils { { _initWorld(x); - // initalize a custom Panoptic pool - _deployCustomPanopticPool(token0, token1, pool); - // Invoke all interactions with the Collateral Tracker from user Bob vm.startPrank(Bob); @@ -2047,9 +2102,6 @@ contract CollateralTrackerTest is Test, PositionUtils { { _initWorld(x); - // initalize a custom Panoptic pool - _deployCustomPanopticPool(token0, token1, pool); - // Invoke all interactions with the Collateral Tracker from user Bob vm.startPrank(Bob); @@ -2128,6 +2180,7 @@ contract CollateralTrackerTest is Test, PositionUtils { // mimic pool activity twoWaySwap(swapSizeSeed); + vm.startPrank(Alice); // check requirement at fuzzed tick { @@ -2245,9 +2298,6 @@ contract CollateralTrackerTest is Test, PositionUtils { { _initWorld(x); - // initalize a custom Panoptic pool - _deployCustomPanopticPool(token0, token1, pool); - // Invoke all interactions with the Collateral Tracker from user Bob vm.startPrank(Bob); @@ -2334,6 +2384,7 @@ contract CollateralTrackerTest is Test, PositionUtils { // mimic pool activity twoWaySwap(swapSizeSeed); + vm.startPrank(Alice); // check requirement at fuzzed tick { @@ -2452,9 +2503,6 @@ contract CollateralTrackerTest is Test, PositionUtils { { _initWorld(x); - // initalize a custom Panoptic pool - _deployCustomPanopticPool(token0, token1, pool); - // Invoke all interactions with the Collateral Tracker from user Bob vm.startPrank(Bob); @@ -2542,6 +2590,7 @@ contract CollateralTrackerTest is Test, PositionUtils { // mimic pool activity twoWaySwap(swapSizeSeed); + vm.startPrank(Alice); // check requirement at fuzzed tick { @@ -2661,9 +2710,6 @@ contract CollateralTrackerTest is Test, PositionUtils { { _initWorld(x); - // initalize a custom Panoptic pool - _deployCustomPanopticPool(token0, token1, pool); - // Invoke all interactions with the Collateral Tracker from user Bob vm.startPrank(Bob); @@ -2782,6 +2828,7 @@ contract CollateralTrackerTest is Test, PositionUtils { // mimic pool activity twoWaySwap(swapSizeSeed); + vm.startPrank(Alice); // check requirement at fuzzed tick { @@ -2903,9 +2950,6 @@ contract CollateralTrackerTest is Test, PositionUtils { { _initWorld(x); - // initalize a custom Panoptic pool - _deployCustomPanopticPool(token0, token1, pool); - // Invoke all interactions with the Collateral Tracker from user Bob vm.startPrank(Bob); @@ -3035,6 +3079,7 @@ contract CollateralTrackerTest is Test, PositionUtils { // mimic pool activity twoWaySwap(swapSizeSeed); + vm.startPrank(Alice); // check requirement at fuzzed tick { @@ -3155,9 +3200,6 @@ contract CollateralTrackerTest is Test, PositionUtils { { _initWorld(x); - // initalize a custom Panoptic pool - _deployCustomPanopticPool(token0, token1, pool); - // Invoke all interactions with the Collateral Tracker from user Bob vm.startPrank(Bob); @@ -3250,6 +3292,7 @@ contract CollateralTrackerTest is Test, PositionUtils { // mimic pool activity twoWaySwap(swapSizeSeed); + vm.startPrank(Alice); // check requirement at fuzzed tick { @@ -3387,9 +3430,6 @@ contract CollateralTrackerTest is Test, PositionUtils { { _initWorld(x); - // initalize a custom Panoptic pool - _deployCustomPanopticPool(token0, token1, pool); - // Invoke all interactions with the Collateral Tracker from user Bob vm.startPrank(Bob); @@ -3482,6 +3522,7 @@ contract CollateralTrackerTest is Test, PositionUtils { // mimic pool activity twoWaySwap(swapSizeSeed); + vm.startPrank(Alice); // check requirement at fuzzed tick { @@ -3605,9 +3646,6 @@ contract CollateralTrackerTest is Test, PositionUtils { { _initWorld(x); - // initalize a custom Panoptic pool - _deployCustomPanopticPool(token0, token1, pool); - // Invoke all interactions with the Collateral Tracker from user Bob vm.startPrank(Bob); @@ -3700,6 +3738,7 @@ contract CollateralTrackerTest is Test, PositionUtils { // mimic pool activity twoWaySwap(swapSizeSeed); + vm.startPrank(Alice); // check requirement at fuzzed tick { @@ -3827,9 +3866,6 @@ contract CollateralTrackerTest is Test, PositionUtils { { _initWorld(x); - // initalize a custom Panoptic pool - _deployCustomPanopticPool(token0, token1, pool); - // Invoke all interactions with the Collateral Tracker from user Bob vm.startPrank(Bob); @@ -3901,6 +3937,7 @@ contract CollateralTrackerTest is Test, PositionUtils { // mimic pool activity twoWaySwap(swapSizeSeed); + vm.startPrank(Bob); // check requirement at fuzzed tick { @@ -4007,9 +4044,6 @@ contract CollateralTrackerTest is Test, PositionUtils { { _initWorld(x); - // initalize a custom Panoptic pool - _deployCustomPanopticPool(token0, token1, pool); - // Invoke all interactions with the Collateral Tracker from user Bob vm.startPrank(Bob); @@ -4079,6 +4113,7 @@ contract CollateralTrackerTest is Test, PositionUtils { // mimic pool activity twoWaySwap(swapSizeSeed); + vm.startPrank(Bob); // check requirement at fuzzed tick { @@ -4210,9 +4245,6 @@ contract CollateralTrackerTest is Test, PositionUtils { { _initWorld(x); - // initalize a custom Panoptic pool - _deployCustomPanopticPool(token0, token1, pool); - // Invoke all interactions with the Collateral Tracker from user Bob vm.startPrank(Bob); @@ -4277,6 +4309,7 @@ contract CollateralTrackerTest is Test, PositionUtils { // mimic pool activity twoWaySwap(swapSizeSeed); + vm.startPrank(Bob); // check requirement at fuzzed tick { @@ -4402,9 +4435,6 @@ contract CollateralTrackerTest is Test, PositionUtils { { _initWorld(x); - // initalize a custom Panoptic pool - _deployCustomPanopticPool(token0, token1, pool); - // Invoke all interactions with the Collateral Tracker from user Bob vm.startPrank(Bob); @@ -4469,6 +4499,7 @@ contract CollateralTrackerTest is Test, PositionUtils { // mimic pool activity twoWaySwap(swapSizeSeed); + vm.startPrank(Bob); // check requirement at fuzzed tick { @@ -4595,9 +4626,6 @@ contract CollateralTrackerTest is Test, PositionUtils { { _initWorld(x); - // initalize a custom Panoptic pool - _deployCustomPanopticPool(token0, token1, pool); - // Invoke all interactions with the Collateral Tracker from user Bob vm.startPrank(Bob); @@ -4658,6 +4686,7 @@ contract CollateralTrackerTest is Test, PositionUtils { // mimic pool activity twoWaySwap(swapSizeSeed); + vm.startPrank(Bob); // check requirement at fuzzed tick { @@ -4770,9 +4799,6 @@ contract CollateralTrackerTest is Test, PositionUtils { { _initWorld(x); - // initalize a custom Panoptic pool - _deployCustomPanopticPool(token0, token1, pool); - // Invoke all interactions with the Collateral Tracker from user Bob vm.startPrank(Bob); @@ -4832,6 +4858,7 @@ contract CollateralTrackerTest is Test, PositionUtils { // mimic pool activity twoWaySwap(swapSizeSeed); + vm.startPrank(Bob); // check requirement at fuzzed tick { @@ -4954,9 +4981,6 @@ contract CollateralTrackerTest is Test, PositionUtils { { _initWorld(x); - // initalize a custom Panoptic pool - _deployCustomPanopticPool(token0, token1, pool); - // Invoke all interactions with the Collateral Tracker from user Bob vm.startPrank(Bob); @@ -5000,6 +5024,7 @@ contract CollateralTrackerTest is Test, PositionUtils { // mimic pool activity twoWaySwap(swapSizeSeed); + vm.startPrank(Bob); // check requirement at fuzzed tick { @@ -5142,9 +5167,6 @@ contract CollateralTrackerTest is Test, PositionUtils { { _initWorld(x); - // initalize a custom Panoptic pool - _deployCustomPanopticPool(token0, token1, pool); - // Invoke all interactions with the Collateral Tracker from user Bob vm.startPrank(Bob); @@ -5261,9 +5283,6 @@ contract CollateralTrackerTest is Test, PositionUtils { { _initWorld(x); - // initalize a custom Panoptic pool - _deployCustomPanopticPool(token0, token1, pool); - // Invoke all interactions with the Collateral Tracker from user Bob vm.startPrank(Bob); @@ -5398,9 +5417,6 @@ contract CollateralTrackerTest is Test, PositionUtils { { _initWorld(x); - // initalize a custom Panoptic pool - _deployCustomPanopticPool(token0, token1, pool); - // Invoke all interactions with the Collateral Tracker from user Bob vm.startPrank(Bob); @@ -5535,9 +5551,6 @@ contract CollateralTrackerTest is Test, PositionUtils { { _initWorld(x); - // initalize a custom Panoptic pool - _deployCustomPanopticPool(token0, token1, pool); - // Invoke all interactions with the Collateral Tracker from user Bob vm.startPrank(Bob); @@ -5695,7 +5708,9 @@ contract CollateralTrackerTest is Test, PositionUtils { // utilization = 9000.0 // //----------------------------------------------------------- - int128 _poolBalance = int128(int256(IERC20Partial(token).balanceOf(address(panopticPool)))); + int128 _poolBalance = int128( + int256(manager.balanceOf(address(panopticPool), uint160(token))) + ); // Solve for a mocked inAMM amount using real lockedFunds and pool bal // satisfy the condition of poolBalance > lockedFunds // let poolBalance and lockedFunds be fuzzed @@ -5706,7 +5721,6 @@ contract CollateralTrackerTest is Test, PositionUtils { // set states collateralToken.setInAMM(int128(inAMM)); - deal(token, address(panopticPool), uint128(_poolBalance)); } /*////////////////////////////////////////////////////////////// @@ -6060,47 +6074,6 @@ contract CollateralTrackerTest is Test, PositionUtils { assertEq(expectedPoolUtilization, currentPoolUtilization); } - function test_Success_name(uint256 x) public { - _initWorld(x); - - // string memory expectedName = - // string.concat( - // "POPT-V1", - // " ", - // IERC20Metadata(s_univ3token0).symbol(), - // " LP on ", - // symbol0, - // "/", - // symbol1, - // " ", - // fee % 100 == 0 - // ? Strings.toString(fee / 100) - // : string.concat(Strings.toString(fee / 100), ".", Strings.toString(fee % 100)), - // "bps" - // ); - - string memory returnedName = collateralToken0.name(); - console2.log(returnedName); - } - - function test_Success_symbol(uint256 x) public { - _initWorld(x); - - // string.concat(TICKER_PREFIX, symbol); - // "po" + symbol IERC20Metadata(s_underlyingToken).symbol() - - string memory returnedSymbol = collateralToken0.symbol(); - console2.log(returnedSymbol); - } - - function test_Success_decimals(uint256 x) public { - _initWorld(x); - - //IERC20Metadata(s_underlyingToken).decimals() - - console2.log(collateralToken0.decimals()); - } - /*////////////////////////////////////////////////////////////// REPLICATED FUNCTIONS (TEST HELPERS) //////////////////////////////////////////////////////////////*/ @@ -6179,20 +6152,6 @@ contract CollateralTrackerTest is Test, PositionUtils { // Initialize variables 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; - - // construct the swap callback struct - data = abi.encode( - CallbackData({ - univ3poolKey: PoolAddress.PoolKey({ - token0: pool.token0(), - token1: pool.token1(), - fee: pool.fee() - }), - payer: address(panopticPool) - }) - ); - if ((itm0 != 0) && (itm1 != 0)) { (uint160 sqrtPriceX96, , , , , , ) = pool.slot0(); @@ -6218,18 +6177,11 @@ contract CollateralTrackerTest is Test, PositionUtils { } // assert the pool has enough funds to complete the swap for an ITM position (exchange tokens to token type) - bytes memory callData = abi.encodeCall( - pool.swap, - ( - address(panopticPool), - zeroForOne, - swapAmount, - zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1, - data - ) - ); - (bool ok, ) = address(pool).call(callData); - vm.assume(ok); + try + V4RouterSimple(address(sfpm)).swap(address(0), poolKey, swapAmount, zeroForOne) + {} catch { + vm.assume(false); + } } // Checks to see that a valid position is minted via simulation @@ -6242,13 +6194,25 @@ contract CollateralTrackerTest is Test, PositionUtils { ) internal { // take a snapshot at this storage state uint256 snapshot = vm.snapshot(); - { - IERC20Partial(token0).approve(address(sfpm), type(uint256).max); - IERC20Partial(token1).approve(address(sfpm), type(uint256).max); + vm.startPrank(address(panopticPool)); - // mock mints and burns from the SFPM - vm.startPrank(address(sfpm)); - } + vm.etch(address(sfpm), address(routerV4).code); + + manager.setOperator(address(routerV4), true); + + routerV4.burnCurrency( + address(0), + poolKey.currency0, + manager.balanceOf(address(panopticPool), uint160(token0)) + ); + routerV4.burnCurrency( + address(0), + poolKey.currency1, + manager.balanceOf(address(panopticPool), uint160(token1)) + ); + + IERC20Partial(token0).approve(address(sfpm), type(uint256).max); + IERC20Partial(token1).approve(address(sfpm), type(uint256).max); int128 itm0; int128 itm1; @@ -6324,40 +6288,64 @@ contract CollateralTrackerTest is Test, PositionUtils { /// simulate mint/burn // mint in pool if short if (tokenId.isLong(i) == 0) { + // try + // pool.mint( + // address(sfpm), + // legLowerTick, + // legUpperTick, + // uint128(liquidity), + // abi.encode( + // CallbackData({ + // univ3poolKey: PoolAddress.PoolKey({ + // token0: pool.token0(), + // token1: pool.token1(), + // fee: pool.fee() + // }), + // payer: address(panopticPool) + // }) + // ) + // ) try - pool.mint( - address(sfpm), + V4RouterSimple(address(sfpm)).modifyLiquidity( + address(0), + poolKey, legLowerTick, legUpperTick, - uint128(liquidity), - abi.encode( - CallbackData({ - univ3poolKey: PoolAddress.PoolKey({ - token0: pool.token0(), - token1: pool.token1(), - fee: pool.fee() - }), - payer: address(panopticPool) - }) - ) + int256(liquidity) ) - returns (uint256 _amount0, uint256 _amount1) { + returns (int256 _amount0, int256 _amount1) { // assert that it meets the dust threshold requirement vm.assume( - (_amount0 > 50 && _amount0 < uint128(type(int128).max)) || - (_amount1 > 50 && _amount1 < uint128(type(int128).max)) + (-_amount0 > 50 && -_amount0 < type(int128).max) || + (-_amount1 > 50 && -_amount1 < type(int128).max) ); if (tokenType == 1) { - itm0 += int128(uint128(_amount0)); + itm0 += int128(-_amount0); } else { - itm1 += int128(uint128(_amount1)); + itm1 += int128(-_amount1); } } catch { vm.assume(false); // invalid position, discard } } else { - pool.burn(legLowerTick, legUpperTick, uint128(liquidity)); + address _caller = caller; + V4RouterSimple(address(sfpm)).modifyLiquidityWithSalt( + address(0), + poolKey, + legLowerTick, + legUpperTick, + int256(liquidity), + keccak256( + abi.encodePacked( + poolKey.toId(), + _caller, + tokenType, + legLowerTick, + legUpperTick + ) + ) + ); } } diff --git a/test/foundry/core/Misc.t.sol b/test/foundry/core/Misc.t.sol index 2e09d15..bf54634 100644 --- a/test/foundry/core/Misc.t.sol +++ b/test/foundry/core/Misc.t.sol @@ -15,15 +15,27 @@ import {TickMath} from "v3-core/libraries/TickMath.sol"; import {TokenId} from "@types/TokenId.sol"; import {LeftRightUnsigned, LeftRightSigned} from "@types/LeftRight.sol"; import {PanopticMath} from "@libraries/PanopticMath.sol"; -import {CallbackLib} from "@libraries/CallbackLib.sol"; import {SafeTransferLib} from "@libraries/SafeTransferLib.sol"; import {PositionUtils} from "../testUtils/PositionUtils.sol"; import {Math} from "@libraries/Math.sol"; +import {IV3CompatibleOracle} from "@interfaces/IV3CompatibleOracle.sol"; import {Errors} from "@libraries/Errors.sol"; -import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; +import {FixedPointMathLib} from "solmate/src/utils/FixedPointMathLib.sol"; import {Constants} from "@libraries/Constants.sol"; import {Pointer} from "@types/Pointer.sol"; -import {ERC20} from "solmate/tokens/ERC20.sol"; +import {ERC20} from "solmate/src/tokens/ERC20.sol"; +import {ClonesWithImmutableArgs} from "clones-with-immutable-args/ClonesWithImmutableArgs.sol"; +// V4 types +import {PoolId} from "v4-core/types/PoolId.sol"; +import {PoolKey} from "v4-core/types/PoolKey.sol"; +import {StateLibrary} from "v4-core/libraries/StateLibrary.sol"; +import {V4StateReader} from "@libraries/V4StateReader.sol"; +import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol"; +import {BalanceDelta} from "v4-core/types/BalanceDelta.sol"; +import {Currency} from "v4-core/types/Currency.sol"; +import {PoolManager} from "v4-core/PoolManager.sol"; +import {IHooks} from "v4-core/interfaces/IHooks.sol"; +import {V4RouterSimple} from "../testUtils/V4RouterSimple.sol"; contract ERC20S is ERC20 { constructor( @@ -38,13 +50,24 @@ contract ERC20S is ERC20 { } contract SwapperC { + struct PoolFeatures { + address token0; + address token1; + uint24 fee; + } + + struct CallbackData { + PoolFeatures poolFeatures; + address payer; + } + 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)); + CallbackData memory decoded = abi.decode(data, (CallbackData)); // Extract the address of the token to be sent (amount0 -> token0, amount1 -> token1) address token = amount0Delta > 0 @@ -66,7 +89,7 @@ contract SwapperC { bytes calldata data ) external { // Decode the mint callback data - CallbackLib.CallbackData memory decoded = abi.decode(data, (CallbackLib.CallbackData)); + CallbackData memory decoded = abi.decode(data, (CallbackData)); // Sends the amount0Owed and amount1Owed quantities provided if (amount0Owed > 0) @@ -92,8 +115,8 @@ contract SwapperC { tickUpper, liquidity, abi.encode( - CallbackLib.CallbackData({ - poolFeatures: CallbackLib.PoolFeatures({ + CallbackData({ + poolFeatures: PoolFeatures({ token0: pool.token0(), token1: pool.token1(), fee: pool.fee() @@ -119,8 +142,8 @@ contract SwapperC { type(int128).max, sqrtPriceX96, abi.encode( - CallbackLib.CallbackData({ - poolFeatures: CallbackLib.PoolFeatures({ + CallbackData({ + poolFeatures: PoolFeatures({ token0: pool.token0(), token1: pool.token1(), fee: pool.fee() @@ -154,6 +177,12 @@ contract Misctest is Test, PositionUtils { CollateralTracker ct1; PanopticHelper ph; + IPoolManager manager; + + V4RouterSimple routerV4; + + PoolKey poolKey; + int24 currentTick; int256 twapTick; int24 slowOracleTick; @@ -206,25 +235,39 @@ contract Misctest is Test, PositionUtils { function setUp() public { vm.startPrank(Deployer); - sfpm = new SemiFungiblePositionManager(V3FACTORY); + manager = IPoolManager(address(new PoolManager())); + + routerV4 = new V4RouterSimple(manager); + + sfpm = new SemiFungiblePositionManager(manager); ph = new PanopticHelper(sfpm); // deploy reference pool and collateral token - poolReference = address(new PanopticPool(sfpm)); + poolReference = address(new PanopticPool(sfpm, manager)); collateralReference = address( - new CollateralTracker(10, 2_000, 1_000, -1_024, 5_000, 9_000, 20_000) + new CollateralTracker(10, 2_000, 1_000, -1_024, 5_000, 9_000, 20, manager) ); token0 = new ERC20S("token0", "T0", 18); token1 = new ERC20S("token1", "T1", 18); uniPool = IUniswapV3Pool(V3FACTORY.createPool(address(token0), address(token1), 500)); + poolKey = PoolKey( + Currency.wrap(address(token0)), + Currency.wrap(address(token1)), + 500, + 10, + IHooks(address(0)) + ); + swapperc = new SwapperC(); vm.startPrank(Swapper); - token0.mint(Swapper, type(uint128).max); - token1.mint(Swapper, type(uint128).max); - token0.approve(address(swapperc), type(uint128).max); - token1.approve(address(swapperc), type(uint128).max); + token0.mint(Swapper, type(uint248).max); + token1.mint(Swapper, type(uint248).max); + token0.approve(address(swapperc), type(uint248).max); + token1.approve(address(swapperc), type(uint248).max); + token0.approve(address(routerV4), type(uint248).max); + token1.approve(address(routerV4), type(uint248).max); // This price causes exactly one unit of liquidity to be minted // above here reverts b/c 0 liquidity cannot be minted @@ -243,10 +286,14 @@ contract Misctest is Test, PositionUtils { swapperc.swapTo(uniPool, 10 ** 17 * 2 ** 96); + manager.initialize(poolKey, 10 ** 17 * 2 ** 96); + swapperc.burn(uniPool, -887270, 887270, 10 ** 18); _createPanopticPool(); + swapperc.mint(uniPool, -10000, 10000, 10 ** 18); + vm.startPrank(Alice); token0.mint(Alice, uint256(type(uint104).max) * 2); @@ -327,7 +374,7 @@ contract Misctest is Test, PositionUtils { factory = new PanopticFactory( address(token1), sfpm, - V3FACTORY, + manager, poolReference, collateralReference, new bytes32[](0), @@ -343,9 +390,8 @@ contract Misctest is Test, PositionUtils { pp = PanopticPool( address( factory.deployNewPool( - address(token0), - address(token1), - 500, + IV3CompatibleOracle(address(uniPool)), + poolKey, uint96(block.timestamp), type(uint256).max, type(uint256).max @@ -355,6 +401,7 @@ contract Misctest is Test, PositionUtils { vm.startPrank(Swapper); swapperc.swapTo(uniPool, 2 ** 96); + routerV4.swapTo(address(0), poolKey, 2 ** 96); // Update median pp.pokeMedian(); @@ -394,7 +441,7 @@ contract Misctest is Test, PositionUtils { $posIdList.push( TokenId .wrap(0) - .addPoolId(PanopticMath.getPoolId(address(uniPool))) + .addPoolId(sfpm.getPoolId(poolKey.toId())) .addLeg(0, 1, 1, 0, 0, 1, 15, 1) .addLeg(1, 1, 1, 0, 1, 0, 15, 1) ); @@ -405,15 +452,15 @@ contract Misctest is Test, PositionUtils { $posIdList, 1_000_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); pp.burnOptions( $posIdList[0], new TokenId[](0), - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -429,7 +476,7 @@ contract Misctest is Test, PositionUtils { $posIdList.push( TokenId .wrap(0) - .addPoolId(PanopticMath.getPoolId(address(uniPool))) + .addPoolId(sfpm.getPoolId(poolKey.toId())) .addLeg(0, 1, 1, 0, 0, 1, 15, 1) .addLeg(1, 1, 1, 0, 1, 0, -15, 1) ); @@ -440,15 +487,15 @@ contract Misctest is Test, PositionUtils { $posIdList, 1_000_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); pp.burnOptions( $posIdList[0], new TokenId[](0), - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -456,7 +503,7 @@ contract Misctest is Test, PositionUtils { vm.startPrank(Seller); $posIdList.push( - TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))).addLeg( + TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( 0, 1, 0, @@ -469,18 +516,18 @@ contract Misctest is Test, PositionUtils { ); vm.expectRevert(Errors.ZeroLiquidity.selector); - pp.mintOptions($posIdList, 537, 0, Constants.MIN_V3POOL_TICK, Constants.MAX_V3POOL_TICK); + pp.mintOptions($posIdList, 537, 0, Constants.MIN_V4POOL_TICK, Constants.MAX_V4POOL_TICK); pp.mintOptions( $posIdList, 2_000_000, 0, - Constants.MIN_V3POOL_TICK, - Constants.MAX_V3POOL_TICK + Constants.MIN_V4POOL_TICK, + Constants.MAX_V4POOL_TICK ); vm.startPrank(Alice); - $posIdList[0] = TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))).addLeg( + $posIdList[0] = TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( 0, 1, 0, @@ -492,7 +539,7 @@ contract Misctest is Test, PositionUtils { ); vm.expectRevert(Errors.ZeroLiquidity.selector); - pp.mintOptions($posIdList, 537, 0, Constants.MIN_V3POOL_TICK, Constants.MAX_V3POOL_TICK); + pp.mintOptions($posIdList, 537, 0, Constants.MIN_V4POOL_TICK, Constants.MAX_V4POOL_TICK); } function test_success_MintBurnCallSpread() public { @@ -506,7 +553,7 @@ contract Misctest is Test, PositionUtils { vm.startPrank(Seller); $posIdList.push( - TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))).addLeg( + TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( 0, 1, 1, @@ -522,14 +569,14 @@ contract Misctest is Test, PositionUtils { $posIdList, 2_000_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); // mint OTM position $posIdList[0] = TokenId .wrap(0) - .addPoolId(PanopticMath.getPoolId(address(uniPool))) + .addPoolId(sfpm.getPoolId(poolKey.toId())) .addLeg(0, 1, 1, 0, 0, 1, 15, 1) .addLeg(1, 1, 1, 1, 0, 0, 35, 1); @@ -539,15 +586,15 @@ contract Misctest is Test, PositionUtils { $posIdList, 1_000_000, type(uint64).max, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); pp.burnOptions( $posIdList[0], new TokenId[](0), - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -562,7 +609,7 @@ contract Misctest is Test, PositionUtils { vm.startPrank(Seller); $posIdList.push( - TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))).addLeg( + TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( 0, 1, 1, @@ -578,14 +625,14 @@ contract Misctest is Test, PositionUtils { $posIdList, 2_000_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); // mint OTM position $posIdList[0] = TokenId .wrap(0) - .addPoolId(PanopticMath.getPoolId(address(uniPool))) + .addPoolId(sfpm.getPoolId(poolKey.toId())) .addLeg(0, 1, 1, 0, 1, 1, -15, 1) .addLeg(1, 1, 1, 1, 1, 0, -35, 1); @@ -595,15 +642,15 @@ contract Misctest is Test, PositionUtils { $posIdList, 1_000_000, type(uint64).max, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); pp.burnOptions( $posIdList[0], new TokenId[](0), - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -617,7 +664,7 @@ contract Misctest is Test, PositionUtils { token1.approve(address(swapperc), type(uint128).max); $posIdList.push( - TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))).addLeg( + TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( 0, 1, 1, @@ -635,11 +682,11 @@ contract Misctest is Test, PositionUtils { $posIdList, 2_000_000, 0, - Constants.MIN_V3POOL_TICK, - Constants.MAX_V3POOL_TICK + Constants.MIN_V4POOL_TICK, + Constants.MAX_V4POOL_TICK ); - $posIdList[0] = TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))).addLeg( + $posIdList[0] = TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( 0, 1, 1, @@ -655,19 +702,20 @@ contract Misctest is Test, PositionUtils { $posIdList, 1_000_000, type(uint64).max, - Constants.MIN_V3POOL_TICK, - Constants.MAX_V3POOL_TICK + Constants.MIN_V4POOL_TICK, + Constants.MAX_V4POOL_TICK ); editCollateral(ct1, Alice, 0); vm.startPrank(Swapper); - PanopticMath.twapFilter(uniPool, 600); + PanopticMath.twapFilter(IV3CompatibleOracle(address(uniPool)), 600); vm.warp(block.timestamp + 600); vm.roll(block.number + 1); + routerV4.swapTo(address(0), poolKey, 10 * 2 ** 96); swapperc.swapTo(uniPool, 10 * 2 ** 96); vm.warp(block.timestamp + 600); @@ -676,52 +724,12 @@ contract Misctest is Test, PositionUtils { swapperc.mint(uniPool, -10, 10, 10 ** 18); swapperc.burn(uniPool, -10, 10, 10 ** 18); - PanopticMath.twapFilter(uniPool, 600); + PanopticMath.twapFilter(IV3CompatibleOracle(address(uniPool)), 600); vm.startPrank(Bob); pp.forceExercise(Alice, $posIdList, new TokenId[](0), new TokenId[](0)); } - function test_success_ITMspreadfee_0_01bp() public { - CollateralTracker(collateralReference).startToken( - true, - address(token0), - address(token1), - 1, - pp - ); - - vm.startPrank(Bob); - token0.mint(Bob, type(uint104).max); - token0.approve(collateralReference, type(uint104).max); - CollateralTracker(collateralReference).deposit(type(uint104).max, Bob); - - vm.startPrank(Alice); - token0.mint(Alice, (uint256(1_000_000_000_000_000) * 10_000) / 9_990); - token0.approve(collateralReference, (uint256(1_000_000_000_000_000) * 10_000) / 9_990); - CollateralTracker(collateralReference).deposit( - (uint256(1_000_000_000_000_000) * 10_000) / 9_990, - Alice - ); - - vm.startPrank(address(pp)); - CollateralTracker(collateralReference).takeCommissionAddData( - Alice, - 0, - 0, - 1_000_000_000, - false - ); - assertEq( - 1_000_000_000_000_000 - - 1 - - CollateralTracker(collateralReference).convertToAssets( - CollateralTracker(collateralReference).balanceOf(Alice) - ), - 1_000_000_000 + 2000 - ); - } - function test_parity_maxmint_previewmint() public { assertEq(ct0.previewMint(ct0.maxMint(Alice)), type(uint104).max); } @@ -736,7 +744,7 @@ contract Misctest is Test, PositionUtils { // mint OTM position $posIdList.push( - TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))).addLeg( + TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( 0, 1, 1, @@ -749,7 +757,7 @@ contract Misctest is Test, PositionUtils { ); $tempIdList.push( - TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))).addLeg( + TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( 0, 1, 1, @@ -766,8 +774,8 @@ contract Misctest is Test, PositionUtils { $posIdList, 1_000_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); vm.startPrank(Bob); @@ -777,8 +785,8 @@ contract Misctest is Test, PositionUtils { $tempIdList, 1_000_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -793,7 +801,7 @@ contract Misctest is Test, PositionUtils { // mint OTM position $posIdList.push( - TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))).addLeg( + TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( 0, 1, 1, @@ -810,8 +818,8 @@ contract Misctest is Test, PositionUtils { $posIdList, 1_000_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); TokenId[] memory longPositionList = new TokenId[](257); @@ -823,8 +831,8 @@ contract Misctest is Test, PositionUtils { longPositionList, 1_000_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -872,7 +880,7 @@ contract Misctest is Test, PositionUtils { // mint OTM position $posIdList.push( - TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))).addLeg( + TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( 0, 1, 1, @@ -885,7 +893,7 @@ contract Misctest is Test, PositionUtils { ); $tempIdList.push( - TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))).addLeg( + TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( 0, 1, 1, @@ -898,7 +906,7 @@ contract Misctest is Test, PositionUtils { ); $tempIdList.push( - TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))).addLeg( + TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( 0, 1, 1, @@ -918,8 +926,8 @@ contract Misctest is Test, PositionUtils { $posIdList, 1_000_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); vm.startPrank(Bob); @@ -927,27 +935,30 @@ contract Misctest is Test, PositionUtils { $posIdList, 1_000_000_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); pp.mintOptions( $tempIdList, 900_000_000, type(uint64).max, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); vm.startPrank(Swapper); + routerV4.swapTo(address(0), poolKey, Math.getSqrtRatioAtTick(10) + 1); swapperc.swapTo(uniPool, Math.getSqrtRatioAtTick(10) + 1); accruePoolFeesInRange( - address(uniPool), - uniPool.liquidity() - 1, + manager, + poolKey, + StateLibrary.getLiquidity(manager, poolKey.toId()) - 1, 1_000_000_000_000_000_000_000, 1_000_000_000_000 ); + routerV4.swapTo(address(0), poolKey, 2 ** 96); swapperc.swapTo(uniPool, 2 ** 96); uint256 snap = vm.snapshot(); @@ -958,15 +969,15 @@ contract Misctest is Test, PositionUtils { $posIdList, 250_000_000, type(uint64).max, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); pp.burnOptions( $posIdList[0], new TokenId[](0), - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -974,8 +985,8 @@ contract Misctest is Test, PositionUtils { pp.burnOptions( $posIdList[0], new TokenId[](0), - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); uint256 delta0 = ct0.convertToAssets(ct0.balanceOf(Alice)) - assetsBefore0; @@ -986,8 +997,8 @@ contract Misctest is Test, PositionUtils { pp.burnOptions( $posIdList[0], new TokenId[](0), - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); // there is a small amount of error in token0 -- this is the commissions from Charlie @@ -1011,7 +1022,7 @@ contract Misctest is Test, PositionUtils { // mint OTM position $posIdList.push( - TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))).addLeg( + TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( 0, 1, 1, @@ -1031,12 +1042,12 @@ contract Misctest is Test, PositionUtils { $posIdList, 1_000_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); $posIdList.push( - TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))).addLeg( + TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( 0, 1, 1, @@ -1057,8 +1068,8 @@ contract Misctest is Test, PositionUtils { $tempIdList, 250_000, type(uint64).max, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); vm.startPrank(Bob); @@ -1066,14 +1077,22 @@ contract Misctest is Test, PositionUtils { $posIdList, 250_000, type(uint64).max, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); vm.startPrank(Swapper); + routerV4.swapTo(address(0), poolKey, Math.getSqrtRatioAtTick(10) + 1); swapperc.swapTo(uniPool, Math.getSqrtRatioAtTick(10) + 1); // 1998600539 - accruePoolFeesInRange(address(uniPool), (uniPool.liquidity() * 2) / 3, 1, 1); + accruePoolFeesInRange( + manager, + poolKey, + (StateLibrary.getLiquidity(manager, poolKey.toId()) * 2) / 3, + 1, + 1 + ); + routerV4.swapTo(address(0), poolKey, 2 ** 96); swapperc.swapTo(uniPool, 2 ** 96); vm.startPrank(Bob); @@ -1081,16 +1100,16 @@ contract Misctest is Test, PositionUtils { pp.burnOptions( $posIdList[1], $tempIdList, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); vm.startPrank(Alice); pp.burnOptions( $posIdList[1], new TokenId[](0), - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -1099,8 +1118,8 @@ contract Misctest is Test, PositionUtils { pp.burnOptions( $posIdList[0], new TokenId[](0), - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -1114,7 +1133,7 @@ contract Misctest is Test, PositionUtils { // mint OTM position $posIdList.push( - TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))).addLeg( + TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( 0, 1, 1, @@ -1134,12 +1153,12 @@ contract Misctest is Test, PositionUtils { $posIdList, 1_000_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); $posIdList.push( - TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))).addLeg( + TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( 0, 1, 1, @@ -1158,20 +1177,28 @@ contract Misctest is Test, PositionUtils { $posIdList, 499_999, type(uint64).max, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); vm.startPrank(Swapper); + routerV4.swapTo(address(0), poolKey, Math.getSqrtRatioAtTick(10) + 1); swapperc.swapTo(uniPool, Math.getSqrtRatioAtTick(10) + 1); // 1998600539 - accruePoolFeesInRange(address(uniPool), uniPool.liquidity() - 1, 1, 1); + accruePoolFeesInRange( + manager, + poolKey, + StateLibrary.getLiquidity(manager, poolKey.toId()) - 1, + 1, + 1 + ); + routerV4.swapTo(address(0), poolKey, 2 ** 96); swapperc.swapTo(uniPool, 2 ** 96); vm.startPrank(Bob); pp.burnOptions( $posIdList[1], $tempIdList, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -1179,8 +1206,8 @@ contract Misctest is Test, PositionUtils { pp.burnOptions( $posIdList[0], new TokenId[](0), - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -1194,7 +1221,7 @@ contract Misctest is Test, PositionUtils { // sell primary chunk $posIdLists[0].push( - TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))).addLeg( + TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( 0, 1, 1, @@ -1217,8 +1244,8 @@ contract Misctest is Test, PositionUtils { $posIdLists[0], 500_000_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); vm.startPrank(Bob); @@ -1227,8 +1254,8 @@ contract Misctest is Test, PositionUtils { $posIdLists[0], 250_000_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); vm.startPrank(Charlie); @@ -1237,15 +1264,15 @@ contract Misctest is Test, PositionUtils { $posIdLists[0], 250_000_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); // sell unrelated, non-overlapping, dummy chunk (to buy for match testing) vm.startPrank(Seller); $posIdLists[1].push( - TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))).addLeg( + TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( 0, 1, 1, @@ -1261,13 +1288,13 @@ contract Misctest is Test, PositionUtils { $posIdLists[1], 1_000_000_000 - 9_884_444 * 3, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); // position type A: 1-leg long primary $posIdLists[2].push( - TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))).addLeg( + TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( 0, 1, 1, @@ -1285,8 +1312,8 @@ contract Misctest is Test, PositionUtils { $posIdLists[2], 9_884_444, type(uint64).max, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -1294,7 +1321,7 @@ contract Misctest is Test, PositionUtils { $posIdLists[2].push( TokenId .wrap(0) - .addPoolId(PanopticMath.getPoolId(address(uniPool))) + .addPoolId(sfpm.getPoolId(poolKey.toId())) .addLeg(0, 1, 1, 1, 0, 0, 15, 1) .addLeg(1, 1, 1, 1, 1, 1, -15, 1) ); @@ -1305,8 +1332,8 @@ contract Misctest is Test, PositionUtils { $posIdLists[2], 9_884_444, type(uint64).max, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -1314,7 +1341,7 @@ contract Misctest is Test, PositionUtils { $posIdLists[2].push( TokenId .wrap(0) - .addPoolId(PanopticMath.getPoolId(address(uniPool))) + .addPoolId(sfpm.getPoolId(poolKey.toId())) .addLeg(0, 1, 1, 1, 0, 0, 15, 1) .addLeg(1, 1, 1, 0, 1, 1, -15, 1) ); @@ -1325,14 +1352,14 @@ contract Misctest is Test, PositionUtils { $posIdLists[2], 9_884_444, type(uint64).max, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } // position type D: 1-leg long dummy $posIdLists[2].push( - TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))).addLeg( + TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( 0, 1, 1, @@ -1350,8 +1377,8 @@ contract Misctest is Test, PositionUtils { $posIdLists[2], 19_768_888, type(uint64).max, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -1374,23 +1401,38 @@ contract Misctest is Test, PositionUtils { vm.startPrank(Swapper); + routerV4.swapTo(address(0), poolKey, Math.getSqrtRatioAtTick(10) + 1); swapperc.swapTo(uniPool, Math.getSqrtRatioAtTick(10) + 1); // There are some precision issues with this (1B is not exactly 1B) but close enough to see the effects - accruePoolFeesInRange(address(uniPool), uniPool.liquidity() - 1, 1_000_000, 1_000_000_000); - console2.log("liquidity", uniPool.liquidity()); + accruePoolFeesInRange( + manager, + poolKey, + StateLibrary.getLiquidity(manager, poolKey.toId()) - 1, + 1_000_000, + 1_000_000_000 + ); + console2.log("liquidity", StateLibrary.getLiquidity(manager, poolKey.toId())); // accumulate lower order of fees on dummy chunk + routerV4.swapTo(address(0), poolKey, Math.getSqrtRatioAtTick(-10)); swapperc.swapTo(uniPool, Math.getSqrtRatioAtTick(-10)); - accruePoolFeesInRange(address(uniPool), uniPool.liquidity() - 1, 10_000, 100_000); - console2.log("liquidity", uniPool.liquidity()); + accruePoolFeesInRange( + manager, + poolKey, + StateLibrary.getLiquidity(manager, poolKey.toId()) - 1, + 10_000, + 100_000 + ); + console2.log("liquidity", StateLibrary.getLiquidity(manager, poolKey.toId())); + routerV4.swapTo(address(0), poolKey, 2 ** 96); swapperc.swapTo(uniPool, 2 ** 96); { - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); LeftRightUnsigned accountLiquidityPrimary = sfpm.getAccountLiquidity( - address(uniPool), + poolKey.toId(), address(pp), 0, 10, @@ -1403,7 +1445,7 @@ contract Misctest is Test, PositionUtils { console2.log("accountLiquidityPrimaryRemoved", accountLiquidityPrimary.leftSlot()); (uint256 shortPremium0Primary, uint256 shortPremium1Primary) = sfpm.getAccountPremium( - address(uniPool), + poolKey.toId(), address(pp), 0, 10, @@ -1426,7 +1468,7 @@ contract Misctest is Test, PositionUtils { ); (uint256 longPremium0Primary, uint256 longPremium1Primary) = sfpm.getAccountPremium( - address(uniPool), + poolKey.toId(), address(pp), 0, 10, @@ -1447,7 +1489,7 @@ contract Misctest is Test, PositionUtils { { LeftRightUnsigned accountLiquidityDummy = sfpm.getAccountLiquidity( - address(uniPool), + poolKey.toId(), address(pp), 1, -20, @@ -1461,7 +1503,7 @@ contract Misctest is Test, PositionUtils { console2.log("accountLiquidityDummyRemoved", accountLiquidityDummy.leftSlot()); (uint256 shortPremium0Dummy, uint256 shortPremium1Dummy) = sfpm.getAccountPremium( - address(uniPool), + poolKey.toId(), address(pp), 1, -20, @@ -1484,7 +1526,7 @@ contract Misctest is Test, PositionUtils { ); (uint256 longPremium0Dummy, uint256 longPremium1Dummy) = sfpm.getAccountPremium( - address(uniPool), + poolKey.toId(), address(pp), 1, -20, @@ -1542,8 +1584,8 @@ contract Misctest is Test, PositionUtils { pp.burnOptions( $posIdLists[0][0], new TokenId[](0), - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); assertEq( @@ -1561,7 +1603,7 @@ contract Misctest is Test, PositionUtils { vm.startPrank(Seller); $posIdLists[1].push( - TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))).addLeg( + TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( 0, 1, 1, @@ -1577,8 +1619,8 @@ contract Misctest is Test, PositionUtils { $posIdLists[1], 1_000_000_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); assetsBefore0Arr.push(ct0.convertToAssets(ct0.balanceOf(Buyers[0]))); @@ -1641,8 +1683,8 @@ contract Misctest is Test, PositionUtils { pp.burnOptions( $posIdLists[0][0], new TokenId[](0), - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); assertEq( @@ -1767,8 +1809,8 @@ contract Misctest is Test, PositionUtils { pp.burnOptions( $posIdLists[0][0], new TokenId[](0), - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); assertEq( @@ -1815,8 +1857,8 @@ contract Misctest is Test, PositionUtils { pp.burnOptions( $posIdLists[2], new TokenId[](0), - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); // the positive premium is from the dummy short chunk @@ -1844,7 +1886,7 @@ contract Misctest is Test, PositionUtils { // mint OTM position $posIdList.push( - TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))).addLeg( + TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( 0, 1, 1, @@ -1872,8 +1914,8 @@ contract Misctest is Test, PositionUtils { $posIdList, 500_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); vm.startPrank(Bob); @@ -1882,8 +1924,8 @@ contract Misctest is Test, PositionUtils { $posIdList, 250_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); vm.startPrank(Charlie); @@ -1892,12 +1934,12 @@ contract Misctest is Test, PositionUtils { $posIdList, 250_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); $posIdList.push( - TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))).addLeg( + TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( 0, 1, 1, @@ -1916,8 +1958,8 @@ contract Misctest is Test, PositionUtils { $posIdList, 44_468, type(uint64).max, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); vm.startPrank(Bob); @@ -1927,17 +1969,25 @@ contract Misctest is Test, PositionUtils { $posIdList, 44_468, type(uint64).max, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); vm.startPrank(Swapper); + routerV4.swapTo(address(0), poolKey, Math.getSqrtRatioAtTick(10) + 1); swapperc.swapTo(uniPool, Math.getSqrtRatioAtTick(10) + 1); // There are some precision issues with this (1B is not exactly 1B) but close enough to see the effects - accruePoolFeesInRange(address(uniPool), uniPool.liquidity() - 1, 1_000_000, 1_000_000_000); + accruePoolFeesInRange( + manager, + poolKey, + StateLibrary.getLiquidity(manager, poolKey.toId()) - 1, + 1_000_000, + 1_000_000_000 + ); + routerV4.swapTo(address(0), poolKey, 2 ** 96); swapperc.swapTo(uniPool, 2 ** 96); vm.startPrank(Bob); @@ -1952,8 +2002,8 @@ contract Misctest is Test, PositionUtils { pp.burnOptions( $posIdList[0], $tempIdList, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); assertEq( @@ -1974,8 +2024,8 @@ contract Misctest is Test, PositionUtils { $posIdList, 1_000_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); $tempIdList[0] = $posIdList[1]; @@ -1985,8 +2035,8 @@ contract Misctest is Test, PositionUtils { pp.burnOptions( $posIdList[0], $tempIdList, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); vm.startPrank(Alice); @@ -1999,8 +2049,8 @@ contract Misctest is Test, PositionUtils { pp.burnOptions( $posIdList[1], $tempIdList, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); assertEq( @@ -2018,8 +2068,8 @@ contract Misctest is Test, PositionUtils { pp.burnOptions( $posIdList[0], new TokenId[](0), - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); vm.startPrank(Charlie); @@ -2031,8 +2081,8 @@ contract Misctest is Test, PositionUtils { pp.burnOptions( $posIdList[1], new TokenId[](0), - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); assertEq( @@ -2057,7 +2107,7 @@ contract Misctest is Test, PositionUtils { // mint OTM position $posIdList.push( - TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))).addLeg( + TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( 0, 1, 1, @@ -2075,8 +2125,8 @@ contract Misctest is Test, PositionUtils { $posIdList, 1_000_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); editCollateral(ct0, Bob, ct0.convertToShares(266263)); @@ -2095,7 +2145,7 @@ contract Misctest is Test, PositionUtils { // mint OTM position $posIdList.push( - TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))).addLeg( + TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( 0, 1, 1, @@ -2113,8 +2163,8 @@ contract Misctest is Test, PositionUtils { $posIdList, 1_000_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); editCollateral(ct0, Bob, ct0.convertToShares(1_000_000)); @@ -2133,7 +2183,7 @@ contract Misctest is Test, PositionUtils { // mint OTM position $posIdList.push( - TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))).addLeg( + TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( 0, 1, 1, @@ -2151,8 +2201,8 @@ contract Misctest is Test, PositionUtils { $posIdList, 1_000_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); editCollateral(ct0, Bob, ct0.convertToShares(266262)); @@ -2172,7 +2222,7 @@ contract Misctest is Test, PositionUtils { // mint OTM position $posIdList.push( - TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))).addLeg( + TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( 0, 1, 1, @@ -2190,8 +2240,8 @@ contract Misctest is Test, PositionUtils { $posIdList, 1_000_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); editCollateral(ct0, Bob, ct0.convertToShares(1_000_000)); @@ -2217,12 +2267,13 @@ contract Misctest is Test, PositionUtils { pp.pokeMedian(); swapperc.burn(uniPool, -10, 10, 10 ** 18); } - swapperc.mint(uniPool, -10000, 10000, 10 ** 18); + + routerV4.modifyLiquidity(address(0), poolKey, -10000, 10000, 10 ** 18); int24 tickSpacing = uniPool.tickSpacing(); // mint ITM position $posIdList.push( - TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))).addLeg( + TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( 0, 1, 1, @@ -2234,6 +2285,7 @@ contract Misctest is Test, PositionUtils { ) ); + routerV4.swapTo(address(0), poolKey, Math.getSqrtRatioAtTick(-955)); swapperc.swapTo(uniPool, Math.getSqrtRatioAtTick(-955)); assertTrue(pp.isSafeMode(), "in safe mode"); @@ -2247,7 +2299,7 @@ contract Misctest is Test, PositionUtils { token1.approve(address(ct1), 1_000_000); // deposit bare minimum - ct0.deposit(100_200, Bob); + ct0.deposit(100_300, Bob); ct1.deposit(0, Bob); // mint fails @@ -2257,8 +2309,8 @@ contract Misctest is Test, PositionUtils { $posIdList, 100_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -2278,12 +2330,12 @@ contract Misctest is Test, PositionUtils { pp.pokeMedian(); swapperc.burn(uniPool, -10, 10, 10 ** 18); } - swapperc.mint(uniPool, -10000, 10000, 10 ** 18); + routerV4.modifyLiquidity(address(0), poolKey, -10000, 10000, 10 ** 18); int24 tickSpacing = uniPool.tickSpacing(); // mint ITM position $posIdList.push( - TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))).addLeg( + TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( 0, 1, 1, @@ -2295,6 +2347,7 @@ contract Misctest is Test, PositionUtils { ) ); + routerV4.swapTo(address(0), poolKey, Math.getSqrtRatioAtTick(954)); swapperc.swapTo(uniPool, Math.getSqrtRatioAtTick(954)); assertTrue(pp.isSafeMode(), "in safe mode"); @@ -2309,7 +2362,7 @@ contract Misctest is Test, PositionUtils { // deposit bare minimum - covered ct0.deposit(0, Bob); - ct1.deposit(100_200, Bob); + ct1.deposit(100_300, Bob); // mint fails vm.expectRevert(Errors.AccountInsolvent.selector); @@ -2317,8 +2370,8 @@ contract Misctest is Test, PositionUtils { $posIdList, 100_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -2338,12 +2391,12 @@ contract Misctest is Test, PositionUtils { pp.pokeMedian(); swapperc.burn(uniPool, -10, 10, 10 ** 18); } - swapperc.mint(uniPool, -10000, 10000, 10 ** 18); + routerV4.modifyLiquidity(address(0), poolKey, -10000, 10000, 10 ** 18); int24 tickSpacing = uniPool.tickSpacing(); // mint ITM position $posIdList.push( - TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))).addLeg( + TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( 0, 1, 1, @@ -2355,8 +2408,9 @@ contract Misctest is Test, PositionUtils { ) ); - (, int24 staleTick, , , , , ) = uniPool.slot0(); + int24 staleTick = V4StateReader.getTick(manager, poolKey.toId()); + routerV4.swapTo(address(0), poolKey, Math.getSqrtRatioAtTick(-954)); swapperc.swapTo(uniPool, Math.getSqrtRatioAtTick(-954)); console2.log("isSafeMode", pp.isSafeMode() ? "safe mode ON" : "safe mode OFF"); @@ -2373,15 +2427,15 @@ contract Misctest is Test, PositionUtils { // deposit bare minimum for naked mints ct0.deposit(0, Bob); - ct1.deposit(17_818, Bob); + ct1.deposit(17_828, Bob); // mint succeeds pp.mintOptions( $posIdList, 100_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); (uint256 totalCollateralBalance0, uint256 totalCollateralRequired0) = ph.checkCollateral( pp, @@ -2392,7 +2446,7 @@ contract Misctest is Test, PositionUtils { assertTrue(totalCollateralBalance0 > totalCollateralRequired0, "Is solvent at stale tick!"); - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); (totalCollateralBalance0, totalCollateralRequired0) = ph.checkCollateral( pp, @@ -2440,6 +2494,7 @@ contract Misctest is Test, PositionUtils { vm.startPrank(Swapper); + routerV4.swapTo(address(0), poolKey, Math.getSqrtRatioAtTick(-955)); swapperc.swapTo(uniPool, Math.getSqrtRatioAtTick(-955)); console2.log("isSafeMode", pp.isSafeMode() ? "safe mode ON" : "safe mode OFF"); @@ -2454,15 +2509,15 @@ contract Misctest is Test, PositionUtils { token1.approve(address(ct1), 1_000_000); // deposit bare minimum for covered mints - ct0.deposit(150504, Bob); + ct0.deposit(150905, Bob); ct1.deposit(0, Bob); pp.mintOptions( $posIdList, 100_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); (uint128 balance, uint64 utilization0, uint64 utilization1) = ph.optionPositionInfo( @@ -2475,7 +2530,7 @@ contract Misctest is Test, PositionUtils { assertEq(utilization0, 10_000); assertEq(utilization1, 10_000); - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); (totalCollateralBalance0, totalCollateralRequired0) = ph.checkCollateral( pp, @@ -2507,12 +2562,12 @@ contract Misctest is Test, PositionUtils { pp.pokeMedian(); swapperc.burn(uniPool, -10, 10, 10 ** 18); } - swapperc.mint(uniPool, -10000, 10000, 10 ** 18); + routerV4.modifyLiquidity(address(0), poolKey, -10000, 10000, 10 ** 18); int24 tickSpacing = uniPool.tickSpacing(); // mint ITM position $posIdList.push( - TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))).addLeg( + TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( 0, 1, 1, @@ -2526,6 +2581,7 @@ contract Misctest is Test, PositionUtils { (, int24 staleTick, , , , , ) = uniPool.slot0(); + routerV4.swapTo(address(0), poolKey, Math.getSqrtRatioAtTick(952)); swapperc.swapTo(uniPool, Math.getSqrtRatioAtTick(952)); console2.log("isSafeMode", pp.isSafeMode() ? "safe mode ON" : "safe mode OFF"); assertTrue(pp.isSafeMode() == false); @@ -2542,15 +2598,15 @@ contract Misctest is Test, PositionUtils { // deposit bare minimum for naked mints ct0.deposit(0, Bob); - ct1.deposit(17_820, Bob); + ct1.deposit(17_830, Bob); // mint succeeds pp.mintOptions( $posIdList, 100_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); (uint256 totalCollateralBalance0, uint256 totalCollateralRequired0) = ph.checkCollateral( pp, @@ -2561,7 +2617,7 @@ contract Misctest is Test, PositionUtils { assertTrue(totalCollateralBalance0 > totalCollateralRequired0, "Is solvent at stale tick!"); - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); (totalCollateralBalance0, totalCollateralRequired0) = ph.checkCollateral( pp, @@ -2607,6 +2663,7 @@ contract Misctest is Test, PositionUtils { vm.startPrank(Swapper); + routerV4.swapTo(address(0), poolKey, Math.getSqrtRatioAtTick(953)); swapperc.swapTo(uniPool, Math.getSqrtRatioAtTick(953)); console2.log("isSafeMode", pp.isSafeMode() ? "safe mode ON" : "safe mode OFF"); @@ -2622,14 +2679,14 @@ contract Misctest is Test, PositionUtils { // deposit bare minimum for covered mints ct0.deposit(0, Bob); - ct1.deposit(150466, Bob); + ct1.deposit(150766, Bob); pp.mintOptions( $posIdList, 100_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); (uint128 balance, uint64 utilization0, uint64 utilization1) = ph.optionPositionInfo( @@ -2642,7 +2699,7 @@ contract Misctest is Test, PositionUtils { assertEq(utilization0, 10_000); assertEq(utilization1, 10_000); - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); (totalCollateralBalance0, totalCollateralRequired0) = ph.checkCollateral( pp, @@ -2668,7 +2725,7 @@ contract Misctest is Test, PositionUtils { // mint OTM position $posIdList.push( - TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))).addLeg( + TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( 0, 1, 1, @@ -2686,8 +2743,8 @@ contract Misctest is Test, PositionUtils { $posIdList, 1_000_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); editCollateral(ct0, Bob, ct0.convertToShares(1_000_000)); @@ -2707,16 +2764,20 @@ contract Misctest is Test, PositionUtils { assertTrue(pp.isSafeMode() == false, "not in safe mode"); + routerV4.swapTo(address(0), poolKey, Math.getSqrtRatioAtTick(-953)); swapperc.swapTo(uniPool, Math.getSqrtRatioAtTick(-953)); - (currentTick, slowOracleTick, , , ) = pp.getOracleTicks(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); + (, slowOracleTick, , , ) = pp.getOracleTicks(); assertTrue(Math.abs(currentTick - slowOracleTick) <= 953, "small price deviation"); assertTrue(pp.isSafeMode() == false, "not in safe mode"); + routerV4.swapTo(address(0), poolKey, Math.getSqrtRatioAtTick(-954)); swapperc.swapTo(uniPool, Math.getSqrtRatioAtTick(-954)); - (currentTick, slowOracleTick, , , ) = pp.getOracleTicks(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); + (, slowOracleTick, , , ) = pp.getOracleTicks(); assertTrue(Math.abs(currentTick - slowOracleTick) > 953, "small price deviation"); assertTrue(pp.isSafeMode(), "in safe mode"); } @@ -2731,16 +2792,20 @@ contract Misctest is Test, PositionUtils { assertTrue(pp.isSafeMode() == false, "not in safe mode"); + routerV4.swapTo(address(0), poolKey, Math.getSqrtRatioAtTick(953)); swapperc.swapTo(uniPool, Math.getSqrtRatioAtTick(953)); - (currentTick, slowOracleTick, , , ) = pp.getOracleTicks(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); + (, slowOracleTick, , , ) = pp.getOracleTicks(); assertTrue(Math.abs(currentTick - slowOracleTick) <= 953, "small price deviation"); assertTrue(pp.isSafeMode() == false, "not in safe mode"); + routerV4.swapTo(address(0), poolKey, Math.getSqrtRatioAtTick(954)); swapperc.swapTo(uniPool, Math.getSqrtRatioAtTick(954)); - (currentTick, slowOracleTick, , , ) = pp.getOracleTicks(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); + (, slowOracleTick, , , ) = pp.getOracleTicks(); assertTrue(Math.abs(currentTick - slowOracleTick) > 953, "small price deviation"); assertTrue(pp.isSafeMode(), "in safe mode"); } @@ -2761,13 +2826,15 @@ contract Misctest is Test, PositionUtils { pp.pokeMedian(); swapperc.burn(uniPool, -10, 10, 10 ** 18); } - swapperc.mint(uniPool, -10, 10, 10 ** 18); + routerV4.modifyLiquidity(address(0), poolKey, -10, 10, 10 ** 18); assertTrue(pp.isSafeMode() == false, "not in safe mode"); + routerV4.swapTo(address(0), poolKey, Math.getSqrtRatioAtTick(-955)); swapperc.swapTo(uniPool, Math.getSqrtRatioAtTick(-955)); - (currentTick, slowOracleTick, , , ) = pp.getOracleTicks(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); + (, slowOracleTick, , , ) = pp.getOracleTicks(); assertTrue(Math.abs(currentTick - slowOracleTick) > 953, "small price deviation"); assertTrue(pp.isSafeMode(), "in safe mode"); @@ -2783,11 +2850,10 @@ contract Misctest is Test, PositionUtils { assertTrue(pp.isSafeMode() == true, "slow oracle tick did not catch up"); - swapperc.mint(uniPool, -10000, 10000, 10 ** 18); + routerV4.modifyLiquidity(address(0), poolKey, -10000, 10000, 10 ** 18); vm.warp(block.timestamp + 120); vm.roll(block.number + 1); pp.pokeMedian(); - swapperc.burn(uniPool, -10000, 10000, 10 ** 18); assertTrue(pp.isSafeMode() == false, "slow oracle tick caught up"); } @@ -2808,13 +2874,15 @@ contract Misctest is Test, PositionUtils { pp.pokeMedian(); swapperc.burn(uniPool, -10, 10, 10 ** 18); } - swapperc.mint(uniPool, -10, 10, 10 ** 18); + routerV4.modifyLiquidity(address(0), poolKey, -10, 10, 10 ** 18); assertTrue(pp.isSafeMode() == false, "not in safe mode"); + routerV4.swapTo(address(0), poolKey, Math.getSqrtRatioAtTick(-954)); swapperc.swapTo(uniPool, Math.getSqrtRatioAtTick(-954)); - (currentTick, slowOracleTick, , , ) = pp.getOracleTicks(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); + (, slowOracleTick, , , ) = pp.getOracleTicks(); assertTrue(Math.abs(currentTick - slowOracleTick) <= 953, "small price deviation"); assertTrue(!pp.isSafeMode(), "not in safe mode"); @@ -2822,7 +2890,7 @@ contract Misctest is Test, PositionUtils { int24 tickSpacing = uniPool.tickSpacing(); // mint OTM position $posIdList.push( - TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))).addLeg( + TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( 0, 1, 1, @@ -2852,15 +2920,17 @@ contract Misctest is Test, PositionUtils { $posIdList, 100_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); vm.revertTo(snap); vm.startPrank(Swapper); + routerV4.swapTo(address(0), poolKey, Math.getSqrtRatioAtTick(-955)); swapperc.swapTo(uniPool, Math.getSqrtRatioAtTick(-955)); - (currentTick, slowOracleTick, , , ) = pp.getOracleTicks(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); + (, slowOracleTick, , , ) = pp.getOracleTicks(); console2.log("currentTick", currentTick); console2.log("slowOracleTick", slowOracleTick); @@ -2880,8 +2950,8 @@ contract Misctest is Test, PositionUtils { $posIdList, 100_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); (uint128 balance, uint64 utilization0, uint64 utilization1) = ph.optionPositionInfo( @@ -2911,13 +2981,15 @@ contract Misctest is Test, PositionUtils { pp.pokeMedian(); swapperc.burn(uniPool, -10, 10, 10 ** 18); } - swapperc.mint(uniPool, -10, 10, 10 ** 18); + routerV4.modifyLiquidity(address(0), poolKey, -10, 10, 10 ** 18); assertTrue(pp.isSafeMode() == false, "not in safe mode"); + routerV4.swapTo(address(0), poolKey, Math.getSqrtRatioAtTick(-955)); swapperc.swapTo(uniPool, Math.getSqrtRatioAtTick(-955)); - (currentTick, slowOracleTick, , , ) = pp.getOracleTicks(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); + (, slowOracleTick, , , ) = pp.getOracleTicks(); assertTrue(Math.abs(currentTick - slowOracleTick) > 953, "small price deviation"); assertTrue(pp.isSafeMode(), "in safe mode"); @@ -2925,7 +2997,7 @@ contract Misctest is Test, PositionUtils { int24 tickSpacing = uniPool.tickSpacing(); // mint ITM position $posIdList.push( - TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))).addLeg( + TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( 0, 1, 1, @@ -2954,8 +3026,8 @@ contract Misctest is Test, PositionUtils { $posIdList, 100_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); ct0.withdraw(ct0.maxWithdraw(Bob), Bob, Bob); @@ -2963,15 +3035,15 @@ contract Misctest is Test, PositionUtils { // deposit only token1 ct0.deposit(0, Bob); - ct1.deposit(181_183, Bob); // + ct1.deposit(181_583, Bob); // // can mint covered positions pp.mintOptions( $posIdList, 100_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); (uint128 balance, uint64 utilization0, uint64 utilization1) = ph.optionPositionInfo( @@ -3001,14 +3073,14 @@ contract Misctest is Test, PositionUtils { pp.pokeMedian(); swapperc.burn(uniPool, -10, 10, 10 ** 18); } - swapperc.mint(uniPool, -10, 10, 10 ** 18); + routerV4.modifyLiquidity(address(0), poolKey, -10, 10, 10 ** 18); assertTrue(pp.isSafeMode() == false, "not in safe mode"); int24 tickSpacing = uniPool.tickSpacing(); // mint OTM position $posIdList.push( - TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))).addLeg( + TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( 0, 1, 1, @@ -3034,14 +3106,16 @@ contract Misctest is Test, PositionUtils { $posIdList, 100_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); vm.startPrank(Swapper); + routerV4.swapTo(address(0), poolKey, Math.getSqrtRatioAtTick(-955)); swapperc.swapTo(uniPool, Math.getSqrtRatioAtTick(-955)); - (currentTick, slowOracleTick, , , ) = pp.getOracleTicks(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); + (, slowOracleTick, , , ) = pp.getOracleTicks(); assertTrue(Math.abs(currentTick - slowOracleTick) > 953, "small price deviation"); assertTrue(pp.isSafeMode(), "in safe mode"); @@ -3053,8 +3127,8 @@ contract Misctest is Test, PositionUtils { pp.burnOptions( $posIdList, new TokenId[](0), - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); uint256 before0 = ct0.convertToAssets(ct0.balanceOf(Bob)); @@ -3066,8 +3140,8 @@ contract Misctest is Test, PositionUtils { pp.burnOptions( $posIdList, new TokenId[](0), - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); uint256 after0 = ct0.convertToAssets(ct0.balanceOf(Bob)); @@ -3092,13 +3166,13 @@ contract Misctest is Test, PositionUtils { pp.pokeMedian(); swapperc.burn(uniPool, -10, 10, 10 ** 18); } - swapperc.mint(uniPool, -10, 10, 10 ** 18); + routerV4.modifyLiquidity(address(0), poolKey, -10, 10, 10 ** 18); (, , slowOracleTick, , medianData) = pp.getOracleTicks(); // mint OTM position $posIdList.push( - TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))).addLeg( + TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( 0, 1, 1, @@ -3110,25 +3184,30 @@ contract Misctest is Test, PositionUtils { ) ); + routerV4.swapTo(address(0), poolKey, Math.getSqrtRatioAtTick(-955)); swapperc.swapTo(uniPool, Math.getSqrtRatioAtTick(-955)); - vm.warp(block.timestamp + 59); vm.roll(block.number + 1); + uint160 sqrtPriceV4 = V4StateReader.getSqrtPriceX96(manager, poolKey.toId()); + vm.startPrank(Swapper); + swapperc.swapTo(uniPool, Math.getSqrtRatioAtTick(-957)); + swapperc.swapTo(uniPool, sqrtPriceV4); vm.startPrank(Alice); pp.mintOptions( $posIdList, 500_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); + pp.burnOptions( $posIdList[0], new TokenId[](0), - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); (, , int24 slowOracleTickStale, , uint256 medianDataStale) = pp.getOracleTicks(); @@ -3143,8 +3222,8 @@ contract Misctest is Test, PositionUtils { $posIdList, 500_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); (, , slowOracleTickStale, , medianDataStale) = pp.getOracleTicks(); @@ -3176,7 +3255,7 @@ contract Misctest is Test, PositionUtils { (, , slowOracleTickStale, , medianDataStale) = pp.getOracleTicks(); - assertTrue(slowOracleTick != slowOracleTickStale, "no slow oracle update"); + assertTrue(slowOracleTick != slowOracleTickStale, "Slow oracle update"); assertTrue(medianData != medianDataStale, "oracle median data update"); } @@ -3196,13 +3275,13 @@ contract Misctest is Test, PositionUtils { pp.pokeMedian(); swapperc.burn(uniPool, -10, 10, 10 ** 18); } - swapperc.mint(uniPool, -10, 10, 10 ** 18); + routerV4.modifyLiquidity(address(0), poolKey, -10, 10, 10 ** 18); (, , slowOracleTick, , medianData) = pp.getOracleTicks(); // mint OTM position $posIdList.push( - TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))).addLeg( + TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( 0, 1, 1, @@ -3214,19 +3293,23 @@ contract Misctest is Test, PositionUtils { ) ); + routerV4.swapTo(address(0), poolKey, Math.getSqrtRatioAtTick(-955)); swapperc.swapTo(uniPool, Math.getSqrtRatioAtTick(-955)); - vm.warp(block.timestamp + 59); vm.roll(block.number + 1); + uint160 sqrtPriceV4 = V4StateReader.getSqrtPriceX96(manager, poolKey.toId()); + vm.startPrank(Swapper); + swapperc.swapTo(uniPool, Math.getSqrtRatioAtTick(-957)); + swapperc.swapTo(uniPool, sqrtPriceV4); vm.startPrank(Alice); pp.mintOptions( $posIdList, 500_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); (, , int24 slowOracleTickStale, , uint256 medianDataStale) = pp.getOracleTicks(); @@ -3240,8 +3323,8 @@ contract Misctest is Test, PositionUtils { pp.burnOptions( $posIdList[0], new TokenId[](0), - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); (, , slowOracleTickStale, , medianDataStale) = pp.getOracleTicks(); @@ -3286,7 +3369,7 @@ contract Misctest is Test, PositionUtils { token1.approve(address(swapperc), type(uint128).max); // mint OTM position $posIdList.push( - TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))).addLeg( + TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( 0, 1, 0, @@ -3308,7 +3391,7 @@ contract Misctest is Test, PositionUtils { $posIdList ); - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); LeftRightUnsigned tokenData0 = ct0.getAccountMarginDetails( Bob, @@ -3340,42 +3423,48 @@ contract Misctest is Test, PositionUtils { function test_success_PremiumRollover() public { vm.startPrank(Swapper); // JIT a bunch of liquidity so swaps at mint can happen normally - swapperc.mint(uniPool, -10, 10, 10 ** 18); + routerV4.modifyLiquidity(address(0), poolKey, -10, 10, 10 ** 18); // L = 1 - uniPool.liquidity(); + StateLibrary.getLiquidity(manager, poolKey.toId()); - TokenId tokenId = TokenId - .wrap(0) - .addPoolId(PanopticMath.getPoolId(address(uniPool))) - .addLeg(0, 1, 1, 0, 0, 0, 0, 4094); + TokenId tokenId = TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 4094 + ); TokenId[] memory posIdList = new TokenId[](1); posIdList[0] = tokenId; vm.startPrank(Bob); // mint 1 liquidity unit of wideish centered position - pp.mintOptions(posIdList, 3, 0, Constants.MAX_V3POOL_TICK, Constants.MIN_V3POOL_TICK); + pp.mintOptions(posIdList, 3, 0, Constants.MAX_V4POOL_TICK, Constants.MIN_V4POOL_TICK); vm.startPrank(Swapper); - swapperc.burn(uniPool, -10, 10, 10 ** 18); + routerV4.modifyLiquidity(address(0), poolKey, -10, 10, -(10 ** 18)); // L = 2 - uniPool.liquidity(); + StateLibrary.getLiquidity(manager, poolKey.toId()); // accumulate the maximum fees per liq SFPM supports - accruePoolFeesInRange(address(uniPool), 1, 2 ** 64 - 1, 0); + accruePoolFeesInRange(manager, poolKey, 1, 2 ** 64 - 1, 0); vm.startPrank(Swapper); - swapperc.mint(uniPool, -10, 10, 10 ** 18); + routerV4.modifyLiquidity(address(0), poolKey, -10, 10, 10 ** 18); vm.startPrank(Bob); // works fine pp.burnOptions( tokenId, new TokenId[](0), - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); uint256 balanceBefore0 = ct0.convertToAssets(ct0.balanceOf(Alice)); @@ -3388,19 +3477,19 @@ contract Misctest is Test, PositionUtils { posIdList, 1_000_000_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); vm.startPrank(Swapper); - swapperc.burn(uniPool, -10, 10, 10 ** 18); + routerV4.modifyLiquidity(address(0), poolKey, -10, 10, -(10 ** 18)); // overflow back to ~1_000_000_000_000 (fees per liq) - accruePoolFeesInRange(address(uniPool), 412639631, 1_000_000_000_000, 1_000_000_000_000); + accruePoolFeesInRange(manager, poolKey, 412639631, 1_000_000_000_000, 1_000_000_000_000); // this should behave like the actual accumulator does and rollover, not revert on overflow (uint256 premium0, uint256 premium1) = sfpm.getAccountPremium( - address(uniPool), + poolKey.toId(), address(pp), 0, -20470, @@ -3412,7 +3501,7 @@ contract Misctest is Test, PositionUtils { assertEq(premium1, 44704247211996718928643); vm.startPrank(Swapper); - swapperc.mint(uniPool, -10, 10, 10 ** 18); + routerV4.modifyLiquidity(address(0), poolKey, -10, 10, 10 ** 18); vm.startPrank(Alice); // tough luck... PLPs just stole ~2**64 tokens per liquidity Alice had because of an overflow @@ -3423,8 +3512,8 @@ contract Misctest is Test, PositionUtils { pp.burnOptions( tokenId, new TokenId[](0), - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); // make sure Alice earns no fees on token 0 (her delta is slightly negative due to commission fees/precision etc) @@ -3432,7 +3521,7 @@ contract Misctest is Test, PositionUtils { // she could have still earned some fees, but now the accumulation is frozen forever. assertEq( int256(ct0.convertToAssets(ct0.balanceOf(Alice))) - int256(balanceBefore0), - -1244790 + -1558763 ); // but she earns all of fees on token 1 since the premium accumulator did not overflow (!) @@ -3450,14 +3539,14 @@ contract Misctest is Test, PositionUtils { token0.approve(address(swapperc), type(uint128).max); token1.approve(address(swapperc), type(uint128).max); - swapperc.mint(uniPool, -10, 10, 10 ** 18); + routerV4.modifyLiquidity(address(0), poolKey, -10, 10, 10 ** 18); vm.startPrank(Seller); $posIdList.push( TokenId .wrap(0) - .addPoolId(PanopticMath.getPoolId(address(uniPool))) + .addPoolId(sfpm.getPoolId(poolKey.toId())) .addLeg( 0, 1, @@ -3484,14 +3573,14 @@ contract Misctest is Test, PositionUtils { $posIdList, 2_000_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); // long put = 1.5, short put 1.25, short call 0.75, long call 0.5 $posIdList[0] = TokenId .wrap(0) - .addPoolId(PanopticMath.getPoolId(address(uniPool))) + .addPoolId(sfpm.getPoolId(poolKey.toId())) .addLeg(0, 1, 1, 1, 1, 0, 4055, 1) .addLeg(1, 1, 1, 0, 1, 1, 2235, 1) .addLeg(2, 1, 1, 0, 0, 2, -2875, 1) @@ -3506,8 +3595,8 @@ contract Misctest is Test, PositionUtils { $posIdList, 1_000_000, type(uint64).max, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); // 0.25, 0.6, 0.9, 1.1, 1.4, 1.6 @@ -3516,14 +3605,15 @@ contract Misctest is Test, PositionUtils { for (uint256 i = 0; i < ticks.length; ++i) { uint256 snap = vm.snapshot(); vm.startPrank(Swapper); + routerV4.swapTo(address(0), poolKey, Math.getSqrtRatioAtTick(ticks[i])); swapperc.swapTo(uniPool, Math.getSqrtRatioAtTick(ticks[i])); vm.startPrank(Alice); pp.burnOptions( $posIdList[0], new TokenId[](0), - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); console2.log( @@ -3554,14 +3644,14 @@ contract Misctest is Test, PositionUtils { pp.pokeMedian(); swapperc.burn(uniPool, -10, 10, 10 ** 18); } - swapperc.mint(uniPool, -10, 10, 10 ** 18); + routerV4.modifyLiquidity(address(0), poolKey, -10, 10, 10 ** 18); vm.startPrank(Seller); $posIdList.push( TokenId .wrap(0) - .addPoolId(PanopticMath.getPoolId(address(uniPool))) + .addPoolId(sfpm.getPoolId(poolKey.toId())) .addLeg( 0, 1, @@ -3588,14 +3678,14 @@ contract Misctest is Test, PositionUtils { $posIdList, 2_000_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); // long call = 1.25, short call = 1.5, short call = 1.75, long call = 2 $posIdList[0] = TokenId .wrap(0) - .addPoolId(PanopticMath.getPoolId(address(uniPool))) + .addPoolId(sfpm.getPoolId(poolKey.toId())) .addLeg(0, 1, 1, 1, 0, 0, 2235, 1) .addLeg(1, 1, 1, 0, 0, 1, 4055, 1) .addLeg(2, 1, 1, 0, 0, 2, 5595, 1) @@ -3610,8 +3700,8 @@ contract Misctest is Test, PositionUtils { $posIdList, 1_000_000, type(uint64).max, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); // 1.3, 1.6, 1.8, 2.1 @@ -3620,14 +3710,15 @@ contract Misctest is Test, PositionUtils { for (uint256 i = 0; i < ticks.length; ++i) { uint256 snap = vm.snapshot(); vm.startPrank(Swapper); + routerV4.swapTo(address(0), poolKey, Math.getSqrtRatioAtTick(int16(ticks[i]))); swapperc.swapTo(uniPool, Math.getSqrtRatioAtTick(int16(ticks[i]))); vm.startPrank(Alice); pp.burnOptions( $posIdList[0], new TokenId[](0), - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); console2.log( @@ -3650,12 +3741,12 @@ contract Misctest is Test, PositionUtils { token0.approve(address(swapperc), type(uint128).max); token1.approve(address(swapperc), type(uint128).max); - swapperc.mint(uniPool, -10, 10, 10 ** 18); + routerV4.modifyLiquidity(address(0), poolKey, -10, 10, 10 ** 18); vm.startPrank(Seller); $posIdList.push( - TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))).addLeg( + TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( 0, 1, 1, @@ -3671,14 +3762,14 @@ contract Misctest is Test, PositionUtils { $posIdList, 2_000_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); // long put = 0.25, short put = 0.5, short put = 0.75, short call = 0.9 $posIdList[0] = TokenId .wrap(0) - .addPoolId(PanopticMath.getPoolId(address(uniPool))) + .addPoolId(sfpm.getPoolId(poolKey.toId())) .addLeg(0, 1, 1, 1, 1, 0, -13_865, 1) .addLeg(1, 1, 1, 0, 1, 1, -6935, 1) .addLeg(2, 1, 1, 0, 1, 2, -2875, 1) @@ -3693,8 +3784,8 @@ contract Misctest is Test, PositionUtils { $posIdList, 1_000_000, type(uint64).max, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); // 0.2, 0.4, 0.6, 0.8, 1.1 @@ -3703,14 +3794,15 @@ contract Misctest is Test, PositionUtils { for (uint256 i = 0; i < ticks.length; ++i) { uint256 snap = vm.snapshot(); vm.startPrank(Swapper); + routerV4.swapTo(address(0), poolKey, Math.getSqrtRatioAtTick(ticks[i])); swapperc.swapTo(uniPool, Math.getSqrtRatioAtTick(ticks[i])); vm.startPrank(Alice); pp.burnOptions( $posIdList[0], new TokenId[](0), - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); console2.log( @@ -3756,7 +3848,7 @@ contract Misctest is Test, PositionUtils { vm.startPrank(Bob); $posIdList.push( - TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))).addLeg( + TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( 0, 1, 1, @@ -3774,11 +3866,12 @@ contract Misctest is Test, PositionUtils { $posIdList, 1_003_003, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); vm.startPrank(Swapper); + routerV4.swapTo(address(0), poolKey, Math.getSqrtRatioAtTick(-800_000)); swapperc.swapTo(uniPool, Math.getSqrtRatioAtTick(-800_000)); for (uint256 j = 0; j < 100; ++j) { @@ -3794,13 +3887,14 @@ contract Misctest is Test, PositionUtils { assertLe(ct1.totalSupply() / totalSupplyBefore, 10_000, "protocol loss failed to cap"); } + /// forge-config: default.fuzz.runs = 10 function test_success_liquidation_fuzzedSwapITM(uint256[4] memory prices) public { vm.startPrank(Swapper); // JIT a bunch of liquidity so swaps at mint can happen normally - swapperc.mint(uniPool, -887270, 887270, 10 ** 24); + routerV4.modifyLiquidity(address(0), poolKey, -887270, 887270, 10 ** 24); // L = 1 - uniPool.liquidity(); + StateLibrary.getLiquidity(manager, poolKey.toId()); uint256 snapshot = vm.snapshot(); @@ -3808,15 +3902,21 @@ contract Misctest is Test, PositionUtils { for (uint256 i; i < 4; ++i) { uint256 asset = i % 2; uint256 tokenType = i / 2; - TokenId tokenId = TokenId - .wrap(0) - .addPoolId(PanopticMath.getPoolId(address(uniPool))) - .addLeg(0, 1, asset, 0, tokenType, 0, 0, 2); + TokenId tokenId = TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( + 0, + 1, + asset, + 0, + tokenType, + 0, + 0, + 2 + ); TokenId[] memory posIdList = new TokenId[](1); posIdList[0] = tokenId; - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); vm.startPrank(Bob); ct0.withdraw(ct0.maxWithdraw(Bob), Bob, Bob); @@ -3835,11 +3935,11 @@ contract Misctest is Test, PositionUtils { posIdList, 3000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); // get base (OTM) collateral requirement for the position we just minted // uint256 basalCR; @@ -3881,6 +3981,10 @@ contract Misctest is Test, PositionUtils { vm.startPrank(Swapper); + vm.assume( + sqrtPriceTargetX96 > TickMath.getSqrtRatioAtTick(-750_000) && + sqrtPriceTargetX96 < TickMath.getSqrtRatioAtTick(750_000) + ); // swap to somewhere between the liquidation price and maximum/minimum prices // limiting "max/min prices" to reasonable levels for now because protocol breaks at tail ends of AMM curve (can't handle >2**128 tokens) swapperc.swapTo( @@ -3888,13 +3992,24 @@ contract Misctest is Test, PositionUtils { uint160( bound( prices[i], - tokenType == 0 ? sqrtPriceTargetX96 : Constants.MIN_V3POOL_SQRT_RATIO + 2, - tokenType == 0 ? Constants.MAX_V3POOL_SQRT_RATIO - 2 : sqrtPriceTargetX96 + tokenType == 0 ? sqrtPriceTargetX96 : TickMath.getSqrtRatioAtTick(-750_000), + tokenType == 0 ? TickMath.getSqrtRatioAtTick(750_000) : sqrtPriceTargetX96 + ) + ) + ); + routerV4.swapTo( + address(0), + poolKey, + uint160( + bound( + prices[i], + tokenType == 0 ? sqrtPriceTargetX96 : TickMath.getSqrtRatioAtTick(-750_000), + tokenType == 0 ? TickMath.getSqrtRatioAtTick(750_000) : sqrtPriceTargetX96 ) ) ); - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); (uint256 totalCollateralBalance0, uint256 totalCollateralRequired0) = ph .checkCollateral(pp, Bob, currentTick, posIdList); @@ -3929,23 +4044,31 @@ contract Misctest is Test, PositionUtils { function test_Fail_DivergentSolvencyCheck_mint() public { vm.startPrank(Swapper); // JIT a bunch of liquidity so swaps at mint can happen normally - swapperc.mint(uniPool, -1000, 1000, 10 ** 18); + routerV4.modifyLiquidity(address(0), poolKey, -1000, 1000, 10 ** 18); // L = 1 - uniPool.liquidity(); + StateLibrary.getLiquidity(manager, poolKey.toId()); uint256 asset = 0; uint256 tokenType = 0; - TokenId tokenId = TokenId - .wrap(0) - .addPoolId(PanopticMath.getPoolId(address(uniPool))) - .addLeg(0, 1, asset, 0, tokenType, 0, 0, 2); + TokenId tokenId = TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( + 0, + 1, + asset, + 0, + tokenType, + 0, + 0, + 2 + ); TokenId[] memory posIdList = new TokenId[](1); posIdList[0] = tokenId; - (currentTick, fastOracleTick, slowOracleTick, lastObservedTick, ) = pp.getOracleTicks(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); + (, fastOracleTick, slowOracleTick, lastObservedTick, ) = pp.getOracleTicks(); + routerV4.swapTo(address(0), poolKey, Math.getSqrtRatioAtTick(int24(currentTick) + 950)); swapperc.swapTo(uniPool, Math.getSqrtRatioAtTick(int24(currentTick) + 950)); vm.warp(block.timestamp + 13); @@ -3958,7 +4081,8 @@ contract Misctest is Test, PositionUtils { swapperc.mint(uniPool, -887200, 887200, 10 ** 18); swapperc.burn(uniPool, -887200, 887200, 10 ** 18); - (currentTick, fastOracleTick, slowOracleTick, lastObservedTick, ) = pp.getOracleTicks(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); + (, fastOracleTick, slowOracleTick, lastObservedTick, ) = pp.getOracleTicks(); assertTrue(!pp.isSafeMode(), "not in safe mode"); @@ -3985,25 +4109,31 @@ contract Misctest is Test, PositionUtils { vm.startPrank(Bob); vm.expectRevert(Errors.AccountInsolvent.selector); - pp.mintOptions(posIdList, 3000, 0, Constants.MAX_V3POOL_TICK, Constants.MIN_V3POOL_TICK); + pp.mintOptions(posIdList, 3000, 0, Constants.MAX_V4POOL_TICK, Constants.MIN_V4POOL_TICK); } function test_Fail_DivergentSolvencyCheck_burn() public { vm.startPrank(Swapper); // JIT a bunch of liquidity so swaps at mint can happen normally - swapperc.mint(uniPool, -1000, 1000, 10 ** 18); + routerV4.modifyLiquidity(address(0), poolKey, -1000, 1000, 10 ** 18); // L = 1 - uniPool.liquidity(); + StateLibrary.getLiquidity(manager, poolKey.toId()); /// @dev single leg, wide atm call, liquidation through price move making options ITM, no-cross collateral uint256 asset = 0; uint256 tokenType = 0; - TokenId tokenId = TokenId - .wrap(0) - .addPoolId(PanopticMath.getPoolId(address(uniPool))) - .addLeg(0, 1, asset, 0, tokenType, 0, 0, 2); + TokenId tokenId = TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( + 0, + 1, + asset, + 0, + tokenType, + 0, + 0, + 2 + ); TokenId[] memory posIdList = new TokenId[](1); posIdList[0] = tokenId; @@ -4022,25 +4152,34 @@ contract Misctest is Test, PositionUtils { vm.startPrank(Bob); - pp.mintOptions(posIdList, 3000, 0, Constants.MAX_V3POOL_TICK, Constants.MIN_V3POOL_TICK); + pp.mintOptions(posIdList, 3000, 0, Constants.MAX_V4POOL_TICK, Constants.MIN_V4POOL_TICK); TokenId[] memory posIdList2 = new TokenId[](2); posIdList2[0] = tokenId; - TokenId tokenId2 = TokenId - .wrap(0) - .addPoolId(PanopticMath.getPoolId(address(uniPool))) - .addLeg(0, 2, asset, 0, tokenType, 0, 0, 2); + TokenId tokenId2 = TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( + 0, + 2, + asset, + 0, + tokenType, + 0, + 0, + 2 + ); posIdList2[1] = tokenId2; // mint second option - pp.mintOptions(posIdList2, 10, 0, Constants.MAX_V3POOL_TICK, Constants.MIN_V3POOL_TICK); + pp.mintOptions(posIdList2, 10, 0, Constants.MAX_V4POOL_TICK, Constants.MIN_V4POOL_TICK); + + currentTick = V4StateReader.getTick(manager, poolKey.toId()); - (currentTick, fastOracleTick, slowOracleTick, lastObservedTick, ) = pp.getOracleTicks(); + (, fastOracleTick, slowOracleTick, lastObservedTick, ) = pp.getOracleTicks(); vm.startPrank(Swapper); + routerV4.swapTo(address(0), poolKey, Math.getSqrtRatioAtTick(int24(currentTick) + 950)); swapperc.swapTo(uniPool, Math.getSqrtRatioAtTick(int24(currentTick) + 950)); vm.warp(block.timestamp + 13); @@ -4053,7 +4192,9 @@ contract Misctest is Test, PositionUtils { swapperc.mint(uniPool, -887200, 887200, 10 ** 18); swapperc.burn(uniPool, -887200, 887200, 10 ** 18); - (currentTick, fastOracleTick, slowOracleTick, lastObservedTick, ) = pp.getOracleTicks(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); + + (, fastOracleTick, slowOracleTick, lastObservedTick, ) = pp.getOracleTicks(); assertTrue(!pp.isSafeMode(), "not in safe mode"); @@ -4072,32 +4213,38 @@ contract Misctest is Test, PositionUtils { pp.burnOptions( posIdList2[1], posIdList, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } function test_Fail_DivergentSolvencyCheck_liquidation() public { vm.startPrank(Swapper); // JIT a bunch of liquidity so swaps at mint can happen normally - swapperc.mint(uniPool, -1000, 1000, 10 ** 18); + routerV4.modifyLiquidity(address(0), poolKey, -1000, 1000, 10 ** 18); // L = 1 - uniPool.liquidity(); + StateLibrary.getLiquidity(manager, poolKey.toId()); /// @dev single leg, wide atm call, liquidation through price move making options ITM, no-cross collateral uint256 asset = 0; uint256 tokenType = 0; - TokenId tokenId = TokenId - .wrap(0) - .addPoolId(PanopticMath.getPoolId(address(uniPool))) - .addLeg(0, 1, asset, 0, tokenType, 0, 0, 100); + TokenId tokenId = TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( + 0, + 1, + asset, + 0, + tokenType, + 0, + 0, + 100 + ); TokenId[] memory posIdList = new TokenId[](1); posIdList[0] = tokenId; - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); vm.startPrank(Bob); ct0.withdraw(ct0.maxWithdraw(Bob), Bob, Bob); @@ -4111,9 +4258,9 @@ contract Misctest is Test, PositionUtils { ct1.deposit(1000, Bob); } - pp.mintOptions(posIdList, 3000, 0, Constants.MAX_V3POOL_TICK, Constants.MIN_V3POOL_TICK); + pp.mintOptions(posIdList, 3000, 0, Constants.MAX_V4POOL_TICK, Constants.MIN_V4POOL_TICK); - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); (uint256 totalCollateralBalance0, uint256 totalCollateralRequired0) = ph.checkCollateral( pp, @@ -4131,7 +4278,12 @@ contract Misctest is Test, PositionUtils { uniPool, tokenType == 0 ? 87150978765690778389772763136 : 72025602285694849958832766976 ); - (, currentTick, , , , , ) = uniPool.slot0(); + routerV4.swapTo( + address(0), + poolKey, + tokenType == 0 ? 87150978765690778389772763136 : 72025602285694849958832766976 + ); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); (totalCollateralBalance0, totalCollateralRequired0) = ph.checkCollateral( pp, @@ -4150,7 +4302,7 @@ contract Misctest is Test, PositionUtils { swapperc.burn(uniPool, -887200, 887200, 10 ** 18); } - twapTick = PanopticMath.twapFilter(uniPool, 600); + twapTick = PanopticMath.twapFilter(IV3CompatibleOracle(address(uniPool)), 600); { (totalCollateralBalance0, totalCollateralRequired0) = ph.checkCollateral( pp, @@ -4162,9 +4314,12 @@ contract Misctest is Test, PositionUtils { assertTrue(totalCollateralBalance0 < totalCollateralRequired0, "Is liquidatable twap!"); } + routerV4.swapTo(address(0), poolKey, Math.getSqrtRatioAtTick(int24(twapTick) - 500)); swapperc.swapTo(uniPool, Math.getSqrtRatioAtTick(int24(twapTick) - 500)); - (currentTick, fastOracleTick, , lastObservedTick, ) = pp.getOracleTicks(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); + + (, fastOracleTick, , lastObservedTick, ) = pp.getOracleTicks(); { (totalCollateralBalance0, totalCollateralRequired0) = ph.checkCollateral( @@ -4207,24 +4362,30 @@ contract Misctest is Test, PositionUtils { function test_success_liquidation_currentTick_bonusOptimization_scenarios() public { vm.startPrank(Swapper); // JIT a bunch of liquidity so swaps at mint can happen normally - swapperc.mint(uniPool, -1000, 1000, 10 ** 18); + routerV4.modifyLiquidity(address(0), poolKey, -1000, 1000, 10 ** 18); // L = 1 - uniPool.liquidity(); + StateLibrary.getLiquidity(manager, poolKey.toId()); /// @dev single leg, wide atm call, liquidation through price move making options ITM, no-cross collateral uint256 asset = 0; uint256 tokenType = 0; - TokenId tokenId = TokenId - .wrap(0) - .addPoolId(PanopticMath.getPoolId(address(uniPool))) - .addLeg(0, 1, asset, 0, tokenType, 0, 0, 100); + TokenId tokenId = TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( + 0, + 1, + asset, + 0, + tokenType, + 0, + 0, + 100 + ); TokenId[] memory posIdList = new TokenId[](1); posIdList[0] = tokenId; - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); vm.startPrank(Bob); ct0.withdraw(ct0.maxWithdraw(Bob), Bob, Bob); @@ -4238,9 +4399,9 @@ contract Misctest is Test, PositionUtils { ct1.deposit(1000, Bob); } - pp.mintOptions(posIdList, 3000, 0, Constants.MAX_V3POOL_TICK, Constants.MIN_V3POOL_TICK); + pp.mintOptions(posIdList, 3000, 0, Constants.MAX_V4POOL_TICK, Constants.MIN_V4POOL_TICK); - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); (uint256 totalCollateralBalance0, uint256 totalCollateralRequired0) = ph.checkCollateral( pp, @@ -4258,7 +4419,12 @@ contract Misctest is Test, PositionUtils { uniPool, tokenType == 0 ? 87150978765690778389772763136 : 72025602285694849958832766976 ); - (, currentTick, , , , , ) = uniPool.slot0(); + routerV4.swapTo( + address(0), + poolKey, + tokenType == 0 ? 87150978765690778389772763136 : 72025602285694849958832766976 + ); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); (totalCollateralBalance0, totalCollateralRequired0) = ph.checkCollateral( pp, @@ -4277,7 +4443,7 @@ contract Misctest is Test, PositionUtils { swapperc.burn(uniPool, -887200, 887200, 10 ** 18); } - twapTick = PanopticMath.twapFilter(uniPool, 600); + twapTick = PanopticMath.twapFilter(IV3CompatibleOracle(address(uniPool)), 600); { (totalCollateralBalance0, totalCollateralRequired0) = ph.checkCollateral( pp, @@ -4302,6 +4468,7 @@ contract Misctest is Test, PositionUtils { for (int24 t = -350; t <= 510; t += 10) { // swap to 1.21*1.05 or 0.82/1.05, depending on tokenType vm.startPrank(Swapper); + routerV4.swapTo(address(0), poolKey, Math.getSqrtRatioAtTick(int24(twapTick) + t)); swapperc.swapTo(uniPool, Math.getSqrtRatioAtTick(int24(twapTick) + t)); vm.startPrank(Alice); @@ -4329,10 +4496,10 @@ contract Misctest is Test, PositionUtils { function test_success_liquidation_ITM_scenarios() public { vm.startPrank(Swapper); // JIT a bunch of liquidity so swaps at mint can happen normally - swapperc.mint(uniPool, -1000, 1000, 10 ** 18); + routerV4.modifyLiquidity(address(0), poolKey, -1000, 1000, 10 ** 18); // L = 1 - uniPool.liquidity(); + StateLibrary.getLiquidity(manager, poolKey.toId()); uint256 snapshot = vm.snapshot(); @@ -4341,16 +4508,22 @@ contract Misctest is Test, PositionUtils { for (uint256 i; i < 4; ++i) { uint256 asset = i % 2; uint256 tokenType = i / 2; - TokenId tokenId = TokenId - .wrap(0) - .addPoolId(PanopticMath.getPoolId(address(uniPool))) - .addLeg(0, 1, asset, 0, tokenType, 0, 0, 2); + TokenId tokenId = TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( + 0, + 1, + asset, + 0, + tokenType, + 0, + 0, + 2 + ); //.addLeg(legIndex, optionRatio, asset, isLong, tokenType, riskPartner, strike, width); TokenId[] memory posIdList = new TokenId[](1); posIdList[0] = tokenId; - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); vm.startPrank(Bob); ct0.withdraw(ct0.maxWithdraw(Bob), Bob, Bob); @@ -4369,11 +4542,11 @@ contract Misctest is Test, PositionUtils { posIdList, 3000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); (uint256 totalCollateralBalance0, uint256 totalCollateralRequired0) = ph .checkCollateral(pp, Bob, currentTick, posIdList); @@ -4387,7 +4560,13 @@ contract Misctest is Test, PositionUtils { uniPool, tokenType == 0 ? 87150978765690778389772763136 : 72025602285694849958832766976 ); - (, currentTick, , , , , ) = uniPool.slot0(); + routerV4.swapTo( + address(0), + poolKey, + tokenType == 0 ? 87150978765690778389772763136 : 72025602285694849958832766976 + ); + + currentTick = V4StateReader.getTick(manager, poolKey.toId()); (totalCollateralBalance0, totalCollateralRequired0) = ph.checkCollateral( pp, @@ -4406,7 +4585,7 @@ contract Misctest is Test, PositionUtils { swapperc.burn(uniPool, -887200, 887200, 10 ** 18); } - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); vm.startPrank(Alice); console2.log(""); @@ -4420,16 +4599,22 @@ contract Misctest is Test, PositionUtils { for (uint256 i; i < 4; ++i) { uint256 asset = i % 2; uint256 tokenType = i / 2; - TokenId tokenId = TokenId - .wrap(0) - .addPoolId(PanopticMath.getPoolId(address(uniPool))) - .addLeg(0, 1, asset, 0, tokenType, 0, 0, 2); + TokenId tokenId = TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( + 0, + 1, + asset, + 0, + tokenType, + 0, + 0, + 2 + ); //.addLeg(legIndex, optionRatio, asset, isLong, tokenType, riskPartner, strike, width); TokenId[] memory posIdList = new TokenId[](1); posIdList[0] = tokenId; - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); vm.startPrank(Bob); ct0.withdraw(ct0.maxWithdraw(Bob), Bob, Bob); @@ -4452,11 +4637,11 @@ contract Misctest is Test, PositionUtils { posIdList, 3000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); (uint256 totalCollateralBalance0, uint256 totalCollateralRequired0) = ph .checkCollateral(pp, Bob, currentTick, posIdList); @@ -4470,8 +4655,13 @@ contract Misctest is Test, PositionUtils { uniPool, tokenType == 0 ? 87150978765690778389772763136 : 72025602285694849958832766976 ); + routerV4.swapTo( + address(0), + poolKey, + tokenType == 0 ? 87150978765690778389772763136 : 72025602285694849958832766976 + ); - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); (totalCollateralBalance0, totalCollateralRequired0) = ph.checkCollateral( pp, Bob, @@ -4506,7 +4696,7 @@ contract Misctest is Test, PositionUtils { uint256 tokenType = ((i % 4) / 2); TokenId tokenId; { - tokenId = TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))); + tokenId = TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())); tokenId = tokenId.addLeg( 0, 1, @@ -4533,7 +4723,7 @@ contract Misctest is Test, PositionUtils { TokenId[] memory posIdList = new TokenId[](1); posIdList[0] = tokenId; - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); vm.startPrank(Bob); ct0.withdraw(ct0.maxWithdraw(Bob), Bob, Bob); @@ -4549,11 +4739,11 @@ contract Misctest is Test, PositionUtils { posIdList, 3000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); { (uint256 totalCollateralBalance0, uint256 totalCollateralRequired0) = ph @@ -4571,8 +4761,13 @@ contract Misctest is Test, PositionUtils { uniPool, i > 3 ? 110919427519970065594087112704 : 56591544653045956680544681984 ); + routerV4.swapTo( + address(0), + poolKey, + i > 3 ? 110919427519970065594087112704 : 56591544653045956680544681984 + ); - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); { (uint256 totalCollateralBalance0, uint256 totalCollateralRequired0) = ph .checkCollateral(pp, Bob, currentTick, posIdList); @@ -4600,7 +4795,7 @@ contract Misctest is Test, PositionUtils { uint256 tokenType = ((i % 4) / 2); TokenId tokenId; { - tokenId = TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))); + tokenId = TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())); tokenId = tokenId.addLeg( 0, 1, @@ -4627,7 +4822,7 @@ contract Misctest is Test, PositionUtils { TokenId[] memory posIdList = new TokenId[](1); posIdList[0] = tokenId; - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); vm.startPrank(Bob); ct0.withdraw(ct0.maxWithdraw(Bob), Bob, Bob); @@ -4642,11 +4837,11 @@ contract Misctest is Test, PositionUtils { posIdList, 3000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); { (uint256 totalCollateralBalance0, uint256 totalCollateralRequired0) = ph @@ -4664,8 +4859,13 @@ contract Misctest is Test, PositionUtils { uniPool, i > 3 ? 110919427519970065594087112704 : 56591544653045956680544681984 ); + routerV4.swapTo( + address(0), + poolKey, + i > 3 ? 110919427519970065594087112704 : 56591544653045956680544681984 + ); - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); { (uint256 totalCollateralBalance0, uint256 totalCollateralRequired0) = ph .checkCollateral(pp, Bob, currentTick, posIdList); @@ -4693,7 +4893,7 @@ contract Misctest is Test, PositionUtils { uint256 tokenType = ((i % 4) / 2); TokenId tokenId; { - tokenId = TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))); + tokenId = TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())); tokenId = tokenId.addLeg( 0, 1, @@ -4720,7 +4920,7 @@ contract Misctest is Test, PositionUtils { TokenId[] memory posIdList = new TokenId[](1); posIdList[0] = tokenId; - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); vm.startPrank(Bob); ct0.withdraw(ct0.maxWithdraw(Bob), Bob, Bob); @@ -4735,11 +4935,11 @@ contract Misctest is Test, PositionUtils { posIdList, 3000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); { (uint256 totalCollateralBalance0, uint256 totalCollateralRequired0) = ph @@ -4757,8 +4957,13 @@ contract Misctest is Test, PositionUtils { uniPool, i > 3 ? 110919427519970065594087112704 : 56591544653045956680544681984 ); + routerV4.swapTo( + address(0), + poolKey, + i > 3 ? 110919427519970065594087112704 : 56591544653045956680544681984 + ); - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); { (uint256 totalCollateralBalance0, uint256 totalCollateralRequired0) = ph .checkCollateral(pp, Bob, currentTick, posIdList); @@ -4784,10 +4989,10 @@ contract Misctest is Test, PositionUtils { function test_success_liquidation_LowCollateral_scenarios() public { vm.startPrank(Swapper); // JIT a bunch of liquidity so swaps at mint can happen normally - swapperc.mint(uniPool, -1000, 1000, 10 ** 18); + routerV4.modifyLiquidity(address(0), poolKey, -1000, 1000, 10 ** 18); // L = 1 - uniPool.liquidity(); + StateLibrary.getLiquidity(manager, poolKey.toId()); uint256 snapshot = vm.snapshot(); @@ -4796,16 +5001,22 @@ contract Misctest is Test, PositionUtils { for (uint256 i; i < 4; ++i) { uint256 asset = i % 2; uint256 tokenType = i / 2; - TokenId tokenId = TokenId - .wrap(0) - .addPoolId(PanopticMath.getPoolId(address(uniPool))) - .addLeg(0, 1, asset, 0, tokenType, 0, 0, 2); + TokenId tokenId = TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( + 0, + 1, + asset, + 0, + tokenType, + 0, + 0, + 2 + ); //.addLeg(legIndex, optionRatio, asset, isLong, tokenType, riskPartner, strike, width); TokenId[] memory posIdList = new TokenId[](1); posIdList[0] = tokenId; - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); vm.startPrank(Bob); ct0.withdraw(ct0.maxWithdraw(Bob), Bob, Bob); @@ -4824,11 +5035,11 @@ contract Misctest is Test, PositionUtils { posIdList, 3000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); (uint256 totalCollateralBalance0, uint256 totalCollateralRequired0) = ph .checkCollateral(pp, Bob, currentTick, posIdList); @@ -4872,16 +5083,22 @@ contract Misctest is Test, PositionUtils { for (uint256 i; i < 4; ++i) { uint256 asset = i % 2; uint256 tokenType = i / 2; - TokenId tokenId = TokenId - .wrap(0) - .addPoolId(PanopticMath.getPoolId(address(uniPool))) - .addLeg(0, 1, asset, 0, tokenType, 0, 0, 2); + TokenId tokenId = TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())).addLeg( + 0, + 1, + asset, + 0, + tokenType, + 0, + 0, + 2 + ); //.addLeg(legIndex, optionRatio, asset, isLong, tokenType, riskPartner, strike, width); TokenId[] memory posIdList = new TokenId[](1); posIdList[0] = tokenId; - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); vm.startPrank(Bob); ct0.withdraw(ct0.maxWithdraw(Bob), Bob, Bob); @@ -4904,11 +5121,11 @@ contract Misctest is Test, PositionUtils { posIdList, 3000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); (uint256 totalCollateralBalance0, uint256 totalCollateralRequired0) = ph .checkCollateral(pp, Bob, currentTick, posIdList); @@ -4923,7 +5140,7 @@ contract Misctest is Test, PositionUtils { editCollateral(ct0, Bob, ct0.convertToShares(550)); } - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); (totalCollateralBalance0, totalCollateralRequired0) = ph.checkCollateral( pp, Bob, @@ -4959,7 +5176,7 @@ contract Misctest is Test, PositionUtils { uint256 tokenType = (i / 2); TokenId tokenId; { - tokenId = TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))); + tokenId = TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())); tokenId = tokenId.addLeg( 0, 1, @@ -4986,7 +5203,7 @@ contract Misctest is Test, PositionUtils { TokenId[] memory posIdList = new TokenId[](1); posIdList[0] = tokenId; - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); vm.startPrank(Bob); ct0.withdraw(ct0.maxWithdraw(Bob), Bob, Bob); @@ -5002,11 +5219,11 @@ contract Misctest is Test, PositionUtils { posIdList, 3000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); { (uint256 totalCollateralBalance0, uint256 totalCollateralRequired0) = ph @@ -5022,7 +5239,7 @@ contract Misctest is Test, PositionUtils { editCollateral(ct0, Bob, ct0.convertToShares(250)); editCollateral(ct1, Bob, ct1.convertToShares(250)); - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); { (uint256 totalCollateralBalance0, uint256 totalCollateralRequired0) = ph .checkCollateral(pp, Bob, currentTick, posIdList); @@ -5050,7 +5267,7 @@ contract Misctest is Test, PositionUtils { uint256 tokenType = (i / 2); TokenId tokenId; { - tokenId = TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))); + tokenId = TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())); tokenId = tokenId.addLeg( 0, 1, @@ -5077,7 +5294,7 @@ contract Misctest is Test, PositionUtils { TokenId[] memory posIdList = new TokenId[](1); posIdList[0] = tokenId; - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); vm.startPrank(Bob); ct0.withdraw(ct0.maxWithdraw(Bob), Bob, Bob); @@ -5092,11 +5309,11 @@ contract Misctest is Test, PositionUtils { posIdList, 3000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); { (uint256 totalCollateralBalance0, uint256 totalCollateralRequired0) = ph @@ -5111,7 +5328,7 @@ contract Misctest is Test, PositionUtils { editCollateral(ct0, Bob, ct0.convertToShares(250)); - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); { (uint256 totalCollateralBalance0, uint256 totalCollateralRequired0) = ph .checkCollateral(pp, Bob, currentTick, posIdList); @@ -5139,7 +5356,7 @@ contract Misctest is Test, PositionUtils { uint256 tokenType = (i / 2); TokenId tokenId; { - tokenId = TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))); + tokenId = TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())); tokenId = tokenId.addLeg( 0, 1, @@ -5166,7 +5383,7 @@ contract Misctest is Test, PositionUtils { TokenId[] memory posIdList = new TokenId[](1); posIdList[0] = tokenId; - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); vm.startPrank(Bob); ct0.withdraw(ct0.maxWithdraw(Bob), Bob, Bob); @@ -5181,11 +5398,11 @@ contract Misctest is Test, PositionUtils { posIdList, 3000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); { (uint256 totalCollateralBalance0, uint256 totalCollateralRequired0) = ph @@ -5200,7 +5417,7 @@ contract Misctest is Test, PositionUtils { editCollateral(ct1, Bob, ct1.convertToShares(250)); - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); { (uint256 totalCollateralBalance0, uint256 totalCollateralRequired0) = ph .checkCollateral(pp, Bob, currentTick, posIdList); @@ -5232,7 +5449,7 @@ contract Misctest is Test, PositionUtils { // sell long leg vm.startPrank(Charlie); - tokenId = TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))); + tokenId = TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())); tokenId = tokenId.addLeg( 0, 1, @@ -5250,12 +5467,12 @@ contract Misctest is Test, PositionUtils { posIdList, 1_000_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); // create spread tokenId - tokenId = TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))); + tokenId = TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())); tokenId = tokenId.addLeg( 0, 1, @@ -5281,7 +5498,7 @@ contract Misctest is Test, PositionUtils { posIdList[0] = tokenId; - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); vm.startPrank(Bob); ct0.withdraw(ct0.maxWithdraw(Bob), Bob, Bob); @@ -5297,11 +5514,11 @@ contract Misctest is Test, PositionUtils { posIdList, 10_000, 2 ** 30, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); { (uint256 totalCollateralBalance0, uint256 totalCollateralRequired0) = ph @@ -5317,7 +5534,7 @@ contract Misctest is Test, PositionUtils { editCollateral(ct0, Bob, ct0.convertToShares(250)); editCollateral(ct1, Bob, ct1.convertToShares(250)); - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); { (uint256 totalCollateralBalance0, uint256 totalCollateralRequired0) = ph .checkCollateral(pp, Bob, currentTick, posIdList); @@ -5350,7 +5567,7 @@ contract Misctest is Test, PositionUtils { // sell long leg vm.startPrank(Charlie); - tokenId = TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))); + tokenId = TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())); tokenId = tokenId.addLeg( 0, 1, @@ -5368,12 +5585,12 @@ contract Misctest is Test, PositionUtils { posIdList, 1_000_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); // create spread tokenId - tokenId = TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))); + tokenId = TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())); tokenId = tokenId.addLeg( 0, 1, @@ -5399,7 +5616,7 @@ contract Misctest is Test, PositionUtils { posIdList[0] = tokenId; - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); vm.startPrank(Bob); ct0.withdraw(ct0.maxWithdraw(Bob), Bob, Bob); @@ -5415,11 +5632,11 @@ contract Misctest is Test, PositionUtils { posIdList, 10_000, 2 ** 30, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); { (uint256 totalCollateralBalance0, uint256 totalCollateralRequired0) = ph @@ -5434,7 +5651,7 @@ contract Misctest is Test, PositionUtils { editCollateral(ct0, Bob, ct0.convertToShares(250)); - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); { (uint256 totalCollateralBalance0, uint256 totalCollateralRequired0) = ph .checkCollateral(pp, Bob, currentTick, posIdList); @@ -5467,7 +5684,7 @@ contract Misctest is Test, PositionUtils { // sell long leg vm.startPrank(Charlie); - tokenId = TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))); + tokenId = TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())); tokenId = tokenId.addLeg( 0, 1, @@ -5485,12 +5702,12 @@ contract Misctest is Test, PositionUtils { posIdList, 1_000_000, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); // create spread tokenId - tokenId = TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(uniPool))); + tokenId = TokenId.wrap(0).addPoolId(sfpm.getPoolId(poolKey.toId())); tokenId = tokenId.addLeg( 0, 1, @@ -5516,7 +5733,7 @@ contract Misctest is Test, PositionUtils { posIdList[0] = tokenId; - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); vm.startPrank(Bob); ct0.withdraw(ct0.maxWithdraw(Bob), Bob, Bob); @@ -5532,11 +5749,11 @@ contract Misctest is Test, PositionUtils { posIdList, 10_000, 2 ** 30, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); { (uint256 totalCollateralBalance0, uint256 totalCollateralRequired0) = ph @@ -5551,7 +5768,7 @@ contract Misctest is Test, PositionUtils { editCollateral(ct1, Bob, ct1.convertToShares(250)); - (, currentTick, , , , , ) = uniPool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); { (uint256 totalCollateralBalance0, uint256 totalCollateralRequired0) = ph .checkCollateral(pp, Bob, currentTick, posIdList); diff --git a/test/foundry/core/PanopticFactory.t.sol b/test/foundry/core/PanopticFactory.t.sol index 6b6aefd..dbf7142 100644 --- a/test/foundry/core/PanopticFactory.t.sol +++ b/test/foundry/core/PanopticFactory.t.sol @@ -9,7 +9,6 @@ import {PanopticPool} from "@contracts/PanopticPool.sol"; import {CollateralTracker} from "@contracts/CollateralTracker.sol"; import {SemiFungiblePositionManager} from "@contracts/SemiFungiblePositionManager.sol"; // Panoptic Libraries -import {CallbackLib} from "@libraries/CallbackLib.sol"; import {Constants} from "@libraries/Constants.sol"; import {SafeTransferLib} from "@libraries/SafeTransferLib.sol"; import {PanopticMath} from "@libraries/PanopticMath.sol"; @@ -18,6 +17,7 @@ import {Errors} from "@libraries/Errors.sol"; // Panoptic Types import {Pointer, PointerLibrary} from "@types/Pointer.sol"; // Panoptic Interfaces +import {IV3CompatibleOracle} from "@interfaces/IV3CompatibleOracle.sol"; import {IERC20Partial} from "@tokens/interfaces/IERC20Partial.sol"; // Uniswap import {IUniswapV3Pool} from "v3-core/interfaces/IUniswapV3Pool.sol"; @@ -28,12 +28,24 @@ import {CallbackValidation} from "v3-periphery/libraries/CallbackValidation.sol" import {TransferHelper} from "v3-periphery/libraries/TransferHelper.sol"; import {Base64} from "solady/utils/Base64.sol"; import {JSONParserLib} from "solady/utils/JSONParserLib.sol"; +import {ClonesWithImmutableArgs} from "clones-with-immutable-args/ClonesWithImmutableArgs.sol"; +// V4 types +import {PoolId} from "v4-core/types/PoolId.sol"; +import {PoolKey} from "v4-core/types/PoolKey.sol"; +import {StateLibrary} from "v4-core/libraries/StateLibrary.sol"; +import {V4StateReader} from "@libraries/V4StateReader.sol"; +import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol"; +import {BalanceDelta} from "v4-core/types/BalanceDelta.sol"; +import {Currency} from "v4-core/types/Currency.sol"; +import {PoolManager} from "v4-core/PoolManager.sol"; +import {IHooks} from "v4-core/interfaces/IHooks.sol"; +import {V4RouterSimple} from "../testUtils/V4RouterSimple.sol"; contract PanopticFactoryHarness is PanopticFactory { constructor( address _WETH9, SemiFungiblePositionManager _SFPM, - IUniswapV3Factory _univ3Factory, + IPoolManager manager, address poolReference, address collateralReference, bytes32[] memory properties, @@ -43,7 +55,7 @@ contract PanopticFactoryHarness is PanopticFactory { PanopticFactory( _WETH9, _SFPM, - _univ3Factory, + manager, poolReference, collateralReference, properties, @@ -55,6 +67,10 @@ contract PanopticFactoryHarness is PanopticFactory { function getPoolReference() external view returns (address) { return POOL_REFERENCE; } + + function create3Address(bytes32 salt) external view returns (address) { + return ClonesWithImmutableArgs.addressOfClone3(salt); + } } contract PanopticFactoryTest is Test { @@ -67,8 +83,12 @@ contract PanopticFactoryTest is Test { // Mainnet factory address IUniswapV3Factory V3FACTORY = IUniswapV3Factory(0x1F98431c8aD98523631AE4a59f267346ea31F984); + IPoolManager manager = IPoolManager(address(new PoolManager())); + + V4RouterSimple routerV4 = new V4RouterSimple(manager); + // deploy the semiFungiblePositionManager - SemiFungiblePositionManager sfpm = new SemiFungiblePositionManager(V3FACTORY); + SemiFungiblePositionManager sfpm = new SemiFungiblePositionManager(manager); address Deployer = makeAddr("Deployer"); @@ -121,6 +141,8 @@ contract PanopticFactoryTest is Test { uint24 fee; int24 tickSpacing; + PoolKey poolKey; + // the amount that's deployed when initializing the SFPM against a new AMM pool. uint128 constant FULL_RANGE_LIQUIDITY_AMOUNT_WETH = 0.1 ether; uint128 constant FULL_RANGE_LIQUIDITY_AMOUNT_TOKEN = 1e6; @@ -149,6 +171,17 @@ contract PanopticFactoryTest is Test { fee = _pool.fee(); tickSpacing = _pool.tickSpacing(); + poolKey = PoolKey( + Currency.wrap(token0), + Currency.wrap(token1), + fee, + tickSpacing, + IHooks(address(0)) + ); + + (uint160 currentSqrtPriceX96, , , , , , ) = pool.slot0(); + manager.initialize(poolKey, currentSqrtPriceX96); + // give test contract a sufficient amount of tokens to deploy a new pool deal(token0, address(this), INITIAL_MOCK_TOKENS); deal(token1, address(this), INITIAL_MOCK_TOKENS); @@ -159,13 +192,8 @@ contract PanopticFactoryTest is Test { IERC20Partial(token0).approve(address(panopticFactory), INITIAL_MOCK_TOKENS); IERC20Partial(token1).approve(address(panopticFactory), INITIAL_MOCK_TOKENS); - // approve sfpm to move tokens, on behalf of the test contract - IERC20Partial(token0).approve(address(sfpm), INITIAL_MOCK_TOKENS); - IERC20Partial(token1).approve(address(sfpm), INITIAL_MOCK_TOKENS); - - // approve self - IERC20Partial(token0).approve(address(this), INITIAL_MOCK_TOKENS); - IERC20Partial(token1).approve(address(this), INITIAL_MOCK_TOKENS); + IERC20Partial(token0).approve(address(routerV4), INITIAL_MOCK_TOKENS); + IERC20Partial(token1).approve(address(routerV4), INITIAL_MOCK_TOKENS); } function setUp() public { @@ -223,9 +251,9 @@ contract PanopticFactoryTest is Test { panopticFactory = new PanopticFactoryHarness( address(_WETH), sfpm, - V3FACTORY, - address(new PanopticPool(sfpm)), - address(new CollateralTracker(10, 2_000, 1_000, -1_024, 5_000, 9_000, 20_000)), + manager, + address(new PanopticPool(sfpm, manager)), + address(new CollateralTracker(10, 2_000, 1_000, -1_024, 5_000, 9_000, 20, manager)), props, indices, pointers @@ -242,21 +270,18 @@ contract PanopticFactoryTest is Test { _initWorld(x); // Compute clone determinsitic Panoptic Factory address - address poolReference = panopticFactory.getPoolReference(); - address preComputedPool = predictDeterministicAddress( - poolReference, + address preComputedPool = panopticFactory.create3Address( bytes32( abi.encodePacked( uint80(uint160(address(this)) >> 80), - uint80(uint160(address(pool)) >> 80), + uint80(uint256(keccak256(abi.encode(poolKey, pool)))), salt ) - ), - address(panopticFactory) + ) ); // Amount of liquidity currently in the univ3 pool - uint128 liquidityBefore = pool.liquidity(); + uint128 liquidityBefore = StateLibrary.getLiquidity(manager, poolKey.toId()); // amount of assets held before mint // Compute amount of liquidity to deploy @@ -266,9 +291,8 @@ contract PanopticFactoryTest is Test { // Deploy pool // links the Uniswap V3 pool to the Panoptic pool PanopticPool deployedPool = panopticFactory.deployNewPool( - token0, - token1, - fee, + IV3CompatibleOracle(address(pool)), + poolKey, salt, type(uint256).max, type(uint256).max @@ -283,10 +307,17 @@ contract PanopticFactoryTest is Test { assertGt(size, 0); // check if pool is linked to the correct panoptic pool in factory - assertEq(address(panopticFactory.getPanopticPool(pool)), address(deployedPool)); + assertEq( + address( + panopticFactory.getPanopticPool(poolKey, IV3CompatibleOracle(address(pool))) + ), + address(deployedPool) + ); // see if correct pool was linked in the panopticPool - IUniswapV3Pool linkedPool = PanopticPool(preComputedPool).univ3pool(); - address linkedPoolAddress = address(PanopticPool(preComputedPool).univ3pool()); + IUniswapV3Pool linkedPool = IUniswapV3Pool( + address(PanopticPool(preComputedPool).oracleContract()) + ); + address linkedPoolAddress = address(PanopticPool(preComputedPool).oracleContract()); assertEq(address(pool), linkedPoolAddress); // check the pool has the correct parameters @@ -297,7 +328,7 @@ contract PanopticFactoryTest is Test { /* Liquidity checks */ // Amount of liquidity in univ3 pool after Panoptic Pool deployment - uint128 liquidityAfter = pool.liquidity(); + uint128 liquidityAfter = StateLibrary.getLiquidity(manager, poolKey.toId()); // ensure liquidity in pool now is sum of liquidity before and user deployed amount assertEq(liquidityAfter - liquidityBefore, fullRangeLiquidity); } @@ -312,11 +343,10 @@ contract PanopticFactoryTest is Test { uint96 salt = uint96(block.timestamp); // Deploy pool - // links the Uniswap V3 pool to the Panoptic pool + // links the uni v3 pool to the Panoptic pool panopticFactory.deployNewPool( - token0, - token1, - fee, + IV3CompatibleOracle(address(pool)), + poolKey, salt, type(uint256).max, type(uint256).max @@ -333,11 +363,10 @@ contract PanopticFactoryTest is Test { uint96 salt = uint96(block.timestamp); // Deploy pool - // links the Uniswap V3 pool to the Panoptic pool + // links the uni v3 pool to the Panoptic pool panopticFactory.deployNewPool( - token0, - token1, - fee, + IV3CompatibleOracle(address(pool)), + poolKey, salt, type(uint256).max, type(uint256).max @@ -351,7 +380,13 @@ contract PanopticFactoryTest is Test { (, uint256 amount0, uint256 amount1) = computeFullRangeLiquidity(); - panopticFactory.deployNewPool(token0, token1, fee, uint96(salt), amount0, amount1); + panopticFactory.deployNewPool( + IV3CompatibleOracle(address(pool)), + poolKey, + uint96(salt), + amount0, + amount1 + ); } function test_Fail_deployNewPool_Slippage0() public { @@ -362,7 +397,13 @@ contract PanopticFactoryTest is Test { (, uint256 amount0, uint256 amount1) = computeFullRangeLiquidity(); vm.expectRevert(Errors.PriceBoundFail.selector); - panopticFactory.deployNewPool(token0, token1, fee, uint96(salt), amount0 - 1, amount1); + panopticFactory.deployNewPool( + IV3CompatibleOracle(address(pool)), + poolKey, + uint96(salt), + amount0 - 1, + amount1 + ); } function test_Fail_deployNewPool_Slippage1() public { @@ -373,7 +414,13 @@ contract PanopticFactoryTest is Test { (, uint256 amount0, uint256 amount1) = computeFullRangeLiquidity(); vm.expectRevert(Errors.PriceBoundFail.selector); - panopticFactory.deployNewPool(token0, token1, fee, uint96(salt), amount0, amount1 - 1); + panopticFactory.deployNewPool( + IV3CompatibleOracle(address(pool)), + poolKey, + uint96(salt), + amount0, + amount1 - 1 + ); } function test_Fail_deployNewPool_Slippage0Both() public { @@ -384,7 +431,13 @@ contract PanopticFactoryTest is Test { (, uint256 amount0, uint256 amount1) = computeFullRangeLiquidity(); vm.expectRevert(Errors.PriceBoundFail.selector); - panopticFactory.deployNewPool(token0, token1, fee, uint96(salt), amount0 - 1, amount1 - 1); + panopticFactory.deployNewPool( + IV3CompatibleOracle(address(pool)), + poolKey, + uint96(salt), + amount0 - 1, + amount1 - 1 + ); } // Revert if trying to deploy a Panoptic Pool ontop of an invalid Uniswap Pool @@ -395,9 +448,8 @@ contract PanopticFactoryTest is Test { // Deploy invalid pool (uninitalized tokens and fee) vm.expectRevert(Errors.UniswapPoolNotInitialized.selector); panopticFactory.deployNewPool( - token0, - token1, - fee, + IV3CompatibleOracle(address(pool)), + poolKey, salt, type(uint256).max, type(uint256).max @@ -415,9 +467,8 @@ contract PanopticFactoryTest is Test { // Deploy pool panopticFactory.deployNewPool( - token0, - token1, - fee, + IV3CompatibleOracle(address(pool)), + poolKey, salt, type(uint256).max, type(uint256).max @@ -427,9 +478,8 @@ contract PanopticFactoryTest is Test { vm.expectRevert(Errors.PoolAlreadyInitialized.selector); unchecked { panopticFactory.deployNewPool( - token0, - token1, - fee, + IV3CompatibleOracle(address(pool)), + poolKey, salt + 1, type(uint256).max, type(uint256).max @@ -445,9 +495,8 @@ contract PanopticFactoryTest is Test { _initalizeWorldState(pools[1]); uint96 salt = uint96(block.timestamp); PanopticPool deployedPool = panopticFactory.deployNewPool( - token0, - token1, - fee, + IV3CompatibleOracle(address(pool)), + poolKey, salt, type(uint256).max, type(uint256).max @@ -493,6 +542,7 @@ contract PanopticFactoryTest is Test { (uint96 bestSalt, uint256 highestRarity) = panopticFactory.minePoolAddress( randomAddress, address(pool), + poolKey, nonce, 50_000, minTargetRarity @@ -501,16 +551,14 @@ contract PanopticFactoryTest is Test { assertEq( highestRarity, PanopticMath.numberOfLeadingHexZeros( - predictDeterministicAddress( - panopticFactory.getPoolReference(), + panopticFactory.create3Address( bytes32( abi.encodePacked( uint80(uint160(randomAddress) >> 80), - uint80(uint160(address(pool)) >> 80), + uint80(uint256(keccak256(abi.encode(poolKey, pool)))), bestSalt ) - ), - address(panopticFactory) + ) ) ) ); @@ -519,34 +567,6 @@ contract PanopticFactoryTest is Test { assertGe(highestRarity, minTargetRarity); } - /*////////////////////////////////////////////////////////////// - PRECOMPUTE CLONE ADDRESS - //////////////////////////////////////////////////////////////*/ - - /* Internal functions used in base contract logic replicated for redundancy - If a change is made to the logic makeup of these functions in the core contracts, - Then they will have to be equally changed in the tests - */ - - /// Computes the address of a clone deployed using {Clones-cloneDeterministic}. - /// Replicated from the Clones library in OZ (internal as it cannot be called directly) - function predictDeterministicAddress( - address implementation, - bytes32 salt, - address deployer - ) internal pure returns (address predicted) { - assembly ("memory-safe") { - let ptr := mload(0x40) - mstore(add(ptr, 0x38), deployer) - mstore(add(ptr, 0x24), 0x5af43d82803e903d91602b57fd5bf3ff) - mstore(add(ptr, 0x14), implementation) - mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73) - mstore(add(ptr, 0x58), salt) - mstore(add(ptr, 0x78), keccak256(add(ptr, 0x0c), 0x37)) - predicted := keccak256(add(ptr, 0x43), 0x55) - } - } - /*////////////////////////////////////////////////////////////// COMPUTE FULL RANGE LIQUIDITY //////////////////////////////////////////////////////////////*/ @@ -554,19 +574,11 @@ contract PanopticFactoryTest is Test { /// Replicated logic from _mintFullRange in Panoptic Factory function computeFullRangeLiquidity() internal - returns (uint128 fullRangeLiquidity, uint256 amount0, uint256 amount1) + returns (uint128 fullRangeLiquidity, uint256, uint256) { // get current tick (uint160 currentSqrtPriceX96, , , , , , ) = pool.slot0(); - // build callback data - bytes memory mintdata = abi.encode( - CallbackData({ // compute by reading values from univ3pool every time - univ3poolKey: PoolAddress.PoolKey({token0: token0, token1: token1, fee: fee}), - payer: address(this) - }) - ); - // For full range: L = Δx * sqrt(P) = Δy / sqrt(P) // We start with fixed delta amounts and apply this equation to calculate the liquidity unchecked { @@ -611,43 +623,18 @@ contract PanopticFactoryTest is Test { // simulate the amounts minted in the Uniswap pool uint256 snapshot = vm.snapshot(); - (amount0, amount1) = IUniswapV3Pool(pool).mint( - address(this), + (int256 amount0, int256 amount1) = routerV4.modifyLiquidity( + address(0), + poolKey, (TickMath.MIN_TICK / tickSpacing) * tickSpacing, (TickMath.MAX_TICK / tickSpacing) * tickSpacing, - fullRangeLiquidity, - mintdata + int256(uint256(fullRangeLiquidity)) ); // revert state vm.revertTo(snapshot); - } - } - 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, V3FACTORY, 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 - ); + return (fullRangeLiquidity, uint256(-amount0), uint256(-amount1)); + } } } diff --git a/test/foundry/core/PanopticPool.t.sol b/test/foundry/core/PanopticPool.t.sol index 3e45014..9d8d7b4 100644 --- a/test/foundry/core/PanopticPool.t.sol +++ b/test/foundry/core/PanopticPool.t.sol @@ -5,7 +5,6 @@ import "forge-std/Test.sol"; import {Errors} from "@libraries/Errors.sol"; import {Math} from "@libraries/Math.sol"; import {PanopticMath} from "@libraries/PanopticMath.sol"; -import {FeesCalc} from "@libraries/FeesCalc.sol"; import {TokenId} from "@types/TokenId.sol"; import {LeftRightUnsigned, LeftRightSigned} from "@types/LeftRight.sol"; import {LiquidityChunk, LiquidityChunkLibrary} from "@types/LiquidityChunk.sol"; @@ -22,19 +21,27 @@ import {ISwapRouter} from "v3-periphery/interfaces/ISwapRouter.sol"; import {SemiFungiblePositionManager} from "@contracts/SemiFungiblePositionManager.sol"; import {PanopticPool} from "@contracts/PanopticPool.sol"; import {CollateralTracker} from "@contracts/CollateralTracker.sol"; +import {IV3CompatibleOracle} from "@interfaces/IV3CompatibleOracle.sol"; import {PanopticFactory} from "@contracts/PanopticFactory.sol"; import {PanopticHelper} from "@test_periphery/PanopticHelper.sol"; import {PositionUtils} from "../testUtils/PositionUtils.sol"; import {UniPoolPriceMock} from "../testUtils/PriceMocks.sol"; import {Constants} from "@libraries/Constants.sol"; import {Pointer} from "@types/Pointer.sol"; +// V4 types +import {PoolId} from "v4-core/types/PoolId.sol"; +import {PoolKey} from "v4-core/types/PoolKey.sol"; +import {StateLibrary} from "v4-core/libraries/StateLibrary.sol"; +import {V4StateReader} from "@libraries/V4StateReader.sol"; +import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol"; +import {BalanceDelta} from "v4-core/types/BalanceDelta.sol"; +import {Currency} from "v4-core/types/Currency.sol"; +import {PoolManager} from "v4-core/PoolManager.sol"; +import {IHooks} from "v4-core/interfaces/IHooks.sol"; +import {V4RouterSimple} from "../testUtils/V4RouterSimple.sol"; contract SemiFungiblePositionManagerHarness is SemiFungiblePositionManager { - constructor(IUniswapV3Factory _factory) SemiFungiblePositionManager(_factory) {} - - function addrToPoolId(address pool) public view returns (uint256) { - return s_AddrToPoolIdData[pool]; - } + constructor(IPoolManager _manager) SemiFungiblePositionManager(_manager) {} } contract PanopticPoolHarness is PanopticPool { @@ -53,8 +60,8 @@ contract PanopticPoolHarness is PanopticPool { * @notice compute the TWAP price from the last 600s = 10mins * @return twapTick the TWAP price in ticks */ - function getUniV3TWAP_() external view returns (int24 twapTick) { - twapTick = PanopticMath.twapFilter(s_univ3pool, TWAP_WINDOW); + function getOracleTWAP_() external view returns (int24 twapTick) { + twapTick = PanopticMath.twapFilter(oracleContract(), TWAP_WINDOW); } function settledTokens(bytes32 chunk) external view returns (LeftRightUnsigned) { @@ -81,7 +88,10 @@ contract PanopticPoolHarness is PanopticPool { return (premiasByLeg, netExchanged); } - constructor(SemiFungiblePositionManager _sfpm) PanopticPool(_sfpm) {} + constructor( + SemiFungiblePositionManager _sfpm, + IPoolManager _manager + ) PanopticPool(_sfpm, _manager) {} } contract PanopticPoolTest is PositionUtils { @@ -168,6 +178,12 @@ contract PanopticPoolTest is PositionUtils { address Charlie = address(0x1234567891); address Seller = address(0x12345678912); + IPoolManager manager; + + V4RouterSimple routerV4; + + PoolKey poolKey; + /*////////////////////////////////////////////////////////////// TEST DATA //////////////////////////////////////////////////////////////*/ @@ -342,7 +358,6 @@ contract PanopticPoolTest is PositionUtils { function _cacheWorldState(IUniswapV3Pool _pool) internal { pool = _pool; - poolId = PanopticMath.getPoolId(address(_pool)); token0 = _pool.token0(); token1 = _pool.token1(); isWETH = token0 == address(WETH) ? 0 : 1; @@ -353,15 +368,45 @@ contract PanopticPoolTest is PositionUtils { feeGrowthGlobal1X128 = _pool.feeGrowthGlobal1X128(); poolBalance0 = IERC20Partial(token0).balanceOf(address(_pool)); poolBalance1 = IERC20Partial(token1).balanceOf(address(_pool)); + + poolKey = PoolKey( + Currency.wrap(token0), + Currency.wrap(token1), + fee, + tickSpacing, + IHooks(address(0)) + ); + poolId = PanopticMath.getPoolId(poolKey.toId(), poolKey.tickSpacing); } function _deployPanopticPool() internal { + vm.startPrank(Swapper); + + deal(token0, Swapper, type(uint248).max); + deal(token1, Swapper, type(uint248).max); + + IERC20Partial(token0).approve(address(router), type(uint256).max); + IERC20Partial(token1).approve(address(router), type(uint256).max); + + IERC20Partial(token0).approve(address(routerV4), type(uint256).max); + IERC20Partial(token1).approve(address(routerV4), type(uint256).max); + + manager.initialize(poolKey, currentSqrtPriceX96); + + routerV4.modifyLiquidity( + address(0), + poolKey, + (TickMath.MIN_TICK / tickSpacing) * tickSpacing, + (TickMath.MAX_TICK / tickSpacing) * tickSpacing, + 1_000_000 ether + ); + vm.startPrank(Deployer); factory = new PanopticFactory( WETH, sfpm, - V3FACTORY, + manager, poolReference, collateralReference, new bytes32[](0), @@ -377,9 +422,8 @@ contract PanopticPoolTest is PositionUtils { pp = PanopticPoolHarness( address( factory.deployNewPool( - token0, - token1, - fee, + IV3CompatibleOracle(address(pool)), + poolKey, uint96(block.timestamp), type(uint256).max, type(uint256).max @@ -392,14 +436,6 @@ contract PanopticPoolTest is PositionUtils { } function _initAccounts() internal { - vm.startPrank(Swapper); - - IERC20Partial(token0).approve(address(router), type(uint256).max); - IERC20Partial(token1).approve(address(router), type(uint256).max); - - deal(token0, Swapper, type(uint104).max); - deal(token1, Swapper, type(uint104).max); - vm.startPrank(Charlie); deal(token0, Charlie, type(uint104).max); @@ -471,14 +507,16 @@ contract PanopticPoolTest is PositionUtils { } function setUp() public { - sfpm = new SemiFungiblePositionManagerHarness(V3FACTORY); + manager = new PoolManager(); + routerV4 = new V4RouterSimple(manager); + sfpm = new SemiFungiblePositionManagerHarness(manager); ph = new PanopticHelper(sfpm); // deploy reference pool and collateral token - poolReference = address(new PanopticPoolHarness(sfpm)); + poolReference = address(new PanopticPoolHarness(sfpm, manager)); collateralReference = address( - new CollateralTracker(10, 2_000, 1_000, -1_024, 5_000, 9_000, 20_000) + new CollateralTracker(10, 2_000, 1_000, -1_024, 5_000, 9_000, 20, manager) ); } @@ -1336,6 +1374,7 @@ contract PanopticPoolTest is PositionUtils { function twoWaySwap(uint256 swapSize) public { vm.startPrank(Swapper); + uint160 originalSqrtPriceX96 = V4StateReader.getSqrtPriceX96(manager, poolKey.toId()); swapSize = bound(swapSize, 10 ** 18, 10 ** 20); for (uint256 i = 0; i < 10; ++i) { router.exactInputSingle( @@ -1351,6 +1390,10 @@ contract PanopticPoolTest is PositionUtils { ) ); + (uint160 swappedSqrtPriceX96, , , , , , ) = pool.slot0(); + + routerV4.swapTo(address(0), poolKey, swappedSqrtPriceX96); + router.exactOutputSingle( ISwapRouter.ExactOutputSingleParams( isWETH == 1 ? token0 : token1, @@ -1363,9 +1406,16 @@ contract PanopticPoolTest is PositionUtils { 0 ) ); + + (swappedSqrtPriceX96, , , , , , ) = pool.slot0(); + + routerV4.swapTo(address(0), poolKey, swappedSqrtPriceX96); } - (currentSqrtPriceX96, currentTick, , , , , ) = pool.slot0(); + routerV4.swapTo(address(0), poolKey, originalSqrtPriceX96); + + currentSqrtPriceX96 = V4StateReader.getSqrtPriceX96(manager, poolKey.toId()); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); } function oneWaySwap(uint256 swapSize, bool swapDirection) public { @@ -1384,6 +1434,9 @@ contract PanopticPoolTest is PositionUtils { 0 ) ); + + (uint160 swappedSqrtPriceX96, , , , , , ) = pool.slot0(); + routerV4.swapTo(address(0), poolKey, swappedSqrtPriceX96); } else { router.exactOutputSingle( ISwapRouter.ExactOutputSingleParams( @@ -1397,19 +1450,55 @@ contract PanopticPoolTest is PositionUtils { 0 ) ); + + (uint160 swappedSqrtPriceX96, , , , , , ) = pool.slot0(); + routerV4.swapTo(address(0), poolKey, swappedSqrtPriceX96); } } + function getTokensOwed( + int24 _tickLower, + int24 _tickUpper, + uint256 _tokenType + ) internal view returns (uint128 _tokensOwed0, uint128 _tokensOwed1) { + (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = StateLibrary + .getFeeGrowthInside(manager, poolKey.toId(), _tickLower, _tickUpper); + + bytes32 positionKey = keccak256( + abi.encodePacked(poolKey.toId(), address(pp), _tokenType, _tickLower, _tickUpper) + ); + + ( + uint128 _liquidity, + uint256 feeGrowthInside0LastX128, + uint256 feeGrowthInside1LastX128 + ) = StateLibrary.getPositionInfo( + manager, + poolKey.toId(), + address(sfpm), + _tickLower, + _tickUpper, + positionKey + ); + + _tokensOwed0 = uint128( + Math.mulDiv128(feeGrowthInside0X128 - feeGrowthInside0LastX128, _liquidity) + ); + _tokensOwed1 = uint128( + Math.mulDiv128(feeGrowthInside1X128 - feeGrowthInside1LastX128, _liquidity) + ); + } + /*////////////////////////////////////////////////////////////// POOL INITIALIZATION: - //////////////////////////////////////////////////////////////*/ - function test_Fail_startPool_PoolAlreadyInitialized(uint256 x) public { + function test_Fail_initialize_PoolAlreadyInitialized(uint256 x) public { _initWorld(x); vm.expectRevert(Errors.PoolAlreadyInitialized.selector); - pp.startPool(pool, token0, token1, ct0, ct1); + pp.initialize(); } /*////////////////////////////////////////////////////////////// @@ -1431,61 +1520,16 @@ contract PanopticPoolTest is PositionUtils { assertEq(vm.load(address(ct0), bytes32(uint256(2))), bytes32(uint256(0))); // allowance slot assertEq(vm.load(address(ct1), bytes32(uint256(2))), bytes32(uint256(0))); // allowance slot - assertEq( - vm.load(address(ct0), bytes32(uint256(3))), - bytes32(uint256(uint256(1 << 160) + uint160(address(token0)))) - ); // underlying token + initialized - assertEq( - vm.load(address(ct1), bytes32(uint256(3))), - bytes32(uint256(uint256(1 << 160) + uint160(address(token1)))) - ); // underlying token + initialized - - assertEq( - vm.load(address(ct0), bytes32(uint256(4))), - bytes32(uint256(uint160(address(token0)))) - ); // token0 - assertEq( - vm.load(address(ct1), bytes32(uint256(4))), - bytes32(uint256(uint160(address(token0)))) - ); // token0 - - assertEq( - vm.load(address(ct0), bytes32(uint256(5))), - bytes32(uint256(uint256(1 << 160) + uint160(address(token1)))) - ); // token1 + underlyingistoken0 - - assertEq( - vm.load(address(ct1), bytes32(uint256(5))), - bytes32(uint256(uint160(address(token1)))) - ); // token1 + underlyingistoken0 - - assertEq( - vm.load(address(ct0), bytes32(uint256(6))), - bytes32(uint256(uint160(address(pp)))) - ); // pool - - assertEq( - vm.load(address(ct1), bytes32(uint256(6))), - bytes32(uint256(uint160(address(pp)))) - ); // pool + assertEq(vm.load(address(ct0), bytes32(uint256(3))), bytes32(uint256(1))); // poolAssets + inAMM - assertEq(vm.load(address(ct0), bytes32(uint256(7))), bytes32(uint256(1))); // poolAssets + inAMM + assertEq(vm.load(address(ct1), bytes32(uint256(3))), bytes32(uint256(1))); // poolAssets + inAMM - assertEq(vm.load(address(ct1), bytes32(uint256(7))), bytes32(uint256(1))); // poolAssets + inAMM + assertEq(vm.load(address(ct0), bytes32(uint256(4))), bytes32(uint256(1))); // initialized + assertEq(vm.load(address(ct1), bytes32(uint256(4))), bytes32(uint256(1))); // initialized - assertEq( - vm.load(address(ct0), bytes32(uint256(8))), - bytes32(uint256((2 * uint256(fee)) + (uint256(fee) << 128))) - ); // ITMSpreadFee + poolFee - - assertEq( - vm.load(address(ct1), bytes32(uint256(8))), - bytes32(uint256((2 * uint256(fee)) + (uint256(fee) << 128))) - ); // ITMSpreadFee + poolFee + assertEq(vm.load(address(ct0), bytes32(uint256(5))), bytes32(uint256(0))); // 0 - assertEq(vm.load(address(ct0), bytes32(uint256(9))), bytes32(uint256(0))); // 0 - - assertEq(vm.load(address(ct1), bytes32(uint256(9))), bytes32(uint256(0))); // 0 + assertEq(vm.load(address(ct1), bytes32(uint256(5))), bytes32(uint256(0))); // 0 } /*////////////////////////////////////////////////////////////// @@ -1599,8 +1643,8 @@ contract PanopticPoolTest is PositionUtils { posIdList, positionSizes[0], 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -1628,8 +1672,8 @@ contract PanopticPoolTest is PositionUtils { posIdList, positionSizes[1], 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); twoWaySwap(swapSizeSeed); @@ -1638,7 +1682,7 @@ contract PanopticPoolTest is PositionUtils { uint256[2] memory expectedPremia; { (uint256 premiumToken0, uint256 premiumToken1) = sfpm.getAccountPremium( - address(pool), + poolKey.toId(), address(pp), 0, tickLowers[0], @@ -1654,7 +1698,7 @@ contract PanopticPoolTest is PositionUtils { { (uint256 premiumToken0, uint256 premiumToken1) = sfpm.getAccountPremium( - address(pool), + poolKey.toId(), address(pp), 0, tickLowers[1], @@ -1734,8 +1778,8 @@ contract PanopticPoolTest is PositionUtils { posIdList, positionSize, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); premiaSeed[0] = bound(premiaSeed[0], 2 ** 64, 2 ** 120); @@ -1747,10 +1791,7 @@ contract PanopticPoolTest is PositionUtils { posIdList ); - accruePoolFeesInRange(address(pool), expectedLiq, premiaSeed[0], premiaSeed[1]); - - vm.startPrank(address(sfpm)); - pool.burn(tickLower, tickUpper, 0); + accruePoolFeesInRange(manager, poolKey, expectedLiq, premiaSeed[0], premiaSeed[1]); ($shortPremia, $longPremia, ) = pp.getAccumulatedFeesAndPositionsData( Alice, @@ -1834,8 +1875,8 @@ contract PanopticPoolTest is PositionUtils { posIdList, positionSize * 2, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); vm.startPrank(Alice); @@ -1862,8 +1903,8 @@ contract PanopticPoolTest is PositionUtils { posIdList, positionSize, type(uint64).max, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); assertEq( @@ -1956,8 +1997,8 @@ contract PanopticPoolTest is PositionUtils { posIdList, positionSize * 2, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); vm.startPrank(Alice); @@ -1984,8 +2025,8 @@ contract PanopticPoolTest is PositionUtils { posIdList, positionSize, type(uint64).max - 1, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); assertEq(sfpm.balanceOf(address(pp), TokenId.unwrap(tokenId)), positionSize); @@ -2070,8 +2111,8 @@ contract PanopticPoolTest is PositionUtils { posIdList, positionSize * 2, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); vm.startPrank(Alice); @@ -2084,8 +2125,8 @@ contract PanopticPoolTest is PositionUtils { posIdList, positionSize, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -2212,8 +2253,8 @@ contract PanopticPoolTest is PositionUtils { posIdList, positionSize, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); assertEq(sfpm.balanceOf(address(pp), TokenId.unwrap(tokenId)), positionSize); @@ -2302,8 +2343,8 @@ contract PanopticPoolTest is PositionUtils { posIdList, positionSize, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); assertEq(sfpm.balanceOf(address(pp), TokenId.unwrap(tokenId)), positionSize); @@ -2397,14 +2438,11 @@ contract PanopticPoolTest is PositionUtils { ); (expectedSwap0, ) = PositionUtils.simulateSwap( - pool, + poolKey, tickLower, tickUpper, expectedLiq, - router, - token0, - token1, - fee, + routerV4, true, -amount1Required ); @@ -2471,8 +2509,8 @@ contract PanopticPoolTest is PositionUtils { int256 notionalVal = int256(expectedSwap0) + amount0Moved - shortAmounts.rightSlot(); int256 ITMSpread = notionalVal > 0 - ? (notionalVal * int24(2 * (fee / 100))) / 10_000 - : -(notionalVal * int24(2 * (fee / 100))) / 10_000; + ? (notionalVal * int24(20)) / 10_000 + : -(notionalVal * int24(20)) / 10_000; assertApproxEqAbs( ct0.balanceOf(Alice), @@ -2650,14 +2688,11 @@ contract PanopticPoolTest is PositionUtils { PanopticMath.convert1to0($amount1Moveds[1], currentSqrtPriceX96); (int256 amount0s, int256 amount1s) = PositionUtils.simulateSwap( - pool, + poolKey, [tickLowers[0], tickLowers[1]], [tickUppers[0], tickUppers[1]], [expectedLiqs[0], expectedLiqs[1]], - router, - token0, - token1, - fee, + routerV4, netSurplus0 < 0, -netSurplus0 ); @@ -2678,14 +2713,16 @@ contract PanopticPoolTest is PositionUtils { posIdList, positionSize, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } - (, currentTick, observationIndex, observationCardinality, , , ) = pool.slot0(); + + currentTick = V4StateReader.getTick(manager, poolKey.toId()); + (, , observationIndex, observationCardinality, , , ) = pool.slot0(); (fastOracleTick, ) = PanopticMath.computeMedianObservedPrice( - pool, + IV3CompatibleOracle(address(pool)), observationIndex, observationCardinality, 3, @@ -2697,7 +2734,7 @@ contract PanopticPoolTest is PositionUtils { observationCardinality, 60, pp.miniMedian(), - pool + IV3CompatibleOracle(address(pool)) ); assertEq(sfpm.balanceOf(address(pp), TokenId.unwrap(tokenId)), positionSize); @@ -2747,11 +2784,11 @@ contract PanopticPoolTest is PositionUtils { ]; int256[2] memory ITMSpreads = [ notionalVals[0] > 0 - ? (notionalVals[0] * int24(2 * (fee / 100))) / 10_000 - : -((notionalVals[0] * int24(2 * (fee / 100))) / 10_000), + ? (notionalVals[0] * int24(20)) / 10_000 + : -((notionalVals[0] * int24(20)) / 10_000), notionalVals[1] > 0 - ? (notionalVals[1] * int24(2 * (fee / 100))) / 10_000 - : -((notionalVals[1] * int24(2 * (fee / 100))) / 10_000) + ? (notionalVals[1] * int24(20)) / 10_000 + : -((notionalVals[1] * int24(20)) / 10_000) ]; assertApproxEqAbs( @@ -2834,14 +2871,15 @@ contract PanopticPoolTest is PositionUtils { posIdList, positionSize, 0, - Constants.MIN_V3POOL_TICK, - Constants.MAX_V3POOL_TICK + Constants.MIN_V4POOL_TICK, + Constants.MAX_V4POOL_TICK ); } - (, currentTick, observationIndex, observationCardinality, , , ) = pool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); + (, , observationIndex, observationCardinality, , , ) = pool.slot0(); (fastOracleTick, ) = PanopticMath.computeMedianObservedPrice( - pool, + IV3CompatibleOracle(address(pool)), observationIndex, observationCardinality, 3, @@ -2853,7 +2891,7 @@ contract PanopticPoolTest is PositionUtils { observationCardinality, 60, pp.miniMedian(), - pool + IV3CompatibleOracle(address(pool)) ); assertEq(sfpm.balanceOf(address(pp), TokenId.unwrap(tokenId)), positionSize); @@ -2980,8 +3018,8 @@ contract PanopticPoolTest is PositionUtils { posIdList, positionSizes[0], 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -2991,11 +3029,12 @@ contract PanopticPoolTest is PositionUtils { tokenId = tokenId.addLeg(1, 1, isWETH, 1, 0, 1, strike1, width1); // price changes afters swap at mint so we need to update the price - (currentSqrtPriceX96, currentTick, observationIndex, observationCardinality, , , ) = pool - .slot0(); + currentSqrtPriceX96 = V4StateReader.getSqrtPriceX96(manager, poolKey.toId()); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); + (, , observationIndex, observationCardinality, , , ) = pool.slot0(); (fastOracleTick, ) = PanopticMath.computeMedianObservedPrice( - pool, + IV3CompatibleOracle(address(pool)), observationIndex, observationCardinality, 3, @@ -3007,7 +3046,7 @@ contract PanopticPoolTest is PositionUtils { observationCardinality, 60, pp.miniMedian(), - pool + IV3CompatibleOracle(address(pool)) ); updatePositionDataLong(); @@ -3015,16 +3054,37 @@ contract PanopticPoolTest is PositionUtils { int256 netSurplus0 = $amount0Moveds[1] - PanopticMath.convert1to0($amount1Moveds[2], currentSqrtPriceX96); + console2.log("netSurplus0", netSurplus0); + console2.log("amount0Moveds", $amount0Moveds[1]); + console2.log("amount1Moveds", $amount1Moveds[2]); + vm.startPrank(address(sfpm)); - (int256 amount0s, int256 amount1s) = PositionUtils.simulateSwapLong( - pool, + (int256 amount0s, int256 amount1s) = this.simulateSwapLong( + poolKey, [tickLowers[0], tickLowers[1]], [tickUppers[0], tickUppers[1]], [int128(expectedLiqs[1]), -int128(expectedLiqs[2])], - router, - token0, - token1, - fee, + [ + keccak256( + abi.encodePacked( + poolKey.toId(), + address(pp), + uint256(1), + tickLowers[0], + tickUppers[0] + ) + ), + keccak256( + abi.encodePacked( + poolKey.toId(), + address(pp), + uint256(0), + tickLowers[1], + tickUppers[1] + ) + ) + ], + routerV4, netSurplus0 < 0, -netSurplus0 ); @@ -3050,11 +3110,11 @@ contract PanopticPoolTest is PositionUtils { ITMSpreads = [ notionalVals[0] > 0 - ? (notionalVals[0] * int24(2 * (fee / 100))) / 10_000 - : -((notionalVals[0] * int24(2 * (fee / 100))) / 10_000), + ? (notionalVals[0] * int24(20)) / 10_000 + : -((notionalVals[0] * int24(20)) / 10_000), notionalVals[1] > 0 - ? (notionalVals[1] * int24(2 * (fee / 100))) / 10_000 - : -((notionalVals[1] * int24(2 * (fee / 100))) / 10_000) + ? (notionalVals[1] * int24(20)) / 10_000 + : -((notionalVals[1] * int24(20)) / 10_000) ]; uint256 tokenToPay = uint256( @@ -3079,13 +3139,13 @@ contract PanopticPoolTest is PositionUtils { posIdList, positionSizes[1], type(uint64).max, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } - // price changes afters swap at mint so we need to update the price - (currentSqrtPriceX96, currentTick, , , , , ) = pool.slot0(); + currentSqrtPriceX96 = V4StateReader.getSqrtPriceX96(manager, poolKey.toId()); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); assertEq(sfpm.balanceOf(address(pp), TokenId.unwrap(tokenId)), positionSizes[1]); @@ -3203,8 +3263,8 @@ contract PanopticPoolTest is PositionUtils { posIdList, positionSizes[0], 0, - Constants.MIN_V3POOL_TICK, - Constants.MAX_V3POOL_TICK + Constants.MIN_V4POOL_TICK, + Constants.MAX_V4POOL_TICK ); } @@ -3214,11 +3274,12 @@ contract PanopticPoolTest is PositionUtils { tokenId = tokenId.addLeg(1, 1, isWETH, 1, 0, 1, strike1, width1); // price changes afters swap at mint so we need to update the price - (currentSqrtPriceX96, currentTick, observationIndex, observationCardinality, , , ) = pool - .slot0(); + currentSqrtPriceX96 = V4StateReader.getSqrtPriceX96(manager, poolKey.toId()); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); + (, , observationIndex, observationCardinality, , , ) = pool.slot0(); (fastOracleTick, ) = PanopticMath.computeMedianObservedPrice( - pool, + IV3CompatibleOracle(address(pool)), observationIndex, observationCardinality, 3, @@ -3230,7 +3291,7 @@ contract PanopticPoolTest is PositionUtils { observationCardinality, 60, pp.miniMedian(), - pool + IV3CompatibleOracle(address(pool)) ); updatePositionDataLong(); @@ -3272,13 +3333,14 @@ contract PanopticPoolTest is PositionUtils { posIdList, positionSizes[1], type(uint64).max, - Constants.MIN_V3POOL_TICK, - Constants.MAX_V3POOL_TICK + Constants.MIN_V4POOL_TICK, + Constants.MAX_V4POOL_TICK ); } // price changes afters swap at mint so we need to update the price - (currentSqrtPriceX96, currentTick, , , , , ) = pool.slot0(); + currentSqrtPriceX96 = V4StateReader.getSqrtPriceX96(manager, poolKey.toId()); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); assertEq(sfpm.balanceOf(address(pp), TokenId.unwrap(tokenId)), positionSizes[1]); @@ -3498,8 +3560,8 @@ contract PanopticPoolTest is PositionUtils { posIdList, positionSize, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -3539,8 +3601,8 @@ contract PanopticPoolTest is PositionUtils { posIdList, positionSize, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); posIdList = new TokenId[](2); @@ -3552,8 +3614,8 @@ contract PanopticPoolTest is PositionUtils { posIdList, uint128(positionSize), 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -3594,8 +3656,8 @@ contract PanopticPoolTest is PositionUtils { posIdList, positionSize * 0, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -3649,8 +3711,8 @@ contract PanopticPoolTest is PositionUtils { posIdList, uint128(positionSize), 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -3686,8 +3748,8 @@ contract PanopticPoolTest is PositionUtils { tokenIds, positionSize, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); if (i < 32) { @@ -3740,10 +3802,10 @@ contract PanopticPoolTest is PositionUtils { posIdList, positionSize, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); - pp.burnOptions(tokenId, emptyList, Constants.MAX_V3POOL_TICK, Constants.MIN_V3POOL_TICK); + pp.burnOptions(tokenId, emptyList, Constants.MAX_V4POOL_TICK, Constants.MIN_V4POOL_TICK); assertEq(sfpm.balanceOf(address(pp), TokenId.unwrap(tokenId)), 0); @@ -3841,23 +3903,16 @@ contract PanopticPoolTest is PositionUtils { posIdList, positionSize, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } - // poke Uniswap pool to update tokens owed - needed because swap happens after mint - vm.startPrank(address(sfpm)); - pool.burn(tickLower, tickUpper, 0); + (uint128 tokensOwed0, ) = getTokensOwed(tickLower, tickUpper, 0); vm.startPrank(Alice); - // calculate additional fees owed to position - (, , , uint128 tokensOwed0, ) = pool.positions( - PositionKey.compute(address(sfpm), tickLower, tickUpper) - ); - // price changes afters swap at mint so we need to update the price - (currentSqrtPriceX96, , , , , , ) = pool.slot0(); + currentSqrtPriceX96 = V4StateReader.getSqrtPriceX96(manager, poolKey.toId()); amount0Moveds[1] = currentSqrtPriceX96 > sqrtUpper ? int256(0) @@ -3877,8 +3932,8 @@ contract PanopticPoolTest is PositionUtils { pp.burnOptions( tokenId, emptyList, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } assertEq(sfpm.balanceOf(address(pp), TokenId.unwrap(tokenId)), 0); @@ -3909,14 +3964,11 @@ contract PanopticPoolTest is PositionUtils { vm.revertTo(0); (uint256[2] memory expectedSwaps, ) = PositionUtils.simulateSwap( - pool, + poolKey, tickLower, tickUpper, expectedLiq, - router, - token0, - token1, - fee, + routerV4, [true, false], amount1Moveds ); @@ -3932,8 +3984,8 @@ contract PanopticPoolTest is PositionUtils { ]; int256 ITMSpread = notionalVals[0] > 0 - ? (notionalVals[0] * int24(2 * (fee / 100))) / 10_000 - : -((notionalVals[0] * int24(2 * (fee / 100))) / 10_000); + ? (notionalVals[0] * int24(20)) / 10_000 + : -((notionalVals[0] * int24(20)) / 10_000); assertApproxEqAbs( balanceBefores[0], @@ -4010,25 +4062,18 @@ contract PanopticPoolTest is PositionUtils { posIdList, positionSize, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } twoWaySwap(swapSizeSeed); - // poke Uniswap pool to update tokens owed - needed because swap happens after mint - vm.startPrank(address(sfpm)); - pool.burn(tickLower, tickUpper, 0); + (uint128 tokensOwed0, uint128 tokensOwed1) = getTokensOwed(tickLower, tickUpper, 0); vm.startPrank(Alice); - // calculate additional fees owed to position - (, , , uint128 tokensOwed0, uint128 tokensOwed1) = pool.positions( - PositionKey.compute(address(sfpm), tickLower, tickUpper) - ); - // price changes afters swap at mint so we need to update the price - (currentSqrtPriceX96, , , , , , ) = pool.slot0(); + currentSqrtPriceX96 = V4StateReader.getSqrtPriceX96(manager, poolKey.toId()); amount0Moveds[1] = currentSqrtPriceX96 > sqrtUpper ? int256(0) @@ -4047,8 +4092,8 @@ contract PanopticPoolTest is PositionUtils { pp.burnOptions( tokenId, emptyList, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } assertEq(sfpm.balanceOf(address(pp), TokenId.unwrap(tokenId)), 0); @@ -4079,14 +4124,11 @@ contract PanopticPoolTest is PositionUtils { vm.revertTo(0); (uint256[2] memory expectedSwaps, ) = PositionUtils.simulateSwap( - pool, + poolKey, tickLower, tickUpper, expectedLiq, - router, - token0, - token1, - fee, + routerV4, [true, false], amount1Moveds ); @@ -4102,8 +4144,13 @@ contract PanopticPoolTest is PositionUtils { ]; int256 ITMSpread = notionalVals[0] > 0 - ? (notionalVals[0] * int24(2 * (fee / 100))) / 10_000 - : -((notionalVals[0] * int24(2 * (fee / 100))) / 10_000); + ? (notionalVals[0] * int24(20)) / 10_000 + : -((notionalVals[0] * int24(20)) / 10_000); + + console2.log("balanceBefores[0]", balanceBefores[0]); + console2.log("balanceBefores[1]", balanceBefores[1]); + console2.log("expectedSwaps[0]", expectedSwaps[0]); + console2.log("expectedSwaps[1]", expectedSwaps[1]); assertApproxEqAbs( balanceBefores[0], @@ -4186,18 +4233,11 @@ contract PanopticPoolTest is PositionUtils { posIdList, positionSize, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); - // poke Uniswap pool to update tokens owed - needed because swap happens after mint - vm.startPrank(address(sfpm)); - pool.burn(tickLower, tickUpper, 0); - - // calculate additional fees owed to position - (, , , tokensOwed0, tokensOwed1) = pool.positions( - PositionKey.compute(address(sfpm), tickLower, tickUpper) - ); + (tokensOwed0, tokensOwed1) = getTokensOwed(tickLower, tickUpper, 0); tokensOwedTemp[0] = tokensOwed0; tokensOwedTemp[1] = tokensOwed1; @@ -4211,20 +4251,13 @@ contract PanopticPoolTest is PositionUtils { posIdList, (positionSize * uint128(bound(longPercentageSeed, 1, 899))) / 1000, type(uint64).max, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); twoWaySwap(swapSizeSeed); - // poke Uniswap pool to update tokens owed - needed because swap happens after mint - vm.startPrank(address(sfpm)); - pool.burn(tickLower, tickUpper, 0); - - // calculate additional fees owed to position - (, , , tokensOwed0, tokensOwed1) = pool.positions( - PositionKey.compute(address(sfpm), tickLower, tickUpper) - ); + (tokensOwed0, tokensOwed1) = getTokensOwed(tickLower, tickUpper, 0); tokensOwedTemp[0] += tokensOwed0; tokensOwedTemp[1] += tokensOwed1; @@ -4238,25 +4271,19 @@ contract PanopticPoolTest is PositionUtils { posIdList, (((positionSize * uint128(bound(longPercentageSeed, 1, 899))) / 1000) * 100) / 89, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); - // poke Uniswap pool to update tokens owed - needed because swap happens after mint - vm.startPrank(address(sfpm)); - pool.burn(tickLower, tickUpper, 0); - - // calculate additional fees owed to position - (, , , tokensOwed0, tokensOwed1) = pool.positions( - PositionKey.compute(address(sfpm), tickLower, tickUpper) - ); + (tokensOwed0, tokensOwed1) = getTokensOwed(tickLower, tickUpper, 0); + vm.startPrank(Seller); tokensOwed0 += tokensOwedTemp[0]; tokensOwed1 += tokensOwedTemp[1]; } // price changes afters swap at mint so we need to update the price - (currentSqrtPriceX96, , , , , , ) = pool.slot0(); + currentSqrtPriceX96 = V4StateReader.getSqrtPriceX96(manager, poolKey.toId()); amount0Moveds[1] = currentSqrtPriceX96 > sqrtUpper ? int256(0) @@ -4277,8 +4304,8 @@ contract PanopticPoolTest is PositionUtils { pp.burnOptions( tokenIds[0], emptyList, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -4288,14 +4315,11 @@ contract PanopticPoolTest is PositionUtils { vm.revertTo(0); (uint256[2] memory expectedSwaps, ) = PositionUtils.simulateSwap( - pool, + poolKey, tickLower, tickUpper, expectedLiq, - router, - token0, - token1, - fee, + routerV4, [true, false], amount1Moveds ); @@ -4311,8 +4335,8 @@ contract PanopticPoolTest is PositionUtils { ]; int256 ITMSpread = notionalVals[0] > 0 - ? (notionalVals[0] * int24(2 * (fee / 100))) / 10_000 - : -((notionalVals[0] * int24(2 * (fee / 100))) / 10_000); + ? (notionalVals[0] * int24(20)) / 10_000 + : -((notionalVals[0] * int24(20)) / 10_000); assertApproxEqAbs( int256(balanceBefores[0]) - int256(uint256(type(uint104).max)), @@ -4395,18 +4419,11 @@ contract PanopticPoolTest is PositionUtils { posIdList, positionSize, 0, - Constants.MIN_V3POOL_TICK, - Constants.MAX_V3POOL_TICK + Constants.MIN_V4POOL_TICK, + Constants.MAX_V4POOL_TICK ); - // poke Uniswap pool to update tokens owed - needed because swap happens after mint - vm.startPrank(address(sfpm)); - pool.burn(tickLower, tickUpper, 0); - - // calculate additional fees owed to position - (, , , tokensOwed0, tokensOwed1) = pool.positions( - PositionKey.compute(address(sfpm), tickLower, tickUpper) - ); + (tokensOwed0, tokensOwed1) = getTokensOwed(tickLower, tickUpper, 0); tokensOwedTemp[0] = tokensOwed0; tokensOwedTemp[1] = tokensOwed1; @@ -4420,20 +4437,13 @@ contract PanopticPoolTest is PositionUtils { posIdList, (positionSize * uint128(bound(longPercentageSeed, 1, 899))) / 1000, type(uint64).max, - Constants.MIN_V3POOL_TICK, - Constants.MAX_V3POOL_TICK + Constants.MIN_V4POOL_TICK, + Constants.MAX_V4POOL_TICK ); twoWaySwap(swapSizeSeed); - // poke Uniswap pool to update tokens owed - needed because swap happens after mint - vm.startPrank(address(sfpm)); - pool.burn(tickLower, tickUpper, 0); - - // calculate additional fees owed to position - (, , , tokensOwed0, tokensOwed1) = pool.positions( - PositionKey.compute(address(sfpm), tickLower, tickUpper) - ); + (tokensOwed0, tokensOwed1) = getTokensOwed(tickLower, tickUpper, 0); tokensOwedTemp[0] += tokensOwed0; tokensOwedTemp[1] += tokensOwed1; @@ -4447,25 +4457,19 @@ contract PanopticPoolTest is PositionUtils { posIdList, (((positionSize * uint128(bound(longPercentageSeed, 1, 899))) / 1000) * 100) / 89, 0, - Constants.MIN_V3POOL_TICK, - Constants.MAX_V3POOL_TICK + Constants.MIN_V4POOL_TICK, + Constants.MAX_V4POOL_TICK ); - // poke Uniswap pool to update tokens owed - needed because swap happens after mint - vm.startPrank(address(sfpm)); - pool.burn(tickLower, tickUpper, 0); - - // calculate additional fees owed to position - (, , , tokensOwed0, tokensOwed1) = pool.positions( - PositionKey.compute(address(sfpm), tickLower, tickUpper) - ); + (tokensOwed0, tokensOwed1) = getTokensOwed(tickLower, tickUpper, 0); + vm.startPrank(Bob); tokensOwed0 += tokensOwedTemp[0]; tokensOwed1 += tokensOwedTemp[1]; } // price changes afters swap at mint so we need to update the price - (currentSqrtPriceX96, , , , , , ) = pool.slot0(); + currentSqrtPriceX96 = V4StateReader.getSqrtPriceX96(manager, poolKey.toId()); amount0Moveds[1] = currentSqrtPriceX96 > sqrtUpper ? int256(0) @@ -4486,8 +4490,8 @@ contract PanopticPoolTest is PositionUtils { pp.burnOptions( tokenIds[0], emptyList, - Constants.MIN_V3POOL_TICK, - Constants.MAX_V3POOL_TICK + Constants.MIN_V4POOL_TICK, + Constants.MAX_V4POOL_TICK ); } @@ -4595,8 +4599,8 @@ contract PanopticPoolTest is PositionUtils { posIdList, positionSizes[0], 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -4609,15 +4613,15 @@ contract PanopticPoolTest is PositionUtils { posIdList, uint128(positionSizes[1]), 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); pp.burnOptions( posIdList, emptyList, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); (uint256 token0Balance, , ) = ph.optionPositionInfo(pp, Alice, tokenId); @@ -4702,8 +4706,8 @@ contract PanopticPoolTest is PositionUtils { $posIdLists[0], positionSize * 10, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -4727,8 +4731,8 @@ contract PanopticPoolTest is PositionUtils { $posIdLists[1], positionSize, type(uint64).max, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); if ($posIdLists[3].length < legsToBurn) { @@ -4747,15 +4751,15 @@ contract PanopticPoolTest is PositionUtils { pp.burnOptions( $posIdLists[3], $posIdLists[2], - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } else { pp.burnOptions( $posIdLists[3][0], $posIdLists[2], - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -4765,7 +4769,7 @@ contract PanopticPoolTest is PositionUtils { int256(lastCollateralBalance1[Alice]); (, , observationIndex, observationCardinality, , , ) = pool.slot0(); (int24 _fastOracleTick, ) = PanopticMath.computeMedianObservedPrice( - pool, + IV3CompatibleOracle(address(pool)), observationIndex, observationCardinality, 3, @@ -4778,7 +4782,8 @@ contract PanopticPoolTest is PositionUtils { $balanceDelta1 = balanceDelta1; } - (currentSqrtPriceX96, currentTick, , , , , ) = pool.slot0(); + currentSqrtPriceX96 = V4StateReader.getSqrtPriceX96(manager, poolKey.toId()); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); (, uint256 totalCollateralRequired0) = ph.checkCollateral( pp, @@ -4863,15 +4868,15 @@ contract PanopticPoolTest is PositionUtils { pp.burnOptions( $posIdLists[3], $posIdLists[2], - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } else { pp.burnOptions( $posIdLists[3][0], $posIdLists[2], - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } } @@ -4912,13 +4917,13 @@ contract PanopticPoolTest is PositionUtils { posIdList, positionSize, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); vm.expectRevert(Errors.InputListFail.selector); - pp.burnOptions(tokenId, posIdList, Constants.MAX_V3POOL_TICK, Constants.MIN_V3POOL_TICK); + pp.burnOptions(tokenId, posIdList, Constants.MAX_V4POOL_TICK, Constants.MIN_V4POOL_TICK); } function test_fail_burnOptions_burnAllOptionsFrom( @@ -5008,16 +5013,16 @@ contract PanopticPoolTest is PositionUtils { posIdList, positionSizes[0], 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); vm.expectRevert(Errors.InputListFail.selector); pp.burnOptions( tokenId, posIdList, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -5030,16 +5035,16 @@ contract PanopticPoolTest is PositionUtils { posIdList, uint128(positionSizes[1]), 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); vm.expectRevert(Errors.InputListFail.selector); pp.burnOptions( tokenId, emptyList, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } { @@ -5052,16 +5057,16 @@ contract PanopticPoolTest is PositionUtils { posIdList, uint128(positionSizes[0]), 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); vm.expectRevert(Errors.InputListFail.selector); pp.burnOptions( tokenId, posIdList, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -5076,8 +5081,8 @@ contract PanopticPoolTest is PositionUtils { posIdList, uint128(positionSizes[1]), 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); TokenId[] memory burnIdList = new TokenId[](2); @@ -5088,8 +5093,8 @@ contract PanopticPoolTest is PositionUtils { pp.burnOptions( burnIdList, posIdList, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); TokenId[] memory leftoverIdList = new TokenId[](2); @@ -5099,8 +5104,8 @@ contract PanopticPoolTest is PositionUtils { pp.burnOptions( burnIdList, leftoverIdList, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } } @@ -5163,7 +5168,7 @@ contract PanopticPoolTest is PositionUtils { uint256 exerciseableCount; // make sure position is exercisable - the uniswap twap is used to determine exercisability // so it could potentially be both OTM and non-exercisable (in-range) - TWAPtick = pp.getUniV3TWAP_(); + TWAPtick = pp.getOracleTWAP_(); for (uint256 i = 0; i < numLegs; ++i) { if ( (TWAPtick < (numLegs == 1 ? tickLower : tickLowers[i]) || @@ -5189,8 +5194,8 @@ contract PanopticPoolTest is PositionUtils { posIdList, positionSize * 2, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); // now we can mint the long option we are force exercising @@ -5222,8 +5227,8 @@ contract PanopticPoolTest is PositionUtils { posIdList, positionSize, type(uint64).max, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ) {} catch (bytes memory reason) { if (bytes4(reason) == Errors.TransferFailed.selector) { @@ -5239,7 +5244,8 @@ contract PanopticPoolTest is PositionUtils { oneWaySwap(swapSizeSeed, swapDirection); - (currentSqrtPriceX96, currentTick, , , , , ) = pool.slot0(); + currentSqrtPriceX96 = V4StateReader.getSqrtPriceX96(manager, poolKey.toId()); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); updatePositionDataVariable(numLegs, isLongs); @@ -5254,8 +5260,9 @@ contract PanopticPoolTest is PositionUtils { ); vm.startPrank(Bob); - (currentSqrtPriceX96, currentTick, observationIndex, observationCardinality, , , ) = pool - .slot0(); + currentSqrtPriceX96 = V4StateReader.getSqrtPriceX96(manager, poolKey.toId()); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); + (, , observationIndex, observationCardinality, , , ) = pool.slot0(); for (uint256 i = 0; i < numLegs; ++i) { if (isLongs[i] == 0) continue; @@ -5570,7 +5577,7 @@ contract PanopticPoolTest is PositionUtils { uint256 exerciseableCount; // make sure position is exercisable - the uniswap twap is used to determine exercisability // so it could potentially be both OTM and non-exercisable (in-range) - TWAPtick = pp.getUniV3TWAP_(); + TWAPtick = pp.getOracleTWAP_(); for (uint256 i = 0; i < numLegs; ++i) { if ( (TWAPtick < (numLegs == 1 ? tickLower : tickLowers[i]) || @@ -5600,8 +5607,8 @@ contract PanopticPoolTest is PositionUtils { $posIdLists[0], positionSize * 10, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -5640,8 +5647,8 @@ contract PanopticPoolTest is PositionUtils { $posIdLists[1], positionSize, type(uint64).max, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -5665,19 +5672,20 @@ contract PanopticPoolTest is PositionUtils { $balanceDelta1 = balanceDelta1; } - (currentSqrtPriceX96, currentTick, , , , , ) = pool.slot0(); + currentSqrtPriceX96 = V4StateReader.getSqrtPriceX96(manager, poolKey.toId()); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); (, uint256 totalCollateralRequired0) = ph.checkCollateral( pp, Bob, - pp.getUniV3TWAP_(), + pp.getOracleTWAP_(), $posIdLists[0] ); - if (pp.getUniV3TWAP_() > 0) + if (pp.getOracleTWAP_() > 0) totalCollateralRequired0 = PanopticMath.convert1to0( totalCollateralRequired0, - Math.getSqrtRatioAtTick(pp.getUniV3TWAP_()) + Math.getSqrtRatioAtTick(pp.getOracleTWAP_()) ); uint256 totalCollateralB0 = bound( @@ -5691,7 +5699,7 @@ contract PanopticPoolTest is PositionUtils { int256( PanopticMath.convert1to0( $balanceDelta1, - Math.getSqrtRatioAtTick(pp.getUniV3TWAP_()) + Math.getSqrtRatioAtTick(pp.getOracleTWAP_()) ) + $balanceDelta0 ) * 2 > @@ -5712,7 +5720,7 @@ contract PanopticPoolTest is PositionUtils { PanopticMath.convert0to1( (totalCollateralB0 * (10_000 - bound(collateralRatioSeed, 5_000, 6_000))) / 10_000, - Math.getSqrtRatioAtTick(pp.getUniV3TWAP_()) + Math.getSqrtRatioAtTick(pp.getOracleTWAP_()) ) ) ); @@ -5781,7 +5789,7 @@ contract PanopticPoolTest is PositionUtils { uint256 exerciseableCount; // make sure position is exercisable - the uniswap twap is used to determine exercisability // so it could potentially be both OTM and non-exercisable (in-range) - TWAPtick = pp.getUniV3TWAP_(); + TWAPtick = pp.getOracleTWAP_(); for (uint256 i = 0; i < numLegs; ++i) { if ( (TWAPtick < (numLegs == 1 ? tickLower : tickLowers[i]) || @@ -5811,8 +5819,8 @@ contract PanopticPoolTest is PositionUtils { $posIdLists[0], positionSize * 2, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -5851,8 +5859,8 @@ contract PanopticPoolTest is PositionUtils { $posIdLists[1], positionSize, type(uint64).max, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -5876,19 +5884,20 @@ contract PanopticPoolTest is PositionUtils { $balanceDelta1 = balanceDelta1; } - (currentSqrtPriceX96, currentTick, , , , , ) = pool.slot0(); + currentSqrtPriceX96 = V4StateReader.getSqrtPriceX96(manager, poolKey.toId()); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); (, uint256 totalCollateralRequired0) = ph.checkCollateral( pp, Alice, - pp.getUniV3TWAP_(), + pp.getOracleTWAP_(), $posIdLists[3] ); - if (pp.getUniV3TWAP_() > 0) + if (pp.getOracleTWAP_() > 0) totalCollateralRequired0 = PanopticMath.convert1to0( totalCollateralRequired0, - Math.getSqrtRatioAtTick(pp.getUniV3TWAP_()) + Math.getSqrtRatioAtTick(pp.getOracleTWAP_()) ); uint256 totalCollateralB0 = bound( @@ -5902,7 +5911,7 @@ contract PanopticPoolTest is PositionUtils { int256( PanopticMath.convert1to0( $balanceDelta1, - Math.getSqrtRatioAtTick(pp.getUniV3TWAP_()) + Math.getSqrtRatioAtTick(pp.getOracleTWAP_()) ) + $balanceDelta0 ) * 2 > @@ -5922,7 +5931,7 @@ contract PanopticPoolTest is PositionUtils { ct1.convertToShares( PanopticMath.convert0to1( (totalCollateralB0 * (10_000 - bound(collateralRatioSeed, 0, 10_000))) / 10_000, - Math.getSqrtRatioAtTick(pp.getUniV3TWAP_()) + Math.getSqrtRatioAtTick(pp.getOracleTWAP_()) ) ) ); @@ -5989,7 +5998,7 @@ contract PanopticPoolTest is PositionUtils { uint256 exerciseableCount; // make sure position is exercisable - the uniswap twap is used to determine exercisability // so it could potentially be both OTM and non-exercisable (in-range) - TWAPtick = pp.getUniV3TWAP_(); + TWAPtick = pp.getOracleTWAP_(); for (uint256 i = 0; i < numLegs; ++i) { if ( (TWAPtick < (numLegs == 1 ? tickLower : tickLowers[i]) || @@ -6019,8 +6028,8 @@ contract PanopticPoolTest is PositionUtils { $posIdLists[0], positionSize * 2, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -6059,8 +6068,8 @@ contract PanopticPoolTest is PositionUtils { $posIdLists[1], positionSize, type(uint64).max, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -6117,8 +6126,8 @@ contract PanopticPoolTest is PositionUtils { posIdList, positionSize, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); posIdList = new TokenId[](2); @@ -6141,8 +6150,8 @@ contract PanopticPoolTest is PositionUtils { posIdList, positionSize, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); vm.startPrank(Bob); @@ -6197,8 +6206,8 @@ contract PanopticPoolTest is PositionUtils { touchedIds, positionSize, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); vm.startPrank(Bob); @@ -6285,8 +6294,8 @@ contract PanopticPoolTest is PositionUtils { $posIdLists[0], positionSize * 2, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -6313,8 +6322,8 @@ contract PanopticPoolTest is PositionUtils { $posIdLists[1], positionSize, type(uint64).max, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -6324,21 +6333,22 @@ contract PanopticPoolTest is PositionUtils { vm.roll(block.number + 10); twoWaySwap(swapSizeSeed); } - (currentSqrtPriceX96, currentTick, , , , , ) = pool.slot0(); + currentSqrtPriceX96 = V4StateReader.getSqrtPriceX96(manager, poolKey.toId()); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); - vm.assume(Math.abs(int256(currentTick) - pp.getUniV3TWAP_()) <= 513); + vm.assume(Math.abs(int256(currentTick) - pp.getOracleTWAP_()) <= 513); (, uint256 totalCollateralRequired0) = ph.checkCollateral( pp, Alice, - pp.getUniV3TWAP_(), + pp.getOracleTWAP_(), $posIdLists[1] ); - if (pp.getUniV3TWAP_() > 0) + if (pp.getOracleTWAP_() > 0) totalCollateralRequired0 = PanopticMath.convert1to0( totalCollateralRequired0, - Math.getSqrtRatioAtTick(pp.getUniV3TWAP_()) + Math.getSqrtRatioAtTick(pp.getOracleTWAP_()) ); uint256 totalCollateralB0 = bound( @@ -6360,13 +6370,14 @@ contract PanopticPoolTest is PositionUtils { ct1.convertToShares( PanopticMath.convert0to1( (totalCollateralB0 * (10_000 - bound(collateralRatioSeed, 0, 10_000))) / 10_000, - Math.getSqrtRatioAtTick(pp.getUniV3TWAP_()) + Math.getSqrtRatioAtTick(pp.getOracleTWAP_()) ) ) ); - TWAPtick = pp.getUniV3TWAP_(); - (currentSqrtPriceX96, currentTick, , , , , ) = pool.slot0(); + TWAPtick = pp.getOracleTWAP_(); + currentSqrtPriceX96 = V4StateReader.getSqrtPriceX96(manager, poolKey.toId()); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); ($shortPremia, $longPremia, $positionBalanceArray) = pp.getAccumulatedFeesAndPositionsData( Alice, @@ -6417,8 +6428,8 @@ contract PanopticPoolTest is PositionUtils { (LeftRightSigned[4][] memory premiasByLeg, LeftRightSigned netExchanged) = pp .burnAllOptionsFrom( $posIdLists[1], - Constants.MIN_V3POOL_TICK, - Constants.MAX_V3POOL_TICK + Constants.MIN_V4POOL_TICK, + Constants.MAX_V4POOL_TICK ); shareDeltasLiquidatee = [ @@ -6426,7 +6437,7 @@ contract PanopticPoolTest is PositionUtils { int256(ct1.balanceOf(Alice)) - shareDeltasLiquidatee[1] ]; - (, currentTickFinal, , , , , ) = pool.slot0(); + currentTickFinal = V4StateReader.getTick(manager, poolKey.toId()); uint256[2][4][] memory settledTokensTemp = new uint256[2][4][]($posIdLists[1].length); for (uint256 i = 0; i < $posIdLists[1].length; ++i) { @@ -6547,12 +6558,13 @@ contract PanopticPoolTest is PositionUtils { (uint256 newBalance0, uint256 newRequired0) = ph.checkCollateral( pp, Alice, - pp.getUniV3TWAP_(), + pp.getOracleTWAP_(), $posIdLists[1] ); vm.assume(newBalance0 < newRequired0); } - (currentTick, fastOracleTick, , lastObservedTick, ) = pp.getOracleTicks(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); + (, fastOracleTick, , lastObservedTick, ) = pp.getOracleTicks(); { (uint256 newBalance0, uint256 newRequired0) = ph.checkCollateral( pp, @@ -6880,8 +6892,8 @@ contract PanopticPoolTest is PositionUtils { $posIdLists[0], positionSize * 2, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); twoWaySwap(swapSizeSeed); @@ -6906,8 +6918,8 @@ contract PanopticPoolTest is PositionUtils { $posIdLists[1], positionSize, type(uint64).max, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); twoWaySwap(swapSizeSeed); @@ -6917,21 +6929,22 @@ contract PanopticPoolTest is PositionUtils { twoWaySwap(1e4); } - (currentSqrtPriceX96, currentTick, , , , , ) = pool.slot0(); + currentSqrtPriceX96 = V4StateReader.getSqrtPriceX96(manager, poolKey.toId()); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); - vm.assume(Math.abs(int256(currentTick) - pp.getUniV3TWAP_()) <= 513); + vm.assume(Math.abs(int256(currentTick) - pp.getOracleTWAP_()) <= 513); (, uint256 totalCollateralRequired0) = ph.checkCollateral( pp, Alice, - pp.getUniV3TWAP_(), + pp.getOracleTWAP_(), $posIdLists[1] ); - if (pp.getUniV3TWAP_() > 0) + if (pp.getOracleTWAP_() > 0) totalCollateralRequired0 = PanopticMath.convert1to0( totalCollateralRequired0, - Math.getSqrtRatioAtTick(pp.getUniV3TWAP_()) + Math.getSqrtRatioAtTick(pp.getOracleTWAP_()) ); uint256 totalCollateralB0 = bound( @@ -6953,13 +6966,14 @@ contract PanopticPoolTest is PositionUtils { ct1.convertToShares( PanopticMath.convert0to1( (totalCollateralB0 * (10_000 - bound(collateralRatioSeed, 0, 10_000))) / 10_000, - Math.getSqrtRatioAtTick(pp.getUniV3TWAP_()) + Math.getSqrtRatioAtTick(pp.getOracleTWAP_()) ) ) ); - TWAPtick = pp.getUniV3TWAP_(); - (currentSqrtPriceX96, currentTick, , , , , ) = pool.slot0(); + TWAPtick = pp.getOracleTWAP_(); + currentSqrtPriceX96 = V4StateReader.getSqrtPriceX96(manager, poolKey.toId()); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); ($shortPremia, $longPremia, $positionBalanceArray) = pp.getAccumulatedFeesAndPositionsData( Alice, @@ -7008,8 +7022,8 @@ contract PanopticPoolTest is PositionUtils { (LeftRightSigned[4][] memory premiasByLeg, LeftRightSigned netExchanged) = pp .burnAllOptionsFrom( $posIdLists[1], - Constants.MIN_V3POOL_TICK, - Constants.MAX_V3POOL_TICK + Constants.MIN_V4POOL_TICK, + Constants.MAX_V4POOL_TICK ); shareDeltasLiquidatee = [ @@ -7017,7 +7031,7 @@ contract PanopticPoolTest is PositionUtils { int256(ct1.balanceOf(Alice)) - shareDeltasLiquidatee[1] ]; - (, currentTickFinal, , , , , ) = pool.slot0(); + currentTickFinal = V4StateReader.getTick(manager, poolKey.toId()); uint256[2][4][] memory settledTokensTemp = new uint256[2][4][]($posIdLists[1].length); for (uint256 i = 0; i < $posIdLists[1].length; ++i) { @@ -7122,12 +7136,13 @@ contract PanopticPoolTest is PositionUtils { (uint256 newBalance0, uint256 newRequired0) = ph.checkCollateral( pp, Alice, - pp.getUniV3TWAP_(), + pp.getOracleTWAP_(), $posIdLists[1] ); vm.assume(newBalance0 < newRequired0); } - (currentTick, fastOracleTick, , lastObservedTick, ) = pp.getOracleTicks(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); + (, fastOracleTick, , lastObservedTick, ) = pp.getOracleTicks(); { (uint256 newBalance0, uint256 newRequired0) = ph.checkCollateral( pp, @@ -7429,8 +7444,8 @@ contract PanopticPoolTest is PositionUtils { posIdList, positionSize, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); posIdList = new TokenId[](2); @@ -7453,8 +7468,8 @@ contract PanopticPoolTest is PositionUtils { posIdList, positionSize, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); vm.startPrank(Bob); @@ -7503,8 +7518,8 @@ contract PanopticPoolTest is PositionUtils { posIdList, positionSize, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); vm.startPrank(Bob); @@ -7513,8 +7528,8 @@ contract PanopticPoolTest is PositionUtils { posIdList, positionSize, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); editCollateral(ct0, Alice, 2); @@ -7561,8 +7576,8 @@ contract PanopticPoolTest is PositionUtils { posIdList, positionSize, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); vm.startPrank(Bob); @@ -7571,8 +7586,8 @@ contract PanopticPoolTest is PositionUtils { posIdList, positionSize, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); editCollateral(ct0, Alice, 2); @@ -7585,26 +7600,23 @@ contract PanopticPoolTest is PositionUtils { pp.liquidate(posIdList, Alice, posIdList); } - function test_Fail_liquidate_StaleTWAP(uint256 x, int256 tickDeltaSeed) public { + function test_Fail_liquidate_StaleTWAP(uint256 x, uint256 sqrtPriceTarget) public { _initPool(x); - int256 tickDelta = int256( - bound( - tickDeltaSeed, - -(int256(currentTick) - int256(Constants.MIN_V3POOL_TICK)), - int256(Constants.MAX_V3POOL_TICK) - int256(currentTick) - ) + + sqrtPriceTarget = bound( + sqrtPriceTarget, + Math.getSqrtRatioAtTick(-800_000), + Math.getSqrtRatioAtTick(800_000) ); - vm.assume(Math.abs((int256(currentTick) + tickDelta) - pp.getUniV3TWAP_()) > 953); - vm.store( - address(pool), - bytes32(0), - bytes32( - (uint256(vm.load(address(pool), bytes32(0))) & - 0xffffffffffffffffff000000ffffffffffffffffffffffffffffffffffffffff) + - (uint256(uint24(int24(int256(currentTick) + int256(tickDelta)))) << 160) - ) + vm.assume( + Math.abs( + int256(TickMath.getTickAtSqrtRatio(uint160(sqrtPriceTarget))) - pp.getOracleTWAP_() + ) > 953 ); + vm.startPrank(Swapper); + routerV4.swapTo(address(0), poolKey, uint160(sqrtPriceTarget)); + vm.expectRevert(Errors.StaleTWAP.selector); pp.liquidate(new TokenId[](0), Alice, $posIdList); } @@ -7683,8 +7695,8 @@ contract PanopticPoolTest is PositionUtils { $posIdLists[0], positionSize * 2, 0, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -7711,8 +7723,8 @@ contract PanopticPoolTest is PositionUtils { $posIdLists[1], positionSize, type(uint64).max, - Constants.MAX_V3POOL_TICK, - Constants.MIN_V3POOL_TICK + Constants.MAX_V4POOL_TICK, + Constants.MIN_V4POOL_TICK ); } @@ -7724,21 +7736,22 @@ contract PanopticPoolTest is PositionUtils { twoWaySwap(1e4); } - (currentSqrtPriceX96, currentTick, , , , , ) = pool.slot0(); + currentSqrtPriceX96 = V4StateReader.getSqrtPriceX96(manager, poolKey.toId()); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); - vm.assume(Math.abs(int256(currentTick) - pp.getUniV3TWAP_()) <= 513); + vm.assume(Math.abs(int256(currentTick) - pp.getOracleTWAP_()) <= 513); (, uint256 twapCollateralRequired0) = ph.checkCollateral( pp, Alice, - pp.getUniV3TWAP_(), + pp.getOracleTWAP_(), $posIdLists[1] ); - if (pp.getUniV3TWAP_() > 0) + if (pp.getOracleTWAP_() > 0) twapCollateralRequired0 = PanopticMath.convert1to0( twapCollateralRequired0, - Math.getSqrtRatioAtTick(pp.getUniV3TWAP_()) + Math.getSqrtRatioAtTick(pp.getOracleTWAP_()) ); (, uint256 currentCollateralRequired0) = ph.checkCollateral( @@ -7788,7 +7801,7 @@ contract PanopticPoolTest is PositionUtils { (totalCollateralB0 * (10_000 - bound(collateralRatioSeed, 0, 10_000))) / 10_000, Math.getSqrtRatioAtTick( twapCollateralRequired0 > currentCollateralRequired0 - ? pp.getUniV3TWAP_() + ? pp.getOracleTWAP_() : currentTick ) ) @@ -7799,12 +7812,13 @@ contract PanopticPoolTest is PositionUtils { (uint256 newBalance0, uint256 newRequired0) = ph.checkCollateral( pp, Alice, - pp.getUniV3TWAP_(), + pp.getOracleTWAP_(), $posIdLists[1] ); vm.assume(newBalance0 > newRequired0); } - (currentTick, fastOracleTick, , lastObservedTick, ) = pp.getOracleTicks(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); + (, fastOracleTick, , lastObservedTick, ) = pp.getOracleTicks(); { (uint256 newBalance0, uint256 newRequired0) = ph.checkCollateral( pp, @@ -7834,7 +7848,7 @@ contract PanopticPoolTest is PositionUtils { } vm.startPrank(Bob); - vm.assume(Math.abs(int256(currentTick) - pp.getUniV3TWAP_()) < 953); + vm.assume(Math.abs(int256(currentTick) - pp.getOracleTWAP_()) < 953); try pp.liquidate(new TokenId[](0), Alice, $posIdLists[1]) { assertFalse(true, "liquidation should have failed"); diff --git a/test/foundry/core/SemiFungiblePositionManager.t.sol b/test/foundry/core/SemiFungiblePositionManager.t.sol index e03046b..7f71e53 100644 --- a/test/foundry/core/SemiFungiblePositionManager.t.sol +++ b/test/foundry/core/SemiFungiblePositionManager.t.sol @@ -6,7 +6,7 @@ import {stdMath} from "forge-std/StdMath.sol"; import {Errors} from "@libraries/Errors.sol"; import {Math} from "@libraries/Math.sol"; import {PanopticMath} from "@libraries/PanopticMath.sol"; -import {CallbackLib} from "@libraries/CallbackLib.sol"; + import {TokenId} from "@types/TokenId.sol"; import {LeftRightUnsigned, LeftRightSigned} from "@types/LeftRight.sol"; import {IERC20Partial} from "@tokens/interfaces/IERC20Partial.sol"; @@ -25,17 +25,24 @@ import {PanopticHelper} from "@test_periphery/PanopticHelper.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {PositionUtils} from "../testUtils/PositionUtils.sol"; import {UniPoolPriceMock} from "../testUtils/PriceMocks.sol"; -import {ReenterMint, ReenterBurn, Reenter1155Initialize, ReenterTransferSingle, ReenterTransferBatch} from "../testUtils/ReentrancyMocks.sol"; +import {ReenterMint, ReenterBurn, Reenter1155Initialize} from "../testUtils/ReentrancyMocks.sol"; +// V4 types +import {PoolId} from "v4-core/types/PoolId.sol"; +import {PoolKey} from "v4-core/types/PoolKey.sol"; +import {StateLibrary} from "v4-core/libraries/StateLibrary.sol"; +import {V4StateReader} from "@libraries/V4StateReader.sol"; +import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol"; +import {BalanceDelta} from "v4-core/types/BalanceDelta.sol"; +import {Currency} from "v4-core/types/Currency.sol"; +import {PoolManager} from "v4-core/PoolManager.sol"; +import {IHooks} from "v4-core/interfaces/IHooks.sol"; +import {V4RouterSimple} from "../testUtils/V4RouterSimple.sol"; contract SemiFungiblePositionManagerHarness is SemiFungiblePositionManager { - constructor(IUniswapV3Factory _factory) SemiFungiblePositionManager(_factory) {} - - function poolIdToAddr(uint64 poolId) public view returns (IUniswapV3Pool) { - return s_poolIdToAddr[poolId]; - } + constructor(IPoolManager _manager) SemiFungiblePositionManager(_manager) {} - function addrToPoolId(address pool) public view returns (uint256) { - return s_AddrToPoolIdData[pool]; + function getPoolIdWithInitBit(PoolId poolId) external view returns (uint256) { + return s_V4toSFPMIdData[poolId]; } } @@ -55,14 +62,10 @@ contract SemiFungiblePositionManagerTest is PositionUtils { /*////////////////////////////////////////////////////////////// MAINNET CONTRACTS //////////////////////////////////////////////////////////////*/ - - // Mainnet factory address - SFPM is dependent on this for several checks and callbacks - IUniswapV3Factory V3FACTORY = IUniswapV3Factory(0x1F98431c8aD98523631AE4a59f267346ea31F984); - // Mainnet router address - used for swaps to test fees/premia ISwapRouter router = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); - address WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; // used as example of price parity IUniswapV3Pool constant USDC_USDT_5 = @@ -75,7 +78,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { IUniswapV3Pool(0xCBCdF9626bC03E24f779434178A73a0B4bad62eD); IUniswapV3Pool constant USDC_WETH_30 = IUniswapV3Pool(0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8); - IUniswapV3Pool[3] public pools = [USDC_WETH_5, USDC_WETH_5, USDC_WETH_30]; + IUniswapV3Pool[2] public pools = [USDC_WETH_5, USDC_WETH_30]; /*////////////////////////////////////////////////////////////// WORLD STATE @@ -171,6 +174,12 @@ contract SemiFungiblePositionManagerTest is PositionUtils { int256[] $amount0MovedsBurn; int256[] $amount1MovedsBurn; + IPoolManager manager; + + V4RouterSimple routerV4; + + PoolKey poolKey; + /*////////////////////////////////////////////////////////////// ENV SETUP //////////////////////////////////////////////////////////////*/ @@ -178,7 +187,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { /// @notice Intialize testing pool in the SFPM instance after world state is setup function _initPool(uint256 seed) internal { _initWorld(seed); - sfpm.initializeAMMPool(token0, token1, fee); + sfpm.initializeAMMPool(poolKey); } /// @notice Set up world state with data from a random pool off the list and fund+approve actors @@ -186,42 +195,62 @@ contract SemiFungiblePositionManagerTest is PositionUtils { // Pick a pool from the seed and cache initial state _cacheWorldState(pools[bound(seed, 0, pools.length - 1)]); + _initAccounts(); + } + + function _initAccounts() internal { // Fund some of the the generic actor accounts vm.startPrank(Bob); deal(token0, Bob, type(uint128).max); deal(token1, Bob, type(uint128).max); - IERC20Partial(token0).approve(address(sfpm), type(uint256).max); - IERC20Partial(token1).approve(address(sfpm), type(uint256).max); + IERC20Partial(token0).approve(address(routerV4), type(uint256).max); + IERC20Partial(token1).approve(address(routerV4), type(uint256).max); - IERC20Partial(token0).approve(address(router), type(uint256).max); - IERC20Partial(token1).approve(address(router), type(uint256).max); + routerV4.mintCurrency(address(0), poolKey.currency0, uint128(type(int128).max)); + routerV4.mintCurrency(address(0), poolKey.currency1, uint128(type(int128).max)); + + manager.setOperator(address(sfpm), true); vm.startPrank(Swapper); + deal(token0, Swapper, type(uint248).max); + deal(token1, Swapper, type(uint248).max); + IERC20Partial(token0).approve(address(router), type(uint256).max); IERC20Partial(token1).approve(address(router), type(uint256).max); - deal(token0, Swapper, type(uint128).max); - deal(token1, Swapper, type(uint128).max); + IERC20Partial(token0).approve(address(routerV4), type(uint256).max); + IERC20Partial(token1).approve(address(routerV4), type(uint256).max); + + manager.initialize(poolKey, currentSqrtPriceX96); + + routerV4.modifyLiquidity( + address(0), + poolKey, + (TickMath.MIN_TICK / tickSpacing) * tickSpacing, + (TickMath.MAX_TICK / tickSpacing) * tickSpacing, + 1_000_000 ether + ); vm.startPrank(Alice); deal(token0, Alice, type(uint128).max); deal(token1, Alice, type(uint128).max); - IERC20Partial(token0).approve(address(sfpm), type(uint256).max); - IERC20Partial(token1).approve(address(sfpm), type(uint256).max); + IERC20Partial(token0).approve(address(routerV4), type(uint256).max); + IERC20Partial(token1).approve(address(routerV4), type(uint256).max); - IERC20Partial(token0).approve(address(router), type(uint256).max); - IERC20Partial(token1).approve(address(router), type(uint256).max); + routerV4.mintCurrency(address(0), poolKey.currency0, uint128(type(int128).max)); + routerV4.mintCurrency(address(0), poolKey.currency1, uint128(type(int128).max)); + + manager.setOperator(address(sfpm), true); } /// @notice Populate world state with data from a given pool function _cacheWorldState(IUniswapV3Pool _pool) internal { pool = _pool; - poolId = PanopticMath.getPoolId(address(_pool)); token0 = _pool.token0(); token1 = _pool.token1(); isWETH = token0 == address(WETH) ? 0 : 1; @@ -230,10 +259,22 @@ contract SemiFungiblePositionManagerTest is PositionUtils { (currentSqrtPriceX96, currentTick, , , , , ) = _pool.slot0(); feeGrowthGlobal0X128 = _pool.feeGrowthGlobal0X128(); feeGrowthGlobal1X128 = _pool.feeGrowthGlobal1X128(); + + poolKey = PoolKey( + Currency.wrap(token0), + Currency.wrap(token1), + fee, + tickSpacing, + IHooks(address(0)) + ); + poolId = PanopticMath.getPoolId(poolKey.toId(), poolKey.tickSpacing); } function setUp() public { - sfpm = new SemiFungiblePositionManagerHarness(V3FACTORY); + manager = new PoolManager(); + routerV4 = new V4RouterSimple(manager); + + sfpm = new SemiFungiblePositionManagerHarness(manager); } /*////////////////////////////////////////////////////////////// @@ -827,15 +868,13 @@ contract SemiFungiblePositionManagerTest is PositionUtils { } vm.startPrank(address(sfpm)); - ($swap0, $swap1) = PositionUtils.simulateSwap( - pool, + ($swap0, $swap1) = this.simulateSwap( + poolKey, tickLower, tickUpper, liquidity, - router, - token0, - token1, - fee, + keccak256(abi.encodePacked(poolKey.toId(), Alice, tokenType, tickLower, tickUpper)), + routerV4, zeroForOne, swapAmount ); @@ -853,34 +892,82 @@ contract SemiFungiblePositionManagerTest is PositionUtils { function twoWaySwap(uint256 swapSize) public { vm.startPrank(Swapper); + uint160 originalSqrtPriceX96 = V4StateReader.getSqrtPriceX96(manager, poolKey.toId()); swapSize = bound(swapSize, 10 ** 18, 10 ** 20); - router.exactInputSingle( - ISwapRouter.ExactInputSingleParams( - isWETH == 0 ? token0 : token1, - isWETH == 1 ? token0 : token1, - fee, - Bob, - block.timestamp, - swapSize, - 0, - 0 - ) - ); + for (uint256 i = 0; i < 10; ++i) { + router.exactInputSingle( + ISwapRouter.ExactInputSingleParams( + isWETH == 0 ? token0 : token1, + isWETH == 1 ? token0 : token1, + fee, + Bob, + block.timestamp, + swapSize, + 0, + 0 + ) + ); - router.exactOutputSingle( - ISwapRouter.ExactOutputSingleParams( - isWETH == 1 ? token0 : token1, - isWETH == 0 ? token0 : token1, - fee, - Bob, - block.timestamp, - (swapSize * (1_000_000 - fee)) / 1_000_000, - type(uint256).max, - 0 - ) + (uint160 swappedSqrtPriceX96, , , , , , ) = pool.slot0(); + + routerV4.swapTo(address(0), poolKey, swappedSqrtPriceX96); + + router.exactOutputSingle( + ISwapRouter.ExactOutputSingleParams( + isWETH == 1 ? token0 : token1, + isWETH == 0 ? token0 : token1, + fee, + Bob, + block.timestamp, + (swapSize * (1_000_000 - fee)) / 1_000_000, + type(uint256).max, + 0 + ) + ); + + (swappedSqrtPriceX96, , , , , , ) = pool.slot0(); + + routerV4.swapTo(address(0), poolKey, swappedSqrtPriceX96); + } + + routerV4.swapTo(address(0), poolKey, originalSqrtPriceX96); + + currentSqrtPriceX96 = V4StateReader.getSqrtPriceX96(manager, poolKey.toId()); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); + } + + function getTokensOwed( + address _owner, + int24 _tickLower, + int24 _tickUpper, + uint256 _tokenType + ) internal view returns (uint128 _tokensOwed0, uint128 _tokensOwed1) { + (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = StateLibrary + .getFeeGrowthInside(manager, poolKey.toId(), _tickLower, _tickUpper); + + bytes32 positionKey = keccak256( + abi.encodePacked(poolKey.toId(), _owner, _tokenType, _tickLower, _tickUpper) ); - (currentSqrtPriceX96, currentTick, , , , , ) = pool.slot0(); + ( + uint128 _liquidity, + uint256 feeGrowthInside0LastX128, + uint256 feeGrowthInside1LastX128 + ) = StateLibrary.getPositionInfo( + manager, + poolKey.toId(), + address(sfpm), + _tickLower, + _tickUpper, + positionKey + ); + + _tokensOwed0 = uint128( + Math.mulDiv128(feeGrowthInside0X128 - feeGrowthInside0LastX128, _liquidity) + ); + _tokensOwed1 = uint128( + Math.mulDiv128(feeGrowthInside1X128 - feeGrowthInside1LastX128, _liquidity) + ); } /*////////////////////////////////////////////////////////////// @@ -891,12 +978,15 @@ contract SemiFungiblePositionManagerTest is PositionUtils { _initPool(x); // Check that the pool address is set correctly - assertEq(address(sfpm.poolIdToAddr(PanopticMath.getPoolId(address(pool)))), address(pool)); + assertEq( + PoolId.unwrap(sfpm.getUniswapV4PoolKeyFromId(poolId).toId()), + PoolId.unwrap(poolKey.toId()) + ); // Check that the pool ID is set correctly assertEq( - sfpm.addrToPoolId(address(pool)), - PanopticMath.getPoolId(address(pool)) + 2 ** 255 + sfpm.getPoolIdWithInitBit(poolKey.toId()), + PanopticMath.getPoolId(poolKey.toId(), poolKey.tickSpacing) + 2 ** 255 ); } @@ -904,83 +994,34 @@ contract SemiFungiblePositionManagerTest is PositionUtils { // Loop through all pools and test for (uint256 i = 0; i < pools.length; i++) { _cacheWorldState(pools[i]); - sfpm.initializeAMMPool(token0, token1, fee); + + manager.initialize(poolKey, currentSqrtPriceX96); + + sfpm.initializeAMMPool(poolKey); // Check that the pool address is set correctly assertEq( - address(sfpm.poolIdToAddr(PanopticMath.getPoolId(address(pool)))), - address(pool) + PoolId.unwrap(sfpm.getUniswapV4PoolKeyFromId(poolId).toId()), + PoolId.unwrap(poolKey.toId()) ); // Check that the pool ID is set correctly assertEq( - sfpm.addrToPoolId(address(pool)), - PanopticMath.getPoolId(address(pool)) + 2 ** 255 + sfpm.getPoolIdWithInitBit(poolKey.toId()), + PanopticMath.getPoolId(poolKey.toId(), poolKey.tickSpacing) + 2 ** 255 ); } } - function test_Success_initializeAMMPool_HandleCollisions() public { - // Create a mock factory that generates colliding pool addresses - UniswapV3FactoryMock factoryMock = new UniswapV3FactoryMock(); - - // Create an instance of the SFPM tied to the factory mock that generates colliding pool addresses - SemiFungiblePositionManagerHarness sfpm_t = new SemiFungiblePositionManagerHarness( - IUniswapV3Factory(address(factoryMock)) - ); - - UniPoolPriceMock pm = new UniPoolPriceMock(); - uint64 poolIdNew = (200 << 48); - for (uint160 i = 0; i < 100; i++) { - factoryMock.increment(); - - vm.etch(address((i + 1) << 24), address(pm).code); - - pm = UniPoolPriceMock(address((i + 1) << 24)); - pm.construct( - UniPoolPriceMock.Slot0({ - sqrtPriceX96: 0, - tick: 0, - observationIndex: 0, - observationCardinality: 0, - observationCardinalityNext: 0, - feeProtocol: 0, - unlocked: false - }), - address(0), - address(0), - 0, - 200 - ); - - // etch tickSpacing - // These values are zero at this point, but they are ignored by the factory mock - sfpm_t.initializeAMMPool(token0, token1, fee); - - if (i != 0) { - poolIdNew = PanopticMath.incrementPoolPattern(poolIdNew); - } - - // Check that the pool address is set correctly - assertEq(address(sfpm_t.poolIdToAddr(poolIdNew)), address((i + 1) << 24)); - - // Check that the pool ID is set correctly - // Addresses output from the factory mock start at 1 to avoid errors so we need to add that to the address - assertEq(sfpm_t.addrToPoolId(address((i + 1) << 24)), 2 ** 255 + poolIdNew); - - token0 = address(uint160(token0) + 1); - } - } - /*////////////////////////////////////////////////////////////// POOL INITIALIZATION: - //////////////////////////////////////////////////////////////*/ function test_Fail_initializeAMMPool_uniswapPoolNotInitialized() public { + _cacheWorldState(pools[0]); vm.expectRevert(Errors.UniswapPoolNotInitialized.selector); - // These values are zero at this point; thus there is no corresponding uni pool and we should revert - sfpm.initializeAMMPool(token0, token1, fee); + sfpm.initializeAMMPool(poolKey); } /// NOTE - the definitions of "call" and "put" can vary by Uniswap pair and which token is considered the asset @@ -1022,6 +1063,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { (LeftRightUnsigned[4] memory collectedByLeg, LeftRightSigned totalSwapped) = sfpm .mintTokenizedPosition( + poolKey, tokenId, uint128(positionSize), TickMath.MIN_TICK, @@ -1042,7 +1084,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { assertEq(sfpm.balanceOf(Alice, TokenId.unwrap(tokenId)), positionSize); accountLiquidities = sfpm.getAccountLiquidity( - address(pool), + poolKey.toId(), Alice, 0, tickLower, @@ -1052,8 +1094,13 @@ contract SemiFungiblePositionManagerTest is PositionUtils { assertEq(accountLiquidities.leftSlot(), 0); assertEq(accountLiquidities.rightSlot(), expectedLiq); - (uint256 realLiq, , , , ) = pool.positions( - keccak256(abi.encodePacked(address(sfpm), tickLower, tickUpper)) + (uint256 realLiq, , ) = StateLibrary.getPositionInfo( + manager, + poolKey.toId(), + address(sfpm), + tickLower, + tickUpper, + keccak256(abi.encodePacked(poolKey.toId(), Alice, uint256(0), tickLower, tickUpper)) ); assertEq(realLiq, expectedLiq); @@ -1090,6 +1137,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { (LeftRightUnsigned[4] memory collectedByLeg, LeftRightSigned totalSwapped) = sfpm .mintTokenizedPosition( + poolKey, tokenId, uint128(positionSize), TickMath.MIN_TICK, @@ -1110,7 +1158,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { assertEq(sfpm.balanceOf(Alice, TokenId.unwrap(tokenId)), positionSize); accountLiquidities = sfpm.getAccountLiquidity( - address(pool), + poolKey.toId(), Alice, 1, tickLower, @@ -1120,8 +1168,13 @@ contract SemiFungiblePositionManagerTest is PositionUtils { assertEq(accountLiquidities.leftSlot(), 0); assertEq(accountLiquidities.rightSlot(), expectedLiq); - (uint256 realLiq, , , , ) = pool.positions( - keccak256(abi.encodePacked(address(sfpm), tickLower, tickUpper)) + (uint256 realLiq, , ) = StateLibrary.getPositionInfo( + manager, + poolKey.toId(), + address(sfpm), + tickLower, + tickUpper, + keccak256(abi.encodePacked(poolKey.toId(), Alice, uint256(1), tickLower, tickUpper)) ); assertEq(realLiq, expectedLiq); @@ -1219,6 +1272,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { (LeftRightUnsigned[4] memory collectedByLeg, LeftRightSigned totalSwapped) = sfpm .mintTokenizedPosition( + poolKey, tokenId, uint128(positionSize), TickMath.MIN_TICK, @@ -1227,6 +1281,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { (LeftRightUnsigned[4] memory collectedByLegLong, LeftRightSigned totalSwappedLong) = sfpm .mintTokenizedPosition( + poolKey, longTokenId, uint128(positionSize), TickMath.MIN_TICK, @@ -1290,7 +1345,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { assertEq(sfpm.balanceOf(Alice, TokenId.unwrap(longTokenId)), positionSize); accountLiquidities = sfpm.getAccountLiquidity( - address(pool), + poolKey.toId(), Alice, 0, tickLower, @@ -1300,15 +1355,23 @@ contract SemiFungiblePositionManagerTest is PositionUtils { assertEq(accountLiquidities.leftSlot(), removedLiq); assertApproxEqAbs(accountLiquidities.rightSlot(), expectedLiq - removedLiq, 10); - (uint256 realLiq, , , uint256 tokensOwed0, uint256 tokensOwed1) = pool.positions( - keccak256(abi.encodePacked(address(sfpm), tickLower, tickUpper)) + (uint256 realLiq, , ) = StateLibrary.getPositionInfo( + manager, + poolKey.toId(), + address(sfpm), + tickLower, + tickUpper, + keccak256(abi.encodePacked(poolKey.toId(), Alice, uint256(0), tickLower, tickUpper)) ); + + (uint256 tokensOwed0, uint256 tokensOwed1) = getTokensOwed(Alice, tickLower, tickUpper, 0); + assertApproxEqAbs(realLiq, expectedLiq - removedLiq, 10); assertEq(tokensOwed0, 0); assertEq(tokensOwed1, 0); - assertEq(IERC20Partial(token0).balanceOf(address(sfpm)), 0); - assertEq(IERC20Partial(token1).balanceOf(address(sfpm)), 0); + assertEq(manager.balanceOf(address(sfpm), uint160(Currency.unwrap(poolKey.currency0))), 0); + assertEq(manager.balanceOf(address(sfpm), uint160(Currency.unwrap(poolKey.currency1))), 0); } /*////////////////////////////////////////////////////////////// @@ -1346,6 +1409,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { (LeftRightUnsigned[4] memory collectedByLeg, LeftRightSigned totalSwapped) = sfpm .mintTokenizedPosition( + poolKey, tokenId, uint128(positionSize), TickMath.MIN_TICK, @@ -1366,7 +1430,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { assertEq(sfpm.balanceOf(Alice, TokenId.unwrap(tokenId)), positionSize); accountLiquidities = sfpm.getAccountLiquidity( - address(pool), + poolKey.toId(), Alice, 1, tickLower, @@ -1376,8 +1440,13 @@ contract SemiFungiblePositionManagerTest is PositionUtils { assertEq(accountLiquidities.leftSlot(), 0); assertEq(accountLiquidities.rightSlot(), expectedLiq); - (uint256 realLiq, , , , ) = pool.positions( - keccak256(abi.encodePacked(address(sfpm), tickLower, tickUpper)) + (uint256 realLiq, , ) = StateLibrary.getPositionInfo( + manager, + poolKey.toId(), + address(sfpm), + tickLower, + tickUpper, + keccak256(abi.encodePacked(poolKey.toId(), Alice, uint256(1), tickLower, tickUpper)) ); assertEq(realLiq, expectedLiq); @@ -1425,14 +1494,11 @@ contract SemiFungiblePositionManagerTest is PositionUtils { ); (, uint256 amount1) = PositionUtils.simulateSwap( - pool, + poolKey, tickLower, tickUpper, expectedLiq, - router, - token0, - token1, - fee, + routerV4, false, -amount0Required ); @@ -1443,6 +1509,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { // We also invert the order; this is how we tell SFPM to trigger a swap (LeftRightUnsigned[4] memory collectedByLeg, LeftRightSigned totalSwapped) = sfpm .mintTokenizedPosition( + poolKey, tokenId, positionSize, TickMath.MAX_TICK - 1, @@ -1464,7 +1531,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { { accountLiquidities = sfpm.getAccountLiquidity( - address(pool), + poolKey.toId(), Alice, 1, tickLower, @@ -1474,15 +1541,23 @@ contract SemiFungiblePositionManagerTest is PositionUtils { assertEq(accountLiquidities.leftSlot(), 0); assertEq(accountLiquidities.rightSlot(), expectedLiq); - (uint256 realLiq, , , , ) = pool.positions( - keccak256(abi.encodePacked(address(sfpm), tickLower, tickUpper)) + (uint256 realLiq, , ) = StateLibrary.getPositionInfo( + manager, + poolKey.toId(), + address(sfpm), + tickLower, + tickUpper, + keccak256(abi.encodePacked(poolKey.toId(), Alice, uint256(1), tickLower, tickUpper)) ); assertEq(realLiq, expectedLiq); } { - assertEq(IERC20Partial(token0).balanceOf(Alice), type(uint128).max); + assertEq( + manager.balanceOf(Alice, uint160(Currency.unwrap(poolKey.currency0))), + uint128(type(int128).max) + ); } } @@ -1528,14 +1603,11 @@ contract SemiFungiblePositionManagerTest is PositionUtils { ); (uint256 amount0, ) = PositionUtils.simulateSwap( - pool, + poolKey, tickLower, tickUpper, expectedLiq, - router, - token0, - token1, - fee, + routerV4, true, -amount1Required ); @@ -1546,6 +1618,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { // We also invert the order; this is how we tell SFPM to trigger a swap (LeftRightUnsigned[4] memory collectedByLeg, LeftRightSigned totalSwapped) = sfpm .mintTokenizedPosition( + poolKey, tokenId, uint128(positionSize), TickMath.MAX_TICK - 1, @@ -1566,7 +1639,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { { accountLiquidities = sfpm.getAccountLiquidity( - address(pool), + poolKey.toId(), Alice, 0, tickLower, @@ -1576,15 +1649,23 @@ contract SemiFungiblePositionManagerTest is PositionUtils { assertEq(accountLiquidities.leftSlot(), 0); assertEq(accountLiquidities.rightSlot(), expectedLiq); - (uint256 realLiq, , , , ) = pool.positions( - keccak256(abi.encodePacked(address(sfpm), tickLower, tickUpper)) + (uint256 realLiq, , ) = StateLibrary.getPositionInfo( + manager, + poolKey.toId(), + address(sfpm), + tickLower, + tickUpper, + keccak256(abi.encodePacked(poolKey.toId(), Alice, uint256(0), tickLower, tickUpper)) ); assertEq(realLiq, expectedLiq); } { - assertEq(IERC20Partial(token1).balanceOf(Alice), type(uint128).max); + assertEq( + manager.balanceOf(Alice, uint160(Currency.unwrap(poolKey.currency1))), + uint128(type(int128).max) + ); } } @@ -1635,14 +1716,11 @@ contract SemiFungiblePositionManagerTest is PositionUtils { PanopticMath.convert1to0($amount1Moveds[1], currentSqrtPriceX96); (int256 amount0s, int256 amount1s) = PositionUtils.simulateSwap( - pool, + poolKey, [tickLowers[0], tickLowers[1]], [tickUppers[0], tickUppers[1]], [expectedLiqs[0], expectedLiqs[1]], - router, - token0, - token1, - fee, + routerV4, netSurplus0 < 0, -netSurplus0 ); @@ -1653,6 +1731,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { // We also invert the order; this is how we tell SFPM to trigger a swap (LeftRightUnsigned[4] memory collectedByLeg, LeftRightSigned totalSwapped) = sfpm .mintTokenizedPosition( + poolKey, tokenId, positionSize, TickMath.MAX_TICK - 1, @@ -1674,7 +1753,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { { accountLiquidities = sfpm.getAccountLiquidity( - address(pool), + poolKey.toId(), Alice, 1, tickLowers[0], @@ -1683,8 +1762,21 @@ contract SemiFungiblePositionManagerTest is PositionUtils { assertEq(accountLiquidities.leftSlot(), 0); assertEq(accountLiquidities.rightSlot(), expectedLiqs[0]); - (uint256 realLiq, , , , ) = pool.positions( - keccak256(abi.encodePacked(address(sfpm), tickLowers[0], tickUppers[0])) + (uint256 realLiq, , ) = StateLibrary.getPositionInfo( + manager, + poolKey.toId(), + address(sfpm), + tickLowers[0], + tickUppers[0], + keccak256( + abi.encodePacked( + poolKey.toId(), + Alice, + uint256(1), + tickLowers[0], + tickUppers[0] + ) + ) ); assertEq(realLiq, expectedLiqs[0]); @@ -1692,7 +1784,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { { accountLiquidities = sfpm.getAccountLiquidity( - address(pool), + poolKey.toId(), Alice, 0, tickLowers[1], @@ -1701,8 +1793,21 @@ contract SemiFungiblePositionManagerTest is PositionUtils { assertEq(accountLiquidities.leftSlot(), 0); assertEq(accountLiquidities.rightSlot(), expectedLiqs[1]); - (uint256 realLiq, , , , ) = pool.positions( - keccak256(abi.encodePacked(address(sfpm), tickLowers[1], tickUppers[1])) + (uint256 realLiq, , ) = StateLibrary.getPositionInfo( + manager, + poolKey.toId(), + address(sfpm), + tickLowers[1], + tickUppers[1], + keccak256( + abi.encodePacked( + poolKey.toId(), + Alice, + uint256(0), + tickLowers[1], + tickUppers[1] + ) + ) ); assertEq(realLiq, expectedLiqs[1]); @@ -1750,6 +1855,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { ); sfpm.mintTokenizedPosition( + poolKey, tokenId, positionSizes[0], TickMath.MIN_TICK + 1, @@ -1764,7 +1870,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { tokenId = tokenId.addLeg(1, 1, isWETH, 1, 0, 1, strike1, width1); // price changes afters swap at mint so we need to update the price - (currentSqrtPriceX96, , , , , , ) = pool.slot0(); + currentSqrtPriceX96 = V4StateReader.getSqrtPriceX96(manager, poolKey.toId()); updatePositionDataLong(); int256 netSurplus0 = $amount0Moveds[1] - @@ -1772,15 +1878,32 @@ contract SemiFungiblePositionManagerTest is PositionUtils { // we have to burn from the SFPM because it owns the liquidity vm.startPrank(address(sfpm)); - (int256 amount0s, int256 amount1s) = PositionUtils.simulateSwapLong( - pool, + (int256 amount0s, int256 amount1s) = this.simulateSwapLong( + poolKey, [tickLowers[0], tickLowers[1]], [tickUppers[0], tickUppers[1]], [int128(expectedLiqs[1]), -int128(expectedLiqs[2])], - router, - token0, - token1, - fee, + [ + keccak256( + abi.encodePacked( + poolKey.toId(), + Alice, + uint256(1), + tickLowers[0], + tickUppers[0] + ) + ), + keccak256( + abi.encodePacked( + poolKey.toId(), + Alice, + uint256(0), + tickLowers[1], + tickUppers[1] + ) + ) + ], + routerV4, netSurplus0 < 0, -netSurplus0 ); @@ -1791,6 +1914,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { // We also invert the order; this is how we tell SFPM to trigger a swap (LeftRightUnsigned[4] memory collectedByLeg, LeftRightSigned totalSwapped) = sfpm .mintTokenizedPosition( + poolKey, tokenId, positionSizes[1], TickMath.MAX_TICK - 1, @@ -1812,7 +1936,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { { accountLiquidities = sfpm.getAccountLiquidity( - address(pool), + poolKey.toId(), Alice, 1, tickLowers[0], @@ -1821,8 +1945,21 @@ contract SemiFungiblePositionManagerTest is PositionUtils { assertEq(accountLiquidities.leftSlot(), 0); assertEq(accountLiquidities.rightSlot(), expectedLiqs[1]); - (uint256 realLiq, , , , ) = pool.positions( - keccak256(abi.encodePacked(address(sfpm), tickLowers[0], tickUppers[0])) + (uint256 realLiq, , ) = StateLibrary.getPositionInfo( + manager, + poolKey.toId(), + address(sfpm), + tickLowers[0], + tickUppers[0], + keccak256( + abi.encodePacked( + poolKey.toId(), + Alice, + uint256(1), + tickLowers[0], + tickUppers[0] + ) + ) ); assertEq(realLiq, expectedLiqs[1]); @@ -1830,7 +1967,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { { accountLiquidities = sfpm.getAccountLiquidity( - address(pool), + poolKey.toId(), Alice, 0, tickLowers[1], @@ -1839,8 +1976,21 @@ contract SemiFungiblePositionManagerTest is PositionUtils { assertEq(accountLiquidities.leftSlot(), expectedLiqs[2]); assertEq(accountLiquidities.rightSlot(), expectedLiqs[0] - expectedLiqs[2]); - (uint256 realLiq, , , , ) = pool.positions( - keccak256(abi.encodePacked(address(sfpm), tickLowers[1], tickUppers[1])) + (uint256 realLiq, , ) = StateLibrary.getPositionInfo( + manager, + poolKey.toId(), + address(sfpm), + tickLowers[1], + tickUppers[1], + keccak256( + abi.encodePacked( + poolKey.toId(), + Alice, + uint256(0), + tickLowers[1], + tickUppers[1] + ) + ) ); assertEq(realLiq, expectedLiqs[0] - expectedLiqs[2]); @@ -1882,6 +2032,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { upperBound = bound(upperBound, currentTick + 1, TickMath.MAX_TICK); sfpm.mintTokenizedPosition( + poolKey, tokenId, uint128(positionSize), int24(lowerBound), @@ -1921,7 +2072,13 @@ contract SemiFungiblePositionManagerTest is PositionUtils { vm.expectRevert(Errors.ZeroLiquidity.selector); - sfpm.mintTokenizedPosition(tokenId, uint128(0), TickMath.MIN_TICK, TickMath.MAX_TICK); + sfpm.mintTokenizedPosition( + poolKey, + tokenId, + uint128(0), + TickMath.MIN_TICK, + TickMath.MAX_TICK + ); } // previously there was a dust threshold on minting for tokens below the amount of 50 @@ -1934,17 +2091,9 @@ contract SemiFungiblePositionManagerTest is PositionUtils { // dust threshold is only in effect if both tokens are <10 wei so it's easiest to use a pool with a price close to 1 _cacheWorldState(USDC_USDT_5); - // since we didn't go through the standard setup flow we need to repeat some of the initialization tasks here - vm.startPrank(Alice); - - deal(token0, Alice, type(uint128).max); - deal(token1, Alice, type(uint128).max); - - IERC20Partial(token0).approve(address(sfpm), type(uint256).max); - IERC20Partial(token1).approve(address(sfpm), type(uint256).max); + _initAccounts(); - // Initialize the world pool - sfpm.initializeAMMPool(token0, token1, fee); + sfpm.initializeAMMPool(poolKey); (int24 width, int24 strike) = PositionUtils.getOutOfRangeSW( widthSeed, @@ -1967,11 +2116,17 @@ contract SemiFungiblePositionManagerTest is PositionUtils { TokenId tokenId = TokenId.wrap(0).addPoolId(poolId).addLeg(0, 1, 0, 0, 0, 0, strike, width); - sfpm.mintTokenizedPosition(tokenId, positionSize, TickMath.MIN_TICK, TickMath.MAX_TICK); + sfpm.mintTokenizedPosition( + poolKey, + tokenId, + positionSize, + TickMath.MIN_TICK, + TickMath.MAX_TICK + ); { accountLiquidities = sfpm.getAccountLiquidity( - address(pool), + poolKey.toId(), Alice, 0, tickLower, @@ -1980,8 +2135,13 @@ contract SemiFungiblePositionManagerTest is PositionUtils { assertEq(accountLiquidities.leftSlot(), 0); assertEq(accountLiquidities.rightSlot(), expectedLiq); - (uint256 realLiq, , , , ) = pool.positions( - keccak256(abi.encodePacked(address(sfpm), tickLower, tickUpper)) + (uint256 realLiq, , ) = StateLibrary.getPositionInfo( + manager, + poolKey.toId(), + address(sfpm), + tickLower, + tickUpper, + keccak256(abi.encodePacked(poolKey.toId(), Alice, uint256(0), tickLower, tickUpper)) ); assertEq(realLiq, expectedLiq); @@ -2018,9 +2178,42 @@ contract SemiFungiblePositionManagerTest is PositionUtils { width ); - vm.expectRevert(Errors.UniswapPoolNotInitialized.selector); + vm.expectRevert(abi.encodeWithSelector(Errors.InvalidTokenIdParameter.selector, 0)); + + sfpm.mintTokenizedPosition( + poolKey, + tokenId, + uint128(positionSize), + TickMath.MIN_TICK, + TickMath.MAX_TICK + ); + } + + function test_Fail_mintTokenizedPosition_PoolIdMismatch( + uint256 x, + uint256 widthSeed, + int256 strikeSeed, + uint256 positionSizeSeed + ) public { + // we call _initWorld here instead of _initPool so that the initializeAMMPool call is skipped and this fails + _initPool(x); + + (int24 width, int24 strike) = PositionUtils.getOutOfRangeSW( + widthSeed, + strikeSeed, + uint24(tickSpacing), + currentTick + ); + + populatePositionData(width, strike, positionSizeSeed); + + /// position size is denominated in the opposite of asset, so we do it in the token that is not WETH + TokenId tokenId = TokenId.wrap(0).addPoolId(1).addLeg(0, 1, isWETH, 0, 0, 0, strike, width); + + vm.expectRevert(abi.encodeWithSelector(Errors.InvalidTokenIdParameter.selector, 0)); sfpm.mintTokenizedPosition( + poolKey, tokenId, uint128(positionSize), TickMath.MIN_TICK, @@ -2069,6 +2262,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { vm.expectRevert(Errors.PriceBoundFail.selector); sfpm.mintTokenizedPosition( + poolKey, tokenId, uint128(positionSize), int24(lowerBound), @@ -2115,6 +2309,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { ); sfpm.mintTokenizedPosition( + poolKey, tokenId, uint128(positionSize), TickMath.MIN_TICK, @@ -2135,6 +2330,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { width ); sfpm.mintTokenizedPosition( + poolKey, tokenId, uint128(positionSize), TickMath.MIN_TICK, @@ -2176,6 +2372,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { ); sfpm.mintTokenizedPosition( + poolKey, tokenId, uint128(positionSize), TickMath.MIN_TICK, @@ -2183,14 +2380,20 @@ contract SemiFungiblePositionManagerTest is PositionUtils { ); // cache the minter's balance - uint256 balance0Before = IERC20Partial(token0).balanceOf(Alice); - uint256 balance1Before = IERC20Partial(token1).balanceOf(Alice); + uint256 balance0Before = manager.balanceOf( + Alice, + uint160(Currency.unwrap(poolKey.currency0)) + ); + uint256 balance1Before = manager.balanceOf( + Alice, + uint160(Currency.unwrap(poolKey.currency1)) + ); - // price changes afters swap at mint so we need to update the price - (currentSqrtPriceX96, , , , , , ) = pool.slot0(); + currentSqrtPriceX96 = V4StateReader.getSqrtPriceX96(manager, poolKey.toId()); (LeftRightUnsigned[4] memory collectedByLeg, LeftRightSigned totalSwapped) = sfpm .burnTokenizedPosition( + poolKey, tokenId, uint128(positionSizeBurn), TickMath.MIN_TICK, @@ -2210,7 +2413,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { assertEq(sfpm.balanceOf(Alice, TokenId.unwrap(tokenId)), positionSize - positionSizeBurn); accountLiquidities = sfpm.getAccountLiquidity( - address(pool), + poolKey.toId(), Alice, 0, tickLower, @@ -2220,21 +2423,26 @@ contract SemiFungiblePositionManagerTest is PositionUtils { assertEq(accountLiquidities.leftSlot(), 0); assertApproxEqAbs(accountLiquidities.rightSlot(), expectedLiq, 10); - (uint256 realLiq, , , , ) = pool.positions( - keccak256(abi.encodePacked(address(sfpm), tickLower, tickUpper)) + (uint256 realLiq, , ) = StateLibrary.getPositionInfo( + manager, + poolKey.toId(), + address(sfpm), + tickLower, + tickUpper, + keccak256(abi.encodePacked(poolKey.toId(), Alice, uint256(0), tickLower, tickUpper)) ); assertApproxEqAbs(realLiq, expectedLiq, 10); // ensure burned amount of tokens was collected and sent to the minter assertApproxEqAbs( - IERC20Partial(token0).balanceOf(Alice), + manager.balanceOf(Alice, uint160(Currency.unwrap(poolKey.currency0))), balance0Before + uint256($amount0MovedBurn), 10 ); assertApproxEqAbs( - IERC20Partial(token1).balanceOf(address(Alice)), + manager.balanceOf(Alice, uint160(Currency.unwrap(poolKey.currency1))), balance1Before + uint256($amount1MovedBurn), 10 ); @@ -2285,27 +2493,22 @@ contract SemiFungiblePositionManagerTest is PositionUtils { // The max/min tick cannot be set as slippage limits, so we subtract/add 1 // We also invert the order; this is how we tell SFPM to trigger a swap sfpm.mintTokenizedPosition( + poolKey, tokenId, uint128(positionSize), TickMath.MAX_TICK - 1, TickMath.MIN_TICK + 1 ); - // poke Uniswap pool to update tokens owed - needed because swap happens after mint - vm.startPrank(address(sfpm)); - pool.burn(tickLower, tickUpper, 0); - vm.startPrank(Alice); - - // calculate additional fees owed to position - (, , , , uint128 tokensOwed1) = pool.positions( - PositionKey.compute(address(sfpm), tickLower, tickUpper) - ); + (, uint128 tokensOwed1) = getTokensOwed(Alice, tickLower, tickUpper, 1); // cache the minter's balance so we can assert the difference after burn - uint256 balance1Before = IERC20Partial(token1).balanceOf(Alice); + uint256 balance1Before = manager.balanceOf( + Alice, + uint160(Currency.unwrap(poolKey.currency1)) + ); - // price changes afters swap at mint so we need to update the price - (currentSqrtPriceX96, , , , , , ) = pool.slot0(); + currentSqrtPriceX96 = V4StateReader.getSqrtPriceX96(manager, poolKey.toId()); // subtract 1 to account for precision loss int256 amount1MovedBurn = sqrtLower > currentSqrtPriceX96 @@ -2326,6 +2529,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { (LeftRightUnsigned[4] memory collectedByLeg, LeftRightSigned totalSwapped) = sfpm .burnTokenizedPosition( + poolKey, tokenId, uint128(positionSizeBurn), TickMath.MAX_TICK - 1, @@ -2336,7 +2540,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { { accountLiquidities = sfpm.getAccountLiquidity( - address(pool), + poolKey.toId(), Alice, 1, tickLower, @@ -2346,19 +2550,30 @@ contract SemiFungiblePositionManagerTest is PositionUtils { assertEq(accountLiquidities.leftSlot(), 0); assertApproxEqAbs(accountLiquidities.rightSlot(), expectedLiq, 10); - (uint256 realLiq, , , , ) = pool.positions( - keccak256(abi.encodePacked(address(sfpm), tickLower, tickUpper)) + (uint256 realLiq, , ) = StateLibrary.getPositionInfo( + manager, + poolKey.toId(), + address(sfpm), + tickLower, + tickUpper, + keccak256(abi.encodePacked(poolKey.toId(), Alice, uint256(1), tickLower, tickUpper)) ); assertEq(realLiq, accountLiquidities.rightSlot()); } { - assertEq(IERC20Partial(token0).balanceOf(Alice), type(uint128).max); + assertEq( + manager.balanceOf(Alice, uint160(Currency.unwrap(poolKey.currency0))), + uint128(type(int128).max) + ); } // get final balance before state is cleared - uint256 balance1Final = IERC20Partial(token1).balanceOf(Alice); + uint256 balance1Final = manager.balanceOf( + Alice, + uint160(Currency.unwrap(poolKey.currency1)) + ); // we have to do this simulation after mint/burn because revertTo deletes all snapshots taken ahead of it vm.revertTo(0); @@ -2366,14 +2581,11 @@ contract SemiFungiblePositionManagerTest is PositionUtils { int256[2] memory amount0Moveds = [-amount0RequiredMint, amount0MovedBurn]; (, uint256[2] memory amount1) = PositionUtils.simulateSwap( - pool, + poolKey, tickLower, tickUpper, [expectedLiqMint, expectedLiqBurn], - router, - token0, - token1, - fee, + routerV4, [false, true], amount0Moveds ); @@ -2504,6 +2716,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { ); sfpm.mintTokenizedPosition( + poolKey, tokenId, uint128(positionSize), TickMath.MIN_TICK, @@ -2511,6 +2724,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { ); sfpm.mintTokenizedPosition( + poolKey, tokenIdLong, uint128(positionSize), TickMath.MIN_TICK, @@ -2518,11 +2732,17 @@ contract SemiFungiblePositionManagerTest is PositionUtils { ); // price changes afters swap at mint so we need to update the price - (currentSqrtPriceX96, , , , , , ) = pool.slot0(); + currentSqrtPriceX96 = V4StateReader.getSqrtPriceX96(manager, poolKey.toId()); // cache the minter's balance - uint256 balance0Before = IERC20Partial(token0).balanceOf(Alice); - uint256 balance1Before = IERC20Partial(token1).balanceOf(Alice); + uint256 balance0Before = manager.balanceOf( + Alice, + uint160(Currency.unwrap(poolKey.currency0)) + ); + uint256 balance1Before = manager.balanceOf( + Alice, + uint160(Currency.unwrap(poolKey.currency1)) + ); // it may seem counterintuitive not to simply calculate from the net liquidity, but since this is the way the math is actually done in the contract, // precision losses would make the results too different @@ -2574,6 +2794,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { (LeftRightUnsigned[4] memory collectedByLegLong, LeftRightSigned totalSwappedLong) = sfpm .burnTokenizedPosition( + poolKey, tokenIdLong, uint128(positionSizeBurn), TickMath.MIN_TICK, @@ -2582,6 +2803,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { (LeftRightUnsigned[4] memory collectedByLeg, LeftRightSigned totalSwapped) = sfpm .burnTokenizedPosition( + poolKey, tokenId, uint128(positionSizeBurn), TickMath.MIN_TICK, @@ -2629,7 +2851,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { { accountLiquidities = sfpm.getAccountLiquidity( - address(pool), + poolKey.toId(), Alice, 0, tickLower, @@ -2639,28 +2861,35 @@ contract SemiFungiblePositionManagerTest is PositionUtils { assertApproxEqAbs(accountLiquidities.rightSlot(), expectedLiq, 10); } - (uint256 realLiq, , , uint256 tokensOwed0, uint256 tokensOwed1) = pool.positions( - keccak256(abi.encodePacked(address(sfpm), tickLower, tickUpper)) + (uint256 realLiq, , ) = StateLibrary.getPositionInfo( + manager, + poolKey.toId(), + address(sfpm), + tickLower, + tickUpper, + keccak256(abi.encodePacked(poolKey.toId(), Alice, uint256(0), tickLower, tickUpper)) ); + (uint256 tokensOwed0, uint256 tokensOwed1) = getTokensOwed(Alice, tickLower, tickUpper, 0); + assertApproxEqAbs(realLiq, expectedLiq, 10); assertEq(tokensOwed0, 0); assertEq(tokensOwed1, 0); assertApproxEqAbs( - int256(IERC20Partial(token0).balanceOf(Alice)), + int256(manager.balanceOf(Alice, uint160(Currency.unwrap(poolKey.currency0)))), int256(balance0Before) + amount0MovedsBurn[0] + amount0MovedsBurn[1], 10 ); assertApproxEqAbs( - int256(IERC20Partial(token1).balanceOf(Alice)), + int256(manager.balanceOf(Alice, uint160(Currency.unwrap(poolKey.currency1)))), int256(balance1Before) + amount1MovedsBurn[0] + amount1MovedsBurn[1], 10 ); } /*////////////////////////////////////////////////////////////// - TRANSFER HOOK LOGIC: + + TRANSFER HOOK LOGIC: - //////////////////////////////////////////////////////////////*/ function testSuccess_afterTokenTransfer_Single( @@ -2693,65 +2922,15 @@ contract SemiFungiblePositionManagerTest is PositionUtils { ); sfpm.mintTokenizedPosition( + poolKey, tokenId, uint128(positionSize), TickMath.MIN_TICK, TickMath.MAX_TICK ); - (int128 feesBase0old, int128 feesBase1old) = sfpm.getAccountFeesBase( - address(pool), - Alice, - 1, - tickLower, - tickUpper - ); - + vm.expectRevert(); sfpm.safeTransferFrom(Alice, Bob, TokenId.unwrap(tokenId), positionSize, ""); - - assertEq(sfpm.balanceOf(Alice, TokenId.unwrap(tokenId)), 0); - assertEq(sfpm.balanceOf(Bob, TokenId.unwrap(tokenId)), positionSize); - { - accountLiquidities = sfpm.getAccountLiquidity( - address(pool), - Alice, - 1, - tickLower, - tickUpper - ); - assertEq(accountLiquidities.leftSlot(), 0); - assertEq(accountLiquidities.rightSlot(), 0); - } - { - accountLiquidities = sfpm.getAccountLiquidity( - address(pool), - Bob, - 1, - tickLower, - tickUpper - ); - assertEq(accountLiquidities.leftSlot(), 0); - assertEq(accountLiquidities.rightSlot(), expectedLiq); - } - - { - (int128 feesBase0new, int128 feesBase1new) = sfpm.getAccountFeesBase( - address(pool), - Bob, - 1, - tickLower, - tickUpper - ); - - assertEq(feesBase0new, feesBase0old); - assertEq(feesBase1new, feesBase1old); - } - - (uint256 realLiq, , , , ) = pool.positions( - keccak256(abi.encodePacked(address(sfpm), tickLower, tickUpper)) - ); - - assertEq(realLiq, expectedLiq); } function testSuccess_afterTokenTransfer_Batch( @@ -2784,6 +2963,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { ); sfpm.mintTokenizedPosition( + poolKey, tokenId, uint128(positionSize), TickMath.MIN_TICK, @@ -2802,6 +2982,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { ); sfpm.mintTokenizedPosition( + poolKey, tokenId2, uint128(positionSize), TickMath.MIN_TICK, @@ -2814,280 +2995,20 @@ contract SemiFungiblePositionManagerTest is PositionUtils { uint256[] memory amounts = new uint256[](2); amounts[0] = positionSize; amounts[1] = positionSize; + + vm.expectRevert(); sfpm.safeBatchTransferFrom(Alice, Bob, tokenIds, amounts, ""); - - assertEq(sfpm.balanceOf(Alice, TokenId.unwrap(tokenId)), 0); - assertEq(sfpm.balanceOf(Bob, TokenId.unwrap(tokenId)), positionSize); - { - accountLiquidities = sfpm.getAccountLiquidity( - address(pool), - Alice, - 1, - tickLower, - tickUpper - ); - assertEq(accountLiquidities.leftSlot(), 0); - assertEq(accountLiquidities.rightSlot(), 0); - } - { - accountLiquidities = sfpm.getAccountLiquidity( - address(pool), - Alice, - 0, - tickLower, - tickUpper - ); - assertEq(accountLiquidities.leftSlot(), 0); - assertEq(accountLiquidities.rightSlot(), 0); - } - { - accountLiquidities = sfpm.getAccountLiquidity( - address(pool), - Bob, - 1, - tickLower, - tickUpper - ); - assertEq(accountLiquidities.leftSlot(), 0); - assertEq(accountLiquidities.rightSlot(), expectedLiq); - } - { - accountLiquidities = sfpm.getAccountLiquidity( - address(pool), - Bob, - 0, - tickLower, - tickUpper - ); - assertEq(accountLiquidities.leftSlot(), 0); - assertEq(accountLiquidities.rightSlot(), expectedLiq); - } - { - uint256 expectedLiqTotal = isWETH == 0 - ? LiquidityAmounts.getLiquidityForAmount0(sqrtLower, sqrtUpper, positionSize * 2) - : LiquidityAmounts.getLiquidityForAmount1(sqrtLower, sqrtUpper, positionSize * 2); - - (uint256 realLiq, , , , ) = pool.positions( - keccak256(abi.encodePacked(address(sfpm), tickLower, tickUpper)) - ); - - assertApproxEqAbs(realLiq, expectedLiqTotal, 10); - } - } - - /*////////////////////////////////////////////////////////////// - TRANSFER HOOK LOGIC: - - //////////////////////////////////////////////////////////////*/ - - function test_Fail_afterTokenTransfer_NotAllLiquidityTransferred( - uint256 x, - uint256 widthSeed, - int256 strikeSeed, - uint256 positionSizeSeed, - uint256 transferSize - ) public { - _initPool(x); - - (int24 width, int24 strike) = PositionUtils.getOutOfRangeSW( - widthSeed, - strikeSeed, - uint24(tickSpacing), - currentTick - ); - - populatePositionData(width, strike, positionSizeSeed); - - transferSize = bound(transferSize, 1, positionSize - 1); - - // it's possible under certain conditions for the delta between the transfer size and the user's balance to be *so small* - // that calculation results in the same amount of liquidity, in which case it would not fail, - // since the assertion is that all liquidity must be transferred, and not necessarily all balance - - vm.assume( - expectedLiq != - ( - isWETH == 0 - ? LiquidityAmounts.getLiquidityForAmount0( - sqrtLower, - sqrtUpper, - transferSize - ) - : LiquidityAmounts.getLiquidityForAmount1( - sqrtLower, - sqrtUpper, - transferSize - ) - ) - ); - - /// position size is denominated in the opposite of asset, so we do it in the token that is not WETH - TokenId tokenId = TokenId.wrap(0).addPoolId(poolId).addLeg( - 0, - 1, - isWETH, - 0, - 1, - 0, - strike, - width - ); - - sfpm.mintTokenizedPosition( - tokenId, - uint128(positionSize), - TickMath.MIN_TICK, - TickMath.MAX_TICK - ); - - vm.expectRevert(Errors.TransferFailed.selector); - - sfpm.safeTransferFrom(Alice, Bob, TokenId.unwrap(tokenId), transferSize, ""); - } - - // mint a short leg, long some of that leg, then transfer the long leg - // should fail as you shouldnt be able to transfer removedliquidity - function test_Fail_afterTokenTransfer_LongChunkTransferredSolo( - uint256 x, - uint256 widthSeed, - int256 strikeSeed, - uint256 positionSizeSeed - ) public { - _initPool(x); - - (int24 width, int24 strike) = PositionUtils.getOutOfRangeSW( - widthSeed, - strikeSeed, - uint24(tickSpacing), - currentTick - ); - - populatePositionData(width, strike, positionSizeSeed); - - /// position size is denominated in the opposite of asset, so we do it in the token that is not WETH - TokenId tokenId1 = TokenId.wrap(0).addPoolId(poolId).addLeg( - 0, - 1, - isWETH, - 0, - 1, - 0, - strike, - width - ); - - sfpm.mintTokenizedPosition( - tokenId1, - uint128(positionSize * 2), - TickMath.MIN_TICK, - TickMath.MAX_TICK - ); - - TokenId tokenId2 = TokenId.wrap(0).addPoolId(poolId).addLeg( - 0, - 1, - isWETH, - 1, - 1, - 0, - strike, - width - ); - - sfpm.mintTokenizedPosition( - tokenId2, - uint128(positionSize), - TickMath.MIN_TICK, - TickMath.MAX_TICK - ); - - vm.expectRevert(Errors.TransferFailed.selector); - - sfpm.safeTransferFrom(Alice, Bob, TokenId.unwrap(tokenId2), positionSize, ""); - } - - function test_Fail_afterTokenTransfer_RecipientAlreadyOwns( - uint256 x, - uint256 widthSeed, - int256 strikeSeed, - uint256[2] memory positionSizeSeeds, - uint256 transferSize - ) public { - _initPool(x); - - (int24 width, int24 strike) = PositionUtils.getOutOfRangeSW( - widthSeed, - strikeSeed, - uint24(tickSpacing), - currentTick - ); - - populatePositionData(width, strike, positionSizeSeeds); - - /// position size is denominated in the opposite of asset, so we do it in the token that is not WETH - TokenId tokenId = TokenId.wrap(0).addPoolId(poolId).addLeg( - 0, - 1, - isWETH, - 0, - 1, - 0, - strike, - width - ); - - sfpm.mintTokenizedPosition( - tokenId, - uint128(positionSizes[0]), - TickMath.MIN_TICK, - TickMath.MAX_TICK - ); - - vm.startPrank(Bob); - - sfpm.mintTokenizedPosition( - tokenId, - uint128(positionSizes[1]), - TickMath.MIN_TICK, - TickMath.MAX_TICK - ); - - vm.startPrank(Alice); - - vm.expectRevert(Errors.TransferFailed.selector); - - transferSize = bound(transferSize, 1, positionSizes[0] - 1); - - sfpm.safeTransferFrom(Alice, Bob, TokenId.unwrap(tokenId), transferSize, ""); - } + } /*////////////////////////////////////////////////////////////// UNISWAP CALLBACKS: - //////////////////////////////////////////////////////////////*/ - function test_Fail_uniswapV3MintCallback_Unauthorized(uint256 x) public { + function test_Fail_UnlockCallback_Unauthorized(uint256 x) public { _initWorld(x); - vm.expectRevert(); - sfpm.uniswapV3MintCallback( - 0, - 0, - abi.encode( - CallbackLib.CallbackData(CallbackLib.PoolFeatures(token0, token1, fee), address(0)) - ) - ); - } - - function test_Fail_uniswapV3SwapCallback_Unauthorized(uint256 x) public { - _initWorld(x); - - vm.expectRevert(); - sfpm.uniswapV3SwapCallback( - 0, - 0, - abi.encode( - CallbackLib.CallbackData(CallbackLib.PoolFeatures(token0, token1, fee), address(0)) - ) - ); + vm.expectRevert(Errors.UnauthorizedUniswapCallback.selector); + sfpm.unlockCallback(""); } function test_Success_getAccountPremium_getAccountFeesBase_ShortOnly( @@ -3121,35 +3042,16 @@ contract SemiFungiblePositionManagerTest is PositionUtils { ); sfpm.mintTokenizedPosition( + poolKey, tokenId, uint128(positionSize), TickMath.MIN_TICK, TickMath.MAX_TICK ); - (int128 feesBase0, int128 feesBase1) = sfpm.getAccountFeesBase( - address(pool), - Alice, - 1, - tickLower, - tickUpper - ); - { - (, uint256 _feeGrowthInside0LastX128, uint256 _feeGrowthInside1LastX128, , ) = pool - .positions(PositionKey.compute(address(sfpm), tickLower, tickUpper)); - assertEq( - feesBase0, - int128(int256(Math.mulDiv128RoundingUp(_feeGrowthInside0LastX128, expectedLiq))) - ); - assertEq( - feesBase1, - int128(int256(Math.mulDiv128RoundingUp(_feeGrowthInside1LastX128, expectedLiq))) - ); - } - { (uint128 premiumToken0, uint128 premiumtoken1) = sfpm.getAccountPremium( - address(pool), + poolKey.toId(), Alice, 1, tickLower, @@ -3161,50 +3063,26 @@ contract SemiFungiblePositionManagerTest is PositionUtils { assertEq(premiumtoken1, 0); } - vm.startPrank(Bob); - - swapSize = bound(swapSize, 10 ** 15, 10 ** 19); - - router.exactInputSingle( - ISwapRouter.ExactInputSingleParams( - isWETH == 0 ? token0 : token1, - isWETH == 1 ? token0 : token1, - fee, - Bob, - block.timestamp, - swapSize, - 0, - 0 - ) - ); + twoWaySwap(swapSize); - router.exactOutputSingle( - ISwapRouter.ExactOutputSingleParams( - isWETH == 1 ? token0 : token1, - isWETH == 0 ? token0 : token1, - fee, - Bob, - block.timestamp, - swapSize - (swapSize * fee) / 1_000_000, - type(uint256).max, - 0 - ) - ); - - (, currentTick, , , , , ) = pool.slot0(); - - // poke Uniswap pool - vm.startPrank(address(sfpm)); - pool.burn(tickLower, tickUpper, 0); vm.startPrank(Alice); - (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, , ) = pool.positions( - PositionKey.compute(address(sfpm), tickLower, tickUpper) + currentTick = V4StateReader.getTick(manager, poolKey.toId()); + + (uint128 positionLiquidity, , ) = StateLibrary.getPositionInfo( + manager, + poolKey.toId(), + address(sfpm), + tickLower, + tickUpper, + keccak256(abi.encodePacked(poolKey.toId(), Alice, uint256(1), tickLower, tickUpper)) ); + uint256 tokensOwed0; + uint256 tokensOwed1; { - (uint128 premiumToken0, uint128 premiumtoken1) = sfpm.getAccountPremium( - address(pool), + (uint128 premiumToken0, uint128 premiumToken1) = sfpm.getAccountPremium( + poolKey.toId(), Alice, 1, tickLower, @@ -3212,42 +3090,15 @@ contract SemiFungiblePositionManagerTest is PositionUtils { currentTick, 0 ); - assertEq( - premiumToken0, - FullMath.mulDiv( - uint128( - int128(int256(Math.mulDiv128(feeGrowthInside0LastX128, expectedLiq))) - - feesBase0 > - 0 - ? int128( - int256(Math.mulDiv128(feeGrowthInside0LastX128, expectedLiq)) - ) - feesBase0 - : int128(0) - ), - uint256(expectedLiq) * 2 ** 64, - uint256(expectedLiq) ** 2 - ) - ); - assertEq( - premiumtoken1, - FullMath.mulDiv( - uint128( - int128(int256(Math.mulDiv128(feeGrowthInside1LastX128, expectedLiq))) - - feesBase1 > - 0 - ? int128( - int256(Math.mulDiv128(feeGrowthInside1LastX128, expectedLiq)) - ) - feesBase1 - : int128(0) - ), - uint256(expectedLiq) * 2 ** 64, - uint256(expectedLiq) ** 2 - ) - ); + + (tokensOwed0, tokensOwed1) = getTokensOwed(Alice, tickLower, tickUpper, 1); + + assertEq(premiumToken0, Math.mulDiv(tokensOwed0, 2 ** 64, positionLiquidity)); + assertEq(premiumToken1, Math.mulDiv(tokensOwed1, 2 ** 64, positionLiquidity)); // cached premia has not been updated yet, so should still be 0 - (premiumToken0, premiumtoken1) = sfpm.getAccountPremium( - address(pool), + (premiumToken0, premiumToken1) = sfpm.getAccountPremium( + poolKey.toId(), Alice, 1, tickLower, @@ -3256,51 +3107,33 @@ contract SemiFungiblePositionManagerTest is PositionUtils { 0 ); assertEq(premiumToken0, 0); - assertEq(premiumtoken1, 0); + assertEq(premiumToken1, 0); } { sfpm.burnTokenizedPosition( + poolKey, tokenId, uint128(positionSize), TickMath.MIN_TICK, TickMath.MAX_TICK ); - (, , , uint256 tokensowed0, uint256 tokensowed1) = pool.positions( - PositionKey.compute(address(sfpm), tickLower, tickUpper) - ); - - assertLe(tokensowed0, 1); - assertLe(tokensowed1, 1); assertApproxEqAbs( - IERC20Partial(token0).balanceOf(Alice), - uint256(type(uint128).max) + - uint128( - int128(int256(Math.mulDiv128(feeGrowthInside0LastX128, expectedLiq))) - - feesBase0 > - 0 - ? int128( - int256(Math.mulDiv128(feeGrowthInside0LastX128, expectedLiq)) - ) - feesBase0 - : int128(0) - ), + manager.balanceOf(Alice, uint160(Currency.unwrap(poolKey.currency0))), + uint256(uint128(type(int128).max)) + tokensOwed0, 10 ); assertApproxEqAbs( - IERC20Partial(token1).balanceOf(Alice), - uint256(type(uint128).max) + - uint128( - int128(int256(Math.mulDiv128(feeGrowthInside1LastX128, expectedLiq))) - - feesBase1 > - 0 - ? int128( - int256(Math.mulDiv128(feeGrowthInside1LastX128, expectedLiq)) - ) - feesBase1 - : int128(0) - ), + manager.balanceOf(Alice, uint160(Currency.unwrap(poolKey.currency1))), + uint256(uint128(type(int128).max)) + tokensOwed1, 10 ); + + (tokensOwed0, tokensOwed1) = getTokensOwed(Alice, tickLower, tickUpper, 1); + + assertLe(tokensOwed0, 1); + assertLe(tokensOwed1, 1); } } @@ -3337,10 +3170,16 @@ contract SemiFungiblePositionManagerTest is PositionUtils { width ); - sfpm.mintTokenizedPosition(tokenId, positionSize, TickMath.MAX_TICK, TickMath.MIN_TICK); + sfpm.mintTokenizedPosition( + poolKey, + tokenId, + positionSize, + TickMath.MAX_TICK, + TickMath.MIN_TICK + ); accountLiquidities = sfpm.getAccountLiquidity( - address(pool), + poolKey.toId(), Alice, tokenType, tickLower, @@ -3352,7 +3191,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { // premia is updated BEFORE the ITM swap, so cached (last collected) premia should still be 0 (premium0Short, premium1Short) = sfpm.getAccountPremium( - address(pool), + poolKey.toId(), Alice, tokenType, tickLower, @@ -3364,7 +3203,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { assertEq(premium1Short, 0); (premium0Long, premium1Long) = sfpm.getAccountPremium( - address(pool), + poolKey.toId(), Alice, tokenType, tickLower, @@ -3376,16 +3215,19 @@ contract SemiFungiblePositionManagerTest is PositionUtils { assertEq(premium1Long, 0); twoWaySwap(swapSizeSeed); - (currentSqrtPriceX96, currentTick, , , , , ) = pool.slot0(); - vm.startPrank(address(sfpm)); - pool.burn(tickLower, tickUpper, 0); - vm.startPrank(Alice); - (, , , uint256 tokensOwed0, uint256 tokensOwed1) = pool.positions( - keccak256(abi.encodePacked(address(sfpm), tickLower, tickUpper)) + currentTick = V4StateReader.getTick(manager, poolKey.toId()); + currentSqrtPriceX96 = V4StateReader.getSqrtPriceX96(manager, poolKey.toId()); + + (uint256 tokensOwed0, uint256 tokensOwed1) = getTokensOwed( + Alice, + tickLower, + tickUpper, + tokenType ); + (premium0Short, premium1Short) = sfpm.getAccountPremium( - address(pool), + poolKey.toId(), Alice, tokenType, tickLower, @@ -3406,7 +3248,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { ); (premium0Long, premium1Long) = sfpm.getAccountPremium( - address(pool), + poolKey.toId(), Alice, tokenType, tickLower, @@ -3437,8 +3279,8 @@ contract SemiFungiblePositionManagerTest is PositionUtils { width ); - balanceBefore0 = IERC20Partial(token0).balanceOf(Alice); - balanceBefore1 = IERC20Partial(token1).balanceOf(Alice); + balanceBefore0 = manager.balanceOf(Alice, uint160(Currency.unwrap(poolKey.currency0))); + balanceBefore1 = manager.balanceOf(Alice, uint160(Currency.unwrap(poolKey.currency1))); effectiveLiqRatio = bound(effectiveLiqRatio, 1000, 900_000); positionSize = uint128((positionSize * effectiveLiqRatio) / 1_000_000); @@ -3448,16 +3290,22 @@ contract SemiFungiblePositionManagerTest is PositionUtils { updateAmountsMovedSingleSwap(-int128(expectedLiqs[1]), tokenType); vm.startPrank(Alice); - sfpm.mintTokenizedPosition(tokenId1, positionSize, TickMath.MAX_TICK, TickMath.MIN_TICK); + sfpm.mintTokenizedPosition( + poolKey, + tokenId1, + positionSize, + TickMath.MAX_TICK, + TickMath.MIN_TICK + ); assertApproxEqAbs( - int256(IERC20Partial(token0).balanceOf(Alice)), + int256(manager.balanceOf(Alice, uint160(Currency.unwrap(poolKey.currency0)))), int256(balanceBefore0) + int256(tokensOwed0) - $amount0Moved, 1 ); assertApproxEqAbs( - int256(IERC20Partial(token1).balanceOf(Alice)), + int256(manager.balanceOf(Alice, uint160(Currency.unwrap(poolKey.currency1)))), int256(balanceBefore1) + int256(tokensOwed1) - $amount1Moved, 1 ); @@ -3470,7 +3318,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { // It's possible to be off-by-one there due to rounding errors (premium0Short, premium1Short) = sfpm.getAccountPremium( - address(pool), + poolKey.toId(), Alice, tokenType, tickLower, @@ -3491,7 +3339,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { ); (premium0Long, premium1Long) = sfpm.getAccountPremium( - address(pool), + poolKey.toId(), Alice, tokenType, tickLower, @@ -3510,14 +3358,10 @@ contract SemiFungiblePositionManagerTest is PositionUtils { (1 * 2 ** 64) / expectedLiq + 10 ); - (currentSqrtPriceX96, currentTick, , , , , ) = pool.slot0(); - vm.startPrank(address(sfpm)); - pool.burn(tickLower, tickUpper, 0); - vm.startPrank(Alice); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); + currentSqrtPriceX96 = V4StateReader.getSqrtPriceX96(manager, poolKey.toId()); - (, , , tokensOwed0, tokensOwed1) = pool.positions( - keccak256(abi.encodePacked(address(sfpm), tickLower, tickUpper)) - ); + (tokensOwed0, tokensOwed1) = getTokensOwed(Alice, tickLower, tickUpper, tokenType); premium0ShortOld = premium0Short; premium0LongOld = premium0Long; @@ -3525,7 +3369,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { premium1LongOld = premium1Long; (premium0Short, premium1Short) = sfpm.getAccountPremium( - address(pool), + poolKey.toId(), Alice, tokenType, tickLower, @@ -3605,7 +3449,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { } (premium0Long, premium1Long) = sfpm.getAccountPremium( - address(pool), + poolKey.toId(), Alice, tokenType, tickLower, @@ -3655,18 +3499,20 @@ contract SemiFungiblePositionManagerTest is PositionUtils { } sfpm.burnTokenizedPosition( + poolKey, tokenId1, uint128((positionSize * effectiveLiqRatio) / 1_000_000), TickMath.MIN_TICK, TickMath.MAX_TICK ); - (currentSqrtPriceX96, currentTick, , , , , ) = pool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); + currentSqrtPriceX96 = V4StateReader.getSqrtPriceX96(manager, poolKey.toId()); premium0LongOld = premium0Long; premium1LongOld = premium1Long; (premium0Long, premium1Long) = sfpm.getAccountPremium( - address(pool), + poolKey.toId(), Alice, tokenType, tickLower, @@ -3678,7 +3524,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { assertApproxEqAbs(premium1Long, premium1LongOld, premiaError1[1]); (premium0Long, premium1Long) = sfpm.getAccountPremium( - address(pool), + poolKey.toId(), Alice, tokenType, tickLower, @@ -3689,15 +3535,22 @@ contract SemiFungiblePositionManagerTest is PositionUtils { assertApproxEqAbs(premium0Long, premium0LongOld, premiaError0[1]); assertApproxEqAbs(premium1Long, premium1LongOld, premiaError1[1]); - sfpm.burnTokenizedPosition(tokenId, positionSize, TickMath.MIN_TICK, TickMath.MAX_TICK); + sfpm.burnTokenizedPosition( + poolKey, + tokenId, + positionSize, + TickMath.MIN_TICK, + TickMath.MAX_TICK + ); - (currentSqrtPriceX96, currentTick, , , , , ) = pool.slot0(); + currentTick = V4StateReader.getTick(manager, poolKey.toId()); + currentSqrtPriceX96 = V4StateReader.getSqrtPriceX96(manager, poolKey.toId()); premium0ShortOld = premium0Short; premium1ShortOld = premium1Short; (premium0Short, premium1Short) = sfpm.getAccountPremium( - address(pool), + poolKey.toId(), Alice, tokenType, tickLower, @@ -3710,7 +3563,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { assertApproxEqAbs(premium1Short, premium1ShortOld, premiaError1[0]); (premium0Short, premium1Short) = sfpm.getAccountPremium( - address(pool), + poolKey.toId(), Alice, tokenType, tickLower, @@ -3723,7 +3576,10 @@ contract SemiFungiblePositionManagerTest is PositionUtils { } // make sure that we allow the premium to overflow and it does not revert when too much is accumulated with a huge multiplier + /// forge-config: ci_test.fuzz.runs = 1 function test_Success_PremiumDOSPrevention(uint256 widthSeed, int256 strikeSeed) public { + vm.skip(true); + _initPool(0); (int24 width, int24 strike) = PositionUtils.getInRangeSW( @@ -3748,6 +3604,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { ); sfpm.mintTokenizedPosition( + poolKey, tokenIdShort, uint128(positionSize), TickMath.MIN_TICK, @@ -3768,48 +3625,24 @@ contract SemiFungiblePositionManagerTest is PositionUtils { // mint a long position with 1 wei of liquidity less than available, resulting in a huge multiplier sfpm.mintTokenizedPosition( + poolKey, tokenIdLong, uint128(Math.mulDiv(positionSize, (2 ** 64 - 1), 2 ** 64)), TickMath.MIN_TICK, TickMath.MAX_TICK ); - vm.startPrank(Bob); - uint256 swapSize = 10 ** 20; for (uint256 i = 0; i < 500; ++i) { - router.exactInputSingle( - ISwapRouter.ExactInputSingleParams( - isWETH == 0 ? token0 : token1, - isWETH == 1 ? token0 : token1, - fee, - Bob, - block.timestamp, - swapSize, - 0, - 0 - ) - ); - - router.exactOutputSingle( - ISwapRouter.ExactOutputSingleParams( - isWETH == 1 ? token0 : token1, - isWETH == 0 ? token0 : token1, - fee, - Bob, - block.timestamp, - swapSize - (swapSize * fee) / 1_000_000, - type(uint256).max, - 0 - ) - ); + twoWaySwap(swapSize); } vm.startPrank(Alice); // this succeeding is the test - it should overflow cleanly instead of reverting and DOS-ing the positions sfpm.burnTokenizedPosition( + poolKey, tokenIdLong, uint128(Math.mulDiv(positionSize, (2 ** 64 - 1), 2 ** 64)), TickMath.MIN_TICK, @@ -3817,6 +3650,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { ); sfpm.burnTokenizedPosition( + poolKey, tokenIdShort, uint128(positionSize), TickMath.MIN_TICK, @@ -3859,9 +3693,9 @@ contract SemiFungiblePositionManagerTest is PositionUtils { // replace the Uniswap pool with a mock contract that can answer some queries correctly, // but will attempt to callback with mintTokenizedPosition on any other call - vm.etch(address(pool), address(new ReenterMint()).code); + vm.etch(address(manager), address(new ReenterMint()).code); - ReenterMint(address(pool)).construct( + ReenterMint(address(manager)).construct( ReenterMint.Slot0( TickMath.getSqrtRatioAtTick(currentTick), currentTick, @@ -3880,126 +3714,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { vm.expectRevert("REENTRANCY"); sfpm.mintTokenizedPosition( - tokenId, - uint128(positionSize), - TickMath.MIN_TICK, - TickMath.MAX_TICK - ); - } - - // make sure single transfers check reentrancy lock state - function test_Fail_TransferSingle_ReentrancyLock( - uint256 x, - uint256 widthSeed, - int256 strikeSeed, - uint256 positionSizeSeed - ) public { - _initPool(x); - - (int24 width, int24 strike) = PositionUtils.getOutOfRangeSW( - widthSeed, - strikeSeed, - uint24(tickSpacing), - currentTick - ); - - populatePositionData(width, strike, positionSizeSeed); - - /// position size is denominated in the opposite of asset, so we do it in the token that is not WETH - TokenId tokenId = TokenId.wrap(0).addPoolId(poolId).addLeg( - 0, - 1, - isWETH, - 0, - 0, - 0, - strike, - width - ); - - // replace the Uniswap pool with a mock contract that can answer some queries correctly, - // but will attempt to callback with mintTokenizedPosition on any other call - vm.etch(address(pool), address(new ReenterTransferSingle()).code); - - ReenterTransferSingle(address(pool)).construct( - ReenterTransferSingle.Slot0( - TickMath.getSqrtRatioAtTick(currentTick), - currentTick, - 0, - 0, - 0, - 0, - true - ), - address(token0), - address(token1), - fee, - tickSpacing - ); - - vm.expectRevert("REENTRANCY"); - - sfpm.mintTokenizedPosition( - tokenId, - uint128(positionSize), - TickMath.MIN_TICK, - TickMath.MAX_TICK - ); - } - - // make sure batch transfers check reentrancy lock state - function test_Fail_TransferBatch_ReentrancyLock( - uint256 x, - uint256 widthSeed, - int256 strikeSeed, - uint256 positionSizeSeed - ) public { - _initPool(x); - - (int24 width, int24 strike) = PositionUtils.getOutOfRangeSW( - widthSeed, - strikeSeed, - uint24(tickSpacing), - currentTick - ); - - populatePositionData(width, strike, positionSizeSeed); - - /// position size is denominated in the opposite of asset, so we do it in the token that is not WETH - TokenId tokenId = TokenId.wrap(0).addPoolId(poolId).addLeg( - 0, - 1, - isWETH, - 0, - 0, - 0, - strike, - width - ); - - // replace the Uniswap pool with a mock contract that can answer some queries correctly, - // but will attempt to callback with mintTokenizedPosition on any other call - vm.etch(address(pool), address(new ReenterTransferBatch()).code); - - ReenterTransferBatch(address(pool)).construct( - ReenterTransferBatch.Slot0( - TickMath.getSqrtRatioAtTick(currentTick), - currentTick, - 0, - 0, - 0, - 0, - true - ), - address(token0), - address(token1), - fee, - tickSpacing - ); - - vm.expectRevert("REENTRANCY"); - - sfpm.mintTokenizedPosition( + poolKey, tokenId, uint128(positionSize), TickMath.MIN_TICK, @@ -4040,11 +3755,12 @@ contract SemiFungiblePositionManagerTest is PositionUtils { // allow Alice to try to initialize and then reenter when getting the onERC1155Received callback vm.etch(address(Alice), address(new Reenter1155Initialize()).code); - Reenter1155Initialize(Alice).construct(address(token0), address(token1), fee, poolId); + Reenter1155Initialize(Alice).construct(poolKey); vm.expectRevert("REENTRANCY"); sfpm.mintTokenizedPosition( + poolKey, tokenId, uint128(positionSize), TickMath.MIN_TICK, @@ -4083,6 +3799,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { ); sfpm.mintTokenizedPosition( + poolKey, tokenId, uint128(positionSize), TickMath.MIN_TICK, @@ -4091,9 +3808,9 @@ contract SemiFungiblePositionManagerTest is PositionUtils { // replace the Uniswap pool with a mock that responds correctly to queries but calls back on // any other operations - vm.etch(address(pool), address(new ReenterBurn()).code); + vm.etch(address(manager), address(new ReenterBurn()).code); - ReenterBurn(address(pool)).construct( + ReenterBurn(address(manager)).construct( ReenterBurn.Slot0( TickMath.getSqrtRatioAtTick(currentTick), currentTick, @@ -4112,6 +3829,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { vm.expectRevert("REENTRANCY"); sfpm.burnTokenizedPosition( + poolKey, tokenId, uint128(positionSizeBurn), TickMath.MIN_TICK, @@ -4179,11 +3897,12 @@ contract SemiFungiblePositionManagerTest is PositionUtils { } function test_Fail_RemovedLiquidity_Overflow() public { - _initPool(0); - // need a super wide position to exxagerate position size units _cacheWorldState(USDC_WETH_30); - sfpm.initializeAMMPool(token0, token1, fee); + + _initAccounts(); + + sfpm.initializeAMMPool(poolKey); int24 width = 4090; int24 strike = 0; @@ -4202,6 +3921,7 @@ contract SemiFungiblePositionManagerTest is PositionUtils { ); sfpm.mintTokenizedPosition( + poolKey, tokenId, uint128(1_000_000), TickMath.MIN_TICK, @@ -4211,87 +3931,30 @@ contract SemiFungiblePositionManagerTest is PositionUtils { tokenId = TokenId.wrap(0).addPoolId(poolId).addLeg(0, 1, isWETH, 1, 0, 0, strike, width); for (uint256 i = 0; i < 10; i++) { - sfpm.mintTokenizedPosition(tokenId, uint128(922), TickMath.MIN_TICK, TickMath.MAX_TICK); + sfpm.mintTokenizedPosition( + poolKey, + tokenId, + uint128(922), + TickMath.MIN_TICK, + TickMath.MAX_TICK + ); - sfpm.burnTokenizedPosition(tokenId, uint128(462), TickMath.MIN_TICK, TickMath.MAX_TICK); + sfpm.burnTokenizedPosition( + poolKey, + tokenId, + uint128(462), + TickMath.MIN_TICK, + TickMath.MAX_TICK + ); } vm.expectRevert(); sfpm.burnTokenizedPosition( + poolKey, tokenId, uint128(10 * (922 - 462)), TickMath.MIN_TICK, TickMath.MAX_TICK ); } - - function test_removedLiquidityOverflow() public { - vm.skip(true); - _initPool(0); - - _cacheWorldState(USDC_WETH_30); - - sfpm.initializeAMMPool(token0, token1, fee); - - int24 width = 4090; - int24 strike = 0; - - populatePositionData(width, strike, 0, 0); - - uint128 psnSize = type(uint128).max / 70; - - TokenId shortTokenId = TokenId.wrap(0).addPoolId(poolId).addLeg( - 0, - 1, - isWETH, - 0, - 0, - 0, - strike, - width - ); - - TokenId longTokenId = TokenId.wrap(0).addPoolId(poolId).addLeg( - 0, - 1, - isWETH, - 1, - 0, - 0, - strike, - width - ); - - for (uint256 i = 0; i < 32311; i++) { - sfpm.mintTokenizedPosition(shortTokenId, psnSize, TickMath.MIN_TICK, TickMath.MAX_TICK); - - sfpm.mintTokenizedPosition(longTokenId, psnSize, TickMath.MIN_TICK, TickMath.MAX_TICK); - } - - accountLiquidities = sfpm.getAccountLiquidity( - address(USDC_WETH_30), - Alice, - 0, - tickLower, - tickUpper - ); - - uint128 accountLiquidities_leftSlot_before_overflow = accountLiquidities.leftSlot(); - assertLt(accountLiquidities_leftSlot_before_overflow, type(uint128).max); - - sfpm.mintTokenizedPosition(shortTokenId, psnSize, TickMath.MIN_TICK, TickMath.MAX_TICK); - - vm.expectRevert(stdError.arithmeticError); - sfpm.mintTokenizedPosition(longTokenId, psnSize, TickMath.MIN_TICK, TickMath.MAX_TICK); - - accountLiquidities = sfpm.getAccountLiquidity( - address(USDC_WETH_30), - Alice, - 0, - tickLower, - tickUpper - ); - - assertGe(accountLiquidities.leftSlot(), accountLiquidities_leftSlot_before_overflow); - } } diff --git a/test/foundry/libraries/CallbackLib.t.sol b/test/foundry/libraries/CallbackLib.t.sol deleted file mode 100644 index 9728309..0000000 --- a/test/foundry/libraries/CallbackLib.t.sol +++ /dev/null @@ -1,101 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.24; - -import "forge-std/Test.sol"; -import {CallbackLib} from "@libraries/CallbackLib.sol"; -import {CallbackLibHarness} from "./harnesses/CallbackLibHarness.sol"; -import {Errors} from "@libraries/Errors.sol"; -import {IUniswapV3Factory} from "v3-core/interfaces/IUniswapV3Factory.sol"; - -/** - * Test the CallbackLib functionality with Foundry and Fuzzing. - * - * @author Axicon Labs Limited - */ -contract CallbackLibTest is Test { - IUniswapV3Factory constant factory = - IUniswapV3Factory(0x1F98431c8aD98523631AE4a59f267346ea31F984); - uint24[] availablePoolFees = [100, 500, 3000, 10000]; - - CallbackLibHarness callbackLib = new CallbackLibHarness(); - - function test_Success_validateCallback(address tokenA, address tokenB, uint256 fee) public { - // order tokens correctly - if (tokenA > tokenB) (tokenA, tokenB) = (tokenB, tokenA); - - // ensure pool can be created - vm.assume(tokenA != tokenB); - vm.assume(tokenA > address(0)); - - // pick a valid pool fee from those available on the factory - fee = availablePoolFees[bound(fee, 0, 3)]; - - // make sure such a pool does not somehow already exist, if it does skip creation - // create the Uniswap pool (tokens are random do not actually need to exist for the purposes of this test) - address pool = factory.getPool(tokenA, tokenB, uint24(fee)); - pool = pool == address(0) ? factory.createPool(tokenA, tokenB, uint24(fee)) : pool; - - // now, check if the computed address of the pool from the claimed features is correct - callbackLib.validateCallback( - pool, - factory, - CallbackLib.PoolFeatures({token0: tokenA, token1: tokenB, fee: uint24(fee)}) - ); - } - - function test_Fail_validateCallback( - address pool, - address tokenA, - address tokenB, - uint24 fee - ) public { - // order tokens correctly - if (tokenA > tokenB) (tokenA, tokenB) = (tokenB, tokenA); - - // ensure pool can be created - vm.assume(tokenA != tokenB); - vm.assume(tokenA > address(0)); - - // ensure pool does not match with parameters - vm.assume(pool != factory.getPool(tokenA, tokenB, fee)); - - vm.expectRevert(Errors.InvalidUniswapCallback.selector); - callbackLib.validateCallback( - pool, - factory, - CallbackLib.PoolFeatures({token0: tokenA, token1: tokenB, fee: uint24(fee)}) - ); - } - - function test_Fail_validateCallback_Targeted( - address wrongPool, - address tokenA, - address tokenB, - uint24 fee - ) public { - // order tokens correctly - if (tokenA > tokenB) (tokenA, tokenB) = (tokenB, tokenA); - - // ensure pool can be created - vm.assume(tokenA != tokenB); - vm.assume(tokenA > address(0)); - - // pick a valid pool fee from those available on the factory - fee = availablePoolFees[bound(fee, 0, 3)]; - - // make sure such a pool does not somehow already exist, if it does skip creation - // create the Uniswap pool (tokens are random do not actually need to exist for the purposes of this test) - address pool = factory.getPool(tokenA, tokenB, fee); - pool = pool == address(0) ? factory.createPool(tokenA, tokenB, fee) : pool; - - // ensure we are validating an incorrect pool address - vm.assume(wrongPool != pool); - - vm.expectRevert(Errors.InvalidUniswapCallback.selector); - callbackLib.validateCallback( - wrongPool, - factory, - CallbackLib.PoolFeatures({token0: tokenA, token1: tokenB, fee: fee}) - ); - } -} diff --git a/test/foundry/libraries/FeesCalc.t.sol b/test/foundry/libraries/FeesCalc.t.sol deleted file mode 100644 index 564c8f9..0000000 --- a/test/foundry/libraries/FeesCalc.t.sol +++ /dev/null @@ -1,209 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.24; - -// Foundry -import "forge-std/Test.sol"; -// Internal -import {TickMath} from "v3-core/libraries/TickMath.sol"; -import {Math} from "@libraries/Math.sol"; -import {PanopticMath} from "@libraries/PanopticMath.sol"; -import {Errors} from "@libraries/Errors.sol"; -import {FeesCalcHarness} from "./harnesses/FeesCalcHarness.sol"; -import {TokenId} from "@types/TokenId.sol"; -import {LeftRightUnsigned, LeftRightSigned} from "@types/LeftRight.sol"; -import {LiquidityAmounts} from "@uniswap/v3-periphery/contracts/libraries/LiquidityAmounts.sol"; -import {LiquidityChunk} from "@types/LiquidityChunk.sol"; -// Uniswap -import {IUniswapV3Pool} from "v3-core/interfaces/IUniswapV3Pool.sol"; -import {FixedPoint96} from "v3-core/libraries/FixedPoint96.sol"; -import {FixedPoint128} from "v3-core/libraries/FixedPoint128.sol"; -// Test util -import {PositionUtils} from "../testUtils/PositionUtils.sol"; - -/** - * Test the FeesCalc functionality with Foundry and Fuzzing. - * - * @author Axicon Labs Limited - */ -contract FeesCalcTest is Test, PositionUtils { - // harness - FeesCalcHarness harness; - - // store a few different mainnet pairs - the pool used is part of the fuzz - IUniswapV3Pool constant USDC_WETH_5 = - IUniswapV3Pool(0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640); - IUniswapV3Pool constant WBTC_ETH_30 = - IUniswapV3Pool(0xCBCdF9626bC03E24f779434178A73a0B4bad62eD); - IUniswapV3Pool constant USDC_WETH_30 = - IUniswapV3Pool(0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8); - IUniswapV3Pool[3] public pools = [USDC_WETH_5, WBTC_ETH_30, USDC_WETH_30]; - - // cache tick data - uint160 sqrtPriceAt; - int24 tickSpacing; - int24 currentTick; - - // current run selected pool - IUniswapV3Pool selectedPool; - - function setUp() public { - harness = new FeesCalcHarness(); - } - - // above/below/in-range branches - function test_Success_calculateAMMSwapFeesLiquidityChunk( - uint256 poolIdSeed, - uint256 optionRatioSeed, - uint256 assetSeed, - uint256 isLongSeed, - uint256 tokenTypeSeed, - int256 strikeSeed, - int256 widthSeed, - uint64 positionSize, - uint64 startingLiquidity - ) public { - vm.assume(positionSize != 0); - TokenId tokenId; - selectedPool = pools[bound(poolIdSeed, 0, 2)]; - - { - // construct one leg token - tokenId = fuzzedPosition( - 1, // total amount of legs - poolIdSeed, - optionRatioSeed, - assetSeed, - isLongSeed, - tokenTypeSeed, - strikeSeed, - widthSeed - ); - } - - LiquidityChunk liquidityChunk = PanopticMath.getLiquidityChunk(tokenId, 0, positionSize); - - (uint256 ammFeesPerLiqToken0X128, uint256 ammFeesPerLiqToken1X128) = harness - .getAMMSwapFeesPerLiquidityCollected( - selectedPool, - currentTick, - liquidityChunk.tickLower(), - liquidityChunk.tickUpper() - ); - - LeftRightSigned expectedFeesEachToken; - expectedFeesEachToken = expectedFeesEachToken - .toRightSlot(int128(int256(Math.mulDiv128(ammFeesPerLiqToken0X128, startingLiquidity)))) - .toLeftSlot(int128(int256(Math.mulDiv128(ammFeesPerLiqToken1X128, startingLiquidity)))); - - LeftRightSigned returnedFeesEachToken = harness.calculateAMMSwapFees( - selectedPool, - currentTick, - liquidityChunk.tickLower(), - liquidityChunk.tickUpper(), - startingLiquidity - ); - - assertEq( - LeftRightSigned.unwrap(expectedFeesEachToken), - LeftRightSigned.unwrap(returnedFeesEachToken) - ); - } - - // returns token containing 'totalLegs' amount of legs - // i.e totalLegs of 1 has a tokenId with 1 legs - // uses a seed to fuzz data so that there is different data for each leg - function fuzzedPosition( - uint256 totalLegs, - uint256 poolIdSeed, - uint256 optionRatioSeed, - uint256 assetSeed, - uint256 isLongSeed, - uint256 tokenTypeSeed, - int256 strikeSeed, - int256 widthSeed - ) internal returns (TokenId) { - tickSpacing = selectedPool.tickSpacing(); - // add univ3pool to token - uint64 poolId = uint64( - ((uint64(bound(poolIdSeed, 1, type(uint64).max)) >> 16)) + - (uint64(uint24(tickSpacing)) << 48) - ); - TokenId tokenId = TokenId.wrap(uint256(poolId)); - - for (uint256 legIndex; legIndex < totalLegs; legIndex++) { - // We don't want the same data for each leg - // int divide each seed by the current legIndex - // gives us a pseudorandom seed - // forge bound does not randomize the output - { - uint256 randomizer = legIndex + 1; - - optionRatioSeed = optionRatioSeed / randomizer; - assetSeed = assetSeed / randomizer; - isLongSeed = isLongSeed / randomizer; - tokenTypeSeed = tokenTypeSeed / randomizer; - strikeSeed = strikeSeed / int24(int256(randomizer)); - widthSeed = widthSeed / int24(int256(randomizer)); - } - - { - // the following are all 1 bit so mask them: - uint16 MASK = 0x1; // takes first 1 bit of the uint16 - assetSeed = assetSeed & MASK; - isLongSeed = isLongSeed & MASK; - tokenTypeSeed = tokenTypeSeed & MASK; - } - - /// bound inputs - int24 strike; - int24 width; - { - // the following must be at least 1 - optionRatioSeed = bound(optionRatioSeed, 1, 127); - - width = int24(bound(widthSeed, 1, 4094)); - int24 oneSidedRange = (width * tickSpacing) / 2; - - (int24 strikeOffset, int24 minTick, int24 maxTick) = PositionUtils.getContextFull( - uint256(uint24(tickSpacing)), - currentTick, - width - ); - - int24 lowerBound = int24(minTick + oneSidedRange - strikeOffset); - int24 upperBound = int24(maxTick - oneSidedRange - strikeOffset); - - // Set current tick and pool price - currentTick = int24(bound(currentTick, minTick, maxTick)); - sqrtPriceAt = TickMath.getSqrtRatioAtTick(currentTick); - - // bound strike - strike = int24( - bound(strikeSeed, lowerBound / tickSpacing, upperBound / tickSpacing) - ); - strike = int24(strike * tickSpacing + strikeOffset); - } - - { - // add a leg - // no risk partner by default (will reference its own leg index) - tokenId = tokenId.addLeg( - legIndex, - optionRatioSeed, - assetSeed, - isLongSeed, - tokenTypeSeed, - legIndex, - strike, - width - ); - } - } - - return tokenId; - } - - function addBalance(TokenId tokenId, uint128 balance) public { - harness.addBalance(tokenId, balance); - } -} diff --git a/test/foundry/libraries/PanopticMath.t.sol b/test/foundry/libraries/PanopticMath.t.sol index cbc4785..f81f7a6 100644 --- a/test/foundry/libraries/PanopticMath.t.sol +++ b/test/foundry/libraries/PanopticMath.t.sol @@ -14,6 +14,7 @@ import {LeftRightUnsigned, LeftRightSigned} from "@types/LeftRight.sol"; import {PanopticMath} from "@libraries/PanopticMath.sol"; import {Math} from "@libraries/Math.sol"; // Uniswap +import {IV3CompatibleOracle} from "@interfaces/IV3CompatibleOracle.sol"; import {IUniswapV3Pool} from "v3-core/interfaces/IUniswapV3Pool.sol"; import {LiquidityAmounts} from "v3-periphery/libraries/LiquidityAmounts.sol"; import {FixedPoint96} from "v3-core/libraries/FixedPoint96.sol"; @@ -25,6 +26,10 @@ import {UniPoolPriceMock} from "../testUtils/PriceMocks.sol"; import {UniPoolObservationMock} from "../testUtils/PriceMocks.sol"; import {LiquidityChunk, LiquidityChunkLibrary} from "@types/LiquidityChunk.sol"; +import {PoolKey} from "v4-core/types/PoolKey.sol"; +import {PoolId} from "v4-core/types/PoolId.sol"; +import {Currency} from "v4-core/types/Currency.sol"; +import {IHooks} from "v4-core/interfaces/IHooks.sol"; /** * Test the PanopticMath functionality with Foundry and Fuzzing. @@ -37,20 +42,20 @@ contract PanopticMathTest is Test, PositionUtils { PanopticMathHarness harness; // store a few different mainnet pairs - the pool used is part of the fuzz - IUniswapV3Pool constant USDC_WETH_5 = - IUniswapV3Pool(0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640); - IUniswapV3Pool constant WBTC_ETH_30 = - IUniswapV3Pool(0xCBCdF9626bC03E24f779434178A73a0B4bad62eD); - IUniswapV3Pool constant USDC_WETH_30 = - IUniswapV3Pool(0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8); - IUniswapV3Pool[3] public pools = [USDC_WETH_5, WBTC_ETH_30, USDC_WETH_30]; + IV3CompatibleOracle constant USDC_WETH_5 = + IV3CompatibleOracle(0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640); + IV3CompatibleOracle constant WBTC_ETH_30 = + IV3CompatibleOracle(0xCBCdF9626bC03E24f779434178A73a0B4bad62eD); + IV3CompatibleOracle constant USDC_WETH_30 = + IV3CompatibleOracle(0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8); + IV3CompatibleOracle[3] public pools = [USDC_WETH_5, WBTC_ETH_30, USDC_WETH_30]; function setUp() public { harness = new PanopticMathHarness(); } // use storage as temp to avoid stack to deeps - IUniswapV3Pool selectedPool; + IV3CompatibleOracle selectedPool; int24 tickSpacing; int24 currentTick; @@ -82,7 +87,7 @@ contract PanopticMathTest is Test, PositionUtils { // bound fuzzed tick selectedPool = pools[bound(positionSize, 0, 2)]; // reuse position size as seed - tickSpacing = selectedPool.tickSpacing(); + tickSpacing = IUniswapV3Pool(address(selectedPool)).tickSpacing(); width = int24(bound(width, 1, 2048)); int24 oneSidedRange = (width * tickSpacing) / 2; @@ -156,7 +161,7 @@ contract PanopticMathTest is Test, PositionUtils { // bound fuzzed tick selectedPool = pools[bound(positionSize, 0, 2)]; // reuse position size as seed - tickSpacing = selectedPool.tickSpacing(); + tickSpacing = IUniswapV3Pool(address(selectedPool)).tickSpacing(); width = int24(bound(width, 1, 2048)); int24 oneSidedRange = (width * tickSpacing) / 2; @@ -208,37 +213,13 @@ contract PanopticMathTest is Test, PositionUtils { ); } - function test_Success_getPoolId(address univ3pool, uint256 _tickSpacing) public { - vm.assume( - univ3pool > address(10) && - univ3pool != address(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D) && - univ3pool != address(0x000000000000000000636F6e736F6c652e6c6f67) && - univ3pool != address(harness) - ); + function test_Success_getPoolId(PoolId poolId, uint256 _tickSpacing) public { _tickSpacing = bound(_tickSpacing, 0, uint16(type(int16).max)); - UniPoolPriceMock pm = new UniPoolPriceMock(); - vm.etch(univ3pool, address(pm).code); - pm = UniPoolPriceMock(univ3pool); - - pm.construct( - UniPoolPriceMock.Slot0({ - sqrtPriceX96: 0, - tick: 0, - observationIndex: 0, - observationCardinality: 0, - observationCardinalityNext: 0, - feeProtocol: 0, - unlocked: false - }), - address(0), - address(0), - 0, - int24(uint24(_tickSpacing)) + assertEq( + (uint64(_tickSpacing) << 48) + uint48(uint256(PoolId.unwrap(poolId))), + harness.getPoolId(poolId, int24(uint24(_tickSpacing))) ); - uint64 poolPattern = uint64(uint160(univ3pool) >> 112); - _tickSpacing <<= 48; - assertEq(_tickSpacing + poolPattern, harness.getPoolId(univ3pool)); } function test_Success_getTicks_normalTickRange( @@ -248,7 +229,7 @@ contract PanopticMathTest is Test, PositionUtils { ) public { // bound fuzzed tick selectedPool = pools[bound(poolSeed, 0, 2)]; - tickSpacing = selectedPool.tickSpacing(); + tickSpacing = IUniswapV3Pool(address(selectedPool)).tickSpacing(); // Width must be > 0 < 4096 int24 width = int24(uint24(bound(widthSeed, 1, 4095))); @@ -280,7 +261,7 @@ contract PanopticMathTest is Test, PositionUtils { ) public { // bound fuzzed tick selectedPool = pools[bound(poolSeed, 0, 2)]; - tickSpacing = selectedPool.tickSpacing(); + tickSpacing = IUniswapV3Pool(address(selectedPool)).tickSpacing(); // Width must be > 0 < 4096 int24 width = int24(uint24(bound(widthSeed, 1, 4095))); @@ -310,7 +291,7 @@ contract PanopticMathTest is Test, PositionUtils { ) public { // bound fuzzed tick selectedPool = pools[bound(poolSeed, 0, 2)]; - tickSpacing = selectedPool.tickSpacing(); + tickSpacing = IUniswapV3Pool(address(selectedPool)).tickSpacing(); // Width must be > 0 < 4096 int24 width = int24(uint24(bound(widthSeed, 1, 4095))); int24 oneSidedRange = (width * tickSpacing) / 2; @@ -338,7 +319,7 @@ contract PanopticMathTest is Test, PositionUtils { ) public { // bound fuzzed tick selectedPool = pools[bound(poolSeed, 0, 2)]; - tickSpacing = selectedPool.tickSpacing(); + tickSpacing = IUniswapV3Pool(address(selectedPool)).tickSpacing(); // Width must be > 0 < 4095 (4095 is full range) int24 width = int24(int256(bound(widthSeed, 1, 4094))); int24 oneSidedRange = (width * tickSpacing) / 2; @@ -395,7 +376,7 @@ contract PanopticMathTest is Test, PositionUtils { // bound fuzzed tick selectedPool = pools[bound(positionSize, 0, 2)]; // reuse position size as seed - tickSpacing = selectedPool.tickSpacing(); + tickSpacing = IUniswapV3Pool(address(selectedPool)).tickSpacing(); width = int24(bound(width, 1, 2048)); int24 oneSidedRange = (width * tickSpacing) / 2; @@ -461,7 +442,7 @@ contract PanopticMathTest is Test, PositionUtils { // bound fuzzed tick selectedPool = pools[bound(optionRatio, 0, 2)]; // reuse optionRatio as seed - tickSpacing = selectedPool.tickSpacing(); + tickSpacing = IUniswapV3Pool(address(selectedPool)).tickSpacing(); width = int24(bound(width, 1, 2048)); int24 oneSidedRange = (width * tickSpacing) / 2; @@ -520,7 +501,7 @@ contract PanopticMathTest is Test, PositionUtils { // bound fuzzed tick selectedPool = pools[bound(optionRatio, 0, 2)]; // reuse optionRatio as seed - tickSpacing = selectedPool.tickSpacing(); + tickSpacing = IUniswapV3Pool(address(selectedPool)).tickSpacing(); width = int24(bound(width, 1, 2048)); int24 oneSidedRange = (width * tickSpacing) / 2; @@ -561,6 +542,7 @@ contract PanopticMathTest is Test, PositionUtils { assertEq(expectedHash, returnedHash); } + /// forge-config: ci_test.fuzz.runs = 1 function test_Success_getLastMedianObservation( uint256 observationIndex, int256[100] memory ticks, @@ -569,8 +551,8 @@ contract PanopticMathTest is Test, PositionUtils { uint256 cardinality, uint256 period ) public { - // skip because out of gas vm.skip(true); + cardinality = bound(cardinality, 1, 50); cardinality = cardinality * 2 - 1; period = bound(period, 1, 100 / cardinality); @@ -627,7 +609,7 @@ contract PanopticMathTest is Test, PositionUtils { } assertEq( harness.computeMedianObservedPrice( - IUniswapV3Pool(address(mockPool)), + IV3CompatibleOracle(address(mockPool)), observationIndex, observationCardinality, cardinality, @@ -1177,7 +1159,7 @@ contract PanopticMathTest is Test, PositionUtils { // bound fuzzed tick selectedPool = pools[bound(optionRatio, 0, 2)]; // reuse optionRatio as seed - tickSpacing = selectedPool.tickSpacing(); + tickSpacing = IUniswapV3Pool(address(selectedPool)).tickSpacing(); width = int24(bound(width, 1, 2048)); @@ -1260,7 +1242,7 @@ contract PanopticMathTest is Test, PositionUtils { tokenType = tokenType & MASK; // bound fuzzed tick - tickSpacing = selectedPool.tickSpacing(); + tickSpacing = IUniswapV3Pool(address(selectedPool)).tickSpacing(); width = int24(bound(width, 1, 2048)); @@ -1343,7 +1325,7 @@ contract PanopticMathTest is Test, PositionUtils { // bound fuzzed tick selectedPool = pools[bound(optionRatio, 0, 2)]; // reuse optionRatio as seed - tickSpacing = selectedPool.tickSpacing(); + tickSpacing = IUniswapV3Pool(address(selectedPool)).tickSpacing(); width = int24(bound(width, 1, 2048)); int24 oneSidedRange = (width * tickSpacing) / 2; @@ -1401,7 +1383,7 @@ contract PanopticMathTest is Test, PositionUtils { // bound fuzzed tick selectedPool = pools[bound(optionRatio, 0, 2)]; // reuse optionRatio as seed - tickSpacing = selectedPool.tickSpacing(); + tickSpacing = IUniswapV3Pool(address(selectedPool)).tickSpacing(); width = int24(bound(width, 1, 2048)); int24 oneSidedRange = (width * tickSpacing) / 2; @@ -1473,7 +1455,7 @@ contract PanopticMathTest is Test, PositionUtils { // bound fuzzed tick selectedPool = pools[bound(optionRatio, 0, 2)]; // reuse optionRatio as seed - tickSpacing = selectedPool.tickSpacing(); + tickSpacing = IUniswapV3Pool(address(selectedPool)).tickSpacing(); width = int24(bound(width, 1, 2048)); int24 oneSidedRange = (width * tickSpacing) / 2; @@ -1532,7 +1514,7 @@ contract PanopticMathTest is Test, PositionUtils { // bound fuzzed tick selectedPool = pools[bound(optionRatio, 0, 2)]; // reuse optionRatio as seed - tickSpacing = selectedPool.tickSpacing(); + tickSpacing = IUniswapV3Pool(address(selectedPool)).tickSpacing(); width = int24(bound(width, 1, 2048)); int24 oneSidedRange = (width * tickSpacing) / 2; diff --git a/test/foundry/libraries/SafeTransferLib.t.sol b/test/foundry/libraries/SafeTransferLib.t.sol index 60b88d2..7bf0488 100644 --- a/test/foundry/libraries/SafeTransferLib.t.sol +++ b/test/foundry/libraries/SafeTransferLib.t.sol @@ -1,18 +1,18 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; -import {RevertingToken} from "solmate/test/utils/weird-tokens/RevertingToken.sol"; -import {ReturnsTwoToken} from "solmate/test/utils/weird-tokens/ReturnsTwoToken.sol"; -import {ReturnsFalseToken} from "solmate/test/utils/weird-tokens/ReturnsFalseToken.sol"; -import {MissingReturnToken} from "solmate/test/utils/weird-tokens/MissingReturnToken.sol"; -import {ReturnsTooMuchToken} from "solmate/test/utils/weird-tokens/ReturnsTooMuchToken.sol"; -import {ReturnsGarbageToken} from "solmate/test/utils/weird-tokens/ReturnsGarbageToken.sol"; -import {ReturnsTooLittleToken} from "solmate/test/utils/weird-tokens/ReturnsTooLittleToken.sol"; - -import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol"; - -import {ERC20} from "solmate/tokens/ERC20.sol"; +import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol"; +import {RevertingToken} from "solmate/src/test/utils/weird-tokens/RevertingToken.sol"; +import {ReturnsTwoToken} from "solmate/src/test/utils/weird-tokens/ReturnsTwoToken.sol"; +import {ReturnsFalseToken} from "solmate/src/test/utils/weird-tokens/ReturnsFalseToken.sol"; +import {MissingReturnToken} from "solmate/src/test/utils/weird-tokens/MissingReturnToken.sol"; +import {ReturnsTooMuchToken} from "solmate/src/test/utils/weird-tokens/ReturnsTooMuchToken.sol"; +import {ReturnsGarbageToken} from "solmate/src/test/utils/weird-tokens/ReturnsGarbageToken.sol"; +import {ReturnsTooLittleToken} from "solmate/src/test/utils/weird-tokens/ReturnsTooLittleToken.sol"; + +import {DSTestPlus} from "solmate/src/test/utils/DSTestPlus.sol"; + +import {ERC20} from "solmate/src/tokens/ERC20.sol"; import {SafeTransferLib} from "@libraries/SafeTransferLib.sol"; contract SafeTransferLibTest is DSTestPlus { diff --git a/test/foundry/libraries/harnesses/CallbackLibHarness.sol b/test/foundry/libraries/harnesses/CallbackLibHarness.sol deleted file mode 100644 index 44a719c..0000000 --- a/test/foundry/libraries/harnesses/CallbackLibHarness.sol +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.24; - -import {CallbackLib} from "@libraries/CallbackLib.sol"; -import {IUniswapV3Factory} from "v3-core/interfaces/IUniswapV3Factory.sol"; - -/// @title CallbackLib: A harness to expose the CallbackLib library for code coverage analysis. -/// @notice Replicates the interface of the CallbackLib library, passing through any function calls -/// @author Axicon Labs Limited -contract CallbackLibHarness { - function validateCallback( - address sender, - IUniswapV3Factory factory, - CallbackLib.PoolFeatures memory features - ) public view { - CallbackLib.validateCallback(sender, factory, features); - } -} diff --git a/test/foundry/libraries/harnesses/FeesCalcHarness.sol b/test/foundry/libraries/harnesses/FeesCalcHarness.sol deleted file mode 100644 index 3b24b0e..0000000 --- a/test/foundry/libraries/harnesses/FeesCalcHarness.sol +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.24; - -import {IUniswapV3Pool} from "univ3-core/interfaces/IUniswapV3Pool.sol"; -import {FeesCalc} from "@libraries/FeesCalc.sol"; - -import {LeftRightUnsigned, LeftRightSigned} from "@types/LeftRight.sol"; -import {TokenId} from "@types/TokenId.sol"; - -/// @title FeesCalcHarness: A harness to expose the Feescalc library for code coverage analysis. -/// @notice Replicates the interface of the Feescalc library, passing through any function calls -/// @author Axicon Labs Limited -contract FeesCalcHarness { - // used to pass into libraries - mapping(TokenId tokenId => LeftRightUnsigned balance) public userBalance; - - function calculateAMMSwapFees( - IUniswapV3Pool univ3pool, - int24 currentTick, - int24 tickLower, - int24 tickUpper, - uint128 liquidity - ) public view returns (LeftRightSigned) { - LeftRightSigned feesEachToken = FeesCalc.calculateAMMSwapFees( - univ3pool, - currentTick, - tickLower, - tickUpper, - liquidity - ); - return (feesEachToken); - } - - function getAMMSwapFeesPerLiquidityCollected( - IUniswapV3Pool univ3pool, - int24 currentTick, - int24 tickLower, - int24 tickUpper - ) public view returns (uint256, uint256) { - (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = FeesCalc - ._getAMMSwapFeesPerLiquidityCollected(univ3pool, currentTick, tickLower, tickUpper); - - return (feeGrowthInside0X128, feeGrowthInside1X128); - } - - function addBalance(TokenId tokenId, uint128 balance) public { - userBalance[tokenId] = LeftRightUnsigned.wrap(0).toRightSlot(balance); - } -} diff --git a/test/foundry/libraries/harnesses/PanopticMathHarness.sol b/test/foundry/libraries/harnesses/PanopticMathHarness.sol index 02e5124..69454d6 100644 --- a/test/foundry/libraries/harnesses/PanopticMathHarness.sol +++ b/test/foundry/libraries/harnesses/PanopticMathHarness.sol @@ -4,13 +4,15 @@ pragma solidity ^0.8.24; // Internal import {PanopticMath} from "@libraries/PanopticMath.sol"; // Uniswap -import {IUniswapV3Pool} from "v3-core/interfaces/IUniswapV3Pool.sol"; +import {IV3CompatibleOracle} from "@interfaces/IV3CompatibleOracle.sol"; // Types import {LiquidityChunk} from "@types/LiquidityChunk.sol"; import {TokenId} from "@types/TokenId.sol"; import {LeftRightUnsigned, LeftRightSigned} from "@types/LeftRight.sol"; import "forge-std/Test.sol"; +import {PoolKey} from "v4-core/types/PoolKey.sol"; +import {PoolId} from "v4-core/types/PoolId.sol"; /// @title PanopticMathHarness: A harness to expose the PanopticMath library for code coverage analysis. /// @notice Replicates the interface of the PanopticMath library, passing through any function calls @@ -44,8 +46,8 @@ contract PanopticMathHarness is Test { return (tickLower, tickUpper); } - function getPoolId(address univ3pool) public view returns (uint64) { - uint64 poolId = PanopticMath.getPoolId(univ3pool); + function getPoolId(PoolId idV4, int24 tickSpacing) public pure returns (uint64) { + uint64 poolId = PanopticMath.getPoolId(idV4, tickSpacing); return poolId; } @@ -77,13 +79,16 @@ contract PanopticMathHarness is Test { return newHash; } - function twapFilter(IUniswapV3Pool univ3pool, uint32 twapWindow) public view returns (int24) { + function twapFilter( + IV3CompatibleOracle univ3pool, + uint32 twapWindow + ) public view returns (int24) { int24 twapTick = PanopticMath.twapFilter(univ3pool, twapWindow); return twapTick; } function computeMedianObservedPrice( - IUniswapV3Pool univ3pool, + IV3CompatibleOracle univ3pool, uint256 observationIndex, uint256 observationCardinality, uint256 cardinality, diff --git a/test/foundry/poc/POC.t.sol b/test/foundry/poc/POC.t.sol new file mode 100644 index 0000000..ad187a2 --- /dev/null +++ b/test/foundry/poc/POC.t.sol @@ -0,0 +1,202 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; +import {SemiFungiblePositionManager} from "@contracts/SemiFungiblePositionManager.sol"; +import {PanopticPool} from "@contracts/PanopticPool.sol"; +import {CollateralTracker} from "@contracts/CollateralTracker.sol"; +import {PanopticFactory} from "@contracts/PanopticFactory.sol"; +import {IERC20Partial} from "@tokens/interfaces/IERC20Partial.sol"; +import {PanopticHelper} from "@test_periphery/PanopticHelper.sol"; +import {ISwapRouter} from "v3-periphery/interfaces/ISwapRouter.sol"; +import {IUniswapV3Factory} from "v3-core/interfaces/IUniswapV3Factory.sol"; +import {IUniswapV3Pool} from "v3-core/interfaces/IUniswapV3Pool.sol"; +import {TickMath} from "v3-core/libraries/TickMath.sol"; +import {TokenId} from "@types/TokenId.sol"; +import {LeftRightUnsigned, LeftRightSigned} from "@types/LeftRight.sol"; +import {PanopticMath} from "@libraries/PanopticMath.sol"; +import {SafeTransferLib} from "@libraries/SafeTransferLib.sol"; +import {PositionUtils} from "../testUtils/PositionUtils.sol"; +import {Math} from "@libraries/Math.sol"; +import {IV3CompatibleOracle} from "@interfaces/IV3CompatibleOracle.sol"; +import {Errors} from "@libraries/Errors.sol"; +import {FixedPointMathLib} from "solmate/src/utils/FixedPointMathLib.sol"; +import {Constants} from "@libraries/Constants.sol"; +import {Pointer} from "@types/Pointer.sol"; +import {ERC20} from "solmate/src/tokens/ERC20.sol"; +import {ClonesWithImmutableArgs} from "clones-with-immutable-args/ClonesWithImmutableArgs.sol"; +import {SwapperC} from "../core/Misc.t.sol"; +import {ERC20S} from "../core/Misc.t.sol"; +// V4 types +import {PoolId} from "v4-core/types/PoolId.sol"; +import {PoolKey} from "v4-core/types/PoolKey.sol"; +import {StateLibrary} from "v4-core/libraries/StateLibrary.sol"; +import {V4StateReader} from "@libraries/V4StateReader.sol"; +import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol"; +import {BalanceDelta} from "v4-core/types/BalanceDelta.sol"; +import {Currency} from "v4-core/types/Currency.sol"; +import {PoolManager} from "v4-core/PoolManager.sol"; +import {IHooks} from "v4-core/interfaces/IHooks.sol"; +import {V4RouterSimple} from "../testUtils/V4RouterSimple.sol"; + +contract POC_Test is Test, PositionUtils { + address Deployer = address(0x1234); + address Alice = address(0x123456); + address Bob = address(0x12345678); + address Swapper = address(0x123456789); + address Charlie = address(0x1234567891); + address Seller = address(0x12345678912); + address Eve = address(0x123456789123); + + // the instance of SFPM we are testing + SemiFungiblePositionManager sfpm; + + // reference implemenatations used by the factory + address poolReference; + + address collateralReference; + + // Mainnet factory address - SFPM is dependent on this for several checks and callbacks + IUniswapV3Factory V3FACTORY = IUniswapV3Factory(0x1F98431c8aD98523631AE4a59f267346ea31F984); + + // Mainnet router address - used for swaps to test fees/premia + ISwapRouter router = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); + + PanopticFactory factory; + PanopticPool pp; + CollateralTracker ct0; + CollateralTracker ct1; + PanopticHelper ph; + + IPoolManager manager; + + V4RouterSimple routerV4; + + PoolKey poolKey; + + IUniswapV3Pool uniPool; + ERC20S token0; + ERC20S token1; + + SwapperC swapperc; + + // creates a new PanopticPool with corresponding Uniswap pool on a mainnet fork + // see test/foundry/misc.t.sol for example usage + function setUp() public { + vm.startPrank(Deployer); + + manager = IPoolManager(address(new PoolManager())); + + routerV4 = new V4RouterSimple(manager); + + sfpm = new SemiFungiblePositionManager(manager); + + ph = new PanopticHelper(sfpm); + + // deploy reference pool and collateral token + poolReference = address(new PanopticPool(sfpm, manager)); + collateralReference = address( + new CollateralTracker(10, 2_000, 1_000, -1_024, 5_000, 9_000, 20, manager) + ); + token0 = new ERC20S("token0", "T0", 18); + token1 = new ERC20S("token1", "T1", 18); + uniPool = IUniswapV3Pool(V3FACTORY.createPool(address(token0), address(token1), 500)); + + poolKey = PoolKey( + Currency.wrap(address(token0)), + Currency.wrap(address(token1)), + 500, + 10, + IHooks(address(0)) + ); + + swapperc = new SwapperC(); + vm.startPrank(Swapper); + token0.mint(Swapper, type(uint248).max); + token1.mint(Swapper, type(uint248).max); + token0.approve(address(swapperc), type(uint248).max); + token1.approve(address(swapperc), type(uint248).max); + token0.approve(address(routerV4), type(uint248).max); + token1.approve(address(routerV4), type(uint248).max); + + IUniswapV3Pool(uniPool).initialize(2 ** 96); + + IUniswapV3Pool(uniPool).increaseObservationCardinalityNext(100); + + // move back to price=1 while generating 100 observations (min required for pool to function) + for (uint256 i = 0; i < 100; ++i) { + vm.warp(block.timestamp + 1); + vm.roll(block.number + 1); + swapperc.mint(uniPool, -887200, 887200, 10 ** 18); + swapperc.burn(uniPool, -887200, 887200, 10 ** 18); + } + swapperc.mint(uniPool, -887270, 887270, 10 ** 18); + + swapperc.swapTo(uniPool, 10 ** 17 * 2 ** 96); + + manager.initialize(poolKey, 10 ** 17 * 2 ** 96); + + swapperc.burn(uniPool, -887270, 887270, 10 ** 18); + vm.startPrank(Deployer); + + factory = new PanopticFactory( + address(token1), + sfpm, + manager, + poolReference, + collateralReference, + new bytes32[](0), + new uint256[][](0), + new Pointer[][](0) + ); + + token0.mint(Deployer, type(uint104).max); + token1.mint(Deployer, type(uint104).max); + token0.approve(address(factory), type(uint104).max); + token1.approve(address(factory), type(uint104).max); + + pp = PanopticPool( + address( + factory.deployNewPool( + IV3CompatibleOracle(address(uniPool)), + poolKey, + uint96(block.timestamp), + type(uint256).max, + type(uint256).max + ) + ) + ); + + vm.startPrank(Swapper); + swapperc.swapTo(uniPool, 2 ** 96); + routerV4.swapTo(address(0), poolKey, 2 ** 96); + + // Update median + pp.pokeMedian(); + vm.warp(block.timestamp + 120); + vm.roll(block.number + 10); + + pp.pokeMedian(); + vm.warp(block.timestamp + 120); + vm.roll(block.number + 10); + + pp.pokeMedian(); + vm.warp(block.timestamp + 120); + vm.roll(block.number + 10); + + pp.pokeMedian(); + vm.warp(block.timestamp + 120); + vm.roll(block.number + 10); + + pp.pokeMedian(); + vm.warp(block.timestamp + 120); + vm.roll(block.number + 10); + + ct0 = pp.collateralToken0(); + ct1 = pp.collateralToken1(); + + vm.stopPrank(); + } + + function test_POC() external {} +} diff --git a/test/foundry/testUtils/PositionUtils.sol b/test/foundry/testUtils/PositionUtils.sol index df22b72..1644802 100644 --- a/test/foundry/testUtils/PositionUtils.sol +++ b/test/foundry/testUtils/PositionUtils.sol @@ -14,6 +14,16 @@ import {PanopticMath} from "@libraries/PanopticMath.sol"; import {CollateralTracker} from "@contracts/CollateralTracker.sol"; import {Math} from "@libraries/Math.sol"; import {LeftRightUnsigned, LeftRightSigned} from "@types/LeftRight.sol"; +import {PoolId} from "v4-core/types/PoolId.sol"; +import {PoolKey} from "v4-core/types/PoolKey.sol"; +import {StateLibrary} from "v4-core/libraries/StateLibrary.sol"; +import {V4StateReader} from "@libraries/V4StateReader.sol"; +import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol"; +import {BalanceDelta} from "v4-core/types/BalanceDelta.sol"; +import {Currency} from "v4-core/types/Currency.sol"; +import {PoolManager} from "v4-core/PoolManager.sol"; +import {IHooks} from "v4-core/interfaces/IHooks.sol"; +import {V4RouterSimple} from "../testUtils/V4RouterSimple.sol"; contract MiniPositionManager { struct CallbackData { @@ -677,15 +687,13 @@ contract PositionUtils is Test { vm.revertTo(snapshot); } + // UPD function simulateSwap( - IUniswapV3Pool uniPool, + PoolKey memory key, int24 tickLower, int24 tickUpper, uint128 liquidity, - ISwapRouter router, - address token0, - address token1, - uint24 fee, + V4RouterSimple routerV4, bool zeroForOne, int256 amountSpecified ) public returns (uint256, uint256) { @@ -693,159 +701,67 @@ contract PositionUtils is Test { vm.startPrank(address(0x123456789)); - deal(token0, address(0x123456789), type(uint128).max); - deal(token1, address(0x123456789), type(uint128).max); - - MiniPositionManager pm = new MiniPositionManager(); - - IERC20Partial(token0).approve(address(router), type(uint256).max); - IERC20Partial(token1).approve(address(router), type(uint256).max); - IERC20Partial(token0).approve(address(pm), type(uint256).max); - IERC20Partial(token1).approve(address(pm), type(uint256).max); - - pm.mintLiquidity(uniPool, tickLower, tickUpper, liquidity, address(0x123456789)); - - if (amountSpecified > 0) { - uint256 amountOut = router.exactInputSingle( - ISwapRouter.ExactInputSingleParams({ - tokenIn: zeroForOne ? token0 : token1, - tokenOut: zeroForOne ? token1 : token0, - fee: fee, - recipient: address(0x123456789), - deadline: block.timestamp, - amountIn: uint256(amountSpecified), - amountOutMinimum: 0, - sqrtPriceLimitX96: 0 - }) - ); - - vm.revertTo(0); - - return - zeroForOne - ? (uint256(amountSpecified), amountOut) - : (amountOut, uint256(amountSpecified)); - } else { - uint256 amountIn = router.exactOutputSingle( - ISwapRouter.ExactOutputSingleParams({ - tokenIn: zeroForOne ? token0 : token1, - tokenOut: zeroForOne ? token1 : token0, - fee: fee, - recipient: address(0x123456789), - deadline: block.timestamp, - amountOut: uint256(-amountSpecified), - amountInMaximum: type(uint256).max, - sqrtPriceLimitX96: 0 - }) - ); + routerV4.modifyLiquidity(address(0), key, tickLower, tickUpper, int128(liquidity)); - vm.revertTo(0); + (int256 delta0, int256 delta1) = routerV4.swap( + address(0), + key, + amountSpecified, + zeroForOne + ); - return - zeroForOne - ? (amountIn, uint256(-amountSpecified)) - : (uint256(-amountSpecified), amountIn); - } + vm.revertTo(0); + return (uint256(Math.abs(delta0)), uint256(Math.abs(delta1))); } + // UPD function simulateSwap( - IUniswapV3Pool uniPool, + PoolKey memory key, int24 tickLower, int24 tickUpper, int128 liquidity, - ISwapRouter router, - address token0, - address token1, - uint24 fee, + bytes32 positionKey, + V4RouterSimple routerV4, bool zeroForOne, int256 amountSpecified - ) public returns (uint256, uint256) { + ) external returns (uint256, uint256) { snap = vm.snapshot(); - (, caller, ) = vm.readCallers(); - - deal(token0, address(caller), type(uint128).max); - deal(token1, address(caller), type(uint128).max); - - MiniPositionManager pm = new MiniPositionManager(); + vm.startPrank(address(0x123456789)); // make it so we can burn existing liq from caller - vm.etch(caller, address(pm).code); - - IERC20Partial(token0).approve(address(router), type(uint256).max); - IERC20Partial(token1).approve(address(router), type(uint256).max); - IERC20Partial(token0).approve(address(caller), type(uint256).max); - IERC20Partial(token1).approve(address(caller), type(uint256).max); - - if (liquidity > 0) { - MiniPositionManager(caller).mintLiquidity( - uniPool, - tickLower, - tickUpper, - uint128(liquidity), - caller - ); - } else { - MiniPositionManager(caller).burnLiquidity( - uniPool, - tickLower, - tickUpper, - uint128(-liquidity) - ); - } + vm.etch(msg.sender, address(routerV4).code); - if (amountSpecified > 0) { - uint256 amountOut = router.exactInputSingle( - ISwapRouter.ExactInputSingleParams({ - tokenIn: zeroForOne ? token0 : token1, - tokenOut: zeroForOne ? token1 : token0, - fee: fee, - recipient: caller, - deadline: block.timestamp, - amountIn: uint256(amountSpecified), - amountOutMinimum: 0, - sqrtPriceLimitX96: 0 - }) - ); + IERC20Partial(Currency.unwrap(key.currency0)).approve(msg.sender, type(uint256).max); + IERC20Partial(Currency.unwrap(key.currency1)).approve(msg.sender, type(uint256).max); - vm.revertTo(snap); + V4RouterSimple(msg.sender).modifyLiquidityWithSalt( + address(0), + key, + tickLower, + tickUpper, + liquidity, + positionKey + ); - return - zeroForOne - ? (uint256(amountSpecified), amountOut) - : (amountOut, uint256(amountSpecified)); - } else { - uint256 amountIn = router.exactOutputSingle( - ISwapRouter.ExactOutputSingleParams({ - tokenIn: zeroForOne ? token0 : token1, - tokenOut: zeroForOne ? token1 : token0, - fee: fee, - recipient: caller, - deadline: block.timestamp, - amountOut: uint256(-amountSpecified), - amountInMaximum: type(uint256).max, - sqrtPriceLimitX96: 0 - }) - ); + (int256 delta0, int256 delta1) = routerV4.swap( + address(0), + key, + amountSpecified, + zeroForOne + ); - vm.revertTo(snap); + vm.revertTo(snap); - return - zeroForOne - ? (amountIn, uint256(-amountSpecified)) - : (uint256(-amountSpecified), amountIn); - } + return (uint256(Math.abs(delta0)), uint256(Math.abs(delta1))); } function simulateSwap( - IUniswapV3Pool uniPool, + PoolKey memory key, int24[2] memory tickLower, int24[2] memory tickUpper, uint128[2] memory liquidity, - ISwapRouter router, - address token0, - address token1, - uint24 fee, + V4RouterSimple routerV4, bool zeroForOne, int256 amountSpecified ) public returns (int256, int256) { @@ -853,162 +769,69 @@ contract PositionUtils is Test { vm.startPrank(address(0x123456789)); - deal(token0, address(0x123456789), type(uint128).max); - deal(token1, address(0x123456789), type(uint128).max); - - MiniPositionManager pm = new MiniPositionManager(); - - IERC20Partial(token0).approve(address(router), type(uint256).max); - IERC20Partial(token1).approve(address(router), type(uint256).max); - IERC20Partial(token0).approve(address(pm), type(uint256).max); - IERC20Partial(token1).approve(address(pm), type(uint256).max); - - pm.mintLiquidity(uniPool, tickLower[0], tickUpper[0], liquidity[0], address(0x123456789)); - pm.mintLiquidity(uniPool, tickLower[1], tickUpper[1], liquidity[1], address(0x123456789)); - - if (amountSpecified > 0) { - int256 amountOut = int256( - router.exactInputSingle( - ISwapRouter.ExactInputSingleParams({ - tokenIn: zeroForOne ? token0 : token1, - tokenOut: zeroForOne ? token1 : token0, - fee: fee, - recipient: address(0x123456789), - deadline: block.timestamp, - amountIn: uint256(amountSpecified), - amountOutMinimum: 0, - sqrtPriceLimitX96: 0 - }) - ) - ); - - vm.revertTo(0); + routerV4.modifyLiquidity(address(0), key, tickLower[0], tickUpper[0], int128(liquidity[0])); + routerV4.modifyLiquidity(address(0), key, tickLower[1], tickUpper[1], int128(liquidity[1])); - return zeroForOne ? (amountSpecified, -amountOut) : (-amountOut, amountSpecified); - } else { - int256 amountIn = int256( - router.exactOutputSingle( - ISwapRouter.ExactOutputSingleParams({ - tokenIn: zeroForOne ? token0 : token1, - tokenOut: zeroForOne ? token1 : token0, - fee: fee, - recipient: address(0x123456789), - deadline: block.timestamp, - amountOut: uint256(-amountSpecified), - amountInMaximum: type(uint256).max, - sqrtPriceLimitX96: 0 - }) - ) - ); + (int256 delta0, int256 delta1) = routerV4.swap( + address(0), + key, + amountSpecified, + zeroForOne + ); - vm.revertTo(0); + vm.revertTo(0); - return zeroForOne ? (amountIn, amountSpecified) : (amountSpecified, amountIn); - } + return (-delta0, -delta1); } function simulateSwapLong( - IUniswapV3Pool uniPool, + PoolKey memory key, int24[2] memory tickLower, int24[2] memory tickUpper, int128[2] memory liquidity, - ISwapRouter router, - address token0, - address token1, - uint24 fee, + bytes32[2] memory positionKeys, + V4RouterSimple routerV4, bool zeroForOne, int256 amountSpecified - ) public returns (int256, int256) { + ) external returns (int256, int256) { vm.snapshot(); - (, caller, ) = vm.readCallers(); - - deal(token0, address(caller), type(uint128).max); - deal(token1, address(caller), type(uint128).max); + vm.startPrank(address(0x123456789)); - MiniPositionManager pm = new MiniPositionManager(); + IERC20Partial(Currency.unwrap(key.currency0)).approve(msg.sender, type(uint256).max); + IERC20Partial(Currency.unwrap(key.currency1)).approve(msg.sender, type(uint256).max); // make it so we can burn existing liq from caller - vm.etch(caller, address(pm).code); - - IERC20Partial(token0).approve(address(router), type(uint256).max); - IERC20Partial(token1).approve(address(router), type(uint256).max); - IERC20Partial(token0).approve(address(caller), type(uint256).max); - IERC20Partial(token1).approve(address(caller), type(uint256).max); - - if (liquidity[0] > 0) { - MiniPositionManager(caller).mintLiquidity( - uniPool, - tickLower[0], - tickUpper[0], - uint128(liquidity[0]), - caller - ); - } else { - MiniPositionManager(caller).burnLiquidity( - uniPool, - tickLower[0], - tickUpper[0], - uint128(-liquidity[0]) - ); - } - - if (liquidity[1] > 0) { - MiniPositionManager(caller).mintLiquidity( - uniPool, - tickLower[1], - tickUpper[1], - uint128(liquidity[1]), - caller - ); - } else { - MiniPositionManager(caller).burnLiquidity( - uniPool, - tickLower[1], - tickUpper[1], - uint128(-liquidity[1]) - ); - } - - if (amountSpecified > 0) { - int256 amountOut = int256( - router.exactInputSingle( - ISwapRouter.ExactInputSingleParams({ - tokenIn: zeroForOne ? token0 : token1, - tokenOut: zeroForOne ? token1 : token0, - fee: fee, - recipient: caller, - deadline: block.timestamp, - amountIn: uint256(amountSpecified), - amountOutMinimum: 0, - sqrtPriceLimitX96: 0 - }) - ) - ); + vm.etch(msg.sender, address(routerV4).code); + + V4RouterSimple(msg.sender).modifyLiquidityWithSalt( + address(0), + key, + tickLower[0], + tickUpper[0], + liquidity[0], + positionKeys[0] + ); - vm.revertTo(0); + V4RouterSimple(msg.sender).modifyLiquidityWithSalt( + address(0), + key, + tickLower[1], + tickUpper[1], + liquidity[1], + positionKeys[1] + ); - return zeroForOne ? (amountSpecified, -amountOut) : (-amountOut, amountSpecified); - } else { - int256 amountIn = int256( - router.exactOutputSingle( - ISwapRouter.ExactOutputSingleParams({ - tokenIn: zeroForOne ? token0 : token1, - tokenOut: zeroForOne ? token1 : token0, - fee: fee, - recipient: caller, - deadline: block.timestamp, - amountOut: uint256(-amountSpecified), - amountInMaximum: type(uint256).max, - sqrtPriceLimitX96: 0 - }) - ) - ); + (int256 delta0, int256 delta1) = V4RouterSimple(msg.sender).swap( + address(0), + key, + amountSpecified, + zeroForOne + ); - vm.revertTo(0); + vm.revertTo(0); - return zeroForOne ? (amountIn, amountSpecified) : (amountSpecified, amountIn); - } + return (-delta0, -delta1); } function simulateSwapSingleBurn( @@ -1104,198 +927,74 @@ contract PositionUtils is Test { } } + // UPD function simulateSwap( - IUniswapV3Pool uniPool, + PoolKey memory key, int24 tickLower, int24 tickUpper, uint128 liquidity, - ISwapRouter router, - address token0, - address token1, - uint24 fee, + V4RouterSimple routerV4, bool[2] memory zeroForOne, int256[2] memory amountSpecified ) public returns (uint256[2] memory amount0, uint256[2] memory amount1) { vm.startPrank(address(0x123456789)); - deal(token0, address(0x123456789), type(uint128).max); - deal(token1, address(0x123456789), type(uint128).max); - - MiniPositionManager pm = new MiniPositionManager(); + routerV4.modifyLiquidity(address(0), key, tickLower, tickUpper, int128(liquidity)); - IERC20Partial(token0).approve(address(router), type(uint256).max); - IERC20Partial(token1).approve(address(router), type(uint256).max); - IERC20Partial(token0).approve(address(pm), type(uint256).max); - IERC20Partial(token1).approve(address(pm), type(uint256).max); + (int256 delta0, int256 delta1) = routerV4.swap( + address(0), + key, + amountSpecified[0], + zeroForOne[0] + ); - pm.mintLiquidity(uniPool, tickLower, tickUpper, liquidity, address(0x123456789)); + amount0[0] = uint256(Math.abs(delta0)); + amount1[0] = uint256(Math.abs(delta1)); - if (amountSpecified[0] > 0) { - uint256 amountOut = router.exactInputSingle( - ISwapRouter.ExactInputSingleParams({ - tokenIn: zeroForOne[0] ? token0 : token1, - tokenOut: zeroForOne[0] ? token1 : token0, - fee: fee, - recipient: address(0x123456789), - deadline: block.timestamp, - amountIn: uint256(amountSpecified[0]), - amountOutMinimum: 0, - sqrtPriceLimitX96: 0 - }) - ); - (amount0[0], amount1[0]) = zeroForOne[0] - ? (uint256(amountSpecified[0]), amountOut) - : (amountOut, uint256(amountSpecified[0])); - } else { - uint256 amountIn = router.exactOutputSingle( - ISwapRouter.ExactOutputSingleParams({ - tokenIn: zeroForOne[0] ? token0 : token1, - tokenOut: zeroForOne[0] ? token1 : token0, - fee: fee, - recipient: address(0x123456789), - deadline: block.timestamp, - amountOut: uint256(-amountSpecified[0]), - amountInMaximum: type(uint256).max, - sqrtPriceLimitX96: 0 - }) - ); - (amount0[0], amount1[0]) = zeroForOne[0] - ? (amountIn, uint256(-amountSpecified[0])) - : (uint256(-amountSpecified[0]), amountIn); - } + routerV4.modifyLiquidity(address(0), key, tickLower, tickUpper, -int128(liquidity)); - pm.burnLiquidity(uniPool, tickLower, tickUpper, liquidity); + (delta0, delta1) = routerV4.swap(address(0), key, amountSpecified[1], zeroForOne[1]); - if (amountSpecified[1] > 0) { - uint256 amountOut = router.exactInputSingle( - ISwapRouter.ExactInputSingleParams({ - tokenIn: zeroForOne[1] ? token0 : token1, - tokenOut: zeroForOne[1] ? token1 : token0, - fee: fee, - recipient: address(0x123456789), - deadline: block.timestamp, - amountIn: uint256(amountSpecified[1]), - amountOutMinimum: 0, - sqrtPriceLimitX96: 0 - }) - ); - (amount0[1], amount1[1]) = zeroForOne[1] - ? (uint256(amountSpecified[1]), amountOut) - : (amountOut, uint256(amountSpecified[1])); - } else { - uint256 amountIn = router.exactOutputSingle( - ISwapRouter.ExactOutputSingleParams({ - tokenIn: zeroForOne[1] ? token0 : token1, - tokenOut: zeroForOne[1] ? token1 : token0, - fee: fee, - recipient: address(0x123456789), - deadline: block.timestamp, - amountOut: uint256(-amountSpecified[1]), - amountInMaximum: type(uint256).max, - sqrtPriceLimitX96: 0 - }) - ); - (amount0[1], amount1[1]) = zeroForOne[1] - ? (amountIn, uint256(-amountSpecified[1])) - : (uint256(-amountSpecified[1]), amountIn); - } + amount0[1] = uint256(Math.abs(delta0)); + amount1[1] = uint256(Math.abs(delta1)); } + // UPD function simulateSwap( - IUniswapV3Pool uniPool, + PoolKey memory key, int24 tickLower, int24 tickUpper, uint128[2] memory liquidity, - ISwapRouter router, - address token0, - address token1, - uint24 fee, + V4RouterSimple routerV4, bool[2] memory zeroForOne, int256[2] memory amountSpecified ) public returns (uint256[2] memory amount0, uint256[2] memory amount1) { - MiniPositionManager pm = new MiniPositionManager(); + vm.startPrank(address(0x123456789)); - IERC20Partial(token0).approve(address(router), type(uint256).max); - IERC20Partial(token1).approve(address(router), type(uint256).max); - IERC20Partial(token0).approve(address(pm), type(uint256).max); - IERC20Partial(token1).approve(address(pm), type(uint256).max); + routerV4.modifyLiquidity(address(0), key, tickLower, tickUpper, int128(liquidity[0])); - pm.mintLiquidity(uniPool, tickLower, tickUpper, liquidity[0], address(0x123456)); + (int256 delta0, int256 delta1) = routerV4.swap( + address(0), + key, + amountSpecified[0], + zeroForOne[0] + ); - if (amountSpecified[0] > 0) { - uint256 amountOut = router.exactInputSingle( - ISwapRouter.ExactInputSingleParams({ - tokenIn: zeroForOne[0] ? token0 : token1, - tokenOut: zeroForOne[0] ? token1 : token0, - fee: fee, - recipient: address(0x123456), - deadline: block.timestamp, - amountIn: uint256(amountSpecified[0]), - amountOutMinimum: 0, - sqrtPriceLimitX96: 0 - }) - ); - (amount0[0], amount1[0]) = zeroForOne[0] - ? (uint256(amountSpecified[0]), amountOut) - : (amountOut, uint256(amountSpecified[0])); - } else { - uint256 amountIn = router.exactOutputSingle( - ISwapRouter.ExactOutputSingleParams({ - tokenIn: zeroForOne[0] ? token0 : token1, - tokenOut: zeroForOne[0] ? token1 : token0, - fee: fee, - recipient: address(0x123456), - deadline: block.timestamp, - amountOut: uint256(-amountSpecified[0]), - amountInMaximum: type(uint256).max, - sqrtPriceLimitX96: 0 - }) - ); - (amount0[0], amount1[0]) = zeroForOne[0] - ? (amountIn, uint256(-amountSpecified[0])) - : (uint256(-amountSpecified[0]), amountIn); - } + amount0[0] = uint256(Math.abs(delta0)); + amount1[0] = uint256(Math.abs(delta1)); - pm.burnLiquidity(uniPool, tickLower, tickUpper, liquidity[1]); + routerV4.modifyLiquidity(address(0), key, tickLower, tickUpper, -int128(liquidity[1])); - if (amountSpecified[1] > 0) { - uint256 amountOut = router.exactInputSingle( - ISwapRouter.ExactInputSingleParams({ - tokenIn: zeroForOne[1] ? token0 : token1, - tokenOut: zeroForOne[1] ? token1 : token0, - fee: fee, - recipient: address(0x123456), - deadline: block.timestamp, - amountIn: uint256(amountSpecified[1]), - amountOutMinimum: 0, - sqrtPriceLimitX96: 0 - }) - ); - (amount0[1], amount1[1]) = zeroForOne[1] - ? (uint256(amountSpecified[1]), amountOut) - : (amountOut, uint256(amountSpecified[1])); - } else { - uint256 amountIn = router.exactOutputSingle( - ISwapRouter.ExactOutputSingleParams({ - tokenIn: zeroForOne[1] ? token0 : token1, - tokenOut: zeroForOne[1] ? token1 : token0, - fee: fee, - recipient: address(0x123456), - deadline: block.timestamp, - amountOut: uint256(-amountSpecified[1]), - amountInMaximum: type(uint256).max, - sqrtPriceLimitX96: 0 - }) - ); - (amount0[1], amount1[1]) = zeroForOne[1] - ? (amountIn, uint256(-amountSpecified[1])) - : (uint256(-amountSpecified[1]), amountIn); - } + (delta0, delta1) = routerV4.swap(address(0), key, amountSpecified[1], zeroForOne[1]); + + amount0[1] = uint256(Math.abs(delta0)); + amount1[1] = uint256(Math.abs(delta1)); } // this only works if the position is in-range function accruePoolFeesInRange( - address uniPool, + IPoolManager manager, + PoolKey memory key, uint256 posLiq, uint256 posFees0, uint256 posFees1 @@ -1303,32 +1002,63 @@ contract PositionUtils is Test { uint256 feeGrowthAdd0X128 = FullMath.mulDiv(posFees0, 2 ** 128, posLiq); uint256 feeGrowthAdd1X128 = FullMath.mulDiv(posFees1, 2 ** 128, posLiq); + uint128 _liquidity = StateLibrary.getLiquidity(manager, key.toId()); // distribute accrued fee amount to Uniswap pool deal( - IUniswapV3Pool(uniPool).token0(), - uniPool, - IERC20Partial(IUniswapV3Pool(uniPool).token0()).balanceOf(uniPool) + - (IUniswapV3Pool(uniPool).liquidity() * posFees0) / + Currency.unwrap(key.currency0), + address(manager), + IERC20Partial(Currency.unwrap(key.currency0)).balanceOf(address(manager)) + + (_liquidity * posFees0) / posLiq ); deal( - IUniswapV3Pool(uniPool).token1(), - uniPool, - IERC20Partial(IUniswapV3Pool(uniPool).token1()).balanceOf(uniPool) + - (IUniswapV3Pool(uniPool).liquidity() * posFees1) / + Currency.unwrap(key.currency1), + address(manager), + IERC20Partial(Currency.unwrap(key.currency1)).balanceOf(address(manager)) + + (_liquidity * posFees1) / posLiq ); + PoolId poolId = key.toId(); // update global fees vm.store( - uniPool, - bytes32(uint256(1)), - bytes32(uint256(vm.load(uniPool, bytes32(uint256(1)))) + feeGrowthAdd0X128) + address(manager), + bytes32( + uint256(StateLibrary._getPoolStateSlot(poolId)) + + StateLibrary.FEE_GROWTH_GLOBAL0_OFFSET + ), + bytes32( + uint256( + vm.load( + address(manager), + bytes32( + uint256(StateLibrary._getPoolStateSlot(poolId)) + + StateLibrary.FEE_GROWTH_GLOBAL0_OFFSET + ) + ) + ) + feeGrowthAdd0X128 + ) ); + vm.store( - uniPool, - bytes32(uint256(2)), - bytes32(uint256(vm.load(uniPool, bytes32(uint256(2)))) + feeGrowthAdd1X128) + address(manager), + bytes32( + uint256(StateLibrary._getPoolStateSlot(poolId)) + + StateLibrary.FEE_GROWTH_GLOBAL0_OFFSET + + 1 + ), + bytes32( + uint256( + vm.load( + address(manager), + bytes32( + uint256(StateLibrary._getPoolStateSlot(poolId)) + + StateLibrary.FEE_GROWTH_GLOBAL0_OFFSET + + 1 + ) + ) + ) + feeGrowthAdd1X128 + ) ); } @@ -1432,12 +1162,12 @@ contract PositionUtils is Test { int256 assetDelta = convertToAssets(ct, shareDelta); vm.store( address(ct), - bytes32(uint256(7)), + bytes32(uint256(3)), bytes32( uint256( LeftRightSigned.unwrap( LeftRightSigned - .wrap(int256(uint256(vm.load(address(ct), bytes32(uint256(7)))))) + .wrap(int256(uint256(vm.load(address(ct), bytes32(uint256(3)))))) .add(LeftRightSigned.wrap(int256(uint256(uint128(int128(assetDelta)))))) ) ) diff --git a/test/foundry/testUtils/ReentrancyMocks.sol b/test/foundry/testUtils/ReentrancyMocks.sol index 6f08d5b..20011d0 100644 --- a/test/foundry/testUtils/ReentrancyMocks.sol +++ b/test/foundry/testUtils/ReentrancyMocks.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.24; import {PanopticMath} from "@libraries/PanopticMath.sol"; import {TokenId} from "@types/TokenId.sol"; import "../core/SemiFungiblePositionManager.t.sol"; +import {PoolKey} from "v4-core/types/PoolKey.sol"; contract ReenterBurn { // ensure storage conflicts don't occur with etched contract @@ -54,9 +55,12 @@ contract ReenterBurn { fallback() external { bool reenter = !activated; activated = true; + + PoolKey memory key; if (reenter) SemiFungiblePositionManagerHarness(msg.sender).burnTokenizedPosition( - TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(this))), + key, + TokenId.wrap(0), 0, 0, 0 @@ -114,9 +118,11 @@ contract ReenterMint { bool reenter = !activated; activated = true; + PoolKey memory key; if (reenter) SemiFungiblePositionManagerHarness(msg.sender).mintTokenizedPosition( - TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(this))), + key, + TokenId.wrap(0), 0, 0, 0 @@ -124,144 +130,23 @@ contract ReenterMint { } } -contract ReenterTransferSingle { - // ensure storage conflicts don't occur with etched contract - uint256[65535] private __gap; - - struct Slot0 { - // the current price - uint160 sqrtPriceX96; - // the current tick - int24 tick; - // the most-recently updated index of the observations array - uint16 observationIndex; - // the current maximum number of observations that are being stored - uint16 observationCardinality; - // the next maximum number of observations to store, triggered in observations.write - uint16 observationCardinalityNext; - // the current protocol fee as a percentage of the swap fee taken on withdrawal - // represented as an integer denominator (1/x)% - uint8 feeProtocol; - // whether the pool is locked - bool unlocked; - } - - Slot0 public slot0; - - int24 public tickSpacing; - - address public token0; - address public token1; - uint24 public fee; - - bool activated; - - function construct( - Slot0 memory _slot0, - address _token0, - address _token1, - uint24 _fee, - int24 _tickSpacing - ) public { - slot0 = _slot0; - token0 = _token0; - token1 = _token1; - fee = _fee; - tickSpacing = _tickSpacing; - } - - fallback() external { - bool reenter = !activated; - activated = true; - - if (reenter) - SemiFungiblePositionManagerHarness(msg.sender).safeTransferFrom( - address(0), - address(0), - TokenId.unwrap(TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(this)))), - 0, - "" - ); - } -} - -contract ReenterTransferBatch { +// through ERC1155 transfer +contract Reenter1155Initialize { // ensure storage conflicts don't occur with etched contract uint256[65535] private __gap; - struct Slot0 { - // the current price - uint160 sqrtPriceX96; - // the current tick - int24 tick; - // the most-recently updated index of the observations array - uint16 observationIndex; - // the current maximum number of observations that are being stored - uint16 observationCardinality; - // the next maximum number of observations to store, triggered in observations.write - uint16 observationCardinalityNext; - // the current protocol fee as a percentage of the swap fee taken on withdrawal - // represented as an integer denominator (1/x)% - uint8 feeProtocol; - // whether the pool is locked - bool unlocked; - } - - Slot0 public slot0; - - int24 public tickSpacing; - - address public token0; - address public token1; - uint24 public fee; + PoolKey key; bool activated; - function construct( - Slot0 memory _slot0, - address _token0, - address _token1, - uint24 _fee, - int24 _tickSpacing - ) public { - slot0 = _slot0; - token0 = _token0; - token1 = _token1; - fee = _fee; - tickSpacing = _tickSpacing; - } - - fallback() external { - bool reenter = !activated; - activated = true; - - uint256[] memory ids = new uint256[](1); - ids[0] = TokenId.unwrap(TokenId.wrap(0).addPoolId(PanopticMath.getPoolId(address(this)))); - if (reenter) - SemiFungiblePositionManagerHarness(msg.sender).safeBatchTransferFrom( - address(0), - address(0), - ids, - new uint256[](1), - "" - ); + function construct(PoolKey memory _key) public { + key = _key; } -} - -// through ERC1155 transfer -contract Reenter1155Initialize { - address public token0; - address public token1; - uint24 public fee; - uint64 poolId; - - bool activated; - function construct(address _token0, address _token1, uint24 _fee, uint64 _poolId) public { - token0 = _token0; - token1 = _token1; - fee = _fee; - poolId = _poolId; + function extsload(bytes32 slot) public view returns (bytes32 ret) { + assembly { + ret := sload(slot) + } } function onERC1155Received( @@ -274,11 +159,11 @@ contract Reenter1155Initialize { bool reenter = !activated; activated = true; - if (reenter) - SemiFungiblePositionManagerHarness(msg.sender).initializeAMMPool(token0, token1, fee); + if (reenter) SemiFungiblePositionManagerHarness(msg.sender).initializeAMMPool(key); if (reenter) SemiFungiblePositionManagerHarness(msg.sender).mintTokenizedPosition( - TokenId.wrap(poolId), + key, + TokenId.wrap(0), 0, 0, 0 diff --git a/test/foundry/testUtils/V4RouterSimple.sol b/test/foundry/testUtils/V4RouterSimple.sol new file mode 100644 index 0000000..0cf5ab6 --- /dev/null +++ b/test/foundry/testUtils/V4RouterSimple.sol @@ -0,0 +1,308 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; +// V4 types +import {PoolId} from "v4-core/types/PoolId.sol"; +import {PoolKey} from "v4-core/types/PoolKey.sol"; +import {StateLibrary} from "v4-core/libraries/StateLibrary.sol"; +import {V4StateReader} from "@libraries/V4StateReader.sol"; +import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol"; +import {BalanceDelta} from "v4-core/types/BalanceDelta.sol"; +import {Currency} from "v4-core/types/Currency.sol"; +import {PoolManager} from "v4-core/PoolManager.sol"; +import {IHooks} from "v4-core/interfaces/IHooks.sol"; +import {SafeTransferLib} from "@libraries/SafeTransferLib.sol"; +import {TickMath} from "v4-core/libraries/TickMath.sol"; + +contract V4RouterSimple { + IPoolManager immutable POOL_MANAGER_V4; + + constructor(IPoolManager _manager) { + POOL_MANAGER_V4 = _manager; + } + + function unlockCallback(bytes calldata data) public returns (bytes memory) { + (uint256 action, bytes memory _data) = abi.decode(data, (uint256, bytes)); + + if (action == 0) { + ( + address caller, + PoolKey memory key, + int24 tickLower, + int24 tickUpper, + int256 liquidity + ) = abi.decode(_data, (address, PoolKey, int24, int24, int256)); + (int256 delta0, int256 delta1) = modifyLiquidity( + caller, + key, + tickLower, + tickUpper, + liquidity + ); + return abi.encode(delta0, delta1); + } else if (action == 1) { + (address caller, PoolKey memory key, uint160 sqrtPriceX96) = abi.decode( + _data, + (address, PoolKey, uint160) + ); + swapTo(caller, key, sqrtPriceX96); + return ""; + } else if (action == 2) { + (address caller, PoolKey memory key, int256 amountSpecified, bool zeroForOne) = abi + .decode(_data, (address, PoolKey, int256, bool)); + (int256 delta0, int256 delta1) = swap(caller, key, amountSpecified, zeroForOne); + return abi.encode(delta0, delta1); + } else if (action == 3) { + ( + address caller, + PoolKey memory key, + int24 tickLower, + int24 tickUpper, + int256 liquidity, + bytes32 salt + ) = abi.decode(_data, (address, PoolKey, int24, int24, int256, bytes32)); + modifyLiquidityWithSalt(caller, key, tickLower, tickUpper, liquidity, salt); + return ""; + } else if (action == 4) { + (address caller, Currency currency, uint256 amount) = abi.decode( + _data, + (address, Currency, uint256) + ); + mintCurrency(caller, currency, amount); + return ""; + } else if (action == 5) { + (address caller, Currency currency, uint256 amount) = abi.decode( + _data, + (address, Currency, uint256) + ); + burnCurrency(caller, currency, amount); + return ""; + } + + return ""; + } + + function mintCurrency(address caller, Currency currency, uint256 amount) public { + if (msg.sender != address(POOL_MANAGER_V4)) { + POOL_MANAGER_V4.unlock(abi.encode(4, abi.encode(msg.sender, currency, amount))); + return; + } + POOL_MANAGER_V4.sync(currency); + SafeTransferLib.safeTransferFrom( + Currency.unwrap(currency), + caller, + address(POOL_MANAGER_V4), + amount + ); + POOL_MANAGER_V4.settle(); + POOL_MANAGER_V4.mint(caller, uint160(Currency.unwrap(currency)), amount); + } + + function burnCurrency(address caller, Currency currency, uint256 amount) public { + if (msg.sender != address(POOL_MANAGER_V4)) { + POOL_MANAGER_V4.unlock(abi.encode(5, abi.encode(msg.sender, currency, amount))); + return; + } + POOL_MANAGER_V4.burn(caller, uint160(Currency.unwrap(currency)), amount); + POOL_MANAGER_V4.take(currency, caller, uint128(amount)); + } + + function modifyLiquidity( + address caller, + PoolKey memory key, + int24 tickLower, + int24 tickUpper, + int256 liquidity + ) public returns (int256, int256) { + if (msg.sender != address(POOL_MANAGER_V4)) { + bytes memory res = POOL_MANAGER_V4.unlock( + abi.encode(0, abi.encode(msg.sender, key, tickLower, tickUpper, liquidity)) + ); + return abi.decode(res, (int256, int256)); + } + + (BalanceDelta delta, ) = POOL_MANAGER_V4.modifyLiquidity( + key, + IPoolManager.ModifyLiquidityParams(tickLower, tickUpper, liquidity, bytes32(0)), + "" + ); + + if (delta.amount0() < 0) { + POOL_MANAGER_V4.sync(key.currency0); + SafeTransferLib.safeTransferFrom( + Currency.unwrap(key.currency0), + caller, + address(POOL_MANAGER_V4), + uint128(-delta.amount0()) + ); + POOL_MANAGER_V4.settle(); + } else if (delta.amount0() > 0) { + POOL_MANAGER_V4.take(key.currency0, caller, uint128(delta.amount0())); + } + + if (delta.amount1() < 0) { + POOL_MANAGER_V4.sync(key.currency1); + SafeTransferLib.safeTransferFrom( + Currency.unwrap(key.currency1), + caller, + address(POOL_MANAGER_V4), + uint128(-delta.amount1()) + ); + POOL_MANAGER_V4.settle(); + } else if (delta.amount1() > 0) { + POOL_MANAGER_V4.take(key.currency1, caller, uint128(delta.amount1())); + } + + return (delta.amount0(), delta.amount1()); + } + + function modifyLiquidityWithSalt( + address caller, + PoolKey memory key, + int24 tickLower, + int24 tickUpper, + int256 liquidity, + bytes32 salt + ) public { + if (msg.sender != address(POOL_MANAGER_V4)) { + POOL_MANAGER_V4.unlock( + abi.encode(3, abi.encode(msg.sender, key, tickLower, tickUpper, liquidity, salt)) + ); + return; + } + + (BalanceDelta delta, ) = POOL_MANAGER_V4.modifyLiquidity( + key, + IPoolManager.ModifyLiquidityParams(tickLower, tickUpper, liquidity, salt), + "" + ); + + if (delta.amount0() < 0) { + POOL_MANAGER_V4.sync(key.currency0); + SafeTransferLib.safeTransferFrom( + Currency.unwrap(key.currency0), + caller, + address(POOL_MANAGER_V4), + uint128(-delta.amount0()) + ); + POOL_MANAGER_V4.settle(); + } else if (delta.amount0() > 0) { + POOL_MANAGER_V4.take(key.currency0, caller, uint128(delta.amount0())); + } + + if (delta.amount1() < 0) { + POOL_MANAGER_V4.sync(key.currency1); + SafeTransferLib.safeTransferFrom( + Currency.unwrap(key.currency1), + caller, + address(POOL_MANAGER_V4), + uint128(-delta.amount1()) + ); + POOL_MANAGER_V4.settle(); + } else if (delta.amount1() > 0) { + POOL_MANAGER_V4.take(key.currency1, caller, uint128(delta.amount1())); + } + } + + function swap( + address caller, + PoolKey memory key, + int256 amountSpecified, + bool zeroForOne + ) public returns (int256, int256) { + if (msg.sender != address(POOL_MANAGER_V4)) { + bytes memory res = POOL_MANAGER_V4.unlock( + abi.encode(2, abi.encode(msg.sender, key, amountSpecified, zeroForOne)) + ); + return abi.decode(res, (int256, int256)); + } + + BalanceDelta swapDelta = POOL_MANAGER_V4.swap( + key, + IPoolManager.SwapParams( + zeroForOne, + -amountSpecified, + zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1 + ), + "" + ); + + if (swapDelta.amount0() < 0) { + POOL_MANAGER_V4.sync(key.currency0); + SafeTransferLib.safeTransferFrom( + Currency.unwrap(key.currency0), + caller, + address(POOL_MANAGER_V4), + uint256(-int256(swapDelta.amount0())) + ); + POOL_MANAGER_V4.settle(); + } else if (swapDelta.amount0() > 0) { + POOL_MANAGER_V4.take(key.currency0, caller, uint128(swapDelta.amount0())); + } + + if (swapDelta.amount1() < 0) { + POOL_MANAGER_V4.sync(key.currency1); + SafeTransferLib.safeTransferFrom( + Currency.unwrap(key.currency1), + caller, + address(POOL_MANAGER_V4), + uint256(-int256(swapDelta.amount1())) + ); + POOL_MANAGER_V4.settle(); + } else if (swapDelta.amount1() > 0) { + POOL_MANAGER_V4.take(key.currency1, caller, uint128(swapDelta.amount1())); + } + + return (swapDelta.amount0(), swapDelta.amount1()); + } + + function swapTo(address caller, PoolKey memory key, uint160 sqrtPriceX96) public { + if (msg.sender != address(POOL_MANAGER_V4)) { + bool done; + // we can only swap type(int128).max tokens at one time, so we need to loop until the price is set + while (!done) { + POOL_MANAGER_V4.unlock(abi.encode(1, abi.encode(msg.sender, key, sqrtPriceX96))); + done = V4StateReader.getSqrtPriceX96(POOL_MANAGER_V4, key.toId()) == sqrtPriceX96; + } + return; + } + uint160 sqrtPriceX96Before = V4StateReader.getSqrtPriceX96(POOL_MANAGER_V4, key.toId()); + + if (sqrtPriceX96Before == sqrtPriceX96) return; + + BalanceDelta swapDelta = POOL_MANAGER_V4.swap( + key, + IPoolManager.SwapParams( + sqrtPriceX96Before > sqrtPriceX96 ? true : false, + type(int128).min + 1, + sqrtPriceX96 + ), + "" + ); + + if (swapDelta.amount0() < 0) { + POOL_MANAGER_V4.sync(key.currency0); + SafeTransferLib.safeTransferFrom( + Currency.unwrap(key.currency0), + caller, + address(POOL_MANAGER_V4), + uint256(-int256(swapDelta.amount0())) + ); + POOL_MANAGER_V4.settle(); + } else if (swapDelta.amount0() > 0) { + POOL_MANAGER_V4.take(key.currency0, caller, uint128(swapDelta.amount0())); + } + + if (swapDelta.amount1() < 0) { + POOL_MANAGER_V4.sync(key.currency1); + SafeTransferLib.safeTransferFrom( + Currency.unwrap(key.currency1), + caller, + address(POOL_MANAGER_V4), + uint256(-int256(swapDelta.amount1())) + ); + POOL_MANAGER_V4.settle(); + } else if (swapDelta.amount1() > 0) { + POOL_MANAGER_V4.take(key.currency1, caller, uint128(swapDelta.amount1())); + } + } +} diff --git a/test/foundry/test_periphery/PanopticHelper.sol b/test/foundry/test_periphery/PanopticHelper.sol index ff50450..2d45683 100644 --- a/test/foundry/test_periphery/PanopticHelper.sol +++ b/test/foundry/test_periphery/PanopticHelper.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.24; // Interfaces -import {IUniswapV3Pool} from "univ3-core/interfaces/IUniswapV3Pool.sol"; +import {IV3CompatibleOracle} from "@interfaces/IV3CompatibleOracle.sol"; import {PanopticPool} from "@contracts/PanopticPool.sol"; import {SemiFungiblePositionManager} from "@contracts/SemiFungiblePositionManager.sol"; // Libraries @@ -14,6 +14,7 @@ import {LeftRightUnsigned} from "@types/LeftRight.sol"; import {TokenId, TokenIdLibrary} from "@types/TokenId.sol"; import {LiquidityChunk} from "@types/LiquidityChunk.sol"; import {PositionBalance, PositionBalanceLibrary} from "@types/PositionBalance.sol"; +import {PoolId} from "v4-core/types/PoolId.sol"; /// @title Utility contract for token ID construction and advanced queries. /// @author Axicon Labs Limited @@ -173,7 +174,7 @@ contract PanopticHelper { /// @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 oracle observations snapshot the closing price of the last block before the first interaction of a given block. /// @dev The maximum frequency of observations is 1 per block, but there is no guarantee that the pool will be observed at every block. /// @dev Each period has a minimum length of blocktime * period, but may be longer if the Uniswap pool is relatively inactive. /// @dev The final price used in the array (of length `cardinality`) is the average of all observations comprising `period` (which is itself a number of observations). @@ -183,7 +184,7 @@ contract PanopticHelper { /// @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 function computeMedianObservedPrice( - IUniswapV3Pool univ3pool, + IV3CompatibleOracle univ3pool, uint256 cardinality, uint256 period ) external view returns (int24) { @@ -200,7 +201,7 @@ contract PanopticHelper { } /// @notice Takes a packed structure representing a sorted 8-slot queue of ticks and returns the median of those values. - /// @dev Also inserts the latest Uniswap observation into the buffer, resorts, and returns if the last entry is at least `period` seconds old. + /// @dev Also inserts the latest oracle observation into the buffer, resorts, and returns if the last entry is at least `period` seconds old. /// @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 @@ -209,7 +210,7 @@ contract PanopticHelper { function computeInternalMedian( uint256 period, uint256 medianData, - IUniswapV3Pool univ3pool + IV3CompatibleOracle univ3pool ) external view returns (int24, uint256) { (, , uint16 observationIndex, uint16 observationCardinality, , , ) = univ3pool.slot0(); @@ -229,7 +230,10 @@ contract PanopticHelper { /// @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) { + function twapFilter( + IV3CompatibleOracle univ3pool, + uint32 twapWindow + ) external view returns (int24) { return PanopticMath.twapFilter(univ3pool, twapWindow); } @@ -262,28 +266,28 @@ contract PanopticHelper { return int256(balanceCross) - int256(requiredCross); } - /// @notice Unwraps the contents of the tokenId into its legs. - /// @param tokenId the input tokenId - /// @return legs an array of leg structs - function unwrapTokenId(TokenId tokenId) public view returns (Leg[] memory) { - uint256 numLegs = tokenId.countLegs(); - Leg[] memory legs = new Leg[](numLegs); - - uint64 poolId = tokenId.poolId(); - address UniswapV3Pool = address(SFPM.getUniswapV3PoolFromId(tokenId.poolId())); - for (uint256 i = 0; i < numLegs; ++i) { - legs[i].poolId = poolId; - legs[i].UniswapV3Pool = UniswapV3Pool; - legs[i].asset = tokenId.asset(i); - legs[i].optionRatio = tokenId.optionRatio(i); - legs[i].tokenType = tokenId.tokenType(i); - legs[i].isLong = tokenId.isLong(i); - legs[i].riskPartner = tokenId.riskPartner(i); - legs[i].strike = tokenId.strike(i); - legs[i].width = tokenId.width(i); - } - return legs; - } + // /// @notice Unwraps the contents of the tokenId into its legs. + // /// @param tokenId the input tokenId + // /// @return legs an array of leg structs + // function unwrapTokenId(TokenId tokenId) public view returns (Leg[] memory) { + // uint256 numLegs = tokenId.countLegs(); + // Leg[] memory legs = new Leg[](numLegs); + + // uint64 poolId = tokenId.poolId(); + // address UniswapV3Pool = address(SFPM.getUniswapV3PoolFromId(tokenId.poolId())); + // for (uint256 i = 0; i < numLegs; ++i) { + // legs[i].poolId = poolId; + // legs[i].UniswapV3Pool = UniswapV3Pool; + // legs[i].asset = tokenId.asset(i); + // legs[i].optionRatio = tokenId.optionRatio(i); + // legs[i].tokenType = tokenId.tokenType(i); + // legs[i].isLong = tokenId.isLong(i); + // legs[i].riskPartner = tokenId.riskPartner(i); + // legs[i].strike = tokenId.strike(i); + // legs[i].width = tokenId.width(i); + // } + // return legs; + // } /// @notice Returns an estimate of the downside liquidation price for a given account on a given pool. /// @dev returns MIN_TICK if the LP is more than 100000 ticks below the current tick. @@ -297,7 +301,7 @@ contract PanopticHelper { TokenId[] calldata positionIdList ) public view returns (int24 liquidationTick) { // initialize right and left bounds from current tick - (, int24 currentTick, , , , , ) = PanopticPool(pool).univ3pool().slot0(); + (, int24 currentTick, , , , , ) = PanopticPool(pool).oracleContract().slot0(); int24 x0 = currentTick - 10000; int24 x1 = currentTick; int24 tol = 100000; @@ -321,7 +325,7 @@ contract PanopticHelper { ); // if price is not within a 100000 tick range of current price, return MIN_TICK if (x1 > currentTick + tol || x1 < currentTick - tol) { - return Constants.MIN_V3POOL_TICK; + return Constants.MIN_V4POOL_TICK; } // stop if price is within 0.01% (1 tick) of LP if ( @@ -345,7 +349,7 @@ contract PanopticHelper { TokenId[] calldata positionIdList ) public view returns (int24 liquidationTick) { // initialize right and left bounds from current tick - (, int24 currentTick, , , , , ) = PanopticPool(pool).univ3pool().slot0(); + (, int24 currentTick, , , , , ) = PanopticPool(pool).oracleContract().slot0(); int24 x0 = currentTick; int24 x1 = currentTick + 10000; int24 tol = 100000; @@ -369,7 +373,7 @@ contract PanopticHelper { ); // if price is not within a 100000 tick range of current price, stop + return MAX_TICK if (x1 > currentTick + tol || x1 < currentTick - tol) { - return Constants.MAX_V3POOL_TICK; + return Constants.MAX_V4POOL_TICK; } // stop if price is within 0.01% (1 tick) of LP if ( @@ -451,7 +455,7 @@ contract PanopticHelper { /// @notice creates "Classic" strangle using a call and a put, with asymmetric upward risk. /// @dev example: createStrangle(uniPoolAddress, 4, 50, -50, 0, 1, 1, 0). - /// @param univ3pool address of the pool + /// @param idV4 Uniswap V4 pool identifier /// @param width width of the strangle /// @param callStrike strike of the call /// @param putStrike strike of the put @@ -461,7 +465,7 @@ contract PanopticHelper { /// @param start leg index where the (2 legs) of the strangle begin (usually 0) /// @return tokenId the position id with the strategy configured function createStrangle( - address univ3pool, + PoolId idV4, int24 width, int24 callStrike, int24 putStrike, @@ -471,7 +475,7 @@ contract PanopticHelper { uint256 start ) public view returns (TokenId tokenId) { // Pool - tokenId = tokenId.addPoolId(SFPM.getPoolId(univ3pool)); + tokenId = tokenId.addPoolId(SFPM.getPoolId(idV4)); // A strangle is composed of // 1. a call with a higher strike price @@ -504,7 +508,7 @@ contract PanopticHelper { /// @notice creates "Classic" straddle using a call and a put, with asymmetric upward risk. /// @dev createStraddle(uniPoolAddress, 4, 0, 0, 1, 1, 0). - /// @param univ3pool address of the pool + /// @param idV4 Uniswap V4 pool identifier /// @param width width of the strangle /// @param strike strike of the call and put /// @param asset asset of the strangle @@ -513,7 +517,7 @@ contract PanopticHelper { /// @param start leg index where the (2 legs) of the straddle begin (usually 0) /// @return tokenId the position id with the strategy configured function createStraddle( - address univ3pool, + PoolId idV4, int24 width, int24 strike, uint256 asset, @@ -522,7 +526,7 @@ contract PanopticHelper { uint256 start ) public view returns (TokenId tokenId) { // Pool - tokenId = tokenId.addPoolId(SFPM.getPoolId(univ3pool)); + tokenId = tokenId.addPoolId(SFPM.getPoolId(idV4)); // A straddle is composed of // 1. a call with an identical strike price @@ -537,7 +541,7 @@ contract PanopticHelper { /// @notice creates a call spread with 1 long leg and 1 short leg. /// @dev example: createCallSpread(uniPoolAddress, 4, -50, 50, 0, 1, 0). - /// @param univ3pool address of the pool + /// @param idV4 Uniswap V4 pool identifier /// @param width width of the spread /// @param strikeLong strike of the long leg /// @param strikeShort strike of the short leg @@ -546,7 +550,7 @@ contract PanopticHelper { /// @param start leg index where the (2 legs) of the spread begin (usually 0) /// @return tokenId the position id with the strategy configured function createCallSpread( - address univ3pool, + PoolId idV4, int24 width, int24 strikeLong, int24 strikeShort, @@ -555,7 +559,7 @@ contract PanopticHelper { uint256 start ) public view returns (TokenId tokenId) { // Pool - tokenId = tokenId.addPoolId(SFPM.getPoolId(univ3pool)); + tokenId = tokenId.addPoolId(SFPM.getPoolId(idV4)); // A call spread is composed of // 1. a long call with a lower strike price @@ -570,7 +574,7 @@ contract PanopticHelper { /// @notice creates a put spread with 1 long leg and 1 short leg. /// @dev example: createPutSpread(uniPoolAddress, 4, -50, 50, 0, 1, 0). - /// @param univ3pool address of the pool + /// @param idV4 Uniswap V4 pool identifier /// @param width width of the spread /// @param strikeLong strike of the long leg /// @param strikeShort strike of the short leg @@ -579,7 +583,7 @@ contract PanopticHelper { /// @param start leg index where the (2 legs) of the spread begin (usually 0) /// @return tokenId the position id with the strategy configured function createPutSpread( - address univ3pool, + PoolId idV4, int24 width, int24 strikeLong, int24 strikeShort, @@ -588,7 +592,7 @@ contract PanopticHelper { uint256 start ) public view returns (TokenId tokenId) { // Pool - tokenId = tokenId.addPoolId(SFPM.getPoolId(univ3pool)); + tokenId = tokenId.addPoolId(SFPM.getPoolId(idV4)); // A put spread is composed of // 1. a long put with a higher strike price @@ -603,7 +607,7 @@ contract PanopticHelper { /// @notice creates a diagonal spread with 1 long leg and 1 short leg.abi. /// @dev example: createCallDiagonalSpread(uniPoolAddress, 4, 8, -50, 50, 0, 1, 0). - /// @param univ3pool address of the pool + /// @param idV4 Uniswap V4 pool identifier /// @param widthLong width of the long leg /// @param widthShort width of the short leg /// @param strikeLong strike of the long leg @@ -613,7 +617,7 @@ contract PanopticHelper { /// @param start leg index where the (2 legs) of the spread begin (usually 0) /// @return tokenId the position id with the strategy configured function createCallDiagonalSpread( - address univ3pool, + PoolId idV4, int24 widthLong, int24 widthShort, int24 strikeLong, @@ -623,7 +627,7 @@ contract PanopticHelper { uint256 start ) public view returns (TokenId tokenId) { // Pool - tokenId = tokenId.addPoolId(SFPM.getPoolId(univ3pool)); + tokenId = tokenId.addPoolId(SFPM.getPoolId(idV4)); // A call diagonal spread is composed of // 1. a long call with a (lower/higher) strike price and (lower/higher) width(expiry) @@ -656,7 +660,7 @@ contract PanopticHelper { /// @notice creates a diagonal spread with 1 long leg and 1 short leg. /// @dev example: createPutDiagonalSpread(uniPoolAddress, 4, 8, -50, 50, 0, 1, 0). - /// @param univ3pool address of the pool + /// @param idV4 Uniswap V4 pool identifier /// @param widthLong width of the long leg /// @param widthShort width of the short leg /// @param strikeLong strike of the long leg @@ -666,7 +670,7 @@ contract PanopticHelper { /// @param start leg index where the (2 legs) of the spread begin (usually 0) /// @return tokenId the position id with the strategy configured function createPutDiagonalSpread( - address univ3pool, + PoolId idV4, int24 widthLong, int24 widthShort, int24 strikeLong, @@ -676,7 +680,7 @@ contract PanopticHelper { uint256 start ) public view returns (TokenId tokenId) { // Pool - tokenId = tokenId.addPoolId(SFPM.getPoolId(univ3pool)); + tokenId = tokenId.addPoolId(SFPM.getPoolId(idV4)); // A bearish diagonal spread is composed of // 1. a long put with a (higher/lower) strike price and (lower/higher) width(expiry) @@ -709,7 +713,7 @@ contract PanopticHelper { /// @notice creates a calendar spread with 1 long leg and 1 short leg. /// @dev example: createCallCalendarSpread(uniPoolAddress, 4, 8, 0, 0, 1, 0). - /// @param univ3pool address of the pool + /// @param idV4 Uniswap V4 pool identifier /// @param widthLong width of the long leg /// @param widthShort width of the short leg /// @param strike strike of the long and short legs @@ -718,7 +722,7 @@ contract PanopticHelper { /// @param start leg index where the (2 legs) of the spread begin (usually 0) /// @return tokenId the position id with the strategy configured function createCallCalendarSpread( - address univ3pool, + PoolId idV4, int24 widthLong, int24 widthShort, int24 strike, @@ -729,7 +733,7 @@ contract PanopticHelper { // calendar spread is a diagonal spread where the legs have identical strike prices // so we can create one using the diagonal spread function tokenId = createCallDiagonalSpread( - univ3pool, + idV4, widthLong, widthShort, strike, @@ -742,7 +746,7 @@ contract PanopticHelper { /// @notice creates a calendar spread with 1 long leg and 1 short leg. /// @dev example: createPutCalendarSpread(uniPoolAddress, 4, 8, 0, 0, 1, 0). - /// @param univ3pool address of the pool + /// @param idV4 Uniswap V4 pool identifier /// @param widthLong width of the long leg /// @param widthShort width of the short leg /// @param strike strike of the long and short legs @@ -751,7 +755,7 @@ contract PanopticHelper { /// @param start leg index where the (2 legs) of the spread begin (usually 0) /// @return tokenId the position id with the strategy configured function createPutCalendarSpread( - address univ3pool, + PoolId idV4, int24 widthLong, int24 widthShort, int24 strike, @@ -762,7 +766,7 @@ contract PanopticHelper { // calendar spread is a diagonal spread where the legs have identical strike prices // so we can create one using the diagonal spread function tokenId = createPutDiagonalSpread( - univ3pool, + idV4, widthLong, widthShort, strike, @@ -775,7 +779,7 @@ contract PanopticHelper { /// @notice creates iron condor w/ call and put spread. /// @dev example: createIronCondor(uniPoolAddress, 4, 50, -50, 50, 0). - /// @param univ3pool address of the pool + /// @param idV4 Uniswap V4 pool identifier /// @param width width of the spread /// @param callStrike strike of the call spread /// @param putStrike strike of the put spread @@ -783,7 +787,7 @@ contract PanopticHelper { /// @param asset asset of the strategy /// @return tokenId the position id with the strategy configured function createIronCondor( - address univ3pool, + PoolId idV4, int24 width, int24 callStrike, int24 putStrike, @@ -796,22 +800,14 @@ contract PanopticHelper { // the "wings" represent how much more OTM the long sides of the spreads are // call spread - tokenId = createCallSpread( - univ3pool, - width, - callStrike + wingWidth, - callStrike, - asset, - 1, - 0 - ); + tokenId = createCallSpread(idV4, width, callStrike + wingWidth, callStrike, asset, 1, 0); // put spread tokenId = TokenId.wrap( TokenId.unwrap(tokenId) + TokenId.unwrap( createPutSpread( - address(0), + PoolId.wrap(0), width, putStrike - wingWidth, putStrike, @@ -825,7 +821,7 @@ contract PanopticHelper { /// @notice creates a jade lizard w/ long call and short asymmetric (traditional) strangle. /// @dev example: createJadeLizard(uniPoolAddress, 4, 100, 50, -50, 0). - /// @param univ3pool address of the pool + /// @param idV4 Uniswap V4 pool identifier /// @param width width of the spread /// @param longCallStrike strike of the long call /// @param shortCallStrike strike of the short call @@ -833,7 +829,7 @@ contract PanopticHelper { /// @param asset asset of the strategy /// @return tokenId the position id with the strategy configured function createJadeLizard( - address univ3pool, + PoolId idV4, int24 width, int24 longCallStrike, int24 shortCallStrike, @@ -845,7 +841,7 @@ contract PanopticHelper { // 2. a long call // short strangle - tokenId = createStrangle(univ3pool, width, shortCallStrike, shortPutStrike, asset, 0, 1, 1); + tokenId = createStrangle(idV4, width, shortCallStrike, shortPutStrike, asset, 0, 1, 1); // long call tokenId = addCallLeg(tokenId, 0, 1, asset, 1, 0, longCallStrike, width); @@ -853,14 +849,14 @@ contract PanopticHelper { /// @notice creates a big lizard w/ long call and short asymmetric (traditional) straddle. /// @dev example: createBigLizard(uniPoolAddress, 4, 100, 50, 0). - /// @param univ3pool address of the pool + /// @param idV4 Uniswap V4 pool identifier /// @param width width of the spread /// @param longCallStrike strike of the long call /// @param straddleStrike strike of the short straddle /// @param asset asset of the strategy /// @return tokenId the position id with the strategy configured function createBigLizard( - address univ3pool, + PoolId idV4, int24 width, int24 longCallStrike, int24 straddleStrike, @@ -871,7 +867,7 @@ contract PanopticHelper { // 2. a long call // short straddle - tokenId = createStraddle(univ3pool, width, straddleStrike, asset, 0, 1, 1); + tokenId = createStraddle(idV4, width, straddleStrike, asset, 0, 1, 1); // long call tokenId = addCallLeg(tokenId, 0, 1, asset, 1, 0, longCallStrike, width); @@ -879,7 +875,7 @@ contract PanopticHelper { /// @notice creates a super bull w/ long call spread and short put. /// @dev example: createSuperBull(uniPoolAddress, 4, -50, 50, 50, 0). - /// @param univ3pool address of the pool + /// @param idV4 Uniswap V4 pool identifier /// @param width width of the spread /// @param longCallStrike strike of the long call /// @param shortCallStrike strike of the short call @@ -887,7 +883,7 @@ contract PanopticHelper { /// @param asset asset of the strategy /// @return tokenId the position id with the strategy configured function createSuperBull( - address univ3pool, + PoolId idV4, int24 width, int24 longCallStrike, int24 shortCallStrike, @@ -899,7 +895,7 @@ contract PanopticHelper { // 2. a short put // long call spread - tokenId = createCallSpread(univ3pool, width, longCallStrike, shortCallStrike, asset, 1, 1); + tokenId = createCallSpread(idV4, width, longCallStrike, shortCallStrike, asset, 1, 1); // short put tokenId = addPutLeg(tokenId, 0, 1, asset, 0, 0, shortPutStrike, width); @@ -907,7 +903,7 @@ contract PanopticHelper { /// @notice creates a super bear w/ long put spread and short call. /// @dev example: createSuperBear(uniPoolAddress, 4, 50, -50, -50, 0). - /// @param univ3pool address of the pool + /// @param idV4 Uniswap V4 pool identifier /// @param width width of the spread /// @param longPutStrike strike of the long put /// @param shortPutStrike strike of the short put @@ -915,7 +911,7 @@ contract PanopticHelper { /// @param asset asset of the strategy /// @return tokenId the position id with the strategy configured function createSuperBear( - address univ3pool, + PoolId idV4, int24 width, int24 longPutStrike, int24 shortPutStrike, @@ -927,7 +923,7 @@ contract PanopticHelper { // 2. a short call // long put spread - tokenId = createPutSpread(univ3pool, width, longPutStrike, shortPutStrike, asset, 1, 1); + tokenId = createPutSpread(idV4, width, longPutStrike, shortPutStrike, asset, 1, 1); // short call tokenId = addCallLeg(tokenId, 0, 1, asset, 0, 0, shortCallStrike, width); @@ -935,14 +931,14 @@ contract PanopticHelper { /// @notice creates a butterfly w/ long call spread and short put spread. /// @dev example: createIronButterfly(uniPoolAddress, 4, 0, 50, 0). - /// @param univ3pool address of the pool + /// @param idV4 Uniswap V4 pool identifier /// @param width width of the spread /// @param strike strike of the long and short legs /// @param wingWidth width of the wings /// @param asset asset of the strategy /// @return tokenId the position id with the strategy configured function createIronButterfly( - address univ3pool, + PoolId idV4, int24 width, int24 strike, int24 wingWidth, @@ -953,20 +949,20 @@ contract PanopticHelper { // 2. a short put spread // long call spread - tokenId = createCallSpread(univ3pool, width, strike, strike + wingWidth, asset, 1, 0); + tokenId = createCallSpread(idV4, width, strike, strike + wingWidth, asset, 1, 0); // short put spread tokenId = TokenId.wrap( TokenId.unwrap(tokenId) + TokenId.unwrap( - createPutSpread(address(0), width, strike, strike - wingWidth, asset, 1, 2) + createPutSpread(PoolId.wrap(0), width, strike, strike - wingWidth, asset, 1, 2) ) ); } /// @notice creates a ratio spread w/ long call and multiple short calls. /// @dev example: createCallRatioSpread(uniPoolAddress, 4, -50, 50, 0, 2, 0). - /// @param univ3pool address of the pool + /// @param idV4 Uniswap V4 pool identifier /// @param width width of the spread /// @param longStrike strike of the long call /// @param shortStrike strike of the short calls @@ -976,7 +972,7 @@ contract PanopticHelper { /// @return tokenId the position id with the strategy configured function createCallRatioSpread( - address univ3pool, + PoolId idV4, int24 width, int24 longStrike, int24 shortStrike, @@ -985,7 +981,7 @@ contract PanopticHelper { uint256 start ) public view returns (TokenId tokenId) { // Pool - tokenId = tokenId.addPoolId(SFPM.getPoolId(univ3pool)); + tokenId = tokenId.addPoolId(SFPM.getPoolId(idV4)); // a call ratio spread is composed of // 1. a long call @@ -1000,7 +996,7 @@ contract PanopticHelper { /// @notice creates a ratio spread w/ long put and multiple short puts. /// @dev example: createPutRatioSpread(uniPoolAddress, 4, -50, 50, 0, 2, 0). - /// @param univ3pool address of the pool + /// @param idV4 Uniswap V4 pool identifier /// @param width width of the spread /// @param longStrike strike of the long put /// @param shortStrike strike of the short puts @@ -1009,7 +1005,7 @@ contract PanopticHelper { /// @param start leg index where the (2 legs) of the spread begin (usually 0) /// @return tokenId the position id with the strategy configured function createPutRatioSpread( - address univ3pool, + PoolId idV4, int24 width, int24 longStrike, int24 shortStrike, @@ -1018,7 +1014,7 @@ contract PanopticHelper { uint256 start ) public view returns (TokenId tokenId) { // Pool - tokenId = tokenId.addPoolId(SFPM.getPoolId(univ3pool)); + tokenId = tokenId.addPoolId(SFPM.getPoolId(idV4)); // a put ratio spread is composed of // 1. a long put @@ -1033,7 +1029,7 @@ contract PanopticHelper { /// @notice creates a ZEBRA spread w/ short call and multiple long calls. /// @dev example: createCallZEBRASpread(uniPoolAddress, 4, -50, 50, 0, 2, 0). - /// @param univ3pool address of the pool + /// @param idV4 Uniswap V4 pool identifier /// @param width width of the spread /// @param longStrike strike of the long calls /// @param shortStrike strike of the short call @@ -1042,7 +1038,7 @@ contract PanopticHelper { /// @param start leg index where the (2 legs) of the spread begin (usually 0) /// @return tokenId the position id with the strategy configured function createCallZEBRASpread( - address univ3pool, + PoolId idV4, int24 width, int24 longStrike, int24 shortStrike, @@ -1051,7 +1047,7 @@ contract PanopticHelper { uint256 start ) public view returns (TokenId tokenId) { // Pool - tokenId = tokenId.addPoolId(SFPM.getPoolId(univ3pool)); + tokenId = tokenId.addPoolId(SFPM.getPoolId(idV4)); // a call ZEBRA(zero extrinsic value back ratio spread) spread is composed of // 1. a short call @@ -1066,7 +1062,7 @@ contract PanopticHelper { /// @notice creates a ZEBRA spread w/ short put and multiple long puts. /// @dev example: createPutZEBRASpread(uniPoolAddress, 4, -50, 50, 0, 2, 0). - /// @param univ3pool address of the pool + /// @param idV4 Uniswap V4 pool identifier /// @param width width of the spread /// @param longStrike strike of the long puts /// @param shortStrike strike of the short put @@ -1075,7 +1071,7 @@ contract PanopticHelper { /// @param start leg index where the (2 legs) of the spread begin (usually 0) /// @return tokenId the position id with the strategy configured function createPutZEBRASpread( - address univ3pool, + PoolId idV4, int24 width, int24 longStrike, int24 shortStrike, @@ -1084,7 +1080,7 @@ contract PanopticHelper { uint256 start ) public view returns (TokenId tokenId) { // Pool - tokenId = tokenId.addPoolId(SFPM.getPoolId(univ3pool)); + tokenId = tokenId.addPoolId(SFPM.getPoolId(idV4)); // a put ZEBRA(zero extrinsic value back ratio spread) spread is composed of // 1. a short put @@ -1099,7 +1095,7 @@ contract PanopticHelper { /// @notice creates a ZEEHBS w/ call and put ZEBRA spreads. /// @dev example: createPutZEBRASpread(uniPoolAddress, 4, -50, 50, 0, 2, 0). - /// @param univ3pool address of the pool + /// @param idV4 Uniswap V4 pool identifier /// @param width width of the spread /// @param longStrike strike of the long legs /// @param shortStrike strike of the short legs @@ -1107,7 +1103,7 @@ contract PanopticHelper { /// @param ratio ratio of the short legs to the long legs /// @return tokenId the position id with the strategy configured function createZEEHBS( - address univ3pool, + PoolId idV4, int24 width, int24 longStrike, int24 shortStrike, @@ -1119,14 +1115,14 @@ contract PanopticHelper { // 2. a put ZEBRA spread // call ZEBRA - tokenId = createCallZEBRASpread(univ3pool, width, longStrike, shortStrike, asset, ratio, 0); + tokenId = createCallZEBRASpread(idV4, width, longStrike, shortStrike, asset, ratio, 0); // put ZEBRA tokenId = TokenId.wrap( TokenId.unwrap(tokenId) + TokenId.unwrap( createPutZEBRASpread( - address(0), + PoolId.wrap(0), width, longStrike, shortStrike, @@ -1140,7 +1136,7 @@ contract PanopticHelper { /// @notice creates a BATS (AKA double ratio spread) w/ call and put ratio spreads. /// @dev example: createBATS(uniPoolAddress, 4, -50, 50, 0, 2). - /// @param univ3pool address of the pool + /// @param idV4 Uniswap V4 pool identifier /// @param width width of the spread /// @param longStrike strike of the long legs /// @param shortStrike strike of the short legs @@ -1148,7 +1144,7 @@ contract PanopticHelper { /// @param ratio ratio of the short legs to the long legs /// @return tokenId the position id with the strategy configured function createBATS( - address univ3pool, + PoolId idV4, int24 width, int24 longStrike, int24 shortStrike, @@ -1160,14 +1156,14 @@ contract PanopticHelper { // 2. a put ratio spread // call ratio spread - tokenId = createCallRatioSpread(univ3pool, width, longStrike, shortStrike, asset, ratio, 0); + tokenId = createCallRatioSpread(idV4, width, longStrike, shortStrike, asset, ratio, 0); // put ratio spread tokenId = TokenId.wrap( TokenId.unwrap(tokenId) + TokenId.unwrap( createPutRatioSpread( - address(0), + PoolId.wrap(0), width, longStrike, shortStrike, diff --git a/test/foundry/types/LeftRight.t.sol b/test/foundry/types/LeftRight.t.sol index 627de20..0ffa1cf 100644 --- a/test/foundry/types/LeftRight.t.sol +++ b/test/foundry/types/LeftRight.t.sol @@ -297,35 +297,6 @@ contract LeftRightTest is Test { } } - function test_Success_SubRectInts(int128 y, int128 z, int128 u, int128 v) public { - LeftRightSigned x = LeftRightSigned.wrap(0); - - x = harness.toLeftSlot(x, y); - x = harness.toRightSlot(x, z); - - LeftRightSigned xx = LeftRightSigned.wrap(0); - xx = harness.toLeftSlot(xx, u); - xx = harness.toRightSlot(xx, v); - - // now test add - unchecked { - if ((y - u > y && u > 0) || (y - u < y && u < 0)) { - // under/overflow - vm.expectRevert(Errors.UnderOverFlow.selector); - harness.subRect(x, xx); - } else if ((z - v > z && v > 0) || (z - v < z && v < 0)) { - // under/overflow - vm.expectRevert(Errors.UnderOverFlow.selector); - harness.subRect(x, xx); - } else { - // normal case - LeftRightSigned other = harness.subRect(x, xx); - assertEq(int128(harness.leftSlot(other)), y - u > 0 ? y - u : int128(0)); - assertEq(int128(harness.rightSlot(other)), z - v > 0 ? z - v : int128(0)); - } - } - } - function test_Success_AddCapped_NoCap( LeftRightUnsigned x, LeftRightUnsigned dx, diff --git a/test/foundry/types/harnesses/LeftRightHarness.sol b/test/foundry/types/harnesses/LeftRightHarness.sol index b0d7100..823d987 100644 --- a/test/foundry/types/harnesses/LeftRightHarness.sol +++ b/test/foundry/types/harnesses/LeftRightHarness.sol @@ -172,17 +172,6 @@ contract LeftRightHarness { return r; } - /** - * @notice Subtract two int256 bit LeftRight-encoded words; rectify to 0 on negative result. - * @param x the minuend - * @param y the subtrahend - * @return z the difference x - y - */ - function subRect(LeftRightSigned x, LeftRightSigned y) public pure returns (LeftRightSigned) { - LeftRightSigned r = LeftRightLibrary.subRect(x, y); - return r; - } - function addCapped( LeftRightUnsigned x, LeftRightUnsigned dx,