Open-source Soroban smart contracts powering transparent, secure and verifiable
humanitarian aid distribution on the Stellar blockchain.
- Overview
- Why Smart Contracts
- Core Features
- Architecture
- Contract Modules
- Technology Stack
- Deployments
- Development Setup
- Build
- Testing
- Deployment
- Security
- Upgrade Strategy
- Contract Interfaces
- Events
- Documentation
| Document | Description |
|---|---|
| UPGRADEABILITY.md | Contract upgrade model and migration strategy |
| SECURITY_BATCH.md | Security audit findings and remediation status |
| GAS_OPTIMIZATION.md | Gas optimization patterns and benchmarks |
| testing/README.md | Testing harness setup and conventions |
| shared/README.md | Shared library utilities and helpers |
| security/README.md | Security model and access control overview |
| docs/WORKERS.md | Background worker framework: job lifecycle, retry policy, dead letters, local runner |
| docs/PROGRESSIVE_DISCLOSURE.md | Advanced transaction detail fields, and why critical warnings can't end up advanced-only |
| docs/CHANGELOG_SCHEMA.md | The machine-readable changelog (changelog/entries.json): schema, validation, when an entry is required |
| docs/RELEASE_READINESS.md | The PR checklist, what's automated vs. reviewer judgment, and the emergency-fix exception process |
| docs/SECURITY_INVARIANTS.md | Formal protocol invariants (token conservation, referral acyclicity, oracle quorum/staleness, admin non-revocability) with the code and error variant that enforces each one |
| docs/THREAT_MODEL.md | Trust assumptions, adversarial attack vectors per contract family, and pause/emergency-response pointers |
- Storage Layout
- Documentation
- Contribution Guide
- Brand
- License
The Trellis Contracts repository contains every on-chain component responsible for securely executing humanitarian aid transactions on the Stellar blockchain using Soroban.
These contracts serve as the trust layer of the platform.
Instead of relying on centralized intermediaries, every payment, verification, referral reward, and settlement is executed transparently on-chain.
The contracts are designed to be:
- Secure
- Auditable
- Upgradeable
- Gas Efficient
- Modular
- Production Ready
Traditional donation systems rely heavily on centralized infrastructure.
Trellis replaces this model by executing critical operations directly on-chain.
The contracts provide:
- Transparent aid settlements
- Immutable transaction records
- Referral commission distribution
- Treasury management
- Multi-signature administration
- Emergency controls
- Upgrade governance
Every important financial action becomes publicly verifiable.
Transfers donor funds directly to verified recipients.
Features
- Atomic transfers
- Escrow support
- Expiration handling
- Claim verification
- Replay protection
Generate secure claim identifiers.
Supports
- One-time claims
- Expiration dates
- Maximum claims
- Signature validation
- Hash verification
Automatically distributes affiliate commissions.
Supports
- Multi-tier referrals
- Percentage configuration
- Reward limits
- Treasury payouts
Central treasury responsible for:
- Holding protocol reserves
- Reward distribution
- Administrative transfers
- Emergency withdrawals
Administrative controls include
- Contract upgrades
- Parameter updates
- Treasury permissions
- Emergency pause
- Role assignments
Stores verification references without exposing private user information.
Supports
- Hash verification
- Metadata pointers
- AI verification references
- Off-chain oracle integration
┌────────────────────────────┐
│ Frontend │
└────────────┬───────────────┘
│
▼
┌────────────────────────────┐
│ Backend API │
└────────────┬───────────────┘
│
▼
┌────────────────────────────┐
│ Soroban Contracts │
├────────────────────────────┤
│ Aid Contract │
│ Treasury Contract │
│ Referral Contract │
│ Governance Contract │
│ Oracle Contract │
│ Registry Contract │
└────────────┬───────────────┘
│
▼
Stellar Ledger
Responsible for
- Aid creation
- Aid claiming
- Settlement
- Escrow
- Refunds
Responsible for
- Treasury balances
- Reward distribution
- Protocol funds
- Emergency reserve
Responsible for
- Referral registration
- Commission calculations
- Tier rewards
- Reward claims
Responsible for
- Upgrade authorization
- Admin roles
- Parameter management
- Contract registry
Responsible for
- AI verification references
- External signatures
- Verification proofs
Responsible for
- Contract discovery
- Address registry
- Version tracking
Shared library containing types and utilities reused by every contract in the workspace. See shared/README.md for error codes, reserved ranges, and event schemas.
Comprehensive testing framework with mocks, helpers, simulation tools, and fuzzing harnesses. See testing/README.md for usage examples and available utilities.
Security audit tooling and CI gating for static analysis and vulnerability scanning. See security/README.md for running audits and managing allowlists.
| Technology | Purpose |
|---|---|
| Rust | Smart contract language |
| Soroban SDK | Contract development |
| Stellar CLI | Deployment |
| Cargo | Package manager |
| Soroban RPC | Network interaction |
| GitHub Actions | CI/CD |
For deployed contract addresses, networks, and versions, see DEPLOYMENTS.md.
| Network | Status | Details |
|---|---|---|
| Testnet | Not yet deployed | See DEPLOYMENTS.md for setup |
| Mainnet | Not yet deployed | Awaiting security review and governance |
To deploy all contracts to testnet:
./scripts/deploy.sh testnet
./scripts/record-deployments.sh testnetThen verify:
./scripts/verify.sh testnetFor mainnet deployments, see UPGRADEABILITY.md for the full upgrade governance process.
All upgrades are tracked in the on-chain upgradeability registry. To view upgrade history for a contract:
soroban contract invoke \
--id <UPGRADEABILITY_CONTRACT_ID> \
-- get_upgrade_history \
--contract-id <CONTRACT_ID>---# Project Structure
contracts/
├── aid-contract/
├── treasury-contract/
├── referral-contract/
├── governance-contract/
├── oracle-contract/
├── registry-contract/
│
├── shared/
│ ├── errors.rs
│ ├── events.rs
│ ├── storage.rs
│ ├── auth.rs
│ ├── math.rs
│ └── utils.rs
│
├── scripts/
│ ├── deploy.sh
│ ├── upgrade.sh
│ ├── initialize.sh
│ └── verify.sh
│
├── tests/
├── Cargo.toml
├── Cargo.lock
└── README.md
rustup update
Install Soroban CLI
cargo install --locked soroban-cli
Clone repository
git clone https://github.com/TRELLIS-STELLAR/Trellis.git
cd Trellis
cargo build --release
Build optimized WASM
cargo build \
--target wasm32v1-none \
--release
Run all tests
cargo test
Run integration tests
cargo test --test integration
Generate coverage
cargo llvm-cov
Deploy to Testnet
soroban contract deploy \
--wasm target/wasm32v1-none/release/aid_contract.wasm \
--source admin
Initialize
soroban contract invoke \
--id CONTRACT_ID \
-- initialize
The contracts implement
- Reentrancy protection
- Signature verification
- Overflow-safe arithmetic
- Access control
- Replay protection
- Input validation
- Treasury limits
- Emergency pause
- Time-based expirations
- Storage validation
Supports controlled upgrades through governance.
Only authorized administrators may:
- Upgrade contracts
- Register new implementations
- Pause protocol
- Resume protocol
- Update treasury
- Modify protocol parameters
Persistent storage includes
Aid Records
- Aid ID
- Donor
- Recipient
- Amount
- Status
- Timestamp
Referral Records
- Wallet
- Referrer
- Commission
- Tier
Treasury
- Balance
- Rewards
- Fees
Governance
- Admins
- Roles
- Versions
- Registry
Core aid disbursement and escrow contract. Recipients claim funds within expiry windows; expired aids are refundable to donors.
Initialize the contract (required before other calls; admin only).
initialize(
admin: Address,
treasury: Address,
token: Address,
default_expiry_secs: u64
) -> Result<(), Error>
-
Parameters
admin— Admin address with governance privilegestreasury— Treasury address for fees or emergency withdrawalstoken— Accepted escrow token addressdefault_expiry_secs— Default expiration time in seconds for new aids
-
Authorization —
adminmust invoke (require_auth()) -
Errors
Error::AlreadyInitialized— Contract was already initialized
-
Events Emitted
ModuleInitialized—("logging", "initialized")with module name, version, caller, timestamp
Get the current admin address.
get_admin(env: Env) -> Address
- Returns — Current admin address
Get the treasury address.
get_treasury(env: Env) -> Option<Address>
- Returns — Treasury address or
Noneif not set
Get the accepted escrow token.
get_token(env: Env) -> Option<Address>
- Returns — Token address or
Noneif not set
Get the default expiry in seconds.
get_default_expiry(env: Env) -> Option<u64>
- Returns — Expiry duration in seconds or
None
Check if the contract is initialized.
is_initialized(env: Env) -> bool
- Returns —
trueif initialized,falseotherwise
Update configuration (admin only).
update_config(
admin: Address,
treasury: Option<Address>,
default_expiry_secs: Option<u64>
) -> Result<(), Error>
-
Parameters
admin— Must be the current admintreasury— New treasury address (None= no change)default_expiry_secs— New default expiry (None= no change)
-
Authorization —
adminmust invoke (require_auth()) -
Errors
Error::Unauthorized— Caller is not the current admin
Create a new aid disbursement and escrow funds from the donor.
create_aid(
donor: Address,
recipient: Address,
amount: i128,
expiry_ledger: u32
) -> u64
-
Parameters
donor— Sender of the aid (must have token balance)recipient— Intended claimantamount— Amount in token units (must be > 0)expiry_ledger— Ledger sequence after which aid cannot be claimed (must be > current)
-
Authorization —
donormust invoke (require_auth()) -
Returns — Newly assigned aid ID
-
Errors (panic)
Error::InvalidAmount— Amount is ≤ 0AidError::NotExpiredYet— Expiry ledger is ≤ current ledger
-
Token Transfer — Transfers
amountfrom donor to contract -
Events Emitted
AidCreated—("aid", "created")with (aid_id, donor, recipient, amount, created_at, expires_at)- Generic
AID_CREATEDlegacy event
Claim a pending aid and transfer funds to recipient.
claim_aid(
aid_id: u64,
recipient: Address
) -> Result<(), AidError>
-
Parameters
aid_id— ID of the aid to claimrecipient— Must be the intended recipient
-
Authorization —
recipientmust invoke (require_auth()) -
Errors
AidError::Paused— Contract is pausedAidError::NotFound— Aid ID does not existAidError::AlreadyClaimed— Aid was already claimed or refundedAidError::Expired— Expiry ledger has passed (cannot claim expired aids)AidError::Unauthorized— Caller is not the intended recipient
-
State Changes — Updates aid status from
PendingtoSettled -
Token Transfer — Transfers
amountfrom contract to recipient -
Events Emitted
AidClaimed—("aid", "claimed")with (aid_id, claimant, claimed_at)AidSettled—("aid", "settled")with (aid_id, recipient, amount, settled_at)- Generic
AID_CLAIMEDandAID_SETTLEDlegacy events
Refund an expired, unclaimed aid to the original donor.
refund_aid(aid_id: u64) -> Result<(), AidError>
-
Parameters
aid_id— ID of the expired aid to refund
-
Errors
AidError::NotFound— Aid ID does not existAidError::AlreadyClaimed— Aid was already claimed (status isSettled)AidError::AlreadyRefunded— Aid was already refundedAidError::NotExpiredYet— Expiry ledger has not passed yet
-
State Changes — Updates aid status from
PendingtoRefunded -
Token Transfer — Transfers
amountfrom contract back to donor -
Events Emitted
AidRefunded—("aid", "refunded")with (aid_id, donor, amount, refunded_at)- Generic
AID_REFUNDEDlegacy event
Get a single aid record by ID.
get_aid(env: Env, aid_id: u64) -> Option<AidRecord>
-
Parameters
aid_id— ID to fetch
-
Returns — Aid record with id, donor, recipient, token, amount, expiry_ledger, status or
Noneif not found
List all aids with pagination.
list_aids(env: Env, limit: u32, cursor: Option<u64>) -> PaginatedAidsResponse
-
Parameters
limit— Max results per pagecursor— Start position (aid ID);Nonestarts at 0
-
Returns —
PaginatedAidsResponse { aids: Vec<AidRecord>, next_cursor: Option<u64> }- Pass
next_cursorback ascursorto fetch the next page next_cursor = Nonemeans end of results
- Pass
List aids created by a specific donor, with pagination.
list_aids_by_donor(
env: Env,
donor: Address,
limit: u32,
cursor: Option<u64>
) -> PaginatedAidsResponse
-
Parameters
donor— Filter by donor addresslimit— Max results per pagecursor— Start position (aid ID);Nonestarts at 0
-
Returns —
PaginatedAidsResponse { aids: Vec<AidRecord>, next_cursor: Option<u64> }
search_aids(viewer, cursor, limit) returns only pending, visible records for
which viewer authenticates as the donor, recipient, contract admin, or an
address granted access by the donor. Completed, hidden, and deleted records
are removed from the derived search index. Administrators can run
repair_search_index(admin) to rebuild that index from canonical aid storage;
the return value reports indexed, added, and removed entries. No migration is
required: older records are included on the next repair run.
Pause or resume the contract (admin only).
set_paused(env: Env, admin: Address, paused: bool)
-
Parameters
admin— Must be the current adminpaused—trueto pause,falseto resume
-
Authorization —
adminmust invoke (require_auth()) -
Effect — When paused,
claim_aidreturnsAidError::Paused -
Events Emitted
PermissionChanged—("logging", "permission")with (module, role, subject, granted, changed_at)
Protocol treasury management. Manages per-category balances, enforces withdrawal limits, and distributes referral rewards.
Initialize the treasury contract (required before other calls; admin only).
initialize(
admin: Address,
max_withdrawal_limit: i128
) -> Result<(), Error>
-
Parameters
admin— Admin address with governance privilegesmax_withdrawal_limit— Max amount per withdrawal transaction (must be > 0)
-
Authorization —
adminmust invoke (require_auth()) -
Errors
Error::InvalidArgument— Limit is ≤ 0
-
Initial State — Admin is automatically granted
TreasuryManagerrole -
Events Emitted
ModuleInitialized—("logging", "initialized")with module name, version, caller, timestamp
Grant the TreasuryManager role to an address (admin only).
add_treasury_manager(
caller: Address,
who: Address
) -> Result<(), Error>
-
Parameters
caller— Must be the current adminwho— Address to grant manager role
-
Authorization —
callermust be admin -
Errors
Error::Unauthorized— Caller is not admin
-
Events Emitted
PermissionChanged—("logging", "permission")with (treasury, manager, who, true, changed_at)
Revoke the TreasuryManager role from an address (admin only).
remove_treasury_manager(
caller: Address,
who: Address
) -> Result<(), Error>
-
Parameters
caller— Must be the current adminwho— Address to revoke manager role
-
Authorization —
callermust be admin -
Errors
Error::Unauthorized— Caller is not admin
-
Events Emitted
PermissionChanged—("logging", "permission")with (treasury, manager, who, false, changed_at)
Update the max per-transaction withdrawal limit (admin only).
set_withdrawal_limit(
caller: Address,
new_limit: i128
) -> Result<(), Error>
-
Parameters
caller— Must be the current adminnew_limit— New max withdrawal amount (must be > 0)
-
Authorization —
callermust be admin -
Errors
Error::Unauthorized— Caller is not adminError::InvalidArgument— Limit is ≤ 0
-
Events Emitted
ActionExecuted—("logging", "action")with (treasury, wd_limit, caller, true, timestamp)
Get the current max per-transaction withdrawal limit.
withdrawal_limit(env: Env) -> i128
- Returns — Max withdrawal amount in tokens
Credit funds into a category balance (treasury manager only).
deposit(
caller: Address,
category: Symbol,
amount: i128
) -> Result<(), Error>
-
Parameters
caller— Must holdTreasuryManagerrolecategory— Category to credit (e.g.,reserve,rewards)amount— Amount to add (must be > 0)
-
Authorization —
callermust be a treasury manager -
Errors
Error::InvalidArgument— Amount is ≤ 0Error::Unauthorized— Caller is not a treasury managerError::Overflow— Adding amount would overflow balance
-
State Changes — Increments category balance
-
Events Emitted
TreasuryDeposit—("treasury", "deposit")with (category, caller, amount, new_balance)ActionExecuted—("logging", "action")with (treasury, deposit, caller, true, timestamp)
Withdraw from a category to a recipient (treasury manager only).
withdraw(
caller: Address,
to: Address,
amount: i128,
category: Symbol
) -> Result<(), Error>
-
Parameters
caller— Must holdTreasuryManagerroleto— Recipient addressamount— Withdrawal amount (must be > 0)category— Category to withdraw from
-
Authorization —
callermust be a treasury manager -
Validation Order (cheap-first for gas optimization)
amount > 0amount ≤ withdrawal_limitamount ≤ category balancecallerhasTreasuryManagerrole
-
Errors
Error::InvalidArgument— Amount is ≤ 0Error::WithdrawalLimitExceeded— Amount exceeds per-tx limitError::InsufficientBalance— Category balance insufficientError::Unauthorized— Caller is not a treasury manager
-
State Changes — Decrements category balance
-
Events Emitted
TreasuryWithdrawal—("treasury", "withdraw")with (category, to, amount, remaining_balance)ActionExecuted—("logging", "action")with (treasury, withdraw, caller, true, timestamp)
Get the current balance for a category.
category_balance(env: Env, category: Symbol) -> i128
-
Parameters
category— Category to query
-
Returns — Balance (0 if never funded)
Admin-only emergency withdrawal from the reserve category while contract is paused.
emergency_withdraw(
caller: Address,
to: Address,
amount: i128
) -> Result<(), Error>
-
Parameters
caller— Must be the current adminto— Recipient addressamount— Withdrawal amount (must be > 0)
-
Authorization —
callermust be admin -
Preconditions
- Contract must be paused
- Amount must be > 0
- Reserve balance must be ≥ amount
-
Errors
Error::InvalidArgument— Amount is ≤ 0Error::NotPaused— Contract is not pausedError::InsufficientBalance— Reserve balance insufficientError::Unauthorized— Caller is not admin
-
State Changes — Decrements reserve category balance
-
Events Emitted
TREASURY_EMERGENCY_WITHDRAWlegacy event with (caller, to, amount)ActionExecuted—("logging", "action")with (treasury, emrg_wd, caller, true, timestamp)
Register the referral contract authorized to call distribute_reward (admin only).
set_referral_contract(
caller: Address,
referral_contract: Address
) -> Result<(), Error>
-
Parameters
caller— Must be the current adminreferral_contract— Address of the referral contract
-
Authorization —
callermust be admin -
Errors
Error::Unauthorized— Caller is not admin
-
Events Emitted
ActionExecuted—("logging", "action")with (treasury, ref_ctr, caller, true, timestamp)
Get the currently registered referral contract address.
referral_contract(env: Env) -> Option<Address>
- Returns — Referral contract address or
Noneif not set
Pay a referral commission from the rewards category (referral contract only).
distribute_reward(
recipient: Address,
amount: i128
) -> Result<(), Error>
-
Parameters
recipient— Address receiving the commissionamount— Commission amount (must be > 0)
-
Authorization — Direct caller must be the registered referral contract (
require_auth()) -
Validation Order (cheap-first for gas optimization)
amount > 0amount ≤ rewards category balance- Direct caller is registered referral contract
-
Errors
Error::InvalidArgument— Amount is ≤ 0Error::InsufficientBalance— Rewards balance insufficientError::Unauthorized— No referral contract registered or caller is not it
-
State Changes — Decrements rewards category balance
-
Events Emitted
CommissionPaid—("comm", "paid")with (recipient, amount, timestamp)ActionExecuted—("logging", "action")with (treasury, reward, recipient, true, timestamp)
Multi-tier referral graph and commission accrual. Tracks referrer chains, computes commissions at each tier, enforces lifetime caps, and distributes rewards.
Initialize the referral contract (required before other calls; admin only).
initialize(env: Env, admin: Address)
-
Parameters
admin— Admin address with governance privileges
-
Initial State
- Default max tiers: 1
- Default reward cap: 0
- Default tier 1 BPS: 0
-
Events Emitted
ModuleInitialized—("logging", "initialized")with module name, version, caller, timestamp
Configure the treasury contract used for reward claims (admin only).
set_treasury(
caller: Address,
treasury: Address
) -> Result<(), Error>
-
Parameters
caller— Must be the current admintreasury— Treasury contract address
-
Authorization —
callermust be admin -
Errors
Error::Unauthorized— Caller is not admin
-
Events Emitted
ActionExecuted—("logging", "action")with (referral, treasury, caller, true, timestamp)
Get the configured treasury contract address.
get_treasury(env: Env) -> Result<Address, Error>
-
Returns — Treasury address
-
Errors
Error::NotFound— No treasury configured
Configure the registry contract that resolves dependencies (admin only).
set_registry(
caller: Address,
registry: Address
) -> Result<(), Error>
-
Parameters
caller— Must be the current adminregistry— Registry contract address
-
Authorization —
callermust be admin -
Errors
Error::Unauthorized— Caller is not admin
Configure tier percentages and lifetime reward cap (admin only).
set_tier_config(
caller: Address,
tier_bps: Vec<i128>,
max_tiers: u32,
reward_cap: i128
) -> Result<(), Error>
-
Parameters
caller— Must be the current admintier_bps— Vector of basis point percentages (1 BPS = 0.01%)max_tiers— Max referral depth to traverse (1-10)reward_cap— Lifetime accrual cap per referrer (0-1_000_000_000_000_000_000)
-
Authorization —
callermust be admin -
Validation
- Each tier BPS must be 0-10_000 (0-100%)
max_tiersmust be 1-10reward_capmust be 0-1_000_000_000_000_000_000- Length of
tier_bpsmust equalmax_tiers
-
Errors
Error::Unauthorized— Caller is not adminError::InvalidArgument— Config values out of range or mismatched lengths
-
Events Emitted
TierConfigSet— Custom event with full configPermissionChanged—("logging", "permission")with (referral, tier, caller, true, timestamp)
Get the active tier configuration.
get_tier_config(env: Env) -> Result<TierConfig, Error>
- Returns —
TierConfig { tier_bps: Vec<i128>, max_tiers: u32, reward_cap: i128 }
Manually register a referral edge (admin only).
set_referrer(
caller: Address,
referred_wallet: Address,
referrer: Address
) -> Result<(), Error>
-
Parameters
caller— Must be the current adminreferred_wallet— Wallet to assign a referrerreferrer— The referrer address
-
Authorization —
callermust be admin -
Validation
- Cannot create self-referral
- Cannot create cycles in the referral graph
-
Errors
Error::Unauthorized— Caller is not adminError::InvalidArgument— Self-referral or would create cycle
-
Events Emitted
ReferrerSet— Custom event with (referred_wallet, referrer)ActionExecuted—("logging", "action")with (referral, referrer, caller, true, timestamp)
Get the direct referrer for a wallet.
get_referrer(env: Env, wallet: Address) -> Option<Address>
-
Parameters
wallet— Wallet to query
-
Returns — Direct referrer address or
Noneif not registered
Self-register under an existing referrer.
register(
wallet: Address,
referrer: Address
) -> Result<(), Error>
-
Parameters
wallet— The wallet registering (must invoke)referrer— Existing referrer to join under
-
Authorization —
walletmust invoke (require_auth()) -
Validation
- Cannot self-refer
- Cannot register if already registered
- Referrer must already exist in the graph
- Cannot create cycles
-
Errors
Error::InvalidArgument— Self-referral, already registered, referrer not found, or would create cycle
-
State Changes — Records referral edge and creates referral record
-
Events Emitted
ReferralRegistered— Custom event with (wallet, referrer)ActionExecuted—("logging", "action")with (referral, register, wallet, true, timestamp)
Get the full referral record for a wallet.
get_referral_record(env: Env, wallet: Address) -> Option<ReferralRecord>
-
Parameters
wallet— Wallet to query
-
Returns —
ReferralRecord { wallet, referrer, commission, tier }orNoneif not registered
Accrue multi-tier referral commissions for a transaction (admin only).
accrue(
caller: Address,
referred_wallet: Address,
base_amount: i128
) -> Result<i128, Error>
-
Parameters
caller— Must be the current adminreferred_wallet— The wallet originating the commissionbase_amount— Base transaction amount (must be > 0)
-
Authorization —
callermust be admin -
Algorithm
- Traverses the referral chain up to
max_tierslevels - At each tier, calculates commission as
base_amount × tier_bps[tier-1] / 10000 - Credits commission to referrer if > 0
- Applies lifetime
reward_capper referrer - Pre-caches tier BPS to optimize gas (avoids repeated storage reads)
- Traverses the referral chain up to
-
Errors
Error::Unauthorized— Caller is not adminError::InvalidArgument— Base amount is ≤ 0Error::NotFound— Tier BPS not configuredError::Overflow— Commission calculation would overflow
-
Returns — Total amount credited across all tiers
-
Events Emitted (per credited referrer)
AccruedReward— Custom event with (referred_wallet, referrer, tier, amount, accrued_balance, lifetime_accrued)
Get the currently claimable accrued balance for a referrer.
accrued_balance(env: Env, referrer: Address) -> i128
-
Parameters
referrer— Referrer to query
-
Returns — Amount ready to claim (0 if nothing accrued or already claimed)
Get total lifetime rewards accrued for a referrer (for cap enforcement).
lifetime_accrued(env: Env, referrer: Address) -> i128
-
Parameters
referrer— Referrer to query
-
Returns — Cumulative lifetime commissions (used to enforce cap)
Claim accrued referral rewards from treasury.
claim_rewards(env: Env, referrer: Address) -> Result<i128, Error>
-
Parameters
referrer— Referrer claiming their balance
-
Authorization —
referrermust invoke (require_auth()) -
Idempotence — A second claim after a successful payout returns
Ok(0)and leaves treasury untouched -
Errors
Error::NotFound— Treasury not configured
-
State Changes — Resets accrued balance to 0
-
Treasury Integration — Calls
treasury.distribute_reward(referrer, amount) -
Events Emitted
CommissionPaid— Custom event with (referrer, amount)ActionExecuted—("logging", "action")with (referral, claim, referrer, true, timestamp)
Multi-signature governance for role management, proposal flow, and protocol parameter tuning.
Initialize the governance contract with an admin set and multi-sig threshold (required; admin only).
initialize(
admin: Address,
threshold: u32,
admin_set: Vec<Address>
) -> Result<(), Error>
-
Parameters
admin— Primary admin address (used for backwards-compatible parameter management)threshold— Min approvals (M) required to execute proposals; must be ≥ 1 and ≤admin_set.len()admin_set— Vector of addresses that can approve proposals; all receive theAdminrole
-
Validation
threshold ≥ 1admin_set.len() ≥ threshold
-
Errors
Error::InvalidArgument— Threshold or admin set invalid
-
Initial State — All parameters seeded with defaults:
AidDefaultExpiry: 604,800 (7 days)TreasuryWithdrawalLimit: 100,000,000,000ReferralTierBps(1): 500 (5%)ReferralTierBps(2): 250 (2.5%)ReferralTierBps(3): 100 (1%)ReferralMaxTiers: 3ReferralRewardCap: 10,000,000,000
-
Events Emitted
ModuleInitialized—("logging", "initialized")with module name, version, caller, timestamp
Grant a role to a user (admin only).
grant_role(
caller: Address,
user: Address,
role: Role
) -> Result<(), Error>
-
Parameters
caller— Must be an adminuser— User to grant rolerole— One of:Admin,Upgrader,TreasuryManager,Pauser,ReferralManager,OracleSigner
-
Authorization —
callermust holdAdminrole -
Errors
Error::Unauthorized— Caller is not an admin
-
Events Emitted
RoleGranted— Custom event with (caller, user, role_name, timestamp)
Revoke a role from a user (admin only).
revoke_role(
caller: Address,
user: Address,
role: Role
) -> Result<(), Error>
-
Parameters
caller— Must be an adminuser— User to revoke rolerole— Role to remove
-
Authorization —
callermust holdAdminrole -
Errors
Error::Unauthorized— Caller is not an admin
-
Events Emitted
RoleRevoked— Custom event with (caller, user, role_name, timestamp)
Check if a user holds a role.
has_role(env: Env, user: Address, role: Role) -> bool
-
Parameters
user— User to checkrole— Role to verify
-
Returns —
trueif user holds role,falseotherwise
Update the multi-sig admin set and threshold (admin only).
set_admin_set(
caller: Address,
new_admin_set: Vec<Address>,
new_threshold: u32
) -> Result<(), Error>
-
Parameters
caller— Must be an adminnew_admin_set— New set of admin addressesnew_threshold— New approval threshold (M)
-
Authorization —
callermust holdAdminrole -
Validation
new_threshold ≥ 1new_admin_set.len() ≥ new_threshold
-
Errors
Error::Unauthorized— Caller is not an adminError::InvalidArgument— Threshold or set invalid
Get the current multi-sig approval threshold.
get_threshold(env: Env) -> u32
- Returns — Current threshold (M)
Get the current admin set.
get_admin_set(env: Env) -> Vec<Address>
- Returns — Vector of admin addresses (N)
Check if the contract is currently paused.
is_paused(env: Env) -> bool
- Returns —
trueif paused,falseotherwise
Create a new proposal (admin only).
propose(
caller: Address,
action: ProposalAction
) -> Result<u64, Error>
-
Parameters
caller— Must be an adminaction— One of:GrantRole(address, role)— Grant role to addressRevokeRole(address, role)— Revoke role from addressSetParameter(key, value)— Update protocol parameterPause— Pause the protocolUnpause— Resume the protocol
-
Authorization —
callermust holdAdminrole -
Returns — Newly assigned proposal ID
-
Errors
Error::Unauthorized— Caller is not an adminError::Overflow— Proposal ID counter overflowed
-
Initial State — Proposal created with
approval_count = 0and statusPending -
Events Emitted
ProposalCreated— Custom event with (proposal_id, proposer, action_symbol, timestamp)
Approve a pending proposal (admin only).
approve(
caller: Address,
proposal_id: u64
) -> Result<(), Error>
-
Parameters
caller— Must be an adminproposal_id— ID of proposal to approve
-
Authorization —
callermust holdAdminrole -
Idempotence — Each admin can only approve once; duplicate approvals rejected
-
Errors
Error::Unauthorized— Caller is not an adminError::ProposalNotFound— Proposal ID doesn't existError::AlreadyExecuted— Proposal was already executedError::AlreadyApproved— Caller already approved this proposalError::Overflow— Approval count overflowed
-
State Changes — Increments proposal
approval_count -
Events Emitted
ProposalApproved— Custom event with (proposal_id, approver, approval_count, timestamp)
Execute a proposal once threshold is met (admin only).
execute(
caller: Address,
proposal_id: u64
) -> Result<(), Error>
-
Parameters
caller— Must be an adminproposal_id— ID of proposal to execute
-
Authorization —
callermust holdAdminrole -
Preconditions
- Proposal must exist and be pending
approval_count ≥ threshold
-
Errors
Error::Unauthorized— Caller is not an adminError::ProposalNotFound— Proposal ID doesn't existError::AlreadyExecuted— Proposal was already executedError::BelowThreshold— Approval count < thresholdError::InvalidArgument— Parameter value out of bounds (forSetParameteractions)
-
Action Execution — Applies the proposal's action (grant/revoke role, set parameter, pause/unpause)
-
State Changes — Sets proposal status to
Executed -
Events Emitted
ProposalExecuted— Custom event with (proposal_id, executor, approval_count, timestamp)
Get a proposal by ID.
get_proposal(env: Env, proposal_id: u64) -> Result<Proposal, Error>
-
Parameters
proposal_id— ID to fetch
-
Returns —
Proposal { id, proposer, action, approval_count, status, created_at } -
Errors
Error::ProposalNotFound— Proposal doesn't exist
Update a protocol parameter (admin only).
set_param(
caller: Address,
key: ParameterKey,
value: i128
) -> Result<(), Error>
-
Parameters
caller— Must be the primary adminkey— Parameter key (see list below)value— New value (must be within documented bounds)
-
Authorization —
callermust be primary admin -
Parameter Keys & Bounds
AidDefaultExpiry: 60-31,536,000 secondsTreasuryWithdrawalLimit: 0-1,000,000,000,000,000,000ReferralTierBps(tier): 0-10,000 (0-100% in basis points)ReferralMaxTiers: 1-10ReferralRewardCap: 0-1,000,000,000,000,000,000
-
Errors
Error::Unauthorized— Caller is not primary adminError::InvalidArgument— Value outside bounds or tier invalid
-
Events Emitted
ParameterChanged— Custom event with (key, value)ActionExecuted—("logging", "action")with (gov, set_param, caller, true, timestamp)
Read a protocol parameter.
get_param(env: Env, key: ParameterKey) -> Result<i128, Error>
-
Parameters
key— Parameter to read
-
Returns — Parameter value
-
Errors
Error::NotFound— Parameter not set
Get documented min/max bounds for a parameter.
get_bounds(env: Env, key: ParameterKey) -> Result<ParameterBounds, Error>
-
Parameters
key— Parameter key
-
Returns —
ParameterBounds { min: i128, max: i128 } -
Errors
Error::InvalidArgument— Parameter key invalid
aid_default_expiry(env: Env) -> Result<i128, Error>
treasury_withdrawal_limit(env: Env) -> Result<i128, Error>
referral_tier_bps(env: Env, tier: u32) -> Result<i128, Error>
referral_max_tiers(env: Env) -> Result<i128, Error>
referral_reward_cap(env: Env) -> Result<i128, Error>
Each returns the current value for the corresponding parameter.
Contract and metadata registry. Tracks contract versions, manages mutable/immutable metadata entries, enables contract discovery.
Initialize the registry contract (required before other calls; admin only).
initialize(env: Env, admin: Address)
-
Parameters
admin— Admin address with governance privileges
-
Events Emitted
ModuleInitialized—("logging", "initialized")with module name, version, caller, timestamp
Register or update a contract address and version (admin only).
set_contract(
caller: Address,
name: Symbol,
address: Address,
version: u32
) -> Result<(), Error>
-
Parameters
caller— Must be the current adminname— Logical contract name (e.g., "aid", "treasury")address— Contract address to registerversion— Version number (typically incremented on upgrades)
-
Authorization —
callermust be admin -
Gas Optimization — Version history Vec only deserialized when genuinely new version registered; common case (same version) skips expensive Vec read + linear scan
-
Errors
Error::Unauthorized— Caller is not admin
-
State Changes — Updates contract mapping and appends to version history if new version
-
Events Emitted
ActionExecuted—("logging", "action")with (registry, set_ctr, caller, true, timestamp)
Resolve the latest registered address and version for a contract name.
get_contract(env: Env, name: Symbol) -> Result<(Address, u32), Error>
-
Parameters
name— Contract name to look up
-
Returns —
(Address, version_number)tuple -
Errors
Error::NotFound— Contract name not registered
Get the complete version history for a contract name.
get_version_history(env: Env, name: Symbol) -> Result<Vec<u32>, Error>
-
Parameters
name— Contract name to query
-
Returns — Vector of all registered versions in chronological order
-
Errors
Error::NotFound— Contract name not registered
Register or update metadata for an identifier (admin only).
set_metadata(
caller: Address,
name: Symbol,
uri: Bytes,
hash: Bytes,
immutable: bool,
schema_version: u32
) -> Result<(), Error>
-
Parameters
caller— Must be the current adminname— Identifier for the metadata (e.g., "treasury_config", "oracle_feed")uri— URI pointing to metadata content (IPFS, HTTPS, etc.)hash— SHA-256 hash (32 bytes) of the metadata for verificationimmutable— Iftrue, this entry cannot be updated after creationschema_version— Version of the metadata schema
-
Authorization —
callermust be admin -
Validation
- Hash must be exactly 32 bytes (SHA-256)
- URI must not be empty
- Cannot update an immutable entry
-
Errors
Error::Unauthorized— Caller is not adminError::ImmutableEntry— Entry exists and is marked immutableError::InvalidHash— Hash is not 32 bytesError::InvalidArgument— URI is empty
-
State Changes — Creates or updates metadata entry with current timestamp
Retrieve full metadata entry for an identifier.
get_metadata(env: Env, name: Symbol) -> Result<MetadataEntry, Error>
-
Parameters
name— Metadata identifier
-
Returns —
MetadataEntry { uri, hash, immutable, updated_at, schema_version } -
Errors
Error::MetadataNotFound— Metadata not registered
Get the metadata hash for an identifier (for verification).
get_metadata_hash(env: Env, name: Symbol) -> Result<Bytes, Error>
-
Parameters
name— Metadata identifier
-
Returns — 32-byte SHA-256 hash
-
Errors
Error::MetadataNotFound— Metadata not registered
Check if a metadata entry is immutable.
is_immutable(env: Env, name: Symbol) -> Result<bool, Error>
-
Parameters
name— Metadata identifier
-
Returns —
trueif entry is immutable,falseotherwise -
Errors
Error::MetadataNotFound— Metadata not registered
Get the full registry entry (contract + metadata) for an identifier.
get_registry_entry(env: Env, name: Symbol) -> Result<RegistryEntry, Error>
-
Parameters
name— Identifier to query
-
Returns —
RegistryEntry { contract: ContractRegistration, metadata: MetadataEntry }ContractRegistration { address, version }MetadataEntry { uri, hash, immutable, updated_at, schema_version }
-
Errors
Error::NotFound— Contract not registeredError::MetadataNotFound— Metadata not registered
List all registered contract names.
list_names(env: Env) -> Result<Vec<Symbol>, Error>
- Returns — Vector of all contract names that have been registered
List all metadata entries.
list_metadata_entries(env: Env) -> Result<Map<Symbol, MetadataEntry>, Error>
- Returns — Map of identifier →
MetadataEntryfor all registered metadata
Example payment gateway demonstrating token deposits, pull-based withdrawals with fee handling, escrow, and batch payouts.
Initialize the payment gateway (required before other calls; admin only).
initialize(
admin: Address,
token: Address,
fee_rate_bps: i128,
fee_recipient: Address
) -> Result<(), Error>
-
Parameters
admin— Admin address with governance privilegestoken— Token address for all deposits/withdrawalsfee_rate_bps— Fee rate in basis points (0-10,000; 0-100%)fee_recipient— Address receiving collected fees
-
Validation
fee_rate_bpsmust be 0-10,000
-
Errors
Error::PaymentInvalidFeeRate— Fee rate outside valid range
-
Initial State — Total deposits set to 0
-
Events Emitted
ModuleInitialized—("logging", "initialized")with module name, version, caller, timestamp
Update the fee rate (admin only).
set_fee_rate(
caller: Address,
new_rate_bps: i128
) -> Result<(), Error>
-
Parameters
caller— Must be the current adminnew_rate_bps— New fee rate (0-10,000 basis points)
-
Authorization —
callermust be admin -
Validation
new_rate_bpsmust be 0-10,000
-
Errors
Error::Unauthorized— Caller is not adminError::PaymentInvalidFeeRate— Fee rate outside valid range
-
Events Emitted
ActionExecuted—("logging", "action")with (pay_gw, fee_set, caller, true, timestamp)
Get the current fee rate in basis points.
fee_rate(env: Env) -> i128
- Returns — Fee rate (0-10,000)
Get the configured fee recipient address.
fee_recipient(env: Env) -> Option<Address>
- Returns — Fee recipient address or
None
Get the configured token address.
get_token(env: Env) -> Option<Address>
- Returns — Token address or
None
Deposit tokens into the contract.
deposit(env: Env, from: Address, amount: i128) -> Result<(), Error>
-
Parameters
from— Depositor address (must invoke withrequire_auth())amount— Deposit amount (must be > 0)
-
Authorization —
frommust invoke (require_auth()) -
Preconditions
frommust have pre-authorized the contract to transfer tokens- Amount must be > 0
-
Errors
Error::PaymentInvalidAmount— Amount is ≤ 0Error::NotFound— Token not configuredError::Overflow— Balance or total would overflow
-
Token Transfer — Transfers
amountfrom depositor to contract -
State Changes — Credits amount to depositor's internal balance; updates total deposits
-
Events Emitted
TreasuryDeposit— Custom event with (category, depositor, amount, new_balance)
Get the internal balance for a depositor.
balance_of(env: Env, who: Address) -> i128
-
Parameters
who— Address to query
-
Returns — Current balance (0 if never deposited)
Get total deposits across all users.
total_deposits(env: Env) -> i128
- Returns — Sum of all deposited amounts
Withdraw the caller's full balance, deducting configured fee.
withdraw(env: Env, who: Address) -> Result<i128, Error>
-
Parameters
who— Withdrawing address (must invoke withrequire_auth())
-
Authorization —
whomust invoke (require_auth()) -
Algorithm
- Calculates fee:
deposited × fee_rate / 10,000 - Calculates net:
deposited - fee - Transfers fee to
fee_recipient - Transfers net to
who
- Calculates fee:
-
Errors
Error::PaymentInsufficientBalance— No deposited balanceError::NotFound— Token or fee recipient not configured
-
State Changes — Clears internal balance; decrements total deposits
-
Returns — Net amount received (after fee)
-
Events Emitted
TreasuryWithdraw— Custom event with (category, who, amount, net)
Withdraw a specific amount from the caller's balance, deducting fee.
withdraw_amount(
env: Env,
who: Address,
amount: i128
) -> Result<i128, Error>
-
Parameters
who— Withdrawing address (must invoke withrequire_auth())amount— Amount to withdraw (must be > 0)
-
Authorization —
whomust invoke (require_auth()) -
Validation
amount > 0amount ≤ balance_of(who)
-
Algorithm
- Calculates fee:
amount × fee_rate / 10,000 - Calculates net:
amount - fee - Transfers fee to
fee_recipient - Transfers net to
who
- Calculates fee:
-
Errors
Error::PaymentInvalidAmount— Amount is ≤ 0Error::PaymentInsufficientBalance— Amount exceeds balanceError::NotFound— Token or fee recipient not configured
-
State Changes — Decrements internal balance; decrements total deposits
-
Returns — Net amount received (after fee)
-
Events Emitted
TreasuryWithdraw— Custom event with (category, who, amount, net)
Create an escrow deposit from depositor to beneficiary.
create_escrow_entry(
depositor: Address,
beneficiary: Address,
amount: i128,
expiry_ledger: u32
) -> Result<u64, Error>
-
Parameters
depositor— Funding address (must invoke withrequire_auth())beneficiary— Recipient addressamount— Escrow amount (must be > 0)expiry_ledger— Ledger sequence after which escrow can be refunded
-
Authorization —
depositormust invoke (require_auth()) -
Returns — Newly created escrow ID
-
Errors
Error::NotFound— Token not configuredError::PaymentInvalidAmount— Amount is ≤ 0
-
State Changes — Transfers amount from depositor to contract; records escrow entry
Release an escrow deposit to the beneficiary (admin only).
release_escrow_entry(
caller: Address,
escrow_id: u64
) -> Result<(), Error>
-
Parameters
caller— Must be the current adminescrow_id— ID of escrow to release
-
Authorization —
callermust be admin -
Errors
Error::Unauthorized— Caller is not adminError::NotFound— Escrow ID doesn't exist or already released- Token transfer errors
-
State Changes — Transfers escrowed amount to beneficiary; marks escrow as released
Refund an escrow deposit back to the depositor (admin only).
refund_escrow_entry(
caller: Address,
escrow_id: u64
) -> Result<(), Error>
-
Parameters
caller— Must be the current adminescrow_id— ID of escrow to refund
-
Authorization —
callermust be admin -
Errors
Error::Unauthorized— Caller is not adminError::NotFound— Escrow ID doesn't exist or already released- Token transfer errors
-
State Changes — Transfers escrowed amount back to depositor; marks escrow as refunded
Read an escrow record.
get_escrow_entry(env: Env, escrow_id: u64) -> Option<EscrowRecord>
-
Parameters
escrow_id— ID to query
-
Returns —
EscrowRecord { depositor, beneficiary, amount, expiry_ledger, status }orNoneif not found
Distribute tokens from contract to multiple recipients atomically (admin only).
batch_payout(
caller: Address,
recipients: Vec<(Address, i128)>
) -> BatchResult
-
Parameters
caller— Must be the current adminrecipients— Vector of(recipient_address, amount)tuples
-
Authorization —
callermust be admin -
Algorithm — Uses shared
multi_transfer_allbatch executor in atomic mode- On any transfer failure, entire batch reverts
- All-or-nothing semantics
-
Returns —
BatchResult { total, succeeded, failed, reverted } -
Note — This is an example of how to use the shared batch payment utilities; production deployments may require additional validation or non-atomic mode for resilience.
Standalone Role-Based Access Control (RBAC) with hierarchical roles, multi-admin management, and cycle detection.
Initialize the access control contract with a super-admin (required; called once).
initialize(env: Env, super_admin: Address)
-
Parameters
super_admin— Super-admin address (root of hierarchy)
-
Initial State
- Super-admin registered as admin
super_adminrole created and granted to super-admin- Super-admin cannot be removed via normal path
Check if an address is a registered admin.
is_admin(env: Env, who: Address) -> bool
-
Parameters
who— Address to check
-
Returns —
trueif admin,falseotherwise
Get the super-admin address set at initialization.
super_admin(env: Env) -> Address
- Returns — Super-admin address
Add a new admin (admin-only; idempotent).
add_admin(
caller: Address,
new_admin: Address
) -> Result<(), AccessControlError>
-
Parameters
caller— Must be an existing adminnew_admin— Address to grant admin status
-
Authorization —
callermust be admin and invoke (require_auth()) -
Idempotence — If address already admin, succeeds without error
-
Errors
AccessControlError::NotAdmin— Caller is not an admin
-
Events Emitted
EV_ADMIN_ADDED— Custom event with (caller, new_admin)
Remove an admin (admin-only; idempotent; cannot remove super-admin).
remove_admin(
caller: Address,
target: Address
) -> Result<(), AccessControlError>
-
Parameters
caller— Must be an existing admintarget— Address to revoke admin status
-
Authorization —
callermust be admin and invoke (require_auth()) -
Preconditions
- Cannot remove the super-admin
-
Idempotence — If address not admin, succeeds without error
-
Errors
AccessControlError::NotAdmin— Caller is not an adminAccessControlError::CannotRemoveSuperAdmin— Target is the super-admin
-
Events Emitted
EV_ADMIN_REMOVED— Custom event with (caller, target)
Get the list of all current admins.
get_all_admins(env: Env) -> Vec<Address>
- Returns — Vector of admin addresses
Create a new role (admin-only).
create_role(
caller: Address,
role: Symbol
) -> Result<(), AccessControlError>
-
Parameters
caller— Must be an adminrole— New role symbol (max 9 chars forsymbol_short!)
-
Authorization —
callermust be admin and invoke (require_auth()) -
Errors
AccessControlError::NotAdmin— Caller is not an adminAccessControlError::RoleAlreadyExists— Role already createdAccessControlError::InvalidRole— Role symbol invalid (empty)
-
Events Emitted
EV_ROLE_CREATED— Custom event with (role, caller)
Check if a role has been registered.
role_exists_check(env: Env, role: Symbol) -> bool
-
Parameters
role— Role to check
-
Returns —
trueif registered,falseotherwise
Get the list of all registered roles.
get_all_roles(env: Env) -> Vec<Symbol>
- Returns — Vector of all role symbols (includes
super_admin)
Set a parent role (establishes hierarchy; admin-only).
set_role_parent(
caller: Address,
role: Symbol,
parent: Symbol
) -> Result<(), AccessControlError>
-
Parameters
caller— Must be an adminrole— Child roleparent— Parent role (holder of parent satisfies child checks)
-
Authorization —
callermust be admin and invoke (require_auth()) -
Semantics — A holder of
parentautomatically satisfies anyhas_rolecheck forrole(and all ancestors ofrole) -
Validation
- Both roles must exist
- Cannot self-reference (
role == parent) - Cannot create cycles (parent cannot have role as ancestor)
-
Errors
AccessControlError::NotAdmin— Caller is not an adminAccessControlError::RoleNotFound— Role or parent doesn't existAccessControlError::SelfReference—role == parentAccessControlError::CycleDetected— Would create cycle
-
Events Emitted
EV_ROLE_PARENT_SET— Custom event with (role, parent, caller)
Get the direct parent of a role.
get_role_parent(env: Env, role: Symbol) -> Option<Symbol>
-
Parameters
role— Role to query
-
Returns — Direct parent role or
Noneif not set
Get the full ancestor chain for a role (ordered parent → grandparent → ...).
get_role_ancestors(env: Env, role: Symbol) -> Vec<Symbol>
-
Parameters
role— Role to query
-
Returns — Vector of ancestor roles (excludes the role itself), ordered from immediate parent upward
Grant a role to a user (admin-only).
grant_role(
caller: Address,
role: Symbol,
user: Address
) -> Result<(), AccessControlError>
-
Parameters
caller— Must be an adminrole— Role to grantuser— User to receive role
-
Authorization —
callermust be admin and invoke (require_auth()) -
Errors
AccessControlError::NotAdmin— Caller is not an adminAccessControlError::RoleNotFound— Role not registered
-
Events Emitted
EV_ROLE_GRANTED— Custom event with (role, user, caller)
Revoke a role from a user (admin-only; idempotent).
revoke_role(
caller: Address,
role: Symbol,
user: Address
) -> Result<(), AccessControlError>
-
Parameters
caller— Must be an adminrole— Role to revokeuser— User to revoke from
-
Authorization —
callermust be admin and invoke (require_auth()) -
Idempotence — Succeeds even if user didn't hold the role
-
Errors
AccessControlError::NotAdmin— Caller is not an adminAccessControlError::RoleNotFound— Role not registered
-
Events Emitted
EV_ROLE_REVOKED— Custom event with (role, user, caller)
Check if a user holds a role (directly or via hierarchy).
has_role(env: Env, role: Symbol, user: Address) -> bool
-
Parameters
role— Role to checkuser— User to verify
-
Returns —
trueif user holds role directly or via any ancestor -
Algorithm
- Checks direct membership in role
- If not direct member, walks up role hierarchy
- Returns
trueif any ancestor holds the user
Get all direct members of a role.
get_role_members(env: Env, role: Symbol) -> Vec<Address>
-
Parameters
role— Role to query
-
Returns — Vector of addresses that directly hold the role (not including indirect members via hierarchy)
Contract upgrade registry and coordinator with migration hooks, proposal flow, and audit trail.
Initialize the upgrade registry (required before other calls; admin only).
initialize(env: Env, admin: Address) -> Result<(), UpgradeError>
-
Parameters
admin— Admin address with governance privileges
-
Initial State
- Admin granted
AdminandUpgraderroles - Registry initialized (empty)
- Admin granted
-
Events Emitted
ModuleInitialized—("logging", "initialized")with module name, version, caller, timestamp
Register a contract for upgrade management (admin only).
register_contract(
caller: Address,
contract_id: Address,
name: Symbol,
version: u32,
wasm_hash: BytesN<32>
) -> Result<(), UpgradeError>
-
Parameters
caller— Must holdAdminrolecontract_id— On-chain address of contract to registername— Logical name (e.g.,symbol_short!("aid"))version— Initial version (must be ≥ 1)wasm_hash— WASM hash of initial deployment (32 bytes)
-
Authorization —
callermust holdAdminrole -
Validation
- Version must be ≥ 1
- Name must not already be registered
- WASM hash must not be empty
-
Errors
UpgradeError::NotUpgrader— Caller doesn't hold Admin roleUpgradeError::InvalidWasmHash— Version < 1 or hash invalidUpgradeError::ContractAlreadyRegistered— Name already registered
-
Events Emitted
ContractRegistered— Custom event with (contract_id, name, version, wasm_hash, timestamp)
Get the registry entry for a contract by ID.
get_registry_entry(env: Env, contract_id: Address) -> Result<RegistryEntry, UpgradeError>
-
Parameters
contract_id— Contract address to query
-
Returns —
RegistryEntry { contract_id, name, current: VersionInfo, migration_hook }VersionInfo { version, wasm_hash, deployed_at, description }
-
Errors
UpgradeError::ContractNotRegistered— Contract not registered
Get the registry entry by logical name.
get_registry_entry_by_name(env: Env, name: Symbol) -> Result<RegistryEntry, UpgradeError>
-
Parameters
name— Logical contract name
-
Returns —
RegistryEntryfor the named contract -
Errors
UpgradeError::ContractNotRegistered— Name not registered
Get the total number of registered contracts.
get_registered_count(env: Env) -> u64
- Returns — Count of registered contracts
Get the current version number for a contract.
get_version(env: Env, contract_id: Address) -> Result<u32, UpgradeError>
-
Parameters
contract_id— Contract address
-
Returns — Current version number
-
Errors
UpgradeError::ContractNotRegistered— Contract not registered
Get the current WASM hash for a contract.
get_wasm_hash(env: Env, contract_id: Address) -> Result<BytesN<32>, UpgradeError>
-
Parameters
contract_id— Contract address
-
Returns — Current WASM hash (32 bytes)
-
Errors
UpgradeError::ContractNotRegistered— Contract not registered
Check if a contract is registered.
is_registered(env: Env, contract_id: Address) -> bool
-
Parameters
contract_id— Contract address
-
Returns —
trueif registered,falseotherwise
Register a migration hook contract (admin only).
set_migration_hook(
caller: Address,
contract_id: Address,
hook_addr: Address
) -> Result<(), UpgradeError>
-
Parameters
caller— Must holdAdminrolecontract_id— Target contracthook_addr— Hook contract address
-
Authorization —
callermust holdAdminrole -
Hook Interface — Hook contract must implement:
pre_upgrade(env, old_version, new_version) -> bool— runs before WASM update; returnfalseto abortpost_upgrade(env, old_version, new_version)— runs after WASM update; for state migrations
-
Errors
UpgradeError::NotUpgrader— Caller doesn't hold Admin roleUpgradeError::ContractNotRegistered— Target contract not registered
-
Events Emitted
MigrationHookSet— Custom event with (contract_id, hook_addr, timestamp)
Get the migration hook address for a contract.
get_migration_hook(env: Env, contract_id: Address) -> Option<Address>
-
Parameters
contract_id— Contract address
-
Returns — Hook contract address or
Noneif not set
Create an upgrade proposal (upgrader only).
propose_upgrade(
caller: Address,
contract_id: Address,
new_wasm_hash: BytesN<32>,
new_version: u32,
note: String
) -> Result<u64, UpgradeError>
-
Parameters
caller— Must holdUpgraderrolecontract_id— Target contractnew_wasm_hash— New WASM hash (32 bytes)new_version— New version (must be > current)note— Optional migration note
-
Authorization —
callermust holdUpgraderrole -
Validation
new_version > current.versionnew_wasm_hash != current.wasm_hash- No pending upgrade already exists
-
Errors
UpgradeError::NotUpgrader— Caller doesn't hold Upgrader roleUpgradeError::ContractNotRegistered— Contract not registeredUpgradeError::NoChangeDetected— Version or hash unchangedUpgradeError::AlreadyPending— Upgrade already pending
-
Returns — Newly created proposal ID
-
Events Emitted
UpgradeProposed— Custom event with (proposal_id, contract_id, new_version, proposer, timestamp)
Execute an upgrade proposal (upgrader only).
execute_upgrade(
caller: Address,
proposal_id: u64
) -> Result<(), UpgradeError>
-
Parameters
caller— Must holdUpgraderroleproposal_id— ID of proposal to execute
-
Authorization —
callermust holdUpgraderrole -
Algorithm
- Validate proposal exists and is pending
- Call pre-upgrade migration hook (if present)
- Update registry with new version/hash
- Call post-upgrade migration hook (if present)
- Mark proposal executed
- Record in upgrade history
- Remove pending status
-
Errors
UpgradeError::NotUpgrader— Caller doesn't hold Upgrader roleUpgradeError::ProposalNotFound— Proposal doesn't existUpgradeError::AlreadyExecuted— Proposal already executedUpgradeError::ContractNotRegistered— Target contract not foundUpgradeError::MigrationHookFailed— Hook call failed or returned false
-
Events Emitted
UpgradeExecuted— Custom event with (proposal_id, executor, new_version, timestamp)
Get a proposal by ID.
get_proposal(env: Env, proposal_id: u64) -> Result<UpgradeProposal, UpgradeError>
-
Parameters
proposal_id— Proposal ID
-
Returns —
UpgradeProposal { id, contract_id, new_wasm_hash, new_version, note, proposer, executed, created_at, executed_at } -
Errors
UpgradeError::ProposalNotFound— Proposal doesn't exist
Get the pending proposal ID for a contract.
get_pending_proposal(env: Env, contract_id: Address) -> Option<u64>
-
Parameters
contract_id— Contract address
-
Returns — Pending proposal ID or
Noneif no upgrade pending
Get the upgrade status for a contract.
get_upgrade_status(env: Env, contract_id: Address) -> Result<UpgradeStatus, UpgradeError>
-
Parameters
contract_id— Contract address
-
Returns — One of:
UpgradeStatus::Current— No upgrade pendingUpgradeStatus::Pending(proposal_id)— Upgrade proposal pendingUpgradeStatus::Completed— Upgrade was recently executed
-
Errors
UpgradeError::ContractNotRegistered— Contract not registered
Cancel a pending upgrade proposal (proposer or admin).
cancel_proposal(
caller: Address,
proposal_id: u64
) -> Result<(), UpgradeError>
-
Parameters
caller— Must be proposer or adminproposal_id— ID of proposal to cancel
-
Authorization —
callermust be proposer or admin -
Errors
UpgradeError::ProposalNotFound— Proposal doesn't existUpgradeError::AlreadyExecuted— Cannot cancel executed proposalUpgradeError::NotUpgrader— Caller is not proposer or admin
-
State Changes — Removes pending status; proposal remains in history
Verify that an upgrade is authorized (called by target contract).
verify_upgrade_authorization(
caller: Address,
contract_id: Address,
wasm_hash: BytesN<32>
) -> Result<(), UpgradeError>
-
Parameters
caller— Caller initiating the upgrade (must be upgrader)contract_id— Target contractwasm_hash— WASM hash to verify
-
Authorization —
callermust holdUpgraderrole -
Validation
- Contract must be registered
- A proposal must exist for this contract
- Proposal's WASM hash must match the provided hash
-
Errors
UpgradeError::NotUpgrader— Caller not upgraderUpgradeError::ContractNotRegistered— Contract not registeredUpgradeError::ProposalNotFound— No pending proposalUpgradeError::NoChangeDetected— WASM hash mismatch
Get the upgrade history for a contract.
get_upgrade_history(env: Env, contract_id: Address) -> Result<Vec<UpgradeRecord>, UpgradeError>
-
Parameters
contract_id— Contract address
-
Returns — Vector of
UpgradeRecord { contract_id, old_version, new_version, old_wasm_hash, new_wasm_hash, executor, executed_at, note } -
Errors
UpgradeError::ContractNotRegistered— Contract not registered
Check if a caller can propose/execute upgrades.
can_upgrade(env: Env, caller: Address) -> bool
-
Parameters
caller— Address to check
-
Returns —
trueif caller holdsUpgraderrole,falseotherwise
Comprehensive NFT marketplace with fixed-price listings, English/Dutch auctions, offers, royalty enforcement, and multi-currency support.
Initialize the NFT marketplace (required before other calls; admin only).
initialize(
admin: Address,
platform_fee_bps: i128,
fee_recipient: Address,
bid_increment_bps: i128,
auto_extension_seconds: u64
) -> Result<(), MarketError>
-
Parameters
admin— Admin address with governance privilegesplatform_fee_bps— Platform fee in basis points (0-1,000; 0-10%)fee_recipient— Address receiving platform feesbid_increment_bps— Min bid increment for auctions (0-5,000; 0-50%)auto_extension_seconds— Auto-extend window for English auctions (≤ 3,600 sec)
-
Validation
- All basis point values must be within range
- Auto-extension must not exceed 1 hour
-
Errors
MarketError::InvalidArgument— Parameters out of range
-
Initial State — Empty collections, listings, auctions; no currencies whitelisted
-
Events Emitted
ModuleInitialized—("logging", "initialized")with module, version, caller, timestamp
Update the platform fee rate (admin only).
set_platform_fee(caller: Address, new_fee_bps: i128) -> Result<(), MarketError>
-
Authorization —
callermust be admin -
Validation — Fee must be 0-1,000 basis points
-
Errors
MarketError::Unauthorized— Caller not adminMarketError::InvalidArgument— Fee out of range
Update the fee recipient address (admin only).
set_fee_recipient(caller: Address, new_recipient: Address) -> Result<(), MarketError>
- Authorization —
callermust be admin
Update the minimum bid increment for auctions (admin only).
set_bid_increment(caller: Address, bps: i128) -> Result<(), MarketError>
-
Authorization —
callermust be admin -
Validation — Increment must be 0-5,000 basis points
Update the auto-extension window for English auctions (admin only).
set_auto_extension(caller: Address, seconds: u64) -> Result<(), MarketError>
-
Authorization —
callermust be admin -
Validation — Window must be ≤ 3,600 seconds (1 hour)
Whitelist or delist a payment currency (admin only).
set_currency(
caller: Address,
currency: Address,
whitelisted: bool
) -> Result<(), MarketError>
- Authorization —
callermust be admin
Configure price oracle address (admin only).
set_oracle(caller: Address, oracle: Address) -> Result<(), MarketError>
- Authorization —
callermust be admin
platform_fee_bps(env: Env) -> i128
fee_recipient_addr(env: Env) -> Address
bid_increment_bps(env: Env) -> i128
auto_extension_seconds(env: Env) -> u64
is_currency_whitelisted(env: Env, currency: Address) -> bool
oracle_address(env: Env) -> Option<Address>
Register an NFT collection with royalty config (admin only).
register_collection(
caller: Address,
collection: Address,
name: Symbol,
ipfs_uri: Bytes,
metadata_hash: Bytes,
standard: TokenStandard,
royalty_recipients: Vec<RoyaltyRecipient>
) -> Result<(), MarketError>
-
Parameters
caller— Must be admincollection— NFT collection contract addressname— Collection name (symbol)ipfs_uri— IPFS metadata URImetadata_hash— Metadata hash (32 bytes)standard—ERC721orERC1155royalty_recipients— List of (address, bps) tuples
-
Authorization —
callermust be admin -
Validation
- Collection not already registered
- Metadata hash must be 32 bytes
- Each royalty rate must be 0-1,000 basis points
- Max 10 royalty recipients
- Total royalties must not exceed 1,000 basis points
-
Errors
MarketError::CollectionAlreadyRegistered— Collection registeredMarketError::InvalidMetadataHash— Wrong hash lengthMarketError::InvalidRoyaltyRate— Rate out of rangeMarketError::InvalidArgument— Too many recipients
Update royalty config for a collection (collection admin only).
set_collection_royalties(
caller: Address,
collection: Address,
royalty_recipients: Vec<RoyaltyRecipient>
) -> Result<(), MarketError>
-
Authorization —
callermust be collection admin -
Errors
MarketError::Unauthorized— Caller not collection adminMarketError::CollectionNotFound— Collection not registeredMarketError::InvalidRoyaltyRate— Rate out of range
Get collection information.
get_collection(env: Env, collection: Address) -> Result<CollectionInfo, MarketError>
-
Returns — Collection metadata, royalty config, and active listing/auction counts
-
Errors
MarketError::CollectionNotFound— Collection not registered
Create a fixed-price listing for an NFT.
list_nft(
seller: Address,
collection: Address,
token_id: u64,
amount: u64,
price: i128,
currency: Address,
metadata_hash: Bytes
) -> Result<u64, MarketError>
-
Parameters
seller— Listing creator (must invoke)collection— NFT collection addresstoken_id— NFT token IDamount— NFT amount (≥ 1 for ERC1155)price— Price in currency units (must be > 0)currency— Payment currency (must be whitelisted)metadata_hash— Metadata hash (32 bytes)
-
Authorization —
sellermust invoke (require_auth()) -
Preconditions — Contract not paused
-
Token Transfer — Transfers NFT from seller to marketplace (escrow)
-
Returns — Newly created listing ID
-
Errors
MarketError::ContractPaused— Marketplace pausedMarketError::InvalidAmount— Price ≤ 0MarketError::CurrencyNotWhitelisted— Currency not allowedMarketError::InvalidMetadataHash— Hash wrong lengthMarketError::CollectionNotFound— Collection not registered
-
Events Emitted
NftListed— Custom event with (listing_id, seller, collection, token_id, price, currency, timestamp)
Purchase a listed NFT.
buy_nft(env: Env, buyer: Address, listing_id: u64) -> Result<(), MarketError>
-
Parameters
buyer— Purchaser (must invoke)listing_id— ID of listing to buy
-
Authorization —
buyermust invoke (require_auth()) -
Preconditions — Contract not paused; listing must be active
-
Payment Flow
- Calculate platform fee:
price × platform_fee_bps / 10,000 - Calculate royalties from collection config
- Calculate seller proceeds:
price - platform_fee - royalties - Transfer platform fee to fee_recipient
- Transfer royalties to each recipient
- Transfer seller proceeds to seller
- Transfer NFT to buyer
- Calculate platform fee:
-
Errors
MarketError::ListingNotFound— Listing doesn't existMarketError::ListingAlreadySold— Listing not activeMarketError::ContractPaused— Marketplace paused
-
State Changes — Listing marked
Sold; NFT transferred -
Events Emitted
NftSold— Custom event with (listing_id, seller, buyer, price, timestamp)
Cancel an active listing and refund NFT (seller only).
cancel_listing(env: Env, caller: Address, listing_id: u64) -> Result<(), MarketError>
-
Parameters
caller— Must be listing sellerlisting_id— ID of listing to cancel
-
Authorization —
callermust invoke (require_auth()) -
Errors
MarketError::ListingNotFound— Listing doesn't existMarketError::ListingAlreadySold— Listing not activeMarketError::NotOwner— Caller not seller
-
State Changes — Listing marked
Cancelled; NFT returned to seller
Get listing details.
get_listing(env: Env, listing_id: u64) -> Result<Listing, MarketError>
-
Returns — Listing metadata including seller, price, status, etc.
-
Errors
MarketError::ListingNotFound— Listing doesn't exist
Create an English auction.
create_english_auction(
seller: Address,
collection: Address,
token_id: u64,
amount: u64,
start_price: i128,
currency: Address,
duration_seconds: u64,
metadata_hash: Bytes
) -> Result<u64, MarketError>
-
Parameters
seller— Auction creator (must invoke)collection— NFT collectionstart_price— Opening bid (must be > 0)duration_seconds— Auction duration (1-604,800 seconds)- Other parameters similar to fixed-price listing
-
Authorization —
sellermust invoke -
Preconditions — Contract not paused
-
Auto-Extension — If a bid is placed within the auto-extension window before end time, auction extends by the window duration
-
Returns — Newly created auction ID
-
Errors — Similar to
list_nftplus duration validation
Place a bid on an English auction.
place_bid(
env: Env,
bidder: Address,
auction_id: u64,
bid_amount: i128
) -> Result<(), MarketError>
-
Parameters
bidder— Bidder address (must invoke)auction_id— Auction to bid onbid_amount— Bid price (must be ≥ start_price initially; then > current + increment)
-
Authorization —
biddermust invoke -
Preconditions
- Contract not paused
- Auction active (end_time not reached)
- Bid meets minimum increment
-
Bid Increment —
min_next_bid = current_bid × (1 + bid_increment_bps / 10,000) -
Errors
MarketError::AuctionEnded— Auction has endedMarketError::BidTooLow— Bid below minimumMarketError::BidderIsCurrentHighest— Already highest bidder
-
State Changes — Updates current bid and bidder; may extend end_time
-
Auto-Extension — If bid placed within auto_extension_window of end_time, extend end_time by window
Settle a completed English auction (payout).
settle_english_auction(env: Env, auction_id: u64) -> Result<(), MarketError>
-
Parameters
auction_id— Auction to settle
-
Preconditions — Auction ended; settlement period passed
-
Payment Flow — Similar to
buy_nft: platform fee, royalties, seller proceeds -
Errors
MarketError::AuctionNotYetEnded— Auction still active
Create a Dutch auction (price decay over time).
create_dutch_auction(
seller: Address,
collection: Address,
token_id: u64,
amount: u64,
start_price: i128,
floor_price: i128,
currency: Address,
duration_seconds: u64,
metadata_hash: Bytes
) -> Result<u64, MarketError>
-
Parameters
start_price— Initial pricefloor_price— Minimum price (must be < start_price)- Other parameters similar to English auction
-
Price Decay — Linear interpolation:
current_price = start_price - (start_price - floor_price) × (elapsed / duration) -
Errors — Similar to English auction plus
InvalidDutchAuctionPrices
Purchase from a Dutch auction at current price.
buy_dutch_auction(
env: Env,
buyer: Address,
auction_id: u64
) -> Result<(), MarketError>
-
Parameters
buyer— Purchaser (must invoke)auction_id— Auction to purchase from
-
Authorization —
buyermust invoke -
Current Price — Calculated based on elapsed time and price decay formula
-
Preconditions — Auction active; price > 0
-
Errors
MarketError::DutchAuctionPriceZero— Price reached zeroMarketError::AuctionEnded— Auction time expired
Make an offer for an NFT (off-chain or on-chain escrow).
make_offer(
offerer: Address,
recipient: Address,
collection: Address,
token_id: u64,
amount: u64,
offer_amount: i128,
currency: Address,
offer_nft_collection: Address,
offer_nft_token_id: u64,
expires_in_seconds: u64,
metadata_hash: Bytes
) -> Result<u64, MarketError>
-
Parameters
offerer— Creator of offer (must invoke)recipient— Intended recipientcollection/token_id— NFT being offered foroffer_amount— Offer pricecurrency— Payment currencyoffer_nft_collection/offer_nft_token_id— NFT offered (if counter-offer)expires_in_seconds— Offer duration
-
Authorization —
offerermust invoke -
Returns — Newly created offer ID
Accept an offer (recipient only).
accept_offer(env: Env, caller: Address, offer_id: u64) -> Result<(), MarketError>
-
Authorization —
callermust be recipient -
Preconditions
- Offer not expired
- Offer not already settled
-
Errors
MarketError::OfferExpired— Offer expiredMarketError::OfferAlreadySettled— Already accepted/cancelledMarketError::NotOfferRecipient— Caller not recipient
Cancel an offer (offerer only).
cancel_offer(env: Env, caller: Address, offer_id: u64) -> Result<(), MarketError>
- Authorization —
callermust be offerer or recipient (depending on implementation)
List multiple NFTs in a single transaction.
bulk_list_nfts(
env: Env,
seller: Address,
items: Vec<BulkListingItem>
) -> Result<BulkResult, MarketError>
-
Parameters — Vector of (collection, token_id, amount, price, currency, metadata_hash)
-
Returns —
BulkResult { succeeded, failed, ids: Vec<u64> } -
Max Items — 50 per call
Create multiple auctions in a single transaction.
bulk_create_auctions(
env: Env,
seller: Address,
items: Vec<BulkAuctionItem>
) -> Result<BulkResult, MarketError>
-
Parameters — Vector of auction specs
-
Max Items — 50 per call
Stub oracle contract for integration with external price feeds and verification services.
Initialize the oracle contract (required before other calls; admin only).
initialize(env: Env, admin: Address)
-
Parameters
admin— Admin address with governance privileges
-
Events Emitted
ModuleInitialized—("logging", "initialized")with module name, version, caller, timestamp
Note — This is a minimal stub contract. Production deployments should extend it with price feed aggregation, signature verification, and data validation logic. See the Trellis architecture documentation for oracle design patterns.
(Placeholder for rebalancer-contract documentation — not yet implemented in this codebase)
Contracts emit events for:
AidCreated
AidClaimed
AidSettled
AidRefunded
CommissionPaid
TreasuryDeposit
TreasuryWithdrawal
ContractPaused
ContractResumed
ContractUpgraded
ModuleInitialized
ActionExecuted
PermissionChanged
Off-chain indexers should match the stable two-part topic tuples and decode the typed payloads documented in shared/README.md.
This repository contains comprehensive documentation for contract design, gas optimization, security practices, and testing infrastructure.
- UPGRADEABILITY.md — System-wide upgrade registry and safe contract upgrade patterns with migration hooks and pre/post-upgrade validation.
- GAS_OPTIMIZATION.md — Gas optimization principles, applied optimizations, benchmark infrastructure, and a checklist for new features.
- SECURITY_BATCH.md — Security analysis of batch operations including reentrancy protection, input validation, and failure semantics.
- docs/IMPORT_PIPELINE.md — Bulk import pipeline with pre-flight dry-run validation, duplicate detection, and prescriptive rollback guidance.
- docs/PREFLIGHT.md — Deterministic pre-submission preflight for high-risk operations with ready/warning/blocked verdicts, remediation steps, and shared error mapping.
- shared/README.md — Shared contract library with error codes, reserved ranges, and event schemas used across all contracts.
- testing/README.md — Comprehensive testing framework with mocks, helpers, simulation tools, fuzzing harnesses, and examples.
- security/README.md — Security audit tooling, CI gating, and allowlist management for static analysis and vulnerability scanning.
- Cross-chain settlement
- Stellar Asset support
- Multi-token donations
- DAO governance
- Zero Knowledge verification
- On-chain reputation
- Human identity proofs
- Streaming donations
- Batch settlements
We welcome contributions from Rust and Soroban developers.
Read CONTRIBUTING.md for the full guide: local setup, build and test instructions, branch and commit conventions, and what reviewers look for.
Please note that this project is released with a Contributor Code of Conduct. By participating you agree to abide by its terms.
Workflow
-
Fork repository
-
Create feature branch
-
Write tests
-
Submit Pull Request
Every contract contribution must include:
-
Unit tests
-
Documentation
-
Security considerations
-
Gas optimization review
The Trellis mark, wordmark and palette live in brand/ — SVG, PNG and ICO
variants for light and dark backgrounds, plus clear-space and minimum-size rules.
| Vine | Amber | Ink | Paper |
|---|---|---|---|
#1C6B55 |
#E39A3C |
#14201C |
#F7F5F0 |
Vine is the structure, amber is the accent — one amber element per composition.
Licensed under the MIT License.
See LICENSE for details.
- Stellar
- Soroban
- Rust
- Open Source Community
Building transparent humanitarian infrastructure for everyone.