Trust and security
This is a testnet preview: payments are sandbox payments and AUSD is a testnet token. The contract is self-reviewed, findings published: read the self-review report and SECURITY.md.
To report a problem, use GitHub’s private vulnerability reporting: open the repository’s Security tab and choose Report a vulnerability.
Threats and the required fix
Section titled “Threats and the required fix”| What an attacker tries | Where | Required fix | Proof |
|---|---|---|---|
| Spend a student’s money by calling the card endpoint with their card | POST /api/card/authorize |
The request carries the account owner’s signature (demo), or the issuer’s HMAC (later). The card token is a random 128-bit value, never the address. Rate limit per card. | Server tests card.test.ts T09-1, T09-2 (no or wrong signature refused before any chain call) and T09-9 (per-card limit) |
| Drain the fee sponsor by asking for top-ups for many addresses | POST /api/fund |
The address being funded must sign the request. Skip addresses already above 3 actions’ worth. Limit per address and per IP; the device id comes from the client, so it is never the only limit. A daily cap of 50 MON in total; past it, top-ups stop and an alert fires. | Server tests fund.test.ts “POST /api/fund checks before any send” and decideFund |
| Replay a signed request seen once | Private text, profile, card | The signed message names the site, chain id 10143, the action, the account, a single-use server nonce and the time. A nonce expires after 5 minutes; used nonces are stored. | Server tests platform.test.ts T05-1 to T05-5 (message, checks, replayed and expired nonces) |
| Fake or replay a payment notification | POST /api/midtrans/webhook |
Check signature_key = SHA-512(order id + status code + gross amount + server key) with a constant-time compare. Then fetch the status from the Midtrans API. Deposit only on settlement, only when the amount equals the order’s, once per order id (unique key in the database). |
Server tests topup.test.ts T08-1, T08-2, T08-4, T08-5, T08-6 and the chain test T08-21 |
| Use test-only tools on a real deployment | Simulate payment, Collect now, Skip time, View as Mama | A server flag DEMO_MODE. When it is off, those routes return 404, and the contract is deployed with its demo switch off, which cannot be changed later. Hiding a button is never the control. |
Server tests T05-16, T08-9, T09-10; contract test test_SEC_skipDaysRevertsWithDemoOff; demo-off bundle check (S15-15) |
| Read another parent’s order by guessing its id | GET /api/topup/[order], invites, reasons |
Every id is a random 128-bit value; nothing is numbered in order. The order endpoint returns status only. | Server tests T08-12 (another account’s order is 404) and the reason and recovery tests |
| Steal the passkey-derived key with injected script | Web app | A strict Content-Security-Policy with nonces: no inline script, no unsafe-eval. No dangerouslySetInnerHTML. Names, reasons and shop names render as plain text. The derived key lives in memory only, never in storage or logs. |
Server test T05-13 (headers and CSP); lint rule react/no-danger; key never stored (06) |
| Read the invite’s one-time key from logs | Invite link | The key goes after # in the link, which browsers never send to a server. Referrer-Policy: no-referrer. The server only ever sees the invite’s address. |
Invite link format (10); server test T05-13 (Referrer-Policy: no-referrer) |
| Reuse an invite or recovery signature elsewhere | Contract | EIP-712 signatures that name chain id 10143 and the contract address, each usable once. | Contract tests test_SEC_inviteSignatureForAnotherChainRejected, test_SEC_inviteSignatureForAnotherContractRejected, test_SA06_inviteKeyWorksOncePerAccount |
| Re-enter the contract during a transfer, or break the money total | Contract | Checks before effects before transfers, ReentrancyGuard on every function that moves AUSD, SafeERC20. Invariant tests for the six rules in section 7. |
Contract test test_SEC_reentrancyBlocked; invariant tests invariant_1 to invariant_6 |
| Use a stolen server key | Server keys | Three wallets, each allowed only what R3 gives it, each holding only a few days’ budget. Keys sit in Vercel’s encrypted settings, never in a NEXT_PUBLIC_ variable. A build step searches the browser bundle for key patterns. |
Contract invariant invariant_5_onlyAllowedCallersSucceed; server tests T05-18, T05-21; bundle scan T05-23 |
| Inject SQL | Database | Queries only through the ORM with parameters, never built from strings. The production database user cannot change tables. | Server test T05-22 (database role); lint rule against sql.raw |
| Flood the server to run up cost | Every POST |
Rate limits per IP and per account. A 10 KB body limit. The 280-character reason limit is checked on the server too. | Server tests T05-9 (rate limits) and T05-10 (body); reason length tests (12) |
| Frame the app, or downgrade the connection | Web app | Headers: HSTS, frame-ancestors 'none', X-Content-Type-Options: nosniff, a Permissions-Policy that allows only what passkeys need. CORS: same origin only. |
Server test T05-13 (headers and CSP), T05-14 (origin) |
| Ship a malicious or vulnerable package | Dependencies | Lockfile committed and installed with npm ci, exact versions. Next.js and React on their newest patch release. Check downloads, maintainers and install scripts before adding any package. |
npm run check:deps (exact versions, lockfile, install scripts, framework patch); OSV-Scanner and Dependabot in CI |
What the server keys can and cannot do
Section titled “What the server keys can and cannot do”Three wallets, each allowed only what R3 gives it. The treasury can only deposit into an account. The card service can only ask the contract to approve, collect, release or refund a purchase; it cannot spend locked or unreleased money or pay a blocked shop. The fee sponsor sends small amounts of test MON for fees and cannot call the contract. Each wallet holds only a few days’ budget.
What the chain shows
Section titled “What the chain shows”Shop names are kept on the server and shown only to the student. The chain records the envelope, the amount, the time and the kind of shop. Anyone who looks up the contract can see those. The same data is served by the public indexer.
Fees are sponsored in this version. A real launch replaces the fee top-up with a relayer, so accounts never hold MON.
Accepted risks
Section titled “Accepted risks”- Testnet only; no real money moves.
- Purchase amounts are public on the chain (R27).
- Someone with many addresses can still get some test MON, within the caps.
- One deployer address sets the contract up.
- No external audit; the contract has a self-audit only. A launch with real money needs an external audit first.