Security model
What is enforced, and what is not
Testnet only, nothing audited. This states what the server, the wallet policy and the contract each guarantee, and names the two paths that have not run against a live network.
What is enforced, and where
Four layers, each covering what the one before it cannot.
- The server derives the plan
POST /api/detentrebuilds the plan from the register snapshot this server reads, not from the request body, and refuses withplan_mismatchwhen the submitted plan hash, calldata, target or chain differ from what the server derives. Blockers are enforced on both the lock and the submit step from that server derived plan.- Lock ids are server minted
- A lock id is 24 random bytes minted by
lib/store.ts, never a value the client chose. A submit whose lock this server instance does not hold, recycled, expired after fifteen minutes, or never issued, is refused withlock_unknownrather than recompiled from whatever the client sent. - The approval quorum
- Two approvals are required, each resolved against the registered officers in
lib/data.ts, and no officer counts twice. This is a registry check, not authentication: there is no session and no signature over an approval, so it proves the request named two distinct registered officers, not that either of them approved anything. - Rate limiting and the operator token
- A fixed window limiter keyed by client address allows 30 requests a minute and 6 lock attempts a minute, held in one process's memory with a ceiling on tracked addresses. When
OPERATOR_API_TOKENis set, the lock intent also requires it in thex-detent-operatorheader. Unset, the lock is open, which is the keyless demo posture. - The wallet policy pins what Privy can compare
- The installed policy pins chain id, the target contract, and the function and partition through Privy's calldata condition. Privy's documented conditions cannot compare an array of holders and amounts, so for a coupon those two arguments are not constrained by the wallet: the exact calldata, byte for byte, is checked by this server before a submitted payload ever reaches the wallet.
- Policy cleanup
- A send that lands, a send that fails, a send that times out, an attach that is refused and a lock that expires unspent all write the wallet's previous policy list back and delete the policy. Nothing is left attached to the treasury key once its one transaction is decided.
Not verified live
Stated exactly as README.md and SECURITY.md state it: what has run against a live service and what has not.
- The register read.
lib/hedera.tsissues real reads over Hashio, but no token has been issued for this submission, so the recorded run serves the cached register infixtures/register.seed.jsonand labels it as cached. - The signature. Live on the deployed site: the approved plan is signed and broadcast by the Privy wallet under the attached policy. A fresh clone with no Privy credentials still runs the same policy evaluator locally, which is why the demo produces a real refusal with no keys and no broadcast.
- The on chain record.
PlanAnchorhas 37 Foundry tests passing, 9 of them fuzz, but has not been deployed. The console and/record/[planHash]both name that state on screen rather than implying a record that does not exist.
Contracts and chain
Read live from lib/public-config.ts. An address prints as plain text, never a link, until it exists on chain.
- Network
- Hedera testnet, chain 296
- RPC relay
- https://testnet.hashio.io/api
- ATS equity token
- Not configured for this deployment: the console serves the cached register.
- PlanAnchor
- PlanAnchor contract address 0x1a393277908834c39D2611Ce9A151474B8Dfea0dHashScan (opens in a new tab)
Report a problem
This is a testnet hackathon build with no user funds at risk, so there is no bounty programme.
Open an issue on the repository. The full policy, including what the API route trusts on a public request and how a Privy owned resource is authorized, is in SECURITY.md (opens in a new tab).