Access Control Patterns
Multi-layer access control architecture implementing zero-trust security across all Integra smart contracts.
Overview
The Integra access control system implements a multi-layer security model that combines attestation-based capabilities, record ownership validation, and per-record executor authorization. Each layer operates independently while complementing the others, ensuring that sensitive operations require multiple forms of validation. This defense-in-depth approach means that even if one security layer is compromised, the remaining layers continue to protect system integrity.
The three-tiered model provides different levels of granularity:
- Attestation-based capabilities -- Fine-grained permissions through 256-bit capability bitmasks with pluggable providers
- Record ownership -- Coarse-grained permissions based on immutable ownership records
- Per-record executor authorization -- Delegated operational authority through an opt-in, zero-trust model
Foundation: Attestation-Based Capabilities
Capability Namespace Architecture
The CapabilityNamespaceV1 contract defines a permanent 256-bit capability namespace organized into tiers:
// TIER 0: Core Capabilities (Bits 0-7)
uint256 public constant CORE_VIEW = 1 << 0; // Read-only access
uint256 public constant CORE_CLAIM = 1 << 1; // Claim reserved tokens
uint256 public constant CORE_TRANSFER = 1 << 2; // Transfer token ownership
uint256 public constant CORE_UPDATE = 1 << 3; // Update metadata
uint256 public constant CORE_DELEGATE = 1 << 4; // Delegate capabilities
uint256 public constant CORE_REVOKE = 1 << 5; // Revoke access
uint256 public constant CORE_ADMIN = 1 << 7; // Full admin (implies all)
// TIER 1: Record Operations (Bits 8-15)
uint256 public constant DOC_SIGN = 1 << 8; // Sign records
uint256 public constant DOC_WITNESS = 1 << 9; // Witness signatures
uint256 public constant DOC_NOTARIZE = 1 << 10; // Notarize records
uint256 public constant DOC_VERIFY = 1 << 11; // Verify authenticity
uint256 public constant DOC_AMEND = 1 << 12; // Amend content
uint256 public constant DOC_ARCHIVE = 1 << 13; // Archive records
// TIER 2: Financial Operations (Bits 16-23)
uint256 public constant FIN_REQUEST_PAYMENT = 1 << 16;
uint256 public constant FIN_APPROVE_PAYMENT = 1 << 17;
uint256 public constant FIN_EXECUTE_PAYMENT = 1 << 18;
uint256 public constant FIN_CANCEL_PAYMENT = 1 << 19;
uint256 public constant FIN_WITHDRAW = 1 << 20;
uint256 public constant FIN_DEPOSIT = 1 << 21;
// TIER 3: Governance Operations (Bits 24-31)
uint256 public constant GOV_PROPOSE = 1 << 24;
uint256 public constant GOV_VOTE = 1 << 25;
uint256 public constant GOV_EXECUTE = 1 << 26;
uint256 public constant GOV_VETO = 1 << 27;
uint256 public constant GOV_DELEGATE_VOTE = 1 << 28;Role Templates
Pre-defined capability compositions for common roles:
uint256 public constant ROLE_VIEWER = CORE_VIEW;
uint256 public constant ROLE_PARTICIPANT =
CORE_VIEW | CORE_CLAIM | CORE_TRANSFER | FIN_REQUEST_PAYMENT;
uint256 public constant ROLE_MANAGER =
ROLE_PARTICIPANT | CORE_UPDATE | FIN_APPROVE_PAYMENT | DOC_SIGN | DOC_WITNESS;
uint256 public constant ROLE_ADMIN = (1 << 128) - 1;Capability Operations
function hasCapability(uint256 granted, uint256 required) external pure returns (bool) {
return ((granted & CORE_ADMIN) != 0) || ((granted & required) == required);
}
function composeCapabilities(uint256[] calldata capabilities)
external pure returns (uint256)
{
uint256 composed = 0;
for (uint256 i = 0; i < capabilities.length; i++) {
composed |= capabilities[i];
}
return composed;
}
function addCapability(uint256 current, uint256 toAdd) external pure returns (uint256) {
return current | toAdd;
}
function removeCapability(uint256 current, uint256 toRemove) external pure returns (uint256) {
return current & ~toRemove;
}Provider Abstraction
The attestation system uses a provider abstraction layer supporting multiple attestation systems:
function _verifyCapability(
address user,
bytes32 contentHash,
uint256 requiredCapability,
bytes calldata attestationProof
) internal nonReentrant whenNotPaused {
// STEP 1: Get provider ID (record-specific or default)
bytes32 providerId = recordProvider[contentHash];
if (providerId == bytes32(0)) {
providerId = defaultProviderId;
}
// STEP 2: Get provider address (with code hash verification)
address provider = PROVIDER_REGISTRY.getProvider(providerId);
if (provider == address(0)) {
revert ProviderNotFound(providerId);
}
// STEP 3: Delegate to provider
(bool verified, uint256 grantedCapabilities) = IAttestationProvider(provider)
.verifyCapabilities(attestationProof, user, contentHash, requiredCapability);
if (!verified) {
revert ProviderVerificationFailed(providerId, "Provider verification failed");
}
// STEP 4: Check capabilities using namespace
if (!NAMESPACE.hasCapability(grantedCapabilities, requiredCapability)) {
revert NoCapability(user, contentHash, requiredCapability);
}
}13-Step EAS Verification
The EASAttestationProviderV1 implements comprehensive verification:
- Fetch attestation from EAS contract
- Verify attestation exists (UID != 0)
- Verify not revoked (revocationTime == 0)
- Verify not expired
- Verify schema matches expected schema
- Verify recipient matches caller (front-running protection)
- Verify attester is authorized issuer
- Verify source chain ID matches (cross-chain replay prevention)
- Verify source EAS contract matches (EAS spoofing prevention)
- Verify record contract matches (contract spoofing prevention)
- Verify schema version
- Verify content hash matches
- Verify attestation age (optional time limits)
Record Ownership Model
The record registry implements a pure ownership model with immutable trust guarantees:
struct RecordData {
address owner;
address tokenizer;
bytes32 contentHash;
bytes32 referenceHash;
uint64 registeredAt;
bool exists;
bytes32 identityExtension;
}Owner-Only Operations
function transferRecordOwnership(
bytes32 integraHash,
address newOwner,
string calldata reason
) external nonReentrant whenNotPaused {
RecordData storage rec = records[integraHash];
if (msg.sender != rec.owner) {
revert Unauthorized(msg.sender, integraHash);
}
if (newOwner == address(0)) revert ZeroAddress();
if (newOwner == rec.owner) revert AlreadyOwner(newOwner, integraHash);
address oldOwner = rec.owner;
rec.owner = newOwner;
emit RecordOwnershipTransferred(integraHash, oldOwner, newOwner, reason, block.timestamp);
}Per-Record Executor Authorization
Zero-Trust Executor Model
modifier requireOwnerOrExecutor(bytes32 integraHash) {
// Validate record uses THIS tokenizer
address recordTokenizer = integraRecordV1.getTokenizer(integraHash);
if (recordTokenizer == address(0)) revert TokenizerNotSet(integraHash);
if (recordTokenizer != address(this)) {
revert WrongTokenizer(integraHash, recordTokenizer, address(this));
}
// PATH 1: Record owner (highest priority)
address owner = integraRecordV1.getRecordOwner(integraHash);
if (msg.sender == owner) { _; return; }
// PATH 2: Per-record authorized executor (opt-in)
address authorizedExecutor = integraRecordV1.getRecordExecutor(integraHash);
if (authorizedExecutor != address(0) && msg.sender == authorizedExecutor) { _; return; }
// PATH 3: Unauthorized - revert
revert Unauthorized(msg.sender, integraHash);
}Executor Types
The system supports three executor types:
- Whitelisted executor (fast path) -- Governance-approved addresses skip interface validation
- Contract executor -- Must implement
IIntegraExecutorinterface - Non-whitelisted EOA -- Allowed for self-hosted instances (owner chose to trust this address)
Executor Lifecycle
// Owner authorizes executor
integraRecordV1.authorizeRecordExecutor(integraHash, executorAddress);
// Owner revokes executor
integraRecordV1.revokeRecordExecutor(integraHash);Multi-Layer Security in Practice
Token Claim Operation
function claimToken(
bytes32 integraHash,
uint256 tokenId,
bytes calldata attestationProof
) external
requiresCapability(integraHash, CORE_CLAIM, attestationProof)
requireOwnerOrExecutor(integraHash)
nonReentrant
whenNotPaused
{
// All three layers passed
}Security Properties
- Attestation compromise: Still need record ownership/authorization
- Ownership compromise: Attestations limit damage scope to granted capabilities
- Executor compromise: Owner can immediately revoke; only authorized records affected
Zero-Trust Principles
- No global privileges exist that access all records
- All access must be explicitly granted
- Minimum capabilities should be granted (least privilege)
- All permissions can be revoked by the record owner