Skip to main content

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:

  1. Attestation-based capabilities -- Fine-grained permissions through 256-bit capability bitmasks with pluggable providers
  2. Record ownership -- Coarse-grained permissions based on immutable ownership records
  3. 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:

  1. Fetch attestation from EAS contract
  2. Verify attestation exists (UID != 0)
  3. Verify not revoked (revocationTime == 0)
  4. Verify not expired
  5. Verify schema matches expected schema
  6. Verify recipient matches caller (front-running protection)
  7. Verify attester is authorized issuer
  8. Verify source chain ID matches (cross-chain replay prevention)
  9. Verify source EAS contract matches (EAS spoofing prevention)
  10. Verify record contract matches (contract spoofing prevention)
  11. Verify schema version
  12. Verify content hash matches
  13. 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:

  1. Whitelisted executor (fast path) -- Governance-approved addresses skip interface validation
  2. Contract executor -- Must implement IIntegraExecutor interface
  3. 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