Integrate17
Solidity
On-chain, CompliR is one interface and one require. AttestationRegistry stores the latest tier per address with its expiry and the decision hash; your contract asks isAllowed before it moves funds. The registry is live on Arc mainnet at 0xA82EDb5e111c31C63E06EF0007f2fa1a9e7EB30d (verified on Sourcify); the code below is what it answers.
ICompliR
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/// Minimal read interface every integrator needs. Copy this file (contracts/src/ICompliR.sol).
interface ICompliR {
enum Tier { UNKNOWN, LOW, MEDIUM, HIGH } // numeric values are part of the ABI
event Attested(address indexed subject, Tier tier, uint64 validUntil, bytes32 decisionHash);
event TierChanged(address indexed subject, Tier from, Tier to); // also on revoke
event Anchored(bytes32 indexed root, uint256 fromSeq, uint256 toSeq);
/// Raw stored tier. UNKNOWN when never attested or revoked; validUntil 0 then. Check validUntil yourself.
function tierOf(address subject) external view returns (Tier tier, uint64 validUntil, bytes32 decisionHash);
/// true iff tier != UNKNOWN && tier <= maxTier && validUntil > block.timestamp
function isAllowed(address subject, Tier maxTier) external view returns (bool);
}- Tier values
- UNKNOWN 0 · LOW 1 · MEDIUM 2 · HIGH 3 - mirrored in src/lib/types.ts TIER_ONCHAIN
- decisionHash
- keccak256 of the canonical decision JSON; the same value sits in the signed attestation and the ledger entry
- Registry
- AttestationRegistry.sol: Ownable2Step + Pausable, attester role, EIP-712 attest (permissionless, signature is the authority), attestDirect / attestBatch (attester), revoke, anchorRoot (onlyAttester)
The one-line integration
require(ICompliR(COMPLIR_REGISTRY).isAllowed(payee, ICompliR.Tier.MEDIUM), "CompliR: payee tier too high");That is the whole integration. It fails closed: an address that was never screened is UNKNOWN, an attestation past validUntil is stale, and both revert before any transfer. Pair it with a watch so a tier change refreshes the registry before your next payout, or read tierOf and apply your own rule.
GuardedTreasury example
// contracts/src/examples/GuardedTreasury.sol - the one-line integration as a complete example
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ICompliR} from "../ICompliR.sol";
contract GuardedTreasury is Ownable {
using SafeERC20 for IERC20;
ICompliR public immutable compliR;
IERC20 public immutable token; // USDC on Arc: 0x3600…0000 (6 dp)
ICompliR.Tier public maxTier = ICompliR.Tier.MEDIUM; // HIGH is never paid
event Released(address indexed payee, uint256 amount, bytes32 decisionHash);
event MaxTierChanged(ICompliR.Tier from, ICompliR.Tier to);
constructor(address registry, address token_, address initialOwner) Ownable(initialOwner) {
compliR = ICompliR(registry);
token = IERC20(token_);
}
/// An agent proposes, the contract decides.
function release(address payee, uint256 amount) external onlyOwner {
require(compliR.isAllowed(payee, maxTier), "CompliR: payee tier too high");
(,, bytes32 decisionHash) = compliR.tierOf(payee);
token.safeTransfer(payee, amount);
emit Released(payee, amount, decisionHash);
}
function setMaxTier(ICompliR.Tier newMax) external onlyOwner {
require(newMax != ICompliR.Tier.UNKNOWN, "GuardedTreasury: UNKNOWN never pays");
emit MaxTierChanged(maxTier, newMax);
maxTier = newMax;
}
function withdraw(address to, uint256 amount) external onlyOwner {
token.safeTransfer(to, amount);
}
}This is the contract behind Guarded payouts. Deployment, funding and the Foundry scripts are in contracts/README.md; gas on Arc is USDC and the broadcaster needs a USDC balance.
Develop
cd contracts
forge build && forge test -vv # 45 tests, incl. typehash parity with the TypeScript signer
forge build && node scripts/export-abi.mjs # → src/lib/contracts/{registryAbi,guardedTreasuryAbi}.tsDependencies are vendored (forge-std, OpenZeppelin 5.4.0); no submodules. evm_version = paris keeps the bytecode runnable on any post-Merge EVM until Arc's fork is pinned. Verified deploy addresses will appear on Welcome and at /api/v1/health.