Skip to content

Latest commit

 

History

History
455 lines (306 loc) · 16.1 KB

File metadata and controls

455 lines (306 loc) · 16.1 KB

StellarSend Contracts

Soroban smart contracts powering the StellarSend global money transfer platform. All contracts are written in Rust and deployed on the Stellar network.


Table of Contents


Architecture Overview

┌─────────────────────────────────────────────────────────┐
│                      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.


Contracts

stellar_send

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,
}

fee_collector

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

token_bridge

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

escrow

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

Differentiator Features

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.

Scheduled & recurring payments

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.

Split / batch payments

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.

Payment requests / invoicing

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 / conditional transfers

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.


Getting Started

Prerequisites

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

Clone and install

git clone https://github.com/StellarSend/contracts.git
cd contracts

The workspace Cargo.toml pins all dependency versions. No additional install step is required.


Building

Build all contracts to WASM:

make build
# or directly:
bash scripts/build.sh

Individual contract:

soroban contract build --package stellar_send
soroban contract build --package fee_collector
soroban contract build --package token_bridge

Compiled WASM files are written to target/wasm32-unknown-unknown/release/.


Testing

Run the full test suite:

make test
# or:
bash scripts/test.sh

Individual contract tests:

cargo test -p stellar_send
cargo test -p fee_collector
cargo test -p token_bridge

Run with output:

cargo test -p stellar_send -- --nocapture

Deployment

Testnet

# 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>

Mainnet

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.


Contract API Reference

stellar_send

initialize(admin, fee_bps, fee_collector) → Result<(), StellarSendError>

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 deployed fee_collector contract.

send_payment(from, to, token, amount, memo) → Result<PaymentRecord, StellarSendError>

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

set_fee(new_fee_bps) → Result<(), StellarSendError>

Update the protocol fee. Admin only.

pause() / unpause() → Result<(), StellarSendError>

Halt or resume new payments. Admin only.

transfer_admin(new_admin) → Result<(), StellarSendError>

Propose a new admin (step 1 of 2). Current admin only.

accept_admin() → Result<(), StellarSendError>

Complete the admin rotation (step 2 of 2). Proposed admin only.

get_config() → Result<ContractConfig, StellarSendError>

Return current configuration.

get_payment_record(from, seq) → Result<PaymentRecord, StellarSendError>

Retrieve a stored payment record by sender address and sequence number.


fee_collector

initialize(admin, treasury) → Result<(), FeeCollectorError>

collect_fee(token, amount) → Result<(), FeeCollectorError>

Record fee receipt. The token transfer must already have been made by stellar_send.

withdraw(token, amount, recipient) → Result<(), FeeCollectorError>

Withdraw fees to recipient. Admin only.

get_balance(token) → i128

Return current token balance held by the contract.

get_total_collected(token) → i128

Return the lifetime total of fees collected in token.


token_bridge

initialize(admin, underlying_token) → Result<(), TokenBridgeError>

wrap(from, amount) → Result<i128, TokenBridgeError>

Deposit amount of underlying token; credit wrapped balance.

unwrap(from, amount) → Result<i128, TokenBridgeError>

Burn amount of wrapped balance; return underlying tokens.

get_wrapped_balance(holder) → i128

get_underlying_token() → Result<Address, TokenBridgeError>


Events

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

Security

Admin key management

  • 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_collector treasury address is separate from the admin key.

Audit status

Contract Auditor Status
stellar_send In progress
fee_collector In progress
token_bridge In progress

Reporting vulnerabilities

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.


Contributing

We welcome contributions! Please read CONTRIBUTING.md before opening a pull request.

  1. Fork the repo
  2. Create a feature branch: git checkout -b feat/my-feature
  3. Make your changes and add tests
  4. Run make test — all tests must pass
  5. Open a PR against main

License

MIT © 2026 StellarSend. See LICENSE for full text.