Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
30c56ef
feat(rwa): add standalone country compliance modules
pasevin Mar 23, 2026
da529e5
refactor(rwa): move country module tests into sibling files
pasevin Mar 24, 2026
a6babd6
Merge branch 'main' into feat/rwa-country-standalone
pasevin Apr 10, 2026
27b08c3
chore: sync Cargo.lock with workspace manifests
pasevin Apr 10, 2026
301019a
refactor: apply storage-first pattern to country modules
pasevin Apr 10, 2026
ff8dadd
Merge remote-tracking branch 'upstream/main' into feat/rwa-country-st…
pasevin Apr 10, 2026
afa1dd8
chore: sync Cargo.lock after merging upstream/main v0.7.1
pasevin Apr 10, 2026
506a03f
refactor: finish country module alignment
pasevin Apr 10, 2026
2188957
refactor: align country modules with native traits
pasevin Apr 29, 2026
b91cd5b
refactor: tidy country membership storage
pasevin Apr 29, 2026
bd01f0b
chore: refresh ethnum lockfile entry
pasevin Apr 29, 2026
43764a1
Merge branch 'main' into feat/rwa-country-standalone
pasevin Apr 29, 2026
b44ecf8
refactor: add country allow event helpers
pasevin Apr 29, 2026
abca49c
refactor: wire country event helpers
pasevin Apr 30, 2026
f4b49bd
style: align country storage docs
pasevin Apr 30, 2026
5da6138
refactor: compose country module traits
pasevin Apr 30, 2026
8865101
style: order country module items
pasevin May 1, 2026
4d9ad42
refactor: use access control admin helpers
pasevin May 5, 2026
716a8a6
style: use full country module paths
pasevin May 5, 2026
5394955
docs: polish country trait docs
pasevin May 5, 2026
a06052b
docs: shorten country storage error links
pasevin May 5, 2026
55da13e
docs: fix country error link targets
pasevin May 5, 2026
3d061fd
chore: refresh country example lockfile
pasevin May 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ members = [
"examples/ownable",
"examples/pausable",
"examples/rwa/*",
"examples/rwa-country-allow",
"examples/rwa-country-restrict",
"examples/sac-admin-generic",
"examples/sac-admin-wrapper",
"examples/multisig-smart-account/*",
Expand Down
15 changes: 15 additions & 0 deletions examples/rwa-country-allow/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "rwa-country-allow"
edition.workspace = true
license.workspace = true
repository.workspace = true
publish = false
version.workspace = true

[lib]
crate-type = ["cdylib", "rlib"]
doctest = false

[dependencies]
soroban-sdk = { workspace = true }
stellar-tokens = { workspace = true }
50 changes: 50 additions & 0 deletions examples/rwa-country-allow/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Country Allow Module
Comment thread
pasevin marked this conversation as resolved.
Outdated

Concrete deployable example of the `CountryAllow` compliance module for Stellar
RWA tokens.

## What it enforces

This module allows tokens to be minted or transferred only to recipients whose
registered identity has at least one country code that appears in the module's
per-token allowlist.

The country lookup is performed through the Identity Registry Storage (IRS), so
the module must be configured with an IRS contract for each token it serves.

## Authorization model

This example uses the bootstrap-admin pattern introduced in this port:

- The constructor stores a one-time `admin`
- Before `set_compliance_address`, privileged configuration calls require that
admin's auth
- After `set_compliance_address`, the same configuration calls require auth
from the bound Compliance contract
- `set_compliance_address` itself remains a one-time admin action

This lets the module be configured from the CLI before it is locked to the
Compliance contract.

## Main entrypoints

- `__constructor(admin)` initializes the bootstrap admin
- `set_identity_registry_storage(token, irs)` stores the IRS address for a
token
- `add_allowed_country(token, country)` adds an ISO 3166-1 numeric code to the
allowlist
- `remove_allowed_country(token, country)` removes a country code
- `batch_allow_countries(token, countries)` updates multiple entries
- `batch_disallow_countries(token, countries)` removes multiple entries
- `is_country_allowed(token, country)` reads the current allowlist state
- `set_compliance_address(compliance)` performs the one-time handoff to the
Compliance contract

## Notes

- Storage is token-scoped, so one deployed module can be reused across many
tokens
- This module validates on the compliance read hooks used for transfers and
mints; it does not require extra state-tracking hooks
- In the deploy example, the module is configured before binding and then wired
to the `CanTransfer` and `CanCreate` hooks
88 changes: 88 additions & 0 deletions examples/rwa-country-allow/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#![no_std]

use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, String, Vec};
use stellar_tokens::rwa::compliance::modules::{
country_allow::{
storage::{is_country_allowed, remove_country_allowed, set_country_allowed},
CountryAllow, CountryAllowed, CountryUnallowed,
},
storage::{set_compliance_address, set_irs_address, ComplianceModuleStorageKey},
};

#[contracttype]
enum DataKey {
Admin,
}

#[contract]
pub struct CountryAllowContract;

fn set_admin(e: &Env, admin: &Address) {
e.storage().instance().set(&DataKey::Admin, admin);
}

fn get_admin(e: &Env) -> Address {
e.storage().instance().get(&DataKey::Admin).expect("admin must be set")
}
Comment thread
pasevin marked this conversation as resolved.
Outdated

fn require_module_admin_or_compliance_auth(e: &Env) {
if let Some(compliance) =
e.storage().instance().get::<_, Address>(&ComplianceModuleStorageKey::Compliance)
{
compliance.require_auth();
} else {
get_admin(e).require_auth();
}
}

#[contractimpl]
impl CountryAllowContract {
pub fn __constructor(e: &Env, admin: Address) {
set_admin(e, &admin);
}
}

#[contractimpl(contracttrait)]
impl CountryAllow for CountryAllowContract {
Comment thread
pasevin marked this conversation as resolved.
Outdated
fn set_identity_registry_storage(e: &Env, token: Address, irs: Address) {
require_module_admin_or_compliance_auth(e);
set_irs_address(e, &token, &irs);
}

fn add_allowed_country(e: &Env, token: Address, country: u32) {
require_module_admin_or_compliance_auth(e);
set_country_allowed(e, &token, country);
CountryAllowed { token, country }.publish(e);
}

fn remove_allowed_country(e: &Env, token: Address, country: u32) {
require_module_admin_or_compliance_auth(e);
remove_country_allowed(e, &token, country);
CountryUnallowed { token, country }.publish(e);
}

fn batch_allow_countries(e: &Env, token: Address, countries: Vec<u32>) {
require_module_admin_or_compliance_auth(e);
for country in countries.iter() {
set_country_allowed(e, &token, country);
CountryAllowed { token: token.clone(), country }.publish(e);
}
}

fn batch_disallow_countries(e: &Env, token: Address, countries: Vec<u32>) {
require_module_admin_or_compliance_auth(e);
for country in countries.iter() {
remove_country_allowed(e, &token, country);
CountryUnallowed { token: token.clone(), country }.publish(e);
}
}

fn is_country_allowed(e: &Env, token: Address, country: u32) -> bool {
is_country_allowed(e, &token, country)
}

fn set_compliance_address(e: &Env, compliance: Address) {
get_admin(e).require_auth();
set_compliance_address(e, &compliance);
}
}
15 changes: 15 additions & 0 deletions examples/rwa-country-restrict/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "rwa-country-restrict"
edition.workspace = true
license.workspace = true
repository.workspace = true
publish = false
version.workspace = true

[lib]
crate-type = ["cdylib", "rlib"]
doctest = false

[dependencies]
soroban-sdk = { workspace = true }
stellar-tokens = { workspace = true }
50 changes: 50 additions & 0 deletions examples/rwa-country-restrict/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Country Restrict Module

Concrete deployable example of the `CountryRestrict` compliance module for
Stellar RWA tokens.

## What it enforces

This module blocks tokens from being minted or transferred to recipients whose
registered identity has a country code that appears in the module's per-token
restriction list.

The country lookup is performed through the Identity Registry Storage (IRS), so
the module must be configured with an IRS contract for each token it serves.

## Authorization model

This example uses the bootstrap-admin pattern introduced in this port:

- The constructor stores a one-time `admin`
- Before `set_compliance_address`, privileged configuration calls require that
admin's auth
- After `set_compliance_address`, the same configuration calls require auth
from the bound Compliance contract
- `set_compliance_address` itself remains a one-time admin action

This lets the module be configured from the CLI before it is locked to the
Compliance contract.

## Main entrypoints

- `__constructor(admin)` initializes the bootstrap admin
- `set_identity_registry_storage(token, irs)` stores the IRS address for a
token
- `add_country_restriction(token, country)` adds an ISO 3166-1 numeric code to
the restriction list
- `remove_country_restriction(token, country)` removes a country code
- `batch_restrict_countries(token, countries)` updates multiple entries
- `batch_unrestrict_countries(token, countries)` removes multiple entries
- `is_country_restricted(token, country)` reads the current restriction state
- `set_compliance_address(compliance)` performs the one-time handoff to the
Compliance contract

## Notes

- Storage is token-scoped, so one deployed module can be reused across many
tokens
- This module validates on the compliance read hooks used for transfers and
mints; it does not require extra state-tracking hooks
- In the deploy example, the module is configured before binding and then wired
to the `CanTransfer` and `CanCreate` hooks
88 changes: 88 additions & 0 deletions examples/rwa-country-restrict/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#![no_std]

use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, String, Vec};
use stellar_tokens::rwa::compliance::modules::{
country_restrict::{
storage::{is_country_restricted, remove_country_restricted, set_country_restricted},
CountryRestrict, CountryRestricted, CountryUnrestricted,
},
storage::{set_compliance_address, set_irs_address, ComplianceModuleStorageKey},
};

#[contracttype]
enum DataKey {
Admin,
}

#[contract]
pub struct CountryRestrictContract;

fn set_admin(e: &Env, admin: &Address) {
e.storage().instance().set(&DataKey::Admin, admin);
}

fn get_admin(e: &Env) -> Address {
e.storage().instance().get(&DataKey::Admin).expect("admin must be set")
}

fn require_module_admin_or_compliance_auth(e: &Env) {
if let Some(compliance) =
e.storage().instance().get::<_, Address>(&ComplianceModuleStorageKey::Compliance)
{
compliance.require_auth();
} else {
get_admin(e).require_auth();
}
}

#[contractimpl]
impl CountryRestrictContract {
pub fn __constructor(e: &Env, admin: Address) {
set_admin(e, &admin);
}
}

#[contractimpl(contracttrait)]
impl CountryRestrict for CountryRestrictContract {
fn set_identity_registry_storage(e: &Env, token: Address, irs: Address) {
require_module_admin_or_compliance_auth(e);
set_irs_address(e, &token, &irs);
}

fn add_country_restriction(e: &Env, token: Address, country: u32) {
require_module_admin_or_compliance_auth(e);
set_country_restricted(e, &token, country);
CountryRestricted { token, country }.publish(e);
}

fn remove_country_restriction(e: &Env, token: Address, country: u32) {
require_module_admin_or_compliance_auth(e);
remove_country_restricted(e, &token, country);
CountryUnrestricted { token, country }.publish(e);
}

fn batch_restrict_countries(e: &Env, token: Address, countries: Vec<u32>) {
require_module_admin_or_compliance_auth(e);
for country in countries.iter() {
set_country_restricted(e, &token, country);
CountryRestricted { token: token.clone(), country }.publish(e);
}
}

fn batch_unrestrict_countries(e: &Env, token: Address, countries: Vec<u32>) {
require_module_admin_or_compliance_auth(e);
for country in countries.iter() {
remove_country_restricted(e, &token, country);
CountryUnrestricted { token: token.clone(), country }.publish(e);
}
}

fn is_country_restricted(e: &Env, token: Address, country: u32) -> bool {
is_country_restricted(e, &token, country)
}

fn set_compliance_address(e: &Env, compliance: Address) {
get_admin(e).require_auth();
set_compliance_address(e, &compliance);
}
}
Loading
Loading