PmavHook
0x97472ae141ff13a4d317328bc4f3f95172fba8cc
Verification
Verified
v0.8.28+commit.7893614a
Type
Contract
30,507 bytes
ABI entries
79
40 read · 22 write
License
none
Contract information
- Address
- 0x97472ae141ff13a4d317328bc4f3f95172fba8cc
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.28+commit.7893614a
- Optimization
- Enabled
- Creator
- 0x4e59b44847…26c0B4956C
- Creation tx
- 0x4e8bae0a33…7d03b79543
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (40)
ANTI_SNIPE_MAX_BPS() → uint256
ANTI_SNIPE_MAX_WINDOW() → uint256
CTO_INACTIVITY() → uint256
CURVE_SUPPLY() → uint256
FEE_BPS() → uint256
FIRST_HOUR() → uint256
FIRST_HOUR_MAX_BUY() → uint256
FULL_TICK_LOWER() → int24
FULL_TICK_UPPER() → int24
GRADUATED_LP_FEE() → uint24
GRADUATION_GAS() → uint256
LP_SUPPLY() → uint256
L_CURVE() → uint128
SQRT_GRAD() → uint160
SQRT_START() → uint160
TICK_LOWER() → int24
TICK_SPACING() → int24
TICK_UPPER() → int24
TOTAL_SUPPLY() → uint256
allCoins(uint256) → address
antiSnipePeakBps() → uint256
antiSnipeWindow() → uint256
coinCount() → uint256
creatorFees(address) → uint256
feeRecipientLocked(address) → bool
feeRecipientOf(address) → address
firstHourBought(bytes32, address) → uint256
getHookPermissions() → tuple
lastActivity(address) → uint256
lastClaim(address) → uint256
owner() → address
platformFees() → uint256
poolKeyOf(address) → tuple
poolManager() → address
poolOf(address) → bytes32
pools(bytes32) → address, address, uint64, uint64, uint16, uint16, bool, uint128, uint128
quoteBuy(address, uint256) → uint256, uint256, uint256
quoteSell(address, uint256) → uint256, uint256
refunds(address) → uint256
state(address) → uint256, uint256, uint256, uint256, uint256, bool, address
Events (13)
AntiSnipeChangedCoinCreatedCreatorFeesClaimedCtoRedirectFeeRecipientChangedFeeRecipientLockedEventGraduatedOwnerChangedPlatformFeesWithdrawnPoolFeesCollectedRefundClaimedRefundQueuedTrade
ABI
[
{
"inputs": [
{
"internalType": "contract IPoolManager",
"name": "_manager",
"type": "address"
},
{
"internalType": "address",
"name": "_owner",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "HookNotImplemented",
"type": "error"
},
{
"inputs": [],
"name": "NotPoolManager",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "peakBps",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "window",
"type": "uint256"
}
],
"name": "AntiSnipeChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "bytes32",
"name": "poolId",
"type": "bytes32"
},
{
"indexed": true,
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "feeRecipient",
"type": "address"
},
{
"indexed": false,
"internalType": "string",
"name": "name",
"type": "string"
},
{
"indexed": false,
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"indexed": false,
"internalType": "string",
"name": "metadata",
"type": "string"
}
],
"name": "CoinCreated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "CreatorFeesClaimed",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "oldRecipient",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newRecipient",
"type": "address"
}
],
"name": "CtoRedirect",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "oldRecipient",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newRecipient",
"type": "address"
}
],
"name": "FeeRecipientChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "recipient",
"type": "address"
}
],
"name": "FeeRecipientLockedEvent",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "bytes32",
"name": "poolId",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "uint256",
"name": "ethInPool",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokensInPool",
"type": "uint256"
}
],
"name": "Graduated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "oldOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnerChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "PlatformFeesWithdrawn",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "creatorRecipient",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "creatorWei",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "creatorTokens",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "platformWei",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "platformTokens",
"type": "uint256"
}
],
"name": "PoolFeesCollected",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "RefundClaimed",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "RefundQueued",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "trader",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "isBuy",
"type": "bool"
},
{
"indexed": false,
"internalType": "uint256",
"name": "ethGross",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "ethNet",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokenAmount",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint160",
"name": "sqrtPriceX96",
"type": "uint160"
},
{
"indexed": false,
"internalType": "uint128",
"name": "tokensSold",
"type": "uint128"
},
{
"indexed": false,
"internalType": "uint128",
"name": "raisedWei",
"type": "uint128"
}
],
"name": "Trade",
"type": "event"
},
{
"inputs": [],
"name": "ANTI_SNIPE_MAX_BPS",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "ANTI_SNIPE_MAX_WINDOW",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "CTO_INACTIVITY",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "CURVE_SUPPLY",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "FEE_BPS",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "FIRST_HOUR",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "FIRST_HOUR_MAX_BUY",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "FULL_TICK_LOWER",
"outputs": [
{
"internalType": "int24",
"name": "",
"type": "int24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "FULL_TICK_UPPER",
"outputs": [
{
"internalType": "int24",
"name": "",
"type": "int24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "GRADUATED_LP_FEE",
"outputs": [
{
"internalType": "uint24",
"name": "",
"type": "uint24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "GRADUATION_GAS",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "LP_SUPPLY",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "L_CURVE",
"outputs": [
{
"internalType": "uint128",
"name": "",
"type": "uint128"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "SQRT_GRAD",
"outputs": [
{
"internalType": "uint160",
"name": "",
"type": "uint160"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "SQRT_START",
"outputs": [
{
"internalType": "uint160",
"name": "",
"type": "uint160"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "TICK_LOWER",
"outputs": [
{
"internalType": "int24",
"name": "",
"type": "int24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "TICK_SPACING",
"outputs": [
{
"internalType": "int24",
"name": "",
"type": "int24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "TICK_UPPER",
"outputs": [
{
"internalType": "int24",
"name": "",
"type": "int24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "TOTAL_SUPPLY",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"components": [
{
"internalType": "Currency",
"name": "currency0",
"type": "address"
},
{
"internalType": "Currency",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "contract IHooks",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "key",
"type": "tuple"
},
{
"components": [
{
"internalType": "int24",
"name": "tickLower",
"type": "int24"
},
{
"internalType": "int24",
"name": "tickUpper",
"type": "int24"
},
{
"internalType": "int256",
"name": "liquidityDelta",
"type": "int256"
},
{
"internalType": "bytes32",
"name": "salt",
"type": "bytes32"
}
],
"internalType": "struct ModifyLiquidityParams",
"name": "params",
"type": "tuple"
},
{
"internalType": "BalanceDelta",
"name": "delta",
"type": "int256"
},
{
"internalType": "BalanceDelta",
"name": "feesAccrued",
"type": "int256"
},
{
"internalType": "bytes",
"name": "hookData",
"type": "bytes"
}
],
"name": "afterAddLiquidity",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
},
{
"internalType": "BalanceDelta",
"name": "",
"type": "int256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"components": [
{
"internalType": "Currency",
"name": "currency0",
"type": "address"
},
{
"internalType": "Currency",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "contract IHooks",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "key",
"type": "tuple"
},
{
"internalType": "uint256",
"name": "amount0",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amount1",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "hookData",
"type": "bytes"
}
],
"name": "afterDonate",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"components": [
{
"internalType": "Currency",
"name": "currency0",
"type": "address"
},
{
"internalType": "Currency",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "contract IHooks",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "key",
"type": "tuple"
},
{
"internalType": "uint160",
"name": "sqrtPriceX96",
"type": "uint160"
},
{
"internalType": "int24",
"name": "tick",
"type": "int24"
}
],
"name": "afterInitialize",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"components": [
{
"internalType": "Currency",
"name": "currency0",
"type": "address"
},
{
"internalType": "Currency",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "contract IHooks",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "key",
"type": "tuple"
},
{
"components": [
{
"internalType": "int24",
"name": "tickLower",
"type": "int24"
},
{
"internalType": "int24",
"name": "tickUpper",
"type": "int24"
},
{
"internalType": "int256",
"name": "liquidityDelta",
"type": "int256"
},
{
"internalType": "bytes32",
"name": "salt",
"type": "bytes32"
}
],
"internalType": "struct ModifyLiquidityParams",
"name": "params",
"type": "tuple"
},
{
"internalType": "BalanceDelta",
"name": "delta",
"type": "int256"
},
{
"internalType": "BalanceDelta",
"name": "feesAccrued",
"type": "int256"
},
{
"internalType": "bytes",
"name": "hookData",
"type": "bytes"
}
],
"name": "afterRemoveLiquidity",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
},
{
"internalType": "BalanceDelta",
"name": "",
"type": "int256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"components": [
{
"internalType": "Currency",
"name": "currency0",
"type": "address"
},
{
"internalType": "Currency",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "contract IHooks",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "key",
"type": "tuple"
},
{
"components": [
{
"internalType": "bool",
"name": "zeroForOne",
"type": "bool"
},
{
"internalType": "int256",
"name": "amountSpecified",
"type": "int256"
},
{
"internalType": "uint160",
"name": "sqrtPriceLimitX96",
"type": "uint160"
}
],
"internalType": "struct SwapParams",
"name": "params",
"type": "tuple"
},
{
"internalType": "BalanceDelta",
"name": "delta",
"type": "int256"
},
{
"internalType": "bytes",
"name": "hookData",
"type": "bytes"
}
],
"name": "afterSwap",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
},
{
"internalType": "int128",
"name": "",
"type": "int128"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "allCoins",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "antiSnipePeakBps",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "antiSnipeWindow",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"components": [
{
"internalType": "Currency",
"name": "currency0",
"type": "address"
},
{
"internalType": "Currency",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "contract IHooks",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "key",
"type": "tuple"
},
{
"components": [
{
"internalType": "int24",
"name": "tickLower",
"type": "int24"
},
{
"internalType": "int24",
"name": "tickUpper",
"type": "int24"
},
{
"internalType": "int256",
"name": "liquidityDelta",
"type": "int256"
},
{
"internalType": "bytes32",
"name": "salt",
"type": "bytes32"
}
],
"internalType": "struct ModifyLiquidityParams",
"name": "params",
"type": "tuple"
},
{
"internalType": "bytes",
"name": "hookData",
"type": "bytes"
}
],
"name": "beforeAddLiquidity",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"components": [
{
"internalType": "Currency",
"name": "currency0",
"type": "address"
},
{
"internalType": "Currency",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "contract IHooks",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "key",
"type": "tuple"
},
{
"internalType": "uint256",
"name": "amount0",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amount1",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "hookData",
"type": "bytes"
}
],
"name": "beforeDonate",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"components": [
{
"internalType": "Currency",
"name": "currency0",
"type": "address"
},
{
"internalType": "Currency",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "contract IHooks",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "key",
"type": "tuple"
},
{
"internalType": "uint160",
"name": "sqrtPriceX96",
"type": "uint160"
}
],
"name": "beforeInitialize",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"components": [
{
"internalType": "Currency",
"name": "currency0",
"type": "address"
},
{
"internalType": "Currency",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "contract IHooks",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "key",
"type": "tuple"
},
{
"components": [
{
"internalType": "int24",
"name": "tickLower",
"type": "int24"
},
{
"internalType": "int24",
"name": "tickUpper",
"type": "int24"
},
{
"internalType": "int256",
"name": "liquidityDelta",
"type": "int256"
},
{
"internalType": "bytes32",
"name": "salt",
"type": "bytes32"
}
],
"internalType": "struct ModifyLiquidityParams",
"name": "params",
"type": "tuple"
},
{
"internalType": "bytes",
"name": "hookData",
"type": "bytes"
}
],
"name": "beforeRemoveLiquidity",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"components": [
{
"internalType": "Currency",
"name": "currency0",
"type": "address"
},
{
"internalType": "Currency",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "contract IHooks",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "key",
"type": "tuple"
},
{
"components": [
{
"internalType": "bool",
"name": "zeroForOne",
"type": "bool"
},
{
"internalType": "int256",
"name": "amountSpecified",
"type": "int256"
},
{
"internalType": "uint160",
"name": "sqrtPriceLimitX96",
"type": "uint160"
}
],
"internalType": "struct SwapParams",
"name": "params",
"type": "tuple"
},
{
"internalType": "bytes",
"name": "hookData",
"type": "bytes"
}
],
"name": "beforeSwap",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
},
{
"internalType": "BeforeSwapDelta",
"name": "",
"type": "int256"
},
{
"internalType": "uint24",
"name": "",
"type": "uint24"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "claimCreatorFees",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "claimRefund",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "coinCount",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "collectPoolFees",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"internalType": "string",
"name": "metadata",
"type": "string"
},
{
"internalType": "address",
"name": "feeRecipient",
"type": "address"
},
{
"internalType": "uint256",
"name": "minTokensOut",
"type": "uint256"
}
],
"name": "create",
"outputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "creatorFees",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "newRecipient",
"type": "address"
}
],
"name": "ctoRedirect",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "feeRecipientLocked",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "feeRecipientOf",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "PoolId",
"name": "",
"type": "bytes32"
},
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "firstHourBought",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "getHookPermissions",
"outputs": [
{
"components": [
{
"internalType": "bool",
"name": "beforeInitialize",
"type": "bool"
},
{
"internalType": "bool",
"name": "afterInitialize",
"type": "bool"
},
{
"internalType": "bool",
"name": "beforeAddLiquidity",
"type": "bool"
},
{
"internalType": "bool",
"name": "afterAddLiquidity",
"type": "bool"
},
{
"internalType": "bool",
"name": "beforeRemoveLiquidity",
"type": "bool"
},
{
"internalType": "bool",
"name": "afterRemoveLiquidity",
"type": "bool"
},
{
"internalType": "bool",
"name": "beforeSwap",
"type": "bool"
},
{
"internalType": "bool",
"name": "afterSwap",
"type": "bool"
},
{
"internalType": "bool",
"name": "beforeDonate",
"type": "bool"
},
{
"internalType": "bool",
"name": "afterDonate",
"type": "bool"
},
{
"internalType": "bool",
"name": "beforeSwapReturnDelta",
"type": "bool"
},
{
"internalType": "bool",
"name": "afterSwapReturnDelta",
"type": "bool"
},
{
"internalType": "bool",
"name": "afterAddLiquidityReturnDelta",
"type": "bool"
},
{
"internalType": "bool",
"name": "afterRemoveLiquidityReturnDelta",
"type": "bool"
}
],
"internalType": "struct Hooks.Permissions",
"name": "",
"type": "tuple"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "lastActivity",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "lastClaim",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "lockFeeRecipient",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "newRecipient",
"type": "address"
}
],
"name": "lockFeeRecipientTo",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "platformFees",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "poolKeyOf",
"outputs": [
{
"components": [
{
"internalType": "Currency",
"name": "currency0",
"type": "address"
},
{
"internalType": "Currency",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "contract IHooks",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "",
"type": "tuple"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "poolManager",
"outputs": [
{
"internalType": "contract IPoolManager",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "poolOf",
"outputs": [
{
"internalType": "PoolId",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "PoolId",
"name": "",
"type": "bytes32"
}
],
"name": "pools",
"outputs": [
{
"internalType": "address",
"name": "coin",
"type": "address"
},
{
"internalType": "address",
"name": "feeRecipient",
"type": "address"
},
{
"internalType": "uint64",
"name": "launchBlock",
"type": "uint64"
},
{
"internalType": "uint64",
"name": "launchTime",
"type": "uint64"
},
{
"internalType": "uint16",
"name": "snipePeakBps",
"type": "uint16"
},
{
"internalType": "uint16",
"name": "snipeWindow",
"type": "uint16"
},
{
"internalType": "bool",
"name": "graduated",
"type": "bool"
},
{
"internalType": "uint128",
"name": "tokensSold",
"type": "uint128"
},
{
"internalType": "uint128",
"name": "raisedWei",
"type": "uint128"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "ethGross",
"type": "uint256"
}
],
"name": "quoteBuy",
"outputs": [
{
"internalType": "uint256",
"name": "tokensOut",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "fee",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "refund",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenAmount",
"type": "uint256"
}
],
"name": "quoteSell",
"outputs": [
{
"internalType": "uint256",
"name": "ethNet",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "fee",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "refunds",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "peakBps",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "window",
"type": "uint256"
}
],
"name": "setAntiSnipeDefaults",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "newRecipient",
"type": "address"
}
],
"name": "setFeeRecipient",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "setOwner",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "state",
"outputs": [
{
"internalType": "uint256",
"name": "priceWei",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "mcapWei",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "ethReserve",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "tokensSold",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "progressBps",
"type": "uint256"
},
{
"internalType": "bool",
"name": "graduated",
"type": "bool"
},
{
"internalType": "address",
"name": "pool",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "unlockCallback",
"outputs": [
{
"internalType": "bytes",
"name": "",
"type": "bytes"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "withdrawPlatformFees",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;
/// ---------------------------------------------------------------------------
/// pmav.fun V2 — the curve IS the pool (Uniswap V4 hook architecture)
///
/// Vendored dependency pins (see contracts/lib/):
/// v4-periphery commit 3779387e5d296f39df543d23524b050f89a62917
/// (last revision carrying src/utils/BaseHook.sol + HookMiner,
/// parent of PR #510 which moved hooks out of the repo)
/// v4-core commit 59d3ecf53afa9264a16bba0e38f4c5d2231f80bc
/// (tag v4.0.0 + 12, the v4-periphery pinned submodule; matches
/// the deployed PoolManager generation on chain 4663)
///
/// HOOK ADDRESS MINING: the low 14 bits of this contract's address MUST equal
/// 0x28CC = BEFORE_INITIALIZE | BEFORE_ADD_LIQUIDITY | BEFORE_SWAP | AFTER_SWAP
/// | BEFORE_SWAP_RETURNS_DELTA | AFTER_SWAP_RETURNS_DELTA
/// = 1<<13 | 1<<11 | 1<<7 | 1<<6 | 1<<3 | 1<<2.
/// Deployment goes through the canonical CREATE2 deployer
/// (0x4e59b44847b379578588920cA78FbF26c0B4956C, live on chain 4663) with a
/// salt found by HookMiner.find() — see script/DeployV2.s.sol, which asserts
/// uint160(address(hook)) & Hooks.ALL_HOOK_MASK == 0x28CC before anything else.
/// The BaseHook constructor additionally runs Hooks.validateHookPermissions.
///
/// V4-CORE SEMANTICS NOTE (verified against the vendored source, differs from
/// parts of docs/research/V4_DESIGN.md): every Hooks library wrapper no-ops
/// when msg.sender == the hook itself ("noSelfCall"). Consequences:
/// - create()'s own initialize/modifyLiquidity/swap calls NEVER re-enter the
/// hook callbacks, so the spec's `_creating`/`_creatingCoin` transient
/// gating is dead code. beforeInitialize instead reverts UNCONDITIONALLY
/// (Flaunch shape): no pool that is not created by create() can ever
/// attach this hook, and create()'s own initialize skips the callback.
/// - the dev buy cannot be metered by beforeSwap/afterSwap; its fee, cap
/// clamp, ledger credit and Trade event are implemented inline in the
/// create() unlock callback with identical arithmetic.
/// - graduation's inner swap/modifyLiquidity never re-trigger callbacks, so
/// no `_inGraduation` transient is needed; `graduated = true` is still set
/// first for idempotence.
/// ---------------------------------------------------------------------------
import {BaseHook} from "v4-periphery/src/utils/BaseHook.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";
import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {PoolId} from "@uniswap/v4-core/src/types/PoolId.sol";
import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol";
import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
import {
BeforeSwapDelta, BeforeSwapDeltaLibrary, toBeforeSwapDelta
} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol";
import {ModifyLiquidityParams, SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
import {LPFeeLibrary} from "@uniswap/v4-core/src/libraries/LPFeeLibrary.sol";
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
import {SqrtPriceMath} from "@uniswap/v4-core/src/libraries/SqrtPriceMath.sol";
import {FullMath} from "@uniswap/v4-core/src/libraries/FullMath.sol";
import {FixedPoint96} from "@uniswap/v4-core/src/libraries/FixedPoint96.sol";
import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
import {LiquidityAmounts} from "v4-periphery/src/libraries/LiquidityAmounts.sol";
import {PmavCoin} from "../PmavCoin.sol";
/// @title PmavHook — the pmav.fun V2 launchpad: factory + Uniswap V4 hook + fee bank
/// @notice Launch a coin for just gas. Every coin: 1B fixed supply, no taxes,
/// no owner. Coins trade from transaction one as a NATIVE Uniswap V4
/// pool (ETH paired) on the canonical PoolManager; the bonding curve
/// is a single concentrated position [176620, 204350] owned by this
/// hook that reproduces the V1 virtual-reserve curve within ~4 bps.
/// When the curve sells its 800M tokens the coin graduates IN THE SAME
/// POOL: the curve position is removed and the raise + the reserved
/// 200M tokens are minted as a permanently locked full-range position.
/// No migration, no locker contract — this hook simply has no code
/// path that removes the full-range position.
///
/// Fair-launch rules enforced by the pool itself (router-proof):
/// - trading opens the block AFTER creation (only the dev's own
/// launch buy executes in the creation block)
/// - first hour: no wallet (tx.origin) buys more than 2% of supply
/// (20M) cumulatively — over-cap buys FILL TO CAP and auto-refund
/// the surplus in the same transaction; never a failed buy. If
/// the PoolManager's native float ever cannot cover the instant
/// refund (settle-after router, young deployment), the surplus
/// queues for a one-click claimRefund() and the buy STILL succeeds
/// - decaying anti-snipe premium on buys (split 70/30 like the base)
/// - 1% fee on curve trades, split 70% creator / 30% platform
/// - sells are NEVER capped, taxed extra, or restricted. Ever.
contract PmavHook is BaseHook, IUnlockCallback {
using StateLibrary for IPoolManager;
// ---------------------------------------------------------------- consts
uint256 public constant TOTAL_SUPPLY = 1_000_000_000e18;
uint256 public constant CURVE_SUPPLY = 800_000_000e18; // sold on the curve
uint256 public constant LP_SUPPLY = 200_000_000e18; // reserved for the full-range position
uint256 public constant FIRST_HOUR = 3600;
uint256 public constant FIRST_HOUR_MAX_BUY = 20_000_000e18; // 2% of supply
uint256 public constant FEE_BPS = 100; // 1% of gross ETH per trade
uint256 public constant ANTI_SNIPE_MAX_BPS = 2_500;
uint256 public constant ANTI_SNIPE_MAX_WINDOW = 120;
uint256 public constant CTO_INACTIVITY = 7 days;
uint24 public constant GRADUATED_LP_FEE = 10_000; // 1% in pips (< MAX_LP_FEE 1_000_000)
int24 public constant TICK_LOWER = 176_620; // graduation tick
int24 public constant TICK_UPPER = 204_350; // curve start, pool init tick
int24 public constant FULL_TICK_LOWER = -887_270;
int24 public constant FULL_TICK_UPPER = 887_270;
int24 public constant TICK_SPACING = 10;
/// gas floor for the completing buy: revert loudly rather than let
/// estimators settle on a path where graduation cannot execute.
/// PROVENANCE (fork finding F1): graduation step measured on COLD
/// chain-4663 mainnet state at fork block 10,090,000 = ~3,823,356 gas
/// (graduating buy 4,095,225 minus a matched non-graduating 4.2-ETH buy
/// 271,869 — ForkGraduation.t.sol::test_graduation_step_gas_vs_floor).
/// Floor = measured cost + ~10% headroom; the fork test asserts the
/// floor stays above the measured step.
uint256 public constant GRADUATION_GAS = 4_200_000;
address internal constant DEAD = 0x000000000000000000000000000000000000dEaD;
uint256 internal constant ETH_ID = 0; // CurrencyLibrary.ADDRESS_ZERO.toId()
/// derived once at deploy from TickMath — bit-exact with the pool's math
uint160 public immutable SQRT_START; // sqrtPrice at TICK_UPPER
uint160 public immutable SQRT_GRAD; // sqrtPrice at TICK_LOWER
uint160 internal immutable SQRT_FULL_LOWER;
uint160 internal immutable SQRT_FULL_UPPER;
uint128 public immutable L_CURVE; // curve position liquidity
// ---------------------------------------------------------------- state
struct Pool {
address coin;
address feeRecipient;
uint64 launchBlock;
uint64 launchTime;
uint16 snipePeakBps; // frozen at create from factory defaults
uint16 snipeWindow; // seconds; frozen at create
bool graduated;
uint128 tokensSold; // informational + invariant cross-check
uint128 raisedWei; // informational + invariant cross-check
}
mapping(PoolId => Pool) public pools;
mapping(address => PoolId) public poolOf; // coin => pool id
address[] public allCoins;
/// cumulative first-hour purchases: pool => tx.origin => tokens bought
mapping(PoolId => mapping(address => uint256)) public firstHourBought;
/// claimable creator fees, keyed by recipient (ETH, pull; backed by the
/// hook's ERC6909 native-ETH claims on the PoolManager)
mapping(address => uint256) public creatorFees;
uint256 public platformFees;
/// queued over-cap refunds (ETH, pull; backed by the hook's ERC6909
/// claims like the fee ledgers). Ruling 19.B waterfall branch 2: written
/// ONLY when a settle-after router hits a PoolManager whose native float
/// cannot cover the instant take() — expected rarely, and only while the
/// platform is young (live float on 4663 measured at ~1234.69 ETH).
mapping(address => uint256) public refunds;
/// CTO rail, ported byte-for-byte from V1 (see Pmav.sol for the doctrine):
/// an ACTIVE dev can never have their fee stream seized; activity =
/// per-coin steward actions + the recipient claiming fees anywhere.
mapping(address => uint256) public lastActivity; // per-coin steward action
mapping(address => uint256) public lastClaim; // per-recipient claim time
/// one-way fee recipient lock (locked coins are CTO-immune)
mapping(address => bool) public feeRecipientLocked;
/// anti-snipe FACTORY DEFAULTS: copied into each pool at create() and
/// frozen there. Owner tuning affects FUTURE launches only, within the
/// hard caps — no function can change a live pool's swap behavior.
uint256 public antiSnipePeakBps = 1_500; // 15%
uint256 public antiSnipeWindow = 30; // seconds
address public owner;
uint256 private _lock = 1;
// -------------------------------------------------------- transient state
/// swap reentrancy lock (risk register #17: EIP-7702 delegated EOA can run
/// code on the refund take() and try to re-enter the pool mid-callback)
bool private transient _swapLock;
/// fee credited / refund taken in beforeSwap, consumed by afterSwap for
/// the Trade event (reset at the top of every beforeSwap)
uint256 private transient _pendingFee;
uint256 private transient _pendingRefund;
// ---------------------------------------------------------------- events
// v1 names preserved — the indexer consumes these unchanged (§14)
event CoinCreated(
address indexed token,
bytes32 indexed poolId,
address indexed creator,
address feeRecipient,
string name,
string symbol,
string metadata
);
event Trade(
address indexed token,
address indexed trader,
bool isBuy,
uint256 ethGross,
uint256 ethNet,
uint256 tokenAmount,
uint160 sqrtPriceX96,
uint128 tokensSold,
uint128 raisedWei
);
event Graduated(address indexed token, bytes32 indexed poolId, uint256 ethInPool, uint256 tokensInPool);
event FeeRecipientChanged(address indexed token, address indexed oldRecipient, address indexed newRecipient);
event CtoRedirect(address indexed token, address indexed oldRecipient, address indexed newRecipient);
event FeeRecipientLockedEvent(address indexed token, address indexed recipient);
event CreatorFeesClaimed(address indexed recipient, uint256 amount);
event RefundQueued(address indexed token, address indexed to, uint256 amount);
event RefundClaimed(address indexed to, uint256 amount);
event PlatformFeesWithdrawn(address indexed to, uint256 amount);
event PoolFeesCollected(
address indexed token,
address indexed creatorRecipient,
uint256 creatorWei,
uint256 creatorTokens,
uint256 platformWei,
uint256 platformTokens
);
event AntiSnipeChanged(uint256 peakBps, uint256 window);
event OwnerChanged(address indexed oldOwner, address indexed newOwner);
// ------------------------------------------------------------- modifiers
modifier nonReentrant() {
require(_lock == 1, "reentrancy");
_lock = 2;
_;
_lock = 1;
}
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
constructor(IPoolManager _manager, address _owner) BaseHook(_manager) {
require(_owner != address(0), "zero addr");
owner = _owner;
SQRT_START = TickMath.getSqrtPriceAtTick(TICK_UPPER);
SQRT_GRAD = TickMath.getSqrtPriceAtTick(TICK_LOWER);
SQRT_FULL_LOWER = TickMath.getSqrtPriceAtTick(FULL_TICK_LOWER);
SQRT_FULL_UPPER = TickMath.getSqrtPriceAtTick(FULL_TICK_UPPER);
// L such that the position holds exactly the 800M curve tokens at the
// top of the range (pool initializes at TICK_UPPER: tokens only)
L_CURVE = LiquidityAmounts.getLiquidityForAmount1(SQRT_GRAD, SQRT_START, CURVE_SUPPLY);
}
// ---------------------------------------------------------- hook wiring
function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
return Hooks.Permissions({
beforeInitialize: true, // gate: only create() pools may attach us
afterInitialize: false,
beforeAddLiquidity: true, // gate: only hook liquidity during curve
afterAddLiquidity: false,
beforeRemoveLiquidity: false, // no third-party curve LP exists to gate
afterRemoveLiquidity: false,
beforeSwap: true, // block+1, cap clamp, ETH-specified fee
afterSwap: true, // cap ledger, ETH-unspecified fee, graduation, Trade
beforeDonate: false,
afterDonate: false,
beforeSwapReturnDelta: true,
afterSwapReturnDelta: true,
afterAddLiquidityReturnDelta: false,
afterRemoveLiquidityReturnDelta: false
});
}
/// Rogue-pool defense: nobody can initialize ANY pool that references this
/// hook. Our own create() initializes via poolManager with the hook itself
/// as msg.sender, which skips this callback entirely (core noSelfCall).
function _beforeInitialize(address, PoolKey calldata, uint160) internal pure override returns (bytes4) {
revert("pmav: pools only via create()");
}
/// During the curve only the hook may add liquidity (third-party liquidity
/// inside the curve range would absorb buys and desync graduation
/// economics). The hook's own adds skip this callback (noSelfCall); after
/// graduation anyone may LP (v1 parity: community LPs on the V3 pool).
function _beforeAddLiquidity(address, PoolKey calldata key, ModifyLiquidityParams calldata, bytes calldata)
internal
view
override
returns (bytes4)
{
require(pools[key.toId()].graduated, "curve phase");
return BaseHook.beforeAddLiquidity.selector;
}
// ------------------------------------------------------------ beforeSwap
function _beforeSwap(address, PoolKey calldata key, SwapParams calldata params, bytes calldata)
internal
override
returns (bytes4, BeforeSwapDelta, uint24)
{
PoolId id = key.toId();
Pool storage p = pools[id];
// graduated: PURE PASSTHROUGH — no caps, no premium, no deltas.
// (graduation's own inner swap never reaches here: noSelfCall.)
if (p.graduated) return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0);
require(!_swapLock, "reentrancy");
_swapLock = true;
_pendingFee = 0;
_pendingRefund = 0;
// (a) block+1 — the Noxa killer feature, enforced at pool level.
// (the dev buy never reaches this: hook-initiated swaps skip callbacks)
require(block.number > p.launchBlock, "trading opens next block");
// decaying anti-snipe premium: buys only, frozen params, base 1% always
uint256 feeBps = FEE_BPS + (params.zeroForOne ? _antiSnipeBps(p) : 0);
// (b) 2% first-hour cap — FILL TO CAP + AUTO REFUND, never a failed buy
uint256 refund = 0;
if (params.zeroForOne && block.timestamp < p.launchTime + FIRST_HOUR) {
uint256 capLeft = FIRST_HOUR_MAX_BUY - firstHourBought[id][tx.origin];
require(capLeft > 0, "first hour: cap reached");
if (params.amountSpecified < 0) {
// buy exactIn (the universal path): clamp to the ETH the cap allows
uint256 maxGross = _maxGrossForCap(id, capLeft, feeBps);
require(maxGross > 0, "first hour: cap reached");
uint256 gross = uint256(-params.amountSpecified);
if (gross > maxGross) refund = gross - maxGross;
} else {
// buy exactOut cannot be partially filled (the router TAKEs the
// exact requested output; no refund exists to give)
require(uint256(params.amountSpecified) <= capLeft, "first hour: max 2% per wallet");
}
}
// (c) ETH-denominated fee, taken on the SPECIFIED side when ETH is
// specified (buy exactIn / sell exactOut). Otherwise afterSwap takes it.
if (_specifiedIsEth(params)) {
uint256 ethGross = _abs(params.amountSpecified) - refund;
uint256 fee = ethGross * feeBps / 10_000;
if (fee > 0) {
poolManager.mint(address(this), ETH_ID, fee); // ERC6909 claim
_credit(p, fee);
}
if (refund > 0) {
// refund waterfall (ruling 19.B). Both branches charge the
// swapper identically through the returned hook delta; the
// buy SUCCEEDS unconditionally, at any PM balance, under
// settle-first AND settle-after routers.
if (address(poolManager).balance >= refund) {
// 1: instant auto-refund, real ETH, straight back to the
// signer (the common path — live PM float dwarfs any
// single-tx refund)
poolManager.take(CurrencyLibrary.ADDRESS_ZERO, tx.origin, refund);
} else {
// 2: floatless PM under a settle-after router — queue the
// surplus as the hook's own ERC6909 claim (float-free
// bookkeeping, backed by the buyer's own settle moments
// later); pull via claimRefund()
poolManager.mint(address(this), ETH_ID, refund);
refunds[tx.origin] += refund;
emit RefundQueued(p.coin, tx.origin, refund);
}
}
_pendingFee = fee;
_pendingRefund = refund;
uint256 hookTake = fee + refund;
if (hookTake > 0) {
return (BaseHook.beforeSwap.selector, toBeforeSwapDelta(int128(uint128(hookTake)), 0), 0);
}
}
return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0);
}
// ------------------------------------------------------------- afterSwap
/// `delta` is the raw pool swap result from the swapper's perspective
/// (positive = swapper receives), BEFORE hook deltas are charged to the
/// swapper. Order of operations is load-bearing: fee -> cap -> Trade ->
/// graduation.
function _afterSwap(address, PoolKey calldata key, SwapParams calldata params, BalanceDelta delta, bytes calldata)
internal
override
returns (bytes4, int128)
{
PoolId id = key.toId();
Pool storage p = pools[id];
if (p.graduated) {
// PURE PASSTHROUGH for pricing/caps/fees — but the v1-named Trade
// event ships on EVERY swap forever (indexer compatibility, §14).
// Graduation's own inner swap never reaches here (noSelfCall).
uint256 e = _abs(int256(delta.amount0()));
uint256 t = _abs(int256(delta.amount1()));
(uint160 sq,,,) = poolManager.getSlot0(id);
emit Trade(p.coin, tx.origin, params.zeroForOne, e, e, t, sq, p.tokensSold, p.raisedWei);
return (BaseHook.afterSwap.selector, 0);
}
int128 hookDelta = _settleCurveSwap(p, id, params, delta);
// (d) graduation trigger: price-based — the bottom edge is reached iff
// ~all 800M curve tokens are sold. Sells move price AWAY from it.
if (params.zeroForOne) {
(uint160 sqrtNow,,,) = poolManager.getSlot0(id);
if (sqrtNow <= SQRT_GRAD) {
require(gasleft() >= GRADUATION_GAS, "graduation buy needs more gas");
_graduate(key, p, id);
}
}
return (BaseHook.afterSwap.selector, hookDelta);
}
/// fee (ETH-unspecified quadrants), first-hour ledger, bookkeeping and the
/// Trade event for a curve-phase swap
function _settleCurveSwap(Pool storage p, PoolId id, SwapParams calldata params, BalanceDelta delta)
internal
returns (int128 hookDelta)
{
// what the pool actually moved (completing buys may consume less than
// stated: unconsumed input through empty liquidity is never taken)
uint256 ethPool = _abs(int256(delta.amount0()));
uint256 tokenAmount = _abs(int256(delta.amount1()));
uint256 fee;
if (_specifiedIsEth(params)) {
fee = _pendingFee; // already credited in beforeSwap
} else {
// (b') ETH fee when ETH is the UNSPECIFIED currency
// (buy exactOut / sell exactIn)
uint256 feeBps = FEE_BPS + (params.zeroForOne ? _antiSnipeBps(p) : 0);
fee = ethPool * feeBps / 10_000;
if (fee > 0) {
poolManager.mint(address(this), ETH_ID, fee);
_credit(p, fee);
hookDelta = int128(uint128(fee)); // taken from swapper's ETH side
}
}
// (c) first-hour ledger: record the ACTUAL fill (clamping already
// happened in beforeSwap; the require is a belt-and-braces invariant)
if (params.zeroForOne && block.timestamp < p.launchTime + FIRST_HOUR) {
uint256 bought = firstHourBought[id][tx.origin] + tokenAmount;
require(bought <= FIRST_HOUR_MAX_BUY, "cap invariant");
firstHourBought[id][tx.origin] = bought;
}
_swapLock = false;
// bookkeeping + parity event (indexer contract, §14). For buys the
// pool ETH is the net (fee charged on top, v1 exact); for sells it is
// the gross (fee taken out of it).
if (params.zeroForOne) {
p.tokensSold += uint128(tokenAmount);
p.raisedWei += uint128(ethPool);
} else {
// SATURATING (correctness audit F-3, defense-in-depth): these are
// informational counters only — the graduation trigger is
// price-based (sqrtPrice <= SQRT_GRAD read from pool state), never
// counter-based, so flooring at 0 changes no economics. Underflow
// is already unreachable by conservation (a sell can never return
// more tokens/ETH to the curve than the curve ever sold), but a
// sell must NEVER revert on an arithmetic underflow: a reverting
// sell is the #1 honeypot signature, and "sells never revert, ever"
// is a hard invariant of our scanner-clean posture. Make it
// structurally guaranteed rather than conservation-guaranteed.
uint128 t = uint128(tokenAmount);
uint128 e = uint128(ethPool);
p.tokensSold = p.tokensSold > t ? p.tokensSold - t : 0;
p.raisedWei = p.raisedWei > e ? p.raisedWei - e : 0;
}
_emitTrade(
p,
id,
params.zeroForOne,
params.zeroForOne ? ethPool + fee : ethPool,
// saturate: a sell-exactOut demanding more ETH than the curve
// holds partially fills; the credited fee can exceed the fill
params.zeroForOne ? ethPool : (ethPool > fee ? ethPool - fee : 0),
tokenAmount
);
}
// ------------------------------------------------------------ graduation
/// Runs INSIDE the completing buy's afterSwap, all in one transaction,
/// inside the router's active unlock. Same poolId forever: no migration,
/// no pool switch, no window where the coin is untradeable. The inner
/// swap/modifyLiquidity calls skip all hook callbacks (noSelfCall).
function _graduate(PoolKey calldata key, Pool storage p, PoolId id) internal {
p.graduated = true; // set FIRST (idempotence)
// 1. remove the whole curve position: hook is credited the raise
// (~4.274 ETH + any donate() dust as fees) and unsold token dust
(BalanceDelta d,) = poolManager.modifyLiquidity(
key,
ModifyLiquidityParams({
tickLower: TICK_LOWER,
tickUpper: TICK_UPPER,
liquidityDelta: -int256(uint256(L_CURVE)),
salt: 0
}),
""
);
uint256 ethAvail = uint256(uint128(d.amount0()));
uint256 tokenCredit = uint256(uint128(d.amount1()));
// 2. repair the boundary overshoot: the completing swap may have walked
// price below TICK_LOWER through empty liquidity. With zero pool
// liquidity a 1-wei-specified swap walks price back UP to exactly
// the graduation price at zero cost (input is never consumed).
(uint160 sqrtP,,,) = poolManager.getSlot0(id);
if (sqrtP < SQRT_GRAD) {
poolManager.swap(
key, SwapParams({zeroForOne: false, amountSpecified: -1, sqrtPriceLimitX96: SQRT_GRAD}), ""
);
}
// 3. flip the dynamic LP fee: plain 1% forever (hook takes no deltas
// post-graduation; scanners see a vanilla 1% V4 pool)
poolManager.updateDynamicLPFee(key, GRADUATED_LP_FEE);
// 4. mint the permanently locked full-range position: the entire raise
// + the 200M reserve, amounts computed at the graduation price.
// (-1 wei headroom on both sides: modifyLiquidity rounds owed
// amounts UP, the liquidity helpers round L DOWN — the headroom
// guarantees the owed amounts never exceed what the hook holds.)
uint128 lFull;
{
uint128 lEth = LiquidityAmounts.getLiquidityForAmount0(SQRT_GRAD, SQRT_FULL_UPPER, ethAvail - 1);
uint128 lTok = LiquidityAmounts.getLiquidityForAmount1(SQRT_FULL_LOWER, SQRT_GRAD, LP_SUPPLY - 1);
lFull = lEth < lTok ? lEth : lTok;
}
(BalanceDelta md,) = poolManager.modifyLiquidity(
key,
ModifyLiquidityParams({
tickLower: FULL_TICK_LOWER,
tickUpper: FULL_TICK_UPPER,
liquidityDelta: int256(uint256(lFull)),
salt: 0
}),
""
);
uint256 ethUsed = uint256(uint128(-md.amount0()));
uint256 tokUsed = uint256(uint128(-md.amount1()));
// 5. settle the token side from the hook's 200M reserve (ERC20 custody
// since create), netting the removal's token-dust credit
if (tokUsed > tokenCredit) {
poolManager.sync(key.currency1);
require(PmavCoin(p.coin).transfer(address(poolManager), tokUsed - tokenCredit), "transfer");
poolManager.settle();
} else if (tokenCredit > tokUsed) {
// (only reachable via absurd token donations) burn the surplus
poolManager.take(key.currency1, DEAD, tokenCredit - tokUsed);
}
// 6. dust sweep, v1 parity: leftover ETH wei -> platformFees (as ERC6909
// claims, which also zeroes the hook's remaining positive delta);
// leftover coin dust -> dead address
uint256 ethDust = ethAvail - ethUsed;
if (ethDust > 0) {
poolManager.mint(address(this), ETH_ID, ethDust);
platformFees += ethDust;
}
uint256 coinDust = PmavCoin(p.coin).balanceOf(address(this));
if (coinDust > 0) PmavCoin(p.coin).transfer(DEAD, coinDust);
emit Graduated(p.coin, PoolId.unwrap(id), ethUsed, tokUsed);
}
// ---------------------------------------------------------------- launch
/// @notice Launch a coin. Costs nothing beyond gas; msg.value (optional)
/// is the dev buy, executed as the first purchase in the same
/// transaction (candle #1 on every terminal) and subject to the
/// same 2% first-hour cap and anti-snipe premium as everyone else.
function create(
string calldata name,
string calldata symbol,
string calldata metadata,
address feeRecipient,
uint256 minTokensOut
) external payable nonReentrant returns (address token) {
require(bytes(name).length > 0 && bytes(name).length <= 64, "name");
// ticker capped at 12 so every $TICKER stays a clickable X cashtag
require(bytes(symbol).length > 0 && bytes(symbol).length <= 12, "symbol");
require(bytes(metadata).length <= 8192, "metadata too big");
// CREATE, not CREATE2: the coin address is unknowable pre-tx, so the
// PoolKey (and pool id) cannot be squatted or front-run
token = address(new PmavCoin(name, symbol)); // 1B minted to the hook
address recipient = feeRecipient == address(0) ? msg.sender : feeRecipient;
PoolKey memory key = _register(token, recipient);
emit CoinCreated(token, PoolId.unwrap(key.toId()), msg.sender, recipient, name, symbol, metadata);
// our own initialize skips beforeInitialize (noSelfCall); every
// external initialize attempt against this hook reverts there
poolManager.initialize(key, SQRT_START);
// seed the curve position (+ optional dev buy) inside one unlock
poolManager.unlock(abi.encode(uint8(1), abi.encode(key, msg.sender, msg.value, minTokensOut)));
}
function _register(address token, address recipient) internal returns (PoolKey memory key) {
key = poolKeyOf(token);
PoolId id = key.toId();
pools[id] = Pool({
coin: token,
feeRecipient: recipient,
launchBlock: uint64(block.number),
launchTime: uint64(block.timestamp),
snipePeakBps: uint16(antiSnipePeakBps),
snipeWindow: uint16(antiSnipeWindow),
graduated: false,
tokensSold: 0,
raisedWei: 0
});
poolOf[token] = id;
allCoins.push(token);
lastActivity[token] = block.timestamp;
}
function coinCount() external view returns (uint256) {
return allCoins.length;
}
/// canonical PoolKey for a pmav coin — fully deterministic, never stored
function poolKeyOf(address token) public view returns (PoolKey memory) {
return PoolKey({
currency0: CurrencyLibrary.ADDRESS_ZERO, // native ETH, always currency0
currency1: Currency.wrap(token),
fee: LPFeeLibrary.DYNAMIC_FEE_FLAG,
tickSpacing: TICK_SPACING,
hooks: this
});
}
// ------------------------------------------------------- unlock callback
/// dispatch for the hook's own unlock operations:
/// 1 = create (seed curve + dev buy), 2 = payout (burn claims + take ETH),
/// 3 = collect graduated pool fees
function unlockCallback(bytes calldata data) external onlyPoolManager returns (bytes memory) {
(uint8 action, bytes memory payload) = abi.decode(data, (uint8, bytes));
if (action == 1) {
(PoolKey memory key, address creator, uint256 value, uint256 minTokensOut) =
abi.decode(payload, (PoolKey, address, uint256, uint256));
_createCallback(key, creator, value, minTokensOut);
} else if (action == 2) {
(address to, uint256 amount) = abi.decode(payload, (address, uint256));
poolManager.burn(address(this), ETH_ID, amount);
poolManager.take(CurrencyLibrary.ADDRESS_ZERO, to, amount);
} else if (action == 3) {
(address token) = abi.decode(payload, (address));
_collectCallback(token);
} else {
revert("bad action");
}
return "";
}
function _createCallback(PoolKey memory key, address creator, uint256 value, uint256 minTokensOut) internal {
PoolId id = key.toId();
Pool storage p = pools[id];
address token = p.coin;
// seed the curve: at the top of the range the position is 100% tokens
(BalanceDelta d,) = poolManager.modifyLiquidity(
key,
ModifyLiquidityParams({
tickLower: TICK_LOWER,
tickUpper: TICK_UPPER,
liquidityDelta: int256(uint256(L_CURVE)),
salt: 0
}),
""
);
// settle the owed tokens (<= 800M by L_CURVE construction; no ETH owed)
uint256 owedTokens = uint256(uint128(-d.amount1()));
poolManager.sync(key.currency1);
require(PmavCoin(token).transfer(address(poolManager), owedTokens), "transfer");
poolManager.settle();
// dev buy: a REAL PoolManager swap in the creation block
if (value > 0) _devBuy(key, p, creator, value, minTokensOut);
}
/// Because the hook itself is the swapper, beforeSwap/afterSwap do NOT run
/// (noSelfCall) — fee, premium, cap clamp, ledgers and the Trade event are
/// applied inline here with arithmetic identical to beforeSwap.
function _devBuy(PoolKey memory key, Pool storage p, address creator, uint256 value, uint256 minTokensOut)
internal
{
PoolId id = key.toId();
uint256 feeBps = FEE_BPS + _antiSnipeBps(p); // premium at peak (t = 0)
uint256 gross = value;
uint256 refund = 0;
{
uint256 maxGross = _maxGrossForCap(id, FIRST_HOUR_MAX_BUY, feeBps);
if (gross > maxGross) {
refund = gross - maxGross;
gross = maxGross;
}
}
uint256 fee = gross * feeBps / 10_000;
uint256 net = gross - fee;
require(net > 0, "dust buy");
uint256 tokensOut;
{
BalanceDelta sd = poolManager.swap(
key,
SwapParams({
zeroForOne: true,
amountSpecified: -int256(net),
sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
}),
""
);
tokensOut = uint256(uint128(sd.amount1()));
}
require(tokensOut >= minTokensOut, "slippage");
require(tokensOut <= FIRST_HOUR_MAX_BUY, "cap invariant");
// settle net (the swap) + fee (real ETH into the PoolManager,
// immediately re-minted as the hook's ERC6909 fee claim)
poolManager.settle{value: net + fee}();
poolManager.mint(address(this), ETH_ID, fee);
_credit(p, fee);
firstHourBought[id][tx.origin] = tokensOut; // fresh pool: first buy
p.tokensSold += uint128(tokensOut);
p.raisedWei += uint128(net);
poolManager.take(key.currency1, creator, tokensOut);
_emitTrade(p, id, true, gross, net, tokensOut);
if (refund > 0) {
(bool ok,) = creator.call{value: refund}("");
require(ok, "refund failed");
}
}
function _emitTrade(Pool storage p, PoolId id, bool isBuy, uint256 gross, uint256 net, uint256 tokens) internal {
(uint160 sqrtNow,,,) = poolManager.getSlot0(id);
emit Trade(p.coin, tx.origin, isBuy, gross, net, tokens, sqrtNow, p.tokensSold, p.raisedWei);
}
// ------------------------------------------------------------------ fees
/// Split of the TOTAL fee (base 1% + any anti-snipe premium): 70% creator /
/// 30% platform, uniform. Platform takes floor(fee * 3 / 10); the creator
/// gets the remainder, so the odd wei rounds to the CREATOR. The premium is
/// NOT special-cased — it splits 70/30 exactly like the base (so the
/// platform also shares in the sniper premium).
function _credit(Pool storage p, uint256 fee) internal {
uint256 platformShare = fee * 3 / 10; // 30% of the total fee (base + premium)
platformFees += platformShare;
creatorFees[p.feeRecipient] += fee - platformShare; // 70%, odd wei to creator
}
function claimCreatorFees() external nonReentrant {
uint256 amount = creatorFees[msg.sender];
require(amount > 0, "nothing to claim");
creatorFees[msg.sender] = 0;
// claiming is the natural "I'm alive" signal: it protects every coin
// paying this recipient from a CTO. only msg.sender can stamp itself.
lastClaim[msg.sender] = block.timestamp;
poolManager.unlock(abi.encode(uint8(2), abi.encode(msg.sender, amount)));
emit CreatorFeesClaimed(msg.sender, amount);
}
/// @notice Pull a refund queued by the ruling-19.B waterfall (branch 2:
/// an over-cap buy through a settle-after router while the
/// PoolManager's native float could not cover the instant take).
/// Permissionless pull, claimCreatorFees shape: burns the hook's
/// ERC6909 claims and pays real ETH — by claim time the buyer's
/// own settle has long since delivered the backing ETH.
function claimRefund() external nonReentrant {
uint256 amount = refunds[msg.sender];
require(amount > 0, "nothing to claim");
refunds[msg.sender] = 0;
poolManager.unlock(abi.encode(uint8(2), abi.encode(msg.sender, amount)));
emit RefundClaimed(msg.sender, amount);
}
function withdrawPlatformFees(address to) external onlyOwner nonReentrant {
uint256 amount = platformFees;
require(amount > 0, "nothing to withdraw");
platformFees = 0;
poolManager.unlock(abi.encode(uint8(2), abi.encode(to, amount)));
emit PlatformFeesWithdrawn(to, amount);
}
/// @notice Realize a graduated pool's accrued 1% LP fees from the locked
/// full-range position. Permissionless — anyone pokes (v1 locker
/// parity). ETH side split 70% creator / 30% platform into the pull
/// ledgers (so a non-payable recipient can never brick the collect);
/// coin side split the same way and transferred directly (PmavCoin
/// transfers cannot revert or reenter).
function collectPoolFees(address token) external nonReentrant {
Pool storage p = pools[poolOf[token]];
require(p.coin == token && token != address(0), "unknown coin");
require(p.graduated, "not graduated");
poolManager.unlock(abi.encode(uint8(3), abi.encode(token)));
}
function _collectCallback(address token) internal {
PoolKey memory key = poolKeyOf(token);
Pool storage p = pools[key.toId()];
// a zero-liquidity-delta poke realizes feesAccrued as caller deltas
(BalanceDelta fees,) = poolManager.modifyLiquidity(
key,
ModifyLiquidityParams({
tickLower: FULL_TICK_LOWER,
tickUpper: FULL_TICK_UPPER,
liquidityDelta: 0,
salt: 0
}),
""
);
uint256 ethFees = uint256(uint128(fees.amount0()));
uint256 coinFees = uint256(uint128(fees.amount1()));
// graduated-pool LP fees split 70% creator / 30% platform (= 3/10
// platform), odd wei to the CREATOR — identical rounding discipline to
// _credit (there is no anti-snipe premium here).
uint256 platEth = ethFees * 3 / 10;
if (ethFees > 0) {
poolManager.mint(address(this), ETH_ID, ethFees);
creatorFees[p.feeRecipient] += ethFees - platEth;
platformFees += platEth;
}
uint256 platCoin = coinFees * 3 / 10;
if (coinFees > 0) {
poolManager.take(key.currency1, address(this), coinFees);
if (coinFees - platCoin > 0) require(PmavCoin(token).transfer(p.feeRecipient, coinFees - platCoin), "t1");
if (platCoin > 0) require(PmavCoin(token).transfer(owner, platCoin), "t2");
}
emit PoolFeesCollected(token, p.feeRecipient, ethFees - platEth, coinFees - platCoin, platEth, platCoin);
}
// ------------------------------------------------------------ governance
// line-for-line ports from audited v1 Pmav.sol — moved, not rewritten
function setFeeRecipient(address token, address newRecipient) external {
Pool storage p = pools[poolOf[token]];
require(p.coin == token && token != address(0), "unknown coin");
require(msg.sender == p.feeRecipient, "not fee recipient");
require(newRecipient != address(0), "zero addr");
require(!feeRecipientLocked[token], "fee recipient locked");
lastActivity[token] = block.timestamp;
emit FeeRecipientChanged(token, p.feeRecipient, newRecipient);
p.feeRecipient = newRecipient;
}
/// @notice Permanently freeze the CURRENT fee recipient of a coin. One-way.
function lockFeeRecipient(address token) external {
Pool storage p = pools[poolOf[token]];
require(p.coin == token && token != address(0), "unknown coin");
require(msg.sender == p.feeRecipient, "not fee recipient");
require(!feeRecipientLocked[token], "already locked");
feeRecipientLocked[token] = true;
lastActivity[token] = block.timestamp;
emit FeeRecipientLockedEvent(token, p.feeRecipient);
}
/// @notice Atomically REASSIGN the fee recipient and permanently lock it.
function lockFeeRecipientTo(address token, address newRecipient) external {
Pool storage p = pools[poolOf[token]];
require(p.coin == token && token != address(0), "unknown coin");
require(msg.sender == p.feeRecipient, "not fee recipient");
require(newRecipient != address(0), "zero addr");
require(!feeRecipientLocked[token], "already locked");
lastActivity[token] = block.timestamp;
emit FeeRecipientChanged(token, p.feeRecipient, newRecipient);
p.feeRecipient = newRecipient;
feeRecipientLocked[token] = true;
emit FeeRecipientLockedEvent(token, newRecipient);
}
/// @notice CTO (community takeover): redirect an abandoned coin's FUTURE
/// creator rewards to a community lead. The one rail no review can
/// waive: the current DEV must be provably inactive — no fee claim
/// and no per-coin steward action for CTO_INACTIVITY. Trading does
/// NOT protect a dev. Locked coins are immune by design.
function ctoRedirect(address token, address newRecipient) external onlyOwner {
Pool storage p = pools[poolOf[token]];
require(p.coin == token && token != address(0), "unknown coin");
require(newRecipient != address(0), "zero addr");
require(!feeRecipientLocked[token], "fee recipient locked");
uint256 devActive = lastActivity[token];
uint256 claimed = lastClaim[p.feeRecipient];
if (claimed > devActive) devActive = claimed;
require(block.timestamp >= devActive + CTO_INACTIVITY, "dev still active");
lastActivity[token] = block.timestamp; // fresh window for the new lead
emit FeeRecipientChanged(token, p.feeRecipient, newRecipient);
emit CtoRedirect(token, p.feeRecipient, newRecipient);
p.feeRecipient = newRecipient;
}
function setOwner(address newOwner) external onlyOwner {
require(newOwner != address(0), "zero addr");
emit OwnerChanged(owner, newOwner);
owner = newOwner;
}
/// @notice Tune the anti-snipe FACTORY DEFAULTS, bounded by the hard caps.
/// TIGHTENED vs v1: affects FUTURE launches only — every live
/// pool's premium/window were frozen into its struct at create().
function setAntiSnipeDefaults(uint256 peakBps, uint256 window) external onlyOwner {
require(peakBps <= ANTI_SNIPE_MAX_BPS, "peak too high");
require(window <= ANTI_SNIPE_MAX_WINDOW, "window too long");
antiSnipePeakBps = peakBps;
antiSnipeWindow = window;
emit AntiSnipeChanged(peakBps, window);
}
// ---------------------------------------------------------------- quotes
/// @notice Mirror of the buy path for UI parity with v1. APPROXIMATE by a
/// few parts in 1e15: the pool consumes input in tick-word chunks
/// with per-chunk round-up, which a closed-form view cannot
/// replicate — use V4Quoter (which simulates the real swap,
/// hook deltas included) for an exact minTokensOut. `refund` is
/// the curve-completion surplus only; the per-wallet cap clamp
/// depends on the buyer and is not modeled here (v1 parity).
function quoteBuy(address token, uint256 ethGross)
external
view
returns (uint256 tokensOut, uint256 fee, uint256 refund)
{
PoolId id = poolOf[token];
Pool storage p = pools[id];
require(p.coin == token && token != address(0) && !p.graduated, "not on curve");
uint256 feeBps = FEE_BPS + _antiSnipeBps(p);
// v2 exactIn semantics: the fee is charged on the STATED gross (the
// difference vs v1's fee-on-used-gross exists only on the completing
// buy — spec §12, accepted)
fee = ethGross * feeBps / 10_000;
uint256 net = ethGross - fee;
uint160 sqrtP = _effSqrtP(id);
require(net > 0, "dust buy");
uint160 sqrtNext = SqrtPriceMath.getNextSqrtPriceFromInput(sqrtP, L_CURVE, net, true);
if (sqrtNext < SQRT_GRAD) {
// completing buy: only the ETH that finishes the curve is consumed
uint256 maxNet = SqrtPriceMath.getAmount0Delta(SQRT_GRAD, sqrtP, L_CURVE, true);
refund = ethGross - fee - maxNet;
tokensOut = SqrtPriceMath.getAmount1Delta(SQRT_GRAD, sqrtP, L_CURVE, false);
} else {
tokensOut = SqrtPriceMath.getAmount1Delta(sqrtNext, sqrtP, L_CURVE, false);
}
}
function quoteSell(address token, uint256 tokenAmount) external view returns (uint256 ethNet, uint256 fee) {
PoolId id = poolOf[token];
Pool storage p = pools[id];
require(p.coin == token && token != address(0) && !p.graduated, "not on curve");
require(tokenAmount > 0, "zero amount");
uint160 sqrtP = _effSqrtP(id);
uint160 sqrtNext = SqrtPriceMath.getNextSqrtPriceFromInput(sqrtP, L_CURVE, tokenAmount, false);
if (sqrtNext > SQRT_START) sqrtNext = SQRT_START; // beyond the curve top the pool stops paying
uint256 ethGross = SqrtPriceMath.getAmount0Delta(sqrtP, sqrtNext, L_CURVE, false);
fee = ethGross * FEE_BPS / 10_000;
ethNet = ethGross - fee;
}
/// @notice spot price in wei per whole token, and market cap in wei.
/// v1-shaped for the site; `pool` is the PoolManager once
/// graduated (v1 semantics: nonzero pool == trade on the DEX).
function state(address token)
external
view
returns (
uint256 priceWei,
uint256 mcapWei,
uint256 ethReserve,
uint256 tokensSold,
uint256 progressBps,
bool graduated,
address pool
)
{
PoolId id = poolOf[token];
Pool storage p = pools[id];
require(p.coin == token && token != address(0), "unknown coin");
(uint160 sqrtP,,,) = poolManager.getSlot0(id);
// ETH per whole token = (2^96 / sqrtP)^2 * 1e18 (both sides 18 decimals)
priceWei = FullMath.mulDiv(1 << 192, 1e18, uint256(sqrtP) * uint256(sqrtP));
mcapWei = priceWei * 1_000_000_000;
ethReserve = p.raisedWei;
tokensSold = p.tokensSold;
progressBps = uint256(p.tokensSold) * 10_000 / CURVE_SUPPLY;
graduated = p.graduated;
pool = p.graduated ? address(poolManager) : address(0);
}
function feeRecipientOf(address token) external view returns (address) {
return pools[poolOf[token]].feeRecipient;
}
// ------------------------------------------------------------- internals
/// specified currency = input for exactIn, output for exactOut; ETH is
/// always currency0, so specified is ETH iff zeroForOne == exactInput
function _specifiedIsEth(SwapParams calldata params) internal pure returns (bool) {
return params.zeroForOne == (params.amountSpecified < 0);
}
function _abs(int256 x) internal pure returns (uint256) {
return x < 0 ? uint256(-x) : uint256(x);
}
/// current curve price for quote/clamp math, bounded to the curve range:
/// a sell can park the spot price ABOVE the curve top through empty
/// liquidity; buys from there walk back down for free, so all token/ETH
/// math must start at the top of the range, not the empty-space price
function _effSqrtP(PoolId id) internal view returns (uint160 sqrtP) {
(sqrtP,,,) = poolManager.getSlot0(id);
if (sqrtP > SQRT_START) sqrtP = SQRT_START;
}
/// max gross ETH (fee included) a first-hour buy may spend so that the
/// delivered tokens never exceed capLeft. Rounding discipline (test
/// enforced): the sqrt target rounds UP (fewer tokens) and the ETH amount
/// rounds DOWN, so core's forward math can never deliver > capLeft.
function _maxGrossForCap(PoolId id, uint256 capLeft, uint256 feeBps) internal view returns (uint256 maxGross) {
uint160 sqrtP = _effSqrtP(id);
if (sqrtP <= SQRT_GRAD) return 0;
// sqrt price after the position hands out capLeft tokens (quotient
// floored => target rounds UP => tokens at target <= capLeft)
uint256 quotient = FullMath.mulDiv(capLeft, FixedPoint96.Q96, L_CURVE);
bool gradBound = quotient >= sqrtP || sqrtP - quotient <= SQRT_GRAD;
uint160 sqrtTarget = gradBound ? SQRT_GRAD : uint160(sqrtP - quotient);
// when the graduation edge binds the fill is bounded by the curve
// itself, so rounding UP is safe (it just guarantees graduation)
uint256 maxNet = SqrtPriceMath.getAmount0Delta(sqrtTarget, sqrtP, L_CURVE, gradBound);
if (maxNet == 0) return 0;
maxGross = FullMath.mulDiv(maxNet, 10_000, 10_000 - feeBps);
// exactness guard: the swap path will charge fee = floor(g*f/1e4) and
// hand the pool g - fee; walk g down (<=2 iterations) until that net
// cannot exceed maxNet
while (maxGross > 0 && maxGross - (maxGross * feeBps / 10_000) > maxNet) {
maxGross--;
}
}
/// Anti-snipe premium (bps) at the current time for a pool: linear decay
/// from the pool's FROZEN peak at launch to 0 at its frozen window edge.
function _antiSnipeBps(Pool storage p) internal view returns (uint256) {
uint256 window = p.snipeWindow;
if (window == 0) return 0;
uint256 elapsed = block.timestamp - p.launchTime;
if (elapsed >= window) return 0;
return uint256(p.snipePeakBps) * (window - elapsed) / window;
}
/// only the PoolManager sends ETH here (refund flows go PM -> tx.origin
/// directly; claim payouts go PM -> recipient directly). The hook itself
/// holds ETH only transiently inside create().
receive() external payable {
require(msg.sender == address(poolManager), "direct eth");
}
}
Chain explorer3454msChain node92ms