diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e455a517..44435d9a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,5 +1,9 @@ name: CI -on: [push] +on: + push: + branches: [main] + pull_request: + branches: [main] jobs: test: name: Test on Node ${{ matrix.node }} and ${{ matrix.os }} @@ -50,4 +54,4 @@ jobs: run: bun run build - name: Lint - run: bun run lint \ No newline at end of file + run: bun run lint diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..dbf4cbf9 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "solidity.compileUsingRemoteVersion": "v0.8.17+commit.8df45f5f" +} \ No newline at end of file diff --git a/dm3-names/.gitignore b/dm3-names/.gitignore new file mode 100644 index 00000000..3360da73 --- /dev/null +++ b/dm3-names/.gitignore @@ -0,0 +1,12 @@ +node_modules +.env +coverage +coverage.json +typechain +typechain-types + +# Hardhat files +cache +artifacts +deployments + diff --git a/dm3-names/LICENSE b/dm3-names/LICENSE new file mode 100644 index 00000000..677d485f --- /dev/null +++ b/dm3-names/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 ENS Labs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/dm3-names/README.md b/dm3-names/README.md new file mode 100644 index 00000000..e94b6b1a --- /dev/null +++ b/dm3-names/README.md @@ -0,0 +1,86 @@ +# @ensdomains/crosschain-resolver + +A resolver contract that is built on top of evm-verifier. + +For a detailed readme and usage instructions, see the [monorepo readme](https://github.com/ensdomains/evmgateway/tree/main). + + +## How it is defined + +When the resolver has the following storage layout, + +``` + contract Resolver { + // node => version + mapping(bytes32 => uint64) public recordVersions; + // versionable_addresses[recordVersions[node]][node][coinType] + // version => node => cointype + mapping(uint64 => mapping(bytes32 => mapping(uint256 => bytes))) versionable_addresses; +``` + +Run `yarn storage` to find out storage slot for each variable + +``` +// Storage slot +// ┌────────────────────────────┬──────────────────────────────┬──────────────┬ +// │ contract │ state_variable │ storage_slot │ +// ├────────────────────────────┼──────────────────────────────┼──────────────┼ +// │ DelegatableResolver │ recordVersions │ 0 │ +// │ DelegatableResolver │ versionable_abis │ 1 │ +// │ DelegatableResolver │ versionable_addresses │ 2 │ +``` + +Then define the l1 function + +``` + function addr( + bytes32 node, + uint256 coinType + ) public view returns (bytes memory) { + EVMFetcher.newFetchRequest(verifier, target) + .getStatic(0) // storage_slot of recordVersions + .element(node) + .getDynamic(2) // storage_slot of versionable_addresses + .ref(0) // Referencing the result of `.getStatic(0)` + .element(node) + .element(coinType) + .fetch(this.addrCoinTypeCallback.selector, ''); // recordVersions +``` + +Storage verificaton can only verify the data of l2. When the function result needs some transformation, transform inside the callback function as follows. + +``` + function addrCallback( + bytes[] memory values, + bytes memory + ) public pure returns (address) { + return bytesToAddress(values[1]); + } +``` + + + +## Deploying (Goerli) + +Create `.env` and set the following variables + + +- DEPLOYER_PRIVATE_KEY +- L1_PROVIDER_URL +- L2_PROVIDER_URL +- L1_ETHERSCAN_API_KEY +- L2_ETHERSCAN_API_KEY +- OP_VERIFIER_ADDRESS=0x0c2746F20C9c97DBf718de10c04943cf408230A3 + +``` +bun run hardhat deploy --network optimismGoerli +``` + +Followed by the L1 contract: + +``` +bun run hardhat deploy --network goerli +``` + + +## Deployments diff --git a/dm3-names/contracts/Dm3NameRegistrar.sol b/dm3-names/contracts/Dm3NameRegistrar.sol new file mode 100644 index 00000000..3f663526 --- /dev/null +++ b/dm3-names/contracts/Dm3NameRegistrar.sol @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import {IAddrResolver} from '@ensdomains/ens-contracts/contracts/resolvers/profiles/IAddrResolver.sol'; +import {INameResolver} from '@ensdomains/ens-contracts/contracts/resolvers/profiles/INameResolver.sol'; +import {ITextResolver} from '@ensdomains/ens-contracts/contracts/resolvers/profiles/ITextResolver.sol'; + +/// @title Dm3NameRegistrar +/// @notice This contract is used for registering names in the ENS system. It is a combination of ENSResolver and ReverseRegistrar contracts. Allowing to register names and set text records for each name. By beeing compatible with ENSResolver and ReverseRegistrar + +contract Dm3NameRegistrar is IAddrResolver, INameResolver, ITextResolver { + //Lookup table for hexadecimal conversion + //Taken from ENS ReverseRegistrar contract + //https://github.com/ensdomains/ens-contracts/blob/21736916300b26cb8ea1802dbf6c9ff054adaeab/contracts/reverseRegistrar/ReverseRegistrar.sol#L12 + bytes32 private constant lookup = + 0x3031323334353637383961626364656600000000000000000000000000000000; + + // Constant for reverse node address + //Taken from ENS ReverseRegistrar contract + //https://github.com/ensdomains/ens-contracts/blob/21736916300b26cb8ea1802dbf6c9ff054adaeab/contracts/reverseRegistrar/ReverseRegistrar.sol#L12 + bytes32 private constant ADDR_REVERSE_NODE = + 0x91d1777781884d03a6757a803996e38de2a42967fb37eeaca72729271025a9e2; + + //Node of the L1 domain. in case of OP name that would be namehash(op.dm3.eth) + //Is not immutable so it can be retrieved from the storage using CCIP + bytes32 public PARENT_NODE; + + // Mapping to store the owner of each node + mapping(bytes32 => address) public owner; + + // Mapping to store the reverse record of each node + mapping(bytes32 => string) public reverse; + + // Mapping to store text records for each node + mapping(bytes32 => mapping(string => string)) public texts; + + // Event emitted when a name is registered + event NameRegistered(address indexed addr, string indexed name); + + // Event emitted when a name is removed + event NameRemoved(address indexed addr, string indexed name); + + /// @notice Constructor to set the parent node + /// @param _parentNode The parent node of the ENS + constructor(bytes32 _parentNode) { + PARENT_NODE = _parentNode; + } + + /// @notice Register a name in the ENS system + /// @param _name The name to register + function register(string calldata _name) external { + string memory oldName = reverse[makeReverseNode(msg.sender)]; + if (bytes(_name).length == 0) { + // Clear name if the new name is empty + delete owner[makeLabelNode(oldName)]; + delete reverse[makeReverseNode(msg.sender)]; + emit NameRemoved(msg.sender, oldName); + return; + } + + if (bytes(oldName).length > 0) { + // Clear old name if it exists + delete owner[makeLabelNode(oldName)]; + emit NameRemoved(msg.sender, oldName); + } + //set owner record + owner[makeLabelNode(_name)] = msg.sender; + //set reverse record + reverse[makeReverseNode(msg.sender)] = _name; + //emit NameRegistered event + emit NameRegistered(msg.sender, _name); + } + /// @notice Set text for a label + /// @param label The label to set the text for + /// @param key The key for the text + /// @param value The text to set + function setText( + bytes32 label, + string calldata key, + string calldata value + ) external { + address owner = owner[label]; + require(owner != address(0), 'Name not registered'); + require(owner == msg.sender, 'Only owner'); + texts[label][key] = value; + // emit TextChanged(node, key, key, value); + } + /// @notice Get the address of a node + /// @param node The node to get the address for + /// @return The address of the node + function addr(bytes32 node) external view returns (address payable) { + return payable(owner[node]); + } + + /// @notice Get the name of a node + /// @param node The node to get the name for + /// @return The name of the node + function name(bytes32 node) external view returns (string memory) { + return reverse[node]; + } + + /// @notice Get the text of a node + /// @param node The node to get the text for + /// @param key The key for the text + /// @return The text of the node + function text( + bytes32 node, + string calldata key + ) external view override returns (string memory) { + return texts[node][key]; + } + /// @notice Make a label node using the PARENT_NODE + /// @param label The label to make a node for + /// @return The node of the label + function makeLabelNode(string memory label) private view returns (bytes32) { + return + keccak256(abi.encodePacked(PARENT_NODE, keccak256(bytes(label)))); + } + /// @notice Make a label node used for the reverse record using the ADDR_REVERSE_NODE + function makeReverseNode(address addr) private pure returns (bytes32) { + return + keccak256( + abi.encodePacked(ADDR_REVERSE_NODE, sha3HexAddress(addr)) + ); + } + + /// @notice Convert an address to a hexadecimal string and hash it + /// @param addr The address to convert and hash + /// @dev taken from ENS ReverseRegistrar contract + /// @dev https://github.com/ensdomains/ens-contracts/blob/21736916300b26cb8ea1802dbf6c9ff054adaeab/contracts/reverseRegistrar/ReverseRegistrar.sol#L164 + function sha3HexAddress(address addr) private pure returns (bytes32 ret) { + assembly { + for { + let i := 40 + } gt(i, 0) { + + } { + i := sub(i, 1) + mstore8(i, byte(and(addr, 0xf), lookup)) + addr := div(addr, 0x10) + i := sub(i, 1) + mstore8(i, byte(and(addr, 0xf), lookup)) + addr := div(addr, 0x10) + } + + ret := keccak256(0, 40) + } + } +} diff --git a/dm3-names/contracts/deps.sol b/dm3-names/contracts/deps.sol new file mode 100644 index 00000000..402a0d3f --- /dev/null +++ b/dm3-names/contracts/deps.sol @@ -0,0 +1,26 @@ +import "@ensdomains/ens-contracts/contracts/registry/ENSRegistry.sol"; +import "@ensdomains/ens-contracts/contracts/wrapper/NameWrapper.sol"; +import "@ensdomains/ens-contracts/contracts/ethregistrar/BaseRegistrarImplementation.sol"; +import "@ensdomains/ens-contracts/contracts/wrapper/StaticMetadataService.sol"; +import "@ensdomains/l1-verifier/contracts/L1Verifier.sol"; +import {ReverseRegistrar} from "@ensdomains/ens-contracts/contracts/reverseRegistrar/ReverseRegistrar.sol"; +import {PublicResolver} from "@ensdomains/ens-contracts/contracts/resolvers/PublicResolver.sol"; +import {DelegatableResolver} from "@ensdomains/ens-contracts/contracts/resolvers/DelegatableResolver.sol"; +import {DelegatableResolverFactory} from "@ensdomains/ens-contracts/contracts/resolvers/DelegatableResolverFactory.sol"; +// Storage slot +// ┌────────────────────────────┬──────────────────────────────┬──────────────┬ +// │ contract │ state_variable │ storage_slot │ +// ├────────────────────────────┼──────────────────────────────┼──────────────┼ +// │ DelegatableResolver │ recordVersions │ 0 │ +// │ DelegatableResolver │ versionable_abis │ 1 │ +// │ DelegatableResolver │ versionable_addresses │ 2 │ +// │ DelegatableResolver │ versionable_hashes │ 3 │ +// │ DelegatableResolver │ versionable_zonehashes │ 4 │ +// │ DelegatableResolver │ versionable_records │ 5 │ +// │ DelegatableResolver │ versionable_nameEntriesCount │ 6 │ +// │ DelegatableResolver │ versionable_interfaces │ 7 │ +// │ DelegatableResolver │ versionable_names │ 8 │ +// │ DelegatableResolver │ versionable_pubkeys │ 9 │ +// │ DelegatableResolver │ versionable_texts │ 10 │ +// │ DelegatableResolver │ operators │ 11 │ +// │ DelegatableResolverFactory │ implementation │ 0 │ diff --git a/dm3-names/contracts/l1/Dm3NameRegistrarEVMFetcher.sol b/dm3-names/contracts/l1/Dm3NameRegistrarEVMFetcher.sol new file mode 100644 index 00000000..9e2c18fb --- /dev/null +++ b/dm3-names/contracts/l1/Dm3NameRegistrarEVMFetcher.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import {EVMFetcher} from '@ensdomains/evm-verifier/contracts/EVMFetcher.sol'; +import {EVMFetchTarget} from '@ensdomains/evm-verifier/contracts/EVMFetchTarget.sol'; +import {BytesUtils} from '@ensdomains/ens-contracts/contracts/dnssec-oracle/BytesUtils.sol'; +import {IAddrResolver} from '@ensdomains/ens-contracts/contracts/resolvers/profiles/IAddrResolver.sol'; +import {ITextResolver} from '@ensdomains/ens-contracts/contracts/resolvers/profiles/ITextResolver.sol'; +import {INameResolver} from '@ensdomains/ens-contracts/contracts/resolvers/profiles/INameResolver.sol'; +import {IEVMVerifier} from '@ensdomains/evm-verifier/contracts/IEVMVerifier.sol'; +import '@openzeppelin/contracts/access/Ownable.sol'; +import './strings.sol'; + +contract Dm3NameRegistrarEVMFetcher is EVMFetchTarget, Ownable { + using EVMFetcher for EVMFetcher.EVMFetchRequest; + using BytesUtils for bytes; + using strings for *; + + IEVMVerifier public verifier; + address public target; + string public parentDomain; + + uint256 private constant PARENT_NODE_SLOT = 0; + uint256 private constant OWNER_SLOT = 1; + uint256 private constant REVERSE_SLOT = 2; + uint256 private constant TEXTS_SLOT = 3; + + //TODO add OZ ownable + constructor( + IEVMVerifier _verifier, + address _target, + string memory _parentDomain + ) { + verifier = _verifier; + target = _target; + parentDomain = _parentDomain; + } + + function setVerifier(IEVMVerifier _verifier) external onlyOwner { + verifier = _verifier; + } + + function setTarget(address _target) external onlyOwner { + target = _target; + } + function setParentDomain(string memory _parentDomain) external onlyOwner { + parentDomain = _parentDomain; + } + + function resolve( + bytes calldata name, + bytes calldata data + ) external view returns (bytes memory result) { + bytes4 selector = bytes4(data); + + if (selector == INameResolver.name.selector) { + bytes32 node = abi.decode(data[4:], (bytes32)); + return _name(node); + } + if (selector == ITextResolver.text.selector) { + (bytes32 node, string memory key) = abi.decode( + data[4:], + (bytes32, string) + ); + return bytes(_text(node, key)); + } + if (selector == IAddrResolver.addr.selector) { + bytes32 node = abi.decode(data[4:], (bytes32)); + return _addr(node); + } + } + function _addr(bytes32 node) private view returns (bytes memory) { + EVMFetcher + .newFetchRequest(verifier, target) + .getStatic(OWNER_SLOT) + .element(node) + .fetch(this.addrCallback.selector, ''); + } + function _name(bytes32 node) private view returns (bytes memory) { + EVMFetcher + .newFetchRequest(verifier, target) + .getStatic(PARENT_NODE_SLOT) + .getDynamic(REVERSE_SLOT) + .element(node) + .fetch(this.nameCallback.selector, ''); + } + function _text( + bytes32 node, + string memory key + ) private view returns (bytes memory) { + EVMFetcher + .newFetchRequest(verifier, target) + .getDynamic(TEXTS_SLOT) + .element(node) + .element(key) + .fetch(this.textCallback.selector, ''); + } + + function textCallback( + bytes[] memory values, + bytes memory + ) public pure returns (bytes memory) { + return abi.encode(string(values[0])); + } + function nameCallback( + bytes[] memory values, + bytes memory + ) public view returns (bytes memory) { + strings.slice[] memory s = new strings.slice[](3); + //The label i.e alice + s[0] = string(values[1]).toSlice(); + //Separator + s[1] = '.'.toSlice(); + //The parent domain i.e example.com + s[2] = parentDomain.toSlice(); + return abi.encode(''.toSlice().join(s)); + } + function addrCallback( + bytes[] memory values, + bytes memory + ) public pure returns (bytes memory) { + return abi.encode(address(uint160(uint256(bytes32(values[0]))))); + } +} diff --git a/dm3-names/contracts/l1/strings.sol b/dm3-names/contracts/l1/strings.sol new file mode 100644 index 00000000..d32b13dc --- /dev/null +++ b/dm3-names/contracts/l1/strings.sol @@ -0,0 +1,727 @@ +/* + * @title String & slice utility library for Solidity contracts. + * @author Nick Johnson + * + * @dev Functionality in this library is largely implemented using an + * abstraction called a 'slice'. A slice represents a part of a string - + * anything from the entire string to a single character, or even no + * characters at all (a 0-length slice). Since a slice only has to specify + * an offset and a length, copying and manipulating slices is a lot less + * expensive than copying and manipulating the strings they reference. + * + * To further reduce gas costs, most functions on slice that need to return + * a slice modify the original one instead of allocating a new one; for + * instance, `s.split(".")` will return the text up to the first '.', + * modifying s to only contain the remainder of the string after the '.'. + * In situations where you do not want to modify the original slice, you + * can make a copy first with `.copy()`, for example: + * `s.copy().split(".")`. Try and avoid using this idiom in loops; since + * Solidity has no memory management, it will result in allocating many + * short-lived slices that are later discarded. + * + * Functions that return two slices come in two versions: a non-allocating + * version that takes the second slice as an argument, modifying it in + * place, and an allocating version that allocates and returns the second + * slice; see `nextRune` for example. + * + * Functions that have to copy string data will return strings rather than + * slices; these can be cast back to slices for further processing if + * required. + * + * For convenience, some functions are provided with non-modifying + * variants that create a new slice and return both; for instance, + * `s.splitNew('.')` leaves s unmodified, and returns two values + * corresponding to the left and right parts of the string. + */ + +pragma solidity ^0.8.0; + +library strings { + struct slice { + uint _len; + uint _ptr; + } + + function memcpy(uint dest, uint src, uint length) private pure { + // Copy word-length chunks while possible + for(; length >= 32; length -= 32) { + assembly { + mstore(dest, mload(src)) + } + dest += 32; + src += 32; + } + + // Copy remaining bytes + uint mask = type(uint).max; + if (length > 0) { + mask = 256 ** (32 - length) - 1; + } + assembly { + let srcpart := and(mload(src), not(mask)) + let destpart := and(mload(dest), mask) + mstore(dest, or(destpart, srcpart)) + } + } + + /* + * @dev Returns a slice containing the entire string. + * @param self The string to make a slice from. + * @return A newly allocated slice containing the entire string. + */ + function toSlice(string memory self) internal pure returns (slice memory) { + uint ptr; + assembly { + ptr := add(self, 0x20) + } + return slice(bytes(self).length, ptr); + } + + /* + * @dev Returns the length of a null-terminated bytes32 string. + * @param self The value to find the length of. + * @return The length of the string, from 0 to 32. + */ + function len(bytes32 self) internal pure returns (uint) { + uint ret; + if (self == 0) + return 0; + if (uint(self) & type(uint128).max == 0) { + ret += 16; + self = bytes32(uint(self) / 0x100000000000000000000000000000000); + } + if (uint(self) & type(uint64).max == 0) { + ret += 8; + self = bytes32(uint(self) / 0x10000000000000000); + } + if (uint(self) & type(uint32).max == 0) { + ret += 4; + self = bytes32(uint(self) / 0x100000000); + } + if (uint(self) & type(uint16).max == 0) { + ret += 2; + self = bytes32(uint(self) / 0x10000); + } + if (uint(self) & type(uint8).max == 0) { + ret += 1; + } + return 32 - ret; + } + + /* + * @dev Returns a slice containing the entire bytes32, interpreted as a + * null-terminated utf-8 string. + * @param self The bytes32 value to convert to a slice. + * @return A new slice containing the value of the input argument up to the + * first null. + */ + function toSliceB32(bytes32 self) internal pure returns (slice memory ret) { + // Allocate space for `self` in memory, copy it there, and point ret at it + assembly { + let ptr := mload(0x40) + mstore(0x40, add(ptr, 0x20)) + mstore(ptr, self) + mstore(add(ret, 0x20), ptr) + } + ret._len = len(self); + } + + /* + * @dev Returns a new slice containing the same data as the current slice. + * @param self The slice to copy. + * @return A new slice containing the same data as `self`. + */ + function copy(slice memory self) internal pure returns (slice memory) { + return slice(self._len, self._ptr); + } + + /* + * @dev Copies a slice to a new string. + * @param self The slice to copy. + * @return A newly allocated string containing the slice's text. + */ + function toString(slice memory self) internal pure returns (string memory) { + string memory ret = new string(self._len); + uint retptr; + assembly { retptr := add(ret, 32) } + + memcpy(retptr, self._ptr, self._len); + return ret; + } + + /* + * @dev Returns the length in runes of the slice. Note that this operation + * takes time proportional to the length of the slice; avoid using it + * in loops, and call `slice.empty()` if you only need to know whether + * the slice is empty or not. + * @param self The slice to operate on. + * @return The length of the slice in runes. + */ + function len(slice memory self) internal pure returns (uint l) { + // Starting at ptr-31 means the LSB will be the byte we care about + uint ptr = self._ptr - 31; + uint end = ptr + self._len; + for (l = 0; ptr < end; l++) { + uint8 b; + assembly { b := and(mload(ptr), 0xFF) } + if (b < 0x80) { + ptr += 1; + } else if(b < 0xE0) { + ptr += 2; + } else if(b < 0xF0) { + ptr += 3; + } else if(b < 0xF8) { + ptr += 4; + } else if(b < 0xFC) { + ptr += 5; + } else { + ptr += 6; + } + } + } + + /* + * @dev Returns true if the slice is empty (has a length of 0). + * @param self The slice to operate on. + * @return True if the slice is empty, False otherwise. + */ + function empty(slice memory self) internal pure returns (bool) { + return self._len == 0; + } + + /* + * @dev Returns a positive number if `other` comes lexicographically after + * `self`, a negative number if it comes before, or zero if the + * contents of the two slices are equal. Comparison is done per-rune, + * on unicode codepoints. + * @param self The first slice to compare. + * @param other The second slice to compare. + * @return The result of the comparison. + */ + function compare(slice memory self, slice memory other) internal pure returns (int) { + uint shortest = self._len; + if (other._len < self._len) + shortest = other._len; + + uint selfptr = self._ptr; + uint otherptr = other._ptr; + for (uint idx = 0; idx < shortest; idx += 32) { + uint a; + uint b; + assembly { + a := mload(selfptr) + b := mload(otherptr) + } + if (a != b) { + // Mask out irrelevant bytes and check again + uint mask = type(uint).max; // 0xffff... + if(shortest < 32) { + mask = ~(2 ** (8 * (32 - shortest + idx)) - 1); + } + unchecked { + uint diff = (a & mask) - (b & mask); + if (diff != 0) + return int(diff); + } + } + selfptr += 32; + otherptr += 32; + } + return int(self._len) - int(other._len); + } + + /* + * @dev Returns true if the two slices contain the same text. + * @param self The first slice to compare. + * @param self The second slice to compare. + * @return True if the slices are equal, false otherwise. + */ + function equals(slice memory self, slice memory other) internal pure returns (bool) { + return compare(self, other) == 0; + } + + /* + * @dev Extracts the first rune in the slice into `rune`, advancing the + * slice to point to the next rune and returning `self`. + * @param self The slice to operate on. + * @param rune The slice that will contain the first rune. + * @return `rune`. + */ + function nextRune(slice memory self, slice memory rune) internal pure returns (slice memory) { + rune._ptr = self._ptr; + + if (self._len == 0) { + rune._len = 0; + return rune; + } + + uint l; + uint b; + // Load the first byte of the rune into the LSBs of b + assembly { b := and(mload(sub(mload(add(self, 32)), 31)), 0xFF) } + if (b < 0x80) { + l = 1; + } else if(b < 0xE0) { + l = 2; + } else if(b < 0xF0) { + l = 3; + } else { + l = 4; + } + + // Check for truncated codepoints + if (l > self._len) { + rune._len = self._len; + self._ptr += self._len; + self._len = 0; + return rune; + } + + self._ptr += l; + self._len -= l; + rune._len = l; + return rune; + } + + /* + * @dev Returns the first rune in the slice, advancing the slice to point + * to the next rune. + * @param self The slice to operate on. + * @return A slice containing only the first rune from `self`. + */ + function nextRune(slice memory self) internal pure returns (slice memory ret) { + nextRune(self, ret); + } + + /* + * @dev Returns the number of the first codepoint in the slice. + * @param self The slice to operate on. + * @return The number of the first codepoint in the slice. + */ + function ord(slice memory self) internal pure returns (uint ret) { + if (self._len == 0) { + return 0; + } + + uint word; + uint length; + uint divisor = 2 ** 248; + + // Load the rune into the MSBs of b + assembly { word:= mload(mload(add(self, 32))) } + uint b = word / divisor; + if (b < 0x80) { + ret = b; + length = 1; + } else if(b < 0xE0) { + ret = b & 0x1F; + length = 2; + } else if(b < 0xF0) { + ret = b & 0x0F; + length = 3; + } else { + ret = b & 0x07; + length = 4; + } + + // Check for truncated codepoints + if (length > self._len) { + return 0; + } + + for (uint i = 1; i < length; i++) { + divisor = divisor / 256; + b = (word / divisor) & 0xFF; + if (b & 0xC0 != 0x80) { + // Invalid UTF-8 sequence + return 0; + } + ret = (ret * 64) | (b & 0x3F); + } + + return ret; + } + + /* + * @dev Returns the keccak-256 hash of the slice. + * @param self The slice to hash. + * @return The hash of the slice. + */ + function keccak(slice memory self) internal pure returns (bytes32 ret) { + assembly { + ret := keccak256(mload(add(self, 32)), mload(self)) + } + } + + /* + * @dev Returns true if `self` starts with `needle`. + * @param self The slice to operate on. + * @param needle The slice to search for. + * @return True if the slice starts with the provided text, false otherwise. + */ + function startsWith(slice memory self, slice memory needle) internal pure returns (bool) { + if (self._len < needle._len) { + return false; + } + + if (self._ptr == needle._ptr) { + return true; + } + + bool equal; + assembly { + let length := mload(needle) + let selfptr := mload(add(self, 0x20)) + let needleptr := mload(add(needle, 0x20)) + equal := eq(keccak256(selfptr, length), keccak256(needleptr, length)) + } + return equal; + } + + /* + * @dev If `self` starts with `needle`, `needle` is removed from the + * beginning of `self`. Otherwise, `self` is unmodified. + * @param self The slice to operate on. + * @param needle The slice to search for. + * @return `self` + */ + function beyond(slice memory self, slice memory needle) internal pure returns (slice memory) { + if (self._len < needle._len) { + return self; + } + + bool equal = true; + if (self._ptr != needle._ptr) { + assembly { + let length := mload(needle) + let selfptr := mload(add(self, 0x20)) + let needleptr := mload(add(needle, 0x20)) + equal := eq(keccak256(selfptr, length), keccak256(needleptr, length)) + } + } + + if (equal) { + self._len -= needle._len; + self._ptr += needle._len; + } + + return self; + } + + /* + * @dev Returns true if the slice ends with `needle`. + * @param self The slice to operate on. + * @param needle The slice to search for. + * @return True if the slice starts with the provided text, false otherwise. + */ + function endsWith(slice memory self, slice memory needle) internal pure returns (bool) { + if (self._len < needle._len) { + return false; + } + + uint selfptr = self._ptr + self._len - needle._len; + + if (selfptr == needle._ptr) { + return true; + } + + bool equal; + assembly { + let length := mload(needle) + let needleptr := mload(add(needle, 0x20)) + equal := eq(keccak256(selfptr, length), keccak256(needleptr, length)) + } + + return equal; + } + + /* + * @dev If `self` ends with `needle`, `needle` is removed from the + * end of `self`. Otherwise, `self` is unmodified. + * @param self The slice to operate on. + * @param needle The slice to search for. + * @return `self` + */ + function until(slice memory self, slice memory needle) internal pure returns (slice memory) { + if (self._len < needle._len) { + return self; + } + + uint selfptr = self._ptr + self._len - needle._len; + bool equal = true; + if (selfptr != needle._ptr) { + assembly { + let length := mload(needle) + let needleptr := mload(add(needle, 0x20)) + equal := eq(keccak256(selfptr, length), keccak256(needleptr, length)) + } + } + + if (equal) { + self._len -= needle._len; + } + + return self; + } + + // Returns the memory address of the first byte of the first occurrence of + // `needle` in `self`, or the first byte after `self` if not found. + function findPtr(uint selflen, uint selfptr, uint needlelen, uint needleptr) private pure returns (uint) { + uint ptr = selfptr; + uint idx; + + if (needlelen <= selflen) { + if (needlelen <= 32) { + bytes32 mask; + if (needlelen > 0) { + mask = bytes32(~(2 ** (8 * (32 - needlelen)) - 1)); + } + + bytes32 needledata; + assembly { needledata := and(mload(needleptr), mask) } + + uint end = selfptr + selflen - needlelen; + bytes32 ptrdata; + assembly { ptrdata := and(mload(ptr), mask) } + + while (ptrdata != needledata) { + if (ptr >= end) + return selfptr + selflen; + ptr++; + assembly { ptrdata := and(mload(ptr), mask) } + } + return ptr; + } else { + // For long needles, use hashing + bytes32 hash; + assembly { hash := keccak256(needleptr, needlelen) } + + for (idx = 0; idx <= selflen - needlelen; idx++) { + bytes32 testHash; + assembly { testHash := keccak256(ptr, needlelen) } + if (hash == testHash) + return ptr; + ptr += 1; + } + } + } + return selfptr + selflen; + } + + // Returns the memory address of the first byte after the last occurrence of + // `needle` in `self`, or the address of `self` if not found. + function rfindPtr(uint selflen, uint selfptr, uint needlelen, uint needleptr) private pure returns (uint) { + uint ptr; + + if (needlelen <= selflen) { + if (needlelen <= 32) { + bytes32 mask; + if (needlelen > 0) { + mask = bytes32(~(2 ** (8 * (32 - needlelen)) - 1)); + } + + bytes32 needledata; + assembly { needledata := and(mload(needleptr), mask) } + + ptr = selfptr + selflen - needlelen; + bytes32 ptrdata; + assembly { ptrdata := and(mload(ptr), mask) } + + while (ptrdata != needledata) { + if (ptr <= selfptr) + return selfptr; + ptr--; + assembly { ptrdata := and(mload(ptr), mask) } + } + return ptr + needlelen; + } else { + // For long needles, use hashing + bytes32 hash; + assembly { hash := keccak256(needleptr, needlelen) } + ptr = selfptr + (selflen - needlelen); + while (ptr >= selfptr) { + bytes32 testHash; + assembly { testHash := keccak256(ptr, needlelen) } + if (hash == testHash) + return ptr + needlelen; + ptr -= 1; + } + } + } + return selfptr; + } + + /* + * @dev Modifies `self` to contain everything from the first occurrence of + * `needle` to the end of the slice. `self` is set to the empty slice + * if `needle` is not found. + * @param self The slice to search and modify. + * @param needle The text to search for. + * @return `self`. + */ + function find(slice memory self, slice memory needle) internal pure returns (slice memory) { + uint ptr = findPtr(self._len, self._ptr, needle._len, needle._ptr); + self._len -= ptr - self._ptr; + self._ptr = ptr; + return self; + } + + /* + * @dev Modifies `self` to contain the part of the string from the start of + * `self` to the end of the first occurrence of `needle`. If `needle` + * is not found, `self` is set to the empty slice. + * @param self The slice to search and modify. + * @param needle The text to search for. + * @return `self`. + */ + function rfind(slice memory self, slice memory needle) internal pure returns (slice memory) { + uint ptr = rfindPtr(self._len, self._ptr, needle._len, needle._ptr); + self._len = ptr - self._ptr; + return self; + } + + /* + * @dev Splits the slice, setting `self` to everything after the first + * occurrence of `needle`, and `token` to everything before it. If + * `needle` does not occur in `self`, `self` is set to the empty slice, + * and `token` is set to the entirety of `self`. + * @param self The slice to split. + * @param needle The text to search for in `self`. + * @param token An output parameter to which the first token is written. + * @return `token`. + */ + function split(slice memory self, slice memory needle, slice memory token) internal pure returns (slice memory) { + uint ptr = findPtr(self._len, self._ptr, needle._len, needle._ptr); + token._ptr = self._ptr; + token._len = ptr - self._ptr; + if (ptr == self._ptr + self._len) { + // Not found + self._len = 0; + } else { + self._len -= token._len + needle._len; + self._ptr = ptr + needle._len; + } + return token; + } + + /* + * @dev Splits the slice, setting `self` to everything after the first + * occurrence of `needle`, and returning everything before it. If + * `needle` does not occur in `self`, `self` is set to the empty slice, + * and the entirety of `self` is returned. + * @param self The slice to split. + * @param needle The text to search for in `self`. + * @return The part of `self` up to the first occurrence of `delim`. + */ + function split(slice memory self, slice memory needle) internal pure returns (slice memory token) { + split(self, needle, token); + } + + /* + * @dev Splits the slice, setting `self` to everything before the last + * occurrence of `needle`, and `token` to everything after it. If + * `needle` does not occur in `self`, `self` is set to the empty slice, + * and `token` is set to the entirety of `self`. + * @param self The slice to split. + * @param needle The text to search for in `self`. + * @param token An output parameter to which the first token is written. + * @return `token`. + */ + function rsplit(slice memory self, slice memory needle, slice memory token) internal pure returns (slice memory) { + uint ptr = rfindPtr(self._len, self._ptr, needle._len, needle._ptr); + token._ptr = ptr; + token._len = self._len - (ptr - self._ptr); + if (ptr == self._ptr) { + // Not found + self._len = 0; + } else { + self._len -= token._len + needle._len; + } + return token; + } + + /* + * @dev Splits the slice, setting `self` to everything before the last + * occurrence of `needle`, and returning everything after it. If + * `needle` does not occur in `self`, `self` is set to the empty slice, + * and the entirety of `self` is returned. + * @param self The slice to split. + * @param needle The text to search for in `self`. + * @return The part of `self` after the last occurrence of `delim`. + */ + function rsplit(slice memory self, slice memory needle) internal pure returns (slice memory token) { + rsplit(self, needle, token); + } + + /* + * @dev Counts the number of nonoverlapping occurrences of `needle` in `self`. + * @param self The slice to search. + * @param needle The text to search for in `self`. + * @return The number of occurrences of `needle` found in `self`. + */ + function count(slice memory self, slice memory needle) internal pure returns (uint cnt) { + uint ptr = findPtr(self._len, self._ptr, needle._len, needle._ptr) + needle._len; + while (ptr <= self._ptr + self._len) { + cnt++; + ptr = findPtr(self._len - (ptr - self._ptr), ptr, needle._len, needle._ptr) + needle._len; + } + } + + /* + * @dev Returns True if `self` contains `needle`. + * @param self The slice to search. + * @param needle The text to search for in `self`. + * @return True if `needle` is found in `self`, false otherwise. + */ + function contains(slice memory self, slice memory needle) internal pure returns (bool) { + return rfindPtr(self._len, self._ptr, needle._len, needle._ptr) != self._ptr; + } + + /* + * @dev Returns a newly allocated string containing the concatenation of + * `self` and `other`. + * @param self The first slice to concatenate. + * @param other The second slice to concatenate. + * @return The concatenation of the two strings. + */ + function concat(slice memory self, slice memory other) internal pure returns (string memory) { + string memory ret = new string(self._len + other._len); + uint retptr; + assembly { retptr := add(ret, 32) } + memcpy(retptr, self._ptr, self._len); + memcpy(retptr + self._len, other._ptr, other._len); + return ret; + } + + /* + * @dev Joins an array of slices, using `self` as a delimiter, returning a + * newly allocated string. + * @param self The delimiter to use. + * @param parts A list of slices to join. + * @return A newly allocated string containing all the slices in `parts`, + * joined with `self`. + */ + function join(slice memory self, slice[] memory parts) internal pure returns (string memory) { + if (parts.length == 0) + return ""; + + uint length = self._len * (parts.length - 1); + for(uint i = 0; i < parts.length; i++) + length += parts[i]._len; + + string memory ret = new string(length); + uint retptr; + assembly { retptr := add(ret, 32) } + + for(uint i = 0; i < parts.length; i++) { + memcpy(retptr, parts[i]._ptr, parts[i]._len); + retptr += parts[i]._len; + if (i < parts.length - 1) { + memcpy(retptr, self._ptr, self._len); + retptr += self._len; + } + } + + return ret; + } +} \ No newline at end of file diff --git a/dm3-names/deploy_l1/10_dm3Registrar.ts b/dm3-names/deploy_l1/10_dm3Registrar.ts new file mode 100644 index 00000000..31dd547b --- /dev/null +++ b/dm3-names/deploy_l1/10_dm3Registrar.ts @@ -0,0 +1,21 @@ +import {HardhatRuntimeEnvironment} from 'hardhat/types'; +import {DeployFunction} from 'hardhat-deploy/types'; + + +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const {deployments, getNamedAccounts} = hre; + const {deploy} = deployments; + + const {deployer} = await getNamedAccounts(); + + const OP_VERIFIER_ADDRESS = process.env.OP_VERIFIER_ADDRESS + if(!OP_VERIFIER_ADDRESS) throw ('Set $OP_VERIFIER_ADDRESS') + console.log({OP_VERIFIER_ADDRESS}) + await deploy('Dm3NameRegistrarEVMFetcher', { + from: deployer, + args: [OP_VERIFIER_ADDRESS], + log: true, + }); +}; +export default func; +func.tags = ['Dm3NameRegistrarEVMFetcher']; diff --git a/dm3-names/deploy_l2/01_dm3NameRegistrar.ts b/dm3-names/deploy_l2/01_dm3NameRegistrar.ts new file mode 100644 index 00000000..16f28d64 --- /dev/null +++ b/dm3-names/deploy_l2/01_dm3NameRegistrar.ts @@ -0,0 +1,20 @@ +import {HardhatRuntimeEnvironment} from 'hardhat/types'; +import {DeployFunction} from 'hardhat-deploy/types'; + +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const {deployments, getNamedAccounts} = hre; + const {deploy} = deployments; + + const {deployer} = await getNamedAccounts(); + console.log({deployer}) + const impl = await deploy('Dm3NameRegistrar', { + from: deployer, + args: [], + log: true, + }); + const implAddress = impl.address + console.log(`Dm3NameRegistrar is deployed at ${implAddress}`) + +}; +export default func; +func.tags = ['Dm3NameRegistrar']; diff --git a/dm3-names/hardhat.config.ts b/dm3-names/hardhat.config.ts new file mode 100644 index 00000000..931bdae4 --- /dev/null +++ b/dm3-names/hardhat.config.ts @@ -0,0 +1,61 @@ +import '@nomicfoundation/hardhat-toolbox'; +import { HardhatUserConfig } from 'hardhat/config'; +import "hardhat-storage-layout"; +import 'hardhat-deploy'; +import 'hardhat-deploy-ethers'; +const DEPLOYER_PRIVATE_KEY = process.env.DEPLOYER_PRIVATE_KEY ?? "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; +const L1_PROVIDER_URL = process.env.L1_PROVIDER_URL || ''; +const L1_ETHERSCAN_API_KEY = process.env.L1_ETHERSCAN_API_KEY || ''; +const L2_ETHERSCAN_API_KEY = process.env.L2_ETHERSCAN_API_KEY || ''; + +const config: HardhatUserConfig = { + solidity: { + version: "0.8.17", + settings: { + optimizer: { + enabled: true, + runs: 1200 + }, + viaIR: true, + }, + }, + networks: { + ganache: { + url: `http://localhost:${parseInt(process.env['RPC_PORT'] || '8545')}`, + }, + goerli: { + url: L1_PROVIDER_URL, + accounts: [DEPLOYER_PRIVATE_KEY], + deploy: [ "deploy_l1/" ], + companionNetworks: { + l2: "optimismGoerli", + }, + }, + optimismGoerli: { + url: "https://goerli.optimism.io", + accounts: [DEPLOYER_PRIVATE_KEY], + deploy: [ "deploy_l2/" ], + } + }, + etherscan: { + apiKey: { + goerli: L1_ETHERSCAN_API_KEY, + optimismGoerli: L2_ETHERSCAN_API_KEY + }, + customChains: [ + { + network: "optimismGoerli", + chainId: 420, + urls: { + apiURL: "https://api-goerli-optimism.etherscan.io/api", + browserURL: "https://goerli-optimism.etherscan.io" + } + } + ] + }, + namedAccounts: { + 'deployer': 0, + } +}; + +export default config; diff --git a/dm3-names/package.json b/dm3-names/package.json new file mode 100644 index 00000000..d525003c --- /dev/null +++ b/dm3-names/package.json @@ -0,0 +1,50 @@ +{ + "name": "@dm3-org/dm3-name-registrar", + "license": "MIT", + "version": "0.1.0", + "scripts": { + "build": "echo 'building dm3-name-registrar' && hardhat compile", + "test": "node scripts/test.js", + "test-hh": "npx hardhat test", + "storage": "hardhat check", + "clean": "rm -fr artifacts cache node_modules typechain-types", + "lint": "exit 0" + }, + "devDependencies": { + "@ensdomains/l1-gateway": "workspace:*", + "@nomicfoundation/hardhat-chai-matchers": "^2.0.0", + "@nomicfoundation/hardhat-ethers": "^3.0.0", + "@nomicfoundation/hardhat-network-helpers": "^1.0.0", + "@nomicfoundation/hardhat-toolbox": "^3.0.0", + "@nomicfoundation/hardhat-verify": "^1.0.0", + "@nomiclabs/hardhat-ganache": "^2.0.1", + "@typechain/ethers-v6": "^0.4.0", + "@typechain/hardhat": "^8.0.0", + "@types/chai": "^4.2.0", + "@types/express": "^4.17.18", + "@types/mocha": ">=9.1.0", + "@types/supertest": "^2.0.14", + "chai": "^4.2.0", + "dns-packet": "^5.6.1", + "ethers": "^6.8.0", + "express": "^4.18.2", + "ganache": "^7.9.1", + "hardhat": "^2.17.4", + "hardhat-deploy": "^0.11.43", + "hardhat-deploy-ethers": "^0.4.1", + "hardhat-gas-reporter": "^1.0.8", + "hardhat-storage-layout": "^0.1.7", + "solidity-bytes-utils": "^0.8.0", + "solidity-coverage": "^0.8.1", + "supertest": "^6.3.3", + "ts-node": "^10.9.1", + "typechain": "^8.2.0", + "typescript": "^5.2.2" + }, + "dependencies": { + "@ensdomains/ens-contracts": "ensdomains/ens-contracts#delegatable-resolver-with-factory", + "@ensdomains/evm-verifier": "^0.1.0", + "@eth-optimism/contracts": "^0.6.0", + "@openzeppelin/contracts": "^4.4.0" + } +} diff --git a/dm3-names/scripts/test.js b/dm3-names/scripts/test.js new file mode 100644 index 00000000..c7bba803 --- /dev/null +++ b/dm3-names/scripts/test.js @@ -0,0 +1,45 @@ +const { fork } = require('node:child_process'); +const ganache = require('ganache'); +const options = { + logging: { + quiet: true, + }, +}; + +async function main() { + const server = ganache.server(options); + console.log('Starting server'); + const port = await new Promise((resolve, reject) => { + server.listen(0, async (err) => { + console.log(`Listening on port ${server.address().port}`); + if (err) reject(err); + resolve(server.address().port); + }); + }); + + console.log('Starting hardhat'); + const code = await new Promise((resolve) => { + const hh = fork( + '../node_modules/.bin/hardhat', + ['test', '--network', 'ganache'], + { + stdio: 'inherit', + env: { + RPC_PORT: port.toString(), + }, + } + ); + hh.on('close', (code) => resolve(code)); + }); + + console.log('Shutting down'); + server.close(); + process.exit(code); +} + +// We recommend this pattern to be able to use async/await everywhere +// and properly handle errors. +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/dm3-names/test/testDm3NameRegistrar.ts b/dm3-names/test/testDm3NameRegistrar.ts new file mode 100644 index 00000000..e3b3e6fd --- /dev/null +++ b/dm3-names/test/testDm3NameRegistrar.ts @@ -0,0 +1,135 @@ +import { HardhatEthersProvider } from '@nomicfoundation/hardhat-ethers/internal/hardhat-ethers-provider'; +import type { HardhatEthersHelpers } from '@nomicfoundation/hardhat-ethers/types'; +import { expect } from 'chai'; +import { Contract, ethers as ethersT } from 'ethers'; +import { ethers } from 'hardhat'; +import { EthereumProvider } from 'hardhat/types'; + +type ethersObj = typeof ethersT & + Omit & { + provider: Omit & { + _hardhatProvider: EthereumProvider; + }; + }; + +declare module 'hardhat/types/runtime' { + const ethers: ethersObj; + interface HardhatRuntimeEnvironment { + ethers: ethersObj; + } +} + +describe('Dm3 name registrar', () => { + let target: Contract; + let signer: ethers.Signer; + + beforeEach(async () => { + const Dm3NameRegistrarFactory = + await ethers.getContractFactory('Dm3NameRegistrar'); + const parentNode = ethers.namehash('op.dm3.eth'); + target = await Dm3NameRegistrarFactory.deploy(parentNode); + signer = (await ethers.getSigners())[0]; + }); + + describe('register', () => { + it('can set dm3 name', async () => { + await target.register('alice'); + const reverseRecord = `${signer.address + .slice(2) + .toLowerCase()}.addr.reverse`; + + const owner = await target.owner(ethers.namehash('alice.op.dm3.eth')); + const name = await target.reverse(ethers.namehash(reverseRecord)); + + expect(owner).to.equal(signer.address); + expect(name).to.equal('alice'); + }); + it('can use addr to retrive address of node', async () => { + await target.register('alice'); + + const addr = await target.addr(ethers.namehash('alice.op.dm3.eth')); + expect(addr).to.equal(signer.address); + }); + it('can use reverse record to retrive name of address', async () => { + await target.register('alice'); + const reverseRecord = `${signer.address + .slice(2) + .toLowerCase()}.addr.reverse`; + const reverseNode = ethers.namehash(reverseRecord); + + const name = await target.name(reverseNode); + expect(name).to.equal('alice'); + }); + it('registering a new name overrides the old name', async () => { + await target.register('alice'); + const reverseRecord = `${signer.address + .slice(2) + .toLowerCase()}.addr.reverse`; + + let owner = await target.owner(ethers.namehash('alice.op.dm3.eth')); + let name = await target.reverse(ethers.namehash(reverseRecord)); + + expect(owner).to.equal(signer.address); + expect(name).to.equal('alice'); + + await target.register('bob'); + + owner = await target.owner(ethers.namehash('bob.op.dm3.eth')); + name = await target.reverse(ethers.namehash(reverseRecord)); + + const oldOwner = await target.owner(ethers.namehash('alice.op.dm3.eth')); + + expect(owner).to.equal(signer.address); + expect(name).to.equal('bob'); + + expect(oldOwner).to.equal(ethers.ZeroAddress); + }); + it('passing an empty name deletes an existing record', async () => { + await target.register('alice'); + const reverseRecord = `${signer.address + .slice(2) + .toLowerCase()}.addr.reverse`; + + let owner = await target.owner(ethers.namehash('alice.op.dm3.eth')); + let name = await target.reverse(ethers.namehash(reverseRecord)); + + expect(owner).to.equal(signer.address); + expect(name).to.equal('alice'); + + await target.register(ethers.toUtf8Bytes('')); + + owner = await target.owner(ethers.namehash('alice.op.dm3.eth')); + name = await target.reverse(ethers.namehash(reverseRecord)); + + expect(owner).to.equal(ethers.ZeroAddress); + expect(name).to.equal(''); + }); + }); + + describe('setText', () => { + it('can set text record if name has been registered before', async () => { + await target.register('alice'); + await target.setText(ethers.namehash('alice.op.dm3.eth'), 'key', 'value'); + const value = await target.text( + ethers.namehash('alice.op.dm3.eth'), + 'key' + ); + expect(value).to.equal('value'); + }); + + it('reverts if name has not been registered', async () => { + await expect( + target.setText(ethers.namehash('alice.op.dm3.eth'), 'key', 'value') + ).to.be.revertedWith('Name not registered'); + }); + it('reverts if msg.sender is not owner of name', async () => { + await target.register('alice'); + const other = (await ethers.getSigners())[1]; + await expect( + target + .connect(other) + .setText(ethers.namehash('alice.op.dm3.eth'), 'key', 'value') + ).to.be.revertedWith('Only owner'); + }); + }); +}); diff --git a/dm3-names/test/testDm3NameResgistrarEVMFetcher.ts b/dm3-names/test/testDm3NameResgistrarEVMFetcher.ts new file mode 100644 index 00000000..cf8c72b2 --- /dev/null +++ b/dm3-names/test/testDm3NameResgistrarEVMFetcher.ts @@ -0,0 +1,198 @@ +import { makeL1Gateway } from '@ensdomains/l1-gateway'; +import { Server } from '@chainlink/ccip-read-server'; +import { HardhatEthersProvider } from '@nomicfoundation/hardhat-ethers/internal/hardhat-ethers-provider'; +import type { HardhatEthersHelpers } from '@nomicfoundation/hardhat-ethers/types'; +import { expect } from 'chai'; +import { + BrowserProvider, + Contract, + JsonRpcProvider, + Signer, + ethers as ethersT, +} from 'ethers'; +import { FetchRequest } from 'ethers'; +import { ethers } from 'hardhat'; +import { EthereumProvider } from 'hardhat/types'; +import request from 'supertest'; + +type ethersObj = typeof ethersT & + Omit & { + provider: Omit & { + _hardhatProvider: EthereumProvider; + }; + }; + +declare module 'hardhat/types/runtime' { + const ethers: ethersObj; + interface HardhatRuntimeEnvironment { + ethers: ethersObj; + } +} + +describe.only('Dm3 Name Registrar Fetcher', () => { + let provider: BrowserProvider; + let signer: Signer; + let verifier: Contract; + let dm3NameRegistrar: Contract; + let dm3NameRegistrarEVMFetcher: Contract; + + let parentDomain: string; + + beforeEach(async () => { + // Hack to get a 'real' ethers provider from hardhat. The default `HardhatProvider` + // doesn't support CCIP-read. + provider = new ethers.BrowserProvider(ethers.provider._hardhatProvider); + // provider.on("debug", (x: any) => console.log(JSON.stringify(x, undefined, 2))); + signer = await provider.getSigner(0); + const gateway = makeL1Gateway(provider as unknown as JsonRpcProvider); + const server = new Server(); + gateway.add(server); + const app = server.makeApp('/'); + const getUrl = FetchRequest.createGetUrlFunc(); + ethers.FetchRequest.registerGetUrl(async (req: FetchRequest) => { + if (req.url != 'test:') return getUrl(req); + + const r = request(app).post('/'); + if (req.hasBody()) { + r.set('Content-Type', 'application/json').send( + ethers.toUtf8String(req.body) + ); + } + const response = await r; + return { + statusCode: response.statusCode, + statusMessage: response.ok ? 'OK' : response.statusCode.toString(), + body: ethers.toUtf8Bytes(JSON.stringify(response.body)), + headers: { + 'Content-Type': 'application/json', + }, + }; + }); + + //Deploy Verifier + const l1VerifierFactory = await ethers.getContractFactory( + 'L1Verifier', + signer + ); + verifier = await l1VerifierFactory.deploy(['test:']); + + //Deploy Dm3NameRegistrar + parentDomain = 'l2.dm3.eth'; + const dm3NameRegistrarFactory = await ethers.getContractFactory( + 'Dm3NameRegistrar', + signer + ); + dm3NameRegistrar = await dm3NameRegistrarFactory.deploy( + ethers.namehash(parentDomain) + ); + + //Deploy Dm3NameRegistrarEVMFetcher + + const dm3NameRegistrarEVMFetcherFactory = await ethers.getContractFactory( + 'Dm3NameRegistrarEVMFetcher', + signer + ); + + dm3NameRegistrarEVMFetcher = await dm3NameRegistrarEVMFetcherFactory.deploy( + verifier.target, + dm3NameRegistrar.target, + parentDomain + ); + // Mine an empty block so we have something to prove against + await provider.send('evm_mine', []); + }); + + it('should resolve ETH Address', async () => { + await dm3NameRegistrar.register('alice'); + await provider.send('evm_mine', []); + + const node = ethers.namehash(`alice.${parentDomain}`); + const encodedName = ethers.dnsEncode(`alice.${parentDomain}`); + + const i = new ethers.Interface(['function addr(bytes32) returns(address)']); + const calldata = i.encodeFunctionData('addr', [node]); + + const result2 = await dm3NameRegistrarEVMFetcher.resolve( + encodedName, + calldata, + { + enableCcipRead: true, + } + ); + const decoded = i.decodeFunctionResult('addr', result2); + + expect(decoded[0]).to.equal(await signer.getAddress()); + }); + it('should resolve name', async () => { + await dm3NameRegistrar.register('alice'); + await provider.send('evm_mine', []); + + const reverseRecord = `${(await signer.getAddress()) + .slice(2) + .toLowerCase()}.addr.reverse`; + + const node = ethers.namehash(reverseRecord); + const encodedName = ethers.dnsEncode(`alice.${parentDomain}`); + + const i = new ethers.Interface(['function name(bytes32) returns(string)']); + const calldata = i.encodeFunctionData('name', [node]); + + const result2 = await dm3NameRegistrarEVMFetcher.resolve( + encodedName, + calldata, + { + enableCcipRead: true, + } + ); + const decoded = i.decodeFunctionResult('name', result2); + + console.log('result', result2); + expect(decoded[0]).to.equal(`alice.${parentDomain}`); + }); + + it('should resolve text record', async () => { + await dm3NameRegistrar.register('alice'); + const key = 'name'; + const value = 'hello world'; + + const node = ethers.namehash(`alice.${parentDomain}`); + const encodedName = ethers.dnsEncode(`alice.${parentDomain}`); + + await dm3NameRegistrar.setText(node, key, value); + + await provider.send('evm_mine', []); + + const i = new ethers.Interface([ + 'function text(bytes32,string) returns(string)', + ]); + const calldata = i.encodeFunctionData('text', [node, key]); + const result2 = await dm3NameRegistrarEVMFetcher.resolve( + encodedName, + calldata, + { enableCcipRead: true } + ); + const decoded = i.decodeFunctionResult('text', result2); + expect(decoded[0]).to.equal(value); + }); + it('Should set the verifier correctly', async function () { + const newVerifier = await (await provider.getSigner(1)).getAddress(); + await dm3NameRegistrarEVMFetcher.connect(signer).setVerifier(newVerifier); + expect(await dm3NameRegistrarEVMFetcher.verifier()).to.equal(newVerifier); + }); + + it('Should set the target correctly', async function () { + const newTarget = await (await provider.getSigner(1)).getAddress(); + await dm3NameRegistrarEVMFetcher.connect(signer).setTarget(newTarget); + expect(await dm3NameRegistrarEVMFetcher.getFunction('target')()).to.equal(newTarget); + }); + + it('Should set the parent domain correctly', async function () { + const parentDomain = 'foo.bar'; + await dm3NameRegistrarEVMFetcher + .connect(signer) + .setParentDomain(parentDomain); + expect(await dm3NameRegistrarEVMFetcher.parentDomain()).to.equal( + parentDomain + ); + }); +}); diff --git a/dm3-names/tsconfig.json b/dm3-names/tsconfig.json new file mode 100644 index 00000000..2e8555d7 --- /dev/null +++ b/dm3-names/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../tsconfig.json", + "include": ["./scripts", "./test"], + "files": ["hardhat.config.ts"], + "compilerOptions": { + "esModuleInterop": true, + "verbatimModuleSyntax": false, + "allowSyntheticDefaultImports": true, + "target": "ES2020", + "module": "CommonJS", + "moduleResolution": "Node", + "skipLibCheck": true + } +} diff --git a/package.json b/package.json index eb970e6f..f7944b4c 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "l1-verifier", "op-verifier", "arb-verifier", - "crosschain-resolver" + "crosschain-resolver", + "dm3-names" ], "scripts": { "env": "env",