Overview
The PermissionedResolver is a powerful, upgradeable resolver that supports multiple ENS names with fine-grained permission control and internal aliasing. It implements all standard ENS resolver profiles and adds advanced features like versioning, aliasing, and resource-level permissions.
Key Features:
- Multi-name support with per-name versioning
- Internal name aliasing with recursive resolution
- Fine-grained role-based access control per resource
- Upgradeable via UUPS proxy pattern
- Full ENS resolver profile support
Implements: IExtendedResolver, IMulticallable, IABIResolver, IAddrResolver, IAddressResolver, IContentHashResolver, IHasAddressResolver, IInterfaceResolver, INameResolver, IPubkeyResolver, ITextResolver, IVersionableResolver, IERC7996
Constructor
constructor(IHCAFactoryBasic hcaFactory)
Initializes the resolver implementation. Note: This is an upgradeable contract, use initialize() after deployment.
HCA (Hierarchical Contract Authentication) factory for access control
Initialization
initialize
function initialize(address admin, uint256 roleBitmap) external
Initializes a new PermissionedResolver proxy instance.
The initial administrator address (cannot be address(0))
Bitmap of roles to grant to the admin. Common patterns:
- Full admin:
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF (all roles)
- Basic management:
ROLE_SET_ADDR | ROLE_SET_TEXT | ROLE_SET_CONTENTHASH
Example:
// Deploy and initialize
PermissionedResolver implementation = new PermissionedResolver(hcaFactory);
ERC1967Proxy proxy = new ERC1967Proxy(
address(implementation),
abi.encodeCall(implementation.initialize, (adminAddress, type(uint256).max))
);
PermissionedResolver resolver = PermissionedResolver(address(proxy));
Permission System
The resolver uses a hierarchical permission system with resource-based access control. Each setter function checks permissions against 4 possible resources:
Resource Hierarchy
| Resource Type | Description | Resource ID |
|---|
| Global + Any Part | Wildcard access to all names and parts | resource(0, 0) |
| Global + Specific Part | Access to specific part across all names | resource(0, <part>) |
| Specific Name + Any Part | Access to all parts of one name | resource(<namehash>, 0) |
| Specific Name + Specific Part | Access to specific part of one name | resource(<namehash>, <part>) |
Permission Roles
// Write operation roles
ROLE_SET_ADDR = 1 << 0 // Set addresses
ROLE_SET_TEXT = 1 << 4 // Set text records
ROLE_SET_CONTENTHASH = 1 << 8 // Set contenthash
ROLE_SET_PUBKEY = 1 << 12 // Set public key
ROLE_SET_ABI = 1 << 16 // Set ABI data
ROLE_SET_INTERFACE = 1 << 20 // Set interface implementer
ROLE_SET_NAME = 1 << 24 // Set reverse record name
ROLE_SET_ALIAS = 1 << 28 // Create aliases (root only)
ROLE_CLEAR = 1 << 32 // Clear all records
ROLE_UPGRADE = 1 << 124 // Upgrade contract
// Each role has a corresponding admin role at +128 bits
ROLE_SET_ADDR_ADMIN = ROLE_SET_ADDR << 128
Part Identifiers
For fine-grained control, you can restrict access to specific sub-resources:
// Address part for specific coin type
bytes32 part = PermissionedResolverLib.addrPart(60); // ETH coin type
// Text part for specific key
bytes32 part = PermissionedResolverLib.textPart("avatar");
// Any part
bytes32 part = 0;
Write Functions
clearRecords
function clearRecords(bytes32 node) external
Clears all records for a node by incrementing its version number. Previous version data becomes inaccessible.
The namehash of the name to clear
Permissions Required: ROLE_CLEAR on resource(node, 0), resource(0, 0), or root
Events Emitted:
VersionChanged(bytes32 indexed node, uint64 newVersion)
Example:
bytes32 node = namehash("vitalik.eth");
resolver.clearRecords(node);
// All previous records are now inaccessible
setAlias
function setAlias(
bytes calldata fromName,
bytes calldata toName
) external
Creates an internal alias that rewrites name resolution.
The source DNS-encoded name (e.g., DNS-encoded “alice.eth”)
The destination DNS-encoded name (e.g., DNS-encoded “bob.eth”)
Permissions Required: ROLE_SET_ALIAS on root resource
Events Emitted:
AliasChanged(bytes indexed indexedFromName, bytes indexed indexedToName, bytes fromName, bytes toName)
Aliasing Behavior:
- Matches longest suffix and rewrites
- Recursive aliasing is supported
- Cycles of length 1 apply once
- Cycles of length 2+ result in out-of-gas
Example:
// Create alias: alice.eth -> bob.eth
resolver.setAlias(dnsEncode("alice.eth"), dnsEncode("bob.eth"));
// Effects:
// resolve("alice.eth") -> resolves as "bob.eth"
// resolve("sub.alice.eth") -> resolves as "sub.bob.eth"
// resolve("x.y.alice.eth") -> resolves as "x.y.bob.eth"
setAddr (Ethereum)
function setAddr(bytes32 node, address addr_) external
Sets the Ethereum mainnet address for a name. Equivalent to setAddr(node, 60, bytes) with special handling.
The Ethereum address (address(0) is stored as empty bytes)
Permissions Required: ROLE_SET_ADDR with appropriate part/resource
Events Emitted:
AddressChanged(bytes32 indexed node, uint256 coinType, bytes newAddress)
AddrChanged(bytes32 indexed node, address a) (for ETH coin type only)
setAddr (Multi-coin)
function setAddr(
bytes32 node,
uint256 coinType,
bytes memory addressBytes
) public
Sets the address for any cryptocurrency coin type (SLIP-0044).
The SLIP-0044 coin type (60 for ETH, 0 for Bitcoin, etc.)
The encoded address bytes (must be 0 or 20 bytes for EVM chains)
Permissions Required: ROLE_SET_ADDR on:
resource(node, addrPart(coinType))
resource(0, addrPart(coinType))
resource(node, 0)
resource(0, 0)
Events Emitted:
AddressChanged(bytes32 indexed node, uint256 coinType, bytes newAddress)
AddrChanged(bytes32 indexed node, address a) (if coinType == 60)
Example:
// Set Ethereum address
resolver.setAddr(node, 60, abi.encodePacked(ethAddress));
// Set Bitcoin address
resolver.setAddr(node, 0, bitcoinAddressBytes);
// Set Linea address (EVM chain 59144)
resolver.setAddr(node, 0x800000000000e39990, abi.encodePacked(lineaAddress));
setText
function setText(
bytes32 node,
string calldata key,
string calldata value
) external
Sets a text record for a name.
The text record key (e.g., “avatar”, “description”, “url”, “email”)
Permissions Required: ROLE_SET_TEXT on:
resource(node, textPart(key))
resource(0, textPart(key))
resource(node, 0)
resource(0, 0)
Events Emitted:
TextChanged(bytes32 indexed node, string indexed indexedKey, string key, string value)
Example:
// Set standard text records
resolver.setText(node, "avatar", "https://example.com/avatar.png");
resolver.setText(node, "description", "ENS developer");
resolver.setText(node, "url", "https://example.com");
resolver.setText(node, "email", "[email protected]");
// Set custom text records
resolver.setText(node, "com.twitter", "@username");
resolver.setText(node, "com.github", "username");
setContenthash
function setContenthash(
bytes32 node,
bytes calldata hash
) external
Sets the content hash for a name (IPFS, IPNS, Swarm, etc.).
The contenthash bytes (ENSIP-7 format)
Permissions Required: ROLE_SET_CONTENTHASH
Events Emitted:
ContenthashChanged(bytes32 indexed node, bytes hash)
Example:
// Set IPFS contenthash
bytes memory ipfsHash = hex"e301..." // ENSIP-7 encoded
resolver.setContenthash(node, ipfsHash);
setPubkey
function setPubkey(
bytes32 node,
bytes32 x,
bytes32 y
) external
Sets the SECP256k1 public key for a name.
The x coordinate of the public key
The y coordinate of the public key
Permissions Required: ROLE_SET_PUBKEY
Events Emitted:
PubkeyChanged(bytes32 indexed node, bytes32 x, bytes32 y)
setName
function setName(
bytes32 node,
string calldata primary
) external
Sets the reverse record name for a node.
Permissions Required: ROLE_SET_NAME
Events Emitted:
NameChanged(bytes32 indexed node, string name)
setABI
function setABI(
bytes32 node,
uint256 contentType,
bytes calldata data
) external
Sets ABI data for a contract.
The content type (must be a power of 2: 1, 2, 4, 8…)
Permissions Required: ROLE_SET_ABI
Events Emitted:
ABIChanged(bytes32 indexed node, uint256 indexed contentType)
setInterface
function setInterface(
bytes32 node,
bytes4 interfaceId,
address implementer
) external
Sets the implementer of an EIP-165 interface.
The EIP-165 interface identifier
The address of the contract implementing the interface
Permissions Required: ROLE_SET_INTERFACE
Events Emitted:
InterfaceChanged(bytes32 indexed node, bytes4 indexed interfaceID, address implementer)
multicall
function multicall(bytes[] calldata calls) public returns (bytes[] memory results)
Executes multiple write operations in a single transaction. Reverts with the first error encountered.
Array of ABI-encoded function calls to execute
Array of return values from each call
Example:
// Set multiple records in one transaction
bytes[] memory calls = new bytes[](3);
calls[0] = abi.encodeCall(resolver.setAddr, (node, ethAddress));
calls[1] = abi.encodeCall(resolver.setText, (node, "avatar", "https://..."));
calls[2] = abi.encodeCall(resolver.setContenthash, (node, ipfsHash));
resolver.multicall(calls);
multicallWithNodeCheck
function multicallWithNodeCheck(
bytes32,
bytes[] calldata calls
) external returns (bytes[] memory)
Alias for multicall(). The node parameter is ignored since there is no trusted operator concept.
Array of ABI-encoded function calls
Read Functions
resolve
function resolve(
bytes calldata fromName,
bytes calldata fromData
) external view returns (bytes memory)
Resolves a query with aliasing support. Implements IExtendedResolver.
The DNS-encoded name being queried
The ABI-encoded resolver call data
The ABI-encoded response data
Example:
// Query through resolve interface
bytes memory query = abi.encodeCall(IAddrResolver.addr, (node));
bytes memory result = resolver.resolve(dnsName, query);
address ethAddr = abi.decode(result, (address));
getAlias
function getAlias(bytes memory fromName) public view returns (bytes memory toName)
Determines the final name after all aliasing is applied.
The source DNS-encoded name
The destination DNS-encoded name, or empty if not aliased
Example:
bytes memory aliased = resolver.getAlias(dnsEncode("alice.eth"));
if (aliased.length > 0) {
// Name is aliased to 'aliased'
}
addr (Ethereum)
function addr(bytes32 node) public view returns (address payable)
Gets the Ethereum address for a name.
The Ethereum address, or address(0) if not set
addr (Multi-coin)
function addr(
bytes32 node,
uint256 coinType
) public view returns (bytes memory addressBytes)
Gets the address for any coin type.
The encoded address bytes. For EVM chains, falls back to default EVM address (coin type with chain ID 0) if specific chain address is not set.
text
function text(
bytes32 node,
string calldata key
) external view returns (string memory)
Gets a text record value.
The text record value, or empty string if not set
contenthash
function contenthash(bytes32 node) external view returns (bytes memory)
Gets the content hash for a name.
The contenthash bytes, or empty if not set
pubkey
function pubkey(
bytes32 node
) external view returns (bytes32 x, bytes32 y)
Gets the SECP256k1 public key.
name
function name(bytes32 node) external view returns (string memory)
Gets the reverse record name.
ABI
function ABI(
bytes32 node,
uint256 contentTypes
) external view returns (uint256 contentType, bytes memory data)
Gets ABI data for requested content types.
Bitmap of acceptable content types
The matched content type, or 0 if none found
hasAddr
function hasAddr(
bytes32 node,
uint256 coinType
) external view returns (bool)
Checks if an address is set for a coin type.
True if address is set, false otherwise
interfaceImplementer
function interfaceImplementer(
bytes32 node,
bytes4 interfaceId
) external view returns (address implementer)
Gets the implementer of an interface.
The implementer address, or address(0) if not set
recordVersions
function recordVersions(bytes32 node) external view returns (uint64)
Gets the current version number for a node’s records.
The current version number (incremented by clearRecords)
supportsInterface
function supportsInterface(bytes4 interfaceId) public view returns (bool)
EIP-165 interface detection.
supportsFeature
function supportsFeature(bytes4 feature) external pure returns (bool)
ERC-7996 feature detection. Returns true for RESOLVE_MULTICALL.
Events
AliasChanged
event AliasChanged(
bytes indexed indexedFromName,
bytes indexed indexedToName,
bytes fromName,
bytes toName
)
Emitted when an alias is created or modified.
Standard ENS Events
The contract also emits all standard ENS resolver events:
AddressChanged(bytes32 indexed node, uint256 coinType, bytes newAddress)
AddrChanged(bytes32 indexed node, address a)
TextChanged(bytes32 indexed node, string indexed indexedKey, string key, string value)
ContenthashChanged(bytes32 indexed node, bytes hash)
PubkeyChanged(bytes32 indexed node, bytes32 x, bytes32 y)
NameChanged(bytes32 indexed node, string name)
ABIChanged(bytes32 indexed node, uint256 indexed contentType)
InterfaceChanged(bytes32 indexed node, bytes4 indexed interfaceID, address implementer)
VersionChanged(bytes32 indexed node, uint64 newVersion)
Errors
UnsupportedResolverProfile
error UnsupportedResolverProfile(bytes4 selector)
Thrown when an unsupported resolver function is called.
Error Selector: 0x7b1c461b
InvalidEVMAddress
error InvalidEVMAddress(bytes addressBytes)
Thrown when setting an EVM address that is not 0 or 20 bytes.
Error Selector: 0x8d666f60
InvalidContentType
error InvalidContentType(uint256 contentType)
Thrown when ABI content type is not a power of 2.
Error Selector: 0x5742bb26
Complete Integration Example
// Deploy and configure a PermissionedResolver
contract ResolverManager {
PermissionedResolver public resolver;
function deployResolver(address hcaFactory, address admin) external {
// Deploy implementation
PermissionedResolver impl = new PermissionedResolver(IHCAFactoryBasic(hcaFactory));
// Deploy proxy
ERC1967Proxy proxy = new ERC1967Proxy(
address(impl),
abi.encodeCall(impl.initialize, (admin, type(uint256).max))
);
resolver = PermissionedResolver(address(proxy));
}
function setupName(bytes32 node, address owner) external {
// Grant owner full control over their name
uint256 roles = PermissionedResolverLib.ROLE_SET_ADDR |
PermissionedResolverLib.ROLE_SET_TEXT |
PermissionedResolverLib.ROLE_SET_CONTENTHASH;
uint256 resource = PermissionedResolverLib.resource(node, 0);
resolver.grantRoles(resource, roles, owner);
}
function grantTextPermission(
bytes32 node,
address user,
string memory textKey
) external {
// Grant permission to set a specific text key
bytes32 part = PermissionedResolverLib.textPart(textKey);
uint256 resource = PermissionedResolverLib.resource(node, part);
resolver.grantRoles(resource, PermissionedResolverLib.ROLE_SET_TEXT, user);
}
}
- PermissionedResolverLib: Storage layout and permission helper functions
- EnhancedAccessControl: Role-based access control system
- HCAContext: Hierarchical Contract Authentication context