For the complete documentation index, see llms.txt. This page is also available as Markdown.

Staking Mechanics (V3)

XDC NFTs in V3 use a weight-based model rather than the V2 "monthly reward pool × multiplier" model. Your slice of the boost stream is proportional to your NFT's weight in the global accumulator, where weight is a function of staked shares, rarity, level, and lock status.


The weight formula

weight(tokenId) = stakedShares × (rarityMultiplier + level + lockBonus)
Term
What it represents

stakedShares

psXDC v3 shares currently deposited into this NFT

rarityMultiplier

Per-rarity constant set at mint (see README). Immutable on the V3 vault.

level

Increases as you stake more psXDC into the NFT, up to per-rarity caps

lockBonus

Additive integer applied while the NFT is locked, zeroed when unlocked

All four terms are stored on-chain; the vault keeps totalWeight in sync so reward distribution stays consistent.


The Synthetix-style accumulator

Boost rewards aren't distributed in monthly batches. Instead the vault uses a Synthetix-style accumulator that runs continuously:

on notifyBoost(x):
  sharesMinted = psXDC_v3.depositNative{value: x}(x, vault)
  rewardPerWeightStored += sharesMinted * 1e18 / totalWeight
  reverts if totalWeight == 0

earned(tokenId) = info.shares * (rewardPerWeightStored − info.rewardIndex) * weight / 1e18

What this means in practice:

  • Whenever the harvester pushes XDC via notifyBoost, every staked NFT's pending reward grows immediately in proportion to its weight at that moment.

  • _settle(tokenId) is called before any weight-changing operation (stake more, lock, unlock, merge, withdraw) so pending boost is captured against the old weight. You never overpay or underpay because of mid-stream changes.

  • notifyBoost reverts if totalWeight == 0, since pushing boost into an empty vault is a no-op.


Interface Actions

Action
Function
What it does

Stake

stake(tokenId, shares)

Deposit psXDC v3 shares against this NFT. Settles pending boost, updates weight, increments level if thresholds are crossed. Reverts if the resulting balance would exceed the per-NFT cap.

Withdraw shares

withdraw(tokenId, shares)

Remove psXDC shares without burning the NFT. Settles pending boost first.

Lock

lock(tokenId, duration)

Locks for a chosen tier duration (30/90/180/365 days), freezing that tier's lockBonus into the NFT and adding it to weight. Disables withdraw, merge, burnAndRedeem until the lock ends.

Poke expired

pokeExpired(tokenIds)

Permissionless keeper hook: retires the boost of any NFT whose lock has ended, so totalWeight stays accurate even for untouched NFTs.

Merge

merge(tokenIdA, tokenIdB)

Two same-rarity NFTs → one higher-rarity NFT. Burns originals and shares are released for restaking. Reverts if the combined shares would exceed the per-NFT cap.

Claim boost

claim(tokenId)

Pays out earned boost in XDC. Can also unwrap to native XDC or keep as shares depending on the call.

burnAndRedeem

burnAndRedeem(tokenId)

Burns the NFT and returns the underlying psXDC v3 shares (or redeems them to XDC) in one transaction.

Transfer

ERC-721 transferFrom

NFT changes hands. The new owner inherits staked shares, weight, pending boost, and lock status.

mintAndStake / mintAndStakeLocked are restricted to the migrator and are not directly callable by users.


Locking

Locking an NFT does two things:

  1. Sets lockEnd = now + duration. Until that timestamp passes, withdraw, merge, and burnAndRedeem revert.

  2. Freezes the chosen tier's lockBonus into the NFT's weight, increasing its slice of every subsequent notifyBoost.

There are four lock tiers - 30, 90, 180 and 365 days - each granting a progressively larger boost. The boost you get is fixed at lock time (later tier-table changes don't affect an existing lock).

Boost expiry. Unlike earlier versions, the lock bonus ends when the lock ends. From the moment lockEnd passes, the NFT's effective weight drops back to its unlocked value, and the stale bonus is cleared on the next interaction (any user action, or a permissionless pokeExpired keeper call). Rewards already earned are never clawed back - the boost simply stops going forward. You can re-lock at any tier once a lock has expired.

The lock does not change stakedShares; the bonus is purely additive on the weight side.

Lock expiry is preserved across migration from V2; see Locked NFTs & Legacy Diamond Bypass.


Per-NFT stake cap

A single XDC NFT can hold at most 100,000 psXDC shares. To stake more than that, hold additional NFTs — one NFT is one capped "slot".

  • Enforced on-chain in both stake() and merge(): any action whose result would push an NFT above maxStakePerNft reverts with ExceedsMaxStakePerNft.

  • Merge respects the cap too. Two NFTs whose combined staked shares exceed the cap cannot be merged — otherwise merging would be a way to hold more than the cap in one NFT. They stay as separate NFTs and keep earning independently.

  • The cap is on share count, not XDC value. psXDC is an ERC-4626 share; a maxed NFT's XDC value still grows over time as the share price appreciates (that's the base yield). The 100,000 limit is on psXDC shares held.

  • Rewards are never blocked by the cap. Base yield accrues in the share price and boost accrues in a separate pendingBoost bucket claimed via claim() — neither increases stakedShares, so a maxed NFT keeps earning and claiming normally.

  • Grandfathering. NFTs that already hold more than the cap (e.g. migrated from a large V2 position) keep their full balance and can still withdraw/claim; they simply can't be topped up or used as a merge input that would exceed the cap.

  • Migrator mint paths are exempt (mintAndStake / mintAndStakeLocked) so pre-existing V2 positions migrate intact.

  • Configurable by governance via setMaxStakePerNft(uint256 maxShares) (DEFAULT_ADMIN_ROLE). Setting 0 disables the cap (unlimited). Changing it only affects future stake/merge; it never touches existing balances.

Caps and tuning

  • setLevelStakedNeeded and setLockBoost can only be changed while totalWeight == 0 (i.e. before any NFT is staked). Once the vault has live positions these setters revert, which prevents silent weight drift. Lock tiers are enabled/adjusted post-launch through the governance-gated setLockBoostPostLaunch, which only affects future locks - already-locked NFTs keep the boost they froze at lock time.

  • setMaxStakePerNft (the per-NFT stake cap) is settable at any time by DEFAULT_ADMIN_ROLE, unlike the pre-staking-only setters above; it only gates future stake/merge.

  • rarityMultiplier has no setter on the V3 vault. Updating multipliers would require deploying a new vault and migrating.

  • setRarityMultiplier does not exist on the live vault by design.

  • The vault is paused with PAUSER_ROLE. Pausing halts stake/withdraw/claim; boost can still be received (intentional, to keep the stream flowing).

Reward Model: Base NAV + BoostSmart Contract ReferenceBoost Harvester (technical)

Last updated