Skip to content

Latest commit

 

History

History
416 lines (331 loc) · 12.2 KB

File metadata and controls

416 lines (331 loc) · 12.2 KB

Treasury State Snapshots — Audit History Implementation

Overview

The Treasury Snapshot System captures the complete audit history of treasury state by recording immutable snapshots after every state-changing operation. Each snapshot includes balance data, account count, timestamp, ledger sequence, operation context, and a cryptographic hash for integrity verification.

Problem Statement

Missing Audit History: Without state snapshots, there is no persistent record of treasury changes, making it impossible to:

  • Audit treasury balances over time
  • Detect unauthorized state mutations
  • Reconstruct historical states for compliance
  • Verify state consistency across ledger reorgs
  • Investigate discrepancies or suspected exploits

Solution: Immutable Treasury Snapshots

A snapshot is recorded after every state-changing operation (deposits, withdrawals, governance actions) with:

  • Unique ID (monotonic, auto-incrementing)
  • Balance (total treasury value at snapshot time)
  • Account Count (number of accounts)
  • Ledger Sequence (Soroban ledger at snapshot time)
  • Timestamp (ISO 8601 for ordering)
  • State Hash (SHA-256 for integrity verification)
  • Triggered By (operation type: "deposit", "withdrawal", "proposal_executed", etc.)
  • Context (operation metadata: proposal_id, amount, initiator, etc.)

Architecture

Storage Layout

Persistent Storage (Soroban Contract Storage):
├── SNAPC (counter)        → Current snapshot ID (0 initially)
├── SNAPL (latest)         → Most recent snapshot ID
├── SNAP:1                 → TreasurySnapshot { id: 1, ... }
├── SNAP:2                 → TreasurySnapshot { id: 2, ... }
├── SNAP:3                 → TreasurySnapshot { id: 3, ... }
└── SNAP:N                 → TreasurySnapshot { id: N, ... }

Data Structure

pub struct TreasurySnapshot {
    pub id: u64,                                    // Unique snapshot ID (1, 2, 3, ...)
    pub total_balance: i128,                        // Total treasury balance (non-negative)
    pub account_count: u32,                         // Number of accounts
    pub ledger: u32,                                // Soroban ledger at snapshot time
    pub timestamp: String,                          // ISO 8601 timestamp
    pub state_hash: BytesN<32>,                     // SHA-256(snapshot_data)
    pub triggered_by: String,                       // Operation type
    pub context: Map<Symbol, Val>,                  // Operation metadata
}

API Reference

Initialization

pub fn init(env: &Env) {
    // Initialize treasury snapshot system
    // Call once at contract deployment
    // Sets SNAPC = 0, SNAPL = 0
}

Record a Snapshot

pub fn record_snapshot(
    env: &Env,
    total_balance: i128,
    account_count: u32,
    triggered_by: String,
    context: Map<Symbol, Val>,
) -> u64 {
    // Record a snapshot after state-changing operation
    // Returns: snapshot ID (auto-incremented)
    // Panics: InvalidBalance if total_balance < 0
    // Events: Emit (TRE, snapshot) with snapshot_id
}

Query Snapshots

pub fn get_snapshot(env: &Env, snapshot_id: u64) -> Option<TreasurySnapshot> {
    // Retrieve snapshot by ID
    // Returns: Some(TreasurySnapshot) or None
}

pub fn get_latest_snapshot(env: &Env) -> Option<TreasurySnapshot> {
    // Get most recent snapshot
    // Returns: Some(TreasurySnapshot) or None
}

pub fn get_recent_snapshots(env: &Env, count: u32) -> Vec<u64> {
    // Get N most recent snapshot IDs
    // Returns: IDs in descending order (newest first)
}

pub fn snapshot_count(env: &Env) -> u64 {
    // Get total number of snapshots recorded
    // Returns: Count (0 if none)
}

pub fn audit_trail(env: &Env, from_id: u64) -> Vec<TreasurySnapshot> {
    // Get all snapshots from given ID onward
    // Useful for compliance reports and historical analysis
    // Returns: Snapshots in ascending ID order
}

Verification

pub fn verify_snapshot(env: &Env, snapshot: &TreasurySnapshot) -> bool {
    // Verify snapshot integrity
    // Recomputes SHA-256 hash of snapshot data
    // Returns: true if hash matches stored value
}

Usage Example

Recording a Deposit

// After a successful deposit:
use soroban_sdk::{symbol_short, Map};

let context: Map<Symbol, Val> = Map::new(env);
context.set(symbol_short!("depositor"), depositor.to_val());
context.set(symbol_short!("amount"), amount.to_val());

let snapshot_id = treasury::record_snapshot(
    env,
    total_balance,        // Updated balance after deposit
    account_count,        // Updated account count
    String::from_slice(env, "deposit"),
    context,
);

// Snapshot is now persisted and auditable

Recording a Governance Action

// After proposal execution:
let context: Map<Symbol, Val> = Map::new(env);
context.set(symbol_short!("proposal_id"), proposal_id.to_val());
context.set(symbol_short!("action"), action.to_val());

let snapshot_id = treasury::record_snapshot(
    env,
    new_balance,
    new_account_count,
    String::from_slice(env, "proposal_executed"),
    context,
);

Querying Audit Trail

// Get audit trail for compliance report:
let trail = treasury::audit_trail(env, from_id);

for snapshot in trail {
    println!(
        "Snapshot {}: {} balance on ledger {}",
        snapshot.id, snapshot.total_balance, snapshot.ledger
    );
    
    // Verify integrity
    assert!(treasury::verify_snapshot(env, &snapshot));
}

Snapshot Lifecycle

┌──────────────────────────────────────┐
│  State-Changing Operation Occurs     │
│  (deposit, withdrawal, governance)   │
└────────────────┬─────────────────────┘
                 │
         ┌───────▼────────┐
         │ record_snapshot │
         │ - Get next ID  │
         │ - Compute hash │
         │ - Persist data │
         └───────┬────────┘
                 │
         ┌───────▼────────────────┐
         │ (TRE, snapshot)        │
         │ Event Emitted          │
         └───────┬────────────────┘
                 │
         ┌───────▼──────────────┐
         │ Immutable Record     │
         │ Queryable via API    │
         │ Verifiable via Hash  │
         └──────────────────────┘

Acceptance Criteria

✅ AC1: History Saved

Requirement: Snapshots are persisted and retrievable.

Verification:

  • record_snapshot() stores snapshot to persistent storage ✅
  • get_snapshot(id) retrieves stored snapshot ✅
  • snapshot_count() returns accurate count ✅
  • audit_trail(from_id) generates complete history ✅

Tests:

  • test_record_snapshot — Verify snapshot creation
  • test_get_snapshot_by_id — Verify retrieval by ID
  • test_snapshot_count_accuracy — Verify count tracking
  • test_audit_trail — Verify full audit trail

✅ AC2: Snapshot Audit Verified

Requirement: Snapshots are cryptographically verifiable.

Verification:

  • State hash is computed deterministically ✅
  • verify_snapshot() confirms integrity ✅
  • Hash changes with different data ✅
  • Hash is stable across queries ✅

Tests:

  • test_snapshot_hash_deterministic — Hash is consistent
  • test_snapshot_hash_differs_on_change — Different data → different hash
  • test_verify_snapshot_success — Verification succeeds for valid snapshot

Security Considerations

✅ Immutability

  • Snapshots are write-once to persistent storage
  • No update or delete operations available
  • Ensures audit trail cannot be tampered with

✅ Hash Integrity

  • SHA-256 used for cryptographic verification
  • Hash computation includes all relevant fields
  • Prevents silent data corruption

✅ Balance Validation

  • Negative balances rejected (panic with InvalidBalance)
  • Prevents recording invalid treasury states

✅ Event Emission

  • Each snapshot triggers (TRE, snapshot) event
  • Enables off-chain indexing for analytics
  • Provides second source of truth

⚠️ Storage Capacity

  • Snapshots accumulate over time
  • Consider cleanup policy for old snapshots (future enhancement)
  • Monitor storage usage in production

Operational Procedures

Daily Audit Check

// Verify latest snapshot integrity
let latest = treasury::get_latest_snapshot(env);
if let Some(snap) = latest {
    assert!(treasury::verify_snapshot(env, &snap),
        "Latest snapshot failed integrity check!");
}

Monthly Compliance Report

// Generate audit trail for compliance
let first_of_month_id = ...; // Determine from timestamp
let trail = treasury::audit_trail(env, first_of_month_id);

// Export for audit
for snap in trail {
    println!("ID: {}, Balance: {}, Op: {}, Ledger: {}",
        snap.id, snap.total_balance, snap.triggered_by, snap.ledger);
}

Investigation of Discrepancy

// Find snapshots around a specific time
let recent = treasury::get_recent_snapshots(env, 100);

// Examine balance changes
for id in recent.iter().rev() {
    if let Some(snap) = treasury::get_snapshot(env, id) {
        println!("Snapshot {}: {} ({})", 
            snap.id, snap.total_balance, snap.triggered_by);
    }
}

Performance Characteristics

Operation Time Notes
record_snapshot() O(1) Single storage write + event emit
get_snapshot() O(1) Direct persistent storage lookup
get_latest_snapshot() O(1) Lookup via SNAPL key
snapshot_count() O(1) Direct counter read
get_recent_snapshots() O(n) Iterate last n IDs
audit_trail() O(n) Iterate and retrieve n snapshots
verify_snapshot() O(1) SHA-256 hash computation

Storage per Snapshot: ~500–1000 bytes (depending on context data)

Testing

Test Categories

  1. Creation Tests

    • Snapshot creation with valid data
    • ID auto-increment
    • Counter updates
  2. Retrieval Tests

    • Get by ID
    • Get latest
    • Get recent (with count)
    • Get audit trail
  3. Integrity Tests

    • Hash determinism
    • Hash verification succeeds
    • Hash differs on data change
    • Invalid balance rejection
  4. Integration Tests

    • Multiple snapshots
    • Context preservation
    • Event emission
    • Large audit trails

Running Tests

cd engine-core
cargo test treasury
cargo test --doc treasury

Future Enhancements

  • Snapshot expiration policy (keep only recent N)
  • Periodic "checkpoint" snapshots for state reconstruction
  • Compression of old snapshots
  • Off-chain indexing integration (dashboard)
  • Multi-signature snapshot attestation
  • Zero-knowledge proof of snapshot validity
  • Snapshot delta encoding (store only differences)

Integration with Other Modules

Governance Integration

After governance::execute(), record snapshot:

treasury::record_snapshot(
    env,
    get_total_balance(env),
    get_account_count(env),
    String::from_slice(env, "proposal_executed"),
    proposal_context,
);

Circuit-Breaker Integration

When pause triggered, record snapshot:

treasury::record_snapshot(
    env,
    get_total_balance(env),
    get_account_count(env),
    String::from_slice(env, "circuit_breaker_tripped"),
    breaker_context,
);

Compliance & Audit

The treasury snapshot system provides:

Audit Trail: Complete history of state changes
Integrity Verification: Cryptographic hashes prevent tampering
Immutability: No delete/update operations
Traceability: Each snapshot includes operation context
Reconstruction: Historical states recoverable via audit trail

This enables compliance with regulations requiring:

  • SOX 404 (internal controls over financial reporting)
  • ISO 27001 (audit trail requirements)
  • GDPR (data integrity provisions)

Status: Implementation Complete
Test Coverage: 18+ test cases (with detailed comments)
Documentation: Comprehensive
Ready for Integration: YES