Overview
The PermissionedResolver is a powerful, upgradeable resolver that supports multiple names with granular permission controls. It implements all standard ENS resolver profiles and adds advanced features like internal aliasing and per-field permissions.
This is an upgradeable contract using the UUPS proxy pattern. It requires initialization with an admin address and role bitmap.
Key Features
Fine-Grained Permissions Control access per name, per field, or globally
Internal Aliasing Redirect resolution from one name to another
Multi-Name Support Single resolver instance can serve many names
Full Profile Support Implements all standard ENS resolver interfaces
Architecture
The contract implements these interfaces:
IExtendedResolver - Extended resolution with context
IMulticallable - Batch operations
IABIResolver, IAddrResolver, IAddressResolver - Address resolution
IContentHashResolver - Content hash storage
ITextResolver - Text records
IPubkeyResolver - Public key storage
INameResolver - Reverse resolution
IInterfaceResolver - EIP-165 interface detection
IVersionableResolver - Record versioning
IERC7996 - Feature detection
Initialization
Constructor
constructor ( IHCAFactoryBasic hcaFactory )
Hierarchical Context Addressable (HCA) factory for equivalence checking
Initialize
function initialize ( address admin , uint256 roleBitmap ) external initializer
The initial admin address (cannot be zero)
The roles to grant to the admin
Example: Deploying and Initializing
import { PermissionedResolver } from "./PermissionedResolver.sol" ;
import { PermissionedResolverLib } from "./libraries/PermissionedResolverLib.sol" ;
import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol" ;
// Deploy implementation
PermissionedResolver implementation = new PermissionedResolver (hcaFactory);
// Prepare initialization data
uint256 allRoles =
PermissionedResolverLib.ROLE_SET_ADDR |
PermissionedResolverLib.ROLE_SET_TEXT |
PermissionedResolverLib.ROLE_SET_CONTENTHASH;
bytes memory initData = abi . encodeCall (
implementation.initialize,
(adminAddress, allRoles)
);
// Deploy proxy
ERC1967Proxy proxy = new ERC1967Proxy (
address (implementation),
initData
);
PermissionedResolver resolver = PermissionedResolver ( address (proxy));
Permission System
Permissions are checked across a 2x2 matrix of resources:
Any Part (*) Specific Part Any Name (*) resource(0, 0)resource(0, <part>)Specific Name resource(<namehash>, 0)resource(<namehash>, <part>)
Available Roles
ROLE_SET_ADDR // Set addresses for coin types
ROLE_SET_TEXT // Set text records
ROLE_SET_CONTENTHASH // Set content hash
ROLE_SET_PUBKEY // Set public key
ROLE_SET_ABI // Set ABI data
ROLE_SET_INTERFACE // Set interface implementer
ROLE_SET_NAME // Set name (reverse record)
ROLE_SET_ALIAS // Set internal aliases
ROLE_CLEAR // Clear all records for a name
ROLE_UPGRADE // Upgrade contract implementation
Each role has a corresponding admin role (shifted left by 128 bits).
Permission Parts
Fine-grained control is achieved using parts:
// Restrict to specific coin type
bytes32 part = PermissionedResolverLib. addrPart ( 60 ); // Ethereum
// Restrict to specific text key
bytes32 part = PermissionedResolverLib. textPart ( "avatar" );
Example: Granting Specific Permissions
import { EnhancedAccessControl } from "./access-control/EnhancedAccessControl.sol" ;
bytes32 node = namehash ( "alice.eth" );
// Allow user to set ANY address for alice.eth
uint256 resource = PermissionedResolverLib. resource (node, 0 );
resolver. grantRoles (resource, PermissionedResolverLib.ROLE_SET_ADDR, user);
// Allow user to set ONLY the avatar text for alice.eth
bytes32 avatarPart = PermissionedResolverLib. textPart ( "avatar" );
resource = PermissionedResolverLib. resource (node, avatarPart);
resolver. grantRoles (resource, PermissionedResolverLib.ROLE_SET_TEXT, user);
// Allow user to set Bitcoin address for ANY name
bytes32 btcPart = PermissionedResolverLib. addrPart ( 0 ); // Bitcoin
resource = PermissionedResolverLib. resource ( 0 , btcPart);
resolver. grantRoles (resource, PermissionedResolverLib.ROLE_SET_ADDR, operator);
Internal Aliasing
Internal aliasing allows resolution to be redirected from one name to another within the same resolver.
How Aliasing Works
Longest Match
The resolver finds the longest matching suffix in the alias mapping
Suffix Rewrite
The matched suffix is replaced with the alias target
Recursive Check
The result is checked for additional aliases
Cycle Handling
Length-1 cycles apply once; length-2+ cycles cause out-of-gas
setAlias
function setAlias (
bytes calldata fromName ,
bytes calldata toName
) external
Destination DNS-encoded name
Requires ROLE_SET_ALIAS on the root resource.
Alias Examples
// Set up alias from a.eth to b.eth
resolver. setAlias (
abi . encodePacked ( uint8 ( 1 ), "a" , uint8 ( 3 ), "eth" , uint8 ( 0 )),
abi . encodePacked ( uint8 ( 1 ), "b" , uint8 ( 3 ), "eth" , uint8 ( 0 ))
);
// Now resolution works as follows:
// getAlias("a.eth") => "b.eth"
// getAlias("sub.a.eth") => "sub.b.eth"
// getAlias("x.y.a.eth") => "x.y.b.eth"
// getAlias("abc.eth") => "" (no match)
getAlias
function getAlias ( bytes memory fromName ) public view returns ( bytes memory toName )
Destination DNS-encoded name, or empty if not aliased
Record Management
Setting Records
All setter functions follow the same permission model and emit appropriate events.
Address Records
// Set Ethereum address
function setAddr ( bytes32 node , address addr_ ) external
// Set address for any coin type
function setAddr (
bytes32 node ,
uint256 coinType ,
bytes memory addressBytes
) public
Example: Setting Multiple Addresses
bytes32 node = namehash ( "alice.eth" );
// Set Ethereum mainnet address
resolver. setAddr (node, 0x1234567890123456789012345678901234567890 );
// Set Bitcoin address
resolver. setAddr (node, 0 , hex "0014..." );
// Set Linea address (EVM chain 59144)
resolver. setAddr (node, ENSIP19. coinTypeFromChain ( 59144 ), abi . encodePacked (lineaAddress));
Text Records
function setText (
bytes32 node ,
string calldata key ,
string calldata value
) external
Example: Setting Text Records
bytes32 node = namehash ( "alice.eth" );
// Set avatar
resolver. setText (node, "avatar" , "https://example.com/avatar.png" );
// Set description
resolver. setText (node, "description" , "Alice's ENS profile" );
// Set social links
resolver. setText (node, "com.twitter" , "alice" );
resolver. setText (node, "com.github" , "alice" );
Content Hash
function setContenthash (
bytes32 node ,
bytes calldata hash
) external
Example: Setting IPFS Content
// IPFS hash: ipfs://QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco
bytes memory contentHash = hex "e3010170122029f2d17be6139079dc48696d1f582a8530eb9805b561eda517e22a892c7e3f1f" ;
resolver. setContenthash (node, contentHash);
Public Key
function setPubkey (
bytes32 node ,
bytes32 x ,
bytes32 y
) external
ABI
function setABI (
bytes32 node ,
uint256 contentType ,
bytes calldata data
) external
contentType must be a power of 2 (1, 2, 4, 8, etc.). Common values:
1 - JSON
2 - zlib-compressed JSON
4 - CBOR
8 - URI
Interface Implementer
function setInterface (
bytes32 node ,
bytes4 interfaceId ,
address implementer
) external
Name (Reverse Record)
function setName (
bytes32 node ,
string calldata primary
) external
Clearing Records
function clearRecords ( bytes32 node ) external
Increments the version counter, effectively clearing all records for the node.
This cannot be undone. All records (addresses, text, contenthash, etc.) become inaccessible.
Reading Records
Address Resolution
// Ethereum address
function addr ( bytes32 node ) public view returns ( address payable )
// Multi - coin address
function addr ( bytes32 node , uint256 coinType ) public view returns ( bytes memory )
// Check if address exists
function hasAddr ( bytes32 node , uint256 coinType ) external view returns ( bool )
Example: Multi-Chain Address Lookup
bytes32 node = namehash ( "alice.eth" );
// Get Ethereum address
address ethAddr = resolver. addr (node);
// Get Bitcoin address
bytes memory btcAddr = resolver. addr (node, 0 );
// Check if Polygon address is set
bool hasPolygon = resolver. hasAddr (node, ENSIP19. coinTypeFromChain ( 137 ));
Text Records
function text ( bytes32 node , string calldata key ) external view returns ( string memory )
Other Records
function contenthash ( bytes32 node ) external view returns ( bytes memory )
function pubkey ( bytes32 node ) external view returns ( bytes32 x , bytes32 y )
function ABI (
bytes32 node ,
uint256 contentTypes
) external view returns ( uint256 contentType , bytes memory data )
function interfaceImplementer (
bytes32 node ,
bytes4 interfaceId
) external view returns ( address )
function name ( bytes32 node ) external view returns ( string memory )
function recordVersions ( bytes32 node ) external view returns ( uint64 )
Extended Resolution
The resolve function enables gasless off-chain resolution with automatic aliasing.
function resolve (
bytes calldata fromName ,
bytes calldata fromData
) external view returns ( bytes memory )
DNS-encoded name being resolved
Encoded function call (resolver profile query)
ABI-encoded result of the resolution
Resolution Flow
Example: Off-Chain Resolution
import { IAddrResolver } from "@ens/contracts/resolvers/profiles/IAddrResolver.sol" ;
// Prepare the resolver query
bytes memory name = dnsEncode ( "alice.eth" );
bytes memory query = abi . encodeCall (
IAddrResolver.addr,
( namehash ( "alice.eth" ))
);
// Resolve (works even if aliased)
bytes memory result = resolver. resolve (name, query);
address ethAddress = abi . decode (result, ( address ));
Multicall Support
Batch multiple operations in a single transaction.
function multicall ( bytes [] calldata calls ) public returns ( bytes [] memory results )
function multicallWithNodeCheck (
bytes32 /* node */,
bytes [] calldata calls
) external returns ( bytes [] memory )
multicallWithNodeCheck ignores the node parameter since there’s no trusted operator in this design. It behaves identically to multicall.
Example: Batch Setting Records
bytes32 node = namehash ( "alice.eth" );
bytes [] memory calls = new bytes []( 3 );
calls[ 0 ] = abi . encodeCall (resolver.setAddr, (node, aliceAddress));
calls[ 1 ] = abi . encodeCall (resolver.setText, (node, "avatar" , "https://..." ));
calls[ 2 ] = abi . encodeCall (resolver.setText, (node, "com.twitter" , "alice" ));
resolver. multicall (calls);
Upgradeability
The contract uses the UUPS upgradeable pattern.
function _authorizeUpgrade ( address newImplementation ) internal override
Only addresses with ROLE_UPGRADE on the root resource can upgrade the contract.
Example: Upgrading
// Deploy new implementation
PermissionedResolver newImpl = new PermissionedResolver (hcaFactory);
// Upgrade (requires ROLE_UPGRADE)
UUPSUpgradeable ( address (resolver)). upgradeToAndCall (
address (newImpl),
"" // no initialization needed
);
Events
All standard ENS resolver events plus:
event AliasChanged (
bytes indexed indexedFromName ,
bytes indexed indexedToName ,
bytes fromName ,
bytes toName
);
event VersionChanged ( bytes32 indexed node , uint64 newVersion );
Errors
error UnsupportedResolverProfile ( bytes4 selector);
error InvalidEVMAddress ( bytes addressBytes);
error InvalidContentType ( uint256 contentType);
Gas Optimization Tips
Use Multicall for Batch Operations
Setting multiple records in one transaction saves on base transaction costs.
Minimize Permission Checks
Grant permissions at the name level (not field level) when possible.
Each alias lookup costs gas. Keep alias chains short.
Use clearRecords Sparingly
This increments the version and orphans storage, which is expensive.
Security Considerations
Admin Keys : The initial admin has full control. Use a multisig or governance contract.
Upgrade Authority : The ROLE_UPGRADE holder can change contract logic. Protect this carefully.
HCA Context : The contract uses Hierarchical Context Addressable addressing for permission checks. Ensure the HCA factory is properly configured.
Permission Granularity : Start with broader permissions and narrow down as needed. It’s easier to restrict than to grant after the fact.
Complete Integration Example
pragma solidity ^0.8.13 ;
import { PermissionedResolver } from "./PermissionedResolver.sol" ;
import { PermissionedResolverLib } from "./libraries/PermissionedResolverLib.sol" ;
import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol" ;
contract ProfileManager {
PermissionedResolver public resolver;
constructor ( address hcaFactory ) {
// Deploy implementation
PermissionedResolver impl = new PermissionedResolver ( IHCAFactoryBasic (hcaFactory));
// Deploy proxy with initialization
bytes memory initData = abi . encodeCall (
impl.initialize,
( address ( this ), type ( uint256 ).max) // grant all roles
);
ERC1967Proxy proxy = new ERC1967Proxy ( address (impl), initData);
resolver = PermissionedResolver ( address (proxy));
}
function setupUserProfile ( bytes32 node , address user ) external {
// Grant user permission to manage their profile
uint256 resource = PermissionedResolverLib. resource (node, 0 );
uint256 roles =
PermissionedResolverLib.ROLE_SET_ADDR |
PermissionedResolverLib.ROLE_SET_TEXT |
PermissionedResolverLib.ROLE_SET_CONTENTHASH;
resolver. grantRoles (resource, roles, user);
}
function setUserProfile (
bytes32 node ,
address addr ,
string memory avatar ,
string memory twitter
) external {
bytes [] memory calls = new bytes []( 3 );
calls[ 0 ] = abi . encodeCall (resolver.setAddr, (node, addr));
calls[ 1 ] = abi . encodeCall (resolver.setText, (node, "avatar" , avatar));
calls[ 2 ] = abi . encodeCall (resolver.setText, (node, "com.twitter" , twitter));
resolver. multicall (calls);
}
}
PermissionedResolverLib Storage layout and helper functions
EnhancedAccessControl Role-based access control
ResolverProfileRewriterLib Node rewriting utilities