Skip to content
Detent

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/detent rebuilds the plan from the register snapshot this server reads, not from the request body, and refuses with plan_mismatch when 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 with lock_unknown rather 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_TOKEN is set, the lock intent also requires it in the x-detent-operator header. 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.ts issues real reads over Hashio, but no token has been issued for this submission, so the recorded run serves the cached register in fixtures/register.seed.json and 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. PlanAnchor has 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).