The on-chain registry for SynapsVault resources. Stores creator address, price (in USDC stroops), metadata pointer (IPFS CID / content hash), tags, and listing status.
Only the registered creator can mutate their resource (require_auth). Ownership can be transferred. Supports paginated listing and metadata updates.
Functions
| Function | Auth | Description |
|---|---|---|
init(admin) |
— (once) | Set upgrade admin |
register(creator, id, price, metadata, tags) |
Creator | Register a resource |
set_price / update_metadata / set_tags |
Creator | Mutate a resource |
set_listed / delist |
Creator | Toggle discoverability |
propose_transfer(id, new_creator) |
Creator | Propose ownership transfer (step 1 of 2) |
accept_transfer(id, new_creator) |
New creator | Accept proposed transfer (step 2 of 2) |
cancel_transfer(id) |
Creator | Cancel a pending transfer |
get / get_owner / exists / count |
None | Reads |
list(start, limit) |
None | Paginated, max 20 per page |
upgrade(new_wasm_hash) |
Admin | Upgrade WASM |
See docs/CONTRACTS.md for the full interface reference.
Time-limited on-chain access grants. The backend issues a Lease struct with a specific expires_at ledger sequence. Any party can verify access with a single read — no need to trust the backend.
Functions
| Function | Auth | Description |
|---|---|---|
init(admin) |
— (once) | Set admin at deploy |
set_admin(new_admin) |
Admin | Rotate admin |
grant_lease(resource_id, buyer, duration_ledgers) |
Admin | Issue a timed lease |
extend_lease(resource_id, buyer, extra_ledgers) |
Admin | Extend lease |
is_valid(resource_id, buyer) |
None | Check active status |
get_lease(resource_id, buyer) |
None | Full lease struct |
revoke_lease(resource_id, buyer) |
Admin | Revoke on refund / ToS |
See docs/CONTRACTS.md for the full interface reference.
Recurring 30-day subscription plans. Publishers define plans; the backend subscribes buyers and renews each cycle. Subscribers can self-cancel with access through the period end.
Functions
| Function | Auth | Description |
|---|---|---|
init(admin) |
— (once) | Set admin at deploy |
set_admin(new_admin) |
Admin | Rotate admin |
create_plan(publisher, plan_id, price_per_cycle) |
Publisher | Define a plan (ids are unique) |
get_plan(plan_id) |
None | Read a plan |
deactivate_plan(plan_id) |
Publisher | Close plan to new subs |
subscribe(plan_id, subscriber) |
Admin | Start subscription |
renew(plan_id, subscriber) |
Admin | Add one billing cycle |
cancel(plan_id, subscriber) |
Subscriber | Self-cancel (access runs to period end) |
is_active(plan_id, subscriber) |
None | Check active status |
get_subscription(plan_id, subscriber) |
None | Full sub struct |
See docs/CONTRACTS.md for the full interface reference.
Detailed contract documentation lives in docs/CONTRACTS.md. It covers the full public interface, data structures, storage layout, and error codes for each contract.
Note: Keep documentation in sync with code. When you change a contract's public interface, storage layout, or error codes, update
docs/CONTRACTS.mdin the same pull request.
# Prerequisites: Rust 1.91+ (rust-toolchain.toml installs the wasm32v1-none target)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Stellar CLI v25.2+ (required to build contracts) — see https://developers.stellar.org/docs/tools/cli
cargo install --locked stellar-cli
# Clone
git clone https://github.com/SynapsVault/contracts SynapsVault-contracts
cd SynapsVault-contracts
# Run tests, lints and docs (same as CI)
cargo test --workspace
cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings
# Build WASM
stellar contract build
# Output: target/wasm32v1-none/release/*.wasm
soroban-sdk28 only builds contract WASM throughstellar contract build(stellar-cli v25.2+), which targetswasm32v1-noneand strips unusedcontract-specentries. Plaincargo build --target ...fails on purpose.
stellar network add testnet \
--rpc-url https://soroban-testnet.stellar.org \
--network-passphrase "Test SDF Network ; September 2015"
stellar keys generate --global deployer
stellar keys fund deployer --network testnet
for PKG in vault_registry access_lease subscription; do
ID=$(stellar contract deploy \
--wasm target/wasm32v1-none/release/$PKG.wasm \
--source-account deployer --network testnet)
# init is one-time and cannot be repeated — run it right after deploy.
stellar contract invoke --id "$ID" --source-account deployer --network testnet \
-- init --admin <PLATFORM_ADMIN_ADDRESS>
echo "$PKG=$ID"
doneMainnet deploys use the Mainnet Deploy workflow (manual dispatch, requires typing deploy-mainnet and approval on the mainnet environment). It runs the test suite, builds, deploys and initialises all three contracts and commits the IDs to deployed/mainnet-contract-ids.env. Never reuse the testnet deployer key on mainnet.
- All tests green on
mainand the release tag is signed. - WASM hashes are recorded (see Upgrades).
- Contract IDs and admin address are written to the backend's mainnet config.
-
initis called exactly once per contract — a second call fails withAlreadyInitialised. - A post-deploy smoke test verifies
is_valid/is_activereads.
Contracts are upgradeable through an admin-gated upgrade entrypoint. To deploy a new WASM without losing storage, you must upload the new WASM to get its hash, and invoke the upgrade function on the existing contract rather than deploying a brand new contract instance. Storage is preserved, so existing resources, leases, plans and subscriptions remain readable.
# 1. Build
stellar contract build
# 2. Upload the WASM blob and capture its hash
HASH=$(stellar contract upload \
--wasm target/wasm32v1-none/release/access_lease.wasm \
--source-account mainnet-deployer --network mainnet)
# 3. Point the live contract at the new hash (admin auth required)
stellar contract invoke --id <C_LEASE_ID> \
--source-account <ADMIN> --network mainnet \
-- upgrade --new_wasm_hash "$HASH"upgradedoes not run data migrations. A change to a stored struct's layout needs a follow-up admin migration.upgradedoes not update the stored version. Callmigrateas the admin afterwards soget_versionreports the new code (see docs/CONTRACTS.md → Upgrades and migration).- Always upgrade on testnet first, verify reads against existing data, then repeat the exact same hash on mainnet.
- Keep the previous WASM hash recorded so a rollback is a single
upgradecall.
PR / push to dev, main ──► fmt + clippy (-D warnings) + rustdoc (-D warnings)
tests (all contracts) + gas report
WASM build (stellar contract build) + 64 KiB size check
Merge to main ──► build + deploy + init on Stellar testnet
Manual dispatch ──► mainnet deploy (confirmation + environment approval)
GitHub secrets:
| Secret | Used by |
|---|---|
DEPLOYER_SECRET |
Testnet deploy (deploy is skipped if unset) |
BACKEND_PUBLIC |
Testnet admin address passed to init |
PUBLISHER1_PUBLIC, PUBLISHER2_PUBLIC |
Funded via friendbot on testnet |
MAINNET_DEPLOYER_SECRET |
Mainnet deploy (mainnet environment) |
MAINNET_ADMIN_PUBLIC |
Mainnet admin address passed to init |
Stellar Ledger
│
├── vault-registry (C_REGISTRY_ID)
│ └── Resource { id, creator, price, metadata, tags, listed }
│
├── access-lease (C_LEASE_ID)
│ └── Lease { resource_id, buyer, granted_at, expires_at, duration_ledgers }
│
└── subscription (C_SUB_ID)
├── Plan { plan_id, publisher, price_per_cycle, active }
└── Subscription { plan_id, subscriber, started_at, current_period_end, cancelled, total_renewals }
Entries are TTL-bumped on every write so actively managed data is never archived:
| Contract | Persistent bump |
|---|---|
vault-registry |
30 days |
access-lease |
90 days |
subscription |
365 days |
- Every mutating call is gated by
require_auth()on the appropriate authority. initis one-time on every contract; the admin can only be changed by the current admin viaset_admin.- The admin role is held by the backend's platform wallet — not a user wallet.
is_validandis_activeare fully permissionless — any frontend or smart contract can verify access without relying on SynapsVault's API.- All ledger-sequence arithmetic is checked; overflow returns an error instead of wrapping.
- Every state change emits an event for off-chain indexing and auditing.
- Expiry boundary:
expires_atis exclusive across all three contracts. A lease or subscription is active whileexpires_at > ledger.sequence(); it is expired on the ledger whose sequence equalsexpires_at. Seedocs/CONTRACTS.md→ Common conventions.
Every mutating entrypoint and its required signer at a glance. The full matrix with corresponding test names lives in SECURITY_AUDIT.md § 2.
| Entrypoint | Required signer |
|---|---|
vault-registry::register |
creator |
vault-registry::set_price / update_metadata / set_tags |
Stored creator |
vault-registry::set_listed / delist |
Stored creator |
vault-registry::propose_transfer |
Stored creator |
vault-registry::accept_transfer |
Proposed new creator |
vault-registry::cancel_transfer |
Stored creator |
vault-registry::upgrade / migrate |
Admin |
access-lease::grant_lease / extend_lease / revoke_lease |
Admin |
access-lease::set_admin / propose_admin / accept_admin |
Current admin (or pending admin for accept_admin) |
access-lease::upgrade / migrate |
Admin |
subscription::subscribe / renew |
Admin |
subscription::create_plan / deactivate_plan / transfer_plan |
Publisher |
subscription::cancel |
Subscriber |
subscription::set_admin / upgrade / migrate |
Admin |
*::init |
None — see SV-12 |
All get* / is_valid / is_active / list reads |
None (permissionless) |
See SECURITY_AUDIT.md for findings and their status.
| Repo | Description |
|---|---|
SynapsVault-frontend |
React UI |
SynapsVault-backend |
Express API |
MIT © 2025 Busiii-adetiba
- #42:
[access-lease]set_adminis single-step; a mistyped address permanently locks admin — propose two-step rotation - #44:
[access-lease]is_compatibleuses exact string match here but major/minor match invault-registry— unify semantics - #50:
[subscription]Publishers cannot change a plan's price after creation - #51:
[subscription]Plan ownership cannot be transferred (vault-registrysupports this) - #53:
[subscription]Resubscribing a cancelled-but-paid subscription adds a full cycle on top of the remaining time; confirm thi...
- #106: [cross-cutting] Gas budget test covers only register, set_price, get and list in vault-registry; add budgets for lease and...
- #116: [docs and tooling] Doc comments on Lease, Subscription and Plan use "30-day" and "ledgers" interchangeably; state the conversi...