// SPDX-License-Identifier: MIT
// Resolver contract for ENS that allows for the setting of addresses and text records for both specified and wildcard resolution.
// This contract implements the resolution interfaces defined in ENSIP-1, ENSIP-5, ENSIP-9 and ENSIP-10.
// Built by clowes.eth for Unruggable
// 13th September 2024
// Across the contract coinType is as defined in ENSIP-11 https://docs.ens.domains/ensip/11
pragma solidity ^0.8.25;
contract UnruggableMappingsResolver {
//ENSIP-1 - https://docs.ens.domains/ensip/1
event AddrChanged(bytes32 indexed node, address a);
//ENSIP-9 - https://docs.ens.domains/ensip/9
event AddressChanged(bytes32 indexed node, uint coinType, bytes newAddress);
//Not standardised => https://docs.ens.domains/resolvers/interfaces
event TextChanged(
bytes32 indexed node,
string indexed indexedKey,
string key,
string value
);
address owner;
mapping(bytes32 node => mapping(uint256 coinType => bytes resolutionAddress)) private namehashToAddress;
mapping(bytes32 node => bool isDisabled) private disableEthDefault;
mapping(bytes32 node => mapping(string key => string value)) private textMapping;
uint constant private COIN_TYPE_ETH = 60;
// ERC-165 => Standard Interface Detection
// supportsInterface(bytes4)
bytes4 constant private INTERFACE_ID_ERC_165 = 0x01ffc9a7;
// The addr Interface defined in ENSIP-1 => ENS
// addr(bytes32 node)
bytes4 constant private INTERFACE_ID_ADDR = 0x3b3b57de;
// ENSIP-9 => Multichain Address resolution
// addr(bytes32 node, uint coinType)
// https://ethtools.com/interface-database/ENSIP-9
bytes4 constant private INTERFACE_ID_ADDR_ENSIP_9 = 0xf1cb7e06;
// ENSIP-10 => Wildcard Resolution
// resolve(bytes calldata name, bytes calldata data) external view returns(bytes)
// https://ethtools.com/interface-database/IExtendedResolver
bytes4 constant private INTERFACE_ID_ENSIP_10 = 0x9061b923;
// ENSIP-5 => Text Records
// text(bytes32 node, string calldata key)
// https://ethtools.com/interface-database/ENSIP-5
bytes4 constant private INTERFACE_ID_TEXT = 0x59d1d43c;
modifier onlyOwner() {
require(msg.sender == owner, "Unauthorized");
_;
}
constructor() {
owner = msg.sender;
}
/**
* Implemented as part of the ERC165 interface => Standard Interface Detection
* https://ethtools.com/interface-database/ERC165
* https://eips.ethereum.org/EIPS/eip-165
*/
function supportsInterface(bytes4 interfaceID) external pure returns (bool) {
return interfaceID == INTERFACE_ID_ADDR
|| interfaceID == INTERFACE_ID_ADDR_ENSIP_9
|| interfaceID == INTERFACE_ID_ERC_165
|| interfaceID == INTERFACE_ID_ENSIP_10
|| interfaceID == INTERFACE_ID_TEXT;
}
/*************
** Setters **
*************/
function setAddr(bytes32 node, address addr) public onlyOwner {
setAddr(node, addr, COIN_TYPE_ETH);
emit AddrChanged(node, addr);
}
function setAddr(bytes32 node, address addr, uint coinType) public onlyOwner {
namehashToAddress[node][coinType] = addressToBytes(addr);
emit AddressChanged(node, coinType, namehashToAddress[node][coinType]);
}
function setAddr(bytes32 node, address addr, uint[] calldata coinTypes) public onlyOwner {
for (uint i = 0; i < coinTypes.length; i++) {
setAddr(node, addr, coinTypes[i]);
}
}
function setText(bytes32 node, string memory key, string memory value) public onlyOwner {
textMapping[node][key] = value;
emit TextChanged(node, key, key, value);
}
function setIsEthDefaultDisabled(bytes32 node, bool isIt) public onlyOwner {
disableEthDefault[node] = isIt;
}
/*************
** Getters **
*************/
/**
* Implemented as part of the addr interface defined in ENSIP-1 => ENS
* ENSIP-1 - https://docs.ens.domains/ensip/1
* https://ethtools.com/interface-database/ENSIP-1-addr
*/
function addr(bytes32 node) public view returns (address) {
bytes memory a = addr(node, COIN_TYPE_ETH);
if(a.length == 0) {
return address(0);
}
return bytesToAddress(a);
}
/**
* Implemented as part of the interface defined in ENSIP-9 => Multichain Address resolution
* ENSIP-9 - https://docs.ens.domains/ensip/9 AND ENSIP-11 - https://docs.ens.domains/ensip/11
* https://ethtools.com/interface-database/ENSIP-9
*/
function addr(bytes32 node, uint coinType) public view returns (bytes memory) {
bytes memory specifiedAddress = namehashToAddress[node][coinType];
if (specifiedAddress.length == 0) {
if (coinType != COIN_TYPE_ETH && !disableEthDefault[node]) {
specifiedAddress = namehashToAddress[node][COIN_TYPE_ETH];
}
}
return specifiedAddress;
}
/**
* Implemented as part of the interface specification defined in ENSIP-5 => Text Records
* ENSIP-5 - https://docs.ens.domains/ensip/5 and ENSIP-18 - https://docs.ens.domains/ensip/18
* https://ethtools.com/interface-database/ENSIP-5
*/
function text(
bytes32 node,
string memory key
) public view returns (string memory) {
return textMapping[node][key];
}
/**
* Implemented as part of the IExtendedResolver interface defined in ENSIP-10 => Wildcard Resolution
* https://ethtools.com/interface-database/IExtendedResolver
* ENSIP-10 - https://docs.ens.domains/ensip/10
*/
function resolve(bytes calldata name, bytes calldata data) external view returns(bytes memory) {
bytes4 selector = bytes4(data[:4]);
//These interfaces only have one method so interface ID === selector ID
if (selector == INTERFACE_ID_ADDR) {
(bytes32 node) = abi.decode(data[4:], (bytes32));
return abi.encode(addr(node));
} else if (selector == INTERFACE_ID_ADDR_ENSIP_9) {
(bytes32 node, uint256 coinType) = abi.decode(data[4:], (bytes32, uint256));
return abi.encode(addr(node, coinType));
} else if (selector == INTERFACE_ID_TEXT) {
(bytes32 node, string memory key) = abi.decode(data[4:], (bytes32, string));
string memory value = text(node, key);
return abi.encode(value);
}
revert("Unsupported");
}
/*************
** Helpers **
*************/
function bytesToAddress(bytes memory b) internal pure returns(address payable a) {
require(b.length == 20);
assembly {
a := div(mload(add(b, 32)), exp(256, 12))
}
}
function addressToBytes(address a) internal pure returns(bytes memory b) {
b = new bytes(20);
assembly {
mstore(add(b, 32), mul(a, exp(256, 12)))
}
}
}
{
"compilationTarget": {
"contracts/UnruggableMappingsResolver.sol": "UnruggableMappingsResolver"
},
"evmVersion": "paris",
"libraries": {},
"metadata": {
"bytecodeHash": "ipfs"
},
"optimizer": {
"enabled": true,
"runs": 200
},
"remappings": [
":@eth-optimism/=lib/unruggable-gateways/lib/optimism/packages/",
":@openzeppelin/contracts/=lib/unruggable-gateways/lib/openzeppelin-contracts/contracts/",
":@unruggable/=lib/unruggable-gateways/",
":ds-test/=lib/unruggable-gateways/lib/openzeppelin-contracts/lib/forge-std/lib/ds-test/src/",
":erc4626-tests/=lib/unruggable-gateways/lib/openzeppelin-contracts/lib/erc4626-tests/",
":forge-std/=lib/unruggable-gateways/lib/forge-std/src/",
":openzeppelin-contracts/=lib/unruggable-gateways/lib/openzeppelin-contracts/",
":optimism/=lib/unruggable-gateways/lib/optimism/packages/contracts-bedrock/src/",
":unruggable-gateways/=lib/unruggable-gateways/contracts/"
]
}
[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":false,"internalType":"address","name":"a","type":"address"}],"name":"AddrChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":false,"internalType":"uint256","name":"coinType","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"newAddress","type":"bytes"}],"name":"AddressChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":true,"internalType":"string","name":"indexedKey","type":"string"},{"indexed":false,"internalType":"string","name":"key","type":"string"},{"indexed":false,"internalType":"string","name":"value","type":"string"}],"name":"TextChanged","type":"event"},{"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"}],"name":"addr","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"uint256","name":"coinType","type":"uint256"}],"name":"addr","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"name","type":"bytes"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"resolve","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"address","name":"addr","type":"address"},{"internalType":"uint256[]","name":"coinTypes","type":"uint256[]"}],"name":"setAddr","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"address","name":"addr","type":"address"}],"name":"setAddr","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"address","name":"addr","type":"address"},{"internalType":"uint256","name":"coinType","type":"uint256"}],"name":"setAddr","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"bool","name":"isIt","type":"bool"}],"name":"setIsEthDefaultDisabled","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"string","name":"key","type":"string"},{"internalType":"string","name":"value","type":"string"}],"name":"setText","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceID","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"string","name":"key","type":"string"}],"name":"text","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"}]