Oracle06
Attestations & registry
Every verdict is signed as EIP-712 typed data by the CompliR attester key, so anyone can check it without asking CompliR. The same signature is what the on-chain registry verifies when the attestation is mirrored to Arc, and the decisionHash inside it links back to the replayable ledger entry.
The struct
EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)
name = "CompliR", version = "1", chainId = 5042, verifyingContract = AttestationRegistry
Attestation(address subject,uint8 tier,uint8 score,bytes32 decisionHash,uint64 issuedAt,uint64 validUntil)
typehash = 0x86d4b56447401a0aa7aea88cef207cd6ff21bb50dd72f46b5e5baccf75e854db
tier: UNKNOWN=0 · LOW=1 · MEDIUM=2 · HIGH=3
decisionHash = keccak256(utf8(canonical(decision core))) // sorted keys, no whitespaceThe TypeScript signer (src/lib/attest/eip712.ts) and the Solidity registry must stay byte-identical; two Foundry tests pin the typehash and accept a viem-signed attestation on the Arc chain id.
Validity per tier
- LOW
- valid 7 days from issuedAt
- MEDIUM
- valid 24 hours
- HIGH
- valid 1 hour
Expiry is the whole point: a contract reading isAllowed never pays on a stale verdict, and a watcher re-screen refreshes the attestation before a watched LOW address runs out. attestation.validUntil always equals typedData.message.validUntil.
isAllowed on-chain
interface ICompliR {
enum Tier { UNKNOWN, LOW, MEDIUM, HIGH }
function tierOf(address subject) external view returns (Tier tier, uint64 validUntil, bytes32 decisionHash);
function isAllowed(address subject, Tier maxTier) external view returns (bool);
// isAllowed == tier != UNKNOWN && tier <= maxTier && validUntil > block.timestamp
event Attested(address indexed subject, Tier tier, uint64 validUntil, bytes32 decisionHash);
event TierChanged(address indexed subject, Tier from, Tier to);
event Anchored(bytes32 indexed root, uint256 fromSeq, uint256 toSeq);
}tierOf returns the raw stored tier; callers must check validUntil themselves. isAllowed folds the three conditions into one call and is what GuardedTreasury uses. UNKNOWN (never attested, or revoked) never passes.
Registry on Arc mainnet: 0xA82EDb5e111c31C63E06EF0007f2fa1a9e7EB30d live · verified. verifyingContract in the EIP-712 domain is this address; every verdict is mirrored with attest() (first one: tx) and registryTx fills in once it lands. Also published at /api/v1/health.
Verify offline
import { CompliR } from "@complir/sdk";
const ok = await CompliR.verifyAttestation(verdict.attestation, {
expectedAttester: "0x…", // from GET /api/v1/health → signer, or the registry's attester()
expectedChainId: 5042,
// ignoreExpiry: true // when auditing history
});
// checks: primaryType, domain name/version, field list, tier ∈ {1,2,3}, score 0–100,
// validUntil > issuedAt and == att.validUntil, bytes32 decisionHash, then viem verifyTypedData.The MCP tool verify_attestation does the same check and returns the decoded subject, tier, score, decisionHash and validUntil. Three steps make a full audit:
- Recover the signer from
typedData+signatureand compare with the published attester. - Call
tierOf(subject)on the registry: the storeddecisionHashmust equaltypedData.message.decisionHash(once the registry is live). - Open
GET /api/v1/decision/:decisionId(or /ledger →/log/:id): the payload hashes into the chain and its batch anchor tx is on Arc.
Mirroring and anchoring
- attest() is permissionless. The signature is the authority, so any relayer may submit it. With
SIGNER_MODE=localthe attester key sends the tx and pays USDC gas; withSIGNER_MODE=circlethe Circle ops wallet relays it (Gas Station-sponsored when a mainnet policy is active) and falls back to local on any failure. - Writes never block a verdict. The registry write is fired after the response;
registryTxis updated in the store when it lands and shows on the attestation endpoint and in webhooks. - Duplicate protection. The contract rejects a second write of the same
decisionHash(DecisionHashUsed), so a relay timeout followed by a local retry is safe. - anchorRoot() is onlyAttester. Every
ANCHOR_EVERY_N(default 50) ledger entries, their Merkle root is written with the sequence range and anANCHORentry is appended. See Ledger & reports.