PmavHook

0x2d0e12fec42cea31022c37e9d714db6d2d49a8cc

Verification
Verified
0.8.28+commit.7893614a
Type
Contract
35,043 bytes
ABI entries
85
43 read · 23 write
License
none

Contract information

Address
0x2d0e12fec42cea31022c37e9d714db6d2d49a8cc
Chain
Robinhood Chain (4663)
Compiler
0.8.28+commit.7893614a
Optimization
Enabled
Creation tx
0x7c40b2af70…ed18e80edb

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (43)

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
LP_SUPPLY()uint256
L_CURVE()uint128
MAX_WALLET()uint256
PAIR_COOLDOWN()uint256
SQRT_GRAD()uint160
SQRT_START()uint160
TICK_LOWER()int24
TICK_SPACING()int24
TICK_UPPER()int24
TOTAL_SUPPLY()uint256
allCoins(uint256)address
antiSnipeEnabled(address)bool
antiSnipePeakBps()uint256
antiSnipeWindow()uint256
coinCount()uint256
creatorFees(address)uint256
ctoProposals(address)address, uint64
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"
      }
    ],
    "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": "LP_SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "L_CURVE",
    "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": "SQRT_GRAD",
    "outputs": [
      {
        "internalType": "uint160",
        "name": "",
        "type": "uint160"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "SQRT_START",
    "outputs": [
      {
        "internalType": "uint160",
        "name": "",
        "type": "uint160"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TICK_LOWER",
    "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",
    "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": "minTokensOut",
        "type": "uint256"
      },
      {
        "internalType": "bool",
        "name": "antiSnipe",
        "type": "bool"
      }
    ],
    "name": "create",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "stateMutability": "payable",
    "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": "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": "ethGross",
        "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": "ethNet",
        "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"
  },
  {
    "stateMutability": "payable",
    "type": "receive"
  }
]

Source code

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

/// ---------------------------------------------------------------------------
/// pmav.fun V2 — the curve IS the pool (Uniswap V4 hook architecture)
///
/// 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 HookMiner.find() — see script/DeployV2.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.
///
/// v2.2 GENERATION (docs/V22_HOOK_GENERATION_SPEC.md, decisions locked
/// 2026-07-20). Derived from the audited v2.1 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 = v2.1 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()
/// ---------------------------------------------------------------------------

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 {PmavCoin} from "../PmavCoin.sol";

/// @title PmavHook (v2.2) — the pmav.fun launchpad: factory + Uniswap V4 hook + fee bank
/// @notice Launch a coin for just gas. Every coin: 1B fixed supply, no taxes,
///         no owner. Coins trade from transaction one as a NATIVE Uniswap V4
///         pool (ETH paired) on the canonical PoolManager; the bonding curve
///         is a single concentrated position [176620, 204350] owned by this
///         hook that reproduces the V1 virtual-reserve curve within ~4 bps.
///         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 native 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 PmavHook 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 ETH 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 — ticker squatting ceases to be
    ///         a mechanic). 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 TICK_LOWER = 176_620; // graduation tick
    int24 public constant TICK_UPPER = 204_350; // curve start, pool init tick
    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 (fork finding F1): graduation step measured on COLD
    /// chain-4663 mainnet state at fork block 10,090,000 = ~3,823,356 gas
    /// (graduating buy 4,095,225 minus a matched non-graduating 4.2-ETH buy
    /// 271,869 — ForkGraduation.t.sol::test_graduation_step_gas_vs_floor).
    /// Floor = measured cost + ~10% headroom; the fork test asserts the
    /// floor stays above the measured step.
    uint256 public constant GRADUATION_GAS = 4_200_000;

    address internal constant DEAD = 0x000000000000000000000000000000000000dEaD;
    uint256 internal constant ETH_ID = 0; // CurrencyLibrary.ADDRESS_ZERO.toId()

    /// derived once at deploy from TickMath — bit-exact with the pool's math
    uint160 public immutable SQRT_START; // sqrtPrice at TICK_UPPER
    uint160 public immutable SQRT_GRAD; // sqrtPrice at TICK_LOWER
    uint160 internal immutable SQRT_FULL_LOWER;
    uint160 internal immutable SQRT_FULL_UPPER;
    uint128 public immutable L_CURVE; // curve position liquidity

    // ---------------------------------------------------------------- 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; // informational + invariant cross-check
    }

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

    /// v2.2 BALANCE BASED 2% cap (founder L3, 2026-07-20): NO persistent
    /// ledger. The cap is a live snapshot of the buyer's holding — no more
    /// than MAX_WALLET may sit in tx.origin's wallet after a buy, and
    /// selling OR transferring tokens away frees the allowance immediately
    /// (balanceOf falls). The only per-tx state is transient (see the cap
    /// helpers below), so nothing here persists across transactions.

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

    /// queued over-cap refunds (ETH, 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 native float
    /// cannot cover the instant take() — expected rarely, and only while the
    /// platform is young (live float on 4663 measured at ~1234.69 ETH).
    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 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 preserved — the indexer consumes these unchanged (§14)

    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) BaseHook(_manager) {
        require(_owner != address(0), "zero addr");
        owner = _owner;

        SQRT_START = TickMath.getSqrtPriceAtTick(TICK_UPPER);
        SQRT_GRAD = TickMath.getSqrtPriceAtTick(TICK_LOWER);
        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
        // top of the range (pool initializes at TICK_UPPER: tokens only)
        L_CURVE = LiquidityAmounts.getLiquidityForAmount1(SQRT_GRAD, SQRT_START, CURVE_SUPPLY);
    }

    // ---------------------------------------------------------- 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, ETH-specified fee
            afterSwap: true, // cap ledger, ETH-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");

        // decaying anti-snipe premium: buys only, frozen params, base 1% always
        uint256 feeBps = FEE_BPS + (params.zeroForOne ? _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 (params.zeroForOne) {
            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 ETH the cap allows
                uint256 maxGross = _maxGrossForCap(id, 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) ETH-denominated fee, taken on the SPECIFIED side when ETH is
        // specified (buy exactIn / sell exactOut). Otherwise afterSwap takes it.
        if (_specifiedIsEth(params)) {
            uint256 ethGross = _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 (maxNet
            // rounded UP, grossed up rounding UP: the fee never undercharges,
            // and an oversized completing buy pays fee on the USED gross, not
            // the stated one). Sell exactOut (also ETH specified) is untouched
            // (audit L4 stays disclosed behavior).
            if (params.zeroForOne) {
                uint256 maxNet = SqrtPriceMath.getAmount0Delta(SQRT_GRAD, _effSqrtP(id), L_CURVE, true);
                uint256 maxFeeGross = FullMath.mulDivRoundingUp(maxNet, 10_000, 10_000 - feeBps);
                if (ethGross > maxFeeGross) ethGross = maxFeeGross;
            }
            uint256 fee = ethGross * feeBps / 10_000;
            if (fee > 0) {
                poolManager.mint(address(this), ETH_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 (a reverting recipient can never fail the buy;
                // AA bundlers can no longer silently pocket the surplus).
                // Any other hookData shape is IGNORED (fuzz safe: garbage
                // bytes must never revert a swap). Empty = v2.1 waterfall.
                address beneficiary = _refundBeneficiary(hookData);
                if (beneficiary != address(0)) {
                    poolManager.mint(address(this), ETH_ID, refund);
                    refunds[beneficiary] += refund;
                    emit RefundQueued(p.coin, beneficiary, refund);
                } else if (address(poolManager).balance >= refund) {
                    // 1: instant auto-refund, real ETH, straight back to the
                    //    signer (the common path — live PM float dwarfs any
                    //    single-tx refund)
                    poolManager.take(CurrencyLibrary.ADDRESS_ZERO, 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), ETH_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];
        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), sold back into
            // the pool by the hook inside the same unlock (noSelfCall: no
            // recursive callbacks), and the proceeds refunded through the
            // curve's exact waterfall. The buyer lands on exactly MAX_WALLET
            // and recovers the tail's value minus the pool's LP fee on the
            // unwind. Buy exactOut over cap still reverts (a return delta
            // cannot claw back the SPECIFIED currency); a buyer with zero
            // remaining capacity reverts (nothing is fillable); sells
            // decrement the ledger saturating and never revert. The v1-named
            // Trade event ships on EVERY swap forever (indexer §14) with the
            // DELIVERED token amount.
            uint256 e = _abs(int256(delta.amount0()));
            uint256 t = _abs(int256(delta.amount1()));
            int128 gradHookDelta = 0;
            if (params.zeroForOne) {
                uint256 held = _capHeld(id, p.coin, tx.origin);
                require(held < MAX_WALLET, "wallet cap: 2% max");
                if (params.amountSpecified < 0 && held + t > MAX_WALLET) {
                    uint256 excess = held + t - MAX_WALLET;
                    t -= excess;
                    _recordCapBuy(id, tx.origin, MAX_WALLET - held); // only the kept fill
                    gradHookDelta = int128(uint128(excess));
                    _unwindGradExcess(key, p, excess, hookData);
                } else {
                    require(held + t <= MAX_WALLET, "wallet cap: 2% max");
                    _recordCapBuy(id, tx.origin, t);
                }
            }
            // sells: nothing to record — balanceOf falls, and the transient
            // buy counter resets on the buyer's next buy (balance moved)
            (uint160 sq,,,) = poolManager.getSlot0(id);
            emit Trade(p.coin, tx.origin, params.zeroForOne, e, e, t, sq, p.tokensSold, p.raisedWei);
            return (BaseHook.afterSwap.selector, gradHookDelta);
        }

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

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


    /// v2.2 (founder 2026-07-20): sell the over cap tail of a graduated
    /// exactIn buy back into the pool and refund the proceeds. The charged
    /// excess arrives as the hook's positive token delta from the afterSwap
    /// return; the sell back consumes it (hook nets flat on tokens) and the
    /// ETH proceeds go out through the exact curve refund waterfall
    /// (beneficiary hookData honored, pull only; else instant push, else
    /// pull ledger). Dust tails that price to zero ETH refund nothing.
    function _unwindGradExcess(PoolKey calldata key, Pool storage p, uint256 excess, bytes calldata hookData)
        internal
    {
        BalanceDelta sd = poolManager.swap(
            key,
            SwapParams({
                zeroForOne: false,
                amountSpecified: -int256(excess),
                sqrtPriceLimitX96: TickMath.MAX_SQRT_PRICE - 1
            }),
            ""
        );
        uint256 ethBack = uint256(uint128(sd.amount0()));
        if (ethBack == 0) return;
        address beneficiary = _refundBeneficiary(hookData);
        if (beneficiary != address(0)) {
            poolManager.mint(address(this), ETH_ID, ethBack);
            refunds[beneficiary] += ethBack;
            emit RefundQueued(p.coin, beneficiary, ethBack);
        } else if (address(poolManager).balance >= ethBack) {
            poolManager.take(CurrencyLibrary.ADDRESS_ZERO, tx.origin, ethBack);
        } else {
            poolManager.mint(address(this), ETH_ID, ethBack);
            refunds[tx.origin] += ethBack;
            emit RefundQueued(p.coin, tx.origin, ethBack);
        }
    }

    /// fee (ETH-unspecified quadrants), first-hour ledger, bookkeeping and the
    /// Trade event for a curve-phase swap
    function _settleCurveSwap(Pool storage p, PoolId id, 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 ethPool = _abs(int256(delta.amount0()));
        uint256 tokenAmount = _abs(int256(delta.amount1()));

        uint256 fee;
        if (_specifiedIsEth(params)) {
            fee = _pendingFee; // already credited in beforeSwap
        } else {
            // (b') ETH fee when ETH is the UNSPECIFIED currency
            // (buy exactOut / sell exactIn)
            uint256 feeBps = FEE_BPS + (params.zeroForOne ? _antiSnipeBps(p) : 0);
            fee = ethPool * feeBps / 10_000;
            if (fee > 0) {
                poolManager.mint(address(this), ETH_ID, fee);
                _credit(p, fee);
                hookDelta = int128(uint128(fee)); // taken from swapper's ETH side
            }
        }
        // (c) v2.2 balance-based cap: buys record the ACTUAL fill into the
        // transient per-unlock counter (clamp already happened in beforeSwap;
        // the require is belt-and-braces against the live holding). Sells
        // record nothing — a sell must NEVER revert and balanceOf falls on
        // its own.
        if (params.zeroForOne) {
            _recordCapBuy(id, tx.origin, tokenAmount);
            require(_capHeld(id, p.coin, tx.origin) <= MAX_WALLET, "cap invariant");
        }
        _swapLock = false;

        // bookkeeping + parity event (indexer contract, §14). For buys the
        // pool ETH is the net (fee charged on top, v1 exact); for sells it is
        // the gross (fee taken out of it).
        if (params.zeroForOne) {
            p.tokensSold += uint128(tokenAmount);
            p.raisedWei += uint128(ethPool);
        } else {
            // SATURATING (correctness audit F-3, defense-in-depth): these are
            // informational counters only — the graduation trigger is
            // price-based (sqrtPrice <= SQRT_GRAD read from pool state), never
            // counter-based, so flooring at 0 changes no economics. Underflow
            // is already unreachable by conservation (a sell can never return
            // more tokens/ETH 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(ethPool);
            p.tokensSold = p.tokensSold > t ? p.tokensSold - t : 0;
            p.raisedWei = p.raisedWei > e ? p.raisedWei - e : 0;
        }
        _emitTrade(
            p,
            id,
            params.zeroForOne,
            params.zeroForOne ? ethPool + fee : ethPool,
            // saturate: a sell-exactOut demanding more ETH than the curve
            // holds partially fills; the credited fee can exceed the fill
            params.zeroForOne ? ethPool : (ethPool > fee ? ethPool - 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) internal {
        p.graduated = true; // set FIRST (idempotence)

        // 1. remove the whole curve position: hook is credited the raise
        //    (~4.274 ETH + any donate() dust as fees) and unsold token dust
        (BalanceDelta d,) = poolManager.modifyLiquidity(
            key,
            ModifyLiquidityParams({
                tickLower: TICK_LOWER,
                tickUpper: TICK_UPPER,
                liquidityDelta: -int256(uint256(L_CURVE)),
                salt: 0
            }),
            ""
        );
        uint256 ethAvail = uint256(uint128(d.amount0()));
        uint256 tokenCredit = uint256(uint128(d.amount1()));

        // 2. repair the boundary overshoot: the completing swap may have walked
        //    price below TICK_LOWER through empty liquidity. With zero pool
        //    liquidity a 1-wei-specified swap walks price back UP to exactly
        //    the graduation price at zero cost (input is never consumed).
        (uint160 sqrtP,,,) = poolManager.getSlot0(id);
        if (sqrtP < SQRT_GRAD) {
            poolManager.swap(
                key, SwapParams({zeroForOne: false, amountSpecified: -1, sqrtPriceLimitX96: SQRT_GRAD}), ""
            );
        }

        // 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 lEth = LiquidityAmounts.getLiquidityForAmount0(SQRT_GRAD, SQRT_FULL_UPPER, ethAvail - 1);
            uint128 lTok = LiquidityAmounts.getLiquidityForAmount1(SQRT_FULL_LOWER, SQRT_GRAD, LP_SUPPLY - 1);
            lFull = lEth < lTok ? lEth : lTok;
        }
        (BalanceDelta md,) = poolManager.modifyLiquidity(
            key,
            ModifyLiquidityParams({
                tickLower: FULL_TICK_LOWER,
                tickUpper: FULL_TICK_UPPER,
                liquidityDelta: int256(uint256(lFull)),
                salt: 0
            }),
            ""
        );
        uint256 ethUsed = uint256(uint128(-md.amount0()));
        uint256 tokUsed = uint256(uint128(-md.amount1()));

        // 5. settle the token side from the hook's 200M reserve (ERC20 custody
        //    since create), netting the removal's token-dust credit
        if (tokUsed > tokenCredit) {
            poolManager.sync(key.currency1);
            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(key.currency1, DEAD, tokenCredit - tokUsed);
        }

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

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

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

    /// @notice Launch a coin. Costs nothing beyond gas; msg.value (optional)
    ///         is the dev buy, executed as the first purchase in the same
    ///         transaction (candle #1 on every terminal) and subject to the
    ///         same 2% first-hour cap and anti-snipe premium as everyone else.
    /// @param antiSnipe v2.2: true = the decaying premium exactly as today
    ///        (factory defaults frozen at create); false = ZERO premium from
    ///        second 0. The choice is public (AntiSnipeOff event + view).
    function create(
        string calldata name,
        string calldata symbol,
        string calldata metadata,
        address feeRecipient,
        uint256 minTokensOut,
        bool antiSnipe
    ) external payable 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 needed).
        _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
        token = address(new PmavCoin(name, symbol)); // 1B minted to the hook
        address recipient = feeRecipient == address(0) ? msg.sender : feeRecipient;

        // stack discipline: _register stores the pool and the emit reads the
        // id back from storage, so no PoolKey local lives across the emit
        // (the antiSnipe param costs one stack slot vs v2.1; this profile
        // compiles without via-ir and must stay that way)
        _register(token, recipient, antiSnipe);
        emit CoinCreated(token, PoolId.unwrap(poolOf[token]), msg.sender, recipient, name, symbol, metadata);
        if (!antiSnipe) emit AntiSnipeOff(token);

        // our own initialize skips beforeInitialize (noSelfCall); every
        // external initialize attempt against this hook reverts there
        PoolKey memory key = poolKeyOf(token);
        poolManager.initialize(key, SQRT_START);

        // seed the curve position (+ optional dev buy) inside one unlock
        poolManager.unlock(abi.encode(uint8(1), abi.encode(key, msg.sender, msg.value, minTokensOut)));
    }

    /// @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 (("ab","c") can
    ///      never equal ("a","bc"), unlike naive concatenation). 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 {
        PoolId id = poolKeyOf(token).toId();
        // v2.2: antiSnipe=false freezes a ZERO premium into the pool struct
        // (the swap path's _antiSnipeBps already short circuits on window 0,
        // so no swap path change exists for the flag at all)
        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
    function poolKeyOf(address token) public view returns (PoolKey memory) {
        return PoolKey({
            currency0: CurrencyLibrary.ADDRESS_ZERO, // native ETH, always currency0
            currency1: Currency.wrap(token),
            fee: LPFeeLibrary.DYNAMIC_FEE_FLAG,
            tickSpacing: TICK_SPACING,
            hooks: this
        });
    }

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

    /// dispatch for the hook's own unlock operations:
    /// 1 = create (seed curve + dev buy), 2 = payout (burn claims + take ETH),
    /// 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), ETH_ID, amount);
            poolManager.take(CurrencyLibrary.ADDRESS_ZERO, 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;

        // seed the curve: at the top of the range the position is 100% tokens
        (BalanceDelta d,) = poolManager.modifyLiquidity(
            key,
            ModifyLiquidityParams({
                tickLower: TICK_LOWER,
                tickUpper: TICK_UPPER,
                liquidityDelta: int256(uint256(L_CURVE)),
                salt: 0
            }),
            ""
        );
        // settle the owed tokens (<= 800M by L_CURVE construction; no ETH owed)
        uint256 owedTokens = uint256(uint128(-d.amount1()));
        poolManager.sync(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, 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.
    function _devBuy(PoolKey memory key, Pool storage p, 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, 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;
        {
            BalanceDelta sd = poolManager.swap(
                key,
                SwapParams({
                    zeroForOne: true,
                    amountSpecified: -int256(net),
                    sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
                }),
                ""
            );
            tokensOut = uint256(uint128(sd.amount1()));
        }
        require(tokensOut >= minTokensOut, "slippage");
        require(tokensOut <= MAX_WALLET, "cap invariant");

        // settle net (the swap) + fee (real ETH into the PoolManager,
        // immediately re-minted as the hook's ERC6909 fee claim)
        poolManager.settle{value: net + fee}();
        poolManager.mint(address(this), ETH_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(key.currency1, creator, tokensOut);
        _emitTrade(p, id, true, gross, net, tokensOut);

        if (refund > 0) {
            (bool ok,) = creator.call{value: refund}("");
            require(ok, "refund failed");
        }
    }

    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 wei 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 wei 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 native float could not cover the instant take).
    ///         Permissionless pull, claimCreatorFees shape: burns the hook's
    ///         ERC6909 claims and pays real ETH — by claim time the buyer's
    ///         own settle has long since delivered the backing ETH.
    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). ETH side split 70% creator / 30% platform into the pull
    ///         ledgers (so a non-payable 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()];

        // 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 ethFees = uint256(uint128(fees.amount0()));
        uint256 coinFees = uint256(uint128(fees.amount1()));

        // graduated-pool LP fees split 70% creator / 30% platform (= 3/10
        // platform), odd wei to the CREATOR — identical rounding discipline to
        // _credit (there is no anti-snipe premium here).
        uint256 platEth = ethFees * 3 / 10;
        if (ethFees > 0) {
            poolManager.mint(address(this), ETH_ID, ethFees);
            creatorFees[p.feeRecipient] += ethFees - platEth;
            platformFees += platEth;
        }
        uint256 platCoin = coinFees * 3 / 10;
        if (coinFees > 0) {
            poolManager.take(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, ethFees - platEth, coinFees - platCoin, platEth, 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 step 1 of 2 (v2.2, audit M1): PROPOSE redirecting an
    ///         abandoned coin's FUTURE creator rewards to a community lead.
    ///         Gates: dev provably inactive for CTO_INACTIVITY (36h; no fee
    ///         claim, no per-coin steward action; trading does NOT protect a
    ///         dev), coin not locked, target neither zero nor the owner
    ///         itself. Emits a public, indexed notice; nothing changes yet.
    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");
        // one claim or one steward action since the proposal = veto
        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).
    function quoteBuy(address token, uint256 ethGross)
        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");
        uint256 feeBps = FEE_BPS + _antiSnipeBps(p);
        uint160 sqrtP = _effSqrtP(id);
        // v2.2 (audit L1): the fee base is bounded by the gross the curve can
        // actually consume to graduation — mirrors the beforeSwap clamp
        // exactly, so an oversized completing buy is quoted AND charged fee
        // on the USED gross only.
        {
            uint256 clampNet = SqrtPriceMath.getAmount0Delta(SQRT_GRAD, sqrtP, L_CURVE, true);
            uint256 maxFeeGross = FullMath.mulDivRoundingUp(clampNet, 10_000, 10_000 - feeBps);
            fee = (ethGross > maxFeeGross ? maxFeeGross : ethGross) * feeBps / 10_000;
        }
        uint256 net = ethGross - fee;
        require(net > 0, "dust buy");
        uint160 sqrtNext = SqrtPriceMath.getNextSqrtPriceFromInput(sqrtP, L_CURVE, net, true);
        if (sqrtNext < SQRT_GRAD) {
            // completing buy: only the ETH that finishes the curve is consumed
            uint256 maxNet = SqrtPriceMath.getAmount0Delta(SQRT_GRAD, sqrtP, L_CURVE, true);
            refund = ethGross - fee - maxNet;
            tokensOut = SqrtPriceMath.getAmount1Delta(SQRT_GRAD, sqrtP, L_CURVE, false);
        } else {
            tokensOut = SqrtPriceMath.getAmount1Delta(sqrtNext, sqrtP, L_CURVE, false);
        }
    }

    function quoteSell(address token, uint256 tokenAmount) external view returns (uint256 ethNet, 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");
        uint160 sqrtP = _effSqrtP(id);
        uint160 sqrtNext = SqrtPriceMath.getNextSqrtPriceFromInput(sqrtP, L_CURVE, tokenAmount, false);
        if (sqrtNext > SQRT_START) sqrtNext = SQRT_START; // beyond the curve top the pool stops paying
        uint256 ethGross = SqrtPriceMath.getAmount0Delta(sqrtP, sqrtNext, L_CURVE, false);
        fee = ethGross * FEE_BPS / 10_000;
        ethNet = ethGross - fee;
    }

    /// @notice spot price in wei per whole token, and market cap in wei.
    ///         v1-shaped for the site; `pool` is the PoolManager once
    ///         graduated (v1 semantics: nonzero pool == trade on the DEX).
    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);
        // ETH per whole token = (2^96 / sqrtP)^2 * 1e18 (both sides 18 decimals)
        priceWei = FullMath.mulDiv(1 << 192, 1e18, uint256(sqrtP) * uint256(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;
    }

    /// @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;
    }

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

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

    /// specified currency = input for exactIn, output for exactOut; ETH is
    /// always currency0, so specified is ETH iff zeroForOne == exactInput
    function _specifiedIsEth(SwapParams calldata params) internal pure returns (bool) {
        return params.zeroForOne == (params.amountSpecified < 0);
    }

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

    /// current curve price for quote/clamp math, bounded to the curve range:
    /// a sell can park the spot price ABOVE the curve top through empty
    /// liquidity; buys from there walk back down for free, so all token/ETH
    /// math must start at the top of the range, not the empty-space price
    function _effSqrtP(PoolId id) internal view returns (uint160 sqrtP) {
        (sqrtP,,,) = poolManager.getSlot0(id);
        if (sqrtP > SQRT_START) sqrtP = SQRT_START;
    }

    /// max gross ETH (fee included) a first-hour buy may spend so that the
    /// delivered tokens never exceed capLeft. Rounding discipline (test
    /// enforced): the sqrt target rounds UP (fewer tokens) and the ETH amount
    /// rounds DOWN, so core's forward math can never deliver > capLeft.
    function _maxGrossForCap(PoolId id, uint256 capLeft, uint256 feeBps) internal view returns (uint256 maxGross) {
        uint160 sqrtP = _effSqrtP(id);
        if (sqrtP <= SQRT_GRAD) return 0;
        // sqrt price after the position hands out capLeft tokens (quotient
        // floored => target rounds UP => tokens at target <= capLeft)
        uint256 quotient = FullMath.mulDiv(capLeft, FixedPoint96.Q96, L_CURVE);
        bool gradBound = quotient >= sqrtP || sqrtP - quotient <= SQRT_GRAD;
        uint160 sqrtTarget = gradBound ? SQRT_GRAD : uint160(sqrtP - quotient);
        // when the graduation edge binds the fill is bounded by the curve
        // itself, so rounding UP is safe (it just guarantees graduation)
        uint256 maxNet = SqrtPriceMath.getAmount0Delta(sqrtTarget, sqrtP, L_CURVE, 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 tx.origin held at the START of this
    //     transaction, snapshotted at the FIRST cap touch (while boughtTx is
    //     still 0) and then FIXED for the tx. It captures any pre-existing
    //     holding (bought in an earlier tx, or received) that 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 (which
    // have not settled into balanceOf yet), boughtTx IS this-tx buys — so their
    // SUM is the exact acquisition-inclusive holding: no double count of a
    // settled buy, and no under count of a wallet that already held tokens and
    // then 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: once
    // startBalance + boughtTx reaches 2% no further buy fills. Transient storage
    // clears at tx end, so a later transaction re-snapshots startBalance from
    // the live balance — a genuine sell or 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. Only PmavCoin-level transfer limits could stop that, and they
    // are the classic honeypot-scanner flag, rejected by doctrine. 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 that bundles buys, and no
    /// balance move can re-open the cap because both terms are 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;
    }

    /// record `amount` tokens acquired by origin in THIS unlock (transient)
    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;
    }

    /// only the PoolManager sends ETH here (refund flows go PM -> tx.origin
    /// directly; claim payouts go PM -> recipient directly). The hook itself
    /// holds ETH only transiently inside create().
    receive() external payable {
        require(msg.sender == address(poolManager), "direct eth");
    }
}
Chain explorer6491msChain node144ms