Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 32 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,26 +24,39 @@ See [`contracts/did-stellar-registry/README.md`](contracts/did-stellar-registry/

---

### `vc-vault-factory`

Deploys and tracks **single-tenant `vc-vault` instances**, one vault contract per holder, rather than a shared multi-tenant contract. Derives deterministic vault addresses from `(owner, salt)`, centralizes fee configuration, and maintains the `is_vault` registry that vaults use to validate cross-vault transfers.

| Category | Functions |
|---|---|
| Deploy | `deploy`, `deploy_sponsored`, `is_vault` |
| Fees | `set_fee_config`, `set_fee_enabled`, `set_fee_standard`, `set_fee_custom`, `remove_fee_custom`, `set_min_fee`, `quote_fee` |
| Admin | `nominate_admin`, `accept_admin`, `get_admin` |

See [`contracts/vc-vault-factory/README.md`](contracts/vc-vault-factory/README.md) for the full ABI.

---

### `vc-vault`

Per-holder vault for Verifiable Credentials on Stellar. Manages VC storage, issuance status, revocation, issuer authorization, and fee collection in USDC. Contract admin is set at deploy time via `__constructor`.
**Single-tenant** Verifiable Credential vault, each holder owns their own instance, deployed by the factory. Manages VC storage, issuance status, revocation, and cross-vault migration. Issuance is **open** (anyone may issue unless denylisted); fees are quoted by the factory at issuance time.

| Category | Functions |
|---|---|
| Admin | `nominate_admin`, `accept_contract_admin`, `version` |
| Vault | `create_vault`, `create_sponsored_vault`, `set_vault_admin`, `authorize_issuer`, `authorize_issuers`, `revoke_issuer`, `revoke_vault`, `list_authorized_issuers`, `list_denied_issuers`, `authorized_issuer_count`, `denied_issuer_count` |
| Credentials | `issue`, `batch_issue`, `issue_linked`, `revoke`, `verify_vc`, `get_vc`, `list_vc_ids`, `vc_count`, `push` |

See [`contracts/vc-vault/README.md`](contracts/vc-vault/README.md) for the full ABI, authorization model, and error codes.
| Vault | `set_vault_admin`, `set_vault_did`, `vault_did`, `vault_owner`, `deny_issuer`, `allow_issuer`, `revoke_vault`, `list_denied_issuers`, `denied_issuer_count` |
| Credentials | `issue`, `batch_issue`, `revoke`, `push`, `receive_push`, `verify_vc`, `get_vc`, `list_vc_ids`, `vc_count` |

---

## Testnet Deployments

| Contract | Contract ID |
|---|---|
| `did-stellar-registry` | `CB7ATU7SF5QUKJMSULJDJVWJZVDXC23HTZX6NFUDTSFPVT6MA575NNZJ` |
| `vc-vault` | `CATL4IDH7XXPDC2UHSEX2GP45PPBVDFSKUDTKCSQICDOJVDLYNKISXFH` |
| Contract | Version | Contract ID / WASM hash |
|---|---|---|
| `did-stellar-registry` | 0.2.0 | `CBUNQ3GX3ZQ4MF64H7JCYZMXLGOS47VPIQQS7NCR6V3KX6YP7O72L5QF` |
| `vc-vault-factory` | 0.1.0 | `CDRFQRIP4FA3WMPWCSAM3XEY6EM6EGKRYZRSCSVZ5NHCF6AGEVR2XEPQ` |
| `vc-vault` | 0.4.0 | template WASM hash `2bd0323a98acb8469606808368da6c79824f2dd8391494b94ddbeb3d22c1a957` (instances deployed via the factory) |

Network: Stellar Testnet (`Test SDF Network ; September 2015`)
Full deployment record: [`docs/deployments/testnet.md`](docs/deployments/testnet.md)
Expand All @@ -53,8 +66,9 @@ Full deployment record: [`docs/deployments/testnet.md`](docs/deployments/testnet
## Build

```bash
# Build a specific contract
# Build a specific contract (or `all`)
./scripts/build.sh vc-vault
./scripts/build.sh vc-vault-factory
./scripts/build.sh did-stellar-registry
```

Expand All @@ -71,11 +85,12 @@ Output files:
./scripts/deploy.sh <package> <network> <source-account>

# Examples
./scripts/deploy.sh did-stellar-registry testnet acta_deployer
./scripts/deploy.sh vc-vault testnet acta_deployer
./scripts/deploy.sh did-stellar-registry testnet acta_deployer # deploy
./scripts/deploy.sh vc-vault testnet acta_deployer # install template, prints WASM hash
./scripts/deploy.sh vc-vault-factory testnet acta_deployer # installs vault + deploys factory
```

Record the resulting contract ID in [`docs/deployments/<network>.md`](docs/deployments/).
`vc-vault` is not deployed standalone — the factory instantiates per-holder vaults via `factory.deploy(...)`. Record the resulting IDs in [`docs/deployments/<network>.md`](docs/deployments/).

---

Expand All @@ -95,8 +110,10 @@ The `did:stellar` v0.1 method specification lives at [`docs/did-spec/did-stellar
## Tests

```bash
cargo test -p vc-vault-contract # 127 tests
cargo test -p did-stellar-registry # 56 tests
cargo test # whole workspace (146 tests)
cargo test -p vc-vault-contract # 61 tests
cargo test -p vc-vault-factory-contract # 27 tests
cargo test -p did-stellar-registry # 58 tests
```

---
Expand Down
9 changes: 3 additions & 6 deletions contracts/did-stellar-registry/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,6 @@ Two-step admin transfer. Per-DID mutations are NOT admin-gated — the admin rol
| `accept_admin()` | Proposed admin accepts the role. Both the current admin (already past) and the proposed admin must have signed the two calls. Emits `AdminTransferred`. Fails with `NoProposedAdmin` if no proposal exists. |
| `get_admin() -> Address` | Read the current admin. No authorization required. |

**The contract WASM is intentionally NOT upgradeable.** There is no `upgrade(new_wasm_hash)` function. To migrate, deploy a new contract and migrate state explicitly.

The auto-generated client struct is `DidStellarRegistryClient`.

---

## Authorization
Expand Down Expand Up @@ -77,13 +73,14 @@ Codes are part of the ABI. Numeric values MUST NOT be renumbered.
| 11 | `KeyEmpty` | `public_key_multibase` is empty. |
| 12 | `ServiceTypeTooLong` | `service_type.len()` > 64 chars. |
| 13 | `ServiceIdTooLong` | `id_suffix.len()` > 32 chars. |
| 14 | `ServiceIdInvalidFormat` | `id_suffix` does not match `^[a-z0-9-]+$`. |
| 14 | `ServiceIdInvalidFormat` | `id_suffix` does not match `^[a-z0-9][a-z0-9-]*[a-z0-9]$` (or a single `[a-z0-9]`); leading/trailing hyphens rejected. |
| 15 | `ServiceEndpointInvalid` | `service_endpoint` is not `https://...` or > 255 chars. |
| 16 | `MetadataUriInvalid` | `metadata_uri` is not `https://...` or > 255 chars. |
| 17 | `NoProposedAdmin` | `accept_admin` called when no proposal exists or proposal expired. |
| 18 | `ServiceTypeEmpty` | `service_type` is empty. |
| 19 | `VersionOverflow` | DID `version` has reached `u32::MAX`; further mutations are rejected. |
| 20 | `MetadataInconsistent` | `metadata_hash` is set but `metadata_uri` is absent. |
| 21 | `DuplicateServiceId` | Two services in the same record share the same `id_suffix`. |

---

Expand Down Expand Up @@ -138,7 +135,7 @@ Defined in `src/model.rs`:
| `key_agreement.len()` | 0–1 |
| `services.len()` | 0–3 |
| `public_key_multibase` | 1–128 chars; unique within each relationship |
| `service.id_suffix` | 1–32 chars; `^[a-z0-9-]+$` |
| `service.id_suffix` | 1–32 chars; `^[a-z0-9][a-z0-9-]*[a-z0-9]$` (or single char); unique across services |
| `service.service_type` | 1–64 chars |
| `service.service_endpoint` | `https://`, ≤ 255 chars |
| `metadata_uri` | `https://`, ≤ 255 chars |
Expand Down
50 changes: 48 additions & 2 deletions contracts/vc-vault-factory/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# vc-vault-factory

Soroban smart contract that deploys and tracks **single-tenant `vc-vault` instances** on Stellar. Each holder gets their own vault contractone deployment per identity rather than sharing a single multi-tenant contract. The factory derives deterministic vault addresses from `(owner, salt)` and maintains a registry used by vaults to validate cross-vault VC transfers.
Soroban smart contract that deploys and tracks **single-tenant `vc-vault` instances** on Stellar. Each holder gets their own vault contract, one deployment per identity, rather than sharing a single multi-tenant contract. The factory derives deterministic vault addresses from `(owner, salt)` and maintains a registry used by vaults to validate cross-vault VC transfers.

---

Expand Down Expand Up @@ -31,6 +31,30 @@ Soroban smart contract that deploys and tracks **single-tenant `vc-vault` instan

The vault constructor called by `deploy_v2` receives `(owner, contract_admin, did_uri, factory_address)`. The factory address is stored inside each vault so it can call `is_vault` during `receive_push`.

### Fee configuration

Fees are centralized in the factory: a vault calls `quote_fee(issuer)` at issuance time and transfers the quoted amount (in the configured token) from the issuer to the configured destination. All setters require the admin.

| Function | Auth | Description |
|---|---|---|
| `set_fee_config(token, dest, standard)` | admin | Set the fee token contract, destination, and standard amount. |
| `set_fee_enabled(enabled)` | admin | Toggle fee charging. Enabling requires token + dest + standard to be set (`FeeNotConfigured`). |
| `set_fee_standard(amount)` | admin | Update the standard fee. |
| `set_fee_custom(issuer, amount, expires_at)` | admin | Per-issuer fee override; `expires_at` (optional) must be in the future. |
| `remove_fee_custom(issuer)` | admin | Remove a per-issuer override. |
| `set_min_fee(amount)` | admin | Floor enforced on all fee amounts. |
| `quote_fee(issuer) -> FeeQuote` | none | Returns `{ enabled, amount, token, dest }`. Disabled → `{false, 0, None, None}`; otherwise the issuer's non-expired custom fee, or the standard. |

All amounts are validated to be in `[min_fee, MAX_FEE_AMOUNT]` (`MAX_FEE_AMOUNT = 10^18`) and non-negative.

### Admin

| Function | Auth | Description |
|---|---|---|
| `nominate_admin(new_admin)` | admin | Propose a successor (two-step). |
| `accept_admin()` | proposed admin | Complete the transfer. Fails with `NoPendingAdmin` if none pending. |
| `get_admin() -> Address` | none | Read the current admin. |

---

## Events
Expand All @@ -39,6 +63,28 @@ The vault constructor called by `deploy_v2` receives `(owner, contract_admin, di
|---|---|---|
| `VaultDeployed` | `owner`, `vault_address` | `deploy` |
| `SponsoredVaultDeployed` | `deployer`, `owner`, `vault_address` | `deploy_sponsored` |
| `AdminNominated` | `current`, `nominee` | `nominate_admin` |
| `AdminTransferred` | `old_admin`, `new_admin` | `accept_admin` |
| `FeeConfigSet` | `token`, `dest`, `standard` | `set_fee_config` |
| `FeeEnabledChanged` | `enabled` | `set_fee_enabled` |
| `FeeStandardSet` | `amount` | `set_fee_standard` |
| `FeeCustomSet` | `issuer`, `amount`, `expires_at` | `set_fee_custom` |
| `FeeCustomRemoved` | `issuer` | `remove_fee_custom` |
| `MinFeeSet` | `amount` | `set_min_fee` |

---

## Error codes

| Code | Variant | Trigger |
|---:|---|---|
| 1 | `NoPendingAdmin` | `accept_admin` with no pending nomination |
| 2 | `InvalidFeeAmount` | Fee amount is negative |
| 3 | `FeeOutOfBounds` | Fee amount exceeds `MAX_FEE_AMOUNT` (10^18) |
| 4 | `FeeBelowMin` | Fee amount below the configured `min_fee` |
| 5 | `FeeNotConfigured` | `set_fee_enabled(true)` before token + dest + standard are set |
| 6 | `ExpiryInPast` | Custom fee `expires_at` is not in the future |
| 7 | `NotInitialized` | `VaultMeta` missing (constructor never ran) |

---

Expand All @@ -63,7 +109,7 @@ deploy_salt = keccak256( user_salt (32 bytes) || XDR(owner) )
vault_address = hash( factory_address || deploy_salt )
```

`XDR(owner)` is the canonical XDR serialization of the owner `Address` (i.e. `Address.toXDR(env)` on-chain / the equivalent ScAddress XDR encoding off-chain) **not** its StrKey display string. A client precomputing a vault address must hash the raw XDR bytes of the owner address, not the `"G..."`/`"C..."` text.
`XDR(owner)` is the canonical XDR serialization of the owner `Address` (i.e. `Address.toXDR(env)` on-chain / the equivalent ScAddress XDR encoding off-chain), **not** its StrKey display string. A client precomputing a vault address must hash the raw XDR bytes of the owner address, not the `"G..."`/`"C..."` text.

Two different owners using the same user salt get different vault addresses. The same owner using different salts also gets different addresses. This means a vault address can be pre-computed client-side before submitting a transaction.

Expand Down
Loading
Loading