Skip to Content
⚔ BattleMeme docs · early preview · expect rough edges
ContractsBattleHook

BattleHook

The Uniswap V4 hook that owns every BattleMeme token’s LP, runs the per-token state machine, accounts ETH raised, splits fees between platform & creator, and enforces the trading freeze on QUEUE/WARMUP/ELIMINATED/LOST states.

Source: contracts/src/core/BattleHook.sol. Hook permissions: all 14 V4 callbacks enabled (0x3FFF).

Roles

CallerWhat they can do
factory (immutable after init)bootstrapToken (via TokenFactory.createToken)
orchestrator (set by owner)All state transitions (transitionTo*), withdrawForOrchestrator, provideTokens, burnLeftoverTokens, disableMintFor, commitRoundScore
claimManagerburnLoserForClaim — burn LOST tokens for the holder during claim
poolManagerunlockCallback and all 14 hook callbacks
anyonesweepLpFees(poolId) — permissionless poke; all view functions

Public views — read state

For an FE/integrator the most useful read paths:

FunctionReturnsNotes
poolToToken(poolId)addressResolve a V4 pool back to its ERC-20
tokenToPool(token)PoolIdInverse lookup
getState(poolId)State enum (0..8)Drives every UI banner — see State machine
getCurrentPrice(poolId)uint256 (sqrtPriceX96 from slot0)Square it / Q96 to get TOKEN-per-ETH
getEthReserves(poolId)uint256The score during a battle — live net ETH in the 5 LP positions
totalETHRaised(poolId)uint256Gross cumulative ETH spent on buys (compared against INIT_ETH_THRESHOLD = 5 ETH)
cumulativeSupply(poolId)uint256Token amount sold out of the curve so far
uniqueBuyers(poolId)uint256Wallets that cumulatively spent ≥ MIN_BUYER_ETH
currentRoundScore(poolId)uint256Net ETH inflow during the active TRADE window
roundScore(poolId, round)uint256Commited score for round 1..4
cumulativeBattleScore(poolId)uint256Sum of all committed round scores
getBattleStats(poolId)(ethRaisedInBattle, uniqueBuyersFiltered, diamondHandsBps)One-shot tuple for arena UI
buyerInfo(poolId, buyer)(firstSeenAt, cumulativeETH, largestBuy, counted)Per-wallet history

Bootstrap & creator pre-buy

function bootstrapToken(address token, address creator) external payable returns (PoolId poolId);

Called once per token by TokenFactory.createToken:

  1. Initialize the V4 pool at sqrtPriceX96 = sqrt(price@tick 173220) (top of the LP range).
  2. Mint LP_MINT = 1.6B tokens to the hook.
  3. poolManager.unlock(BootstrapAddLp) → deposit all 1.6B across 5 chained single-sided positions at salts bytes32(0..4).
  4. If msg.value > 0: poolManager.unlock(CreatorPrebuy) → exact-input ETH→TOKEN swap of msg.value at the freshly initialised price. Output TOKEN goes to creator. The ETH leg credits totalETHRaised and counts toward the 5 ETH queue threshold.

Emits TokenBootstrapped(token, poolId).

Fee sweep — 1.0% / 0.3% split

function sweepLpFees(PoolId poolId) external returns (uint256 ethSwept, uint256 tokenSwept);

Permissionless. Walks all 5 positions with modifyLiquidity(delta=0) to harvest accrued fees, then routes the combined BalanceDelta:

  • CREATOR_FEE_BPS / POOL_FEE = 3000/13000 ≈ 0.3% of tradeBattleMemeToken.creator
  • Remainder = 10000/13000 ≈ 1.0% of tradefactory.feeRecipient()

The same split is applied automatically before any LP unwind (graduation, withdrawal) so the vault captures principal only.

Emits LpFeesSwept(poolId, ethToPlatform, ethToCreator, tokenToPlatform, tokenToCreator).

State-transition gates

All callable only by orchestrator:

FunctionTransition
transitionToQueueINIT → QUEUE (also auto-called from afterSwap when totalETHRaised ≥ 5 ETH)
transitionToWarmupQUEUE → WARMUP
transitionToBattleWARMUP → BATTLE (round 1 active)
transitionToRestingBATTLE → RESTING
transitionToEliminatedBATTLE/RESTING → ELIMINATED
transitionToGraduatedBATTLE/RESTING → GRADUATED
transitionToLostELIMINATED → LOST (at finalize)
unlockFromQueueQUEUE → INIT (queue alone, timeout fired)
commitRoundScoreSnapshot current TRADE-window net ETH to roundScores[round]

Lifecycle support — orchestrator-only

FunctionWhat it does
withdrawForOrchestrator(poolId)Unwinds all 5 LPs, sweeps fees first, forwards ETH to caller. Used at elimination + finalize.
provideTokens(token, recipient, amount)Sends tokens to recipient — uses hook’s leftover balance first, mints the shortfall. Used to pre-fund ClaimManager + seed GraduationLP wide LP.
burnLeftoverTokens(token)Burns 100% of the hook’s residual balance. Run after elimination (loser tokens are dead) and after graduation (excess winner tokens become supply pollution).
disableMintFor(token)Permanent freeze on BattleMemeToken.mint. Called once on the winner at finalize.
burnLoserForClaim(loserToken, holder, amount)Called by ClaimManager.claim to burn the holder’s loser balance during a claim.

Uniswap V4 callbacks

All 14 callbacks are enabled (mask 0x3FFF) but only a few are load-bearing:

CallbackBehavior
beforeInitializeValidates pool key (ETH = currency0, fee 1.3%, hook = self)
beforeAddLiquidityGates external LP adds — only selfLpAdding (hook-owned) allowed pre-grad; opens up for GRADUATED pools
beforeRemoveLiquidityGates external LP removes — only selfLpRemoving pre-grad; opens up post-grad
beforeSwapReverts on QUEUE/WARMUP/ELIMINATED/LOST/NONE (TokenFrozenForTrade / TokenLost); pass-through otherwise
afterSwapUpdates totalETHRaised, ethRaisedInPool, currentRoundNetEth. Tracks buyers. Auto-triggers orchestrator.enterQueue at 5 ETH threshold. Opportunistically calls runRound for safe (no-cut) rounds.
OthersNo-op pass-through (declared for forward-compat)

Events

event TokenBootstrapped(address indexed token, PoolId indexed poolId); event StateChanged(PoolId indexed poolId, State previous, State next); event Trade(PoolId indexed poolId, address indexed trader, bool isBuy, int128 amount0, int128 amount1, uint256 ethRaisedInPool); event QualifiedForQueue(PoolId indexed poolId); event LpFeesSwept(PoolId indexed poolId, uint256 ethToPlatform, uint256 ethToCreator, uint256 tokenToPlatform, uint256 tokenToCreator); event RoundScoreCommitted(PoolId indexed poolId, uint256 indexed round, uint256 score);

Indexers typically subscribe to:

  • Trade — price chart + fill book
  • StateChanged — drives UI banners
  • TokenBootstrapped + QualifiedForQueue — token lifecycle
  • LpFeesSwept — platform / creator revenue accounting

Errors

ErrorCauseFix
NotFactoryCaller wasn’t the registered factoryOnly TokenFactory.createToken can call bootstrapToken
NotOrchestratorCaller wasn’t orchestrator (or claimManager for the claim path)Use the keeper / wait for the lifecycle to schedule it
NotPoolManagerCaller wasn’t Uniswap V4’s PoolManagerHook callbacks are PM-only
TokenAlreadyRegisteredRe-bootstrapping a poolEach (currency0, currency1, fee, tickSpacing, hooks) combo is unique
UnknownTokenpoolId has no registered tokenPool was never bootstrapped through this hook
TokenFrozenForTradeswap attempted on QUEUE/WARMUP/ELIMINATEDWait for the next tradable state
TokenLostswap attempted on a LOST tokenUse ClaimManager.claim instead
InvalidStateTransitionOrchestrator called a transition that isn’t valid from the current stateCheck getState(poolId) first
InvalidCurrencyOrderingPool initialized with wrong currency0Currency0 MUST be address(0) (ETH)
UnauthorizedLiquidityAddExternal LP add attempted on a non-graduated poolOnly the hook can add LP pre-grad
LiquidityRemovalForbiddenExternal LP remove attempted on a non-graduated poolOnly the hook can unwind pre-grad
UnknownUnlockKindBad UnlockKind passed to unlockCallbackInternal — shouldn’t happen unless hook is misused

Common usage patterns

Reading current state from a frontend

import { useReadContract } from "wagmi"; import { BattleHookAbi } from "@/lib/abis"; import { ADDRESSES } from "@/lib/addresses"; const { data: state } = useReadContract({ address: ADDRESSES.BattleHook, abi: BattleHookAbi, functionName: "getState", args: [poolId], }); // state: 0=NONE 1=INIT 2=QUEUE 3=WARMUP 4=BATTLE 5=RESTING 6=ELIMINATED 7=GRADUATED 8=LOST

Live score during a battle

const { data: ethInPool } = useReadContract({ address: ADDRESSES.BattleHook, abi: BattleHookAbi, functionName: "getEthReserves", args: [poolId], query: { refetchInterval: 3_000 }, // 3s polling is enough }); // `ethInPool` is the live battle score, in wei.

Sweeping fees as a keeper

import { useWriteContract } from "wagmi"; const { writeContract } = useWriteContract(); writeContract({ address: ADDRESSES.BattleHook, abi: BattleHookAbi, functionName: "sweepLpFees", args: [poolId], }); // No payment needed; anyone can do it. The 1.0%/0.3% split happens automatically.

See also: BattleOrchestrator, State machine, Tokenomics.