diff --git a/contracts/dnsregistrar/OffchainDNSResolver.sol b/contracts/dnsregistrar/OffchainDNSResolver.sol index dd39a6ac..abbb4869 100644 --- a/contracts/dnsregistrar/OffchainDNSResolver.sol +++ b/contracts/dnsregistrar/OffchainDNSResolver.sol @@ -87,7 +87,7 @@ contract OffchainDNSResolver is IExtendedResolver, IERC165 { // Ignore records with wrong name, type, or class bytes memory rrname = RRUtils.readName(iter.data, iter.offset); if ( - !rrname.equals(name) || + !rrname.equals(stripWildcard(name)) || iter.class != CLASS_INET || iter.dnstype != TYPE_TXT ) { @@ -168,6 +168,19 @@ contract OffchainDNSResolver is IExtendedResolver, IERC165 { ); } + function stripWildcard( + bytes memory name + ) public pure returns (bytes memory) { + if (name.length > 4 && name[0] == "*" && name[1] == ".") { + bytes memory strippedName = new bytes(name.length - 2); + for (uint i = 2; i < name.length; i++) { + strippedName[i - 2] = name[i]; + } + return strippedName; + } + return name; + } + function parseRR( bytes memory data, uint256 idx, @@ -199,10 +212,16 @@ contract OffchainDNSResolver is IExtendedResolver, IERC165 { uint256 startIdx, uint256 lastIdx ) internal pure returns (bytes memory) { - // TODO: Concatenate multiple text fields - uint256 fieldLength = data.readUint8(startIdx); - assert(startIdx + fieldLength < lastIdx); - return data.substring(startIdx + 1, fieldLength); + bytes memory result = new bytes(0); + uint256 idx = startIdx; + while (idx < lastIdx) { + uint256 fieldLength = data.readUint8(idx); + assert(idx + fieldLength + 1 <= lastIdx); + bytes memory field = data.substring(idx + 1, fieldLength); + result = abi.encodePacked(result, field); + idx += fieldLength + 1; + } + return result; } function parseAndResolve( diff --git a/contracts/dnsregistrar/mocks/DummyExtendedDNSSECResolver2.sol b/contracts/dnsregistrar/mocks/DummyExtendedDNSSECResolver2.sol new file mode 100644 index 00000000..e6136ee5 --- /dev/null +++ b/contracts/dnsregistrar/mocks/DummyExtendedDNSSECResolver2.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "@openzeppelin/contracts/utils/introspection/IERC165.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"; + +contract DummyExtendedDNSSECResolver2 is IExtendedDNSResolver, IERC165 { + using HexUtils for *; + + uint256 private constant COIN_TYPE_ETH = 60; + uint256 private constant ADDRESS_LENGTH = 40; + + error NotImplemented(); + error InvalidAddressFormat(); + + 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 || + selector == IAddressResolver.addr.selector + ) { + // Parse address from context + bytes memory addrBytes = _parseAddressFromContext(context); + return abi.encode(address(uint160(uint256(bytes32(addrBytes))))); + } else if (selector == ITextResolver.text.selector) { + // Parse text value from context + (, string memory key) = abi.decode(data[4:], (bytes32, string)); + string memory value = _parseTextFromContext(context, key); + return abi.encode(value); + } + revert NotImplemented(); + } + + function _parseAddressFromContext( + bytes memory context + ) internal pure returns (bytes memory) { + // Parse address from concatenated context + for (uint256 i = 0; i < context.length - ADDRESS_LENGTH + 2; i++) { + if (context[i] == "0" && context[i + 1] == "x") { + bytes memory candidate = new bytes(ADDRESS_LENGTH); + for (uint256 j = 0; j < ADDRESS_LENGTH; j++) { + candidate[j] = context[i + j + 2]; + } + + (address candidateAddr, bool valid) = candidate.hexToAddress( + 0, + ADDRESS_LENGTH + ); + if (valid) { + return abi.encode(candidateAddr); + } + } + } + revert InvalidAddressFormat(); + } + + function _parseTextFromContext( + bytes calldata context, + string memory key + ) internal pure returns (string memory) { + // Parse key-value pairs from concatenated context + string memory value = ""; + bool foundKey = false; + for (uint256 i = 0; i < context.length; i++) { + if (foundKey && context[i] == "=") { + i++; + while (i < context.length && context[i] != " ") { + string memory charStr = string( + abi.encodePacked(bytes1(context[i])) + ); + value = string(abi.encodePacked(value, charStr)); + i++; + } + return value; + } + if (!foundKey && bytes(key)[0] == context[i]) { + bool isMatch = true; + for (uint256 j = 1; j < bytes(key).length; j++) { + if (context[i + j] != bytes(key)[j]) { + isMatch = false; + break; + } + } + foundKey = isMatch; + } + } + return ""; + } +} diff --git a/test/dnsregistrar/TestOffchainDNSResolver.js b/test/dnsregistrar/TestOffchainDNSResolver.js index 3c325d4f..4be9a9ca 100644 --- a/test/dnsregistrar/TestOffchainDNSResolver.js +++ b/test/dnsregistrar/TestOffchainDNSResolver.js @@ -11,6 +11,11 @@ const PublicResolver = artifacts.require('./PublicResolver.sol') const DummyExtendedDNSSECResolver = artifacts.require( './DummyExtendedDNSSECResolver.sol', ) + +const DummyExtendedDNSSECResolver2 = artifacts.require( + './DummyExtendedDNSSECResolver2.sol', +) + const DummyLegacyTextResolver = artifacts.require( './DummyLegacyTextResolver.sol', ) @@ -148,8 +153,8 @@ contract('OffchainDNSResolver', function (accounts) { ) const dnsName = utils.hexEncodeName(name) const extraData = ethers.utils.defaultAbiCoder.encode( - ['bytes', 'bytes', 'bytes4'], - [dnsName, callData, '0x00000000'], + ['bytes', 'bytes'], + [dnsName, callData], ) return offchainDNSResolver.resolveCallback(response, extraData) } @@ -456,4 +461,79 @@ contract('OffchainDNSResolver', function (accounts) { doDNSResolveCallback(name, [`ENS1 ${dummyResolver.address}`], callData), ).to.be.revertedWith('InvalidOperation') }) + + it('should correctly concatenate multiple texts in the TXT record and resolve', async function () { + const COIN_TYPE_ETH = 60 + const name = 'test.test' + const testAddress = '0xfefeFEFeFEFEFEFEFeFefefefefeFEfEfefefEfe' + const resolver = await DummyExtendedDNSSECResolver2.new() + const pr = await PublicResolver.at(resolver.address) + const callDataAddr = pr.contract.methods['addr(bytes32,uint256)']( + namehash.hash(name), + COIN_TYPE_ETH, + ).encodeABI() + const resultAddr = await doDNSResolveCallback( + name, + [`ENS1 ${resolver.address} ${testAddress} smth=smth.eth`], + callDataAddr, + ) + expect( + ethers.utils.defaultAbiCoder.decode(['address'], resultAddr)[0], + ).to.equal(testAddress) + + const callDataText = pr.contract.methods['text(bytes32,string)']( + namehash.hash(name), + 'smth', + ).encodeABI() + const resultText = await doDNSResolveCallback( + name, + [`ENS1 ${resolver.address} ${testAddress} smth=smth.eth`], + callDataText, + ) + + expect( + ethers.utils.defaultAbiCoder.decode(['string'], resultText)[0], + ).to.equal('smth.eth') + }) + + it('should correctly do text resolution regardless of order', async function () { + const name = 'test.test' + const testAddress = '0xfefeFEFeFEFEFEFEFeFefefefefeFEfEfefefEfe' + const resolver = await DummyExtendedDNSSECResolver2.new() + const pr = await PublicResolver.at(resolver.address) + + const callDataText = pr.contract.methods['text(bytes32,string)']( + namehash.hash(name), + 'smth', + ).encodeABI() + const resultText = await doDNSResolveCallback( + name, + [`ENS1 ${resolver.address} smth=smth.eth ${testAddress}`], + callDataText, + ) + + expect( + ethers.utils.defaultAbiCoder.decode(['string'], resultText)[0], + ).to.equal('smth.eth') + }) + + it('should correctly do text resolution regardless of key-value pair amount', async function () { + const name = 'test.test' + const resolver = await DummyExtendedDNSSECResolver2.new() + const pr = await PublicResolver.at(resolver.address) + + const callDataText = pr.contract.methods['text(bytes32,string)']( + namehash.hash(name), + 'bla', + ).encodeABI() + const resultText = await doDNSResolveCallback( + name, + [`ENS1 ${resolver.address} smth=smth.eth bla=bla.eth`], + callDataText, + ) + + expect( + ethers.utils.defaultAbiCoder.decode(['string'], resultText)[0], + ).to.equal('bla.eth') + }) })