Skip to content

feat: vault factory POC — single-tenant vc-vault + vc-vault-factory#52

Merged
aguilar1x merged 15 commits into
devfrom
feat/vault-factory-poc
Jun 3, 2026
Merged

feat: vault factory POC — single-tenant vc-vault + vc-vault-factory#52
aguilar1x merged 15 commits into
devfrom
feat/vault-factory-poc

Conversation

@aguilar1x
Copy link
Copy Markdown
Contributor

@aguilar1x aguilar1x commented May 17, 2026

Summary

  • Refactors vc-vault from multi-tenant to single-tenant: each holder gets their own deployed contract instance, owner: Address removed from all public functions, owner stored at construction
  • Adds vc-vault-factory contract: deploys vault instances deterministically via deploy_v2, registers them, and exposes is_vault for cross-contract verification
  • Adds deploy_sponsored: anyone can deploy a vault on behalf of an owner (deployer signs/pays, vault belongs to owner from creation)
  • Adds push / receive_push: production-ready cross-vault VC transfer — source vault calls destination's receive_push, which verifies the source is a legitimate factory vault before accepting the credential

Changes

contracts/vc-vault

  • New constructor: (vault_owner, contract_admin, did_uri, factory_address)
  • Removed: create_vault, create_sponsored_vault, sponsored-vault whitelist functions
  • Added: push(vc_id, dest_vault), receive_push(source_vault, vc_id, vc_data, issuer_did)
  • Storage: new VaultOwner and VaultFactory persistent keys; all keys flattened (owner no longer part of key)

contracts/vc-vault-factory

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Vault Factory contract that deploys and manages single-tenant vault instances per identity, with deterministic vault address generation and deployment tracking.
  • Changes

    • Refactored vault contract to single-tenant architecture; vault ownership is now set at deployment time rather than per transaction, simplifying the public API.
    • Vault credentials and issuer management operations no longer require owner parameters.
    • Simplified credential transfer mechanism via updated push/receive functionality.
  • Removed

    • Sponsored vault management functionality.
    • Linked credential features.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 17, 2026

Review Change Stack

Warning

Review limit reached

@aguilar1x, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 26 minutes and 39 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 20e1149a-6442-4e68-8892-f8012146afaf

📥 Commits

Reviewing files that changed from the base of the PR and between fe3fa2b and ac332b6.

📒 Files selected for processing (3)
  • .github/workflows/rust.yml
  • contracts/vc-vault-factory/src/test.rs
  • contracts/vc-vault/src/contract.rs
📝 Walkthrough

Walkthrough

Adds a new vc-vault-factory crate and contract for deterministic single-tenant vault deployment, and refactors vc-vault into single-vault-per-contract: constructor-stored owner, owner-less storage keys, simplified events/API, and removal of sponsored/linked-vault features.

Changes

New Vault Factory

Layer / File(s) Summary
Workspace + factory crate manifest & lib
Cargo.toml, contracts/vc-vault-factory/Cargo.toml, contracts/vc-vault-factory/src/lib.rs
Adds contracts/vc-vault-factory to the workspace and introduces the new crate with crate-type, features, and re-exports.
Factory contract and storage
contracts/vc-vault-factory/src/contract.rs, contracts/vc-vault-factory/src/storage.rs, contracts/vc-vault-factory/src/events.rs
Implements VaultFactory with constructor (VaultInitMeta), deterministic salt derivation (keccak256(user_salt
Factory tests
contracts/vc-vault-factory/src/test.rs
Integration-style tests covering registry checks, deterministic address properties, deploy/deploy_sponsored semantics, event counts, end-to-end issuance/verification flows, and cross-vault push/receive behaviors.

VC Vault Single-Instance Refactoring

Layer / File(s) Summary
Constructor and entrypoints
contracts/vc-vault/src/contract.rs
Constructor now records vault_owner, contract_admin, did_uri, and factory_address; public entrypoints converted to vault-scoped signatures and sponsored/linked entrypoints removed.
Storage enum & module
contracts/vc-vault/src/storage/mod.rs
VcVaultDataKey variants reorganized to drop per-owner Address parameters; sponsor module removed and top-of-file doc updated.
Credential storage & VC index
contracts/vc-vault/src/storage/credential.rs
VC payloads, index, parent links, and status storage refactored to use vc_id-only and position-based keys; O(1) index semantics preserved.
Issuer index storage
contracts/vc-vault/src/storage/issuer.rs
Authorized and denied issuer indices converted to vault-global keys with owner parameter removed; append/remove and position helpers updated.
Vault metadata & TTL helpers
contracts/vc-vault/src/storage/vault.rs, contracts/vc-vault/src/storage/ttl.rs
Vault owner/admin/DID/revoked persisted under fixed keys; TTL extension helpers simplified to vault-scoped signatures.
Events, interface, and errors
contracts/vc-vault/src/events.rs, contracts/vc-vault/src/interface.rs, contracts/vc-vault/src/error.rs
Event payloads simplified to remove owner fields; publisher signatures updated; VcVaultTrait signatures converted to owner-less APIs; two sponsor-related errors removed and SourceNotAVault added.
Validator and guards
contracts/vc-vault/src/validator.rs
Auth guard helpers updated to vault-global checks without owner parameter.
Credential & issuer ops
contracts/vc-vault/src/vault/credential.rs, contracts/vc-vault/src/vault/issuer.rs, contracts/vc-vault/src/vault/mod.rs
store_vc, revoke_vc, authorize_issuer(s), and revoke_issuer updated to vault-scoped signatures and storage; push/receive_push updated for factory-based source validation; push_vc export removed.

Sequence Diagram — Factory Deployment Flow

sequenceDiagram
  participant Caller
  participant VaultFactory
  participant Deployer
  participant VaultInstance
  Caller->>VaultFactory: deploy(owner, did_uri, user_salt)
  VaultFactory->>VaultFactory: keccak256(user_salt || owner)
  VaultFactory->>Deployer: deploy_v2(vault_wasm_hash, init_args, salt)
  Deployer->>VaultInstance: instantiate
  VaultFactory->>VaultFactory: set_deployed(vault_address)
  VaultFactory->>Caller: return vault_address
Loading

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly Related PRs

Poem

🐰 I hop and hash salts in a moonlit patch,
I build each vault — one owner, one latch.
No per-call owner to chase or to track,
The vault holds its keeper; the rabbit hops back. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: vault factory POC — single-tenant vc-vault + vc-vault-factory' clearly and concisely describes the main change: introducing a new vault factory contract alongside a refactored single-tenant vc-vault design.
Linked Issues check ✅ Passed All coding objectives from issue #54 are met: single-tenant vc-vault refactor with flattened storage, new constructor, removed multi-tenant APIs, cross-vault transfer via push/receive_push, and vc-vault-factory with deterministic deploy/deploy_sponsored/is_vault registry.
Out of Scope Changes check ✅ Passed All changes align with the linked issue scope: vc-vault refactoring to single-tenant, new vc-vault-factory contract, storage key flattening, and cross-vault VC transfer. No extraneous modifications detected.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/vault-factory-poc

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.

@aguilar1x aguilar1x self-assigned this May 17, 2026
Copy link
Copy Markdown

@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: 1

🧹 Nitpick comments (1)
contracts/vc-vault/src/storage/issuer.rs (1)

198-206: 💤 Low value

Consider adding a limit check for denied issuers.

append_denied_issuer_to_index lacks the MAX_ISSUERS_LIST check that append_issuer_to_index has at line 84. While the denied list is typically smaller, this allows unbounded storage growth if many issuers are denied over time.

Suggested fix
 pub fn append_denied_issuer_to_index(e: &Env, issuer: &Address) {
     if denied_issuer_index_contains(e, issuer) {
         return;
     }
     let count = read_denied_issuer_count(e);
+    if count >= MAX_ISSUERS_LIST {
+        panic_with_error!(e, ContractError::IssuerListTooLong);
+    }
     write_denied_issuer_at(e, count, issuer);
     write_denied_issuer_position(e, issuer, count);
     write_denied_issuer_count(e, count + 1);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/vc-vault/src/storage/issuer.rs` around lines 198 - 206,
append_denied_issuer_to_index allows unbounded growth; add the same
MAX_ISSUERS_LIST check used by append_issuer_to_index: read the current count
with read_denied_issuer_count(e) and if count >= MAX_ISSUERS_LIST, bail out
(return or revert as the contract pattern requires) before writing; keep the
existing denied_issuer_index_contains check and only perform
write_denied_issuer_at, write_denied_issuer_position, and
write_denied_issuer_count when the count is below MAX_ISSUERS_LIST.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@contracts/vc-vault-factory/src/test.rs`:
- Around line 163-180: The test function
test_deploy_and_deploy_sponsored_same_owner_same_salt_same_address is
inconsistent with its assertions: it declares the intent "same owner + same salt
+ same address" but creates owner2 and asserts the two derived addresses differ;
either restore the intended same-owner scenario by using the original owner for
the deploy_sponsored call (replace owner2 with owner and change addr_sponsored =
client.deploy_sponsored(&deployer, &owner, &did_uri, &salt) and
assert_eq!(addr_normal, addr_sponsored)), or rename the test and its comment to
reflect "different owner -> different address" and keep owner2 and assert_ne;
update the test name and any inline comment accordingly and ensure the
assertions match client.deploy and client.deploy_sponsored behavior.

---

Nitpick comments:
In `@contracts/vc-vault/src/storage/issuer.rs`:
- Around line 198-206: append_denied_issuer_to_index allows unbounded growth;
add the same MAX_ISSUERS_LIST check used by append_issuer_to_index: read the
current count with read_denied_issuer_count(e) and if count >= MAX_ISSUERS_LIST,
bail out (return or revert as the contract pattern requires) before writing;
keep the existing denied_issuer_index_contains check and only perform
write_denied_issuer_at, write_denied_issuer_position, and
write_denied_issuer_count when the count is below MAX_ISSUERS_LIST.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 838de906-b5f4-4047-ae12-335df3aaba17

📥 Commits

Reviewing files that changed from the base of the PR and between 0eb1600 and e30b412.

📒 Files selected for processing (22)
  • Cargo.toml
  • contracts/vc-vault-factory/Cargo.toml
  • contracts/vc-vault-factory/src/contract.rs
  • contracts/vc-vault-factory/src/events.rs
  • contracts/vc-vault-factory/src/lib.rs
  • contracts/vc-vault-factory/src/storage.rs
  • contracts/vc-vault-factory/src/test.rs
  • contracts/vc-vault/src/contract.rs
  • contracts/vc-vault/src/error.rs
  • contracts/vc-vault/src/events.rs
  • contracts/vc-vault/src/interface.rs
  • contracts/vc-vault/src/storage/credential.rs
  • contracts/vc-vault/src/storage/issuer.rs
  • contracts/vc-vault/src/storage/mod.rs
  • contracts/vc-vault/src/storage/sponsor.rs
  • contracts/vc-vault/src/storage/ttl.rs
  • contracts/vc-vault/src/storage/vault.rs
  • contracts/vc-vault/src/test.rs
  • contracts/vc-vault/src/validator.rs
  • contracts/vc-vault/src/vault/credential.rs
  • contracts/vc-vault/src/vault/issuer.rs
  • contracts/vc-vault/src/vault/mod.rs
💤 Files with no reviewable changes (2)
  • contracts/vc-vault/src/storage/sponsor.rs
  • contracts/vc-vault/src/error.rs

Comment thread contracts/vc-vault-factory/src/test.rs Outdated
aguilar1x added 11 commits May 16, 2026 21:13
Each vault is now its own contract, so no key needs to be scoped by
owner address. VaultOwner is written once at construction and read
from instance storage. sponsor.rs removed — sponsored vault deferred.
All require_* helpers now read vault state from instance storage
instead of taking owner: &Address. require_vault_admin reads
VaultAdmin directly; require_vault_active reads VaultRevoked.
store_vc, store_vc_with_fee, revoke_vc, authorize_issuer,
authorize_issuers, revoke_issuer and is_authorized no longer take
owner: &Address. push_vc removed — cross-contract push is deferred.
… entrypoints

__constructor(vault_owner, contract_admin, did_uri) replaces create_vault.
All public functions drop owner: Address. Removed: create_vault,
create_sponsored_vault, sponsor management, push, issue_linked,
get_vc_parent. Dead error codes VaultAlreadyExists and
NotAuthorizedSponsor removed.
Constructor registers via env.register(VcVaultContract, (owner, admin, did_uri)).
All client calls drop the owner first arg. Removed tests for create_vault,
sponsored vault, push, issue_linked and get_vc_parent. 93 tests passing.
Factory deploys single-tenant vc-vault instances via deploy_v2.
Salt = keccak256(user_salt || owner_bytes) prevents frontrunning and
makes vault addresses deterministic per (owner, salt) pair.

deploy(owner, did_uri, salt) — owner signs their own vault.
deploy_sponsored(deployer, owner, did_uri, salt) — any third party
  signs and pays; vault ownership goes to owner from creation.

is_vault(address) queries the persistent registry of deployed vaults.
10 tests passing including full integration and sponsored flow.
Adds VaultFactory persistent key to store the factory that deployed each
vault, enabling cross-vault source verification in receive_push.
push() moves a VC from the source vault to a destination vault via a
cross-contract call. receive_push() verifies the source is a legitimate
factory-deployed vault before accepting the credential and sets itself
as the new issuance authority.
@aguilar1x aguilar1x force-pushed the feat/vault-factory-poc branch from c3c58f4 to fe78dab Compare May 17, 2026 03:15
aguilar1x added 2 commits June 3, 2026 00:10
Resolves Cargo.toml conflict by keeping vc-vault-factory in workspace
members and dropping vc-issuer-registry (already removed in dev).
The vc-vault-factory test suite uses contractimport! to load
vc_vault_contract WASM at compile time. CI was failing with
"No such file or directory" because the workflow ran cargo
test without first producing the wasm32v1-none artifact.

Two changes:
- Add rustup target install and a cargo rustc step to build
  vc-vault as cdylib for wasm32v1-none before tests run.
- Switch the contractimport! path from .optimized.wasm to
  the plain .wasm so CI doesn't need to install the stellar
  CLI / wasm-opt just to run tests. Optimization is only
  relevant for deploy; Soroban's sandbox executes either
  binary identically.
Copy link
Copy Markdown

@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: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@contracts/vc-vault-factory/src/contract.rs`:
- Around line 34-40: In derive_salt, don't use owner.to_string() into a fixed
56-byte buffer; instead serialize the Address to its canonical bytes and hash
those with user_salt (so the salt = keccak256(user_salt ||
canonical_address_bytes)). Locate derive_salt and replace the owner.to_string()
copy logic with the Address XDR/raw payload serialization (e.g., call the
Address XDR/ToXdr or extract an AddressPayload/raw bytes), append those
canonical bytes to the user_salt bytes, and then keccak256 that combined byte
array so the computed salt matches the README/client-side preimages.

In `@contracts/vc-vault/src/contract.rs`:
- Around line 429-431: The duplicate VC check currently uses
storage::vc_index_contains(&e, &vc_id) which misses revoked-but-removed-index
cases; replace it with the same logic used in issue()/batch_issue(): if
storage::read_vault_vc(&e, &vc_id).is_some() || storage::read_vc_status(&e,
&vc_id) != VCStatus::Invalid { panic_with_error!(e,
ContractError::VCAlreadyExists); } — this ensures receive_push (or the function
containing the vc_index_contains check) rejects VCs that already exist or are
revoked.
- Around line 414-418: Add the missing issuer DID length check in receive_push:
call require_issuer_did_len(&e, &issuer_did) (the same validation used in
issue()) before storing the VC (i.e., after require_vc_data_len(&e, &vc_data)
and before proceeding with require_vault_active/source_vault.require_auth). This
ensures issuer_did is validated consistently with issue().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 60213be4-bb29-4e9a-a45c-1572c9c0ac9d

📥 Commits

Reviewing files that changed from the base of the PR and between e30b412 and fe3fa2b.

📒 Files selected for processing (23)
  • Cargo.toml
  • contracts/vc-vault-factory/Cargo.toml
  • contracts/vc-vault-factory/README.md
  • contracts/vc-vault-factory/src/contract.rs
  • contracts/vc-vault-factory/src/events.rs
  • contracts/vc-vault-factory/src/lib.rs
  • contracts/vc-vault-factory/src/storage.rs
  • contracts/vc-vault-factory/src/test.rs
  • contracts/vc-vault/src/contract.rs
  • contracts/vc-vault/src/error.rs
  • contracts/vc-vault/src/events.rs
  • contracts/vc-vault/src/interface.rs
  • contracts/vc-vault/src/storage/credential.rs
  • contracts/vc-vault/src/storage/issuer.rs
  • contracts/vc-vault/src/storage/mod.rs
  • contracts/vc-vault/src/storage/sponsor.rs
  • contracts/vc-vault/src/storage/ttl.rs
  • contracts/vc-vault/src/storage/vault.rs
  • contracts/vc-vault/src/test.rs
  • contracts/vc-vault/src/validator.rs
  • contracts/vc-vault/src/vault/credential.rs
  • contracts/vc-vault/src/vault/issuer.rs
  • contracts/vc-vault/src/vault/mod.rs
💤 Files with no reviewable changes (1)
  • contracts/vc-vault/src/storage/sponsor.rs
✅ Files skipped from review due to trivial changes (1)
  • contracts/vc-vault-factory/Cargo.toml
🚧 Files skipped from review as they are similar to previous changes (13)
  • Cargo.toml
  • contracts/vc-vault-factory/src/lib.rs
  • contracts/vc-vault/src/vault/mod.rs
  • contracts/vc-vault/src/validator.rs
  • contracts/vc-vault/src/vault/issuer.rs
  • contracts/vc-vault/src/storage/mod.rs
  • contracts/vc-vault/src/storage/ttl.rs
  • contracts/vc-vault/src/events.rs
  • contracts/vc-vault/src/storage/issuer.rs
  • contracts/vc-vault/src/vault/credential.rs
  • contracts/vc-vault/src/interface.rs
  • contracts/vc-vault/src/storage/vault.rs
  • contracts/vc-vault/src/storage/credential.rs

Comment on lines +34 to +40
fn derive_salt(e: &Env, user_salt: BytesN<32>, owner: &Address) -> BytesN<32> {
// Salt = keccak256(user_salt || owner_bytes) — prevents frontrunning.
let mut owner_bytes: [u8; 56] = [0; 56];
owner.to_string().copy_into_slice(&mut owner_bytes);
let mut salt_bytes: Bytes = user_salt.into_val(e);
salt_bytes.extend_from_array(&owner_bytes);
e.crypto().keccak256(&salt_bytes).into()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In the current Soroban SDK, what is the canonical byte/XDR serialization API for Addresswhen hashing deterministically, and isto_string() appropriate for stable on-chain address derivation or should raw serialized bytes be used instead?

💡 Result:

In the Soroban SDK, the canonical way to serialize an Address for deterministic hashing is by using its XDR representation, typically accessed via the to_xdr() method provided by the soroban_sdk::xdr::ToXdr trait [1]. Using to_string() is not recommended for on-chain address derivation or stable hashing [1]. The to_string() method produces a human-readable StrKey (e.g., starting with 'G' or 'C'), which is designed for display and off-chain interoperability rather than stable, canonical binary identification [1]. Relying on string representations introduces unnecessary complexity and potential inconsistencies if the encoding format were to change or if different string representations were used. For stable on-chain derivation and deterministic hashing, you should use the raw XDR bytes [1]. The Address type can be serialized to XDR bytes using address.to_xdr(&env), providing a stable, canonical binary format suitable for hashing and storage [1]. If you need to access the underlying components of an address (such as the 32-byte contract hash or Ed25519 public key), you can use the AddressPayload type, which can be extracted from an Address to obtain the raw binary identifier directly [2][3][4]. Top results: [2][3][1]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and print the relevant contract code region
if [ -f contracts/vc-vault-factory/src/contract.rs ]; then
  echo "== contract.rs derive_salt =="
  nl -ba contracts/vc-vault-factory/src/contract.rs | sed -n '1,120p' | sed -n '34,60p'
else
  echo "Missing contracts/vc-vault-factory/src/contract.rs"
fi

# Print README section that documents salt derivation (best-effort)
if [ -f contracts/vc-vault-factory/README.md ]; then
  echo "== README salt derivation section (best-effort) =="
  rg -n "salt|keccak|derive_salt|user_salt|owner_bytes|owner" contracts/vc-vault-factory/README.md | head -n 50
  # show around matches
  rg -n "salt|keccak|derive_salt|user_salt|owner_bytes|owner" contracts/vc-vault-factory/README.md | head -n 10 | cut -d: -f1 | sort -u | while read -r ln; do
    echo "-- context around line $ln --"
    nl -ba contracts/vc-vault-factory/README.md | sed -n "$((ln-5)),$((ln+15))p"
  done
else
  echo "Missing contracts/vc-vault-factory/README.md"
fi

Repository: ACTA-Team/contracts-acta

Length of output: 159


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="contracts/vc-vault-factory/src/contract.rs"
README="contracts/vc-vault-factory/README.md"

echo "== Checking file existence =="
echo "contract.rs: $(test -f "$FILE" && echo yes || echo no)"
echo "README.md:   $(test -f "$README" && echo yes || echo no)"

echo
echo "== contract.rs derive_salt (lines 25-60) =="
if [ -f "$FILE" ]; then
  awk 'NR>=25 && NR<=60 {printf "%d:%s\n", NR, $0}' "$FILE" | sed -n '1,80p'
else
  echo "Missing $FILE"
fi

echo
echo "== README salt-related documentation (best-effort) =="
if [ -f "$README" ]; then
  rg -n "derive_salt|keccak|salt|user_salt|owner_bytes|owner_bytes|owner" "$README" | head -n 50 || true
  # If there are matches, print a small window around the first few match lines
  matches=$(rg -n "derive_salt|keccak|salt|user_salt|owner_bytes|owner" "$README" | head -n 10 | cut -d: -f1 | tr '\n' ' ')
  if [ -n "${matches:-}" ]; then
    for ln in $matches; do
      echo "-- context around README line $ln --"
      awk -v start=$((ln-8)) -v end=$((ln+20)) 'NR>=start && NR<=end {printf "%d:%s\n", NR, $0}' "$README"
      echo
    done
  fi
else
  echo "Missing $README"
fi

Repository: ACTA-Team/contracts-acta

Length of output: 15423


Derive deploy salt from canonical Address bytes (XDR/payload), not owner.to_string()

contracts/vc-vault-factory/src/contract.rs builds owner_bytes by copying owner.to_string() (StrKey text) into a fixed [u8; 56] buffer and hashing it. This doesn’t match contracts/vc-vault-factory/README.md, which defines deploy_salt = keccak256(user_salt || owner_address_bytes) and implies raw/canonical address bytes. The current approach ties determinism to today’s display-string encoding/length and can cause client-side precomputed vault addresses to differ from on-chain results.

Use the canonical serialized address bytes for hashing (e.g., XDR via to_xdr(&env) / ToXdr, or raw bytes via AddressPayload), then hash those bytes with user_salt.

🧰 Tools
🪛 Clippy (1.96.0)

[warning] 34-34: function derive_salt is never used

(warning)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/vc-vault-factory/src/contract.rs` around lines 34 - 40, In
derive_salt, don't use owner.to_string() into a fixed 56-byte buffer; instead
serialize the Address to its canonical bytes and hash those with user_salt (so
the salt = keccak256(user_salt || canonical_address_bytes)). Locate derive_salt
and replace the owner.to_string() copy logic with the Address XDR/raw payload
serialization (e.g., call the Address XDR/ToXdr or extract an AddressPayload/raw
bytes), append those canonical bytes to the user_salt bytes, and then keccak256
that combined byte array so the computed salt matches the README/client-side
preimages.

Comment thread contracts/vc-vault/src/contract.rs
Comment thread contracts/vc-vault/src/contract.rs Outdated
aguilar1x added 2 commits June 3, 2026 00:35
receive_push only checked vc_index_contains for duplicates. revoke()
removes the index entry but the Revoked status persists, so a pushed VC
reusing a revoked vc_id passed the check and silently overwrote the
status back to Valid — undoing a revocation. This is the same class as
audit finding A-26.

Align the duplicate guard with issue()/batch_issue() by checking the
persisted status (read_vault_vc/read_vc_status) instead of the index.
Also add the missing require_issuer_did_len validation so receive_push
validates inputs consistently with issue().

Adds a regression test (test_push_cannot_revive_revoked_vc_in_destination)
verified to fail against the pre-fix build and pass after.
The test was named ..._same_owner_same_salt_same_address but used two
different owners and asserted the addresses differ. Rename to reflect
what it actually verifies (different owners -> different addresses via
both deploy and deploy_sponsored) and correct the comment. The
same-owner collapse can't be asserted via double-deploy since the
second deploy of an identical (owner, salt) pair panics.
@aguilar1x aguilar1x merged commit a3706c1 into dev Jun 3, 2026
3 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.

feat: vc-vault-factory POC — single-tenant vaults deployed per holder

1 participant