PmavHook

0x97472ae141ff13a4d317328bc4f3f95172fba8cc

Verification
Verified
v0.8.28+commit.7893614a
Type
Contract
30,507 bytes
ABI entries
79
40 read · 22 write
License
none

Contract information

Address
0x97472ae141ff13a4d317328bc4f3f95172fba8cc
Chain
Robinhood Chain (4663)
Compiler
v0.8.28+commit.7893614a
Optimization
Enabled
Creation tx
0x4e8bae0a33…7d03b79543

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (40)

ANTI_SNIPE_MAX_BPS()uint256
ANTI_SNIPE_MAX_WINDOW()uint256
CTO_INACTIVITY()uint256
CURVE_SUPPLY()uint256
FEE_BPS()uint256
FIRST_HOUR()uint256
FIRST_HOUR_MAX_BUY()uint256
FULL_TICK_LOWER()int24
FULL_TICK_UPPER()int24
GRADUATED_LP_FEE()uint24
GRADUATION_GAS()uint256
LP_SUPPLY()uint256
L_CURVE()uint128
SQRT_GRAD()uint160
SQRT_START()uint160
TICK_LOWER()int24
TICK_SPACING()int24
TICK_UPPER()int24
TOTAL_SUPPLY()uint256
allCoins(uint256)address
antiSnipePeakBps()uint256
antiSnipeWindow()uint256
coinCount()uint256
creatorFees(address)uint256
feeRecipientLocked(address)bool
feeRecipientOf(address)address
firstHourBought(bytes32, address)uint256
getHookPermissions()tuple
lastActivity(address)uint256
lastClaim(address)uint256
owner()address
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 (13)

AntiSnipeChangedCoinCreatedCreatorFeesClaimedCtoRedirectFeeRecipientChangedFeeRecipientLockedEventGraduatedOwnerChangedPlatformFeesWithdrawnPoolFeesCollectedRefundClaimedRefundQueuedTrade

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"
      },
      {
        "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"
      }
    ],
    "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": "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": "FIRST_HOUR",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "FIRST_HOUR_MAX_BUY",
    "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": "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": [],
    "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"
      }
    ],
    "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": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "newRecipient",
        "type": "address"
      }
    ],
    "name": "ctoRedirect",
    "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": [
      {
        "internalType": "PoolId",
        "name": "",
        "type": "bytes32"
      },
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "firstHourBought",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "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": [],
    "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": "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.
/// ---------------------------------------------------------------------------

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 — the pmav.fun V2 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
    uint256 public constant FIRST_HOUR = 3600;
    uint256 public constant FIRST_HOUR_MAX_BUY = 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 = 7 days;
    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;

    /// cumulative first-hour purchases: pool => tx.origin => tokens bought
    mapping(PoolId => mapping(address => uint256)) public firstHourBought;

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

    /// 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 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)
        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) 2% first-hour cap — FILL TO CAP + AUTO REFUND, never a failed buy
        uint256 refund = 0;
        if (params.zeroForOne && block.timestamp < p.launchTime + FIRST_HOUR) {
            uint256 capLeft = FIRST_HOUR_MAX_BUY - firstHourBought[id][tx.origin];
            require(capLeft > 0, "first hour: cap reached");
            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, "first hour: cap reached");
                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, "first hour: max 2% per wallet");
            }
        }

        // (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;
            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.
                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)
        internal
        override
        returns (bytes4, int128)
    {
        PoolId id = key.toId();
        Pool storage p = pools[id];
        if (p.graduated) {
            // PURE PASSTHROUGH for pricing/caps/fees — but the v1-named Trade
            // event ships on EVERY swap forever (indexer compatibility, §14).
            // Graduation's own inner swap never reaches here (noSelfCall).
            uint256 e = _abs(int256(delta.amount0()));
            uint256 t = _abs(int256(delta.amount1()));
            (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, 0);
        }

        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);
    }

    /// 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) first-hour ledger: record the ACTUAL fill (clamping already
        // happened in beforeSwap; the require is a belt-and-braces invariant)
        if (params.zeroForOne && block.timestamp < p.launchTime + FIRST_HOUR) {
            uint256 bought = firstHourBought[id][tx.origin] + tokenAmount;
            require(bought <= FIRST_HOUR_MAX_BUY, "cap invariant");
            firstHourBought[id][tx.origin] = bought;
        }
        _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.
    function create(
        string calldata name,
        string calldata symbol,
        string calldata metadata,
        address feeRecipient,
        uint256 minTokensOut
    ) 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");

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

        PoolKey memory key = _register(token, recipient);
        emit CoinCreated(token, PoolId.unwrap(key.toId()), msg.sender, recipient, name, symbol, metadata);

        // our own initialize skips beforeInitialize (noSelfCall); every
        // external initialize attempt against this hook reverts there
        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)));
    }

    function _register(address token, address recipient) internal returns (PoolKey memory key) {
        key = poolKeyOf(token);
        PoolId id = key.toId();
        pools[id] = Pool({
            coin: token,
            feeRecipient: recipient,
            launchBlock: uint64(block.number),
            launchTime: uint64(block.timestamp),
            snipePeakBps: uint16(antiSnipePeakBps),
            snipeWindow: uint16(antiSnipeWindow),
            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, FIRST_HOUR_MAX_BUY, 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 <= FIRST_HOUR_MAX_BUY, "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);

        firstHourBought[id][tx.origin] = tokensOut; // fresh pool: first buy
        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 (community takeover): redirect an abandoned coin's FUTURE
    ///         creator rewards to a community lead. The one rail no review can
    ///         waive: the current DEV must be provably inactive — no fee claim
    ///         and no per-coin steward action for CTO_INACTIVITY. Trading does
    ///         NOT protect a dev. Locked coins are immune by design.
    function ctoRedirect(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(!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");
        lastActivity[token] = block.timestamp; // fresh window for the new lead
        emit FeeRecipientChanged(token, p.feeRecipient, newRecipient);
        emit CtoRedirect(token, p.feeRecipient, newRecipient);
        p.feeRecipient = 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);
        // v2 exactIn semantics: the fee is charged on the STATED gross (the
        // difference vs v1's fee-on-used-gross exists only on the completing
        // buy — spec §12, accepted)
        fee = ethGross * feeBps / 10_000;
        uint256 net = ethGross - fee;
        uint160 sqrtP = _effSqrtP(id);
        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;
    }

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

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

    /// 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 explorer3454msChain node92ms