This folder contains Soroban smart contracts written in Rust.
- Rust (stable)
- Soroban CLI (via
stellarCLI) installed and configured
The contracts directory is a Cargo workspace. Each subdirectory is a separate contract crate:
| Contract | Purpose |
|---|---|
rent_wallet/ |
Custodial wallet for credit/debit flows (tenant rent balances) |
rent_payments/ |
Deal receipts and paginated payment history |
deal_escrow/ |
Escrow logic holding funds until deal conditions are met |
staking_pool/ |
Staking pool for platform liquidity providers |
mvp_staking_pool/ |
Minimal staking pool reference implementation |
staking_rewards/ |
Reward distribution for stakers |
timelock/ |
Time-locked governance actions |
transaction-receipt-contract/ |
On-chain transaction receipts with canonical tx_id |
whistleblower_rewards/ |
Reward allocation and claiming for whistleblowers who report fraudulent listings |
upgradeable_proxy/ |
Proxy pattern for contract upgrades |
soroban_access_control/ |
Shared access-control primitives |
soroban_pausable/ |
Shared pausable primitive used across contracts |
contract_access/ |
Contract-level access helpers |
The staking_pool contract includes Kani proof harnesses in
staking_pool/src/formal_properties.rs that verify safety properties such as
overflow/underflow protection, lock-period enforcement, balance conservation,
reentrancy guarding, and upgrade timelock compliance.
- Rust nightly:
rustup install nightly - Kani Rust Verifier:
cargo install --locked kani-verifier && cargo kani setup
cd contracts
make kani-staking-pool
# or:
cargo kani --package staking_poolKani verification also runs in CI on push to main (when staking_pool changes)
and on a weekly schedule (see .github/workflows/kani.yml).
cd contracts
# Formatting
cargo fmt
# Linting
cargo clippy --all-targets --all-features -- -D warnings
# 1) Run unit tests
cargo test
# 2) Build the deployable contract WASM (recommended)
# Produces an optimized .wasm under each contract crate's `target/` directory.
stellar contract build
# 3) Build the deployable contract WASM (cargo-only alternative)
rustup target add wasm32-unknown-unknown
cargo build --release --target wasm32-unknown-unknown -p rent_walletThe cargo build --release (native) build is not the deployable artifact for Soroban.
-
Tooling versions
rustc --versionshould be stable.stellar --versionshould be recent enough to supportstellar contract build.- This repo uses
soroban-sdk = 22.0.7(seerent_wallet/Cargo.toml).
-
error: toolchain ... does not support target 'wasm32-unknown-unknown'- Install the target:
rustup target add wasm32-unknown-unknown
- Install the target:
-
stellar: command not found/stellar contract: unknown command- Install/upgrade the Stellar CLI with Soroban support and verify:
stellar --versionstellar contract --help
- Install/upgrade the Stellar CLI with Soroban support and verify:
-
Build succeeds but you can’t find the
.wasm- If you used
stellar contract build, look under the relevant contract crate’starget/directory. - If you used the cargo-only build, look under:
contracts/target/wasm32-unknown-unknown/release/
- If you used
Exact commands depend on your Soroban CLI version.
High-level flow:
- Choose network (local sandbox or testnet)
- Build the contract WASM
- Deploy
- Initialize with an admin address
Recommended contributor expectations for deployment-related issues:
- Provide exact CLI commands you used
- Include network info (rpc url + passphrase)
- Include contract id and init params
init(admin: Address)credit(user: Address, amount: i128)(admin-auth)credit_as_operator(user: Address, amount: i128)(operator-auth)debit(user: Address, amount: i128)(admin-auth)balance(user: Address) -> i128set_admin(new_admin: Address)(admin-auth)pause()(admin-auth)unpause()(admin-auth)is_paused() -> booladd_operator(operator: Address)(admin-auth)remove_operator(operator: Address)(admin-auth)is_operator(operator: Address) -> bool
The rent_wallet contract emits Soroban events to support indexing and auditability.
-
credit- Topic
("credit", user: Address)
- Data
(amount: i128, new_balance: i128)
- Topic
-
debit- Topic
("debit", user: Address)
- Data
(amount: i128, new_balance: i128)
- Topic
For both events:
-
useris the address whose balance was modified. -
amountis the delta applied (always positive). -
new_balanceis the resulting balance after applying the delta. -
pause- Topic
("pause",)
- Data
()
- Emitted when the contract is paused by the admin.
- Topic
-
unpause- Topic
("unpause",)
- Data
()
- Emitted when the contract is unpaused by the admin.
- Topic
-
add_operator- Topic
("add_operator",)
- Data
(operator: Address)
- Emitted when an operator is added by the admin.
- Topic
-
remove_operator- Topic
("remove_operator",)
- Data
(operator: Address)
- Emitted when an operator is removed by the admin.
- Topic
The contract includes a pause mechanism for emergency stops:
pause(): Pauses the contract. Only the admin can call this function.unpause(): Unpauses the contract. Only the admin can call this function.is_paused(): Returnstrueif the contract is paused,falseotherwise.
Behavior when paused:
credit()anddebit()operations will fail with a panic if called while the contract is paused.balance()and other read-only operations continue to work normally when paused.- Only the admin can pause or unpause the contract.
The contract implements a role-based access control system with three roles:
- Admin: Has full control over the contract, including operator management.
- Operator: Can perform credit operations only (cannot debit or manage other operators).
- User: Any address that can have a balance but has no special permissions.
add_operator(operator: Address): Adds a new operator. Only the admin can call this function.remove_operator(operator: Address): Removes an existing operator. Only the admin can call this function.is_operator(operator: Address) -> bool: Returnstrueif the address is an operator,falseotherwise.
| Function | Admin | Operator | User |
|---|---|---|---|
credit() |
✅ | ❌ | ❌ |
credit_as_operator() |
❌ | ✅ | ❌ |
debit() |
✅ | ❌ | ❌ |
add_operator() |
✅ | ❌ | ❌ |
remove_operator() |
✅ | ❌ | ❌ |
set_admin() |
✅ | ❌ | ❌ |
pause() |
✅ | ❌ | ❌ |
unpause() |
✅ | ❌ | ❌ |
balance() |
✅ | ✅ | ✅ |
is_paused() |
✅ | ✅ | ✅ |
is_operator() |
✅ | ✅ | ✅ |
The operator role is designed for delegation scenarios where a property manager or similar role needs to credit tenant accounts (e.g., for security deposits or rent payments) without having full administrative control over the contract.
The deal_escrow contract securely holds tokens until a deal is finalized. Full-payments are routed with split rewards upon release.
init(admin: Address, operator: Address, token: Address, receipt_contract: Address)deposit(from: Address, deal_id: String, amount: i128)(auth viafrom)release(caller: Address, deal_id: String, to: Address, principal_amount: i128, platform_addr: Address, platform_amount: i128, reporter_addr: Address, reporter_amount: i128, external_ref_source: Symbol, external_ref: String)(auth viaadminoroperator) ->i128balance(deal_id: String) -> i128pause()(admin-auth)unpause()(admin-auth)is_paused() -> bool
deposit- Topic:
("deal_escrow", "deposit") - Data:
(deal_id: String, from: Address, amount: i128)
- Topic:
release- Topic:
("deal_escrow", "release") - Data:
(deal_id: String, to: Address, principal_amount: i128, platform_addr: Address, platform_amount: i128, reporter_addr: Address, reporter_amount: i128, external_ref_source: Symbol, tx_id: BytesN<32>)
- Topic:
init(admin: Address)create_receipt(deal_id: DealId, amount: i128, payer: Address) -> Receipt(admin-auth)list_receipts_by_deal(deal_id: DealId, limit: u32, cursor: Option<Cursor>) -> ReceiptPagereceipt_count(deal_id: DealId) -> u64
The list_receipts_by_deal function implements cursor-based pagination for efficient retrieval of receipts.
Cursor Format:
- The cursor is a struct containing:
timestamp: Timestamp(u64) - The timestamp of the last receipt in the previous pagetx_id: TxId(BytesN<32>) - The transaction ID of the last receipt in the previous page
Ordering: Receipts are ordered by a stable, deterministic ordering:
- Primary:
timestamp(ascending) - Secondary:
tx_id(ascending, byte-wise comparison)
This ensures:
- Consistent pagination results even if multiple receipts share the same timestamp
- No skipping or duplication of receipts across pages
- Deterministic ordering that remains stable over time
Usage:
- First page: Pass
Noneas the cursor - Subsequent pages: Use
next_cursorfrom the previousReceiptPage - Check
has_nextto determine if more pages are available
Example:
// First page
let page1 = client.list_receipts_by_deal(&deal_id, &10u32, &None);
// Next page (if available)
if page1.has_next {
let page2 = client.list_receipts_by_deal(&deal_id, &10u32, &Some(page1.next_cursor));
}Limits:
limitmust be between 1 and 100 (inclusive)- The function will panic if an invalid limit is provided
receipt_created- Topic
("receipt_created", deal_id: DealId)
- Data
(receipt_id: ReceiptId, amount: i128, payer: Address)
- Emitted when a new receipt is created for a deal.
- Topic