Resolvers
Pluggable logic modules that extend Integra records with validation, automation, metadata, and cross-protocol integration.
Overview
Resolvers are smart contracts that attach to Integra records to provide modular, composable functionality. A single record managed by IntegraRecordV1 can have up to 10 resolvers attached simultaneously, each contributing a different capability -- from blocking unauthorized ownership transfers, to managing a full dispute resolution lifecycle, to storing encrypted contact information.
The resolver architecture separates what a record is (identity, ownership, content hash -- managed by IntegraRecordV1) from what a record can do (validation, workflows, automation -- managed by resolvers). This separation means new capabilities can be developed and deployed independently without modifying the core record contract.
The Five Resolver Categories
Resolvers are classified into five categories using a bitmask system. Each category has a corresponding interface that defines its API surface. All resolvers inherit from a single BaseResolver base class regardless of category.
| Category | Bitmask | Constant | Purpose |
|---|---|---|---|
| Metadata | 1 << 0 (1) | CATEGORY_METADATA | Read-heavy data describing the record (contact info, structured metadata) |
| Gatekeeper | 1 << 1 (2) | CATEGORY_GATEKEEPER | Blocking validation rules controlling ownership transfers, tokenization, and expiry |
| Behavioral | 1 << 2 (4) | CATEGORY_BEHAVIORAL | Stateful logic with lifecycle hooks and named actions (dispute resolution, workflow engines) |
| Automation | 1 << 3 (8) | CATEGORY_AUTOMATION | Trigger conditions evaluated by keeper bots for automated state transitions |
| Integration | 1 << 4 (16) | CATEGORY_INTEGRATION | External callbacks and cross-protocol bridges |
Categories are defined in ResolverConstants.sol:
uint256 constant CATEGORY_METADATA = 1 << 0; // 1
uint256 constant CATEGORY_GATEKEEPER = 1 << 1; // 2
uint256 constant CATEGORY_BEHAVIORAL = 1 << 2; // 4
uint256 constant CATEGORY_AUTOMATION = 1 << 3; // 8
uint256 constant CATEGORY_INTEGRATION = 1 << 4; // 16A single resolver can implement multiple categories. For example, the ADR resolver implements Behavioral + Gatekeeper + Automation, returning a bitmask of 4 | 2 | 8 = 14 from resolverCategories().
Category Interfaces
IMetadataResolver
Describes the record with structured data and contact endpoints.
interface IMetadataResolver is IResolver {
function getMetadata(bytes32 integraHash) external view returns (string memory metadata);
function getContactEndpoint(bytes32 integraHash, address caller, string calldata method)
external view returns (string memory endpoint);
function hasMetadata(bytes32 integraHash) external view returns (bool);
}IGatekeeperResolver
Enforces rules that can block operations. Functions return (bool, string) where the string provides a human-readable rejection reason.
interface IGatekeeperResolver is IResolver {
function canOwn(bytes32 integraHash, address newOwner)
external view returns (bool allowed, string memory reason);
function canTokenize(bytes32 integraHash, address tokenizer)
external view returns (bool allowed, string memory reason);
function isExpired(bytes32 integraHash)
external view returns (bool expired, uint256 expiryTime);
}IBehavioralResolver
Adds active capabilities with lifecycle hooks and named actions.
interface IBehavioralResolver is IResolver {
function onRegistered(bytes32 integraHash, bytes32 contentHash, address owner, bytes calldata initData) external;
function onTransferred(bytes32 integraHash, address from, address to) external;
function onTokenizerAssociated(bytes32 integraHash, address tokenizer) external;
function executeAction(bytes32 integraHash, string calldata action, bytes calldata data)
external returns (bool success, bytes memory result);
function availableActions(bytes32 integraHash) external view returns (string[] memory actions);
}IAutomationResolver
Provides trigger conditions evaluated by keeper bots. evaluateConditions is intentionally non-view -- evaluation may update internal trigger state.
interface IAutomationResolver is IResolver {
function evaluateConditions(bytes32 integraHash)
external returns (bool triggered, bytes32 conditionId, bytes memory data);
function onConditionTriggered(bytes32 integraHash, bytes32 conditionId, bytes calldata data) external;
function activeConditionCount(bytes32 integraHash) external view returns (uint256);
}IIntegrationResolver
Bridges to external protocols with callback handling and status reporting.
interface IIntegrationResolver is IResolver {
function onExternalCallback(bytes32 integraHash, bytes32 source, bytes calldata data)
external returns (bool success);
function bridgedSystems() external view returns (bytes32[] memory systems);
function integrationStatus(bytes32 integraHash, bytes32 system)
external view returns (bool active, bytes memory metadata);
}Concrete Resolvers
The protocol ships with two concrete resolver implementations:
ADRResolverV3
Categories: Behavioral + Gatekeeper + Automation
A full arbitration and dispute resolution lifecycle: clauses, disputes, evidence anchoring, multi-party settlement with quorum confirmation, and automation triggers for deadline enforcement. This is the most complex resolver in the protocol.
AAAResolverV1
Categories: Behavioral + Gatekeeper + Automation
Agreement-bound dispute resolution with token-based agreement binding, commercial party access control, and integrated settlement. See the AAAResolverV1 reference for details.
How Resolvers Attach to Records
When a record owner adds a resolver, IntegraRecordV1 calls supportsInterface() to verify the contract implements IResolver, then stores the resolver address. From that point on, the record contract calls lifecycle hooks when relevant state changes occur.
// Record owner attaches a resolver
integraRecordV1.addResolver(integraHash, resolverAddress);Up to 10 resolvers can be attached to a single record. The record contract calls hooks on all attached resolvers that implement the relevant category interface.
Lifecycle Hooks
Behavioral resolvers receive calls at key moments in a record's lifecycle:
onRegistered-- Called when the record is first createdonTransferred-- Called when ownership changesonTokenizerAssociated-- Called when a tokenizer is linked to the record
Gatekeeper Validation
Gatekeeper resolvers are consulted before state changes. If any gatekeeper returns (false, reason), the operation reverts:
// Before ownership transfer, the record contract checks:
(bool allowed, string memory reason) = gatekeeperResolver.canOwn(integraHash, newOwner);
require(allowed, reason);Resolver Composition
The power of resolvers lies in composition. Mix and match to create record-specific service configurations:
Real Estate Sale:
+-- ADRResolverV3 (behavioral: dispute resolution)
+-- AAAResolverV1 (behavioral: agreement-bound dispute resolution)
Service Agreement:
+-- AAAResolverV1 (behavioral: agreement-bound dispute resolution + settlement)
+-- ADRResolverV3 (behavioral: arbitration lifecycle)Progressive Enhancement
Start minimal and add resolvers over time:
// Day 1: Register with dispute resolution
integraRecordV1.addResolver(integraHash, adrResolverAddress);
// Day 30: Add agreement-bound dispute resolution
integraRecordV1.addResolver(integraHash, aaaResolverAddress);Building Custom Resolvers
Step 1: Inherit from BaseResolver
All resolvers inherit from BaseResolver and implement the category interface(s) matching their function:
import {BaseResolver} from "./base/BaseResolver.sol";
import {IGatekeeperResolver} from "./interfaces/IGatekeeperResolver.sol";
contract MyGatekeeperResolver is BaseResolver, IGatekeeperResolver {
constructor(address integraRecord, address trustedForwarder)
BaseResolver(integraRecord, trustedForwarder)
{}
function resolverCategories() external pure override returns (uint256) {
return CATEGORY_GATEKEEPER;
}
function resolverVersion() external pure override returns (uint256) {
return (1 << 32); // v1.0.0
}
}Step 2: Override Category Functions
Override only the functions you need. Unoverridden functions use safe defaults:
function canOwn(bytes32 integraHash, address newOwner)
external view override returns (bool allowed, string memory reason)
{
if (!isEligible(newOwner)) {
return (false, "Address not eligible");
}
return (true, "");
}Step 3: Multi-Category Resolvers
For resolvers spanning multiple categories, inherit from BaseResolver directly and implement each category interface:
contract MyMultiResolver is BaseResolver, IBehavioralResolver, IGatekeeperResolver {
function resolverCategories() external pure override returns (uint256) {
return CATEGORY_BEHAVIORAL | CATEGORY_GATEKEEPER;
}
function supportsInterface(bytes4 interfaceId)
public view virtual override(BaseResolver, IResolver) returns (bool)
{
return interfaceId == type(IBehavioralResolver).interfaceId
|| interfaceId == type(IGatekeeperResolver).interfaceId
|| super.supportsInterface(interfaceId);
}
// Implement all required functions from both interfaces...
}Step 4: Handle Stateful Cleanup (Optional)
If your resolver stores per-record state, implement IStatefulResolver so state is cleaned up when the resolver is detached:
contract MyStatefulResolver is BaseResolver, IMetadataResolver, IStatefulResolver {
mapping(bytes32 => MyData) internal _data;
function hasState(bytes32 integraHash) external view override returns (bool) {
return _data[integraHash].exists;
}
function clearState(bytes32 integraHash) external override onlyRecord {
delete _data[integraHash];
}
}Support Libraries
The resolver framework includes five reusable libraries for common patterns:
| Library | Purpose |
|---|---|
| StateMachine | Generic state lifecycle enforcement with two-tier (allowed + restricted) transitions |
| MultiPartyConfirmation | Quorum-based confirmation tracking with nonce replay protection and cooldowns |
| HashAnchor | Append-only hash-based proof-of-existence entries for evidence anchoring |
| DeadlineTracker | Time-based deadline management with relative and absolute modes |
| AuthorizedActors | Per-record, per-role actor authorization with optional delegation |
These libraries are used by ADRResolverV3 and AAAResolverV1 and are available for custom resolver development.
Base Class Hierarchy
IResolver (3 methods: resolverCategories, resolverVersion, supportsInterface)
|
BaseResolver (ERC2771 + AccessControl + Pausable + ReentrancyGuard + onlyRecord)
|
+-- ADRResolverV3 (Behavioral + Gatekeeper + Automation)
+-- AAAResolverV1 (Behavioral + Gatekeeper + Automation)All resolvers inherit directly from BaseResolver, which provides ERC-2771 meta-transaction support, role-based access control, transient-storage reentrancy protection (EIP-1153), emergency pause, and the onlyRecord modifier that restricts lifecycle hooks to calls from IntegraRecordV1. Category-specific behavior is defined by implementing the corresponding category interface(s) rather than inheriting from category-specific base classes.
Resolver Constraints
Resolvers follow strict boundaries to maintain system security:
What resolvers can do:
- Store record-specific metadata
- Enforce validation rules that block operations
- Emit events for off-chain indexing
- Call external contracts within gas limits
- Query record information from
IntegraRecordV1
What resolvers cannot do:
- Modify the record's content hash (immutable)
- Change record ownership directly (only the record contract can)
- Access other records' resolver state (isolated by design)
- Exceed gas limits enforced by the record contract