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
AccessControlwith aGOVERNOR_ROLEfor administrative operations - Meta-transaction support via
ERC2771Contextso 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
_isRecordPartyfunction - Record validation helpers that delegate to the
IntegraRecordcontract 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| Parent | Source | Purpose |
|---|---|---|
IResolver | src/resolvers/interfaces/IResolver.sol | Core resolver interface (3 functions) |
ERC165 | OpenZeppelin | Interface detection (supportsInterface) |
AccessControl | OpenZeppelin | Role-based access control (DEFAULT_ADMIN_ROLE, GOVERNOR_ROLE) |
ERC2771Context | OpenZeppelin | Meta-transaction sender extraction from trusted forwarder |
ReentrancyGuardTransient | OpenZeppelin | Reentrancy guard using transient storage (EIP-1153) |
Pausable | OpenZeppelin | Emergency pause/unpause mechanism |
ResolverEvents | src/resolvers/ResolverEvents.sol | Shared event definitions inherited by all resolvers |
Key Concepts
Roles
BaseResolver defines two roles via OpenZeppelin AccessControl:
| Role | Hash | Purpose |
|---|---|---|
DEFAULT_ADMIN_ROLE | 0x00 | Can grant and revoke all roles, including itself |
GOVERNOR_ROLE | keccak256("GOVERNOR_ROLE") | Pause/unpause the resolver |
Both roles are granted to the deployer (_msgSender()) in the constructor. Concrete resolvers may define additional roles.
Modifiers
| Modifier | Description |
|---|---|
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:
- The record owner -- checked via
IIntegraRecord.getOwner() - An authorized executor -- checked via
IIntegraRecord.isAuthorizedExecutor() - 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 toERC2771Context._msgSender()_msgData()-- resolves toERC2771Context._msgData()_contextSuffixLength()-- resolves toERC2771Context._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
| Property | Value |
|---|---|
| Solidity version | 0.8.28 |
| License | MIT |
| Contract type | abstract contract |
| Inherits | IResolver, ERC165, AccessControl, ERC2771Context, ReentrancyGuardTransient, Pausable, ResolverEvents |
Constructor Parameters
constructor(address integraRecord, address trustedForwarder)
ERC2771Context(trustedForwarder)| Parameter | Type | Description |
|---|---|---|
integraRecord | address | The IntegraRecord contract address for owner/executor/tokenizer lookups. Must be non-zero; reverts with ResolverErrors.RecordNotFound(bytes32(0)) if zero |
trustedForwarder | address | The 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
| Constant | Visibility | Type | Value | Purpose |
|---|---|---|---|---|
GOVERNOR_ROLE | public | bytes32 | keccak256("GOVERNOR_ROLE") | Role identifier for pause/unpause authorization |
_TOKEN_HOLDER_GAS_LIMIT | private | uint256 | 100_000 | Gas 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
| Name | Type | Description |
|---|---|---|
INTEGRA_RECORD | address | IntegraRecord contract address, set at construction time. All ownership, executor, and tokenizer lookups delegate to this contract via the IIntegraRecord interface |
Modifiers
| Modifier | Description |
|---|---|
onlyRecord() | Restricts function access to the INTEGRA_RECORD contract. Reverts with ResolverErrors.OnlyRecord() if _msgSender() is any other address |
Functions
External Functions
| Function | Access | Description |
|---|---|---|
pause() | GOVERNOR_ROLE | Pause the resolver, blocking all whenNotPaused functions in concrete resolvers |
unpause() | GOVERNOR_ROLE | Unpause the resolver, resuming normal operations |
Public View Functions
| Function | Access | Description |
|---|---|---|
supportsInterface(bytes4 interfaceId) | View | ERC-165 interface detection. Returns true for IResolver and all interfaces supported by AccessControl and ERC165 |
Internal View Functions
| Function | Description |
|---|---|
_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)
| Function | Description |
|---|---|
_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.
| Error | Parameters | When Thrown |
|---|---|---|
OnlyRecord() | -- | Caller is not the INTEGRA_RECORD contract |
RecordNotFound(bytes32 integraHash) | The record identifier | Record does not exist in IntegraRecord (also used when integraRecord constructor argument is zero) |
NotRecordOwner(bytes32 integraHash, address caller) | Record identifier, caller address | Caller is not the record owner |
NotOwnerOrExecutor(bytes32 integraHash, address caller) | Record identifier, caller address | Caller 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.
| Event | Parameters | When Emitted |
|---|---|---|
ResolverStateChanged | bytes32 indexed integraHash, bytes32 indexed stateKey, bytes32 oldValue, bytes32 newValue | Resolver state changes for a record |
MetadataUpdated | bytes32 indexed integraHash, string field | Resolver 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.
| Constant | Value | Description |
|---|---|---|
CATEGORY_METADATA | 1 << 0 | Read-only metadata management (names, descriptions, schemas) |
CATEGORY_GATEKEEPER | 1 << 1 | Access control and compliance enforcement |
CATEGORY_BEHAVIORAL | 1 << 2 | Active lifecycle capabilities -- state machines, actions, hooks |
CATEGORY_AUTOMATION | 1 << 3 | Condition-triggered automation (deadlines, thresholds) |
CATEGORY_INTEGRATION | 1 << 4 | External 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
| Function | Description |
|---|---|
_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
| Event | Parameters | When Emitted |
|---|---|---|
ActorAuthorized | bytes32 indexed role, bytes32 indexed integraHash, address indexed actor, address principal, address authorizedBy | Actor authorization granted |
ActorRevoked | bytes32 indexed role, bytes32 indexed integraHash, address indexed actor, address revokedBy | Actor authorization revoked |
Errors
| Error | When 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);
}| Function | Returns | Description |
|---|---|---|
resolverCategories() | uint256 | Bitmask of CATEGORY_* flags declaring which categories this resolver implements |
resolverVersion() | uint256 | Semantic version encoded as major << 32 | minor << 16 | patch |
supportsInterface(bytes4) | bool | ERC-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.
| Library | Type | Description |
|---|---|---|
StateMachine | Library | Generic state lifecycle enforcement with two-tier transitions (allowed/restricted) using bitmask-based rules |
MultiPartyConfirmation | Library | Quorum-based confirmation tracking with nonce replay protection and cooldowns |
HashAnchor | Library | Append-only hash-based proof-of-existence list for evidence anchoring |
DeadlineTracker | Library | Time-based deadline management with absolute and relative deadline setting |
Counter | Library | Per-record keyed counter management using the mapping-as-parameter pattern |
PeriodicSchedule | Library | Time-based periodic schedule management for recurring obligations (finite or unlimited) |
ProportionSplit | Library | Basis-point allocation and proportional calculation with dust handling |
VestingSchedule | Library | Pure math library for linear vesting calculations with optional cliff |
AuthorizedActors | Abstract contract | Per-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
whenNotPausedon all state-changing functions to respect the emergency pause mechanism - Use
nonReentranton functions that make external calls or modify state - Use
_requireOwneror_requireOwnerOrExecutorfor authorization checks - Use
_isRecordPartywhen broader access is appropriate (e.g., view functions or party-restricted actions) - Use
onlyRecordfor callback functions that should only be called by IntegraRecord - Declare resolver categories as a bitmask of
CATEGORY_*constants fromResolverConstants.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.