Access Control Patterns
Access Control Patterns
Section titled “Access Control Patterns”Multi-layer access control architecture implementing zero-trust security across all Integra smart contracts.
Overview
Section titled “Overview”The Integra access control system implements a sophisticated multi-layer security model that combines attestation-based capabilities, document ownership validation, and per-document executor authorization to create a comprehensive zero-trust security architecture. Each layer operates independently while complementing the others, ensuring that sensitive operations require multiple forms of validation before execution. 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 for access control across the platform. At the foundation level, attestation-based capabilities enable fine-grained permissions through 256-bit capability bitmasks, supporting multiple attestation providers including EAS, Verifiable Credentials, ZK proofs, and DIDs with built-in front-running protection. The document ownership layer provides coarse-grained permissions based on immutable ownership records, guaranteeing owner sovereignty over document configuration and lifecycle. Finally, the per-document executor authorization layer enables secure delegation of operational authority through an opt-in model that supports EOAs, DAOs, multisigs, and custom contract executors while maintaining zero-trust defaults and preserving ultimate owner control.
Foundation: Attestation-Based Capabilities
Section titled “Foundation: Attestation-Based Capabilities”Capability Namespace Architecture
Section titled “Capability Namespace Architecture”The CapabilityNamespace_Immutable contract defines a permanent 256-bit capability namespace organized into tiers:
/** * CAPABILITY NAMESPACE: * Bits 0-7: Core capabilities (view, claim, transfer, update, delegate, revoke, admin) * Bits 8-15: Document operations (sign, witness, notarize, verify, amend, archive) * Bits 16-23: Financial operations (request, approve, execute, cancel, withdraw, deposit) * Bits 24-31: Governance operations (propose, vote, execute, veto, delegate) * Bits 32-255: Reserved for future protocol extensions */
// TIER 0: Core Capabilities (Bits 0-7)uint256 public constant CORE_VIEW = 1 << 0; // Read-only accessuint256 public constant CORE_CLAIM = 1 << 1; // Claim reserved tokensuint256 public constant CORE_TRANSFER = 1 << 2; // Transfer token ownershipuint256 public constant CORE_UPDATE = 1 << 3; // Update metadatauint256 public constant CORE_DELEGATE = 1 << 4; // Delegate capabilitiesuint256 public constant CORE_REVOKE = 1 << 5; // Revoke accessuint256 public constant CORE_ADMIN = 1 << 7; // Full admin (implies all)
// TIER 1: Document Operations (Bits 8-15)uint256 public constant DOC_SIGN = 1 << 8; // Sign documentsuint256 public constant DOC_WITNESS = 1 << 9; // Witness signaturesuint256 public constant DOC_NOTARIZE = 1 << 10; // Notarize documentsuint256 public constant DOC_VERIFY = 1 << 11; // Verify authenticityuint256 public constant DOC_AMEND = 1 << 12; // Amend contentuint256 public constant DOC_ARCHIVE = 1 << 13; // Archive documents
// TIER 2: Financial Operations (Bits 16-23)uint256 public constant FIN_REQUEST_PAYMENT = 1 << 16; // Request paymentsuint256 public constant FIN_APPROVE_PAYMENT = 1 << 17; // Approve paymentsuint256 public constant FIN_EXECUTE_PAYMENT = 1 << 18; // Execute paymentsuint256 public constant FIN_CANCEL_PAYMENT = 1 << 19; // Cancel paymentsuint256 public constant FIN_WITHDRAW = 1 << 20; // Withdraw fundsuint256 public constant FIN_DEPOSIT = 1 << 21; // Deposit funds
// TIER 3: Governance Operations (Bits 24-31)uint256 public constant GOV_PROPOSE = 1 << 24; // Propose actionsuint256 public constant GOV_VOTE = 1 << 25; // Vote on proposalsuint256 public constant GOV_EXECUTE = 1 << 26; // Execute proposalsuint256 public constant GOV_VETO = 1 << 27; // Veto proposalsuint256 public constant GOV_DELEGATE_VOTE = 1 << 28; // Delegate votingRole Templates
Section titled “Role Templates”Pre-defined capability compositions for common roles:
// Viewer: Read-only accessuint256 public constant ROLE_VIEWER = CORE_VIEW;
// Participant: Basic document interaction// Can view, claim tokens, transfer, request paymentsuint256 public constant ROLE_PARTICIPANT = CORE_VIEW | CORE_CLAIM | CORE_TRANSFER | FIN_REQUEST_PAYMENT;
// Manager: Operational management// Participant + update, approve payments, sign, witnessuint256 public constant ROLE_MANAGER = ROLE_PARTICIPANT | CORE_UPDATE | FIN_APPROVE_PAYMENT | DOC_SIGN | DOC_WITNESS;
// Admin: Full capabilities (all bits 0-127)// Upper 128 bits reserved for future protocol extensionsuint256 public constant ROLE_ADMIN = (1 << 128) - 1;Capability Checking
Section titled “Capability Checking”The namespace provides utility functions for capability operations:
/** * @notice Check if granted capabilities include required capability * @dev CORE_ADMIN grants all capabilities automatically */function hasCapability(uint256 granted, uint256 required) external pure returns (bool) { // Admin has all capabilities OR all required bits are set return ((granted & CORE_ADMIN) != 0) || ((granted & required) == required);}
/** * @notice Compose multiple capabilities into single bitmask */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;}
/** * @notice Add capability to existing set */function addCapability(uint256 current, uint256 toAdd) external pure returns (uint256) { return current | toAdd;}
/** * @notice Remove capability from existing set */function removeCapability(uint256 current, uint256 toRemove) external pure returns (uint256){ return current & ~toRemove;}Provider Abstraction Pattern
Section titled “Provider Abstraction Pattern”The attestation system uses a provider abstraction layer to support multiple attestation systems:
// AttestationAccessControl.sol - Provider selection/// @notice Default provider for all documentsbytes32 public defaultProviderId;
/// @notice Per-document provider overridemapping(bytes32 => bytes32) public documentProvider;
function _verifyCapability( address user, bytes32 documentHash, uint256 requiredCapability, bytes calldata attestationProof) internal nonReentrant whenNotPaused { // STEP 1: Get provider ID (document-specific or default) bytes32 providerId = documentProvider[documentHash]; 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 (standard interface) (bool verified, uint256 grantedCapabilities) = IAttestationProvider(provider) .verifyCapabilities(attestationProof, user, documentHash, requiredCapability);
if (!verified) { revert ProviderVerificationFailed(providerId, "Provider verification failed"); }
// STEP 4: Check capabilities using namespace if (!NAMESPACE.hasCapability(grantedCapabilities, requiredCapability)) { revert NoCapability(user, documentHash, requiredCapability); }
emit CapabilityVerified(user, documentHash, grantedCapabilities, providerId, attestationProof);}13-Step Verification Process (EAS Provider)
Section titled “13-Step Verification Process (EAS Provider)”The EASAttestationProvider implements comprehensive verification:
/** * VERIFICATION STEPS (13 total): * 1. ✅ Fetch attestation from EAS contract * 2. ✅ Verify attestation exists (UID != 0) * 3. ✅ Verify not revoked (revocationTime == 0) * 4. ✅ Verify not expired (expirationTime == 0 or > now) * 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 document contract matches (contract spoofing prevention) * 11. ✅ Verify schema version (schema version validation) * 12. ✅ Verify document hash matches * 13. ✅ Verify attestation age (optional time limits) */function verifyCapabilities( bytes calldata proof, address recipient, bytes32 documentHash, uint256 requiredCapability) external view returns (bool verified, uint256 grantedCapabilities) { // Decode proof (bytes32 uid, bytes32 expectedDocHash, address expectedIssuer) = abi.decode(proof, (bytes32, bytes32, address));
// STEP 1: Fetch attestation Attestation memory attestation = eas.getAttestation(uid);
// STEP 2: Verify exists if (attestation.uid != uid) return (false, 0);
// STEP 3: Verify not revoked if (attestation.revocationTime != 0) return (false, 0);
// STEP 4: Verify not expired if (attestation.expirationTime != 0 && block.timestamp > attestation.expirationTime) { return (false, 0); }
// STEP 5: Verify schema if (attestation.schema != capabilitySchema) return (false, 0);
// STEP 6: FRONT-RUNNING PROTECTION - Verify recipient if (attestation.recipient != recipient) return (false, 0);
// STEP 7: Verify attester is authorized if (!authorizedIssuers[attestation.attester]) return (false, 0);
// STEP 8-12: Decode and verify attestation data ( uint256 capabilities, bytes32 docHash, uint256 chainId, address easContract, address docContract, uint256 schemaVersion ) = abi.decode(attestation.data, (uint256, bytes32, uint256, address, address, uint256));
if (chainId != block.chainid) return (false, 0); // STEP 8 if (easContract != address(eas)) return (false, 0); // STEP 9 if (docHash != documentHash) return (false, 0); // STEP 12
// STEP 13: Optional time limit if (maxAttestationAge > 0) { if (block.timestamp - attestation.time > maxAttestationAge) { return (false, 0); } }
return (true, capabilities);}Benefits of Attestation-Based Capabilities
Section titled “Benefits of Attestation-Based Capabilities”- Fine-Grained Control: 256 capability bits allow precise permissions
- Composable: Combine capabilities using bitwise OR
- Extensible: Upper 128 bits reserved for protocol extensions
- Provider Agnostic: Works with any attestation system (EAS, VCs, ZK, DIDs)
- Front-Running Safe: Recipient validation prevents proof theft
- Cross-Chain Safe: Chain ID validation prevents replay attacks
Integration Example
Section titled “Integration Example”// Using capability-based access control in a tokenizerfunction claimToken( bytes32 integraHash, uint256 tokenId, bytes calldata attestationProof) external requiresCapability( integraHash, CAPABILITY_CLAIM_TOKEN, // = CORE_CLAIM attestationProof ) nonReentrant whenNotPaused{ // Caller has proven they have CORE_CLAIM capability // ... claim token logic}Document Ownership Model
Section titled “Document Ownership Model”Pure Ownership Pattern
Section titled “Pure Ownership Pattern”The document registry implements a pure ownership model with immutable trust guarantees:
struct DocumentRecord { address owner; // Document owner address tokenizer; // Associated tokenizer bytes32 documentHash; // Content hash bytes32 referenceHash; // Parent document uint64 registeredAt; // Registration timestamp bool exists; // Existence flag bytes32 identityExtension; // Protocol extension hook
// Service Layer (via Resolvers) bytes32 primaryResolverId; // Primary resolver ID bytes32[] additionalResolvers; // Additional resolver IDs bool resolversLocked; // Resolver lock}
mapping(bytes32 => DocumentRecord) public documents;Owner-Only Operations
Section titled “Owner-Only Operations”/** * @notice Transfer document ownership * @dev Only current owner can transfer */function transferDocumentOwnership( bytes32 integraHash, address newOwner, string calldata reason) external nonReentrant whenNotPaused { DocumentRecord storage doc = documents[integraHash];
// Ownership validation if (msg.sender != doc.owner) { revert Unauthorized(msg.sender, integraHash); }
if (newOwner == address(0)) revert ZeroAddress(); if (newOwner == doc.owner) revert AlreadyOwner(newOwner, integraHash);
address oldOwner = doc.owner; doc.owner = newOwner;
emit DocumentOwnershipTransferred( integraHash, oldOwner, newOwner, reason, block.timestamp );}Ownership Queries
Section titled “Ownership Queries”/** * @notice Get document owner * @dev Reverts if document doesn't exist */function getDocumentOwner(bytes32 integraHash) public view returns (address) { DocumentRecord storage doc = documents[integraHash]; if (!doc.exists) revert DocumentNotRegistered(integraHash); return doc.owner;}
/** * @notice Check if address is document owner */function isDocumentOwner(bytes32 integraHash, address account) external view returns (bool){ DocumentRecord storage doc = documents[integraHash]; if (!doc.exists) return false; return doc.owner == account;}Benefits of Document Ownership
Section titled “Benefits of Document Ownership”- Simplicity: Clear owner → document mapping
- Immutable Trust: Cannot be upgraded (preserves ownership guarantees)
- Owner Sovereignty: Owner always has ultimate control
- Gas Efficient: Single storage slot lookup
- Transparent: Clear audit trail via events
Per-Document Executor Authorization
Section titled “Per-Document Executor Authorization”Zero-Trust Executor Model
Section titled “Zero-Trust Executor Model”Per-document authorization implements opt-in executor authorization with zero-trust defaults:
/** * @notice SECURE ACCESS CONTROL: Per-document executor authorization * @dev Implements zero-trust model with opt-in executor * * ACCESS PATHS (in priority order): * * 1. DOCUMENT OWNER (highest priority) * - Owner's ephemeral wallet (Privy) * - Always has full access (cannot be revoked) * - Owner sovereignty guaranteed * * 2. PER-DOCUMENT EXECUTOR (opt-in) * - Must be explicitly authorized by owner * - Can be EOA (backend server) or contract (DAO, multi-sig, escrow) * - Owner can revoke at any time * - DEFAULT: address(0) (no executor = owner-only access) * * 3. NO FALLBACK * - No global executor role * - No legacy compatibility * - Clean, secure, simple */modifier requireOwnerOrExecutor(bytes32 integraHash) { // VALIDATION: Ensure document uses THIS tokenizer address documentTokenizer = documentRegistry.getTokenizer(integraHash);
// Check tokenizer is set (not address(0)) if (documentTokenizer == address(0)) { revert TokenizerNotSet(integraHash); }
// Check document uses THIS tokenizer (not a different one) if (documentTokenizer != address(this)) { revert WrongTokenizer(integraHash, documentTokenizer, address(this)); }
// PATH 1: Check document owner (highest priority, most common) address owner = documentRegistry.getDocumentOwner(integraHash); if (msg.sender == owner) { _; return; }
// PATH 2: Check per-document authorized executor (opt-in) address authorizedExecutor = documentRegistry.getDocumentExecutor(integraHash); if (authorizedExecutor != address(0) && msg.sender == authorizedExecutor) { _; return; }
// PATH 3: Unauthorized - revert revert Unauthorized(msg.sender, integraHash);}Executor Authorization
Section titled “Executor Authorization”/** * @notice Authorize executor for specific document * @dev Owner can authorize backend server, DAO, multisig, or escrow */function authorizeDocumentExecutor( bytes32 integraHash, address executor) external nonReentrant whenNotPaused { DocumentRecord storage doc = documents[integraHash];
// Only owner can authorize if (msg.sender != doc.owner) { revert Unauthorized(msg.sender, integraHash); }
// Cannot authorize self if (executor == msg.sender) { revert CannotAuthorizeSelf(msg.sender); }
// Validate executor if (executor != address(0)) { _validateExecutor(executor); }
documentExecutor[integraHash] = executor; executorAuthorizedAt[integraHash] = block.timestamp;
emit DocumentExecutorAuthorized( integraHash, executor, msg.sender, _isContract(executor), block.timestamp );}
/** * @notice Revoke executor authorization */function revokeDocumentExecutor(bytes32 integraHash) external nonReentrant whenNotPaused{ DocumentRecord storage doc = documents[integraHash];
if (msg.sender != doc.owner) { revert Unauthorized(msg.sender, integraHash); }
address oldExecutor = documentExecutor[integraHash]; delete documentExecutor[integraHash]; delete executorAuthorizedAt[integraHash];
emit DocumentExecutorRevoked( integraHash, oldExecutor, msg.sender, block.timestamp );}Executor Validation
Section titled “Executor Validation”The system supports three types of executors with different validation paths:
/** * @dev Validate executor authorization * * THREE EXECUTOR TYPES: * 1. Whitelisted executor (fast path, 85%+ of cases) * 2. Contract executor with IIntegraExecutor interface * 3. Non-whitelisted EOA (self-hosted instances) */function _validateExecutor(address executor) internal view { // PATH 1: Whitelisted executor (governance-approved) // Fast path for known good executors (IntegraExecutor, DAOs, etc.) if (approvedExecutors[executor]) return;
// PATH 2: Contract executor with interface validation if (_isContract(executor)) { try IIntegraExecutor(executor).isLegitimateExecutor() returns (bool isLegit) { if (!isLegit) revert InvalidExecutorContract(executor, "Not legitimate"); return; } catch { revert InvalidExecutorContract(executor, "Interface check failed"); } }
// PATH 3: Non-whitelisted EOA // Allows self-hosted instances (user's own backend server) // No validation needed - owner chose to trust this EOA}Executor Whitelisting
Section titled “Executor Whitelisting”Governance can approve common executors for gas optimization:
/** * @notice Approve executor contract (governance only) * @dev Whitelisted executors skip interface validation (gas optimization) */function approveExecutor( address executor, bool approved, string calldata name) external onlyRole(GOVERNOR_ROLE) { if (executor == address(0)) revert ZeroAddress();
approvedExecutors[executor] = approved;
emit ExecutorApproved(executor, approved, name, block.timestamp);}Benefits of Per-Document Authorization
Section titled “Benefits of Per-Document Authorization”- Zero Trust: No global privileges, all access opt-in
- Owner Sovereignty: Owner maintains ultimate control
- Flexible: Supports EOAs, DAOs, multisigs, escrows, custom contracts
- Revocable: Owner can revoke at any time
- Tokenizer-Specific: Each document explicitly bound to tokenizer
- Gas Optimized: Whitelisting for common executors
Multi-Layer Security in Practice
Section titled “Multi-Layer Security in Practice”Example: Token Claim Operation
Section titled “Example: Token Claim Operation”/** * Token claim requires ALL THREE ACCESS CONTROL LAYERS to pass: * * Attestation-Based Capabilities: Caller must have CORE_CLAIM capability (attestation proof) * Document Ownership: Document must exist and have valid owner * Per-Document Authorization: Caller must be owner OR authorized executor */function claimToken( bytes32 integraHash, uint256 tokenId, bytes calldata attestationProof) external // Attestation-Based Capabilities: Verify capability via attestation requiresCapability(integraHash, CAPABILITY_CLAIM_TOKEN, attestationProof) // Per-Document Authorization: Verify owner or executor requireOwnerOrExecutor(integraHash) // Security modifiers nonReentrant whenNotPaused{ // Document Ownership: Implicit in requireOwnerOrExecutor (checks document exists)
// ... claim token logic}Example: Payment Request
Section titled “Example: Payment Request”/** * Payment request requires capability and document validation: */function sendPaymentRequest( bytes32 integraHash, address payer, uint256 amount, bytes calldata attestationProof) external requiresCapability(integraHash, FIN_REQUEST_PAYMENT, attestationProof) nonReentrant whenNotPaused{ // Document must exist (implicit validation) if (!documentRegistry.exists(integraHash)) { revert DocumentNotRegistered(integraHash); }
// ... payment request logic}Testing Strategy
Section titled “Testing Strategy”Attestation-Based Capability Tests
Section titled “Attestation-Based Capability Tests”describe("Attestation-Based Capabilities", () => { it("should verify valid attestation with correct capabilities", async () => { const attestation = await issueAttestation( user.address, integraHash, ROLE_PARTICIPANT );
await expect( tokenizer.claimToken(integraHash, tokenId, attestation) ).to.not.be.reverted; });
it("should reject attestation with insufficient capabilities", async () => { const attestation = await issueAttestation( user.address, integraHash, CORE_VIEW // Only view, not claim );
await expect( tokenizer.claimToken(integraHash, tokenId, attestation) ).to.be.revertedWithCustomError(tokenizer, "NoCapability"); });
it("should prevent front-running with wrong recipient", async () => { const attestation = await issueAttestation( alice.address, // Issued to Alice integraHash, ROLE_PARTICIPANT );
// Bob tries to use Alice's attestation await expect( tokenizer.connect(bob).claimToken(integraHash, tokenId, attestation) ).to.be.revertedWithCustomError(provider, "RecipientMismatch"); });});Document Ownership Tests
Section titled “Document Ownership Tests”describe("Document Ownership", () => { it("should allow owner to transfer ownership", async () => { await expect( registry.connect(owner).transferDocumentOwnership( integraHash, newOwner.address, "Transfer to new owner" ) ).to.emit(registry, "DocumentOwnershipTransferred"); });
it("should prevent non-owner from transferring", async () => { await expect( registry.connect(attacker).transferDocumentOwnership( integraHash, attacker.address, "Malicious transfer" ) ).to.be.revertedWithCustomError(registry, "Unauthorized"); });});Per-Document Executor Tests
Section titled “Per-Document Executor Tests”describe("Per-Document Executor", () => { it("should allow owner to authorize executor", async () => { await expect( registry.connect(owner).authorizeDocumentExecutor( integraHash, executor.address ) ).to.emit(registry, "DocumentExecutorAuthorized"); });
it("should allow authorized executor to claim token", async () => { await registry.connect(owner).authorizeDocumentExecutor( integraHash, executor.address );
await expect( tokenizer.connect(executor).claimToken( integraHash, tokenId, attestationProof ) ).to.not.be.reverted; });
it("should prevent unauthorized executor from claiming", async () => { await expect( tokenizer.connect(unauthorized).claimToken( integraHash, tokenId, attestationProof ) ).to.be.revertedWithCustomError(tokenizer, "Unauthorized"); });
it("should allow owner to revoke executor", async () => { await registry.connect(owner).authorizeDocumentExecutor( integraHash, executor.address );
await registry.connect(owner).revokeDocumentExecutor(integraHash);
await expect( tokenizer.connect(executor).claimToken( integraHash, tokenId, attestationProof ) ).to.be.revertedWithCustomError(tokenizer, "Unauthorized"); });});Security Considerations
Section titled “Security Considerations”Defense in Depth
Section titled “Defense in Depth”Multiple independent security layers provide defense in depth:
- Attestation-Based Capabilities Compromise: Even if attestation system compromised, still need document ownership/authorization
- Document Ownership Compromise: Even if owner key compromised, attestations limit damage scope
- Per-Document Authorization Compromise: Even if executor compromised, owner can immediately revoke
Zero-Trust Principles
Section titled “Zero-Trust Principles”- No Global Privileges: No roles can access all documents
- Explicit Authorization: All access must be explicitly granted
- Least Privilege: Grant minimum capabilities needed
- Revocable: All permissions can be revoked by document owner
Front-Running Protection
Section titled “Front-Running Protection”- Recipient Validation: Attestations bound to specific address
- Mempool Safety: Safe to broadcast transactions with proofs
- No Proof Theft: Stolen proofs can’t be used by attacker
Integration Guidelines
Section titled “Integration Guidelines”For dApp Developers
Section titled “For dApp Developers”-
Issue Attestations with Proper Recipients:
const attestation = await issueAttestation({recipient: user.address, // ✅ Must match callerdocumentHash,capabilities: ROLE_PARTICIPANT,// ... other fields}); -
Use Appropriate Capability Checks:
// For token claimsrequiresCapability(integraHash, CORE_CLAIM, proof)// For payment requestsrequiresCapability(integraHash, FIN_REQUEST_PAYMENT, proof)// For document updatesrequiresCapability(integraHash, CORE_UPDATE, proof) -
Implement Executor Authorization UI:
// Allow users to authorize backend serverawait registry.authorizeDocumentExecutor(integraHash, backendAddress);// Display current executorconst executor = await registry.getDocumentExecutor(integraHash);// Allow revocationawait registry.revokeDocumentExecutor(integraHash);
See Also
Section titled “See Also”- Security Patterns - Comprehensive security architecture
- Upgradeability Patterns - Progressive ossification
- Registry Patterns - Code hash verification
- Foundation Documentation - Attestation system details