Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

concatenate multiple text fields #332

Open
wants to merge 10 commits into
base: fix/refactor-wrapped-callback
Choose a base branch
from
29 changes: 24 additions & 5 deletions contracts/dnsregistrar/OffchainDNSResolver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
) {
Expand Down Expand Up @@ -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] == ".") {
mdtanrikulu marked this conversation as resolved.
Show resolved Hide resolved
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,
Expand Down Expand Up @@ -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);
mdtanrikulu marked this conversation as resolved.
Show resolved Hide resolved
idx += fieldLength + 1;
}
return result;
}

function parseAndResolve(
Expand Down
103 changes: 103 additions & 0 deletions contracts/dnsregistrar/mocks/DummyExtendedDNSSECResolver2.sol
Original file line number Diff line number Diff line change
@@ -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 "";
}
}
84 changes: 82 additions & 2 deletions test/dnsregistrar/TestOffchainDNSResolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
)
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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')
})
})
Loading