-
Notifications
You must be signed in to change notification settings - Fork 0
feat: implement Sponsored Vault #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
54b1e17
0a74667
4a22fc6
9a3bab1
1d957e2
5bb86b9
18411e0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Access Control and Authorization: Unconsented vault initialization for owner in create_sponsored_vault
Require explicit owner consent (e.g., Actions
|
||
| 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. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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); | ||
| } | ||
|
Comment on lines
+444
to
+452
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
These assertions can pass even if vault creation silently failed, because 💡 Suggested test assertion hardening- // Vault exists: list_vc_ids returns empty without panicking.
- assert_eq!(client.list_vc_ids(&owner).len(), 0);
+ // Proves vault initialization path succeeded.
+ let new_admin = Address::generate(&env);
+ client.set_vault_admin(&owner, &new_admin);Also applies to: 455-464, 480-490 🤖 Prompt for AI Agents |
||
|
|
||
| #[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); | ||
| } | ||

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
soroban-sdk 23.4.0 breaking changes from version 21 release notes changelog💡 Result:
Breaking changes when upgrading soroban-sdk v21 → v23.4.0
v21 → v22 (breaking API changes)
Env::register/Env::register_atreplaceEnv::register_contract/Env::register_contract_wasm. [1]DeployerWithAddress::deploy_v2replacesDeployerWithAddress::deploy(adds constructor-args parameter; pass()if none). [1]fuzz_catch_panicdeprecated; useEnv::try_invoke_contractand generatedtry_client methods. [1]v22 → v23 (breaking behavior / interface changes)
#[contractevent]replacesEvents::publish(old publish path deprecated). [1]MuxedAddressreplacesAddressas thetoargument ofTokenInterface::transfer(insoroban-token-sdk, but it impacts contracts using that interface). [1]v23.0.0 → v23.4.0
Sources: docs.rs migration guide [1], GitHub releases v23.4.0 [2] and v23.0.0 [3].
🌐 Web query:
soroban-sdk Address require_auth Env mock_all_auths breaking changes 22 23💡 Result:
In the soroban-sdk v22 → v23 timeframe, the main auth-related “gotchas” behind
Address::require_auth+Env::mock_all_authstest failures are:Address::require_auth()is “require_auth_for_argswith implied args”require_auth()authorizes the current contract invocation including all invocation arguments, which are automatically inferred (it’s equivalent to callingrequire_auth_for_argswith the invocation’s args). This matters because auth matching/mocking is sensitive to the args being authorized. (docs.rs)Env::mock_all_auths()became stricter about “root invocation” auth coverageIn testutils,
mock_all_auths()mocks allrequire_auth/require_auth_for_argscalls, but it will error if an address is only authorized in a non-root sub-invocation and that address wasn’trequire_auth’d in the root invocation. This commonly shows up when Contract B calls Contract A, and only A doesrequire_auth. (docs.rs)New escape hatch:
mock_all_auths_allowing_non_root_auth()To support the “auth only happens in a subcall” pattern in tests, v23-era SDKs provide
Env::mock_all_auths_allowing_non_root_auth(), which is identical tomock_all_auths()except it does not fail when the root invocation didn’trequire_authfor a given address. (docs.rs)If you share the exact error/panic text and a minimal test snippet (root call + any cross-contract calls), I can point to which of the above is triggering and what to change.
Citations:
🌐 Web query:
soroban-sdk storage TTL API changes version 23 migration guide💡 Result:
What changed in Protocol/Soroban SDK v23 around storage TTL / archival
InvokeHostFunctioncan automatically restore archived entries in the transaction footprint before contract execution, so “manual restoration” is generally no longer required. This also changes how apps should think about “expired/archived” state. [1][2]soroban-sdkchanged to match P23: In SDK 23, the test environment no longer errors when you read an entry whose TTL you’ve “expired” by advancing the ledger; instead it emulates auto-restoration, so reads may succeed where SDK 22 tests expected failure (this includes instance storage, since it is persistent too). [3]Migration guide (v22 ➜ v23)
Fix tests that expected archived reads to fail
get_ttlin testutils) rather than relying on an “archived read throws” behavior. [3]Re-check your “restore vs extend TTL” transaction flow (off-chain)
ExtendFootprintTTLOptakesextendTo(a new minimum TTL applied to entries in the transaction’s read-only footprint; entries already above it are skipped). [2][4]Sources: Protocol 23 announcement (auto-restoration / no manual restore) [1], Stellar Core P23 release notes (auto-restoration) [2], Soroban SDK v23 migration note for tests [3], JS SDK docs for
ExtendFootprintTTLOpsemantics [4].Validate Soroban SDK v21 → v23 migration before merge.
The upgrade spans two major versions with confirmed breaking changes:
Env::register_contractrenamed toEnv::register;DeployerWithAddress::deployreplaced bydeploy_v2(requires constructor args parameter)Env::mock_all_auths()became stricter about root invocation auth coverage; usemock_all_auths_allowing_non_root_auth()if auth only occurs in subcalls#[contractevent]replacesEvents::publish; check if contract uses old event publishingConfirm the test suite handles these changes, particularly around auth mocking and TTL assertions.
🤖 Prompt for AI Agents