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.
Returns NotInitialized if the contract itself has never been
initialized, or PaymentRecordNotFound if it has, but no record exists
for the given (from, seq) pair.
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.