Skip to content

Latest commit

 

History

History
305 lines (232 loc) · 10.3 KB

File metadata and controls

305 lines (232 loc) · 10.3 KB

Shelterflex Contracts (Soroban)

This folder contains Soroban smart contracts written in Rust.

Prerequisites

  • Rust (stable)
  • Soroban CLI (via stellar CLI) installed and configured

Workspace

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

Formal Verification

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.

Prerequisites

Run proofs locally

cd contracts
make kani-staking-pool
# or:
cargo kani --package staking_pool

Kani verification also runs in CI on push to main (when staking_pool changes) and on a weekly schedule (see .github/workflows/kani.yml).

Build & test

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_wallet

The cargo build --release (native) build is not the deployable artifact for Soroban.

Troubleshooting

  • Tooling versions

    • rustc --version should be stable.
    • stellar --version should be recent enough to support stellar contract build.
    • This repo uses soroban-sdk = 22.0.7 (see rent_wallet/Cargo.toml).
  • error: toolchain ... does not support target 'wasm32-unknown-unknown'

    • Install the target:
      • rustup target add wasm32-unknown-unknown
  • stellar: command not found / stellar contract: unknown command

    • Install/upgrade the Stellar CLI with Soroban support and verify:
      • stellar --version
      • stellar contract --help
  • Build succeeds but you can’t find the .wasm

    • If you used stellar contract build, look under the relevant contract crate’s target/ directory.
    • If you used the cargo-only build, look under:
      • contracts/target/wasm32-unknown-unknown/release/

Deploy (example)

Exact commands depend on your Soroban CLI version.

High-level flow:

  1. Choose network (local sandbox or testnet)
  2. Build the contract WASM
  3. Deploy
  4. 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

Contract interface

rent_wallet

  • 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) -> i128
  • set_admin(new_admin: Address) (admin-auth)
  • pause() (admin-auth)
  • unpause() (admin-auth)
  • is_paused() -> bool
  • add_operator(operator: Address) (admin-auth)
  • remove_operator(operator: Address) (admin-auth)
  • is_operator(operator: Address) -> bool

Events

The rent_wallet contract emits Soroban events to support indexing and auditability.

  • credit

    • Topic
      • ("credit", user: Address)
    • Data
      • (amount: i128, new_balance: i128)
  • debit

    • Topic
      • ("debit", user: Address)
    • Data
      • (amount: i128, new_balance: i128)

For both events:

  • user is the address whose balance was modified.

  • amount is the delta applied (always positive).

  • new_balance is the resulting balance after applying the delta.

  • pause

    • Topic
      • ("pause",)
    • Data
      • ()
    • Emitted when the contract is paused by the admin.
  • unpause

    • Topic
      • ("unpause",)
    • Data
      • ()
    • Emitted when the contract is unpaused by the admin.
  • add_operator

    • Topic
      • ("add_operator",)
    • Data
      • (operator: Address)
    • Emitted when an operator is added by the admin.
  • remove_operator

    • Topic
      • ("remove_operator",)
    • Data
      • (operator: Address)
    • Emitted when an operator is removed by the admin.

Pause Functionality

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(): Returns true if the contract is paused, false otherwise.

Behavior when paused:

  • credit() and debit() 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.

Operator Role Model

The contract implements a role-based access control system with three roles:

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.

Operator Management

  • 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: Returns true if the address is an operator, false otherwise.

Permission Matrix

Function Admin Operator User
credit()
credit_as_operator()
debit()
add_operator()
remove_operator()
set_admin()
pause()
unpause()
balance()
is_paused()
is_operator()

Use Case

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.

deal_escrow

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 via from)
  • 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 via admin or operator) -> i128
  • balance(deal_id: String) -> i128
  • pause() (admin-auth)
  • unpause() (admin-auth)
  • is_paused() -> bool

Events

  • deposit
    • Topic: ("deal_escrow", "deposit")
    • Data: (deal_id: String, from: Address, amount: i128)
  • 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>)

rent_payments

  • 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>) -> ReceiptPage
  • receipt_count(deal_id: DealId) -> u64

Cursor-Based Pagination

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 page
    • tx_id: TxId (BytesN<32>) - The transaction ID of the last receipt in the previous page

Ordering: Receipts are ordered by a stable, deterministic ordering:

  1. Primary: timestamp (ascending)
  2. 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 None as the cursor
  • Subsequent pages: Use next_cursor from the previous ReceiptPage
  • Check has_next to 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:

  • limit must be between 1 and 100 (inclusive)
  • The function will panic if an invalid limit is provided

Events

  • 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.