PrimaryHook
The post-graduation manager for the native↔token pool on V4-family chains. Implements the swap callbacks, owns the LP position, and provides permissionless fee distribution, reinvestment, buy-and-burn and CTO voting.
Singleton — one per chain, on Base, BSC, Polygon and Robinhood. Uniswap V4 hook (PancakeSwap Infinity port on BSC). No owner, no admin keys. Gnosis uses GraduationManager instead.
Graduation
createPositionAndRegister(token: address, deployer: address) -> uint256 external payableCallable only by a factory-registered bonding curve. Initializes the pool, mints the full-range position, registers the token, and pushes the secondary envelope. Returns the position id.
$LP24 takes the full envelope with no split. Every other token mints 90% into the primary pool and pushes 10% to the SecondaryHook in the same transaction.
"Only a factory curve may graduate"—Factory.is_curve(msg.sender)is false"Already registered""Invalid deployer""Insufficient graduation funds"—msg.value < GRADUATION_NATIVE"Secondary native push failed"
TokenRegistered(token, deployer, positionId), then SecondaryReserved(token, deployer) for non-protocol tokens
creditProtocolBalance() external payableAccept a protocol share. Called by bonding curves on refund initiation and on sweep. Adds msg.value to protocolBalance.
Fee Distribution
distributeFees(token: address) externalSplit pending fees 30% deployer, 10% protocol, 60% reinvested. Anyone can call. Not for $LP24.
Deployer and protocol shares are credited and pendingFees zeroed before the reinvest swap. If the MEV cap prevents the full reinvest amount being processed, the remainder is added to protocolBalance rather than back to pendingFees — it has already been split and must not be taxed twice.
"Use processProtocolFees"— token is $LP24"No position"— token not registered"No fees"—pendingFeesis zero
FeesDistributed(token, total, reinvested), then Reinvested(...) or BuyAndBurn(...)
processProtocolFees() externalProcess the accumulated protocol balance against the $LP24 pool. Anyone can call. Buys and burns $LP24 when the pool holds ≥ 24,000,000 tokens; reinvests to deepen the pool when it holds fewer.
Only the MEV-capped chunk is processed per call; the remainder rolls forward.
"Nothing to process"— balance at or below 1,000,000 wei"No protocol token""Pool not initialized""Pool too thin for any safe swap""No position"— on the reinvest branch
BuyAndBurn(token, nativeSpent) or Reinvested(...)
withdrawDeployerFees(token: address) externalDeployer only. Withdraws the full accrued balance for that token. After a successful CTO this is the new deployer.
"No deployer fees for protocol token""Not deployer""No fees"
DeployerWithdrawal(token, deployer, amount)
CTO
initiate_cto_vote(token: address) -> address external payableClone a vote contract and transfer the initiator's stake into it. Returns the vote address. Requires the exact chain fee and 60,000,000 × 10⁹ (60M, 5% of supply) approved to this contract.
"CTO not enabled""Protocol token cannot be CTO'd""Send exact CTO fee"— exact, no overage refund"Token not registered""CTO vote already active""Stake transfer failed"
CTOVoteInitiated(token, vote_contract, initiator)
finalize_cto(token: address, new_deployer: address) externalVote contract only. Transfers the deployer role, mirrors the change into the factory registry, pays out the outgoing deployer's balance (best effort), and transfers socials ownership.
"No active vote""Not vote contract""Invalid new deployer"— zero address would lock fees permanently
DeployerUpdated(token, oldDeployer, newDeployer, paidOut)
cancel_failed_cto(token: address) externalClear a failed vote so a new one can be initiated. Anyone can call.
"No active vote""Vote still active""Vote was finalized""Vote passed - must finalize"
Hook Callbacks
Called by the pool manager. Not for direct use; all three revert for any other caller.
| Callback | Behaviour |
|---|---|
beforeSwap(sender, key, params, hookData) | Taxes buys on the input side. Asserts amountSpecified < 0 — exact-output buys are disabled. Skipped for internal swaps. |
afterSwap(sender, key, params, swapDelta, hookData) | Taxes sells on the native output delta. Skipped for internal swaps. |
beforeInitialize(sender, key, sqrtPriceX96) | Reverts "Pool init not permitted" unless the transient _initializing flag is set — which only createPositionAndRegister sets, and clears immediately after. |
View Functions
| Function | Returns |
|---|---|
getTokenInfo(token) -> TokenInfo | (deployer, positionId) — the authoritative current deployer |
getPendingFees(token) -> uint256 | Undistributed native fees |
getFeeInfo(token) -> (uint256, uint256) | (currentFeeBps, nativeInPool), computed live |
isTokenRegistered(token) -> bool | Whether the token graduated through this hook |
canDistribute(token) -> bool | Pending fees, has a position, and is not $LP24 |
canProcessProtocolFees() -> bool | Protocol balance above dust and $LP24 is set |
getPoolLiquidity(token) -> (uint128, uint160, int24) | (liquidity, sqrtPriceX96, tick) |
getPoolId(token) -> bytes32 | Pool id for the primary pool |
canInitiateCTO(token) -> bool | Registered, no active vote, template configured |
getHookPermissions() -> uint256 | 8396 (0x20CC). On BSC this is getHooksRegistrationBitmap() returning 0x0CC1. |
protocolBalance() -> uint256 | Accumulated protocol share |
deployerBalance(token) -> uint256 | Withdrawable deployer balance |
pendingFees(token) -> uint256 | Public mapping accessor |
poolIdOf(token) -> bytes32 | Cached pool id, populated at registration |
active_cto_votes(token) -> address | Current vote contract, or zero |
cto_vote_count(token) -> uint256 | Attempts to date — informational, uncapped |
Constants
| Constant | Base | BSC | Polygon | Robinhood |
|---|---|---|---|---|
FEE_TIER_1_BPS | 120 bps (1.2%) | |||
FEE_TIER_2_BPS | 80 bps (0.8%) | |||
FEE_TIER_3_BPS | 40 bps (0.4%) | |||
FEE_THRESHOLD_TIER_2 | 12 ETH | 36 BNB | 288,000 POL | 12 ETH |
FEE_THRESHOLD_TIER_3 | 24 ETH | 72 BNB | 576,000 POL | 24 ETH |
GRADUATION_NATIVE | 4 ETH | 12 BNB | 96,000 POL | 4 ETH |
PRIMARY_NATIVE_RAW | 3.6 ETH | 10.8 BNB | 86,400 POL | 3.6 ETH |
SECONDARY_NATIVE_RAW | 0.4 ETH | 1.2 BNB | 9,600 POL | 0.4 ETH |
CTO_VOTE_FEE | 0.01 ETH | 0.03 BNB | 240 POL | 0.01 ETH |
INITIAL_SQRT_PRICE | 25054144837504793118641380156 | 14465017266249264964771739287 | 161723809515207654377831473 | 25054144837504793118641380156 |
GRADUATION_LIQUIDITY | 1264911064067351732 | 2190890230020664453 | 195959179422654247855 | 1264911064067351732 |
PRIMARY_GRADUATION_LIQUIDITY | 1138419957660616558 | 1971801207018598007 | 176363261480388823069 | 1138419957660616558 |
GRADUATION_TOKENS | 400,000,000 × 10⁹ (400M) | |||
PRIMARY_TOKEN_RAW | 360,000,000 × 10⁹ (360M) | |||
SECONDARY_TOKEN_RAW | 40,000,000 × 10⁹ (40M) | |||
CTO_INITIATOR_STAKE | 60,000,000 × 10⁹ (60M, 5% of supply) | |||
MAX_REINVEST_SLIPPAGE_BPS | 480 bps (4.8%) | |||
CAP_MULT_BPS | 100 (1.00× the live fee tier) | |||
MIN_TOKEN_DUST | 1,000,000 | |||
TICK_SPACING | 200 | |||
POOL_FEE | 0 — fees taken by the hook | |||