// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import "../../resolvers/profiles/IExtendedDNSResolver.sol";
import "../../resolvers/profiles/IAddressResolver.sol";
import "../../resolvers/profiles/IAddrResolver.sol";
import "../../resolvers/profiles/ITextResolver.sol";
import "../../utils/HexUtils.sol";
import "../../utils/BytesUtils.sol";
/// @dev Resolves names on ENS by interpreting record data stored in a DNS TXT record.
/// This resolver implements the IExtendedDNSResolver interface, meaning that when
/// a DNS name specifies it as the resolver via a TXT record, this resolver's
/// resolve() method is invoked, and is passed any additional information from that
/// text record. This resolver implements a simple text parser allowing a variety
/// of records to be specified in text, which will then be used to resolve the name
/// in ENS.
///
/// To use this, set a TXT record on your DNS name in the following format:
/// ENS1
///
/// For example:
/// ENS1 2.dnsname.ens.eth a[60]=0x1234...
///
/// The record data consists of a series of key=value pairs, separated by spaces. Keys
/// may have an optional argument in square brackets, and values may be either unquoted
/// - in which case they may not contain spaces - or single-quoted. Single quotes in
/// a quoted value may be backslash-escaped.
///
///
/// ┌────────┐
/// │ ┌───┐ │
/// ┌──────────────────────────────┴─┤" "│◄─┴────────────────────────────────────────┐
/// │ └───┘ │
/// │ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌────────────┐ ┌───┐ │
/// ^─┴─►│key├─┬─►│"["├───►│arg├───►│"]"├─┬─►│"="├─┬─►│"'"├───►│quoted_value├───►│"'"├─┼─$
/// └───┘ │ └───┘ └───┘ └───┘ │ └───┘ │ └───┘ └────────────┘ └───┘ │
/// └──────────────────────────┘ │ ┌──────────────┐ │
/// └─────────►│unquoted_value├─────────┘
/// └──────────────┘
///
/// Record types:
/// - a[] - Specifies how an `addr()` request should be resolved for the specified
/// `coinType`. Ethereum has `coinType` 60. The value must be 0x-prefixed hexadecimal, and will
/// be returned unmodified; this means that non-EVM addresses will need to be translated
/// into binary format and then encoded in hex.
/// Examples:
/// - a[60]=0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7
/// - a[0]=0x00149010587f8364b964fcaa70687216b53bd2cbd798
/// - a[e] - Specifies how an `addr()` request should be resolved for the specified
/// `chainId`. The value must be 0x-prefixed hexadecimal. When encoding an address for an
/// EVM-based cryptocurrency that uses a chainId instead of a coinType, this syntax *must*
/// be used in place of the coin type - eg, Optimism is `a[e10]`, not `a[2147483658]`.
/// A list of supported cryptocurrencies for both syntaxes can be found here:
/// https://github.com/ensdomains/address-encoder/blob/master/docs/supported-cryptocurrencies.md
/// Example:
/// - a[e10]=0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7
/// - t[] - Specifies how a `text()` request should be resolved for the specified `key`.
/// Examples:
/// - t[com.twitter]=nicksdjohnson
/// - t[url]='https://ens.domains/'
/// - t[note]='I\'m great'
contract ExtendedDNSResolver is IExtendedDNSResolver, IERC165 {
using HexUtils for *;
using BytesUtils for *;
using Strings for *;
uint256 private constant COIN_TYPE_ETH = 60;
error NotImplemented();
error InvalidAddressFormat(bytes addr);
function supportsInterface(
bytes4 interfaceId
) external view virtual override returns (bool) {
return interfaceId == type(IExtendedDNSResolver).interfaceId;
}
function resolve(
bytes calldata /* name */,
bytes calldata data,
bytes calldata context
) external pure override returns (bytes memory) {
bytes4 selector = bytes4(data);
if (selector == IAddrResolver.addr.selector) {
return _resolveAddr(context);
} else if (selector == IAddressResolver.addr.selector) {
return _resolveAddress(data, context);
} else if (selector == ITextResolver.text.selector) {
return _resolveText(data, context);
}
revert NotImplemented();
}
function _resolveAddress(
bytes calldata data,
bytes calldata context
) internal pure returns (bytes memory) {
(, uint256 coinType) = abi.decode(data[4:], (bytes32, uint256));
bytes memory value;
// Per https://docs.ens.domains/ensip/11#specification
if (coinType & 0x80000000 != 0) {
value = _findValue(
context,
bytes.concat(
"a[e",
bytes((coinType & 0x7fffffff).toString()),
"]="
)
);
} else {
value = _findValue(
context,
bytes.concat("a[", bytes(coinType.toString()), "]=")
);
}
if (value.length == 0) {
return value;
}
(address record, bool valid) = value.hexToAddress(2, value.length);
if (!valid) revert InvalidAddressFormat(value);
return abi.encode(record);
}
function _resolveAddr(
bytes calldata context
) internal pure returns (bytes memory) {
bytes memory value = _findValue(context, "a[60]=");
if (value.length == 0) {
return value;
}
(address record, bool valid) = value.hexToAddress(2, value.length);
if (!valid) revert InvalidAddressFormat(value);
return abi.encode(record);
}
function _resolveText(
bytes calldata data,
bytes calldata context
) internal pure returns (bytes memory) {
(, string memory key) = abi.decode(data[4:], (bytes32, string));
bytes memory value = _findValue(
context,
bytes.concat("t[", bytes(key), "]=")
);
return abi.encode(value);
}
uint256 constant STATE_START = 0;
uint256 constant STATE_IGNORED_KEY = 1;
uint256 constant STATE_IGNORED_KEY_ARG = 2;
uint256 constant STATE_VALUE = 3;
uint256 constant STATE_QUOTED_VALUE = 4;
uint256 constant STATE_UNQUOTED_VALUE = 5;
uint256 constant STATE_IGNORED_VALUE = 6;
uint256 constant STATE_IGNORED_QUOTED_VALUE = 7;
uint256 constant STATE_IGNORED_UNQUOTED_VALUE = 8;
/// @dev Implements a DFA to parse the text record, looking for an entry
/// matching `key`.
/// @param data The text record to parse.
/// @param key The exact key to search for.
/// @return value The value if found, or an empty string if `key` does not exist.
function _findValue(
bytes memory data,
bytes memory key
) internal pure returns (bytes memory value) {
// Here we use a simple state machine to parse the text record. We
// process characters one at a time; each character can trigger a
// transition to a new state, or terminate the DFA and return a value.
// For states that expect to process a number of tokens, we use
// inner loops for efficiency reasons, to avoid the need to go
// through the outer loop and switch statement for every character.
uint256 state = STATE_START;
uint256 len = data.length;
for (uint256 i = 0; i < len; ) {
if (state == STATE_START) {
// Look for a matching key.
if (data.equals(i, key, 0, key.length)) {
i += key.length;
state = STATE_VALUE;
} else {
state = STATE_IGNORED_KEY;
}
} else if (state == STATE_IGNORED_KEY) {
for (; i < len; i++) {
if (data[i] == "=") {
state = STATE_IGNORED_VALUE;
i += 1;
break;
} else if (data[i] == "[") {
state = STATE_IGNORED_KEY_ARG;
i += 1;
break;
}
}
} else if (state == STATE_IGNORED_KEY_ARG) {
for (; i < len; i++) {
if (data[i] == "]") {
state = STATE_IGNORED_VALUE;
i += 1;
if (data[i] == "=") {
i += 1;
}
break;
}
}
} else if (state == STATE_VALUE) {
if (data[i] == "'") {
state = STATE_QUOTED_VALUE;
i += 1;
} else {
state = STATE_UNQUOTED_VALUE;
}
} else if (state == STATE_QUOTED_VALUE) {
uint256 start = i;
uint256 valueLen = 0;
bool escaped = false;
for (; i < len; i++) {
if (escaped) {
data[start + valueLen] = data[i];
valueLen += 1;
escaped = false;
} else {
if (data[i] == "\\") {
escaped = true;
} else if (data[i] == "'") {
return data.substring(start, valueLen);
} else {
data[start + valueLen] = data[i];
valueLen += 1;
}
}
}
} else if (state == STATE_UNQUOTED_VALUE) {
uint256 start = i;
for (; i < len; i++) {
if (data[i] == " ") {
return data.substring(start, i - start);
}
}
return data.substring(start, len - start);
} else if (state == STATE_IGNORED_VALUE) {
if (data[i] == "'") {
state = STATE_IGNORED_QUOTED_VALUE;
i += 1;
} else {
state = STATE_IGNORED_UNQUOTED_VALUE;
}
} else if (state == STATE_IGNORED_QUOTED_VALUE) {
bool escaped = false;
for (; i < len; i++) {
if (escaped) {
escaped = false;
} else {
if (data[i] == "\\") {
escaped = true;
} else if (data[i] == "'") {
i += 1;
while (data[i] == " ") {
i += 1;
}
state = STATE_START;
break;
}
}
}
} else {
assert(state == STATE_IGNORED_UNQUOTED_VALUE);
for (; i < len; i++) {
if (data[i] == " ") {
while (data[i] == " ") {
i += 1;
}
state = STATE_START;
break;
}
}
}
}
return "";
}
}