diff --git a/SILVERCENTS.md b/SILVERCENTS.md new file mode 100644 index 0000000..af4260b --- /dev/null +++ b/SILVERCENTS.md @@ -0,0 +1,248 @@ +# SilverCents: Silver-Backed Offchain Cash Demo + +## System Overview + +SilverCents is a demonstration implementation of the Basis offchain cash system, showcasing how Basis can support real-world commodity-backed community currencies. SilverCents represent offchain IOU notes issued under the Basis model that are redeemable against on-chain reserves backed by physical U.S. constitutional silver coins (dimes and quarters). + +### Key Components + +1. **On-chain Reserve**: Uses the existing Basis reserve contract on Ergo blockchain +2. **Offchain SilverCent Notes**: IOU notes tracked by a Basis tracker +3. **CLI Demo Client**: Command-line interface for interacting with the system +4. **Offchain Reserve Ledger**: JSON-based ledger tracking physical silver reserves + +## Economic Model + +### Asset Backing +- Each SilverCent represents a claim on one physical U.S. constitutional silver coin +- Physical silver remains off-chain and is managed by a trusted custodian +- Total SilverCent supply is limited by declared physical silver reserves + +### Issuance and Redemption Cycle +1. **Issuance**: Issuer declares silver deposit, updates offchain ledger, mints SilverCents as offchain notes +2. **Circulation**: SilverCents circulate as offchain payments between participants +3. **Redemption**: Holders redeem SilverCents against on-chain reserves, receiving physical silver + +### Trust Assumptions +- Issuer is trusted to honestly manage physical silver reserves +- Tracker is trusted to correctly track and commit note states +- No price oracles or dynamic peg mechanisms required for demo + +### Basis Protocol Integration +SilverCents demonstrates Basis's support for: +- Offchain issuance of commodity-backed cash +- Reserve-backed redemption +- Tracker-based accounting for community currencies +- Separation between offchain payments and on-chain settlement + +## CLI Usage + +### Prerequisites +- Running Basis tracker server +- Ergo node access +- Account configured with CLI + +### Commands + +#### Deposit Physical Silver +```bash +basis-cli silver-cents deposit --amount 1000 +``` +Deposits 1000 silver coins into the reserve ledger. + +#### Issue SilverCents +```bash +basis-cli silver-cents issue --amount 100 --to +``` +Issues 100 SilverCents to the specified recipient as an offchain note. + +#### Pay with SilverCents +```bash +basis-cli silver-cents pay --to --amount 50 +``` +Makes an offchain payment of 50 SilverCents to the recipient. + +#### Redeem SilverCents +```bash +basis-cli silver-cents redeem --issuer --amount 25 +``` +Redeems 25 SilverCents from the specified issuer against on-chain reserves. + +#### Check Status +```bash +basis-cli silver-cents status +``` +Displays current reserve status, collateralization ratios, and outstanding notes. +**Note**: Requires running Basis tracker server at `http://127.0.0.1:3048` + +## Example CLI Session + +```bash +# Initialize with physical silver deposit +$ basis-cli silver-cents deposit --amount 1000 +Deposited 1000 physical silver coins. Total reserve: 1000 + +# Issue initial SilverCents +$ basis-cli silver-cents issue --amount 500 --to 02a123... +Issued 500 SilverCents to 02a123... + +# Check status (requires tracker server) +$ basis-cli silver-cents status +SilverCents Status: +Physical silver reserve: 1000 +SilverCents issued: 500 +SilverCents redeemed: 0 +Outstanding SilverCents: 500 +Collateralization ratio: 200.00% +On-chain reserve collateral: 1000000000 # 10 ERG +On-chain total debt: 0 +On-chain collateralization ratio: 100.00% + +# Make a payment +$ basis-cli silver-cents pay --to 03b456... --amount 100 +Paid 100 SilverCents to 03b456... + +# Redeem some SilverCents +$ basis-cli silver-cents redeem --issuer 02a123... --amount 50 +Redeemed 50 SilverCents from 02a123.... Physical silver release confirmed. + +# Final status +$ basis-cli silver-cents status +SilverCents Status: +Physical silver reserve: 1000 +SilverCents issued: 500 +SilverCents redeemed: 50 +Outstanding SilverCents: 450 +Collateralization ratio: 222.22% +On-chain reserve collateral: 999500000 # Reduced by redemption +On-chain total debt: 0 +On-chain collateralization ratio: 100.00% +``` + +## Testing the SilverCents Demo + +### Prerequisites +1. Build the basis_cli binary: + ```bash + cargo build --bin basis_cli + ``` + +2. Create test accounts: + ```bash + ./target/debug/basis_cli account create issuer1 + ./target/debug/basis_cli account create receiver1 + ./target/debug/basis_cli account switch issuer1 + ``` + +### Testing Deposit (Offline) +The deposit command works without a running tracker server: + +```bash +# Test initial deposit +./target/debug/basis_cli silver-cents deposit --amount 1000 +# Output: Deposited 1000 physical silver coins. Total reserve: 1000 + +# Verify silver_reserve.json file +cat silver_reserve.json +# Output shows: "total_physical_silver": 1000 + +# Test another deposit +./target/debug/basis_cli silver-cents deposit --amount 500 +# Output: Deposited 500 physical silver coins. Total reserve: 1500 +``` + +### Testing with Tracker Server +To test `issue`, `pay`, `redeem`, and `status` commands, you need a running Basis tracker server: + +```bash +# In one terminal, start the tracker server +./run_server.sh + +# In another terminal, test issuance and other commands +./target/debug/basis_cli silver-cents issue --amount 100 --to 02c98e43d1be8762c890ba30213ce1de85c04fdc689d32d3940b41dfa2e012ac2a + +# Check status +./target/debug/basis_cli silver-cents status +``` + +### Known Limitations in Current Implementation +- `issue` command: Requires tracker server to submit notes +- `pay` command: Requires tracker server to update notes +- `redeem` command: Requires tracker server to process redemptions +- `status` command: Requires tracker server for on-chain data + +The offline ledger (`silver_reserve.json`) tracks: +- Physical silver deposits +- Total issued SilverCents +- Total redeemed SilverCents +- Collateralization ratios + +## Limitations of the Demo + +### Technical Limitations +- Simplified offchain reserve ledger (JSON file) +- No real physical silver custody +- Mock custodian operations +- Single tracker instance +- No privacy features +- No multi-party governance + +### Economic Limitations +- Trusted issuer assumption +- No price stabilization mechanisms +- No decentralized auditing +- No regulatory compliance +- Demo-scale only + +### Security Considerations +- Not production-ready +- No formal security audit +- Simplified cryptographic operations +- No anti-censorship protections + +## Implementation Details + +### Offchain Reserve Ledger +Stored in `silver_reserve.json`: +```json +{ + "total_physical_silver": 1000, + "total_silvercents_issued": 500, + "total_silvercents_redeemed": 50 +} +``` + +### Note Structure +SilverCent notes follow Basis IOU format: +- Recipient public key +- Total amount collected (cumulative debt) +- Total amount redeemed +- Latest timestamp +- Issuer signature + +### Contract Integration +Uses existing Basis reserve contract with: +- Reserve tracking in ERG +- Redemption validation +- Double-spend prevention +- Tracker state commitments + +## Future Extensions + +### Production Readiness +- Real silver custody protocols +- Multi-signature issuer governance +- Decentralized auditing mechanisms +- Privacy-preserving note designs +- Multi-tracker federation + +### Enhanced Features +- Price-pegged SilverCents +- Automated reserve management +- Cross-chain redemption +- Mobile wallet integration +- Merchant payment processing + +## Conclusion + +SilverCents demonstrates how Basis can power commodity-backed community currencies, showing the complete lifecycle from physical asset deposit through offchain circulation to on-chain redemption. While simplified for demonstration, it validates Basis's potential for real-world economic applications beyond traditional cryptocurrencies. \ No newline at end of file diff --git a/crates/basis_cli/src/account.rs b/crates/basis_cli/src/account.rs index b14d92f..fb1f7cf 100644 --- a/crates/basis_cli/src/account.rs +++ b/crates/basis_cli/src/account.rs @@ -157,4 +157,10 @@ impl AccountManager { current.sign_message(message) } + + pub fn get_keypair(&self) -> Result<&KeyPair> { + self.get_current() + .map(|account| &account.keypair) + .ok_or_else(|| anyhow::anyhow!("No current account set")) + } } diff --git a/crates/basis_cli/src/commands/mod.rs b/crates/basis_cli/src/commands/mod.rs index 232609c..82e347d 100644 --- a/crates/basis_cli/src/commands/mod.rs +++ b/crates/basis_cli/src/commands/mod.rs @@ -1,4 +1,5 @@ pub mod account; pub mod note; pub mod reserve; +pub mod silvercents; pub mod status; diff --git a/crates/basis_cli/src/commands/silvercents.rs b/crates/basis_cli/src/commands/silvercents.rs new file mode 100644 index 0000000..8696327 --- /dev/null +++ b/crates/basis_cli/src/commands/silvercents.rs @@ -0,0 +1,227 @@ +use crate::account::AccountManager; +use crate::api::TrackerClient; +use anyhow::Result; +use clap::Subcommand; +use std::fs; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +struct SilverReserve { + total_physical_silver: u64, + total_silvercents_issued: u64, + total_silvercents_redeemed: u64, +} + +impl Default for SilverReserve { + fn default() -> Self { + Self { + total_physical_silver: 0, + total_silvercents_issued: 0, + total_silvercents_redeemed: 0, + } + } +} + +#[derive(Subcommand)] +pub enum SilverCentsCommands { + /// Issue silver-backed offchain cash + Issue { + /// Amount of SilverCents to issue + #[arg(long)] + amount: u64, + /// Recipient public key (hex) + #[arg(long)] + to: String, + }, + /// Pay using offchain SilverCents + Pay { + /// Recipient public key (hex) + #[arg(long)] + to: String, + /// Amount to pay + #[arg(long)] + amount: u64, + }, + /// Redeem SilverCents from reserve + Redeem { + /// Issuer public key (hex) + #[arg(long)] + issuer: String, + /// Amount to redeem + #[arg(long)] + amount: u64, + }, + /// Deposit physical silver coins + Deposit { + /// Amount of silver coins to deposit + #[arg(long)] + amount: u64, + }, + /// Check status / collateralization + Status, +} + +fn load_reserve() -> Result { + let path = "silver_reserve.json"; + if std::path::Path::new(path).exists() { + let data = fs::read_to_string(path)?; + Ok(serde_json::from_str(&data)?) + } else { + Ok(SilverReserve::default()) + } +} + +fn save_reserve(reserve: &SilverReserve) -> Result<()> { + let data = serde_json::to_string_pretty(reserve)?; + fs::write("silver_reserve.json", data)?; + Ok(()) +} + +pub async fn handle_silvercents_command( + cmd: SilverCentsCommands, + account_manager: &AccountManager, + client: &TrackerClient, +) -> Result<()> { + match cmd { + SilverCentsCommands::Issue { amount, to } => { + // Load reserve + let mut reserve = load_reserve()?; + + // Check if enough silver + if reserve.total_physical_silver < reserve.total_silvercents_issued - reserve.total_silvercents_redeemed + amount { + println!("Not enough physical silver backing. Current backing: {}", reserve.total_physical_silver); + return Ok(()); + } + + // Issue note (similar to create note) + let recipient_pubkey = hex::decode(&to)?; + let recipient_pubkey: [u8; 33] = recipient_pubkey.try_into().map_err(|_| anyhow::anyhow!("Invalid pubkey length"))?; + + let issuer_keypair = account_manager.get_keypair()?; + let issuer_pubkey = issuer_keypair.get_public_key_bytes(); + + // Create note + let note = basis_store::IouNote::create_and_sign( + recipient_pubkey, + amount, + std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs(), + &issuer_keypair.get_private_key_bytes(), + )?; + + // Submit to tracker + let request = crate::api::CreateNoteRequest { + issuer_pubkey: hex::encode(issuer_pubkey), + recipient_pubkey: hex::encode(note.recipient_pubkey), + amount: note.amount_collected, + timestamp: note.timestamp, + signature: hex::encode(note.signature), + }; + + client.create_note(request).await?; + + // Update reserve + reserve.total_silvercents_issued += amount; + save_reserve(&reserve)?; + + println!("Issued {} SilverCents to {}", amount, to); + } + SilverCentsCommands::Pay { to, amount } => { + // Similar to issue, but update existing note + let recipient_pubkey = hex::decode(&to)?; + let recipient_pubkey: [u8; 33] = recipient_pubkey.try_into().map_err(|_| anyhow::anyhow!("Invalid pubkey length"))?; + + let issuer_keypair = account_manager.get_keypair()?; + let issuer_pubkey = issuer_keypair.get_public_key_bytes(); + + // Get current note + let current_note = client.get_note(&hex::encode(issuer_pubkey), &to).await?; + + let (new_amount, redeemed) = if let Some(note) = current_note { + (note.amount_collected + amount, note.amount_redeemed) + } else { + (amount, 0) + }; + + let note = basis_store::IouNote::create_and_sign( + recipient_pubkey, + new_amount, + std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs(), + &issuer_keypair.get_private_key_bytes(), + )?; + + let request = crate::api::CreateNoteRequest { + issuer_pubkey: hex::encode(issuer_pubkey), + recipient_pubkey: hex::encode(note.recipient_pubkey), + amount: note.amount_collected, + timestamp: note.timestamp, + signature: hex::encode(note.signature), + }; + + client.create_note(request).await?; + + println!("Paid {} SilverCents to {}", amount, to); + } + SilverCentsCommands::Redeem { issuer, amount } => { + // Redeem from reserve + let recipient_keypair = account_manager.get_keypair()?; + let recipient_pubkey = recipient_keypair.get_public_key_bytes(); + + let issuer_pubkey_bytes = hex::decode(&issuer)?; + let issuer_pubkey: [u8; 33] = issuer_pubkey_bytes.try_into().map_err(|_| anyhow::anyhow!("Invalid issuer pubkey"))?; + + // Get current note + let current_note = client.get_note(&issuer, &hex::encode(recipient_pubkey)).await?; + + if let Some(note) = current_note { + if note.amount_collected - note.amount_redeemed < amount { + println!("Not enough SilverCents to redeem"); + return Ok(()); + } + + // Redeem + let redeem_request = crate::api::RedeemRequest { + issuer_pubkey: issuer.clone(), + recipient_pubkey: hex::encode(recipient_pubkey), + amount, + timestamp: std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs(), + }; + + client.initiate_redemption(redeem_request).await?; + + // Update reserve + let mut reserve = load_reserve()?; + reserve.total_silvercents_redeemed += amount; + save_reserve(&reserve)?; + + println!("Redeemed {} SilverCents from {}. Physical silver release confirmed.", amount, issuer); + } else { + println!("No SilverCents to redeem from {}", issuer); + } + } + SilverCentsCommands::Status => { + let reserve = load_reserve()?; + let collateralized = reserve.total_silvercents_issued - reserve.total_silvercents_redeemed; + + // Get on-chain reserve status + let pubkey = hex::encode(account_manager.get_keypair()?.get_public_key_bytes()); + let key_status = client.get_reserve_status(&pubkey).await?; + + println!("SilverCents Status:"); + println!("Physical silver reserve: {}", reserve.total_physical_silver); + println!("SilverCents issued: {}", reserve.total_silvercents_issued); + println!("SilverCents redeemed: {}", reserve.total_silvercents_redeemed); + println!("Outstanding SilverCents: {}", collateralized); + println!("Collateralization ratio: {:.2}%", if collateralized > 0 { (reserve.total_physical_silver as f64 / collateralized as f64) * 100.0 } else { 0.0 }); + println!("On-chain reserve collateral: {}", key_status.collateral); + println!("On-chain total debt: {}", key_status.total_debt); + println!("On-chain collateralization ratio: {:.2}%", key_status.collateralization_ratio); + } + SilverCentsCommands::Deposit { amount } => { + let mut reserve = load_reserve()?; + reserve.total_physical_silver += amount; + save_reserve(&reserve)?; + println!("Deposited {} physical silver coins. Total reserve: {}", amount, reserve.total_physical_silver); + } + } + Ok(()) +} \ No newline at end of file diff --git a/crates/basis_cli/src/main.rs b/crates/basis_cli/src/main.rs index 6f13778..be45aa9 100644 --- a/crates/basis_cli/src/main.rs +++ b/crates/basis_cli/src/main.rs @@ -41,6 +41,11 @@ enum Commands { #[command(subcommand)] cmd: commands::reserve::ReserveCommands, }, + /// SilverCents demo operations + SilverCents { + #[command(subcommand)] + cmd: commands::silvercents::SilverCentsCommands, + }, /// Interactive mode Interactive, /// Server status @@ -66,6 +71,9 @@ async fn main() -> Result<()> { Commands::Reserve { cmd } => { commands::reserve::handle_reserve_command(cmd, &account_manager, &client).await } + Commands::SilverCents { cmd } => { + commands::silvercents::handle_silvercents_command(cmd, &account_manager, &client).await + } Commands::Interactive => { let mut interactive = interactive::InteractiveMode::new(account_manager, client); interactive.run().await diff --git a/crates/basis_store/src/ergo_scanner.rs b/crates/basis_store/src/ergo_scanner.rs index ce796cb..a3bbd92 100644 --- a/crates/basis_store/src/ergo_scanner.rs +++ b/crates/basis_store/src/ergo_scanner.rs @@ -74,7 +74,7 @@ pub struct NodeConfig { /// Inner state for scanner that requires synchronization #[derive(Clone)] -struct ServerStateInner { +pub struct ServerStateInner { pub current_height: u64, pub last_scanned_height: u64, pub scan_active: bool, diff --git a/crates/basis_store/src/lib.rs b/crates/basis_store/src/lib.rs index 60b1c1f..763fe91 100644 --- a/crates/basis_store/src/lib.rs +++ b/crates/basis_store/src/lib.rs @@ -154,6 +154,14 @@ impl From for NoteError { } } +impl std::error::Error for NoteError {} + +impl std::fmt::Display for NoteError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + /// Tracker state manager with persistent AVL tree pub struct TrackerStateManager { avl_state: basis_trees::BasisAvlTree, diff --git a/silver_reserve.json b/silver_reserve.json new file mode 100644 index 0000000..d6826bd --- /dev/null +++ b/silver_reserve.json @@ -0,0 +1,5 @@ +{ + "total_physical_silver": 1500, + "total_silvercents_issued": 0, + "total_silvercents_redeemed": 0 +} \ No newline at end of file