ClaimManager
Holds the winner-token reserve and registry of loser→winner conversion ratios. Loser holders call claim() to burn their bag and receive winner tokens at the rate locked in at elimination time. No deadline — the window stays open forever.
Source:
contracts/src/core/ClaimManager.sol·ReentrancyGuard. Singleton — one ClaimManager handles every battle’s claims.
Roles
| Caller | What they can do |
|---|---|
orchestrator (set by owner) | registerLosingToken at battle finalize — pre-funds the winner reserve and records the ratio |
| anyone holding loser tokens | claim(loserToken, amount) — burns their bag, receives winner tokens |
| anyone | All view functions (previewClaim, losingTokenInfo) |
Public API
function registerLosingToken(address loserToken, address winnerToken, uint256 ratio) external;
function claim(address loserToken, uint256 amount) external returns (uint256 winnerAmount);
function previewClaim(address loserToken, uint256 amount) external view returns (uint256);
function losingTokenInfo(address loserToken) external view returns (address winnerToken, uint256 ratio);Conversion ratio
The ratio is scaled by 1e18:
winnerAmount = burnedLoser × ratio / 1e18Where, at finalize:
ratio = (winnerPriceQ96 × 1e18) / loserPriceQ96winnerPriceQ96 and loserPriceQ96 are sqrtPriceX96² snapshots — the winner’s at finalize, the loser’s at elimination.
The result is USD-fair at the moment of elimination: being eliminated late (after the loser pumped) costs less than being eliminated early.
claim flow
function claim(address loserToken, uint256 amount) external returns (uint256 winnerAmount);- Look up
(winnerToken, ratio)forloserToken. RevertsUnknownTokenif not registered. - Compute
winnerAmount = amount × ratio / 1e18. RevertsNothingToClaimif 0. - Approve check — caller must have
approve(ClaimManager, ≥ amount)on the loser token. hook.burnLoserForClaim(loserToken, msg.sender, amount)— burns the caller’s loser balance via the hook’s privileged path (LOST tokens are otherwise frozen).- Transfer
winnerAmountof the winner token from this contract’s reserve tomsg.sender.
Reverts InsufficientWinnerSupply only in pathological cases where the reserve has been drained (e.g. if orchestrator.registerLosingToken math underflowed).
Events
event LosingTokenRegistered(address indexed loserToken, address indexed winnerToken, uint256 ratio);
event Claimed(address indexed loserToken, address indexed user, uint256 burnedAmount, uint256 winnerAmount);Indexers typically subscribe to:
LosingTokenRegistered— to render claim-available cards on /portfolioClaimed— for activity feed + analytics
Errors
| Error | Cause | Fix |
|---|---|---|
AlreadyRegistered | Re-registering the same loser token | Each loser is registered exactly once at finalize |
UnknownToken | claim on a token that wasn’t registered | Battle hasn’t been finalized yet, or token wasn’t part of any battle |
NotOrchestrator | Non-orchestrator called registerLosingToken | Setup mistake — set orchestrator via owner |
NothingToClaim | amount × ratio / 1e18 == 0 | Increase amount (dust burn), or check previewClaim first |
InsufficientWinnerSupply | Reserve too low (shouldn’t happen if registerLosingToken ran with the right amount) | Audit the orchestrator finalize trace |
Common usage patterns
Preview a claim before submitting
const preview = await publicClient.readContract({
address: ADDRESSES.ClaimManager,
abi: ClaimManagerAbi,
functionName: "previewClaim",
args: [loserToken, userLoserBalance],
});
// preview = winner tokens the caller will receive (in wei)Full claim flow (with allowance)
const balance = await publicClient.readContract({
address: loserToken,
abi: BattleMemeTokenAbi,
functionName: "balanceOf",
args: [account],
});
// 1. Approve ClaimManager (one-time max)
await walletClient.writeContract({
address: loserToken,
abi: BattleMemeTokenAbi,
functionName: "approve",
args: [ADDRESSES.ClaimManager, (1n << 256n) - 1n],
});
// 2. Claim
const tx = await walletClient.writeContract({
address: ADDRESSES.ClaimManager,
abi: ClaimManagerAbi,
functionName: "claim",
args: [loserToken, balance],
});Resolve winner from a loser token
const [winnerToken, ratio] = await publicClient.readContract({
address: ADDRESSES.ClaimManager,
abi: ClaimManagerAbi,
functionName: "losingTokenInfo",
args: [loserToken],
});
// winnerToken == 0x0 → not registered yet (battle not finalized).See also: Claim rewards, BattleOrchestrator.