diff --git a/Cargo.toml b/Cargo.toml index 65e2123..87f3ac6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,13 +4,13 @@ resolver = "2" members = ["contracts/vc-vault"] [workspace.package] -version = "0.20.0" +version = "0.21.0" edition = "2021" license = "Apache-2.0" repository = "https://github.com/ACTA-Team/contracts" [workspace.dependencies] -soroban-sdk = { version = "21.0.0" } +soroban-sdk = { version = "23.4.0" } [profile.release] opt-level = "z" @@ -22,6 +22,7 @@ panic = "abort" codegen-units = 1 lto = true +# For more information about this profile see https://soroban.stellar.org/docs/basic-tutorials/logging#cargotoml-profile [profile.release-with-logs] inherits = "release" debug-assertions = true diff --git a/contracts/vc-vault/src/api/mod.rs b/contracts/vc-vault/src/api/mod.rs index e67bd9b..3b3873f 100644 --- a/contracts/vc-vault/src/api/mod.rs +++ b/contracts/vc-vault/src/api/mod.rs @@ -45,4 +45,9 @@ pub trait VcVaultTrait { ) -> String; fn revoke(e: Env, vc_id: String, date: String); fn migrate(e: Env, owner: Option
); + fn create_sponsored_vault(e: Env, sponsor: Address, owner: Address, did_uri: String); + fn set_sponsored_vault_open_to_all(e: Env, open: bool); + fn get_sponsored_vault_open_to_all(e: Env) -> bool; + fn add_sponsored_vault_sponsor(e: Env, sponsor: Address); + fn remove_sponsored_vault_sponsor(e: Env, sponsor: Address); } diff --git a/contracts/vc-vault/src/contract.rs b/contracts/vc-vault/src/contract.rs index fee8fe7..3bfe839 100644 --- a/contracts/vc-vault/src/contract.rs +++ b/contracts/vc-vault/src/contract.rs @@ -300,6 +300,62 @@ impl VcVaultTrait for VcVaultContract { storage::extend_vc_status_ttl(&e, &vc_id); } + // --- Sponsored vault --- + + /// Create a vault on behalf of `owner`. `sponsor` must sign. + /// If restricted (open_to_all = false): sponsor must be contract admin or in sponsors list. + /// If open (open_to_all = true): any signer can be sponsor. + fn create_sponsored_vault(e: Env, sponsor: Address, owner: Address, did_uri: String) { + sponsor.require_auth(); + if !storage::has_contract_admin(&e) { + panic_with_error!(e, ContractError::NotInitialized); + } + if !storage::read_sponsored_vault_open_to_all(&e) { + let admin = storage::read_contract_admin(&e); + if sponsor != admin && !storage::is_authorized_sponsor(&e, &sponsor) { + panic_with_error!(e, ContractError::NotAuthorizedSponsor); + } + } + if storage::has_vault_admin(&e, &owner) { + panic_with_error!(e, ContractError::AlreadyInitialized); + } + storage::write_vault_admin(&e, &owner, &owner); + storage::write_vault_did(&e, &owner, &did_uri); + storage::write_vault_revoked(&e, &owner, &false); + storage::write_vault_issuers(&e, &owner, &Vec::new(&e)); + storage::extend_vault_ttl(&e, &owner); + storage::extend_instance_ttl(&e); + } + + /// Enable or disable the sponsor restriction. Admin only. + /// open = false (default): only admin or sponsors list can create sponsored vaults. + /// open = true: anyone can create sponsored vaults. + fn set_sponsored_vault_open_to_all(e: Env, open: bool) { + validate_contract_admin(&e); + storage::write_sponsored_vault_open_to_all(&e, &open); + storage::extend_instance_ttl(&e); + } + + /// Query whether sponsored vault creation is open to all. + fn get_sponsored_vault_open_to_all(e: Env) -> bool { + storage::extend_instance_ttl(&e); + storage::read_sponsored_vault_open_to_all(&e) + } + + /// Add an address to the authorized sponsors list. Admin only. + fn add_sponsored_vault_sponsor(e: Env, sponsor: Address) { + validate_contract_admin(&e); + storage::add_sponsored_vault_sponsor(&e, &sponsor); + storage::extend_instance_ttl(&e); + } + + /// Remove an address from the authorized sponsors list. Admin only. + fn remove_sponsored_vault_sponsor(e: Env, sponsor: Address) { + validate_contract_admin(&e); + storage::remove_sponsored_vault_sponsor(&e, &sponsor); + storage::extend_instance_ttl(&e); + } + // --- Migrations --- /// Migrate legacy storage. Some(owner) = vault migration; None = issuance registry migration. diff --git a/contracts/vc-vault/src/error.rs b/contracts/vc-vault/src/error.rs index 9875d5a..d68e0c3 100644 --- a/contracts/vc-vault/src/error.rs +++ b/contracts/vc-vault/src/error.rs @@ -26,4 +26,6 @@ pub enum ContractError { NotInitialized = 9, /// vault_contract param is not this contract. InvalidVaultContract = 10, + /// Signer is not the contract admin nor an authorized sponsor. + NotAuthorizedSponsor = 11, } diff --git a/contracts/vc-vault/src/storage/mod.rs b/contracts/vc-vault/src/storage/mod.rs index baf3e11..f37dee2 100644 --- a/contracts/vc-vault/src/storage/mod.rs +++ b/contracts/vc-vault/src/storage/mod.rs @@ -37,6 +37,8 @@ pub enum DataKey { LegacyIssuanceRevocations, LegacyIssuanceVCs, LegacyVaultVCs(Address), + SponsoredVaultOpenToAll, + SponsoredVaultSponsors, } /// Legacy revocation record for migration. @@ -393,6 +395,54 @@ pub fn extend_vc_status_ttl(e: &Env, vc_id: &String) { } } +// --- Sponsored vault config (instance) --- + +pub fn read_sponsored_vault_open_to_all(e: &Env) -> bool { + e.storage() + .instance() + .get(&DataKey::SponsoredVaultOpenToAll) + .unwrap_or(false) +} + +pub fn write_sponsored_vault_open_to_all(e: &Env, open: &bool) { + e.storage() + .instance() + .set(&DataKey::SponsoredVaultOpenToAll, open); +} + +pub fn read_sponsored_vault_sponsors(e: &Env) -> Vec
{ + e.storage() + .instance() + .get(&DataKey::SponsoredVaultSponsors) + .unwrap_or_else(|| Vec::new(e)) +} + +pub fn write_sponsored_vault_sponsors(e: &Env, sponsors: &Vec
) { + e.storage() + .instance() + .set(&DataKey::SponsoredVaultSponsors, sponsors); +} + +pub fn is_authorized_sponsor(e: &Env, sponsor: &Address) -> bool { + read_sponsored_vault_sponsors(e).contains(sponsor.clone()) +} + +pub fn add_sponsored_vault_sponsor(e: &Env, sponsor: &Address) { + let mut sponsors = read_sponsored_vault_sponsors(e); + if !sponsors.contains(sponsor.clone()) { + sponsors.push_front(sponsor.clone()); + write_sponsored_vault_sponsors(e, &sponsors); + } +} + +pub fn remove_sponsored_vault_sponsor(e: &Env, sponsor: &Address) { + let mut sponsors = read_sponsored_vault_sponsors(e); + if let Some(idx) = sponsors.first_index_of(sponsor.clone()) { + sponsors.remove(idx); + write_sponsored_vault_sponsors(e, &sponsors); + } +} + // --- Legacy (migration) --- pub fn read_legacy_issuance_vcs(e: &Env) -> Option> { diff --git a/contracts/vc-vault/src/test.rs b/contracts/vc-vault/src/test.rs index 9be1fc0..d036acc 100644 --- a/contracts/vc-vault/src/test.rs +++ b/contracts/vc-vault/src/test.rs @@ -430,3 +430,100 @@ fn test_migrate_some_without_legacy_vault_panics() { client.create_vault(&owner, &String::from_str(&env, "did:pkh:stellar:testnet:OWNER")); client.migrate(&Some(owner)); } + +// --- Sponsored vault tests --- + +#[test] +fn test_sponsored_vault_open_to_all_defaults_false() { + let (env, admin, _issuer, _contract_id, client) = setup(); + client.initialize(&admin, &String::from_str(&env, "did:acta:default")); + assert!(!client.get_sponsored_vault_open_to_all()); +} + +#[test] +fn test_admin_creates_sponsored_vault() { + let (env, admin, _issuer, _contract_id, client) = setup(); + client.initialize(&admin, &String::from_str(&env, "did:acta:default")); + let owner = Address::generate(&env); + let did_uri = String::from_str(&env, "did:pkh:stellar:testnet:OWNER"); + client.create_sponsored_vault(&admin, &owner, &did_uri); + // Vault exists: list_vc_ids returns empty without panicking. + assert_eq!(client.list_vc_ids(&owner).len(), 0); +} + +#[test] +fn test_authorized_sponsor_creates_sponsored_vault() { + let (env, admin, _issuer, _contract_id, client) = setup(); + client.initialize(&admin, &String::from_str(&env, "did:acta:default")); + let sponsor = Address::generate(&env); + client.add_sponsored_vault_sponsor(&sponsor); + let owner = Address::generate(&env); + let did_uri = String::from_str(&env, "did:pkh:stellar:testnet:OWNER"); + client.create_sponsored_vault(&sponsor, &owner, &did_uri); + assert_eq!(client.list_vc_ids(&owner).len(), 0); +} + +#[test] +#[should_panic] +fn test_unauthorized_address_cannot_create_sponsored_vault_in_restricted_mode() { + let (env, admin, _issuer, _contract_id, client) = setup(); + client.initialize(&admin, &String::from_str(&env, "did:acta:default")); + // Confirm restricted mode (default). + assert!(!client.get_sponsored_vault_open_to_all()); + let random = Address::generate(&env); + let owner = Address::generate(&env); + let did_uri = String::from_str(&env, "did:pkh:stellar:testnet:OWNER"); + client.create_sponsored_vault(&random, &owner, &did_uri); +} + +#[test] +fn test_open_mode_allows_anyone_to_create_sponsored_vault() { + let (env, admin, _issuer, _contract_id, client) = setup(); + client.initialize(&admin, &String::from_str(&env, "did:acta:default")); + client.set_sponsored_vault_open_to_all(&true); + assert!(client.get_sponsored_vault_open_to_all()); + let random = Address::generate(&env); + let owner = Address::generate(&env); + let did_uri = String::from_str(&env, "did:pkh:stellar:testnet:OWNER"); + client.create_sponsored_vault(&random, &owner, &did_uri); + assert_eq!(client.list_vc_ids(&owner).len(), 0); +} + +#[test] +#[should_panic] +fn test_back_to_restricted_mode_blocks_unauthorized() { + let (env, admin, _issuer, _contract_id, client) = setup(); + client.initialize(&admin, &String::from_str(&env, "did:acta:default")); + client.set_sponsored_vault_open_to_all(&true); + client.set_sponsored_vault_open_to_all(&false); + let random = Address::generate(&env); + let owner = Address::generate(&env); + let did_uri = String::from_str(&env, "did:pkh:stellar:testnet:OWNER"); + client.create_sponsored_vault(&random, &owner, &did_uri); +} + +#[test] +#[should_panic] +fn test_removed_sponsor_cannot_create_sponsored_vault() { + let (env, admin, _issuer, _contract_id, client) = setup(); + client.initialize(&admin, &String::from_str(&env, "did:acta:default")); + let sponsor = Address::generate(&env); + client.add_sponsored_vault_sponsor(&sponsor); + client.remove_sponsored_vault_sponsor(&sponsor); + let owner = Address::generate(&env); + let did_uri = String::from_str(&env, "did:pkh:stellar:testnet:OWNER"); + // Must fail: sponsor was removed. + client.create_sponsored_vault(&sponsor, &owner, &did_uri); +} + +#[test] +#[should_panic] +fn test_duplicate_sponsored_vault_panics() { + let (env, admin, _issuer, _contract_id, client) = setup(); + client.initialize(&admin, &String::from_str(&env, "did:acta:default")); + let owner = Address::generate(&env); + let did_uri = String::from_str(&env, "did:pkh:stellar:testnet:OWNER"); + client.create_sponsored_vault(&admin, &owner, &did_uri); + // Second creation for same owner must fail. + client.create_sponsored_vault(&admin, &owner, &did_uri); +}