Soroban smart contracts for Lafiya's on-chain trust layer — an attestation registry and attester allowlist that let a health worker's verification of an emergency health record be checked cryptographically, without the underlying health data ever touching the blockchain.
Your vitals, verified. When you can't speak, Lafiya does.
Lafiya is Hausa for health, safety, and wellbeing.
Status: Pre-alpha · Stellar testnet · not yet audited · not a medical device. See Disclaimer.
Lafiya is a free, patient-owned emergency health card. The handful of facts that change how you are treated in an emergency (blood group, genotype, allergies, current medications, chronic conditions) travel with you as a scannable QR code, and a health worker's verification of them can be cryptographically checked on the spot.
This repository holds only the Soroban smart contract layer:
attester-registry: the admin-managed allowlist of health workers (attesters).attestation-registry: records that an allowlisted attester verified a record commitment. It stores only an opaque hash; health data never goes on-chain.multisig-account: an N-of-M custom account intended as the registries' admin.
The full documentation lives at https://lafiya-xyz.github.io/Lafiya-contract/main/ (versioned per release, with search).
In Nigeria, health records are paper, siloed per facility, and effectively lost the moment a patient moves, is referred, or arrives unconscious. In an emergency, the facts that decide treatment — especially genotype (AS/SS sickle-cell status), blood group, and drug allergies — are usually unknown to whoever is treating you, and wrong assumptions cost lives.
Even once that data is digitized (in lafiya-web), a responder still has no way to know whether a card's contents were ever checked by a real health worker. Without an independent, tamper-evident verification layer:
- Responders can't trust the data — anyone could edit a public emergency page, so a "verified" label is meaningless unless it's backed by something the patient (or an attacker) can't forge
- Health workers have no portable proof of their verification work — nothing links a specific attester to a specific record across systems
- Community health workers (CHWs) can't be paid reliably for last-mile registration and verification without a transparent, low-fee settlement rail
- Attests — records, on-chain, that a licensed health worker verified a specific patient record at a specific time, without storing any health data itself
- Allowlists — maintains the set of health workers authorized to submit attestations, so a "verified" indicator on a card actually means something
- Anchors trust — gives
lafiya-webandlafiya-verifiera single, independently checkable source of truth that a responder's QR scan can query directly
- Attestation registry (Soroban) — when a licensed health worker verifies a record, an on-chain attestation stores a hash of the record + the attester's identity + a timestamp — never the health data itself
- Attester allowlist — only allowlisted attesters can write to the registry, so verification can't be forged by an arbitrary wallet
- Hash-only on-chain footprint — personal data lives in
lafiya-web's encrypted, access-controlled off-chain database; Stellar holds only hashes, attestations, and payments - USDC incentive rails — CHWs are paid micro-amounts on Stellar per verified registration; near-zero fees and stablecoin settlement make last-mile outreach economically viable
- Transparent funding — grant and donor funds flow on-chain into the CHW incentive pool, so every dollar maps to a countable number of verified cards
graph TB
subgraph OffChain["Off-chain (lafiya-web)"]
PROFILE[Patient profile — Supabase]
CARD[Public emergency page + QR]
end
subgraph Contracts["lafiya-contracts (Soroban)"]
ALLOW[Attester allowlist]
REG[Attestation registry]
POOL[Incentive pool]
end
subgraph Actors["Actors"]
CHW[Community health worker\nlicensed attester]
RESP[Responder / clinician]
DONOR[Grant / donor funds]
end
PROFILE --> CARD
PROFILE -->|hash of record| REG
CHW -->|submits attestation| REG
ALLOW -->|checks attester is licensed| REG
REG -->|hash + attester id + timestamp| CARD
RESP -->|scans QR, checks attestation| CARD
DONOR -->|USDC incentive pool| POOL
POOL -->|USDC payout per verified registration| CHW
attester-registry— the on-chain allowlist of health workers authorized to write attestationsattestation-registry— the on-chain record of which attester verified which record hash, and when; calls intoattester-registryon every writemultisig-account— a reusable N-of-M Soroban account contract that secures both registries' admin authorization. It deliberately ignores Soroban's authorization contexts in__check_auth— an accepted pre-alpha risk documented in ADR-0007 — so a valid signer quorum's authority is not scoped to the registriesincentive-pool— bounded USDC payout/escrow contract for CHW incentives; manages donor funding, work-item approval, per-attestation replay protection, and configurable payout limits
All three core registry contracts are implemented and unit-tested (target milestone M1, see Roadmap); none has been deployed to testnet yet. incentive-pool implements the M2 milestone.
Four Soroban contracts, each in its own crate under contracts/.
Design principle: no personal health data ever touches the blockchain. Personal data lives in lafiya-web's encrypted, access-controlled off-chain database. Stellar holds only hashes, attestations, and payments. This is what keeps Lafiya both privacy-respecting and regulator-compatible.
| Function | Description |
|---|---|
__constructor(admin: Address) |
Sets the admin atomically at deployment; the supplied admin must authorize the deployment. |
rotate_attester(previous_attester: Address, new_attester: Address) |
See the contract rustdoc for details. |
revoke_attester(attester: Address) |
Allows an attester to revoke its own key by authorizing as attester. Removes it from the allowlist, clears suspension state, and decrements the count. Available while paused. Emits AttesterRevoked. |
get_attester_rotation(attester: Address) -> Option<Address> |
See the contract rustdoc for details. |
get_admin() -> Result<Address, Error> |
Returns the current admin address. |
grant_role(role: Role, account: Address) |
See the contract rustdoc for details. |
revoke_role(role: Role, account: Address) |
See the contract rustdoc for details. |
has_role(role: Role, account: Address) -> bool |
See the contract rustdoc for details. |
set_regional_registrar(registrar: Address, region: String, quota: u32) |
See the contract rustdoc for details. |
revoke_regional_registrar(registrar: Address) |
See the contract rustdoc for details. |
get_regional_registrar(registrar: Address) -> Option<RegionalRegistrarInfo> |
See the contract rustdoc for details. |
get_regional_registrar_count(registrar: Address) -> u32 |
See the contract rustdoc for details. |
propose_admin(new_admin: Address) |
Proposes a new admin. Requires admin auth. |
accept_admin() |
Finalizes the admin transfer. Requires proposed/pending admin auth. Emits AdminTransferred. |
cancel_admin_proposal() |
See the contract rustdoc for details. |
pause(guardian: Address) |
Blocks add_attester, add_attester_with_info, remove_attester, suspend_attester, and reinstate_attester until unpaused. Self-revocation remains available while paused. Requires admin auth. Emits Paused. |
unpause() |
Restores normal operation after pause. Requires admin auth. Emits Unpaused. |
is_paused() -> bool |
Whether the contract is currently paused. Callable while paused. |
add_attester(registrar: Address, attester: Address) |
Allowlists attester. Requires admin auth. Blocked while paused (Error::ContractPaused). Emits AttesterAdded. |
add_attester_with_info(registrar: Address, attester: Address, license_hash: Option<BytesN<32>>, region: Option<String>, valid_from: Option<u64>, valid_until: Option<u64>) |
Allowlists attester with optional metadata. region, when present, must be an uppercase ISO 3166-2 code such as NG-LA. Requires admin auth. Blocked while paused (Error::ContractPaused). Emits AttesterAdded. |
update_attester_info(registrar: Address, attester: Address, license_hash: Option<BytesN<32>>, region: Option<String>, valid_from: Option<u64>, valid_until: Option<u64>) |
Updates metadata for an already-allowlisted attester. Regions use uppercase ISO 3166-2 codes such as NG-LA. Requires admin auth. Blocked while paused (Error::ContractPaused). Fails with Error::AttesterNotFound if attester isn't currently allowlisted. Emits AttesterInfoUpdated, distinguishable from enrollment's AttesterAdded. |
add_attesters(registrar: Address, attesters: Vec<Address>) |
See the contract rustdoc for details. |
remove_attesters(registrar: Address, attesters: Vec<Address>) |
See the contract rustdoc for details. |
remove_attester(registrar: Address, attester: Address) |
Removes attester from the allowlist. Requires admin auth. Blocked while paused (Error::ContractPaused). Emits AttesterRemoved. |
set_max_attesters(max_attesters: u32) |
Sets the soft cap on the number of allowlisted attesters. Requires admin auth. Does not evict existing attesters if lowered below the current count. |
get_max_attesters() -> u32 |
The current soft cap on the number of allowlisted attesters. |
get_attester_count() -> u32 |
The current number of allowlisted attesters. |
suspend_attester(registrar: Address, attester: Address) |
Suspends an allowlisted attester without removing it, using the administrative reason. The suspension timestamp becomes the trust cutoff for that attester's later attestations. Requires admin auth. Blocked while paused (Error::ContractPaused). Emits AttesterSuspended with reason and timestamp. |
suspend_attester_with_reason(registrar: Address, attester: Address, reason: Symbol) |
Suspends an allowlisted attester with an explicit status reason (for example misconduct or license_pending). |
reinstate_attester(registrar: Address, attester: Address) |
Reinstates a suspended attester. Requires admin auth. Blocked while paused (Error::ContractPaused). Emits AttesterReinstated. |
is_attester(attester: Address) -> bool |
Whether attester is currently allowlisted (and not suspended). Open to any caller, including other contracts. Callable while paused. |
is_attester_at(attester: Address, timestamp: u64) -> bool |
See the contract rustdoc for details. |
is_attester_for_region(attester: Address, region: String) -> bool |
See the contract rustdoc for details. |
get_attester_info(attester: Address) -> Option<AttesterInfo> |
Returns stored metadata for an allowlisted attester. Callable while paused. |
get_attester_trust_revoked_after(attester: Address) -> Option<u64> |
See the contract rustdoc for details. |
get_attester_status(attester: Address) -> Option<AttesterStatus> |
Returns attester's metadata together with its current suspension state in one call. None if attester isn't currently allowlisted (never added, or since removed). Callable while paused. |
get_interface() -> InterfaceInfo |
Contract kind, interface version, enabled features, and storage/event schema versions for runtime compatibility negotiation (see docs/releasing.md). Open to any caller. |
get_schema_version() -> u32 |
Storage schema version recorded for the instance. Open to any caller. |
upgrade(new_wasm_hash: BytesN<32>) |
Replaces the contract's code with the already-uploaded wasm blob at new_wasm_hash. Requires admin auth; storage is untouched. See Contract upgrades. |
migrate() |
Runs any pending storage-schema migration, then records the new schema version. Requires admin auth; errors with MigrationNotRequired when nothing is pending. |
In the repository: glossary · ADRs · runbooks · local sandbox · TLA+ models
| Function | Description |
|---|---|
__constructor(admin: Address, attester_registry: Address) |
Sets the admin and validates/configures the attester-registry atomically at deployment; the supplied admin must authorize the deployment. |
get_admin() -> Address |
Returns the current admin address. |
get_attester_registry() -> Address |
Returns the configured attester-registry contract address. |
propose_admin(new_admin: Address) |
Proposes a new admin. Requires admin auth. |
accept_admin() |
Finalizes the admin transfer. Requires proposed/pending admin auth. Emits AdminTransferred. |
set_attester_registry(new_registry: Address) |
Repoints the attester-registry contract this registry consults for regional allowlist checks. Requires owner auth and verifies the regional interface. Emits AttesterRegistryRepointed. |
pause(guardian: Address) |
Blocks attest until unpaused. Requires the Guardian role. Emits Paused. |
unpause() |
Restores normal operation after pause. Requires owner auth. Emits Unpaused. |
is_paused() -> bool |
Whether the contract is currently paused. Callable while paused. |
set_max_attestation_age(max_age: u64) |
Sets, in seconds, how long an attestation remains current. Requires admin auth. The initial default is 365 days. |
get_max_attestation_age() -> u64 |
Returns the maximum age used by is_verified. |
attest(attester: Address, record_hash: BytesN<32>) -> Attestation |
Requires attester's auth and that attester is allowlisted (checked via a cross-contract call to attester-registry::is_attester). Stores { attester, timestamp } keyed by record_hash, keeping a bounded history per hash. Blocked while paused (Error::ContractPaused). Emits AttestationRecorded. |
is_attestation_trusted(record_hash: BytesN<32>) -> bool |
Checks the latest attestation against the attester's recorded trust-revocation cutoff while preserving the raw historical attestation. |
revoke_attestation(record_hash: BytesN<32>) |
Revokes all attestations for record_hash. Requires admin auth. Emits AttestationRevoked. |
get_attestation(record_hash: BytesN<32>) -> Option<Attestation> |
Looks up the latest attestation for a record hash. Open to any caller — this is what lets a responder's QR scan verify a card without an external oracle. |
get_attestations(record_hashes: Vec<BytesN<32>>) -> Vec<Option<Attestation>> |
Looks up many latest attestations in one read; results preserve input order and return None for missing records. |
is_verified(record_hash: BytesN<32>) -> Result<bool, Error> |
Applies one shared verification rule: a non-revoked latest attestation within the configured age limit, made by an attester who remains allowlisted and unsuspended. Returns AttesterRegistryUnavailable if its status cannot be checked. |
get_attestation_history(record_hash: BytesN<32>) -> Vec<Attestation> |
Returns the full bounded attestation history for a record hash, oldest first. Open to any caller. |
get_interface() -> InterfaceInfo |
Contract kind, interface version, enabled features, and storage/event schema versions for runtime compatibility negotiation (see docs/releasing.md). Open to any caller. |
set_attestation_rate_limit(max_per_window: u32, window_ledgers: u32) |
Caps each attester at max_per_window attestations per window_ledgers ledgers (0 disables; off by default). Over-limit attest calls fail with Error::RateLimited. Requires admin auth. Emits RateLimitSet. See docs/storage-cost.md. |
get_attestation_rate_limit() -> Option<RateLimit> |
Returns the global rate limit, if one is configured. |
set_attester_rate_limit(attester: Address, max_per_window: u32) |
Overrides max_per_window for one attester (e.g. a high-volume clinical site). Requires admin auth. |
remove_attester_rate_limit(attester: Address) |
Removes an attester's override, reverting it to the global limit. Requires admin auth. |
get_rate_limit_retry_after(attester: Address) -> Option<u32> |
If the attester's window is full, returns the first ledger it may attest again. |
For relayer-paid submissions, see Relayer-Sponsored Attestations.
attester-registry is upgradeable by its admin (upgrade/migrate/get_schema_version
above), with storage schema versioning to make schema-changing upgrades explicit
and verifiable. attestation-registry now exposes get_schema_version and
migrate for its additive v1-to-v2 storage change, but still has no in-contract
upgrade entry point. Operators must follow
docs/runbooks/contract-upgrade.md — it covers the
pre-upgrade checklist, the upgrade() call sequence, verifying the wasm hash against
reviewed source, and migrate() handling for storage-schema-changing upgrades. The
mechanical steps are automated by scripts/upgrade.sh.
| Function | Description |
|---|---|
__constructor(admin: Address, attester_registry: Address) |
See the contract rustdoc for details. |
cancel_admin_proposal() |
See the contract rustdoc for details. |
get_admin() -> Result<Address, Error> |
See the contract rustdoc for details. |
grant_role(role: Role, account: Address) |
See the contract rustdoc for details. |
revoke_role(role: Role, account: Address) |
See the contract rustdoc for details. |
has_role(role: Role, account: Address) -> bool |
See the contract rustdoc for details. |
get_attester_registry() -> Result<Address, Error> |
See the contract rustdoc for details. |
get_schema_version() -> u32 |
See the contract rustdoc for details. |
migrate() |
See the contract rustdoc for details. |
upgrade(new_wasm_hash: BytesN<32>) |
See the contract rustdoc for details. |
propose_admin(new_admin: Address) |
See the contract rustdoc for details. |
accept_admin() |
See the contract rustdoc for details. |
set_attester_registry(new_registry: Address) |
See the contract rustdoc for details. |
pause(guardian: Address) |
See the contract rustdoc for details. |
unpause() |
See the contract rustdoc for details. |
is_paused() -> bool |
See the contract rustdoc for details. |
set_max_attestation_age(max_age: u64) |
See the contract rustdoc for details. |
get_max_attestation_age() -> u64 |
See the contract rustdoc for details. |
consent_attestation(patient: Address, attester: Address, record_hash: BytesN<32>, attestation_expires_at: u64) |
See the contract rustdoc for details. |
attest(attester: Address, patient: Address, record_hash: BytesN<32>) -> Result<Attestation, Error> |
See the contract rustdoc for details. |
attest_versioned(attester: Address, patient: Address, record_hash: BytesN<32>, commitment_version: u32) -> Result<Attestation, Error> |
See the contract rustdoc for details. |
attest_version(attester: Address, patient: Address, record_hash: BytesN<32>, previous_record_hash: BytesN<32>) -> Result<Attestation, Error> |
See the contract rustdoc for details. |
batch_attest(requests: Vec<AttestationRequest>) -> Result<Vec<Attestation>, Error> |
See the contract rustdoc for details. |
anchor_batch(attester: Address, root: BytesN<32>, leaf_count: u32) -> Result<AttestationBatch, Error> |
See the contract rustdoc for details. |
get_attestation_batch(root: BytesN<32>) -> Option<AttestationBatch> |
See the contract rustdoc for details. |
revoke_attestation(revoker: Address, record_hash: BytesN<32>, reason: Symbol) |
See the contract rustdoc for details. |
withdraw_attestation(attester: Address, record_hash: BytesN<32>) |
See the contract rustdoc for details. |
is_verified(record_hash: BytesN<32>) -> Result<bool, Error> |
See the contract rustdoc for details. |
is_attestation_trusted(record_hash: BytesN<32>) -> bool |
See the contract rustdoc for details. |
get_attestation(record_hash: BytesN<32>) -> Option<Attestation> |
See the contract rustdoc for details. |
get_attestations(record_hashes: Vec<BytesN<32>>) -> Vec<Option<Attestation>> |
See the contract rustdoc for details. |
get_attestation_status(record_hash: BytesN<32>) -> Option<AttestationStatus> |
See the contract rustdoc for details. |
get_record_status(record_hash: BytesN<32>) -> RecordStatus |
See the contract rustdoc for details. |
get_attester_attestation_status(record_hash: BytesN<32>, attester: Address) -> AttesterAttestationStatus |
See the contract rustdoc for details. |
get_previous_record_hash(record_hash: BytesN<32>) -> Option<BytesN<32>> |
See the contract rustdoc for details. |
get_next_record_hash(record_hash: BytesN<32>) -> Option<BytesN<32>> |
See the contract rustdoc for details. |
get_interface() -> InterfaceInfo |
See the contract rustdoc for details. |
get_attestation_history(record_hash: BytesN<32>) -> Vec<Attestation> |
See the contract rustdoc for details. |
get_attestation_history_status(record_hash: BytesN<32>) -> Vec<AttestationStatus> |
See the contract rustdoc for details. |
set_attestation_rate_limit(max_per_window: u32, window_ledgers: u32) |
See the contract rustdoc for details. |
get_attestation_rate_limit() -> Option<RateLimit> |
See the contract rustdoc for details. |
set_attester_rate_limit(attester: Address, max_per_window: u32) |
See the contract rustdoc for details. |
remove_attester_rate_limit(attester: Address) |
See the contract rustdoc for details. |
get_rate_limit_retry_after(attester: Address) -> Option<u32> |
See the contract rustdoc for details. |
| Function | Description |
|---|---|
initialize(admin: Address, approver: Address, token: Address, attester_registry: Address, max_per_claim: i128, max_per_attester: i128) |
See the contract rustdoc for details. |
get_admin() -> Result<Address, Error> |
See the contract rustdoc for details. |
get_approver() -> Result<Address, Error> |
See the contract rustdoc for details. |
get_token() -> Result<Address, Error> |
See the contract rustdoc for details. |
get_attester_registry() -> Result<Address, Error> |
See the contract rustdoc for details. |
get_max_per_claim() -> Result<i128, Error> |
See the contract rustdoc for details. |
get_max_per_attester() -> Result<i128, Error> |
See the contract rustdoc for details. |
get_total_deposited() -> Result<i128, Error> |
See the contract rustdoc for details. |
get_total_paid() -> Result<i128, Error> |
See the contract rustdoc for details. |
is_work_item_approved(work_item_id: BytesN<32>) -> bool |
See the contract rustdoc for details. |
is_work_item_claimed(work_item_id: BytesN<32>) -> bool |
See the contract rustdoc for details. |
get_work_item(work_item_id: BytesN<32>) -> Option<WorkItem> |
See the contract rustdoc for details. |
get_attester_total_claimed(attester: Address) -> i128 |
See the contract rustdoc for details. |
propose_admin(new_admin: Address) |
See the contract rustdoc for details. |
accept_admin() |
See the contract rustdoc for details. |
set_approver(new_approver: Address) |
See the contract rustdoc for details. |
set_attester_registry(new_registry: Address) |
See the contract rustdoc for details. |
set_max_per_claim(max: i128) |
See the contract rustdoc for details. |
set_max_per_attester(max: i128) |
See the contract rustdoc for details. |
fund(amount: i128) |
See the contract rustdoc for details. |
withdraw(to: Address, amount: i128) |
See the contract rustdoc for details. |
approve_work_item(work_item_id: BytesN<32>, attester: Address, payout_amount: i128) |
See the contract rustdoc for details. |
claim(work_item_id: BytesN<32>) |
See the contract rustdoc for details. |
pause() |
See the contract rustdoc for details. |
unpause() |
See the contract rustdoc for details. |
is_paused() -> bool |
See the contract rustdoc for details. |
| Function | Description |
|---|---|
__constructor(signers: Vec<BytesN<32>>, threshold: u32) |
Configures the ed25519 signer set and required N-of-M threshold at deployment. |
__check_auth(...) |
Verifies ordered, unique signatures from configured signers whenever another contract calls require_auth() for this account address. |
get_interface() -> InterfaceInfo |
Contract kind, interface version, enabled features, and storage/event schema versions for runtime compatibility negotiation (see docs/releasing.md). Open to any caller. |
attestation-registry calls attester-registry through a local #[contractclient] trait interface (is_attester and is_attester_for_region), not a direct crate dependency — depending on the whole crate would link attester-registry's own contract implementation into attestation-registry's wasm build too, which is both wasted size and, at least on the Soroban SDK version this repo pins, produces a linker warning from the two contracts' colliding initialize exports.
Bounded USDC payout/escrow contract for the M2 milestone. Donors fund the pool, an approver authorizes work items, and approved allowlisted attesters claim per-item payouts with per-attestation replay protection.
| Function | Description |
|---|---|
initialize(admin, approver, token, attester_registry, max_per_claim, max_per_attester) |
Sets admin, approver, token contract, attester-registry, and payout caps. Performs best-effort interface checks against both attester_registry and token. Callable once. |
propose_admin(new_admin: Address) |
Proposes a new admin. Requires admin auth. |
accept_admin() |
Finalizes the admin transfer. Requires pending-admin auth. Emits AdminTransferred. |
set_approver(new_approver: Address) |
Sets the address authorized to approve work items. Requires admin auth. |
set_attester_registry(new_registry: Address) |
Changes the attester-registry contract. Requires admin auth. Emits AttesterRegistryRepointed. |
set_max_per_claim(max: i128) |
Sets the per-claim payout cap. Requires admin auth. |
set_max_per_attester(max: i128) |
Sets the per-attester cumulative claim cap. Requires admin auth. |
fund(amount: i128) |
Deposits tokens from the caller into the pool. Requires admin auth. Blocked while paused. Emits PoolFunded. |
withdraw(to: Address, amount: i128) |
Withdraws tokens from the pool to to. Requires admin auth. Blocked while paused. Emits PoolWithdrawn. |
approve_work_item(work_item_id, attester, payout_amount) |
Approves a work item for payout. Requires approver auth. Verifies attester is allowlisted, enforces per-claim cap. Blocked while paused. Emits WorkItemApproved. |
claim(work_item_id) |
Claims payout for an approved work item. Requires the approved attester's auth. Enforces replay protection, per-attester cap, and pool balance. Transfers tokens on success. Blocked while paused. Emits PayoutClaimed. |
pause() |
Blocks fund, withdraw, approve_work_item, and claim. Requires admin auth. Emits Paused. |
unpause() |
Resumes normal operation. Requires admin auth. Emits Unpaused. |
is_paused() -> bool |
Whether the contract is currently paused. Callable while paused. |
get_admin() / get_approver() / get_token() / get_attester_registry() |
Configuration getters. |
get_max_per_claim() / get_max_per_attester() |
Cap getters. |
get_total_deposited() / get_total_paid() |
Pool accounting getters. |
is_work_item_approved(id) / is_work_item_claimed(id) / get_work_item(id) |
Work-item state getters. |
get_attester_total_claimed(attester) |
Per-attester cumulative claim getter. |
incentive-pool calls attester-registry through the same local #[contractclient] interface pattern used by attestation-registry (see ADR-0002).
bindings/
├── attestation-registry/ # generated TS client for attestation contract
└── attester-registry/ # generated TS client for allowlist contract
contracts/
├── multisig-account/ # reusable N-of-M admin account
│ ├── Cargo.toml
│ └── src/
│ ├── lib.rs
│ ├── test.rs
│ └── integration_test.rs
├── attester-registry/ # allowlist contract
│ ├── Cargo.toml
│ └── src/
│ ├── lib.rs # constructor, add_attester, remove_attester, is_attester, upgrade, migrate, get_schema_version
│ └── test.rs
├── attestation-registry/ # attestation contract
│ ├── Cargo.toml
│ └── src/
│ ├── lib.rs # initialize, attest, get_attestation, upgrade, migrate, get_schema_version
│ └── test.rs
└── incentive-pool/ # bounded USDC payout/escrow (M2)
├── Cargo.toml
└── src/
├── lib.rs # initialize, fund, withdraw, approve_work_item, claim, pause/unpause
└── test.rs
docs/
└── adr/ # architecture decisions, index, and template
Cargo.toml # workspace + release profile
Cargo.lock # committed for reproducible builds
CHANGELOG.md # release notes incl. schema-impact statements
rust-toolchain.toml # pins stable + wasm32v1-none
Makefile # build/test/fmt/clippy/wasm/bindings/check
.github/workflows/ci.yml # runs the same checks on push/PR
LICENSE # MIT
CONTRIBUTING.md # local dev workflow
Client bindings are generated from the built WASM contracts using the stellar-cli tool. They allow frontend applications (like lafiya-web) to interact with the deployed contracts with full type safety.
To generate the bindings, run:
make bindingsThis builds the contracts and outputs TypeScript packages to the bindings/ directory:
bindings/attester-registrybindings/attestation-registry
To compile the generated packages:
cd bindings/attester-registry && npm install && npm run build
cd ../attestation-registry && npm install && npm run buildThe generated bindings are committed directly to this repository under the bindings/ directory. lafiya-web (or any other consumer) can consume them via:
- Direct git path dependency in
package.jsonpointing to the repo or subdirectory. - A git submodule in the consuming project.
Publishing these directories as packages to the @lafiya npm organization is a planned, but
not yet implemented, secondary option. See PUBLISHING.md for the full
strategy and the follow-up work required before that's live.
- On-chain: Soroban smart contracts (Rust),
soroban-sdk28.x, on Stellar; USDC on Stellar for CHW payments viaincentive-poolcontract - Network: Stellar testnet first
- Standards informing design: W3C Verifiable Credentials data model (issuer/holder/verifier roles, hash-based attestation)
git clone https://github.com/Lafiya-xyz/Lafiya-contract.git
cd Lafiya-contract
make check # fmt-check + clippy + test + wasm build
cargo xtask sandbox up --scenario edge-cases # seeded local chain (Docker + stellar CLI)The registry contracts need no multisig-specific logic. Their existing admin.require_auth() calls invoke the account contract's __check_auth, so an admin operation succeeds only when its authorization entry contains at least N valid signatures.
Authorization scope:
multisig-accountis a general-purpose N-of-M account. It does not inspect Soroban's authorization contexts or restrict the contract, function, arguments, asset movement, or nested invocations that a valid quorum may approve. It is not a registry-scoped or least-privileged account.
For pre-alpha use, assign a signer set dedicated exclusively to Lafiya registry administration; do not reuse those keys or that quorum for treasury or unrelated authority. Do not use the multisig address as a treasury: keep only the bounded XLM fee reserve recorded for the deployment and sweep any excess. Before signing, each signer must inspect the decoded authorization tree and independently verify the registry address, function, arguments, asset movements, and sub-invocations. A payload hash or transaction label alone is not sufficient.
The deployment record must contain the signer-set identifier (never secret keys), threshold, approved registry addresses, fee-reserve ceiling, and balance-sweep and authorization-review procedures. This account must not administer a production or mainnet deployment until its unscoped authority is explicitly accepted for that environment or replaced by an on-chain scoping policy. See ADR-0007 for the decision and residual risks.
Not yet deployed to testnet — deployment scripts and instructions land with the rest of milestone M1.
- Nigeria Data Protection Act (2023) governs all personal data held across the Lafiya project. Consent, encryption, and minimal disclosure are designed in from day one.
- No health data is ever written on-chain — only non-reversible hashes and attestations, by design (see Smart Contract Layer).
- M0 — Public card (testnet). One patient can create a profile and expose a working read-only emergency page via QR. (
lafiya-web) - M1 — Attestation. Soroban registry lets an allowlisted attester verify a record; the card shows a verified indicator. ← this repo — contracts implemented and unit-tested; testnet deployment and
lafiya-webintegration still open. - M2 — Incentives. USDC-on-Stellar payout to a CHW per verified registration. Target
custody architecture (separate treasury from registry admin, bounded payout contract)
specified in ADR-0009. ←
incentive-poolcontract — implemented and unit-tested; testnet deployment pending. - M3 — Pilot. Small supervised field pilot; measure verified cards created and scan events.
- M4 — Mainnet + funding. Launch on mainnet; open transparent funding pool.
Stellar/Soroban does two things Lafiya genuinely needs that a plain web app cannot: it makes verification tamper-evident and independently checkable without exposing data, and it moves stablecoin micropayments to health workers cheaply and across borders. Remove Stellar and the trust layer and the incentive engine both disappear — Soroban is core to Lafiya, not shoehorned in.
cargo xtask docs && mdbook serve docsCovers, per contract (see contracts/*/src/test.rs and tests/integration/run.sh):
- ✅ Atomic registry constructor configuration and invalid-wiring rejection
- ✅ Admin-gated writes (
add_attester,remove_attester), including rejection when the caller's auth entry doesn't match - ✅ Allowlist lookups (
is_attester) - ✅
attestby allowlisted vs. non-allowlisted attesters - ✅ Bounded
get_attestationhistory, including same-attester refresh without evicting other attesters - ✅ Emitted events (
AttesterAdded,AttesterRemoved,AttestationRecorded) - ✅ Multisig threshold, signer validation, signature ordering, and invalid-signature rejection
- ✅ Multisig-backed initialization and admin operations through the contract-account authorization path
- ✅ Pool funding, withdrawal, and accounting
- ✅ Work-item approval and claim with replay protection
- ✅ Per-claim and per-attester payout cap enforcement
- ✅ Insufficient pool balance rejection
- ✅ Suspended/removed attester claim rejection
- ✅ Pause/unpause cycle blocking all state-changing operations
- Rust (stable) +
wasm32v1-nonetarget — seerust-toolchain.toml soroban-sdk28.x- Stellar testnet account and USDC trustline, once deployment scripts land
MIT.
Contributions are welcome! As an open-source Digital Public Good, we rely on community contributions to build and maintain Lafiya.
Please refer to CONTRIBUTING.md for our detailed guidelines, which cover:
- The interface stability and deprecation policy for public contract, event, bindings, and CLI surfaces
- Local development environment setup
- Branching and commit conventions (Conventional Commits)
- Cross-repo shared-contract coordination guidelines
- Database/Supabase migration details
- Smart contract quality standards and testing checklist
This repository specifically needs collaborators with experience in:
- Stellar / Soroban smart contract development (Rust)
- On-chain data modeling and attestation/verifiable-credential design
This repo is one of five in the lafiya-xyz organization.
| Repo | URL | Purpose | Priority |
|---|---|---|---|
lafiya-web |
github.com/Lafiya-xyz/lafiya-web | Patient + responder web app (Next.js). Public emergency page, authed profile editor, QR generation. | Build first |
lafiya-contracts (this repo) |
github.com/Lafiya-xyz/Lafiya-contract | Soroban smart contracts (Rust): attestation registry + attester allowlist. Testnet first. | Build next |
lafiya-docs |
github.com/Lafiya-xyz/lafiya-docs | Concept note, data model, threat model, privacy design, funding/DPG materials, references. | Start now (lightweight) |
.github |
github.com/Lafiya-xyz/.github | Organization profile README and contribution guidelines. | Start now |
lafiya-verifier |
github.com/Lafiya-xyz/lafiya-verifier | CHW verification tool. Begins as a route inside lafiya-web; split out only if it grows. |
Later |
Resist scaffolding empty repos. Two working repos (
lafiya-web,lafiya-contracts) beat five half-built ones. Build one honest milestone at a time.
lafiya-web ──(record hash)──▶ lafiya-contracts
│
CHW attests ──(licensed?)──▶ │ (attester allowlist check)
▼
attestation: hash + attester id + timestamp
│
▼
lafiya-web public emergency page
│
▼
responder scans QR, sees verified indicator
Donor funds ──(USDC)──▶ incentive-pool
│
Approver ──(approve)──▶ │ (work item + attester + amount)
▼
attester claims ──▶ USDC payout (replay-protected)
lafiya-webholds the patient's private profile and computes a hash of the emergency-relevant record.- A licensed CHW, verified against the attester allowlist, submits an attestation to the attestation registry in this repo — a hash, the attester's identity, and a timestamp, never the health data itself.
lafiya-web's public emergency page reads the attestation to show a verified indicator; a responder scanning the QR can independently trust it without an external oracle.attestation-registry(later) gives CHWs a dedicated flow for step 2 as it splits out oflafiya-web.- Donors fund the incentive pool with USDC. An approver authorizes work items (attester + payout). The attester claims payout with replay protection and cap enforcement.
Attestation schema — a hash of the record + the attester's identity + a timestamp, defined by the contracts in this repo and consumed by lafiya-web's public emergency page. If the shape of an attestation changes here, lafiya-web's verification-display logic must be updated in the same change set (or a tracked follow-up opened there).
Incentive pool interface — the incentive-pool contract's function signatures, event schemas, and error codes are consumed by lafiya-web (or future CHW tooling) to display pool balance, claim status, and payout history. Changes to the pool interface require coordinated updates.
- Treat this section as the source of truth for cross-repo contracts. Each repo's own README covers repo-local conventions.
- The contracts are implemented and unit-tested but not yet deployed to testnet — don't assume a live contract ID or deployment scripts exist; check Repository Structure before referencing a path.
- When a change here affects the attestation schema or either contract's function signatures, call it out explicitly so
lafiya-webcan be updated to match.
For issues and questions:
- GitHub Issues: Create an issue
Lafiya is an information aid, not a medical device and not a substitute for professional medical judgment. Verified indicators reflect that a record was attested by a registered health worker; they are not a clinical guarantee. Treatment decisions remain the responsibility of the attending clinician.
Found a vulnerability? Please don't open a public issue — see SECURITY.md for how to report it privately. The contract-layer threat model is in docs/security/threat-model.md.
These works directly informed Lafiya's design and are the intended reading for contributors, particularly the attestation/trust-layer work in this repo.
Books
- Preukschat, A., & Reed, D. (2021). Self-Sovereign Identity: Decentralized Digital Identity and Verifiable Credentials. Manning. — The blueprint for Lafiya's attestation layer: issuer/holder/verifier roles, verifiable credentials, hash-based attestation, key management, and offline verification.
- Kleppmann, M. (2017). Designing Data-Intensive Applications. O'Reilly. — Informs the boundary between what lives in the off-chain database and what is anchored on-chain.
- Martin, R. C. (2017). Clean Architecture: A Craftsman's Guide to Software Structure and Design. Prentice Hall. — Discipline for an AI-assisted codebase: clear boundaries so the contracts, app, and data layer stay independently maintainable.
- Shortliffe, E. H., & Cimino, J. J. (Eds.). (2021). Biomedical Informatics: Computer Applications in Health Care and Biomedicine (5th ed.). Springer. — Grounds which fields are decision-relevant in an emergency, informing what a record hash here actually represents.
- Toyama, K. (2015). Geek Heresy: Rescuing Social Change from the Cult of Technology. PublicAffairs. — Keeps the project honest: the attestation layer amplifies trust in community health workers rather than replacing them.
Standards & documentation
- Stellar Development Foundation — Stellar and Soroban developer documentation.
- W3C — Verifiable Credentials Data Model.
- Nigeria Data Protection Act (2023) — Nigeria Data Protection Commission.
- Digital Public Goods Alliance — DPG Standard.