Skip to content

Rewards Overview

Rewards are the economic engine of the XPower staking protocol. Every staked APowerNft continuously accrues XPOW-denominated rewards, claimable as APOW tokens. The system balances three interacting mechanisms — a level-based base rate, an age-based loyalty bonus, and a market-driven scalar — to incentivize sustained, diversified staking.

Reward Chain

The reward lifecycle follows a two-contract pipeline:

  1. MoeTreasury computes the reward formula and tracks claims per (account, nftId)
  2. APower wraps XPOW from the treasury and mints APOW at a supply-limited rate
Staker calls claim() → MoeTreasury computes rewardOf() → MoeTreasury.mint(account, claim) → APOW minted to staker

The APower mint() function enforces a long-term supply constraint: it wraps XPOW at a 1:1 target ratio, adjusts by a running exponential moving average of claims, and targets an average supply rate of one APOW per minute. Excess claims beyond the supply curve have zero marginal minting output — the treasury pays only what the supply ceiling allows.

Three Reward Components

Rewards for a single staked NFT decompose into three multiplicative factors:

ComponentSymbolSourceDetermined By
Annual Percentage RateAPRaprOf(nftId)NFT level, polynomial parameters
Annual Percentage BonusAPBapbOf(nftId)Stake age (vintage), polynomial parameters
Dynamic Scalar_apr[id][0] addendPer-level share distribution

The APR and APB are additive — the total rate is APR+APB. The dynamic scalar is folded into the APR polynomial's additive constant (array[0]), making it part of the level-based rate rather than a separate multiplier.

Reward Formula

For an NFT with level , denomination D=10, and age t (seconds since staking):

R=(APR+APB)×t×D×1018106×C

Where:

TermMeaningValue
APRLevel-based annual rateParts per million (PPM)
APBAge-based annual bonusParts per million (PPM)
tElapsed staking secondsblock.timestamp × balance - _age
DDenomination10level XPOW
1018XPOW decimalsNative token precision
106PPM denominatorConverts rate to fraction
CCENTURY constant36,525 days (3,155,760,000 seconds)

In the contract, this is computed by MoeTreasury.rewardOf() at MoeTreasury.sol:207-213:

solidity
uint256 rate = aprOf(nftId) + apbOf(nftId);
uint256 reward = rate * age * 10 ** _moe.decimals();
uint256 denomination = _ppt.denominationOf(_ppt.levelOf(nftId));
return Math.mulDiv(reward, denomination, 1e6 * Constant.CENTURY);

The division by CENTURY normalizes the annual rate to a per-second accrual. Since CENTURY = 36,525 days, a rate of 106 PPM (100%) would yield exactly D XPOW of reward over one century.

APR — Level-Based Rate

The APR maps an NFT's level to a base reward rate via a configurable polynomial:

APR()=eval3(,[add,div,mul,exp])

The dynamic scalar adjusts add to redistribute rewards across levels based on staking demand. See APR Calculation for the full derivation and parameter space.

APB — Age-Based Bonus

The APB rewards longer stake durations via a second polynomial on the vintage (stake age in years):

APB(years)=eval3(years,[add,div,mul,exp])

APB is additive to APR — a level-30 NFT staked for 3 years earns APR(30)+APB(3). See APB Calculation for details on age tracking and reset behavior.

Dynamic Scalar

Each NFT level has a share accumulator (total denomination staked at that level). When refreshRates() is called — automatically triggered on every stake and unstake — the MoeTreasury recomputes the additive scalar for each active level:

add=mul×totalSharesbins×shares

This creates a redistribution effect:

  • Under-supplied levels (shares below average): additive boost
  • Over-supplied levels (shares above average): additive reduction

The total reward budget is redistributed, not increased or decreased. The mechanism incentivizes stakers to fill neglected levels.

Calculation Triggers

Reward calculations are refreshed on these contract interactions:

TriggerMethodEffect
StakeNftTreasury.stake()Calls refreshRates(false); mints APowerNft with age=0
UnstakeNftTreasury.unstake()Calls refreshRates(false); proportionally reduces age
Partial unstake_pushBurn()refreshClaimed() rescales claimed counter
ClaimMoeTreasury.claim()Computes claimable() = rewardOf() - claimed
Rate refreshMoeTreasury.refreshRates()Sweeps all levels, updates scalar addends, appends integrator entries
APR/APB setsetAPR() / setAPB()Validates via Rpp, appends integrator entry, stores new parameters
TransferAPowerNft.safeTransferFrom()Calls refreshClaimed() to prevent double-claim by new owner

Claiming Rewards

Rewards are claimed via MoeTreasury.claim(account, nftId, amount, nonce):

  1. Compute claimable() — the difference between rewardOf() and claimed accumulator
  2. Increment _claimed by the claim amount
  3. Call _sov.mint(account, claim) — wraps XPOW and mints APOW
  4. Store minted amount in _minted

The claimBatch() variant processes multiple NFTs in a single transaction, requiring unique, unsorted NFT IDs.

Banq Mechanism

Every claim passes through the banq modifier (defined in Banq.sol:57-61), which:

  1. Records the claimant's APOW balance before the claim
  2. After minting, detects the net APOW increase
  3. If a lending pool is configured (pool != address(0)), auto-supplies the APOW increase to the pool for yield
  4. If no pool is configured, enforces the free supply cap of 43,830 APOW
  5. Burns any portion of the net balance exceeding the claim amount (ratio > MIN_NET2RIP = 100:1)

This ensures that claimed APOW is automatically put to productive use or held within supply limits.

Gas Cost

A single claim() call costs approximately ~200k gas. The dominant cost components are:

  • ~60k for the XPOW transfer and APOW mint
  • ~40k for storage updates (_claimed, _minted, integrator reads)
  • ~100k for the optional banq pool supply call

Batch claiming amortizes the per-NFT overhead but each NFT still requires its own storage access and minting call.

Rate Smoothing via Integrator

Both APR and APB use the Integrator library to compute duration-weighted arithmetic means over rate history. When parameters change via setAPR() or setAPB(), the old rate is appended to a linked list of (stamp, value, area, prev) tuples. The effective rate at any time is:

r¯=iri×(ti+1ti)tt0

This prevents flash-staking (entering during a high-rate window and then leaving while the rate persists). See Integrator Mechanism for the full mathematical treatment.

Rpp — Rug Pull Protection

All parameter changes pass through the Rpp library (Rpp.sol:1-65), which enforces:

ConstraintCheckPurpose
Array validityLength = 4; div, mul non-zeroPrevents division-by-zero
Value bounds0.5×lastnext2.0×lastLimits rate changes to ±50%
Timingnextlast+MONTHMinimum 1 month between changes

These invariants protect stakers from sudden, extreme parameter shifts.

Further Reading