Skip to content

audit: security fixes and fuzzing infrastructure for vc-vault v1 acta#4

Open
aguilar1x wants to merge 20 commits intodevfrom
audit/vc-vault-acta-v1
Open

audit: security fixes and fuzzing infrastructure for vc-vault v1 acta#4
aguilar1x wants to merge 20 commits intodevfrom
audit/vc-vault-acta-v1

Conversation

@aguilar1x
Copy link
Contributor

@aguilar1x aguilar1x commented Mar 1, 2026

Summary by CodeRabbit

  • New Features

    • Two-step admin nomination/accept flow, sponsored vaults with sponsor management, vault lifecycle controls, issuer management, fee configuration, and new events for vault/credential actions.
  • Updates

    • Verification now returns structured VC status; init/migrate/revoke require explicit owner/admin context; workspace version and SDK upgraded; library build/packaging workflow improved.
  • Error Handling

    • New errors for unauthorized sponsors, duplicate credentials, and missing pending admin.
  • Chores / Tests

    • Expanded ignore patterns, enhanced build/release scripts, extensive unit tests and new fuzzing harnesses.

- VCStatus key changed from VCStatus(String) to VCStatus(Address, String)
  so status entries are scoped per vault owner, preventing cross-vault
  collision where any issuer could overwrite another vault's revocation status
- VCOwner key removed; revoke now takes explicit owner parameter
- FeeCustom(Address) moved from instance to persistent storage to avoid
  exhausting the instance storage budget with per-issuer entries
- SponsoredVaultSponsors Vec replaced with individual persistent entries
  keyed by SponsoredVaultSponsor(Address); authorization is now O(1)
- PendingAdmin key added for two-step admin transfer
- NoPendingAdmin and VCAlreadyExists error codes added
- Debug derive added to VCStatus to support assert_eq! in tests
Emits on-chain events using the #[contractevent] macro for:
VaultCreated, SponsoredVaultCreated, VaultRevoked, IssuerAuthorized,
IssuerRevoked, VCIssued, VCRevoked

Previously no events were emitted, making it impossible for indexers
and off-chain tools to observe state changes without polling storage.
pub mod visibility added to contract and model for external access.
- authorize_issuer now removes the issuer from VaultDeniedIssuers when
  re-authorizing, keeping the authorized and denied lists consistent
- authorize_issuers deduplicates the input list before writing to prevent
  a revoke_issuer call from leaving ghost authorized entries
- revoke_issuer rewritten to filter all occurrences in a single pass
  instead of removing only the first match by index
- create_vault: remove bootstrap side-effect that allowed the first caller
  to claim contract admin by calling create_vault before initialize
- verify_vc: return typed VCStatus instead of untyped Map<String, String>;
  issuance_status_to_map helper removed
- issue: add VCAlreadyExists check to block re-issuance of an existing
  vc_id; also blocks re-issuance of a vc_id that was pushed to another
  vault by checking VCStatus in addition to vault payload existence
- revoke: check vault payload exists (not just status) to block revoking
  a vc that was pushed away; also blocks double-revocation
- push: require VCStatus == Valid before moving a credential, blocking
  transfer of revoked credentials
- nominate_admin + accept_contract_admin replace set_contract_admin with
  a two-step transfer requiring the nominee to sign acceptance
- remove set_vault_did (DID immutability is intentional design)
- remove redundant validate_vault_initialized calls from push
- initialize: remove unused default_issuer_did parameter and storage key
- migrate: fix read_legacy_issuance_revocations to return empty map
  instead of panicking when no revocations exist in legacy storage
- setup_no_mock helper added for tests that require explicit auth mocking
- Five targeted auth tests added using mock_auths() to verify require_auth()
  guards on initialize, nominate_admin, create_vault, authorize_issuer and issue
- test_nominate_and_accept_admin replaces test_set_contract_admin
- All verify_vc assertions updated to use typed VCStatus comparisons
- Three regression tests added for post-fuzz findings:
  test_issue_after_push_same_vc_id_panics (A-22)
  test_revoke_after_push_panics (A-23)
  test_push_revoked_vc_panics (A-24)
build.sh:
- set -eu added so any failure exits immediately
- switched to cargo rustc -- --crate-type cdylib to produce the WASM
  artifact without declaring cdylib in Cargo.toml (required for fuzzing
  compatibility on macOS)

release.sh:
- set -eu added
- stellar config network add and stellar keys generate wrapped in
  idempotency checks instead of silently swallowing errors with || true

storage/mod.rs:
- TTL constants reduced from 31_536_000 to values within all known
  network limits (THRESHOLD: 518_400, EXTEND_TO: 3_110_400 ledgers)
  to prevent deterministic trap on networks with lower max_entry_ttl
Removes cdylib from crate-type. Having both cdylib and rlib caused cargo
to build a native dylib during non-WASM builds (tests, fuzzing) which
failed to link sancov symbols on macOS aarch64. The WASM artifact is now
produced explicitly by build.sh via cargo rustc -- --crate-type cdylib.
fuzz/corpus/ grows unboundedly and is rebuilt automatically by the fuzzer.
fuzz/artifacts/ contains crash inputs for already-fixed bugs and has no
value in git history once the corresponding fix is applied.
@aguilar1x aguilar1x self-assigned this Mar 1, 2026
@coderabbitai
Copy link

coderabbitai bot commented Mar 1, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Reworks vc-vault: replaces default-issuer with admin nomination/accept flow, scopes VC status to owner, adds sponsored-vault and fee APIs, introduces events and new errors, exposes modules, updates tests, adds fuzz targets, bumps workspace deps, changes crate type and build/release scripts, and updates .gitignore.

Changes

Cohort / File(s) Summary
Repo config
/.gitignore, Cargo.toml
Use recursive ignore patterns; bump workspace version to 0.21.0 and soroban-sdk to 23.4.0.
vc-vault crate manifest
contracts/vc-vault/Cargo.toml
Change crate crate-type from cdylib to rlib.
Public API surface
contracts/vc-vault/src/api/mod.rs
Large trait refactor: remove default_issuer param, add nominate_admin/accept_contract_admin, sponsor/vault and fee APIs, owner-scoped signatures, and verify_vc now returns VCStatus.
Contract implementation
contracts/vc-vault/src/contract.rs
Implement pending-admin nomination/acceptance, sponsor/vault flows, owner-scoped VC status, stricter issue/push/revoke/migrate semantics, and emit new events.
Storage & schema
contracts/vc-vault/src/storage/mod.rs
Redesign DataKey (PendingAdmin, owner-scoped VCStatus(Address,String)), add sponsored-vault keys, sponsor management, TTL adjustments, and many read/write signature changes.
Events
contracts/vc-vault/src/events/mod.rs
New events module with publish helpers: VaultCreated, SponsoredVaultCreated, VaultRevoked, IssuerAuthorized, IssuerRevoked, VCIssued, VCRevoked.
Errors
contracts/vc-vault/src/error.rs
Add NotAuthorizedSponsor, VCAlreadyExists, and NoPendingAdmin error variants (codes 11–13).
Issuance & issuer logic
contracts/vc-vault/src/issuance/mod.rs, contracts/vc-vault/src/vault/issuer.rs
revoke_vc and storage calls are owner-scoped; authorize_issuers deduplicates; revoke_issuer filters duplicates, panics if absent, and adds to denied list.
Model & visibility
contracts/vc-vault/src/model/vc_status.rs, contracts/vc-vault/src/lib.rs
Add Debug derive to VCStatus; make contract and model modules public; add events module.
Tests
contracts/vc-vault/src/test.rs
Update tests for new init API, nominate/accept flow, use VCStatus, use new env registration, expand mock-auth scaffolding and sponsored-vault tests.
Fuzz harnesses
contracts/vc-vault/fuzz/*
Add fuzz workspace, targets, and shared helpers exercising issue/revoke/verify/push/issuer/lifecycle flows.
Build & release scripts
scripts/build.sh, scripts/release.sh
Switch to explicit cargo rustc WASM build, Stellar CLI deploy steps, add set -eu and idempotent checks.
Misc
.gitignore additions
Ignore fuzz corpus/artifacts and use recursive lock/target patterns.

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant Contract
    participant Storage
    participant Events

    rect rgba(100,150,200,0.5)
    note over Caller,Contract: Admin nomination lifecycle
    Caller->>Contract: initialize(e, contract_admin)
    Contract->>Storage: write_contract_admin(contract_admin)
    Caller->>Contract: nominate_admin(e, new_admin)
    Contract->>Storage: write_pending_admin(new_admin)
    Caller->>Contract: accept_contract_admin(e) (signed by new_admin)
    Contract->>Storage: read_pending_admin()
    Contract->>Storage: write_contract_admin(new_admin)
    Contract->>Storage: remove_pending_admin()
    end

    rect rgba(150,200,100,0.5)
    note over Caller,Contract: Issue / Revoke (owner-scoped)
    Caller->>Contract: issue(e, owner, vc_id, issuer_ref)
    Contract->>Storage: read_vc_status(owner, vc_id)
    alt VC missing
    Contract->>Storage: write_vc_status(owner, vc_id, Valid)
    Contract->>Events: vc_issued(owner, vc_id, issuer_ref)
    else VC exists
    Contract-->>Caller: VCAlreadyExists error
    end
    Caller->>Contract: revoke(e, owner, vc_id, date)
    Contract->>Storage: read_vc_status(owner, vc_id)
    Contract->>Storage: write_vc_status(owner, vc_id, Revoked(date))
    Contract->>Events: vc_revoked(owner, vc_id, date)
    end
Loading
sequenceDiagram
    participant Sponsor
    participant ContractAdmin
    participant Contract
    participant Storage

    rect rgba(200,150,100,0.5)
    note over Sponsor,Contract: Sponsored vault creation
    ContractAdmin->>Contract: set_sponsored_vault_open_to_all(e, true)
    Contract->>Storage: write_sponsored_vault_open_to_all(true)
    Sponsor->>Contract: create_sponsored_vault(e, sponsor, owner, did_uri)
    Contract->>Storage: is_authorized_sponsor(sponsor)
    alt authorized or open_to_all
    Contract->>Storage: write_vault_did(owner, did_uri)
    Contract->>Storage: write_vault_admin(owner, sponsor)
    Contract->>Events: sponsored_vault_created(sponsor, owner, did_uri)
    else not authorized
    Contract-->>Sponsor: NotAuthorizedSponsor error
    end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 I hopped through keys and TTLs today,

Admins can nominate, sponsors can play.
Events now echo each vault and VC,
Owner-scoped status keeps order for me.
A tiny rabbit cheers: "Push the patch—hooray!"

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately captures the main changes: security fixes and fuzzing infrastructure additions for the vc-vault contract v1 acta release.
Docstring Coverage ✅ Passed Docstring coverage is 90.98% 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 unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch audit/vc-vault-acta-v1

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.

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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
contracts/vc-vault/src/contract.rs (1)

258-273: ⚠️ Potential issue | 🔴 Critical

Move VCStatus when pushing a VC between vaults.

At Line [260], status is validated using owner-scoped keys, but Line [265]-Line [269] only move payload and ID. The destination status remains Invalid, so verify_vc/revoke break after push, and stale source status can block re-issuance for the source owner.

🔧 Proposed fix
-        if storage::read_vc_status(&e, &from_owner, &vc_id) != VCStatus::Valid {
+        let status = storage::read_vc_status(&e, &from_owner, &vc_id);
+        if status != VCStatus::Valid {
             panic_with_error!(e, ContractError::VCNotFound);
         }
         let vc = vc_opt.unwrap();
@@
         storage::remove_vault_vc(&e, &from_owner, &vc_id);
         storage::remove_vault_vc_id(&e, &from_owner, &vc_id);
         storage::write_vault_vc(&e, &to_owner, &vc_id, &vc);
         storage::append_vault_vc_id(&e, &to_owner, &vc_id);
+        storage::write_vc_status(&e, &to_owner, &vc_id, &status);
+        storage::write_vc_status(&e, &from_owner, &vc_id, &VCStatus::Invalid);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/vc-vault/src/contract.rs` around lines 258 - 273, The push logic
only moves the VC payload and id but leaves the owner-scoped VCStatus behind;
read the status with storage::read_vc_status(&e, &from_owner, &vc_id) into a
variable, then remove the source status (e.g.,
storage::remove_vault_vc_status(&e, &from_owner, &vc_id)) and write the same
status for the destination (e.g., storage::write_vault_vc_status(&e, &to_owner,
&vc_id, &status)) before extending TTLs; if the remove/write status helpers
don't exist add them to storage to mirror remove_vault_vc / write_vault_vc
behavior so verify_vc and revoke work correctly after a push.
🧹 Nitpick comments (1)
contracts/vc-vault/src/issuance/mod.rs (1)

11-14: Error message may be misleading for non-existent VCs.

When vc_status is VCStatus::Invalid (VC doesn't exist), the function panics with VCAlreadyRevoked, which is misleading. Consider distinguishing between "VC not found" and "VC already revoked" scenarios.

♻️ Proposed fix to use appropriate error codes
 pub fn revoke_vc(e: &Env, owner: &Address, vc_id: String, date: String) {
     let vc_status = storage::read_vc_status(e, owner, &vc_id);
-    if vc_status != VCStatus::Valid {
-        panic_with_error!(e, ContractError::VCAlreadyRevoked)
+    match vc_status {
+        VCStatus::Valid => {},
+        VCStatus::Invalid => panic_with_error!(e, ContractError::VCNotFound),
+        VCStatus::Revoked(_) => panic_with_error!(e, ContractError::VCAlreadyRevoked),
     }
     storage::write_vc_status(e, owner, &vc_id, &VCStatus::Revoked(date))
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/vc-vault/src/issuance/mod.rs` around lines 11 - 14, The current
check treats any non-Valid status as VCAlreadyRevoked; update the logic after
calling storage::read_vc_status(e, owner, &vc_id) to distinguish
VCStatus::Invalid (non-existent) vs VCStatus::Revoked (actually revoked): if
vc_status == VCStatus::Invalid return/panic_with_error!(e,
ContractError::VCNotFound) (or add that error variant), else if vc_status ==
VCStatus::Revoked panic_with_error!(e, ContractError::VCAlreadyRevoked); leave
the Valid branch unchanged; ensure you reference the VCStatus enum,
read_vc_status function and panic_with_error! macro when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@contracts/vc-vault/src/contract.rs`:
- Around line 345-360: The create_sponsored_vault function currently only
authenticates the sponsor, allowing callers to create a vault for any owner when
open_to_all is true; add an owner.require_auth() call immediately after
sponsor.require_auth() in create_sponsored_vault (same placement used by
create_vault) so the owner must consent to vault/DID assignment and cannot be
targeted by a sponsor without their authorization.

In `@contracts/vc-vault/src/storage/mod.rs`:
- Around line 423-427: is_authorized_sponsor currently only reads existence of
DataKey::SponsoredVaultSponsor(sponsor.clone()) and does not refresh its TTL,
allowing sponsors to expire unexpectedly; change it so that when the key exists
you refresh its TTL (as done in other auth checks) by re-writing or explicitly
updating the persistent storage entry for
DataKey::SponsoredVaultSponsor(sponsor.clone()) with the same value to extend
the TTL (use the same TTL duration used elsewhere for sponsor keys) and then
return true; otherwise return false.

In `@contracts/vc-vault/src/vault/issuer.rs`:
- Around line 18-27: authorize_issuers replaces the issuer list but doesn't
clear any denied flags like authorize_issuer does; update authorize_issuers (the
function that builds deduped and calls storage::write_vault_issuers) to also
remove denied status for each issuer in the deduped list by calling the same
storage helper used by authorize_issuer (e.g., storage::remove_vault_denied(e,
owner, issuer) or the equivalent function) for each issuer before writing the
new issuers.

In `@scripts/release.sh`:
- Around line 5-8: The network existence check uses a substring grep ("grep -q
testnet") which can false-positive; change it to test for an exact alias match
before skipping creation—e.g., replace the grep check on the output of "stellar
config network ls" with an exact-match test (match the whole line or the alias
column) so that only an alias literally equal to "testnet" will cause the script
to skip running "stellar config network add testnet".

---

Outside diff comments:
In `@contracts/vc-vault/src/contract.rs`:
- Around line 258-273: The push logic only moves the VC payload and id but
leaves the owner-scoped VCStatus behind; read the status with
storage::read_vc_status(&e, &from_owner, &vc_id) into a variable, then remove
the source status (e.g., storage::remove_vault_vc_status(&e, &from_owner,
&vc_id)) and write the same status for the destination (e.g.,
storage::write_vault_vc_status(&e, &to_owner, &vc_id, &status)) before extending
TTLs; if the remove/write status helpers don't exist add them to storage to
mirror remove_vault_vc / write_vault_vc behavior so verify_vc and revoke work
correctly after a push.

---

Nitpick comments:
In `@contracts/vc-vault/src/issuance/mod.rs`:
- Around line 11-14: The current check treats any non-Valid status as
VCAlreadyRevoked; update the logic after calling storage::read_vc_status(e,
owner, &vc_id) to distinguish VCStatus::Invalid (non-existent) vs
VCStatus::Revoked (actually revoked): if vc_status == VCStatus::Invalid
return/panic_with_error!(e, ContractError::VCNotFound) (or add that error
variant), else if vc_status == VCStatus::Revoked panic_with_error!(e,
ContractError::VCAlreadyRevoked); leave the Valid branch unchanged; ensure you
reference the VCStatus enum, read_vc_status function and panic_with_error! macro
when making the change.

ℹ️ 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 c1d9de0.

📒 Files selected for processing (15)
  • .gitignore
  • Cargo.toml
  • contracts/vc-vault/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/events/mod.rs
  • contracts/vc-vault/src/issuance/mod.rs
  • contracts/vc-vault/src/lib.rs
  • contracts/vc-vault/src/model/vc_status.rs
  • contracts/vc-vault/src/storage/mod.rs
  • contracts/vc-vault/src/test.rs
  • contracts/vc-vault/src/vault/issuer.rs
  • scripts/build.sh
  • scripts/release.sh

…ing and understanding each function of the audit
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.

♻️ Duplicate comments (2)
contracts/vc-vault/src/contract.rs (2)

250-264: ⚠️ Potential issue | 🔴 Critical

VC status is not migrated to the destination vault during push.

After transferring the VC payload from from_owner to to_owner, the status entry remains keyed under (from_owner, vc_id). This means:

  • verify_vc(to_owner, vc_id) returns Invalid (no status exists for to_owner)
  • revoke(to_owner, vc_id, ...) fails with VCNotFound

The recipient cannot verify or manage the pushed credential.

Proposed fix: migrate status when pushing
         storage::remove_vault_vc(&e, &from_owner, &vc_id);
         storage::remove_vault_vc_id(&e, &from_owner, &vc_id);
+        // Migrate VC status to destination owner
+        let status = storage::read_vc_status(&e, &from_owner, &vc_id);
+        storage::write_vc_status(&e, &to_owner, &vc_id, &status);
+        storage::remove_vc_status(&e, &from_owner, &vc_id);
         storage::write_vault_vc(&e, &to_owner, &vc_id, &vc);
         storage::append_vault_vc_id(&e, &to_owner, &vc_id);

,

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

In `@contracts/vc-vault/src/contract.rs` around lines 250 - 264, The VC status
isn't migrated when pushing a credential: after verifying status with
storage::read_vc_status and moving the payload (vc via vc_opt) using
storage::remove_vault_vc / storage::write_vault_vc and the id helpers, read the
existing status for (from_owner, vc_id), write that status for (to_owner, vc_id)
and remove the old status for (from_owner, vc_id) so verify_vc and revoke work
for the recipient; update the same block (around read_vc_status, vc_opt,
storage::write_vault_vc, storage::append_vault_vc_id, storage::extend_vc_ttl) to
perform status migration and handle any potential missing-status cases
consistently (e.g., preserve error behavior or map to
ContractError::VCNotFound).

333-354: ⚠️ Potential issue | 🔴 Critical

Missing owner.require_auth() allows non-consensual vault creation.

A sponsor can create a vault for any arbitrary owner address without that owner's consent. This permanently assigns a DID to the owner's identity, and the owner cannot later call create_vault due to AlreadyInitialized. This enables identity squatting.

Proposed fix
 fn create_sponsored_vault(e: Env, sponsor: Address, owner: Address, did_uri: String) {
     sponsor.require_auth();
+    owner.require_auth();
     if !storage::has_contract_admin(&e) {

,

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

In `@contracts/vc-vault/src/contract.rs` around lines 333 - 354, In
create_sponsored_vault add an explicit consent check by calling
owner.require_auth() so the target owner must authorize creation; keep this call
(owner.require_auth()) right after sponsor.require_auth() and before the
existing storage::has_vault_admin check and any writes, so a sponsor cannot
unilaterally assign a DID to an owner (this will prevent the AlreadyInitialized
lockout described by storage::has_vault_admin and
ContractError::AlreadyInitialized).
🧹 Nitpick comments (3)
contracts/vc-vault/src/contract.rs (2)

450-460: Consider emitting an event when auto-authorizing issuers.

When ensure_issuer_authorized auto-authorizes an issuer (line 458), no event is emitted. In contrast, explicit authorize_issuer calls emit issuer_authorized. This makes it harder to audit when issuers were implicitly added.

Suggested improvement
         if storage::is_issuer_denied(e, owner, issuer_addr) {
             panic_with_error!(e, ContractError::IssuerNotAuthorized)
         }
         vault::authorize_issuer(e, owner, issuer_addr);
+        events::issuer_authorized(e, owner, issuer_addr);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/vc-vault/src/contract.rs` around lines 450 - 460, The
ensure_issuer_authorized function currently auto-authorizes issuers via
vault::authorize_issuer without emitting the issuer_authorized event; update
ensure_issuer_authorized to emit the same issuer_authorized event immediately
after calling vault::authorize_issuer so implicit authorizations are auditable,
using the same event payload/fields as the explicit authorize_issuer flow (i.e.,
match the event name and payload structure used elsewhere) and keep the denial
check (storage::is_issuer_denied) and panic behavior unchanged.

473-483: Clarify fee behavior when fee_override is zero.

When fee_enabled is true but fee_override <= 0, no fee is transferred. If zero means "waive fee," this is fine. If the intent is to fall back to a standard fee amount, the logic needs adjustment.

Consider adding a comment clarifying the expected behavior, or implement fallback to read_fee_standard when fee_override is zero.

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

In `@contracts/vc-vault/src/contract.rs` around lines 473 - 483, The current
branch checks storage::read_fee_enabled(e) but only transfers when fee_override
> 0, leaving behavior ambiguous when fee_override == 0; update the logic in the
block using storage::read_fee_token_contract, storage::read_fee_dest and
fee_override so that when fee_override <= 0 you read a fallback via
storage::read_fee_standard(e) and use that amount instead (only invoke
e.invoke_contract::<()>(..., symbol_short!("transfer"), (issuer_addr.clone(),
fee_dest, amount).into_val(e)) when the chosen amount > 0), or alternatively add
a clear comment above the branch explaining that fee_override == 0 intentionally
waives fees—pick one and implement consistently.
contracts/vc-vault/src/issuance/mod.rs (1)

11-12: Error variant VCAlreadyRevoked is misleading for the Invalid case.

The condition vc_status != VCStatus::Valid catches both Revoked and Invalid statuses, but the error VCAlreadyRevoked only describes one scenario. Consider either:

  1. Adding a separate check for Invalid with a distinct error (e.g., VCNotFound), or
  2. Renaming/documenting the error to clarify it covers "VC not in Valid state."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/vc-vault/src/issuance/mod.rs` around lines 11 - 12, The check using
VCStatus (`if vc_status != VCStatus::Valid`) conflates Revoked and Invalid
states while the error variant ContractError::VCAlreadyRevoked only describes
one case; split the logic to discriminate statuses (check `vc_status ==
VCStatus::Revoked` and `vc_status == VCStatus::Invalid`) and panic with distinct
errors (e.g., keep ContractError::VCAlreadyRevoked for Revoked and add
ContractError::VCNotFound or ContractError::VCNotValid for Invalid), or
alternatively rename/update the existing ContractError variant to something like
VCNotValid and update the panic_with_error!(...) call accordingly so the error
accurately reflects the VCStatus.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@contracts/vc-vault/src/contract.rs`:
- Around line 250-264: The VC status isn't migrated when pushing a credential:
after verifying status with storage::read_vc_status and moving the payload (vc
via vc_opt) using storage::remove_vault_vc / storage::write_vault_vc and the id
helpers, read the existing status for (from_owner, vc_id), write that status for
(to_owner, vc_id) and remove the old status for (from_owner, vc_id) so verify_vc
and revoke work for the recipient; update the same block (around read_vc_status,
vc_opt, storage::write_vault_vc, storage::append_vault_vc_id,
storage::extend_vc_ttl) to perform status migration and handle any potential
missing-status cases consistently (e.g., preserve error behavior or map to
ContractError::VCNotFound).
- Around line 333-354: In create_sponsored_vault add an explicit consent check
by calling owner.require_auth() so the target owner must authorize creation;
keep this call (owner.require_auth()) right after sponsor.require_auth() and
before the existing storage::has_vault_admin check and any writes, so a sponsor
cannot unilaterally assign a DID to an owner (this will prevent the
AlreadyInitialized lockout described by storage::has_vault_admin and
ContractError::AlreadyInitialized).

---

Nitpick comments:
In `@contracts/vc-vault/src/contract.rs`:
- Around line 450-460: The ensure_issuer_authorized function currently
auto-authorizes issuers via vault::authorize_issuer without emitting the
issuer_authorized event; update ensure_issuer_authorized to emit the same
issuer_authorized event immediately after calling vault::authorize_issuer so
implicit authorizations are auditable, using the same event payload/fields as
the explicit authorize_issuer flow (i.e., match the event name and payload
structure used elsewhere) and keep the denial check (storage::is_issuer_denied)
and panic behavior unchanged.
- Around line 473-483: The current branch checks storage::read_fee_enabled(e)
but only transfers when fee_override > 0, leaving behavior ambiguous when
fee_override == 0; update the logic in the block using
storage::read_fee_token_contract, storage::read_fee_dest and fee_override so
that when fee_override <= 0 you read a fallback via
storage::read_fee_standard(e) and use that amount instead (only invoke
e.invoke_contract::<()>(..., symbol_short!("transfer"), (issuer_addr.clone(),
fee_dest, amount).into_val(e)) when the chosen amount > 0), or alternatively add
a clear comment above the branch explaining that fee_override == 0 intentionally
waives fees—pick one and implement consistently.

In `@contracts/vc-vault/src/issuance/mod.rs`:
- Around line 11-12: The check using VCStatus (`if vc_status !=
VCStatus::Valid`) conflates Revoked and Invalid states while the error variant
ContractError::VCAlreadyRevoked only describes one case; split the logic to
discriminate statuses (check `vc_status == VCStatus::Revoked` and `vc_status ==
VCStatus::Invalid`) and panic with distinct errors (e.g., keep
ContractError::VCAlreadyRevoked for Revoked and add ContractError::VCNotFound or
ContractError::VCNotValid for Invalid), or alternatively rename/update the
existing ContractError variant to something like VCNotValid and update the
panic_with_error!(...) call accordingly so the error accurately reflects the
VCStatus.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c1d9de0 and 2064d04.

📒 Files selected for processing (3)
  • contracts/vc-vault/src/contract.rs
  • contracts/vc-vault/src/issuance/mod.rs
  • contracts/vc-vault/src/vault/issuer.rs

push moved the VC payload but never wrote the VCStatus entry in the
destination namespace. verify_vc(to_owner, vc_id) returned Invalid and
revoke(to_owner, vc_id) panicked with VCNotFound for any pushed credential.

Two regression tests added: test_verify_vc_valid_after_push_on_destination
and test_revoke_after_push_on_destination_succeeds.
push unconditionally wrote VCStatus::Valid for the destination, which
could overwrite an existing Revoked status — allowing a credential
revocation to be silently undone by a third party with a colliding vc_id.

Added the same precondition check already present in issue: destination
must have no existing payload and no existing non-Invalid status before
the push is allowed to proceed.

One regression test added: test_push_to_destination_with_existing_vc_id_panics.
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: 1

🧹 Nitpick comments (3)
contracts/vc-vault/src/contract.rs (1)

456-466: Auto-authorization during issue does not emit issuer_authorized event.

When ensure_issuer_authorized auto-authorizes an issuer (line 464), no event is emitted. This differs from the explicit authorize_issuer entrypoint (line 178) which emits issuer_authorized. This inconsistency may cause issues for off-chain indexers tracking issuer authorizations.

Suggested fix
         if !vault::is_authorized(&issuers, issuer_addr) {
             if storage::is_issuer_denied(e, owner, issuer_addr) {
                 panic_with_error!(e, ContractError::IssuerNotAuthorized)
             }
             vault::authorize_issuer(e, owner, issuer_addr);
+            events::issuer_authorized(e, owner, issuer_addr);
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/vc-vault/src/contract.rs` around lines 456 - 466,
ensure_issuer_authorized currently auto-authorizes an issuer via
vault::authorize_issuer but does not emit the same issuer_authorized event that
the public authorize_issuer entrypoint emits; after calling
vault::authorize_issuer within ensure_issuer_authorized, invoke the same event
emission used by the authorize_issuer entrypoint (the issuer_authorized event
emitter) with the same parameters (owner and issuer_addr) so off-chain indexers
see auto-authorizations consistently.
contracts/vc-vault/src/test.rs (2)

561-570: Consider adding test for owner vault operations after sponsored creation.

The sponsored vault tests verify creation authorization but don't test that the owner can subsequently perform vault operations (e.g., authorize_issuer, set_vault_admin). Adding such a test would confirm the vault is fully functional for the owner.

🤖 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 561 - 570, Add a new test (e.g.,
test_owner_operations_after_sponsored_creation) that uses the existing setup(),
calls client.initialize(&admin) and client.create_sponsored_vault(&admin,
&owner, &did_uri) as in test_admin_creates_sponsored_vault, then acts as the
owner to perform typical vault operations: call client.authorize_issuer(&owner,
&some_issuer), verify the issuer appears in the authorized list (or other
appropriate getter), call client.set_vault_admin(&owner, &new_admin) and assert
admin change took effect, and finally ensure client.list_vc_ids(&owner) still
behaves (e.g., returns empty Vec) to prove the owner can perform vault
operations after sponsored creation; reference the existing functions
create_sponsored_vault, authorize_issuer, set_vault_admin, and list_vc_ids on
the client to locate the code.

542-550: Missing positive test case for migrate function.

Only the error path (no legacy vault) is tested. A positive test that performs migration and then calls verify_vc on a migrated credential would help catch the issue where migrated VCs lack status entries.

🤖 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 542 - 550, Add a positive test
that verifies successful migration: use setup(), client.initialize(&admin) and
Address::generate to create an owner, create a legacy vault entry (using the
same legacy creation path your migration expects), issue a credential under that
legacy vault that includes a status entry, call client.migrate(&owner), then
call client.verify_vc(&credential) and assert it returns valid/ok and that the
migrated VC's status is present in the new vault storage; name the test
something like test_migrate_with_legacy_vault_succeeds and use the same
functions referenced in the diff (setup, client.create_vault/legacy creation,
client.migrate, client.verify_vc) so the behavior and presence of status entries
post-migration are asserted.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@contracts/vc-vault/src/contract.rs`:
- Around line 398-409: The migrated VCs are stored via vault::store_vc but no
status entry is written, so read_vc_status falls back to VCStatus::Invalid;
after storing each vc in the migration loop (the block calling vault::store_vc
for each vc.id), write a corresponding status record (use the storage module's
status-writing API, e.g., storage::write_vc_status or the existing storage
function that persists VC status) to set the correct initial status (e.g.,
Valid/Issued) for vc.id and owner before calling
storage::remove_legacy_vault_vcs and storage::extend_vault_ttl so migrated VCs
are not treated as Invalid by verify_vc.

---

Nitpick comments:
In `@contracts/vc-vault/src/contract.rs`:
- Around line 456-466: ensure_issuer_authorized currently auto-authorizes an
issuer via vault::authorize_issuer but does not emit the same issuer_authorized
event that the public authorize_issuer entrypoint emits; after calling
vault::authorize_issuer within ensure_issuer_authorized, invoke the same event
emission used by the authorize_issuer entrypoint (the issuer_authorized event
emitter) with the same parameters (owner and issuer_addr) so off-chain indexers
see auto-authorizations consistently.

In `@contracts/vc-vault/src/test.rs`:
- Around line 561-570: Add a new test (e.g.,
test_owner_operations_after_sponsored_creation) that uses the existing setup(),
calls client.initialize(&admin) and client.create_sponsored_vault(&admin,
&owner, &did_uri) as in test_admin_creates_sponsored_vault, then acts as the
owner to perform typical vault operations: call client.authorize_issuer(&owner,
&some_issuer), verify the issuer appears in the authorized list (or other
appropriate getter), call client.set_vault_admin(&owner, &new_admin) and assert
admin change took effect, and finally ensure client.list_vc_ids(&owner) still
behaves (e.g., returns empty Vec) to prove the owner can perform vault
operations after sponsored creation; reference the existing functions
create_sponsored_vault, authorize_issuer, set_vault_admin, and list_vc_ids on
the client to locate the code.
- Around line 542-550: Add a positive test that verifies successful migration:
use setup(), client.initialize(&admin) and Address::generate to create an owner,
create a legacy vault entry (using the same legacy creation path your migration
expects), issue a credential under that legacy vault that includes a status
entry, call client.migrate(&owner), then call client.verify_vc(&credential) and
assert it returns valid/ok and that the migrated VC's status is present in the
new vault storage; name the test something like
test_migrate_with_legacy_vault_succeeds and use the same functions referenced in
the diff (setup, client.create_vault/legacy creation, client.migrate,
client.verify_vc) so the behavior and presence of status entries post-migration
are asserted.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2064d04 and 3891d09.

📒 Files selected for processing (2)
  • contracts/vc-vault/src/contract.rs
  • contracts/vc-vault/src/test.rs

Six fuzz targets covering the full contract surface: fuzz_issue,
fuzz_revoke, fuzz_verify_vc, fuzz_push, fuzz_issuer_ops, and
fuzz_lifecycle. fuzz_lifecycle discovered A-22, A-23, and A-24 during
the audit.
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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@contracts/vc-vault/fuzz/fuzz_targets/common.rs`:
- Around line 8-11: The current s function slices input by bytes which can panic
on a multi-byte UTF-8 boundary; update s to safely truncate to a valid UTF-8
boundary before calling SStr::from_str: compute the largest byte index <= 256
that is at a char boundary (e.g., use input.char_indices() to find the last
index < = 256, or fall back to whole input), create safe from that valid slice,
and pass it to SStr::from_str(env, safe); keep references to the same symbols
(s, safe, SStr::from_str, Env) so reviewers can find the change quickly.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3891d09 and c11e46e.

📒 Files selected for processing (8)
  • contracts/vc-vault/fuzz/Cargo.toml
  • contracts/vc-vault/fuzz/fuzz_targets/common.rs
  • contracts/vc-vault/fuzz/fuzz_targets/fuzz_issue.rs
  • contracts/vc-vault/fuzz/fuzz_targets/fuzz_issuer_ops.rs
  • contracts/vc-vault/fuzz/fuzz_targets/fuzz_lifecycle.rs
  • contracts/vc-vault/fuzz/fuzz_targets/fuzz_push.rs
  • contracts/vc-vault/fuzz/fuzz_targets/fuzz_revoke.rs
  • contracts/vc-vault/fuzz/fuzz_targets/fuzz_verify_vc.rs

Comment on lines +8 to +11
pub fn s(env: &Env, input: &str) -> SStr {
let safe = &input[..input.len().min(256)];
SStr::from_str(env, safe)
}
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

Potential panic on multi-byte UTF-8 boundary slice.

Slicing &input[..input.len().min(256)] at a byte index can panic if input contains multi-byte UTF-8 characters and the slice boundary falls mid-character. Fuzz inputs from arbitrary can produce arbitrary UTF-8 sequences, making this likely to trigger.

🐛 Proposed fix using char_indices for safe truncation
 pub fn s(env: &Env, input: &str) -> SStr {
-    let safe = &input[..input.len().min(256)];
+    let safe = if input.len() <= 256 {
+        input
+    } else {
+        // Find the last valid char boundary at or before byte 256
+        match input.char_indices().take_while(|(i, _)| *i < 256).last() {
+            Some((i, c)) => &input[..i + c.len_utf8()],
+            None => "",
+        }
+    };
     SStr::from_str(env, safe)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub fn s(env: &Env, input: &str) -> SStr {
let safe = &input[..input.len().min(256)];
SStr::from_str(env, safe)
}
pub fn s(env: &Env, input: &str) -> SStr {
let safe = if input.len() <= 256 {
input
} else {
// Find the last valid char boundary at or before byte 256
match input.char_indices().take_while(|(i, _)| *i < 256).last() {
Some((i, c)) => &input[..i + c.len_utf8()],
None => "",
}
};
SStr::from_str(env, safe)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/vc-vault/fuzz/fuzz_targets/common.rs` around lines 8 - 11, The
current s function slices input by bytes which can panic on a multi-byte UTF-8
boundary; update s to safely truncate to a valid UTF-8 boundary before calling
SStr::from_str: compute the largest byte index <= 256 that is at a char boundary
(e.g., use input.char_indices() to find the last index < = 256, or fall back to
whole input), create safe from that valid slice, and pass it to
SStr::from_str(env, safe); keep references to the same symbols (s, safe,
SStr::from_str, Env) so reviewers can find the change quickly.

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.

🧹 Nitpick comments (2)
docs/setup-fuzzing.md (2)

58-58: Clarify the relationship between set -eu and stale artifacts.

The current phrasing suggests that set -eu prevents deploying stale artifacts, but set -eu only ensures the script fails fast on errors. It won't prevent deployment of a stale optimized artifact if a previous build succeeded but subsequent source changes were made without rebuilding. Consider rephrasing to clarify that set -eu ensures build failures are caught, and separately emphasize always running build.sh before deployment.

✍️ Suggested rewording
-The build script uses `cargo rustc -- --crate-type cdylib` to force cdylib output for the WASM build without declaring it in `Cargo.toml`. This is required because declaring `cdylib` in `Cargo.toml` would cause cargo to build a native `.dylib` during fuzzing, which fails to link sancov symbols on macOS. The script runs `set -eu` so it will fail fast on any error. Never deploy a stale artifact.
+The build script uses `cargo rustc -- --crate-type cdylib` to force cdylib output for the WASM build without declaring it in `Cargo.toml`. This is required because declaring `cdylib` in `Cargo.toml` would cause cargo to build a native `.dylib` during fuzzing, which fails to link sancov symbols on macOS. The script runs `set -eu` so it will fail fast on any error. Always run `build.sh` before deployment to ensure you deploy the latest code.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/setup-fuzzing.md` at line 58, Update the paragraph so it no longer
implies that `set -eu` prevents deploying stale artifacts: explicitly state that
`set -eu` only makes the script fail fast on errors, and add a separate sentence
advising to always run the build (e.g., run `build.sh`) before deployment to
avoid shipping stale optimized artifacts; keep references to the existing
phrasing about `cargo rustc -- --crate-type cdylib` intact and replace the
original combined sentence with two clear sentences — one about `set -eu`
behavior and one about the requirement to rebuild to avoid stale artifacts.

25-36: Consider adding a language specifier to the code block.

Adding a language specifier (e.g., text) improves markdown rendering consistency across different viewers.

📝 Proposed improvement
-```
+```text
 contracts/
   vc-vault/
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/setup-fuzzing.md` around lines 25 - 36, The fenced code block in
docs/setup-fuzzing.md (the block showing the project tree starting with
"contracts/" and "vc-vault/") lacks a language specifier; update the opening
fence from ``` to ```text (or another appropriate language) so markdown
renderers treat it as plain text and preserve formatting, e.g., change the
triple-backtick that precedes the "contracts/" tree to ```text.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@docs/setup-fuzzing.md`:
- Line 58: Update the paragraph so it no longer implies that `set -eu` prevents
deploying stale artifacts: explicitly state that `set -eu` only makes the script
fail fast on errors, and add a separate sentence advising to always run the
build (e.g., run `build.sh`) before deployment to avoid shipping stale optimized
artifacts; keep references to the existing phrasing about `cargo rustc --
--crate-type cdylib` intact and replace the original combined sentence with two
clear sentences — one about `set -eu` behavior and one about the requirement to
rebuild to avoid stale artifacts.
- Around line 25-36: The fenced code block in docs/setup-fuzzing.md (the block
showing the project tree starting with "contracts/" and "vc-vault/") lacks a
language specifier; update the opening fence from ``` to ```text (or another
appropriate language) so markdown renderers treat it as plain text and preserve
formatting, e.g., change the triple-backtick that precedes the "contracts/" tree
to ```text.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c11e46e and ea09a0a.

📒 Files selected for processing (1)
  • docs/setup-fuzzing.md

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