Ahjoor is a decentralized Rotating Savings and Credit Association (ROSCA) platform built on the Stellar blockchain. It empowers communities and savings groups to pool funds and take turns receiving the collective pot with complete transparency, security, and no middlemen.
ROSCAs are one of the oldest and most widely used savings systems in the world, yet they still rely entirely on trust and manual processes. Ahjoor brings this tradition on-chain using Stellar's fast, low-cost blockchain infrastructure to provide:
- Trustless Savings Circles: Automated round management with no central authority
- Transparent Operations: All participants can verify contributions and payouts on-chain
- Secure Funds: Cryptographically secured group wallets and contribution records
- Cost-Effective: Built on Stellar's efficient, low-fee blockchain infrastructure
- Scalable: Designed to support many groups running simultaneously
- ROSCA – Rotating Savings and Credit Association: a group where members pool fixed amounts each cycle and one member receives the full pot per round.
- Ajo – Nigerian (Yoruba) term for a community savings circle
- Esusu – West African community savings circle
- Susu – Caribbean / West African savings circle variant
- Tanda – Latin American ROSCA
- Chit Fund – South Asian, often formalized and legally regulated ROSCA
- Community savings circles (Ajo, Esusu, Susu, Tanda, Chit Funds)
- Corporate employee savings programs
- Diaspora remittance and group savings
- Micro-lending and credit-building for underbanked communities
- Multi-party escrow and collective fund management
- Rust (latest stable)
- Stellar CLI
- Make (optional, for convenience commands)
# Fork the repository
# Then clone your fork into your local environment
git clone https://github.com/Ahjoor/ahjoor-contract.git
cd ahjoor-contracts
# Add wasm32 target
rustup target add wasm32-unknown-unknown# Using Make
make buildOR
# Using cargo
cargo build --target wasm32-unknown-unknown --release# Or directly with Stellar CLI
stellar contract build# Run all tests
make test# Install once
cargo install cargo-llvm-cov --locked
# Enforce thresholds (line >= 90%, branch/region >= 85%)
make coverage# Format code
make fmt# OR
cargo fmt# Check formatting
make fmt-check# OR
cargo check --all# Run clippy lints
make lint# Fund a testnet account
stellar keys generate --global alice --network testnet
stellar keys fund alice --network testnet
# Deploy the contract
stellar contract deploy \
--wasm target/wasm32-unknown-unknown/release/ahjoor_rosca.wasm \
--source alice \
--network testnet
# Verify deployment
stellar contract invoke \
--id <CONTRACT_ID> \
--source alice \
--network testnet \
-- get_fee_bpsNote: The
deploycommand prints aCONTRACT_ID— save it immediately, as you'll need it for all subsequentstellar contract invokecalls.
Ahjoor is composed of five Soroban smart contracts. The diagram below shows how users, admins, and contracts relate to each other.
graph TD
User --> ROSCA[ahjoor-rosca\nGroup savings rounds]
User --> Escrow[ahjoor-escrow\nEscrow with dispute/arbiter]
User --> Payments[ahjoor-payments\nTwo-step auth + capture]
User --> Refund[ahjoor-refund\nRefund handling]
Admin --> Whitelist[ahjoor-token-whitelist\nControls allowed tokens]
Whitelist --> ROSCA
Whitelist --> Escrow
Whitelist --> Payments
| Contract | Role |
|---|---|
| ahjoor-rosca | Manages rotating savings groups — round lifecycle, contributions, and payouts |
| ahjoor-escrow | Holds funds in escrow with optional arbiter and dispute/timeout resolution |
| ahjoor-payments | Two-step authorization and capture flow for merchant-style payments |
| ahjoor-refund | Handles refund issuance and claim logic for cancelled or reversed transactions |
| ahjoor-token-whitelist | Admin-controlled registry of tokens permitted across all other contracts |
The token whitelist sits at the foundation — ahjoor-rosca, ahjoor-escrow, and ahjoor-payments each consult it before accepting any token transfer, giving admins a single control point for token policy across the platform.
Ahjoor's contracts are designed with defense-in-depth across every interaction:
- Every state-mutating function requires the caller to sign with their Stellar keypair via
require_auth(). Unauthorized callers are rejected at the SDK level before any logic executes. - Role-based checks (buyer, seller, arbiter, inspector) are enforced per-function so that, for example, only the designated arbiter can resolve a dispute and only the buyer can release funds.
- Funds are held exclusively by the deployed contract address — never by an EOA. Token transfers only occur through explicit, permissioned entry-points (
release,refund,resolve_dispute). - Multi-party seller payouts are split in basis-points (BPS), ensuring the sum always equals 10 000 before any transfer is executed.
- Disputes freeze the escrow, preventing unilateral fund movement by either party.
- A configurable
dispute_timeout_secondsensures that an unresponsive arbiter cannot lock funds indefinitely: after the timeout the configured default winner (buyer or seller) can claim. - The cooling-off window after an arbiter verdict gives the losing party a defined period to review before finalisation.
- Optional seller collateral (configured in BPS at creation) is locked until dispute resolution. On a buyer-favour ruling, a configurable
collateral_forfeit_bpsshare is slashed as a penalty, deterring bad-faith sellers. - An
UnderCollateralizedstatus blocks release if the collateral value drops below the required ratio, protecting buyers in volatile markets.
- All contracts integrate with the
ahjoor-token-whitelistcontract. Only explicitly whitelisted SEP-41 tokens are accepted, preventing interactions with malicious or spoofed token contracts.
- The Soroban runtime enforces transaction uniqueness; replayed invocations are rejected at the ledger level.
- Rust's
overflow-checks = truerelease profile setting (seeCargo.toml) causes any arithmetic overflow to panic rather than wrap silently.
- TTL bump logic is called on every write path so that active contract state is never silently archived mid-operation. Callers can also invoke
bump_storage()manually to extend TTL during periods of low activity.
The ahjoor-token-whitelist contract (contracts/ahjoor-token-whitelist) restricts which tokens can be used across Ahjoor ROSCA, Escrow, and Payment groups. When a group contract is configured with a whitelist contract address, token operations are rejected unless the token is allowed.
Only the contract admin can modify the whitelist. The admin is set once at deployment via initialize(admin). All write operations require the admin address to authorize the transaction.
| Function | Description |
|---|---|
add_token(admin, token) |
Add a token contract address to the global whitelist. |
remove_token(admin, token) |
Remove a token from the global whitelist. |
is_whitelisted(token) → bool |
Return whether a token is on the global whitelist (read-only). |
Suspension is a temporary admin-controlled restriction on an already whitelisted token. It is distinct from full removal:
remove_token(admin, token)permanently deletes the token from the global whitelist and clears any active suspension record for that token.suspend_token_timed(admin, token, duration, reason_hash)keeps the token on the whitelist but blocksis_token_allowed(token)until the suspension window expires.- A suspended token is not “delisted”; it remains globally whitelisted, but calls that rely on
is_token_allowedwill be rejected while the suspension is active.
Only the contract admin can suspend a token, and the token must already be present in the global whitelist before suspension is allowed. A suspension is timed by ledger height: the contract stores an expiry_ledger and an optional reason_hash for the suspension record. While the token is suspended, is_token_allowed(token) returns false for downstream contracts that check the whitelist contract.
A suspended token can be reinstated in either of two ways:
- Automatic expiry: when
is_token_allowed(token)is queried after the suspension expiry ledger, the contract lazily clears the suspension record and returnstrue. - Manual lift: the admin can call
lift_token_suspension(admin, token)before expiry to remove the suspension early.
The admin can also extend an active suspension with extend_token_suspension(admin, token, additional_ledgers), which appends more ledgers to the existing expiry window. Suspension history is retained for the most recent suspensions, capped at ten recorded entries.
stellar contract invoke \
--id <WHITELIST_CONTRACT_ID> \
--network testnet \
-- is_whitelisted --token <TOKEN_ADDRESS>In addition to the global token whitelist, the ahjoor-token-whitelist contract supports a Contract Allowlist.
Difference from Global Whitelist:
While the global token whitelist permits a token to be used across all Ahjoor groups (ROSCA, Escrow, and Payments), the contract allowlist permits a token to be used exclusively within a specific deployed group contract (e.g., a specific Escrow or ROSCA instance). Contract allowlist entries can also be configured with an optional expiry_ledger to restrict the token's usage to a certain timeframe.
Who Controls It:
Just like the global whitelist, only the admin of the ahjoor-token-whitelist contract can add or remove entries from the contract allowlist. This is done via the set_contract_token and remove_contract_token functions.
Gated Operations:
When a group contract (like an Escrow or ROSCA) checks if a token is permitted via the is_token_allowed_for_contract function, the whitelist contract first checks the contract allowlist. If the token is explicitly allowed for that specific group contract and the expiry ledger hasn't passed, the operation is permitted. Otherwise, it falls back to checking the global whitelist. This gating mechanism secures fund deposits, contributions, and any other token transfers managed by the group contracts.
Stellar/Soroban utilizes State Archival to manage network storage footprint. Idle contracts and data entries will eventually be archived. Ahjoor handles state preservation automatically when members interact with it (e.g. init or contribute). However, if the contract goes unused for a long period, participants should occasionally call the bump_storage() function to manually extend the time-to-live (TTL) of the contract's instance storage and avoid sudden state archival.
Use the Stellar CLI to extend the contract's TTL from any participant account:
stellar contract invoke \
--id <CONTRACT_ID> \
--source alice \
--network testnet \
-- bump_storageReplace <CONTRACT_ID> with your deployed contract address and alice with the name of your configured Stellar CLI identity.
Recommended frequency: Call bump_storage() at least once every 30 days during periods of inactivity to keep the contract's instance storage live. Active groups that call contribute or other state-writing functions regularly do not need to call it manually — those interactions bump storage automatically.
If archival occurs: Archived state is not lost permanently. You can restore it using:
stellar contract restore \
--id <CONTRACT_ID> \
--source alice \
--network testnetAfter restoration, call bump_storage() immediately to reset the TTL and prevent the contract from being archived again in the short term.
For step-by-step recovery when a group has already been archived, see State Archival Troubleshooting.
- Blockchain: Stellar (Soroban smart contracts)
- Language: Rust
- SDK: Soroban SDK v21.0.0
- Token Standard: SEP-41 / Stellar Asset Contract (XLM or any compatible token)
- Testing: Soroban test utilities
Q: What happens if I miss a contribution round?
A: You receive a penalty. After max_defaults consecutive missed rounds you are suspended from the group.
Q: Can I pay my contribution in parts?
A: Yes. The contract supports partial installments — contribute any amount up to the remaining balance and the round tracks your cumulative total.
Q: What tokens are accepted?
A: Only tokens on the admin-controlled whitelist (ahjoor-token-whitelist). XLM and any SEP-41 compatible token can be whitelisted.
Q: What is the maximum protocol fee?
A: 5% (500 basis points), hard-capped and enforced on-chain. Admins cannot set a higher fee.
Q: What happens if a dispute arbiter goes inactive?
A: After the configured timeout (default 7 days), anyone can call enforce_dispute_timeout(escrow_id) to release funds to the pre-configured default winner.
Q: How do I prevent my contract state from being archived?
A: Call bump_storage() periodically (recommended every ~30 days of inactivity). If archival does occur, state can be restored via stellar contract restore. See State Archival Troubleshooting for CLI commands and what data is preserved.
For a comprehensive table of contents and topic breakdown, see the Documentation Index.
- Documentation Index — Complete table of contents organized by contract and feature.
- Payments Authorization and Capture Flow — Lifecycle guide for
authorize_payment,capture_payment, missed capture expiry, and related events. - Contract Error Codes — Consolidated reference of every numeric
#[contracterror]code exposed by the Ahjoor contracts. - State Archival Troubleshooting — Check archived status, restore a dormant ROSCA contract, and prevent future archival.
- Escrow Dispute Flow — Dispute lifecycle, arbiter timeouts, default winner rules, and cooling-off period mechanics.
- DAO Mediation — On-chain DAO mediation and voting process for disputed merchant payments.
- Multi-Token Invoices — Invoicing with multi-token support, oracle price feeds, and settlement.
- ROSCA Co-signer Guarantee — Co-signer nomination and default coverage mechanisms for ROSCA groups.
- Refund Contract Guide — Refund request/approval flows, senior escalation, and abuse score tracking.
- ROSCA Migration Guide — Upgrade and migration process for deployed ROSCA contracts.