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
| Caller | What they can do |
|---|---|
orchestrator (set by owner) | deployForGraduation (called once per battle, at finalize) |
| anyone | collectFees(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).
- Determine anchor tick = nearest
TICK_SPACINGto the post-unwind pool tick. - Compute ranges:
- Wide LP:
[anchor − 16080 + small, anchor + min(GRAD_WIDE_UPSIDE_TICKS, headroom)]— approximately ±5× downside / +1000× upside, multiples ofTICK_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.
- Wide LP:
- Mint winner tokens for the wide LP via
hook.provideTokens(winnerToken, this, ...). - Add both positions via
poolManager.modifyLiquidity(...)inside anunlockcallback. - Stamp
unlockAt = block.timestamp + GRADUATION_LP_LOCKand emitGraduationLpDeployed.
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’screator- 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 pageFeesCollected— accounting for platform / creator revenueGraduationLpUnlocked— for the rare 3-year-later event
Errors
| Error | Cause | Fix |
|---|---|---|
NotOrchestrator | Non-orchestrator called deployForGraduation | Setup mistake — set orchestrator via owner |
NotHookAuthorized | Hook lookup mismatch during deploy | Hook address out of sync with factory |
AlreadyDeployed | Re-deploying a battle’s graduation LP | Each battle is finalized once |
NoLockForBattle | collectFees / unlock on a non-existent battle | Pass a valid battleId whose battle resolved |
StillLocked | unlock before unlockAt | Wait until the lock window passes |
PoolNotInitialized | Pool didn’t exist when deployForGraduation ran | Token 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.