From 76f210e5857cd9099ce87d34605822bdd723806d Mon Sep 17 00:00:00 2001 From: boalambo Date: Mon, 24 Aug 2026 22:40:19 +0100 Subject: [PATCH] docs: add per-contract READMEs for six Stellar crates (#167) --- stellar/README.md | 14 +- stellar/contracts/governance/README.md | 297 +++++++++++++++++++++++++ stellar/stealth-announcer/README.md | 85 +++++++ stellar/stealth-batch-sender/README.md | 114 ++++++++++ stellar/stealth-vault/README.md | 193 ++++++++++++++++ stellar/wraith-asset-policy/README.md | 135 +++++++++++ stellar/wraith-metrics/README.md | 146 ++++++++++++ 7 files changed, 980 insertions(+), 4 deletions(-) create mode 100644 stellar/contracts/governance/README.md create mode 100644 stellar/stealth-announcer/README.md create mode 100644 stellar/stealth-batch-sender/README.md create mode 100644 stellar/stealth-vault/README.md create mode 100644 stellar/wraith-asset-policy/README.md create mode 100644 stellar/wraith-metrics/README.md diff --git a/stellar/README.md b/stellar/README.md index 678388c..4b36e8b 100644 --- a/stellar/README.md +++ b/stellar/README.md @@ -4,10 +4,16 @@ This directory contains the Soroban smart contracts for the Wraith multichain st ## Contracts -- `stealth-announcer`: Emits announcement events for stealth payments. -- `stealth-registry`: Maps addresses to 64-byte stealth meta-addresses. -- `stealth-sender`: Handles atomic transfers and announcements. -- `wraith-names`: Privacy-preserving name registry for `.wraith` names. +- `stealth-announcer`: Emits announcement events for stealth payments. [README](./stealth-announcer/README.md) +- `stealth-batch-sender`: Atomically sends tokens to multiple stealth addresses in a single transaction. [README](./stealth-batch-sender/README.md) +- `stealth-registry`: Maps addresses to 64-byte stealth meta-addresses. [README](./stealth-registry/README.md) +- `stealth-sender`: Handles atomic transfers and announcements. [README](./stealth-sender/README.md) +- `stealth-vault`: Time-locked vault for stealth payments with refund safety net. [README](./stealth-vault/README.md) +- `stealth-splitter`: 1-to-N stealth payment splitter. [README](./stealth-splitter/README.md) +- `wraith-asset-policy`: Admin-controlled asset allowlist for stealth payments. [README](./wraith-asset-policy/README.md) +- `wraith-metrics`: Shared metrics library for standardized event emission. [README](./wraith-metrics/README.md) +- `wraith-names`: Privacy-preserving name registry for `.wraith` names. [README](./wraith-names/README.md) +- `contracts/governance`: Token-weighted governance (PoC, not production ready). [README](./contracts/governance/README.md) ## Prerequisites diff --git a/stellar/contracts/governance/README.md b/stellar/contracts/governance/README.md new file mode 100644 index 0000000..ece9123 --- /dev/null +++ b/stellar/contracts/governance/README.md @@ -0,0 +1,297 @@ +# Governance Contract (`contracts/governance`) + +**⚠️ THIS IS NOT PRODUCTION READY — PROOF OF CONCEPT ONLY** + +This governance contract is a proof of concept for token-weighted on-chain governance. See [GOVERNANCE.md](../../GOVERNANCE.md) for design decisions, known limitations, and the upgrade path to a production-grade system. + +## Purpose + +Token-weighted governance for protocol upgrades and parameter changes. This PoC implements a basic propose → vote → execute flow with quorum requirements and timelock delays. + +## Flow + +1. **Propose** — Anyone with a token balance creates a proposal describing an action +2. **Vote** — Token holders vote for or against during a fixed voting window +3. **Execute** — After voting ends + timelock delay, anyone can execute if: + - Total votes >= quorum (absolute token threshold) + - `for_votes > against_votes` + +## Entrypoints + +| Function | Description | Authorization | +|----------|-------------|----------------| +| `init(env, admin, token, quorum, voting_period, timelock)` | Initialize governance contract | None (one-time initialization) | +| `get_config(env)` | Return current governance configuration | None (read-only) | +| `propose(env, proposer, target, function, args, description)` | Create a new governance proposal | `proposer` must authorize | +| `get_proposal(env, proposal_id)` | Return a proposal by ID | None (read-only) | +| `vote(env, voter, proposal_id, support)` | Cast a vote on an active proposal | `voter` must authorize | +| `get_vote(env, proposal_id, voter)` | Return the vote record for a voter on a proposal | None (read-only) | +| `execute(env, proposal_id)` | Execute a proposal that has passed | None (permissionless) | +| `cancel(env, proposal_id)` | Cancel a proposal | Admin or permissionless (see rules) | + +### `init` + +Initialize the governance contract. + +**Parameters:** +- `admin: Address` — Address with super-admin powers (PoC only) +- `token: Address` — SAC token used for voting weight +- `quorum: i128` — Absolute minimum total tokens required for a valid vote +- `voting_period: u32` — Duration of voting window in ledgers +- `timelock: u32` — Delay after voting closes before execution (ledgers) + +**Returns:** `Result<(), GovernanceError>` + +**Errors:** +- `AlreadyInitialized` — Contract already initialized + +### `get_config` + +Return the current governance configuration. + +**Returns:** `Result` + +**GovernanceConfig struct:** +```rust +pub struct GovernanceConfig { + pub token: Address, + pub quorum: i128, + pub voting_period: u32, + pub timelock: u32, +} +``` + +### `propose` + +Create a new governance proposal. + +**Parameters:** +- `proposer: Address` — Address creating the proposal (must authorize) +- `target: Address` — Contract to call on execution +- `function: Symbol` — Function name to invoke on the target +- `args: Bytes` — Raw argument bytes forwarded to the target +- `description: String` — Human-readable proposal description + +**Returns:** `Result` — The new proposal ID + +**Events emitted:** +- `("propose", proposal_id)` with `(proposer, description)` + +**Errors:** +- `NotInitialized` — Contract not initialized + +### `get_proposal` + +Return a proposal by ID. + +**Parameters:** +- `proposal_id: u32` — Proposal ID + +**Returns:** `Result` + +**Proposal struct:** +```rust +pub struct Proposal { + pub id: u32, + pub proposer: Address, + pub target: Address, + pub function: Symbol, + pub args: Bytes, + pub description: String, + pub start_ledger: u32, + pub end_ledger: u32, + pub for_votes: i128, + pub against_votes: i128, + pub executed: bool, + pub cancelled: bool, +} +``` + +### `vote` + +Cast a vote on an active proposal. + +**Parameters:** +- `voter: Address` — Address casting the vote (must authorize) +- `proposal_id: u32` — Target proposal +- `support: bool` — `true` = for, `false` = against + +**Returns:** `Result<(), GovernanceError>` + +**Voting weight:** Equals the voter's token balance at the time the vote is cast. Each address may vote once per proposal. + +**Events emitted:** +- `("vote", proposal_id)` with `(voter, support, balance)` + +**Errors:** +- `ProposalNotFound` — Proposal does not exist +- `AlreadyExecuted` — Proposal already executed +- `AlreadyCancelled` — Proposal already cancelled +- `VotingNotActive` — Not within voting window +- `AlreadyVoted` — Address already voted on this proposal +- `NoVotingPower` — Voter has zero token balance + +### `get_vote` + +Return the vote record for a given voter on a proposal. + +**Parameters:** +- `proposal_id: u32` — Proposal ID +- `voter: Address` — Voter address + +**Returns:** `Result` + +**Vote struct:** +```rust +pub struct Vote { + pub support: bool, // true = for, false = against + pub weight: i128, // Voting weight (token balance at vote time) +} +``` + +### `execute` + +Execute a proposal that has passed. + +**Conditions (all must hold):** +1. Voting window has closed +2. Timelock delay has elapsed since voting closed +3. Total votes cast >= quorum +4. `for_votes > against_votes` +5. Not already executed or cancelled + +**Parameters:** +- `proposal_id: u32` — Proposal ID + +**Returns:** `Result<(), GovernanceError>` + +**Events emitted:** +- `("execute", proposal_id)` with `()` + +**Errors:** +- `ProposalNotFound` — Proposal does not exist +- `AlreadyExecuted` — Proposal already executed +- `AlreadyCancelled` — Proposal already cancelled +- `VotingStillActive` — Voting window still open +- `TimelockNotElapsed` — Timelock delay has not elapsed +- `QuorumNotMet` — Total votes < quorum +- `ProposalDefeated` — `for_votes <= against_votes` +- `ExecutionFailed` — Target contract call failed + +### `cancel` + +Cancel a proposal. + +**Rules (PoC):** +- During voting: only the admin may cancel +- After voting, without quorum: anyone may cancel (failed proposal) +- After voting, with quorum: only the admin may cancel (emergency override — a production system would remove this power) + +**Parameters:** +- `proposal_id: u32` — Proposal ID + +**Returns:** `Result<(), GovernanceError>` + +**Events emitted:** +- `("cancel", proposal_id)` with `()` + +**Errors:** +- `ProposalNotFound` — Proposal does not exist +- `AlreadyExecuted` — Proposal already executed +- `AlreadyCancelled` — Proposal already cancelled + +## Error Variants + +| Error Code | Description | +|------------|-------------| +| `AlreadyInitialized = 1` | Contract already initialized | +| `NotInitialized = 2` | Contract not initialized | +| `NotAdmin = 3` | Caller is not admin | +| `ProposalNotFound = 4` | Proposal does not exist | +| `AlreadyVoted = 5` | Address already voted on this proposal | +| `VotingNotActive = 6` | Not within voting window | +| `VotingStillActive = 7` | Voting window still open | +| `QuorumNotMet = 8` | Total votes < quorum | +| `ProposalDefeated = 9` | `for_votes <= against_votes` | +| `TimelockNotElapsed = 10` | Timelock delay has not elapsed | +| `AlreadyExecuted = 11` | Proposal already executed | +| `AlreadyCancelled = 12` | Proposal already cancelled | +| `ExecutionFailed = 13` | Target contract call failed | +| `NoVotingPower = 14` | Voter has zero token balance | + +## Event Topics + +| Topic | Data | Description | +|-------|------|-------------| +| `("propose", proposal_id)` | `(proposer, description)` | Proposal created | +| `("vote", proposal_id)` | `(voter, support, balance)` | Vote cast | +| `("execute", proposal_id)` | `()` | Proposal executed | +| `("cancel", proposal_id)` | `()` | Proposal cancelled | + +## Storage Layout + +### Instance Storage +- `DataKey::Admin: Address` — Admin address +- `DataKey::Token: Address` — Voting token address +- `DataKey::Quorum: i128` — Quorum threshold +- `DataKey::VotingPeriod: u32` — Voting period duration +- `DataKey::Timelock: u32` — Timelock delay +- `DataKey::NextProposalId: u32` — Next proposal ID counter +- `DataKey::Proposal(proposal_id): Proposal` — Proposal entries + +### Persistent Storage +- `DataKey::Vote(proposal_id, voter): Vote` — Individual vote records + +**TTL Strategy:** +- Instance storage: Extended to `TTL_EXTEND_TO` (518400 ledgers, ~30 days) on every write +- Vote storage: Extended to `TTL_EXTEND_TO` on creation + +## Pause / Admin / Metrics Posture + +| Feature | Status | +|---------|--------| +| Pausable | No — no pause mechanism implemented | +| Admin | Yes — admin has super-admin powers (PoC only, should be removed in production) | +| Metrics | No — no metric events emitted | + +## Related Docs + +- [PAUSE.md](../../PAUSE.md) — Pause posture documentation +- [MULTISIG.md](../../MULTISIG.md) — Multisig setup documentation (for admin key) +- [METRICS.md](../../METRICS.md) — Metrics standard documentation +- [GOVERNANCE.md](../../GOVERNANCE.md) — Full governance design documentation with production upgrade path + +## Constants + +- `TTL_THRESHOLD: u32 = 17280` — ~1 day, TTL extension threshold +- `TTL_EXTEND_TO: u32 = 518400` — ~30 days, TTL extension target + +## Known Limitations (PoC) + +This is a proof of concept with known limitations: +- Admin has emergency cancel power even after quorum is met (production should remove this) +- No delegation mechanism +- No vote replay protection across upgrades +- Raw Bytes args require manual encoding/decoding (production would use structured types) +- No proposal types or validation (any target/function can be called) + +See [GOVERNANCE.md](../../GOVERNANCE.md) for the full list and upgrade path. + +## Testing + +```bash +cargo test -p governance +``` + +Tests cover: +- Happy path: propose → vote → execute +- Failed quorum cancellation +- Proposal defeat (majority against) +- Double-vote rejection +- Voting window enforcement +- No voting power rejection +- Vote record retrieval +- Admin cancel during voting window +- Timelock enforcement +- Config retrieval +- Double-init rejection diff --git a/stellar/stealth-announcer/README.md b/stellar/stealth-announcer/README.md new file mode 100644 index 0000000..7ef0ed0 --- /dev/null +++ b/stellar/stealth-announcer/README.md @@ -0,0 +1,85 @@ +# Stealth Announcer Contract (`stealth-announcer`) + +**⚠️ THIS CONTRACT IS DELIBERATELY FROZEN** — The v2 announcer deployment is a stable, stateless event emitter. No state changes are possible. + +The `stealth-announcer` contract emits stealth address announcement events on Soroban. It is a pure event-emission function with no access control and no storage. Indexers watch for these events to let recipients detect incoming payments. + +## Purpose + +Emits v2 stealth address announcement events with bucketed view tags for efficient RPC filtering. The announcer is called by `stealth-sender` and `stealth-vault` after transfers to notify recipients. + +## Entrypoints + +| Function | Description | Authorization | +|----------|-------------|----------------| +| `announce(env, scheme_id, stealth_address, ephemeral_pub_key, metadata)` | Emits a v2 stealth address announcement event | None (permissionless) | + +### `announce` + +Emits a Stellar v2 stealth address announcement event. + +**Parameters:** +- `scheme_id: u32` — Must be `2` for the v2 Stellar announcer deployment +- `stealth_address: Address` — The one-time stealth address that received funds +- `ephemeral_pub_key: BytesN<32>` — The ephemeral public key used to derive the stealth address +- `metadata: Bytes` — Non-empty metadata whose first byte is the view tag + +**Returns:** None + +**v2 event shape:** +- Topics: `("announce", scheme_id, view_tag_bucket, metadata_kind)` +- Data: `(stealth_address, ephemeral_pub_key, metadata)` + +The stable `view_tag_bucket` derivation is `metadata[0] as u32`, where `metadata_kind = 1` (`METADATA_KIND_VIEW_TAG`) means the first metadata byte is the view tag and the remaining bytes are scheme-specific. This lets wallets and indexers filter Stellar RPC `getEvents` by scheme and bucket before doing client-side cryptographic validation. + +**Migration note:** v1 announcements used the old Stellar layout `("announce", scheme_id, stealth_address)` with `(caller, ephemeral_pub_key, metadata)`. Do not reinterpret historical v1 events as v2. The compatibility path is a new announcer deployment using `scheme_id = 2`. + +## Error Variants + +The contract uses panics for validation (no custom error enum): + +| Condition | Panic Reason | +|-----------|--------------| +| `scheme_id != 2` | Assertion failure (v1 scheme rejected) | +| Empty `metadata` | `metadata.get(0)` panic (view tag required) | + +## Event Topics + +| Topic | Data | Description | +|-------|------|-------------| +| `("announce", scheme_id, view_tag_bucket, metadata_kind)` | `(stealth_address, ephemeral_pub_key, metadata)` | V2 stealth payment announcement | + +## Storage Layout + +**None** — This contract is stateless and uses no persistent storage. + +## Pause / Admin / Metrics Posture + +| Feature | Status | +|---------|--------| +| Pausable | No — stateless event emitter, nothing to pause | +| Admin | No — no admin controls | +| Metrics | No — no metric events emitted | + +## Related Docs + +- [PAUSE.md](../PAUSE.md) — Pause posture documentation +- [MULTISIG.md](../MULTISIG.md) — Multisig setup documentation +- [METRICS.md](../METRICS.md) — Metrics standard documentation + +## Constants + +- `STELLAR_V2_SCHEME_ID: u32 = 2` — The v2 Stellar deployment scheme ID +- `METADATA_KIND_VIEW_TAG: u32 = 1` — Initial metadata kind for v2 announcements + +## Testing + +```bash +cargo test -p stealth-announcer +``` + +Tests cover: +- Event emission with correct topic and data structure +- View tag bucket derivation from first metadata byte +- Rejection of v1 scheme ID +- Rejection of missing view tag (empty metadata) diff --git a/stellar/stealth-batch-sender/README.md b/stellar/stealth-batch-sender/README.md new file mode 100644 index 0000000..f34f976 --- /dev/null +++ b/stellar/stealth-batch-sender/README.md @@ -0,0 +1,114 @@ +# Stealth Batch Sender Contract (`stealth-batch-sender`) + +The `stealth-batch-sender` contract atomically sends tokens from a single sender to multiple pre-computed stealth addresses in a single transaction. This provides ~100x efficiency over N individual `stealth-sender::send` calls by requiring only one authorization and one ledger round-trip. + +## Purpose + +Batch stealth transfers for efficiency. Instead of calling `stealth-sender::send` N times (N transactions, N auth signatures, N ledger round-trips), a single `batch_send` handles all transfers atomically in one transaction. + +## Entrypoints + +| Function | Description | Authorization | +|----------|-------------|----------------| +| `batch_send(env, from, transfers, asset)` | Atomically send `asset` tokens to N stealth addresses | `from` must authorize | +| `max_batch_size(env)` | Query the maximum allowed batch size | None (read-only) | + +### `batch_send` + +Atomically send `asset` tokens from `from` to N pre-computed stealth addresses in a single transaction. + +**All-or-nothing semantics:** Soroban's transaction model guarantees atomicity. If any individual transfer panics (e.g., insufficient balance mid-batch), the entire transaction is rolled back. No partial sends are possible. + +**Parameters:** +- `from: Address` — Sender address (must authorize) +- `transfers: Vec` — Array of stealth transfers +- `asset: Address` — Token contract address + +**Returns:** None + +**Transfer struct:** +```rust +pub struct Transfer { + pub stealth_address: Address, // Pre-computed stealth address (recipient) + pub ephemeral_pub_key: Bytes, // Ephemeral public key for recipient scanning + pub amount: i128, // Token amount (in asset's base unit) +} +``` + +**Validation:** +- Batch must contain at least 1 transfer +- Batch size cannot exceed `MAX_BATCH_SIZE` (100) +- Each transfer amount must be positive +- Each `ephemeral_pub_key` must not be empty + +**Events emitted:** +- Per-transfer: `("ANNOUNCE",)` with `(stealth_address, ephemeral_pub_key, amount, asset)` +- Batch summary: `("BATCH",)` with `(from, count, asset)` +- Metric events: `batch_send_count`, `batch_send_volume`, `batch_size` (see [METRICS.md](../METRICS.md)) + +### `max_batch_size` + +Query the maximum allowed batch size. + +**Returns:** `u32` — Current `MAX_BATCH_SIZE` constant (100) + +## Error Variants + +The contract uses panics for validation (no custom error enum): + +| Condition | Panic Reason | +|-----------|--------------| +| Empty `transfers` array | "batch must contain at least one transfer" | +| `transfers.len() > MAX_BATCH_SIZE` | "batch exceeds MAX_BATCH_SIZE" | +| `transfer.amount <= 0` | "transfer amount must be positive" | +| `transfer.ephemeral_pub_key.is_empty()` | "ephemeral_pub_key must not be empty" | + +## Event Topics + +| Topic | Data | Description | +|-------|------|-------------| +| `("ANNOUNCE",)` | `(stealth_address, ephemeral_pub_key, amount, asset)` | Per-transfer stealth payment announcement | +| `("BATCH",)` | `(from, count, asset)` | Batch-level summary event | +| `("metric", contract_id, metric_name)` | `(value, dimensions)` | Metric events (see [METRICS.md](../METRICS.md)) | + +## Storage Layout + +**None** — This contract is stateless and uses no persistent storage. + +## Pause / Admin / Metrics Posture + +| Feature | Status | +|---------|--------| +| Pausable | No — stateless, nothing to pause | +| Admin | No — no admin controls | +| Metrics | Yes — emits `batch_send_count`, `batch_send_volume`, `batch_size` metrics | + +## Related Docs + +- [PAUSE.md](../PAUSE.md) — Pause posture documentation +- [MULTISIG.md](../MULTISIG.md) — Multisig setup documentation +- [METRICS.md](../METRICS.md) — Metrics standard documentation + +## Constants + +- `MAX_BATCH_SIZE: u32 = 100` — Maximum transfers per batch, justified against Soroban's ~100M instruction budget. Each transfer costs ~500K instructions (token transfer + event emit). 100 transfers = ~50M instructions, leaving headroom for overhead. + +## Resource Budget + +- **Instruction usage:** ~500K instructions per transfer (token transfer + event emit) +- **Max batch:** 100 transfers = ~50M instructions (under Soroban's ~100M limit) +- **Efficiency gain:** ~100x vs N individual stealth-sender calls (1 auth vs N auths, 1 ledger round-trip vs N) + +## Testing + +```bash +cargo test -p stealth-batch-sender +``` + +Tests cover: +- Batch send with multiple transfers +- Empty batch rejection +- Batch size limit enforcement +- Positive amount validation +- Non-empty ephemeral_pub_key validation +- Metric event emission diff --git a/stellar/stealth-vault/README.md b/stellar/stealth-vault/README.md new file mode 100644 index 0000000..ecf497f --- /dev/null +++ b/stellar/stealth-vault/README.md @@ -0,0 +1,193 @@ +# Stealth Vault Contract (`stealth-vault`) + +The `stealth-vault` contract implements a time-locked vault for stealth payments. Senders can deposit tokens for a recipient with a time-locked release window. The recipient can claim after the unlock ledger, and the sender can refund after a refund ledger if unclaimed. + +## Purpose + +Time-locked stealth payments with a refund safety net. Enables scenarios like: +- Scheduled payments (release at a specific future ledger) +- Escrow-style transactions (recipient has a window to claim, otherwise refund) +- Privacy-preserving time-locked transfers + +## Entrypoints + +| Function | Description | Authorization | +|----------|-------------|----------------| +| `init(env, announcer)` | Initialize the vault with an announcer address | None (one-time initialization) | +| `deposit(env, sender, recipient, amount, asset, unlock_ledger, refund_after, ephemeral_pub_key)` | Deposit tokens for a recipient with time-lock | `sender` must authorize | +| `claim(env, deposit_id, recipient)` | Claim a deposit after unlock time | `recipient` must authorize | +| `refund(env, deposit_id)` | Refund a deposit after refund window | `sender` must authorize | + +### `init` + +Initialize the vault with an announcer address. + +**Parameters:** +- `announcer: Address` — The stealth announcer contract address + +**Returns:** `Result<(), VaultError>` + +**Errors:** +- `AlreadyInitialized` — Contract already initialized + +### `deposit` + +Deposit tokens for a recipient with a time-lock window. + +**Parameters:** +- `sender: Address` — Sender address (must authorize) +- `recipient: Address` — Recipient stealth address +- `amount: i128` — Token amount to deposit +- `asset: Address` — Token contract address +- `unlock_ledger: u32` — Ledger number when recipient can claim +- `refund_after: u32` — Ledger number when sender can refund (must be > unlock_ledger + GRACE_PERIOD) +- `ephemeral_pub_key: BytesN<32>` — Ephemeral public key for recipient scanning + +**Returns:** `Result, VaultError>` — The deposit ID (SHA-256 hash of deposit parameters) + +**Validation:** +- `refund_after > unlock_ledger + GRACE_PERIOD` (1000 ledgers minimum grace period) +- Contract must be initialized + +**Events emitted:** +- `("deposit", deposit_id)` with `(sender, amount, asset, unlock_ledger)` +- Announcement event via announcer contract (scheme_id=1, metadata=[view_tag]) + +**Errors:** +- `NotInitialized` — Contract not initialized +- `InvalidWindow` — `refund_after` is not > `unlock_ledger + GRACE_PERIOD` + +### `claim` + +Claim a deposit after the unlock ledger. + +**Parameters:** +- `deposit_id: BytesN<32>` — The deposit ID returned by `deposit` +- `recipient: Address` — Recipient address (must authorize) + +**Returns:** `Result<(), VaultError>` + +**Validation:** +- Deposit must exist +- Current ledger >= `unlock_ledger` +- Caller must be the deposit recipient + +**Events emitted:** +- `("claim", deposit_id)` with `(recipient, amount)` + +**Errors:** +- `DepositNotFound` — Deposit ID does not exist +- `NotYetUnlocked` — Current ledger < `unlock_ledger` +- `WrongRecipient` — Caller is not the deposit recipient + +### `refund` + +Refund a deposit after the refund window. + +**Parameters:** +- `deposit_id: BytesN<32>` — The deposit ID returned by `deposit` + +**Returns:** `Result<(), VaultError>` + +**Validation:** +- Deposit must exist +- Current ledger >= `refund_after` +- Caller must be the deposit sender + +**Events emitted:** +- `("refund", deposit_id)` with `(sender, amount)` + +**Errors:** +- `DepositNotFound` — Deposit ID does not exist +- `NotYetRefundable` — Current ledger < `refund_after` + +## Error Variants + +| Error Code | Description | +|------------|-------------| +| `AlreadyInitialized = 1` | Contract already initialized | +| `NotInitialized = 2` | Contract not initialized | +| `InvalidWindow = 3` | Refund window is not > unlock_ledger + GRACE_PERIOD | +| `DepositNotFound = 4` | Deposit ID does not exist | +| `NotYetUnlocked = 5` | Current ledger < unlock_ledger | +| `NotYetRefundable = 6` | Current ledger < refund_after | +| `WrongRecipient = 7` | Caller is not the deposit recipient | + +## Event Topics + +| Topic | Data | Description | +|-------|------|-------------| +| `("deposit", deposit_id)` | `(sender, amount, asset, unlock_ledger)` | Deposit created | +| `("claim", deposit_id)` | `(recipient, amount)` | Deposit claimed | +| `("refund", deposit_id)` | `(sender, amount)` | Deposit refunded | + +## Storage Layout + +### Instance Storage +- `DataKey::Announcer: Address` — The stealth announcer contract address + +### Persistent Storage +- `DataKey::Deposit(deposit_id): DepositEntry` — Individual deposit entries + +**DepositEntry struct:** +```rust +pub struct DepositEntry { + pub sender: Address, + pub recipient: Address, + pub amount: i128, + pub asset: Address, + pub unlock_ledger: u32, + pub refund_after: u32, +} +``` + +**TTL Strategy:** +- Instance storage: Extended to `TTL_EXTEND_TO` (518400 ledgers, ~30 days) on every write +- Deposit storage: Extended to `TTL_EXTEND_TO` on creation, removed on claim/refund + +## Pause / Admin / Metrics Posture + +| Feature | Status | +|---------|--------| +| Pausable | No — no pause mechanism implemented | +| Admin | No — no admin controls after initialization | +| Metrics | No — no metric events emitted | + +## Related Docs + +- [PAUSE.md](../PAUSE.md) — Pause posture documentation +- [MULTISIG.md](../MULTISIG.md) — Multisig setup documentation +- [METRICS.md](../METRICS.md) — Metrics standard documentation + +## Constants + +- `GRACE_PERIOD: u32 = 1000` — Minimum ledgers between unlock and refund window +- `TTL_THRESHOLD: u32 = 17280` — ~1 day, TTL extension threshold +- `TTL_EXTEND_TO: u32 = 518400` — ~30 days, TTL extension target + +## Deposit ID Derivation + +The deposit ID is a SHA-256 hash of: +- `amount` (big-endian bytes) +- `unlock_ledger` (big-endian bytes) +- `refund_after` (big-endian bytes) +- `ephemeral_pub_key` (32 bytes) +- Current ledger sequence (big-endian bytes) + +This ensures deterministic, unique IDs for each deposit. + +## Testing + +```bash +cargo test -p stealth-vault +``` + +Tests cover: +- Deposit and claim flow +- Claim before unlock rejection +- Refund after window success +- Refund before window rejection +- Double claim rejection +- Wrong recipient claim rejection +- Refund window validation +- Sender early refund rejection diff --git a/stellar/wraith-asset-policy/README.md b/stellar/wraith-asset-policy/README.md new file mode 100644 index 0000000..7e46cbd --- /dev/null +++ b/stellar/wraith-asset-policy/README.md @@ -0,0 +1,135 @@ +# Wraith Asset Policy Contract (`wraith-asset-policy`) + +The `wraith-asset-policy` contract provides an admin-controlled asset allowlist for stealth payments. It implements the standard asset policy interface used by `stealth-sender` to filter which assets are allowed for stealth transfers. + +## Purpose + +Asset allowlist policy for stealth payments. This contract protects the unlinkability and user experience of stealth transfers by preventing clawback-enabled or freeze-enabled assets from being sent (as identified in audit #43). When configured in `stealth-sender`, this contract is called before every transfer to ensure the asset is allowed. + +## Entrypoints + +| Function | Description | Authorization | +|----------|-------------|----------------| +| `init(env, admin, default_assets)` | Initialize the policy with an admin and default allowlist | None (one-time initialization) | +| `add_asset(env, asset)` | Add an asset to the allowlist | Admin must authorize | +| `remove_asset(env, asset)` | Remove an asset from the allowlist | Admin must authorize | +| `check_asset(env, asset)` | Check if an asset is allowed | None (read-only) | + +### `init` + +Initialize the policy with an admin address and optional default assets. + +**Parameters:** +- `admin: Address` — Admin address that can add/remove assets +- `default_assets: Vec
` — Initial list of allowed assets + +**Returns:** None + +**Validation:** +- Contract must not already be initialized + +### `add_asset` + +Add an asset to the allowlist. + +**Parameters:** +- `asset: Address` — Token contract address to allow + +**Returns:** None + +**Authorization:** Admin must authorize + +**Validation:** +- Contract must be initialized + +### `remove_asset` + +Remove an asset from the allowlist. + +**Parameters:** +- `asset: Address` — Token contract address to disallow + +**Returns:** None + +**Authorization:** Admin must authorize + +**Validation:** +- Contract must be initialized + +### `check_asset` + +Check if an asset is allowed for stealth payments. + +**Parameters:** +- `asset: Address` — Token contract address to check + +**Returns:** `bool` — `true` if asset is allowed, `false` otherwise + +**Authorization:** None (read-only) + +This is the standard interface called by `stealth-sender` before transfers. + +## Error Variants + +The contract uses panics for validation (no custom error enum): + +| Condition | Panic Reason | +|-----------|--------------| +| Already initialized | "already initialized" | +| Not initialized | "not initialized" (on admin operations) | + +## Event Topics + +**None** — This contract emits no events. + +## Storage Layout + +### Instance Storage +- `DataKey::Admin: Address` — Admin address that can add/remove assets + +### Persistent Storage +- `DataKey::Asset(asset): bool` — Asset allowlist entries (true = allowed) + +**TTL Strategy:** Not explicitly managed in this contract (relies on default Soroban TTL behavior). + +## Pause / Admin / Metrics Posture + +| Feature | Status | +|---------|--------| +| Pausable | No — no pause mechanism implemented | +| Admin | Yes — admin can add/remove assets (set at init) | +| Metrics | No — no metric events emitted | + +## Related Docs + +- [PAUSE.md](../PAUSE.md) — Pause posture documentation +- [MULTISIG.md](../MULTISIG.md) — Multisig setup documentation (for admin key) +- [METRICS.md](../METRICS.md) — Metrics standard documentation + +## Asset Policy Interface + +This contract implements the standard asset policy interface expected by `stealth-sender`: + +```rust +pub fn check_asset(env: Env, asset: Address) -> bool; +``` + +- **asset**: The contract address of the Stellar Asset Contract (SAC) being checked +- **Returns**: `true` if the asset is allowed for stealth payments, or `false` otherwise + +If `false` is returned, `stealth-sender` rejects the transaction with `SenderError::TokenNotAllowed`. + +## Custom Policy Contracts + +Any contract can act as an asset policy as long as it implements the `check_asset` interface above. Callers who want custom rules (such as check-free transfers, or automated query-based enforcement) can deploy their own contract matching the interface and configure it in `stealth-sender` during initialization. + +## Testing + +```bash +cargo test -p wraith-asset-policy +``` + +Tests cover: +- Policy allowlist flow (add, check, remove) +- Initialize with default assets +- Double initialization rejection diff --git a/stellar/wraith-metrics/README.md b/stellar/wraith-metrics/README.md new file mode 100644 index 0000000..031c032 --- /dev/null +++ b/stellar/wraith-metrics/README.md @@ -0,0 +1,146 @@ +# Wraith Metrics Library (`wraith-metrics`) + +**⚠️ THIS IS A LIBRARY, NOT A DEPLOYABLE CONTRACT** — This crate provides shared types and helper functions for metric event emission across Wraith Protocol contracts. + +The `wraith-metrics` library defines the standard metric event schema and helper functions used by Wraith Protocol Stellar contracts to enable standardized off-chain observability and monitoring. + +## Purpose + +Shared metrics infrastructure for Wraith Protocol contracts. All Wraith contracts emit standardized metric events using this library to enable off-chain dashboards, monitoring, and analytics. + +## Usage + +This is a library crate, not a deployable contract. It is included as a dependency by other Wraith contracts that need to emit metric events. + +### Adding to a Contract + +Add to `Cargo.toml`: +```toml +[dependencies] +wraith-metrics = { path = "../wraith-metrics" } +``` + +### Emitting a Metric Event + +```rust +use wraith_metrics::{contract_ids, dimension_names, emit_metric, metric_names}; + +// Emit a counter metric +emit_metric( + &env, + contract_ids::STEALTH_SENDER, + metric_names::SEND_COUNT, + 1, + soroban_sdk::vec![&env, (dimension_names::TOKEN_ADDRESS, token_address.into_val(&env))], +); +``` + +## Data Structures + +### WraithMetricEvent + +The standard metric event structure: + +```rust +#[contracttype] +#[derive(Clone)] +pub struct WraithMetricEvent { + pub contract: Symbol, // Contract identifier (e.g., "stealth-registry") + pub metric_name: Symbol, // Metric name (e.g., "register_count") + pub value: i128, // Numeric value of the metric + pub dimensions: Vec<(Symbol, Val)>, // Optional dimensions for filtering/grouping +} +``` + +## Helper Functions + +### emit_metric + +Emit a metric event using the standard schema. + +**Parameters:** +- `env: &Env` — The Soroban environment +- `contract: Symbol` — Contract identifier +- `metric_name: Symbol` — Metric name +- `value: i128` — Metric value +- `dimensions: Vec<(Symbol, Val)>` — Optional dimensions + +**Event emitted:** +- Topics: `("metric", contract, metric_name)` +- Data: `(value, dimensions)` + +## Standard Metric Names + +Defined in `metric_names` module: + +| Constant | Symbol | Description | +|----------|--------|-------------| +| `REGISTER_COUNT` | `reg_cnt` | Number of registrations | +| `REMOVE_COUNT` | `rem_cnt` | Number of removals | +| `LOOKUP_COUNT` | `lkp_cnt` | Number of lookups | +| `SEND_COUNT` | `send_cnt` | Number of sends | +| `SEND_VOLUME` | `send_vol` | Total volume sent | +| `BATCH_SEND_COUNT` | `bat_send` | Number of batch sends | +| `BATCH_SEND_VOLUME` | `bat_vol` | Total batch send volume | +| `BATCH_SIZE` | `bat_size` | Size of a batch operation | +| `ERROR_COUNT` | `err_cnt` | Number of errors | + +## Standard Contract Identifiers + +Defined in `contract_ids` module: + +| Constant | Symbol | Contract | +|----------|--------|----------| +| `STEALTH_REGISTRY` | `st_reg` | stealth-registry | +| `STEALTH_SENDER` | `st_send` | stealth-sender | +| `STEALTH_BATCH_SENDER` | `st_bat_sd` | stealth-batch-sender | +| `STEALTH_ANNOUNCER` | `st_ann` | stealth-announcer | + +## Standard Dimension Names + +Defined in `dimension_names` module: + +| Constant | Symbol | Description | +|----------|--------|-------------| +| `SCHEME_ID` | `scheme_id` | Stealth address scheme identifier | +| `TOKEN_ADDRESS` | `tok_addr` | Token contract address | +| `ASSET_ADDRESS` | `ast_addr` | Asset contract address | +| `ERROR_CODE` | `err_code` | Error code | + +## Event Schema + +All metric events use the following event topic pattern: + +```rust +env.events().publish( + (symbol_short!("metric"), contract_name, metric_name), + (value, dimensions), +); +``` + +## Pause / Admin / Metrics Posture + +| Feature | Status | +|---------|--------| +| Pausable | N/A — library crate | +| Admin | N/A — library crate | +| Metrics | N/A — this library provides metrics infrastructure | + +## Related Docs + +- [METRICS.md](../METRICS.md) — Full metrics standard documentation with indexer implementation and Prometheus exporter format + +## Integration with Indexer + +A reference indexer implementation is provided in `stellar/scripts/metrics-indexer/` that: +1. Connects to a Stellar RPC node +2. Subscribes to contract events +3. Parses WraithMetricEvent format +4. Aggregates metrics in memory +5. Exposes metrics in Prometheus format + +See [METRICS.md](../METRICS.md) for details on the indexer and dashboard integration. + +## Testing + +This library has no standalone tests (it is a pure utility library). Testing is done by the contracts that use it.