Skip to main content

BaseResolver

Abstract foundation contract for all Integra Protocol v1.7 resolvers -- composes access control, meta-transaction support, pausability, and reentrancy protection into a single inheritable base with resolver-specific authorization helpers.

Source: src/resolvers/base/BaseResolver.sol


Overview

BaseResolver is the root abstract contract in the resolver inheritance hierarchy. Every concrete resolver -- whether behavioral, gatekeeper, automation, or observer -- inherits from BaseResolver to get a consistent set of infrastructure capabilities:

  • Access control via OpenZeppelin AccessControl with a GOVERNOR_ROLE for administrative operations
  • Meta-transaction support via ERC2771Context so gasless transactions work transparently through all resolver functions
  • Pausability for emergency stops
  • Reentrancy protection via ReentrancyGuardTransient (transient storage variant for lower gas)
  • Party detection that unifies owner, executor, and token holder checks into a single _isRecordParty function
  • Record validation helpers that delegate to the IntegraRecord contract for ownership and existence checks

BaseResolver does not implement any resolver logic itself. It provides the scaffolding that concrete resolvers build on. The two required functions from IResolver -- resolverCategories() and resolverVersion() -- are left abstract for each concrete resolver to implement.


Inheritance Chain

abstract contract BaseResolver is
    IResolver,
    ERC165,
    AccessControl,
    ERC2771Context,
    ReentrancyGuardTransient,
    Pausable,
    ResolverEvents
ParentSourcePurpose
IResolversrc/resolvers/interfaces/IResolver.solCore resolver interface (3 functions)
ERC165OpenZeppelinInterface detection (supportsInterface)
AccessControlOpenZeppelinRole-based access control (DEFAULT_ADMIN_ROLE, GOVERNOR_ROLE)
ERC2771ContextOpenZeppelinMeta-transaction sender extraction from trusted forwarder
ReentrancyGuardTransientOpenZeppelinReentrancy guard using transient storage (EIP-1153)
PausableOpenZeppelinEmergency pause/unpause mechanism
ResolverEventssrc/resolvers/ResolverEvents.solShared event definitions inherited by all resolvers

Key Concepts

Roles

BaseResolver defines two roles via OpenZeppelin AccessControl:

RoleHashPurpose
DEFAULT_ADMIN_ROLE0x00Can grant and revoke all roles, including itself
GOVERNOR_ROLEkeccak256("GOVERNOR_ROLE")Pause/unpause the resolver

Both roles are granted to the deployer (_msgSender()) in the constructor. Concrete resolvers may define additional roles.

Modifiers

ModifierDescription
onlyRecord()Restricts the caller to the INTEGRA_RECORD contract address. Reverts with ResolverErrors.OnlyRecord() if any other address calls

The onlyRecord modifier is used by concrete resolvers for callback functions that should only be invoked by the IntegraRecord contract during record lifecycle operations.

Party Detection with ITokenParty

The _isRecordParty function provides a unified check for whether an address has any relationship to a record. A "record party" is defined as any of:

  1. The record owner -- checked via IIntegraRecord.getOwner()
  2. An authorized executor -- checked via IIntegraRecord.isAuthorizedExecutor()
  3. A token holder on the record's tokenizer -- checked via ITokenParty.isTokenHolder()

The token holder check uses ITokenParty, a minimal interface (src/interfaces/ITokenParty.sol) that all tokenizers implement regardless of their underlying ERC standard (ERC-20, ERC-721, ERC-1155). This provides cross-standard token holder detection without the resolver needing to know which token standard is in use.

The external call to isTokenHolder is bounded to _TOKEN_HOLDER_GAS_LIMIT (100,000 gas) to prevent denial-of-service from malicious or expensive tokenizer implementations. If the call runs out of gas or the tokenizer does not implement ITokenParty, the try/catch returns false as a safe default.

function _isRecordParty(bytes32 integraHash, address account) internal view virtual returns (bool) {
    if (account == IIntegraRecord(INTEGRA_RECORD).getOwner(integraHash)) return true;
    if (IIntegraRecord(INTEGRA_RECORD).isAuthorizedExecutor(integraHash, account)) return true;
    address tokenizer = IIntegraRecord(INTEGRA_RECORD).getTokenizer(integraHash);
    if (tokenizer != address(0)) {
        try ITokenParty(tokenizer).isTokenHolder{gas: _TOKEN_HOLDER_GAS_LIMIT}(account) returns (bool holds) {
            return holds;
        } catch {}
    }
    return false;
}

ERC-2771 Meta-Transaction Support

BaseResolver inherits ERC2771Context for gasless meta-transaction support. Both AccessControl and Pausable also inherit from Context, creating a diamond inheritance pattern. BaseResolver resolves this by overriding _msgSender(), _msgData(), and _contextSuffixLength() to always delegate to ERC2771Context:

  • _msgSender() -- resolves to ERC2771Context._msgSender()
  • _msgData() -- resolves to ERC2771Context._msgData()
  • _contextSuffixLength() -- resolves to ERC2771Context._contextSuffixLength()

This means all _msgSender() calls throughout the resolver hierarchy -- including those in AccessControl role checks and Pausable modifiers -- correctly extract the original sender from meta-transactions forwarded by the trusted forwarder.

Pause / Unpause

The pause() and unpause() functions are restricted to GOVERNOR_ROLE. When paused, concrete resolvers use the inherited whenNotPaused modifier to block state-changing operations. The pause() / unpause() functions themselves do not use whenNotPaused -- you can always unpause a paused contract.


Contract Details

PropertyValue
Solidity version0.8.28
LicenseMIT
Contract typeabstract contract
InheritsIResolver, ERC165, AccessControl, ERC2771Context, ReentrancyGuardTransient, Pausable, ResolverEvents

Constructor Parameters

constructor(address integraRecord, address trustedForwarder)
    ERC2771Context(trustedForwarder)
ParameterTypeDescription
integraRecordaddressThe IntegraRecord contract address for owner/executor/tokenizer lookups. Must be non-zero; reverts with ResolverErrors.RecordNotFound(bytes32(0)) if zero
trustedForwarderaddressThe ERC-2771 trusted forwarder address for meta-transaction support. Must be non-zero; reverts with ResolverErrors.ZeroForwarder() if zero

The constructor grants both DEFAULT_ADMIN_ROLE and GOVERNOR_ROLE to _msgSender().


Constants

ConstantVisibilityTypeValuePurpose
GOVERNOR_ROLEpublicbytes32keccak256("GOVERNOR_ROLE")Role identifier for pause/unpause authorization
_TOKEN_HOLDER_GAS_LIMITprivateuint256100_000Gas cap for external isTokenHolder calls to prevent DoS

Why _TOKEN_HOLDER_GAS_LIMIT is 100,000: ERC-1155 isTokenHolder implementations may iterate up to 100 storage slots (~210k gas cold). 100k is intentionally conservative -- if the call runs out of gas, the try/catch returns false (safe default). Higher limits would increase DoS exposure from malicious tokenizers.


Immutables

NameTypeDescription
INTEGRA_RECORDaddressIntegraRecord contract address, set at construction time. All ownership, executor, and tokenizer lookups delegate to this contract via the IIntegraRecord interface

Modifiers

ModifierDescription
onlyRecord()Restricts function access to the INTEGRA_RECORD contract. Reverts with ResolverErrors.OnlyRecord() if _msgSender() is any other address

Functions

External Functions

FunctionAccessDescription
pause()GOVERNOR_ROLEPause the resolver, blocking all whenNotPaused functions in concrete resolvers
unpause()GOVERNOR_ROLEUnpause the resolver, resuming normal operations

Public View Functions

FunctionAccessDescription
supportsInterface(bytes4 interfaceId)ViewERC-165 interface detection. Returns true for IResolver and all interfaces supported by AccessControl and ERC165

Internal View Functions

FunctionDescription
_requireOwner(bytes32 integraHash)Reverts with ResolverErrors.NotRecordOwner(integraHash, caller) if _msgSender() is not the record owner. Virtual -- concrete resolvers can override
_requireOwnerOrExecutor(bytes32 integraHash)Reverts with ResolverErrors.NotOwnerOrExecutor(integraHash, caller) if _msgSender() is not the record owner or an authorized executor. Virtual -- concrete resolvers can override
_requireRecordExists(bytes32 integraHash)Reverts with ResolverErrors.RecordNotFound(integraHash) if the record does not exist in IntegraRecord
_getOwner(bytes32 integraHash)Returns the owner address of a record by delegating to IIntegraRecord.getOwner()
_isRecordParty(bytes32 integraHash, address account)Returns true if the account is the record owner, an authorized executor, or a token holder on the record's tokenizer. Virtual -- concrete resolvers can override

Internal Context Overrides (Diamond Resolution)

FunctionDescription
_msgSender()Overrides both Context and ERC2771Context; delegates to ERC2771Context._msgSender()
_msgData()Overrides both Context and ERC2771Context; delegates to ERC2771Context._msgData()
_contextSuffixLength()Overrides both Context and ERC2771Context; delegates to ERC2771Context._contextSuffixLength()

Shared Errors

Source: src/resolvers/ResolverErrors.sol

The ResolverErrors library defines errors shared across all resolvers. BaseResolver uses these errors directly; concrete resolvers may use them as well.

ErrorParametersWhen Thrown
OnlyRecord()--Caller is not the INTEGRA_RECORD contract
RecordNotFound(bytes32 integraHash)The record identifierRecord does not exist in IntegraRecord (also used when integraRecord constructor argument is zero)
NotRecordOwner(bytes32 integraHash, address caller)Record identifier, caller addressCaller is not the record owner
NotOwnerOrExecutor(bytes32 integraHash, address caller)Record identifier, caller addressCaller is neither the owner nor an authorized executor
ZeroForwarder()--Trusted forwarder address is address(0)

Shared Events

Source: src/resolvers/ResolverEvents.sol

ResolverEvents is an abstract contract inherited by BaseResolver. All resolvers receive these events through the inheritance chain.

EventParametersWhen Emitted
ResolverStateChangedbytes32 indexed integraHash, bytes32 indexed stateKey, bytes32 oldValue, bytes32 newValueResolver state changes for a record
MetadataUpdatedbytes32 indexed integraHash, string fieldResolver metadata is updated for a record

Resolver Constants

Source: src/resolvers/ResolverConstants.sol

File-level constants used by resolvers to declare their category via resolverCategories(). Categories are composable bitmasks -- a resolver can declare multiple categories (e.g., CATEGORY_BEHAVIORAL | CATEGORY_GATEKEEPER). Used by IntegraLens and off-chain indexers for resolver classification.

ConstantValueDescription
CATEGORY_METADATA1 << 0Read-only metadata management (names, descriptions, schemas)
CATEGORY_GATEKEEPER1 << 1Access control and compliance enforcement
CATEGORY_BEHAVIORAL1 << 2Active lifecycle capabilities -- state machines, actions, hooks
CATEGORY_AUTOMATION1 << 3Condition-triggered automation (deadlines, thresholds)
CATEGORY_INTEGRATION1 << 4External system integration (oracles, bridges, cross-chain)

AuthorizedActors

Source: src/resolvers/lib/AuthorizedActors.sol

AuthorizedActors is an abstract contract (not a library) that provides per-record, per-role actor authorization with optional delegation. It is inherited by concrete resolvers that need to manage authorized parties such as providers, agents, or arbitrators on a per-record basis.

Authorization Model

The authorization model is three-dimensional: role x record x actor. Each actor can optionally have a principal (delegation), tracking who they represent.

role (bytes32) => integraHash (bytes32) => actor (address) => authorized (bool)
role (bytes32) => integraHash (bytes32) => actor (address) => Delegation { principal, authorizedAt }

Delegation Struct

struct Delegation {
    address principal;      // The address the actor represents (address(0) if not a delegate)
    uint64  authorizedAt;   // Timestamp when the actor was authorized
}

Functions

FunctionDescription
_authorizeActor(bytes32 role, bytes32 integraHash, address actor, address principal, address authorizedBy)Grant authorization to an actor. Reverts if actor is address(0) (InvalidActorAddress) or already authorized (ActorAlreadyAuthorized). Records delegation info if principal != address(0). Emits ActorAuthorized
_revokeActor(bytes32 role, bytes32 integraHash, address actor, address revokedBy)Revoke authorization and clear delegation info. Reverts if actor is not currently authorized (ActorNotAuthorized). Emits ActorRevoked
_isAuthorized(bytes32 role, bytes32 integraHash, address actor)Check whether an actor is authorized for a specific role and record. Returns bool
_getPrincipal(bytes32 role, bytes32 integraHash, address actor)Get the principal (delegator) for an authorized actor. Returns address(0) if not a delegate or not authorized

Events

EventParametersWhen Emitted
ActorAuthorizedbytes32 indexed role, bytes32 indexed integraHash, address indexed actor, address principal, address authorizedByActor authorization granted
ActorRevokedbytes32 indexed role, bytes32 indexed integraHash, address indexed actor, address revokedByActor authorization revoked

Errors

ErrorWhen Thrown
ActorAlreadyAuthorized(bytes32 role, bytes32 integraHash, address actor)Actor is already authorized for this role and record
ActorNotAuthorized(bytes32 role, bytes32 integraHash, address actor)Actor is not currently authorized
InvalidActorAddress()Actor address is address(0)

IResolver Interface

Source: src/resolvers/interfaces/IResolver.sol

The minimal interface that every resolver implements. BaseResolver provides the supportsInterface implementation; the remaining two functions are left abstract for concrete resolvers.

interface IResolver {
    function resolverCategories() external pure returns (uint256);
    function resolverVersion() external pure returns (uint256);
    function supportsInterface(bytes4 interfaceId) external view returns (bool);
}
FunctionReturnsDescription
resolverCategories()uint256Bitmask of CATEGORY_* flags declaring which categories this resolver implements
resolverVersion()uint256Semantic version encoded as major << 32 | minor << 16 | patch
supportsInterface(bytes4)boolERC-165 interface detection

Resolver Library Contracts

The src/resolvers/lib/ directory contains reusable libraries and abstract contracts that provide composable building blocks for concrete resolvers. See the ADR Resolver documentation for detailed usage of these libraries in a production resolver.

LibraryTypeDescription
StateMachineLibraryGeneric state lifecycle enforcement with two-tier transitions (allowed/restricted) using bitmask-based rules
MultiPartyConfirmationLibraryQuorum-based confirmation tracking with nonce replay protection and cooldowns
HashAnchorLibraryAppend-only hash-based proof-of-existence list for evidence anchoring
DeadlineTrackerLibraryTime-based deadline management with absolute and relative deadline setting
CounterLibraryPer-record keyed counter management using the mapping-as-parameter pattern
PeriodicScheduleLibraryTime-based periodic schedule management for recurring obligations (finite or unlimited)
ProportionSplitLibraryBasis-point allocation and proportional calculation with dust handling
VestingScheduleLibraryPure math library for linear vesting calculations with optional cliff
AuthorizedActorsAbstract contractPer-record, per-role actor authorization with optional delegation (documented above)

Usage: Creating a Custom Resolver

To create a new resolver, extend BaseResolver and implement the two abstract functions from IResolver:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;

import {BaseResolver} from "../base/BaseResolver.sol";
import {ResolverErrors} from "../ResolverErrors.sol";
import {CATEGORY_BEHAVIORAL} from "../ResolverConstants.sol";

contract MyResolver is BaseResolver {
    constructor(address integraRecord, address trustedForwarder)
        BaseResolver(integraRecord, trustedForwarder)
    {}

    /// @notice Declare this resolver as behavioral
    function resolverCategories() external pure override returns (uint256) {
        return CATEGORY_BEHAVIORAL;
    }

    /// @notice Version 1.0.0 encoded as (1 << 32 | 0 << 16 | 0)
    function resolverVersion() external pure override returns (uint256) {
        return (1 << 32);
    }

    /// @notice Example state-changing function restricted to record owner
    function doSomething(bytes32 integraHash)
        external
        whenNotPaused
        nonReentrant
    {
        _requireRecordExists(integraHash);
        _requireOwner(integraHash);
        // resolver-specific logic here
    }

    /// @notice Example function using party detection
    function viewAsParty(bytes32 integraHash) external view returns (bool) {
        return _isRecordParty(integraHash, _msgSender());
    }
}

Key patterns when extending BaseResolver:

  • Use whenNotPaused on all state-changing functions to respect the emergency pause mechanism
  • Use nonReentrant on functions that make external calls or modify state
  • Use _requireOwner or _requireOwnerOrExecutor for authorization checks
  • Use _isRecordParty when broader access is appropriate (e.g., view functions or party-restricted actions)
  • Use onlyRecord for callback functions that should only be called by IntegraRecord
  • Declare resolver categories as a bitmask of CATEGORY_* constants from ResolverConstants.sol

Security Considerations

Reentrancy. BaseResolver inherits ReentrancyGuardTransient but does not apply nonReentrant to any of its own functions. Concrete resolvers must add nonReentrant to state-changing functions that perform external calls.

Gas-Limited External Calls. The _isRecordParty function makes an external call to the tokenizer's isTokenHolder with a 100,000 gas limit. This prevents a malicious tokenizer from consuming unbounded gas, but it also means that tokenizers with expensive isTokenHolder implementations may return false even when the account holds tokens. This is a safe default -- false negatives are acceptable; false positives are not.

Meta-Transaction Sender. All _msgSender() calls resolve through ERC2771Context. If the trusted forwarder is compromised, an attacker could impersonate any address. The trusted forwarder address is immutable (set at construction time via ERC2771Context) and cannot be changed after deployment.

Pause Scope. The pause() / unpause() functions affect only the concrete resolver that inherits BaseResolver. Each resolver instance has its own pause state. Pausing one resolver does not affect other resolvers attached to the same record.

Zero-Address Validation. The constructor validates both integraRecord and trustedForwarder are non-zero. However, it does not verify that these addresses are actually contracts implementing the expected interfaces. Deployers must ensure correct addresses are provided.

Role Administration. The deployer receives DEFAULT_ADMIN_ROLE, which can grant and revoke any role including itself. If the admin role is renounced without transferring it, the GOVERNOR_ROLE can no longer be managed. Concrete resolvers that define additional roles should document their admin hierarchy.