Dual Pools
On V4-family chains every graduated token gets two pools: a primary pool against the chain's native asset, and a secondary pool against $LP24. The secondary is funded at graduation from the same envelope and exists to tie every launched token to the protocol token.
Uniswap V3 has no hook mechanism, so Gnosis runs a single native↔token pool managed by GraduationManager. Everything on this page applies to Base, BSC, Polygon and Robinhood.
The two pools
| Primary | Secondary | |
|---|---|---|
| Pair | native ↔ token | token ↔ $LP24 |
| Owner | PrimaryHook | SecondaryHook |
| Fee | Dynamic — 120 bps (1.2%) / 80 bps (0.8%) / 40 bps (0.4%) | Flat 60 bps (0.6%) |
| Fee currency | Native | The token |
| Fee destination | Split 30 / 10 / 60 | Burned to 0x…dEaD |
| Fee direction | Both | Both |
| Created | At graduation, automatically | By a later permissionless call |
| Liquidity | 90% of envelope | 10% of envelope |
How the secondary is funded
Funding and pool creation are deliberately separated. Graduation must not depend on a second pool's price being sane at that instant, so the graduation transaction only delivers and earmarks the funds.
At graduation, inside createPositionAndRegister:
├─→ IERC20(token).transfer(SECONDARY_HOOK, SECONDARY_TOKEN_RAW)
├─→ SECONDARY_HOOK.notifyReserved(token) {value: SECONDARY_NATIVE_RAW}
│ └─→ nativeEarmark[token] += msg.value
└─→ log SecondaryReserved(token, deployer)
Later, by anyone:
SecondaryHook.createSecondary(token)
├─→ assert PrimaryHook.tokenInfo(token).deployer != 0
├─→ assert no secondary exists yet
├─→ assert balanceOf(self) >= SECONDARY_TOKEN_RAW
├─→ assert nativeEarmark[token] >= SECONDARY_NATIVE_RAW (and consume it)
├─→ swap earmarked native → $LP24 on the primary $LP24 pool
├─→ initialize token/$LP24 pool at the resulting ratio
└─→ mint a full-range position owned by this contract
Both transfers happen in the graduation transaction, which gives the invariant the secondary relies on: if the SecondaryHook holds a token, it holds that token's native share too. The native push is asserted rather than fire-and-forget — a failure there would mean the secondary could never be funded for that token.
An earlier design inferred the native share from a token balance: "40M of this token is here, therefore 0.4 native is here too." That holds only if token balances can arrive solely from the PrimaryHook, which nothing enforces — anyone can send ERC-20s to a contract. nativeEarmark[token] is credited only by notifyReserved(), and only the PrimaryHook may call it, so an attacker who sends a graduated token's 40M to the SecondaryHook cannot create a pool using some other token's native.
Acquiring the $LP24 side
The secondary pool needs $LP24, not native, so createSecondary swaps the earmarked native for $LP24 on the primary $LP24 pool first. This is an ordinary swap — the SecondaryHook is just another swapper and pays the PrimaryHook's normal tiered fee. There is no fee-exemption path between the two contracts, which is what keeps them decoupled: the PrimaryHook needs no knowledge of the SecondaryHook beyond the address it pushes funds to.
Slippage on that swap is bounded at 200 bps (2%), and the expected output is derived from the $LP24 pool's reserves rather than its spot price:
native_reserve ≈ L * 2^96 / sqrtP
plft_reserve ≈ L * sqrtP / 2^96
expected_out ≈ plft_reserve * dx / (native_reserve + dx)
A reserve-derived expectation is manipulation-resistant. A sandwich that merely shifts spot price leaves real reserves untouched, so the expected figure barely moves; to push realized output below the floor an attacker would have to actually remove liquidity — expensive and visible. A revert here is harmless: the custodied funds and the earmark stay put and the call can be retried.
Secondary pool fees
The secondary charges a flat 60 bps (0.6%) in the launched token, in both directions, and burns it immediately. There are no tiers, no reinvestment, and no deployer or protocol share.
- Token is the input —
beforeSwaptakes the fee from the specified amount. - Token is the output —
afterSwaptakes the fee from the output delta.
In both cases take() sends the fee straight to the dead address. take() debits the caller — this hook — regardless of recipient, which clears the delta the callback returns, so a single call both burns the tokens and settles the pool's accounting.
Why a secondary pool at all
Every graduated token has a permanent, protocol-owned market against $LP24. Trading it burns supply of the launched token, and routing between two venues for the same asset creates arbitrage that flows through the $LP24 pools. Because $LP24 lives at the same address on all five chains and those chains settle in four different native assets, price divergence across chains is arbitrageable — and that arbitrage passes through pools whose fees feed the protocol's buy-and-burn.