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:
- MoeTreasury computes the reward formula and tracks claims per (account, nftId)
- 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 stakerThe 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:
| Component | Symbol | Source | Determined By |
|---|---|---|---|
| Annual Percentage Rate | APR | aprOf(nftId) | NFT level, polynomial parameters |
| Annual Percentage Bonus | APB | apbOf(nftId) | Stake age (vintage), polynomial parameters |
| Dynamic Scalar | — | _apr[id][0] addend | Per-level share distribution |
The APR and APB are additive — the total rate is array[0]), making it part of the level-based rate rather than a separate multiplier.
Reward Formula
For an NFT with level
Where:
| Term | Meaning | Value |
|---|---|---|
| Level-based annual rate | Parts per million (PPM) | |
| Age-based annual bonus | Parts per million (PPM) | |
| Elapsed staking seconds | block.timestamp × balance - _age | |
| Denomination | ||
| XPOW decimals | Native token precision | |
| PPM denominator | Converts rate to fraction | |
| CENTURY constant | 36,525 days ( |
In the contract, this is computed by MoeTreasury.rewardOf() at MoeTreasury.sol:207-213:
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
APR — Level-Based Rate
The APR maps an NFT's level to a base reward rate via a configurable polynomial:
The dynamic scalar adjusts
APB — Age-Based Bonus
The APB rewards longer stake durations via a second polynomial on the vintage (stake age in years):
APB is additive to APR — a level-30 NFT staked for 3 years earns
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:
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:
| Trigger | Method | Effect |
|---|---|---|
| Stake | NftTreasury.stake() | Calls refreshRates(false); mints APowerNft with age=0 |
| Unstake | NftTreasury.unstake() | Calls refreshRates(false); proportionally reduces age |
| Partial unstake | _pushBurn() | refreshClaimed() rescales claimed counter |
| Claim | MoeTreasury.claim() | Computes claimable() = rewardOf() - claimed |
| Rate refresh | MoeTreasury.refreshRates() | Sweeps all levels, updates scalar addends, appends integrator entries |
| APR/APB set | setAPR() / setAPB() | Validates via Rpp, appends integrator entry, stores new parameters |
| Transfer | APowerNft.safeTransferFrom() | Calls refreshClaimed() to prevent double-claim by new owner |
Claiming Rewards
Rewards are claimed via MoeTreasury.claim(account, nftId, amount, nonce):
- Compute
claimable()— the difference betweenrewardOf()andclaimedaccumulator - Increment
_claimedby the claim amount - Call
_sov.mint(account, claim)— wraps XPOW and mints APOW - 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:
- Records the claimant's APOW balance before the claim
- After minting, detects the net APOW increase
- If a lending pool is configured (
pool != address(0)), auto-supplies the APOW increase to the pool for yield - If no pool is configured, enforces the free supply cap of 43,830 APOW
- 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:
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:
| Constraint | Check | Purpose |
|---|---|---|
| Array validity | Length = 4; div, mul non-zero | Prevents division-by-zero |
| Value bounds | Limits rate changes to ±50% | |
| Timing | Minimum 1 month between changes |
These invariants protect stakers from sudden, extreme parameter shifts.
Further Reading
- APR Calculation — polynomial evaluation, parameters, and rate curves
- APB Calculation — vintage bonus, age tracking, and reset behavior
- Integrator Mechanism — time-weighted smoothing and flash-staking prevention