Docs · Testnet Explorer · Contributing · Security
SplitStreamVault is a Soroban smart contract that pools funds (a single SEP-41 token per deployment — USDC or native XLM SAC) and settles them to contributors via three strategies:
- Merkle-proof claims — Cycle-based points payouts. The oracle posts a
payout-manifest root per cycle; after a 24-hour dispute window contributors
claim by proving a
(contributor, amount)leaf against the root and are credited a pull-payment balance they withdraw themselves. - Fixed basis-point waterfalls — recurring team/community/reserve splits. The admin configures a share list summing to exactly 10,000 bps and the oracle (or admin, manually) triggers distributions.
- Linear vesting streams — maintainer retention rewards, paid directly on claim.
The contract is pull-payment throughout: it never loops over an unbounded contributor list in a single transaction.
Part of SplitStream — this repo is one of three: splitstream-core (this repo, the Soroban vault contract that holds and settles the funds), splitstream-actions (the GitHub→chain bridge that computes each cycle's payout manifest and relays its Merkle root on-chain), and splitstream-sdk-cli (the client SDK and CLI contributors claim with and maintainers simulate and report with).
A posted cycle root is not immediately claimable. Maintainers have a fixed
24-hour window (CHALLENGE_WINDOW_SECS = 86_400) to catch a bad points
calculation and correct it on-chain, once, via
challenge_and_replace_root — before any contributor can extract funds against
it. Once the first claim succeeds, the root is final: replacement is rejected
after claims_started is set, and the window itself is bounded to 24h from the
original post (replacement never restarts it).
Rust 1.84+ is required (the wasm32v1-none target). Pinned SDK:
soroban-sdk = "27.0.6" (latest stable; do not use release candidates).
# Tests (soroban-sdk testutils; no network needed)
cargo test --workspace
# Lint
cargo clippy --all-targets -- -D warnings
# Deployable wasm — build with stellar-cli, never `cargo build` for the crate
stellar contract build --package splitstream-vault
# artifact: target/wasm32v1-none/release/splitstream_vault.wasm (~20 KB)CI (.github/workflows/ci.yml) runs cargo check, cargo test,
cargo clippy -D warnings, and stellar contract build on every push and PR
to main.
Admin, Oracle, Token live in instance storage (TTL bumped on every
admin-gated call). CycleData, CycleClaimed, Balance, FixedShares,
Vesting and SweepRequest live in persistent storage; every write
extends the entry TTL in the same call (storage::bump_persistent) — a missing
extend_ttl is the classic Soroban bug where accounting silently expires.
| Function | Auth | Purpose |
|---|---|---|
initialize(admin, oracle, token) |
admin | One-time setup; stores admin/oracle/SEP-41 token. |
deposit(from, amount) |
from | Pulls amount of the token into the vault. |
post_cycle_root(cycle_id, root, total_amount) |
oracle | Posts a payout-manifest root; one post per cycle. |
challenge_and_replace_root(cycle_id, new_root, new_total_amount) |
admin | Replaces the root within the dispute window (once). |
credit_claim(contributor, cycle_id, amount, proof) |
contributor | Verifies the Merkle proof and credits the claimable balance. |
withdraw(contributor) |
contributor | Pays out the claimable balance (checks-effects-interactions). |
configure_fixed_shares(shares) |
admin | Sets the bps split list (must sum to 10_000). |
distribute_fixed(amount) |
oracle | Credits each recipient amount * bps / 10_000. |
admin_distribute_fixed(amount) |
admin | Manual distribution; same body/events as distribute_fixed. |
create_vesting(contributor, total, duration_ledgers) |
admin | Creates/re-creates a linear vesting schedule (preserves claimed). |
claim_vested(contributor) |
contributor | Claims newly vested tokens, transferring them directly. |
request_sweep(to, amount) |
admin | Registers a sweep request, starting the 72h timelock. |
execute_sweep() |
admin | Executes the sweep once the timelock has elapsed. |
cancel_sweep() |
admin | Clears a pending sweep request (idempotent). |
get_balance(contributor) / get_cycle_info(cycle_id) / has_claimed(cycle_id, contributor) / get_vesting(contributor) / get_fixed_shares() |
— | Read-only views. |
| Vault contract | CCC2LP2LOYZOLA2JW4C4K7JMR3TRJZIKHDSQYSFJ3R3MCDJLVBT3PZOC |
| Token contract | CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC (native SAC) |
| Explorer | https://stellar.expert/explorer/testnet/contract/CCC2LP2LOYZOLA2JW4C4K7JMR3TRJZIKHDSQYSFJ3R3MCDJLVBT3PZOC |
| Network | Test SDF Network ; September 2015 (Testnet) |
contracts/vault/src/
├── lib.rs # #[contract] + #[contractimpl] entry points
├── storage.rs # DataKey enum + typed get/set helpers with TTL bumps
├── types.rs # CycleInfo, VestingData, SweepRequestData
├── errors.rs # #[contracterror] SplitStreamError
├── merkle.rs # leaf hashing + sorted-pair proof verification
├── claims.rs # credit_claim, withdraw
├── fixed_split.rs # configure_fixed_shares, distribute_fixed, admin_distribute_fixed
├── vesting.rs # create_vesting, claim_vested
├── sweep.rs # request/execute/cancel sweep
└── test.rs # one test module per feature
These are deliberate, documented decisions (each noted in its commit message):
SweepRequestis persistent, not temporary. Temporary entries have a Mainnet max TTL of 17,280 ledgers (~1 day), shorter than the 72-hour sweep timelock — a request there would expire before it could execute.distribute_fixedis split into two auth-gated functions. SDK 27 removedcurrent_contract_invoker(the caller-introspection API) in the 22.x auth rework, and a failedrequire_authaborts the invocation, so an "admin OR oracle" check cannot be expressed in one function.distribute_fixedis oracle-gated (the automated trigger);admin_distribute_fixedis the admin-gated manual twin. Both delegate to one shared internal body, so they can never drift.claim_vestedwith no schedule returnsNoVestingSchedule(error 17) — a dedicated variant so downstream SDKs can distinguish "no schedule" from "no balance".- Event topic names are full
Symbols (e.g."claim_credited","cycle_root_replaced") via the (deprecated-but-supported)Events::publishAPI, preserving the frozen topic names from the spec.
Contributions are welcome — see CONTRIBUTING.md for the build/test workflow and PR expectations, and SECURITY.md for the security model and responsible-disclosure process. Found a bug or have a feature idea? Open an issue.
- 💬 GitHub Issues — bug reports, feature requests, and design discussion
- 🔒 Security — report vulnerabilities privately per SECURITY.md
- 🗨️ Discord — https://discord.gg/DzSUheDtQ
- 📢 Telegram — https://t.me/+zOMeL6fD6uY1ODhk
Oyinkans0la12 Smart Contract Engineer GitHub |
Contact
GitHub Issues — primary channel for bugs, feature requests, and design discussion 🔒 For vulnerabilities, use a private security advisory per SECURITY.md |
This project is licensed under the MIT License — see LICENSE for details.