Locks (veCLAIM)
VeClaimNFT is the ERC-721 contract that represents locked CLAIM positions. Each lock has an amount, expiry, and optional AutoMax flag. The lock’s veCLAIM balance determines its share of Baron royalties via ShareholderRoyalties. In the CLAIM stream, this is where committed CLAIM earns its yield.
What a lock is
- veCLAIM is an ERC721 NFT.
- Each tokenId represents a lock:
- amount (CLAIM principal + any locked bonus)
- lockEnd (unlock timestamp when autoMax is OFF; when autoMax is ON, reads use an effective end = block.timestamp + MAX_LOCK_DURATION, rolling)
- autoMax (bool)
- listed (bool, used by MarketRouter)
Constraints (v1.0.0 constants):
- MIN_LOCK_AMOUNT = 1,000 CLAIM
- MIN_LOCK_DURATION = 7 days
- MAX_LOCK_DURATION = 365 days
- MAX_VE_NFTS_PER_USER = 32
ve math (linear decay)
For a lock with:
- amount = A
- lockEnd = E
- now = N
ve at time N is:
if N >= E: ve = 0
else: ve = floor( A * (E - N) / MAX_LOCK_DURATION )Implications:
- more CLAIM => more ve
- more remaining time => more ve (for non-AutoMax locks)
- For non-AutoMax locks, ve decays linearly to 0 at lockEnd
- For AutoMax locks, remaining time is always MAX_LOCK_DURATION, so ve stays equal to amount (no decay) while AutoMax is ON
AutoMax
AutoMax is an automatic “keep me at max ve forever” switch.
Rules:
- AutoMax is opt-in per lock. It can be set at creation or toggled later via
setAutoMax(tokenId, enabled). - While autoMax is true:
- the effective lockEnd is always
block.timestamp + MAX_LOCK_DURATION(rolling) - remaining time is always MAX_LOCK_DURATION, so
ve == amount(no decay) - the lock MUST NOT be treated as expired while autoMax is true
- the lock never becomes unlockable;
unlock(tokenId)MUST revert
- the effective lockEnd is always
- Turning AutoMax off sets
lockEnd = block.timestamp + MAX_LOCK_DURATION(a fresh max-duration decaying lock). The user can unlock after 1 year.- Explicit path:
setAutoMax(tokenId, false)→ wait untillockEnd→unlock(tokenId). No shortcut — the full MAX_LOCK_DURATION cooldown applies.
- Explicit path:
AutoMax automatic bonus growth:
- A key advantage of AutoMax: the protocol accrues extension bonuses automatically via
Furnace.claimAutoMaxBonus(tokenId)orFurnace.claimAutoMaxBonusBatch(tokenIds[], maxLocks)(permissionless, keeper-triggered, 24h cooldown per lock, ineligible locks return 0). AutoMax lockers receive hands-free compounding — no manual extensions, no gas costs — making AutoMax the most rewarding lock mode. See Furnace — AutoMax automatic bonus growth for details.
Integrator notes:
- Treat
getLockInfo(tokenId).lockEndas an effective lockEnd. For AutoMax it is time-dependent. getShareholderLockParams(user)returns the RAW stored lockEnd, NOT the effective end. For AutoMax locks, callers MUST treat ve == amount directly and ignorelockEnds[i].- For offchain ve math: if
autoMax == true, useve = amount.
Merge locks
mergeLocks(fromTokenId, intoTokenId) (owner-only) and mergeLocksForUser(user, fromTokenId, intoTokenId) (delegated via P_MERGE_LOCKS_FOR):
- Burns the
fromlock and adds its amount to theintolock. - Owner path (
mergeLocks): allows merging AutoMax and non-AutoMax locks; if either is AutoMax, the result is AutoMax with lockEnd refreshed toblock.timestamp + MAX_LOCK_DURATION. - Delegated path (
mergeLocksForUser): reverts withAutoMaxMismatchif the two locks have differentautoMaxflags. - Both locks must be owned by the caller (or by
userfor the delegated path), not listed, and not expired.
Transfer restrictions (Furnace-only)
Direct transfers are restricted.
Strict mode invariant:
- The Furnace is the only counterparty for listing settlement.
- There are no user-to-user lock transfers or sales.
Expected behavior:
- MarketRouter (
mineMarket) is the only supported transfer gateway for lock management. - Transfer-based market exits route to the Furnace (listing settlement or instant sellback). Standard expiry unlocks do not:
unlock(...)/unlockExpiredForUser(...)return CLAIM directly from VeClaimNFT to the chosen recipient after burn. - Strict mode:
mineMarketmay only transfer a lock into the Furnace (to == furnace) for settlement/sellback.- MarketRouter may transfer a lock into the Furnace during settlement/sellback.
- The Furnace never transfers veCLAIM NFTs; it burns locks via
furnaceBurnAndWithdrawafter taking custody.
- VeClaimNFT fails closed on caller drift: Furnace-only helpers and MarketRouter-only transfer/listing hooks cross-check the live Furnace, MarketRouter, MineCore, ClaimToken, and wired royalties roots before accepting mutations.
- Locks marked listed = true are considered frozen for mutation until delisted or settled.
Checkpointing and cached totals
VeClaimNFT exposes global checkpointing to keep aggregates consistent and gas-bounded.
Key views:
- totalLockedClaim()
- totalVeCached()
- totalVeBiasScaled()
- globalLastTs()
- getShareholderLockParams(user)
ABI note:
- keep shipped ABI artifacts in sync with these views
- offchain consumers that rely on generated ABI/type bundles must include both getters after the historical shareholder-reward fix
Key calls:
- checkpointGlobalState()
- checkpointTotalVe()
Where they are used:
- MineCore runs a gas-guarded checkpoint loop before takeover finalization and keeps looping until ve catches up, gas gets low, or no forward progress is made.
- MaintenanceHub.poke(args) runs best-effort checkpointing as general upkeep.
UI: show Total ve and royalty share (%)
The official UI uses ve data to show players their current power and weight in royalties.
Recommended reads (ordered):
userVe = VeClaimNFT.veBalanceOf(user)(accurate per-user aggregate)totalVe = VeClaimNFT.totalVeCurrent()(view-only; denom for UI share display)
UI share calculation:
- If
totalVe == 0: share = 0 - Else:
shareBps = floor(userVe * 10_000 / totalVe) - Display
sharePct = shareBps / 100with 2 decimals (use<0.01%for tiny values).
Notes:
totalVeCurrentis view-only (no checkpoint in this call); it is only as fresh as the last global checkpoint.- If checkpoints lag, share can be stale. Best-effort display.
Integrator notes
- For on-chain rewards accounting, ShareholderRoyalties uses
totalVeBiasScaled()afterve.checkpointTotalVe()so the denominator matches VeClaimNFT’s processed bias model exactly. - ShareholderRoyalties reconstructs delayed rewards from
getShareholderLockParams(user)plus historical flush timestamps. _checkpointShareholderRoyalties(user)before every ve mutation is correctness-critical. Removing or skipping it reintroduces retroactive capture / under-accrual bugs.- For user balances,
veBalanceOf(user)is definitive but can be more expensive than cached reads. - Never assume tokenIds are sequential per user.
- Lock extensions route through
Furnace.extendWithBonus(tokenId, durationSeconds, minBonusOut), which awards a bonus on existing capital for the incremental duration commitment.VeClaimNFTdoes not expose user-facing extension functions; the Furnace-onlyextendLockToFor(...)is used internally by Furnace.
ERC-721 metadata
VeClaimNFT implements ERC-721 metadata with ERC-4906 (per-token metadata update signals) and ERC-7572 (collection-level contractURI).
Owner-configurable URIs
| Function | Purpose |
|---|---|
setBaseURI(string uri) | Set the base URI used by tokenURI(). Wallets fetch baseURI + tokenId to resolve per-token JSON metadata. Emits BaseURISet + BatchMetadataUpdate(0, MAX_UINT). |
setContractURI(string uri) | Set the collection-level metadata URI (ERC-7572). Emits ContractURISet + ContractURIUpdated. |
Both accept URIs up to 512 bytes (_MAX_URI_LENGTH); longer strings revert with URITooLong. Both remain owner-configurable after freezeConfig() — metadata is explicitly excluded from the config freeze.
Read-only
| Function | Purpose |
|---|---|
tokenURI(uint256 tokenId) | Returns baseURI + tokenId (inherited from OpenZeppelin ERC721) |
baseURI() | Returns the current base URI |
contractURI() | Returns the collection-level metadata URI |
supportsInterface(bytes4) | Reports ERC-4906 (0x49064906) and ERC-7572 (0xe8a3d485) in addition to standard ERC-721/165 |
Metadata update events
MetadataUpdate(uint256 tokenId)— emitted on lock state changes (merge, unlock, etc.) to signal wallets to refresh individual token metadata.BatchMetadataUpdate(uint256 fromTokenId, uint256 toTokenId)— emitted on base URI changes to signal a global refresh.BaseURISet(string oldURI, string newURI)/ContractURISet(string oldURI, string newURI)— admin audit trail.
Off-chain metadata API
The frontend serves per-token and collection metadata via Next.js API routes:
GET /api/nft/veclaim/[tokenId]— returns ERC-721 metadata JSON (name, description, image, attributes) for a specific lock.GET /api/nft/veclaim/collection— returns ERC-7572 collection metadata JSON.
Point setBaseURI at the token endpoint and setContractURI at the collection endpoint.
See also
- Furnace — bonus engine and lock entry
- ShareholderRoyalties (Barons) — ETH royalty distribution to veCLAIM holders
- Market (MarketRouter) — listing and selling locks
- Core Mechanics — takeover loop and emission schedule
- User manual: Locks