Skip to main content

Splitter V2 Contract

The Splitter V2 (soro-splitter-v2) is the current-generation distribution contract for Stellar Soroban. It replaces V1's push-based distribution model with a lazy pull-based architecture that scales to 10,000+ shareholders at constant gas cost.

Repository: github.com/Dobprotocol/stellar-distribution-contracts

V1 vs V2

AspectV1 (Push)V2 (Pull)
Distribution creationO(n) -- loops all shareholdersO(1) -- records round metadata
ClaimingAutomatic (during distribute call)User calls claim() -- O(1) per claim
Gas scalingLinear with shareholdersConstant regardless of shareholders
Max shareholders~500 (gas limit)10,000+ (no gas limit issue)
Share snapshotAt distribution timeAt claim time (reflects transfers)
CommissionDeducted per-recipientDeducted once at round creation

Why Pull-Based

In V1, the admin calls distribute() and the contract loops through every shareholder, calculating and sending their share in a single transaction. At ~500 shareholders, the transaction hits Soroban's gas limit and fails.

In V2, the admin calls create_distribution() which records the total amount and total shares for that round -- O(1). Each shareholder then calls claim() individually to receive their portion -- also O(1). The gas cost is independent of the number of shareholders.

Core Functions

Initialization

fn init(
env: Env,
admin: Address,
shares: Vec<(Address, i128)>,
allow_entry: bool,
participation_token: Address,
)

Initializes the pool with an admin, initial share allocations, and the SAC participation token address. The allow_entry flag controls whether new shareholders can join after initialization.

fn init_with_type(
env: Env,
admin: Address,
shares: Vec<(Address, i128)>,
allow_entry: bool,
participation_token: Address,
pool_type: PoolType,
)

Extended initialization that sets the pool type (Reward, Payroll, Treasury, or Crowdfunding).

Distribution

fn create_distribution(
env: Env,
admin: Address,
reward_token: Address,
amount: i128,
) -> u64

Creates a new distribution round. The admin deposits amount of reward_token into the contract. Returns the round ID. Commission is deducted at this point (once, not per-recipient).

Claiming

fn claim(
env: Env,
user: Address,
round_id: u64,
) -> i128

Claims the user's share from a specific distribution round. The claimable amount is calculated based on the user's share proportion at claim time:

user_amount = round_total_amount * (user_shares / total_shares)

Returns the amount claimed.

fn claim_all(
env: Env,
user: Address,
) -> Vec<(u64, i128)>

Claims from all unclaimed rounds in a single call. Returns a vector of (round_id, amount_claimed) pairs.

Share Management

fn transfer_shares(
env: Env,
from: Address,
to: Address,
amount: i128,
)

Transfers shares between addresses. Because V2 uses SAC participation tokens, shares are DEX-compatible and can be traded on Stellar's native DEX.

Share System

Shares in V2 use a 10,000-point scale:

10,000 shares = 100% ownership
1,000 shares = 10% ownership
100 shares = 1% ownership
1 share = 0.01% ownership

Shares are represented as Stellar Asset Contract (SAC) tokens, which means they are native Stellar assets that can be:

  • Transferred between wallets using standard Stellar operations
  • Listed on Stellar's native DEX (SDEX)
  • Traded on DobProtocol's marketplace
  • Held in any Stellar wallet (Freighter, Lobstr, etc.)

SAC Participation Tokens

When a pool is initialized, it is associated with a SAC token address. This token represents pool shares:

Pool: CCJHTPU3QHKULJEEVTMIH7YUHGGA6QGV7P3FXRVPSTSY2F7AODN6XLSL
SAC Token: CDTWX7U2HRD3N742IRLO4VY26HKNKVRIWM3CDLRL65SKJZQILH3ZNGML

Holder A: 5,000 shares (50%)
Holder B: 3,000 shares (30%)
Holder C: 2,000 shares (20%)

If Holder A transfers 1,000 shares to a new address, the new address becomes a shareholder and can claim from future distribution rounds.

Time-Gating

V2 introduces configurable time-gating to prevent distribution spam:

pub struct DistributionConfig {
pub min_interval_seconds: u64, // Default: 43200 (12 hours)
// ...
}

If the admin tries to create a distribution within min_interval_seconds of the last one, the transaction fails with DistributionTooSoon.

Setting min_interval_seconds to 0 disables time-gating.

Claim Window

A configurable delay between distribution creation and when claims open:

pub struct DistributionConfig {
pub claim_delay_seconds: u64, // e.g., 3600 (1 hour)
// ...
}

After a distribution round is created, users must wait claim_delay_seconds before they can claim. Attempting to claim too early fails with ClaimsNotOpenYet.

This allows time for share transfers to settle before claims begin, ensuring fair distribution.

Round Expiry and Reclaim

Distribution rounds can expire after a configurable period:

pub struct DistributionConfig {
pub round_expiry_seconds: u64, // e.g., 365 * 24 * 60 * 60 (1 year)
// ...
}

After expiry:

  • Users can no longer claim from the expired round (RoundExpired error)
  • The admin can reclaim unclaimed funds:
fn reclaim_expired_round(
env: Env,
admin: Address,
round_id: u64,
) -> i128

Returns the amount reclaimed. Fails with RoundNotExpired if the round has not expired, or NothingToReclaim if all funds were already claimed.

Auto-Scheduling

V2 supports automatic distribution scheduling:

fn set_schedule(
env: Env,
admin: Address,
first_time: u64,
interval_seconds: u64,
total_distributions: u64, // 0 = unlimited
)

Configures a schedule where distributions can be triggered at regular intervals:

first_time:          March 1, 2026 00:00 UTC
interval_seconds: 2592000 (30 days)
total_distributions: 12 (monthly for 1 year)

Once a scheduled distribution is due, anyone can trigger it:

fn trigger_scheduled_distribution(
env: Env,
reward_token: Address,
) -> u64

Fails with ScheduledDistributionNotDue if triggered early, or ScheduleCompleted if all scheduled distributions have been created.

Disable scheduling:

fn disable_schedule(env: Env, admin: Address)

Schedule Storage

pub struct ScheduleConfig {
pub enabled: bool,
pub first_distribution_time: u64,
pub interval_seconds: u64,
pub total_distributions: u64, // 0 = unlimited
pub completed_distributions: u64,
}

Pool Types

V2 supports pool type classification as metadata:

pub enum PoolType {
Reward = 0, // Default -- reward distribution pools
Payroll = 1, // Employee/contractor payments
Treasury = 2, // DAO treasury management
Crowdfunding = 3, // Crowdfunding campaigns
}

Pool type is set at initialization via init_with_type() and affects frontend display and behavior (e.g., crowdfunding pools show "Contributors" instead of "Members").

Admin Transfer

Following the Stellar SEP standard pattern:

fn set_admin(env: Env, new_admin: Address)

Transfers admin rights to a new address. Requires authorization from the current admin. Emits a SetAdmin event.

Events

EventDescriptionData
SetAdminAdmin transferredadmin (topic), new_admin (data)
dist_createdDistribution round createdRound ID, token, amount, shares
claimedUser claimed from roundRound ID, user, amount
config_updatedDistribution config changedUpdated config fields
schedule_setSchedule configuredSchedule config
sched_offSchedule disabled-
sched_distScheduled distribution triggeredRound ID
reclaimedAdmin reclaimed expired roundRound ID, amount

Error Types

ErrorCodeDescription
DistributionTooSoon25Time-gating interval not met
ClaimsNotOpenYet26Claim window delay not elapsed
RoundExpired27Round has passed expiry time
ScheduleNotConfigured28No schedule set
ScheduleNotEnabled29Schedule is disabled
ScheduleCompleted30All scheduled distributions done
ScheduledDistributionNotDue31Too early to trigger
InvalidScheduleConfig32Bad schedule parameters
RoundNotExpired33Cannot reclaim active round
NothingToReclaim34All funds already claimed

Data Structures

DistributionRound

pub struct DistributionRound {
pub id: u64,
pub token: Address,
pub total_amount: i128,
pub total_shares: i128,
pub is_finalized: bool,
pub created_at: u64,
pub claimable_from: u64, // created_at + claim_delay_seconds
pub expires_at: u64, // created_at + round_expiry_seconds
pub total_claimed: i128,
}

DistributionConfig

pub struct DistributionConfig {
pub min_interval_seconds: u64, // Time-gating (default: 43200)
pub claim_delay_seconds: u64, // Claim window delay
pub round_expiry_seconds: u64, // Round expiry time
pub last_distribution_time: u64, // Timestamp of last distribution
}

Building and Testing

cd /path/to/stellar-distribution-contracts

# Run tests (65 tests)
cargo test -p soro-splitter-v2

# Build WASM
cargo build --release --target wasm32-unknown-unknown -p soro-splitter-v2

# Deploy to mainnet
stellar contract install \
--wasm target/wasm32-unknown-unknown/release/soro_splitter_v2.wasm \
--source admin \
--network mainnet