You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The Nova Rewards smart contract ecosystem is a modular, Soroban-based loyalty platform deployed on the Stellar network. Each contract owns a single responsibility; cross-contract interactions are kept explicit and minimal to reduce attack surface.
Admin-gated mutations — all state-changing privileged operations require require_auth() from the stored admin address.
Two-step admin transfer — ownership changes go through a propose → accept flow to prevent accidental lockout.
Fixed-point arithmetic — yield and payout calculations use i128 with a SCALE_FACTOR of 1_000_000 to eliminate rounding dust.
TTL management — persistent storage entries are extended on every read/write (31-day TTL for token state, 365-day TTL for vesting schedules).
Idempotent migrations — the migrate() entry-point is version-gated and safe to call multiple times.
Contract Reference
1. NovaToken
Source:nova_token/src/lib.rs
Purpose
ERC-20-style fungible token on Soroban. Manages balances and allowances for the NOVA reward token. Mint is admin-gated; burn and transfer are caller-gated.
Storage Layout
Key
Type
Storage Tier
Description
DataKey::Admin
Address
Instance
Contract administrator
DataKey::Balance(Address)
i128
Persistent (TTL 31d)
Token balance per account
DataKey::Allowance(Address, Address)
i128
Persistent (TTL 31d)
Spend allowance: owner → spender
Public Functions
Signature
Parameters
Returns
Access Control
initialize(env, admin)
admin: Address
()
One-time; panics if already set
mint(env, to, amount)
to: Address, amount: i128
()
Admin::require_auth()
burn(env, from, amount)
from: Address, amount: i128
()
from.require_auth()
transfer(env, from, to, amount)
from: Address, to: Address, amount: i128
()
from.require_auth()
approve(env, owner, spender, amount)
owner: Address, spender: Address, amount: i128
()
owner.require_auth()
balance(env, addr)
addr: Address
i128
Public read
allowance(env, owner, spender)
owner: Address, spender: Address
i128
Public read
Emitted Events
Event Name
Topics
Data
mint
("nova_tok", "mint")
(to: Address, amount: i128)
burn
("nova_tok", "burn")
(from: Address, amount: i128)
transfer
("nova_tok", "transfer")
(from: Address, to: Address, amount: i128)
approve
("nova_tok", "approve")
(owner: Address, spender: Address, amount: i128)
2. RewardPool
Source:reward_pool/src/lib.rs
Purpose
Custodial treasury that holds NOVA tokens earmarked for reward distributions. Acts as the funding source for ClaimDistribution and Vesting payouts. Deposit and withdrawal are admin-gated to prevent unauthorised draining.
Storage Layout
Key
Type
Storage Tier
Description
Admin
Address
Instance
Pool administrator
PoolBalance
i128
Instance
Total tokens held in the pool
Public Functions
Signature
Parameters
Returns
Access Control
initialize(env, admin)
admin: Address
()
One-time initialisation
deposit(env, amount)
amount: i128
()
Admin::require_auth()
withdraw(env, to, amount)
to: Address, amount: i128
()
Admin::require_auth(); panics on overdraft
balance(env)
—
i128
Public read
Emitted Events
Event Name
Topics
Data
deposit
("reward_pool", "deposit")
(amount: i128)
withdraw
("reward_pool", "withdraw")
(to: Address, amount: i128)
3. ClaimDistribution
Source: Implemented within the nova-rewards module; claim logic is coordinated between RewardPool and on-chain Merkle proof verification.
Purpose
Enables users to claim pre-allocated NOVA rewards by submitting a valid Merkle proof. Prevents double-claims via a per-address claimed flag. On successful verification, instructs RewardPool to transfer tokens and emits a claimed event consumed by the backend webhook.
Allows users to lock NOVA tokens and earn time-proportional yield. The annual rate is set in basis points by the admin. Yield is calculated using fixed-point arithmetic (SCALE_FACTOR = 1_000_000, SECONDS_PER_YEAR = 31_536_000) to avoid rounding errors. Only one active stake per address is permitted at a time.
Centralised access-control contract. Implements a two-step admin transfer (propose → accept) and an optional multisig threshold with a configurable signer list. All privileged stubs (mint, withdraw, update_rate, pause) are gated behind require_auth().
Not a standalone contract — event emission is embedded directly in each contract using env.events().publish(topics, data). This section documents the canonical event taxonomy used across the system for indexing and webhook consumption.
Global Event Index
Contract
Topic 1
Topic 2
Data Payload
NovaToken
"nova_tok"
"mint"
(to, amount)
NovaToken
"nova_tok"
"burn"
(from, amount)
NovaToken
"nova_tok"
"transfer"
(from, to, amount)
NovaToken
"nova_tok"
"approve"
(owner, spender, amount)
AdminRoles
"adm_roles"
"adm_prop"
(current_admin, proposed)
AdminRoles
"adm_roles"
"adm_xfer"
(old_admin, new_admin)
Vesting
"vesting"
"tok_rel"
(beneficiary, amount, timestamp)
ReferralHub
"referral"
"ref_reg"
(referrer, referred)
ReferralHub
"referral"
"ref_cred"
(referrer, referred, reward_amount)
Staking
"staked"
staker
(amount, timestamp)
Staking
"unstaked"
staker
(principal, yield, timestamp)
CrossAssetSwap
"swap"
user
(nova_amount, xlm_received, path)
Upgrade
"upgrade"
old_hash
new_hash, migration_version
ClaimDistribution
"claim_dist"
"claimed"
(claimant, amount)
7. Vesting
Source:vesting/src/lib.rs
Purpose
Time-locked token release for team allocations, investor grants, and long-term incentives. Supports multiple independent schedules per beneficiary, each with a configurable cliff and linear vesting duration. Tokens are drawn from an internal pool funded by the admin.
On-chain referral registry. Each wallet can be referred exactly once. Tracks referral counts per referrer for leaderboard purposes. The admin credits referrers with NOVA rewards drawn from an internal pool.
Burns NOVA points for the caller and routes them through a configured DEX router contract to receive XLM (or another output asset). Enforces a slippage guard (min_xlm_out) and caps multi-hop paths at 5 intermediate assets per Stellar protocol limits.
The pause() entry-point is defined in AdminRoles and acts as the system-wide circuit breaker. When invoked by the admin, downstream contracts check the paused state before executing sensitive operations. This prevents further state changes during incident response without requiring a full contract upgrade.
Storage Layout
Key
Type
Storage Tier
Description
DataKey::Paused
bool
Instance
Global pause flag (in AdminRoles)
Public Functions
Signature
Parameters
Returns
Access Control
pause(env)
—
()
Admin::require_auth()
unpause(env)
—
()
Admin::require_auth()
is_paused(env)
—
bool
Public read
Note: Contracts that respect the pause flag call AdminRoles::is_paused() at the top of any state-mutating function and panic with "contract is paused" if the flag is set.
Emitted Events
Event Name
Topics
Data
paused
("emergency", "paused")
(admin: Address, timestamp: u64)
unpaused
("emergency", "unpaused")
(admin: Address, timestamp: u64)
Token Claim Flow
sequenceDiagram
title Token Claim Flow
actor User
participant CD as ClaimDistribution
participant RP as RewardPool
participant EXT as External (Backend Webhook)
User->>CD: claim(claimant, amount, merkle_proof)
activate CD
CD->>CD: verify Merkle proof against stored root
CD->>CD: assert !is_claimed(claimant)
CD->>CD: mark claimant as claimed
CD->>RP: withdraw(claimant, amount)
activate RP
RP->>RP: assert pool_balance >= amount
RP->>RP: deduct amount from pool_balance
RP-->>CD: transfer confirmed
deactivate RP
CD->>CD: emit ("claim_dist", "claimed") → (claimant, amount)
CD-->>User: claim successful
deactivate CD
EXT->>EXT: ingest on-chain "claimed" event via Horizon stream
EXT->>EXT: trigger post-claim webhook (notify, analytics, off-chain sync)