Soroban smart contracts for the sorokit ecosystem.
Three independently deployable Rust contracts for Stellar Soroban:
Escrow, Splitter, and Registry.
Part of the sorokit ecosystem.
sorokit-contracts provides three production-ready Soroban smart contracts designed to be invoked through sorokit-core's Soroban module. Each contract is independently deployable and fully tested.
This repository is a Rust workspace containing:
- Escrow — Hold and release (or refund) funds with two-party coordination and dispute resolution
- Splitter — Distribute payments proportionally among multiple recipients with admin controls
- Registry — A general-purpose key-value store with versioning, TTL expiry, and access control
- Installation
- Build
- Test
- Architecture
- Contracts
- API Reference
- Integration with sorokit-core
- Deployment
- Development
- Contributing
- Security
- License
- Rust 1.70+ (install here)
- Soroban CLI (install here)
git clone https://github.com/Sorokit/contracts.git
cd contractsBuild all contracts:
make build # Build all contracts
make build-wasm # Build WASM targets
cargo build --releaseBuild a specific contract:
cargo build --release -p escrow
cargo build --release -p splitter
cargo build --release -p registryThe compiled WASM binaries will be in target/wasm32-unknown-unknown/release/.
Run all tests:
make test # Run all unit and integration tests
make test-verbose # Run tests with output
cargo test --allRun tests for a specific contract:
cargo test -p escrow
cargo test -p splitter
cargo test -p registryCode quality checks:
make fmt # Format code
make lint # Lint code
make check # Format, lint, and test all┌─────────────────────────────────────────────────────┐
│ sorokit-core │
│ (TypeScript/JavaScript SDK) │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ wallet │ │ account │ │ transaction │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ ┌────────────────────┐ │
│ │ soroban module │ │
│ └────────────────────┘ │
└─────────────────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────┐
│ Stellar Soroban Network │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ Escrow │ │ Splitter │ │ Registry │ │
│ │ Contract │ │ Contract │ │ Contract │ │
│ └──────────────┘ └──────────────┘ └────────────┘ │
│ │
│ ┌────────────────────────────────────────────┐ │
│ │ sorokit-shared (utilities) │ │
│ │ • Error types │ │
│ │ • Math utilities │ │
│ └────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
Purpose: Two-party escrow for conditional fund release with dispute resolution.
State:
pub struct Escrow {
pub depositor: Address,
pub recipient: Address,
pub arbitrator: Address,
pub amount: u128,
pub status: EscrowStatus, // Pending | Released | Refunded | Disputed | Resolved
pub token: Address,
}Public Functions:
Deposit funds into escrow.
Parameters:
token— Token contract address (e.g., USDC)sender— Depositor account (must be caller)amount— Amount in stroopsrecipient— Recipient accountarbitrator— Arbitrator for disputes
Example (via sorokit-core):
const depositResult = await client.soroban.invoke(
{
contractId: escrowContractAddress,
publicKey: depositorAddress,
method: "deposit",
args: [
"CDD7...token", // token address
"1000000", // 0.1 XLM in stroops
"recipientAddr",
"arbitratorAddr",
],
contractAbi: escrowAbi,
},
async (xdr) => {
const signed = await client.wallet.signTransaction(adapter, {
transactionXdr: xdr,
networkPassphrase: client.networkConfig.networkPassphrase,
});
return signed.status === "ok" ? signed.data : null;
},
);Release funds to recipient (depositor only).
Refund funds to depositor (depositor only).
Flag escrow as disputed (depositor or recipient).
Resolve dispute and release funds to winner (arbitrator only).
Get full escrow details.
Get escrow status.
Events:
("escrow", "deposited")— emitted on deposit("escrow", "released")— emitted on release("escrow", "refunded")— emitted on refund("escrow", "disputed")— emitted on dispute("escrow", "resolved")— emitted on resolution
Purpose: Proportional payment distribution to multiple recipients with admin controls.
State:
pub struct SplitterConfig {
pub admin: Address,
pub splits: Vec<Split>,
}
pub struct Split {
pub recipient: Address,
pub basis_points: u32, // e.g., 5000 = 50%
}
pub struct DistributionRecord {
pub ledger: u32,
pub token: Address,
pub total_distributed: u128,
}Public Functions:
Configure payment splits (admin only).
Parameters:
admin— Admin account (must be caller)recipients— Vector of (recipient, basis_points) pairs- Validation: Basis points must sum to exactly 10000
Distribute tokens according to configured splits.
Parameters:
token— Token contract addresssender— Payer (must be caller)amount— Total amount to distribute
Update split configuration (admin only).
Transfer admin role to a new address.
Get current split configuration.
Get current admin address.
Get last 10 distribution records.
Events:
("splitter", "splits_set")— emitted on configuration("splitter", "distributed")— emitted per recipient("splitter", "admin_transferred")— emitted on admin transfer("splitter", "splits_updated")— emitted on split update
Example (via sorokit-core):
const distributeResult = await client.soroban.invoke(
{
contractId: splitterContractAddress,
publicKey: senderAddress,
method: "distribute",
args: [
"CDD7...token", // token address
"100000000", // 10 XLM in stroops
],
contractAbi: splitterAbi,
},
signTransaction,
);Purpose: General-purpose on-chain key-value store with versioning, TTL expiry, and access control.
State:
pub struct Entry {
pub value: String,
pub version: u64,
pub expiry: Option<u32>, // Ledger number when entry expires
}Public Functions:
Set a key-value pair (admin only).
Parameters:
admin— Admin account (must be caller)key— Registry keyvalue— Registry valuettl— Optional TTL in ledgers; None means no expiry
Get a value by key (with access control).
Parameters:
reader— Reader account (must be authorized if whitelist active)key— Registry key
Remove a key-value pair (admin only).
Set multiple key-value pairs in one transaction (admin only).
Parameters:
entries— Vector of (key, value, optional_ttl) tuples
Remove multiple keys in one transaction (admin only).
Add an address to the read whitelist (admin only).
Remove an address from the read whitelist (admin only).
Get all authorized readers (None if no whitelist).
Check if read access is restricted to whitelist.
Get version counter (increments on each write).
Get entire registry (entries with expiry metadata).
Check if a key exists (with access control).
Events:
("registry", "set")— emitted on set("registry", "removed")— emitted on remove("registry", "batch_set")— emitted on batch set("registry", "batch_remove")— emitted on batch remove("registry", "reader_added")— emitted on whitelist add("registry", "reader_removed")— emitted on whitelist remove
Example (via sorokit-core):
const setResult = await client.soroban.invoke(
{
contractId: registryContractAddress,
publicKey: adminAddress,
method: "set",
args: [
"contract_address", // key
"CA123...XYZ", // value
100, // TTL in ledgers
],
contractAbi: registryAbi,
},
signTransaction,
);See CONTRACTS.md for deployed contract addresses and endpoint documentation.
Each contract is designed to be invoked via sorokit-core's client.soroban module.
Typical workflow:
import { createSorokitClient, FreighterAdapter } from "sorokit-core";
// 1. Create client
const result = createSorokitClient({ network: "testnet" });
if (result.status === "error") throw new Error(result.error.message);
const client = result.data;
// 2. Connect wallet
const adapter = new FreighterAdapter(swkInstance);
const conn = await client.wallet.connect(adapter);
if (conn.status === "error") throw new Error(conn.error.message);
// 3. Invoke contract
const invokeResult = await client.soroban.invoke(
{
contractId: "CA123...XYZ",
publicKey: conn.data.publicKey,
method: "contractMethod",
args: [arg1, arg2, arg3],
contractAbi: contractAbi,
},
async (xdr) => {
const signed = await client.wallet.signTransaction(adapter, {
transactionXdr: xdr,
networkPassphrase: client.networkConfig.networkPassphrase,
});
return signed.status === "ok" ? signed.data : null;
},
);
if (invokeResult.status === "ok") {
console.log("Success! Tx hash:", invokeResult.data);
} else {
console.error("Failed:", invokeResult.error.message);
}For more details, see sorokit-core README.
-
Install the Soroban CLI:
cargo install soroban-cli
-
Set your network:
soroban network add --global testnet \ --rpc-url "https://soroban-testnet.stellar.org" \ --network-passphrase "Test SDF Network ; September 2015" soroban config identity create --global myaccount
-
Deploy a contract:
soroban contract deploy \ --wasm target/wasm32-unknown-unknown/release/escrow.wasm \ --id 1 \ --network testnet
-
Verify deployment:
soroban contract invoke \ --id CA123...XYZ \ --network testnet \ -- get_escrow
For production deployments to mainnet, see Soroban deployment guide.
# All tests
make test
# Specific contract
cargo test -p escrow
# With output
make test-verbose
# Code coverage (requires cargo-tarpaulin)
cargo tarpaulin --all# Format
make fmt
# Lint
make lint
# All checks
make checkmake build-wasm
ls target/wasm32-unknown-unknown/release/*.wasmContributions welcome! Please:
- Fork the repository
- Create a feature branch (
git checkout -b feature/my-feature) - Make your changes and add tests
- Ensure all checks pass (
make check) - Commit with clear messages
- Submit a pull request
For significant changes, open an issue first to discuss.
If you discover a security vulnerability, please email security@sorokit.org instead of opening a public issue.
For full security policy, see SECURITY.md.
- v0.1.0 — Initial release, community review ongoing
Questions? Open an issue or start a discussion on GitHub.