Soroban · Stellar

On-chain compliance primitives for Stellar

Allowlist/denylist token gating and jurisdiction checks for RWA and stablecoin issuers — a small set of standardized, auditable, independently testable Soroban contracts, so issuers stop hand-rolling compliance logic inside their own token contracts.

The problem

RWA and stablecoin issuers building on Stellar/Soroban all end up needing the same handful of compliance gates — allowlists, denylists, jurisdiction restrictions — before they can legally let a token move. Today every issuer hand-rolls this logic inside their own token contract, which means more audit surface, more chances for a subtle bug to become a compliance incident, and no shared, reviewed reference to build on.

Three focused contracts

Each does one job, and each is independently deployable and testable.

allowlist-token

A token wrapper that only permits transfers between two addresses that are both present on an on-chain allowlist. Admin-managed; forwards cleared transfers to an underlying SEP-41 token contract.

admin-managed wraps SEP-41
add_to_allowlist(admin, address) is_allowed(address) -> bool transfer(from, to, amount) -> bool

denylist-gate

A standalone denylist other contracts call via cross-contract invocation before executing a transfer. Not a token itself — meant to be composed into one.

composable cross-contract
add_to_denylist(admin, address) check(address) -> bool

jurisdiction-flag

Attaches an issuer-controlled jurisdiction code (e.g. an ISO country code) to an address, with a helper other contracts can call to check an address against a permitted-jurisdictions list.

issuer-controlled ISO codes
set_jurisdiction(issuer, address, code) is_permitted_jurisdiction(address, allowed_codes) -> bool

Building blocks, not a product

These contracts are meant to be composed into a real token or RWA wrapper via cross-contract calls — for example, a token's transfer calling denylist-gate.check(address) before moving funds, or checking jurisdiction-flag.is_permitted_jurisdiction(...) before permitting a transaction. allowlist-token is the one exception: a thin wrapper you deploy in front of an existing SEP-41 token, which still delegates the real transfer to the underlying token contract.

issuer's token contract
  transfer(from, to, amount)
    → denylist-gate.check(from)         (cross-contract call)
    → denylist-gate.check(to)
    → jurisdiction-flag.is_permitted_jurisdiction(to, allowed)
    → proceed with transfer, or reject

See /examples/denylist-gate-consumer for a minimal reference token contract showing this pattern end to end.

Quick start

# Clone
git clone https://github.com/stellar-compliance-kit/compliance-primitives.git
cd compliance-primitives

# Run the full test suite (all three contracts + example)
cargo test --workspace

# Lint
cargo clippy --workspace --all-targets -- -D warnings

# Build a contract to wasm (requires the wasm32v1-none target:
# rustup target add wasm32v1-none)
stellar contract build

# Deploy a built contract to testnet, e.g. denylist-gate
stellar contract deploy \
  --wasm target/wasm32v1-none/release/denylist_gate.wasm \
  --source <your-testnet-identity> \
  --network testnet