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.
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
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.)
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, ... }
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
}pub fn init(env: &Env) {
// Initialize treasury snapshot system
// Call once at contract deployment
// Sets SNAPC = 0, SNAPL = 0
}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
}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
}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
}// 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// 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,
);// 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));
}┌──────────────────────────────────────┐
│ 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 │
└──────────────────────┘
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 creationtest_get_snapshot_by_id— Verify retrieval by IDtest_snapshot_count_accuracy— Verify count trackingtest_audit_trail— Verify full audit trail
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 consistenttest_snapshot_hash_differs_on_change— Different data → different hashtest_verify_snapshot_success— Verification succeeds for valid snapshot
- Snapshots are write-once to persistent storage
- No update or delete operations available
- Ensures audit trail cannot be tampered with
- SHA-256 used for cryptographic verification
- Hash computation includes all relevant fields
- Prevents silent data corruption
- Negative balances rejected (panic with
InvalidBalance) - Prevents recording invalid treasury states
- Each snapshot triggers
(TRE, snapshot)event - Enables off-chain indexing for analytics
- Provides second source of truth
- Snapshots accumulate over time
- Consider cleanup policy for old snapshots (future enhancement)
- Monitor storage usage in production
// 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!");
}// 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);
}// 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);
}
}| 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)
-
Creation Tests
- Snapshot creation with valid data
- ID auto-increment
- Counter updates
-
Retrieval Tests
- Get by ID
- Get latest
- Get recent (with count)
- Get audit trail
-
Integrity Tests
- Hash determinism
- Hash verification succeeds
- Hash differs on data change
- Invalid balance rejection
-
Integration Tests
- Multiple snapshots
- Context preservation
- Event emission
- Large audit trails
cd engine-core
cargo test treasury
cargo test --doc treasury- 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)
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,
);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,
);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