Skip to content

feat: implement Sponsored Vault#3

Merged
aguilar1x merged 7 commits intodevfrom
feat/poc-sponsored-vault
Mar 1, 2026
Merged

feat: implement Sponsored Vault#3
aguilar1x merged 7 commits intodevfrom
feat/poc-sponsored-vault

Conversation

@aguilar1x
Copy link
Contributor

@aguilar1x aguilar1x commented Feb 27, 2026

Summary

Adds the Sponsored Vault feature to vc-vault-contract. A sponsor (the contract admin or an authorized address) can now create a vault on behalf of a user who hasn't signed yet. The vault is keyed to the beneficiary's address from day one — the sponsor only pays and signs at creation; all subsequent vault operations (authorize issuers, receive credentials, etc.) are controlled by the owner.

Changes

  • error.rs — Added NotAuthorizedSponsor = 11 error code so integrators can distinguish authorization failures.
  • storage/mod.rs — Added SponsoredVaultOpenToAll and SponsoredVaultSponsors storage keys with all read/write helpers.
  • api/mod.rs — Added 5 new function signatures to VcVaultTrait.
  • contract.rs — Implemented:
    • create_sponsored_vault(sponsor, owner, did_uri) — sponsor signs, vault is created for owner.
    • set_sponsored_vault_open_to_all(open: bool) — admin toggles restricted vs open mode.
    • get_sponsored_vault_open_to_all() — query current mode.
    • add_sponsored_vault_sponsor(sponsor) — admin adds an authorized sponsor.
    • remove_sponsored_vault_sponsor(sponsor) — admin removes a sponsor.
  • test.rs — 8 new tests covering all flows described in the issue.

Behavior

Mode Who can call create_sponsored_vault
Restricted (default) Contract admin or addresses in the sponsors list
Open (open_to_all = true) Anyone

Test coverage

  • Admin creates sponsored vault
  • Authorized sponsor creates sponsored vault
  • Unauthorized address fails in restricted mode
  • Open mode allows anyone to create
  • Returning to restricted mode re-blocks unauthorized
  • Removed sponsor can no longer create
  • Duplicate vault for same owner fails
  • open_to_all defaults to false

Closes

Summary by CodeRabbit

  • New Features

    • Added sponsored vault functionality, enabling creation and management of sponsored vaults with configurable sponsor authorization.
    • Added ability to toggle open-to-all access mode and manage authorized sponsors.
  • Chores

    • Bumped package version to 0.21.0.
    • Updated soroban-sdk dependency to 23.4.0.
  • Tests

    • Added comprehensive test coverage for sponsored vault operations and access control scenarios.

@aguilar1x aguilar1x self-assigned this Feb 27, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 27, 2026

📝 Walkthrough

Walkthrough

This PR introduces a sponsored vault feature enabling sponsors to create vaults on behalf of owners. It includes dependency updates, five new contract entrypoints for vault sponsorship management, implementation logic with authorization checks, storage layer updates, a new error variant, and comprehensive test coverage for the feature.

Changes

Cohort / File(s) Summary
Dependency Updates
Cargo.toml
Package version bumped from 0.20.0 to 0.21.0; soroban-sdk updated from 21.0.0 to 23.4.0.
Public API Definitions
contracts/vc-vault/src/api/mod.rs
Five new methods added to VcVaultTrait: create_sponsored_vault, set_sponsored_vault_open_to_all, get_sponsored_vault_open_to_all, add_sponsored_vault_sponsor, and remove_sponsored_vault_sponsor.
Implementation Logic
contracts/vc-vault/src/contract.rs
Implemented sponsored vault feature with sponsor authorization validation, open-to-all gating, vault initialization, duplicate prevention, and TTL extension.
Error Handling
contracts/vc-vault/src/error.rs
Added NotAuthorizedSponsor error variant (discriminant 11) for unauthorized sponsor operations.
Storage Management
contracts/vc-vault/src/storage/mod.rs
Added SponsoredVaultOpenToAll and SponsoredVaultSponsors data keys; implemented seven helper functions for reading/writing flags, managing sponsor lists, and checking authorization.
Test Coverage
contracts/vc-vault/src/test.rs
Added comprehensive test suite covering default behavior, admin/authorized sponsor creation, access control enforcement, open-mode toggling, sponsor removal, and duplicate vault prevention.

Sequence Diagram(s)

sequenceDiagram
    participant Sponsor as Sponsor/Admin
    participant Contract as VcVault Contract
    participant Storage as Storage Layer

    Sponsor->>Contract: create_sponsored_vault(sponsor, owner, did_uri)
    Contract->>Contract: sponsor.require_auth()
    Contract->>Storage: Check if vault exists for owner
    Storage-->>Contract: Vault status
    
    alt Vault not found
        Contract->>Storage: read_sponsored_vault_open_to_all()
        Storage-->>Contract: open_to_all flag
        
        alt open_to_all or sponsor authorized
            Contract->>Storage: write vault_admin(owner) = owner
            Contract->>Storage: write vault_did(owner) = did_uri
            Contract->>Storage: initialize issuers, revoked flag
            Contract->>Contract: extend_ttl()
            Contract-->>Sponsor: Success
        else Sponsor not authorized
            Contract-->>Sponsor: NotAuthorizedSponsor error
        end
    else Vault exists
        Contract-->>Sponsor: Duplicate vault error
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 A sponsor hops with generous intent,
Creating vaults for owners heaven-sent,
Authorization checks ensure the way,
Storage guards the vaults both night and day,
TTLs extend like morning's golden ray!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: implement Sponsored Vault' clearly and concisely summarizes the main feature addition in the changeset.
Linked Issues check ✅ Passed All objectives from issue #2 are met: create_sponsored_vault with sponsor auth and gating logic, storage for SponsoredVaultOpenToAll and SponsoredVaultSponsors with helpers, admin functions, NotAuthorizedSponsor error, proper vault structure, API exposure, and comprehensive tests.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the Sponsored Vault feature; version bump and SDK dependency update are reasonable maintenance activities supporting the feature.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/poc-sponsored-vault

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

panic_with_error!(e, ContractError::AlreadyInitialized);
}
storage::write_vault_admin(&e, &owner, &owner);
storage::write_vault_did(&e, &owner, &did_uri);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium Severity severity

Access Control and Authorization: Unconsented vault initialization for owner in create_sponsored_vault

create_sponsored_vault only requires sponsor.require_auth() and then writes vault state for an arbitrary owner, including VaultDid. In open_to_all mode this allows any signer to pre-create (squat) a vault for any target address and permanently set arbitrary DID metadata, blocking the target from calling create_vault later (AlreadyInitialized) and potentially poisoning off-chain identity assumptions since there is no DID-update entrypoint.

Require explicit owner consent (e.g., owner.require_auth() or a signed owner authorization payload) OR make did_uri mutable by the vault admin so the owner can correct sponsored initialization; additionally consider restricting open_to_all to owner == sponsor if you want "self-sponsoring" only.


Actions
  • Reply /almanax ask <question> to ask a follow-up question.
  • Reply /almanax dismiss [<reason>] and it won't appear again in future scans.
  • Reply /almanax resolve [<reason>] to mark the finding as resolved.
  • Reply /almanax severity <severity> [<reason>] to override the severity.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
contracts/vc-vault/src/test.rs (1)

467-477: Narrow should_panic checks to expected contract errors.

Generic panic checks can pass for unrelated failures; asserting expected error codes will make these tests deterministic and meaningful.

💡 Example tightening
-#[should_panic]
+#[should_panic(expected = "Error(Contract, `#11`)")]
 fn test_unauthorized_address_cannot_create_sponsored_vault_in_restricted_mode() {

Also applies to: 493-503, 506-517, 520-529

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/vc-vault/src/test.rs` around lines 467 - 477, The test uses a
generic #[should_panic] which can hide unrelated failures; update
test_unauthorized_address_cannot_create_sponsored_vault_in_restricted_mode (and
the other tests noted at 493-503, 506-517, 520-529) to assert the specific
contract error instead of expecting any panic: call the operation
(client.create_sponsored_vault) in a way that captures the Result/Err and assert
it equals the expected error variant/code (e.g., Unauthorized or the contract's
specific error enum/value) or use the test harness helper that checks for a
specific contract error, rather than relying on #[should_panic], so the test
deterministically verifies the intended failure.
contracts/vc-vault/src/storage/mod.rs (1)

413-443: Consider replacing the sponsor Vec<Address> with key-based membership storage.

Current checks/removals are linear-time. A key-per-sponsor approach scales better and avoids growing per-call cost on create_sponsored_vault.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/vc-vault/src/storage/mod.rs` around lines 413 - 443, Replace the
Vec<Address>-backed sponsored-sponsors list with key-based membership storage to
avoid O(n) contains/removal; stop using DataKey::SponsoredVaultSponsors vector
functions (read_sponsored_vault_sponsors, write_sponsored_vault_sponsors) and
instead store a per-sponsor key (e.g. DataKey::SponsoredVaultSponsor(Address) or
a mapping key derived from sponsor) whose presence or boolean value indicates
membership. Update is_authorized_sponsor to check the per-sponsor key
presence/get boolean from e.storage().instance() (constant-time), change
add_sponsored_vault_sponsor to set that per-sponsor key when adding, and change
remove_sponsored_vault_sponsor to remove/clear that key when deleting; remove or
deprecate the Vec-based helpers and migrate any callers (like
create_sponsored_vault) to use the new per-sponsor key checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Cargo.toml`:
- Line 13: The Soroban SDK bump from v21→v23 introduces breaking API and
behavior changes; update tests and contract code accordingly: replace
Env::register_contract calls with Env::register, migrate any
DeployerWithAddress::deploy usage to deploy_v2 and pass the required constructor
args, switch Env::mock_all_auths() to mock_all_auths_allowing_non_root_auth()
for tests that only authenticate in subcalls, adjust TTL/storage tests to expect
auto-restoration of archived entries instead of panics, and replace any
Events::publish-based event tests/usage with the new #[contractevent]-based
publishing API; run the test suite and fix any resulting failures referencing
these symbols (Env::register, deploy_v2, mock_all_auths_allowing_non_root_auth,
#[contractevent], archived/TTL assertions).

In `@contracts/vc-vault/src/test.rs`:
- Around line 444-452: The test currently uses client.list_vc_ids(&owner) to
infer vault creation which can be true even if vault creation silently failed;
update the test_admin_creates_sponsored_vault (and the other affected tests at
the noted ranges) to assert vault creation explicitly by checking the vault
state or creation result: verify the return value or success of
client.create_sponsored_vault, then call a direct existence/state accessor such
as client.get_vault(&owner) or client.vault_exists(&owner) (or inspect vault
metadata/owner mapping exposed by the contract) and assert the expected values
(e.g., non-None result or matching owner/did_uri) instead of relying on
list_vc_ids being empty.

---

Nitpick comments:
In `@contracts/vc-vault/src/storage/mod.rs`:
- Around line 413-443: Replace the Vec<Address>-backed sponsored-sponsors list
with key-based membership storage to avoid O(n) contains/removal; stop using
DataKey::SponsoredVaultSponsors vector functions (read_sponsored_vault_sponsors,
write_sponsored_vault_sponsors) and instead store a per-sponsor key (e.g.
DataKey::SponsoredVaultSponsor(Address) or a mapping key derived from sponsor)
whose presence or boolean value indicates membership. Update
is_authorized_sponsor to check the per-sponsor key presence/get boolean from
e.storage().instance() (constant-time), change add_sponsored_vault_sponsor to
set that per-sponsor key when adding, and change remove_sponsored_vault_sponsor
to remove/clear that key when deleting; remove or deprecate the Vec-based
helpers and migrate any callers (like create_sponsored_vault) to use the new
per-sponsor key checks.

In `@contracts/vc-vault/src/test.rs`:
- Around line 467-477: The test uses a generic #[should_panic] which can hide
unrelated failures; update
test_unauthorized_address_cannot_create_sponsored_vault_in_restricted_mode (and
the other tests noted at 493-503, 506-517, 520-529) to assert the specific
contract error instead of expecting any panic: call the operation
(client.create_sponsored_vault) in a way that captures the Result/Err and assert
it equals the expected error variant/code (e.g., Unauthorized or the contract's
specific error enum/value) or use the test harness helper that checks for a
specific contract error, rather than relying on #[should_panic], so the test
deterministically verifies the intended failure.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cc5df79 and 18411e0.

📒 Files selected for processing (6)
  • Cargo.toml
  • contracts/vc-vault/src/api/mod.rs
  • contracts/vc-vault/src/contract.rs
  • contracts/vc-vault/src/error.rs
  • contracts/vc-vault/src/storage/mod.rs
  • contracts/vc-vault/src/test.rs


[workspace.dependencies]
soroban-sdk = { version = "21.0.0" }
soroban-sdk = { version = "23.4.0" }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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)

  • Contract test registration API renamed/unified
    • Env::register / Env::register_at replace Env::register_contract / Env::register_contract_wasm. [1]
  • Contract deployment API changed
    • DeployerWithAddress::deploy_v2 replaces DeployerWithAddress::deploy (adds constructor-args parameter; pass () if none). [1]
  • Fuzz/panic catching API deprecated
    • fuzz_catch_panic deprecated; use Env::try_invoke_contract and generated try_ client methods. [1]
  • Test snapshot event set changed
    • Test snapshots no longer include diagnostic events—only contract + system events remain. [1]

v22 → v23 (breaking behavior / interface changes)

  • Events API migration
    • #[contractevent] replaces Events::publish (old publish path deprecated). [1]
  • Token transfer “to” type change
    • MuxedAddress replaces Address as the to argument of TokenInterface::transfer (in soroban-token-sdk, but it impacts contracts using that interface). [1]
  • Testing behavior for archived persistent entries changed
    • Accessing archived persistent entries in tests no longer panics; restoration is emulated (instance storage is also persistent). [1]

v23.0.0 → v23.4.0

  • No additional “Breaking Changes” section was called out in the v23.4.0 release notes; it’s primarily incremental updates (e.g., making trait functions with default implementations callable, test snapshot source options). [2]
  • Most of the major v23-line breakage is concentrated at v23.0.0 (muxed address support, contractevent introduction, deprecations around events). [1][3]

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_auths test failures are:

  1. Address::require_auth() is “require_auth_for_args with implied args”
    require_auth() authorizes the current contract invocation including all invocation arguments, which are automatically inferred (it’s equivalent to calling require_auth_for_args with the invocation’s args). This matters because auth matching/mocking is sensitive to the args being authorized. (docs.rs)

  2. Env::mock_all_auths() became stricter about “root invocation” auth coverage
    In testutils, mock_all_auths() mocks all require_auth / require_auth_for_args calls, but it will error if an address is only authorized in a non-root sub-invocation and that address wasn’t require_auth’d in the root invocation. This commonly shows up when Contract B calls Contract A, and only A does require_auth. (docs.rs)

  3. 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 to mock_all_auths() except it does not fail when the root invocation didn’t require_auth for 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

  • Archived-entry access behavior changed (automatic restoration): In Protocol 23, InvokeHostFunction can 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]
  • Test behavior in soroban-sdk changed 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)

  1. Fix tests that expected archived reads to fail

    • If you had tests that advanced the ledger past an entry’s TTL and asserted a read fails, update them.
    • Prefer asserting the TTL value directly after your bump/extend logic (e.g., use get_ttl in testutils) rather than relying on an “archived read throws” behavior. [3]
  2. Re-check your “restore vs extend TTL” transaction flow (off-chain)

    • With P23 auto-restoration, the workflow shifts toward:
      • Preflight/simulate to build the correct footprint (including any archived entries that will be restored), then
      • Use Extend Footprint TTL when you want to keep entries alive longer.
    • In the JS SDK, ExtendFootprintTTLOp takes extendTo (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 ExtendFootprintTTLOp semantics [4].


Validate Soroban SDK v21 → v23 migration before merge.

The upgrade spans two major versions with confirmed breaking changes:

  • Test utilities: Env::register_contract renamed to Env::register; DeployerWithAddress::deploy replaced by deploy_v2 (requires constructor args parameter)
  • Auth testing: Env::mock_all_auths() became stricter about root invocation auth coverage; use mock_all_auths_allowing_non_root_auth() if auth only occurs in subcalls
  • Storage/TTL behavior: Test environment now emulates auto-restoration of archived entries instead of panicking on reads; update tests relying on "archived read fails" assertions
  • Events: #[contractevent] replaces Events::publish; check if contract uses old event publishing

Confirm the test suite handles these changes, particularly around auth mocking and TTL assertions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Cargo.toml` at line 13, The Soroban SDK bump from v21→v23 introduces breaking
API and behavior changes; update tests and contract code accordingly: replace
Env::register_contract calls with Env::register, migrate any
DeployerWithAddress::deploy usage to deploy_v2 and pass the required constructor
args, switch Env::mock_all_auths() to mock_all_auths_allowing_non_root_auth()
for tests that only authenticate in subcalls, adjust TTL/storage tests to expect
auto-restoration of archived entries instead of panics, and replace any
Events::publish-based event tests/usage with the new #[contractevent]-based
publishing API; run the test suite and fix any resulting failures referencing
these symbols (Env::register, deploy_v2, mock_all_auths_allowing_non_root_auth,
#[contractevent], archived/TTL assertions).

Comment on lines +444 to +452
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);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

list_vc_ids is a weak proxy for “vault was created.”

These assertions can pass even if vault creation silently failed, because list_vc_ids can return an empty list for missing vault state as well.

💡 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
Verify each finding against the current code and only fix it if needed.

In `@contracts/vc-vault/src/test.rs` around lines 444 - 452, The test currently
uses client.list_vc_ids(&owner) to infer vault creation which can be true even
if vault creation silently failed; update the test_admin_creates_sponsored_vault
(and the other affected tests at the noted ranges) to assert vault creation
explicitly by checking the vault state or creation result: verify the return
value or success of client.create_sponsored_vault, then call a direct
existence/state accessor such as client.get_vault(&owner) or
client.vault_exists(&owner) (or inspect vault metadata/owner mapping exposed by
the contract) and assert the expected values (e.g., non-None result or matching
owner/did_uri) instead of relying on list_vc_ids being empty.

@aguilar1x aguilar1x changed the base branch from main to dev February 27, 2026 22:22
@aguilar1x aguilar1x merged commit fc95241 into dev Mar 1, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant