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

GraduationLP

Per-battle custodian for the post-graduation wide + wall LP positions. Locks principal in-protocol for GRADUATION_LP_LOCK; fees accrue normally and can be swept any time. API mirrors UNCX’s IUNCXV4Locker.

Source: contracts/src/core/GraduationLP.sol · ReentrancyGuard.

Roles

CallerWhat they can do
orchestrator (set by owner)deployForGraduation (called once per battle, at finalize)
anyonecollectFees(battleId), unlock(battleId, recipient) (once eligible)

Public API

function deployForGraduation( uint256 battleId, address winnerToken, uint256 winnerEth, uint256 loserEth ) external payable; function collectFees(uint256 battleId) external returns (uint256 ethCollected, uint256 tokenCollected); function unlock(uint256 battleId, address recipient) external;

deployForGraduation — wide + wall

Called by the orchestrator at battle finalize. Receives winnerEth + loserEth (msg.value).

  1. Determine anchor tick = nearest TICK_SPACING to the post-unwind pool tick.
  2. Compute ranges:
    • Wide LP: [anchor − 16080 + small, anchor + min(GRAD_WIDE_UPSIDE_TICKS, headroom)] — approximately ±5× downside / +1000× upside, multiples of TICK_SPACING (60).
    • Wall LP: [anchor + TICK_SPACING, anchor + TICK_SPACING + GRAD_WALL_DEPTH (540)] — narrow single-sided buy support, sits just above the current tick.
  3. Mint winner tokens for the wide LP via hook.provideTokens(winnerToken, this, ...).
  4. Add both positions via poolManager.modifyLiquidity(...) inside an unlock callback.
  5. Stamp unlockAt = block.timestamp + GRADUATION_LP_LOCK and emit GraduationLpDeployed.

After this, the pool’s hook flips to GRADUATED — external LPs can join too, but the protocol-owned wide+wall positions are locked here.

collectFees — permissionless fee sweep

Walks both the wide and wall positions, harvests accrued fees, and splits them per the standard rule:

  • CREATOR_FEE_BPS / POOL_FEE = 3000/13000 ≈ 0.3% of trade → winner token’s creator
  • Remainder ≈ 1.0% → factory.feeRecipient()

Anyone can call. No payment, no permission. Emits FeesCollected.

unlock — withdraw principal after the lock

After block.timestamp ≥ unlockAt, anyone can call to remove both positions and forward principal (ETH + winner tokens) to recipient. Emits GraduationLpUnlocked.

⚠️ The recipient argument is caller-chosen. There’s no access control on who receives the unlocked liquidity. In practice the protocol’s keeper/operator should call this with the appropriate destination; the design intends ownership transfer to a long-term LP custodian or a community-controlled multisig at unlock time.

GradLock struct

struct GradLock { address winnerToken; uint256 wideTokenId; // V4 position id (wide LP) uint256 wallTokenId; // V4 position id (wall LP) uint256 unlockAt; // block.timestamp + GRADUATION_LP_LOCK bool deployed; // true after deployForGraduation }

Read via getLock(battleId) or isUnlockable(battleId).

Events

event GraduationLpDeployed( uint256 indexed battleId, PoolId indexed poolId, address indexed winnerToken, uint256 wideLiquidity, uint256 wallLiquidity, uint256 ethDeployed, uint256 tokensDeployed, uint256 unlockAt ); event FeesCollected( uint256 indexed battleId, address indexed recipient, uint256 ethCollected, uint256 tokenCollected ); event GraduationLpUnlocked(uint256 indexed battleId, address indexed recipient);

Indexers typically subscribe to:

  • GraduationLpDeployed — show “graduated” badge + LP composition on token page
  • FeesCollected — accounting for platform / creator revenue
  • GraduationLpUnlocked — for the rare 3-year-later event

Errors

ErrorCauseFix
NotOrchestratorNon-orchestrator called deployForGraduationSetup mistake — set orchestrator via owner
NotHookAuthorizedHook lookup mismatch during deployHook address out of sync with factory
AlreadyDeployedRe-deploying a battle’s graduation LPEach battle is finalized once
NoLockForBattlecollectFees / unlock on a non-existent battlePass a valid battleId whose battle resolved
StillLockedunlock before unlockAtWait until the lock window passes
PoolNotInitializedPool didn’t exist when deployForGraduation ranToken must have been bootstrapped first

Common usage patterns

Reading whether a battle’s LP is unlockable

const unlockable = await publicClient.readContract({ address: ADDRESSES.GraduationLP, abi: GraduationLPAbi, functionName: "isUnlockable", args: [battleId], });

Sweeping fees from a graduated battle

const [ethFees, tokenFees] = await walletClient.writeContract({ address: ADDRESSES.GraduationLP, abi: GraduationLPAbi, functionName: "collectFees", args: [battleId], }); // Eth + token fees auto-split 1.0% LP fee + 0.3% creator. // No payment required from caller. Anyone can do it.

Full lock snapshot for a token page

const lock = await publicClient.readContract({ address: ADDRESSES.GraduationLP, abi: GraduationLPAbi, functionName: "getLock", args: [battleId], }); // lock = { winnerToken, wideTokenId, wallTokenId, unlockAt, deployed }

See also: Graduation LP (mechanism explainer), BattleOrchestrator.