nvana_rev_epoch

Crates.ionvana_rev_epoch
lib.rsnvana_rev_epoch
version0.1.0
created_at2025-12-23 00:09:55.56276+00
updated_at2025-12-23 00:09:55.56276+00
descriptionHigh-precision revenue distribution library with time-decay bonus epochs
homepage
repository
max_upload_size
id2000499
size119,227
(nirvana-sid)

documentation

https://docs.rs/nvana_rev_epoch

README

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 nvana_rev_epoch::*;

// 1. Implement the traits for your storage backend
struct MyConfig { /* your storage */ }
impl RevEpochConfig<MyId> for MyConfig { /* ... */ }

// 2. Use the provided functions
fn distribute_revenue() -> Result<()> {
    // Create an epoch that lasts 24 hours with 50% max bonus
    create_new_epoch(&mut config, current_time)?;

    // User adds shares - gets time-decay bonus
    add_shares(&config, &mut distributor, &mut user, 1000, current_time)?;

    // Accumulate revenue
    distributor.increase_pot_by(50_000)?;

    // After epoch ends, settle and distribute
    settle_epoch(&config, &mut distributor, end_time)?;
    settle_epoch_for_user(&distributor, &mut user_local, &mut user_global)?;

    // User withdraws
    let amount = user_collect_rev(&mut user_global);

    Ok(())
}

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 RevEpochConfig per tenant - Your application has one global config that defines epoch duration and max bonus
  • N RevEpochDistributor per config - Each epoch gets its own distributor. You can run epochs sequentially or overlapping
  • 1 RevEpochUserGlobalAccount per (config, user) pair - Each user has exactly one global account where settled revenue accumulates
  • N RevEpochUserLocalAccount per 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

  1. Epoch starts: Call create_new_epoch() to create a new RevEpochDistributor
  2. Users participate: Users call add_shares() which:
    • Creates their RevEpochUserLocalAccount if needed
    • Calculates time-decay bonus based on current timestamp
    • Updates both the user's local account and the distributor's totals
  3. Revenue accumulates: Your application calls distributor.increase_pot_by() as revenue comes in
  4. Epoch ends: After epoch_duration_seconds, call settle_epoch() to:
    • Calculate revenue_per_share
    • Mark the distributor as settled
    • Lock it from further changes
  5. 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)
  6. 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
pub fn create_new_epoch(config: &mut C, now: u64) -> Result<()>

// User participation
pub fn add_shares(
    config: &C,
    distributor: &mut D,
    user: &mut U,
    raw_shares: u64,
    now: u64
) -> Result<()>

// Settlement
pub fn settle_epoch(config: &C, distributor: &mut D, now: u64) -> Result<()>
pub fn settle_epoch_for_user(
    distributor: &D,
    user_local: &mut UL,
    user_global: &mut UG
) -> Result<u64>

// Withdrawal
pub fn user_collect_rev(user_global: &mut UG) -> u64

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
cargo test

# Run with optimizations (catches different bugs)
cargo test --release

# Check for breaking changes (requires published version)
cargo install cargo-semver-checks
cargo semver-checks

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 early
  • EpochAlreadyFinished - Tried to add shares after settlement
  • TimestampBeforeEpochStart - Invalid timestamp
  • SharesOverflow - 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

Commit count: 0

cargo fmt