This document describes the authorization model for the SwiftRemit Soroban smart contract and records the findings of the security audit conducted against issue #937.
Production deployments must restrict inbound network access so that only the public
HTTP(S) entrypoint is reachable from the internet. Backend and database ports must
never be exposed publicly. These rules complement the Security Checklist in
SETUP_GUIDE.md.
| Port | Service | Allowed source | Notes |
|---|---|---|---|
| 443 | HTTPS (reverse proxy / load balancer) | 0.0.0.0/0, ::/0 |
Public entrypoint only |
| 80 | HTTP (redirect to HTTPS) | 0.0.0.0/0, ::/0 |
Redirect to 443; disable if TLS terminates upstream |
| 22 | SSH | Operator/VPN CIDR only | Never 0.0.0.0/0; prefer bastion or VPN |
| 3000 | Backend API | Private network / proxy only | Not publicly reachable |
| 5432 | PostgreSQL | Private network / app subnet only | Not publicly reachable |
- Inbound: deny by default; allow only the rules above.
- Outbound: allow established/related; restrict egress to required services.
- Bind backend and database listeners to private interfaces (e.g.
127.0.0.1or the private subnet address), not0.0.0.0. - Database access is limited to the backend application subnet/security group.
# Default deny inbound, allow outbound
ufw default deny incoming
ufw default allow outgoing
# Public web entrypoint
ufw allow 443/tcp
ufw allow 80/tcp
# SSH from operator network only
ufw allow from <OPERATOR_CIDR> to any port 22 proto tcp
# Backend API and PostgreSQL: private network only
ufw allow from <PRIVATE_SUBNET_CIDR> to any port 3000 proto tcp
ufw allow from <PRIVATE_SUBNET_CIDR> to any port 5432 proto tcp
ufw enableweb-sg: inbound 443/80 from0.0.0.0/0; inbound 22 from operator CIDR.api-sg: inbound 3000 fromweb-sgonly.db-sg: inbound 5432 fromapi-sgonly.
Verify after applying: nmap -Pn <host> from an external network must show only
ports 80/443 (and 22 if intentionally exposed).
An external security audit of the smart contract is required before mainnet deployment. No mainnet deploy proceeds until the re-audit passes.
| Document | Path |
|---|---|
| Audit scope | docs/audit/AUDIT_SCOPE.md |
| Contract architecture | docs/audit/ARCHITECTURE.md |
| Known issues (pre-disclosure) | docs/audit/KNOWN_ISSUES.md |
| Findings tracker | docs/audit/FINDINGS_TRACKER.md |
| Pre-engagement checklist | docs/audit/AUDIT_CHECKLIST.md |
The audit freeze CI workflow (.github/workflows/audit-freeze.yml) blocks new
pub fn additions to src/lib.rs without auditor agreement during the audit window.
See docs/THREAT_MODEL.md for the full STRIDE threat model
covering all system trust boundaries, adversary profiles, and residual risks.
The threat model is reviewed at every release.
| Role | Description |
|---|---|
| Admin | Can mutate global contract configuration, manage agents and admins, withdraw fees, pause/unpause, and configure limits. Multiple admins are supported via the add_admin / remove_admin functions. At least one admin must always remain. |
| Settler | Registered agent authorized to confirm (settle) remittance payouts. Granted automatically when an admin calls register_agent. |
| Sender | Any address that calls create_remittance; authenticated via require_auth on their own address. |
Every state-mutating function is listed below with its authorization mechanism.
Read-only (get_*, is_*, has_*) functions that never mutate state require no
caller authentication and are omitted.
| Function | Auth mechanism | Notes |
|---|---|---|
register_agent |
get_admin()? + require_admin() |
|
remove_agent |
get_admin()? + require_admin() |
|
update_fee |
get_admin()? + require_admin() |
|
withdraw_fees |
get_admin()? + require_admin() |
|
pause |
get_admin()? + require_admin() |
|
unpause |
get_admin()? + require_admin() |
|
add_admin |
require_admin(&env, &caller) |
caller supplied explicitly |
remove_admin |
require_admin(&env, &caller) |
caller supplied explicitly |
add_whitelisted_token |
get_admin()? + require_admin() |
|
remove_whitelisted_token |
get_admin()? + require_admin() |
|
update_rate_limit |
get_admin()? + admin.require_auth() |
|
set_daily_limit |
get_admin()? + admin.require_auth() |
|
update_rate_limit_config |
require_admin(&env, &caller) |
|
update_fee_strategy |
require_admin(&env, &caller) |
|
update_protocol_fee |
require_admin(&env, &caller) |
|
update_treasury |
require_admin(&env, &caller) |
|
set_asset_verification |
get_admin()? + admin.require_auth() |
|
set_fee_corridor |
see impl | delegated to fee_service module |
remove_fee_corridor |
see impl | delegated to fee_service module |
assign_role |
caller.require_auth() + require_role_admin() |
|
remove_role |
caller.require_auth() + require_role_admin() |
|
set_multisig_config |
require_admin(&env, &caller) |
new in #253 |
propose_operation |
require_admin() + proposer.require_auth() |
new in #253 |
approve_operation |
require_admin() + approver.require_auth() |
new in #253 |
export_migration_snapshot |
require_admin(&env, &caller) |
|
import_migration_batch |
require_admin(&env, &caller) |
|
blacklist_user |
delegates to set_blacklist_status → require_admin |
|
remove_from_blacklist |
delegates to set_blacklist_status → require_admin |
| Function | Auth mechanism | Notes |
|---|---|---|
create_remittance |
sender.require_auth() |
|
create_remittance_with_corridor |
sender.require_auth() |
|
batch_create_remittances |
sender.require_auth() |
|
cancel_remittance |
checks remittance.sender == caller |
|
create_escrow |
sender.require_auth() |
|
withdraw_integrator_fees |
integrator.require_auth() |
integrator only |
| Function | Auth mechanism | Notes |
|---|---|---|
confirm_payout |
checks is_agent_registered + agent.require_auth() |
|
finalize_remittance |
checks caller is agent or admin | |
mark_failed |
checks is_agent_registered |
|
batch_settle_with_netting |
checks is_agent_registered |
| Function | Notes |
|---|---|
expire_operation |
Anyone can sweep expired pending operations — no harm in public access |
process_expired_remittances |
Permissionless; only refunds expire-eligible records |
All get_* / is_* / has_* |
Read-only; no state mutation |
health |
Diagnostic only |
Per the Security Checklist in SETUP_GUIDE.md, every admin and security-sensitive
operation must emit a structured audit record. Audit records are emitted as Soroban
contract events so they are captured by the indexer and retained off-chain.
Each audit event carries the following fields:
| Field | Description |
|---|---|
actor |
Address that initiated the operation (the authenticated caller) |
action |
Stable action identifier (see table below) |
target |
Address, token, or config key affected by the operation |
timestamp |
Ledger timestamp at which the operation executed |
outcome |
success or failure (with error code when applicable) |
| Action | Trigger | Actor | Target |
|---|---|---|---|
admin.add |
add_admin |
Admin | New admin address |
admin.remove |
remove_admin |
Admin | Removed admin address |
agent.register |
register_agent |
Admin | Agent address |
agent.remove |
remove_agent |
Admin | Agent address |
role.assign |
assign_role |
Role admin | Target address |
role.remove |
remove_role |
Role admin | Target address |
config.fee.update |
update_fee, update_protocol_fee, update_fee_strategy |
Admin | Fee config key |
config.treasury.update |
update_treasury |
Admin | Treasury address |
config.limit.update |
set_daily_limit, update_rate_limit, update_rate_limit_config |
Admin | Limit config key |
config.whitelist.add |
add_whitelisted_token |
Admin | Token address |
config.whitelist.remove |
remove_whitelisted_token |
Admin | Token address |
config.asset_verification |
set_asset_verification |
Admin | Token address |
circuit_breaker.pause |
pause |
Admin | — |
circuit_breaker.unpause |
unpause |
Admin | — |
fees.withdraw |
withdraw_fees |
Admin | Destination address |
blacklist.add |
blacklist_user |
Admin | Target address |
blacklist.remove |
remove_from_blacklist |
Admin | Target address |
multisig.config |
set_multisig_config |
Admin | — |
multisig.propose |
propose_operation |
Admin | Operation id |
multisig.approve |
approve_operation |
Admin | Operation id |
multisig.execute |
threshold reached | — | Operation id |
multisig.expire |
expire_operation |
Any | Operation id |
migration.export |
export_migration_snapshot |
Admin | — |
migration.import |
import_migration_batch |
Admin | Batch id |
- Audit events are emitted after authorization succeeds and before the
state mutation is committed, so a failed authorization never produces a
successrecord. - Failed authorization attempts emit an audit record with
outcome = failureand the corresponding error code. - The multi-sig flow already emits
msig/proposed,msig/approved,msig/executed, andmsig/expiredevents; these are the canonical audit records for high-impact operations and must not be suppressed. - Audit logging must remain enabled in all environments, including testnet and mainnet; it is not gated behind a feature flag.
The following operations go through the M-of-N multi-sig flow rather than executing immediately on a single admin signature:
| Operation | AdminOperationType variant |
|---|---|
| Platform fee changes | UpdateFee |
| Fee withdrawal to external address | WithdrawFees |
| Emergency pause | Pause |
| Unpause | Unpause |
Flow:
- Any admin calls
propose_operation— creates aPendingOperation, emitsmsig/proposed, and auto-approves the proposer. - Additional admins call
approve_operation— emitsmsig/approvedper approval. - When
approvers.len() >= threshold, the operation executes and emitsmsig/executed. - Operations that do not reach threshold within
ttl_secondsexpire; anyone can callexpire_operationto emitmsig/expiredand clean up storage.
Defaults: threshold=1, TTL=86 400 s (24 h). Configure with set_multisig_config.
| Measure | Where implemented |
|---|---|
| Re-entrancy: Soroban VM is single-threaded; no callbacks during execution | SDK guarantee |
| Duplicate settlement prevention | SettlementData / SettlementPacked storage keys |
| Blacklist | UserBlacklisted storage key checked in create_remittance |
| Daily send limits | enforce_daily_send_limit called in every remittance creation path |
| Rate limiting | RateLimitConfig applied per address |
| Circuit breaker | pause / unpause block all user-facing state mutations |
| Migration guard | MigrationInProgress flag blocks concurrent writes during data migration |
| Token whitelist | Only whitelisted tokens accepted for new remittances |
| Admin count guard | CannotRemoveLastAdmin error prevents admin lockout |
| Network firewall | Production inbound rules restrict backend/database ports (see above) |
See docs/COMPLIANCE_CONTROLS.md for the full compliance control inventory mapping regulatory obligations to implemented controls.
Key controls:
- Sanctions and PEP screening:
backend/src/aml/sanctions-screening.ts - Transaction monitoring (structuring, velocity):
backend/src/aml/transaction-monitoring.ts - SAR workflow:
backend/src/aml/sar-workflow.ts - Travel rule data collection:
backend/src/aml/travel-rule.ts - Data retention:
backend/src/aml/retention.ts - Review queue API:
backend/src/routes/aml.ts
See docs/KEY_MANAGEMENT_POLICY.md for the admin key custody, rotation, and compromise response procedures (SR-111). All mainnet admin keys require h
/* … truncated 925 chars — edit only what you need near the top … */