Skip to content

Latest commit

 

History

57 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Guardian — Transaction Monitoring & Fraud Enforcement on Stellar

Guardian watches Stellar accounts and Soroban contract activity in real time, scores each transaction for fraud risk, and enforces graduated protective actions — from alerting, to requiring multi-sig approval, to freezing assets — using Stellar's native account/asset controls plus a custom Soroban policy contract. Every enforcement decision is recorded as an on-chain event, so the audit trail is verifiable independently of this software.

Network: Stellar Testnet only. Mainnet is out of scope until explicitly enabled and the pre-Mainnet checklist is satisfied.

Design principles

  1. A false positive that freezes legitimate funds is the worst-case failure. Enforcement defaults to manual human approval; automation is opt-in and gated behind a conservative confidence bar.
  2. No silent enforcement. Every state-changing action (freeze, unfreeze, policy change) emits a typed Soroban event — the on-chain audit trail is a core feature, not a log.
  3. Native controls first. Stellar multi-sig and AUTH_REQUIRED/AUTH_REVOCABLE asset flags are preferred over custom custody logic. The contract never holds funds.
  4. Fail safe on enforcement, fail open on detection. No component failure can produce an erroneous freeze or silently unfreeze; a listener outage only delays detection, and the cursor replays missed transactions on recovery. See failure modes.
  5. Decoupled layers. The rules engine and enforcement bridge evolve without contract redeploys.

How it works

                Stellar Testnet ledger
                        │  payments
                        ▼
  ┌─────────────────────────────────────────────────────────┐
  │  Horizon Listener  ──▶  Postgres  ◀──  Guardian Events   │
  │  (cursor-resumable)     (history,      Listener          │
  │                          policy mirror, (mirrors on-chain │
  │                          audit log)     status + events)  │
  └───────────────┬─────────────────────────────────────────┘
                  │ onTransaction
                  ▼
        Risk Scoring Engine  ──▶  score + tier (low / medium / high)
        (5 pluggable rules)
                  │
                  ▼
        Enforcement Bridge  ──┬─ low     → audit only
        (automation gate)     ├─ medium  → pending flag + alert
                              └─ high    → manual: pending flag + alert
                                           notify (≥95): auto-freeze + alert
                  │ approve                         │ flag_and_freeze
                  ▼                                 ▼
        Dashboard (Next.js) ◀── REST API ──▶ Guardian Contract (Soroban)
        review queue, policy,                set_policy / check_transaction /
        audit log, Freighter                 flag_and_freeze / unfreeze / events

End-to-end flow: a payment settles on-ledger → the listener ingests it and updates the counterparty graph → the scoring engine assigns a risk score and tier → medium/high raises a flag and an alert → a human approves in the dashboard (or, for notify accounts at very high confidence, the gate auto-freezes) → the contract's flag_and_freeze runs and emits events → a frozen account is released only by a multi-sig unfreeze from its configured approvers.

Repository layout

contracts/guardian/   Soroban policy contract (Rust) — policy, freeze/unfreeze, events
backend/              TypeScript service:
  src/listener/         Horizon + Soroban RPC listeners (cursor-resumable)
  src/scoring/          pluggable rules engine + context builder
  src/enforcement/      tier routing, automation gate, review service, contract client
  src/api/              REST API (Express)
  src/db/               Postgres schema, migrations, repositories
dashboard/            Next.js console — review queue, policy config, audit log, Freighter
docs/                 threat-model.md, security-review.md, deployments/

Risk scoring

Five heuristics each contribute a weighted score; the total is capped at 100 and mapped to a tier. The engine is pluggable — new rules are added without touching the pipeline, and a broken rule is skipped rather than crashing ingestion.

Rule Fires when Score
Denylist Counterparty is on the maintained denylist 100
Size anomaly Amount ≥ 3× the account's historical p95 (needs ≥5 tx of history) 25–40
Velocity > 10 transactions in a 5-minute rolling window 25–40
Structuring ≥ 3 transactions within 10% below the policy limit in 1 hour 40
New counterparty First-ever transaction with this counterparty 15

Tiers: low < 40 → audit only · medium 40–69 → flag + alert · high ≥ 70 → flag + alert (+ auto-freeze only if the account is notify and score ≥ 95).

Prerequisites

  • Rust (stable, 1.84+) with the wasm32v1-none target: rustup target add wasm32v1-none
  • stellar-cli: cargo install --locked stellar-cli
  • Node.js ≥ 20
  • PostgreSQL ≥ 14

Local setup

# 1. Contracts — build and test
cd contracts
cargo test                                     # 27 tests
cargo build --target wasm32v1-none --release   # produces guardian.wasm

# 2. Database
createdb guardian                              # or: psql -c 'CREATE DATABASE guardian'

# 3. Backend
cd ../backend
npm install
cp .env.example .env                           # then edit (see Configuration below)
npm run migrate                                 # apply schema
npm test                                        # 76 tests (needs a reachable Postgres)
npm run dev                                      # API on :4000 + listener/scoring loop

# 4. Dashboard
cd ../dashboard
npm install
echo "NEXT_PUBLIC_API_URL=http://localhost:4000" > .env.local
npm run dev                                      # console on :3000

The backend must have DASHBOARD_ORIGIN set to the dashboard's origin (e.g. http://localhost:3000) or the browser's CORS preflight will block API calls.

Configuration

Backend environment (backend/.env, see .env.example):

Variable Required Purpose
DATABASE_URL yes Postgres connection string
GUARDIAN_CONTRACT_ID yes Deployed Guardian contract (C…)
HORIZON_URL Horizon endpoint (default: Testnet)
SOROBAN_RPC_URL Soroban RPC endpoint (default: Testnet)
NETWORK_PASSPHRASE Network passphrase (Mainnet is refused at startup)
ENFORCEMENT_SECRET_KEY for freezes Admin key that signs flag_and_freeze; never commit it
DASHBOARD_ORIGIN for the UI Exact browser origin allowed via CORS
ALERT_WEBHOOK_URL Webhook for high-risk alerts
POLL_INTERVAL_MS Listener poll cadence (default 5000)
PORT API port (default 4000)

Dashboard: NEXT_PUBLIC_API_URL (backend base URL).

Deploying the Guardian contract to Testnet

# One-time: create and fund a deployer/admin identity via Friendbot
stellar keys generate guardian-deployer --network testnet --fund

cd contracts
cargo build --target wasm32v1-none --release
stellar contract deploy \
  --wasm target/wasm32v1-none/release/guardian.wasm \
  --source guardian-deployer --network testnet \
  -- --admin "$(stellar keys address guardian-deployer)"

The --admin constructor argument sets the enforcement-service key (the only key that may freeze). The printed contract ID goes into backend/.env as GUARDIAN_CONTRACT_ID; the admin secret goes into ENFORCEMENT_SECRET_KEY. See docs/deployments/testnet.md for the current deployment.

REST API

Base URL http://localhost:4000. All responses use the envelope { success, data, error }.

Method Path Description
GET /health Liveness check
GET /accounts List monitored accounts
POST /accounts Start monitoring { address, label? }
DELETE /accounts/:address Stop monitoring
POST /accounts/:address/automation Set automation { level: "manual" | "notify" }
POST /accounts/:address/request-review Fast-track appeal { reviewer, note? }
GET /accounts/:address/transactions Recent transactions (?limit)
GET /accounts/:address/policy Mirrored policy + live on-chain status
GET /flags Review queue (?status=pending|approved|dismissed)
POST /flags/:id/approve Approve → on-chain freeze { reviewer, note? }
POST /flags/:id/dismiss Dismiss as false positive { reviewer, note? }
POST /denylist Add counterparty { address, reason }
DELETE /denylist/:address Remove from denylist
GET /audit Audit trail, newest first (?limit)

Approving a flag is the only path from the API to an on-chain freeze, and it is fully audited.

Guardian contract interface

set_policy · check_transaction (read-only) · flag_and_freeze (admin only) · unfreeze (multi-sig) · get_policy / get_status / get_flag / get_admin. Events: policy_set, account_flagged, account_frozen, account_unfrozen.

Testing

cd contracts && cargo test          # 27 contract tests
cd backend   && npm test            # 76 tests (repos, listeners, scoring, enforcement, API)

Backend tests run against a real Postgres (TEST_DATABASE_URL, default a guardian_test database), created automatically on first run.

Security

  • Threat model — assets, trust boundaries, threats/mitigations, and explicit failure modes (fail-open detection, fail-safe enforcement).
  • Security review — including GRD-1 (HIGH, fixed): a frozen account's owner could rewrite its own unfreeze policy and self-release; the contract now blocks non-admin policy edits while frozen.

Secrets are never committed: the enforcement key lives only in local stellar-cli identities and .env (gitignored). A Mainnet passphrase is rejected at backend startup.

Build status

Phase Scope Status
0 Monorepo scaffold, CI, Testnet accounts
1 Guardian Soroban contract + tests + Testnet deploy
2 Off-chain listener + Postgres storage
3 Rule-based risk scoring engine
4 Enforcement bridge (manual approval first)
5 Dashboard
6 Gated automation
7 Hardening, threat model, Mainnet pilot plan

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages