diff --git a/src/L2/UpgradeableL2Resolver.sol b/src/L2/UpgradeableL2Resolver.sol new file mode 100644 index 0000000..c375166 --- /dev/null +++ b/src/L2/UpgradeableL2Resolver.sol @@ -0,0 +1,256 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {ENS} from "ens-contracts/registry/ENS.sol"; +import {ExtendedResolver} from "ens-contracts/resolvers/profiles/ExtendedResolver.sol"; +import {IExtendedResolver} from "ens-contracts/resolvers/profiles/IExtendedResolver.sol"; +import {Initializable} from "lib/openzeppelin-contracts/contracts/proxy/utils/Initializable.sol"; +import {Multicallable} from "ens-contracts/resolvers/Multicallable.sol"; +import {Ownable} from "solady/auth/Ownable.sol"; + +import {ABIResolver} from "./resolver/ABIResolver.sol"; +import {AddrResolver} from "./resolver/AddrResolver.sol"; +import {ContentHashResolver} from "./resolver/ContentHashResolver.sol"; +import {DNSResolver} from "./resolver/DNSResolver.sol"; +import {InterfaceResolver} from "./resolver/InterfaceResolver.sol"; +import {NameResolver} from "./resolver/NameResolver.sol"; +import {PubkeyResolver} from "./resolver/PubkeyResolver.sol"; +import {TextResolver} from "./resolver/TextResolver.sol"; +import {IReverseRegistrar} from "src/L2/interface/IReverseRegistrar.sol"; + +/// @title Upgradeable L2 Resolver +/// +/// @notice The upgradeable public resolver for Basenames. This contract implements the functionality of the ENS +/// PublicResolver while also inheriting ExtendedResolver for compatibility with CCIP-read. +/// Public Resolver: https://github.com/ensdomains/ens-contracts/blob/staging/contracts/resolvers/PublicResolver.sol +/// Extended Resolver: https://github.com/ensdomains/ens-contracts/blob/staging/contracts/resolvers/profiles/ExtendedResolver.sol +/// +/// @author Coinbase (https://github.com/base-org/basenames) +contract UpgradeableL2Resolver is + ABIResolver, + AddrResolver, + ContentHashResolver, + DNSResolver, + ExtendedResolver, + Initializable, + InterfaceResolver, + Multicallable, + NameResolver, + Ownable, + PubkeyResolver, + TextResolver +{ + /// @notice EIP-7201 storage location. + // keccak256(abi.encode(uint256(keccak256("resolver.storage")) - 1)) & ~bytes32(uint256(0xff)); + bytes32 private constant RESOLVER_STORAGE_LOCATION = + 0xa75da70a48b778f6d7794a48ad897d5e41dff6abea13a6164e9a58efe57a7200; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* STORAGE */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + struct ResolverStorage { + /// @notice The registry contract. + ENS registry; + /// @notice The trusted registrar controller contract. + address registrarController; + /// @notice The reverse registrar contract. + address reverseRegistrar; + /// @notice A mapping of operators per owner address. An operator is authorized to make changes to + /// all names owned by the `owner`. + mapping(address owner => mapping(address operator => bool isApproved)) _operatorApprovals; + /// @notice A mapping of delegates per owner per name (stored as a node). A delegate that is authorised + /// by an owner for a name may make changes to the name's resolver. + mapping(address owner => mapping(bytes32 node => mapping(address delegate => bool isApproved))) _tokenApprovals; + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* ERRORS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice Thrown when msg.sender tries to set itself as an operator. + error CantSetSelfAsOperator(); + + /// @notice Thrown when msg.sender tries to set itself as a delegate for one of its names. + error CantSetSelfAsDelegate(); + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* EVENTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice Emitted when an operator is added or removed. + /// + /// @param owner The address of the owner of names. + /// @param operator The address of the approved operator for the `owner`. + /// @param approved Whether the `operator` is approved or not. + event ApprovalForAll(address indexed owner, address indexed operator, bool approved); + + /// @notice Emitted when a delegate is approved or an approval is revoked. + /// + /// @param owner The address of the owner of the name. + /// @param node The namehash of the name. + /// @param delegate The address of the operator for the specified `node`. + /// @param approved Whether the `delegate` is approved for the specified `node`. + event Approved(address owner, bytes32 indexed node, address indexed delegate, bool indexed approved); + + /// @notice Emitted when the owner of this contract updates the Registrar Controller addrress. + /// + /// @param newRegistrarController The address of the new RegistrarController contract. + event RegistrarControllerUpdated(address indexed newRegistrarController); + + /// @notice Emitted when the owner of this contract updates the Reverse Registrar address. + /// + /// @param newReverseRegistrar The address of the new ReverseRegistrar contract. + event ReverseRegistrarUpdated(address indexed newReverseRegistrar); + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* IMPLEMENTATION */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + constructor() { + _disableInitializers(); + } + + /// @notice L2 Resolver constructor used to establish the necessary contract configuration. + /// + /// @param registry_ The Registry contract. + /// @param registrarController_ The address of the RegistrarController contract. + /// @param reverseRegistrar_ The address of the ReverseRegistrar contract. + /// @param owner_ The permissioned address initialized as the `owner` in the `Ownable` context. + function initialize(ENS registry_, address registrarController_, address reverseRegistrar_, address owner_) + public + initializer + { + ResolverStorage storage $ = _getResolverStorage(); + $.registry = registry_; + $.registrarController = registrarController_; + $.reverseRegistrar = reverseRegistrar_; + _initializeOwner(owner_); + IReverseRegistrar(reverseRegistrar_).claim(owner_); + } + + /// @notice Allows the `owner` to set the registrar controller contract address. + /// + /// @dev Emits `RegistrarControllerUpdated` after setting the `registrarController` address. + /// + /// @param registrarController_ The address of the new RegistrarController contract. + function setRegistrarController(address registrarController_) external onlyOwner { + _getResolverStorage().registrarController = registrarController_; + emit RegistrarControllerUpdated(registrarController_); + } + + /// @notice Allows the `owner` to set the reverse registrar contract address. + /// + /// @dev Emits `ReverseRegistrarUpdated` after setting the `reverseRegistrar` address. + /// + /// @param reverseRegistrar_ The address of the new ReverseRegistrar contract. + function setReverseRegistrar(address reverseRegistrar_) external onlyOwner { + _getResolverStorage().reverseRegistrar = reverseRegistrar_; + emit ReverseRegistrarUpdated(reverseRegistrar_); + } + + /// @dev See {IERC1155-setApprovalForAll}. + function setApprovalForAll(address operator, bool approved) external { + if (msg.sender == operator) revert CantSetSelfAsOperator(); + + _getResolverStorage()._operatorApprovals[msg.sender][operator] = approved; + emit ApprovalForAll(msg.sender, operator, approved); + } + + /// @dev See {IERC1155-isApprovedForAll}. + function isApprovedForAll(address account, address operator) public view returns (bool) { + return _getResolverStorage()._operatorApprovals[account][operator]; + } + + /// @notice Modify the permissions for a specified `delegate` for the specified `node`. + /// + /// @dev This method only sets the approval status for msg.sender's nodes. This is performed without checking + /// the ownership of the specified `node`. + /// + /// @param node The namehash `node` whose permissions are being updated. + /// @param delegate The address of the `delegate`. + /// @param approved Whether the `delegate` has approval to modify records for `msg.sender`'s `node`. + function approve(bytes32 node, address delegate, bool approved) external { + if (msg.sender == delegate) revert CantSetSelfAsDelegate(); + + _getResolverStorage()._tokenApprovals[msg.sender][node][delegate] = approved; + emit Approved(msg.sender, node, delegate, approved); + } + + /// @notice Check to see if the `delegate` has been approved by the `owner` for the `node`. + /// + /// @param owner The address of the name owner. + /// @param node The namehash `node` whose permissions are being checked. + /// @param delegate The address of the `delegate` whose permissions are being checked. + /// + /// @return `true` if `delegate` is approved to modify `msg.sender`'s `node`, else `false`. + function isApprovedFor(address owner, bytes32 node, address delegate) public view returns (bool) { + return _getResolverStorage()._tokenApprovals[owner][node][delegate]; + } + + /// @notice Check to see whether `msg.sender` is authorized to modify records for the specified `node`. + /// + /// @dev Override for `ResolverBase:isAuthorised()`. Used in the context of each inherited resolver "profile". + /// Validates that `msg.sender` is one of: + /// 1. The stored registrarController (for setting records upon registration) + /// 2 The stored reverseRegistrar (for setting reverse records) + /// 3. The owner of the node in the Registry + /// 4. An approved operator for owner + /// 5. An approved delegate for owner of the specified `node` + /// + /// @param node The namehashed `node` being authorized. + /// + /// @return `true` if `msg.sender` is authorized to modify records for the specified `node`, else `false`. + function isAuthorised(bytes32 node) internal view override returns (bool) { + ResolverStorage storage $ = _getResolverStorage(); + if (msg.sender == $.registrarController || msg.sender == $.reverseRegistrar) { + return true; + } + address owner = $.registry.owner(node); + return owner == msg.sender || isApprovedForAll(owner, msg.sender) || isApprovedFor(owner, node, msg.sender); + } + + /// @notice ERC165 compliant signal for interface support. + /// + /// @dev Checks interface support for each inherited resolver profile + /// https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] + /// + /// @param interfaceID the ERC165 iface id being checked for compliance. + /// + /// @return bool Whether this contract supports the provided interfaceID. + function supportsInterface(bytes4 interfaceID) + public + view + override( + Multicallable, + ABIResolver, + AddrResolver, + ContentHashResolver, + DNSResolver, + InterfaceResolver, + NameResolver, + PubkeyResolver, + TextResolver + ) + returns (bool) + { + return (interfaceID == type(IExtendedResolver).interfaceId || super.supportsInterface(interfaceID)); + } + + /// @notice Returns the address of the trusted registrar controller contract. + function registrarController() external view returns (address) { + return _getResolverStorage().registrarController; + } + + /// @notice Returns the address of the reverse registrar contract. + function reverseRegistrar() external view returns (address) { + return _getResolverStorage().reverseRegistrar; + } + + /// @notice EIP-7201 storage pointer fetch helper. + function _getResolverStorage() internal pure returns (ResolverStorage storage $) { + assembly { + $.slot := RESOLVER_STORAGE_LOCATION + } + } +} diff --git a/src/L2/resolver/ABIResolver.sol b/src/L2/resolver/ABIResolver.sol new file mode 100644 index 0000000..203507b --- /dev/null +++ b/src/L2/resolver/ABIResolver.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {IABIResolver} from "ens-contracts/resolvers/profiles/IABIResolver.sol"; + +import {ResolverBase} from "./ResolverBase.sol"; + +/// @title ABI Resolver +/// +/// @notice ENSIP-4 compliant ABI Resolver. Adaptation of the ENS ABIResolver.sol profile contract, with +/// EIP-7201 storage compliance. +/// https://github.com/ensdomains/ens-contracts/blob/staging/contracts/resolvers/profiles/ABIResolver.sol +/// +/// @author Coinbase (https://github.com/base-org/basenames) +abstract contract ABIResolver is IABIResolver, ResolverBase { + struct ABIResolverStorage { + /// @notice ABI record (`bytes`) by content type, node, and version. + mapping(uint64 version => mapping(bytes32 node => mapping(uint256 contentType => bytes data))) versionable_abis; + } + + /// @notice Thrown when setting an ABI with an invalid content type. + error InvalidContentType(); + + /// @notice EIP-7201 storage location. + /// keccak256(abi.encode(uint256(keccak256("abi.resolver.storage")) - 1)) & ~bytes32(uint256(0xff)); + bytes32 private constant ABI_RESOLVER_STORAGE = 0x76dc89e1c49d3cda8f11a131d381f3dbd0df1919a4e1a669330a2763d2821400; + + /// @notice Sets the ABI associated with an ENS node. + /// + /// @dev 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 + if (((contentType - 1) & contentType) != 0) revert InvalidContentType(); + + _getABIResolverStorage().versionable_abis[_getResolverBaseStorage().recordVersions[node]][node][contentType] = + data; + emit ABIChanged(node, contentType); + } + + /// @notice Returns the ABI associated with an ENS node for a specific content type. + /// + /// @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("")); + } + + /// @notice ERC-165 compliance. + function supportsInterface(bytes4 interfaceID) public view virtual override returns (bool) { + return interfaceID == type(IABIResolver).interfaceId || super.supportsInterface(interfaceID); + } + + /// @notice EIP-7201 storage pointer fetch helper. + function _getABIResolverStorage() internal pure returns (ABIResolverStorage storage $) { + assembly { + $.slot := ABI_RESOLVER_STORAGE + } + } +} diff --git a/src/L2/resolver/AddrResolver.sol b/src/L2/resolver/AddrResolver.sol new file mode 100644 index 0000000..c908348 --- /dev/null +++ b/src/L2/resolver/AddrResolver.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {IAddrResolver} from "ens-contracts/resolvers/profiles/IAddrResolver.sol"; +import {IAddressResolver} from "ens-contracts/resolvers/profiles/IAddressResolver.sol"; + +import {ResolverBase} from "./ResolverBase.sol"; + +/// @title Address Resolver +/// +/// @notice ENSIP-11 compliant Address Resolver. Adaptation of the ENS AddrResolver.sol profile contract, with +/// EIP-7201 storage compliance. +/// https://github.com/ensdomains/ens-contracts/blob/staging/contracts/resolvers/profiles/AddrResolver.sol +/// +/// @author Coinbase (https://github.com/base-org/basenames) +abstract contract AddrResolver is IAddrResolver, IAddressResolver, ResolverBase { + struct AddrResolverStorage { + /// @notice Address record per cointype, node and version. + mapping(uint64 version => mapping(bytes32 node => mapping(uint256 cointype => bytes addr))) + versionable_addresses; + } + + /// @notice EIP-7201 storage location. + // keccak256(abi.encode(uint256(keccak256("addr.resolver.storage")) - 1)) & ~bytes32(uint256(0xff)); + bytes32 constant ADDR_RESOLVER_STORAGE = 0x1871a91a9a944f867849820431bb11c2d1625edae573523bceb5b38b8b8a7500; + + /// @notice Ethereum mainnet network-as-cointype. + uint256 private constant COIN_TYPE_ETH = 60; + + /// @notice Sets the address associated with an ENS node. + /// + /// @dev 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)); + } + + /// @notice Returns the address associated with a specified ENS `node`. + /// + /// @dev Returns the `addr` record for the Ethereum Mainnet network-as-cointype. + /// + /// @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); + } + + /// @notice Set the network or coin-specific address for an ENS `node`. + /// + /// @param node The ENS node to update. + /// @param coinType The coinType for this address. + /// @param a The network-agnostic bytes of the address. + 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; + } + + /// @notice Returns the address of the `node` for a specified `coinType`. + /// + /// @dev Complies with ENSIP-9 and ENSIP-11. + /// + /// @param node The ENS node to update. + /// @param coinType The coinType to fetch. + /// + /// @return The address of the specified `node` for the specified `coinType`. + function addr(bytes32 node, uint256 coinType) public view virtual override returns (bytes memory) { + return _getAddrResolverStorage().versionable_addresses[_getResolverBaseStorage().recordVersions[node]][node][coinType]; + } + + /// @notice ERC-165 compliance. + function supportsInterface(bytes4 interfaceID) public view virtual override returns (bool) { + return interfaceID == type(IAddrResolver).interfaceId || interfaceID == type(IAddressResolver).interfaceId + || super.supportsInterface(interfaceID); + } + + /// @notice Helper to convert bytes into an EVM address object. + 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)) + } + } + + /// @notice Helper to convert an EVM address to a bytes` object. + function addressToBytes(address a) internal pure returns (bytes memory b) { + b = new bytes(20); + assembly { + mstore(add(b, 32), mul(a, exp(256, 12))) + } + } + + /// @notice EIP-7201 storage pointer fetch helper. + function _getAddrResolverStorage() internal pure returns (AddrResolverStorage storage $) { + assembly { + $.slot := ADDR_RESOLVER_STORAGE + } + } +} diff --git a/src/L2/resolver/ContentHashResolver.sol b/src/L2/resolver/ContentHashResolver.sol new file mode 100644 index 0000000..0f74816 --- /dev/null +++ b/src/L2/resolver/ContentHashResolver.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {IContentHashResolver} from "ens-contracts/resolvers/profiles/IContentHashResolver.sol"; + +import {ResolverBase} from "./ResolverBase.sol"; + +/// @title Content Hash Resolver +/// +/// @notice ENSIP-7 compliant Content Hash Resolver profile. Adaptation of the ENS ContentHashResolver.sol profile contract, +/// with EIP-7201 storage compliance. +/// https://github.com/ensdomains/ens-contracts/blob/staging/contracts/resolvers/profiles/ContentHashResolver.sol +/// +/// @author Coinbase (https://github.com/base-org/basenames) +abstract contract ContentHashResolver is IContentHashResolver, ResolverBase { + struct ContentHashResolverStorage { + /// @notice Content hashes by node and version. + mapping(uint64 version => mapping(bytes32 node => bytes contenthash)) versionable_hashes; + } + + /// @notice EIP-7201 storage location. + // keccak256(abi.encode(uint256(keccak256("content.hash.resolver.storage")) - 1)) & ~bytes32(uint256(0xff)); + bytes32 private constant CONTENT_HASH_RESOLVER_STORAGE = + 0x3cead3a342b450f6c566db8bcc5888396a4bada4d226d84f6075be8f3245c100; + + /// @notice Sets the contenthash associated with an ENS node. + /// + /// @dev 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); + } + + /// @notice 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]; + } + + /// @notice ERC-165 compliance. + function supportsInterface(bytes4 interfaceID) public view virtual override returns (bool) { + return interfaceID == type(IContentHashResolver).interfaceId || super.supportsInterface(interfaceID); + } + + /// @notice EIP-7201 storage pointer fetch helper. + function _getContentHashResolverStorage() internal pure returns (ContentHashResolverStorage storage $) { + assembly { + $.slot := CONTENT_HASH_RESOLVER_STORAGE + } + } +} diff --git a/src/L2/resolver/DNSResolver.sol b/src/L2/resolver/DNSResolver.sol new file mode 100644 index 0000000..553ec33 --- /dev/null +++ b/src/L2/resolver/DNSResolver.sol @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +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"; + +/// @title DNS Resolver +/// +/// @notice ENSIP-6 compliant DNS Resolver profile. Adaptation of the ENS DNSResolver.sol profile contract, +/// with EIP-7201 storage compliance. +/// https://github.com/ensdomains/ens-contracts/blob/staging/contracts/resolvers/profiles/DNSResolver.sol +/// +/// @author Coinbase (https://github.com/base-org/basenames) +abstract contract DNSResolver is IDNSRecordResolver, IDNSZoneResolver, ResolverBase { + using RRUtils for *; + using BytesUtils for bytes; + + struct DNSResolverStorage { + /// @notice 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; + /// @notice The records themselves, stored as binary RRSETs + mapping( + uint64 version + => mapping(bytes32 node => mapping(bytes32 namehash => mapping(uint16 resource => bytes data))) + ) versionable_records; + /// @notice 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; + } + + /// @notice EIP-7201 storage location. + // keccak256(abi.encode(uint256(keccak256("dns.resolver.storage")) - 1)) & ~bytes32(uint256(0xff)); + bytes32 constant DNS_RESOLVER_STORAGE = 0x563d533dd0798ef1806840ff9a36667f1ac5e6f948db03cf7022b575f40ccd00; + + /// @notice Set one or more DNS records. Records are supplied in wire-format. + /// + /// @dev 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; + bytes memory value; + 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); + } + } + + /// @notice 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]; + } + + /// @notice Check if a given node has records. + /// + /// @param node The namehash of the node for which to check the records. + /// @param name The keccak-256 hash of the fully-qualified name for which to fetch the record. + /// + /// @return `True` if records are stored for this node + name, else `False`. + function hasDNSRecords(bytes32 node, bytes32 name) public view virtual returns (bool) { + return ( + _getDNSResolverStorage().versionable_nameEntriesCount[_getResolverBaseStorage().recordVersions[node]][node][name] + != 0 + ); + } + + /// @notice Sets the hash for the zone. + /// + /// @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); + } + + /// @notice Obtains the hash for the zone. + /// + /// @param node The ENS node to query. + /// + /// @return The associated zonehash. + function zonehash(bytes32 node) external view virtual override returns (bytes memory) { + return _getDNSResolverStorage().versionable_zonehashes[_getResolverBaseStorage().recordVersions[node]][node]; + } + + /// @notice ERC-165 compliance. + function supportsInterface(bytes4 interfaceID) public view virtual override returns (bool) { + return interfaceID == type(IDNSRecordResolver).interfaceId || interfaceID == type(IDNSZoneResolver).interfaceId + || super.supportsInterface(interfaceID); + } + + /// @notice Internal helper for RRSet. + 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); + DNSResolverStorage storage $ = _getDNSResolverStorage(); + if (deleteRecord) { + if ($.versionable_records[version][node][nameHash][resource].length != 0) { + $.versionable_nameEntriesCount[version][node][nameHash]--; + } + delete ($.versionable_records[version][node][nameHash][resource]); + emit DNSRecordDeleted(node, name, resource); + } else { + if ($.versionable_records[version][node][nameHash][resource].length == 0) { + $.versionable_nameEntriesCount[version][node][nameHash]++; + } + $.versionable_records[version][node][nameHash][resource] = rrData; + emit DNSRecordChanged(node, name, resource, rrData); + } + } + + /// @notice EIP-7201 storage pointer fetch helper. + function _getDNSResolverStorage() internal pure returns (DNSResolverStorage storage $) { + assembly { + $.slot := DNS_RESOLVER_STORAGE + } + } +} diff --git a/src/L2/resolver/InterfaceResolver.sol b/src/L2/resolver/InterfaceResolver.sol new file mode 100644 index 0000000..e120d15 --- /dev/null +++ b/src/L2/resolver/InterfaceResolver.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {IERC165} from "lib/openzeppelin-contracts/contracts/utils/introspection/IERC165.sol"; +import {IInterfaceResolver} from "ens-contracts/resolvers/profiles/IInterfaceResolver.sol"; +import {IAddrResolver} from "ens-contracts/resolvers/profiles/IAddrResolver.sol"; + +import {ResolverBase} from "./ResolverBase.sol"; + +/// @title Interface Resolver +/// +/// @notice ENSIP-8 compliant Interface Discovery resolver. Adaptation of the ENS InterfaceResolver.sol profile contract, with +/// EIP-7201 storage compliance. +/// https://github.com/ensdomains/ens-contracts/blob/staging/contracts/resolvers/profiles/InterfaceResolver.sol +/// +/// @author Coinbase (https://github.com/base-org/basenames) +abstract contract InterfaceResolver is IInterfaceResolver, ResolverBase { + struct InterfaceResolverStorage { + /// @notice Interface implementer address by interface id, node and version. + mapping(uint64 version => mapping(bytes32 node => mapping(bytes4 interfaceId => address implemenentor))) + versionable_interfaces; + } + + /// @notice EIP-7201 storage location. + // keccak256(abi.encode(uint256(keccak256("interface.resolver.storage")) - 1)) & ~bytes32(uint256(0xff)); + bytes32 constant INTERFACE_RESOLVER_STORAGE = 0x933ab330cd660334bb219a68b3bfaf86387ecd49e4e53a39e8310a5bd6910c00; + + /// @notice Sets an interface implementer address. + /// + /// @dev Setting the address to 0 restores the default behaviour of querying the contract at `addr()` for interface support. + /// + /// @param node The node to update. + /// @param interfaceID The EIP-165 interface ID. + /// @param implementer The address of a contract that implements this interface for this node. + function setInterface(bytes32 node, bytes4 interfaceID, address implementer) external virtual authorised(node) { + _getInterfaceResolverStorage().versionable_interfaces[_getResolverBaseStorage().recordVersions[node]][node][interfaceID] + = implementer; + emit InterfaceChanged(node, interfaceID, implementer); + } + + /// @notice Returns the address of a contract that implements the specified interface for this name. + /// + /// @dev If an implementer has not been set for this interfaceID and name, this resolver will query + /// itself at `addr(node)`. If `addr()` is set, a contract exists at that address, and that + /// contract implements EIP165 and returns `true` for the specified interfaceID, its address + /// will be returned. + /// @param node The ENS node to query. + /// @param interfaceID The EIP 165 interface ID to check for. + /// + /// @return The address that implements this interface, or address(0) if the interface is unsupported. + function interfaceImplementer(bytes32 node, bytes4 interfaceID) external view virtual override returns (address) { + address implementer = _getInterfaceResolverStorage().versionable_interfaces[_getResolverBaseStorage() + .recordVersions[node]][node][interfaceID]; + if (implementer != address(0)) { + return implementer; + } + + address a = IAddrResolver(address(this)).addr(node); + if (a == address(0)) { + return address(0); + } + + (bool success, bytes memory returnData) = + a.staticcall(abi.encodeWithSignature("supportsInterface(bytes4)", type(IERC165).interfaceId)); + if (!success || returnData.length < 32 || returnData[31] == 0) { + // EIP 165 not supported by target + return address(0); + } + + (success, returnData) = a.staticcall(abi.encodeWithSignature("supportsInterface(bytes4)", interfaceID)); + if (!success || returnData.length < 32 || returnData[31] == 0) { + // Specified interface not supported by target + return address(0); + } + + return a; + } + + /// @notice ERC-165 compliance. + function supportsInterface(bytes4 interfaceID) public view virtual override returns (bool) { + return interfaceID == type(IInterfaceResolver).interfaceId || super.supportsInterface(interfaceID); + } + + /// @notice EIP-7201 storage pointer fetch helper. + function _getInterfaceResolverStorage() internal pure returns (InterfaceResolverStorage storage $) { + assembly { + $.slot := INTERFACE_RESOLVER_STORAGE + } + } +} diff --git a/src/L2/resolver/NameResolver.sol b/src/L2/resolver/NameResolver.sol new file mode 100644 index 0000000..1bfe90e --- /dev/null +++ b/src/L2/resolver/NameResolver.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {INameResolver} from "ens-contracts/resolvers/profiles/INameResolver.sol"; + +import {ResolverBase} from "./ResolverBase.sol"; + +/// @title Name Resolver +/// +/// @notice ENSIP-3 compliant Name resolver. Adaptation of the ENS NameResolver.sol profile contract, with +/// EIP-7201 storage compliance. +/// https://github.com/ensdomains/ens-contracts/blob/staging/contracts/resolvers/profiles/NameResolver.sol +/// +/// @author Coinbase (https://github.com/base-org/basenames) +abstract contract NameResolver is INameResolver, ResolverBase { + struct NameResolverStorage { + /// @notice Names by node and version. + mapping(uint64 version => mapping(bytes32 node => string name)) versionable_names; + } + + /// @notice EIP-7201 storage location. + // keccak256(abi.encode(uint256(keccak256("name.resolver.storage")) - 1)) & ~bytes32(uint256(0xff)); + bytes32 constant NAME_RESOLVER_STORAGE = 0x23d7cb83bcf6186ccf590f4291f50469cd60b0ac3c413e76ea47a810986d8500; + + /// @notice Sets the name associated with an ENS node. + /// + /// @param node The node to update. + function setName(bytes32 node, string calldata newName) external virtual authorised(node) { + _getNameResolver().versionable_names[_getResolverBaseStorage().recordVersions[node]][node] = newName; + emit NameChanged(node, newName); + } + + /// @notice Returns the name associated with an ENS node. + /// + /// @param node The ENS node to query. + /// + /// @return The associated name. + function name(bytes32 node) external view virtual override returns (string memory) { + return _getNameResolver().versionable_names[_getResolverBaseStorage().recordVersions[node]][node]; + } + + /// @notice ERC-165 compliance. + function supportsInterface(bytes4 interfaceID) public view virtual override returns (bool) { + return interfaceID == type(INameResolver).interfaceId || super.supportsInterface(interfaceID); + } + + /// @notice EIP-7201 storage pointer fetch helper. + function _getNameResolver() internal pure returns (NameResolverStorage storage $) { + assembly { + $.slot := NAME_RESOLVER_STORAGE + } + } +} diff --git a/src/L2/resolver/PubkeyResolver.sol b/src/L2/resolver/PubkeyResolver.sol new file mode 100644 index 0000000..f5feaca --- /dev/null +++ b/src/L2/resolver/PubkeyResolver.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {IPubkeyResolver} from "ens-contracts/resolvers/profiles/IPubkeyResolver.sol"; + +import {ResolverBase} from "./ResolverBase.sol"; + +/// @title Pubkey Resolver +/// +/// @notice Adaptation of the ENS PubkeyResolver.sol profile contract, with EIP-7201 storage compliance. +/// https://github.com/ensdomains/ens-contracts/blob/staging/contracts/resolvers/profiles/PubkeyResolver.sol +/// +/// @author Coinbase (https://github.com/base-org/basenames) +abstract contract PubkeyResolver is IPubkeyResolver, ResolverBase { + /// @notice Tuple containing the x and y coordinates of a public key. + struct PublicKey { + bytes32 x; + bytes32 y; + } + + struct PubkeyResolverStorage { + /// @notice Public keys by node and version. + mapping(uint64 version => mapping(bytes32 node => PublicKey pubkey)) versionable_pubkeys; + } + + /// @notice EIP-7201 storage location. + // keccak256(abi.encode(uint256(keccak256("pubkey.resolver.storage")) - 1)) & ~bytes32(uint256(0xff)); + bytes32 constant PUBKEY_RESOLVER_STORAGE = 0x59a318c6a4da81295c2a32b42a02c3db057bb9422e2eb1f6e516ee3694b1ef00; + + /// @notice Sets the SECP256k1 public key associated with an ENS node. + /// + /// @param node The ENS node to query. + /// + /// @param x the X coordinate of the curve point for the public key. + /// @param y the Y coordinate of the curve point for the public key. + function setPubkey(bytes32 node, bytes32 x, bytes32 y) external virtual authorised(node) { + _getPubkeyResolverStorage().versionable_pubkeys[_getResolverBaseStorage().recordVersions[node]][node] = + PublicKey(x, y); + emit PubkeyChanged(node, x, y); + } + + /// @notice Returns the SECP256k1 public key associated with an ENS node. + /// + /// @dev See EIP-619. + /// + /// @param node The ENS node to query. + /// + /// @return x The X coordinate of the curve point for the public key. + /// @return y The Y coordinate of the curve point for the public key. + function pubkey(bytes32 node) external view virtual override returns (bytes32 x, bytes32 y) { + uint64 currentRecordVersion = _getResolverBaseStorage().recordVersions[node]; + PubkeyResolverStorage storage $ = _getPubkeyResolverStorage(); + return + ($.versionable_pubkeys[currentRecordVersion][node].x, $.versionable_pubkeys[currentRecordVersion][node].y); + } + + /// @notice ERC-165 compliance. + function supportsInterface(bytes4 interfaceID) public view virtual override returns (bool) { + return interfaceID == type(IPubkeyResolver).interfaceId || super.supportsInterface(interfaceID); + } + + /// @notice EIP-7201 storage pointer fetch helper. + function _getPubkeyResolverStorage() internal pure returns (PubkeyResolverStorage storage $) { + assembly { + $.slot := PUBKEY_RESOLVER_STORAGE + } + } +} diff --git a/src/L2/resolver/ResolverBase.sol b/src/L2/resolver/ResolverBase.sol new file mode 100644 index 0000000..3d3a24c --- /dev/null +++ b/src/L2/resolver/ResolverBase.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {ERC165} from "lib/openzeppelin-contracts/contracts/utils/introspection/ERC165.sol"; +import {IVersionableResolver} from "ens-contracts/resolvers/profiles/IVersionableResolver.sol"; + +abstract contract ResolverBase is ERC165, IVersionableResolver { + struct ResolverBaseStorage { + mapping(bytes32 node => uint64 version) recordVersions; + } + + error NotAuthorized(bytes32 node, address caller); + + // keccak256(abi.encode(uint256(keccak256("resolver.base.storage")) - 1)) & ~bytes32(uint256(0xff)); + bytes32 private constant RESOLVER_BASE_LOCATION = 0x421bc1b234e222da5ef3c41832b689b450ae239e8b18cf3c05f5329ae7d99700; + + function isAuthorised(bytes32 node) internal view virtual returns (bool); + + modifier authorised(bytes32 node) { + if (!isAuthorised(node)) revert NotAuthorized(node, msg.sender); + _; + } + + /** + * Increments the record version 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. + */ + function clearRecords(bytes32 node) public virtual authorised(node) { + ResolverBaseStorage storage $ = _getResolverBaseStorage(); + $.recordVersions[node]++; + emit VersionChanged(node, $.recordVersions[node]); + } + + function recordVersions(bytes32 node) external view returns (uint64) { + return _getResolverBaseStorage().recordVersions[node]; + } + + function supportsInterface(bytes4 interfaceID) public view virtual override returns (bool) { + return interfaceID == type(IVersionableResolver).interfaceId || super.supportsInterface(interfaceID); + } + + function _getResolverBaseStorage() internal pure returns (ResolverBaseStorage storage $) { + assembly { + $.slot := RESOLVER_BASE_LOCATION + } + } +} diff --git a/src/L2/resolver/TextResolver.sol b/src/L2/resolver/TextResolver.sol new file mode 100644 index 0000000..2771567 --- /dev/null +++ b/src/L2/resolver/TextResolver.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {ITextResolver} from "ens-contracts/resolvers/profiles/ITextResolver.sol"; + +import {ResolverBase} from "./ResolverBase.sol"; + +/// @title Text Resolver +/// +/// @notice ENSIP-5 compliant Text Resolver. Adaptation of the ENS TextResolver.sol profile contract, with +/// EIP-7201 storage compliance. +/// https://github.com/ensdomains/ens-contracts/blob/staging/contracts/resolvers/profiles/TextResolver.sol +/// +/// @author Coinbase (https://github.com/base-org/basenames) +abstract contract TextResolver is ITextResolver, ResolverBase { + struct TextResolverStorage { + /// @notice Text value by text key, node, and version. + mapping(uint64 version => mapping(bytes32 node => mapping(string text_key => string text_value))) + versionable_texts; + } + + /// @notice EIP-7201 storage location. + // keccak256(abi.encode(uint256(keccak256("text.resolver.storage")) - 1)) & ~bytes32(uint256(0xff)); + bytes32 constant TEXT_RESOLVER_STORAGE = 0x0795ed949e6fff5efdc94a1021939889222c7fb041954dcfee28c913f2af9200; + + /// @notice Sets the text data associated with an ENS node and key. + /// + /// @param node The node to update. + /// @param key The key to set. + /// @param value The text data value to set. + function setText(bytes32 node, string calldata key, string calldata value) external virtual authorised(node) { + _getTextResolverStorage().versionable_texts[_getResolverBaseStorage().recordVersions[node]][node][key] = value; + emit TextChanged(node, key, key, value); + } + + /// @notice Returns the text data associated with an ENS node and key. + /// + /// @param node The ENS node to query. + /// @param key The text data key to query. + /// + /// @return The associated text data. + function text(bytes32 node, string calldata key) external view virtual override returns (string memory) { + return _getTextResolverStorage().versionable_texts[_getResolverBaseStorage().recordVersions[node]][node][key]; + } + + /// @notice ERC-165 compliance. + function supportsInterface(bytes4 interfaceID) public view virtual override returns (bool) { + return interfaceID == type(ITextResolver).interfaceId || super.supportsInterface(interfaceID); + } + + /// @notice EIP-7201 storage pointer fetch helper. + function _getTextResolverStorage() internal pure returns (TextResolverStorage storage $) { + assembly { + $.slot := TEXT_RESOLVER_STORAGE + } + } +} diff --git a/test/UpgradeableL2Resolver/Approve.t.sol b/test/UpgradeableL2Resolver/Approve.t.sol new file mode 100644 index 0000000..58d9937 --- /dev/null +++ b/test/UpgradeableL2Resolver/Approve.t.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {UpgradeableL2ResolverBase} from "./UpgradeableL2ResolverBase.t.sol"; +import {UpgradeableL2Resolver} from "src/L2/UpgradeableL2Resolver.sol"; + +contract Approve is UpgradeableL2ResolverBase { + function test_revertsIfCalledForSelf() public { + vm.expectRevert(UpgradeableL2Resolver.CantSetSelfAsDelegate.selector); + vm.prank(user); + resolver.approve(node, user, true); + } + + function test_allowsSenderToSetDelegate(address operator) public { + vm.assume(operator != user); + vm.expectEmit(address(resolver)); + emit UpgradeableL2Resolver.Approved(user, node, operator, true); + vm.prank(user); + resolver.approve(node, operator, true); + assertTrue(resolver.isApprovedFor(user, node, operator)); + } +} diff --git a/test/UpgradeableL2Resolver/ClearRecords.t.sol b/test/UpgradeableL2Resolver/ClearRecords.t.sol new file mode 100644 index 0000000..f9048a2 --- /dev/null +++ b/test/UpgradeableL2Resolver/ClearRecords.t.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {UpgradeableL2ResolverBase} from "./UpgradeableL2ResolverBase.t.sol"; +import {ResolverBase} from "src/L2/resolver/ResolverBase.sol"; + +import {IVersionableResolver} from "ens-contracts/resolvers/profiles/IVersionableResolver.sol"; + +contract ClearRecords is UpgradeableL2ResolverBase { + function test_reverts_forUnauthorizedUser() public { + vm.expectRevert(abi.encodeWithSelector(ResolverBase.NotAuthorized.selector, node, notUser)); + vm.prank(notUser); + resolver.clearRecords(node); + } + + function test_clearRecords() public { + uint64 currentRecordVersion = resolver.recordVersions(node); + vm.prank(user); + vm.expectEmit(address(resolver)); + emit IVersionableResolver.VersionChanged(node, currentRecordVersion + 1); + resolver.clearRecords(node); + } +} diff --git a/test/UpgradeableL2Resolver/IsAuthorised.t.sol b/test/UpgradeableL2Resolver/IsAuthorised.t.sol new file mode 100644 index 0000000..04a9722 --- /dev/null +++ b/test/UpgradeableL2Resolver/IsAuthorised.t.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {UpgradeableL2ResolverBase} from "./UpgradeableL2ResolverBase.t.sol"; +import {BASE_ETH_NODE} from "src/util/Constants.sol"; +import {ResolverBase} from "src/L2/resolver/ResolverBase.sol"; + +// Because isAuthorised() is an internal method, we test it indirectly here by using `setAddr()` which +// checks the authorization status via `isAuthorised()`. +contract IsAuthorised is UpgradeableL2ResolverBase { + function test_returnsTrue_ifSenderIsController() public { + vm.prank(controller); + resolver.setAddr(node, user); + assertEq(resolver.addr(node), user); + } + + function test_returnsTrue_ifSenderIsReverse() public { + vm.prank(reverse); + resolver.setAddr(node, user); + assertEq(resolver.addr(node), user); + } + + function test_returnsFalse_ifSenderIsNotAuthorised(address operator) public notProxyAdmin(operator) { + vm.assume(operator != controller && operator != reverse && operator != user); + + vm.prank(operator); + vm.expectRevert(abi.encodeWithSelector(ResolverBase.NotAuthorized.selector, node, operator)); + resolver.setAddr(node, user); + } + + function test_returnsTrue_ifSenderIOwnerOfNode() public { + vm.prank(owner); + registry.setSubnodeOwner(BASE_ETH_NODE, label, user); + vm.prank(user); + resolver.setAddr(node, user); + assertEq(resolver.addr(node), user); + } + + function test_returnsTrue_ifSenderIOperatorOfNode(address operator) public notProxyAdmin(operator) { + vm.assume(operator != user); + vm.prank(owner); + registry.setSubnodeOwner(BASE_ETH_NODE, label, user); + vm.prank(user); + resolver.setApprovalForAll(operator, true); + vm.prank(operator); + resolver.setAddr(node, user); + assertEq(resolver.addr(node), user); + } + + function test_returnsTrue_ifSenderIDelegateOfNode(address operator) public notProxyAdmin(operator) { + vm.assume(operator != user); + vm.prank(owner); + registry.setSubnodeOwner(BASE_ETH_NODE, label, user); + vm.prank(user); + resolver.approve(node, operator, true); + vm.prank(operator); + resolver.setAddr(node, user); + assertEq(resolver.addr(node), user); + } +} diff --git a/test/UpgradeableL2Resolver/SetABI.t.sol b/test/UpgradeableL2Resolver/SetABI.t.sol new file mode 100644 index 0000000..f739159 --- /dev/null +++ b/test/UpgradeableL2Resolver/SetABI.t.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {UpgradeableL2ResolverBase} from "./UpgradeableL2ResolverBase.t.sol"; +import {ResolverBase} from "src/L2/resolver/ResolverBase.sol"; +import {ABIResolver} from "src/L2/resolver/ABIResolver.sol"; + +contract SetABI is UpgradeableL2ResolverBase { + uint256 constant JSON_CONTENT = 1; + uint256 constant ZLIB_JSON_CONTENT = 2; + uint256 constant CBOR_CONTENT = 4; + uint256 constant URI_CONTENT = 8; + uint256 constant INVALID_CONTENT = 3; + bytes data = "data"; + + function test_reverts_forUnauthorizedUser() public { + vm.expectRevert(abi.encodeWithSelector(ResolverBase.NotAuthorized.selector, node, notUser)); + vm.prank(notUser); + resolver.setABI(node, JSON_CONTENT, data); + } + + function test_reverts_withInvalidContentType() public { + vm.expectRevert(ABIResolver.InvalidContentType.selector); + vm.prank(user); + resolver.setABI(node, INVALID_CONTENT, data); + } + + function test_setsTheABICorrectly_forJSONContent() public { + vm.prank(user); + resolver.setABI(node, JSON_CONTENT, data); + (uint256 retType, bytes memory retData) = resolver.ABI(node, JSON_CONTENT); + _validateReturnedContent(retType, JSON_CONTENT, retData); + } + + function test_setsTheABICorrectly_forZlibJSONContent() public { + vm.prank(user); + resolver.setABI(node, ZLIB_JSON_CONTENT, data); + (uint256 retType, bytes memory retData) = resolver.ABI(node, ZLIB_JSON_CONTENT); + _validateReturnedContent(retType, ZLIB_JSON_CONTENT, retData); + } + + function test_setsTheABICorrectly_forCBORContent() public { + vm.prank(user); + resolver.setABI(node, CBOR_CONTENT, data); + (uint256 retType, bytes memory retData) = resolver.ABI(node, CBOR_CONTENT); + _validateReturnedContent(retType, CBOR_CONTENT, retData); + } + + function test_setsTheABICorrectly_forURIContent() public { + vm.prank(user); + resolver.setABI(node, URI_CONTENT, data); + (uint256 retType, bytes memory retData) = resolver.ABI(node, URI_CONTENT); + _validateReturnedContent(retType, URI_CONTENT, retData); + } + + function test_doesNotRevertIfNotSet() public view { + (uint256 retType, bytes memory retData) = resolver.ABI(node, JSON_CONTENT); + _validateDefaultReturn(retType, retData); + } + + function test_doesNotRevertIfIncompatible() public { + vm.prank(user); + resolver.setABI(node, URI_CONTENT, data); + (uint256 retType, bytes memory retData) = resolver.ABI(node, JSON_CONTENT); + _validateDefaultReturn(retType, retData); + } + + function test_canClearRecord() public { + vm.startPrank(user); + + resolver.setABI(node, JSON_CONTENT, data); + (uint256 retType, bytes memory retData) = resolver.ABI(node, JSON_CONTENT); + _validateReturnedContent(retType, JSON_CONTENT, retData); + + resolver.clearRecords(node); + (retType, retData) = resolver.ABI(node, JSON_CONTENT); + _validateDefaultReturn(retType, retData); + + vm.stopPrank(); + } + + function _validateReturnedContent(uint256 retType, uint256 expectedType, bytes memory retData) internal view { + assertEq(retType, expectedType); + assertEq(keccak256(retData), keccak256(data)); + } + + function _validateDefaultReturn(uint256 retType, bytes memory retData) internal pure { + assertEq(retType, 0); + assertEq(retData, ""); + } +} diff --git a/test/UpgradeableL2Resolver/SetAddr.t.sol b/test/UpgradeableL2Resolver/SetAddr.t.sol new file mode 100644 index 0000000..4bfe38d --- /dev/null +++ b/test/UpgradeableL2Resolver/SetAddr.t.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {UpgradeableL2ResolverBase} from "./UpgradeableL2ResolverBase.t.sol"; +import {ResolverBase} from "src/L2/resolver/ResolverBase.sol"; +import {AddrResolver} from "src/L2/resolver/AddrResolver.sol"; + +contract SetAddr is UpgradeableL2ResolverBase { + uint256 BTC_COINTYPE = 0; + uint256 ETH_COINTYPE = 60; + uint256 BASE_COINTYPE = 2147492101; + + function test_reverts_forUnauthorizedUser() public { + vm.expectRevert(abi.encodeWithSelector(ResolverBase.NotAuthorized.selector, node, notUser)); + vm.prank(notUser); + resolver.setAddr(node, notUser); + } + + function test_reverts_for_invalidAddress() public { + vm.prank(user); + vm.expectRevert(); + resolver.setAddr(node, 60, ""); + } + + function test_setsAnETHAddress_byDefault(address a) public { + vm.prank(user); + resolver.setAddr(node, a); + assertEq(resolver.addr(node), a); + assertEq(bytesToAddress(resolver.addr(node, ETH_COINTYPE)), a); + } + + function test_setsAnETHAddress(address a) public { + vm.prank(user); + resolver.setAddr(node, ETH_COINTYPE, addressToBytes(a)); + assertEq(resolver.addr(node), a); + assertEq(bytesToAddress(resolver.addr(node, ETH_COINTYPE)), a); + } + + function test_setsABaseAddress(address a) public { + vm.prank(user); + resolver.setAddr(node, BASE_COINTYPE, addressToBytes(a)); + assertEq(bytesToAddress(resolver.addr(node, BASE_COINTYPE)), a); + } + + function test_setsABtcAddress() public { + bytes memory satoshi = hex"76a91462e907b15cbf27d5425399ebf6f0fb50ebb88f1888ac"; + vm.prank(user); + resolver.setAddr(node, BTC_COINTYPE, satoshi); + assertEq(keccak256(resolver.addr(node, BTC_COINTYPE)), keccak256(satoshi)); + } + + function test_canClearRecord(address a) public { + vm.startPrank(user); + + resolver.setAddr(node, a); + assertEq(resolver.addr(node), a); + + resolver.clearRecords(node); + assertEq(resolver.addr(node), address(0)); + + vm.stopPrank(); + } + + /// @notice Helper to convert bytes into an EVM address object. + 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)) + } + } + + /// @notice Helper to convert an EVM address to a bytes object. + function addressToBytes(address a) internal pure returns (bytes memory b) { + b = new bytes(20); + assembly { + mstore(add(b, 32), mul(a, exp(256, 12))) + } + } +} diff --git a/test/UpgradeableL2Resolver/SetApprovalForAll.t.sol b/test/UpgradeableL2Resolver/SetApprovalForAll.t.sol new file mode 100644 index 0000000..1a5f9a7 --- /dev/null +++ b/test/UpgradeableL2Resolver/SetApprovalForAll.t.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {UpgradeableL2ResolverBase} from "./UpgradeableL2ResolverBase.t.sol"; +import {L2Resolver} from "src/L2/L2Resolver.sol"; + +contract SetApprovalForAll is UpgradeableL2ResolverBase { + function test_revertsIfCalledForSelf() public { + vm.expectRevert(L2Resolver.CantSetSelfAsOperator.selector); + vm.prank(user); + resolver.setApprovalForAll(user, true); + } + + function test_allowsSenderToSetApproval(address operator, bool approve) public { + vm.assume(operator != user); + vm.expectEmit(address(resolver)); + emit L2Resolver.ApprovalForAll(user, operator, approve); + vm.prank(user); + resolver.setApprovalForAll(operator, approve); + assertEq(resolver.isApprovedForAll(user, operator), approve); + } +} diff --git a/test/UpgradeableL2Resolver/SetContentHash.t.sol b/test/UpgradeableL2Resolver/SetContentHash.t.sol new file mode 100644 index 0000000..a642842 --- /dev/null +++ b/test/UpgradeableL2Resolver/SetContentHash.t.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {UpgradeableL2ResolverBase} from "./UpgradeableL2ResolverBase.t.sol"; +import {ResolverBase} from "src/L2/resolver/ResolverBase.sol"; +import {ContentHashResolver} from "src/L2/resolver/ContentHashResolver.sol"; + +contract SetContenthash is UpgradeableL2ResolverBase { + bytes IPFS_Data = hex"e3010170122029f2d17be6139079dc48696d1f582a8530eb9805b561eda517e22a892c7e3f1f"; + + function test_reverts_forUnauthorizedUser() public { + vm.expectRevert(abi.encodeWithSelector(ResolverBase.NotAuthorized.selector, node, notUser)); + vm.prank(notUser); + resolver.setContenthash(node, IPFS_Data); + } + + function test_setsAContenthash() public { + vm.prank(user); + resolver.setContenthash(node, IPFS_Data); + assertEq(keccak256(resolver.contenthash(node)), keccak256(IPFS_Data)); + } + + function test_canClearRecord() public { + vm.startPrank(user); + + resolver.setContenthash(node, IPFS_Data); + assertEq(resolver.contenthash(node), IPFS_Data); + + resolver.clearRecords(node); + assertEq(resolver.contenthash(node), ""); + + vm.stopPrank(); + } +} diff --git a/test/UpgradeableL2Resolver/SetDNSRecords.t.sol b/test/UpgradeableL2Resolver/SetDNSRecords.t.sol new file mode 100644 index 0000000..e08e671 --- /dev/null +++ b/test/UpgradeableL2Resolver/SetDNSRecords.t.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {UpgradeableL2ResolverBase} from "./UpgradeableL2ResolverBase.t.sol"; +import {ResolverBase} from "src/L2/resolver/ResolverBase.sol"; +import {DNSResolver} from "src/L2/resolver/DNSResolver.sol"; + +import {NameEncoder} from "ens-contracts/utils/NameEncoder.sol"; + +contract SetDNSRecords is UpgradeableL2ResolverBase { + // Test data encoding taken from ENS text fixture: + // https://github.com/ensdomains/ens-contracts/blob/5421b5689e695531dc9739f0ad861839bdd231cb/test/resolvers/TestPublicResolver.ts#L69 + // Wire-encoded records: + // a.eth. 3600 IN A 1.2.3.4 + bytes arec = hex"016103657468000001000100000e10000401020304"; + // b.eth. 3600 IN A 2.3.4.5 + bytes b1rec = hex"016203657468000001000100000e10000402030405"; + // b.eth. 3600 IN A 3.4.5.6 + bytes b2rec = hex"016203657468000001000100000e10000403040506"; + // eth. 86400 IN SOA ns1.ethdns.xyz. hostmaster.test.eth. 2018061501 15620 1800 1814400 14400 + bytes soarec = + hex"03657468000006000100015180003a036e733106657468646e730378797a000a686f73746d6173746572057465737431036574680078492cbd00003d0400000708001baf8000003840"; + bytes dnsRecord = bytes.concat(arec, b1rec, b2rec, soarec); + + // DNS Record types: https://en.wikipedia.org/wiki/List_of_DNS_record_types + uint16 constant A_RESOURCE = 1; + uint16 constant SOA_RESOURCE = 6; + + function test_reverts_forUnauthorizedUser() public { + vm.expectRevert(abi.encodeWithSelector(ResolverBase.NotAuthorized.selector, node, notUser)); + vm.prank(notUser); + resolver.setDNSRecords(node, dnsRecord); + } + + function test_setsTheDNSRecord() public { + vm.prank(user); + resolver.setDNSRecords(node, dnsRecord); + + (bytes memory aDnsName,) = NameEncoder.dnsEncodeName("a.eth"); + bytes memory arecRet = resolver.dnsRecord(node, keccak256(aDnsName), A_RESOURCE); + assertEq(keccak256(arecRet), keccak256(arec)); + + (bytes memory bDnsName,) = NameEncoder.dnsEncodeName("b.eth"); + bytes memory brecRet = resolver.dnsRecord(node, keccak256(bDnsName), A_RESOURCE); + assertEq(keccak256(brecRet), keccak256(bytes.concat(b1rec, b2rec))); + + (bytes memory ethDnsName,) = NameEncoder.dnsEncodeName("eth"); + bytes memory soarecRet = resolver.dnsRecord(node, keccak256(ethDnsName), SOA_RESOURCE); + assertEq(keccak256(soarecRet), keccak256(soarec)); + } + + function test_shouldKeepTrackOfEntries() public { + vm.startPrank(user); + resolver.setDNSRecords(node, dnsRecord); + + // c.eth. 3600 IN A 1.2.3.4 + bytes memory crec = hex"016303657468000001000100000e10000401020304"; + resolver.setDNSRecords(node, crec); + + (bytes memory cDnsName,) = NameEncoder.dnsEncodeName("c.eth"); + (bytes memory dDnsName,) = NameEncoder.dnsEncodeName("d.eth"); + + // Initial check + assertTrue(resolver.hasDNSRecords(node, keccak256(cDnsName))); + assertFalse(resolver.hasDNSRecords(node, keccak256(dDnsName))); + + // Update with no new data makes no difference + resolver.setDNSRecords(node, crec); + assertTrue(resolver.hasDNSRecords(node, keccak256(cDnsName))); + + // c.eth. 3600 IN A + bytes memory crec2 = hex"016303657468000001000100000e100000"; + resolver.setDNSRecords(node, crec2); + + assertFalse(resolver.hasDNSRecords(node, keccak256("c.eth"))); + + vm.stopPrank(); + } + + function test_canClearRecord() public { + vm.startPrank(user); + + (bytes memory aDnsName,) = NameEncoder.dnsEncodeName("a.eth"); + resolver.setDNSRecords(node, dnsRecord); + assertEq(resolver.dnsRecord(node, keccak256(aDnsName), A_RESOURCE), arec); + + resolver.clearRecords(node); + assertEq(resolver.dnsRecord(node, keccak256(aDnsName), A_RESOURCE), ""); + + vm.stopPrank(); + } +} diff --git a/test/UpgradeableL2Resolver/SetInterface.t.sol b/test/UpgradeableL2Resolver/SetInterface.t.sol new file mode 100644 index 0000000..4c4541b --- /dev/null +++ b/test/UpgradeableL2Resolver/SetInterface.t.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {ERC165} from "lib/openzeppelin-contracts/contracts/utils/introspection/ERC165.sol"; +import {UpgradeableL2ResolverBase} from "./UpgradeableL2ResolverBase.t.sol"; +import {ResolverBase} from "src/L2/resolver/ResolverBase.sol"; +import {InterfaceResolver} from "src/L2/resolver/InterfaceResolver.sol"; + +contract SetInterface is UpgradeableL2ResolverBase { + Counter counter; + + function setUp() public override { + super.setUp(); + counter = new Counter(); + } + + function test_reverts_forUnauthorizedUser() public { + vm.expectRevert(abi.encodeWithSelector(ResolverBase.NotAuthorized.selector, node, notUser)); + vm.prank(notUser); + resolver.setInterface(node, type(ICounter).interfaceId, address(counter)); + } + + function test_setsTheInterface_whenTheAddressIsSpecifiedExplicitly() public { + vm.prank(user); + resolver.setInterface(node, type(ICounter).interfaceId, address(counter)); + assertEq(resolver.interfaceImplementer(node, type(ICounter).interfaceId), address(counter)); + } + + function test_returnsTheInterface_whenTheAddressIsSetToTheAddrProfile() public { + vm.prank(user); + resolver.setAddr(node, address(counter)); + assertEq(resolver.interfaceImplementer(node, type(ICounter).interfaceId), address(counter)); + } + + function test_returnsZeroAddress_whenNotSet() public view { + assertEq(resolver.interfaceImplementer(node, type(ICounter).interfaceId), address(0)); + } + + function test_returnsZeroAddress_whenAddressIsNotContract(address notContract) public { + vm.assume(notContract.code.length == 0); + assumeNotPrecompile(notContract); + + vm.prank(user); + resolver.setAddr(node, address(notContract)); + assertEq(resolver.addr(node), notContract); + + assertEq(resolver.interfaceImplementer(node, type(ICounter).interfaceId), address(0)); + } + + function test_returnsZeroAddr_whenNotImplemented() public { + vm.prank(user); + resolver.setAddr(node, address(counter)); + assertEq(resolver.interfaceImplementer(node, type(IGreeter).interfaceId), address(0)); + } + + function test_canClearRecord() public { + vm.startPrank(user); + + resolver.setInterface(node, type(ICounter).interfaceId, address(counter)); + assertEq(resolver.interfaceImplementer(node, type(ICounter).interfaceId), address(counter)); + + resolver.clearRecords(node); + assertEq(resolver.interfaceImplementer(node, type(ICounter).interfaceId), address(0)); + + vm.stopPrank(); + } +} + +interface ICounter { + function set(uint256 x) external; +} + +interface IGreeter { + function greet() external returns (string memory); +} + +contract Counter is ICounter, ERC165 { + uint256 public x; + + function set(uint256 x_) external { + x = x_; + } + + /// @notice ERC-165 compliance. + function supportsInterface(bytes4 interfaceID) public view virtual override returns (bool) { + return interfaceID == type(ICounter).interfaceId || super.supportsInterface(interfaceID); + } +} diff --git a/test/UpgradeableL2Resolver/SetName.t.sol b/test/UpgradeableL2Resolver/SetName.t.sol new file mode 100644 index 0000000..b88a0cb --- /dev/null +++ b/test/UpgradeableL2Resolver/SetName.t.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {UpgradeableL2ResolverBase} from "./UpgradeableL2ResolverBase.t.sol"; +import {ResolverBase} from "src/L2/resolver/ResolverBase.sol"; +import {NameResolver} from "src/L2/resolver/NameResolver.sol"; + +contract SetName is UpgradeableL2ResolverBase { + function test_reverts_forUnauthorizedUser() public { + vm.expectRevert(abi.encodeWithSelector(ResolverBase.NotAuthorized.selector, node, notUser)); + vm.prank(notUser); + resolver.setName(node, name); + } + + function test_setsTheName() public { + vm.prank(user); + resolver.setName(node, name); + string memory retName = resolver.name(node); + assertEq(keccak256(bytes(name)), keccak256(bytes(retName))); + } + + function test_canClearRecord() public { + vm.startPrank(user); + + resolver.setName(node, name); + assertEq(resolver.name(node), name); + + resolver.clearRecords(node); + assertEq(resolver.name(node), ""); + + vm.stopPrank(); + } +} diff --git a/test/UpgradeableL2Resolver/SetPubkey.t.sol b/test/UpgradeableL2Resolver/SetPubkey.t.sol new file mode 100644 index 0000000..4a74d04 --- /dev/null +++ b/test/UpgradeableL2Resolver/SetPubkey.t.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {UpgradeableL2ResolverBase} from "./UpgradeableL2ResolverBase.t.sol"; +import {ResolverBase} from "src/L2/resolver/ResolverBase.sol"; +import {PubkeyResolver} from "src/L2/resolver/PubkeyResolver.sol"; + +contract SetPubkey is UpgradeableL2ResolverBase { + bytes32 x = 0x65a2fa44daad46eab0278703edb6c4dcf5e30b8a9aec09fdc71a56f52aa392e4; + bytes32 y = 0x4a7a9e4604aa36898209997288e902ac544a555e4b5e0a9efef2b59233f3f437; + + function test_reverts_forUnauthorizedUser() public { + vm.expectRevert(abi.encodeWithSelector(ResolverBase.NotAuthorized.selector, node, notUser)); + vm.prank(notUser); + resolver.setPubkey(node, x, y); + } + + function test_setsThePubkey() public { + vm.prank(user); + resolver.setPubkey(node, x, y); + (bytes32 retX, bytes32 retY) = resolver.pubkey(node); + assertEq(retX, x); + assertEq(retY, y); + } + + function test_canClearRecord() public { + vm.startPrank(user); + + resolver.setPubkey(node, x, y); + (bytes32 retX, bytes32 retY) = resolver.pubkey(node); + assertEq(retX, x); + assertEq(retY, y); + + resolver.clearRecords(node); + (retX, retY) = resolver.pubkey(node); + assertEq(retX, 0); + assertEq(retY, 0); + + vm.stopPrank(); + } +} diff --git a/test/UpgradeableL2Resolver/SetRegistrarController.t.sol b/test/UpgradeableL2Resolver/SetRegistrarController.t.sol new file mode 100644 index 0000000..0b34d3c --- /dev/null +++ b/test/UpgradeableL2Resolver/SetRegistrarController.t.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {UpgradeableL2ResolverBase} from "./UpgradeableL2ResolverBase.t.sol"; +import {UpgradeableL2Resolver} from "src/L2/UpgradeableL2Resolver.sol"; +import {Ownable} from "solady/auth/Ownable.sol"; + +contract SetRegistrarController is UpgradeableL2ResolverBase { + function test_reverts_ifCalledByNonOwner(address caller, address newController) public notProxyAdmin(caller) { + vm.assume(caller != owner); + vm.expectRevert(Ownable.Unauthorized.selector); + vm.prank(caller); + resolver.setRegistrarController(newController); + } + + function test_setsTheRegistrarControllerAccordingly(address newController) public { + vm.expectEmit(); + emit UpgradeableL2Resolver.RegistrarControllerUpdated(newController); + vm.prank(owner); + resolver.setRegistrarController(newController); + assertEq(resolver.registrarController(), newController); + } +} diff --git a/test/UpgradeableL2Resolver/SetReverseRegistrar.t.sol b/test/UpgradeableL2Resolver/SetReverseRegistrar.t.sol new file mode 100644 index 0000000..11343c5 --- /dev/null +++ b/test/UpgradeableL2Resolver/SetReverseRegistrar.t.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {UpgradeableL2ResolverBase} from "./UpgradeableL2ResolverBase.t.sol"; +import {UpgradeableL2Resolver} from "src/L2/UpgradeableL2Resolver.sol"; +import {Ownable} from "solady/auth/Ownable.sol"; + +contract SetReverseRegistrar is UpgradeableL2ResolverBase { + function test_reverts_ifCalledByNonOwner(address caller, address newReverse) public notProxyAdmin(caller) { + vm.assume(caller != owner); + vm.expectRevert(Ownable.Unauthorized.selector); + vm.prank(caller); + resolver.setReverseRegistrar(newReverse); + } + + function test_setsTheReverseRegistrarAccordingly(address newReverse) public { + vm.expectEmit(); + emit UpgradeableL2Resolver.ReverseRegistrarUpdated(newReverse); + vm.prank(owner); + resolver.setReverseRegistrar(newReverse); + assertEq(resolver.reverseRegistrar(), newReverse); + } +} diff --git a/test/UpgradeableL2Resolver/SetText.t.sol b/test/UpgradeableL2Resolver/SetText.t.sol new file mode 100644 index 0000000..4d0ee31 --- /dev/null +++ b/test/UpgradeableL2Resolver/SetText.t.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {UpgradeableL2ResolverBase} from "./UpgradeableL2ResolverBase.t.sol"; +import {ResolverBase} from "src/L2/resolver/ResolverBase.sol"; +import {TextResolver} from "src/L2/resolver/TextResolver.sol"; + +contract SetText is UpgradeableL2ResolverBase { + string key = "key"; + string value = "value"; + + function test_reverts_forUnauthorizedUser() public { + vm.expectRevert(abi.encodeWithSelector(ResolverBase.NotAuthorized.selector, node, notUser)); + vm.prank(notUser); + resolver.setText(node, key, value); + } + + function test_setsTheTextValue_forTheSpecifiedKey() public { + vm.prank(user); + resolver.setText(node, key, value); + string memory retValue = resolver.text(node, key); + assertEq(keccak256(bytes(retValue)), keccak256(bytes(value))); + } + + function test_canClearRecord() public { + vm.startPrank(user); + + resolver.setText(node, key, value); + assertEq(resolver.text(node, key), value); + + resolver.clearRecords(node); + assertEq(resolver.text(node, key), ""); + + vm.stopPrank(); + } +} diff --git a/test/UpgradeableL2Resolver/SetZonehash.t.sol b/test/UpgradeableL2Resolver/SetZonehash.t.sol new file mode 100644 index 0000000..9b31cab --- /dev/null +++ b/test/UpgradeableL2Resolver/SetZonehash.t.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {UpgradeableL2ResolverBase} from "./UpgradeableL2ResolverBase.t.sol"; +import {ResolverBase} from "src/L2/resolver/ResolverBase.sol"; +import {DNSResolver} from "src/L2/resolver/DNSResolver.sol"; + +contract SetZonehash is UpgradeableL2ResolverBase { + bytes zonehash = bytes("zonehash"); + + function test_reverts_forUnauthorizedUser() public { + vm.expectRevert(abi.encodeWithSelector(ResolverBase.NotAuthorized.selector, node, notUser)); + vm.prank(notUser); + resolver.setZonehash(node, zonehash); + } + + function test_setsZonehash() public { + vm.prank(user); + resolver.setZonehash(node, zonehash); + assertEq(keccak256(resolver.zonehash(node)), keccak256(zonehash)); + } + + function test_canClearRecord() public { + vm.startPrank(user); + + resolver.setZonehash(node, zonehash); + assertEq(resolver.zonehash(node), zonehash); + + resolver.clearRecords(node); + assertEq(resolver.zonehash(node), ""); + + vm.stopPrank(); + } +} diff --git a/test/UpgradeableL2Resolver/SupportsInterface.t.sol b/test/UpgradeableL2Resolver/SupportsInterface.t.sol new file mode 100644 index 0000000..34f8b65 --- /dev/null +++ b/test/UpgradeableL2Resolver/SupportsInterface.t.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {UpgradeableL2ResolverBase} from "./UpgradeableL2ResolverBase.t.sol"; + +import {IABIResolver} from "ens-contracts/resolvers/profiles/IABIResolver.sol"; +import {IAddrResolver} from "ens-contracts/resolvers/profiles/IAddrResolver.sol"; +import {IContentHashResolver} from "ens-contracts/resolvers/profiles/IContentHashResolver.sol"; +import {IDNSRecordResolver} from "ens-contracts/resolvers/profiles/IDNSRecordResolver.sol"; +import {IDNSZoneResolver} from "ens-contracts/resolvers/profiles/IDNSZoneResolver.sol"; +import {IInterfaceResolver} from "ens-contracts/resolvers/profiles/IInterfaceResolver.sol"; +import {IMulticallable} from "ens-contracts/resolvers/IMulticallable.sol"; +import {INameResolver} from "ens-contracts/resolvers/profiles/INameResolver.sol"; +import {IPubkeyResolver} from "ens-contracts/resolvers/profiles/IPubkeyResolver.sol"; +import {ITextResolver} from "ens-contracts/resolvers/profiles/ITextResolver.sol"; +import {IExtendedResolver} from "ens-contracts/resolvers/profiles/IExtendedResolver.sol"; +import {IVersionableResolver} from "ens-contracts/resolvers/profiles/IVersionableResolver.sol"; + +contract SupportsInterface is UpgradeableL2ResolverBase { + function test_supportsABIResolver() public view { + assertTrue(resolver.supportsInterface(type(IABIResolver).interfaceId)); + } + + function test_supportsAddrResolver() public view { + assertTrue(resolver.supportsInterface(type(IAddrResolver).interfaceId)); + } + + function test_supportsContentHashResolver() public view { + assertTrue(resolver.supportsInterface(type(IContentHashResolver).interfaceId)); + } + + function test_supportsDNSRecordResolver() public view { + assertTrue(resolver.supportsInterface(type(IDNSRecordResolver).interfaceId)); + } + + function test_supportsDNSZoneResolver() public view { + assertTrue(resolver.supportsInterface(type(IDNSZoneResolver).interfaceId)); + } + + function test_supportsInterfaceResolver() public view { + assertTrue(resolver.supportsInterface(type(IInterfaceResolver).interfaceId)); + } + + function test_supportsMulticallable() public view { + assertTrue(resolver.supportsInterface(type(IMulticallable).interfaceId)); + } + + function test_supportsNameResolver() public view { + assertTrue(resolver.supportsInterface(type(INameResolver).interfaceId)); + } + + function test_supportsPubkeyResolver() public view { + assertTrue(resolver.supportsInterface(type(IPubkeyResolver).interfaceId)); + } + + function test_supportsTextResolver() public view { + assertTrue(resolver.supportsInterface(type(ITextResolver).interfaceId)); + } + + function test_supportsExtendedResolver() public view { + assertTrue(resolver.supportsInterface(type(IExtendedResolver).interfaceId)); + } + + function test_supportsVersionableResolver() public view { + assertTrue(resolver.supportsInterface(type(IVersionableResolver).interfaceId)); + } +} diff --git a/test/UpgradeableL2Resolver/UpgradeableL2ResolverBase.t.sol b/test/UpgradeableL2Resolver/UpgradeableL2ResolverBase.t.sol new file mode 100644 index 0000000..db9f9e1 --- /dev/null +++ b/test/UpgradeableL2Resolver/UpgradeableL2ResolverBase.t.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Test} from "forge-std/Test.sol"; +import {UpgradeableL2Resolver} from "src/L2/UpgradeableL2Resolver.sol"; +import {TransparentUpgradeableProxy} from + "openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {Registry} from "src/L2/Registry.sol"; +import {ENS} from "ens-contracts/registry/ENS.sol"; +import {ETH_NODE, BASE_ETH_NODE, REVERSE_NODE} from "src/util/Constants.sol"; +import {NameEncoder} from "ens-contracts/utils/NameEncoder.sol"; +import {MockReverseRegistrar} from "test/mocks/MockReverseRegistrar.sol"; + +contract UpgradeableL2ResolverBase is Test { + bytes32 internal constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; + + UpgradeableL2Resolver public resolverImpl; + TransparentUpgradeableProxy public proxy; + UpgradeableL2Resolver public resolver; + Registry public registry; + address reverse; + address controller = makeAddr("controller"); + address admin = makeAddr("admin"); + address owner = makeAddr("owner"); + address user = makeAddr("user"); + address notUser = makeAddr("notUser"); + address proxyAdmin; + string name = "test.base.eth"; + bytes32 label = keccak256("test"); + bytes32 node; + + modifier notProxyAdmin(address addr) { + // The TransparentUpgradeableProxy admin can only call the `upgradeToAndCall` method. + // Therefore, a fuzz test that selects this address will not be able to interact with the resolver. + vm.assume(addr != proxyAdmin); + _; + } + + function setUp() public virtual { + registry = new Registry(owner); + reverse = address(new MockReverseRegistrar()); + resolverImpl = new UpgradeableL2Resolver(); + proxy = new TransparentUpgradeableProxy( + address(resolverImpl), + admin, + abi.encodeWithSelector( + UpgradeableL2Resolver.initialize.selector, registry, address(controller), address(reverse), owner + ) + ); + resolver = UpgradeableL2Resolver(address(proxy)); + (, node) = NameEncoder.dnsEncodeName(name); + _establishNamespace(); + + bytes32 adminSlotValue = vm.load(address(proxy), ADMIN_SLOT); + proxyAdmin = address(uint160(uint256(adminSlotValue))); + } + + function _establishNamespace() internal virtual { + // establish the base.eth namespace + bytes32 ethLabel = keccak256("eth"); + bytes32 baseLabel = keccak256("base"); + vm.startPrank(owner); + registry.setSubnodeOwner(0x0, ethLabel, owner); + registry.setSubnodeOwner(ETH_NODE, baseLabel, owner); + // create `name` for user + registry.setSubnodeRecord(BASE_ETH_NODE, label, user, address(resolver), 0); + + // establish the 80002105.reverse namespace + registry.setSubnodeOwner(0x0, keccak256("reverse"), owner); + registry.setSubnodeOwner(REVERSE_NODE, keccak256("80002105"), address(reverse)); + vm.stopPrank(); + } +}