GraduationManager
The Gnosis equivalent of PrimaryHook. Uniswap V3 has no hook mechanism, so fees must be collected explicitly and the distribution logic differs — but the external surface is deliberately close to the V4 hooks.
Singleton — Gnosis only. Manages WNATIVE/token V3 pools at the 1% fee tier. No owner, no admin keys. There is no secondary pool on this chain.
Token-side LP fees are never sold to the market. The deployer (40%) and protocol (20%) cuts come exclusively from native-side fees. Token-side fees are bucketed in pendingTokens and either burned or paired back into liquidity. Buy-and-burn genuinely buys tokens, reducing supply — the inverse of selling — so it does not violate this.
Pool Lifecycle
preInitializePool(token: address) externalInitialize the V3 pool atomically with token deployment. Called by BondingCurveTemplate.initialize(), itself called by the factory inside deploy_pair.
This closes the V3 pre-init griefing window: a frontrunner who already initialized the pool at a different price causes this to revert, which reverts the whole deployment. The deployer retries with a fresh salt.
PoolPreInitialized(token, pool, sqrtPriceX96)
createPositionAndRegister(token: address, deployer: address) -> uint256 external payableCallable only by a factory-registered curve. Wraps the native asset, re-reads slot0 and asserts the price still matches the expected value as defense in depth, then mints a full-range position. Excess native and residual WNATIVE go to protocolBalance.
"Only a factory curve may graduate""Already registered""Invalid deployer""Insufficient graduation funds""Pool sqrtPrice mismatch"— pool was re-initialized at a different price
TokenRegistered(token, deployer, positionId)
Fee Functions
collectFees(token: address) externalPull accrued V3 fees out of the position into the contract's buckets. Anyone can call. This step has no V4 equivalent — there, fees arrive through the swap callbacks.
- Native side → unwrapped, then
pendingFees[token](orprotocolBalancefor $LP24) - Token side →
pendingTokens[token], above the dust threshold
There is no dust threshold on the native side — dropping sub-threshold collections would leak native into untracked contract balance.
"Not registered","No position"
FeesCollected(token, nativeAmount)
distributeFees(token: address) externalSplit collected native fees 40 / 20 / 40 and process the token bucket. Anyone can call. Pool state is read and validated before any state change, so the failure mode is deterministic across both paths.
The processed chunk is capped at max_safe × 9/4 so the reinvest swap stays inside the MEV-safe band. Only the processed chunk is split; the remainder carries forward un-split so it is never taxed twice.
- Burn all pending tokens outright
- Buy-and-burn the entire reinvest budget — no LP'ing
- Pair pending tokens with the reinvest budget into liquidity
- Burn excess tokens; run leftover native through buy-half-and-LP
"Nothing to distribute"— at or below dust"No position""Pool not initialized""Pool too thin for any safe swap"
FeesDistributed, and some of TokensBurned, TokenFeesLPed, BuyAndBurn, Reinvested
processProtocolFees() externalProcess the protocol balance against the $LP24 pool, using the same DEEP/THIN logic. Anyone can call.
withdrawDeployerFees(token: address) externalDeployer only. Same semantics as the V4 hook.
creditProtocolBalance() external payableAccept a protocol share from a bonding curve's refund or sweep.
CTO
initiate_cto_vote, finalize_cto and cancel_failed_cto behave identically to the V4 hook, including the state-first best-effort transition in _changeDeployer. The vote fee on Gnosis is 24 xDAI.
View Functions
| Function | Returns |
|---|---|
getTokenInfo(token) -> TokenInfo | (deployer, positionId) |
getPendingFees(token) -> uint256 | Native fees awaiting distribution |
getPendingTokens(token) -> uint256 | Token-side fees awaiting burn-or-LP |
getDeployerBalance(token) -> uint256 | Withdrawable deployer balance |
isTokenRegistered(token) -> bool | Whether the token graduated |
canDistribute(token) -> bool | Whether distribution would succeed now |
canProcessProtocolFees() -> bool | Whether protocol processing would succeed now |
getPoolLiquidity(token) -> (uint128, uint160, int24) | (liquidity, sqrtPriceX96, tick) |
getPoolAddress(token) -> address | The V3 pool contract for this pair |
getFeeInfo(token) -> (uint256, uint256) | Fee bps and native depth |
getSlippageInfo(token, swap_amount) | Expected slippage figures for a hypothetical swap |
getMaxSafeSwap(token) -> uint256 | The current MEV-capped swap ceiling |
canInitiateCTO(token) -> bool | Registered, no active vote |
getActiveCTOVote(token) -> address | Current vote contract, or zero |
getCTOVoteCount(token) -> uint256 | Attempts to date |
Constants
| Constant | Value |
|---|---|
POOL_FEE | 10000 (1% V3 tier) |
GRADUATION_NATIVE | 9,600 xDAI |
GRADUATION_TOKENS | 400,000,000 × 10⁹ (400M) |
DISTRIBUTE_THRESHOLD | 96 xDAI |
PROTOCOL_THRESHOLD | 240 xDAI |
POOL_DEPTH_THRESHOLD | 48,000,000 × 10⁹ (48M tokens) |
SLIPPAGE_TIER_1_BPS | 480 bps (4.8%) |
CAP_MULT_BPS | 100 — 1.00× the pool fee, so the cap is 1% |
CTO_VOTE_FEE | 24 xDAI |
CTO_INITIATOR_STAKE | 60,000,000 × 10⁹ (60M, 5% of supply) |
MIN_TOKEN_DUST | 1,000,000 |
INITIAL_SQRT_PRICE | 511415589947267542714504636 |
Gnosis has one static fee tier, so the MEV cap is a flat constant rather than being derived from a live tier read. Break-even still sits at roughly one fee tier, since an atomic sandwich pays two legs of it.