From cf7b37597db10ba23338df991d2aaa682b31aa39 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 28 May 2026 22:58:12 +0100 Subject: [PATCH 1/2] Add price storage functions (set_price, get_price) with validation and event --- contracts/vault/src/lib.rs | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/contracts/vault/src/lib.rs b/contracts/vault/src/lib.rs index bfbef2f..bee55b8 100644 --- a/contracts/vault/src/lib.rs +++ b/contracts/vault/src/lib.rs @@ -75,6 +75,8 @@ pub enum VaultError { OfferingIdTooLong = 26, /// Metadata exceeds maximum length (code 27). MetadataTooLong = 27, + /// Price parsing error or non‑positive price (code 28). + PriceParseError = 28, } #[contracttype] @@ -113,6 +115,7 @@ pub enum StorageKey { MaxDeduct, Paused, Metadata(String), + Price(String), PendingOwner, PendingAdmin, DepositorList, @@ -850,6 +853,38 @@ impl CalloraVault { Ok(metadata) } + /// Set price for an offering (owner only). + /// + /// # Errors + /// - `VaultError::OfferingIdTooLong` when `offering_id` exceeds maximum length. + /// - `VaultError::PriceParseError` when `price` cannot be parsed to a positive i128. + pub fn set_price(env: Env, caller: Address, offering_id: String, price: String) -> Result<(), VaultError> { + caller.require_auth(); + Self::require_owner(env.clone(), caller.clone())?; + if offering_id.len() > MAX_OFFERING_ID_LEN { + return Err(VaultError::OfferingIdTooLong); + } + let price_i128: i128 = price.parse().map_err(|_| VaultError::PriceParseError)?; + if price_i128 <= 0 { + return Err(VaultError::PriceParseError); + } + env.storage() + .instance() + .set(&StorageKey::Price(offering_id.clone()), &price); + env.events().publish( + (Symbol::new(&env, "price_set"), caller, offering_id), + price.clone(), + ); + Ok(()) + } + + /// Get stored price for an offering. + pub fn get_price(env: Env, offering_id: String) -> Option { + env.storage() + .instance() + .get(&StorageKey::Price(offering_id)) + } + pub fn update_metadata( env: Env, caller: Address, From f1bd3127842dc8b70db6d1369c27c0b82f2b123b Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 28 May 2026 23:15:59 +0100 Subject: [PATCH 2/2] fix --- contracts/vault/src/test_setter_validation.rs | 38 ++++++- docs/interfaces/vault.json | 107 +++++++++++++++--- 2 files changed, 127 insertions(+), 18 deletions(-) diff --git a/contracts/vault/src/test_setter_validation.rs b/contracts/vault/src/test_setter_validation.rs index 20d37a9..af7de8a 100644 --- a/contracts/vault/src/test_setter_validation.rs +++ b/contracts/vault/src/test_setter_validation.rs @@ -1,16 +1,19 @@ extern crate std; use soroban_sdk::testutils::Address as _; -use soroban_sdk::{token, Address, Env}; +use soroban_sdk::{token, Address, Env, Symbol, String}; use super::*; + fn create_usdc(env: &Env, admin: &Address) -> (Address, token::StellarAssetClient) { let ca = env.register_stellar_asset_contract_v2(admin.clone()); let addr = ca.address(); (addr.clone(), token::StellarAssetClient::new(env, &addr)) } + fn create_vault(env: &Env) -> (Address, CalloraVaultClient) { let addr = env.register(CalloraVault, ()); (addr, CalloraVaultClient::new(env, &addr)) } + fn setup(env: &Env) -> (Address, CalloraVaultClient, Address, Address) { env.mock_all_auths(); let admin = Address::generate(env); @@ -19,6 +22,39 @@ fn setup(env: &Env) -> (Address, CalloraVaultClient, Address, Address) { client.init(&admin, &usdc, &None, &None, &None, &None, &None); (vault_addr, client, usdc, admin) } + +#[test] +#[should_panic(expected = "OfferingIdTooLong")] +fn set_price_offering_id_too_long() { + let env = Env::default(); + let (_, client, _, admin) = setup(&env); + let long_id = "a".repeat((MAX_OFFERING_ID_LEN + 1) as usize); + client.set_price(&admin, &long_id, "100"); +} + +#[test] +#[should_panic(expected = "PriceParseError")] +fn set_price_zero_price() { + let env = Env::default(); + let (_, client, _, admin) = setup(&env); + client.set_price(&admin, "off1", "0"); +} + +#[test] +fn set_price_successful() { + let env = Env::default(); + let (_, client, _, admin) = setup(&env); + client.set_price(&admin, "off1", "1000").unwrap(); + // Verify readback + let stored = client.get_price(&"off1".to_string()); + assert_eq!(stored, Some("1000".to_string())); + // Verify event emitted (using try call to capture events) + let events = env.events().all(); + // Find price_set event + let price_set = events.iter().find(|e| e.topics[0].to_string() == "price_set"); + assert!(price_set.is_some(), "price_set event not emitted"); +} + #[test] #[should_panic(expected = "settlement cannot be vault address")] fn set_settlement_vault_address_panics() { diff --git a/docs/interfaces/vault.json b/docs/interfaces/vault.json index 96ceea3..ad2a624 100644 --- a/docs/interfaces/vault.json +++ b/docs/interfaces/vault.json @@ -1,4 +1,4 @@ -{ +{ "contract": "callora-vault", "version": "0.0.1", "description": "Primary USDC prepaid vault for the Callora API marketplace. Handles deposits, per-call deductions, withdrawals, and optional routing of deducted funds to a settlement or revenue-pool contract.", @@ -34,7 +34,8 @@ { "code": 24, "name": "NoOwnershipTransferPending", "description": "No ownership transfer is pending." }, { "code": 25, "name": "NoAdminTransferPending", "description": "No admin transfer is pending." }, { "code": 26, "name": "OfferingIdTooLong", "description": "Offering ID exceeds maximum length." }, - { "code": 27, "name": "MetadataTooLong", "description": "Metadata exceeds maximum length." } + { "code": 27, "name": "MetadataTooLong", "description": "Metadata exceeds maximum length." }, + { "code": 28, "name": "PriceParseError", "description": "Price parsing error or non‑positive price." } ] }, @@ -60,7 +61,7 @@ "functions": [ { "name": "init", - "description": "Initialize the vault. Can only be called once. The owner must authorize the transaction.", + "description": "Initialize the vault. Can only be called once. Owner must sign the transaction.", "access": "owner (must sign)", "params": [ { "name": "owner", "type": "Address", "optional": false, "description": "Vault owner address; must authorize." }, @@ -73,39 +74,111 @@ ], "returns": "VaultMeta", "panics": [ - "\"vault already initialized\" — called more than once.", - "\"initial balance must be non-negative\" — initial_balance < 0.", - "\"min_deposit must be non-negative\" — min_deposit < 0.", - "\"max_deduct must be positive\" — max_deduct <= 0.", - "\"min_deposit cannot exceed max_deduct\" — constraint violation.", - "\"usdc_token cannot be vault address\" — self-reference guard.", - "\"revenue_pool cannot be vault address\" — self-reference guard." + "\"vault already initialized\" — called more than once.", + "\"initial balance must be non-negative\" — initial_balance < 0.", + "\"min_deposit must be non-negative\" — min_deposit < 0.", + "\"max_deduct must be positive\" — max_deduct <= 0.", + "\"min_deposit cannot exceed max_deduct\" — constraint violation.", + "\"usdc_token cannot be vault address\" — self-reference guard.", + "\"revenue_pool cannot be vault address\" — self-reference guard." ], "events": [ { "topics": ["\"init\"", "owner"], "data": "balance (i128)" } ] }, - { "name": "deposit", "description": "Transfer USDC from depositor into vault and increase tracked balance. Blocked when paused.", "access": "owner OR allowed depositor", "params": [ { "name": "depositor", "type": "Address", "optional": false, "description": "Must be owner or an allowed depositor; must authorize." }, - { "name": "amount", "type": "i128", "optional": false, "description": "Amount to deposit; must be > 0 and >= min_deposit." } + { "name": "amount", "type": "i128", "optional": false, "description": "Amount to deposit; must be > 0 and >= min_deposit." } ], "returns": "i128 (new balance)", "panics": [ - "\"vault is paused\" — circuit breaker is active.", - "\"amount must be positive\" — amount <= 0.", - "\"unauthorized: only owner or allowed depositor can deposit\" — auth failure.", - "\"deposit below minimum: X < Y\" — below min_deposit.", - "\"balance overflow\" — extremely unlikely arithmetic overflow." + "\"vault is paused\" — circuit breaker is active.", + "\"amount must be positive\" — amount <= 0.", + "\"unauthorized: only owner or allowed depositor can deposit\" — auth failure.", + "\"deposit below minimum: X < Y\" — below min_deposit.", + "\"balance overflow\" — extremely unlikely arithmetic overflow." ], "events": [ { "topics": ["\"deposit\"", "depositor"], "data": "(amount, new_balance) tuple" } ] }, + { + "name": "set_price", + "description": "Store off‑chain price for an offering. Owner‑only. Offering ID limited to 64 characters. Price must be a positive integer string.", + "access": "owner", + "params": [ + { "name": "caller", "type": "Address", "optional": false, "description": "Vault owner; must authorize." }, + { "name": "offering_id", "type": "String", "optional": false, "description": "Offering identifier; max 64 characters." }, + { "name": "price", "type": "String", "optional": false, "description": "Price value as string; must parse to a positive i128." } + ], + "returns": "void", + "panics": [ + "\"unauthorized: owner only\" — caller is not the owner.", + "\"offering_id exceeds max length\" — offering_id.len() > 64.", + "\"price parse error\" — price cannot be parsed to a positive integer." + ], + "events": [ + { "topics": ["\"price_set\"", "caller", "offering_id"], "data": "price (String)" } + ] + }, + { + "name": "get_price", + "description": "Retrieve stored price for an offering. Returns null if not set.", + "access": "any", + "params": [ + { "name": "offering_id", "type": "String", "optional": false, "description": "Offering identifier to look up." } + ], + "returns": "String | null", + "panics": [], + "events": [] + }, + { + "name": "set_metadata", + "description": "Store off-chain metadata (e.g. IPFS CID) for an offering. Owner-only. offering_id max 64 chars; metadata max 256 chars.", + "access": "owner", + "params": [ + { "name": "caller", "type": "Address", "optional": false, "description": "Must be the vault owner; must authorize." }, + { "name": "offering_id", "type": "String", "optional": false, "description": "Offering identifier; max 64 characters." }, + { "name": "metadata", "type": "String", "optional": false, "description": "Metadata value (e.g. IPFS CID or URI); max 256 characters." } + ], + "returns": "String (stored metadata)", + "panics": [ + "\"unauthorized: owner only\" — caller is not the owner.", + "\"offering_id exceeds max length\" — offering_id.len() > 64.", + "\"metadata exceeds max length\" — metadata.len() > 256." + ], + "events": [ + { "topics": ["\"metadata_set\"", "offering_id", "caller"], "data": "metadata (String)" } + ] + }, + { + "name": "get_metadata", + "description": "Retrieve stored offering metadata. Returns null if not set.", + "access": "any", + "params": [ + { "name": "offering_id", "type": "String", "optional": false, "description": "Offering identifier to look up." } + ], + "returns": "String | null", + "panics": [], + "events": [] + }, + { + "name": "require_owner", + "description": "Utility: panic with 'unauthorized: owner only' if caller is not the vault owner. Exposed publicly so external contracts can use it as a guard.", + "access": "any (panics if caller != owner)", + "params": [ + { "name": "caller", "type": "Address", "optional": false, "description": "Address to validate as owner." } + ], + "returns": "void", + "panics": [ + "\"unauthorized: owner only\" — caller is not the owner." + ], + "events": [] + }, { "name": "deduct",