Fund your dependencies on-chain. Weighted, nestable, pull-based fund splits for Stellar.
A split routes everything it receives to a weighted list of recipients. A recipient is an
account (G... or C...) or another split, so projects can build dependency trees: an app
routes 10% of its income to a split of its open-source dependencies, which routes a share to
their dependencies.
The goal is payout rails for dependency-weighted public-goods funding on Stellar: weights can come from a dependency graph (e.g. PG Atlas), and any income stream (grant tranches, bounty pools, revenue, x402 payments) can be routed through a split.
Status: pre-audit MVP on
soroban-sdk27, deployed on Stellar testnet asCCNQHI3P…5POG. Not deployed to mainnet. Do not use with real funds.The project landing page lives in
site/: static HTML, CSS and JavaScript with no build step.
Earlier Soroban splitters are admin-triggered, push funds to every recipient, or are no longer maintained. This contract is designed around Soroban's constraints instead:
| Upstream | |
|---|---|
| Who triggers payouts | Anyone (collect, pull are permissionless) |
| Cost of a deposit | O(1), independent of recipient count |
| One bad recipient (missing trustline, frozen balance, archived entry) | Only affects itself: nothing is pushed in bulk |
| Changing recipients | Starts a new epoch; money already received can never be redirected |
| Permanence | lock makes recipients immutable |
| Nesting | Recipients can be splits; funds move one hop per pull |
| Fee-on-transfer / misreporting tokens | Credits the measured balance change, capped at the requested amount |
| Rounding | Cumulative settlement: at most 1 base unit lost per recipient per epoch |
| License | Apache-2.0 |
funder ──deposit──▶ split #1 (epoch 0)
├─ 90% maintainer ──collect──▶ maintainer
└─ 10% split #0 ────pull─────▶ split #0 (epoch 0)
├─ 60% lib-one ──collect──▶ lib-one
└─ 40% lib-two ──collect──▶ lib-two
- Weights are parts per million and must sum to exactly
1_000_000. Max 50 recipients per split. - Each
(split, epoch, token)tracksacc, the total ever credited. A recipient with weightwis owedfloor(w * acc / 1_000_000)over the epoch's lifetime; each settlement pays the difference against its stored checkpoint. update_recipientsstarts a new epoch. Deposits always credit the current epoch; past epochs remain claimable forever by their recipients.- All tokens are held by this single contract with per-split, per-epoch, per-token accounting, so a misbehaving token can only affect its own balances.
- Touched storage entries have their TTL extended (towards 120 days, at most 30 days per call), so no caller pays unbounded rent. Entries idle for longer archive and are restored automatically by the next transaction that touches them.
Estimated mainnet fees per call, measured with cargo test cost_report -- --ignored --nocapture
using mainnet TTL settings and the live rent rate (1,000 stroops/KB; the SDK estimator's 12,000 is a
deliberate overestimate). These come from the native test host and exclude Wasm VM costs, so treat
them as lower-bound estimates until confirmed by testnet simulation.
| Call | CPU instructions | Writes | Est. fee |
|---|---|---|---|
create_split (50 recipients, prepays 120 days of rent) |
~1.96M | 4 | ~1.06 XLM |
deposit (first per token) |
~0.38M | 4 | ~0.075 XLM |
deposit (subsequent) |
~0.41M | 4 | ~0.011 XLM |
collect (first time, creates checkpoint) |
~0.50M | 3 | ~0.078 XLM |
pull (nested split) |
~0.31M | 2 | ~0.062 XLM |
update_recipients (50 recipients) |
~1.98M | 3 | ~1.03 XLM |
All calls use well under 1% of the 400M instruction and 200 write-entry per-transaction limits. Rent dominates: a split's recipient list costs about 0.17 XLM per KB per 120 days.
Deployed on testnet (Protocol 28, stellar-cli 28.0.0) as
CCNQHI3PBXFDMGOM32WAY5UB6WLPU2X4RLK6XF7FRRFAZ3GCHRVY5POG
(optimized Wasm: 13,709 bytes). A 70/30 split received 10 XLM and 1 XLM deposits; both recipients
collected exactly 7 + 0.7 XLM and 3 XLM.
| Call | Fee charged |
|---|---|
deposit (steady state) |
0.0088 XLM |
collect (steady state) |
0.0242 XLM |
First calls on a fresh deployment (create_split, first deposit, first collects) |
3.1-4.0 XLM each |
The first-call fees are almost entirely storage rent (e.g. the first deposit: 40.29M stroops refundable rent vs 16.7K stroops CPU/IO). Testnet's minimum persistent TTL is 7 days, so early calls top the contract instance, Wasm code and new entries up towards the 120-day target in 30-day steps. On mainnet, entries start at the 120-day minimum, so these top-ups are small daily extensions instead.
| Function | Auth | Description |
|---|---|---|
create_split(owner, recipients, locked) -> u64 |
owner | Create a split |
update_recipients(split_id, recipients) -> u32 |
owner | New recipients, new epoch (fails if locked) |
lock(split_id) |
owner | Make recipients permanent |
transfer_ownership(split_id, new_owner) |
owner + new owner | Change owner |
deposit(from, split_id, token, amount) -> i128 |
from | Fund a split; returns amount credited |
pull(parent_id, parent_epoch, child_id, token) -> i128 |
none | Move a nested split's share into it |
collect(split_id, epoch, account, token) -> i128 |
none | Pay an account its share, to itself |
collect_to(split_id, epoch, account, token, to) -> i128 |
account | Pay to any (muxed) address |
claimable(split_id, epoch, payee, token) -> i128 |
none | Amount settleable now |
get_split, get_recipients, get_received, next_split_id |
none | Views |
Events (#[contractevent]): split_created, recipients_updated, split_locked,
ownership_transferred, deposited, pulled, collected. Split id (and token where relevant)
are topics for indexing.
Requires Rust (the toolchain pinned in rust-toolchain.toml, including the wasm32v1-none target, installs automatically) and Stellar CLI 28.0.0 or newer. See CONTRIBUTING.md for the full workflow.
To call the deployed contract yourself, follow the testnet walkthrough: every command, transaction hash and fee from the live run, including why the first calls cost 4 XLM and the ones after cost 0.0088.
cargo test # unit tests (mainnet resource limits enforced)
stellar contract build # builds target/wasm32v1-none/release/upstream.wasmMapped to SCF Build Award milestones.
MVP
- Core splits contract: create, update (epochs), lock, ownership, deposit, pull, collect
- Unit tests incl. rounding, auth, nesting, token isolation, max-size split under network limits
- Testnet deployment and end-to-end verification
- TypeScript bindings/SDK
- Fuzz / property tests for accounting invariants
Testnet
- Repo payees: fund a GitHub repo before its maintainer claims it, with a refund deadline and a swappable attestation verifier
- PG Atlas weight import: generate dependency splits from the dependency graph
- Per-split deposit addresses so plain payments (e.g. x402
payTo) route into a split - Indexer + dashboard for splits, flows and claimable balances
Mainnet
- Audit via the Soroban Audit Bank
- Mainnet deployment, docs, monitoring
- Streams on top of splits
Contributions are welcome: tests, the TypeScript SDK, and the roadmap features above. Read CONTRIBUTING.md to get set up, and look for issues labeled good first issue. Everyone taking part follows the Code of Conduct.
Upstream is unaudited. Report vulnerabilities privately as described in SECURITY.md, never in a public issue.
Apache-2.0. See LICENSE. Notable changes are recorded in CHANGELOG.md.