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

Security

Self-review tracking for testnet deployment readiness. Not a substitute for an external audit before mainnet. Bug bounty starts at testnet launch.

Trust assumptions

  • No upfront team grant. The creator’s 0.01 ETH is spent as an atomic pre-buy at the pool’s floor price — they receive only what 0.01 ETH buys at price 1×, the same as any walk-up first buyer.
  • Mint gated. All mints go through the hook; disableMint() permanently freezes mint on the winner at graduation. Leftover winner tokens are burned at finalize so total supply never exceeds what holders actually need.
  • LP lock is in-protocol. Principal cannot be withdrawn until the lock window passes — anyone can call unlock(battleId, recipient) once eligible.
  • Public entrypoints. tickQueue, commenceBattle, runRound are permissionless; anyone can advance the protocol if Chainlink Automation lags.
  • No on-chain randomness. Round cadence is fully deterministic — no VRF, no block.prevrandao.

Creator alignment

  • Creator receives a first-buy stake (tokens bought with their 0.01 ETH at price 1×) — same price any other first buyer would pay. Skin in the game without a pre-mint.
  • Creator earns 0.3% of every swap (out of the 1.3% pool fee) — perpetually, both during the battle and after graduation. Alignment incentive: more volume = more fee revenue.
  • BattleMemeToken.creator is immutable (set at deployment, can never be transferred). If the creator address is somehow address(0) (extreme edge case), the 0.3% share is forfeited and the full 1.3% goes to platform.

Architecture-level

ItemStatusNotes
Native AMM (no BeforeSwapDelta) means no custom curve invariants to breakCurve dynamics emerge from concentrated LP + Uniswap v4 standard math
Single shared hook for all tokens (state keyed by PoolId)Reduces deploy cost; permissions still encoded in hook proxy address
BattleMemeToken.creator is immutableCan never be transferred; aligns creator fee accrual forever
Hook permissions encoded in proxy address (CREATE2 + HookMiner)Mask 0x3FFF (all 14 callbacks) — forward-compat without redeploy

Reentrancy

ItemStatusNotes
BattleOrchestrator.runRound guarded with nonReentrantInherits ReentrancyGuard
ClaimManager.claim guarded with nonReentrantSame
VoucherManager.redeem / redeemWhitelist guarded with nonReentrantSame
GraduationLP.collectFees / unlock guarded with nonReentrantSame
BattleHook external calls happen inside PM unlock callbackPM enforces single in-flight unlock — implicit reentrancy guard
Checks-effects-interactions on claimBurns loser via hook → transfers winner

Access control

FunctionGate
BattleHook.bootstrapTokenonlyFactory
BattleHook.transitionTo*onlyOrchestrator
BattleHook.withdrawForOrchestratoronlyOrchestrator
BattleHook.provideTokensonlyOrchestrator (or graduationLp)
BattleHook.burnLeftoverTokensonlyOrchestrator
BattleHook.disableMintForonlyOrchestrator
BattleHook.burnLoserForClaimmsg.sender == claimManager
BattleHook.set{Orchestrator,Factory,ClaimManager,GraduationLp}onlyOwner
BattleHook.beforeAddLiquidityselfLpAdding && sender == hook (pre-grad); open for GRADUATED
BattleHook.beforeRemoveLiquidityselfLpRemoving && sender == hook (pre-grad); open for GRADUATED
BattleHook.beforeSwap/afterSwap/...onlyPoolManager
BattleHook.unlockCallbackonlyPoolManager
BattleHook.sweepLpFeespublic (permissionless) — fees auto-split 1.0% / 0.3%
BattleOrchestrator.enterQueuemsg.sender == hook
BattleOrchestrator.emergencyEndBattleonlyOwner
BattleOrchestrator.tickQueue/commenceBattle/runRoundpublic (trustless fallback — Chainlink Automation primary, anyone secondary)
BattleOrchestrator.performUpkeeppublic (validates state via dispatched method)
ClaimManager.registerLosingTokenmsg.sender == orchestrator
ClaimManager.setOrchestratoronlyOwner
ClaimManager.claimpublic (any holder)
GraduationLP.deployForGraduationmsg.sender == orchestrator
GraduationLP.collectFeespublic (permissionless — auto-split)
GraduationLP.unlockpublic (gated by unlockAt)
VoucherManager.cancelCampaign / cancelAllmsg.sender == campaign.creator

State machine integrity

InvariantStatus
State transitions match documented graph (no skips, no reverse)
Once GRADUATED, mint disabled forever
Token in QUEUE/WARMUP/ELIMINATED/LOST cannot trade
INIT-phase token has totalETHRaised < INIT_ETH_THRESHOLD
Total winner supply bounded by LP_MINT + claim coverage, then burned down
Hook never accumulates physical ETH (transient only inside unlock)

ETH flow

Path
Creation fee 0.01 ETH → atomic creator pre-buy (NOT directly to platform)
Buy: ETH paid by user → PM → LP positions hold it as currency0 reserve
Sell: tokens paid by user → PM, ETH released from LP reserves
Eliminated/winner ETH withdrawal: hook unwinds 5 LP positions, takes ETH, forwards to orchestrator
Orchestrator receives ETH, forwards to GraduationLP.deployForGraduation
GraduationLP custodies positions; principal locked until unlockAt
Hook never accumulates physical ETH (transient only inside PM unlock)

Token flow

Path
Bootstrap: 1.6B mint → all into 5 LP positions on PM, hook holds 0 balance
Atomic creator pre-buy: 0.01 ETH → exact-input swap → tokens land in creator
Buy: PM → user via standard AMM swap
Sell: user → PM via standard AMM swap
Graduation: hook mints additional winner tokens directly to ClaimManager
Burn-leftover at finalize: hook burns 100% of its own residual token balance
LOST token: transfers frozen except via burn-to-claim path
Burn-to-claim: hook burns loser, ClaimManager transfers winner tokens

Fee flow

Path
Pre-grad sweep: hook.sweepLpFees(poolId) → 1.0% LP fee + 0.3% creator
Post-grad sweep: GraduationLP.collectFees(battleId) → same 1.0% / 0.3% split
Sweep auto-runs before any LP unwind (vault captures principal only)
Rounding goes to platform, not lost (creator share computed first; platform takes remainder)

MEV considerations

VectorMitigation
Sandwich on user swapsUser-supplied slippage limit via Uniswap UI / aggregator (out of our scope)
Front-run INIT→Queue triggerThe auto-trigger in afterSwap happens atomically — no separate tx to MEV
Battle scoring last-block manipulationScore reads hook.getEthReserves(poolId) (live ethRaisedInPool). Sells immediately reduce score — a last-block sandwich could shift rank, but the deterministic nextRoundAt schedule is public so all participants can prepare. Future: TWAP or commit-reveal for hardening.
Just-in-time LP add/removeLP add/remove gated to hook-self only pre-grad — external JIT impossible. Post-grad, standard LP rules apply.
Round-time predictionBy design. ROUND_GAP is constant; users SEE the next cutoff and can plan around it. No randomness.
Voucher signature replayEach (campaignId, voucherIndex) is single-use; consumed mapping prevents replay. Sigs are also EIP-191 personal-sign tied to chainId.

Edge cases handled

  • ✅ Empty queue + tickQueue → no-op
  • ✅ 1 token in queue + timeout → _unlockQueue (back to INIT)
  • ✅ ≥ 2 tokens + queue full or timeout → start battle
  • runRound called too early → revert with TooEarly
  • runRound called twice in a row → second call reverts (nextRoundAt updated after first)
  • commenceBattle called twice → revert (WarmupAlreadyCommenced)
  • emergencyEndBattle called on resolved battle → revert (BattleAlreadyResolved)
  • ✅ Insufficient ETH reserves on sell → AMM math reverts naturally (no panic)
  • ✅ External user adds liquidity directly pre-grad → revert (UnauthorizedLiquidityAdd)
  • ✅ External user removes liquidity pre-grad → revert (LiquidityRemovalForbidden)
  • ✅ Pool initialized with wrong currency ordering → revert (InvalidCurrencyOrdering)
  • claim with 0 result (dust) → revert (NothingToClaim)
  • ✅ Voucher redeem on QUEUE/WARMUP token → revert (BadTokenState)
  • ✅ Voucher signature wrong signer → revert (BadSignature)

Known limitations / future work

ItemPlan
TWAP for scoring (currently spot from slot0)Post-MVP — guards against last-block manipulation
Wallet age / hold-time anti-Sybil checksPost-MVP — Sismo / Privy attestations
Multi-sig + 48h timelock on owner roleMainnet deployment script
Real UNCX V4 locker integrationFuture — IUNCXV4Locker interface already exists for the swap
Composite score (ETH + buyers + price-growth)Library scaffolded (BattleScoring.sol); not wired today
unlockedCooldownUntil enforcementField written but not read — currently a no-op

Known design trade-offs

  • Score = live ETH in pool means sells immediately reduce rank — by design, this puts pressure on holders and rewards conviction. A wallet pumping then dumping in the same trade window swings their own score back down.
  • Cut table is bracket-aware (varies with M) — UI should compute the at-risk preview client-side from getBattle + _cutSizeForRound.
  • GraduationLP.unlock recipient is caller-chosen — by design the lock window enforces time, not recipient. The protocol’s keeper/operator should call this with the appropriate destination at unlock time (e.g. a community multisig).
  • R1 has no warning event — the schedule is public via nextRoundAt; UI computes its own countdown. Eliminates the surface area of an on-chain warning emitter.

Files

  • src/core/BattleMemeToken.sol — ERC-20, hook-controlled mint/burn (immutable)
  • src/core/TokenFactory.sol — deploys token + forwards 0.01 ETH for atomic creator pre-buy
  • src/core/BattleHook.sol — single shared hook for all tokens
  • src/core/BattleOrchestrator.sol — queue + battle + Chainlink Automation
  • src/core/ClaimManager.sol — loser → winner conversion
  • src/core/GraduationLP.sol — per-winner wide+wall LP custodian
  • src/core/VoucherManager.sol — creator promo (signed + Merkle)
  • src/libraries/HookMiner.sol — CREATE2 salt mining for hook proxy

See also: Architecture, Tokenomics, all contract refs under /contracts/.