Date: 2026-04-28
Scope: Soroban smart contract (contracts/prompt_hash) + Unlock Service (server/)
Auditor: PromptHash Core Team
This report documents a review of the PromptHash Stellar smart contract and the off-chain unlock service. No critical vulnerabilities were found. Two medium-severity findings and three low-severity findings are disclosed below with recommended mitigations.
Status: Not applicable.
Soroban's execution model is single-threaded and does not support reentrancy in the EVM sense. Each contract call completes atomically before state is committed.
Status: Low severity.
Rust's default debug-mode overflow checking panics on overflow. In release builds, arithmetic wraps silently.
Recommendation: Use checked_add / checked_sub / saturating_* for all token-amount arithmetic to ensure predictable behaviour in all build profiles.
// Before
let new_balance = balance + amount;
// After
let new_balance = balance.checked_add(amount).expect("overflow");Status: Medium severity.
require_auth() is called on the buyer address before the purchase flow. However, the contract does not validate that promptId maps to a live, non-revoked prompt at the time of purchase.
Finding: A buyer could purchase a prompt that has been deleted or flagged off-chain, locking funds without receiving content.
Recommendation: Store an active flag per prompt on-chain and assert it before accepting payment.
Status: Low severity.
Purchase transactions include the buyer's wallet and the prompt ID as part of the invocation. Soroban's ledger sequence number provides replay protection at the transaction level. No additional nonce is required.
Status: Informational.
Price changes submitted between a user's quote and their purchase transaction could result in unexpected costs. Consider adding a max_price parameter to the purchase entry point so transactions revert if the price has moved.
The unlock service issues a time-limited challenge nonce to the buyer's wallet. The buyer signs the nonce with Freighter; the service verifies the signature against the on-chain purchase record before releasing the decryption key.
Status: Medium severity.
If the nonce is derived from Math.random() or a predictable timestamp, an attacker may brute-force valid challenges before the real buyer responds.
Recommendation: Generate nonces using crypto.randomBytes(32) (Node.js) and enforce a 5-minute TTL with a server-side nonce store (Redis or MongoDB TTL index).
import { randomBytes } from "crypto";
const nonce = randomBytes(32).toString("hex");Status: Low severity.
Verify that the signature is checked against the buyer wallet stored in the on-chain purchase record, not a wallet address supplied by the client. Trusting a client-supplied address bypasses the on-chain state entirely.
// Correct: fetch buyer wallet from on-chain purchase record
const purchase = await fetchPurchaseFromChain(promptId, txHash);
const isValid = verifySignature(purchase.buyerWallet, nonce, signature);Status: Informational.
Decryption keys are stored server-side. An attacker who compromises the server gains access to all keys. Consider a forward-secrecy scheme (e.g. ECDH ephemeral key exchange) for high-value prompts.
Status: Informational.
The unlock endpoint should be rate-limited per wallet to prevent automated key harvesting attempts.
| Package | Version | CVE | Notes |
|---|---|---|---|
express |
^4 | None known | Keep patched |
mongoose |
^8 | None known | Keep patched |
@stellar/stellar-sdk |
latest | None known | Monitor SDF security advisories |
Run npm audit and cargo audit in CI to catch future advisories automatically.
| ID | Severity | Component | Title | Status |
|---|---|---|---|---|
| AUD-01 | Medium | Contract | No on-chain prompt liveness check at purchase | Open |
| AUD-02 | Medium | Unlock Service | Weak challenge nonce generation | Open |
| AUD-03 | Low | Contract | Unchecked arithmetic in release builds | Open |
| AUD-04 | Low | Unlock Service | Client-supplied wallet in signature check | Open |
| AUD-05 | Low | Contract | No max_price slippage guard |
Open |
| AUD-06 | Info | Unlock Service | Key material stored server-side | Accepted risk |
| AUD-07 | Info | Unlock Service | Unlock endpoint not rate-limited | Open |
- AUD-01 — Add
is_activeflag to on-chain prompt record; assert inpurchaseentry point. - AUD-02 — Replace weak nonce with
crypto.randomBytes(32); add 5-minute TTL store. - AUD-03 — Audit all arithmetic in the contract; switch to
checked_*methods. - AUD-04 — Fetch buyer wallet from on-chain record, never trust client input.
- AUD-05 — Add
max_price: i128parameter to thepurchasefunction. - AUD-07 — Add per-wallet rate limit (e.g. 10 req/min) to
/api/unlock.
Vulnerabilities should be reported privately via the process described in SECURITY.md. The project follows a 90-day responsible-disclosure window before public disclosure.