Soroban smart contracts powering the StellarSend global money transfer platform. All contracts are written in Rust and deployed on the Stellar network.
- Architecture Overview
- Contracts
- Differentiator Features
- Getting Started
- Building
- Testing
- Deployment
- Contract API Reference
- Events
- Security
- Contributing
- License
┌─────────────────────────────────────────────────────────┐
│ User / dApp │
└──────────────────────┬──────────────────────────────────┘
│ send_payment / send_path_payment
▼
┌─────────────────────────────────────────────────────────┐
│ stellar_send (Entry Point) │
│ │
│ • Validates sender + amount │
│ • Deducts protocol fee (bps) │
│ • Routes payment to recipient │
│ • Emits PaymentSent / PathPaymentSent events │
└─────────┬───────────────────────────┬───────────────────┘
│ forward fee │ DEX path swap
▼ ▼
┌──────────────────────┐ ┌─────────────────────────────┐
│ fee_collector │ │ token_bridge │
│ │ │ │
│ • Accumulates fees │ │ • wrap / unwrap assets │
│ • Admin withdrawal │ │ • Enables cross-asset │
│ • Per-token totals │ │ path payments │
└──────────────────────┘ └─────────────────────────────┘
The system is intentionally modular: stellar_send is the only contract that end users interact with directly. fee_collector and token_bridge are infrastructure contracts managed by the protocol admin.
Path: contracts/stellar_send/
The main entry point for the StellarSend protocol. Supports direct token transfers and DEX path payments between any Stellar addresses.
| Feature | Description |
|---|---|
| Direct payments | Transfer any SEP-41 token from sender to recipient |
| Path payments | Route through the Stellar DEX for cross-asset transfers |
| Fee deduction | Protocol fee in basis points (bps), deducted at source |
| Pause / unpause | Admin can halt new payments in an emergency |
| Payment records | Every transfer is stored on-ledger for auditability |
| Admin rotation | Two-step admin handover (propose → accept) |
Key data types:
pub struct ContractConfig {
pub admin: Address,
pub fee_bps: u32, // 100 bps = 1%
pub fee_collector: Address,
pub active: bool,
pub min_amount: i128,
pub max_amount: i128,
}
pub struct PaymentRecord {
pub from: Address,
pub to: Address,
pub token: Address,
pub net_amount: i128,
pub fee_amount: i128,
pub memo: String,
pub ledger: u32,
}Path: contracts/fee_collector/
Receives protocol fees forwarded by stellar_send and allows the treasury admin to withdraw accumulated balances.
| Feature | Description |
|---|---|
| Fee accounting | Tracks lifetime totals collected per token |
| Withdrawal | Admin-only; supports arbitrary recipient |
| Treasury management | Treasury address updatable by admin |
| Period limits | Configurable withdrawal limit per epoch |
| Multi-token | Operates on any SEP-41 token |
Path: contracts/token_bridge/
A lightweight wrap / unwrap bridge for non-native Stellar assets. Users deposit an underlying token and receive an equal amount of wrapped tokens, enabling cross-asset path payments through the StellarSend DEX flow.
| Feature | Description |
|---|---|
| wrap | Deposit underlying → receive wrapped balance |
| unwrap | Burn wrapped balance → receive underlying |
| Bridge fee | Configurable bps fee on wrap operations |
| Supply cap | Configurable maximum wrapped supply |
| Pause / unpause | Admin can halt wrapping in an emergency |
| Admin rotation | Two-step admin handover |
Path: contracts/escrow/
A standalone contract holding funds in on-chain custody until a time or arbiter condition releases them — deposits, marketplace holdbacks, milestone payments. Split out as its own crate (like fee_collector/token_bridge) since it has its own fund-custody lifecycle independent of stellar_send.
| Feature | Description |
|---|---|
| Time-locked release | Beneficiary can claim once unlock_time has passed |
| Optional arbiter | Can release to the beneficiary or refund to the depositor at any time |
| Anti-griefing refund | Depositor can only self-refund after unlock_time + a 1-week grace period |
| Escrow lookup | get_escrow for status/UI polling |
StellarSend isn't just a wallet — these on-chain building blocks are what any plain Stellar wallet can't do. All four are implemented directly in the contracts in this repo (stellar_send for subscriptions/batch/requests, a new escrow crate for conditional transfers), with unit tests covering the happy path and at least one failure case each.
stellar_send/src/subscription.rs — a payer authorizes a recurring transfer once; a keeper (anyone, including an unprivileged bot) can then trigger each due payment without the payer being online.
create_subscription(env, payer, recipient, token, amount, interval_seconds, start_time) -> Result<u64, StellarSendError>
cancel_subscription(env, id) -> Result<(), StellarSendError>
execute_subscription(env, id) -> Result<i128, StellarSendError>
get_subscription(env, id) -> Result<Subscription, StellarSendError>The payer grants the contract a standard SEP-41 token allowance (token.approve) when creating the subscription; execution pulls funds via transfer_from rather than requiring the payer's live signature, which is what makes unattended, scheduled execution possible while staying non-custodial — the contract enforces the authorization, not a trusted backend. Re-execution before next_execution_time is rejected.
stellar_send/src/batch.rs — one sender, many recipients, one call.
send_batch_payment(env, from, token, payments: Vec<(Address, i128)>) -> Result<Vec<PaymentRecord>, StellarSendError>Reuses the same fee-splitting logic as send_payment. Batches are all-or-nothing: every leg is validated up front, and since a Soroban host transaction is atomic, any transfer failure reverts the whole call — there's no partial batch state to reconcile.
stellar_send/src/payment_request.rs — turns StellarSend into a two-sided payment tool: a requester creates a shareable "invoice," and a payer fulfills it.
create_payment_request(env, requester, payer: Option<Address>, token, amount, memo, expiry) -> Result<u64, StellarSendError>
fulfill_payment_request(env, request_id, payer) -> Result<i128, StellarSendError>
cancel_payment_request(env, request_id) -> Result<(), StellarSendError>
get_payment_request(env, request_id) -> Result<PaymentRequest, StellarSendError>payer is optional — None means anyone can fulfill it. Expired or already-fulfilled/cancelled requests are rejected; the protocol fee is deducted exactly as in send_payment.
escrow/src/lib.rs — funds locked in-contract until a time or arbiter condition releases them.
create_escrow(env, depositor, beneficiary, token, amount, unlock_time, arbiter: Option<Address>) -> Result<u64, EscrowError>
release_escrow(env, escrow_id, caller) -> Result<(), EscrowError>
refund_escrow(env, escrow_id, caller) -> Result<(), EscrowError>
get_escrow(env, escrow_id) -> Result<Escrow, EscrowError>caller must authorize the call itself (caller.require_auth()) — the beneficiary can release after unlock_time, and an optional arbiter can release or refund at any time. The depositor can only self-refund after unlock_time plus a one-week grace period, so the beneficiary always gets a fair window to claim before the depositor can walk away with the funds.
Note on automation: because release/refund require the caller's own signature, they can't be executed by an unattended keeper the way subscriptions can — unless the keeper is the configured arbiter. The backend's escrow endpoints build an unsigned transaction for whichever party is acting to sign client-side, the same pattern used for a regular payment.
| Tool | Version | Install |
|---|---|---|
| Rust | stable (see rust-toolchain.toml) |
curl https://sh.rustup.rs -sSf | sh |
| Soroban CLI | ≥ 21.0.0 | cargo install --locked soroban-cli |
| wasm32 target | — | rustup target add wasm32-unknown-unknown |
git clone https://github.com/StellarSend/contracts.git
cd contractsThe workspace Cargo.toml pins all dependency versions. No additional install step is required.
Build all contracts to WASM:
make build
# or directly:
bash scripts/build.shIndividual contract:
soroban contract build --package stellar_send
soroban contract build --package fee_collector
soroban contract build --package token_bridgeCompiled WASM files are written to target/wasm32-unknown-unknown/release/.
Run the full test suite:
make test
# or:
bash scripts/test.shIndividual contract tests:
cargo test -p stellar_send
cargo test -p fee_collector
cargo test -p token_bridgeRun with output:
cargo test -p stellar_send -- --nocapture# Set environment
export STELLAR_NETWORK=testnet
export STELLAR_RPC_URL=https://soroban-testnet.stellar.org
export STELLAR_ACCOUNT=<your-secret-key>
# Deploy fee_collector first (stellar_send depends on its address)
bash scripts/deploy.sh fee_collector
# Deploy token_bridge
bash scripts/deploy.sh token_bridge
# Deploy stellar_send, passing fee_collector address
bash scripts/deploy.sh stellar_send <fee_collector_address>Same steps as testnet; update STELLAR_NETWORK=mainnet and STELLAR_RPC_URL=https://horizon.stellar.org.
Note: Mainnet deployments require a multi-sig admin key. Refer to the Security section.
Initialise the contract. Must be called exactly once by admin.
fee_bps: Protocol fee in basis points. Range:0..=10_000.fee_collector: Address of the deployedfee_collectorcontract.
Transfer amount of token from from to to. The protocol fee is deducted from amount before transfer.
Requires auth from: from
send_path_payment(from, to, send_token, send_amount, dest_token, min_dest_amount, path) → Result<i128, StellarSendError>
Route a payment through the Stellar DEX. The fee is deducted from send_amount before the swap is attempted.
Requires auth from: from
Update the protocol fee. Admin only.
Halt or resume new payments. Admin only.
Propose a new admin (step 1 of 2). Current admin only.
Complete the admin rotation (step 2 of 2). Proposed admin only.
Return current configuration.
Retrieve a stored payment record by sender address and sequence number.
Record fee receipt. The token transfer must already have been made by stellar_send.
Withdraw fees to recipient. Admin only.
Return current token balance held by the contract.
Return the lifetime total of fees collected in token.
Deposit amount of underlying token; credit wrapped balance.
Burn amount of wrapped balance; return underlying tokens.
All contracts emit structured events consumable by the Horizon API or Soroban event subscriptions.
| Contract | Topic | Data |
|---|---|---|
| stellar_send | payment_sent |
(from, to, token, net_amount, fee_amount, memo) |
| stellar_send | path_payment_sent |
(from, to, send_token, send_amount, dest_token, dest_amount, fee_amount) |
| stellar_send | fee_updated |
(old_bps, new_bps) |
| stellar_send | paused / unpaused |
ledger |
| fee_collector | fee_received |
(token, amount) |
| fee_collector | fee_withdrawn |
(token, recipient, amount) |
| token_bridge | wrapped |
(underlying, amount) |
| token_bridge | unwrapped |
(underlying, amount) |
Subscribe to events via Horizon:
soroban events --contract-id <CONTRACT_ID> --network testnet- All admin keys should be multi-sig accounts on mainnet.
- Admin rotation uses a two-step propose/accept pattern to prevent key loss.
- The
fee_collectortreasury address is separate from the admin key.
| Contract | Auditor | Status |
|---|---|---|
| stellar_send | — | In progress |
| fee_collector | — | In progress |
| token_bridge | — | In progress |
Please report security issues privately via email to security@stellarsend.io. Do not open a public issue for a security vulnerability. See SECURITY.md for our responsible disclosure policy.
We welcome contributions! Please read CONTRIBUTING.md before opening a pull request.
- Fork the repo
- Create a feature branch:
git checkout -b feat/my-feature - Make your changes and add tests
- Run
make test— all tests must pass - Open a PR against
main
MIT © 2026 StellarSend. See LICENSE for full text.