Architecture
Architecture
Sentinel Treasury is a single-chain HashKey MVP. Every component is read-only or draft-only on Sentinel's side; the customer's Safe owner quorum is the sole authority for any on-chain write.
Single-chain HashKey
The MVP targets HashKey Chain only (testnet 133; mainnet 177 is signal-gated). Multi-chain ingestion was retired to keep the trust surface small and the integration HashKey-native. Safe data is read through the HashKey-hosted Safe Transaction Service; on-chain reads go through a configurable HashKey RPC endpoint.
KYC SBT gate
The HashKey-native compliance core. Sentinel reads each Safe owner's tier from the configured HashKey KYC SBT via isHuman(address) and getKycInfo(address). Tiers are NONE / BASIC / ADVANCED / PREMIUM / ULTIMATE; statuses are NONE / APPROVED / REVOKED.
The gate is compliance-gated drafting, not compliance-enforced execution. The tier decides whether Sentinel's UI offers to prepare a proposal draft. It never blocks the chain: Safe owners can always operate their Safe directly, outside Sentinel, regardless of tier. The read is fail-open — an unreadable SBT surfaces tier UNKNOWN rather than inventing a tier.
Customer-Safe-anchored EvidenceRegistry
Evidence is anchored on-chain in a minimal append-only contract. Records are namespaced per customer by msg.sender:
anchor(bytes32 batchId, bytes32 root)writes toanchors[msg.sender][batchId]. Only the caller can write into their own namespace — there is no admin, no privileged writer.- Reverts on a zero root, a zero batchId, or an attempt to overwrite an existing anchor (append-only).
- Emits
EvidenceAnchored(customer, batchId, root, timestamp, blockNumber).
Because the namespace key is msg.sender, Sentinel cannot write into any customer's evidence record even in principle — it would have to be the customer's Safe to do so.
Hash-chained audit log
Before anything is anchored, every state-changing operation (policy change, draft created, warning raised, proposal surfaced) is appended to a hash-chained audit log. Each entry stores a prev_hash and an entry_hash, where the entry hash folds the previous hash and the customer Safe address into a canonical JSON body:
entry_hash = keccak256(canonical_json({...payload, _prev_hash, _customer}))Folding _customer into the leaf binds every entry to one Safe by construction: two customers with identical payloads at identical chain positions produce different hashes, so a leaf from one customer's log can never be replayed as evidence in another's. Tampering any historical entry breaks the next entry's prev_hash link on re-walk.
Sorted-pair Merkle anchoring
A batch of audit entries is Merkle-rooted using commutative, sorted-pair keccak256 hashing — identical between Sentinel's generator and OpenZeppelin's MerkleProof.verifyCalldataused on-chain in EvidenceRegistry.verify. The same entry_hash doubles as the Merkle leaf, so there is a single canonical hash per entry across the off-chain log and the on-chain proof.
The anchor flow
Sentinel prepares; the customer signs. Concretely:
- An operator-side tool computes the batch root, writes a
pre_anchor_commitentry to the audit log (so Sentinel's stated intent is itself tamper-evident), and builds an unsignedMetaTransactionDataforEvidenceRegistry.anchor(batchId, root). - The customer reviews the proposal in the demo UI and verifies the root locally (see For developers).
- The customer's Safe owner quorum signs and executes through Safe Wallet. Sentinel never signs, never posts to the Safe Transaction Service, and never executes.
