PmavHookQuoted
0x24f90d176cdb7f288d4390083dfaa41d6ff968cc
Verification
Verified
0.8.28+commit.7893614a
Type
Contract
39,157 bytes
ABI entries
91
50 read · 23 write
License
none
Contract information
- Address
- 0x24f90d176cdb7f288d4390083dfaa41d6ff968cc
- Chain
- Robinhood Chain (4663)
- Compiler
- 0.8.28+commit.7893614a
- Optimization
- Enabled
- Creator
- 0x4e59b44847…26c0B4956C
- Creation tx
- 0x9f13618f7e…76dc28743d
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (50)
ANTI_SNIPE_MAX_BPS() → uint256
ANTI_SNIPE_MAX_WINDOW() → uint256
CTO_INACTIVITY() → uint256
CTO_VETO_DELAY() → uint256
CURVE_SUPPLY() → uint256
FEE_BPS() → uint256
FULL_TICK_LOWER() → int24
FULL_TICK_UPPER() → int24
GRADUATED_LP_FEE() → uint24
GRADUATION_GAS() → uint256
GRAD_RAISE() → uint256
LP_SUPPLY() → uint256
L_CURVE_C0() → uint128
L_CURVE_Q0() → uint128
MAX_WALLET() → uint256
PAIR_COOLDOWN() → uint256
QUOTE_DECIMALS() → uint8
QUOTE_TOKEN() → address
SQRT_GRAD_C0() → uint160
SQRT_GRAD_Q0() → uint160
SQRT_START_C0() → uint160
SQRT_START_Q0() → uint160
TICK_LOWER_Q0() → int24
TICK_SPACING() → int24
TICK_UPPER_Q0() → int24
TOTAL_SUPPLY() → uint256
allCoins(uint256) → address
antiSnipeEnabled(address) → bool
antiSnipePeakBps() → uint256
antiSnipeWindow() → uint256
coinCount() → uint256
creatorFees(address) → uint256
ctoProposals(address) → address, uint64
curveParamsOf(address) → bool, int24, int24, uint160, uint160, uint128
feeRecipientLocked(address) → bool
feeRecipientOf(address) → address
getHookPermissions() → tuple
lastActivity(address) → uint256
lastClaim(address) → uint256
owner() → address
pairLastLaunch(bytes32) → uint64
platformFees() → uint256
poolKeyOf(address) → tuple
poolManager() → address
poolOf(address) → bytes32
pools(bytes32) → address, address, uint64, uint64, uint16, uint16, bool, uint128, uint128
quoteBuy(address, uint256) → uint256, uint256, uint256
quoteSell(address, uint256) → uint256, uint256
refunds(address) → uint256
state(address) → uint256, uint256, uint256, uint256, uint256, bool, address
Events (15)
AntiSnipeChangedAntiSnipeOffCoinCreatedCreatorFeesClaimedCtoRedirectCtoRedirectProposedFeeRecipientChangedFeeRecipientLockedEventGraduatedOwnerChangedPlatformFeesWithdrawnPoolFeesCollectedRefundClaimedRefundQueuedTrade
ABI
[
{
"inputs": [
{
"internalType": "contract IPoolManager",
"name": "_manager",
"type": "address"
},
{
"internalType": "address",
"name": "_owner",
"type": "address"
},
{
"internalType": "address",
"name": "_quoteToken",
"type": "address"
},
{
"internalType": "uint8",
"name": "_quoteDecimals",
"type": "uint8"
},
{
"internalType": "uint256",
"name": "_gradRaise",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "HookNotImplemented",
"type": "error"
},
{
"inputs": [],
"name": "NotPoolManager",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "peakBps",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "window",
"type": "uint256"
}
],
"name": "AntiSnipeChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "AntiSnipeOff",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "bytes32",
"name": "poolId",
"type": "bytes32"
},
{
"indexed": true,
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "feeRecipient",
"type": "address"
},
{
"indexed": false,
"internalType": "string",
"name": "name",
"type": "string"
},
{
"indexed": false,
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"indexed": false,
"internalType": "string",
"name": "metadata",
"type": "string"
}
],
"name": "CoinCreated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "CreatorFeesClaimed",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "oldRecipient",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newRecipient",
"type": "address"
}
],
"name": "CtoRedirect",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "oldRecipient",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newRecipient",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "executableAt",
"type": "uint256"
}
],
"name": "CtoRedirectProposed",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "oldRecipient",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newRecipient",
"type": "address"
}
],
"name": "FeeRecipientChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "recipient",
"type": "address"
}
],
"name": "FeeRecipientLockedEvent",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "bytes32",
"name": "poolId",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "uint256",
"name": "ethInPool",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokensInPool",
"type": "uint256"
}
],
"name": "Graduated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "oldOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnerChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "PlatformFeesWithdrawn",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "creatorRecipient",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "creatorWei",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "creatorTokens",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "platformWei",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "platformTokens",
"type": "uint256"
}
],
"name": "PoolFeesCollected",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "RefundClaimed",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "RefundQueued",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "trader",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "isBuy",
"type": "bool"
},
{
"indexed": false,
"internalType": "uint256",
"name": "ethGross",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "ethNet",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokenAmount",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint160",
"name": "sqrtPriceX96",
"type": "uint160"
},
{
"indexed": false,
"internalType": "uint128",
"name": "tokensSold",
"type": "uint128"
},
{
"indexed": false,
"internalType": "uint128",
"name": "raisedWei",
"type": "uint128"
}
],
"name": "Trade",
"type": "event"
},
{
"inputs": [],
"name": "ANTI_SNIPE_MAX_BPS",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "ANTI_SNIPE_MAX_WINDOW",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "CTO_INACTIVITY",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "CTO_VETO_DELAY",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "CURVE_SUPPLY",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "FEE_BPS",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "FULL_TICK_LOWER",
"outputs": [
{
"internalType": "int24",
"name": "",
"type": "int24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "FULL_TICK_UPPER",
"outputs": [
{
"internalType": "int24",
"name": "",
"type": "int24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "GRADUATED_LP_FEE",
"outputs": [
{
"internalType": "uint24",
"name": "",
"type": "uint24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "GRADUATION_GAS",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "GRAD_RAISE",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "LP_SUPPLY",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "L_CURVE_C0",
"outputs": [
{
"internalType": "uint128",
"name": "",
"type": "uint128"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "L_CURVE_Q0",
"outputs": [
{
"internalType": "uint128",
"name": "",
"type": "uint128"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MAX_WALLET",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "PAIR_COOLDOWN",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "QUOTE_DECIMALS",
"outputs": [
{
"internalType": "uint8",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "QUOTE_TOKEN",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "SQRT_GRAD_C0",
"outputs": [
{
"internalType": "uint160",
"name": "",
"type": "uint160"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "SQRT_GRAD_Q0",
"outputs": [
{
"internalType": "uint160",
"name": "",
"type": "uint160"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "SQRT_START_C0",
"outputs": [
{
"internalType": "uint160",
"name": "",
"type": "uint160"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "SQRT_START_Q0",
"outputs": [
{
"internalType": "uint160",
"name": "",
"type": "uint160"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "TICK_LOWER_Q0",
"outputs": [
{
"internalType": "int24",
"name": "",
"type": "int24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "TICK_SPACING",
"outputs": [
{
"internalType": "int24",
"name": "",
"type": "int24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "TICK_UPPER_Q0",
"outputs": [
{
"internalType": "int24",
"name": "",
"type": "int24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "TOTAL_SUPPLY",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"components": [
{
"internalType": "Currency",
"name": "currency0",
"type": "address"
},
{
"internalType": "Currency",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "contract IHooks",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "key",
"type": "tuple"
},
{
"components": [
{
"internalType": "int24",
"name": "tickLower",
"type": "int24"
},
{
"internalType": "int24",
"name": "tickUpper",
"type": "int24"
},
{
"internalType": "int256",
"name": "liquidityDelta",
"type": "int256"
},
{
"internalType": "bytes32",
"name": "salt",
"type": "bytes32"
}
],
"internalType": "struct ModifyLiquidityParams",
"name": "params",
"type": "tuple"
},
{
"internalType": "BalanceDelta",
"name": "delta",
"type": "int256"
},
{
"internalType": "BalanceDelta",
"name": "feesAccrued",
"type": "int256"
},
{
"internalType": "bytes",
"name": "hookData",
"type": "bytes"
}
],
"name": "afterAddLiquidity",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
},
{
"internalType": "BalanceDelta",
"name": "",
"type": "int256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"components": [
{
"internalType": "Currency",
"name": "currency0",
"type": "address"
},
{
"internalType": "Currency",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "contract IHooks",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "key",
"type": "tuple"
},
{
"internalType": "uint256",
"name": "amount0",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amount1",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "hookData",
"type": "bytes"
}
],
"name": "afterDonate",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"components": [
{
"internalType": "Currency",
"name": "currency0",
"type": "address"
},
{
"internalType": "Currency",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "contract IHooks",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "key",
"type": "tuple"
},
{
"internalType": "uint160",
"name": "sqrtPriceX96",
"type": "uint160"
},
{
"internalType": "int24",
"name": "tick",
"type": "int24"
}
],
"name": "afterInitialize",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"components": [
{
"internalType": "Currency",
"name": "currency0",
"type": "address"
},
{
"internalType": "Currency",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "contract IHooks",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "key",
"type": "tuple"
},
{
"components": [
{
"internalType": "int24",
"name": "tickLower",
"type": "int24"
},
{
"internalType": "int24",
"name": "tickUpper",
"type": "int24"
},
{
"internalType": "int256",
"name": "liquidityDelta",
"type": "int256"
},
{
"internalType": "bytes32",
"name": "salt",
"type": "bytes32"
}
],
"internalType": "struct ModifyLiquidityParams",
"name": "params",
"type": "tuple"
},
{
"internalType": "BalanceDelta",
"name": "delta",
"type": "int256"
},
{
"internalType": "BalanceDelta",
"name": "feesAccrued",
"type": "int256"
},
{
"internalType": "bytes",
"name": "hookData",
"type": "bytes"
}
],
"name": "afterRemoveLiquidity",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
},
{
"internalType": "BalanceDelta",
"name": "",
"type": "int256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"components": [
{
"internalType": "Currency",
"name": "currency0",
"type": "address"
},
{
"internalType": "Currency",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "contract IHooks",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "key",
"type": "tuple"
},
{
"components": [
{
"internalType": "bool",
"name": "zeroForOne",
"type": "bool"
},
{
"internalType": "int256",
"name": "amountSpecified",
"type": "int256"
},
{
"internalType": "uint160",
"name": "sqrtPriceLimitX96",
"type": "uint160"
}
],
"internalType": "struct SwapParams",
"name": "params",
"type": "tuple"
},
{
"internalType": "BalanceDelta",
"name": "delta",
"type": "int256"
},
{
"internalType": "bytes",
"name": "hookData",
"type": "bytes"
}
],
"name": "afterSwap",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
},
{
"internalType": "int128",
"name": "",
"type": "int128"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "allCoins",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "antiSnipeEnabled",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "antiSnipePeakBps",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "antiSnipeWindow",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"components": [
{
"internalType": "Currency",
"name": "currency0",
"type": "address"
},
{
"internalType": "Currency",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "contract IHooks",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "key",
"type": "tuple"
},
{
"components": [
{
"internalType": "int24",
"name": "tickLower",
"type": "int24"
},
{
"internalType": "int24",
"name": "tickUpper",
"type": "int24"
},
{
"internalType": "int256",
"name": "liquidityDelta",
"type": "int256"
},
{
"internalType": "bytes32",
"name": "salt",
"type": "bytes32"
}
],
"internalType": "struct ModifyLiquidityParams",
"name": "params",
"type": "tuple"
},
{
"internalType": "bytes",
"name": "hookData",
"type": "bytes"
}
],
"name": "beforeAddLiquidity",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"components": [
{
"internalType": "Currency",
"name": "currency0",
"type": "address"
},
{
"internalType": "Currency",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "contract IHooks",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "key",
"type": "tuple"
},
{
"internalType": "uint256",
"name": "amount0",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amount1",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "hookData",
"type": "bytes"
}
],
"name": "beforeDonate",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"components": [
{
"internalType": "Currency",
"name": "currency0",
"type": "address"
},
{
"internalType": "Currency",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "contract IHooks",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "key",
"type": "tuple"
},
{
"internalType": "uint160",
"name": "sqrtPriceX96",
"type": "uint160"
}
],
"name": "beforeInitialize",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"components": [
{
"internalType": "Currency",
"name": "currency0",
"type": "address"
},
{
"internalType": "Currency",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "contract IHooks",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "key",
"type": "tuple"
},
{
"components": [
{
"internalType": "int24",
"name": "tickLower",
"type": "int24"
},
{
"internalType": "int24",
"name": "tickUpper",
"type": "int24"
},
{
"internalType": "int256",
"name": "liquidityDelta",
"type": "int256"
},
{
"internalType": "bytes32",
"name": "salt",
"type": "bytes32"
}
],
"internalType": "struct ModifyLiquidityParams",
"name": "params",
"type": "tuple"
},
{
"internalType": "bytes",
"name": "hookData",
"type": "bytes"
}
],
"name": "beforeRemoveLiquidity",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"components": [
{
"internalType": "Currency",
"name": "currency0",
"type": "address"
},
{
"internalType": "Currency",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "contract IHooks",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "key",
"type": "tuple"
},
{
"components": [
{
"internalType": "bool",
"name": "zeroForOne",
"type": "bool"
},
{
"internalType": "int256",
"name": "amountSpecified",
"type": "int256"
},
{
"internalType": "uint160",
"name": "sqrtPriceLimitX96",
"type": "uint160"
}
],
"internalType": "struct SwapParams",
"name": "params",
"type": "tuple"
},
{
"internalType": "bytes",
"name": "hookData",
"type": "bytes"
}
],
"name": "beforeSwap",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
},
{
"internalType": "BeforeSwapDelta",
"name": "",
"type": "int256"
},
{
"internalType": "uint24",
"name": "",
"type": "uint24"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "claimCreatorFees",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "claimRefund",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "coinCount",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "collectPoolFees",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"internalType": "string",
"name": "metadata",
"type": "string"
},
{
"internalType": "address",
"name": "feeRecipient",
"type": "address"
},
{
"internalType": "uint256",
"name": "quoteIn",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "minTokensOut",
"type": "uint256"
},
{
"internalType": "bool",
"name": "antiSnipe",
"type": "bool"
}
],
"name": "create",
"outputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "creatorFees",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "ctoProposals",
"outputs": [
{
"internalType": "address",
"name": "newRecipient",
"type": "address"
},
{
"internalType": "uint64",
"name": "proposedAt",
"type": "uint64"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "curveParamsOf",
"outputs": [
{
"internalType": "bool",
"name": "coinIs0",
"type": "bool"
},
{
"internalType": "int24",
"name": "tickLower",
"type": "int24"
},
{
"internalType": "int24",
"name": "tickUpper",
"type": "int24"
},
{
"internalType": "uint160",
"name": "sqrtStart",
"type": "uint160"
},
{
"internalType": "uint160",
"name": "sqrtGrad",
"type": "uint160"
},
{
"internalType": "uint128",
"name": "lCurve",
"type": "uint128"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "executeCtoRedirect",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "feeRecipientLocked",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "feeRecipientOf",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "getHookPermissions",
"outputs": [
{
"components": [
{
"internalType": "bool",
"name": "beforeInitialize",
"type": "bool"
},
{
"internalType": "bool",
"name": "afterInitialize",
"type": "bool"
},
{
"internalType": "bool",
"name": "beforeAddLiquidity",
"type": "bool"
},
{
"internalType": "bool",
"name": "afterAddLiquidity",
"type": "bool"
},
{
"internalType": "bool",
"name": "beforeRemoveLiquidity",
"type": "bool"
},
{
"internalType": "bool",
"name": "afterRemoveLiquidity",
"type": "bool"
},
{
"internalType": "bool",
"name": "beforeSwap",
"type": "bool"
},
{
"internalType": "bool",
"name": "afterSwap",
"type": "bool"
},
{
"internalType": "bool",
"name": "beforeDonate",
"type": "bool"
},
{
"internalType": "bool",
"name": "afterDonate",
"type": "bool"
},
{
"internalType": "bool",
"name": "beforeSwapReturnDelta",
"type": "bool"
},
{
"internalType": "bool",
"name": "afterSwapReturnDelta",
"type": "bool"
},
{
"internalType": "bool",
"name": "afterAddLiquidityReturnDelta",
"type": "bool"
},
{
"internalType": "bool",
"name": "afterRemoveLiquidityReturnDelta",
"type": "bool"
}
],
"internalType": "struct Hooks.Permissions",
"name": "",
"type": "tuple"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "lastActivity",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "lastClaim",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "lockFeeRecipient",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "newRecipient",
"type": "address"
}
],
"name": "lockFeeRecipientTo",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"name": "pairLastLaunch",
"outputs": [
{
"internalType": "uint64",
"name": "",
"type": "uint64"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "platformFees",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "poolKeyOf",
"outputs": [
{
"components": [
{
"internalType": "Currency",
"name": "currency0",
"type": "address"
},
{
"internalType": "Currency",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "contract IHooks",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "",
"type": "tuple"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "poolManager",
"outputs": [
{
"internalType": "contract IPoolManager",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "poolOf",
"outputs": [
{
"internalType": "PoolId",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "PoolId",
"name": "",
"type": "bytes32"
}
],
"name": "pools",
"outputs": [
{
"internalType": "address",
"name": "coin",
"type": "address"
},
{
"internalType": "address",
"name": "feeRecipient",
"type": "address"
},
{
"internalType": "uint64",
"name": "launchBlock",
"type": "uint64"
},
{
"internalType": "uint64",
"name": "launchTime",
"type": "uint64"
},
{
"internalType": "uint16",
"name": "snipePeakBps",
"type": "uint16"
},
{
"internalType": "uint16",
"name": "snipeWindow",
"type": "uint16"
},
{
"internalType": "bool",
"name": "graduated",
"type": "bool"
},
{
"internalType": "uint128",
"name": "tokensSold",
"type": "uint128"
},
{
"internalType": "uint128",
"name": "raisedWei",
"type": "uint128"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "newRecipient",
"type": "address"
}
],
"name": "proposeCtoRedirect",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "quoteGross",
"type": "uint256"
}
],
"name": "quoteBuy",
"outputs": [
{
"internalType": "uint256",
"name": "tokensOut",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "fee",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "refund",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenAmount",
"type": "uint256"
}
],
"name": "quoteSell",
"outputs": [
{
"internalType": "uint256",
"name": "quoteNet",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "fee",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "refunds",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "peakBps",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "window",
"type": "uint256"
}
],
"name": "setAntiSnipeDefaults",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "newRecipient",
"type": "address"
}
],
"name": "setFeeRecipient",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "setOwner",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "state",
"outputs": [
{
"internalType": "uint256",
"name": "priceWei",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "mcapWei",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "ethReserve",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "tokensSold",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "progressBps",
"type": "uint256"
},
{
"internalType": "bool",
"name": "graduated",
"type": "bool"
},
{
"internalType": "address",
"name": "pool",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "unlockCallback",
"outputs": [
{
"internalType": "bytes",
"name": "",
"type": "bytes"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "withdrawPlatformFees",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;
/// ---------------------------------------------------------------------------
/// pmav.fun V2 — the curve IS the pool (Uniswap V4 hook architecture)
///
/// QUOTED VARIANT ("v2q") — the RWA quoted curve lane hook per
/// docs/RWA_QUOTED_CURVE_PLAN.md. Derived from the audited PmavHook v2.1
/// (contracts/src/v2/PmavHook.sol, LIVE on Robinhood Chain 4663) with EXACTLY
/// four categories of change, all forced by the quote currency becoming an
/// IMMUTABLE ERC20 (USDG first; stock tokens in Phase 2) instead of native ETH:
///
/// (a) SETTLEMENT: every native-ETH settle/take/mint path operates on the
/// quote ERC20. Buys settle quote through the PoolManager (routers pull
/// the swapper's quote via Permit2/SETTLE_ALL; the pool currency IS the
/// quote token, never CurrencyLibrary.ADDRESS_ZERO). Sells pay quote out
/// through TAKE_ALL. claimCreatorFees / claimRefund / withdrawPlatformFees
/// pay quote via poolManager.take (an ERC20 transfer by the PM). The dev
/// buy inside create() pulls quote from the creator by transferFrom
/// (one-time ERC20 allowance to this hook) and settles it into the PM.
/// No .call{value:} exists anywhere; create() is not payable; there is
/// no receive() — any native ETH sent here reverts.
/// (b) CURRENCY ORDERING: native ETH was always currency0; an ERC20 quote
/// sorts against each coin address dynamically. The hook derives
/// coinIs0 per pool (coin < QUOTE_TOKEN) and EVERY math site honors it:
/// curve pricing, fee capture side, first-hour cap clamp, graduation
/// trigger direction, graduation reshape, quoter views. Both
/// orientations' constants are precomputed in the constructor.
/// (c) UNITS + CURVE CONSTANTS: all raise/fee/premium/ledger amounts are
/// quote units at QUOTE_DECIMALS. The curve's tick range and liquidity
/// are DERIVED IN THE CONSTRUCTOR from (gradRaise, CURVE_SUPPLY) using
/// the exact v1/v2 boundary conditions (derivation below) — the v2
/// literals [176620, 204350] are ETH/18dp specific and are NOT copied.
/// (d) TICKS: the graduated full-range position bounds stay the spacing-
/// aligned full range [-887270, 887270]; the CURVE range is computed
/// via TickMath from the derived prices and the ordering, aligned to
/// tickSpacing 10.
///
/// Everything else is byte-for-byte v2.1 semantics: 1% fee uniform 70/30
/// creator/platform with the odd unit to the creator, anti-snipe decay,
/// block+1, 2% first-hour fill-to-cap + refund waterfall, 90-minute ticker
/// cooldown, in-place graduation, permanent lock, and the v1-named events
/// (CoinCreated / Trade / Graduated / FeeRecipientChanged ...) with UNCHANGED
/// field names — the Trade fields still read ethGross/ethNet and the Pool
/// struct still reads raisedWei for indexer/terminal ABI compatibility, but
/// the VALUES they carry are quote units at QUOTE_DECIMALS.
///
/// CURVE CONSTANT DERIVATION (the same boundary conditions the v2 curve
/// documents in docs/research/V3_RESEARCH_CURVEDESIGN.md §1.3):
///
/// The curve is a constant-product AMM with virtual reserves (x0, y0)
/// (x = quote, y = coin, both in RAW units), selling S = CURVE_SUPPLY
/// tokens for a total raise of R = gradRaise, chosen so that
/// (1) selling exactly S tokens raises exactly R: R = x0*S/(y0-S)
/// (2) the price moves exactly 16x start->graduation (4x in sqrt):
/// (y0/(y0-S)) * ((x0+R)/x0) = 16
/// By constant product (x0+R)/x0 == y0/(y0-S), so (2) gives y0/(y0-S) = 4,
/// hence y0 = (4/3)*S and from (1) x0 = R/3.
/// (v2 sanity: R = 4.275 ETH gives x0 = 1.425 ETH, y0 = 1,066,666,667
/// tokens — exactly the v1 constants the research doc re-derived.)
///
/// Price start (coin per quote, currency1/currency0 with quote = currency0):
/// Pstart = y0/x0 = 4S/R Pgrad = Pstart/16 = S/(4R)
/// sqrtPstartX96 = 2*sqrt(S/R)*2^96 sqrtPgradX96 = sqrtPstartX96/4
/// Both edges are converted to ticks (TickMath.getTickAtSqrtPrice), rounded
/// to the NEAREST tickSpacing-10 multiple — the exact alignment discipline
/// that produced v2's [176620, 204350] from the derived 176619.27/204346.54.
/// The canonical curve constants are then re-read from the ALIGNED ticks
/// (getSqrtPriceAtTick) and L is sized so the position holds exactly
/// CURVE_SUPPLY tokens at the start edge; the token side is therefore exact
/// by construction and the ≤5-tick alignment shifts land in the realized
/// raise (≤ ~5 bps, the same tolerance class as v2's ~4 bps).
///
/// When the coin sorts BELOW the quote (coinIs0), price = quote/coin is the
/// exact reciprocal, so the aligned ticks NEGATE: the curve runs from
/// -tickStart (init, all tokens) UP to -tickGrad (graduation). Both
/// orientations' sqrt prices and liquidity are precomputed as immutables.
///
/// Vendored dependency pins (see contracts/lib/):
/// v4-periphery commit 3779387e5d296f39df543d23524b050f89a62917
/// (last revision carrying src/utils/BaseHook.sol + HookMiner,
/// parent of PR #510 which moved hooks out of the repo)
/// v4-core commit 59d3ecf53afa9264a16bba0e38f4c5d2231f80bc
/// (tag v4.0.0 + 12, the v4-periphery pinned submodule; matches
/// the deployed PoolManager generation on chain 4663)
///
/// HOOK ADDRESS MINING: the low 14 bits of this contract's address MUST equal
/// 0x28CC = BEFORE_INITIALIZE | BEFORE_ADD_LIQUIDITY | BEFORE_SWAP | AFTER_SWAP
/// | BEFORE_SWAP_RETURNS_DELTA | AFTER_SWAP_RETURNS_DELTA
/// = 1<<13 | 1<<11 | 1<<7 | 1<<6 | 1<<3 | 1<<2.
/// Deployment goes through the canonical CREATE2 deployer
/// (0x4e59b44847b379578588920cA78FbF26c0B4956C, live on chain 4663) with a
/// salt found by an in-script first-flag-satisfying scan — see
/// script/DeployV2Q.s.sol, which asserts
/// uint160(address(hook)) & Hooks.ALL_HOOK_MASK == 0x28CC before anything else.
/// The BaseHook constructor additionally runs Hooks.validateHookPermissions.
///
/// V4-CORE SEMANTICS NOTE (verified against the vendored source, differs from
/// parts of docs/research/V4_DESIGN.md): every Hooks library wrapper no-ops
/// when msg.sender == the hook itself ("noSelfCall"). Consequences:
/// - create()'s own initialize/modifyLiquidity/swap calls NEVER re-enter the
/// hook callbacks, so the spec's `_creating`/`_creatingCoin` transient
/// gating is dead code. beforeInitialize instead reverts UNCONDITIONALLY
/// (Flaunch shape): no pool that is not created by create() can ever
/// attach this hook, and create()'s own initialize skips the callback.
/// - the dev buy cannot be metered by beforeSwap/afterSwap; its fee, cap
/// clamp, ledger credit and Trade event are implemented inline in the
/// create() unlock callback with identical arithmetic.
/// - graduation's inner swap/modifyLiquidity never re-trigger callbacks, so
/// no `_inGraduation` transient is needed; `graduated = true` is still set
/// first for idempotence.
///
/// QUOTE ISSUER PAUSE (disclosed product consequence, not a bug): the quote
/// issuer (USDG: Paxos; stocks: Robinhood Europe UAB) can pause its token.
/// A paused quote freezes this lane's trading — buys, sells, dev buys, fee
/// claims and refund claims revert at the token — and nothing in the hook's
/// ledgers or the pool corrupts; everything resumes when the token unpauses.
/// FEE-ON-TRANSFER quote tokens are NOT supported: the dev-buy pull measures
/// the received amount and reverts on any shortfall, and router settlement of
/// a skimming token fails inside the PoolManager (CurrencyNotSettled). USDG
/// is not fee-on-transfer.
/// ---------------------------------------------------------------------------
import {BaseHook} from "v4-periphery/src/utils/BaseHook.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";
import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {PoolId} from "@uniswap/v4-core/src/types/PoolId.sol";
import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol";
import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
import {
BeforeSwapDelta, BeforeSwapDeltaLibrary, toBeforeSwapDelta
} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol";
import {ModifyLiquidityParams, SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
import {LPFeeLibrary} from "@uniswap/v4-core/src/libraries/LPFeeLibrary.sol";
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
import {SqrtPriceMath} from "@uniswap/v4-core/src/libraries/SqrtPriceMath.sol";
import {FullMath} from "@uniswap/v4-core/src/libraries/FullMath.sol";
import {FixedPoint96} from "@uniswap/v4-core/src/libraries/FixedPoint96.sol";
import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
import {LiquidityAmounts} from "v4-periphery/src/libraries/LiquidityAmounts.sol";
import {IERC20Minimal} from "@uniswap/v4-core/src/interfaces/external/IERC20Minimal.sol";
import {PmavCoin} from "../PmavCoin.sol";
/// v2.2 GENERATION (docs/V22_HOOK_GENERATION_SPEC.md, decisions locked
/// 2026-07-20). Derived from the audited v2q source; every diff is one of:
/// A. name+ticker PAIR cooldown (was ticker only)
/// B. two step CTO: propose + 12h veto window, inactivity floor 36h
/// C. opt in hookData refund beneficiary (pull only; empty = v2q exact)
/// D. completing buy fee clamp (fee charged on USED gross, audit L1)
/// L. PERMANENT 2% wallet cap (was first hour only): net position bought
/// through this pool per tx.origin, forever; sells free capacity;
/// curve buys fill to cap, post graduation over cap buys revert
/// M. per coin anti snipe ON/OFF chosen by the creator at create()
/// I3 DISCLOSURE (audit): the quote is an ISSUER CONTROLLED token. The
/// issuer can pause transfers or blocklist addresses at any time; a paused
/// quote freezes trading on every coin in this lane (curve included) until
/// unpaused, and a blocklisted fee recipient's accrued claims stay frozen
/// until unblocked. Funds are never lost by this hook; the native ETH lane
/// is unaffected by any quote issuer action.
///
/// @title PmavHookQuoted (v2.2) — the pmav.fun quoted-curve launchpad: factory +
/// Uniswap V4 hook + fee bank, curve denominated in an immutable ERC20
/// @notice Launch a coin for just gas. Every coin: 1B fixed supply, no taxes,
/// no owner. Coins trade from transaction one as a Uniswap V4 pool
/// PAIRED WITH THE QUOTE TOKEN (USDG first) on the canonical
/// PoolManager; the bonding curve is a single concentrated position
/// owned by this hook whose range is derived from gradRaise at
/// deployment and reproduces the v1 virtual-reserve curve law. When
/// the curve sells its 800M tokens the coin graduates IN THE SAME
/// POOL: the curve position is removed and the raise + the reserved
/// 200M tokens are minted as a permanently locked full-range position.
/// No migration, no locker contract — this hook simply has no code
/// path that removes the full-range position.
///
/// Fair-launch rules enforced by the pool itself (router-proof):
/// - trading opens the block AFTER creation (only the dev's own
/// launch buy executes in the creation block)
/// - first hour: no wallet (tx.origin) buys more than 2% of supply
/// (20M) cumulatively — over-cap buys FILL TO CAP and auto-refund
/// the surplus in the same transaction; never a failed buy. If
/// the PoolManager's quote float ever cannot cover the instant
/// refund (settle-after router, young deployment), the surplus
/// queues for a one-click claimRefund() and the buy STILL succeeds
/// - decaying anti-snipe premium on buys (split 70/30 like the base)
/// - 1% fee on curve trades, split 70% creator / 30% platform
/// - sells are NEVER capped, taxed extra, or restricted. Ever.
contract PmavHookQuoted is BaseHook, IUnlockCallback {
using StateLibrary for IPoolManager;
// ---------------------------------------------------------------- consts
uint256 public constant TOTAL_SUPPLY = 1_000_000_000e18;
uint256 public constant CURVE_SUPPLY = 800_000_000e18; // sold on the curve
uint256 public constant LP_SUPPLY = 200_000_000e18; // reserved for the full-range position
/// v2.2: the 2% cap is PERMANENT (net tokens bought through this pool
/// per tx.origin, sells free capacity). No time window exists anymore.
uint256 public constant MAX_WALLET = 20_000_000e18; // 2% of supply
uint256 public constant FEE_BPS = 100; // 1% of gross QUOTE per trade
uint256 public constant ANTI_SNIPE_MAX_BPS = 2_500;
uint256 public constant ANTI_SNIPE_MAX_WINDOW = 120;
uint256 public constant CTO_INACTIVITY = 36 hours; // v2.2 (founder 2026-07-20, was 7 days)
uint256 public constant CTO_VETO_DELAY = 12 hours; // v2.2: public notice window, one claim vetoes
/// @notice v2.2: a just-used exact (name, ticker) PAIR cannot be launched
/// again for this long, enforced IN CONSENSUS. The same ticker
/// under a DIFFERENT name is allowed at any time (locked founder
/// decision; anti exact-vamp only). A hard constant, not
/// owner-tunable.
uint256 public constant PAIR_COOLDOWN = 90 minutes;
uint24 public constant GRADUATED_LP_FEE = 10_000; // 1% in pips (< MAX_LP_FEE 1_000_000)
int24 public constant FULL_TICK_LOWER = -887_270;
int24 public constant FULL_TICK_UPPER = 887_270;
int24 public constant TICK_SPACING = 10;
/// gas floor for the completing buy: revert loudly rather than let
/// estimators settle on a path where graduation cannot execute.
/// PROVENANCE (v2q re-measure, INTENTIONAL DIFF vs v2.1's 4_200_000):
/// the QUOTED graduation step measured ~4,476,504 gas on COLD chain-4663
/// mainnet state at fork block 10,090,000 (graduating buy minus a matched
/// 99.5%-of-curve non-graduating buy —
/// ForkGraduationQ.t.sol::test_graduation_step_gas_vs_floor), about 650k
/// above the v2 native measurement of ~3,823,356. Floor = measured cost
/// + ~11% headroom, the same F1 discipline that set v2's constant; the
/// fork test asserts the floor stays above the measured step.
uint256 public constant GRADUATION_GAS = 5_300_000; // v2.2: F1 re-measure 4,604,569 at fork block 15M; 5.0M was only +8.6% (rule: measured +10%)
address internal constant DEAD = 0x000000000000000000000000000000000000dEaD;
// ------------------------------------------------------ quote immutables
/// the immutable quote ERC20 the whole lane is denominated in (USDG for
/// the Phase 1 deployment; per-stock tokens for Phase 2 deployments of
/// this same artifact). Fixed forever at deployment.
address public immutable QUOTE_TOKEN;
/// the quote token's decimals, validated 6..18 (and cross-checked against
/// the token's own decimals() when it exposes one). INFORMATIONAL for
/// indexers/UIs: on-chain math runs entirely in raw units.
uint8 public immutable QUOTE_DECIMALS;
/// the graduation raise in RAW quote units (Phase 1: 38_000e6 = $38k
/// USDG). The curve constants below derive from it; unlike the ETH lane
/// the raise never drifts with ETH price.
uint256 public immutable GRAD_RAISE;
/// ERC6909 claim id of the quote currency on the PoolManager
/// (CurrencyLibrary: id == uint160 of the token address)
uint256 internal immutable QUOTE_ID;
// ---------------------------------------- derived curve constants, per
// ordering. "_Q0" = quote is currency0 (coin address > quote address):
// price = coin/quote, the curve walks DOWN from TICK_UPPER_Q0 to
// TICK_LOWER_Q0 exactly like v2's ETH pools. "_C0" = coin is currency0:
// price = quote/coin, ticks negate, the curve walks UP from -TICK_UPPER_Q0
// to -TICK_LOWER_Q0.
int24 public immutable TICK_LOWER_Q0; // graduation tick (quote = currency0)
int24 public immutable TICK_UPPER_Q0; // curve start / pool init tick (quote = currency0)
/// derived once at deploy from TickMath — bit-exact with the pool's math
uint160 public immutable SQRT_START_Q0; // sqrtPrice at TICK_UPPER_Q0
uint160 public immutable SQRT_GRAD_Q0; // sqrtPrice at TICK_LOWER_Q0
uint160 public immutable SQRT_START_C0; // sqrtPrice at -TICK_UPPER_Q0
uint160 public immutable SQRT_GRAD_C0; // sqrtPrice at -TICK_LOWER_Q0
uint160 internal immutable SQRT_FULL_LOWER;
uint160 internal immutable SQRT_FULL_UPPER;
/// curve position liquidity per ordering: L such that the position holds
/// exactly the 800M curve tokens at the start edge (the token side is
/// currency1 when quote is currency0, currency0 otherwise — the two L
/// values differ only by the helpers' rounding)
uint128 public immutable L_CURVE_Q0;
uint128 public immutable L_CURVE_C0;
// ---------------------------------------------------------------- state
struct Pool {
address coin;
address feeRecipient;
uint64 launchBlock;
uint64 launchTime;
uint16 snipePeakBps; // frozen at create from factory defaults
uint16 snipeWindow; // seconds; frozen at create
bool graduated;
uint128 tokensSold; // informational + invariant cross-check
uint128 raisedWei; // RAW QUOTE UNITS (name kept for ABI/indexer parity)
}
mapping(PoolId => Pool) public pools;
mapping(address => PoolId) public poolOf; // coin => pool id
address[] public allCoins;
/// cumulative first-hour purchases: pool => tx.origin => tokens bought
/// v2.2 BALANCE BASED 2% cap (founder L3, 2026-07-20): NO persistent
/// ledger. The cap is a live snapshot of tx.origin's holding — selling
/// OR transferring tokens away frees the allowance at once. Only
/// transient per-tx state exists (see the cap helpers below).
/// claimable creator fees, keyed by recipient (QUOTE raw units, pull;
/// backed by the hook's ERC6909 quote claims on the PoolManager)
mapping(address => uint256) public creatorFees;
uint256 public platformFees;
/// queued over-cap refunds (QUOTE raw units, pull; backed by the hook's
/// ERC6909 claims like the fee ledgers). Ruling 19.B waterfall branch 2:
/// written ONLY when a settle-after router hits a PoolManager whose quote
/// float cannot cover the instant take() — expected rarely, and only
/// while the lane is young.
mapping(address => uint256) public refunds;
/// CTO rail, ported byte-for-byte from V1 (see Pmav.sol for the doctrine):
/// an ACTIVE dev can never have their fee stream seized; activity =
/// per-coin steward actions + the recipient claiming fees anywhere.
mapping(address => uint256) public lastActivity; // per-coin steward action
mapping(address => uint256) public lastClaim; // per-recipient claim time
/// one-way fee recipient lock (locked coins are CTO-immune)
mapping(address => bool) public feeRecipientLocked;
/// v2.2: last launch timestamp per case-insensitive (name, ticker) PAIR
/// hash (hash of hashes, collision unambiguous). create() reverts while
/// an exact pair is inside its PAIR_COOLDOWN window, in consensus, no
/// matter what frontend (or none) the caller uses.
mapping(bytes32 => uint64) public pairLastLaunch;
/// v2.2 two step CTO: pending proposal per coin (zeroed on execute; a
/// re-propose overwrites). Any dev liveness signal newer than proposedAt
/// cancels the execute — no silent seizure path exists.
struct CtoProposal {
address newRecipient;
uint64 proposedAt;
}
mapping(address => CtoProposal) public ctoProposals;
/// anti-snipe FACTORY DEFAULTS: copied into each pool at create() and
/// frozen there. Owner tuning affects FUTURE launches only, within the
/// hard caps — no function can change a live pool's swap behavior.
uint256 public antiSnipePeakBps = 1_500; // 15%
uint256 public antiSnipeWindow = 30; // seconds
address public owner;
uint256 private _lock = 1;
// -------------------------------------------------------- transient state
/// swap reentrancy lock (risk register #17: EIP-7702 delegated EOA — or a
/// quote token with transfer callbacks — can run code on the refund
/// take() and try to re-enter the pool mid-callback)
bool private transient _swapLock;
/// fee credited / refund taken in beforeSwap, consumed by afterSwap for
/// the Trade event (reset at the top of every beforeSwap)
uint256 private transient _pendingFee;
uint256 private transient _pendingRefund;
// ---------------------------------------------------------------- events
// v1 names AND field names preserved — the indexer consumes these
// unchanged (§14). The eth* fields carry QUOTE RAW UNITS on this variant.
event CoinCreated(
address indexed token,
bytes32 indexed poolId,
address indexed creator,
address feeRecipient,
string name,
string symbol,
string metadata
);
event Trade(
address indexed token,
address indexed trader,
bool isBuy,
uint256 ethGross,
uint256 ethNet,
uint256 tokenAmount,
uint160 sqrtPriceX96,
uint128 tokensSold,
uint128 raisedWei
);
event Graduated(address indexed token, bytes32 indexed poolId, uint256 ethInPool, uint256 tokensInPool);
event FeeRecipientChanged(address indexed token, address indexed oldRecipient, address indexed newRecipient);
event CtoRedirect(address indexed token, address indexed oldRecipient, address indexed newRecipient);
event CtoRedirectProposed(
address indexed token, address indexed oldRecipient, address indexed newRecipient, uint256 executableAt
);
event AntiSnipeOff(address indexed token);
event FeeRecipientLockedEvent(address indexed token, address indexed recipient);
event CreatorFeesClaimed(address indexed recipient, uint256 amount);
event RefundQueued(address indexed token, address indexed to, uint256 amount);
event RefundClaimed(address indexed to, uint256 amount);
event PlatformFeesWithdrawn(address indexed to, uint256 amount);
event PoolFeesCollected(
address indexed token,
address indexed creatorRecipient,
uint256 creatorWei,
uint256 creatorTokens,
uint256 platformWei,
uint256 platformTokens
);
event AntiSnipeChanged(uint256 peakBps, uint256 window);
event OwnerChanged(address indexed oldOwner, address indexed newOwner);
// ------------------------------------------------------------- modifiers
modifier nonReentrant() {
require(_lock == 1, "reentrancy");
_lock = 2;
_;
_lock = 1;
}
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
constructor(IPoolManager _manager, address _owner, address _quoteToken, uint8 _quoteDecimals, uint256 _gradRaise)
BaseHook(_manager)
{
require(_owner != address(0), "zero addr");
require(_quoteToken != address(0) && _quoteToken.code.length > 0, "quote not a contract");
require(_quoteDecimals >= 6 && _quoteDecimals <= 18, "quote decimals");
require(_gradRaise > 0, "zero raise");
// deploy-time typo guard: when the quote exposes decimals() it must
// agree with the argument. Tolerant staticcall: a quote without a
// decimals() getter (none planned) simply skips the cross-check.
(bool ok, bytes memory ret) = _quoteToken.staticcall(abi.encodeWithSignature("decimals()"));
if (ok && ret.length >= 32) {
require(abi.decode(ret, (uint256)) == _quoteDecimals, "decimals mismatch");
}
owner = _owner;
QUOTE_TOKEN = _quoteToken;
QUOTE_DECIMALS = _quoteDecimals;
GRAD_RAISE = _gradRaise;
QUOTE_ID = uint256(uint160(_quoteToken));
// ---- curve constant derivation (see the header derivation note) ----
// x0 = R/3, y0 = 4S/3 => Pstart = 4S/R, Pgrad = S/(4R) (coin/quote).
// sqrt(S/R)*2^96, computed as sqrt(floor(S*2^96/R)) << 48 — the floor
// before the sqrt and the integer sqrt cost < 1 part in 1e12 across
// the whole supported (decimals, gradRaise) envelope, far inside the
// tick alignment granularity (a tick is 1 part in 1e4).
uint256 sqrtSRX96 = _sqrt(FullMath.mulDiv(CURVE_SUPPLY, FixedPoint96.Q96, _gradRaise)) << 48;
uint256 sqrtStartExact = sqrtSRX96 * 2; // sqrt(4S/R)*2^96 — the curve start (quote = currency0)
uint256 sqrtGradExact = sqrtSRX96 / 2; // sqrt(S/(4R))*2^96 — 16x price, 4x sqrt below start
// both edges must live strictly inside V4's price space AND inside the
// graduated full range (so the graduation reshape always has room)
require(
sqrtGradExact > TickMath.MIN_SQRT_PRICE && sqrtStartExact < TickMath.MAX_SQRT_PRICE,
"raise out of range"
);
int24 tickStart = _alignNearest(TickMath.getTickAtSqrtPrice(uint160(sqrtStartExact)));
int24 tickGrad = _alignNearest(TickMath.getTickAtSqrtPrice(uint160(sqrtGradExact)));
require(tickGrad > FULL_TICK_LOWER && tickStart < FULL_TICK_UPPER, "raise out of range");
require(tickGrad < tickStart, "degenerate curve"); // 16x span: always ~27,730 ticks
TICK_LOWER_Q0 = tickGrad;
TICK_UPPER_Q0 = tickStart;
SQRT_START_Q0 = TickMath.getSqrtPriceAtTick(tickStart);
SQRT_GRAD_Q0 = TickMath.getSqrtPriceAtTick(tickGrad);
// coin-is-currency0 orientation: price is the exact reciprocal, ticks
// negate (negating a multiple of 10 stays aligned)
SQRT_START_C0 = TickMath.getSqrtPriceAtTick(-tickStart);
SQRT_GRAD_C0 = TickMath.getSqrtPriceAtTick(-tickGrad);
SQRT_FULL_LOWER = TickMath.getSqrtPriceAtTick(FULL_TICK_LOWER);
SQRT_FULL_UPPER = TickMath.getSqrtPriceAtTick(FULL_TICK_UPPER);
// L such that the position holds exactly the 800M curve tokens at the
// start edge (pool initializes there: tokens only). Tokens are
// currency1 in the Q0 orientation, currency0 in the C0 orientation.
L_CURVE_Q0 = LiquidityAmounts.getLiquidityForAmount1(SQRT_GRAD_Q0, SQRT_START_Q0, CURVE_SUPPLY);
L_CURVE_C0 = LiquidityAmounts.getLiquidityForAmount0(SQRT_START_C0, SQRT_GRAD_C0, CURVE_SUPPLY);
require(L_CURVE_Q0 > 0 && L_CURVE_C0 > 0, "zero curve liquidity");
}
// ---------------------------------------------------------- hook wiring
function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
return Hooks.Permissions({
beforeInitialize: true, // gate: only create() pools may attach us
afterInitialize: false,
beforeAddLiquidity: true, // gate: only hook liquidity during curve
afterAddLiquidity: false,
beforeRemoveLiquidity: false, // no third-party curve LP exists to gate
afterRemoveLiquidity: false,
beforeSwap: true, // block+1, cap clamp, quote-specified fee
afterSwap: true, // cap ledger, quote-unspecified fee, graduation, Trade
beforeDonate: false,
afterDonate: false,
beforeSwapReturnDelta: true,
afterSwapReturnDelta: true,
afterAddLiquidityReturnDelta: false,
afterRemoveLiquidityReturnDelta: false
});
}
/// Rogue-pool defense: nobody can initialize ANY pool that references this
/// hook. Our own create() initializes via poolManager with the hook itself
/// as msg.sender, which skips this callback entirely (core noSelfCall).
function _beforeInitialize(address, PoolKey calldata, uint160) internal pure override returns (bytes4) {
revert("pmav: pools only via create()");
}
/// During the curve only the hook may add liquidity (third-party liquidity
/// inside the curve range would absorb buys and desync graduation
/// economics). The hook's own adds skip this callback (noSelfCall); after
/// graduation anyone may LP (v1 parity: community LPs on the V3 pool).
function _beforeAddLiquidity(address, PoolKey calldata key, ModifyLiquidityParams calldata, bytes calldata)
internal
view
override
returns (bytes4)
{
require(pools[key.toId()].graduated, "curve phase");
return BaseHook.beforeAddLiquidity.selector;
}
// ------------------------------------------------------------ beforeSwap
function _beforeSwap(address, PoolKey calldata key, SwapParams calldata params, bytes calldata hookData)
internal
override
returns (bytes4, BeforeSwapDelta, uint24)
{
PoolId id = key.toId();
Pool storage p = pools[id];
// graduated: PURE PASSTHROUGH — no caps, no premium, no deltas.
// (graduation's own inner swap never reaches here: noSelfCall.)
if (p.graduated) return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0);
require(!_swapLock, "reentrancy");
_swapLock = true;
_pendingFee = 0;
_pendingRefund = 0;
// (a) block+1 — the Noxa killer feature, enforced at pool level.
// (the dev buy never reaches this: hook-initiated swaps skip callbacks)
require(block.number > p.launchBlock, "trading opens next block");
// ordering (delta (b) in the header): a buy is quote-in, which is
// zeroForOne when the quote is currency0 and oneForZero otherwise
bool coinIs0 = Currency.unwrap(key.currency0) != QUOTE_TOKEN;
bool isBuy = params.zeroForOne != coinIs0;
// decaying anti-snipe premium: buys only, frozen params, base 1% always
uint256 feeBps = FEE_BPS + (isBuy ? _antiSnipeBps(p) : 0);
// (b) v2.2 PERMANENT 2% cap — FILL TO CAP + AUTO REFUND on the curve,
// never a failed buy. No time window: the cap is forever.
uint256 refund = 0;
if (isBuy) {
uint256 held0 = _capHeld(id, p.coin, tx.origin);
uint256 capLeft = held0 >= MAX_WALLET ? 0 : MAX_WALLET - held0;
require(capLeft > 0, "wallet cap: 2% max");
if (params.amountSpecified < 0) {
// buy exactIn (the universal path): clamp to the quote the cap allows
uint256 maxGross = _maxGrossForCap(id, coinIs0, capLeft, feeBps);
require(maxGross > 0, "wallet cap: 2% max");
uint256 gross = uint256(-params.amountSpecified);
if (gross > maxGross) refund = gross - maxGross;
} else {
// buy exactOut cannot be partially filled (the router TAKEs the
// exact requested output; no refund exists to give)
require(uint256(params.amountSpecified) <= capLeft, "wallet cap: 2% max");
}
}
// (c) quote-denominated fee, taken on the SPECIFIED side when the
// quote is specified (buy exactIn / sell exactOut). Otherwise
// afterSwap takes it.
if (isBuy == (params.amountSpecified < 0)) {
uint256 quoteGross = _abs(params.amountSpecified) - refund;
// v2.2 (audit L1): on a buy exactIn the fee base is bounded by the
// gross the CURVE can actually consume to graduation (per
// orientation maxNet rounded UP, grossed up rounding UP: the fee
// never undercharges and an oversized completing buy pays fee on
// the USED gross). Sell exactOut is untouched (audit L4).
if (isBuy) {
uint256 maxNet = coinIs0
? SqrtPriceMath.getAmount1Delta(_effSqrtP(id, coinIs0), SQRT_GRAD_C0, L_CURVE_C0, true)
: SqrtPriceMath.getAmount0Delta(SQRT_GRAD_Q0, _effSqrtP(id, coinIs0), L_CURVE_Q0, true);
uint256 maxFeeGross = FullMath.mulDivRoundingUp(maxNet, 10_000, 10_000 - feeBps);
if (quoteGross > maxFeeGross) quoteGross = maxFeeGross;
}
uint256 fee = quoteGross * feeBps / 10_000;
if (fee > 0) {
poolManager.mint(address(this), QUOTE_ID, fee); // ERC6909 claim
_credit(p, fee);
}
if (refund > 0) {
// refund waterfall (ruling 19.B). Both branches charge the
// swapper identically through the returned hook delta; the
// buy SUCCEEDS unconditionally, at any PM balance, under
// settle-first AND settle-after routers.
// v2.2 (audit L2): 32 byte hookData naming a nonzero
// beneficiary routes the surplus to refunds[beneficiary],
// PULL ONLY. Garbage hookData is IGNORED, never a revert.
address beneficiary = _refundBeneficiary(hookData);
if (beneficiary != address(0)) {
poolManager.mint(address(this), QUOTE_ID, refund);
refunds[beneficiary] += refund;
emit RefundQueued(p.coin, beneficiary, refund);
} else if (IERC20Minimal(QUOTE_TOKEN).balanceOf(address(poolManager)) >= refund) {
// 1: instant auto-refund, real quote tokens, straight back
// to the signer (the common path once the lane has
// graduated pools holding quote float)
poolManager.take(Currency.wrap(QUOTE_TOKEN), tx.origin, refund);
} else {
// 2: floatless PM under a settle-after router — queue the
// surplus as the hook's own ERC6909 claim (float-free
// bookkeeping, backed by the buyer's own settle moments
// later); pull via claimRefund()
poolManager.mint(address(this), QUOTE_ID, refund);
refunds[tx.origin] += refund;
emit RefundQueued(p.coin, tx.origin, refund);
}
}
_pendingFee = fee;
_pendingRefund = refund;
uint256 hookTake = fee + refund;
if (hookTake > 0) {
return (BaseHook.beforeSwap.selector, toBeforeSwapDelta(int128(uint128(hookTake)), 0), 0);
}
}
return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0);
}
// ------------------------------------------------------------- afterSwap
/// `delta` is the raw pool swap result from the swapper's perspective
/// (positive = swapper receives), BEFORE hook deltas are charged to the
/// swapper. Order of operations is load-bearing: fee -> cap -> Trade ->
/// graduation.
function _afterSwap(address, PoolKey calldata key, SwapParams calldata params, BalanceDelta delta, bytes calldata hookData)
internal
override
returns (bytes4, int128)
{
PoolId id = key.toId();
Pool storage p = pools[id];
bool coinIs0 = Currency.unwrap(key.currency0) != QUOTE_TOKEN;
if (p.graduated) {
// v2.2: pricing and fees stay PURE PASSTHROUGH, but the PERMANENT
// 2% cap is enforced here forever — and an over cap exactIn buy
// FILLS TO CAP instead of reverting (founder 2026-07-20): the
// excess tokens are charged back through the afterSwap return
// delta (the UNSPECIFIED side of a buy exactIn, either
// orientation), sold back into the pool inside the same unlock
// (noSelfCall), proceeds refunded in QUOTE through the curve's
// exact waterfall. Buy exactOut over cap still reverts; zero
// remaining capacity reverts; sells decrement saturating and
// never revert. Trade ships with the DELIVERED token amount.
// whole graduated branch lives in a helper to keep this frame
// flat (legacy codegen stack room): cap walk, tail unwind, Trade
return (BaseHook.afterSwap.selector, _afterSwapGraduated(key, p, coinIs0, params, delta, hookData));
}
int128 hookDelta = _settleCurveSwap(p, id, coinIs0, params, delta);
// (d) graduation trigger: price-based — the graduation edge is reached
// iff ~all 800M curve tokens are sold. Sells move price AWAY from it.
// Q0 pools walk DOWN to the edge, C0 pools walk UP to it.
if (params.zeroForOne != coinIs0) {
(uint160 sqrtNow,,,) = poolManager.getSlot0(id);
if (coinIs0 ? sqrtNow >= SQRT_GRAD_C0 : sqrtNow <= SQRT_GRAD_Q0) {
require(gasleft() >= GRADUATION_GAS, "graduation buy needs more gas");
_graduate(key, p, id, coinIs0);
}
}
return (BaseHook.afterSwap.selector, hookDelta);
}
/// v2.2 graduated afterSwap: over cap exactIn buys fill to cap (excess
/// clawed via the return delta and unwound + refunded); sells decrement
/// the ledger saturating; the v1-named Trade ships on every swap. Split
/// out of _afterSwap purely for legacy codegen stack room.
function _afterSwapGraduated(
PoolKey calldata key,
Pool storage p,
bool coinIs0,
SwapParams calldata params,
BalanceDelta delta,
bytes calldata hookData
) internal returns (int128 gradHookDelta) {
PoolId id = key.toId();
bool isBuy = params.zeroForOne != coinIs0;
uint256 e = _abs(int256(coinIs0 ? delta.amount1() : delta.amount0()));
uint256 t = _abs(int256(coinIs0 ? delta.amount0() : delta.amount1()));
if (isBuy) {
(t, gradHookDelta) = _gradBuyCap(key, coinIs0, t, params.amountSpecified < 0, hookData);
}
// sells: nothing to record — balanceOf falls, the transient counter
// resets on the buyer's next buy (balance moved)
// gross == net post graduation (hook takes no fee delta); _emitTrade
// folds sqrtPrice + counters, keeping this frame flat
_emitTrade(p, id, isBuy, e, e, t);
}
/// v2.2: the graduated buy side cap walk (split out of _afterSwap for
/// legacy codegen stack room). Returns (tokens DELIVERED, the afterSwap
/// return delta charging the clawed excess). Zero remaining capacity
/// reverts; exactOut over cap reverts; exactIn over cap fills to cap.
function _gradBuyCap(PoolKey calldata key, bool coinIs0, uint256 t, bool exactIn, bytes calldata hookData)
internal
returns (uint256, int128)
{
PoolId id = key.toId();
address coin = pools[id].coin;
uint256 held = _capHeld(id, coin, tx.origin);
require(held < MAX_WALLET, "wallet cap: 2% max");
if (exactIn && held + t > MAX_WALLET) {
uint256 excess = held + t - MAX_WALLET;
_recordCapBuy(id, tx.origin, MAX_WALLET - held); // only the kept fill
_unwindGradExcess(key, coinIs0, excess, hookData);
return (t - excess, int128(uint128(excess)));
}
require(held + t <= MAX_WALLET, "wallet cap: 2% max");
_recordCapBuy(id, tx.origin, t);
return (t, 0);
}
/// v2.2 (founder 2026-07-20): sell the over cap tail of a graduated
/// exactIn buy back into the pool (orientation aware) and refund the
/// QUOTE proceeds through the curve's exact waterfall (beneficiary
/// hookData honored, pull only; else instant push when the PM's quote
/// float covers it, else pull ledger). Dust tails refund nothing.
function _unwindGradExcess(PoolKey calldata key, bool coinIs0, uint256 excess, bytes calldata hookData)
internal
{
address coin = pools[key.toId()].coin;
BalanceDelta sd = poolManager.swap(
key,
SwapParams({
zeroForOne: coinIs0,
amountSpecified: -int256(excess),
sqrtPriceLimitX96: coinIs0 ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1
}),
""
);
uint256 quoteBack = uint256(uint128(coinIs0 ? sd.amount1() : sd.amount0()));
if (quoteBack == 0) return;
address beneficiary = _refundBeneficiary(hookData);
if (beneficiary != address(0)) {
poolManager.mint(address(this), QUOTE_ID, quoteBack);
refunds[beneficiary] += quoteBack;
emit RefundQueued(coin, beneficiary, quoteBack);
} else if (IERC20Minimal(QUOTE_TOKEN).balanceOf(address(poolManager)) >= quoteBack) {
poolManager.take(Currency.wrap(QUOTE_TOKEN), tx.origin, quoteBack);
} else {
poolManager.mint(address(this), QUOTE_ID, quoteBack);
refunds[tx.origin] += quoteBack;
emit RefundQueued(coin, tx.origin, quoteBack);
}
}
/// fee (quote-unspecified quadrants), first-hour ledger, bookkeeping and
/// the Trade event for a curve-phase swap
function _settleCurveSwap(Pool storage p, PoolId id, bool coinIs0, SwapParams calldata params, BalanceDelta delta)
internal
returns (int128 hookDelta)
{
// what the pool actually moved (completing buys may consume less than
// stated: unconsumed input through empty liquidity is never taken)
uint256 quotePool = _abs(int256(coinIs0 ? delta.amount1() : delta.amount0()));
uint256 tokenAmount = _abs(int256(coinIs0 ? delta.amount0() : delta.amount1()));
bool isBuy = params.zeroForOne != coinIs0;
uint256 fee;
if (isBuy == (params.amountSpecified < 0)) {
fee = _pendingFee; // already credited in beforeSwap
} else {
// (b') quote fee when the quote is the UNSPECIFIED currency
// (buy exactOut / sell exactIn)
uint256 feeBps = FEE_BPS + (isBuy ? _antiSnipeBps(p) : 0);
fee = quotePool * feeBps / 10_000;
if (fee > 0) {
poolManager.mint(address(this), QUOTE_ID, fee);
_credit(p, fee);
hookDelta = int128(uint128(fee)); // taken from swapper's quote side
}
}
// (c) first-hour ledger: record the ACTUAL fill (clamping already
// happened in beforeSwap; the require is a belt-and-braces invariant)
if (isBuy) {
_recordCapBuy(id, tx.origin, tokenAmount);
require(_capHeld(id, p.coin, tx.origin) <= MAX_WALLET, "cap invariant");
}
// sells: nothing to record — balanceOf falls; a sell must never revert
_swapLock = false;
// bookkeeping + parity event (indexer contract, §14). For buys the
// pool quote is the net (fee charged on top, v1 exact); for sells it
// is the gross (fee taken out of it).
if (isBuy) {
p.tokensSold += uint128(tokenAmount);
p.raisedWei += uint128(quotePool);
} else {
// SATURATING (correctness audit F-3, defense-in-depth): these are
// informational counters only — the graduation trigger is
// price-based (pool sqrtPrice vs the graduation edge), never
// counter-based, so flooring at 0 changes no economics. Underflow
// is already unreachable by conservation (a sell can never return
// more tokens/quote to the curve than the curve ever sold), but a
// sell must NEVER revert on an arithmetic underflow: a reverting
// sell is the #1 honeypot signature, and "sells never revert, ever"
// is a hard invariant of our scanner-clean posture. Make it
// structurally guaranteed rather than conservation-guaranteed.
uint128 t = uint128(tokenAmount);
uint128 e = uint128(quotePool);
p.tokensSold = p.tokensSold > t ? p.tokensSold - t : 0;
p.raisedWei = p.raisedWei > e ? p.raisedWei - e : 0;
}
_emitTrade(
p,
id,
isBuy,
isBuy ? quotePool + fee : quotePool,
// saturate: a sell-exactOut demanding more quote than the curve
// holds partially fills; the credited fee can exceed the fill
isBuy ? quotePool : (quotePool > fee ? quotePool - fee : 0),
tokenAmount
);
}
// ------------------------------------------------------------ graduation
/// Runs INSIDE the completing buy's afterSwap, all in one transaction,
/// inside the router's active unlock. Same poolId forever: no migration,
/// no pool switch, no window where the coin is untradeable. The inner
/// swap/modifyLiquidity calls skip all hook callbacks (noSelfCall).
function _graduate(PoolKey calldata key, Pool storage p, PoolId id, bool coinIs0) internal {
p.graduated = true; // set FIRST (idempotence)
uint160 sqrtGrad = coinIs0 ? SQRT_GRAD_C0 : SQRT_GRAD_Q0;
(int24 tickLo, int24 tickHi) = coinIs0
? (-TICK_UPPER_Q0, -TICK_LOWER_Q0)
: (TICK_LOWER_Q0, TICK_UPPER_Q0);
// 1. remove the whole curve position: hook is credited the raise
// (~GRAD_RAISE quote + any donate() dust as fees) and unsold token
// dust
uint256 quoteAvail;
uint256 tokenCredit;
{
(BalanceDelta d,) = poolManager.modifyLiquidity(
key,
ModifyLiquidityParams({
tickLower: tickLo,
tickUpper: tickHi,
liquidityDelta: -int256(uint256(coinIs0 ? L_CURVE_C0 : L_CURVE_Q0)),
salt: 0
}),
""
);
quoteAvail = uint256(uint128(coinIs0 ? d.amount1() : d.amount0()));
tokenCredit = uint256(uint128(coinIs0 ? d.amount0() : d.amount1()));
}
// 2. repair the boundary overshoot: the completing swap may have walked
// price past the graduation edge through empty liquidity. With zero
// pool liquidity a 1-wei-specified swap walks price back to exactly
// the graduation price at zero cost (input is never consumed).
// Q0 pools overshoot BELOW the edge (repair swaps price UP);
// C0 pools overshoot ABOVE it (repair swaps price DOWN).
{
(uint160 sqrtP,,,) = poolManager.getSlot0(id);
if (coinIs0 ? sqrtP > sqrtGrad : sqrtP < sqrtGrad) {
poolManager.swap(
key, SwapParams({zeroForOne: coinIs0, amountSpecified: -1, sqrtPriceLimitX96: sqrtGrad}), ""
);
}
}
// 3. flip the dynamic LP fee: plain 1% forever (hook takes no deltas
// post-graduation; scanners see a vanilla 1% V4 pool)
poolManager.updateDynamicLPFee(key, GRADUATED_LP_FEE);
// 4. mint the permanently locked full-range position: the entire raise
// + the 200M reserve, amounts computed at the graduation price.
// (-1 wei headroom on both sides: modifyLiquidity rounds owed
// amounts UP, the liquidity helpers round L DOWN — the headroom
// guarantees the owed amounts never exceed what the hook holds.)
uint128 lFull;
{
uint128 lQuote = coinIs0
? LiquidityAmounts.getLiquidityForAmount1(SQRT_FULL_LOWER, sqrtGrad, quoteAvail - 1)
: LiquidityAmounts.getLiquidityForAmount0(sqrtGrad, SQRT_FULL_UPPER, quoteAvail - 1);
uint128 lTok = coinIs0
? LiquidityAmounts.getLiquidityForAmount0(sqrtGrad, SQRT_FULL_UPPER, LP_SUPPLY - 1)
: LiquidityAmounts.getLiquidityForAmount1(SQRT_FULL_LOWER, sqrtGrad, LP_SUPPLY - 1);
lFull = lQuote < lTok ? lQuote : lTok;
}
(BalanceDelta md,) = poolManager.modifyLiquidity(
key,
ModifyLiquidityParams({
tickLower: FULL_TICK_LOWER,
tickUpper: FULL_TICK_UPPER,
liquidityDelta: int256(uint256(lFull)),
salt: 0
}),
""
);
uint256 quoteUsed = uint256(uint128(-(coinIs0 ? md.amount1() : md.amount0())));
uint256 tokUsed = uint256(uint128(-(coinIs0 ? md.amount0() : md.amount1())));
// 5. settle the token side from the hook's 200M reserve (ERC20 custody
// since create), netting the removal's token-dust credit
Currency coinCur = coinIs0 ? key.currency0 : key.currency1;
if (tokUsed > tokenCredit) {
poolManager.sync(coinCur);
require(PmavCoin(p.coin).transfer(address(poolManager), tokUsed - tokenCredit), "transfer");
poolManager.settle();
} else if (tokenCredit > tokUsed) {
// (only reachable via absurd token donations) burn the surplus
poolManager.take(coinCur, DEAD, tokenCredit - tokUsed);
}
// 6. dust sweep, v1 parity: leftover quote units -> platformFees (as
// ERC6909 claims, which also zeroes the hook's remaining positive
// delta); leftover coin dust -> dead address
uint256 quoteDust = quoteAvail - quoteUsed;
if (quoteDust > 0) {
poolManager.mint(address(this), QUOTE_ID, quoteDust);
platformFees += quoteDust;
}
uint256 coinDust = PmavCoin(p.coin).balanceOf(address(this));
if (coinDust > 0) PmavCoin(p.coin).transfer(DEAD, coinDust);
emit Graduated(p.coin, PoolId.unwrap(id), quoteUsed, tokUsed);
}
// ---------------------------------------------------------------- launch
/// @notice Launch a coin. Costs nothing beyond gas; `quoteIn` (optional)
/// is the dev buy in RAW QUOTE UNITS, pulled from the caller by
/// transferFrom (approve this hook first) and executed as the
/// first purchase in the same transaction (candle #1 on every
/// terminal), subject to the same 2% first-hour cap and anti-snipe
/// premium as everyone else. Replaces v2.1's payable msg.value dev
/// buy — the only signature change on this variant.
function create(
string calldata name,
string calldata symbol,
string calldata metadata,
address feeRecipient,
uint256 quoteIn,
uint256 minTokensOut,
bool antiSnipe
) external nonReentrant returns (address token) {
require(bytes(name).length > 0 && bytes(name).length <= 64, "name");
// ticker capped at 12 so every $TICKER stays a clickable X cashtag
require(bytes(symbol).length > 0 && bytes(symbol).length <= 12, "symbol");
require(bytes(metadata).length <= 8192, "metadata too big");
// v2.2 on-chain anti-vamp: the exact (name, ticker) PAIR locks
// protocol-wide for PAIR_COOLDOWN after any launch that uses it
// (helper keeps create()'s stack flat; still no via-ir).
_checkPairCooldown(name, symbol);
// CREATE, not CREATE2: the coin address is unknowable pre-tx, so the
// PoolKey (and pool id) cannot be squatted or front-run. The coin's
// address also decides the pool's currency ordering (delta (b)).
token = address(new PmavCoin(name, symbol)); // 1B minted to the hook
// register + CoinCreated + pool initialize (helper keeps create()'s
// stack flat — this profile compiles without via-ir, the v2.1
// discipline; the extra quoteIn parameter costs the slot v2.1 spent
// on the inline PoolKey local)
_launch(token, feeRecipient == address(0) ? msg.sender : feeRecipient, name, symbol, metadata, antiSnipe);
// dev buy funding: pull the quote up front so the unlock callback can
// settle it
if (quoteIn > 0) _pullDevBuyQuote(quoteIn);
// seed the curve position (+ optional dev buy) inside one unlock
// (poolKeyOf is deterministic, so recomputing it here is exact)
poolManager.unlock(abi.encode(uint8(1), abi.encode(poolKeyOf(token), msg.sender, quoteIn, minTokensOut)));
}
/// @dev registration, the CoinCreated event and the pool initialize for a
/// fresh coin — split out of create() for legacy-codegen stack room.
/// Our own initialize skips beforeInitialize (noSelfCall); every
/// external initialize attempt against this hook reverts there.
function _launch(
address token,
address recipient,
string calldata name,
string calldata symbol,
string calldata metadata,
bool antiSnipe
) internal {
PoolKey memory key = _register(token, recipient, antiSnipe);
emit CoinCreated(token, PoolId.unwrap(key.toId()), msg.sender, recipient, name, symbol, metadata);
if (!antiSnipe) emit AntiSnipeOff(token);
poolManager.initialize(key, token < QUOTE_TOKEN ? SQRT_START_C0 : SQRT_START_Q0);
}
/// @dev pulls the dev-buy quote from the creator by transferFrom.
/// Fee-on-transfer quotes are rejected by measuring the received
/// amount (documented stance: not supported, revert). USDG passes.
function _pullDevBuyQuote(uint256 quoteIn) internal {
uint256 balBefore = IERC20Minimal(QUOTE_TOKEN).balanceOf(address(this));
_safeTransferFrom(QUOTE_TOKEN, msg.sender, address(this), quoteIn);
require(IERC20Minimal(QUOTE_TOKEN).balanceOf(address(this)) - balBefore == quoteIn, "fee on transfer quote");
}
/// @dev v2.2 cooldown gate: case-insensitive over ASCII on the exact
/// (name, ticker) PAIR. First-ever use of a pair (slot 0) always
/// passes, every use re-arms the window. Same ticker + different
/// name NEVER collides here (locked founder decision 2026-07-20).
function _checkPairCooldown(string calldata name, string calldata symbol) internal {
bytes32 ph = _pairHash(name, symbol);
uint64 last = pairLastLaunch[ph];
require(last == 0 || block.timestamp >= uint256(last) + PAIR_COOLDOWN, "name+ticker cooldown");
pairLastLaunch[ph] = uint64(block.timestamp);
}
/// @dev hash of hashes over the ASCII-lowercased name and ticker bytes:
/// keccak(keccak(lowerName) || keccak(lowerTicker)). Fixed width
/// inner hashes make the key COLLISION UNAMBIGUOUS. Non ASCII bytes
/// pass through unchanged (homoglyph lookalikes are a distinct pair
/// on chain and remain a display-layer concern by construction).
function _pairHash(string calldata name, string calldata symbol) internal pure returns (bytes32) {
return keccak256(abi.encodePacked(_lowerHash(bytes(name)), _lowerHash(bytes(symbol))));
}
function _lowerHash(bytes memory b) internal pure returns (bytes32) {
for (uint256 i = 0; i < b.length; i++) {
if (b[i] >= 0x41 && b[i] <= 0x5A) b[i] = bytes1(uint8(b[i]) + 32);
}
return keccak256(b);
}
function _register(address token, address recipient, bool antiSnipe) internal returns (PoolKey memory key) {
key = poolKeyOf(token);
PoolId id = key.toId();
// v2.2: antiSnipe=false freezes a ZERO premium into the pool struct
// (the swap path's _antiSnipeBps already short circuits on window 0)
pools[id] = Pool({
coin: token,
feeRecipient: recipient,
launchBlock: uint64(block.number),
launchTime: uint64(block.timestamp),
snipePeakBps: antiSnipe ? uint16(antiSnipePeakBps) : 0,
snipeWindow: antiSnipe ? uint16(antiSnipeWindow) : 0,
graduated: false,
tokensSold: 0,
raisedWei: 0
});
poolOf[token] = id;
allCoins.push(token);
lastActivity[token] = block.timestamp;
}
function coinCount() external view returns (uint256) {
return allCoins.length;
}
/// canonical PoolKey for a pmav coin — fully deterministic, never stored.
/// The quote ERC20 sorts against the coin address (V4 requires
/// currency0 < currency1), so the ordering is per-coin (delta (b)).
function poolKeyOf(address token) public view returns (PoolKey memory) {
(Currency c0, Currency c1) = token < QUOTE_TOKEN
? (Currency.wrap(token), Currency.wrap(QUOTE_TOKEN))
: (Currency.wrap(QUOTE_TOKEN), Currency.wrap(token));
return PoolKey({
currency0: c0,
currency1: c1,
fee: LPFeeLibrary.DYNAMIC_FEE_FLAG,
tickSpacing: TICK_SPACING,
hooks: this
});
}
/// convenience for tests/UIs: the orientation and curve constants a given
/// coin's pool actually uses
function curveParamsOf(address token)
external
view
returns (bool coinIs0, int24 tickLower, int24 tickUpper, uint160 sqrtStart, uint160 sqrtGrad, uint128 lCurve)
{
coinIs0 = token < QUOTE_TOKEN;
(tickLower, tickUpper) = coinIs0 ? (-TICK_UPPER_Q0, -TICK_LOWER_Q0) : (TICK_LOWER_Q0, TICK_UPPER_Q0);
sqrtStart = coinIs0 ? SQRT_START_C0 : SQRT_START_Q0;
sqrtGrad = coinIs0 ? SQRT_GRAD_C0 : SQRT_GRAD_Q0;
lCurve = coinIs0 ? L_CURVE_C0 : L_CURVE_Q0;
}
// ------------------------------------------------------- unlock callback
/// dispatch for the hook's own unlock operations:
/// 1 = create (seed curve + dev buy), 2 = payout (burn claims + take
/// quote), 3 = collect graduated pool fees
function unlockCallback(bytes calldata data) external onlyPoolManager returns (bytes memory) {
(uint8 action, bytes memory payload) = abi.decode(data, (uint8, bytes));
if (action == 1) {
(PoolKey memory key, address creator, uint256 value, uint256 minTokensOut) =
abi.decode(payload, (PoolKey, address, uint256, uint256));
_createCallback(key, creator, value, minTokensOut);
} else if (action == 2) {
(address to, uint256 amount) = abi.decode(payload, (address, uint256));
poolManager.burn(address(this), QUOTE_ID, amount);
poolManager.take(Currency.wrap(QUOTE_TOKEN), to, amount);
} else if (action == 3) {
(address token) = abi.decode(payload, (address));
_collectCallback(token);
} else {
revert("bad action");
}
return "";
}
function _createCallback(PoolKey memory key, address creator, uint256 value, uint256 minTokensOut) internal {
PoolId id = key.toId();
Pool storage p = pools[id];
address token = p.coin;
bool coinIs0 = token < QUOTE_TOKEN;
(int24 tickLo, int24 tickHi) = coinIs0
? (-TICK_UPPER_Q0, -TICK_LOWER_Q0)
: (TICK_LOWER_Q0, TICK_UPPER_Q0);
// seed the curve: at the start edge the position is 100% tokens
(BalanceDelta d,) = poolManager.modifyLiquidity(
key,
ModifyLiquidityParams({
tickLower: tickLo,
tickUpper: tickHi,
liquidityDelta: int256(uint256(coinIs0 ? L_CURVE_C0 : L_CURVE_Q0)),
salt: 0
}),
""
);
// settle the owed tokens (<= 800M by L construction; no quote owed)
uint256 owedTokens = uint256(uint128(-(coinIs0 ? d.amount0() : d.amount1())));
poolManager.sync(coinIs0 ? key.currency0 : key.currency1);
require(PmavCoin(token).transfer(address(poolManager), owedTokens), "transfer");
poolManager.settle();
// dev buy: a REAL PoolManager swap in the creation block
if (value > 0) _devBuy(key, p, coinIs0, creator, value, minTokensOut);
}
/// Because the hook itself is the swapper, beforeSwap/afterSwap do NOT run
/// (noSelfCall) — fee, premium, cap clamp, ledgers and the Trade event are
/// applied inline here with arithmetic identical to beforeSwap. The quote
/// was already pulled from the creator in create(); it settles into the PM
/// here and any over-cap surplus transfers straight back to the creator.
function _devBuy(
PoolKey memory key,
Pool storage p,
bool coinIs0,
address creator,
uint256 value,
uint256 minTokensOut
) internal {
PoolId id = key.toId();
uint256 feeBps = FEE_BPS + _antiSnipeBps(p); // premium at peak (t = 0)
uint256 gross = value;
uint256 refund = 0;
{
uint256 maxGross = _maxGrossForCap(id, coinIs0, MAX_WALLET, feeBps);
if (gross > maxGross) {
refund = gross - maxGross;
gross = maxGross;
}
}
uint256 fee = gross * feeBps / 10_000;
uint256 net = gross - fee;
require(net > 0, "dust buy");
uint256 tokensOut = _curveBuySwap(key, coinIs0, net);
require(tokensOut >= minTokensOut, "slippage");
require(tokensOut <= MAX_WALLET, "cap invariant");
// settle net (the swap) + fee (real quote tokens into the PoolManager,
// the fee leg immediately re-minted as the hook's ERC6909 fee claim)
poolManager.sync(Currency.wrap(QUOTE_TOKEN));
_safeTransfer(QUOTE_TOKEN, address(poolManager), net + fee);
poolManager.settle();
poolManager.mint(address(this), QUOTE_ID, fee);
_credit(p, fee);
_recordCapBuy(id, tx.origin, tokensOut); // fresh pool: first buy (creator holds 0 coin at create)
p.tokensSold += uint128(tokensOut);
p.raisedWei += uint128(net);
poolManager.take(coinIs0 ? key.currency0 : key.currency1, creator, tokensOut);
_emitTrade(p, id, true, gross, net, tokensOut);
// over-cap surplus: plain ERC20 transfer back (no .call{value}, and a
// standard ERC20 cannot re-enter — the nonReentrant lock guards the
// callback-capable exotic case anyway)
if (refund > 0) _safeTransfer(QUOTE_TOKEN, creator, refund);
}
/// @dev the dev buy's inner pool swap (split out of _devBuy for
/// legacy-codegen stack room): quote in, oriented by the ordering,
/// returns the coin-side fill
function _curveBuySwap(PoolKey memory key, bool coinIs0, uint256 net) internal returns (uint256 tokensOut) {
BalanceDelta sd = poolManager.swap(
key,
SwapParams({
zeroForOne: !coinIs0, // buy: quote in
amountSpecified: -int256(net),
sqrtPriceLimitX96: coinIs0 ? TickMath.MAX_SQRT_PRICE - 1 : TickMath.MIN_SQRT_PRICE + 1
}),
""
);
tokensOut = uint256(uint128(coinIs0 ? sd.amount0() : sd.amount1()));
}
function _emitTrade(Pool storage p, PoolId id, bool isBuy, uint256 gross, uint256 net, uint256 tokens) internal {
(uint160 sqrtNow,,,) = poolManager.getSlot0(id);
emit Trade(p.coin, tx.origin, isBuy, gross, net, tokens, sqrtNow, p.tokensSold, p.raisedWei);
}
// ------------------------------------------------------------------ fees
/// Split of the TOTAL fee (base 1% + any anti-snipe premium): 70% creator /
/// 30% platform, uniform. Platform takes floor(fee * 3 / 10); the creator
/// gets the remainder, so the odd unit rounds to the CREATOR. The premium
/// is NOT special-cased — it splits 70/30 exactly like the base (so the
/// platform also shares in the sniper premium).
function _credit(Pool storage p, uint256 fee) internal {
uint256 platformShare = fee * 3 / 10; // 30% of the total fee (base + premium)
platformFees += platformShare;
creatorFees[p.feeRecipient] += fee - platformShare; // 70%, odd unit to creator
}
function claimCreatorFees() external nonReentrant {
uint256 amount = creatorFees[msg.sender];
require(amount > 0, "nothing to claim");
creatorFees[msg.sender] = 0;
// claiming is the natural "I'm alive" signal: it protects every coin
// paying this recipient from a CTO. only msg.sender can stamp itself.
lastClaim[msg.sender] = block.timestamp;
poolManager.unlock(abi.encode(uint8(2), abi.encode(msg.sender, amount)));
emit CreatorFeesClaimed(msg.sender, amount);
}
/// @notice Pull a refund queued by the ruling-19.B waterfall (branch 2:
/// an over-cap buy through a settle-after router while the
/// PoolManager's quote float could not cover the instant take).
/// Permissionless pull, claimCreatorFees shape: burns the hook's
/// ERC6909 claims and pays real quote tokens — by claim time the
/// buyer's own settle has long since delivered the backing quote.
function claimRefund() external nonReentrant {
uint256 amount = refunds[msg.sender];
require(amount > 0, "nothing to claim");
refunds[msg.sender] = 0;
poolManager.unlock(abi.encode(uint8(2), abi.encode(msg.sender, amount)));
emit RefundClaimed(msg.sender, amount);
}
function withdrawPlatformFees(address to) external onlyOwner nonReentrant {
uint256 amount = platformFees;
require(amount > 0, "nothing to withdraw");
platformFees = 0;
poolManager.unlock(abi.encode(uint8(2), abi.encode(to, amount)));
emit PlatformFeesWithdrawn(to, amount);
}
/// @notice Realize a graduated pool's accrued 1% LP fees from the locked
/// full-range position. Permissionless — anyone pokes (v1 locker
/// parity). Quote side split 70% creator / 30% platform into the
/// pull ledgers (so an exotic recipient can never brick the
/// collect); coin side split the same way and transferred directly
/// (PmavCoin transfers cannot revert or reenter).
function collectPoolFees(address token) external nonReentrant {
Pool storage p = pools[poolOf[token]];
require(p.coin == token && token != address(0), "unknown coin");
require(p.graduated, "not graduated");
poolManager.unlock(abi.encode(uint8(3), abi.encode(token)));
}
function _collectCallback(address token) internal {
PoolKey memory key = poolKeyOf(token);
Pool storage p = pools[key.toId()];
bool coinIs0 = token < QUOTE_TOKEN;
// a zero-liquidity-delta poke realizes feesAccrued as caller deltas
(BalanceDelta fees,) = poolManager.modifyLiquidity(
key,
ModifyLiquidityParams({
tickLower: FULL_TICK_LOWER,
tickUpper: FULL_TICK_UPPER,
liquidityDelta: 0,
salt: 0
}),
""
);
uint256 quoteFees = uint256(uint128(coinIs0 ? fees.amount1() : fees.amount0()));
uint256 coinFees = uint256(uint128(coinIs0 ? fees.amount0() : fees.amount1()));
// graduated-pool LP fees split 70% creator / 30% platform (= 3/10
// platform), odd unit to the CREATOR — identical rounding discipline
// to _credit (there is no anti-snipe premium here).
uint256 platQuote = quoteFees * 3 / 10;
if (quoteFees > 0) {
poolManager.mint(address(this), QUOTE_ID, quoteFees);
creatorFees[p.feeRecipient] += quoteFees - platQuote;
platformFees += platQuote;
}
uint256 platCoin = coinFees * 3 / 10;
if (coinFees > 0) {
poolManager.take(coinIs0 ? key.currency0 : key.currency1, address(this), coinFees);
if (coinFees - platCoin > 0) require(PmavCoin(token).transfer(p.feeRecipient, coinFees - platCoin), "t1");
if (platCoin > 0) require(PmavCoin(token).transfer(owner, platCoin), "t2");
}
emit PoolFeesCollected(token, p.feeRecipient, quoteFees - platQuote, coinFees - platCoin, platQuote, platCoin);
}
// ------------------------------------------------------------ governance
// line-for-line ports from audited v1 Pmav.sol — moved, not rewritten
function setFeeRecipient(address token, address newRecipient) external {
Pool storage p = pools[poolOf[token]];
require(p.coin == token && token != address(0), "unknown coin");
require(msg.sender == p.feeRecipient, "not fee recipient");
require(newRecipient != address(0), "zero addr");
require(!feeRecipientLocked[token], "fee recipient locked");
lastActivity[token] = block.timestamp;
emit FeeRecipientChanged(token, p.feeRecipient, newRecipient);
p.feeRecipient = newRecipient;
}
/// @notice Permanently freeze the CURRENT fee recipient of a coin. One-way.
function lockFeeRecipient(address token) external {
Pool storage p = pools[poolOf[token]];
require(p.coin == token && token != address(0), "unknown coin");
require(msg.sender == p.feeRecipient, "not fee recipient");
require(!feeRecipientLocked[token], "already locked");
feeRecipientLocked[token] = true;
lastActivity[token] = block.timestamp;
emit FeeRecipientLockedEvent(token, p.feeRecipient);
}
/// @notice Atomically REASSIGN the fee recipient and permanently lock it.
function lockFeeRecipientTo(address token, address newRecipient) external {
Pool storage p = pools[poolOf[token]];
require(p.coin == token && token != address(0), "unknown coin");
require(msg.sender == p.feeRecipient, "not fee recipient");
require(newRecipient != address(0), "zero addr");
require(!feeRecipientLocked[token], "already locked");
lastActivity[token] = block.timestamp;
emit FeeRecipientChanged(token, p.feeRecipient, newRecipient);
p.feeRecipient = newRecipient;
feeRecipientLocked[token] = true;
emit FeeRecipientLockedEvent(token, newRecipient);
}
/// @notice CTO (community takeover): redirect an abandoned coin's FUTURE
/// creator rewards to a community lead. The one rail no review can
/// waive: the current DEV must be provably inactive — no fee claim
/// and no per-coin steward action for CTO_INACTIVITY. Trading does
/// NOT protect a dev. Locked coins are immune by design.
function proposeCtoRedirect(address token, address newRecipient) external onlyOwner {
Pool storage p = pools[poolOf[token]];
require(p.coin == token && token != address(0), "unknown coin");
require(newRecipient != address(0), "zero addr");
require(newRecipient != owner, "owner cannot self target");
require(!feeRecipientLocked[token], "fee recipient locked");
uint256 devActive = lastActivity[token];
uint256 claimed = lastClaim[p.feeRecipient];
if (claimed > devActive) devActive = claimed;
require(block.timestamp >= devActive + CTO_INACTIVITY, "dev still active");
ctoProposals[token] = CtoProposal({newRecipient: newRecipient, proposedAt: uint64(block.timestamp)});
emit CtoRedirectProposed(token, p.feeRecipient, newRecipient, block.timestamp + CTO_VETO_DELAY);
}
/// @notice CTO step 2 of 2: EXECUTE after the 12h veto window. ANY dev
/// liveness signal newer than the proposal (one fee claim, one
/// steward action, a lock) cancels it — no silent seizure path.
function executeCtoRedirect(address token) external onlyOwner {
Pool storage p = pools[poolOf[token]];
require(p.coin == token && token != address(0), "unknown coin");
CtoProposal memory prop = ctoProposals[token];
require(prop.newRecipient != address(0), "no proposal");
require(block.timestamp >= uint256(prop.proposedAt) + CTO_VETO_DELAY, "veto window open");
require(!feeRecipientLocked[token], "fee recipient locked");
require(lastActivity[token] < prop.proposedAt, "vetoed: dev acted");
require(lastClaim[p.feeRecipient] < prop.proposedAt, "vetoed: dev claimed");
delete ctoProposals[token];
lastActivity[token] = block.timestamp; // fresh window for the new lead
emit FeeRecipientChanged(token, p.feeRecipient, prop.newRecipient);
emit CtoRedirect(token, p.feeRecipient, prop.newRecipient);
p.feeRecipient = prop.newRecipient;
}
function setOwner(address newOwner) external onlyOwner {
require(newOwner != address(0), "zero addr");
emit OwnerChanged(owner, newOwner);
owner = newOwner;
}
/// @notice Tune the anti-snipe FACTORY DEFAULTS, bounded by the hard caps.
/// TIGHTENED vs v1: affects FUTURE launches only — every live
/// pool's premium/window were frozen into its struct at create().
function setAntiSnipeDefaults(uint256 peakBps, uint256 window) external onlyOwner {
require(peakBps <= ANTI_SNIPE_MAX_BPS, "peak too high");
require(window <= ANTI_SNIPE_MAX_WINDOW, "window too long");
antiSnipePeakBps = peakBps;
antiSnipeWindow = window;
emit AntiSnipeChanged(peakBps, window);
}
// ---------------------------------------------------------------- quotes
/// @notice Mirror of the buy path for UI parity with v1. APPROXIMATE by a
/// few parts in 1e15: the pool consumes input in tick-word chunks
/// with per-chunk round-up, which a closed-form view cannot
/// replicate — use V4Quoter (which simulates the real swap,
/// hook deltas included) for an exact minTokensOut. `refund` is
/// the curve-completion surplus only; the per-wallet cap clamp
/// depends on the buyer and is not modeled here (v1 parity).
/// `quoteGross` and all returns are RAW QUOTE UNITS.
function quoteBuy(address token, uint256 quoteGross)
external
view
returns (uint256 tokensOut, uint256 fee, uint256 refund)
{
PoolId id = poolOf[token];
Pool storage p = pools[id];
require(p.coin == token && token != address(0) && !p.graduated, "not on curve");
bool coinIs0 = token < QUOTE_TOKEN;
uint128 lCurve = coinIs0 ? L_CURVE_C0 : L_CURVE_Q0;
uint160 sqrtGrad = coinIs0 ? SQRT_GRAD_C0 : SQRT_GRAD_Q0;
uint256 feeBps = FEE_BPS + _antiSnipeBps(p);
uint160 sqrtP = _effSqrtP(id, coinIs0);
// v2.2 (audit L1): fee base bounded by the gross the curve can
// actually consume to graduation — mirrors the beforeSwap clamp, so
// an oversized completing buy is quoted AND charged on USED gross.
{
uint256 clampNet = coinIs0
? SqrtPriceMath.getAmount1Delta(sqrtP, sqrtGrad, lCurve, true)
: SqrtPriceMath.getAmount0Delta(sqrtGrad, sqrtP, lCurve, true);
uint256 maxFeeGross = FullMath.mulDivRoundingUp(clampNet, 10_000, 10_000 - feeBps);
fee = (quoteGross > maxFeeGross ? maxFeeGross : quoteGross) * feeBps / 10_000;
}
uint256 net = quoteGross - fee;
require(net > 0, "dust buy");
// quote in: currency0 in (price down) for Q0 pools, currency1 in
// (price up) for C0 pools
uint160 sqrtNext = SqrtPriceMath.getNextSqrtPriceFromInput(sqrtP, lCurve, net, !coinIs0);
if (coinIs0 ? sqrtNext > sqrtGrad : sqrtNext < sqrtGrad) {
// completing buy: only the quote that finishes the curve is consumed
uint256 maxNet = coinIs0
? SqrtPriceMath.getAmount1Delta(sqrtP, sqrtGrad, lCurve, true)
: SqrtPriceMath.getAmount0Delta(sqrtGrad, sqrtP, lCurve, true);
refund = quoteGross - fee - maxNet;
tokensOut = coinIs0
? SqrtPriceMath.getAmount0Delta(sqrtP, sqrtGrad, lCurve, false)
: SqrtPriceMath.getAmount1Delta(sqrtGrad, sqrtP, lCurve, false);
} else {
tokensOut = coinIs0
? SqrtPriceMath.getAmount0Delta(sqrtP, sqrtNext, lCurve, false)
: SqrtPriceMath.getAmount1Delta(sqrtNext, sqrtP, lCurve, false);
}
}
/// @notice returns are RAW QUOTE UNITS
function quoteSell(address token, uint256 tokenAmount) external view returns (uint256 quoteNet, uint256 fee) {
PoolId id = poolOf[token];
Pool storage p = pools[id];
require(p.coin == token && token != address(0) && !p.graduated, "not on curve");
require(tokenAmount > 0, "zero amount");
bool coinIs0 = token < QUOTE_TOKEN;
uint128 lCurve = coinIs0 ? L_CURVE_C0 : L_CURVE_Q0;
uint160 sqrtStart = coinIs0 ? SQRT_START_C0 : SQRT_START_Q0;
uint160 sqrtP = _effSqrtP(id, coinIs0);
// token in: currency1 in (price up) for Q0 pools, currency0 in
// (price down) for C0 pools
uint160 sqrtNext = SqrtPriceMath.getNextSqrtPriceFromInput(sqrtP, lCurve, tokenAmount, coinIs0);
// beyond the curve start the pool stops paying
if (coinIs0 ? sqrtNext < sqrtStart : sqrtNext > sqrtStart) sqrtNext = sqrtStart;
uint256 quoteGross = coinIs0
? SqrtPriceMath.getAmount1Delta(sqrtNext, sqrtP, lCurve, false)
: SqrtPriceMath.getAmount0Delta(sqrtP, sqrtNext, lCurve, false);
fee = quoteGross * FEE_BPS / 10_000;
quoteNet = quoteGross - fee;
}
/// @notice spot price in RAW QUOTE UNITS per whole (1e18) token, and
/// market cap in raw quote units. v1-shaped for the site; `pool`
/// is the PoolManager once graduated (v1 semantics: nonzero pool
/// == trade on the DEX). Return names keep the v2 ABI shape; the
/// values are quote units at QUOTE_DECIMALS.
function state(address token)
external
view
returns (
uint256 priceWei,
uint256 mcapWei,
uint256 ethReserve,
uint256 tokensSold,
uint256 progressBps,
bool graduated,
address pool
)
{
PoolId id = poolOf[token];
Pool storage p = pools[id];
require(p.coin == token && token != address(0), "unknown coin");
(uint160 sqrtP,,,) = poolManager.getSlot0(id);
// quote per whole token. Q0 pools: price(coin/quote) = (sqrtP/2^96)^2,
// so quote per 1e18 token-wei = 1e18 * 2^192 / sqrtP^2; C0 pools:
// price(quote/coin) = (sqrtP/2^96)^2, so quote per 1e18 token-wei =
// 1e18 * sqrtP^2 / 2^192. Both computed as two chained mulDivs so no
// sqrtP magnitude can overflow the naive sqrtP*sqrtP product.
if (token < QUOTE_TOKEN) {
priceWei = FullMath.mulDiv(FullMath.mulDiv(sqrtP, 1e18, FixedPoint96.Q96), sqrtP, FixedPoint96.Q96);
} else {
priceWei = FullMath.mulDiv(FullMath.mulDiv(FixedPoint96.Q96, 1e18, sqrtP), FixedPoint96.Q96, sqrtP);
}
mcapWei = priceWei * 1_000_000_000;
ethReserve = p.raisedWei;
tokensSold = p.tokensSold;
progressBps = uint256(p.tokensSold) * 10_000 / CURVE_SUPPLY;
graduated = p.graduated;
pool = p.graduated ? address(poolManager) : address(0);
}
function feeRecipientOf(address token) external view returns (address) {
return pools[poolOf[token]].feeRecipient;
}
// ------------------------------------------------------------- internals
function _abs(int256 x) internal pure returns (uint256) {
return x < 0 ? uint256(-x) : uint256(x);
}
/// @notice v2.2: whether this coin carries the decaying anti snipe
/// premium (the creator's public choice at create()).
function antiSnipeEnabled(address token) external view returns (bool) {
Pool storage p = pools[poolOf[token]];
require(p.coin == token && token != address(0), "unknown coin");
return p.snipePeakBps > 0 && p.snipeWindow > 0;
}
/// v2.2 (audit L2): opt in refund beneficiary carried in hookData.
/// EXACTLY 32 bytes decodes as an address by raw truncation (never
/// abi.decode: malformed hookData must IGNORE, not revert a swap).
function _refundBeneficiary(bytes calldata hookData) internal pure returns (address) {
if (hookData.length != 32) return address(0);
return address(uint160(uint256(bytes32(hookData[0:32]))));
}
/// current curve price for quote/clamp math, bounded to the curve range:
/// a sell can park the spot price BEYOND the curve start through empty
/// liquidity (above it on Q0 pools, below it on C0 pools); buys from
/// there walk back for free, so all token/quote math must start at the
/// start edge, not the empty-space price
function _effSqrtP(PoolId id, bool coinIs0) internal view returns (uint160 sqrtP) {
(sqrtP,,,) = poolManager.getSlot0(id);
if (coinIs0) {
if (sqrtP < SQRT_START_C0) sqrtP = SQRT_START_C0;
} else {
if (sqrtP > SQRT_START_Q0) sqrtP = SQRT_START_Q0;
}
}
/// max gross quote (fee included) a first-hour buy may spend so that the
/// delivered tokens never exceed capLeft. Rounding discipline (test
/// enforced): the sqrt target rounds TOWARD the current price (fewer
/// tokens) and the quote amount rounds DOWN, so core's forward math can
/// never deliver > capLeft.
function _maxGrossForCap(PoolId id, bool coinIs0, uint256 capLeft, uint256 feeBps)
internal
view
returns (uint256 maxGross)
{
uint160 sqrtP = _effSqrtP(id, coinIs0);
uint160 sqrtGrad = coinIs0 ? SQRT_GRAD_C0 : SQRT_GRAD_Q0;
uint128 lCurve = coinIs0 ? L_CURVE_C0 : L_CURVE_Q0;
uint256 maxNet;
bool gradBound;
if (coinIs0) {
// tokens are currency0: capLeft = L*Q96*(1/sqrtP - 1/target), so
// target = L*Q96*sqrtP / (L*Q96 - capLeft*sqrtP). The quotient
// FLOORS => target rounds DOWN (toward sqrtP) => tokens at target
// <= capLeft.
if (sqrtP >= sqrtGrad) return 0;
uint256 lQ96 = uint256(lCurve) << 96;
uint256 sold = capLeft * sqrtP; // < 2^244 across the whole envelope
gradBound = sold >= lQ96;
uint160 sqrtTarget = sqrtGrad;
if (!gradBound) {
uint256 t = FullMath.mulDiv(lQ96, sqrtP, lQ96 - sold);
if (t >= sqrtGrad) gradBound = true;
else sqrtTarget = uint160(t);
}
// when the graduation edge binds the fill is bounded by the curve
// itself, so rounding UP is safe (it just guarantees graduation)
maxNet = SqrtPriceMath.getAmount1Delta(sqrtP, sqrtTarget, lCurve, gradBound);
} else {
// tokens are currency1 (the v2 shape verbatim): sqrt price after
// the position hands out capLeft tokens (quotient floored =>
// target rounds UP => tokens at target <= capLeft)
if (sqrtP <= sqrtGrad) return 0;
uint256 quotient = FullMath.mulDiv(capLeft, FixedPoint96.Q96, lCurve);
gradBound = quotient >= sqrtP || sqrtP - quotient <= sqrtGrad;
uint160 sqrtTarget = gradBound ? sqrtGrad : uint160(sqrtP - quotient);
maxNet = SqrtPriceMath.getAmount0Delta(sqrtTarget, sqrtP, lCurve, gradBound);
}
if (maxNet == 0) return 0;
if (gradBound) {
// v2.2 FIX (found by the port suite, red test
// test_completing_buy_graduates_in_one_tx): when the graduation
// edge binds, tokens delivered are bounded by the CURVE (remaining
// curve <= capLeft by the gradBound condition), so round the gross
// UP and SKIP the walk down. The floor + walk down combination
// could leave a completing buy a sub wei hair above the edge
// (strict trigger misses; the coin failed to graduate in the same
// tx). A wei generous net just walks price past the edge into
// EMPTY liquidity, where input is never consumed and flows back
// through the router (the v2.1 post hour overshoot semantics the
// _graduate boundary repair already handles) -- it can never over
// deliver tokens and it guarantees same tx graduation.
return FullMath.mulDivRoundingUp(maxNet, 10_000, 10_000 - feeBps);
}
maxGross = FullMath.mulDiv(maxNet, 10_000, 10_000 - feeBps);
// exactness guard: the swap path will charge fee = floor(g*f/1e4) and
// hand the pool g - fee; walk g down (<=2 iterations) until that net
// cannot exceed maxNet
while (maxGross > 0 && maxGross - (maxGross * feeBps / 10_000) > maxNet) {
maxGross--;
}
}
// ---- v2.2 HARD 2% wallet cap (founder ruling 2026-07-20) --------------
// No wallet may hold more than 2% as a result of buying, and no tx.origin
// may ACQUIRE more than 2% through this pool in a single transaction —
// bundlers, MEV bots, snipers, any custom contract, all bound. Over-cap
// buys fill to exactly 2% and refund the surplus (fee only on the 2%).
//
// The cap binds on `held = startBalance + boughtTx`:
// * startBalance — the COIN balance (18dp, either orientation) tx.origin
// held at the START of this transaction, snapshotted at the FIRST cap
// touch (while boughtTx is still 0) and then FIXED. It captures any
// pre-existing holding a buy must not push past 2%.
// * boughtTx — a MONOTONIC per (pool, origin) TRANSIENT counter of
// everything tx.origin has bought through this pool this transaction,
// NEVER reset.
// The two sets are DISJOINT — startBalance excludes this-tx buys (not yet
// settled into balanceOf), boughtTx IS this-tx buys — so their SUM is the
// exact acquisition-inclusive holding: no double count of a settled buy, no
// under count of a warm wallet that bundles buys in one unlock. A FIXED
// start snapshot plus a monotonic never-reset counter means no balance move
// mid-tx (a 1-wei delivery to origin, a full delivery, a transfer to a
// stash) can re-open the cap. Transient clears at tx end, so a later tx
// re-snapshots startBalance from the live balance (a sell/transfer away in a
// separate tx frees capacity).
//
// Documented residual (unpreventable without token transfer restrictions,
// which stay rejected for scanner cleanliness): spreading buys across many
// transactions/wallets and funneling to one address via raw ERC20 transfers
// (incl. a transfer INTO origin AFTER the start snapshot in the same tx).
// The cap is a hard per-transaction acquisition + start-of-tx holding bound,
// not a cross-transaction identity bound.
function _capBoughtSlot(PoolId id, address origin) private pure returns (bytes32 slot) {
slot = keccak256(abi.encode("pmav.cap.bought", id, origin));
}
function _capStartSlot(PoolId id, address origin) private pure returns (bytes32 slot) {
slot = keccak256(abi.encode("pmav.cap.start", id, origin));
}
/// tokens tx.origin effectively holds against the cap = its start-of-tx COIN
/// balance (snapshotted at the first cap touch, when boughtTx is still 0)
/// PLUS everything bought through this pool this tx (a monotonic transient
/// counter). The SUM of these two disjoint sets: never double counts a
/// settled buy, never under counts a warm wallet bundling buys, and no
/// balance move can re-open the cap (both terms fixed/monotonic).
function _capHeld(PoolId id, address coin, address origin) internal returns (uint256 held) {
bytes32 bSlot = _capBoughtSlot(id, origin);
bytes32 sSlot = _capStartSlot(id, origin);
uint256 bought;
assembly ("memory-safe") {
bought := tload(bSlot)
}
uint256 start;
if (bought == 0) {
// first cap touch this tx: snapshot the pre-buy balance and fix it
start = PmavCoin(coin).balanceOf(origin);
assembly ("memory-safe") {
tstore(sSlot, start)
}
} else {
assembly ("memory-safe") {
start := tload(sSlot)
}
}
held = start + bought;
}
function _recordCapBuy(PoolId id, address origin, uint256 amount) internal {
bytes32 bSlot = _capBoughtSlot(id, origin);
uint256 bought;
assembly ("memory-safe") {
bought := tload(bSlot)
}
assembly ("memory-safe") {
tstore(bSlot, add(bought, amount))
}
}
/// Anti-snipe premium (bps) at the current time for a pool: linear decay
/// from the pool's FROZEN peak at launch to 0 at its frozen window edge.
function _antiSnipeBps(Pool storage p) internal view returns (uint256) {
uint256 window = p.snipeWindow;
if (window == 0) return 0;
uint256 elapsed = block.timestamp - p.launchTime;
if (elapsed >= window) return 0;
return uint256(p.snipePeakBps) * (window - elapsed) / window;
}
/// standard Babylonian integer sqrt (floor). Only used once, in the
/// constructor's curve derivation.
function _sqrt(uint256 x) internal pure returns (uint256 z) {
if (x == 0) return 0;
uint256 y = x;
z = 1;
// seed z near sqrt(x) by halving the bit length
if (y >= 1 << 128) {
y >>= 128;
z <<= 64;
}
if (y >= 1 << 64) {
y >>= 64;
z <<= 32;
}
if (y >= 1 << 32) {
y >>= 32;
z <<= 16;
}
if (y >= 1 << 16) {
y >>= 16;
z <<= 8;
}
if (y >= 1 << 8) {
y >>= 8;
z <<= 4;
}
if (y >= 1 << 4) {
y >>= 4;
z <<= 2;
}
if (y >= 1 << 2) {
z <<= 1;
}
z = (z + x / z) >> 1;
z = (z + x / z) >> 1;
z = (z + x / z) >> 1;
z = (z + x / z) >> 1;
z = (z + x / z) >> 1;
z = (z + x / z) >> 1;
z = (z + x / z) >> 1;
// floor correction (Babylonian can land one above)
if (z > x / z) z--;
}
/// round a tick to the NEAREST TICK_SPACING multiple (ties away from
/// zero) — the alignment discipline that produced v2's [176620, 204350]
function _alignNearest(int24 tick) internal pure returns (int24) {
int24 r = tick % TICK_SPACING;
int24 base = tick - r;
if (r >= 5) base += TICK_SPACING;
else if (r <= -5) base -= TICK_SPACING;
return base;
}
/// minimal safe-ERC20 (USDT-style missing-return tolerated, reverts on
/// explicit false or failed call) — the hook's only direct quote moves
/// are the dev-buy pull, its PM settle leg, and the dev-buy surplus refund
function _safeTransfer(address token, address to, uint256 amount) internal {
(bool ok, bytes memory ret) = token.call(abi.encodeWithSelector(0xa9059cbb, to, amount));
require(ok && (ret.length == 0 || abi.decode(ret, (bool))), "quote transfer failed");
}
function _safeTransferFrom(address token, address from, address to, uint256 amount) internal {
(bool ok, bytes memory ret) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, amount));
require(ok && (ret.length == 0 || abi.decode(ret, (bool))), "quote transferFrom failed");
}
// NOTE: no receive() on this variant — the quote is an ERC20, no code
// path handles native ETH, and any direct ETH send reverts by default.
}
Chain explorer4444msChain node97ms