> For the complete documentation index, see [llms.txt](https://docs.primestaking.xyz/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.primestaking.xyz/products/xdc-staking-nfts/xdc-staking-nfts-mechanics.md).

# 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](/products/xdc-staking-nfts.md#rarity-tiers)). 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](#per-nft-stake-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](#per-nft-stake-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](/products/xdc-staking-nfts/locked-nft-migration.md).

***

## 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](#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 + Boost](/products/xdc-staking-nfts/xdc-nft-staking-reward-system.md) → [Smart Contract Reference](/products/xdc-staking-nfts/smart-contract-functions.md) → [Boost Harvester (technical)](/products/xdc-staking-nfts/boost-harvester.md)
