README.md
Staker
Liquidity mining and reward distribution for LP positions.
Overview
Staker manages distribution of internal (GNS emission) and external (user-provided) rewards to staked LP positions, with time-weighted rewards and warmup periods.
Gnoweb
The root Render("") delegates to the active implementation and shows realm identity, the halt flag, stored record counts, cumulative GNS emissions, the cached emission rate, incentive requirements, tier allocations, unstaking fees, and warmup stages.
GNS amounts use six-decimal base units; external rewards use their token's base units. Warmup durations are per-stage seconds, with the final stage shown as unbounded. Rendering reads stored counts and fixed configuration without traversing positions or incentives. Unsupported paths return 404.
Configuration
- Deposit GNS Amount: 100,000 GNS per external incentive (default; governance-adjustable)
- Minimum Reward Amount: 1,000 token units (default for external incentive creation)
- Unstaking Fee: 1% default (100 basis points; configurable from 0 to 10%)
- Internal Pool Tiers: 1, 2, or 3 (assigned per pool); external-only pools can also be stakeable
- Warmup Schedule: 30/50/70/100% over default cumulative windows of 0-5, 5-15, 15-45, and 45+ days
- External Token Policy: Approved reward tokens; pool-pair tokens are also accepted for their own pool unless explicitly denied
Core Features
Internal Rewards (GNS Emission)
- Allocated to tiered pools (tiers 1, 2, 3)
- Split across tiers by TierRatio
- Distributed proportionally to in-range liquidity
- Unclaimed rewards go to community pool
External Rewards (User Incentives)
- Created for specific pools
- Constant reward per second over the incentive window; the stored rate is Q128-scaled as
(rewardAmount << 128) / duration - Proportional to staked liquidity
EndExternalIncentivereturns only the unclaimable/remainder portion and GNS deposit to its explicit refund address; rewards still owed by live positions remain claimable
Warmup Periods
Every staked position progresses through warmup periods. The default finite durations are 5, 10,
and 30 days, followed by a final math.MaxInt64 tier:
- 0-5 days: 30% of the calculated reward
- 5-15 days: 50% of the calculated reward
- 15-45 days: 70% of the calculated reward
- 45+ days: 100% of the calculated reward
Governance may change the finite durations. Warmup ratios are applied before the staking-reward fee:
internal GNS penalties go to the community pool, while external penalties accumulate on the
incentive and are collected separately to an explicit address after EndExternalIncentive.
Key Functions
StakeToken
Stakes LP position NFT to earn rewards.
UnStakeToken
Unstakes a position and records an exit checkpoint for its rewards. It neither calculates nor pays them: withdrawing must never depend on the reward side.
CollectReward
Collects accumulated rewards. Takes a position that was unstaked without collecting as well as a
staked one, so withdrawing is UnStakeToken plus one collect. A collect on an unstaked position
is permissionless, since it can only ever pay that position's owner.
CreateExternalIncentive
Creates an external reward program for a specific pool. Any caller may create one after satisfying the reward-token allowlist/denial, duration, start-time, reward-minimum, and GNS-deposit checks.
EndExternalIncentive
Ends an incentive after its end timestamp and finalizes its refundable unclaimable/remainder
amount. The reward tokens and GNS deposit are sent to the caller-supplied refundAddress; only
the creator or admin may call it, and an outstanding exit checkpoint for that incentive blocks ending.
CancelExternalIncentive
Removes an incentive that has not started and refunds its reward tokens and GNS deposit to the creator. Callable by admin, governance, or the creator; the reward-token refund is capped by the balance held by the staker.
Reward Calculation Logic
Tier Ratio Distribution
Emission split across tiers based on active pools:
If only tier 1 has pools: [100%, 0%, 0%]
If tiers 1 & 3 have pools: [80%, 0%, 20%]
If tiers 1 & 2 have pools: [70%, 30%, 0%]
If all tiers have pools: [50%, 30%, 20%]
Mathematical representation:
TierRatio(t) =
[100, 0, 0] if Count(2) = 0 ∧ Count(3) = 0
[80, 0, 20] if Count(2) = 0
[70, 30, 0] if Count(3) = 0
[50, 30, 20] otherwise
Pool Reward Formula
poolReward(pool) = (emission × TierRatio[tier(pool)] / 100) / Count(tier(pool))
Here emission is the already-allocated per-second GNS amount returned by the emission module
for liquidity stakers. It is split by the tier percentage and then divided among pools in that
tier:
emission = GetStakerEmissionAmountPerSecond()
Position Reward Calculation
The reward for each position is calculated through:
- Resolve the persisted/halving per-second reward schedule (read-only)
- Retrieve position state from deposit records or an exit checkpoint
- Calculate internal rewards if the pool has an internal tier
- Calculate external rewards for the incentive IDs
- Apply warmup ratios and penalties based on stake duration
Collection may separately advance reward caches and persist newly discovered incentive IDs; the read-only calculation itself does not write those caches.
Mathematical formula for total reward ratio:
TotalRewardRatio(s,e) = Σ[i=0 to m-1] ΔRaw(αᵢ, βᵢ) × rᵢ
where:
αᵢ = max(s, Hᵢ₋₁)
βᵢ = min(e, Hᵢ)
ΔRaw(a, b) = CalcRaw(b) - CalcRaw(a)
CalcRaw(h) =
L(h) - U(h) if tick(h) < ℓ
U(h) - L(h) if tick(h) ≥ u
G(h) - (L(h) + U(h)) otherwise
where:
L(h) = tickLower.OutsideAccumulation(h)
U(h) = tickUpper.OutsideAccumulation(h)
G(h) = globalRewardRatioAccumulation(h)
ℓ = tickLower.id
u = tickUpper.id
Final position reward:
finalReward = TotalRewardRatio × poolReward × positionLiquidity
= ∫[s to e] (poolReward × positionLiquidity) / TotalStakedLiquidity(h) dh
Tick Cross Hook
When price crosses an initialized tick with staked positions:
- Updates staked liquidity - Adjusts total staked liquidity
- Updates reward accumulation - Recalculates
globalRewardRatioAccumulation - Manages unclaimable periods - Starts/ends periods with no in-range liquidity
- Updates tick accumulation - Adjusts
CurrentOutsideAccumulation
The globalRewardRatioAccumulation tracks the integral:
globalRewardRatioAccumulation = ∫ 1/TotalStakedLiquidity(h) dh
This integral is only computed when TotalStakedLiquidity(h) ≠ 0, enabling precise reward calculation even as liquidity changes.
Reward State Tracking
The system maintains:
- Global accumulation: Tracks reward ratio across all positions
- Tick accumulation: Tracks rewards "outside" each tick
- Position state: Individual reward calculation parameters
Approval Requirements
StakeTokenmoves the position NFT to the staker realm throughgnft.TransferFrom, so the caller must approve the staker on that NFT first withgnft.Approve(cross(cur), stakerAddress, positionId), or grantgnft.SetApprovalForAll(cross(cur), stakerAddress, true).CreateExternalIncentivepulls two amounts into the staker realm: the reward token amount and the GNS deposit. Approve the staker realm for both token contracts before calling.UnStakeToken, the reward-collection functions,EndExternalIncentive, andCancelExternalIncentivepay out to the caller or to a supplied address and require no approval.
1// Approve the staker on the position NFT, then stake
2stakerAddress := access.MustGetAddress(prabc.ROLE_STAKER.String())
3gnft.Approve(cross(cur), stakerAddress, grc721.TokenID("123"))
4StakeToken(cross(cur), 123, "")
5
6// GNS pays both the external reward and the required deposit
7gns.Approve(cross(cur), stakerAddress, 1_000_000_000+GetDepositGnsAmount())
Usage
The proxy functions receive a realm argument. From a caller realm with cur realm, pass
cross(cur) as that first argument:
1// Stake an existing position
2StakeToken(cross(cur), 123, "g1referrer...")
3
4// Create an external incentive (rewardAmount is an int64 token-unit amount)
5CreateExternalIncentive(
6 cross(cur),
7 "gno.land/r/gnoland/wugnot.wugnot:gno.land/r/gnoswap/gns.GNS:3000",
8 "gno.land/r/gnoswap/gns.GNS",
9 1_000_000_000,
10 startTime,
11 endTime,
12)
13
14// Collect while the position is staked
15CollectReward(cross(cur), 123)
16
17// Unstake: this returns the NFT and creates an exit checkpoint; it does not collect
18UnStakeToken(cross(cur), 123)
19
20// Collect the checkpoint, either per source or all at once
21CollectEmissionReward(cross(cur), 123)
22CollectExternalIncentiveReward(cross(cur), 123, incentiveId)
Security
- Positions locked during staking
- External incentives require GNS deposit
- Warmup periods prevent gaming
- Unclaimed rewards properly redirected
- Hook integration ensures accurate tracking