Resolver Composition: Extending Real World Contracts
Resolver Composition: Extending Real World Contracts
Section titled “Resolver Composition: Extending Real World Contracts”Overview
Section titled “Overview”The Resolver Pattern is Integra’s powerful extensibility system that lets you attach custom services to documents without modifying core contracts. This enables unlimited functionality while keeping the document registry immutable and secure.
The Power of Resolvers
Section titled “The Power of Resolvers”Resolvers transform static documents into programmable, service-rich contracts that can:
- Automate workflows (expiry notifications, renewal reminders)
- Store metadata (contact info, compliance records)
- Enforce rules (geographic restrictions, accreditation requirements)
- Trigger actions (payments, notifications, integrations)
- Extend functionality (custom business logic)
All without touching the core document registry.
How Resolvers Work
Section titled “How Resolvers Work”Document-Resolver Binding
Section titled “Document-Resolver Binding”When you register a document, you can attach resolvers:
bytes32 integraHash = documentRegistry.registerDocument( documentHash, referenceHash, tokenizer, executor, processHash, identityExtension, contactResolverId, // Primary resolver [lifecycleResolverId, complianceResolverId] // Additional resolvers);What happens:
- Document gets permanent
integraHashidentity - Resolvers are registered with the document
- Registry calls resolver hooks at lifecycle events
- Resolvers can store data, trigger logic, enforce rules
Resolver Lifecycle Hooks
Section titled “Resolver Lifecycle Hooks”Resolvers implement the IDocumentResolver interface with hooks called at key moments:
interface IDocumentResolver { // Called when document first registered function onDocumentRegistered( bytes32 integraHash, bytes32 documentHash, address owner, bytes calldata metadata ) external;
// Called when ownership transfers function onOwnershipTransferred( bytes32 integraHash, address oldOwner, address newOwner, string calldata reason ) external;
// Called when tokenizer changes function onTokenizerAssociated( bytes32 integraHash, address tokenizer, address owner ) external;
// Called on custom updates function onDocumentUpdated( bytes32 integraHash, bytes calldata updateData ) external;}Primary vs Additional Resolvers
Section titled “Primary vs Additional Resolvers”Primary Resolver (Critical Services)
Section titled “Primary Resolver (Critical Services)”Characteristics:
- ONE per document
- Called FIRST
- Must SUCCEED (transaction reverts if fails)
- Higher gas limit (200k default)
Use cases:
- Compliance checks that must pass
- Required data storage
- Critical validation logic
Example:
// Compliance resolver as primarybytes32 integraHash = documentRegistry.registerDocument( ..., complianceResolverId, // MUST succeed []);
// If compliance check fails → entire registration revertsAdditional Resolvers (Optional Services)
Section titled “Additional Resolvers (Optional Services)”Characteristics:
- UP TO 10 per document
- Called AFTER primary
- Best-effort (failure logged, doesn’t revert)
- Lower gas limit (100k default)
Use cases:
- Analytics/tracking
- Notifications
- Optional metadata
- Non-critical automation
Example:
bytes32 integraHash = documentRegistry.registerDocument( ..., primaryResolverId, [analyticsResolver, notificationResolver, archiveResolver]);
// If notification fails → operation still succeeds, failure loggedReal-World Use Cases
Section titled “Real-World Use Cases”Use Case 1: Property Rental with Automation
Section titled “Use Case 1: Property Rental with Automation”// Register rental agreement with multiple resolversbytes32 rentalHash = documentRegistry.registerDocument( leaseAgreementHash, ipfsCID, address(rentalTokenizer), address(0), processHash, bytes32(0), contactResolverId, // Primary: Contact storage [ lifecycleResolverId, // Track lease expiry paymentResolverId, // Automate rent requests complianceResolverId // Log regulatory data ]);What each resolver does:
Contact Resolver (Primary):
onDocumentRegistered() → Store landlord contact infoonOwnershipTransferred() → Update to new landlord contactLifecycle Resolver (Additional):
onDocumentRegistered() → Set lease end date (12 months)// Off-chain service monitors:// - 30 days before expiry → send renewal reminder// - On expiry → mark lease as expiredPayment Resolver (Additional):
onDocumentRegistered() → Create monthly payment schedule// Automatically:// - Generate monthly rent invoices// - Send payment signals to tenant// - Track payment historyCompliance Resolver (Additional):
onDocumentRegistered() → Log KYC check timestamponOwnershipTransferred() → Verify new owner accreditationUse Case 2: Security Token with Compliance
Section titled “Use Case 2: Security Token with Compliance”// Register security token with compliance enforcementbytes32 tokenHash = documentRegistry.registerDocument( offeringMemorandumHash, ipfsCID, address(securityTokenTokenizer), address(0), processHash, bytes32(0), accreditationResolverId, // Primary: Must verify accredited [ jurisdictionResolverId, // Check jurisdiction compliance transferRestrictionsId // Enforce transfer limits ]);Accreditation Resolver (Primary):
onDocumentRegistered() { // MUST verify owner is accredited investor require(isAccredited(owner), "Not accredited"); // If fails → registration reverts}
onOwnershipTransferred() { // MUST verify new owner is accredited require(isAccredited(newOwner), "New owner not accredited"); // If fails → transfer reverts}Use Case 3: Intellectual Property with Royalties
Section titled “Use Case 3: Intellectual Property with Royalties”// Register music copyright with royalty automationbytes32 copyrightHash = documentRegistry.registerDocument( musicCopyrightHash, ipfsCID, address(royaltyTokenizer), address(0), processHash, bytes32(0), royaltyDistributionResolverId, // Primary: Track distributions [ usageTrackingResolverId, // Monitor streaming plays paymentSplitResolverId // Calculate splits ]);Creating Custom Resolvers
Section titled “Creating Custom Resolvers”Basic Resolver Template
Section titled “Basic Resolver Template”import "../registry/interfaces/IDocumentResolver.sol";
contract MyCustomResolver is IDocumentResolver { // Your custom storage mapping(bytes32 => CustomData) private data;
function onDocumentRegistered( bytes32 integraHash, bytes32 documentHash, address owner, bytes calldata metadata ) external override { // Your custom logic on registration data[integraHash] = decodeAndStore(metadata); emit CustomDataStored(integraHash, owner); }
function onOwnershipTransferred( bytes32 integraHash, address oldOwner, address newOwner, string calldata reason ) external override { // Your custom logic on transfer data[integraHash].owner = newOwner; emit OwnershipUpdated(integraHash, oldOwner, newOwner); }
function onTokenizerAssociated( bytes32 integraHash, address tokenizer, address owner ) external override { // Your custom logic on tokenizer change }
function onDocumentUpdated( bytes32 integraHash, bytes calldata updateData ) external override { // Your custom logic on updates }
// Add custom query functions function getCustomData(bytes32 integraHash) external view returns (CustomData memory) { return data[integraHash]; }}Advanced: Compliance Resolver
Section titled “Advanced: Compliance Resolver”contract AccreditationResolver is IDocumentResolver { mapping(bytes32 => bool) private verified; mapping(address => bool) private accreditedInvestors;
// Only allow accredited investors function onDocumentRegistered( bytes32 integraHash, bytes32, address owner, bytes calldata ) external override { require( accreditedInvestors[owner], "Owner must be accredited investor" );
verified[integraHash] = true; emit DocumentVerified(integraHash, owner); }
// Block transfers to non-accredited users function onOwnershipTransferred( bytes32 integraHash, address, address newOwner, string calldata ) external override { require( accreditedInvestors[newOwner], "New owner must be accredited" );
emit TransferVerified(integraHash, newOwner); }
// Governance function to verify investors function addAccreditedInvestor(address investor) external onlyRole(GOVERNOR_ROLE) { accreditedInvestors[investor] = true; }}Resolver Capabilities
Section titled “Resolver Capabilities”What Resolvers Can Do
Section titled “What Resolvers Can Do”-
Store Data
// Store document-specific metadatamapping(bytes32 => DocumentMetadata) private metadata; -
Enforce Rules
// Revert if conditions not metrequire(meetsRequirements(owner), "Requirements not met"); -
Emit Events
// Trigger off-chain systemsemit DocumentProcessed(integraHash, timestamp); -
Call External Contracts
// Integrate with other systemsexternalOracle.recordEvent(integraHash); -
Query Document Registry
// Access document infoaddress owner = documentRegistry.getDocumentOwner(integraHash);
What Resolvers CANNOT Do
Section titled “What Resolvers CANNOT Do”- ❌ Modify document hash (immutable)
- ❌ Change document owner (only registry can)
- ❌ Access other documents’ data (isolated)
- ❌ Exceed gas limits (enforced by registry)
Extensibility Patterns
Section titled “Extensibility Patterns”Pattern 1: Modular Services
Section titled “Pattern 1: Modular Services”Build a library of resolvers, mix and match per document:
// Standard resolvers available:bytes32 CONTACT_RESOLVER = keccak256("ContactResolver");bytes32 LIFECYCLE_RESOLVER = keccak256("LifecycleResolver");bytes32 COMPLIANCE_RESOLVER = keccak256("ComplianceResolver");bytes32 PAYMENT_RESOLVER = keccak256("PaymentResolver");
// Real estate documentdocumentRegistry.registerDocument( ..., CONTACT_RESOLVER, [LIFECYCLE_RESOLVER, PAYMENT_RESOLVER]);
// Security token documentdocumentRegistry.registerDocument( ..., COMPLIANCE_RESOLVER, [LIFECYCLE_RESOLVER]);Pattern 2: Progressive Enhancement
Section titled “Pattern 2: Progressive Enhancement”Start minimal, add services over time:
// Day 1: Register with just contact infodocumentRegistry.registerDocument(..., CONTACT_RESOLVER, []);
// Day 30: Add lifecycle trackingdocumentRegistry.addAdditionalResolver(integraHash, LIFECYCLE_RESOLVER);
// Day 60: Add payment automationdocumentRegistry.addAdditionalResolver(integraHash, PAYMENT_RESOLVER);
// Day 90: Lock configuration (no more changes)documentRegistry.lockResolvers(integraHash);Pattern 3: Custom Business Logic
Section titled “Pattern 3: Custom Business Logic”Create resolvers for your specific needs:
// Geographic restriction resolvercontract GeographicResolver is IDocumentResolver { mapping(bytes32 => string[]) private allowedCountries;
function onOwnershipTransferred(...) external override { string memory country = getUserCountry(newOwner); string[] memory allowed = allowedCountries[integraHash];
require(isAllowed(country, allowed), "Country not allowed"); }}
// Environmental compliance resolvercontract CarbonOffsetResolver is IDocumentResolver { function onDocumentRegistered(...) external override { // Require carbon offset purchase require(hasCarbonOffset(owner), "Offset required"); }}Advanced Features
Section titled “Advanced Features”Resolver Locking
Section titled “Resolver Locking”Make resolver configuration permanent:
// Owner locks resolvers (can't be changed)documentRegistry.lockResolvers(integraHash);
// Future attempts to change resolvers will revertdocumentRegistry.setPrimaryResolver(integraHash, newId);// ❌ Reverts: ResolverConfigurationLockedUse cases:
- Finalized documents (no more changes)
- Regulatory requirements (config must be immutable)
- Trust signal (resolver config can’t be manipulated)
Gas Limit Protection
Section titled “Gas Limit Protection”Resolvers have configurable gas limits to prevent DOS:
// Default limits- Primary resolver: 200,000 gas- Additional resolvers: 100,000 gas each
// Custom limits per resolverdocumentRegistry.setResolverGasLimitOverride( expensiveResolverId, 500_000 // Higher limit for complex logic);Protection:
- Prevents malicious/buggy resolvers from consuming all gas
- Ensures predictable transaction costs
- Allows per-resolver optimization
Graceful Degradation
Section titled “Graceful Degradation”If a resolver becomes unavailable (deactivated or code changed):
// Registry queries component registryaddress resolver = integraRegistry.getComponent(resolverId);
if (resolver == address(0)) { // Primary: Log and continue (graceful) emit PrimaryResolverUnavailable(integraHash, resolverId); return true;}
// Additional: Skip silentlyBenefits:
- Documents don’t break if resolver has issues
- Operations continue with degraded functionality
- Time to fix resolver without blocking users
Real-World Resolver Examples
Section titled “Real-World Resolver Examples”1. Automated Renewal Resolver
Section titled “1. Automated Renewal Resolver”contract RenewalResolver is IDocumentResolver { struct RenewalConfig { uint256 expiryDate; uint256 renewalPeriod; uint256 renewalFee; bool autoRenew; }
mapping(bytes32 => RenewalConfig) private configs;
function onDocumentRegistered( bytes32 integraHash, bytes32, address, bytes calldata metadata ) external { (uint256 period, uint256 fee, bool auto) = abi.decode(metadata, (uint256, uint256, bool));
configs[integraHash] = RenewalConfig({ expiryDate: block.timestamp + period, renewalPeriod: period, renewalFee: fee, autoRenew: auto }); }
// Off-chain service calls this function checkExpiry(bytes32 integraHash) external view returns (bool expired, bool canRenew) { RenewalConfig memory config = configs[integraHash]; expired = block.timestamp > config.expiryDate; canRenew = config.autoRenew; }}Use case: Automatically track and notify about expiring licenses, permits, subscriptions.
2. Geographic Restriction Resolver
Section titled “2. Geographic Restriction Resolver”contract GeographicComplianceResolver is IDocumentResolver { mapping(bytes32 => string[]) private allowedJurisdictions;
function onDocumentRegistered( bytes32 integraHash, bytes32, address, bytes calldata metadata ) external { string[] memory jurisdictions = abi.decode(metadata, (string[]));
allowedJurisdictions[integraHash] = jurisdictions; }
function onOwnershipTransferred( bytes32 integraHash, address, address newOwner, string calldata ) external view { // Verify new owner's jurisdiction string memory ownerCountry = getCountryCode(newOwner); string[] memory allowed = allowedJurisdictions[integraHash];
bool isAllowed = false; for (uint i = 0; i < allowed.length; i++) { if (keccak256(bytes(allowed[i])) == keccak256(bytes(ownerCountry))) { isAllowed = true; break; } }
require(isAllowed, "Jurisdiction not permitted"); }}Use case: Restrict security token ownership to specific countries.
3. Multi-Sig Approval Resolver
Section titled “3. Multi-Sig Approval Resolver”contract MultiSigApprovalResolver is IDocumentResolver { mapping(bytes32 => address[]) private approvers; mapping(bytes32 => mapping(address => bool)) private hasApproved; mapping(bytes32 => uint256) private requiredApprovals;
function onOwnershipTransferred( bytes32 integraHash, address, address newOwner, string calldata ) external { // Count approvals uint256 approvalCount = 0; for (uint i = 0; i < approvers[integraHash].length; i++) { if (hasApproved[integraHash][approvers[integraHash][i]]) { approvalCount++; } }
require( approvalCount >= requiredApprovals[integraHash], "Insufficient approvals" );
// Reset approvals for next transfer _resetApprovals(integraHash); }
// Approvers call this before transfer function approve(bytes32 integraHash) external { require(_isApprover(integraHash, msg.sender), "Not approver"); hasApproved[integraHash][msg.sender] = true; emit TransferApproved(integraHash, msg.sender); }}Use case: Require board approval before transferring company shares.
4. Audit Trail Resolver
Section titled “4. Audit Trail Resolver”contract AuditTrailResolver is IDocumentResolver { event DocumentEvent( bytes32 indexed integraHash, string eventType, address actor, uint256 timestamp, bytes data );
function onDocumentRegistered( bytes32 integraHash, bytes32 documentHash, address owner, bytes calldata ) external { emit DocumentEvent( integraHash, "REGISTERED", owner, block.timestamp, abi.encode(documentHash) ); }
function onOwnershipTransferred( bytes32 integraHash, address oldOwner, address newOwner, string calldata reason ) external { emit DocumentEvent( integraHash, "TRANSFERRED", newOwner, block.timestamp, abi.encode(oldOwner, newOwner, reason) ); }}Use case: Maintain complete, immutable audit trail for compliance.
The Power of Composition
Section titled “The Power of Composition”Unlimited Combinations
Section titled “Unlimited Combinations”Mix and match resolvers for any use case:
Real Estate Sale: ├─ Primary: Title verification └─ Additional: [Escrow, Tax calculation, Deed recording]
Rental Agreement: ├─ Primary: Contact storage └─ Additional: [Rent automation, Maintenance tracking, Insurance]
Business Partnership: ├─ Primary: Multi-sig approval └─ Additional: [Profit distribution, Voting, Audit trail]
Security Token: ├─ Primary: Accreditation check └─ Additional: [Jurisdiction, Transfer limits, Reporting]
Patent License: ├─ Primary: Royalty tracking └─ Additional: [Usage monitoring, Geographic limits, Sublicensing]No Core Contract Changes
Section titled “No Core Contract Changes”Add new capabilities without upgrading document registry:
Need new feature? → Create new resolver → Register in component registry → Attach to documents
NO changes to: ✅ IntegraDocumentRegistry_Immutable ✅ Tokenizer contracts ✅ Existing documentsIntegration Example
Section titled “Integration Example”Complete example showing resolver power:
contract RealEstateManager { IntegraDocumentRegistry_Immutable public documentRegistry; IntegraRegistry_Immutable public componentRegistry;
// Resolver IDs bytes32 constant CONTACT_RESOLVER = keccak256("ContactResolver"); bytes32 constant ESCROW_RESOLVER = keccak256("EscrowResolver"); bytes32 constant TITLE_RESOLVER = keccak256("TitleVerification");
function createPropertySale( bytes32 deedHash, bytes32 ipfsCID, address buyer, uint256 salePrice ) external returns (bytes32 integraHash) { // Register deed with comprehensive resolver suite integraHash = documentRegistry.registerDocument( deedHash, ipfsCID, address(ownershipTokenizer), address(0), bytes32(0), bytes32(0), TITLE_RESOLVER, // Primary: Must verify title is clear [CONTACT_RESOLVER, ESCROW_RESOLVER] // Additional: Contact + escrow );
// Resolvers automatically: // - Title: Verify no liens (blocks if issues) // - Contact: Store seller contact info // - Escrow: Set up escrow account
// Reserve token for buyer ownershipTokenizer.reserveToken(integraHash, 0, buyer, 1, bytes32(0));
return integraHash; }}Extensibility Benefits
Section titled “Extensibility Benefits”For Platform Developers
Section titled “For Platform Developers”- Customize Behavior: Add your business logic via resolvers
- No Forks Required: Extend without modifying core contracts
- Maintainable: Update resolvers independently
- Composable: Combine multiple service providers
- Upgradeable: Resolvers can be upgraded (via UUPS)
For Ecosystem
Section titled “For Ecosystem”- Resolver Marketplace: Third parties can build resolvers
- Specialized Services: Industry-specific resolvers (real estate, healthcare, etc.)
- Innovation: New use cases enabled by new resolvers
- Interoperability: Share resolvers across applications
- Competition: Multiple implementations of same service type
For Users
Section titled “For Users”- Feature Rich: Documents have extensive functionality
- Customizable: Choose which services you need
- Future Proof: New services added without disruption
- Portable: Resolvers work across chains
- Transparent: All resolver calls logged on-chain
Summary
Section titled “Summary”Integra’s Resolver Composition Pattern:
- Extends document functionality without modifying core contracts
- Separates document identity from services (clean architecture)
- Enables unlimited customization via IDocumentResolver interface
- Supports both critical (primary) and optional (additional) services
- Protects against DOS via gas limits and graceful degradation
- Allows locking for immutability when needed
- Powers real-world contract automation and compliance
This makes Integra’s document registry a programmable platform where developers can build any document-related service imaginable while core contracts remain immutable and secure.
Learn More
Section titled “Learn More”- Document Registry - Core document management
- SimpleContactResolver - Contact resolver example
- TokenClaimResolver - EAS resolver example
- Extensibility Overview - System extensibility
- Document-Token Binding - How documents and tokens work together