Skip to Content
Locks (veCLAIM)

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
  • 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 until lockEndunlock(tokenId). No shortcut — the full MAX_LOCK_DURATION cooldown applies.

AutoMax automatic bonus growth:

  • A key advantage of AutoMax: the protocol accrues extension bonuses automatically via Furnace.claimAutoMaxBonus(tokenId) or Furnace.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).lockEnd as 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 ignore lockEnds[i].
  • For offchain ve math: if autoMax == true, use ve = amount.

Merge locks

mergeLocks(fromTokenId, intoTokenId) (owner-only) and mergeLocksForUser(user, fromTokenId, intoTokenId) (delegated via P_MERGE_LOCKS_FOR):

  • Burns the from lock and adds its amount to the into lock.
  • Owner path (mergeLocks): allows merging AutoMax and non-AutoMax locks; if either is AutoMax, the result is AutoMax with lockEnd refreshed to block.timestamp + MAX_LOCK_DURATION.
  • Delegated path (mergeLocksForUser): reverts with AutoMaxMismatch if the two locks have different autoMax flags.
  • Both locks must be owned by the caller (or by user for 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: mineMarket may 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 furnaceBurnAndWithdraw after 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 / 100 with 2 decimals (use <0.01% for tiny values).

Notes:

  • totalVeCurrent is 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() after ve.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. VeClaimNFT does not expose user-facing extension functions; the Furnace-only extendLockToFor(...) 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

FunctionPurpose
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

FunctionPurpose
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