Protocol Architecture
LP24 is a set of Vyper 0.4.3 contracts that manage the full token lifecycle. Tokens, bonding curves and CTO votes are deployed as minimal proxies (EIP-1167); everything else is a singleton.
Contract Overview
| Contract | Pattern | Chains | Role |
|---|---|---|---|
Factory | Singleton | All | Entry point. Deploys the protocol pair once, then user token + curve pairs via CREATE2 with vanity enforcement. Owns the deployer-token registry and the curve registry. |
TokenTemplate | Minimal proxy | All | ERC-20 with minter-only mint/burn, transfers locked until graduation, 9 decimals. |
BondingCurveTemplate | Minimal proxy | All | Curve trading, pricing, graduation trigger, refund system with claim window and sweep. |
PrimaryHook | Singleton (V4 hook) | V4 family | Primary native↔token pool: graduation, swap fees, distribution, reinvestment, buy-and-burn, CTO. |
SecondaryHook | Singleton (V4 hook) | V4 family | Token↔$LP24 pool per graduated token. Flat 60 bps (0.6%) fee taken in the token and burned. |
GraduationManager | Singleton | Gnosis | V3 equivalent of PrimaryHook. Manual fee collection, adaptive DEEP/THIN distribution, CTO. No secondary pool. |
CTOVoteTemplate | Minimal proxy | All | Community Takeover voting. Cloned per vote by the hook or manager. |
TokenSocials | Singleton | All | On-chain metadata registry. Packed bytes32 storage, owner tracked per token. |
TokenVesting | Singleton | All | Ownerless release of the protocol token's initial mint. 24 weeks cliff, then 240,000 × 10⁹ (240k tokens) per week. |
V4 family — Base, Polygon and Robinhood on Uniswap V4; BSC on PancakeSwap Infinity. These run PrimaryHook + SecondaryHook and get two pools per token. The Infinity port differs only in the integration layer: a six-field PoolKey with hooks third and tick spacing folded into parameters, settlement against the Vault rather than the pool manager, different callback selectors, and getHooksRegistrationBitmap() instead of address-mined permission bits.
V3 family — Gnosis on Uniswap V3, running GraduationManager. V3 has no hooks, so fees must be collected explicitly and the split differs (40 / 20 / 40 rather than 30 / 10 / 60).
Deployment
User → Factory.deploy_pair(name, symbol, salt, packed_socials) {value: deployment_fee}
├─→ validate anchor block + blockhash prefix, within INCLUSION_WINDOW
├─→ effective_salt = keccak256(salt ‖ msg.sender) // binds salt to caller
├─→ create_minimal_proxy_to(token_master, effective_salt) → token
├─→ assert first byte == 0x24 // vanity
├─→ create_minimal_proxy_to(bonding_curve_master) → curve
├─→ is_curve[curve] = True
├─→ TokenTemplate.initialize(name, symbol, curve, msg.sender, msg.sender)
├─→ BondingCurve.initialize(token, msg.sender, False) {value: msg.value}
├─→ TokenSocials.register_token(token, msg.sender, packed_socials)
└─→ tokensByDeployer[msg.sender].append(token)
Buy on the Bonding Curve
User → BondingCurve.buy(token_lots) {value: total}
├─→ balanceOf(user) / LOT_SIZE + lots <= MAX_USER_LOTS
├─→ _calculate_price(supply, lots, True) // quadratic + linear + tax
├─→ assert msg.value == total // exact payment
├─→ TokenTemplate.mint(user, lots * LOT_SIZE)
└─→ if new_supply == MAX_SUPPLY_LOTS:
└─→ _graduate(fees)
Graduation — V4 family
BondingCurve._graduate(fees)
├─→ protocol_share = fees // 10; deployer_fee = fees - protocol_share
├─→ raw_call(deployer, deployer_fee, revert_on_failure=False)
│ // best-effort: a reverting deployer wallet forfeits to the hook
├─→ TokenTemplate.mint(PRIMARY_HOOK, GRADUATION_TOKENS)
├─→ TokenTemplate.enableTrading()
└─→ PrimaryHook.createPositionAndRegister(token, deployer) {value: hook_native}
├─→ assert Factory.is_curve(msg.sender) // only a factory-born curve
├─→ approve PERMIT2 → POSITION_MANAGER
├─→ _initializing = True // transient init gate
├─→ PositionManager.multicall([initializePool, modifyLiquidities])
├─→ _initializing = False
├─→ excess beyond GRADUATION_NATIVE → protocolBalance
├─→ tokenInfo[token] = {deployer, positionId}; poolIdOf[token] = id
└─→ if token != PROTOCOL_TOKEN:
├─→ IERC20(token).transfer(SECONDARY_HOOK, SECONDARY_TOKEN_RAW)
└─→ SECONDARY_HOOK.notifyReserved(token) {value: SECONDARY_NATIVE_RAW}
Graduation — Gnosis (V3)
BondingCurve.initialize(...)
└─→ GraduationManager.preInitializePool(token)
// pool initialized atomically with deploy_pair; a frontrunner who
// pre-inits at a bad price reverts the whole deployment tx
BondingCurve._graduate(fees)
└─→ GraduationManager.createPositionAndRegister(token, deployer) {value}
├─→ wrap native → WNATIVE
├─→ assert actual slot0 sqrtPrice == expected // defense in depth
├─→ NonfungiblePositionManager.mint(full range, 1% fee tier)
└─→ excess + residual WNATIVE → protocolBalance
Post-Graduation Swap
V4 family:
User → Router.execute(swap)
└─→ PoolManager calls PrimaryHook:
├─→ beforeSwap() // tax buys: take native fee
└─→ afterSwap() // tax sells: take native fee
// fee → pendingFees[token], or protocolBalance for $LP24
Gnosis:
User → SwapRouter.exactInputSingle(...) // fees accrue inside the V3 position
Anyone → GraduationManager.collectFees(token)
├─→ native side → pendingFees[token] (or protocolBalance for $LP24)
└─→ token side → pendingTokens[token] // never sold
Design Principles
No admin keys
No owner, no admin, no multisig, no upgrade path. Once deployed the contracts are immutable. The only privileged role is deployer, which receives a fee share — and that can be transferred by community vote.
Nothing third-party can block
Paths that touch untrusted addresses are best-effort. The graduation payout to the deployer, the CTO payout to an outgoing deployer, and the registry mirror on moveDeployerToken all use revert_on_failure=False, so a contract wallet that reverts on receive cannot brick graduation or a takeover. Failed deployer payouts roll back into deployerBalance for later withdrawal.
Pool initialization gates
Both V4 hooks implement beforeInitialize and reject any initialization unless a transient _initializing flag is set — which only their own creation path sets, and which is cleared immediately after the multicall rather than left to expire at end of transaction. On Gnosis the equivalent guarantee comes from initializing the V3 pool atomically inside deploy_pair.
Gas optimization
- No per-user storage on the bonding curve — holdings are read via
balanceOf() poolIdOf[token]caches the pool id, read on every swap callback- Transient storage (EIP-1153) for the internal-swap and pool-init flags
- Standalone
pendingFeesmapping instead of a struct field - Deployer enumeration appends in place — O(1) regardless of how many tokens an address holds
Minimal proxy pattern
Tokens, bonding curves and CTO vote contracts are EIP-1167 minimal proxies delegating to a shared master. Deployment costs roughly 45k gas instead of ~2M.