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)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 / 1e18What 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.notifyBoostreverts iftotalWeight == 0, since pushing boost into an empty vault is a no-op.
Interface Actions
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:
Sets
lockEnd = now + duration. Until that timestamp passes,withdraw,merge, andburnAndRedeemrevert.Freezes the chosen tier's
lockBonusinto the NFT's weight, increasing its slice of every subsequentnotifyBoost.
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()andmerge(): any action whose result would push an NFT abovemaxStakePerNftreverts withExceedsMaxStakePerNft.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
pendingBoostbucket claimed viaclaim()— neither increasesstakedShares, 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). Setting0disables the cap (unlimited). Changing it only affects futurestake/merge; it never touches existing balances.
Caps and tuning
setLevelStakedNeededandsetLockBoostcan only be changed whiletotalWeight == 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-gatedsetLockBoostPostLaunch, 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 byDEFAULT_ADMIN_ROLE, unlike the pre-staking-only setters above; it only gates futurestake/merge.rarityMultiplierhas no setter on the V3 vault. Updating multipliers would require deploying a new vault and migrating.setRarityMultiplierdoes 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 + Boost → Smart Contract Reference → Boost Harvester (technical)
Last updated