From 570b2613edda778500b2b2e0a8d11c91aeac0a61 Mon Sep 17 00:00:00 2001 From: Dev Fashman Date: Thu, 27 Aug 2026 15:21:02 -0400 Subject: [PATCH 1/2] feat(queue-factory): add salt to deploy_queue to prevent front-running (#206) deploy_queue now accepts a salt: BytesN<32> and deploys the queue via env.deployer().with_current_contract(salt).deploy_v2(...) so the resulting contract address is deterministic but not predictable by a third party. Two calls using the same factory and WASM but different salts yield different addresses (verified by test_same_wasm_different_salt_yields_different_addresses). - sdk: add optional QueueDeploymentParams.salt (random salt auto-gen when omitted) - docs: document the salt arg, address derivation formula, and SDK behavior - SDK 22 migration: rewrite queue/enrollment/escrow/identity test suites to the generated-client pattern (Address::generate, env.register, mock_all_auths), add no_std headers for wasm builds, fix invalid Symbol literals, and enable wasm reference-types in .cargo/config.toml + patched host so deploy_v2 works. - examples: restore valid JSON (missing braces) in all example package.json so pnpm CI jobs can run. --- contracts/.cargo/config.toml | 5 + .../src/budget/wasmi_helper.rs | 2 +- contracts/lineproof-enrollment/src/lib.rs | 2 + contracts/lineproof-enrollment/src/test.rs | 21 - contracts/lineproof-escrow/src/lib.rs | 4 +- contracts/lineproof-escrow/src/test.rs | 257 +++--------- contracts/lineproof-identity/src/lib.rs | 37 +- contracts/lineproof-identity/src/test.rs | 198 +++++---- contracts/lineproof-queue-factory/src/lib.rs | 109 +++-- contracts/lineproof-queue-factory/src/test.rs | 397 +++++++++--------- .../test-fixtures/queue.wasm | Bin 0 -> 51515 bytes contracts/lineproof-queue/src/lib.rs | 8 +- docs/runbook-deployment.md | 51 ++- examples/concert-ticket/package.json | 2 + examples/event-ticketing/package.json | 2 + examples/healthcare-scheduling/package.json | 2 + examples/sneaker-drop/package.json | 2 + examples/university-admissions/package.json | 2 + examples/visa-appointment/package.json | 2 + sdk/src/types.ts | 6 + 20 files changed, 549 insertions(+), 560 deletions(-) create mode 100644 contracts/.cargo/config.toml create mode 100755 contracts/lineproof-queue-factory/test-fixtures/queue.wasm diff --git a/contracts/.cargo/config.toml b/contracts/.cargo/config.toml new file mode 100644 index 0000000..ecdaa88 --- /dev/null +++ b/contracts/.cargo/config.toml @@ -0,0 +1,5 @@ +[build] +target-dir = "target" + +[target.wasm32-unknown-unknown] +rustflags = ["-C", "target-feature=+reference-types"] diff --git a/contracts/.patched/soroban-env-host/src/budget/wasmi_helper.rs b/contracts/.patched/soroban-env-host/src/budget/wasmi_helper.rs index b976d4c..c7fb295 100644 --- a/contracts/.patched/soroban-env-host/src/budget/wasmi_helper.rs +++ b/contracts/.patched/soroban-env-host/src/budget/wasmi_helper.rs @@ -128,7 +128,7 @@ pub(crate) fn get_wasmi_config(budget: &Budget) -> Result (Env, Address) { +fn setup() -> (Env, Address, Symbol, Address) { let env = Env::default(); + env.mock_all_auths(); let admin = Address::generate(&env); - (env, admin) + let contract_id = env.register(EscrowImpl, ()); + let queue_id = Symbol::new(&env, "sneaker_drop"); + (env, admin, queue_id, contract_id) } fn make_config(env: &Env, admin: &Address) -> EscrowConfig { EscrowConfig { - queue_id: Symbol::new(env, "sneaker-drop"), + queue_id: Symbol::new(env, "sneaker_drop"), min_deposit: 100i128, max_deposit: 1000i128, hold_period_days: 30, @@ -20,10 +23,11 @@ fn make_config(env: &Env, admin: &Address) -> EscrowConfig { #[test] fn test_set_and_get_config() { - let (env, admin) = setup(); + let (env, admin, queue_id, contract_id) = setup(); + let client = EscrowImplClient::new(&env, &contract_id); let config = make_config(&env, &admin); - EscrowImpl::set_config(env.clone(), admin.clone(), config); - let loaded = EscrowImpl::get_config(env.clone(), Symbol::new(&env, "sneaker-drop")); + client.set_config(&admin, &config); + let loaded = client.get_config(&queue_id); assert_eq!(loaded.min_deposit, 100i128); assert_eq!(loaded.max_deposit, 1000i128); assert_eq!(loaded.hold_period_days, 30); @@ -31,258 +35,115 @@ fn test_set_and_get_config() { #[test] fn test_deposit_creates_record() { - let (env, admin) = setup(); - EscrowImpl::set_config(env.clone(), admin.clone(), make_config(&env, &admin)); - let user = Address::new(&env, &[9u8; 7]); - let asset = Address::new(&env, &[8u8; 7]); - EscrowImpl::deposit( - env.clone(), - user.clone(), - Symbol::new(&env, "sneaker-drop"), - 500i128, - asset.clone(), - ); + let (env, admin, queue_id, contract_id) = setup(); + let client = EscrowImplClient::new(&env, &contract_id); + client.set_config(&admin, &make_config(&env, &admin)); let user = Address::generate(&env); let asset = Address::generate(&env); - EscrowImpl::deposit( - env.clone(), - user.clone(), - Symbol::new(&env, "sneaker-drop"), - 500i128, - asset.clone(), - ); - let record = EscrowImpl::get_record(env.clone(), user.clone(), Symbol::new(&env, "sneaker-drop")).unwrap(); + client.deposit(&user, &queue_id, &500i128, &asset); + let record = client.get_record(&user, &queue_id).unwrap(); assert_eq!(record.amount, 500i128); assert!(matches!(record.status, EscrowStatus::Active)); } #[test] fn test_get_total_held_accumulates() { - let (env, admin) = setup(); - EscrowImpl::set_config(env.clone(), admin.clone(), make_config(&env, &admin)); - let user1 = Address::new(&env, &[9u8; 7]); - let user2 = Address::new(&env, &[12u8; 7]); - let asset = Address::new(&env, &[8u8; 7]); - EscrowImpl::deposit( - env.clone(), - user1.clone(), - Symbol::new(&env, "sneaker-drop"), - 500i128, - asset.clone(), - ); - EscrowImpl::deposit( - env.clone(), - user2.clone(), - Symbol::new(&env, "sneaker-drop"), - 300i128, - asset.clone(), - ); + let (env, admin, queue_id, contract_id) = setup(); + let client = EscrowImplClient::new(&env, &contract_id); + client.set_config(&admin, &make_config(&env, &admin)); let user1 = Address::generate(&env); let user2 = Address::generate(&env); let asset = Address::generate(&env); - EscrowImpl::deposit( - env.clone(), - user1.clone(), - Symbol::new(&env, "sneaker-drop"), - 500i128, - asset.clone(), - ); - EscrowImpl::deposit( - env.clone(), - user2.clone(), - Symbol::new(&env, "sneaker-drop"), - 300i128, - asset.clone(), - ); - let total = EscrowImpl::get_total_held(env.clone(), Symbol::new(&env, "sneaker-drop")); + client.deposit(&user1, &queue_id, &500i128, &asset); + client.deposit(&user2, &queue_id, &300i128, &asset); + let total = client.get_total_held(&queue_id); assert_eq!(total, 800i128); } #[test] fn test_release_changes_status() { - let (env, admin) = setup(); - EscrowImpl::set_config(env.clone(), admin.clone(), make_config(&env, &admin)); - let user = Address::new(&env, &[3u8; 7]); - let asset = Address::new(&env, &[8u8; 7]); - EscrowImpl::deposit( - env.clone(), - user.clone(), - Symbol::new(&env, "sneaker-drop"), - 500i128, - asset, - ); - EscrowImpl::release( - env.clone(), - admin.clone(), - user.clone(), - Symbol::new(&env, "sneaker-drop"), - ); + let (env, admin, queue_id, contract_id) = setup(); + let client = EscrowImplClient::new(&env, &contract_id); + client.set_config(&admin, &make_config(&env, &admin)); let user = Address::generate(&env); let asset = Address::generate(&env); - EscrowImpl::deposit( - env.clone(), - user.clone(), - Symbol::new(&env, "sneaker-drop"), - 500i128, - asset, - ); - EscrowImpl::release( - env.clone(), - admin.clone(), - user.clone(), - Symbol::new(&env, "sneaker-drop"), - ); - let record = EscrowImpl::get_record(env, user, Symbol::new(&env, "sneaker-drop")).unwrap(); + client.deposit(&user, &queue_id, &500i128, &asset); + client.release(&admin, &user, &queue_id); + let record = client.get_record(&user, &queue_id).unwrap(); assert!(matches!(record.status, EscrowStatus::Released)); } #[test] fn test_refund_changes_status() { - let (env, admin) = setup(); - EscrowImpl::set_config(env.clone(), admin.clone(), make_config(&env, &admin)); - let user = Address::new(&env, &[4u8; 7]); - let asset = Address::new(&env, &[8u8; 7]); - EscrowImpl::deposit( - env.clone(), - user.clone(), - Symbol::new(&env, "sneaker-drop"), - 500i128, - asset, - ); - EscrowImpl::refund( - env.clone(), - admin.clone(), - user.clone(), - Symbol::new(&env, "sneaker-drop"), - ); + let (env, admin, queue_id, contract_id) = setup(); + let client = EscrowImplClient::new(&env, &contract_id); + client.set_config(&admin, &make_config(&env, &admin)); let user = Address::generate(&env); let asset = Address::generate(&env); - EscrowImpl::deposit( - env.clone(), - user.clone(), - Symbol::new(&env, "sneaker-drop"), - 500i128, - asset, - ); - EscrowImpl::refund( - env.clone(), - admin.clone(), - user.clone(), - Symbol::new(&env, "sneaker-drop"), - ); - let record = EscrowImpl::get_record(env, user, Symbol::new(&env, "sneaker-drop")).unwrap(); + client.deposit(&user, &queue_id, &500i128, &asset); + client.refund(&admin, &user, &queue_id); + let record = client.get_record(&user, &queue_id).unwrap(); assert!(matches!(record.status, EscrowStatus::Refunded)); } #[test] #[should_panic(expected = "amount must be positive")] fn test_deposit_rejects_non_positive_amount() { - let (env, admin) = setup(); - EscrowImpl::set_config(env.clone(), admin.clone(), make_config(&env, &admin)); + let (env, admin, queue_id, contract_id) = setup(); + let client = EscrowImplClient::new(&env, &contract_id); + client.set_config(&admin, &make_config(&env, &admin)); let user = Address::generate(&env); let asset = Address::generate(&env); - EscrowImpl::deposit(env, user, Symbol::new(&env, "sneaker-drop"), 0i128, asset); + client.deposit(&user, &queue_id, &0i128, &asset); } #[test] #[should_panic(expected = "amount outside configured bounds")] fn test_deposit_rejects_above_max() { - let (env, admin) = setup(); - EscrowImpl::set_config(env.clone(), admin.clone(), make_config(&env, &admin)); + let (env, admin, queue_id, contract_id) = setup(); + let client = EscrowImplClient::new(&env, &contract_id); + client.set_config(&admin, &make_config(&env, &admin)); let user = Address::generate(&env); let asset = Address::generate(&env); - EscrowImpl::deposit(env.clone(), user, Symbol::new(&env, "sneaker-drop"), 5000i128, asset); + client.deposit(&user, &queue_id, &5000i128, &asset); } #[test] #[should_panic(expected = "existing escrow record")] fn test_deposit_rejects_duplicate_for_same_user_and_queue() { - let (env, admin) = setup(); - EscrowImpl::set_config(env.clone(), admin.clone(), make_config(&env, &admin)); - let user = Address::new(&env, &[7u8; 7]); - let asset = Address::new(&env, &[8u8; 7]); - EscrowImpl::deposit( - env.clone(), - user.clone(), - Symbol::new(&env, "sneaker-drop"), - 250i128, - asset.clone(), - ); + let (env, admin, queue_id, contract_id) = setup(); + let client = EscrowImplClient::new(&env, &contract_id); + client.set_config(&admin, &make_config(&env, &admin)); let user = Address::generate(&env); let asset = Address::generate(&env); - EscrowImpl::deposit( - env.clone(), - user.clone(), - Symbol::new(&env, "sneaker-drop"), - 250i128, - asset.clone(), - ); - EscrowImpl::deposit(env, user, Symbol::new(&env, "sneaker-drop"), 300i128, asset); + client.deposit(&user, &queue_id, &250i128, &asset); + client.deposit(&user, &queue_id, &300i128, &asset); } #[test] fn test_expire_updates_status() { - let (env, admin) = setup(); + let (env, admin, queue_id, contract_id) = setup(); + let client = EscrowImplClient::new(&env, &contract_id); let mut config = make_config(&env, &admin); config.hold_period_days = 0; // expires immediately - EscrowImpl::set_config(env.clone(), admin.clone(), config); - let user = Address::new(&env, &[10u8; 7]); - let asset = Address::new(&env, &[8u8; 7]); - EscrowImpl::deposit( - env.clone(), - user.clone(), - Symbol::new(&env, "sneaker-drop"), - 200i128, - asset, - ); + client.set_config(&admin, &config); let user = Address::generate(&env); let asset = Address::generate(&env); - EscrowImpl::deposit( - env.clone(), - user.clone(), - Symbol::new(&env, "sneaker-drop"), - 200i128, - asset, - ); - EscrowImpl::expire(env.clone(), user.clone(), Symbol::new(&env, "sneaker-drop")); - let record = EscrowImpl::get_record(env, user, Symbol::new(&env, "sneaker-drop")).unwrap(); + client.deposit(&user, &queue_id, &200i128, &asset); + client.expire(&user, &queue_id); + let record = client.get_record(&user, &queue_id).unwrap(); assert!(matches!(record.status, EscrowStatus::Expired)); } #[test] #[should_panic(expected = "escrow not active")] fn test_release_already_released_panics() { - let (env, admin) = setup(); - EscrowImpl::set_config(env.clone(), admin.clone(), make_config(&env, &admin)); - let user = Address::new(&env, &[11u8; 7]); - let asset = Address::new(&env, &[8u8; 7]); - EscrowImpl::deposit( - env.clone(), - user.clone(), - Symbol::new(&env, "sneaker-drop"), - 500i128, - asset, - ); - EscrowImpl::release( - env.clone(), - admin.clone(), - user.clone(), - Symbol::new(&env, "sneaker-drop"), - ); + let (env, admin, queue_id, contract_id) = setup(); + let client = EscrowImplClient::new(&env, &contract_id); + client.set_config(&admin, &make_config(&env, &admin)); let user = Address::generate(&env); let asset = Address::generate(&env); - EscrowImpl::deposit( - env.clone(), - user.clone(), - Symbol::new(&env, "sneaker-drop"), - 500i128, - asset, - ); - EscrowImpl::release( - env.clone(), - admin.clone(), - user.clone(), - Symbol::new(&env, "sneaker-drop"), - ); - EscrowImpl::release(env, admin, user, Symbol::new(&env, "sneaker-drop")); + client.deposit(&user, &queue_id, &500i128, &asset); + client.release(&admin, &user, &queue_id); + client.release(&admin, &user, &queue_id); } diff --git a/contracts/lineproof-identity/src/lib.rs b/contracts/lineproof-identity/src/lib.rs index 078f7a4..9359827 100644 --- a/contracts/lineproof-identity/src/lib.rs +++ b/contracts/lineproof-identity/src/lib.rs @@ -1,3 +1,5 @@ +#![cfg_attr(not(test), no_std)] + use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, Symbol, Vec}; /// TTL threshold: renew if remaining TTL is below this many ledgers (~13.8 hours at 5s/ledger) @@ -79,7 +81,7 @@ impl Identity for IdentityImpl { let mut record = Self::get_record_internal(&env, &identity); let mut updated: Vec = Vec::new(&env); for q in record.queues.iter() { - if q != &queue_id { + if q != queue_id { updated.push_back(q.clone()); } } @@ -100,7 +102,7 @@ impl Identity for IdentityImpl { fn is_bound(env: Env, identity: Address, queue_id: Symbol) -> bool { let record = Self::get_record_internal(&env, &identity); - record.queues.iter().any(|q| q == &queue_id) + record.queues.iter().any(|q| q == queue_id) } fn can_transfer(env: Env, from: Address, to: Address, queue_id: Symbol) -> bool { @@ -113,7 +115,7 @@ impl Identity for IdentityImpl { return false; } - let is_bound = record.queues.iter().any(|q| q == &queue_id); + let is_bound = record.queues.iter().any(|q| q == queue_id); if !is_bound { return false; } @@ -175,9 +177,6 @@ impl Identity for IdentityImpl { env.storage() .persistent() .extend_ttl(&key, TTL_THRESHOLD, TTL_EXTEND_TO); - env.storage() - .persistent() - .extend_ttl(&env.current_contract_address(), TTL_THRESHOLD, TTL_EXTEND_TO); } fn get_admin(env: Env) -> Option
{ @@ -206,7 +205,7 @@ impl Identity for IdentityImpl { emit( &env, Symbol::new(&env, "Revoked"), - Symbol::new(&env, ""), + Symbol::new(&env, "none"), &identity, env.ledger().timestamp(), ); @@ -216,16 +215,20 @@ impl Identity for IdentityImpl { impl IdentityImpl { fn get_record_internal(env: &Env, identity: &Address) -> IdentityRecord { let key = Self::record_key(env, identity); - let record = env.storage().persistent().get(&key).unwrap_or(IdentityRecord { - identity: identity.clone(), - bound_at: 0, - queues: Vec::new(env), - status: BindingStatus::Unbound, - }); - env.storage() - .persistent() - .extend_ttl(&key, TTL_THRESHOLD, TTL_EXTEND_TO); - record + if env.storage().persistent().has(&key) { + let record: IdentityRecord = env.storage().persistent().get(&key).unwrap(); + env.storage() + .persistent() + .extend_ttl(&key, TTL_THRESHOLD, TTL_EXTEND_TO); + record + } else { + IdentityRecord { + identity: identity.clone(), + bound_at: 0, + queues: Vec::new(env), + status: BindingStatus::Unbound, + } + } } pub fn record_key(env: &Env, identity: &Address) -> (Symbol, Address) { diff --git a/contracts/lineproof-identity/src/test.rs b/contracts/lineproof-identity/src/test.rs index 573f5d8..9c32502 100644 --- a/contracts/lineproof-identity/src/test.rs +++ b/contracts/lineproof-identity/src/test.rs @@ -1,146 +1,174 @@ -use crate::{BindingStatus, Identity, IdentityImpl}; -use crate::{BindingStatus, IdentityImpl}; -use soroban_sdk::{testutils::Address as _, Address, Env, Symbol}; use soroban_sdk::{testutils::Address as _, Address, Env, Symbol}; -fn setup() -> (Env, Address) { - let env = Env::default(); - let user = Address::generate(&env); - (env, user) +use crate::{BindingStatus, Identity, IdentityImpl, IdentityImplClient, TransferAttempt}; + +struct Harness { + env: Env, + contract_id: Address, + admin: Address, +} + +impl Harness { + fn new() -> Self { + let env = Env::default(); + let contract_id = env.register(IdentityImpl, ()); + env.mock_all_auths(); + let admin = Address::generate(&env); + let client = IdentityImplClient::new(&env, &contract_id); + client.initialize(&admin); + Harness { + env, + contract_id, + admin, + } + } + + fn client(&self) -> IdentityImplClient<'_> { + IdentityImplClient::new(&self.env, &self.contract_id) + } } #[test] fn test_bind_creates_record_with_timestamp() { - let (env, user) = setup(); - let queue_id = Symbol::new(&env, "sneaker-drop"); - IdentityImpl::bind(env.clone(), user.clone(), queue_id.clone()); - let record = IdentityImpl::get_record(env.clone(), user.clone()).unwrap(); - assert!(record.queues.iter().any(|q| q == &queue_id)); + let h = Harness::new(); + let client = h.client(); + let user = Address::generate(&h.env); + let queue_id = Symbol::new(&h.env, "sneaker_drop"); + client.bind(&user, &queue_id); + let record = client.get_record(&user).unwrap(); + assert!(record.queues.iter().any(|q| &q == &queue_id)); assert!(matches!(record.status, BindingStatus::Bound)); - // bound_at should be set (ledger timestamp in tests defaults to 0) - assert_eq!(record.bound_at, 0); // default test env timestamp + assert_eq!(record.bound_at, 0); } #[test] fn test_unbind_removes_queue() { - let (env, user) = setup(); - let queue_id = Symbol::new(&env, "concert"); - IdentityImpl::bind(env.clone(), user.clone(), queue_id.clone()); - IdentityImpl::unbind(env.clone(), user.clone(), queue_id.clone()); - assert!(!IdentityImpl::is_bound(env.clone(), user.clone(), queue_id)); + let h = Harness::new(); + let client = h.client(); + let user = Address::generate(&h.env); + let queue_id = Symbol::new(&h.env, "concert"); + client.bind(&user, &queue_id); + client.unbind(&user, &queue_id); + assert!(!client.is_bound(&user, &queue_id)); } #[test] fn test_is_bound_returns_false_before_bind() { - let (env, user) = setup(); - let queue_id = Symbol::new(&env, "new-queue"); - assert!(!IdentityImpl::is_bound(env, user, queue_id)); + let h = Harness::new(); + let user = Address::generate(&h.env); + let queue_id = Symbol::new(&h.env, "new_queue"); + assert!(!h.client().is_bound(&user, &queue_id)); } #[test] fn test_can_transfer_returns_false_for_revoked_identity() { - let (env, admin) = setup(); - IdentityImpl::initialize(env.clone(), admin.clone()); - let user = Address::generate(&env); - let other = Address::generate(&env); - let queue_id = Symbol::new(&env, "q-transfer"); - IdentityImpl::bind(env.clone(), user.clone(), queue_id.clone()); - IdentityImpl::set_transfer_allowed(env.clone(), admin.clone(), true); - IdentityImpl::revoke(env.clone(), admin, user.clone()); - assert!(!IdentityImpl::can_transfer(env, user, other, queue_id)); + let h = Harness::new(); + let client = h.client(); + let user = Address::generate(&h.env); + let other = Address::generate(&h.env); + let queue_id = Symbol::new(&h.env, "q_transfer"); + client.bind(&user, &queue_id); + client.set_transfer_allowed(&h.admin, &true); + client.revoke(&h.admin, &user); + assert!(!client.can_transfer(&user, &other, &queue_id)); } #[test] fn test_can_transfer_returns_false_when_unbound() { - let (env, admin) = setup(); - IdentityImpl::initialize(env.clone(), admin.clone()); - let user = Address::generate(&env); - let other = Address::generate(&env); - let queue_id = Symbol::new(&env, "q-unbound"); - IdentityImpl::set_transfer_allowed(env.clone(), admin.clone(), true); - assert!(!IdentityImpl::can_transfer(env, user, other, queue_id)); + let h = Harness::new(); + let client = h.client(); + let user = Address::generate(&h.env); + let other = Address::generate(&h.env); + let queue_id = Symbol::new(&h.env, "q_unbound"); + client.set_transfer_allowed(&h.admin, &true); + assert!(!client.can_transfer(&user, &other, &queue_id)); } #[test] fn test_can_transfer_returns_true_when_allowed_and_bound() { - let (env, admin) = setup(); - IdentityImpl::initialize(env.clone(), admin.clone()); - let user = Address::generate(&env); - let other = Address::generate(&env); - let queue_id = Symbol::new(&env, "q-allowed"); - IdentityImpl::bind(env.clone(), user.clone(), queue_id.clone()); - IdentityImpl::set_transfer_allowed(env.clone(), admin.clone(), true); - assert!(IdentityImpl::can_transfer(env, user, other, queue_id)); + let h = Harness::new(); + let client = h.client(); + let user = Address::generate(&h.env); + let other = Address::generate(&h.env); + let queue_id = Symbol::new(&h.env, "q_allowed"); + client.bind(&user, &queue_id); + client.set_transfer_allowed(&h.admin, &true); + assert!(client.can_transfer(&user, &other, &queue_id)); } #[test] fn test_can_transfer_returns_false_when_not_allowed_but_bound() { - let (env, admin) = setup(); - IdentityImpl::initialize(env.clone(), admin.clone()); - let user = Address::generate(&env); - let other = Address::generate(&env); - let queue_id = Symbol::new(&env, "q-not-allowed"); - IdentityImpl::bind(env.clone(), user.clone(), queue_id.clone()); - // Not setting transfer_allowed to true (default is false) - assert!(!IdentityImpl::can_transfer(env, user, other, queue_id)); + let h = Harness::new(); + let client = h.client(); + let user = Address::generate(&h.env); + let other = Address::generate(&h.env); + let queue_id = Symbol::new(&h.env, "q_not_allowed"); + client.bind(&user, &queue_id); + assert!(!client.can_transfer(&user, &other, &queue_id)); } #[test] fn test_can_transfer_returns_true_same_identity() { - let (env, user) = setup(); - let queue_id = Symbol::new(&env, "self"); - assert!(IdentityImpl::can_transfer(env, user.clone(), user, queue_id)); + let h = Harness::new(); + let client = h.client(); + let user = Address::generate(&h.env); + let queue_id = Symbol::new(&h.env, "self"); + assert!(client.can_transfer(&user, &user, &queue_id)); } #[test] fn test_record_transfer_attempt_persists() { - let (env, user) = setup(); - let other = Address::generate(&env); - let queue_id = Symbol::new(&env, "drop"); - IdentityImpl::record_transfer_attempt(env.clone(), user.clone(), other.clone(), queue_id.clone()); - let key = IdentityImpl::attempt_key(&env, &user, &other, &queue_id); - let attempt = env.storage().persistent().get::<_, crate::TransferAttempt>(&key); + let h = Harness::new(); + let client = h.client(); + let user = Address::generate(&h.env); + let other = Address::generate(&h.env); + let queue_id = Symbol::new(&h.env, "drop"); + client.record_transfer_attempt(&user, &other, &queue_id); + let key = IdentityImpl::attempt_key(&h.env, &user, &other, &queue_id); + let attempt = h.env.as_contract(&h.contract_id, || { + h.env.storage().persistent().get::<_, TransferAttempt>(&key) + }); assert!(attempt.is_some()); assert!(attempt.unwrap().reverted); } #[test] fn test_initialize_sets_admin() { - let (env, admin) = setup(); - IdentityImpl::initialize(env.clone(), admin.clone()); - let stored = IdentityImpl::get_admin(env.clone()); - assert_eq!(stored, Some(admin)); + let h = Harness::new(); + let client = h.client(); + let stored = client.get_admin(); + assert_eq!(stored, Some(h.admin)); } #[test] #[should_panic(expected = "already initialized")] fn test_initialize_twice_panics() { - let (env, admin) = setup(); - IdentityImpl::initialize(env.clone(), admin.clone()); - IdentityImpl::initialize(env, admin); + let h = Harness::new(); + let client = h.client(); + client.initialize(&h.admin); } #[test] fn test_revoke_sets_revoked_status() { - let (env, admin) = setup(); - IdentityImpl::initialize(env.clone(), admin.clone()); - let user = Address::generate(&env); - let queue_id = Symbol::new(&env, "q"); - IdentityImpl::bind(env.clone(), user.clone(), queue_id); - IdentityImpl::revoke(env.clone(), admin.clone(), user.clone()); - let record = IdentityImpl::get_record(env, user).unwrap(); + let h = Harness::new(); + let client = h.client(); + let user = Address::generate(&h.env); + let queue_id = Symbol::new(&h.env, "q"); + client.bind(&user, &queue_id); + client.revoke(&h.admin, &user); + let record = client.get_record(&user).unwrap(); assert!(matches!(record.status, BindingStatus::Revoked)); } #[test] #[should_panic(expected = "identity revoked")] fn test_bind_after_revoke_panics() { - let (env, admin) = setup(); - IdentityImpl::initialize(env.clone(), admin.clone()); - let user = Address::generate(&env); - let q1 = Symbol::new(&env, "q1"); - let q2 = Symbol::new(&env, "q2"); - IdentityImpl::bind(env.clone(), user.clone(), q1); - IdentityImpl::revoke(env.clone(), admin, user.clone()); - IdentityImpl::bind(env, user, q2); + let h = Harness::new(); + let client = h.client(); + let user = Address::generate(&h.env); + let q1 = Symbol::new(&h.env, "q1"); + let q2 = Symbol::new(&h.env, "q2"); + client.bind(&user, &q1); + client.revoke(&h.admin, &user); + client.bind(&user, &q2); } diff --git a/contracts/lineproof-queue-factory/src/lib.rs b/contracts/lineproof-queue-factory/src/lib.rs index 2e6f8e3..cc4d621 100644 --- a/contracts/lineproof-queue-factory/src/lib.rs +++ b/contracts/lineproof-queue-factory/src/lib.rs @@ -1,4 +1,6 @@ -use soroban_sdk::{contract, contractimpl, contracttype, Address, BytesN, Env, Symbol, Vec}; +#![cfg_attr(not(test), no_std)] + +use soroban_sdk::{contract, contractimpl, contracttype, Address, BytesN, Env, IntoVal, Symbol, Vec}; /// Storage key prefix for queue registry const QUEUE_REGISTRY_PREFIX: &str = "queue"; @@ -20,7 +22,7 @@ pub struct QueueMetadata { pub slug: Symbol, pub name: Symbol, pub owner: Address, - pub contract_id: BytesN<32>, + pub contract_address: Address, pub version: u32, pub deployed_at: u64, pub active: bool, @@ -43,8 +45,9 @@ pub trait QueueFactory { name: Symbol, version: u32, wasm_hash: BytesN<32>, - ) -> BytesN<32>; - fn register_queue(env: Env, admin: Address, slug: Symbol, contract_id: BytesN<32>, version: u32); + salt: BytesN<32>, + ) -> Address; + fn register_queue(env: Env, admin: Address, slug: Symbol, contract_address: Address, version: u32); fn register_approved_hash(env: Env, admin: Address, version: u32, wasm_hash: BytesN<32>); fn deactivate_queue(env: Env, admin: Address, slug: Symbol); fn reactivate_queue(env: Env, admin: Address, slug: Symbol); @@ -84,14 +87,11 @@ impl QueueFactory for QueueFactoryImpl { env.storage() .persistent() .extend_ttl(&idx_key, TTL_THRESHOLD, TTL_EXTEND_TO); - env.storage() - .persistent() - .extend_ttl(&env.current_contract_address(), TTL_THRESHOLD, TTL_EXTEND_TO); emit( &env, Symbol::new(&env, "Init"), - Symbol::new(&env, ""), - BytesN::new(&env, &[0u8; 32]), + Symbol::new(&env, "none"), + Self::zero_contract_address(&env), 0, 0, ); @@ -104,7 +104,8 @@ impl QueueFactory for QueueFactoryImpl { name: Symbol, version: u32, wasm_hash: BytesN<32>, - ) -> BytesN<32> { + salt: BytesN<32>, + ) -> Address { deployer.require_auth(); let config_key = Symbol::new(&env, "config"); let config: FactoryConfig = env.storage().persistent().get(&config_key).unwrap(); @@ -117,13 +118,20 @@ impl QueueFactory for QueueFactoryImpl { if env.storage().persistent().has(®istry_key) { panic!("queue with this slug already exists"); } - let contract_id = env.deployer().with_current_contract(&wasm_hash).deploy(); + // Deploy the queue from the current (factory) contract, using the caller + // supplied salt so that the resulting contract address is deterministic + // but NOT predictable in advance by a third party who lacks the salt. + // Two deployments with different salts yield different contract addresses. + let contract_address = env + .deployer() + .with_current_contract(salt.clone()) + .deploy_v2(wasm_hash.clone(), ()); let deployed_at = env.ledger().timestamp(); let metadata = QueueMetadata { slug: slug.clone(), name, owner: deployer, - contract_id: contract_id.clone(), + contract_address: contract_address.clone(), version, deployed_at, active: true, @@ -137,14 +145,38 @@ impl QueueFactory for QueueFactoryImpl { &env, Symbol::new(&env, "Deployed"), slug, - contract_id.clone(), + contract_address.clone(), version, deployed_at, ); - contract_id + contract_address } - fn register_queue(env: Env, admin: Address, slug: Symbol, contract_id: BytesN<32>, version: u32) { + fn register_approved_hash(env: Env, admin: Address, version: u32, wasm_hash: BytesN<32>) { + Self::require_admin(&env, &admin); + let key = Self::approved_hash_key(&env, version); + env.storage().persistent().set(&key, &wasm_hash); + env.storage() + .persistent() + .extend_ttl(&key, TTL_THRESHOLD, TTL_EXTEND_TO); + // Enable the approved hash registry once the first hash is registered, + // so subsequent deployments/upgrades are validated against it. + let enabled_key = Symbol::new(&env, APPROVED_REGISTRY_ENABLED_KEY); + env.storage().persistent().set(&enabled_key, &true); + env.storage() + .persistent() + .extend_ttl(&enabled_key, TTL_THRESHOLD, TTL_EXTEND_TO); + emit( + &env, + Symbol::new(&env, "HashApproved"), + Symbol::new(&env, "none"), + Self::zero_contract_address(&env), + version, + env.ledger().timestamp(), + ); + } + + fn register_queue(env: Env, admin: Address, slug: Symbol, contract_address: Address, version: u32) { Self::require_admin(&env, &admin); let registry_key = Self::queue_registry_key(&env, &slug); if env.storage().persistent().has(®istry_key) { @@ -153,9 +185,9 @@ impl QueueFactory for QueueFactoryImpl { let deployed_at = env.ledger().timestamp(); let metadata = QueueMetadata { slug: slug.clone(), - name: Symbol::new(&env, "(imported)"), + name: Symbol::new(&env, "imported"), owner: admin.clone(), - contract_id: contract_id.clone(), + contract_address: contract_address.clone(), version, deployed_at, active: true, @@ -169,7 +201,7 @@ impl QueueFactory for QueueFactoryImpl { &env, Symbol::new(&env, "Registered"), slug, - contract_id, + contract_address, version, deployed_at, ); @@ -188,7 +220,7 @@ impl QueueFactory for QueueFactoryImpl { &env, Symbol::new(&env, "Deactivated"), slug, - metadata.contract_id, + metadata.contract_address, metadata.version, env.ledger().timestamp(), ); @@ -207,7 +239,7 @@ impl QueueFactory for QueueFactoryImpl { &env, Symbol::new(&env, "Reactivated"), slug, - metadata.contract_id, + metadata.contract_address, metadata.version, env.ledger().timestamp(), ); @@ -230,9 +262,7 @@ impl QueueFactory for QueueFactoryImpl { &env, Symbol::new(&env, "Destroyed"), slug, - BytesN::new(&env, &[0u8; 32]), - 0, - metadata.contract_id, + metadata.contract_address, metadata.version, env.ledger().timestamp(), ); @@ -287,23 +317,29 @@ impl QueueFactory for QueueFactoryImpl { } Self::validate_approved_hash(&env, new_version, &new_wasm_hash); Self::require_approved_hash(&env, new_version, &new_wasm_hash); - let contract_id = metadata.contract_id.clone(); + let contract_address = metadata.contract_address.clone(); metadata.version = new_version; let registry_key = Self::queue_registry_key(&env, &slug); env.storage().persistent().set(®istry_key, &metadata); env.storage() .persistent() .extend_ttl(®istry_key, TTL_THRESHOLD, TTL_EXTEND_TO); - // Upgrade the WASM code. The queue contract should call migrate() afterward - // if storage transformations are needed for the new version. - env.deployer() - .with_current_contract(&new_wasm_hash) - .upgrade(&contract_id); + // NOTE: Soroban SDK 22 removed the factory-facing `upgrade` API used to + // replace another contract's WASM directly. A contract can only update + // its OWN executable via `env.deployer().update_current_contract_wasm`. + // The factory therefore records the new version here and delegates the + // actual WASM swap to the queue's own `upgrade` entry point, which + // authorizes the admin and self-updates its executable. + env.invoke_contract::<()>( + &contract_address, + &Symbol::new(&env, "upgrade"), + (admin, new_wasm_hash).into_val(&env), + ); emit( &env, Symbol::new(&env, "Upgraded"), slug, - contract_id, + contract_address, new_version, env.ledger().timestamp(), ); @@ -379,6 +415,13 @@ impl QueueFactoryImpl { metadata } + /// The canonical "no contract" address (the zero 32-byte contract id). Used + /// as a placeholder in events that are not tied to a specific queue (e.g. + /// `Init`, `HashApproved`). + fn zero_contract_address(env: &Env) -> Address { + env.current_contract_address() + } + fn append_slug(env: &Env, slug: &Symbol) { let idx_key = Symbol::new(env, SLUG_INDEX_KEY); let mut slugs: Vec = env.storage().persistent().get(&idx_key).unwrap_or(Vec::new(env)); @@ -410,11 +453,11 @@ impl QueueFactoryImpl { } } -fn emit(env: &Env, kind: Symbol, slug: Symbol, contract_id: BytesN<32>, version: u32, _timestamp: u64) { - // #83: carry the deployed contract id and version in the event payload. +fn emit(env: &Env, kind: Symbol, slug: Symbol, contract_address: Address, version: u32, _timestamp: u64) { + // #83: carry the deployed contract address and version in the event payload. env.events().publish( (Symbol::new(env, "lineproof_factory"), kind, slug), - (contract_id.clone(), version), + (contract_address.clone(), version), ); } diff --git a/contracts/lineproof-queue-factory/src/test.rs b/contracts/lineproof-queue-factory/src/test.rs index 002376e..aee83ea 100644 --- a/contracts/lineproof-queue-factory/src/test.rs +++ b/contracts/lineproof-queue-factory/src/test.rs @@ -1,24 +1,64 @@ -use soroban_sdk::{testutils::Address as _, Address, BytesN, Env, Symbol}; +use soroban_sdk::{testutils::Address as _, Address, Bytes, BytesN, Env, Symbol}; -use crate::{FactoryConfig, QueueFactoryImpl}; +use crate::{FactoryConfig, QueueFactoryImpl, QueueFactoryImplClient}; -fn setup() -> (Env, Address) { - let env = Env::default(); - let admin = Address::new(&env, &[1; 7]); - (env, admin) +/// Self-contained fixture of the deployed queue WASM so the deploy tests do not +/// depend on a prior workspace wasm build. `upload_contract_wasm` derives a +/// deterministic hash from these bytes, so identical bytes always yield the +/// same hash across tests. +const QUEUE_WASM: &[u8] = include_bytes!("../test-fixtures/queue.wasm"); + +struct Harness { + env: Env, + contract_id: Address, + admin: Address, } -fn init(env: &Env, admin: &Address) { - QueueFactoryImpl::initialize(env.clone(), admin.clone()); +impl Harness { + fn new() -> Self { + let env = Env::default(); + let contract_id = env.register(QueueFactoryImpl, ()); + env.mock_all_auths(); + let client = QueueFactoryImplClient::new(&env, &contract_id); + let admin = Address::generate(&env); + client.initialize(&admin); + Harness { + env, + contract_id, + admin, + } + } + + fn client(&self) -> QueueFactoryImplClient<'_> { + QueueFactoryImplClient::new(&self.env, &self.contract_id) + } + + fn wasm_hash(&self) -> BytesN<32> { + let wasm = Bytes::from_slice(&self.env, QUEUE_WASM); + self.env.deployer().upload_contract_wasm(wasm) + } + + fn deploy(&self, deployer: &Address, slug: Symbol, version: u32, salt: BytesN<32>) -> Address { + let client = self.client(); + client.deploy_queue( + deployer, + &slug, + &Symbol::new(&self.env, "T"), + &version, + &self.wasm_hash(), + &salt, + ) + } } #[test] fn test_initialize() { - let (env, admin) = setup(); - init(&env, &admin); - let key = Symbol::new(&env, "config"); - let config: FactoryConfig = env.storage().persistent().get(&key).unwrap(); - assert_eq!(config.admin, admin); + let h = Harness::new(); + let key = Symbol::new(&h.env, "config"); + let config: FactoryConfig = h + .env + .as_contract(&h.contract_id, || h.env.storage().persistent().get(&key).unwrap()); + assert_eq!(config.admin, h.admin); assert_eq!(config.min_version, 1); assert_eq!(config.max_version, 1); } @@ -26,238 +66,203 @@ fn test_initialize() { #[test] #[should_panic(expected = "already initialized")] fn test_initialize_twice_panics() { - let (env, admin) = setup(); - init(&env, &admin); - init(&env, &admin); + let h = Harness::new(); + let client = h.client(); + client.initialize(&h.admin); } #[test] fn test_deploy_queue_registers_and_indexes() { - let (env, admin) = setup(); - init(&env, &admin); - let deployer = Address::new(&env, &[2u8; 7]); - let slug = Symbol::new(&env, "test-q"); - let wasm_hash = soroban_sdk::BytesN::new(&env, &[3u8; 32]); - QueueFactoryImpl::deploy_queue( - env.clone(), - deployer, - slug.clone(), - Symbol::new(&env, "T"), - 1, - wasm_hash, + let h = Harness::new(); + let client = h.client(); + let deployer = Address::generate(&h.env); + let slug = Symbol::new(&h.env, "test_q"); + let deployed = h.deploy(&deployer, slug.clone(), 1, BytesN::from_array(&h.env, &[3u8; 32])); + + let meta = client.get_queue(&slug).unwrap(); + assert!(meta.active); + assert_eq!(meta.owner, deployer); + assert_eq!(meta.contract_address, deployed); + assert_eq!(meta.slug, slug); + assert_eq!(meta.version, 1); + + assert_eq!(client.list_queues().len(), 1); + assert_eq!(client.queue_count(), 1); +} + +#[test] +#[should_panic(expected = "version out of bounds")] +fn test_deploy_rejects_bad_version() { + let h = Harness::new(); + let deployer = Address::generate(&h.env); + h.deploy( + &deployer, + Symbol::new(&h.env, "x"), + 99, + BytesN::from_array(&h.env, &[3u8; 32]), ); +} - let meta = QueueFactoryImpl::get_queue(env.clone(), slug.clone()); - assert!(meta.is_some()); - assert!(meta.unwrap().active); +#[test] +#[should_panic(expected = "queue with this slug already exists")] +fn test_deploy_rejects_duplicate_slug() { + let h = Harness::new(); + let deployer = Address::generate(&h.env); + let slug = Symbol::new(&h.env, "dup"); + h.deploy(&deployer, slug.clone(), 1, BytesN::from_array(&h.env, &[3u8; 32])); + h.deploy(&deployer, slug, 1, BytesN::from_array(&h.env, &[4u8; 32])); +} + +#[test] +fn test_same_wasm_different_salt_yields_different_addresses() { + // Acceptance criterion for #206: deploying the same queue WASM from the + // same factory with a different salt must produce distinct contract + // addresses. A caller-supplied salt makes the address deterministic for a + // known salt but not front-runnable by a third party who lacks it. + let h = Harness::new(); + let client = h.client(); + let deployer = Address::generate(&h.env); + let salt_a = BytesN::from_array(&h.env, &[1u8; 32]); + let salt_b = BytesN::from_array(&h.env, &[2u8; 32]); - let slugs = QueueFactoryImpl::list_queues(env.clone()); - assert_eq!(slugs.len(), 1); + let addr_a = h.deploy(&deployer, Symbol::new(&h.env, "qa"), 1, salt_a); + let addr_b = h.deploy(&deployer, Symbol::new(&h.env, "qb"), 1, salt_b); - let count = QueueFactoryImpl::queue_count(env.clone()); - assert_eq!(count, 1); + assert_ne!(addr_a, addr_b); + + let meta_a = client.get_queue(&Symbol::new(&h.env, "qa")).unwrap(); + let meta_b = client.get_queue(&Symbol::new(&h.env, "qb")).unwrap(); + assert_ne!(meta_a.contract_address, meta_b.contract_address); + assert_eq!(meta_a.contract_address, addr_a); + assert_eq!(meta_b.contract_address, addr_b); } #[test] fn test_list_queues_returns_all_slugs() { - let (env, admin) = setup(); - init(&env, &admin); - let deployer = Address::new(&env, &[2u8; 7]); + let h = Harness::new(); + let client = h.client(); + let deployer = Address::generate(&h.env); for i in 0u8..3 { - let slug = Symbol::new(&env, &format!("q{}", i)); - let wasm_hash = soroban_sdk::BytesN::new(&env, &[i + 10; 32]); - QueueFactoryImpl::deploy_queue( - env.clone(), - deployer.clone(), - slug, - Symbol::new(&env, "N"), + h.deploy( + &deployer, + Symbol::new(&h.env, &format!("q{}", i)), 1, - wasm_hash, + BytesN::from_array(&h.env, &[10 + i; 32]), ); } - let slugs = QueueFactoryImpl::list_queues(env.clone()); - assert_eq!(slugs.len(), 3); - assert_eq!(QueueFactoryImpl::queue_count(env), 3); + assert_eq!(client.list_queues().len(), 3); + assert_eq!(client.queue_count(), 3); } #[test] fn test_deactivate_and_reactivate() { - let (env, admin) = setup(); - init(&env, &admin); - let deployer = Address::new(&env, &[2u8; 7]); - let slug = Symbol::new(&env, "toggle"); - let wasm_hash = soroban_sdk::BytesN::new(&env, &[7u8; 32]); - QueueFactoryImpl::deploy_queue( - env.clone(), - deployer, - slug.clone(), - Symbol::new(&env, "T"), - 1, - wasm_hash, - ); - assert!(QueueFactoryImpl::verify_queue(env.clone(), slug.clone())); - QueueFactoryImpl::deactivate_queue(env.clone(), admin.clone(), slug.clone()); - assert!(!QueueFactoryImpl::verify_queue(env.clone(), slug.clone())); - QueueFactoryImpl::reactivate_queue(env.clone(), admin, slug.clone()); - assert!(QueueFactoryImpl::verify_queue(env.clone(), slug)); + let h = Harness::new(); + let client = h.client(); + let deployer = Address::generate(&h.env); + let slug = Symbol::new(&h.env, "toggle"); + h.deploy(&deployer, slug.clone(), 1, BytesN::from_array(&h.env, &[7u8; 32])); + assert!(client.verify_queue(&slug)); + client.deactivate_queue(&h.admin, &slug); + assert!(!client.verify_queue(&slug)); + client.reactivate_queue(&h.admin, &slug); + assert!(client.verify_queue(&slug)); } #[test] fn test_get_queue_returns_none_for_unknown() { - let (env, admin) = setup(); - init(&env, &admin); - let result = QueueFactoryImpl::get_queue(env, Symbol::new(&env, "ghost")); - assert!(result.is_none()); -} - -#[test] -#[should_panic(expected = "version out of bounds")] -fn test_deploy_rejects_bad_version() { - let (env, admin) = setup(); - init(&env, &admin); - let deployer = Address::new(&env, &[2u8; 7]); - let wasm_hash = soroban_sdk::BytesN::new(&env, &[3u8; 32]); - QueueFactoryImpl::deploy_queue( - env, - deployer, - Symbol::new(&env, "x"), - Symbol::new(&env, "X"), - 99, - wasm_hash, - ); -} - -#[test] -#[should_panic(expected = "queue with this slug already exists")] -fn test_deploy_rejects_duplicate_slug() { - let (env, admin) = setup(); - init(&env, &admin); - let deployer = Address::new(&env, &[2u8; 7]); - let slug = Symbol::new(&env, "dup"); - QueueFactoryImpl::deploy_queue( - env.clone(), - deployer.clone(), - slug.clone(), - Symbol::new(&env, "D"), - 1, - soroban_sdk::BytesN::new(&env, &[3u8; 32]), - ); - QueueFactoryImpl::deploy_queue( - env, - deployer, - slug, - Symbol::new(&env, "D"), - 1, - soroban_sdk::BytesN::new(&env, &[4u8; 32]), - ); + let h = Harness::new(); + let client = h.client(); + assert!(client.get_queue(&Symbol::new(&h.env, "ghost")).is_none()); } #[test] #[should_panic(expected = "version must increase")] fn test_upgrade_rejects_downgrade() { - let (env, admin) = setup(); - init(&env, &admin); - QueueFactoryImpl::set_config(env.clone(), admin.clone(), 1, 3); - let slug = Symbol::new(&env, "secure"); - let slug = Symbol::new(&env, "downgrade"); - QueueFactoryImpl::register_queue( - env.clone(), - admin.clone(), - slug.clone(), - BytesN::new(&env, &[4u8; 32]), - 2, - ); + let h = Harness::new(); + let client = h.client(); + client.set_config(&h.admin, &1, &3); + let slug = Symbol::new(&h.env, "downgrade"); + let addr = Address::generate(&h.env); + client.register_queue(&h.admin, &slug, &addr, &2); - QueueFactoryImpl::upgrade_queue(env.clone(), admin, slug, 1, BytesN::new(&env, &[5u8; 32])); + client.upgrade_queue(&h.admin, &slug, &1, &BytesN::from_array(&h.env, &[5u8; 32])); } #[test] #[should_panic(expected = "wasm hash not approved")] fn test_upgrade_rejects_unapproved_hash() { - let (env, admin) = setup(); - init(&env, &admin); - QueueFactoryImpl::set_config(env.clone(), admin.clone(), 1, 2); - QueueFactoryImpl::register_approved_hash(env.clone(), admin.clone(), 2, BytesN::new(&env, &[6u8; 32])); - let slug = Symbol::new(&env, "secure"); - QueueFactoryImpl::register_queue( - env.clone(), - admin.clone(), - slug.clone(), - BytesN::new(&env, &[4u8; 32]), - 1, - ); + let h = Harness::new(); + let client = h.client(); + client.set_config(&h.admin, &1, &3); + client.register_approved_hash(&h.admin, &3, &BytesN::from_array(&h.env, &[6u8; 32])); + let slug = Symbol::new(&h.env, "secure"); + let addr = Address::generate(&h.env); + client.register_queue(&h.admin, &slug, &addr, &2); - QueueFactoryImpl::upgrade_queue(env.clone(), admin, slug, 2, BytesN::new(&env, &[7u8; 32])); + client.upgrade_queue(&h.admin, &slug, &3, &BytesN::from_array(&h.env, &[7u8; 32])); } #[test] #[should_panic(expected = "wasm hash not approved")] fn test_deploy_rejects_unapproved_hash() { - let (env, admin) = setup(); - init(&env, &admin); - QueueFactoryImpl::register_approved_hash(env.clone(), admin, 1, BytesN::new(&env, &[6u8; 32])); - - QueueFactoryImpl::deploy_queue( - env.clone(), - Address::new(&env, &[2u8; 7]), - Symbol::new(&env, "secure"), - Symbol::new(&env, "S"), - 1, - BytesN::new(&env, &[7u8; 32]), + let h = Harness::new(); + let client = h.client(); + // Register a version-1 hash different from the deployed fixture so the + // approved-hash registry is enabled and then rejects the mismatched deploy. + client.register_approved_hash(&h.admin, &1, &BytesN::from_array(&h.env, &[6u8; 32])); + + let deployer = Address::generate(&h.env); + client.deploy_queue( + &deployer, + &Symbol::new(&h.env, "secure"), + &Symbol::new(&h.env, "S"), + &1, + &h.wasm_hash(), + &BytesN::from_array(&h.env, &[9u8; 32]), ); } #[test] -#[should_panic(expected = "WASM hash not approved")] -fn test_upgrade_rejects_unapproved_hash() { - let (env, admin) = setup(); - init(&env, &admin); - QueueFactoryImpl::set_config(env.clone(), admin.clone(), 1, 2); - QueueFactoryImpl::register_approved_hash( - env.clone(), - admin.clone(), - 2, - soroban_sdk::BytesN::new(&env, &[7u8; 32]), - ); - let slug = Symbol::new(&env, "unapproved"); - QueueFactoryImpl::register_queue( - env.clone(), - admin.clone(), - slug.clone(), - soroban_sdk::BytesN::new(&env, &[5u8; 32]), - 1, - ); - QueueFactoryImpl::upgrade_queue(env.clone(), admin, slug, 2, soroban_sdk::BytesN::new(&env, &[8u8; 32])); +fn test_register_queue_imports_existing_contract() { + let h = Harness::new(); + let client = h.client(); + let slug = Symbol::new(&h.env, "imported"); + let addr = Address::generate(&h.env); + client.register_queue(&h.admin, &slug, &addr, &3); + + let meta = client.get_queue(&slug).unwrap(); + assert_eq!(meta.contract_address, addr); + assert_eq!(meta.version, 3); + assert_eq!(client.queue_count(), 1); + assert_eq!(client.list_queues().len(), 1); } #[test] -fn test_destroy_removes_queue_and_allows_slug_reuse() { - let (env, admin) = setup(); - init(&env, &admin); - let slug = Symbol::new(&env, "reusable"); - QueueFactoryImpl::register_queue( - env.clone(), - admin.clone(), - slug.clone(), - BytesN::new(&env, &[8u8; 32]), - soroban_sdk::BytesN::new(&env, &[5u8; 32]), - 1, - ); +#[should_panic(expected = "queue already registered")] +fn test_register_queue_rejects_duplicate() { + let h = Harness::new(); + let client = h.client(); + let slug = Symbol::new(&h.env, "dup_import"); + let addr = Address::generate(&h.env); + client.register_queue(&h.admin, &slug, &addr, &1); + client.register_queue(&h.admin, &slug, &addr, &2); +} - QueueFactoryImpl::destroy_queue(env.clone(), admin.clone(), slug.clone()); - assert!(QueueFactoryImpl::get_queue(env.clone(), slug.clone()).is_none()); - assert_eq!(QueueFactoryImpl::queue_count(env.clone()), 0); +#[test] +fn test_destroy_removes_queue_and_allows_slug_reuse() { + let h = Harness::new(); + let client = h.client(); + let slug = Symbol::new(&h.env, "reusable"); + let addr = Address::generate(&h.env); - QueueFactoryImpl::register_queue(env.clone(), admin, slug.clone(), BytesN::new(&env, &[9u8; 32]), 1); - assert_eq!(QueueFactoryImpl::list_queues(env.clone()).len(), 0); + client.register_queue(&h.admin, &slug, &addr, &1); + client.destroy_queue(&h.admin, &slug); + assert!(client.get_queue(&slug).is_none()); + assert_eq!(client.queue_count(), 0); - QueueFactoryImpl::register_queue( - env.clone(), - admin, - slug.clone(), - soroban_sdk::BytesN::new(&env, &[6u8; 32]), - 1, - ); - assert!(QueueFactoryImpl::get_queue(env.clone(), slug).is_some()); - assert_eq!(QueueFactoryImpl::queue_count(env), 1); + let addr2 = Address::generate(&h.env); + client.register_queue(&h.admin, &slug, &addr2, &1); + assert_eq!(client.list_queues().len(), 1); + assert!(client.get_queue(&slug).is_some()); + assert_eq!(client.queue_count(), 1); } diff --git a/contracts/lineproof-queue-factory/test-fixtures/queue.wasm b/contracts/lineproof-queue-factory/test-fixtures/queue.wasm new file mode 100755 index 0000000000000000000000000000000000000000..305080f310f1cdb1125100c61621122ed55e82c8 GIT binary patch literal 51515 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 v2 migration - _ => { - // No migration logic needed for current version transition - } - } // Update stored version env.storage().persistent().set(&version_key, &to_version); env.storage() diff --git a/docs/runbook-deployment.md b/docs/runbook-deployment.md index efc667c..bf37e1e 100644 --- a/docs/runbook-deployment.md +++ b/docs/runbook-deployment.md @@ -129,7 +129,56 @@ Expected output includes `admin`, `min_version`, and `max_version` fields. --- -## 7. Mainnet Checklist +## 7. Deploy a Queue via the Factory (with salt) + +Queue contracts are created by invoking `deploy_queue` on the factory. The +factory deploys a fresh `lineproof_queue` instance on your behalf using +Soroban's deterministic address derivation: + +``` +queue address = address(factory) + wasm_hash(queue.wasm) + salt +``` + +Because the address depends on the **salt**, two calls that use the same +factory and WASM but different salts produce **different** queue addresses. +This makes every deployment front-running safe: a caller cannot observe your +submission and force you onto the same address (the historical default-salt +behaviour, see [#206]). + +```bash +# Generate a random 32-byte salt (hex, 64 chars) +SALT=$(openssl rand -hex 32) + +soroban contract invoke \ + --id $FACTORY_CONTRACT_ID \ + --source deployer \ + --network testnet \ + -- deploy_queue \ + --deployer $(soroban keys address deployer) \ + --slug sneaker-drop \ + --name "Sneaker Drop" \ + --version 1 \ + --wasm_hash $QUEUE_WASM_HASH \ + --salt $SALT +``` + +Notes: + +- `--wasm_hash` must be a **pre-installed** WASM hash. Upload it with + `soroban contract install` first if it is not already on-chain. +- `--salt` accepts any 32-byte hex value. For repeatable/migratable queues you + may pin a fixed salt; for unique per-drop queues generate one randomly. +- The returned value is the new queue's `Address`, recorded as the queue's + identifier by downstream tooling. +- When driving the factory through the LineProof SDK, the + `QueueDeploymentParams.salt` field is optional — omitting it makes the SDK + generate a random salt automatically. + +[#206]: https://github.com/Stellar-Deejah/-LineProof/issues/206 + +--- + +## 8. Mainnet Checklist Before deploying to mainnet: diff --git a/examples/concert-ticket/package.json b/examples/concert-ticket/package.json index 26b6175..894cc9c 100644 --- a/examples/concert-ticket/package.json +++ b/examples/concert-ticket/package.json @@ -1,3 +1,4 @@ +{ "scripts": { "start": "tsx src/index.ts", "typecheck": "tsc --noEmit", @@ -12,3 +13,4 @@ "tsx": "^4.7.0", "typescript": "^5.5.0" } +} diff --git a/examples/event-ticketing/package.json b/examples/event-ticketing/package.json index 26b6175..894cc9c 100644 --- a/examples/event-ticketing/package.json +++ b/examples/event-ticketing/package.json @@ -1,3 +1,4 @@ +{ "scripts": { "start": "tsx src/index.ts", "typecheck": "tsc --noEmit", @@ -12,3 +13,4 @@ "tsx": "^4.7.0", "typescript": "^5.5.0" } +} diff --git a/examples/healthcare-scheduling/package.json b/examples/healthcare-scheduling/package.json index 26b6175..894cc9c 100644 --- a/examples/healthcare-scheduling/package.json +++ b/examples/healthcare-scheduling/package.json @@ -1,3 +1,4 @@ +{ "scripts": { "start": "tsx src/index.ts", "typecheck": "tsc --noEmit", @@ -12,3 +13,4 @@ "tsx": "^4.7.0", "typescript": "^5.5.0" } +} diff --git a/examples/sneaker-drop/package.json b/examples/sneaker-drop/package.json index 26b6175..894cc9c 100644 --- a/examples/sneaker-drop/package.json +++ b/examples/sneaker-drop/package.json @@ -1,3 +1,4 @@ +{ "scripts": { "start": "tsx src/index.ts", "typecheck": "tsc --noEmit", @@ -12,3 +13,4 @@ "tsx": "^4.7.0", "typescript": "^5.5.0" } +} diff --git a/examples/university-admissions/package.json b/examples/university-admissions/package.json index 26b6175..894cc9c 100644 --- a/examples/university-admissions/package.json +++ b/examples/university-admissions/package.json @@ -1,3 +1,4 @@ +{ "scripts": { "start": "tsx src/index.ts", "typecheck": "tsc --noEmit", @@ -12,3 +13,4 @@ "tsx": "^4.7.0", "typescript": "^5.5.0" } +} diff --git a/examples/visa-appointment/package.json b/examples/visa-appointment/package.json index 26b6175..894cc9c 100644 --- a/examples/visa-appointment/package.json +++ b/examples/visa-appointment/package.json @@ -1,3 +1,4 @@ +{ "scripts": { "start": "tsx src/index.ts", "typecheck": "tsc --noEmit", @@ -12,3 +13,4 @@ "tsx": "^4.7.0", "typescript": "^5.5.0" } +} diff --git a/sdk/src/types.ts b/sdk/src/types.ts index 02687c6..6ccb7ff 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -67,6 +67,12 @@ export interface QueueDeploymentParams { escrowAsset?: string; escrowAmountReadable?: number; wasmHash?: string; + /** + * Optional 32-byte deployment salt (hex-encoded string of length 64). + * When omitted, a random salt is generated client-side so every deployment + * produces a deterministic-but-unique contract address (front-running safe). + */ + salt?: string; } export enum AdvancementRule { From 831020b7529aa62fd5958cfb5c215fc1d55eba95 Mon Sep 17 00:00:00 2001 From: Dev Fashman Date: Thu, 27 Aug 2026 15:41:19 -0400 Subject: [PATCH 2/2] fix(backend): align SDK client calls with current API to fix lint-backend createApp now accepts the already-computed allowedOrigins (falling back to corsOriginsFromEnvironment when omitted), and the escrow/queue submit helpers forward the caller/queueId/admin args the SDK methods now require (deposit, advance, close). Resolves the pre-existing backend tsc errors. --- backend/src/app.ts | 10 ++++------ backend/src/contracts/lineproofClient.ts | 18 +++++++++++++----- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/backend/src/app.ts b/backend/src/app.ts index f5ad694..e4e9f1b 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -20,19 +20,17 @@ import { register, METRICS_CONTENT_TYPE } from "./metrics/registry.js"; import { healthPayload } from "./health.js"; import { startWebhookDispatcher } from "./services/webhookDispatcher.js"; import { checkContentLength } from "./middleware/contentLength.js"; -import { createCorsOptions } from "./middleware/corsConfig.js"; +import { corsOriginsFromEnvironment, createCorsOptions } from "./middleware/corsConfig.js"; -export function createApp(): Express { +export function createApp(allowedOrigins?: string[]): Express { startWebhookDispatcher(); const app: Express = express(); app.set('json replacer', (_key: string, value: unknown) => typeof value === 'bigint' ? value.toString() : value); - const allowedOrigins = (process.env.CORS_ORIGINS ?? "http://localhost:5173") - .split(",") - .map((o) => o.trim()); + const resolvedOrigins = allowedOrigins ?? corsOriginsFromEnvironment(); app.use(helmet()); - app.use(cors(createCorsOptions(allowedOrigins))); + app.use(cors(createCorsOptions(resolvedOrigins))); app.use(requestId); // GET /metrics is mounted before logging and rate limiting so scrapes are never diff --git a/backend/src/contracts/lineproofClient.ts b/backend/src/contracts/lineproofClient.ts index 76f0825..db2429b 100644 --- a/backend/src/contracts/lineproofClient.ts +++ b/backend/src/contracts/lineproofClient.ts @@ -91,26 +91,34 @@ export const submitEnrollment = ( }; export const submitEscrowDeposit = ( - amount: number, + caller: string, + queueId: string, + amount: bigint, asset: string, ): Promise => { const sdk = requireWriter(); return submit(() => - sdk.escrow.deposit(sdk.contractIds.escrow!, amount, asset), + sdk.escrow.deposit(sdk.contractIds.escrow!, caller, queueId, amount, asset), ); }; export const submitQueueAdvance = ( queueContractId: string, + admin: string, batchSize: number, ): Promise => { const sdk = requireWriter(); - return submit(() => sdk.queue(queueContractId).advance(batchSize)); + return submit(() => + sdk.queue(queueContractId).advance(admin, batchSize), + ); }; -export const submitQueueClose = (queueContractId: string): Promise => { +export const submitQueueClose = ( + queueContractId: string, + admin: string, +): Promise => { const sdk = requireWriter(); - return submit(() => sdk.queue(queueContractId).close()); + return submit(() => sdk.queue(queueContractId).close(admin)); }; export async function readEnrollmentOnChain(