A production-ready suite of Soroban smart contracts and tooling for tokenizing real-world assets (RWAs) on the Stellar network — with KYC/AML compliance, investor whitelisting, transfer/holding limits, and snapshot-based dividend distributions.
The "hug and ripples" concept — an RWA token "hugs" its holders through compliance (KYC-approved, whitelisted investors only) while dividend payouts ripple outward to every qualifying holder in proportion to their holdings.
| Contract | Description |
|---|---|
RWA Token (rwa-token) |
SEP-41 compliant token with compliance extensions: pause, KYC hook, investor registry, transfer/holding limits, dividend distribution |
KYC Hook (kyc-hook) |
KYC/AML verification with expiry, tiered levels, and batch approval |
Investor Registry (investor-registry) |
Whitelist with accredited status, purchase limits, purchase tracking, and batch operations |
Dividend Distributor (dividend-distributor) |
Multi-round dividend distribution with snapshot-based pro-rata calculation, claiming, round closure, and unclaimed fund withdrawal |
┌──────────────────┐
│ RWA Token │◄──── SEP-41 Standard
│ (rwa-token) │ (transfer, approve, mint, burn)
├──────────────────┤
│ Compliance Layer │
│ ┌────────────┐ │
│ │ KYC Hook │──│──► check_kyc(investor) → bool
│ └────────────┘ │
│ ┌────────────────┐
│ │Investor Registry│──► is_investor(investor) → bool
│ └────────────────┘
│ ┌──────────────────┐
│ │ Transfer/Holding │──► configurable limits
│ │ Limits │
│ └──────────────────┘
├──────────────────┤
│ Dividend Dist. │────► snapshot, pro-rata, claim
└──────────────────┘
├── contracts/ # Soroban smart contracts (Rust)
│ ├── rwa-token/ # SEP-41 token + compliance + dividends
│ ├── kyc-hook/ # KYC/AML hook
│ ├── investor-registry/ # Investor whitelist
│ └── dividend-distributor/ # Multi-round dividend distributor
├── cli/ # TypeScript CLI (deploy & manage)
├── scripts/ # Bash CLI + one-command Docker deploy
├── .github/workflows/ci.yml # CI: tests, coverage, lint, Docker build
├── Dockerfile # Runnable CLI image (contracts + stellar-cli)
├── docker-compose.yml # Local Soroban sandbox (pinned quickstart)
└── Makefile # All dev/deploy targets
- Rust — the toolchain is pinned in
rust-toolchain.toml(1.97.1+wasm32v1-none);rustupinstalls it automatically. - Node.js ≥ 22.12 (for the TypeScript CLI).
- Docker (optional, for the one-command local sandbox).
- stellar-cli ≥ 22 (optional if using Docker) —
stellar-cli 27.xis the pinned version in the image: https://github.com/stellar/stellar-cli - A Stellar account with testnet/mainnet funds (only for real deployments).
Spin up a local Soroban sandbox and deploy the entire contract suite:
./scripts/docker-deploy.shIf STELLAR_SECRET_KEY is not set, the script generates a fresh key for you and saves
it to the git-ignored .env. Done — the full suite is deployed to your local sandbox
and the config is saved to .rwa/deployment-local.json.
# 1. Build contracts (produces WASM in target/wasm32v1-none/release)
cargo build --release --target wasm32v1-none --workspace
# 2. Run tests
cargo test --workspace
# 3. Deploy to testnet (or set SOROBAN_NETWORK=mainnet)
export STELLAR_SECRET_KEY=S...
./scripts/rwa-cli.sh deploy --name "T-Bill Token" --symbol "TBILL" --decimals 6make check-all # fmt + clippy + tests + coverage + wasm + CLI checks (same as CI)
make docker-deploy # one-command local deployment
make deploy-testnet # deploy to testnet (STELLAR_SECRET_KEY + TOKEN_* vars)| Variable | Required | Default | Description |
|---|---|---|---|
STELLAR_SECRET_KEY |
yes | — | Admin/deployer secret key. Never commit a real key. |
SOROBAN_NETWORK |
no | testnet |
testnet | mainnet | local |
SOROBAN_RPC_URL |
no | per-network | Override the Soroban RPC endpoint |
RWA_CONFIG_DIR |
no | <repo>/.rwa |
Where deployment configs are stored |
RWA_ROOT |
no | auto-detected | Repo root (used by the Docker image) |
TOKEN_NAME/SYMBOL/DECIMALS |
no | see example | Token parameters for make deploy-* |
Copy .env.example to .env — it is auto-loaded by the scripts and is
git-ignored.
./scripts/rwa-cli.sh deploy --name "T-Bill Token" --symbol TBILL --decimals 6
./scripts/rwa-cli.sh configure --transfer-limit 1000000 --holding-limit 50000000
./scripts/rwa-cli.sh configure --set-kyc-hook C... --set-investor-registry C...
./scripts/rwa-cli.sh investors --add G... --accredited true --purchase-limit 100000
./scripts/rwa-cli.sh mint G... 10000
./scripts/rwa-cli.sh dividends --distribute 5000 --token C...
./scripts/rwa-cli.sh dividends --claim G...
./scripts/rwa-cli.sh pause | unpause | statuscd cli
npm install
npm run build
STELLAR_SECRET_KEY=S... node bin/rwa-cli.js deploy -n testnet --name "T-Bill Token" --symbol TBILL --decimals 6
node bin/rwa-cli.js configure -n testnet --transfer-limit 1000000 --holding-limit 50000000
node bin/rwa-cli.js investors -n testnet --add G... --accredited true --purchase-limit 100000
node bin/rwa-cli.js dividends -n testnet --distribute 5000 --token C...Both CLIs validate inputs (addresses, amounts, networks), read the saved deployment config, and print a summary after every operation.
Full function tables for all four contracts live in docs/CONTRACTS.md
(SEP-41 interface, compliance extensions, and dividend lifecycle). Every state-changing
function emits events (initialize, transfer, approve, mint, burn,
set_paused, kyc_approved, investor_added, dividend_distributed, …).
CI runs every check below; the local equivalents are identical.
| Check | Command | Gate |
|---|---|---|
| Rust formatting | cargo fmt --all --check |
clean |
| Clippy lint | cargo clippy --workspace --all-targets -- -D warnings |
zero warnings |
| Contract tests | cargo test --workspace |
60 tests, all green |
| Contract coverage | cargo llvm-cov --workspace --fail-under-lines 85 |
~95% lines |
| WASM release build | cargo build --release --target wasm32v1-none |
succeeds |
| Docs | RUSTDOCFLAGS=-D warnings cargo doc --workspace --no-deps |
clean |
| CLI typecheck/lint/format | npm run typecheck && npm run lint && npm run format:check |
clean |
| CLI tests | npm test |
45 tests, all green |
| CLI coverage | npm run coverage |
~98% lines |
| ShellCheck | docker run --rm -v $PWD:/mnt koalaman/shellcheck:stable /mnt/scripts/rwa-cli.sh |
clean |
| Docker image | docker build -t rwa-cli:ci . |
succeeds |
Dependency updates are automated via Dependabot for Cargo, npm and GitHub Actions. The Rust toolchain is pinned for reproducible builds.
docker compose up -d soroban-local # standalone network, RPC on :8000/rpc
./scripts/rwa-cli.sh deploy --network local # via SOROBAN_NETWORK=local
# or: make docker-deploy (builds the image and deploys everything)export STELLAR_SECRET_KEY=S...
make deploy-testnet # or: SOROBAN_NETWORK=mainnet make deploy-mainnet
./scripts/rwa-cli.sh statusDeployment configs (contract IDs, RPC, admin, token params) are written to
.rwa/deployment-<network>.json after every deploy.
- Admin-only state changes are guarded by
require_auth()/admin.require_auth(). - All math uses
checked_add/checked_sub/checked_mulwith overflow panics. - Inputs are validated in both CLIs (Stellar address format, non-negative amounts, decimal bounds) before any RPC call.
- Secrets are never committed — see
.env.exampleand SECURITY.md.
See CONTRIBUTING.md for setup, conventions, and the PR checklist.
MIT — see LICENSE.