nvana_rev_epoch
High-precision revenue distribution with time-decay bonuses.
What is this?
A library for distributing revenue to participants using time-weighted bonuses. Early participants get higher multipliers on their shares, creating a natural incentive for timely participation.
Use cases:
- Revenue sharing protocols (staking rewards, protocol fees)
- Liquidity mining programs with time-decay incentives
- Fair launch mechanisms with early participant bonuses
- Any system where you want to reward early participation
Key Features
- Pure integer math - No floating point, no heavy dependencies
- High precision - Micro basis points (0.0001% granularity) with WAD scaling (18 decimals)
- Overflow-safe - All arithmetic uses checked operations
Quick Start
use *;
// 1. Implement the traits for your storage backend
// 2. Use the provided functions
How It Works
Time-Decay Bonus
The bonus decays linearly from max at epoch start to zero at epoch end:
time_remaining = epoch_duration - (now - epoch_start)
bonus_mbps = (max_bonus_mbps × time_remaining) / epoch_duration
effective_shares = raw_shares × (1 + bonus_mbps / 10_000_000)
Example with 50% max bonus (5,000,000 mbps):
- At epoch start (100% time remaining): 1.5x shares (50% bonus)
- At epoch midpoint (50% time remaining): 1.25x shares (25% bonus)
- At epoch end (0% time remaining): 1.0x shares (0% bonus)
Revenue Distribution
After settling:
revenue_per_share = total_pot / total_effective_shares (WAD-scaled)
user_revenue = user_shares × revenue_per_share (floored to u64)
All intermediate calculations use WAD scaling (×10^18) to preserve precision.
Architecture
Four traits define the system:
| Trait | Purpose | Lifetime |
|---|---|---|
RevEpochConfig |
Global configuration singleton | Permanent |
RevEpochDistributor |
Per-epoch accounting ledger | Persists after epoch ends |
RevEpochUserGlobalAccount |
Per-user revenue accumulator | Permanent |
RevEpochUserLocalAccount |
Per-user per-epoch shares | Ephemeral (deleted after claim) |
Data Structure Relationships
Cardinality
- 1
RevEpochConfigper tenant - Your application has one global config that defines epoch duration and max bonus - N
RevEpochDistributorper config - Each epoch gets its own distributor. You can run epochs sequentially or overlapping - 1
RevEpochUserGlobalAccountper (config, user) pair - Each user has exactly one global account where settled revenue accumulates - N
RevEpochUserLocalAccountper user - Each time a user adds shares to an epoch, they get a local account for that epoch
Creation Patterns
Config: Created once when you deploy your application. Contains settings like epoch_duration_seconds and max_bonus_mbps.
Distributor: Created by calling create_new_epoch(). This operation can be permissionless - anyone can start a new epoch once the previous one ends. Each distributor tracks:
- When the epoch started
- Total shares added (with bonuses applied)
- Revenue pot
- Settlement state
Global User Account: Created before a user's first interaction. Some applications create these eagerly (during user onboarding), others create them lazily (first time user adds shares).
Local User Account: Created automatically when a user calls add_shares() for an epoch. Stores the user's raw shares and effective shares (after bonus) for that specific epoch.
Usage Flow
- Epoch starts: Call
create_new_epoch()to create a newRevEpochDistributor - Users participate: Users call
add_shares()which:- Creates their
RevEpochUserLocalAccountif needed - Calculates time-decay bonus based on current timestamp
- Updates both the user's local account and the distributor's totals
- Creates their
- Revenue accumulates: Your application calls
distributor.increase_pot_by()as revenue comes in - Epoch ends: After
epoch_duration_seconds, callsettle_epoch()to:- Calculate
revenue_per_share - Mark the distributor as settled
- Lock it from further changes
- Calculate
- Users claim: Each user calls
settle_epoch_for_user()to:- Transfer their share from local account to global account
- Delete their local account (no longer needed)
- Users withdraw: Call
user_collect_rev()to withdraw from their global account
Foreign Key Structure
Config (1)
├── Distributor (N) - one per epoch
│ └── LocalUserAccount (N) - one per user per epoch
└── GlobalUserAccount (N) - one per user
Each distributor references its config. Each local user account references both its distributor and its global user account. The library validates these relationships through typed IDs.
API Reference
Core Functions
// Epoch management
Types
pub type Seconds = u64; // Unix timestamp
pub type Mbps = u32; // Micro basis points (10M = 100%)
pub const MBP_100: u32 = 10_000_000; // Constant for 100%
Testing
# Run all tests
# Run with optimizations (catches different bugs)
# Check for breaking changes (requires published version)
Safety Guarantees
| Feature | Implementation |
|---|---|
| Memory safety | #![deny(unsafe_code)] |
| Overflow protection | All arithmetic uses checked_*() operations |
| Precision | WAD scaling (18 decimals) with explicit flooring |
| Type safety | Strong typing for IDs, prevents mixing entities |
Error Handling
All functions return Result<T, RevEpochError>. Errors are:
Copy(cheap to pass around)PartialEq(easy to test)- Descriptive (clear error messages)
Common errors:
EpochNotFinished- Tried to settle too earlyEpochAlreadyFinished- Tried to add shares after settlementTimestampBeforeEpochStart- Invalid timestampSharesOverflow- Calculation would overflow u128
Performance Characteristics
| Operation | Complexity | Allocations |
|---|---|---|
create_new_epoch |
O(1) | 0 |
add_shares |
O(1) | 0 |
settle_epoch |
O(1) | 0 |
settle_epoch_for_user |
O(1) | 0 |
user_collect_rev |
O(1) | 0 |
All operations are constant-time with zero heap allocations.
FAQ
Q: What happens if the pot is zero? A: Users get zero revenue but their shares are tracked. You can add revenue later.
Q: What if no one adds shares? A: Settlement will succeed with revenue_per_share = 0. The pot stays locked (you can't reclaim it).
Q: Are there any decimal rounding concerns? A: All division happens last and floors down (defensive). WAD scaling preserves 18 decimal places throughout.
License
MIT