PmavHookQuoted

0x24f90d176cdb7f288d4390083dfaa41d6ff968cc

Verification
Verified
0.8.28+commit.7893614a
Type
Contract
39,157 bytes
ABI entries
91
50 read · 23 write
License
none

Contract information

Address
0x24f90d176cdb7f288d4390083dfaa41d6ff968cc
Chain
Robinhood Chain (4663)
Compiler
0.8.28+commit.7893614a
Optimization
Enabled
Creation tx
0x9f13618f7e…76dc28743d

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (50)

ANTI_SNIPE_MAX_BPS()uint256
ANTI_SNIPE_MAX_WINDOW()uint256
CTO_INACTIVITY()uint256
CTO_VETO_DELAY()uint256
CURVE_SUPPLY()uint256
FEE_BPS()uint256
FULL_TICK_LOWER()int24
FULL_TICK_UPPER()int24
GRADUATED_LP_FEE()uint24
GRADUATION_GAS()uint256
GRAD_RAISE()uint256
LP_SUPPLY()uint256
L_CURVE_C0()uint128
L_CURVE_Q0()uint128
MAX_WALLET()uint256
PAIR_COOLDOWN()uint256
QUOTE_DECIMALS()uint8
QUOTE_TOKEN()address
SQRT_GRAD_C0()uint160
SQRT_GRAD_Q0()uint160
SQRT_START_C0()uint160
SQRT_START_Q0()uint160
TICK_LOWER_Q0()int24
TICK_SPACING()int24
TICK_UPPER_Q0()int24
TOTAL_SUPPLY()uint256
allCoins(uint256)address
antiSnipeEnabled(address)bool
antiSnipePeakBps()uint256
antiSnipeWindow()uint256
coinCount()uint256
creatorFees(address)uint256
ctoProposals(address)address, uint64
curveParamsOf(address)bool, int24, int24, uint160, uint160, uint128
feeRecipientLocked(address)bool
feeRecipientOf(address)address
getHookPermissions()tuple
lastActivity(address)uint256
lastClaim(address)uint256
owner()address
pairLastLaunch(bytes32)uint64
platformFees()uint256
poolKeyOf(address)tuple
poolManager()address
poolOf(address)bytes32
pools(bytes32)address, address, uint64, uint64, uint16, uint16, bool, uint128, uint128
quoteBuy(address, uint256)uint256, uint256, uint256
quoteSell(address, uint256)uint256, uint256
refunds(address)uint256
state(address)uint256, uint256, uint256, uint256, uint256, bool, address

Events (15)

AntiSnipeChangedAntiSnipeOffCoinCreatedCreatorFeesClaimedCtoRedirectCtoRedirectProposedFeeRecipientChangedFeeRecipientLockedEventGraduatedOwnerChangedPlatformFeesWithdrawnPoolFeesCollectedRefundClaimedRefundQueuedTrade

ABI

[
  {
    "inputs": [
      {
        "internalType": "contract IPoolManager",
        "name": "_manager",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_owner",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_quoteToken",
        "type": "address"
      },
      {
        "internalType": "uint8",
        "name": "_quoteDecimals",
        "type": "uint8"
      },
      {
        "internalType": "uint256",
        "name": "_gradRaise",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "HookNotImplemented",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotPoolManager",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "peakBps",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "window",
        "type": "uint256"
      }
    ],
    "name": "AntiSnipeChanged",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "AntiSnipeOff",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "feeRecipient",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "metadata",
        "type": "string"
      }
    ],
    "name": "CoinCreated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "CreatorFeesClaimed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "oldRecipient",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newRecipient",
        "type": "address"
      }
    ],
    "name": "CtoRedirect",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "oldRecipient",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newRecipient",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "executableAt",
        "type": "uint256"
      }
    ],
    "name": "CtoRedirectProposed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "oldRecipient",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newRecipient",
        "type": "address"
      }
    ],
    "name": "FeeRecipientChanged",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      }
    ],
    "name": "FeeRecipientLockedEvent",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethInPool",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokensInPool",
        "type": "uint256"
      }
    ],
    "name": "Graduated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "oldOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnerChanged",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "PlatformFeesWithdrawn",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "creatorRecipient",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "creatorWei",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "creatorTokens",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "platformWei",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "platformTokens",
        "type": "uint256"
      }
    ],
    "name": "PoolFeesCollected",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "RefundClaimed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "RefundQueued",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "trader",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "isBuy",
        "type": "bool"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethGross",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethNet",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenAmount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint160",
        "name": "sqrtPriceX96",
        "type": "uint160"
      },
      {
        "indexed": false,
        "internalType": "uint128",
        "name": "tokensSold",
        "type": "uint128"
      },
      {
        "indexed": false,
        "internalType": "uint128",
        "name": "raisedWei",
        "type": "uint128"
      }
    ],
    "name": "Trade",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "ANTI_SNIPE_MAX_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "ANTI_SNIPE_MAX_WINDOW",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "CTO_INACTIVITY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "CTO_VETO_DELAY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "CURVE_SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "FEE_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "FULL_TICK_LOWER",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "FULL_TICK_UPPER",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "GRADUATED_LP_FEE",
    "outputs": [
      {
        "internalType": "uint24",
        "name": "",
        "type": "uint24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "GRADUATION_GAS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "GRAD_RAISE",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "LP_SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "L_CURVE_C0",
    "outputs": [
      {
        "internalType": "uint128",
        "name": "",
        "type": "uint128"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "L_CURVE_Q0",
    "outputs": [
      {
        "internalType": "uint128",
        "name": "",
        "type": "uint128"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_WALLET",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "PAIR_COOLDOWN",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "QUOTE_DECIMALS",
    "outputs": [
      {
        "internalType": "uint8",
        "name": "",
        "type": "uint8"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "QUOTE_TOKEN",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "SQRT_GRAD_C0",
    "outputs": [
      {
        "internalType": "uint160",
        "name": "",
        "type": "uint160"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "SQRT_GRAD_Q0",
    "outputs": [
      {
        "internalType": "uint160",
        "name": "",
        "type": "uint160"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "SQRT_START_C0",
    "outputs": [
      {
        "internalType": "uint160",
        "name": "",
        "type": "uint160"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "SQRT_START_Q0",
    "outputs": [
      {
        "internalType": "uint160",
        "name": "",
        "type": "uint160"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TICK_LOWER_Q0",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TICK_SPACING",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TICK_UPPER_Q0",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TOTAL_SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "sender",
        "type": "address"
      },
      {
        "components": [
          {
            "internalType": "Currency",
            "name": "currency0",
            "type": "address"
          },
          {
            "internalType": "Currency",
            "name": "currency1",
            "type": "address"
          },
          {
            "internalType": "uint24",
            "name": "fee",
            "type": "uint24"
          },
          {
            "internalType": "int24",
            "name": "tickSpacing",
            "type": "int24"
          },
          {
            "internalType": "contract IHooks",
            "name": "hooks",
            "type": "address"
          }
        ],
        "internalType": "struct PoolKey",
        "name": "key",
        "type": "tuple"
      },
      {
        "components": [
          {
            "internalType": "int24",
            "name": "tickLower",
            "type": "int24"
          },
          {
            "internalType": "int24",
            "name": "tickUpper",
            "type": "int24"
          },
          {
            "internalType": "int256",
            "name": "liquidityDelta",
            "type": "int256"
          },
          {
            "internalType": "bytes32",
            "name": "salt",
            "type": "bytes32"
          }
        ],
        "internalType": "struct ModifyLiquidityParams",
        "name": "params",
        "type": "tuple"
      },
      {
        "internalType": "BalanceDelta",
        "name": "delta",
        "type": "int256"
      },
      {
        "internalType": "BalanceDelta",
        "name": "feesAccrued",
        "type": "int256"
      },
      {
        "internalType": "bytes",
        "name": "hookData",
        "type": "bytes"
      }
    ],
    "name": "afterAddLiquidity",
    "outputs": [
      {
        "internalType": "bytes4",
        "name": "",
        "type": "bytes4"
      },
      {
        "internalType": "BalanceDelta",
        "name": "",
        "type": "int256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "sender",
        "type": "address"
      },
      {
        "components": [
          {
            "internalType": "Currency",
            "name": "currency0",
            "type": "address"
          },
          {
            "internalType": "Currency",
            "name": "currency1",
            "type": "address"
          },
          {
            "internalType": "uint24",
            "name": "fee",
            "type": "uint24"
          },
          {
            "internalType": "int24",
            "name": "tickSpacing",
            "type": "int24"
          },
          {
            "internalType": "contract IHooks",
            "name": "hooks",
            "type": "address"
          }
        ],
        "internalType": "struct PoolKey",
        "name": "key",
        "type": "tuple"
      },
      {
        "internalType": "uint256",
        "name": "amount0",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "amount1",
        "type": "uint256"
      },
      {
        "internalType": "bytes",
        "name": "hookData",
        "type": "bytes"
      }
    ],
    "name": "afterDonate",
    "outputs": [
      {
        "internalType": "bytes4",
        "name": "",
        "type": "bytes4"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "sender",
        "type": "address"
      },
      {
        "components": [
          {
            "internalType": "Currency",
            "name": "currency0",
            "type": "address"
          },
          {
            "internalType": "Currency",
            "name": "currency1",
            "type": "address"
          },
          {
            "internalType": "uint24",
            "name": "fee",
            "type": "uint24"
          },
          {
            "internalType": "int24",
            "name": "tickSpacing",
            "type": "int24"
          },
          {
            "internalType": "contract IHooks",
            "name": "hooks",
            "type": "address"
          }
        ],
        "internalType": "struct PoolKey",
        "name": "key",
        "type": "tuple"
      },
      {
        "internalType": "uint160",
        "name": "sqrtPriceX96",
        "type": "uint160"
      },
      {
        "internalType": "int24",
        "name": "tick",
        "type": "int24"
      }
    ],
    "name": "afterInitialize",
    "outputs": [
      {
        "internalType": "bytes4",
        "name": "",
        "type": "bytes4"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "sender",
        "type": "address"
      },
      {
        "components": [
          {
            "internalType": "Currency",
            "name": "currency0",
            "type": "address"
          },
          {
            "internalType": "Currency",
            "name": "currency1",
            "type": "address"
          },
          {
            "internalType": "uint24",
            "name": "fee",
            "type": "uint24"
          },
          {
            "internalType": "int24",
            "name": "tickSpacing",
            "type": "int24"
          },
          {
            "internalType": "contract IHooks",
            "name": "hooks",
            "type": "address"
          }
        ],
        "internalType": "struct PoolKey",
        "name": "key",
        "type": "tuple"
      },
      {
        "components": [
          {
            "internalType": "int24",
            "name": "tickLower",
            "type": "int24"
          },
          {
            "internalType": "int24",
            "name": "tickUpper",
            "type": "int24"
          },
          {
            "internalType": "int256",
            "name": "liquidityDelta",
            "type": "int256"
          },
          {
            "internalType": "bytes32",
            "name": "salt",
            "type": "bytes32"
          }
        ],
        "internalType": "struct ModifyLiquidityParams",
        "name": "params",
        "type": "tuple"
      },
      {
        "internalType": "BalanceDelta",
        "name": "delta",
        "type": "int256"
      },
      {
        "internalType": "BalanceDelta",
        "name": "feesAccrued",
        "type": "int256"
      },
      {
        "internalType": "bytes",
        "name": "hookData",
        "type": "bytes"
      }
    ],
    "name": "afterRemoveLiquidity",
    "outputs": [
      {
        "internalType": "bytes4",
        "name": "",
        "type": "bytes4"
      },
      {
        "internalType": "BalanceDelta",
        "name": "",
        "type": "int256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "sender",
        "type": "address"
      },
      {
        "components": [
          {
            "internalType": "Currency",
            "name": "currency0",
            "type": "address"
          },
          {
            "internalType": "Currency",
            "name": "currency1",
            "type": "address"
          },
          {
            "internalType": "uint24",
            "name": "fee",
            "type": "uint24"
          },
          {
            "internalType": "int24",
            "name": "tickSpacing",
            "type": "int24"
          },
          {
            "internalType": "contract IHooks",
            "name": "hooks",
            "type": "address"
          }
        ],
        "internalType": "struct PoolKey",
        "name": "key",
        "type": "tuple"
      },
      {
        "components": [
          {
            "internalType": "bool",
            "name": "zeroForOne",
            "type": "bool"
          },
          {
            "internalType": "int256",
            "name": "amountSpecified",
            "type": "int256"
          },
          {
            "internalType": "uint160",
            "name": "sqrtPriceLimitX96",
            "type": "uint160"
          }
        ],
        "internalType": "struct SwapParams",
        "name": "params",
        "type": "tuple"
      },
      {
        "internalType": "BalanceDelta",
        "name": "delta",
        "type": "int256"
      },
      {
        "internalType": "bytes",
        "name": "hookData",
        "type": "bytes"
      }
    ],
    "name": "afterSwap",
    "outputs": [
      {
        "internalType": "bytes4",
        "name": "",
        "type": "bytes4"
      },
      {
        "internalType": "int128",
        "name": "",
        "type": "int128"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "allCoins",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "antiSnipeEnabled",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "antiSnipePeakBps",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "antiSnipeWindow",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "sender",
        "type": "address"
      },
      {
        "components": [
          {
            "internalType": "Currency",
            "name": "currency0",
            "type": "address"
          },
          {
            "internalType": "Currency",
            "name": "currency1",
            "type": "address"
          },
          {
            "internalType": "uint24",
            "name": "fee",
            "type": "uint24"
          },
          {
            "internalType": "int24",
            "name": "tickSpacing",
            "type": "int24"
          },
          {
            "internalType": "contract IHooks",
            "name": "hooks",
            "type": "address"
          }
        ],
        "internalType": "struct PoolKey",
        "name": "key",
        "type": "tuple"
      },
      {
        "components": [
          {
            "internalType": "int24",
            "name": "tickLower",
            "type": "int24"
          },
          {
            "internalType": "int24",
            "name": "tickUpper",
            "type": "int24"
          },
          {
            "internalType": "int256",
            "name": "liquidityDelta",
            "type": "int256"
          },
          {
            "internalType": "bytes32",
            "name": "salt",
            "type": "bytes32"
          }
        ],
        "internalType": "struct ModifyLiquidityParams",
        "name": "params",
        "type": "tuple"
      },
      {
        "internalType": "bytes",
        "name": "hookData",
        "type": "bytes"
      }
    ],
    "name": "beforeAddLiquidity",
    "outputs": [
      {
        "internalType": "bytes4",
        "name": "",
        "type": "bytes4"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "sender",
        "type": "address"
      },
      {
        "components": [
          {
            "internalType": "Currency",
            "name": "currency0",
            "type": "address"
          },
          {
            "internalType": "Currency",
            "name": "currency1",
            "type": "address"
          },
          {
            "internalType": "uint24",
            "name": "fee",
            "type": "uint24"
          },
          {
            "internalType": "int24",
            "name": "tickSpacing",
            "type": "int24"
          },
          {
            "internalType": "contract IHooks",
            "name": "hooks",
            "type": "address"
          }
        ],
        "internalType": "struct PoolKey",
        "name": "key",
        "type": "tuple"
      },
      {
        "internalType": "uint256",
        "name": "amount0",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "amount1",
        "type": "uint256"
      },
      {
        "internalType": "bytes",
        "name": "hookData",
        "type": "bytes"
      }
    ],
    "name": "beforeDonate",
    "outputs": [
      {
        "internalType": "bytes4",
        "name": "",
        "type": "bytes4"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "sender",
        "type": "address"
      },
      {
        "components": [
          {
            "internalType": "Currency",
            "name": "currency0",
            "type": "address"
          },
          {
            "internalType": "Currency",
            "name": "currency1",
            "type": "address"
          },
          {
            "internalType": "uint24",
            "name": "fee",
            "type": "uint24"
          },
          {
            "internalType": "int24",
            "name": "tickSpacing",
            "type": "int24"
          },
          {
            "internalType": "contract IHooks",
            "name": "hooks",
            "type": "address"
          }
        ],
        "internalType": "struct PoolKey",
        "name": "key",
        "type": "tuple"
      },
      {
        "internalType": "uint160",
        "name": "sqrtPriceX96",
        "type": "uint160"
      }
    ],
    "name": "beforeInitialize",
    "outputs": [
      {
        "internalType": "bytes4",
        "name": "",
        "type": "bytes4"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "sender",
        "type": "address"
      },
      {
        "components": [
          {
            "internalType": "Currency",
            "name": "currency0",
            "type": "address"
          },
          {
            "internalType": "Currency",
            "name": "currency1",
            "type": "address"
          },
          {
            "internalType": "uint24",
            "name": "fee",
            "type": "uint24"
          },
          {
            "internalType": "int24",
            "name": "tickSpacing",
            "type": "int24"
          },
          {
            "internalType": "contract IHooks",
            "name": "hooks",
            "type": "address"
          }
        ],
        "internalType": "struct PoolKey",
        "name": "key",
        "type": "tuple"
      },
      {
        "components": [
          {
            "internalType": "int24",
            "name": "tickLower",
            "type": "int24"
          },
          {
            "internalType": "int24",
            "name": "tickUpper",
            "type": "int24"
          },
          {
            "internalType": "int256",
            "name": "liquidityDelta",
            "type": "int256"
          },
          {
            "internalType": "bytes32",
            "name": "salt",
            "type": "bytes32"
          }
        ],
        "internalType": "struct ModifyLiquidityParams",
        "name": "params",
        "type": "tuple"
      },
      {
        "internalType": "bytes",
        "name": "hookData",
        "type": "bytes"
      }
    ],
    "name": "beforeRemoveLiquidity",
    "outputs": [
      {
        "internalType": "bytes4",
        "name": "",
        "type": "bytes4"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "sender",
        "type": "address"
      },
      {
        "components": [
          {
            "internalType": "Currency",
            "name": "currency0",
            "type": "address"
          },
          {
            "internalType": "Currency",
            "name": "currency1",
            "type": "address"
          },
          {
            "internalType": "uint24",
            "name": "fee",
            "type": "uint24"
          },
          {
            "internalType": "int24",
            "name": "tickSpacing",
            "type": "int24"
          },
          {
            "internalType": "contract IHooks",
            "name": "hooks",
            "type": "address"
          }
        ],
        "internalType": "struct PoolKey",
        "name": "key",
        "type": "tuple"
      },
      {
        "components": [
          {
            "internalType": "bool",
            "name": "zeroForOne",
            "type": "bool"
          },
          {
            "internalType": "int256",
            "name": "amountSpecified",
            "type": "int256"
          },
          {
            "internalType": "uint160",
            "name": "sqrtPriceLimitX96",
            "type": "uint160"
          }
        ],
        "internalType": "struct SwapParams",
        "name": "params",
        "type": "tuple"
      },
      {
        "internalType": "bytes",
        "name": "hookData",
        "type": "bytes"
      }
    ],
    "name": "beforeSwap",
    "outputs": [
      {
        "internalType": "bytes4",
        "name": "",
        "type": "bytes4"
      },
      {
        "internalType": "BeforeSwapDelta",
        "name": "",
        "type": "int256"
      },
      {
        "internalType": "uint24",
        "name": "",
        "type": "uint24"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "claimCreatorFees",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "claimRefund",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "coinCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "collectPoolFees",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "metadata",
        "type": "string"
      },
      {
        "internalType": "address",
        "name": "feeRecipient",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "quoteIn",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minTokensOut",
        "type": "uint256"
      },
      {
        "internalType": "bool",
        "name": "antiSnipe",
        "type": "bool"
      }
    ],
    "name": "create",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "creatorFees",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "ctoProposals",
    "outputs": [
      {
        "internalType": "address",
        "name": "newRecipient",
        "type": "address"
      },
      {
        "internalType": "uint64",
        "name": "proposedAt",
        "type": "uint64"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "curveParamsOf",
    "outputs": [
      {
        "internalType": "bool",
        "name": "coinIs0",
        "type": "bool"
      },
      {
        "internalType": "int24",
        "name": "tickLower",
        "type": "int24"
      },
      {
        "internalType": "int24",
        "name": "tickUpper",
        "type": "int24"
      },
      {
        "internalType": "uint160",
        "name": "sqrtStart",
        "type": "uint160"
      },
      {
        "internalType": "uint160",
        "name": "sqrtGrad",
        "type": "uint160"
      },
      {
        "internalType": "uint128",
        "name": "lCurve",
        "type": "uint128"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "executeCtoRedirect",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "feeRecipientLocked",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "feeRecipientOf",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "getHookPermissions",
    "outputs": [
      {
        "components": [
          {
            "internalType": "bool",
            "name": "beforeInitialize",
            "type": "bool"
          },
          {
            "internalType": "bool",
            "name": "afterInitialize",
            "type": "bool"
          },
          {
            "internalType": "bool",
            "name": "beforeAddLiquidity",
            "type": "bool"
          },
          {
            "internalType": "bool",
            "name": "afterAddLiquidity",
            "type": "bool"
          },
          {
            "internalType": "bool",
            "name": "beforeRemoveLiquidity",
            "type": "bool"
          },
          {
            "internalType": "bool",
            "name": "afterRemoveLiquidity",
            "type": "bool"
          },
          {
            "internalType": "bool",
            "name": "beforeSwap",
            "type": "bool"
          },
          {
            "internalType": "bool",
            "name": "afterSwap",
            "type": "bool"
          },
          {
            "internalType": "bool",
            "name": "beforeDonate",
            "type": "bool"
          },
          {
            "internalType": "bool",
            "name": "afterDonate",
            "type": "bool"
          },
          {
            "internalType": "bool",
            "name": "beforeSwapReturnDelta",
            "type": "bool"
          },
          {
            "internalType": "bool",
            "name": "afterSwapReturnDelta",
            "type": "bool"
          },
          {
            "internalType": "bool",
            "name": "afterAddLiquidityReturnDelta",
            "type": "bool"
          },
          {
            "internalType": "bool",
            "name": "afterRemoveLiquidityReturnDelta",
            "type": "bool"
          }
        ],
        "internalType": "struct Hooks.Permissions",
        "name": "",
        "type": "tuple"
      }
    ],
    "stateMutability": "pure",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "lastActivity",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "lastClaim",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "lockFeeRecipient",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "newRecipient",
        "type": "address"
      }
    ],
    "name": "lockFeeRecipientTo",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "name": "pairLastLaunch",
    "outputs": [
      {
        "internalType": "uint64",
        "name": "",
        "type": "uint64"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "platformFees",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "poolKeyOf",
    "outputs": [
      {
        "components": [
          {
            "internalType": "Currency",
            "name": "currency0",
            "type": "address"
          },
          {
            "internalType": "Currency",
            "name": "currency1",
            "type": "address"
          },
          {
            "internalType": "uint24",
            "name": "fee",
            "type": "uint24"
          },
          {
            "internalType": "int24",
            "name": "tickSpacing",
            "type": "int24"
          },
          {
            "internalType": "contract IHooks",
            "name": "hooks",
            "type": "address"
          }
        ],
        "internalType": "struct PoolKey",
        "name": "",
        "type": "tuple"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "poolManager",
    "outputs": [
      {
        "internalType": "contract IPoolManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "poolOf",
    "outputs": [
      {
        "internalType": "PoolId",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "PoolId",
        "name": "",
        "type": "bytes32"
      }
    ],
    "name": "pools",
    "outputs": [
      {
        "internalType": "address",
        "name": "coin",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "feeRecipient",
        "type": "address"
      },
      {
        "internalType": "uint64",
        "name": "launchBlock",
        "type": "uint64"
      },
      {
        "internalType": "uint64",
        "name": "launchTime",
        "type": "uint64"
      },
      {
        "internalType": "uint16",
        "name": "snipePeakBps",
        "type": "uint16"
      },
      {
        "internalType": "uint16",
        "name": "snipeWindow",
        "type": "uint16"
      },
      {
        "internalType": "bool",
        "name": "graduated",
        "type": "bool"
      },
      {
        "internalType": "uint128",
        "name": "tokensSold",
        "type": "uint128"
      },
      {
        "internalType": "uint128",
        "name": "raisedWei",
        "type": "uint128"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "newRecipient",
        "type": "address"
      }
    ],
    "name": "proposeCtoRedirect",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "quoteGross",
        "type": "uint256"
      }
    ],
    "name": "quoteBuy",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "tokensOut",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "fee",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "refund",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokenAmount",
        "type": "uint256"
      }
    ],
    "name": "quoteSell",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "quoteNet",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "fee",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "refunds",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "peakBps",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "window",
        "type": "uint256"
      }
    ],
    "name": "setAntiSnipeDefaults",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "newRecipient",
        "type": "address"
      }
    ],
    "name": "setFeeRecipient",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "setOwner",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "state",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "priceWei",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "mcapWei",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "ethReserve",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "tokensSold",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "progressBps",
        "type": "uint256"
      },
      {
        "internalType": "bool",
        "name": "graduated",
        "type": "bool"
      },
      {
        "internalType": "address",
        "name": "pool",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes",
        "name": "data",
        "type": "bytes"
      }
    ],
    "name": "unlockCallback",
    "outputs": [
      {
        "internalType": "bytes",
        "name": "",
        "type": "bytes"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      }
    ],
    "name": "withdrawPlatformFees",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;

/// ---------------------------------------------------------------------------
/// pmav.fun V2 — the curve IS the pool (Uniswap V4 hook architecture)
///
/// QUOTED VARIANT ("v2q") — the RWA quoted curve lane hook per
/// docs/RWA_QUOTED_CURVE_PLAN.md. Derived from the audited PmavHook v2.1
/// (contracts/src/v2/PmavHook.sol, LIVE on Robinhood Chain 4663) with EXACTLY
/// four categories of change, all forced by the quote currency becoming an
/// IMMUTABLE ERC20 (USDG first; stock tokens in Phase 2) instead of native ETH:
///
///   (a) SETTLEMENT: every native-ETH settle/take/mint path operates on the
///       quote ERC20. Buys settle quote through the PoolManager (routers pull
///       the swapper's quote via Permit2/SETTLE_ALL; the pool currency IS the
///       quote token, never CurrencyLibrary.ADDRESS_ZERO). Sells pay quote out
///       through TAKE_ALL. claimCreatorFees / claimRefund / withdrawPlatformFees
///       pay quote via poolManager.take (an ERC20 transfer by the PM). The dev
///       buy inside create() pulls quote from the creator by transferFrom
///       (one-time ERC20 allowance to this hook) and settles it into the PM.
///       No .call{value:} exists anywhere; create() is not payable; there is
///       no receive() — any native ETH sent here reverts.
///   (b) CURRENCY ORDERING: native ETH was always currency0; an ERC20 quote
///       sorts against each coin address dynamically. The hook derives
///       coinIs0 per pool (coin < QUOTE_TOKEN) and EVERY math site honors it:
///       curve pricing, fee capture side, first-hour cap clamp, graduation
///       trigger direction, graduation reshape, quoter views. Both
///       orientations' constants are precomputed in the constructor.
///   (c) UNITS + CURVE CONSTANTS: all raise/fee/premium/ledger amounts are
///       quote units at QUOTE_DECIMALS. The curve's tick range and liquidity
///       are DERIVED IN THE CONSTRUCTOR from (gradRaise, CURVE_SUPPLY) using
///       the exact v1/v2 boundary conditions (derivation below) — the v2
///       literals [176620, 204350] are ETH/18dp specific and are NOT copied.
///   (d) TICKS: the graduated full-range position bounds stay the spacing-
///       aligned full range [-887270, 887270]; the CURVE range is computed
///       via TickMath from the derived prices and the ordering, aligned to
///       tickSpacing 10.
///
/// Everything else is byte-for-byte v2.1 semantics: 1% fee uniform 70/30
/// creator/platform with the odd unit to the creator, anti-snipe decay,
/// block+1, 2% first-hour fill-to-cap + refund waterfall, 90-minute ticker
/// cooldown, in-place graduation, permanent lock, and the v1-named events
/// (CoinCreated / Trade / Graduated / FeeRecipientChanged ...) with UNCHANGED
/// field names — the Trade fields still read ethGross/ethNet and the Pool
/// struct still reads raisedWei for indexer/terminal ABI compatibility, but
/// the VALUES they carry are quote units at QUOTE_DECIMALS.
///
/// CURVE CONSTANT DERIVATION (the same boundary conditions the v2 curve
/// documents in docs/research/V3_RESEARCH_CURVEDESIGN.md §1.3):
///
///   The curve is a constant-product AMM with virtual reserves (x0, y0)
///   (x = quote, y = coin, both in RAW units), selling S = CURVE_SUPPLY
///   tokens for a total raise of R = gradRaise, chosen so that
///     (1) selling exactly S tokens raises exactly R:  R = x0*S/(y0-S)
///     (2) the price moves exactly 16x start->graduation (4x in sqrt):
///         (y0/(y0-S)) * ((x0+R)/x0) = 16
///   By constant product (x0+R)/x0 == y0/(y0-S), so (2) gives y0/(y0-S) = 4,
///   hence   y0 = (4/3)*S   and from (1)   x0 = R/3.
///   (v2 sanity: R = 4.275 ETH gives x0 = 1.425 ETH, y0 = 1,066,666,667
///   tokens — exactly the v1 constants the research doc re-derived.)
///
///   Price start (coin per quote, currency1/currency0 with quote = currency0):
///     Pstart = y0/x0 = 4S/R          Pgrad = Pstart/16 = S/(4R)
///     sqrtPstartX96 = 2*sqrt(S/R)*2^96      sqrtPgradX96 = sqrtPstartX96/4
///   Both edges are converted to ticks (TickMath.getTickAtSqrtPrice), rounded
///   to the NEAREST tickSpacing-10 multiple — the exact alignment discipline
///   that produced v2's [176620, 204350] from the derived 176619.27/204346.54.
///   The canonical curve constants are then re-read from the ALIGNED ticks
///   (getSqrtPriceAtTick) and L is sized so the position holds exactly
///   CURVE_SUPPLY tokens at the start edge; the token side is therefore exact
///   by construction and the ≤5-tick alignment shifts land in the realized
///   raise (≤ ~5 bps, the same tolerance class as v2's ~4 bps).
///
///   When the coin sorts BELOW the quote (coinIs0), price = quote/coin is the
///   exact reciprocal, so the aligned ticks NEGATE: the curve runs from
///   -tickStart (init, all tokens) UP to -tickGrad (graduation). Both
///   orientations' sqrt prices and liquidity are precomputed as immutables.
///
/// Vendored dependency pins (see contracts/lib/):
///   v4-periphery  commit 3779387e5d296f39df543d23524b050f89a62917
///                 (last revision carrying src/utils/BaseHook.sol + HookMiner,
///                  parent of PR #510 which moved hooks out of the repo)
///   v4-core       commit 59d3ecf53afa9264a16bba0e38f4c5d2231f80bc
///                 (tag v4.0.0 + 12, the v4-periphery pinned submodule; matches
///                  the deployed PoolManager generation on chain 4663)
///
/// HOOK ADDRESS MINING: the low 14 bits of this contract's address MUST equal
/// 0x28CC = BEFORE_INITIALIZE | BEFORE_ADD_LIQUIDITY | BEFORE_SWAP | AFTER_SWAP
///        | BEFORE_SWAP_RETURNS_DELTA | AFTER_SWAP_RETURNS_DELTA
///        = 1<<13 | 1<<11 | 1<<7 | 1<<6 | 1<<3 | 1<<2.
/// Deployment goes through the canonical CREATE2 deployer
/// (0x4e59b44847b379578588920cA78FbF26c0B4956C, live on chain 4663) with a
/// salt found by an in-script first-flag-satisfying scan — see
/// script/DeployV2Q.s.sol, which asserts
/// uint160(address(hook)) & Hooks.ALL_HOOK_MASK == 0x28CC before anything else.
/// The BaseHook constructor additionally runs Hooks.validateHookPermissions.
///
/// V4-CORE SEMANTICS NOTE (verified against the vendored source, differs from
/// parts of docs/research/V4_DESIGN.md): every Hooks library wrapper no-ops
/// when msg.sender == the hook itself ("noSelfCall"). Consequences:
///   - create()'s own initialize/modifyLiquidity/swap calls NEVER re-enter the
///     hook callbacks, so the spec's `_creating`/`_creatingCoin` transient
///     gating is dead code. beforeInitialize instead reverts UNCONDITIONALLY
///     (Flaunch shape): no pool that is not created by create() can ever
///     attach this hook, and create()'s own initialize skips the callback.
///   - the dev buy cannot be metered by beforeSwap/afterSwap; its fee, cap
///     clamp, ledger credit and Trade event are implemented inline in the
///     create() unlock callback with identical arithmetic.
///   - graduation's inner swap/modifyLiquidity never re-trigger callbacks, so
///     no `_inGraduation` transient is needed; `graduated = true` is still set
///     first for idempotence.
///
/// QUOTE ISSUER PAUSE (disclosed product consequence, not a bug): the quote
/// issuer (USDG: Paxos; stocks: Robinhood Europe UAB) can pause its token.
/// A paused quote freezes this lane's trading — buys, sells, dev buys, fee
/// claims and refund claims revert at the token — and nothing in the hook's
/// ledgers or the pool corrupts; everything resumes when the token unpauses.
/// FEE-ON-TRANSFER quote tokens are NOT supported: the dev-buy pull measures
/// the received amount and reverts on any shortfall, and router settlement of
/// a skimming token fails inside the PoolManager (CurrencyNotSettled). USDG
/// is not fee-on-transfer.
/// ---------------------------------------------------------------------------

import {BaseHook} from "v4-periphery/src/utils/BaseHook.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";
import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {PoolId} from "@uniswap/v4-core/src/types/PoolId.sol";
import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol";
import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
import {
    BeforeSwapDelta, BeforeSwapDeltaLibrary, toBeforeSwapDelta
} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol";
import {ModifyLiquidityParams, SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
import {LPFeeLibrary} from "@uniswap/v4-core/src/libraries/LPFeeLibrary.sol";
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
import {SqrtPriceMath} from "@uniswap/v4-core/src/libraries/SqrtPriceMath.sol";
import {FullMath} from "@uniswap/v4-core/src/libraries/FullMath.sol";
import {FixedPoint96} from "@uniswap/v4-core/src/libraries/FixedPoint96.sol";
import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
import {LiquidityAmounts} from "v4-periphery/src/libraries/LiquidityAmounts.sol";
import {IERC20Minimal} from "@uniswap/v4-core/src/interfaces/external/IERC20Minimal.sol";

import {PmavCoin} from "../PmavCoin.sol";

/// v2.2 GENERATION (docs/V22_HOOK_GENERATION_SPEC.md, decisions locked
/// 2026-07-20). Derived from the audited v2q source; every diff is one of:
///   A. name+ticker PAIR cooldown (was ticker only)
///   B. two step CTO: propose + 12h veto window, inactivity floor 36h
///   C. opt in hookData refund beneficiary (pull only; empty = v2q exact)
///   D. completing buy fee clamp (fee charged on USED gross, audit L1)
///   L. PERMANENT 2% wallet cap (was first hour only): net position bought
///      through this pool per tx.origin, forever; sells free capacity;
///      curve buys fill to cap, post graduation over cap buys revert
///   M. per coin anti snipe ON/OFF chosen by the creator at create()
/// I3 DISCLOSURE (audit): the quote is an ISSUER CONTROLLED token. The
/// issuer can pause transfers or blocklist addresses at any time; a paused
/// quote freezes trading on every coin in this lane (curve included) until
/// unpaused, and a blocklisted fee recipient's accrued claims stay frozen
/// until unblocked. Funds are never lost by this hook; the native ETH lane
/// is unaffected by any quote issuer action.
///
/// @title PmavHookQuoted (v2.2) — the pmav.fun quoted-curve launchpad: factory +
///        Uniswap V4 hook + fee bank, curve denominated in an immutable ERC20
/// @notice Launch a coin for just gas. Every coin: 1B fixed supply, no taxes,
///         no owner. Coins trade from transaction one as a Uniswap V4 pool
///         PAIRED WITH THE QUOTE TOKEN (USDG first) on the canonical
///         PoolManager; the bonding curve is a single concentrated position
///         owned by this hook whose range is derived from gradRaise at
///         deployment and reproduces the v1 virtual-reserve curve law. When
///         the curve sells its 800M tokens the coin graduates IN THE SAME
///         POOL: the curve position is removed and the raise + the reserved
///         200M tokens are minted as a permanently locked full-range position.
///         No migration, no locker contract — this hook simply has no code
///         path that removes the full-range position.
///
///         Fair-launch rules enforced by the pool itself (router-proof):
///           - trading opens the block AFTER creation (only the dev's own
///             launch buy executes in the creation block)
///           - first hour: no wallet (tx.origin) buys more than 2% of supply
///             (20M) cumulatively — over-cap buys FILL TO CAP and auto-refund
///             the surplus in the same transaction; never a failed buy. If
///             the PoolManager's quote float ever cannot cover the instant
///             refund (settle-after router, young deployment), the surplus
///             queues for a one-click claimRefund() and the buy STILL succeeds
///           - decaying anti-snipe premium on buys (split 70/30 like the base)
///           - 1% fee on curve trades, split 70% creator / 30% platform
///           - sells are NEVER capped, taxed extra, or restricted. Ever.
contract PmavHookQuoted is BaseHook, IUnlockCallback {
    using StateLibrary for IPoolManager;

    // ---------------------------------------------------------------- consts

    uint256 public constant TOTAL_SUPPLY = 1_000_000_000e18;
    uint256 public constant CURVE_SUPPLY = 800_000_000e18; // sold on the curve
    uint256 public constant LP_SUPPLY = 200_000_000e18; // reserved for the full-range position
    /// v2.2: the 2% cap is PERMANENT (net tokens bought through this pool
    /// per tx.origin, sells free capacity). No time window exists anymore.
    uint256 public constant MAX_WALLET = 20_000_000e18; // 2% of supply
    uint256 public constant FEE_BPS = 100; // 1% of gross QUOTE per trade
    uint256 public constant ANTI_SNIPE_MAX_BPS = 2_500;
    uint256 public constant ANTI_SNIPE_MAX_WINDOW = 120;
    uint256 public constant CTO_INACTIVITY = 36 hours; // v2.2 (founder 2026-07-20, was 7 days)
    uint256 public constant CTO_VETO_DELAY = 12 hours; // v2.2: public notice window, one claim vetoes
    /// @notice v2.2: a just-used exact (name, ticker) PAIR cannot be launched
    ///         again for this long, enforced IN CONSENSUS. The same ticker
    ///         under a DIFFERENT name is allowed at any time (locked founder
    ///         decision; anti exact-vamp only). A hard constant, not
    ///         owner-tunable.
    uint256 public constant PAIR_COOLDOWN = 90 minutes;
    uint24 public constant GRADUATED_LP_FEE = 10_000; // 1% in pips (< MAX_LP_FEE 1_000_000)

    int24 public constant FULL_TICK_LOWER = -887_270;
    int24 public constant FULL_TICK_UPPER = 887_270;
    int24 public constant TICK_SPACING = 10;

    /// gas floor for the completing buy: revert loudly rather than let
    /// estimators settle on a path where graduation cannot execute.
    /// PROVENANCE (v2q re-measure, INTENTIONAL DIFF vs v2.1's 4_200_000):
    /// the QUOTED graduation step measured ~4,476,504 gas on COLD chain-4663
    /// mainnet state at fork block 10,090,000 (graduating buy minus a matched
    /// 99.5%-of-curve non-graduating buy —
    /// ForkGraduationQ.t.sol::test_graduation_step_gas_vs_floor), about 650k
    /// above the v2 native measurement of ~3,823,356. Floor = measured cost
    /// + ~11% headroom, the same F1 discipline that set v2's constant; the
    /// fork test asserts the floor stays above the measured step.
    uint256 public constant GRADUATION_GAS = 5_300_000; // v2.2: F1 re-measure 4,604,569 at fork block 15M; 5.0M was only +8.6% (rule: measured +10%)

    address internal constant DEAD = 0x000000000000000000000000000000000000dEaD;

    // ------------------------------------------------------ quote immutables

    /// the immutable quote ERC20 the whole lane is denominated in (USDG for
    /// the Phase 1 deployment; per-stock tokens for Phase 2 deployments of
    /// this same artifact). Fixed forever at deployment.
    address public immutable QUOTE_TOKEN;
    /// the quote token's decimals, validated 6..18 (and cross-checked against
    /// the token's own decimals() when it exposes one). INFORMATIONAL for
    /// indexers/UIs: on-chain math runs entirely in raw units.
    uint8 public immutable QUOTE_DECIMALS;
    /// the graduation raise in RAW quote units (Phase 1: 38_000e6 = $38k
    /// USDG). The curve constants below derive from it; unlike the ETH lane
    /// the raise never drifts with ETH price.
    uint256 public immutable GRAD_RAISE;
    /// ERC6909 claim id of the quote currency on the PoolManager
    /// (CurrencyLibrary: id == uint160 of the token address)
    uint256 internal immutable QUOTE_ID;

    // ---------------------------------------- derived curve constants, per
    // ordering. "_Q0" = quote is currency0 (coin address > quote address):
    // price = coin/quote, the curve walks DOWN from TICK_UPPER_Q0 to
    // TICK_LOWER_Q0 exactly like v2's ETH pools. "_C0" = coin is currency0:
    // price = quote/coin, ticks negate, the curve walks UP from -TICK_UPPER_Q0
    // to -TICK_LOWER_Q0.

    int24 public immutable TICK_LOWER_Q0; // graduation tick (quote = currency0)
    int24 public immutable TICK_UPPER_Q0; // curve start / pool init tick (quote = currency0)

    /// derived once at deploy from TickMath — bit-exact with the pool's math
    uint160 public immutable SQRT_START_Q0; // sqrtPrice at TICK_UPPER_Q0
    uint160 public immutable SQRT_GRAD_Q0; // sqrtPrice at TICK_LOWER_Q0
    uint160 public immutable SQRT_START_C0; // sqrtPrice at -TICK_UPPER_Q0
    uint160 public immutable SQRT_GRAD_C0; // sqrtPrice at -TICK_LOWER_Q0
    uint160 internal immutable SQRT_FULL_LOWER;
    uint160 internal immutable SQRT_FULL_UPPER;
    /// curve position liquidity per ordering: L such that the position holds
    /// exactly the 800M curve tokens at the start edge (the token side is
    /// currency1 when quote is currency0, currency0 otherwise — the two L
    /// values differ only by the helpers' rounding)
    uint128 public immutable L_CURVE_Q0;
    uint128 public immutable L_CURVE_C0;

    // ---------------------------------------------------------------- state

    struct Pool {
        address coin;
        address feeRecipient;
        uint64 launchBlock;
        uint64 launchTime;
        uint16 snipePeakBps; // frozen at create from factory defaults
        uint16 snipeWindow; // seconds; frozen at create
        bool graduated;
        uint128 tokensSold; // informational + invariant cross-check
        uint128 raisedWei; // RAW QUOTE UNITS (name kept for ABI/indexer parity)
    }

    mapping(PoolId => Pool) public pools;
    mapping(address => PoolId) public poolOf; // coin => pool id
    address[] public allCoins;

    /// cumulative first-hour purchases: pool => tx.origin => tokens bought
    /// v2.2 BALANCE BASED 2% cap (founder L3, 2026-07-20): NO persistent
    /// ledger. The cap is a live snapshot of tx.origin's holding — selling
    /// OR transferring tokens away frees the allowance at once. Only
    /// transient per-tx state exists (see the cap helpers below).

    /// claimable creator fees, keyed by recipient (QUOTE raw units, pull;
    /// backed by the hook's ERC6909 quote claims on the PoolManager)
    mapping(address => uint256) public creatorFees;
    uint256 public platformFees;

    /// queued over-cap refunds (QUOTE raw units, pull; backed by the hook's
    /// ERC6909 claims like the fee ledgers). Ruling 19.B waterfall branch 2:
    /// written ONLY when a settle-after router hits a PoolManager whose quote
    /// float cannot cover the instant take() — expected rarely, and only
    /// while the lane is young.
    mapping(address => uint256) public refunds;

    /// CTO rail, ported byte-for-byte from V1 (see Pmav.sol for the doctrine):
    /// an ACTIVE dev can never have their fee stream seized; activity =
    /// per-coin steward actions + the recipient claiming fees anywhere.
    mapping(address => uint256) public lastActivity; // per-coin steward action
    mapping(address => uint256) public lastClaim; // per-recipient claim time

    /// one-way fee recipient lock (locked coins are CTO-immune)
    mapping(address => bool) public feeRecipientLocked;

    /// v2.2: last launch timestamp per case-insensitive (name, ticker) PAIR
    /// hash (hash of hashes, collision unambiguous). create() reverts while
    /// an exact pair is inside its PAIR_COOLDOWN window, in consensus, no
    /// matter what frontend (or none) the caller uses.
    mapping(bytes32 => uint64) public pairLastLaunch;

    /// v2.2 two step CTO: pending proposal per coin (zeroed on execute; a
    /// re-propose overwrites). Any dev liveness signal newer than proposedAt
    /// cancels the execute — no silent seizure path exists.
    struct CtoProposal {
        address newRecipient;
        uint64 proposedAt;
    }

    mapping(address => CtoProposal) public ctoProposals;

    /// anti-snipe FACTORY DEFAULTS: copied into each pool at create() and
    /// frozen there. Owner tuning affects FUTURE launches only, within the
    /// hard caps — no function can change a live pool's swap behavior.
    uint256 public antiSnipePeakBps = 1_500; // 15%
    uint256 public antiSnipeWindow = 30; // seconds

    address public owner;
    uint256 private _lock = 1;

    // -------------------------------------------------------- transient state

    /// swap reentrancy lock (risk register #17: EIP-7702 delegated EOA — or a
    /// quote token with transfer callbacks — can run code on the refund
    /// take() and try to re-enter the pool mid-callback)
    bool private transient _swapLock;
    /// fee credited / refund taken in beforeSwap, consumed by afterSwap for
    /// the Trade event (reset at the top of every beforeSwap)
    uint256 private transient _pendingFee;
    uint256 private transient _pendingRefund;

    // ---------------------------------------------------------------- events
    // v1 names AND field names preserved — the indexer consumes these
    // unchanged (§14). The eth* fields carry QUOTE RAW UNITS on this variant.

    event CoinCreated(
        address indexed token,
        bytes32 indexed poolId,
        address indexed creator,
        address feeRecipient,
        string name,
        string symbol,
        string metadata
    );
    event Trade(
        address indexed token,
        address indexed trader,
        bool isBuy,
        uint256 ethGross,
        uint256 ethNet,
        uint256 tokenAmount,
        uint160 sqrtPriceX96,
        uint128 tokensSold,
        uint128 raisedWei
    );
    event Graduated(address indexed token, bytes32 indexed poolId, uint256 ethInPool, uint256 tokensInPool);
    event FeeRecipientChanged(address indexed token, address indexed oldRecipient, address indexed newRecipient);
    event CtoRedirect(address indexed token, address indexed oldRecipient, address indexed newRecipient);
    event CtoRedirectProposed(
        address indexed token, address indexed oldRecipient, address indexed newRecipient, uint256 executableAt
    );
    event AntiSnipeOff(address indexed token);
    event FeeRecipientLockedEvent(address indexed token, address indexed recipient);
    event CreatorFeesClaimed(address indexed recipient, uint256 amount);
    event RefundQueued(address indexed token, address indexed to, uint256 amount);
    event RefundClaimed(address indexed to, uint256 amount);
    event PlatformFeesWithdrawn(address indexed to, uint256 amount);
    event PoolFeesCollected(
        address indexed token,
        address indexed creatorRecipient,
        uint256 creatorWei,
        uint256 creatorTokens,
        uint256 platformWei,
        uint256 platformTokens
    );
    event AntiSnipeChanged(uint256 peakBps, uint256 window);
    event OwnerChanged(address indexed oldOwner, address indexed newOwner);

    // ------------------------------------------------------------- modifiers

    modifier nonReentrant() {
        require(_lock == 1, "reentrancy");
        _lock = 2;
        _;
        _lock = 1;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "not owner");
        _;
    }

    constructor(IPoolManager _manager, address _owner, address _quoteToken, uint8 _quoteDecimals, uint256 _gradRaise)
        BaseHook(_manager)
    {
        require(_owner != address(0), "zero addr");
        require(_quoteToken != address(0) && _quoteToken.code.length > 0, "quote not a contract");
        require(_quoteDecimals >= 6 && _quoteDecimals <= 18, "quote decimals");
        require(_gradRaise > 0, "zero raise");
        // deploy-time typo guard: when the quote exposes decimals() it must
        // agree with the argument. Tolerant staticcall: a quote without a
        // decimals() getter (none planned) simply skips the cross-check.
        (bool ok, bytes memory ret) = _quoteToken.staticcall(abi.encodeWithSignature("decimals()"));
        if (ok && ret.length >= 32) {
            require(abi.decode(ret, (uint256)) == _quoteDecimals, "decimals mismatch");
        }
        owner = _owner;
        QUOTE_TOKEN = _quoteToken;
        QUOTE_DECIMALS = _quoteDecimals;
        GRAD_RAISE = _gradRaise;
        QUOTE_ID = uint256(uint160(_quoteToken));

        // ---- curve constant derivation (see the header derivation note) ----
        // x0 = R/3, y0 = 4S/3  =>  Pstart = 4S/R, Pgrad = S/(4R) (coin/quote).
        // sqrt(S/R)*2^96, computed as sqrt(floor(S*2^96/R)) << 48 — the floor
        // before the sqrt and the integer sqrt cost < 1 part in 1e12 across
        // the whole supported (decimals, gradRaise) envelope, far inside the
        // tick alignment granularity (a tick is 1 part in 1e4).
        uint256 sqrtSRX96 = _sqrt(FullMath.mulDiv(CURVE_SUPPLY, FixedPoint96.Q96, _gradRaise)) << 48;
        uint256 sqrtStartExact = sqrtSRX96 * 2; // sqrt(4S/R)*2^96 — the curve start (quote = currency0)
        uint256 sqrtGradExact = sqrtSRX96 / 2; // sqrt(S/(4R))*2^96 — 16x price, 4x sqrt below start
        // both edges must live strictly inside V4's price space AND inside the
        // graduated full range (so the graduation reshape always has room)
        require(
            sqrtGradExact > TickMath.MIN_SQRT_PRICE && sqrtStartExact < TickMath.MAX_SQRT_PRICE,
            "raise out of range"
        );
        int24 tickStart = _alignNearest(TickMath.getTickAtSqrtPrice(uint160(sqrtStartExact)));
        int24 tickGrad = _alignNearest(TickMath.getTickAtSqrtPrice(uint160(sqrtGradExact)));
        require(tickGrad > FULL_TICK_LOWER && tickStart < FULL_TICK_UPPER, "raise out of range");
        require(tickGrad < tickStart, "degenerate curve"); // 16x span: always ~27,730 ticks

        TICK_LOWER_Q0 = tickGrad;
        TICK_UPPER_Q0 = tickStart;
        SQRT_START_Q0 = TickMath.getSqrtPriceAtTick(tickStart);
        SQRT_GRAD_Q0 = TickMath.getSqrtPriceAtTick(tickGrad);
        // coin-is-currency0 orientation: price is the exact reciprocal, ticks
        // negate (negating a multiple of 10 stays aligned)
        SQRT_START_C0 = TickMath.getSqrtPriceAtTick(-tickStart);
        SQRT_GRAD_C0 = TickMath.getSqrtPriceAtTick(-tickGrad);
        SQRT_FULL_LOWER = TickMath.getSqrtPriceAtTick(FULL_TICK_LOWER);
        SQRT_FULL_UPPER = TickMath.getSqrtPriceAtTick(FULL_TICK_UPPER);
        // L such that the position holds exactly the 800M curve tokens at the
        // start edge (pool initializes there: tokens only). Tokens are
        // currency1 in the Q0 orientation, currency0 in the C0 orientation.
        L_CURVE_Q0 = LiquidityAmounts.getLiquidityForAmount1(SQRT_GRAD_Q0, SQRT_START_Q0, CURVE_SUPPLY);
        L_CURVE_C0 = LiquidityAmounts.getLiquidityForAmount0(SQRT_START_C0, SQRT_GRAD_C0, CURVE_SUPPLY);
        require(L_CURVE_Q0 > 0 && L_CURVE_C0 > 0, "zero curve liquidity");
    }

    // ---------------------------------------------------------- hook wiring

    function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
        return Hooks.Permissions({
            beforeInitialize: true, // gate: only create() pools may attach us
            afterInitialize: false,
            beforeAddLiquidity: true, // gate: only hook liquidity during curve
            afterAddLiquidity: false,
            beforeRemoveLiquidity: false, // no third-party curve LP exists to gate
            afterRemoveLiquidity: false,
            beforeSwap: true, // block+1, cap clamp, quote-specified fee
            afterSwap: true, // cap ledger, quote-unspecified fee, graduation, Trade
            beforeDonate: false,
            afterDonate: false,
            beforeSwapReturnDelta: true,
            afterSwapReturnDelta: true,
            afterAddLiquidityReturnDelta: false,
            afterRemoveLiquidityReturnDelta: false
        });
    }

    /// Rogue-pool defense: nobody can initialize ANY pool that references this
    /// hook. Our own create() initializes via poolManager with the hook itself
    /// as msg.sender, which skips this callback entirely (core noSelfCall).
    function _beforeInitialize(address, PoolKey calldata, uint160) internal pure override returns (bytes4) {
        revert("pmav: pools only via create()");
    }

    /// During the curve only the hook may add liquidity (third-party liquidity
    /// inside the curve range would absorb buys and desync graduation
    /// economics). The hook's own adds skip this callback (noSelfCall); after
    /// graduation anyone may LP (v1 parity: community LPs on the V3 pool).
    function _beforeAddLiquidity(address, PoolKey calldata key, ModifyLiquidityParams calldata, bytes calldata)
        internal
        view
        override
        returns (bytes4)
    {
        require(pools[key.toId()].graduated, "curve phase");
        return BaseHook.beforeAddLiquidity.selector;
    }

    // ------------------------------------------------------------ beforeSwap

    function _beforeSwap(address, PoolKey calldata key, SwapParams calldata params, bytes calldata hookData)
        internal
        override
        returns (bytes4, BeforeSwapDelta, uint24)
    {
        PoolId id = key.toId();
        Pool storage p = pools[id];
        // graduated: PURE PASSTHROUGH — no caps, no premium, no deltas.
        // (graduation's own inner swap never reaches here: noSelfCall.)
        if (p.graduated) return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0);

        require(!_swapLock, "reentrancy");
        _swapLock = true;
        _pendingFee = 0;
        _pendingRefund = 0;

        // (a) block+1 — the Noxa killer feature, enforced at pool level.
        // (the dev buy never reaches this: hook-initiated swaps skip callbacks)
        require(block.number > p.launchBlock, "trading opens next block");

        // ordering (delta (b) in the header): a buy is quote-in, which is
        // zeroForOne when the quote is currency0 and oneForZero otherwise
        bool coinIs0 = Currency.unwrap(key.currency0) != QUOTE_TOKEN;
        bool isBuy = params.zeroForOne != coinIs0;

        // decaying anti-snipe premium: buys only, frozen params, base 1% always
        uint256 feeBps = FEE_BPS + (isBuy ? _antiSnipeBps(p) : 0);

        // (b) v2.2 PERMANENT 2% cap — FILL TO CAP + AUTO REFUND on the curve,
        // never a failed buy. No time window: the cap is forever.
        uint256 refund = 0;
        if (isBuy) {
            uint256 held0 = _capHeld(id, p.coin, tx.origin);
            uint256 capLeft = held0 >= MAX_WALLET ? 0 : MAX_WALLET - held0;
            require(capLeft > 0, "wallet cap: 2% max");
            if (params.amountSpecified < 0) {
                // buy exactIn (the universal path): clamp to the quote the cap allows
                uint256 maxGross = _maxGrossForCap(id, coinIs0, capLeft, feeBps);
                require(maxGross > 0, "wallet cap: 2% max");
                uint256 gross = uint256(-params.amountSpecified);
                if (gross > maxGross) refund = gross - maxGross;
            } else {
                // buy exactOut cannot be partially filled (the router TAKEs the
                // exact requested output; no refund exists to give)
                require(uint256(params.amountSpecified) <= capLeft, "wallet cap: 2% max");
            }
        }

        // (c) quote-denominated fee, taken on the SPECIFIED side when the
        // quote is specified (buy exactIn / sell exactOut). Otherwise
        // afterSwap takes it.
        if (isBuy == (params.amountSpecified < 0)) {
            uint256 quoteGross = _abs(params.amountSpecified) - refund;
            // v2.2 (audit L1): on a buy exactIn the fee base is bounded by the
            // gross the CURVE can actually consume to graduation (per
            // orientation maxNet rounded UP, grossed up rounding UP: the fee
            // never undercharges and an oversized completing buy pays fee on
            // the USED gross). Sell exactOut is untouched (audit L4).
            if (isBuy) {
                uint256 maxNet = coinIs0
                    ? SqrtPriceMath.getAmount1Delta(_effSqrtP(id, coinIs0), SQRT_GRAD_C0, L_CURVE_C0, true)
                    : SqrtPriceMath.getAmount0Delta(SQRT_GRAD_Q0, _effSqrtP(id, coinIs0), L_CURVE_Q0, true);
                uint256 maxFeeGross = FullMath.mulDivRoundingUp(maxNet, 10_000, 10_000 - feeBps);
                if (quoteGross > maxFeeGross) quoteGross = maxFeeGross;
            }
            uint256 fee = quoteGross * feeBps / 10_000;
            if (fee > 0) {
                poolManager.mint(address(this), QUOTE_ID, fee); // ERC6909 claim
                _credit(p, fee);
            }
            if (refund > 0) {
                // refund waterfall (ruling 19.B). Both branches charge the
                // swapper identically through the returned hook delta; the
                // buy SUCCEEDS unconditionally, at any PM balance, under
                // settle-first AND settle-after routers.
                // v2.2 (audit L2): 32 byte hookData naming a nonzero
                // beneficiary routes the surplus to refunds[beneficiary],
                // PULL ONLY. Garbage hookData is IGNORED, never a revert.
                address beneficiary = _refundBeneficiary(hookData);
                if (beneficiary != address(0)) {
                    poolManager.mint(address(this), QUOTE_ID, refund);
                    refunds[beneficiary] += refund;
                    emit RefundQueued(p.coin, beneficiary, refund);
                } else if (IERC20Minimal(QUOTE_TOKEN).balanceOf(address(poolManager)) >= refund) {
                    // 1: instant auto-refund, real quote tokens, straight back
                    //    to the signer (the common path once the lane has
                    //    graduated pools holding quote float)
                    poolManager.take(Currency.wrap(QUOTE_TOKEN), tx.origin, refund);
                } else {
                    // 2: floatless PM under a settle-after router — queue the
                    //    surplus as the hook's own ERC6909 claim (float-free
                    //    bookkeeping, backed by the buyer's own settle moments
                    //    later); pull via claimRefund()
                    poolManager.mint(address(this), QUOTE_ID, refund);
                    refunds[tx.origin] += refund;
                    emit RefundQueued(p.coin, tx.origin, refund);
                }
            }
            _pendingFee = fee;
            _pendingRefund = refund;
            uint256 hookTake = fee + refund;
            if (hookTake > 0) {
                return (BaseHook.beforeSwap.selector, toBeforeSwapDelta(int128(uint128(hookTake)), 0), 0);
            }
        }
        return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0);
    }

    // ------------------------------------------------------------- afterSwap

    /// `delta` is the raw pool swap result from the swapper's perspective
    /// (positive = swapper receives), BEFORE hook deltas are charged to the
    /// swapper. Order of operations is load-bearing: fee -> cap -> Trade ->
    /// graduation.
    function _afterSwap(address, PoolKey calldata key, SwapParams calldata params, BalanceDelta delta, bytes calldata hookData)
        internal
        override
        returns (bytes4, int128)
    {
        PoolId id = key.toId();
        Pool storage p = pools[id];
        bool coinIs0 = Currency.unwrap(key.currency0) != QUOTE_TOKEN;
        if (p.graduated) {
            // v2.2: pricing and fees stay PURE PASSTHROUGH, but the PERMANENT
            // 2% cap is enforced here forever — and an over cap exactIn buy
            // FILLS TO CAP instead of reverting (founder 2026-07-20): the
            // excess tokens are charged back through the afterSwap return
            // delta (the UNSPECIFIED side of a buy exactIn, either
            // orientation), sold back into the pool inside the same unlock
            // (noSelfCall), proceeds refunded in QUOTE through the curve's
            // exact waterfall. Buy exactOut over cap still reverts; zero
            // remaining capacity reverts; sells decrement saturating and
            // never revert. Trade ships with the DELIVERED token amount.
            // whole graduated branch lives in a helper to keep this frame
            // flat (legacy codegen stack room): cap walk, tail unwind, Trade
            return (BaseHook.afterSwap.selector, _afterSwapGraduated(key, p, coinIs0, params, delta, hookData));
        }

        int128 hookDelta = _settleCurveSwap(p, id, coinIs0, params, delta);

        // (d) graduation trigger: price-based — the graduation edge is reached
        // iff ~all 800M curve tokens are sold. Sells move price AWAY from it.
        // Q0 pools walk DOWN to the edge, C0 pools walk UP to it.
        if (params.zeroForOne != coinIs0) {
            (uint160 sqrtNow,,,) = poolManager.getSlot0(id);
            if (coinIs0 ? sqrtNow >= SQRT_GRAD_C0 : sqrtNow <= SQRT_GRAD_Q0) {
                require(gasleft() >= GRADUATION_GAS, "graduation buy needs more gas");
                _graduate(key, p, id, coinIs0);
            }
        }
        return (BaseHook.afterSwap.selector, hookDelta);
    }




    /// v2.2 graduated afterSwap: over cap exactIn buys fill to cap (excess
    /// clawed via the return delta and unwound + refunded); sells decrement
    /// the ledger saturating; the v1-named Trade ships on every swap. Split
    /// out of _afterSwap purely for legacy codegen stack room.
    function _afterSwapGraduated(
        PoolKey calldata key,
        Pool storage p,
        bool coinIs0,
        SwapParams calldata params,
        BalanceDelta delta,
        bytes calldata hookData
    ) internal returns (int128 gradHookDelta) {
        PoolId id = key.toId();
        bool isBuy = params.zeroForOne != coinIs0;
        uint256 e = _abs(int256(coinIs0 ? delta.amount1() : delta.amount0()));
        uint256 t = _abs(int256(coinIs0 ? delta.amount0() : delta.amount1()));
        if (isBuy) {
            (t, gradHookDelta) = _gradBuyCap(key, coinIs0, t, params.amountSpecified < 0, hookData);
        }
        // sells: nothing to record — balanceOf falls, the transient counter
        // resets on the buyer's next buy (balance moved)
        // gross == net post graduation (hook takes no fee delta); _emitTrade
        // folds sqrtPrice + counters, keeping this frame flat
        _emitTrade(p, id, isBuy, e, e, t);
    }

    /// v2.2: the graduated buy side cap walk (split out of _afterSwap for
    /// legacy codegen stack room). Returns (tokens DELIVERED, the afterSwap
    /// return delta charging the clawed excess). Zero remaining capacity
    /// reverts; exactOut over cap reverts; exactIn over cap fills to cap.
    function _gradBuyCap(PoolKey calldata key, bool coinIs0, uint256 t, bool exactIn, bytes calldata hookData)
        internal
        returns (uint256, int128)
    {
        PoolId id = key.toId();
        address coin = pools[id].coin;
        uint256 held = _capHeld(id, coin, tx.origin);
        require(held < MAX_WALLET, "wallet cap: 2% max");
        if (exactIn && held + t > MAX_WALLET) {
            uint256 excess = held + t - MAX_WALLET;
            _recordCapBuy(id, tx.origin, MAX_WALLET - held); // only the kept fill
            _unwindGradExcess(key, coinIs0, excess, hookData);
            return (t - excess, int128(uint128(excess)));
        }
        require(held + t <= MAX_WALLET, "wallet cap: 2% max");
        _recordCapBuy(id, tx.origin, t);
        return (t, 0);
    }

    /// v2.2 (founder 2026-07-20): sell the over cap tail of a graduated
    /// exactIn buy back into the pool (orientation aware) and refund the
    /// QUOTE proceeds through the curve's exact waterfall (beneficiary
    /// hookData honored, pull only; else instant push when the PM's quote
    /// float covers it, else pull ledger). Dust tails refund nothing.
    function _unwindGradExcess(PoolKey calldata key, bool coinIs0, uint256 excess, bytes calldata hookData)
        internal
    {
        address coin = pools[key.toId()].coin;
        BalanceDelta sd = poolManager.swap(
            key,
            SwapParams({
                zeroForOne: coinIs0,
                amountSpecified: -int256(excess),
                sqrtPriceLimitX96: coinIs0 ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1
            }),
            ""
        );
        uint256 quoteBack = uint256(uint128(coinIs0 ? sd.amount1() : sd.amount0()));
        if (quoteBack == 0) return;
        address beneficiary = _refundBeneficiary(hookData);
        if (beneficiary != address(0)) {
            poolManager.mint(address(this), QUOTE_ID, quoteBack);
            refunds[beneficiary] += quoteBack;
            emit RefundQueued(coin, beneficiary, quoteBack);
        } else if (IERC20Minimal(QUOTE_TOKEN).balanceOf(address(poolManager)) >= quoteBack) {
            poolManager.take(Currency.wrap(QUOTE_TOKEN), tx.origin, quoteBack);
        } else {
            poolManager.mint(address(this), QUOTE_ID, quoteBack);
            refunds[tx.origin] += quoteBack;
            emit RefundQueued(coin, tx.origin, quoteBack);
        }
    }

    /// fee (quote-unspecified quadrants), first-hour ledger, bookkeeping and
    /// the Trade event for a curve-phase swap
    function _settleCurveSwap(Pool storage p, PoolId id, bool coinIs0, SwapParams calldata params, BalanceDelta delta)
        internal
        returns (int128 hookDelta)
    {
        // what the pool actually moved (completing buys may consume less than
        // stated: unconsumed input through empty liquidity is never taken)
        uint256 quotePool = _abs(int256(coinIs0 ? delta.amount1() : delta.amount0()));
        uint256 tokenAmount = _abs(int256(coinIs0 ? delta.amount0() : delta.amount1()));
        bool isBuy = params.zeroForOne != coinIs0;

        uint256 fee;
        if (isBuy == (params.amountSpecified < 0)) {
            fee = _pendingFee; // already credited in beforeSwap
        } else {
            // (b') quote fee when the quote is the UNSPECIFIED currency
            // (buy exactOut / sell exactIn)
            uint256 feeBps = FEE_BPS + (isBuy ? _antiSnipeBps(p) : 0);
            fee = quotePool * feeBps / 10_000;
            if (fee > 0) {
                poolManager.mint(address(this), QUOTE_ID, fee);
                _credit(p, fee);
                hookDelta = int128(uint128(fee)); // taken from swapper's quote side
            }
        }
        // (c) first-hour ledger: record the ACTUAL fill (clamping already
        // happened in beforeSwap; the require is a belt-and-braces invariant)
        if (isBuy) {
            _recordCapBuy(id, tx.origin, tokenAmount);
            require(_capHeld(id, p.coin, tx.origin) <= MAX_WALLET, "cap invariant");
        }
        // sells: nothing to record — balanceOf falls; a sell must never revert
        _swapLock = false;

        // bookkeeping + parity event (indexer contract, §14). For buys the
        // pool quote is the net (fee charged on top, v1 exact); for sells it
        // is the gross (fee taken out of it).
        if (isBuy) {
            p.tokensSold += uint128(tokenAmount);
            p.raisedWei += uint128(quotePool);
        } else {
            // SATURATING (correctness audit F-3, defense-in-depth): these are
            // informational counters only — the graduation trigger is
            // price-based (pool sqrtPrice vs the graduation edge), never
            // counter-based, so flooring at 0 changes no economics. Underflow
            // is already unreachable by conservation (a sell can never return
            // more tokens/quote to the curve than the curve ever sold), but a
            // sell must NEVER revert on an arithmetic underflow: a reverting
            // sell is the #1 honeypot signature, and "sells never revert, ever"
            // is a hard invariant of our scanner-clean posture. Make it
            // structurally guaranteed rather than conservation-guaranteed.
            uint128 t = uint128(tokenAmount);
            uint128 e = uint128(quotePool);
            p.tokensSold = p.tokensSold > t ? p.tokensSold - t : 0;
            p.raisedWei = p.raisedWei > e ? p.raisedWei - e : 0;
        }
        _emitTrade(
            p,
            id,
            isBuy,
            isBuy ? quotePool + fee : quotePool,
            // saturate: a sell-exactOut demanding more quote than the curve
            // holds partially fills; the credited fee can exceed the fill
            isBuy ? quotePool : (quotePool > fee ? quotePool - fee : 0),
            tokenAmount
        );
    }

    // ------------------------------------------------------------ graduation

    /// Runs INSIDE the completing buy's afterSwap, all in one transaction,
    /// inside the router's active unlock. Same poolId forever: no migration,
    /// no pool switch, no window where the coin is untradeable. The inner
    /// swap/modifyLiquidity calls skip all hook callbacks (noSelfCall).
    function _graduate(PoolKey calldata key, Pool storage p, PoolId id, bool coinIs0) internal {
        p.graduated = true; // set FIRST (idempotence)

        uint160 sqrtGrad = coinIs0 ? SQRT_GRAD_C0 : SQRT_GRAD_Q0;
        (int24 tickLo, int24 tickHi) = coinIs0
            ? (-TICK_UPPER_Q0, -TICK_LOWER_Q0)
            : (TICK_LOWER_Q0, TICK_UPPER_Q0);

        // 1. remove the whole curve position: hook is credited the raise
        //    (~GRAD_RAISE quote + any donate() dust as fees) and unsold token
        //    dust
        uint256 quoteAvail;
        uint256 tokenCredit;
        {
            (BalanceDelta d,) = poolManager.modifyLiquidity(
                key,
                ModifyLiquidityParams({
                    tickLower: tickLo,
                    tickUpper: tickHi,
                    liquidityDelta: -int256(uint256(coinIs0 ? L_CURVE_C0 : L_CURVE_Q0)),
                    salt: 0
                }),
                ""
            );
            quoteAvail = uint256(uint128(coinIs0 ? d.amount1() : d.amount0()));
            tokenCredit = uint256(uint128(coinIs0 ? d.amount0() : d.amount1()));
        }

        // 2. repair the boundary overshoot: the completing swap may have walked
        //    price past the graduation edge through empty liquidity. With zero
        //    pool liquidity a 1-wei-specified swap walks price back to exactly
        //    the graduation price at zero cost (input is never consumed).
        //    Q0 pools overshoot BELOW the edge (repair swaps price UP);
        //    C0 pools overshoot ABOVE it (repair swaps price DOWN).
        {
            (uint160 sqrtP,,,) = poolManager.getSlot0(id);
            if (coinIs0 ? sqrtP > sqrtGrad : sqrtP < sqrtGrad) {
                poolManager.swap(
                    key, SwapParams({zeroForOne: coinIs0, amountSpecified: -1, sqrtPriceLimitX96: sqrtGrad}), ""
                );
            }
        }

        // 3. flip the dynamic LP fee: plain 1% forever (hook takes no deltas
        //    post-graduation; scanners see a vanilla 1% V4 pool)
        poolManager.updateDynamicLPFee(key, GRADUATED_LP_FEE);

        // 4. mint the permanently locked full-range position: the entire raise
        //    + the 200M reserve, amounts computed at the graduation price.
        //    (-1 wei headroom on both sides: modifyLiquidity rounds owed
        //    amounts UP, the liquidity helpers round L DOWN — the headroom
        //    guarantees the owed amounts never exceed what the hook holds.)
        uint128 lFull;
        {
            uint128 lQuote = coinIs0
                ? LiquidityAmounts.getLiquidityForAmount1(SQRT_FULL_LOWER, sqrtGrad, quoteAvail - 1)
                : LiquidityAmounts.getLiquidityForAmount0(sqrtGrad, SQRT_FULL_UPPER, quoteAvail - 1);
            uint128 lTok = coinIs0
                ? LiquidityAmounts.getLiquidityForAmount0(sqrtGrad, SQRT_FULL_UPPER, LP_SUPPLY - 1)
                : LiquidityAmounts.getLiquidityForAmount1(SQRT_FULL_LOWER, sqrtGrad, LP_SUPPLY - 1);
            lFull = lQuote < lTok ? lQuote : lTok;
        }
        (BalanceDelta md,) = poolManager.modifyLiquidity(
            key,
            ModifyLiquidityParams({
                tickLower: FULL_TICK_LOWER,
                tickUpper: FULL_TICK_UPPER,
                liquidityDelta: int256(uint256(lFull)),
                salt: 0
            }),
            ""
        );
        uint256 quoteUsed = uint256(uint128(-(coinIs0 ? md.amount1() : md.amount0())));
        uint256 tokUsed = uint256(uint128(-(coinIs0 ? md.amount0() : md.amount1())));

        // 5. settle the token side from the hook's 200M reserve (ERC20 custody
        //    since create), netting the removal's token-dust credit
        Currency coinCur = coinIs0 ? key.currency0 : key.currency1;
        if (tokUsed > tokenCredit) {
            poolManager.sync(coinCur);
            require(PmavCoin(p.coin).transfer(address(poolManager), tokUsed - tokenCredit), "transfer");
            poolManager.settle();
        } else if (tokenCredit > tokUsed) {
            // (only reachable via absurd token donations) burn the surplus
            poolManager.take(coinCur, DEAD, tokenCredit - tokUsed);
        }

        // 6. dust sweep, v1 parity: leftover quote units -> platformFees (as
        //    ERC6909 claims, which also zeroes the hook's remaining positive
        //    delta); leftover coin dust -> dead address
        uint256 quoteDust = quoteAvail - quoteUsed;
        if (quoteDust > 0) {
            poolManager.mint(address(this), QUOTE_ID, quoteDust);
            platformFees += quoteDust;
        }
        uint256 coinDust = PmavCoin(p.coin).balanceOf(address(this));
        if (coinDust > 0) PmavCoin(p.coin).transfer(DEAD, coinDust);

        emit Graduated(p.coin, PoolId.unwrap(id), quoteUsed, tokUsed);
    }

    // ---------------------------------------------------------------- launch

    /// @notice Launch a coin. Costs nothing beyond gas; `quoteIn` (optional)
    ///         is the dev buy in RAW QUOTE UNITS, pulled from the caller by
    ///         transferFrom (approve this hook first) and executed as the
    ///         first purchase in the same transaction (candle #1 on every
    ///         terminal), subject to the same 2% first-hour cap and anti-snipe
    ///         premium as everyone else. Replaces v2.1's payable msg.value dev
    ///         buy — the only signature change on this variant.
    function create(
        string calldata name,
        string calldata symbol,
        string calldata metadata,
        address feeRecipient,
        uint256 quoteIn,
        uint256 minTokensOut,
        bool antiSnipe
    ) external nonReentrant returns (address token) {
        require(bytes(name).length > 0 && bytes(name).length <= 64, "name");
        // ticker capped at 12 so every $TICKER stays a clickable X cashtag
        require(bytes(symbol).length > 0 && bytes(symbol).length <= 12, "symbol");
        require(bytes(metadata).length <= 8192, "metadata too big");

        // v2.2 on-chain anti-vamp: the exact (name, ticker) PAIR locks
        // protocol-wide for PAIR_COOLDOWN after any launch that uses it
        // (helper keeps create()'s stack flat; still no via-ir).
        _checkPairCooldown(name, symbol);

        // CREATE, not CREATE2: the coin address is unknowable pre-tx, so the
        // PoolKey (and pool id) cannot be squatted or front-run. The coin's
        // address also decides the pool's currency ordering (delta (b)).
        token = address(new PmavCoin(name, symbol)); // 1B minted to the hook

        // register + CoinCreated + pool initialize (helper keeps create()'s
        // stack flat — this profile compiles without via-ir, the v2.1
        // discipline; the extra quoteIn parameter costs the slot v2.1 spent
        // on the inline PoolKey local)
        _launch(token, feeRecipient == address(0) ? msg.sender : feeRecipient, name, symbol, metadata, antiSnipe);

        // dev buy funding: pull the quote up front so the unlock callback can
        // settle it
        if (quoteIn > 0) _pullDevBuyQuote(quoteIn);

        // seed the curve position (+ optional dev buy) inside one unlock
        // (poolKeyOf is deterministic, so recomputing it here is exact)
        poolManager.unlock(abi.encode(uint8(1), abi.encode(poolKeyOf(token), msg.sender, quoteIn, minTokensOut)));
    }

    /// @dev registration, the CoinCreated event and the pool initialize for a
    ///      fresh coin — split out of create() for legacy-codegen stack room.
    ///      Our own initialize skips beforeInitialize (noSelfCall); every
    ///      external initialize attempt against this hook reverts there.
    function _launch(
        address token,
        address recipient,
        string calldata name,
        string calldata symbol,
        string calldata metadata,
        bool antiSnipe
    ) internal {
        PoolKey memory key = _register(token, recipient, antiSnipe);
        emit CoinCreated(token, PoolId.unwrap(key.toId()), msg.sender, recipient, name, symbol, metadata);
        if (!antiSnipe) emit AntiSnipeOff(token);
        poolManager.initialize(key, token < QUOTE_TOKEN ? SQRT_START_C0 : SQRT_START_Q0);
    }

    /// @dev pulls the dev-buy quote from the creator by transferFrom.
    ///      Fee-on-transfer quotes are rejected by measuring the received
    ///      amount (documented stance: not supported, revert). USDG passes.
    function _pullDevBuyQuote(uint256 quoteIn) internal {
        uint256 balBefore = IERC20Minimal(QUOTE_TOKEN).balanceOf(address(this));
        _safeTransferFrom(QUOTE_TOKEN, msg.sender, address(this), quoteIn);
        require(IERC20Minimal(QUOTE_TOKEN).balanceOf(address(this)) - balBefore == quoteIn, "fee on transfer quote");
    }

    /// @dev v2.2 cooldown gate: case-insensitive over ASCII on the exact
    ///      (name, ticker) PAIR. First-ever use of a pair (slot 0) always
    ///      passes, every use re-arms the window. Same ticker + different
    ///      name NEVER collides here (locked founder decision 2026-07-20).
    function _checkPairCooldown(string calldata name, string calldata symbol) internal {
        bytes32 ph = _pairHash(name, symbol);
        uint64 last = pairLastLaunch[ph];
        require(last == 0 || block.timestamp >= uint256(last) + PAIR_COOLDOWN, "name+ticker cooldown");
        pairLastLaunch[ph] = uint64(block.timestamp);
    }

    /// @dev hash of hashes over the ASCII-lowercased name and ticker bytes:
    ///      keccak(keccak(lowerName) || keccak(lowerTicker)). Fixed width
    ///      inner hashes make the key COLLISION UNAMBIGUOUS. Non ASCII bytes
    ///      pass through unchanged (homoglyph lookalikes are a distinct pair
    ///      on chain and remain a display-layer concern by construction).
    function _pairHash(string calldata name, string calldata symbol) internal pure returns (bytes32) {
        return keccak256(abi.encodePacked(_lowerHash(bytes(name)), _lowerHash(bytes(symbol))));
    }

    function _lowerHash(bytes memory b) internal pure returns (bytes32) {
        for (uint256 i = 0; i < b.length; i++) {
            if (b[i] >= 0x41 && b[i] <= 0x5A) b[i] = bytes1(uint8(b[i]) + 32);
        }
        return keccak256(b);
    }

    function _register(address token, address recipient, bool antiSnipe) internal returns (PoolKey memory key) {
        key = poolKeyOf(token);
        PoolId id = key.toId();
        // v2.2: antiSnipe=false freezes a ZERO premium into the pool struct
        // (the swap path's _antiSnipeBps already short circuits on window 0)
        pools[id] = Pool({
            coin: token,
            feeRecipient: recipient,
            launchBlock: uint64(block.number),
            launchTime: uint64(block.timestamp),
            snipePeakBps: antiSnipe ? uint16(antiSnipePeakBps) : 0,
            snipeWindow: antiSnipe ? uint16(antiSnipeWindow) : 0,
            graduated: false,
            tokensSold: 0,
            raisedWei: 0
        });
        poolOf[token] = id;
        allCoins.push(token);
        lastActivity[token] = block.timestamp;
    }

    function coinCount() external view returns (uint256) {
        return allCoins.length;
    }

    /// canonical PoolKey for a pmav coin — fully deterministic, never stored.
    /// The quote ERC20 sorts against the coin address (V4 requires
    /// currency0 < currency1), so the ordering is per-coin (delta (b)).
    function poolKeyOf(address token) public view returns (PoolKey memory) {
        (Currency c0, Currency c1) = token < QUOTE_TOKEN
            ? (Currency.wrap(token), Currency.wrap(QUOTE_TOKEN))
            : (Currency.wrap(QUOTE_TOKEN), Currency.wrap(token));
        return PoolKey({
            currency0: c0,
            currency1: c1,
            fee: LPFeeLibrary.DYNAMIC_FEE_FLAG,
            tickSpacing: TICK_SPACING,
            hooks: this
        });
    }

    /// convenience for tests/UIs: the orientation and curve constants a given
    /// coin's pool actually uses
    function curveParamsOf(address token)
        external
        view
        returns (bool coinIs0, int24 tickLower, int24 tickUpper, uint160 sqrtStart, uint160 sqrtGrad, uint128 lCurve)
    {
        coinIs0 = token < QUOTE_TOKEN;
        (tickLower, tickUpper) = coinIs0 ? (-TICK_UPPER_Q0, -TICK_LOWER_Q0) : (TICK_LOWER_Q0, TICK_UPPER_Q0);
        sqrtStart = coinIs0 ? SQRT_START_C0 : SQRT_START_Q0;
        sqrtGrad = coinIs0 ? SQRT_GRAD_C0 : SQRT_GRAD_Q0;
        lCurve = coinIs0 ? L_CURVE_C0 : L_CURVE_Q0;
    }

    // ------------------------------------------------------- unlock callback

    /// dispatch for the hook's own unlock operations:
    /// 1 = create (seed curve + dev buy), 2 = payout (burn claims + take
    /// quote), 3 = collect graduated pool fees
    function unlockCallback(bytes calldata data) external onlyPoolManager returns (bytes memory) {
        (uint8 action, bytes memory payload) = abi.decode(data, (uint8, bytes));
        if (action == 1) {
            (PoolKey memory key, address creator, uint256 value, uint256 minTokensOut) =
                abi.decode(payload, (PoolKey, address, uint256, uint256));
            _createCallback(key, creator, value, minTokensOut);
        } else if (action == 2) {
            (address to, uint256 amount) = abi.decode(payload, (address, uint256));
            poolManager.burn(address(this), QUOTE_ID, amount);
            poolManager.take(Currency.wrap(QUOTE_TOKEN), to, amount);
        } else if (action == 3) {
            (address token) = abi.decode(payload, (address));
            _collectCallback(token);
        } else {
            revert("bad action");
        }
        return "";
    }

    function _createCallback(PoolKey memory key, address creator, uint256 value, uint256 minTokensOut) internal {
        PoolId id = key.toId();
        Pool storage p = pools[id];
        address token = p.coin;
        bool coinIs0 = token < QUOTE_TOKEN;
        (int24 tickLo, int24 tickHi) = coinIs0
            ? (-TICK_UPPER_Q0, -TICK_LOWER_Q0)
            : (TICK_LOWER_Q0, TICK_UPPER_Q0);

        // seed the curve: at the start edge the position is 100% tokens
        (BalanceDelta d,) = poolManager.modifyLiquidity(
            key,
            ModifyLiquidityParams({
                tickLower: tickLo,
                tickUpper: tickHi,
                liquidityDelta: int256(uint256(coinIs0 ? L_CURVE_C0 : L_CURVE_Q0)),
                salt: 0
            }),
            ""
        );
        // settle the owed tokens (<= 800M by L construction; no quote owed)
        uint256 owedTokens = uint256(uint128(-(coinIs0 ? d.amount0() : d.amount1())));
        poolManager.sync(coinIs0 ? key.currency0 : key.currency1);
        require(PmavCoin(token).transfer(address(poolManager), owedTokens), "transfer");
        poolManager.settle();

        // dev buy: a REAL PoolManager swap in the creation block
        if (value > 0) _devBuy(key, p, coinIs0, creator, value, minTokensOut);
    }

    /// Because the hook itself is the swapper, beforeSwap/afterSwap do NOT run
    /// (noSelfCall) — fee, premium, cap clamp, ledgers and the Trade event are
    /// applied inline here with arithmetic identical to beforeSwap. The quote
    /// was already pulled from the creator in create(); it settles into the PM
    /// here and any over-cap surplus transfers straight back to the creator.
    function _devBuy(
        PoolKey memory key,
        Pool storage p,
        bool coinIs0,
        address creator,
        uint256 value,
        uint256 minTokensOut
    ) internal {
        PoolId id = key.toId();
        uint256 feeBps = FEE_BPS + _antiSnipeBps(p); // premium at peak (t = 0)
        uint256 gross = value;
        uint256 refund = 0;
        {
            uint256 maxGross = _maxGrossForCap(id, coinIs0, MAX_WALLET, feeBps);
            if (gross > maxGross) {
                refund = gross - maxGross;
                gross = maxGross;
            }
        }
        uint256 fee = gross * feeBps / 10_000;
        uint256 net = gross - fee;
        require(net > 0, "dust buy");

        uint256 tokensOut = _curveBuySwap(key, coinIs0, net);
        require(tokensOut >= minTokensOut, "slippage");
        require(tokensOut <= MAX_WALLET, "cap invariant");

        // settle net (the swap) + fee (real quote tokens into the PoolManager,
        // the fee leg immediately re-minted as the hook's ERC6909 fee claim)
        poolManager.sync(Currency.wrap(QUOTE_TOKEN));
        _safeTransfer(QUOTE_TOKEN, address(poolManager), net + fee);
        poolManager.settle();
        poolManager.mint(address(this), QUOTE_ID, fee);
        _credit(p, fee);

        _recordCapBuy(id, tx.origin, tokensOut); // fresh pool: first buy (creator holds 0 coin at create)
        p.tokensSold += uint128(tokensOut);
        p.raisedWei += uint128(net);

        poolManager.take(coinIs0 ? key.currency0 : key.currency1, creator, tokensOut);
        _emitTrade(p, id, true, gross, net, tokensOut);

        // over-cap surplus: plain ERC20 transfer back (no .call{value}, and a
        // standard ERC20 cannot re-enter — the nonReentrant lock guards the
        // callback-capable exotic case anyway)
        if (refund > 0) _safeTransfer(QUOTE_TOKEN, creator, refund);
    }

    /// @dev the dev buy's inner pool swap (split out of _devBuy for
    ///      legacy-codegen stack room): quote in, oriented by the ordering,
    ///      returns the coin-side fill
    function _curveBuySwap(PoolKey memory key, bool coinIs0, uint256 net) internal returns (uint256 tokensOut) {
        BalanceDelta sd = poolManager.swap(
            key,
            SwapParams({
                zeroForOne: !coinIs0, // buy: quote in
                amountSpecified: -int256(net),
                sqrtPriceLimitX96: coinIs0 ? TickMath.MAX_SQRT_PRICE - 1 : TickMath.MIN_SQRT_PRICE + 1
            }),
            ""
        );
        tokensOut = uint256(uint128(coinIs0 ? sd.amount0() : sd.amount1()));
    }

    function _emitTrade(Pool storage p, PoolId id, bool isBuy, uint256 gross, uint256 net, uint256 tokens) internal {
        (uint160 sqrtNow,,,) = poolManager.getSlot0(id);
        emit Trade(p.coin, tx.origin, isBuy, gross, net, tokens, sqrtNow, p.tokensSold, p.raisedWei);
    }

    // ------------------------------------------------------------------ fees

    /// Split of the TOTAL fee (base 1% + any anti-snipe premium): 70% creator /
    /// 30% platform, uniform. Platform takes floor(fee * 3 / 10); the creator
    /// gets the remainder, so the odd unit rounds to the CREATOR. The premium
    /// is NOT special-cased — it splits 70/30 exactly like the base (so the
    /// platform also shares in the sniper premium).
    function _credit(Pool storage p, uint256 fee) internal {
        uint256 platformShare = fee * 3 / 10; // 30% of the total fee (base + premium)
        platformFees += platformShare;
        creatorFees[p.feeRecipient] += fee - platformShare; // 70%, odd unit to creator
    }

    function claimCreatorFees() external nonReentrant {
        uint256 amount = creatorFees[msg.sender];
        require(amount > 0, "nothing to claim");
        creatorFees[msg.sender] = 0;
        // claiming is the natural "I'm alive" signal: it protects every coin
        // paying this recipient from a CTO. only msg.sender can stamp itself.
        lastClaim[msg.sender] = block.timestamp;
        poolManager.unlock(abi.encode(uint8(2), abi.encode(msg.sender, amount)));
        emit CreatorFeesClaimed(msg.sender, amount);
    }

    /// @notice Pull a refund queued by the ruling-19.B waterfall (branch 2:
    ///         an over-cap buy through a settle-after router while the
    ///         PoolManager's quote float could not cover the instant take).
    ///         Permissionless pull, claimCreatorFees shape: burns the hook's
    ///         ERC6909 claims and pays real quote tokens — by claim time the
    ///         buyer's own settle has long since delivered the backing quote.
    function claimRefund() external nonReentrant {
        uint256 amount = refunds[msg.sender];
        require(amount > 0, "nothing to claim");
        refunds[msg.sender] = 0;
        poolManager.unlock(abi.encode(uint8(2), abi.encode(msg.sender, amount)));
        emit RefundClaimed(msg.sender, amount);
    }

    function withdrawPlatformFees(address to) external onlyOwner nonReentrant {
        uint256 amount = platformFees;
        require(amount > 0, "nothing to withdraw");
        platformFees = 0;
        poolManager.unlock(abi.encode(uint8(2), abi.encode(to, amount)));
        emit PlatformFeesWithdrawn(to, amount);
    }

    /// @notice Realize a graduated pool's accrued 1% LP fees from the locked
    ///         full-range position. Permissionless — anyone pokes (v1 locker
    ///         parity). Quote side split 70% creator / 30% platform into the
    ///         pull ledgers (so an exotic recipient can never brick the
    ///         collect); coin side split the same way and transferred directly
    ///         (PmavCoin transfers cannot revert or reenter).
    function collectPoolFees(address token) external nonReentrant {
        Pool storage p = pools[poolOf[token]];
        require(p.coin == token && token != address(0), "unknown coin");
        require(p.graduated, "not graduated");
        poolManager.unlock(abi.encode(uint8(3), abi.encode(token)));
    }

    function _collectCallback(address token) internal {
        PoolKey memory key = poolKeyOf(token);
        Pool storage p = pools[key.toId()];
        bool coinIs0 = token < QUOTE_TOKEN;

        // a zero-liquidity-delta poke realizes feesAccrued as caller deltas
        (BalanceDelta fees,) = poolManager.modifyLiquidity(
            key,
            ModifyLiquidityParams({
                tickLower: FULL_TICK_LOWER,
                tickUpper: FULL_TICK_UPPER,
                liquidityDelta: 0,
                salt: 0
            }),
            ""
        );
        uint256 quoteFees = uint256(uint128(coinIs0 ? fees.amount1() : fees.amount0()));
        uint256 coinFees = uint256(uint128(coinIs0 ? fees.amount0() : fees.amount1()));

        // graduated-pool LP fees split 70% creator / 30% platform (= 3/10
        // platform), odd unit to the CREATOR — identical rounding discipline
        // to _credit (there is no anti-snipe premium here).
        uint256 platQuote = quoteFees * 3 / 10;
        if (quoteFees > 0) {
            poolManager.mint(address(this), QUOTE_ID, quoteFees);
            creatorFees[p.feeRecipient] += quoteFees - platQuote;
            platformFees += platQuote;
        }
        uint256 platCoin = coinFees * 3 / 10;
        if (coinFees > 0) {
            poolManager.take(coinIs0 ? key.currency0 : key.currency1, address(this), coinFees);
            if (coinFees - platCoin > 0) require(PmavCoin(token).transfer(p.feeRecipient, coinFees - platCoin), "t1");
            if (platCoin > 0) require(PmavCoin(token).transfer(owner, platCoin), "t2");
        }
        emit PoolFeesCollected(token, p.feeRecipient, quoteFees - platQuote, coinFees - platCoin, platQuote, platCoin);
    }

    // ------------------------------------------------------------ governance
    // line-for-line ports from audited v1 Pmav.sol — moved, not rewritten

    function setFeeRecipient(address token, address newRecipient) external {
        Pool storage p = pools[poolOf[token]];
        require(p.coin == token && token != address(0), "unknown coin");
        require(msg.sender == p.feeRecipient, "not fee recipient");
        require(newRecipient != address(0), "zero addr");
        require(!feeRecipientLocked[token], "fee recipient locked");
        lastActivity[token] = block.timestamp;
        emit FeeRecipientChanged(token, p.feeRecipient, newRecipient);
        p.feeRecipient = newRecipient;
    }

    /// @notice Permanently freeze the CURRENT fee recipient of a coin. One-way.
    function lockFeeRecipient(address token) external {
        Pool storage p = pools[poolOf[token]];
        require(p.coin == token && token != address(0), "unknown coin");
        require(msg.sender == p.feeRecipient, "not fee recipient");
        require(!feeRecipientLocked[token], "already locked");
        feeRecipientLocked[token] = true;
        lastActivity[token] = block.timestamp;
        emit FeeRecipientLockedEvent(token, p.feeRecipient);
    }

    /// @notice Atomically REASSIGN the fee recipient and permanently lock it.
    function lockFeeRecipientTo(address token, address newRecipient) external {
        Pool storage p = pools[poolOf[token]];
        require(p.coin == token && token != address(0), "unknown coin");
        require(msg.sender == p.feeRecipient, "not fee recipient");
        require(newRecipient != address(0), "zero addr");
        require(!feeRecipientLocked[token], "already locked");
        lastActivity[token] = block.timestamp;
        emit FeeRecipientChanged(token, p.feeRecipient, newRecipient);
        p.feeRecipient = newRecipient;
        feeRecipientLocked[token] = true;
        emit FeeRecipientLockedEvent(token, newRecipient);
    }

    /// @notice CTO (community takeover): redirect an abandoned coin's FUTURE
    ///         creator rewards to a community lead. The one rail no review can
    ///         waive: the current DEV must be provably inactive — no fee claim
    ///         and no per-coin steward action for CTO_INACTIVITY. Trading does
    ///         NOT protect a dev. Locked coins are immune by design.
    function proposeCtoRedirect(address token, address newRecipient) external onlyOwner {
        Pool storage p = pools[poolOf[token]];
        require(p.coin == token && token != address(0), "unknown coin");
        require(newRecipient != address(0), "zero addr");
        require(newRecipient != owner, "owner cannot self target");
        require(!feeRecipientLocked[token], "fee recipient locked");
        uint256 devActive = lastActivity[token];
        uint256 claimed = lastClaim[p.feeRecipient];
        if (claimed > devActive) devActive = claimed;
        require(block.timestamp >= devActive + CTO_INACTIVITY, "dev still active");
        ctoProposals[token] = CtoProposal({newRecipient: newRecipient, proposedAt: uint64(block.timestamp)});
        emit CtoRedirectProposed(token, p.feeRecipient, newRecipient, block.timestamp + CTO_VETO_DELAY);
    }

    /// @notice CTO step 2 of 2: EXECUTE after the 12h veto window. ANY dev
    ///         liveness signal newer than the proposal (one fee claim, one
    ///         steward action, a lock) cancels it — no silent seizure path.
    function executeCtoRedirect(address token) external onlyOwner {
        Pool storage p = pools[poolOf[token]];
        require(p.coin == token && token != address(0), "unknown coin");
        CtoProposal memory prop = ctoProposals[token];
        require(prop.newRecipient != address(0), "no proposal");
        require(block.timestamp >= uint256(prop.proposedAt) + CTO_VETO_DELAY, "veto window open");
        require(!feeRecipientLocked[token], "fee recipient locked");
        require(lastActivity[token] < prop.proposedAt, "vetoed: dev acted");
        require(lastClaim[p.feeRecipient] < prop.proposedAt, "vetoed: dev claimed");
        delete ctoProposals[token];
        lastActivity[token] = block.timestamp; // fresh window for the new lead
        emit FeeRecipientChanged(token, p.feeRecipient, prop.newRecipient);
        emit CtoRedirect(token, p.feeRecipient, prop.newRecipient);
        p.feeRecipient = prop.newRecipient;
    }

    function setOwner(address newOwner) external onlyOwner {
        require(newOwner != address(0), "zero addr");
        emit OwnerChanged(owner, newOwner);
        owner = newOwner;
    }

    /// @notice Tune the anti-snipe FACTORY DEFAULTS, bounded by the hard caps.
    ///         TIGHTENED vs v1: affects FUTURE launches only — every live
    ///         pool's premium/window were frozen into its struct at create().
    function setAntiSnipeDefaults(uint256 peakBps, uint256 window) external onlyOwner {
        require(peakBps <= ANTI_SNIPE_MAX_BPS, "peak too high");
        require(window <= ANTI_SNIPE_MAX_WINDOW, "window too long");
        antiSnipePeakBps = peakBps;
        antiSnipeWindow = window;
        emit AntiSnipeChanged(peakBps, window);
    }

    // ---------------------------------------------------------------- quotes

    /// @notice Mirror of the buy path for UI parity with v1. APPROXIMATE by a
    ///         few parts in 1e15: the pool consumes input in tick-word chunks
    ///         with per-chunk round-up, which a closed-form view cannot
    ///         replicate — use V4Quoter (which simulates the real swap,
    ///         hook deltas included) for an exact minTokensOut. `refund` is
    ///         the curve-completion surplus only; the per-wallet cap clamp
    ///         depends on the buyer and is not modeled here (v1 parity).
    ///         `quoteGross` and all returns are RAW QUOTE UNITS.
    function quoteBuy(address token, uint256 quoteGross)
        external
        view
        returns (uint256 tokensOut, uint256 fee, uint256 refund)
    {
        PoolId id = poolOf[token];
        Pool storage p = pools[id];
        require(p.coin == token && token != address(0) && !p.graduated, "not on curve");
        bool coinIs0 = token < QUOTE_TOKEN;
        uint128 lCurve = coinIs0 ? L_CURVE_C0 : L_CURVE_Q0;
        uint160 sqrtGrad = coinIs0 ? SQRT_GRAD_C0 : SQRT_GRAD_Q0;
        uint256 feeBps = FEE_BPS + _antiSnipeBps(p);
        uint160 sqrtP = _effSqrtP(id, coinIs0);
        // v2.2 (audit L1): fee base bounded by the gross the curve can
        // actually consume to graduation — mirrors the beforeSwap clamp, so
        // an oversized completing buy is quoted AND charged on USED gross.
        {
            uint256 clampNet = coinIs0
                ? SqrtPriceMath.getAmount1Delta(sqrtP, sqrtGrad, lCurve, true)
                : SqrtPriceMath.getAmount0Delta(sqrtGrad, sqrtP, lCurve, true);
            uint256 maxFeeGross = FullMath.mulDivRoundingUp(clampNet, 10_000, 10_000 - feeBps);
            fee = (quoteGross > maxFeeGross ? maxFeeGross : quoteGross) * feeBps / 10_000;
        }
        uint256 net = quoteGross - fee;
        require(net > 0, "dust buy");
        // quote in: currency0 in (price down) for Q0 pools, currency1 in
        // (price up) for C0 pools
        uint160 sqrtNext = SqrtPriceMath.getNextSqrtPriceFromInput(sqrtP, lCurve, net, !coinIs0);
        if (coinIs0 ? sqrtNext > sqrtGrad : sqrtNext < sqrtGrad) {
            // completing buy: only the quote that finishes the curve is consumed
            uint256 maxNet = coinIs0
                ? SqrtPriceMath.getAmount1Delta(sqrtP, sqrtGrad, lCurve, true)
                : SqrtPriceMath.getAmount0Delta(sqrtGrad, sqrtP, lCurve, true);
            refund = quoteGross - fee - maxNet;
            tokensOut = coinIs0
                ? SqrtPriceMath.getAmount0Delta(sqrtP, sqrtGrad, lCurve, false)
                : SqrtPriceMath.getAmount1Delta(sqrtGrad, sqrtP, lCurve, false);
        } else {
            tokensOut = coinIs0
                ? SqrtPriceMath.getAmount0Delta(sqrtP, sqrtNext, lCurve, false)
                : SqrtPriceMath.getAmount1Delta(sqrtNext, sqrtP, lCurve, false);
        }
    }

    /// @notice returns are RAW QUOTE UNITS
    function quoteSell(address token, uint256 tokenAmount) external view returns (uint256 quoteNet, uint256 fee) {
        PoolId id = poolOf[token];
        Pool storage p = pools[id];
        require(p.coin == token && token != address(0) && !p.graduated, "not on curve");
        require(tokenAmount > 0, "zero amount");
        bool coinIs0 = token < QUOTE_TOKEN;
        uint128 lCurve = coinIs0 ? L_CURVE_C0 : L_CURVE_Q0;
        uint160 sqrtStart = coinIs0 ? SQRT_START_C0 : SQRT_START_Q0;
        uint160 sqrtP = _effSqrtP(id, coinIs0);
        // token in: currency1 in (price up) for Q0 pools, currency0 in
        // (price down) for C0 pools
        uint160 sqrtNext = SqrtPriceMath.getNextSqrtPriceFromInput(sqrtP, lCurve, tokenAmount, coinIs0);
        // beyond the curve start the pool stops paying
        if (coinIs0 ? sqrtNext < sqrtStart : sqrtNext > sqrtStart) sqrtNext = sqrtStart;
        uint256 quoteGross = coinIs0
            ? SqrtPriceMath.getAmount1Delta(sqrtNext, sqrtP, lCurve, false)
            : SqrtPriceMath.getAmount0Delta(sqrtP, sqrtNext, lCurve, false);
        fee = quoteGross * FEE_BPS / 10_000;
        quoteNet = quoteGross - fee;
    }

    /// @notice spot price in RAW QUOTE UNITS per whole (1e18) token, and
    ///         market cap in raw quote units. v1-shaped for the site; `pool`
    ///         is the PoolManager once graduated (v1 semantics: nonzero pool
    ///         == trade on the DEX). Return names keep the v2 ABI shape; the
    ///         values are quote units at QUOTE_DECIMALS.
    function state(address token)
        external
        view
        returns (
            uint256 priceWei,
            uint256 mcapWei,
            uint256 ethReserve,
            uint256 tokensSold,
            uint256 progressBps,
            bool graduated,
            address pool
        )
    {
        PoolId id = poolOf[token];
        Pool storage p = pools[id];
        require(p.coin == token && token != address(0), "unknown coin");
        (uint160 sqrtP,,,) = poolManager.getSlot0(id);
        // quote per whole token. Q0 pools: price(coin/quote) = (sqrtP/2^96)^2,
        // so quote per 1e18 token-wei = 1e18 * 2^192 / sqrtP^2; C0 pools:
        // price(quote/coin) = (sqrtP/2^96)^2, so quote per 1e18 token-wei =
        // 1e18 * sqrtP^2 / 2^192. Both computed as two chained mulDivs so no
        // sqrtP magnitude can overflow the naive sqrtP*sqrtP product.
        if (token < QUOTE_TOKEN) {
            priceWei = FullMath.mulDiv(FullMath.mulDiv(sqrtP, 1e18, FixedPoint96.Q96), sqrtP, FixedPoint96.Q96);
        } else {
            priceWei = FullMath.mulDiv(FullMath.mulDiv(FixedPoint96.Q96, 1e18, sqrtP), FixedPoint96.Q96, sqrtP);
        }
        mcapWei = priceWei * 1_000_000_000;
        ethReserve = p.raisedWei;
        tokensSold = p.tokensSold;
        progressBps = uint256(p.tokensSold) * 10_000 / CURVE_SUPPLY;
        graduated = p.graduated;
        pool = p.graduated ? address(poolManager) : address(0);
    }

    function feeRecipientOf(address token) external view returns (address) {
        return pools[poolOf[token]].feeRecipient;
    }

    // ------------------------------------------------------------- internals

    function _abs(int256 x) internal pure returns (uint256) {
        return x < 0 ? uint256(-x) : uint256(x);
    }

    /// @notice v2.2: whether this coin carries the decaying anti snipe
    ///         premium (the creator's public choice at create()).
    function antiSnipeEnabled(address token) external view returns (bool) {
        Pool storage p = pools[poolOf[token]];
        require(p.coin == token && token != address(0), "unknown coin");
        return p.snipePeakBps > 0 && p.snipeWindow > 0;
    }

    /// v2.2 (audit L2): opt in refund beneficiary carried in hookData.
    /// EXACTLY 32 bytes decodes as an address by raw truncation (never
    /// abi.decode: malformed hookData must IGNORE, not revert a swap).
    function _refundBeneficiary(bytes calldata hookData) internal pure returns (address) {
        if (hookData.length != 32) return address(0);
        return address(uint160(uint256(bytes32(hookData[0:32]))));
    }

    /// current curve price for quote/clamp math, bounded to the curve range:
    /// a sell can park the spot price BEYOND the curve start through empty
    /// liquidity (above it on Q0 pools, below it on C0 pools); buys from
    /// there walk back for free, so all token/quote math must start at the
    /// start edge, not the empty-space price
    function _effSqrtP(PoolId id, bool coinIs0) internal view returns (uint160 sqrtP) {
        (sqrtP,,,) = poolManager.getSlot0(id);
        if (coinIs0) {
            if (sqrtP < SQRT_START_C0) sqrtP = SQRT_START_C0;
        } else {
            if (sqrtP > SQRT_START_Q0) sqrtP = SQRT_START_Q0;
        }
    }

    /// max gross quote (fee included) a first-hour buy may spend so that the
    /// delivered tokens never exceed capLeft. Rounding discipline (test
    /// enforced): the sqrt target rounds TOWARD the current price (fewer
    /// tokens) and the quote amount rounds DOWN, so core's forward math can
    /// never deliver > capLeft.
    function _maxGrossForCap(PoolId id, bool coinIs0, uint256 capLeft, uint256 feeBps)
        internal
        view
        returns (uint256 maxGross)
    {
        uint160 sqrtP = _effSqrtP(id, coinIs0);
        uint160 sqrtGrad = coinIs0 ? SQRT_GRAD_C0 : SQRT_GRAD_Q0;
        uint128 lCurve = coinIs0 ? L_CURVE_C0 : L_CURVE_Q0;
        uint256 maxNet;
        bool gradBound;
        if (coinIs0) {
            // tokens are currency0: capLeft = L*Q96*(1/sqrtP - 1/target), so
            // target = L*Q96*sqrtP / (L*Q96 - capLeft*sqrtP). The quotient
            // FLOORS => target rounds DOWN (toward sqrtP) => tokens at target
            // <= capLeft.
            if (sqrtP >= sqrtGrad) return 0;
            uint256 lQ96 = uint256(lCurve) << 96;
            uint256 sold = capLeft * sqrtP; // < 2^244 across the whole envelope
            gradBound = sold >= lQ96;
            uint160 sqrtTarget = sqrtGrad;
            if (!gradBound) {
                uint256 t = FullMath.mulDiv(lQ96, sqrtP, lQ96 - sold);
                if (t >= sqrtGrad) gradBound = true;
                else sqrtTarget = uint160(t);
            }
            // when the graduation edge binds the fill is bounded by the curve
            // itself, so rounding UP is safe (it just guarantees graduation)
            maxNet = SqrtPriceMath.getAmount1Delta(sqrtP, sqrtTarget, lCurve, gradBound);
        } else {
            // tokens are currency1 (the v2 shape verbatim): sqrt price after
            // the position hands out capLeft tokens (quotient floored =>
            // target rounds UP => tokens at target <= capLeft)
            if (sqrtP <= sqrtGrad) return 0;
            uint256 quotient = FullMath.mulDiv(capLeft, FixedPoint96.Q96, lCurve);
            gradBound = quotient >= sqrtP || sqrtP - quotient <= sqrtGrad;
            uint160 sqrtTarget = gradBound ? sqrtGrad : uint160(sqrtP - quotient);
            maxNet = SqrtPriceMath.getAmount0Delta(sqrtTarget, sqrtP, lCurve, gradBound);
        }
        if (maxNet == 0) return 0;
        if (gradBound) {
            // v2.2 FIX (found by the port suite, red test
            // test_completing_buy_graduates_in_one_tx): when the graduation
            // edge binds, tokens delivered are bounded by the CURVE (remaining
            // curve <= capLeft by the gradBound condition), so round the gross
            // UP and SKIP the walk down. The floor + walk down combination
            // could leave a completing buy a sub wei hair above the edge
            // (strict trigger misses; the coin failed to graduate in the same
            // tx). A wei generous net just walks price past the edge into
            // EMPTY liquidity, where input is never consumed and flows back
            // through the router (the v2.1 post hour overshoot semantics the
            // _graduate boundary repair already handles) -- it can never over
            // deliver tokens and it guarantees same tx graduation.
            return FullMath.mulDivRoundingUp(maxNet, 10_000, 10_000 - feeBps);
        }
        maxGross = FullMath.mulDiv(maxNet, 10_000, 10_000 - feeBps);
        // exactness guard: the swap path will charge fee = floor(g*f/1e4) and
        // hand the pool g - fee; walk g down (<=2 iterations) until that net
        // cannot exceed maxNet
        while (maxGross > 0 && maxGross - (maxGross * feeBps / 10_000) > maxNet) {
            maxGross--;
        }
    }

    // ---- v2.2 HARD 2% wallet cap (founder ruling 2026-07-20) --------------
    // No wallet may hold more than 2% as a result of buying, and no tx.origin
    // may ACQUIRE more than 2% through this pool in a single transaction —
    // bundlers, MEV bots, snipers, any custom contract, all bound. Over-cap
    // buys fill to exactly 2% and refund the surplus (fee only on the 2%).
    //
    // The cap binds on `held = startBalance + boughtTx`:
    //   * startBalance — the COIN balance (18dp, either orientation) tx.origin
    //     held at the START of this transaction, snapshotted at the FIRST cap
    //     touch (while boughtTx is still 0) and then FIXED. It captures any
    //     pre-existing holding a buy must not push past 2%.
    //   * boughtTx — a MONOTONIC per (pool, origin) TRANSIENT counter of
    //     everything tx.origin has bought through this pool this transaction,
    //     NEVER reset.
    // The two sets are DISJOINT — startBalance excludes this-tx buys (not yet
    // settled into balanceOf), boughtTx IS this-tx buys — so their SUM is the
    // exact acquisition-inclusive holding: no double count of a settled buy, no
    // under count of a warm wallet that bundles buys in one unlock. A FIXED
    // start snapshot plus a monotonic never-reset counter means no balance move
    // mid-tx (a 1-wei delivery to origin, a full delivery, a transfer to a
    // stash) can re-open the cap. Transient clears at tx end, so a later tx
    // re-snapshots startBalance from the live balance (a sell/transfer away in a
    // separate tx frees capacity).
    //
    // Documented residual (unpreventable without token transfer restrictions,
    // which stay rejected for scanner cleanliness): spreading buys across many
    // transactions/wallets and funneling to one address via raw ERC20 transfers
    // (incl. a transfer INTO origin AFTER the start snapshot in the same tx).
    // The cap is a hard per-transaction acquisition + start-of-tx holding bound,
    // not a cross-transaction identity bound.

    function _capBoughtSlot(PoolId id, address origin) private pure returns (bytes32 slot) {
        slot = keccak256(abi.encode("pmav.cap.bought", id, origin));
    }

    function _capStartSlot(PoolId id, address origin) private pure returns (bytes32 slot) {
        slot = keccak256(abi.encode("pmav.cap.start", id, origin));
    }

    /// tokens tx.origin effectively holds against the cap = its start-of-tx COIN
    /// balance (snapshotted at the first cap touch, when boughtTx is still 0)
    /// PLUS everything bought through this pool this tx (a monotonic transient
    /// counter). The SUM of these two disjoint sets: never double counts a
    /// settled buy, never under counts a warm wallet bundling buys, and no
    /// balance move can re-open the cap (both terms fixed/monotonic).
    function _capHeld(PoolId id, address coin, address origin) internal returns (uint256 held) {
        bytes32 bSlot = _capBoughtSlot(id, origin);
        bytes32 sSlot = _capStartSlot(id, origin);
        uint256 bought;
        assembly ("memory-safe") {
            bought := tload(bSlot)
        }
        uint256 start;
        if (bought == 0) {
            // first cap touch this tx: snapshot the pre-buy balance and fix it
            start = PmavCoin(coin).balanceOf(origin);
            assembly ("memory-safe") {
                tstore(sSlot, start)
            }
        } else {
            assembly ("memory-safe") {
                start := tload(sSlot)
            }
        }
        held = start + bought;
    }

    function _recordCapBuy(PoolId id, address origin, uint256 amount) internal {
        bytes32 bSlot = _capBoughtSlot(id, origin);
        uint256 bought;
        assembly ("memory-safe") {
            bought := tload(bSlot)
        }
        assembly ("memory-safe") {
            tstore(bSlot, add(bought, amount))
        }
    }

    /// Anti-snipe premium (bps) at the current time for a pool: linear decay
    /// from the pool's FROZEN peak at launch to 0 at its frozen window edge.
    function _antiSnipeBps(Pool storage p) internal view returns (uint256) {
        uint256 window = p.snipeWindow;
        if (window == 0) return 0;
        uint256 elapsed = block.timestamp - p.launchTime;
        if (elapsed >= window) return 0;
        return uint256(p.snipePeakBps) * (window - elapsed) / window;
    }

    /// standard Babylonian integer sqrt (floor). Only used once, in the
    /// constructor's curve derivation.
    function _sqrt(uint256 x) internal pure returns (uint256 z) {
        if (x == 0) return 0;
        uint256 y = x;
        z = 1;
        // seed z near sqrt(x) by halving the bit length
        if (y >= 1 << 128) {
            y >>= 128;
            z <<= 64;
        }
        if (y >= 1 << 64) {
            y >>= 64;
            z <<= 32;
        }
        if (y >= 1 << 32) {
            y >>= 32;
            z <<= 16;
        }
        if (y >= 1 << 16) {
            y >>= 16;
            z <<= 8;
        }
        if (y >= 1 << 8) {
            y >>= 8;
            z <<= 4;
        }
        if (y >= 1 << 4) {
            y >>= 4;
            z <<= 2;
        }
        if (y >= 1 << 2) {
            z <<= 1;
        }
        z = (z + x / z) >> 1;
        z = (z + x / z) >> 1;
        z = (z + x / z) >> 1;
        z = (z + x / z) >> 1;
        z = (z + x / z) >> 1;
        z = (z + x / z) >> 1;
        z = (z + x / z) >> 1;
        // floor correction (Babylonian can land one above)
        if (z > x / z) z--;
    }

    /// round a tick to the NEAREST TICK_SPACING multiple (ties away from
    /// zero) — the alignment discipline that produced v2's [176620, 204350]
    function _alignNearest(int24 tick) internal pure returns (int24) {
        int24 r = tick % TICK_SPACING;
        int24 base = tick - r;
        if (r >= 5) base += TICK_SPACING;
        else if (r <= -5) base -= TICK_SPACING;
        return base;
    }

    /// minimal safe-ERC20 (USDT-style missing-return tolerated, reverts on
    /// explicit false or failed call) — the hook's only direct quote moves
    /// are the dev-buy pull, its PM settle leg, and the dev-buy surplus refund
    function _safeTransfer(address token, address to, uint256 amount) internal {
        (bool ok, bytes memory ret) = token.call(abi.encodeWithSelector(0xa9059cbb, to, amount));
        require(ok && (ret.length == 0 || abi.decode(ret, (bool))), "quote transfer failed");
    }

    function _safeTransferFrom(address token, address from, address to, uint256 amount) internal {
        (bool ok, bytes memory ret) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, amount));
        require(ok && (ret.length == 0 || abi.decode(ret, (bool))), "quote transferFrom failed");
    }

    // NOTE: no receive() on this variant — the quote is an ERC20, no code
    // path handles native ETH, and any direct ETH send reverts by default.
}
Chain explorer4444msChain node97ms