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

Upgradeable L2 Resolver #98

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
11eda3e
First pass at upgradeable L2 Resolver with EIP-7201 namespaced storage
stevieraykatz Oct 17, 2024
83dbacb
Move UpgradeableL2Resolver to dir
stevieraykatz Oct 17, 2024
f1e9a73
Migrate L2 Resolver tests to upgradeable
stevieraykatz Oct 24, 2024
fb6f8b9
ABI Resolver tests
stevieraykatz Nov 14, 2024
362ad95
Cleanup tests, add user-owned/managed node for resolver profile tests
stevieraykatz Nov 14, 2024
5a55934
Fix pragmas
stevieraykatz Nov 14, 2024
9f1cf35
lint
stevieraykatz Nov 14, 2024
359b3fb
Finish Addr Resolver + unit tests
stevieraykatz Nov 14, 2024
845f497
Add contenthash tests, natspec, add auth check to existing unit tests
stevieraykatz Nov 14, 2024
eb8760a
fix testfile name
stevieraykatz Nov 14, 2024
953172f
Cleanup DNSResolver, start tests for it
stevieraykatz Nov 15, 2024
4b12718
Cleanup natspec in InterfaceResolver, add unit tests
stevieraykatz Nov 15, 2024
8928c15
lint
stevieraykatz Nov 15, 2024
d9ce766
Name Resolver natspec and unit tests
stevieraykatz Nov 15, 2024
d54b4b4
Pubkey Resolver natspec and test
stevieraykatz Nov 15, 2024
09a52e9
Text resolver natspec and tests
stevieraykatz Nov 15, 2024
f344d26
lint
stevieraykatz Nov 15, 2024
21ceab2
Cleanup to UpgradeableL2Resolver natspec
stevieraykatz Nov 15, 2024
65c58a8
Finish DNS record tests
stevieraykatz Nov 15, 2024
90710d0
feat: add `notProxyAdmin` modifier for fuzz tests
abdulla-cb Nov 25, 2024
feb5e22
feat: add view methods for registrarController and reverseRegistrar
abdulla-cb Nov 25, 2024
00e3514
test: add additional cases
abdulla-cb Nov 26, 2024
d2c06eb
test: add tests for versionable resolver
abdulla-cb Nov 26, 2024
ff94866
test: setAddr reverts if invalid
abdulla-cb Nov 27, 2024
64ad9e1
test: add DNS records test
abdulla-cb Nov 27, 2024
c5145ae
chore: fix typo
abdulla-cb Nov 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions src/L2/resolver/ABIResolver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.4;

import {IABIResolver} from "ens-contracts/resolvers/profiles/IABIResolver.sol";

import {ResolverBase} from "./ResolverBase.sol";

abstract contract ABIResolver is IABIResolver, ResolverBase {
struct ABIResolverStorage {
mapping(uint64 version => mapping(bytes32 node => mapping(uint256 contentType => bytes data))) versionable_abis;
}

// keccak256(abi.encode(uint256(keccak256("abi.resolver.storage")) - 1)) & ~bytes32(uint256(0xff));
bytes32 private constant ABI_RESOLVER_STORAGE = 0x76dc89e1c49d3cda8f11a131d381f3dbd0df1919a4e1a669330a2763d2821400;

/**
* Sets the ABI associated with an ENS node.
* Nodes may have one ABI of each content type. To remove an ABI, set it to
* the empty string.
* @param node The node to update.
* @param contentType The content type of the ABI
* @param data The ABI data.
*/
function setABI(bytes32 node, uint256 contentType, bytes calldata data) external virtual authorised(node) {
// Content types must be powers of 2
require(((contentType - 1) & contentType) == 0);

_getABIResolverStorage().versionable_abis[_getResolverBaseStorage().recordVersions[node]][node][contentType] =
data;
emit ABIChanged(node, contentType);
}

/**
* Returns the ABI associated with an ENS node.
* Defined in EIP205.
* @param node The ENS node to query
* @param contentTypes A bitwise OR of the ABI formats accepted by the caller.
* @return contentType The content type of the return value
* @return data The ABI data
*/
function ABI(bytes32 node, uint256 contentTypes) external view virtual override returns (uint256, bytes memory) {
mapping(uint256 => bytes) storage abiset =
_getABIResolverStorage().versionable_abis[_getResolverBaseStorage().recordVersions[node]][node];

for (uint256 contentType = 1; contentType <= contentTypes; contentType <<= 1) {
if ((contentType & contentTypes) != 0 && abiset[contentType].length > 0) {
return (contentType, abiset[contentType]);
}
}

return (0, bytes(""));
}

function supportsInterface(bytes4 interfaceID) public view virtual override returns (bool) {
return interfaceID == type(IABIResolver).interfaceId || super.supportsInterface(interfaceID);
}

function _getABIResolverStorage() internal pure returns (ABIResolverStorage storage $) {

Check failure

Code scanning / Slither

Uninitialized storage variables High

ABIResolver._getABIResolverStorage().$ is a storage variable never initialized
assembly {
$.slot := ABI_RESOLVER_STORAGE
}
}

Check warning

Code scanning / Slither

Assembly usage Warning

}
80 changes: 80 additions & 0 deletions src/L2/resolver/AddrResolver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.4;

import {IAddrResolver} from "ens-contracts/resolvers/profiles/IAddrResolver.sol";
import {IAddressResolver} from "ens-contracts/resolvers/profiles/IAddressResolver.sol";

import {ResolverBase} from "./ResolverBase.sol";

abstract contract AddrResolver is IAddrResolver, IAddressResolver, ResolverBase {
struct AddrResolverStorage {
mapping(uint64 version => mapping(bytes32 node => mapping(uint256 cointype => bytes addr)))
versionable_addresses;
}

// keccak256(abi.encode(uint256(keccak256("addr.resolver.storage")) - 1)) & ~bytes32(uint256(0xff));
bytes32 constant ADDR_RESOLVER_STORAGE = 0x1871a91a9a944f867849820431bb11c2d1625edae573523bceb5b38b8b8a7500;

Check warning

Code scanning / Slither

Unused state variable Warning


uint256 private constant COIN_TYPE_ETH = 60;

/**
* Sets the address associated with an ENS node.
* May only be called by the owner of that node in the ENS registry.
* @param node The node to update.
* @param a The address to set.
*/
function setAddr(bytes32 node, address a) external virtual authorised(node) {
setAddr(node, COIN_TYPE_ETH, addressToBytes(a));
}

/**
* Returns the address associated with an ENS node.
* @param node The ENS node to query.
* @return The associated address.
*/
function addr(bytes32 node) public view virtual override returns (address payable) {
bytes memory a = addr(node, COIN_TYPE_ETH);
if (a.length == 0) {
return payable(0);
}
return bytesToAddress(a);
}

function setAddr(bytes32 node, uint256 coinType, bytes memory a) public virtual authorised(node) {
emit AddressChanged(node, coinType, a);
if (coinType == COIN_TYPE_ETH) {
emit AddrChanged(node, bytesToAddress(a));
}
_getAddrResolverStorage().versionable_addresses[_getResolverBaseStorage().recordVersions[node]][node][coinType]
= a;
}

function addr(bytes32 node, uint256 coinType) public view virtual override returns (bytes memory) {
return _getAddrResolverStorage().versionable_addresses[_getResolverBaseStorage().recordVersions[node]][node][coinType];
}

function supportsInterface(bytes4 interfaceID) public view virtual override returns (bool) {
return interfaceID == type(IAddrResolver).interfaceId || interfaceID == type(IAddressResolver).interfaceId
|| super.supportsInterface(interfaceID);
}

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))
}
}
Dismissed Show dismissed Hide dismissed

function addressToBytes(address a) internal pure returns (bytes memory b) {
b = new bytes(20);
assembly {
mstore(add(b, 32), mul(a, exp(256, 12)))
}
}
Dismissed Show dismissed Hide dismissed

function _getAddrResolverStorage() internal pure returns (AddrResolverStorage storage $) {

Check failure

Code scanning / Slither

Uninitialized storage variables High

AddrResolver._getAddrResolverStorage().$ is a storage variable never initialized
assembly {
$.slot := ADDR_RESOLVER_STORAGE
}
}
Dismissed Show dismissed Hide dismissed
}
46 changes: 46 additions & 0 deletions src/L2/resolver/ContentHashResolver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.4;

import {IContentHashResolver} from "ens-contracts/resolvers/profiles/IContentHashResolver.sol";

import {ResolverBase} from "./ResolverBase.sol";

abstract contract ContentHashResolver is IContentHashResolver, ResolverBase {
struct ContentHashResolverStorage {
mapping(uint64 version => mapping(bytes32 node => bytes contenthash)) versionable_hashes;
}

// keccak256(abi.encode(uint256(keccak256("content.hash.base.storage")) - 1)) & ~bytes32(uint256(0xff));
bytes32 private constant CONTENT_HASH_RESOLVER_STORAGE =
0x3cead3a342b450f6c566db8bcc5888396a4bada4d226d84f6075be8f3245c100;

/**
* Sets the contenthash associated with an ENS node.
* May only be called by the owner of that node in the ENS registry.
* @param node The node to update.
* @param hash The contenthash to set
*/
function setContenthash(bytes32 node, bytes calldata hash) external virtual authorised(node) {
_getContentHashResolverStorage().versionable_hashes[_getResolverBaseStorage().recordVersions[node]][node] = hash;
emit ContenthashChanged(node, hash);
}

/**
* Returns the contenthash associated with an ENS node.
* @param node The ENS node to query.
* @return The associated contenthash.
*/
function contenthash(bytes32 node) external view virtual override returns (bytes memory) {
return _getContentHashResolverStorage().versionable_hashes[_getResolverBaseStorage().recordVersions[node]][node];
}

function supportsInterface(bytes4 interfaceID) public view virtual override returns (bool) {
return interfaceID == type(IContentHashResolver).interfaceId || super.supportsInterface(interfaceID);
}

function _getContentHashResolverStorage() internal pure returns (ContentHashResolverStorage storage $) {

Check failure

Code scanning / Slither

Uninitialized storage variables High

ContentHashResolver._getContentHashResolverStorage().$ is a storage variable never initialized
assembly {
$.slot := CONTENT_HASH_RESOLVER_STORAGE
}
}
Dismissed Show dismissed Hide dismissed
}
172 changes: 172 additions & 0 deletions src/L2/resolver/DNSResolver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.4;

import {BytesUtils, RRUtils} from "ens-contracts/dnssec-oracle/RRUtils.sol";
import {IDNSRecordResolver} from "ens-contracts/resolvers/profiles/IDNSRecordResolver.sol";
import {IDNSZoneResolver} from "ens-contracts/resolvers/profiles/IDNSZoneResolver.sol";

import {ResolverBase} from "./ResolverBase.sol";

abstract contract DNSResolver is IDNSRecordResolver, IDNSZoneResolver, ResolverBase {
using RRUtils for *;
using BytesUtils for bytes;

struct DNSResolverStorage {
// Zone hashes for the domains.
// A zone hash is an EIP-1577 content hash in binary format that should point to a
// resource containing a single zonefile.
mapping(uint64 version => mapping(bytes32 node => bytes zonehash)) versionable_zonehashes;
// The records themselves, stored as binary RRSETs
mapping(
uint64 version
=> mapping(bytes32 node => mapping(bytes32 namehash => mapping(uint16 resource => bytes data)))
) versionable_records;
// Count of number of entries for a given name. Required for DNS resolvers
// when resolving wildcards.
mapping(uint64 version => mapping(bytes32 node => mapping(bytes32 namehash => uint16 count)))
versionable_nameEntriesCount;
}

// keccak256(abi.encode(uint256(keccak256("dns.resolver.storage")) - 1)) & ~bytes32(uint256(0xff));
bytes32 constant DNS_RESOLVER_STORAGE = 0x563d533dd0798ef1806840ff9a36667f1ac5e6f948db03cf7022b575f40ccd00;
Dismissed Show dismissed Hide dismissed

/**
* Set one or more DNS records. Records are supplied in wire-format.
* Records with the same node/name/resource must be supplied one after the
* other to ensure the data is updated correctly. For example, if the data
* was supplied:
* a.example.com IN A 1.2.3.4
* a.example.com IN A 5.6.7.8
* www.example.com IN CNAME a.example.com.
* then this would store the two A records for a.example.com correctly as a
* single RRSET, however if the data was supplied:
* a.example.com IN A 1.2.3.4
* www.example.com IN CNAME a.example.com.
* a.example.com IN A 5.6.7.8
* then this would store the first A record, the CNAME, then the second A
* record which would overwrite the first.
*
* @param node the namehash of the node for which to set the records
* @param data the DNS wire format records to set
*/
function setDNSRecords(bytes32 node, bytes calldata data) external virtual authorised(node) {
uint16 resource = 0;
uint256 offset = 0;
bytes memory name;
Fixed Show fixed Hide fixed
bytes memory value;
Fixed Show fixed Hide fixed
bytes32 nameHash;
uint64 version = _getResolverBaseStorage().recordVersions[node];
// Iterate over the data to add the resource records
for (RRUtils.RRIterator memory iter = data.iterateRRs(0); !iter.done(); iter.next()) {
if (resource == 0) {
resource = iter.dnstype;
name = iter.name();
nameHash = keccak256(abi.encodePacked(name));
value = bytes(iter.rdata());
} else {
bytes memory newName = iter.name();
if (resource != iter.dnstype || !name.equals(newName)) {
setDNSRRSet(node, name, resource, data, offset, iter.offset - offset, value.length == 0, version);
resource = iter.dnstype;
offset = iter.offset;
name = newName;
nameHash = keccak256(name);
value = bytes(iter.rdata());
}
}
}
if (name.length > 0) {
setDNSRRSet(node, name, resource, data, offset, data.length - offset, value.length == 0, version);
}
}

/**
* Obtain a DNS record.
* @param node the namehash of the node for which to fetch the record
* @param name the keccak-256 hash of the fully-qualified name for which to fetch the record
* @param resource the ID of the resource as per https://en.wikipedia.org/wiki/List_of_DNS_record_types
* @return the DNS record in wire format if present, otherwise empty
*/
function dnsRecord(bytes32 node, bytes32 name, uint16 resource)
public
view
virtual
override
returns (bytes memory)
{
return _getDNSResolverStorage().versionable_records[_getResolverBaseStorage().recordVersions[node]][node][name][resource];
}

/**
* Check if a given node has records.
* @param node the namehash of the node for which to check the records
* @param name the namehash of the node for which to check the records
*/
function hasDNSRecords(bytes32 node, bytes32 name) public view virtual returns (bool) {
return (
_getDNSResolverStorage().versionable_nameEntriesCount[_getResolverBaseStorage().recordVersions[node]][node][name]
!= 0
);
}

/**
* setZonehash sets the hash for the zone.
* May only be called by the owner of that node in the ENS registry.
* @param node The node to update.
* @param hash The zonehash to set
*/
function setZonehash(bytes32 node, bytes calldata hash) external virtual authorised(node) {
uint64 currentRecordVersion = _getResolverBaseStorage().recordVersions[node];
DNSResolverStorage storage $ = _getDNSResolverStorage();
bytes memory oldhash = $.versionable_zonehashes[currentRecordVersion][node];
$.versionable_zonehashes[currentRecordVersion][node] = hash;
emit DNSZonehashChanged(node, oldhash, hash);
}

/**
* zonehash obtains the hash for the zone.
* @param node The ENS node to query.
* @return The associated contenthash.
*/
function zonehash(bytes32 node) external view virtual override returns (bytes memory) {
return _getDNSResolverStorage().versionable_zonehashes[_getResolverBaseStorage().recordVersions[node]][node];
}

function supportsInterface(bytes4 interfaceID) public view virtual override returns (bool) {
return interfaceID == type(IDNSRecordResolver).interfaceId || interfaceID == type(IDNSZoneResolver).interfaceId
|| super.supportsInterface(interfaceID);
}

function setDNSRRSet(
bytes32 node,
bytes memory name,
uint16 resource,
bytes memory data,
uint256 offset,
uint256 size,
bool deleteRecord,
uint64 version
) private {
bytes32 nameHash = keccak256(name);
bytes memory rrData = data.substring(offset, size);
if (deleteRecord) {
if (_getDNSResolverStorage().versionable_records[version][node][nameHash][resource].length != 0) {
_getDNSResolverStorage().versionable_nameEntriesCount[version][node][nameHash]--;
}
delete (_getDNSResolverStorage().versionable_records[version][node][nameHash][resource]);
emit DNSRecordDeleted(node, name, resource);
} else {
if (_getDNSResolverStorage().versionable_records[version][node][nameHash][resource].length == 0) {
_getDNSResolverStorage().versionable_nameEntriesCount[version][node][nameHash]++;
}
_getDNSResolverStorage().versionable_records[version][node][nameHash][resource] = rrData;
emit DNSRecordChanged(node, name, resource, rrData);
}
}

function _getDNSResolverStorage() internal pure returns (DNSResolverStorage storage $) {

Check failure

Code scanning / Slither

Uninitialized storage variables High

DNSResolver._getDNSResolverStorage().$ is a storage variable never initialized
assembly {
$.slot := DNS_RESOLVER_STORAGE
}
}
Dismissed Show dismissed Hide dismissed
}
Loading