Architecture Overview
System architecture for the Integra Protocol v1.7 smart contracts.
System Overview
The Integra protocol is an 11-layer smart contract system for on-chain document identity. Each layer has a single responsibility and communicates with adjacent layers through immutable contract references set at deployment. The entire system is built on Solidity 0.8.28 with OpenZeppelin v5.x and compiles with no upgradeability -- every contract is deployed once and is immutable.
The protocol is organized into 11 layers: Core, Access Control, Records, Execution, Messaging, Extensions, Lens, Policy, Interfaces, Tokenizers, and Resolvers. All tokenizers implement IContractV2 for uniform state and action discovery, and ITokenParty for cross-standard token holder detection.
graph TB
subgraph L11["Layer 11: Resolvers"]
BR["BaseResolver"]
ADR["ADRResolverV3"]
AAA["AAAResolverV1"]
end
subgraph L10["Layer 10: Tokenizers"]
ERC721["ERC-721 Tokenizers<br/>(Ownership, Escrow, Soulbound, ...)"]
ERC1155["ERC-1155 Tokenizers<br/>(Agreement, MultiParty, Royalty, ...)"]
ERC20["ERC-20 Tokenizers<br/>(Fractional, Governance, Shares, ...)"]
end
subgraph L9["Layer 9: Interfaces"]
ICV2["IContractV2"]
ITP["ITokenParty"]
IT2["ITokenizerV2"]
IR2["IResolverV2"]
end
subgraph L8["Layer 8: Policy"]
ATP["AttestationTransferPolicy"]
TP["TransitionPolicy"]
end
subgraph L7["Layer 7: Lens"]
LENS["IntegraLens"]
end
subgraph L6["Layer 6: Extensions"]
RE["RecordExtension (abstract base)"]
BB["Building Blocks<br/>(CustodyChain, DataCommitment,<br/>FundsCustody, PaymentLedger)"]
TB["Token Bindings<br/>(TokenBinding721/1155/20)"]
TE["Diamond Resolvers<br/>(TokenizedExtension721/1155/20)"]
CE["Concrete Extensions<br/>(Ownership, Escrow, Approval)"]
end
subgraph L5["Layer 5: Messaging"]
SIG["IntegraSignalV1"]
MSG["IntegraMessageV1"]
end
subgraph L4["Layer 4: Execution"]
EXEC["IntegraExecutorV1"]
FWD["IntegraForwarder"]
end
subgraph L3["Layer 3: Records"]
REC["IntegraRecordV1"]
end
subgraph L2["Layer 2: Access Control"]
AAC["AttestationAccessControlV1<br/>(abstract)"]
EAS["EASAttestationProviderV1"]
end
subgraph L1["Layer 1: Core"]
EXIST["IntegraExistenceV1"]
REG["IntegraRegistryV1"]
LED["IntegraLedgerV1"]
CAP["CapabilityNamespaceV1"]
end
%% Vertical dependencies
REC -->|"immutable ref"| EXIST
REC -->|"immutable ref"| REG
AAC -->|"immutable ref"| REG
AAC -->|"immutable ref"| CAP
EAS -->|"immutable ref"| CAP
EAS -->|"immutable ref"| REC
ERC721 -->|"inherits"| AAC
ERC1155 -->|"inherits"| AAC
ERC20 -->|"inherits"| AAC
ERC721 -->|"immutable ref"| REC
ERC1155 -->|"immutable ref"| REC
ERC20 -->|"immutable ref"| REC
ADR -->|"immutable ref"| REC
AAA -->|"immutable ref"| REC
SIG -->|"immutable ref"| REC
LENS -->|"immutable ref"| REC
LENS -->|"immutable ref"| REG
RE -->|"immutable ref"| REC
RE -->|"inherits"| AAC
ATP -->|"immutable ref"| REC
FWD -.->|"trusted forwarder"| REC
FWD -.->|"trusted forwarder"| ERC721
FWD -.->|"trusted forwarder"| ADR
ERC721 -.->|"implements"| ICV2
ERC1155 -.->|"implements"| ICV2
ERC20 -.->|"implements"| ICV2
ERC721 -.->|"implements"| ITP
ERC1155 -.->|"implements"| ITP
ERC20 -.->|"implements"| ITP
ADR -.->|"implements"| ICV2
AAA -.->|"implements"| ICV2
LENS -.->|"calls"| ICV2
ATP -.->|"implements"| ICV2The layers, bottom to top:
| # | Layer | Purpose |
|---|---|---|
| 1 | Core | Immutable proof-of-existence, capability definitions, registries, and protocol directory |
| 2 | Access Control | Attestation-gated capability verification through provider abstraction |
| 3 | Records | Document ownership, metadata, tokenizer/resolver binding, and policy resolver attachment |
| 4 | Execution | Gas abstraction and ERC-2771 meta-transaction forwarding |
| 5 | Messaging | Encrypted payment requests and ZK-gated workflow messages between token holders |
| 6 | Extensions | Composable building blocks for domain-specific record logic |
| 7 | Lens | Stateless view composition across all contracts attached to a record |
| 8 | Policy | Transfer authorization policies for tokenized records |
| 9 | Interfaces | Universal introspection: IContractV2, ITokenParty, ITokenizerV2, IResolverV2 |
| 10 | Tokenizers | 23 token contracts representing document rights across ERC-721, ERC-1155, and ERC-20 |
| 11 | Resolvers | Pluggable on-chain logic for dispute resolution, compliance, and lifecycle management |
Layer Descriptions
Layer 1: Core
Four contracts that form the foundation of the protocol. Every other layer depends on at least one core contract.
IntegraExistenceV1
Source: src/registry/IntegraExistenceV1.sol
A minimal, write-once registry for content hash proof-of-existence. Has no admin functions, no access control, and no upgradeability. Once a content hash is registered, it cannot be re-registered or modified. First registration wins.
Key properties:
- Permissionless -- any address can register a content hash.
- Write-once -- subsequent registrations of the same hash revert with
AlreadyExists. - Batch support --
registerBatch()skips already-registered hashes (no revert on duplicates within a batch). - No dependencies -- no imports beyond Solidity itself.
function register(bytes32 contentHash) external returns (uint64 timestamp);
function exists(bytes32 contentHash) external view returns (bool);IntegraLedgerV1
Source: src/core/registries/IntegraLedgerV1.sol
An on-chain directory of all Integra protocol contracts on a given chain. Governance-controlled (GOVERNOR_ROLE). Supports both bytes32 key lookups (gas-efficient) and human-readable string name lookups. The key array is append-only, ensuring stable indices.
The ledger stores code-hash integrity tracking (codeHashes), EAS attestation UIDs (attestationUids), and IPFS metadata URIs (metadataUris) for every registered contract. getContractInfo() returns 8 fields and getAllContractsWithMetadata() returns 9 parallel arrays.
function getContract(string calldata name) external view returns (address);
function getAddress(bytes32 key) external view returns (address);
function getContractInfo(string calldata name) external view returns (...);IntegraRegistryV1
Source: src/core/registries/IntegraRegistryV1.sol
A permissionless component registry with publisher sovereignty. Any address can register a provider, verifier, resolver, tokenizer, or policy contract by paying a fee. Publishers retain full control over their records (activate/deactivate, update metadata). Governance is limited to economic parameters (fee amounts, fee recipient).
The RegistryType enum includes five component types, with POLICY (value 4) supporting transfer policy resolvers.
Components are categorized by RegistryType:
| Type | Description |
|---|---|
PROVIDER | Attestation providers (EAS, VC, ZK, DID) |
VERIFIER | ZK proof verifiers (Groth16, PLONK) |
RESOLVER | Record resolvers (lifecycle, compliance) |
TOKENIZER | Token implementations (ERC-721, ERC-1155, ERC-20) |
POLICY | Transfer policy resolvers (attestation-gated transfers) |
Trust differentiation is handled via EAS attestations, not registry approval. The registry captures the extcodehash at registration time and validates it on every getComponent() call -- if the code at the address changes, the component is silently treated as invalid.
function registerComponent(address, RegistryType, bytes32, string calldata, address)
external payable returns (bytes32 componentId);
function getComponent(bytes32 componentId) external view returns (address);
// Returns address(0) if inactive, not found, or code hash changedCapabilityNamespaceV1
Source: src/core/capabilities/CapabilityNamespaceV1.sol
A permanent, non-upgradeable definition of capability bit positions. Deployed once per chain. All contracts that check capabilities reference these same bit positions, ensuring consistent meaning across the entire protocol and across time.
The 256-bit namespace is organized into tiers:
| Bits | Tier | Examples |
|---|---|---|
| 0--7 | Core | CORE_VIEW, CORE_CLAIM, CORE_TRANSFER, CORE_UPDATE, CORE_ADMIN |
| 8--15 | Record Operations | DOC_SIGN, DOC_WITNESS, DOC_NOTARIZE, DOC_VERIFY, DOC_AMEND |
| 16--23 | Financial | FIN_REQUEST_PAYMENT, FIN_APPROVE_PAYMENT, FIN_EXECUTE_PAYMENT |
| 24--31 | Governance | GOV_PROPOSE, GOV_VOTE, GOV_EXECUTE, GOV_VETO |
| 32--255 | Reserved | Reserved for future protocol extensions |
Pre-composed role templates combine capabilities into common profiles:
ROLE_VIEWER = CORE_VIEW
ROLE_PARTICIPANT = CORE_VIEW | CORE_CLAIM | CORE_TRANSFER | FIN_REQUEST_PAYMENT
ROLE_MANAGER = ROLE_PARTICIPANT | CORE_UPDATE | FIN_APPROVE_PAYMENT | DOC_SIGN | DOC_WITNESS
ROLE_ADMIN = (1 << 128) - 1 // all bits 0-127The CORE_ADMIN bit (bit 7) acts as a superuser flag -- when set, hasCapability() returns true for any required capability.
Layer 2: Access Control
AttestationAccessControlV1
Source: src/core/access/AttestationAccessControlV1.sol
An abstract base contract that gates operations behind attestation-verified capabilities. All tokenizers and extensions inherit from this contract. It holds immutable references to CapabilityNamespaceV1 and IntegraRegistryV1, and supports multiple attestation systems through the IAttestationProvider interface.
The core verification flow:
- Determine the provider for the record (
recordProvider[contentHash]ordefaultProviderId). - Look up the provider address from
IntegraRegistryV1. - Call
IAttestationProvider.verifyCapabilities()on the provider. - Check that granted capabilities satisfy the required capability via
CapabilityNamespaceV1.hasCapability().
This contract also implements progressive ossification and a custom transient storage reentrancy guard (see Security).
modifier requiresCapability(
bytes32 contentHash,
uint256 requiredCapability,
bytes calldata attestationProof
);EASAttestationProviderV1
Source: src/core/providers/EASAttestationProviderV1.sol
The reference IAttestationProvider implementation backed by the Ethereum Attestation Service. Performs a 13-step verification pipeline including:
- Cross-chain replay prevention (validates
sourceChainId) - EAS contract verification (validates
sourceEASContract) - Record contract binding (validates
recordContractmatches this deployment) - Schema version validation (
INTEGRA_CAPABILITY_V1.0.0) - Attestation expiration and revocation checks
- Issuer authorization (record owner or owner-delegated issuer)
- Capability bitmask extraction
Holds immutable references to the EAS contract, the access capability schema UID, CapabilityNamespaceV1, and IntegraRecordV1.
Layer 3: Records
IntegraRecordV1
Source: src/registry/IntegraRecordV1.sol
The central contract of the protocol. It manages document ownership, metadata, tokenizer associations, resolver bindings, policy resolver attachment, and executor authorization. Each record is identified by an integraHash (globally unique bytes32).
Registration is a multi-step atomic operation:
- Ensure existence -- registers the
contentHashinIntegraExistenceV1(if not already registered). - Collect fee -- collects the registration fee in ETH or ERC-20.
- Validate inputs -- checks integraHash uniqueness, tokenizer approval, resolver existence, ZK proof for parent references.
- Create record -- stores ownership, content hash, identity extension, reference hash, tokenizer, resolver, and policy resolver.
- Authorize executor -- sets the per-record executor if provided.
- Emit events --
RecordCreated,RegistrationConfirmed,Referenced,TokenizerAssociated,ExecutorAuthorized. - Invoke resolver hooks -- calls
onRegistered()on all attached behavioral resolvers.
Key features:
- Per-record executor -- the owner can delegate operations to a specific executor address per record, enabling gasless workflows.
- Resolver array -- each record can attach up to 10 resolvers, which are notified of lifecycle events.
- Policy resolver -- per-record transfer policy resolver, changeable by the owner, validated against
IntegraRegistryV1. - Resolver locking -- once locked, resolver configuration becomes immutable (with time-limited emergency override).
- Emergency controls -- a separate emergency address has time-limited override powers (180 days from deployment).
- ZK-verified parent references -- when
referenceHashis non-zero, a Groth16 proof is required.
function register(RegistrationParams calldata params, ProofData calldata proof)
external payable returns (bytes32);
function transferOwnership(bytes32 integraHash, address newOwner, string calldata reason)
external;
function setPolicyResolver(bytes32 integraHash, address policyAddress) external;
function authorizeExecutor(bytes32 integraHash, address executor) external;Layer 4: Execution
IntegraExecutorV1
Source: src/execution/IntegraExecutorV1.sol
A gas abstraction layer that enables relayers to execute whitelisted operations on behalf of users. Implements nonce-based replay protection and fine-grained access control through (target, selector) pair whitelisting. Each forwarded call is limited to MAX_GAS_PER_OPERATION = 5,000,000 gas and return data is capped at MAX_RETURN_DATA_SIZE = 10,000 bytes.
IntegraForwarder
Source: src/forwarder/IntegraForwarder.sol
A thin wrapper over OpenZeppelin's ERC2771Forwarder that serves as the single trusted forwarder for all ERC-2771-compatible contracts in the protocol. Uses EIP-712 typed structured data signing with the domain name "IntegraForwarder".
The forwarding flow:
- User signs an EIP-712
ForwardRequestoff-chain. - Relayer calls
execute(request, signature)on the forwarder. - Forwarder validates signature, increments nonce, and forwards the call with the original sender appended to calldata.
- Target contract uses
_msgSender()(fromERC2771Context) to extract the real sender.
Layer 5: Messaging
IntegraSignalV1
Source: src/messaging/IntegraSignalV1.sol
Token-to-token payment request messaging. Enables token holders to send encrypted payment requests to other token holders. Verification is performed against actual token ownership via the record's associated tokenizer. Payloads are encrypted with hybrid encryption (AES-256-GCM session key, per-party key wrapping). EAS attestations provide integrity guarantees for the payload hash.
Features:
- Configurable timeouts (7--365 days) with extension mechanism (up to 180 days cumulative).
- Three lifecycle states:
PENDING,PAID,CANCELLED. - Process correlation via
processHash.
IntegraMessageV1
Source: src/messaging/IntegraMessageV1.sol
A lightweight, event-sourced messaging system for workflow events. Uses Poseidon hashes for ID generation and requires Groth16 ZK proofs for anti-spam protection (proof of knowledge of the processHash). No state is stored on-chain -- all data is emitted as events for off-chain indexing.
Layer 6: Extensions
Source: src/extensions/
Extensions are composable building blocks for domain-specific record logic. They compose small, purpose-built building blocks via Solidity inheritance, keeping each component focused and auditable.
Extensions follow a strict 3-tier inheritance architecture:
Tier 1 -- Base + Building Blocks:
RecordExtension-- abstract base withIContractV2, auth helpers, IntegraRecord bridge, transfer policy bridgeCustodyChain-- physical custody transfer trackingDataCommitment-- hash commitment and encrypted blob storageFundsCustody-- ETH and ERC-20 fund escrowPaymentLedger-- payment obligation tracking
Tier 2 -- Token Bindings + Diamond Resolvers:
TokenBinding721,TokenBinding1155,TokenBinding20-- implementITokenizerV1lifecycle for each ERC standardTokenizedExtension721,TokenizedExtension1155,TokenizedExtension20-- resolve Solidity diamond inheritance betweenRecordExtensionandTokenBinding*
Tier 3 -- Concrete Extensions:
OwnershipExtension-- simplest concrete extension, ERC-721 ownership proofEscrowExtension-- full escrow lifecycle withDataCommitment+FundsCustodyApprovalExtension-- multi-party document approval workflow
Layer 7: Lens
Source: src/lens/IntegraLens.sol
A stateless, permissionless view contract that composes IContractV2 responses from every contract attached to a record -- tokenizer, policy resolver, and resolvers -- into a single RecordView struct. One off-chain eth_call returns the complete state, all available actions with routing information, and transfer policy status.
IntegraLens holds no state of its own. All IContractV2 calls are wrapped in try/catch for fault tolerance -- if a contract does not implement IContractV2 or reverts, the Lens gracefully returns empty state for that source. A single eth_call to IntegraLens provides the complete record state, eliminating the need for off-chain state reconstruction logic.
Layer 8: Policy
Source: src/policy/
The policy layer introduces pluggable transfer restrictions for tokenized records. When a token is transferred peer-to-peer (not minted or burned), the tokenizer's _update hook calls the policy resolver to check whether the transfer is allowed, with a 100,000 gas cap.
The ITransferPolicy interface follows the ERC-1404 pattern:
checkTransfer()-- enforcement function called by tokenizer_update(). Reverts if transfer is not allowed.detectTransferRestriction()-- pre-flight check returning a restriction code for UI display.messageForTransferRestriction()-- human-readable message for a restriction code.
Concrete policies:
- AttestationTransferPolicy -- requires parties to hold valid
CORE_TRANSFERattestations via EAS, with bilateral and recipient-only modes. ImplementsIContractV2for Lens visibility. - TransitionPolicy -- temporary policy for graceful migration between policy resolvers with a deadline-based grace period. Immutable parameters, no admin functions.
Layer 9: Interfaces
Source: src/interfaces/, src/tokenizers/interfaces/, src/resolvers/interfaces/
Four interfaces that provide universal introspection across the protocol:
| Interface | Functions | Purpose |
|---|---|---|
IContractV2 | getRecordState, getAvailableActions, stateSchema | Universal state and action discovery for any contract |
ITokenParty | isTokenHolder | Cross-standard token holder detection |
ITokenizerV2 | ITokenizerV1 + IContractV2 | Composed interface for v1.7 tokenizers |
IResolverV2 | IResolver + IContractV2 | Composed interface for v1.7 resolvers |
IContractV2 is implemented by all tokenizers, both concrete resolvers, all extensions, and AttestationTransferPolicy. ITokenParty is implemented by all tokenizers via BaseTokenizerV1.
ITokenParty provides a single cross-standard query for token holder detection. Resolvers call ITokenParty(tokenizer).isTokenHolder(account) without knowing which token standard the tokenizer uses.
Layer 10: Tokenizers
Source: src/tokenizers/
23 concrete tokenizer contracts that represent different document rights and relationships. All tokenizers share a common inheritance hierarchy rooted at BaseTokenizerV1, which inherits from AttestationAccessControlV1.
Key design properties:
- All tokenizers implement
IContractV2(directly or viaITokenizerV2) for universal state and action discovery. - All tokenizers implement
ITokenPartyviaBaseTokenizerV1for cross-standard holder detection. - Transfer policy enforcement is built into all token-standard base contracts via
ITransferPolicy.checkTransfer()with a 100,000 gas cap. BaseERC20TokenizerV1provides both named and anonymous reservation support directly.
By Token Standard
ERC-721 (BaseERC721TokenizerV1 -- 1 token per record, 8 contracts):
OwnershipTokenizerV1-- transferable single-owner representationDebtTokenizerV1-- debt instrument tokensEscrowTokenizerV1-- escrow-held document rightsInsuranceTokenizerV1-- insurance policy tokensInvoiceTokenizerV1-- tokenized invoicesOptionsTokenizerV1-- option contract tokensSupplyChainTokenizerV1-- supply chain provenanceVaultTokenizerV1-- vault share tokens
Soulbound ERC-721 (BaseSoulboundERC721TokenizerV1 -- non-transferable, 3 contracts):
SoulboundTokenizerV1-- non-transferable identity-bound tokenLicenseTokenizerV1-- license grant tokensMembershipTokenizerV1-- membership access tokens
ERC-1155 (BaseERC1155TokenizerV1 -- multiple tokens per record, 7 contracts):
AgreementTokenizerV1-- three-party agreement recordsBadgeTokenizerV1-- achievement/credential badgeMultiPartyTokenizerV1-- multi-party document participationRentalTokenizerV1-- time-bounded rental accessRoyaltyTokenizerV1-- royalty share distributionSemiFungibleTokenizerV1-- mixed fungible/non-fungible positionsTrustTokenizerV1-- trust relationship tokens
ERC-20 (BaseERC20TokenizerV1 -- fungible shares per record, 5 contracts):
FractionalTokenizerV1-- fractional ownership sharesGovernanceTokenizerV1-- governance voting tokensSecurityTokenTokenizerV1-- regulated security tokensSharesTokenizerV1-- equity share tokensStreamTokenizerV1-- payment stream tokens
Every tokenizer holds an immutable reference to IntegraRecordV1 and validates that the record uses this specific tokenizer before allowing any operation.
Layer 11: Resolvers
Source: src/resolvers/
Resolvers are pluggable on-chain logic modules that attach to records. Each record can bind up to 10 resolvers. Resolvers are organized into 5 categories (identified by bitmask):
| Category | Bitmask | Purpose |
|---|---|---|
| Metadata | 1 << 0 | Read-heavy, describes the record |
| Gatekeeper | 1 << 1 | Blocking validation (can the transfer happen?) |
| Behavioral | 1 << 2 | Stateful lifecycle hooks and actions |
| Automation | 1 << 3 | Trigger conditions, permissionless evaluation |
| Integration | 1 << 4 | External system bridges |
All resolvers inherit from BaseResolver, which provides ERC2771 meta-transaction support, AccessControl, Pausable, ReentrancyGuard, and the _isRecordParty() helper for party detection across owner, executor, and token holder (via ITokenParty).
All resolvers inherit directly from BaseResolver and implement the category interfaces they need (flat inheritance model).
Concrete Resolvers
ADRResolverV3 -- Alternative Dispute Resolution. A multi-category resolver (Behavioral + Gatekeeper + Automation) implementing a full arbitration lifecycle: arbitration clause anchoring, 11-state dispute state machine, evidence via hash anchors, multi-party settlement, deadline management, automated triggers (payment expiry, attestation expiry, custom evaluators), and EAS attestation integration. Implements IContractV2.
AAAResolverV1 -- Agreement-bound dispute resolution. Structurally similar to ADRResolverV3 but replaces the ArbitrationClause prerequisite with agreement binding via AgreementTokenizerV1. Disputes can be initiated when all three agreement tokens are claimed. Introduces the "commercial party" concept for settlement access control. Implements IContractV2.
Support Libraries
| Library | Purpose |
|---|---|
StateMachine | Generic state lifecycle enforcement with bitmask-based two-tier transitions |
MultiPartyConfirmation | Quorum-based confirmation with nonce replay protection and cooldowns |
HashAnchor | Append-only hash-based proof-of-existence list |
DeadlineTracker | Time-based deadline management with absolute and relative modes |
Counter | Per-record keyed counter management |
PeriodicSchedule | Periodic schedule management for recurring obligations |
ProportionSplit | Basis-point allocation and proportional calculation |
VestingSchedule | Linear vesting calculations |
Contract Inventory
| # | Contract | Layer | Source |
|---|---|---|---|
| 1 | IntegraExistenceV1 | Core | src/registry/IntegraExistenceV1.sol |
| 2 | IntegraLedgerV1 | Core | src/core/registries/IntegraLedgerV1.sol |
| 3 | IntegraRegistryV1 | Core | src/core/registries/IntegraRegistryV1.sol |
| 4 | CapabilityNamespaceV1 | Core | src/core/capabilities/CapabilityNamespaceV1.sol |
| 5 | AttestationAccessControlV1 | Access Control | src/core/access/AttestationAccessControlV1.sol |
| 6 | EASAttestationProviderV1 | Access Control | src/core/providers/EASAttestationProviderV1.sol |
| 7 | IntegraRecordV1 | Records | src/registry/IntegraRecordV1.sol |
| 8 | IntegraExecutorV1 | Execution | src/execution/IntegraExecutorV1.sol |
| 9 | IntegraForwarder | Execution | src/forwarder/IntegraForwarder.sol |
| 10 | IntegraSignalV1 | Messaging | src/messaging/IntegraSignalV1.sol |
| 11 | IntegraMessageV1 | Messaging | src/messaging/IntegraMessageV1.sol |
| 12 | RecordExtension | Extensions | src/extensions/RecordExtension.sol |
| 13 | OwnershipExtension | Extensions | src/extensions/OwnershipExtension.sol |
| 14 | CustodyChain | Extensions | src/extensions/CustodyChain.sol |
| 15 | DataCommitment | Extensions | src/extensions/DataCommitment.sol |
| 16 | FundsCustody | Extensions | src/extensions/FundsCustody.sol |
| 17 | PaymentLedger | Extensions | src/extensions/PaymentLedger.sol |
| 18 | TokenBinding721 | Extensions | src/extensions/TokenBinding721.sol |
| 19 | TokenBinding1155 | Extensions | src/extensions/TokenBinding1155.sol |
| 20 | TokenBinding20 | Extensions | src/extensions/TokenBinding20.sol |
| 21 | TokenizedExtension721 | Extensions | src/extensions/TokenizedExtension721.sol |
| 22 | TokenizedExtension1155 | Extensions | src/extensions/TokenizedExtension1155.sol |
| 23 | TokenizedExtension20 | Extensions | src/extensions/TokenizedExtension20.sol |
| 24 | EscrowExtension | Extensions | src/extensions/EscrowExtension.sol |
| 25 | ApprovalExtension | Extensions | src/extensions/ApprovalExtension.sol |
| 26 | IntegraLens | Lens | src/lens/IntegraLens.sol |
| 27 | ITransferPolicy | Policy | src/policy/ITransferPolicy.sol |
| 28 | TransferRestrictionCodes | Policy | src/policy/TransferRestrictionCodes.sol |
| 29 | AttestationTransferPolicy | Policy | src/policy/AttestationTransferPolicy.sol |
| 30 | TransitionPolicy | Policy | src/policy/TransitionPolicy.sol |
| 31 | IContractV2 | Interfaces | src/interfaces/IContractV2.sol |
| 32 | ITokenParty | Interfaces | src/interfaces/ITokenParty.sol |
| 33 | ITokenizerV2 | Interfaces | src/tokenizers/interfaces/ITokenizerV2.sol |
| 34 | IResolverV2 | Interfaces | src/resolvers/interfaces/IResolverV2.sol |
| 35 | BaseTokenizerV1 | Tokenizers | src/tokenizers/abstract/BaseTokenizerV1.sol |
| 36 | BaseERC721TokenizerV1 | Tokenizers | src/tokenizers/abstract/BaseERC721TokenizerV1.sol |
| 37 | BaseSoulboundERC721TokenizerV1 | Tokenizers | src/tokenizers/abstract/BaseSoulboundERC721TokenizerV1.sol |
| 38 | BaseERC1155TokenizerV1 | Tokenizers | src/tokenizers/abstract/BaseERC1155TokenizerV1.sol |
| 39 | BaseERC20TokenizerV1 | Tokenizers | src/tokenizers/abstract/BaseERC20TokenizerV1.sol |
| 40 | OwnershipTokenizerV1 | Tokenizers | src/tokenizers/OwnershipTokenizerV1.sol |
| 41 | DebtTokenizerV1 | Tokenizers | src/tokenizers/DebtTokenizerV1.sol |
| 42 | EscrowTokenizerV1 | Tokenizers | src/tokenizers/EscrowTokenizerV1.sol |
| 43 | InsuranceTokenizerV1 | Tokenizers | src/tokenizers/InsuranceTokenizerV1.sol |
| 44 | InvoiceTokenizerV1 | Tokenizers | src/tokenizers/InvoiceTokenizerV1.sol |
| 45 | OptionsTokenizerV1 | Tokenizers | src/tokenizers/OptionsTokenizerV1.sol |
| 46 | SupplyChainTokenizerV1 | Tokenizers | src/tokenizers/SupplyChainTokenizerV1.sol |
| 47 | VaultTokenizerV1 | Tokenizers | src/tokenizers/VaultTokenizerV1.sol |
| 48 | SoulboundTokenizerV1 | Tokenizers | src/tokenizers/SoulboundTokenizerV1.sol |
| 49 | LicenseTokenizerV1 | Tokenizers | src/tokenizers/LicenseTokenizerV1.sol |
| 50 | MembershipTokenizerV1 | Tokenizers | src/tokenizers/MembershipTokenizerV1.sol |
| 51 | AgreementTokenizerV1 | Tokenizers | src/tokenizers/AgreementTokenizerV1.sol |
| 52 | BadgeTokenizerV1 | Tokenizers | src/tokenizers/BadgeTokenizerV1.sol |
| 53 | MultiPartyTokenizerV1 | Tokenizers | src/tokenizers/MultiPartyTokenizerV1.sol |
| 54 | RentalTokenizerV1 | Tokenizers | src/tokenizers/RentalTokenizerV1.sol |
| 55 | RoyaltyTokenizerV1 | Tokenizers | src/tokenizers/RoyaltyTokenizerV1.sol |
| 56 | SemiFungibleTokenizerV1 | Tokenizers | src/tokenizers/SemiFungibleTokenizerV1.sol |
| 57 | TrustTokenizerV1 | Tokenizers | src/tokenizers/TrustTokenizerV1.sol |
| 58 | FractionalTokenizerV1 | Tokenizers | src/tokenizers/FractionalTokenizerV1.sol |
| 59 | GovernanceTokenizerV1 | Tokenizers | src/tokenizers/GovernanceTokenizerV1.sol |
| 60 | SecurityTokenTokenizerV1 | Tokenizers | src/tokenizers/SecurityTokenTokenizerV1.sol |
| 61 | SharesTokenizerV1 | Tokenizers | src/tokenizers/SharesTokenizerV1.sol |
| 62 | StreamTokenizerV1 | Tokenizers | src/tokenizers/StreamTokenizerV1.sol |
| 63 | BaseResolver | Resolvers | src/resolvers/base/BaseResolver.sol |
| 64 | ADRResolverV3 | Resolvers | src/resolvers/adr/ADRResolverV3.sol |
| 65 | AAAResolverV1 | Resolvers | src/resolvers/adr/AAAResolverV1.sol |
Key Design Patterns
Immutable References
All inter-contract references are set at construction time and stored as immutable state variables. This eliminates an entire class of admin-key attacks -- there is no setRecord() or setRegistry() function that could redirect a tokenizer to a malicious record contract.
// In BaseTokenizerV1
IntegraRecordV1 public immutable RECORD;
// In AttestationAccessControlV1
CapabilityNamespaceV1 public immutable NAMESPACE;
IntegraRegistryV1 public immutable REGISTRY;
// In IntegraRecordV1
IntegraExistenceV1 public immutable EXISTENCE;
IntegraRegistryV1 public immutable REGISTRY;
// In IntegraLens
IntegraRecordV1 public immutable INTEGRA_RECORD;
IntegraRegistryV1 public immutable REGISTRY;
// In RecordExtension
IntegraRecordV1 public immutable RECORD;The tradeoff is that upgrading a referenced contract requires deploying a new version of the referencing contract. This is intentional -- it forces explicit migration rather than silent reference swaps.
Progressive Ossification
The protocol's governance model follows a progressive ossification path:
BOOTSTRAP --> MULTISIG --> DAO --> OSSIFIED| Stage | Controller | Purpose |
|---|---|---|
BOOTSTRAP | Team EOA | Rapid iteration during launch |
MULTISIG | Guardian multisig (e.g., 3-of-5) | Shared custody, reduced single-point risk |
DAO | On-chain governor (community) | Decentralized governance |
OSSIFIED | Permanently frozen | Governance model locked forever |
AttestationAccessControlV1 accepts a _bootstrapGovernor at construction, granting it DEFAULT_ADMIN_ROLE and GOVERNOR_ROLE. Since DEFAULT_ADMIN_ROLE controls all other roles via OpenZeppelin AccessControl, governance transitions are performed by revoking roles from the current controller and granting them to the next entity. Ossification freezes the governance model (who governs) but does not freeze operational management -- the controlling entity retains the ability to rotate operator keys, update providers, and manage roles.
Zero-Trust Executor Access
Tokenizer operations use a zero-trust model with no global fallback roles:
- Owner -- always has full access to their records.
- Per-record executor -- a specific address authorized by the owner for a specific record.
- No one else -- there is no global admin, no protocol-wide operator, and no fallback that could bypass per-record authorization.
The requireOwnerOrExecutor modifier validates this on every operation:
modifier requireOwnerOrExecutor(bytes32 integraHash) {
(address registeredTokenizer, address owner, address authorizedExecutor) =
RECORD.getAccessInfo(integraHash);
// Must be THIS tokenizer
if (registeredTokenizer != address(this)) revert WrongTokenizer(...);
// Must be owner OR authorized executor
if (_msgSender() != owner && _msgSender() != authorizedExecutor) revert Unauthorized(...);
_;
}IContractV2 Universal Introspection
All protocol contracts -- tokenizers, resolvers, extensions, and policy contracts -- implement three uniform functions:
getRecordState(integraHash)-- ABI-encoded domain state snapshotgetAvailableActions(integraHash, caller)-- function selectors the caller can invokestateSchema()-- schema identifier string for decoding state
This eliminates the need for per-contract adapters in the service layer. IntegraLens composes these responses into a single RecordView for off-chain consumption.
Extension Composition
Extensions provide composable building blocks for domain-specific record logic:
- Building blocks (50-150 lines each) provide a single capability (custody, commitments, fund management).
- Token bindings implement
ITokenizerV1lifecycle for each ERC standard. - Diamond resolvers handle Solidity diamond inheritance between
RecordExtensionandTokenBinding*. - Concrete extensions compose building blocks and inherit from a diamond resolver or directly from
RecordExtension.
Capability Bitmask Access Control
Instead of role-based access (where you either have a role or you do not), the protocol uses a 256-bit capability bitmask. Each bit represents a specific permission. Capabilities can be composed, delegated, and verified through attestations.
if (!NAMESPACE.hasCapability(grantedCapabilities, requiredCapability)) {
revert NoCapability(user, contentHash, requiredCapability);
}ERC-2771 Meta-Transactions for Gasless Operation
All user-facing contracts inherit ERC2771Context and accept the IntegraForwarder as their trusted forwarder. Users sign EIP-712 typed data off-chain; a relayer submits the signed request on-chain. All contracts resolve the diamond inheritance between Context and ERC2771Context in favor of ERC2771Context.
Provider Abstraction
Attestation verification is decoupled from specific attestation systems through the IAttestationProvider interface. The reference implementation uses EAS, but the protocol can support Verifiable Credentials, ZK proofs, DID-based systems, or any other attestation mechanism by deploying a new provider and registering it in IntegraRegistryV1.
Providers can be configured at two levels:
- Default provider -- applies to all records unless overridden.
- Per-record provider -- overrides the default for a specific record.
Data Flow
The lifecycle of a document in the Integra protocol follows this path:
sequenceDiagram
participant User
participant Existence as IntegraExistenceV1
participant Record as IntegraRecordV1
participant Registry as IntegraRegistryV1
participant Tokenizer as Tokenizer (ERC-721/1155/20)
participant Provider as EASAttestationProviderV1
participant Policy as ITransferPolicy
participant Lens as IntegraLens
participant Resolver as Resolver
Note over User: 1. Existence Registration
User->>Existence: register(contentHash)
Existence-->>User: timestamp
Note over User: 2. Record Creation
User->>Record: register(params, proof)
Record->>Existence: _ensureExists(contentHash)
Record->>Registry: validate tokenizer is registered
Record->>Record: store record (owner, tokenizer, resolvers, policyResolver)
Record->>Resolver: onRegistered() lifecycle hook
Note over User: 3. Token Reservation
User->>Tokenizer: reserveTokenAnonymous(integraHash, ...)
Tokenizer->>Record: getAccessInfo(integraHash)
Tokenizer->>Tokenizer: validate owner/executor authorization
Tokenizer->>Tokenizer: reserve token slot
Note over User: 4. Token Claim (attestation-gated)
User->>Tokenizer: claimToken(integraHash, tokenId, attestationUID)
Tokenizer->>Provider: verifyCapabilities(proof, user, contentHash, capability)
Provider->>Provider: 13-step EAS verification
Provider-->>Tokenizer: (verified, grantedCapabilities)
Tokenizer->>Tokenizer: mint token to claimant
Note over User: 5. Transfer (policy-gated)
User->>Tokenizer: safeTransferFrom(from, to, tokenId)
Tokenizer->>Record: getPolicyResolver(integraHash)
Tokenizer->>Policy: checkTransfer(integraHash, from, to, amount)
Tokenizer->>Tokenizer: execute transfer
Note over User: 6. State Query (via Lens)
User->>Lens: getRecordView(integraHash, caller)
Lens->>Record: lookup tokenizer, resolvers, policyResolver
Lens->>Tokenizer: getRecordState(), getAvailableActions()
Lens->>Resolver: getRecordState(), getAvailableActions()
Lens-->>User: RecordView (composed state + actions)Solidity Version and Dependencies
| Dependency | Version |
|---|---|
| Solidity | 0.8.28 (exact, not a range) |
| OpenZeppelin Contracts | v5.x |
| EAS (external) | Ethereum Attestation Service |
| ERC Standards | ERC-20, ERC-721, ERC-1155, ERC-2771, ERC-2612, ERC-4906, ERC-5192, ERC-165 |
All contracts use pragma solidity 0.8.28; (not ^0.8.28), ensuring deterministic compilation. OpenZeppelin v5.x provides:
AccessControl-- role-based admin permissionsReentrancyGuardTransient-- transient-storage-based reentrancy protection (EIP-1153)Pausable-- emergency stop mechanismERC2771Context/ERC2771Forwarder-- meta-transaction supportERC721,ERC1155,ERC20,ERC20Permit,ERC20Votes-- token standardsSafeERC20-- safe token transfer wrappers
No contracts use proxy patterns or upgradeability. Every contract is deployed as a standalone, immutable instance.
Security
Integra v1.7 implements a defense-in-depth security architecture across its production Solidity 0.8.28 contracts. The protocol's security philosophy rests on five principles:
-
Attestation-gated access. Every state-mutating operation on a record requires a cryptographically verified attestation from an authorized issuer. There are no backdoor admin overrides that bypass attestation verification.
-
Progressive ossification. Governance authority follows a one-way ratchet from team control to permanent immutability (BOOTSTRAP -> MULTISIG -> DAO -> OSSIFIED). Each transition is irreversible and narrows the set of actors who can modify governance parameters.
-
Immutability where it matters. Core references (EAS contract, capability namespace, registry) are set at construction time as Solidity
immutablevalues. The existence registry (IntegraExistenceV1) is write-once with no admin functions. Contract bytecode hashes are captured at registration and verified on every lookup. -
Zero-trust execution. The executor (
IntegraExecutorV1) operates on an explicit allowlist model -- target contracts AND function selectors must both be whitelisted. There are no global fallback roles. Per-record executors are scoped to individual records, not granted broad authority. -
Non-upgradeability by design. All contracts are non-upgradeable. There are no proxy patterns, no
delegatecallto mutable implementations, and no storage layout concerns. Security comes from immutability; evolution comes from deploying new contracts and migrating records.
The protocol builds on OpenZeppelin v5.x for battle-tested primitives: AccessControl for role management, ReentrancyGuardTransient for EIP-1153 transient storage reentrancy protection, Pausable for emergency stops, SafeERC20 for token transfers, and ERC2771Context for meta-transaction support.
Attestation Verification
EASAttestationProviderV1 implements a 13-step verification chain that validates every attestation before granting capabilities. Each step targets a specific attack vector.
Step 1: Attestation Retrieval
attestation = EAS.getAttestation(attestationUID);Fetches the attestation struct from the Ethereum Attestation Service contract. This is the raw data source for all subsequent checks.
Step 2: Existence Check
if (attestation.uid == bytes32(0)) {
revert AttestationNotFound(attestationUID);
}Prevents: Fabricated attestation UIDs. An attacker cannot submit a random bytes32 value and have it accepted -- the attestation must exist on-chain in the EAS contract.
Step 3: Revocation Check
if (attestation.revocationTime != 0) {
revert AttestationRevoked(attestationUID, attestation.revocationTime);
}Prevents: Use of revoked credentials. When an issuer revokes an attestation (e.g., after a key compromise or permission change), this step ensures the revoked attestation is immediately rejected. The check is first-class -- it runs before any data decoding.
Step 4: Expiration Check
if (attestation.expirationTime != 0 && attestation.expirationTime <= block.timestamp) {
revert AttestationExpired(attestationUID, attestation.expirationTime);
}Prevents: Use of expired credentials. Attestations can carry an expiration timestamp. Zero means no expiration. This prevents stale attestations from granting perpetual access after their intended validity window.
Step 5: Schema Validation
if (attestation.schema != ACCESS_CAPABILITY_SCHEMA) {
revert InvalidSchema(attestation.schema, ACCESS_CAPABILITY_SCHEMA);
}Prevents: Schema confusion attacks. An attacker might create an attestation using a different EAS schema that happens to decode to plausible-looking data. By pinning the schema UID at construction time, only attestations created under Integra's specific capability schema are accepted.
Step 6: Recipient Binding (Front-Running Protection)
if (attestation.recipient != user) {
revert InvalidRecipient(attestation.recipient, user);
}Prevents: Attestation theft and front-running. An attacker who observes a valid attestation UID in the mempool cannot use it -- the attestation is bound to a specific recipient address. This is the primary front-running defense for attestation-gated operations.
Step 7: Issuer Authorization
address issuer = _getActiveIssuer(contentHash);
if (attestation.attester != issuer) {
revert InvalidIssuer(attestation.attester, issuer);
}Prevents: Unauthorized attestation issuance. Only the explicitly authorized issuer for each record can create valid attestations. The _getActiveIssuer() function checks revocation status first -- if the issuer has been revoked, it returns address(0), causing all attestations from that issuer to fail immediately.
Step 8: Chain ID Validation (Cross-Chain Replay Prevention)
if (sourceChainId != block.chainid) {
revert WrongChain(block.chainid, sourceChainId);
}Prevents: Cross-chain replay attacks. An attestation created on Ethereum mainnet cannot be replayed on Arbitrum, Base, or any other chain. The chain ID is embedded in the attestation data and verified against the current block.chainid.
Step 9: EAS Contract Verification (EAS Spoofing Prevention)
if (sourceEASContract != address(EAS)) {
revert WrongEASContract(address(EAS), sourceEASContract);
}Prevents: EAS contract spoofing. An attacker could deploy a malicious EAS contract that returns fabricated attestation data. By verifying that the attestation was created on the specific EAS contract instance that the provider trusts, this step ensures attestation data integrity.
Step 10: Record Contract Binding (Contract Spoofing Prevention)
if (recordContract != msg.sender) {
revert WrongRecordContract(msg.sender, recordContract);
}Prevents: Cross-contract attestation reuse. An attestation issued for TokenizerA cannot be replayed against TokenizerB. The attestation data embeds the address of the specific contract it was issued for, and that address must match msg.sender (the tokenizer contract calling the provider). Note: msg.sender is correct here because tokenizers inherit from AttestationAccessControlV1, making msg.sender the tokenizer contract itself.
Step 11: Schema Version Validation
if (attestationVersion != SCHEMA_VERSION) {
revert InvalidSchemaVersion(attestationVersion, SCHEMA_VERSION);
}Prevents: Schema version mismatch. As the protocol evolves, the attestation schema may change. This step ensures attestations created under older schema versions are not accidentally accepted by newer provider versions, preventing data interpretation mismatches.
Step 12: Content Hash Binding
if (attestedHash != contentHash) {
revert WrongRecord(attestedHash, contentHash);
}Prevents: Cross-record attestation reuse. An attestation issued for Record A cannot be used to authorize operations on Record B. The content hash (integraHash) in the attestation data must match the record being operated on.
Step 13: Attestation Age Validation (Optional)
if (maxAttestationAge > 0) {
if (issuedAt > block.timestamp) {
revert InvalidIssuedAtTimestamp(issuedAt, block.timestamp);
}
uint256 age;
unchecked { age = block.timestamp - issuedAt; }
if (age > maxAttestationAge) {
revert AttestationTooOld(issuedAt, maxAttestationAge);
}
}Prevents: Use of stale attestations and future-dated timestamps. When enabled (configurable by the governor, bounded between 1 hour and 365 days), this step enforces attestation freshness. It also rejects attestations with issuedAt timestamps in the future, which would indicate manipulation.
Access Control
Capability-Based Bitmask Permissions
CapabilityNamespaceV1 defines a 256-bit capability bitmask where each bit position represents a specific permission. Bit positions are permanent -- once defined, a bit position's meaning never changes across all contracts and time periods.
The namespace is organized into four tiers of 8 bits each (bits 0-31):
| Tier | Bits | Capabilities |
|---|---|---|
| Core | 0-7 | VIEW, CLAIM, TRANSFER, UPDATE, DELEGATE, REVOKE, RESERVED, ADMIN |
| Record (DOC) | 8-15 | SIGN, WITNESS, NOTARIZE, VERIFY, AMEND, ARCHIVE, RESERVED x2 |
| Financial (FIN) | 16-23 | REQUEST_PAYMENT, APPROVE_PAYMENT, EXECUTE_PAYMENT, CANCEL_PAYMENT, WITHDRAW, DEPOSIT, RESERVED x2 |
| Governance (GOV) | 24-31 | PROPOSE, VOTE, EXECUTE, VETO, DELEGATE_VOTE, RESERVED x3 |
The CORE_ADMIN bit (bit 7) acts as a superuser flag -- when set, hasCapability() returns true for any required capability:
function hasCapability(uint256 granted, uint256 required) external pure returns (bool) {
return ((granted & CORE_ADMIN) != 0) || ((granted & required) == required);
}Capabilities are composed via bitwise OR and checked via bitwise AND. This allows a single attestation to grant multiple permissions simultaneously, and a single check to require multiple permissions.
Progressive Ossification (Governance Model)
The protocol's governance model is designed around a progressive ossification path:
BOOTSTRAP -> MULTISIG -> DAO -> OSSIFIEDThis is an operational governance model, not an on-chain state machine. AttestationAccessControlV1 accepts a _bootstrapGovernor address at construction, which receives DEFAULT_ADMIN_ROLE and GOVERNOR_ROLE. Governance transitions are performed by revoking roles from the current controller and granting them to the next:
| Stage | Controller | How to Transition |
|---|---|---|
| Bootstrap | Team EOA | Revoke team roles, grant to multisig |
| Multisig | Guardian multisig (e.g., 3-of-5) | Revoke multisig roles, grant to DAO |
| DAO | On-chain governor (community) | Freeze role assignments |
Since DEFAULT_ADMIN_ROLE controls all other roles via OpenZeppelin AccessControl, role transfers between stages are straightforward admin operations. The model ensures governance stability for legal record tokens while allowing the controlling entity to rotate compromised operator keys, update providers, and manage operational roles.
Zero-Trust Executor Model
IntegraRecordV1 implements per-record executor authorization rather than broad role-based access:
mapping(bytes32 => address) public recordExecutor;Each record has at most one authorized executor. Executors are explicitly bound to specific records -- there is no global "executor" role that can operate on any record. Self-authorization is prevented:
if (executor == _msgSender()) revert CannotAuthorizeSelf(_msgSender());On ownership transfer, the executor is automatically revoked (zero-trust model). The new owner must explicitly re-authorize an executor.
Role-Based Access in Tokenizers and Resolvers
Tokenizers use OpenZeppelin AccessControl with three tiers (inherited from AttestationAccessControlV1):
- GOVERNOR_ROLE: System-wide configuration (provider management, fee parameters, pause/unpause)
- OPERATOR_ROLE: Operational management (issuer configuration, batch operations)
- EXECUTOR_ROLE: Record-level operations (whitelisted contract calls, record state changes)
Resolvers use a simpler model with only GOVERNOR_ROLE (for pause/unpause and configuration) and DEFAULT_ADMIN_ROLE (for role management).
Roles are enforced via the onlyRole modifier. The DEFAULT_ADMIN_ROLE follows the governance stage -- it transfers from bootstrap governor to multisig to DAO during progressive ossification.
Replay Protection
Role-Based Replay Prevention in IntegraExecutorV1
IntegraExecutorV1 prevents unauthorized replay through EXECUTOR_ROLE access control. Only addresses granted the executor role can call executeOperation(). Since each operation is a direct call (not a signed meta-transaction), replay is prevented by the standard Ethereum transaction model -- each transaction has a unique nonce at the protocol level. The role check ensures that only authorized relayers can submit operations, and the (target, selector) whitelist ensures they can only call approved functions.
Chain ID Validation in EASAttestationProviderV1
Step 8 of the 13-step verification (described above) prevents cross-chain replay by comparing sourceChainId against block.chainid. An attestation created on chain 1 (Ethereum mainnet) cannot be used on chain 42161 (Arbitrum).
Source EAS Contract Verification
Step 9 prevents EAS contract spoofing. Even on the same chain, an attestation from a different EAS deployment (e.g., a test instance or malicious clone) is rejected.
Contract Address Binding
Step 10 binds attestations to the specific calling contract. Even if an attacker deploys a contract at the same address on a different chain (CREATE2), the chain ID check (Step 8) would catch it.
Reentrancy Protection
ReentrancyGuardTransient on Critical Contracts
All state-mutating contracts inherit OpenZeppelin's ReentrancyGuardTransient, which uses EIP-1153 transient storage (TSTORE/TLOAD) for gas-efficient reentrancy guards. Transient storage is automatically cleared at the end of each transaction, eliminating the warm/cold storage cost differential of traditional reentrancy guards.
The nonReentrant modifier is applied to all state-mutating external functions on:
IntegraExecutorV1(operation execution)IntegraRecordV1(record management)- All tokenizer contracts (reserve, claim, cancel, transfer operations)
- All resolver contracts (state mutations)
- All extension contracts (building block operations)
- Token binding contracts (
TokenBinding721,TokenBinding1155,TokenBinding20)
Custom Transient Storage Guard in AttestationAccessControlV1
AttestationAccessControlV1 implements a custom reentrancy guard that shares the same transient storage slot as OpenZeppelin's ReentrancyGuardTransient:
function _setReentrancyGuard(bool entered) private {
assembly {
tstore(0x9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00, entered)
}
}This design solves a specific problem: the requiresCapability modifier makes external calls to attestation providers before the function body executes. A standard nonReentrant modifier would only set the guard after modifier execution, leaving the external provider call unprotected.
By using OpenZeppelin's exact ERC-7201 namespaced storage slot, the custom guard and OpenZeppelin's nonReentrant share state. This means:
_reentrancyGuardEntered()(inherited from OpenZeppelin) reads the same slot this function writes to- Cross-function reentrancy between
requiresCapability-guarded functions andnonReentrant-guarded functions is properly detected - A reentrant call from a malicious attestation provider into any protected function on the same contract will revert
The requiresCapabilityWithUID modifier provides the same protection for single-value proof variants.
Gas Limiting
Executor Gas Caps
IntegraExecutorV1 enforces two gas-related safety limits:
MAX_GAS_PER_OPERATION = 5,000,000-- each forwarded call to a target contract is limited to 5M gas. This prevents a single operation from consuming an entire block's gas limit, which could be used as a griefing vector against the relayer.MAX_RETURN_DATA_SIZE = 10,000-- return data is capped at 10,000 bytes. This prevents a malicious target contract from returning an enormous payload that inflates gas costs for the executor transaction (since return data is copied to memory).
Transfer Policy Gas Cap
All token-standard base contracts (BaseERC721TokenizerV1, BaseERC1155TokenizerV1, BaseERC20TokenizerV1) call ITransferPolicy.checkTransfer() with a 100,000 gas cap during the _update hook. This prevents a malicious or buggy policy resolver from consuming unbounded gas during token transfers. If the policy call exceeds the gas limit, the transfer reverts.
ITokenParty Gas Limit
When resolvers call ITokenParty(tokenizer).isTokenHolder(account) for party detection via _isRecordParty() in BaseResolver, the call is made with an explicit 100,000 gas cap (_TOKEN_HOLDER_GAS_LIMIT):
try ITokenParty(tokenizer).isTokenHolder{gas: _TOKEN_HOLDER_GAS_LIMIT}(account) returns (bool holds) {If the isTokenHolder call exceeds the gas limit or reverts for any reason, the try/catch block handles the failure gracefully (the account is not treated as a token holder). This prevents a malicious or buggy tokenizer from consuming unbounded gas during party detection.
Resolver Gas Limits
IntegraRecordV1 calls resolver lifecycle hooks (onRegistered, onTransferred, onTokenizerAssociated) with gas-limited, non-blocking calls. A failing resolver does not block the operation -- it emits a ResolverNotificationFailed event. Gatekeeper resolver validation via canOwn() is also gas-bounded; a broken or unavailable resolver is skipped with a ResolverValidationFailed event.
Condition Evaluator Gas Cap
The ADR and AAA resolvers call custom IConditionEvaluator implementations with a gas cap of EVALUATOR_GAS_LIMIT = 50,000. This prevents griefing via expensive or infinite-loop evaluators.
Transfer Policy Security
Enforcement vs. Pre-Flight Separation
The ITransferPolicy interface separates enforcement from pre-flight validation:
checkTransfer()is called on-chain by the tokenizer during every peer-to-peer transfer. It must revert if the transfer is not allowed. It is called as astaticcall(no state changes).detectTransferRestriction()is called off-chain by wallets and UIs before submitting a transaction. It returns a restriction code without reverting.
This separation ensures that on-chain enforcement cannot be bypassed by pre-flight results, and that pre-flight checks do not modify state.
Scope Limitation
Mint (from == address(0)) and burn (to == address(0)) are NOT subject to transfer policy. Minting is gated by the attestation/capability system via claimToken. Only peer-to-peer transfers (from != 0 && to != 0) invoke the policy resolver. This prevents policy contracts from blocking protocol-level operations.
Policy Resolver Validation
Policy resolvers must be registered and active in IntegraRegistryV1 before they can be attached to a record via setPolicyResolver(). The registry's code-hash integrity check ensures the policy contract has not been redeployed behind a proxy. Setting the policy resolver to address(0) removes the policy (unrestricted transfers).
TransitionPolicy Design
The TransitionPolicy contract is designed for safe migration between policy resolvers. It has:
- Immutable parameters -- predecessor, successor, and deadline are set at construction and cannot be changed.
- No admin functions -- there is no governor, no pause, no configuration.
- Self-limiting behavior -- during the grace period, it allows transfers if either the predecessor or successor policy allows them. After the deadline, only the successor is checked.
Resolver Validation
Fail-Open vs. Fail-Closed Design
Resolver interactions follow a deliberate fail-open / fail-closed split:
Fail-open (non-blocking notifications): Lifecycle hooks (onRegistered, onTransferred, onTokenizerAssociated) are called with gas-limited, non-blocking calls. If a resolver reverts or runs out of gas, the operation proceeds and a ResolverNotificationFailed event is emitted. This prevents a broken resolver from permanently blocking record operations.
Fail-closed (blocking validation): Gatekeeper resolvers (canOwn, canTokenize) can actively reject operations by returning (false, reason). An active rejection (the call succeeds and the resolver returns false) reverts the transfer. However, if the resolver itself reverts or is unavailable, the validation is skipped with a ResolverValidationFailed event -- this is a fail-open for resolver failures, fail-closed for resolver rejections.
This design balances safety (resolvers can block invalid transfers) with liveness (broken resolvers cannot permanently lock records).
_isRecordParty in BaseResolver
BaseResolver provides _isRecordParty(integraHash, account) for cross-cutting party detection. It checks three conditions:
- Is the account the record owner? (via
INTEGRA_RECORD.getOwner()) - Is the account an authorized executor? (via
INTEGRA_RECORD.isAuthorizedExecutor()) - Is the account a token holder? (via
ITokenParty(tokenizer).isTokenHolder())
This centralizes party detection logic, ensuring consistent behavior across all resolvers.
Emergency Controls
Pause/Unpause Mechanisms
All critical contracts implement OpenZeppelin's Pausable with whenNotPaused guards on state-mutating external functions. Pause authority is restricted to GOVERNOR_ROLE:
function pause() external virtual onlyRole(GOVERNOR_ROLE) { _pause(); }
function unpause() external virtual onlyRole(GOVERNOR_ROLE) { _unpause(); }When paused:
EASAttestationProviderV1.verifyCapabilities()reverts, blocking all attestation-gated operationsIntegraExecutorV1.executeOperation()reverts, blocking all executor operations- Record registration and state changes are blocked
- All tokenizer operations (including standard ERC transfers) are blocked
- Extension operations are blocked
View functions remain accessible during pause to allow monitoring and diagnostics.
Emergency Fund Withdrawal
AttestationAccessControlV1 provides an emergency withdrawal function for accidentally sent ETH or tokens:
function emergencyWithdraw(address token, address to, uint256 amount)
external onlyRole(GOVERNOR_ROLE)
{
if (to == address(0)) revert ZeroAddress();
if (token == address(0)) {
(bool success, ) = to.call{value: amount}("");
if (!success) revert ETHTransferFailed();
} else {
IERC20(token).safeTransfer(to, amount);
}
}Uses SafeERC20 for robust token handling across all chains and token implementations.
Time-Limited Emergency Override (180 Days)
IntegraRecordV1 implements a time-gated emergency authority:
uint256 public constant EMERGENCY_PERIOD = 180 days;
address public immutable EMERGENCY_ADDRESS;
uint256 public immutable EMERGENCY_EXPIRY;The emergency address and expiry timestamp are both immutable -- set at construction and never modifiable. During the emergency period (first 180 days after deployment), the emergency address OR the governor can perform emergency operations. After expiry, only governance can act:
function _requireEmergencyAuth() internal view {
if (block.timestamp < EMERGENCY_EXPIRY) {
if (_msgSender() != EMERGENCY_ADDRESS && !hasRole(GOVERNOR_ROLE, _msgSender())) {
revert UnauthorizedEmergencyUnlock(_msgSender());
}
} else {
if (!hasRole(GOVERNOR_ROLE, _msgSender())) {
revert EmergencyPowersExpired();
}
}
}Emergency powers include:
- Emergency unlock of resolver configurations
- Emergency disable/re-enable of registration fees
All emergency actions require a justification string and emit detailed events including whether the emergency address was used and whether it was within the active period.
This provides a safety net during initial deployment while ensuring emergency powers are automatically revoked after the protocol stabilizes.
Immutability Guarantees
Write-Once Registry (IntegraExistenceV1)
IntegraExistenceV1 is a minimal, purpose-built contract with no admin functions, no upgradeability, no roles, and no pause mechanism. It provides a single guarantee: once a content hash is registered, it cannot be re-registered, modified, or deleted.
function register(bytes32 contentHash) external returns (uint64 timestamp) {
if (contentHash == bytes32(0)) revert InvalidContentHash();
ExistenceRecord storage record = records[contentHash];
if (record.exists) {
revert AlreadyExists(contentHash, record.registeredAt, record.registeredBy);
}
// ... write once, then immutable
}The AlreadyExists error returns the original registration timestamp and registrant, providing full provenance. The contract has no selfdestruct, no owner, and no governance -- it is maximally immutable.
Batch registration (registerBatch) skips already-registered hashes rather than reverting, enabling idempotent bulk operations.
Immutable Contract References
Critical cross-contract references are declared as Solidity immutable, meaning they are embedded in the contract bytecode at deployment and can never be changed:
| Contract | Immutable References |
|---|---|
AttestationAccessControlV1 | NAMESPACE, REGISTRY |
EASAttestationProviderV1 | EAS, ACCESS_CAPABILITY_SCHEMA, NAMESPACE, RECORD |
IntegraRecordV1 | EXISTENCE, REGISTRY, EMERGENCY_ADDRESS, EMERGENCY_EXPIRY |
BaseTokenizerV1 | RECORD |
RecordExtension | RECORD |
IntegraLens | INTEGRA_RECORD, REGISTRY |
AttestationTransferPolicy | PROVIDER, RECORD |
This prevents governance from redirecting contract calls to malicious implementations after deployment. The trust model is established at construction time and cannot be subverted.
Code Hash Integrity Checking (IntegraRegistryV1)
IntegraRegistryV1 captures the extcodehash of every component at registration time:
bytes32 codeHash;
assembly {
codeHash := extcodehash(componentAddress)
}On every subsequent lookup via getComponent(), the current code hash is compared against the stored hash:
bytes32 currentHash;
assembly {
currentHash := extcodehash(componentAddr)
}
if (currentHash != info.codeHash) return address(0);If the code hash has changed (which should be impossible for non-upgradeable contracts, but could occur if a proxy's implementation is swapped), the registry returns address(0) -- effectively deactivating the component. This provides a runtime integrity check against implementation changes in registered components.
Privacy Design
Hash-Based Identity Metadata
The EAS attestation schema stores identity information as a hash, not as plaintext:
string public constant SCHEMA_DEFINITION =
"bytes32 integraHash,"
"uint256 tokenId,"
"uint256 capabilities,"
"bytes32 identityMetadataHash," // Hash only -- no plaintext identity data
"uint256 sourceChainId,"
"address sourceEASContract,"
"address recordContract,"
"uint64 issuedAt,"
"bytes32 attestationVersion";The identityMetadataHash field contains a hash of the identity metadata, not the metadata itself. The actual identity information is stored off-chain and verified off-chain. The on-chain hash provides a commitment that can be verified without revealing the underlying data.
Store Commitments, Not Plaintext
The extension building block DataCommitment follows the same principle -- it stores hashes and encrypted blobs, never cleartext domain data. The FundsCustody building block stores fund amounts and addresses but not the purpose or parties involved in the transaction. Sensitive domain data is always encrypted client-side before being stored on-chain.
No Plaintext Sensitive Data On-Chain
The protocol's privacy model follows a consistent pattern: on-chain storage contains hashes and encrypted blobs; plaintext sensitive data is never written to the blockchain. This is enforced by schema design (hash fields for identity metadata), by building block design (DataCommitment stores encrypted blobs), and by convention (encrypted payloads for messaging and resolver data).
Front-Running Protection
Recipient Matching in Attestation Verification
Step 6 of the 13-step verification binds attestations to a specific recipient address:
if (attestation.recipient != user) {
revert InvalidRecipient(attestation.recipient, user);
}This is the primary front-running defense. If an attacker observes a valid attestation UID in the mempool, they cannot use it because:
- The attestation is bound to a specific
recipientaddress - The
userparameter comes from_msgSender()in the calling modifier - The attacker's address will not match the intended recipient
Nonce-Based Ordering in Executor
IntegraExecutorV1 nonces provide ordering guarantees. Each address has a monotonically increasing nonce counter. Meta-transactions signed with a specific nonce can only be executed in order, preventing transaction reordering attacks.
Per-Record Case Reference Uniqueness (ADR/AAA Resolvers)
In the dispute resolution system, case references are globally unique. This prevents duplicate dispute filings. Case references should be submitted promptly to minimize the window for front-running.
Meta-Transaction Security
ERC-2771 Sender Extraction
All user-facing contracts inherit ERC2771Context and resolve _msgSender() to extract the real sender from forwarded calldata. The diamond inheritance between Context (from AccessControl/Pausable) and ERC2771Context is resolved in favor of ERC2771Context:
function _msgSender() internal view override(Context, ERC2771Context) returns (address) {
return ERC2771Context._msgSender();
}Governance Functions Use msg.sender
Some governance-level functions (ossification transitions, emergency controls) use msg.sender rather than _msgSender(). This is intentional -- governance operations should come from the multisig/DAO directly, not through meta-transaction relayers. This prevents a compromised relayer from initiating governance transitions.
Trusted Forwarder Validation
Only the IntegraForwarder contract is accepted as a trusted forwarder. The forwarder address is set at construction time (immutable in ERC2771Context). The forwarder validates EIP-712 signatures, checks deadlines, and consumes nonces before forwarding calls.
Extension Security
Building Block Isolation
Each extension building block (CustodyChain, DataCommitment, FundsCustody, PaymentLedger) is 50-150 lines of code, auditable in isolation. Building blocks define abstract hooks for authorization (_requireOwnerOrExecutor, _verifyClaimCapability) that RecordExtension implements. The building block never makes authorization decisions itself -- it delegates to the base.
Token Binding Auth Delegation
TokenBinding721, TokenBinding1155, and TokenBinding20 implement the ITokenizerV1 lifecycle but delegate all authorization checks to abstract hooks that RecordExtension provides. This ensures that token lifecycle operations (reserve, claim, cancel) are always gated by the same attestation-based access control used by the rest of the protocol.
Diamond Inheritance Resolution
TokenizedExtension721, TokenizedExtension1155, and TokenizedExtension20 resolve the Solidity diamond inheritance between RecordExtension and the corresponding TokenBinding*. This resolution is done once per token standard, eliminating diamond boilerplate from concrete extensions.
Integration Security Checklist
When integrating with Integra v1.7, follow these practices:
Attestation Handling
- Always verify attestation freshness. If your use case is time-sensitive, configure
maxAttestationAgeon the EAS provider. The default is 0 (no limit), which may not be appropriate for high-value operations. - Do not cache attestation UIDs across sessions. Attestations can be revoked at any time. Always submit the UID for on-chain verification.
- Use per-record providers when appropriate. The
setRecordProvider()function allows overriding the default attestation provider for specific records. Use this for records that require different verification levels. - Handle all 13 revert reasons. Each step in the verification chain has a specific custom error. Decode these for user-facing error messages rather than showing generic "transaction failed."
Executor Integration
- Use per-record executors, not broad permissions. Authorize executors per-record via
IntegraRecordV1.authorizeExecutor(). Avoid grantingEXECUTOR_ROLEbroadly unless the address genuinely needs system-wide authority. - Whitelist specific target-selector pairs. Use
setTargetSelectorAllowed()for the narrowest possible permission surface. - Handle ExecutionFailed reverts. Parse the
returnDatabytes inExecutionFailederrors to extract the underlying revert reason from the target contract.
Transfer Policy Considerations
- Pre-flight before submitting transfers. Call
detectTransferRestriction()off-chain before submitting a transfer transaction. This avoids wasted gas on transfers that will be rejected by policy. - Use IntegraLens for composed pre-flight.
IntegraLens.canTransfer()wrapsdetectTransferRestrictionandmessageForTransferRestrictioninto a single call. - Handle the 100,000 gas cap. Custom policy resolvers must complete their
checkTransfer()logic within 100,000 gas. Exceeding this limit will cause the transfer to revert. - Understand mint/burn scope exclusion. Transfer policies are only enforced on peer-to-peer transfers. Minting and burning are not subject to policy checks.
Meta-Transaction Considerations
- Use
_msgSender()consistently. All Integra contracts useERC2771Context._msgSender()for caller identification. If you build contracts that interact with Integra, follow the same pattern. - Be aware that governance functions use
msg.sender. Governance operations should come from the multisig/DAO directly, not through meta-transaction relayers.
Record Management
- Check
EMERGENCY_EXPIRYbefore relying on emergency authority. The 180-day emergency period is a deployment-time parameter. Verify the current state viagetEmergencyStatus(). - Verify code hash integrity. When resolving components from
IntegraRegistryV1, agetComponent()return ofaddress(0)may indicate a code hash mismatch, not just an unregistered component. - Handle the pause state. All state-mutating operations revert when paused. Check the pause state before submitting transactions to avoid wasted gas.
Token Operations
- Always use
SafeERC20for token transfers. Integra contracts use OpenZeppelin'sSafeERC20wrapper. If your integration transfers tokens to or from Integra contracts, use the same pattern. - Respect the
whenNotPausedguard. Token operations on all tokenizers are gated by pause state. - Verify record ownership before executor authorization. Only record owners can authorize executors. Verify ownership via
IntegraRecordV1.getOwner()before attempting executor setup.
IContractV2 Integration
- Use IntegraLens for composed state queries. Rather than calling
IContractV2on individual contracts, useIntegraLens.getRecordView()for a single composed view of all state and actions. - Handle try/catch gracefully. IntegraLens wraps all
IContractV2calls in try/catch. If a contract does not implementIContractV2or reverts, the Lens returns empty state for that source. - Decode state using stateSchema. Each contract's
stateSchema()returns an identifier string used to select the correct ABI decoder forgetRecordState()return values.
Security Contact
Report security vulnerabilities to security@integraledger.com.
For responsible disclosure, please include:
- Affected contract(s) and function(s)
- Description of the vulnerability
- Steps to reproduce or proof of concept
- Estimated severity and impact
Do not disclose vulnerabilities publicly before coordinated remediation.