Skip to content

merge: dev into main#51

Merged
aguilar1x merged 45 commits into
mainfrom
dev
May 15, 2026
Merged

merge: dev into main#51
aguilar1x merged 45 commits into
mainfrom
dev

Conversation

@aguilar1x
Copy link
Copy Markdown
Contributor

No description provided.

aguilar1x added 30 commits May 7, 2026 19:47
Closes #21.

list_vc_ids gains offset/limit parameters and returns the slice
[offset, min(offset + limit, count)). Empty result when offset >=
count or limit == 0. Panics with the new LimitTooLarge = 16 error
when limit exceeds MAX_LIST_LIMIT (200). The cap keeps the worst-
case enumeration well under Soroban's 1.4M instruction budget while
still allowing the full MAX_VCS_PER_VAULT (1000) to be retrieved in
five paginated calls.

vc_count is a new O(1) entrypoint that reads VaultVCCount directly,
so SDKs can size their iteration without paying for any slot read.
Returns 0 for unknown vaults — no error path needed for callers
inspecting addresses they do not own.

Breaking change: callers of list_vc_ids must pass offset and limit.
All existing test callsites were updated.

Adds eight regression tests: pagination consistency across windows,
zero limit, offset beyond count, limit clamped to count, limit
above MAX_LIST_LIMIT, vc_count tracking through issue/revoke/push,
empty vault, and unknown vault.

Tests: 77 passed (69 prior + 8 new), 0 failed.
Merge pull request #31 from ACTA-Team/feat/vc-pagination
Closes #22.

Vaults that existed before #20 still hold their vc_ids in the legacy
`VaultVCIds(owner)` Vec. After upgrade, the new O(1) helpers ignore
that Vec, so list_vc_ids returns empty and reissuance starts the
position counter from zero. migrate_vc_index moves each legacy vc_id
into the new VaultVCCount/VaultVCIndex/VaultVCPosition layout,
preserving stored order.

The function takes no auth: the migration is fully deterministic from
on-chain state and only relocates vc_ids, so any caller can drive it.
Backend services can migrate vaults proactively, avoiding a
"first-write-after-upgrade is expensive" cliff for users who would
otherwise pay the legacy-Vec cost on their next issue() call.

Semantics:

- Panics with VCSAlreadyMigrated when vc_count > 0. Catches both
  double-calls and post-v0.2 vaults that never had legacy data.
- Empty vault (no legacy entry, vc_count == 0) is a clean no-op.
- Iterates the legacy Vec in stored order so list_vc_ids returns the
  same sequence post-migration.
- Removes VaultVCIds(owner) afterward.
- Extends vault TTL after the writes.

Six new tests:

- successful migration of three vc_ids into the new index
- double-call panics
- no-legacy vault is a no-op
- post-upgrade vault with new entries panics (catches misuse)
- no-auth: migration runs after env.set_auths(&[])
- legacy order is preserved in the new positions

Tests: 83 passed (77 prior + 6 new), 0 failed.
Merge pull request #32 from ACTA-Team/feat/vc-migrate-index
Closes #24.

Issuers like BAF (130+ credentials per program) need a batch path so
they don't pay 130 separate transaction fees. batch_issue accepts up
to MAX_BATCH_SIZE = 5 (vc_id, vc_data) tuples and reuses one auth
signature, one fee transfer, and one vault TTL extension across the
batch.

Cap rationale: each VC writes 4 ledger entries (VaultVC,
VaultVCIndex, VaultVCPosition, VCStatus). At 5 VCs that's 20 + 1
shared VaultVCCount = 21, leaving margin under Soroban's ~25 entry
write limit for the optional fee transfer (~3 entries: token, source
balance, destination balance).

Semantics:

- Single issuer.require_auth() for the whole batch.
- Single fee transfer of fee_override × n when fees enabled and
  fee_override > 0. Uses saturating_mul; absurd inputs cap at i128::MAX
  and the token contract rejects on insufficient balance.
- Per VC: existence check (catches both intra-batch and pre-existing
  duplicates), VaultVC + VaultVCIndex + VaultVCPosition writes via
  vault::store_vc, VCStatus = Valid, per-VC TTL extend, individual
  VCIssued event.
- Single extend_vault_ttl after the loop.
- Mirrors single issue() auto-authorization: an issuer not yet on the
  vault's authorized list and not denied is auto-added.
- Returns the vc_ids in input order.

New error codes:
- BatchTooLarge = 17 (n > MAX_BATCH_SIZE)
- BatchEmpty = 18 (n == 0)

Ten new tests:

- writes all VCs in input order
- batch of 5 (at MAX_BATCH_SIZE) succeeds
- batch of 6 panics with BatchTooLarge
- empty batch panics with BatchEmpty
- duplicate vc_id within the batch panics with VCAlreadyExists
- duplicate against an existing VC panics with VCAlreadyExists
- revoked vault panics with VaultRevoked
- wrong vault contract panics with InvalidVaultContract
- emits one VCIssued event per VC in the batch
- auto-authorizes an unknown issuer (parity with single issue)

Tests: 93 passed (83 prior + 10 new), 0 failed.
Merge pull request #33 from ACTA-Team/feat/vc-batch-issue
Closes #23.

Bumps vc-vault-contract version from 0.1.0 to 0.2.0 and records the
new testnet deployment.

Contract ID:  CBXC6LXBY5FGEG46VZ4AJ2AH2EJBINBA7BMILIEO4EJYI6ZTY7K7J5D5
WASM hash:    c8da61dd3dd46b2810a743d50a388c09a00f0b7e8e2df7ceb5a71c8ce5dc4dd8

The v0.1.0 entry stays in docs/deployments/testnet.md as historical
record. The new contract is a fresh deploy (not an upgrade) since
v0.1.0 was never `initialize`d on chain and has no production state.

Tests: 93 passed. WASM 44188 bytes (optimized).
Merge pull request #34 from ACTA-Team/chore/release-v0.2.0
Closes #25.

Every public entrypoint that accepts a user-controlled string or
unbounded list now validates length before doing any storage I/O. An
attacker submitting megabyte-sized vc_data, did_uri, or a 10k-address
issuer list previously could:

- inflate per-vault storage rent indefinitely,
- cost legitimate readers extra CPU on every list/lookup, and
- amplify the cost of any future migration that re-reads the entry.

The caps are conservative — 4-10x the largest realistic value — so
they bite only on adversarial or buggy callers, never on real flows.

Caps:

  vc_id          64 bytes   (UUIDs are 36)
  vc_data        10,000 bytes  (encrypted payloads typically 1-5KB)
  did_uri        256 bytes  (longest realistic DIDs are ~60 chars)
  issuer_did     256 bytes  (same shape as did_uri)
  date           64 bytes   (ISO 8601 is 20-30 chars)
  issuers list   100 entries

New error codes:

  InputTooLong        = 19   (oversize string)
  IssuerListTooLong   = 20   (oversize authorize_issuers list)

Caps apply at every entrypoint that accepts the relevant input,
including read paths (get_vc, verify_vc, get_vc_parent, push) so a
caller can't force the contract to spend instructions hashing a 1MB
key before the lookup misses.

Eleven new tests:

- create_vault accepts did_uri at MAX_DID_URI_LEN
- create_vault rejects did_uri over the cap
- issue accepts vc_id at MAX_VC_ID_LEN
- issue rejects vc_id over the cap
- issue rejects vc_data over MAX_VC_DATA_LEN
- issue rejects issuer_did over MAX_ISSUER_DID_LEN
- revoke rejects date over MAX_DATE_LEN
- authorize_issuers accepts list at MAX_ISSUERS_LIST
- authorize_issuers rejects list over the cap
- batch_issue rejects an oversize vc_id within the batch
- get_vc rejects oversize vc_id

Tests: 104 passed (93 prior + 11 new), 0 failed.
CodeRabbit flagged that the cap added in the previous commit only
covered the bulk replace path (authorize_issuers). Single-add and the
auto-authorization triggered by issue / batch_issue / issue_linked
through ensure_issuer_authorized still let the stored list grow past
the cap, leaving the DoS surface partially open: an attacker could
spam issue() from many fresh addresses and inflate VaultIssuers
indefinitely.

The fix moves the cap check into vault::authorize_issuer, the helper
shared by both single-add and the auto-auth fallback. authorize_issuers
(bulk replace) keeps its entrypoint-level check because it overwrites
the stored list entirely — the new list's size is what matters there.

Two regression tests:

- direct authorize_issuer past 100 panics with IssuerListTooLong
- issue() from a 101st auto-authorized address panics on the auto-auth
  step inside ensure_issuer_authorized

Tests: 106 passed (104 prior + 2 new), 0 failed.
Merge pull request #36 from ACTA-Team/feat/input-caps
Closes #26.

vc-vault previously emitted events for vault and VC lifecycle but every
administrative mutation was silent: initialize, admin transfer (both
nominate and accept), fee config, upgrade, sponsor management, and
the two migration entrypoints all changed state without observable
output. Indexers had to diff storage between blocks to track these,
which is brittle and expensive.

This change adds 15 #[contractevent] structs covering every previously
silent path:

  Admin / governance
    ContractInitialized       admin
    AdminNominated            current_admin, nominee
    AdminTransferred          old_admin, new_admin

  Upgrade
    ContractUpgraded          new_wasm_hash

  Fees
    FeeEnabledChanged         enabled
    FeeConfigSet              token_contract, fee_dest, fee_amount
    FeeAdminSet               amount
    FeeStandardSet            amount
    FeeEarlySet               amount
    FeeCustomSet              issuer, amount

  Sponsors
    SponsorOpenToAllChanged   open
    SponsorAdded              sponsor
    SponsorRemoved            sponsor

  Migrations
    VaultMigrated             owner
    VaultIndexMigrated        owner, migrated_count

Design notes:

- AdminNominated and AdminTransferred are two events for the two-step
  transfer so the indexer sees the full flow, not just the result. The
  accept path captures the outgoing admin before overwriting so the
  event carries both sides.
- Fee presets emit separate events per kind (admin / standard / early /
  custom) instead of a combined FeePresetSet { kind, amount }. Indexers
  can filter by exact preset without parsing an enum.
- ContractUpgraded carries only new_wasm_hash. Soroban v23 has no
  getter for the current WASM hash, so emitting old + new would require
  manually tracking the hash in instance storage — extra storage and
  initialize-flow complexity for data already in ledger history.
- VaultIndexMigrated reports migrated_count so an indexer can size the
  move without diffing state.
- Upgrade emits BEFORE update_current_contract_wasm so the event is
  registered against the WASM driving the upgrade, not the incoming one
  (which may have a different schema).

14 new tests cover every event emitter except upgrade. The upgrade
entrypoint reverts when given an unresolved WASM hash, which discards
the event from the same invocation; testing it requires uploading a
second WASM and using its hash, out of scope here. The publisher
follows the identical pattern as every other event in this file.

Tests: 121 passed (106 prior + 15 new), 0 failed.
Merge pull request #37 from ACTA-Team/feat/events-state-transitions
Replaces VaultIssuers and VaultDeniedIssuers monolithic Vec entries with
a three-key index per side (count + slot→issuer + issuer→slot), making
authorize, revoke, and existence checks O(1) regardless of list size.

Adds migrate_issuer_index entrypoint to bridge legacy data forward, plus
list_authorized_issuers, list_denied_issuers, authorized_issuer_count,
and denied_issuer_count for paginated enumeration.
Merge pull request #38 from ACTA-Team/feat/o1-issuer-index-closes-27
)

On the auto-authorize path, is_authorized and denied_issuer_index_contains
already confirm the issuer is absent from both indexes before we reach the
append step. Calling vault::authorize_issuer from there triggered two extra
persistent reads: a duplicate VaultIssuerPosition check and a no-op
VaultDeniedIssuerPosition lookup inside remove_denied_issuer_from_index.

Replace the vault::authorize_issuer call with storage::append_issuer_to_index
directly, saving 2 persistent reads (~6-10k instructions) on every issue,
batch_issue, and issue_linked invocation with a new issuer.
Merge pull request #39 from ACTA-Team/feat/dedupe-issuer-reads-closes-28
Adds InvalidFeeAmount (#22) and FeeOutOfBounds (#23) errors, a
MAX_FEE_AMOUNT constant (10^18 stroops), and a require_fee_amount helper
called at set_fee_config, set_fee_admin, set_fee_standard, set_fee_early,
set_fee_custom, issue, and batch_issue. Negative amounts and values above
the cap are rejected before any storage write or auth check.
Merge pull request #40 from ACTA-Team/feat/validate-fee-bounds-closes-29
…closes #35)

Replace the arbitrary 1,000-VC cap with a u32 overflow guard in append_vc_to_index.
Add migrate_vc_index_chunk for large legacy vaults that cannot be migrated in a
single transaction due to Soroban's ledger-write limit.
…35)

Merge pull request #41 from ACTA-Team/feat/remove-vc-cap-closes-35
Remove the four migration entrypoints (migrate, migrate_vc_index,
migrate_vc_index_chunk, migrate_issuer_index), their storage keys,
helper functions, events, error codes, tests, and seed helpers.
All data has been migrated on testnet; the O(1) index is the sole
active write path since v0.2.0.
Merge pull request #43 from ACTA-Team/chore/remove-legacy-migration-scaffolding
- Move all MAX_* and TTL constants to constants.rs; storage re-exports
  them via pub use so all existing callers are unchanged
- Extract 13 private helpers (auth guards + input validators) to
  validator.rs; rename validate_* to require_* to match Soroban idiom
- Rename api/mod.rs to interface.rs and update contract.rs import
- Replace issuance::revoke_vc with vault::revoke_vc

contract.rs shrinks from 841 to ~560 lines (entrypoints only)
… layer

- api/mod.rs (single trait file in a solo folder) renamed to interface.rs
- issuance/mod.rs (single-function module) absorbed into vault/credential.rs;
  revoke_vc and store_vc_with_fee now live alongside store_vc in the vault layer
- Delete the api/ and issuance/ directories
storage/mod.rs (844 lines) split into focused files:
- config.rs   — admin and fee helpers (instance storage)
- vault.rs    — vault metadata helpers
- issuer.rs   — authorized and denied issuer index (O(1) swap-and-pop)
- credential.rs — VC payloads, O(1) VC index, parent links, VC status
- ttl.rs      — TTL extension helpers
- sponsor.rs  — sponsored vault config

mod.rs becomes a ~60-line hub: DataKey enum + pub use re-exports.
All callers continue to use storage::* unchanged.
If the tail slot is absent during a swap-and-pop, the previous
if-let-Some silently skipped the swap but still decremented count
and removed the position mapping, leaving a stale forward index entry.
Replace with .unwrap() so the operation panics instead of corrupting
the index.

Affects remove_vc_from_index, remove_issuer_from_index, and
remove_denied_issuer_from_index.
Merge pull request #45 from ACTA-Team/refactor/architecture-v2-patterns
aguilar1x added 15 commits May 14, 2026 13:41
Eliminates the initialize/frontrunning window: contract admin is now set
atomically at deploy time via the Soroban constructor. Removes the
AlreadyInitialized guard (constructors run exactly once) and the
require_auth call (deployer controls constructor args).

Tests updated: register() now passes (admin,) as constructor args;
standalone client.initialize() calls removed from all tests;
obsolete initialize-specific test cases replaced with constructor equivalents.
The AlreadyInitialized error variant no longer describes contract
initialization — that path was removed when initialize() was replaced
by the constructor. Both remaining usages guard against duplicate vault
creation in create_vault and create_sponsored_vault. Discriminant stays
1 for on-chain compatibility.
Eliminates a single-file solo directory; consistent with the
api/mod.rs → interface.rs rename already applied. No functional change.
Moves VC transfer logic (tombstone management, parent link migration,
index updates, TTL extensions) to vault/credential.rs alongside
store_vc, store_vc_with_fee, and revoke_vc. contract.rs push() is now
a thin dispatcher: 5 guard calls + vault::push_vc().
model/ implies MVC/ORM semantics that don't apply here. types/ is the
idiomatic name in the Soroban ecosystem for contracttype-annotated
domain types (VCStatus, VerifiableCredential). Updates all import sites.
- Replace initialize() row with __constructor deployment note
- Rename error #1 AlreadyInitialized → VaultAlreadyExists
- Add missing batch_issue, vc_count, list_authorized_issuers,
  list_denied_issuers, authorized_issuer_count, denied_issuer_count
- Fix list_vc_ids signature to include offset + limit params
- Add ContractInitialized to events table
- Fix deploy script example: vc-vault-contract → vc-vault
- Remove Initialize from contract_admin capabilities in auth table
…a constraint

- Add ServiceTypeEmpty (#18), VersionOverflow (#19), MetadataInconsistent (#20)
  to error codes table
- Fix ServiceTypeTooLong (#12) description: was 'empty or > 64 chars',
  empty is now its own code #18
- Document metadata_hash + metadata_uri consistency rule in validation bounds
- Remove initialize from vc-vault admin functions (now __constructor)
- Add batch_issue, vc_count, authorize_issuers, list_authorized_issuers,
  list_denied_issuers to vc-vault function summary
- Update vc-vault test count: 63 → 127
VCStatus(from_owner, vc_id) is intentionally left as Valid after push
to block re-issuance of the same vc_id in the source vault. Without
refreshing its TTL at push time, the entry expires ~180 days after
issue — allowing re-issuance once the tombstone is gone.
Merge pull request #47 from ACTA-Team/refactor/constructor-and-rename
Bump version to 0.3.0. Deploy to testnet:
  Contract ID: CATL4IDH7XXPDC2UHSEX2GP45PPBVDFSKUDTKCSQICDOJVDLYNKISXFH
  WASM hash:   775a141520de56fb4b1ebeb55d63e49fadf03f467ea8444cddb2caed2756ca8c

Also fix deploy.sh to pass --contract_admin constructor arg for vc-vault.
Merge pull request #49 from ACTA-Team/chore/release-vc-vault-v0.3.0
Merge pull request #50 from ACTA-Team/chore/release-vc-vault-v0.3.0
@aguilar1x aguilar1x self-assigned this May 15, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 15, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9c1fa561-0ffc-4b37-b19a-f9afaed28f26

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

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 merged commit 575bd4d into main May 15, 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