Skip to main content

Splitter V2 Contract

Source on GitHub: github.com/Dobprotocol/stellar-distribution-contractscontracts/splitter_v2/

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.

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+
Share snapshotAt distribution timeTotal-supply snapshot at creation; user balance read at claim time
CommissionPer-recipientOnce at round creation

Core Functions

Initialization

fn init(
env: Env,
admin: Address,
shares: Vec<ShareDataKey>, // initial shareholders, must sum to 10,000
mutable: bool, // false = locked, admin cannot modify
participation_token: Address, // SAC token used for participation shares
) -> Result<(), Error>;

The mutable flag controls whether the admin can later call update_shares and similar mutating functions. Once lock_contract() is called, mutable becomes false permanently.

fn init_with_type(
env: Env,
admin: Address,
shares: Vec<ShareDataKey>,
mutable: bool,
participation_token: Address,
pool_type: PoolType, // Reward | Payroll | Treasury | Crowdfunding
) -> Result<(), Error>;

Distribution

fn create_distribution(env: Env, token_address: Address) -> Result<u64, Error>;

Calculates the distributable amount sitting in the contract for token_address, snapshots the participation-token total supply, deducts the platform commission once, and creates a DistributionRound with claimable_from = now + claim_delay_seconds and expires_at = now + round_expiry_seconds. Returns the new round ID.

Claiming

fn claim(env: Env, shareholder: Address, round_id: u64) -> Result<i128, Error>;
fn claim_all(env: Env, shareholder: Address) -> Result<i128, Error>;

Each claim computes:

user_amount = round.total_amount * (user_share_balance / round.total_supply_snapshot)

claim_all iterates active rounds and claims those that are currently within their claim window.

Share Management

fn transfer_shares(env: Env, from: Address, to: Address, amount: i128) -> Result<(), Error>;
fn update_shares(env: Env, shares: Vec<ShareDataKey>) -> Result<(), Error>; // admin-only, mutable contracts only

Because shares are SAC tokens, they can also be transferred via standard Stellar token operations.

Share System

The frontend (Token Studio) treats 10,000 shares = 100% as a UI convention so that users see clean percentages:

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

This 10,000 unit total is enforced at init and update_shares only (storage.rs::TOTAL_SHARES = 10_000). The participation token itself is a regular SAC with i128 balances; the contract does not enforce that future totals stay at 10,000 if the SAC is modified directly.

SAC Participation Tokens

Pools are wired to a Stellar Asset Contract token at init. Holders can transfer those tokens via standard Stellar operations, list them on SDEX, hold them in any Stellar wallet (Freighter, Lobstr), or trade them on the DobProtocol marketplace.

Distribution Configuration

pub struct DistributionConfig {
pub min_interval_seconds: u64, // default 12h (DEFAULT_MIN_DISTRIBUTION_INTERVAL)
pub claim_delay_seconds: u64, // default 0
pub round_expiry_seconds: u64, // default 1 year
pub last_distribution_time: u64,
}
  • Time-gating (min_interval_seconds): admin attempting create_distribution before the interval elapses gets Error::DistributionTooSoon. Set to 0 to disable.
  • Claim window (claim_delay_seconds): claims open at created_at + claim_delay_seconds.
  • Round expiry (round_expiry_seconds): after expiry, claims revert and the admin may reclaim_expired_round.

Auto-Scheduling

fn set_schedule(env: Env, first_distribution_time: u64, interval_seconds: u64, total_distributions: u32) -> Result<(), Error>;
fn disable_schedule(env: Env) -> Result<(), Error>;
fn trigger_scheduled_distribution(env: Env, token_address: Address) -> Result<u64, Error>;

Anyone can call trigger_scheduled_distribution once the next slot is due — the contract enforces the schedule, so bots can act as triggers without admin intervention.

Pool Types

pub enum PoolType {
Reward = 0,
Payroll = 1,
Treasury = 2,
Crowdfunding = 3,
}

Pool type is metadata used by the frontend (e.g. crowdfunding pools display "Contributors" rather than "Members").

Commission

const DEFAULT_COMMISSION_ADDRESS: &str = "GCYBJHXG4JRODEFRVXHFWDHRQQSEYYBM2P455ME3OGETCURTQJLZVX72";
const BUY_COMMISSION_BPS: i128 = 150; // 1.5% on share buys via the contract
const DISTRIBUTION_COMMISSION_BPS: i128 = 50; // 0.5% on distributions

The commission recipient (default address above) can update both rates and rotate the recipient via set_commission_recipient, set_buy_commission_rate, set_distribution_commission_rate (capped at 5000 bps).

Marketplace (sale listings)

SaleListingDataKey lets a holder escrow participation tokens with a price-per-share denominated in any payment token (typically USDC). Active listings are tracked in ActiveListings.

Admin Transfer & Lock

fn set_admin(env: Env, new_admin: Address) -> Result<(), Error>;
fn lock_contract(env: Env) -> Result<(), Error>;

lock_contract flips mutable to false permanently — useful once a pool's structure is finalised.

Events

Event topicEmitted on
("admin", "updated")set_admin
("dist", "created")create_distribution
("dist", "claimed")claim, claim_all
("config", "updated")set_distribution_config
("schedule", "set") / ("schedule", "off")set_schedule / disable_schedule
("schedule", "trig")trigger_scheduled_distribution
("dist", "reclaimed")reclaim_expired_round

(See splitter_v2/src/event.rs for exact topics.)

Errors

ErrorDescription
DistributionTooSoonmin_interval_seconds not yet elapsed.
ClaimsNotOpenYetclaim_delay_seconds not yet elapsed.
RoundExpiredPast expires_at.
ScheduleNotConfigured / ScheduleNotEnabled / ScheduleCompleted / ScheduledDistributionNotDueAuto-schedule failures.
RoundNotExpired / NothingToReclaimreclaim_expired_round guards.
InvalidCommissionRateSetting commission outside [0, 5000] bps.

Data Structures

pub struct DistributionRound {
pub id: u64,
pub token: Address,
pub total_amount: i128,
pub total_supply_snapshot: i128,
pub created_at: u64,
pub claimable_from: u64,
pub expires_at: u64,
pub is_finalized: bool,
pub total_claimed: i128,
}

pub struct ConfigDataKey {
pub admin: Address,
pub mutable: bool,
pub participation_token: Address,
pub pool_type: PoolType,
}

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
stellar contract install \
--wasm target/wasm32-unknown-unknown/release/soro_splitter_v2.wasm \
--source admin \
--network mainnet

Deployed WASM Hashes

TODO: verify and fill in. The active per-network WASM hashes are stored in the Token Studio backend networks table (stellar_pool_wasm_hash_v2 column).