From 3d04d1f3bc0358755a814b398cf2c86ae7c0a247 Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Mon, 12 Aug 2024 13:43:57 +0100 Subject: [PATCH 01/10] Fuse burning changes token ID --- contracts/src/registry/BaseRegistry.sol | 27 +++---- contracts/src/registry/ETHRegistry.sol | 78 ++++++++++++------- contracts/src/registry/IRegistryDatastore.sol | 23 ++++-- contracts/src/registry/RegistryDatastore.sol | 33 ++++---- contracts/src/registry/RootRegistry.sol | 19 +++-- contracts/src/registry/UserRegistry.sol | 22 +++--- 6 files changed, 113 insertions(+), 89 deletions(-) diff --git a/contracts/src/registry/BaseRegistry.sol b/contracts/src/registry/BaseRegistry.sol index 761d559..d0d8ddc 100644 --- a/contracts/src/registry/BaseRegistry.sol +++ b/contracts/src/registry/BaseRegistry.sol @@ -14,9 +14,9 @@ import {IRegistryDatastore} from "./IRegistryDatastore.sol"; import {IRegistry} from "./IRegistry.sol"; abstract contract BaseRegistry is IRegistry, ERC1155Singleton { - error AccessDenied(string label, address owner, address caller); - error InvalidSubregistryFlags(string label, uint96 flags, uint96 expected); - error InvalidResolverFlags(string label, uint96 flags, uint96 expected); + error AccessDenied(uint256 tokenId, address owner, address caller); + error InvalidSubregistryFlags(uint256 tokenId, uint96 flags, uint96 expected); + error InvalidResolverFlags(uint256 tokenId, uint96 flags, uint96 expected); IRegistryDatastore public datastore; @@ -24,27 +24,26 @@ abstract contract BaseRegistry is IRegistry, ERC1155Singleton { datastore = _datastore; } - modifier onlyTokenOwner(string calldata label) { - uint256 tokenId = uint256(keccak256(bytes(label))); + modifier onlyTokenOwner(uint256 tokenId) { address owner = ownerOf(tokenId); if (owner != msg.sender) { - revert AccessDenied(label, owner, msg.sender); + revert AccessDenied(tokenId, owner, msg.sender); } _; } - modifier withSubregistryFlags(string calldata label, uint96 mask, uint96 expected) { - (, uint96 flags) = datastore.getSubregistry(uint256(keccak256(bytes(label)))); + modifier withSubregistryFlags(uint256 tokenId, uint96 mask, uint96 expected) { + (, uint96 flags) = datastore.getSubregistry(tokenId); if (flags & mask != expected) { - revert InvalidSubregistryFlags(label, flags & mask, expected); + revert InvalidSubregistryFlags(tokenId, flags & mask, expected); } _; } - modifier withResolverFlags(string calldata label, uint96 mask, uint96 expected) { - (, uint96 flags) = datastore.getResolver(uint256(keccak256(bytes(label)))); + modifier withResolverFlags(uint256 tokenId, uint96 mask, uint96 expected) { + (, uint96 flags) = datastore.getResolver(tokenId); if (flags & mask != expected) { - revert InvalidResolverFlags(label, flags & mask, expected); + revert InvalidResolverFlags(tokenId, flags & mask, expected); } _; } @@ -60,11 +59,9 @@ abstract contract BaseRegistry is IRegistry, ERC1155Singleton { super.supportsInterface(interfaceId); } - function _mint(string calldata label, address owner, IRegistry registry, uint96 flags) internal { - uint256 tokenId = uint256(keccak256(bytes(label))); + function _mint(uint256 tokenId, address owner, IRegistry registry, uint96 flags) internal { _mint(owner, tokenId, 1, ""); datastore.setSubregistry(tokenId, address(registry), flags); - emit NewSubname(label); } /*********************** diff --git a/contracts/src/registry/ETHRegistry.sol b/contracts/src/registry/ETHRegistry.sol index e6422ed..ddf9394 100644 --- a/contracts/src/registry/ETHRegistry.sol +++ b/contracts/src/registry/ETHRegistry.sol @@ -11,11 +11,13 @@ import {BaseRegistry} from "./BaseRegistry.sol"; contract ETHRegistry is BaseRegistry, AccessControl { bytes32 public constant REGISTRAR_ROLE = keccak256("REGISTRAR_ROLE"); - uint96 public constant SUBREGISTRY_FLAGS_MASK = 0x100000000; - uint96 public constant SUBREGISTRY_FLAG_LOCKED = 0x100000000; + uint32 public constant FLAGS_MASK = 0x7; + uint32 public constant FLAG_SUBREGISTRY_LOCKED = 0x1; + uint32 public constant FLAG_RESOLVER_LOCKED = 0x2; + uint32 public constant FLAG_FLAGS_LOCKED = 0x4; error NameAlreadyRegistered(string label); - error NameExpired(string label); + error NameExpired(uint256 tokenId); error CannotReduceExpiration(uint64 oldExpiration, uint64 newExpiration); constructor(IRegistryDatastore _datastore) @@ -43,62 +45,82 @@ contract ETHRegistry is BaseRegistry, AccessControl { string calldata label, address owner, IRegistry registry, - uint96 flags - ) public onlyRole(REGISTRAR_ROLE) { - uint256 tokenId = uint256(keccak256(bytes(label))); + uint32 flags, + uint64 expires + ) public onlyRole(REGISTRAR_ROLE) returns(uint256 tokenId) { + flags &= FLAGS_MASK; + tokenId = (uint256(keccak256(bytes(label))) & ~FLAGS_MASK) | flags; (, uint96 oldFlags) = datastore.getSubregistry(tokenId); - uint64 expires = uint64(oldFlags); - if (expires >= block.timestamp) { + uint64 oldExpiry = uint64(oldFlags >> 32); + if (oldExpiry >= block.timestamp) { revert NameAlreadyRegistered(label); } - _mint(label, owner, registry, flags); + _mint(tokenId, owner, registry, uint96(flags) | (uint96(expires) << 32)); + emit NewSubname(label); + return tokenId; } function renew( - string calldata label, + uint256 tokenId, uint64 expires ) public onlyRole(REGISTRAR_ROLE) { - uint256 tokenId = uint256(keccak256(bytes(label))); (address subregistry, uint96 flags) = datastore.getSubregistry(tokenId); - uint64 oldExpiration = uint64(flags); + uint64 oldExpiration = uint64(flags >> 32); if (oldExpiration < block.timestamp) { - revert NameExpired(label); + revert NameExpired(tokenId); } if (expires < oldExpiration) { revert CannotReduceExpiration(oldExpiration, expires); } - datastore.setSubregistry(tokenId, subregistry, (flags & SUBREGISTRY_FLAGS_MASK) | uint96(expires)); + datastore.setSubregistry(tokenId, subregistry, (flags & FLAGS_MASK) | (uint96(expires) << 32)); } - function locked( - string memory label - ) external view returns (bool) { - uint256 tokenId = uint256(keccak256(bytes(label))); - (, uint96 flags) = datastore.getSubregistry(tokenId); - return flags & SUBREGISTRY_FLAG_LOCKED != 0; + function nameData(uint256 tokenId) external view returns(uint64 expiry, uint32 flags) { + (, uint96 _flags) = datastore.getSubregistry(tokenId); + return (uint64(_flags >> 32), uint32(_flags)); } - function lock(string calldata label) external onlyTokenOwner(label) { - uint256 tokenId = uint256(keccak256(bytes(label))); - (address subregistry, uint96 flags) = datastore.getSubregistry(tokenId); - datastore.setSubregistry(tokenId, subregistry, flags & SUBREGISTRY_FLAG_LOCKED); + function lock(uint256 tokenId, uint32 flags) + external + onlyTokenOwner(tokenId) + withSubregistryFlags(tokenId, FLAG_FLAGS_LOCKED, 0) + { + (address subregistry, uint96 oldFlags) = datastore.getSubregistry(tokenId); + uint96 newFlags = (oldFlags & ~FLAGS_MASK) | flags; + if(newFlags != oldFlags) { + address owner = ownerOf(tokenId); + _burn(owner, tokenId, 1); + uint256 newTokenId = (tokenId & ~FLAGS_MASK) | newFlags; + _mint(newTokenId, owner, IRegistry(subregistry), newFlags); + } } function setSubregistry( - string calldata label, + uint256 tokenId, IRegistry registry ) external - onlyTokenOwner(label) - withSubregistryFlags(label, SUBREGISTRY_FLAG_LOCKED, 0) + onlyTokenOwner(tokenId) + withSubregistryFlags(tokenId, FLAG_SUBREGISTRY_LOCKED, 0) { - uint256 tokenId = uint256(keccak256(bytes(label))); (, uint96 flags) = datastore.getSubregistry(tokenId); datastore.setSubregistry(tokenId, address(registry), flags); } + function setResolver( + uint256 tokenId, + address resolver + ) + external + onlyTokenOwner(tokenId) + withSubregistryFlags(tokenId, FLAG_RESOLVER_LOCKED, 0) + { + (, uint96 flags) = datastore.getResolver(tokenId); + datastore.setResolver(tokenId, resolver, flags); + } + function supportsInterface( bytes4 interfaceId ) public view override(BaseRegistry, AccessControl) returns (bool) { diff --git a/contracts/src/registry/IRegistryDatastore.sol b/contracts/src/registry/IRegistryDatastore.sol index 54a7a0e..ea96240 100644 --- a/contracts/src/registry/IRegistryDatastore.sol +++ b/contracts/src/registry/IRegistryDatastore.sol @@ -1,14 +1,21 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.13; +/** + * @dev Interface for the ENSv2 registry datastore, which stores subregistry and resolver addresses and flags + * for all names, keyed by registry address and `keccak256(label)`. + * The lower 32 bits of label hashes are masked out for storage and retreival, allowing these bits to be used + * by registry implementations for different versions of tokens that reference the same underlying name. This + * means that two labelHashes that differ only in the least-significant 32 bits will resolve to the same name. + */ interface IRegistryDatastore { - event SubregistryUpdate(address indexed registry, uint256 indexed tokenId, address subregistry, uint96 flags); - event ResolverUpdate(address indexed registry, uint256 indexed tokenId, address resolver, uint96 flags); + event SubregistryUpdate(address indexed registry, uint256 indexed labelHash, address subregistry, uint96 flags); + event ResolverUpdate(address indexed registry, uint256 indexed labelHash, address resolver, uint96 flags); - function getSubregistry(address registry, uint256 tokenId) external view returns(address subregistry, uint96 flags); - function getSubregistry(uint256 tokenId) external view returns(address subregistry, uint96 flags); - function getResolver(address registry, uint256 tokenId) external view returns(address resolver, uint96 flags); - function getResolver(uint256 tokenId) external view returns(address resolver, uint96 flags); - function setSubregistry(uint256 tokenId, address subregistry, uint96 flags) external; - function setResolver(uint256 tokenId, address resolver, uint96 flags) external; + function getSubregistry(address registry, uint256 labelHash) external view returns(address subregistry, uint96 flags); + function getSubregistry(uint256 labelHash) external view returns(address subregistry, uint96 flags); + function getResolver(address registry, uint256 labelHash) external view returns(address resolver, uint96 flags); + function getResolver(uint256 labelHash) external view returns(address resolver, uint96 flags); + function setSubregistry(uint256 labelHash, address subregistry, uint96 flags) external; + function setResolver(uint256 labelHash, address resolver, uint96 flags) external; } diff --git a/contracts/src/registry/RegistryDatastore.sol b/contracts/src/registry/RegistryDatastore.sol index 3f17e64..c88ad84 100644 --- a/contracts/src/registry/RegistryDatastore.sol +++ b/contracts/src/registry/RegistryDatastore.sol @@ -4,36 +4,37 @@ pragma solidity >=0.8.13; import { IRegistryDatastore } from './IRegistryDatastore.sol'; contract RegistryDatastore is IRegistryDatastore { - mapping(address registry => mapping(uint256 tokenId => uint256)) internal subregistries; - mapping(address registry => mapping(uint256 tokenId => uint256)) internal resolvers; + uint256 LABEL_HASH_MASK = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000; + mapping(address registry => mapping(uint256 labelHash => uint256)) internal subregistries; + mapping(address registry => mapping(uint256 labelHash => uint256)) internal resolvers; - function getSubregistry(address registry, uint256 tokenId) public view returns(address subregistry, uint96 flags) { - uint256 data = subregistries[registry][tokenId]; + function getSubregistry(address registry, uint256 labelHash) public view returns(address subregistry, uint96 flags) { + uint256 data = subregistries[registry][labelHash & LABEL_HASH_MASK]; subregistry = address(uint160(data)); flags = uint96(data >> 160); } - function getSubregistry(uint256 tokenId) external view returns(address subregistry, uint96 flags) { - return getSubregistry(msg.sender, tokenId); + function getSubregistry(uint256 labelHash) external view returns(address subregistry, uint96 flags) { + return getSubregistry(msg.sender, labelHash); } - function getResolver(address registry, uint256 tokenId) public view returns(address resolver, uint96 flags) { - uint256 data = subregistries[registry][tokenId]; + function getResolver(address registry, uint256 labelHash) public view returns(address resolver, uint96 flags) { + uint256 data = subregistries[registry][labelHash & LABEL_HASH_MASK]; resolver = address(uint160(data)); flags = uint96(data >> 160); } - function getResolver(uint256 tokenId) external view returns(address resolver, uint96 flags) { - return getResolver(msg.sender, tokenId); + function getResolver(uint256 labelHash) external view returns(address resolver, uint96 flags) { + return getResolver(msg.sender, labelHash); } - function setSubregistry(uint256 tokenId, address subregistry, uint96 flags) external { - subregistries[msg.sender][tokenId] = (uint256(flags) << 160) | uint256(uint160(subregistry)); - emit SubregistryUpdate(msg.sender, tokenId, subregistry, flags); + function setSubregistry(uint256 labelHash, address subregistry, uint96 flags) external { + subregistries[msg.sender][labelHash & LABEL_HASH_MASK] = (uint256(flags) << 160) | uint256(uint160(subregistry)); + emit SubregistryUpdate(msg.sender, labelHash & LABEL_HASH_MASK, subregistry, flags); } - function setResolver(uint256 tokenId, address resolver, uint96 flags) external { - resolvers[msg.sender][tokenId] = (uint256(flags) << 160) | uint256(uint160(resolver)); - emit ResolverUpdate(msg.sender, tokenId, resolver, flags); + function setResolver(uint256 labelHash, address resolver, uint96 flags) external { + resolvers[msg.sender][labelHash & LABEL_HASH_MASK] = (uint256(flags) << 160) | uint256(uint160(resolver)); + emit ResolverUpdate(msg.sender, labelHash & LABEL_HASH_MASK, resolver, flags); } } diff --git a/contracts/src/registry/RootRegistry.sol b/contracts/src/registry/RootRegistry.sol index 5b0d89b..c5a19f3 100644 --- a/contracts/src/registry/RootRegistry.sol +++ b/contracts/src/registry/RootRegistry.sol @@ -26,35 +26,34 @@ contract RootRegistry is BaseRegistry, AccessControl { external onlyRole(SUBDOMAIN_ISSUER_ROLE) { - _mint(label, owner, registry, locked ? SUBREGISTRY_FLAG_LOCKED : 0); + uint256 tokenId = uint256(keccak256(bytes(label))); + _mint(tokenId, owner, registry, locked ? SUBREGISTRY_FLAG_LOCKED : 0); + emit NewSubname(label); } - function burn(string calldata label) + function burn(uint256 tokenId) external onlyRole(SUBDOMAIN_ISSUER_ROLE) - withSubregistryFlags(label, SUBREGISTRY_FLAGS_MASK, 0) + withSubregistryFlags(tokenId, SUBREGISTRY_FLAGS_MASK, 0) { - uint256 tokenId = uint256(keccak256(bytes(label))); address owner = ownerOf(tokenId); _burn(owner, tokenId, 1); datastore.setSubregistry(tokenId, address(0), 0); } - function lock(string calldata label) + function lock(uint256 tokenId) external onlyRole(SUBDOMAIN_ISSUER_ROLE) { - uint256 tokenId = uint256(keccak256(bytes(label))); (address subregistry, uint96 flags) = datastore.getSubregistry(tokenId); datastore.setSubregistry(tokenId, subregistry, flags & SUBREGISTRY_FLAG_LOCKED); } - function setSubregistry(string calldata label, IRegistry registry) + function setSubregistry(uint256 tokenId, IRegistry registry) external - onlyTokenOwner(label) - withSubregistryFlags(label, SUBREGISTRY_FLAGS_MASK, 0) + onlyTokenOwner(tokenId) + withSubregistryFlags(tokenId, SUBREGISTRY_FLAGS_MASK, 0) { - uint256 tokenId = uint256(keccak256(bytes(label))); (, uint96 flags) = datastore.getSubregistry(tokenId); datastore.setSubregistry(tokenId, address(registry), flags); } diff --git a/contracts/src/registry/UserRegistry.sol b/contracts/src/registry/UserRegistry.sol index f0c691b..264cb27 100644 --- a/contracts/src/registry/UserRegistry.sol +++ b/contracts/src/registry/UserRegistry.sol @@ -29,7 +29,7 @@ contract UserRegistry is BaseRegistry { modifier onlyNameOwner() { address owner = parent.ownerOf(uint256(keccak256(bytes(label)))); if (owner != msg.sender) { - revert AccessDenied("", owner, msg.sender); + revert AccessDenied(0, owner, msg.sender); } _; } @@ -44,37 +44,35 @@ contract UserRegistry is BaseRegistry { IRegistry registry, uint96 flags ) external onlyNameOwner { - _mint(_label, owner, registry, flags); + uint256 tokenId = uint256(keccak256(bytes(_label))); + _mint(tokenId, owner, registry, flags); + emit NewSubname(label); } function burn( - string calldata _label - ) external onlyNameOwner withSubregistryFlags(_label, SUBREGISTRY_FLAG_LOCKED, 0) { - uint256 tokenId = uint256(keccak256(bytes(_label))); + uint256 tokenId + ) external onlyNameOwner withSubregistryFlags(tokenId, SUBREGISTRY_FLAG_LOCKED, 0) { address owner = ownerOf(tokenId); _burn(owner, tokenId, 1); datastore.setSubregistry(tokenId, address(0), 0); } function locked( - string memory _label + uint256 tokenId ) external view returns (bool) { - uint256 tokenId = uint256(keccak256(bytes(_label))); (, uint96 flags) = datastore.getSubregistry(tokenId); return flags & SUBREGISTRY_FLAG_LOCKED != 0; } - function lock(string calldata _label) external onlyTokenOwner(_label) { - uint256 tokenId = uint256(keccak256(bytes(_label))); + function lock(uint256 tokenId) external onlyTokenOwner(tokenId) { (address subregistry, uint96 flags) = datastore.getSubregistry(tokenId); datastore.setSubregistry(tokenId, subregistry, flags & SUBREGISTRY_FLAG_LOCKED); } function setSubregistry( - string calldata _label, + uint256 tokenId, IRegistry registry - ) external onlyTokenOwner(_label) withSubregistryFlags(_label, SUBREGISTRY_FLAG_LOCKED, 0) { - uint256 tokenId = uint256(keccak256(bytes(_label))); + ) external onlyTokenOwner(tokenId) withSubregistryFlags(tokenId, SUBREGISTRY_FLAG_LOCKED, 0) { (, uint96 flags) = datastore.getSubregistry(tokenId); datastore.setSubregistry(tokenId, address(registry), flags); } From 8782cc9ac7856b8a56c84c7443e7e95611b1fd7c Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Mon, 12 Aug 2024 14:56:43 +0100 Subject: [PATCH 02/10] Tests, fixes --- bun.lockb | Bin 217301 -> 217301 bytes contracts/src/registry/ETHRegistry.sol | 6 +- contracts/test/Ens.t.ts | 64 +++++++++++++++++++- contracts/test/fixtures/deployEnsFixture.ts | 7 ++- 4 files changed, 70 insertions(+), 7 deletions(-) diff --git a/bun.lockb b/bun.lockb index 0705dccf8a2a061a6d1ea61165b1f38e2da5850a..12eb3008e4f55ea1803af4b30f00c482103a58e1 100755 GIT binary patch delta 3733 zcmXBW$Mddc9ftAw#fU~dSfa#=8Y8h{4R(wrXe=bLaIDy|V-0rfM9mpCXCyl&*|21? zdb4BYyh~y`*s){B*s!B{?rU>@p6~S!z`2`FoxADO&DY*~a&oq~_4MsKYQ3+Ucd4DU z{x?jiw6iw&rYW^{(T3kLrO~e1=-Z~W+D#juGo{n++GH}N*IM5(?^Jux+TS%L(+*na zd#2>tqt^YtDTQ{_dOt9w)J|Ieho)58SsVPwlv=xJ!`YNZyK18!o6>4GZTu5cI_<7a zeriguwSH#asrI0?e{M>q9kk9bOv$xJt@}$;3hk)%7E?;?r1gJgN~N8(!LLoJwTm|V zjVX-^D_Tzk~Ix0zCCN3HiK zQ%dcm_5W;2rJc3GUredBi#GhLDUEj3Mt?J<)o$8&Go{n++T`!1^jhm5=ACK}TKk`- zWZFUN{L7SFd(^uBHl@&xTJJxml-fz_|JRgCJ8Og8lv=xJ!~dDmXjijejZVzA+Rf|@ z@rmeQhvYoP9;`d)O5p(Z`HC3~;M`F$haC@^e28KLD?|@fY+-}=V#N-2NFJuxgLR3n6b@iNTrqn@h3F}YEo=}!Rk4E|lBX&5U_D(| z3J0*Cp_st{&NCHrID-2u#R5j~Ld6m$@Sm+%!3@FWiZv_{K1Z>E6{6=Vwy;6`JjD)n zNS?3QgLQ?j6b@iViWv;xyg)IBBe*YAEMNrhO2rZ;@L!}@!3@ER6>C@^e2HQMD?~3< zY+-{qR_tJh#F;Q;pQ6f+pWdA(u|M{rZc0!HxOpjg5L z{u>o5m?3zRVhsy~Z&qwzh3GAcEo>0KRk4E|lD8@LU}d^eIDq|j#S8{;-l3So5!`nw z7BGVMF2xci@ZYUi!3@EB6l+)@e6L~yD@3_s3me4mQ|w@edY9fom#NyKOz)TqRY8WlTgG}tk*V8_l_u@fuSU?-MHEOXe*k?c6hhUI%) zksT|~yCk;3jvX~B8Vhzb&wXv~&-1X|#rgYj} zoBYg_gVy@Fd8gW5YyZNOOgm_uUz(C@k6QOvrWD#y>n)~~+DYsG+LTH=YlGjIQfn7& z_*+vN?W&D#F{RaR+W2>-blP2;{N9v<*7}2ar`ld?ucl<$LF@d{lw5n%x_>gI(2iQ~ zR#Qstr1k%7N~N8(!Cy?NwTm|Vt0|3k)kc3arPXfQcr&Hb?%L$JT_FDU& zrexYd>-@`;Ko3N;_+V-IQ9pXv6=R(r8z+UyV-8w%X0? z_3?@5V29*9#RFL9>q?;q`vS!b25@e#n8OiVOR<0vygMkCFoA!eVg)k z4XhAdq}aj+@tqYr*daNkcmT`Rl|m2pT@*7Iz`3hp4o7hBrdYrT-o=U~OyJ*Lv4R2p_1}zzWfW6kFIJzErV;9g+tt9>98tt`vH(AF7za0FI}a!x7wv zDHbq-_i)7$Ch#AjSiuazBNb~{AbgZ!11m(2R%~H|_%Vtd?2!112e2NiD}^5H$0=qo zfb)389FE{#rdYrT-V+o{n81IcVg)k998t`vH(BgG5`aGs}_!x7x)D;6+<_X5QdCh%XVSiuazixg{EAbhc611m%? zQEXv@I9BXnhvcP-2e4kID}^5Hmn&v4fb$B)9FE|=Qn7#$yjLlfFoFMS#R_H!62%%8 z2u~|EaNBQ!`P49g^269>99Nt`vH(-=LVm0L~i~b2x&VDi$z;_a?;> zCh*^^SiuazTNG_`0a`v?2x=e@c>q)D}^5Hs}wUBzDsbt`vH(Kctw! z0M3UMb2x(g5yb*V@UB)YVFJHUtYC)Vqlz^w5PnRtffb^UE4Hvf{0YSlc1S*{cmV5D zx>D%D{> 32); @@ -88,11 +88,11 @@ contract ETHRegistry is BaseRegistry, AccessControl { withSubregistryFlags(tokenId, FLAG_FLAGS_LOCKED, 0) { (address subregistry, uint96 oldFlags) = datastore.getSubregistry(tokenId); - uint96 newFlags = (oldFlags & ~FLAGS_MASK) | flags; + uint96 newFlags = oldFlags | (flags & FLAGS_MASK); if(newFlags != oldFlags) { address owner = ownerOf(tokenId); _burn(owner, tokenId, 1); - uint256 newTokenId = (tokenId & ~FLAGS_MASK) | newFlags; + uint256 newTokenId = (tokenId & ~uint256(FLAGS_MASK)) | (newFlags & FLAGS_MASK); _mint(newTokenId, owner, IRegistry(subregistry), newFlags); } } diff --git a/contracts/test/Ens.t.ts b/contracts/test/Ens.t.ts index a64f13c..a50ef4d 100644 --- a/contracts/test/Ens.t.ts +++ b/contracts/test/Ens.t.ts @@ -1,9 +1,71 @@ import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers"; import { expect } from "chai"; -import { getAddress } from "viem"; +import { getAddress, keccak256, parseEventLogs, fromHex } from "viem"; import { deployEnsFixture, registerName } from "./fixtures/deployEnsFixture"; import { dnsEncodeName } from "./utils/utils"; +describe("ETHRegistry", function () { + it("registers names", async () => { + const client = await hre.viem.getPublicClient(); + const walletClients = await hre.viem.getWalletClients(); + const { universalResolver, ethRegistry } = await loadFixture( + deployEnsFixture + ); + const tx = await registerName({ ethRegistry, label: "test2" }); + const receipt = await client.waitForTransactionReceipt({hash: tx}); + const logs = parseEventLogs({abi: ethRegistry.abi, logs: receipt.logs }) + const id = logs[0].args.id; + const expectedId = fromHex(keccak256("test2"), "bigint") & 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8n; + expect(id).to.equal(expectedId); + expect((await ethRegistry.read.ownerOf([id])).toLowerCase()).to.equal(walletClients[0].account.address); + }); + + it("registers locked names", async () => { + const client = await hre.viem.getPublicClient(); + const walletClients = await hre.viem.getWalletClients(); + const { universalResolver, ethRegistry } = await loadFixture( + deployEnsFixture + ); + const tx = await registerName({ ethRegistry, label: "test", subregistryLocked: true, resolverLocked: true }); + const receipt = await client.waitForTransactionReceipt({hash: tx}); + const logs = parseEventLogs({abi: ethRegistry.abi, logs: receipt.logs }) + const id = logs[0].args.id; + const expectedId = (fromHex(keccak256("test"), "bigint") & 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8n) | 3n; + expect(id).to.equal(expectedId); + expect((await ethRegistry.read.ownerOf([id])).toLowerCase()).to.equal(walletClients[0].account.address); + }); + + it("supports locking names", async () => { + const client = await hre.viem.getPublicClient(); + const walletClients = await hre.viem.getWalletClients(); + const { universalResolver, ethRegistry } = await loadFixture( + deployEnsFixture + ); + const id = fromHex(keccak256("test2"), "bigint") & 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8n; + await registerName({ ethRegistry, label: "test2" }); + expect((await ethRegistry.read.ownerOf([id])).toLowerCase()).to.equal(walletClients[0].account.address); + expect((await ethRegistry.read.ownerOf([id | 3n])).toLowerCase()).to.equal("0x0000000000000000000000000000000000000000"); + await ethRegistry.write.lock([id, 0x3]); + expect((await ethRegistry.read.ownerOf([id | 3n])).toLowerCase()).to.equal(walletClients[0].account.address); + expect((await ethRegistry.read.ownerOf([id])).toLowerCase()).to.equal("0x0000000000000000000000000000000000000000"); + }); + + it("cannot unlock names", async () => { + const client = await hre.viem.getPublicClient(); + const walletClients = await hre.viem.getWalletClients(); + const { universalResolver, ethRegistry } = await loadFixture( + deployEnsFixture + ); + const id = fromHex(keccak256("test2"), "bigint") & 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8n | 3n; + await registerName({ ethRegistry, label: "test2", subregistryLocked: true, resolverLocked: true }); + expect((await ethRegistry.read.ownerOf([id])).toLowerCase()).to.equal(walletClients[0].account.address); + expect((await ethRegistry.read.ownerOf([id ^ 3n])).toLowerCase()).to.equal("0x0000000000000000000000000000000000000000"); + await ethRegistry.write.lock([id, 0x0]); + expect((await ethRegistry.read.ownerOf([id])).toLowerCase()).to.equal(walletClients[0].account.address); + expect((await ethRegistry.read.ownerOf([id ^ 3n])).toLowerCase()).to.equal("0x0000000000000000000000000000000000000000"); + }); +}); + describe("Ens", function () { it("returns eth registry for eth", async () => { const { universalResolver, ethRegistry } = await loadFixture( diff --git a/contracts/test/fixtures/deployEnsFixture.ts b/contracts/test/fixtures/deployEnsFixture.ts index 368a7d8..815c820 100644 --- a/contracts/test/fixtures/deployEnsFixture.ts +++ b/contracts/test/fixtures/deployEnsFixture.ts @@ -57,7 +57,8 @@ export const registerName = async ({ expiry = BigInt(Math.floor(Date.now() / 1000) + 1000000), owner: owner_, subregistry = "0x0000000000000000000000000000000000000000", - locked = false, + subregistryLocked = false, + resolverLocked = false, }: Pick & { label: string; expiry?: bigint; @@ -67,6 +68,6 @@ export const registerName = async ({ }) => { const owner = owner_ ?? (await hre.viem.getWalletClients())[0].account.address; - const flags = (locked ? BigInt(0x100000000) : BigInt(0)) | expiry; - await ethRegistry.write.register([label, owner, subregistry, flags]); + const flags = (subregistryLocked ? 1n : 0n) | (resolverLocked ? 2n : 0n); + return await ethRegistry.write.register([label, owner, subregistry, flags, expiry]); }; From 2906704f518d8a2c95ff942b100a85d9feab7f87 Mon Sep 17 00:00:00 2001 From: tate Date: Tue, 13 Aug 2024 11:15:09 +1000 Subject: [PATCH 03/10] fixed tests --- bun.lockb | Bin 217301 -> 214944 bytes contracts/hardhat.config.cts | 19 +++ contracts/hardhat.config.ts | 15 --- contracts/package.json | 11 +- contracts/tasks/esm_fix.cts | 21 ++++ contracts/test/ETHRegistry.t.ts | 112 +++++++++++++++++ contracts/test/Ens.t.ts | 77 ++---------- contracts/test/fixtures/deployEnsFixture.ts | 40 ++++-- contracts/tsconfig.json | 41 +++--- package.json | 3 + ...nomicfoundation%2Fhardhat-viem@2.0.1.patch | 118 ++++++++++++++++++ 11 files changed, 336 insertions(+), 121 deletions(-) create mode 100644 contracts/hardhat.config.cts delete mode 100644 contracts/hardhat.config.ts create mode 100644 contracts/tasks/esm_fix.cts create mode 100644 contracts/test/ETHRegistry.t.ts create mode 100644 patches/@nomicfoundation%2Fhardhat-viem@2.0.1.patch diff --git a/bun.lockb b/bun.lockb index 12eb3008e4f55ea1803af4b30f00c482103a58e1..949336f2aa9b67dbbc97c6008d27c064cfc8788c 100755 GIT binary patch delta 41890 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 delta 43619 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 diff --git a/contracts/hardhat.config.cts b/contracts/hardhat.config.cts new file mode 100644 index 0000000..e2b1660 --- /dev/null +++ b/contracts/hardhat.config.cts @@ -0,0 +1,19 @@ +import type { HardhatUserConfig } from "hardhat/config"; + +import "@nomicfoundation/hardhat-foundry"; +import "@nomicfoundation/hardhat-verify"; +import "@nomicfoundation/hardhat-viem"; +import "./tasks/esm_fix.cjs"; + +import("@ensdomains/hardhat-chai-matchers-viem"); + +const config = { + solidity: { + version: "0.8.25", + settings: { + evmVersion: "cancun", + }, + }, +} satisfies HardhatUserConfig; + +export default config; diff --git a/contracts/hardhat.config.ts b/contracts/hardhat.config.ts deleted file mode 100644 index f81f07f..0000000 --- a/contracts/hardhat.config.ts +++ /dev/null @@ -1,15 +0,0 @@ -import "@nomicfoundation/hardhat-foundry"; -import "@nomicfoundation/hardhat-toolbox"; -import "@nomicfoundation/hardhat-viem"; -import type { HardhatUserConfig } from "hardhat/config"; - -const config: HardhatUserConfig = { - solidity: { - version: "0.8.25", - settings: { - evmVersion: "cancun" - } - } -}; - -export default config; diff --git a/contracts/package.json b/contracts/package.json index 1abed01..b0f131a 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -1,17 +1,22 @@ { "name": "contracts", - "module": "index.ts", + "type": "module", "scripts": { - "test:hardhat": "hardhat test", + "compile:hardhat": "NODE_OPTIONS=\"--experimental-loader ts-node/esm/transpile-only\" hardhat compile", + "test:hardhat": "NODE_OPTIONS=\"--experimental-loader ts-node/esm/transpile-only\" hardhat test", "test:forge": "echo \"Skipping forge test for now since there are no tests...\"", "test": "bun run test:forge && bun run test:hardhat" }, "devDependencies": { + "@ensdomains/hardhat-chai-matchers-viem": "^0.0.7", "@nomicfoundation/hardhat-foundry": "^1.1.1", - "@nomicfoundation/hardhat-toolbox": "^5.0.0", + "@nomicfoundation/hardhat-toolbox-viem": "^3.0.0", "@nomicfoundation/hardhat-viem": "^2.0.0", "@types/bun": "latest", + "@vitest/expect": "^1.6.0", + "chai": "^5.1.1", "hardhat": "^2.22.2", + "ts-node": "^10.9.2", "viem": "^2.9.12" }, "peerDependencies": { diff --git a/contracts/tasks/esm_fix.cts b/contracts/tasks/esm_fix.cts new file mode 100644 index 0000000..13e532f --- /dev/null +++ b/contracts/tasks/esm_fix.cts @@ -0,0 +1,21 @@ +import fs = require("fs/promises"); +import task_names = require("hardhat/builtin-tasks/task-names"); +import config = require("hardhat/config"); +import path = require("path"); + +config + .subtask(task_names.TASK_COMPILE_SOLIDITY) + .setAction(async (_, { config }, runSuper) => { + const superRes = await runSuper(); + + try { + await fs.writeFile( + path.join(config.paths.artifacts, "package.json"), + '{ "type": "commonjs" }' + ); + } catch (error) { + console.error("Error writing package.json: ", error); + } + + return superRes; + }); diff --git a/contracts/test/ETHRegistry.t.ts b/contracts/test/ETHRegistry.t.ts new file mode 100644 index 0000000..37e0cd2 --- /dev/null +++ b/contracts/test/ETHRegistry.t.ts @@ -0,0 +1,112 @@ +import { loadFixture } from "@nomicfoundation/hardhat-toolbox-viem/network-helpers.js"; +import { expect } from "chai"; +import { fromHex, getAddress, labelhash, zeroAddress } from "viem"; +import { deployEnsFixture, registerName } from "./fixtures/deployEnsFixture.js"; + +describe("ETHRegistry", function () { + it("registers names", async () => { + const { accounts, ethRegistry } = await loadFixture(deployEnsFixture); + + const tx = await registerName({ ethRegistry, label: "test2" }); + const expectedId = + fromHex(labelhash("test2"), "bigint") & + 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8n; + + await expect(ethRegistry) + .transaction(tx) + .toEmitEvent("TransferSingle") + .withArgs( + getAddress(accounts[0].address), + zeroAddress, + accounts[0].address, + expectedId, + 1n + ); + await expect( + ethRegistry.read.ownerOf([expectedId]) + ).resolves.toEqualAddress(accounts[0].address); + }); + + it("registers locked names", async () => { + const { accounts, ethRegistry } = await loadFixture(deployEnsFixture); + + const tx = await registerName({ + ethRegistry, + label: "test2", + subregistryLocked: true, + resolverLocked: true, + }); + const expectedId = + (fromHex(labelhash("test2"), "bigint") & + 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8n) | + 3n; + + await expect(ethRegistry) + .transaction(tx) + .toEmitEvent("TransferSingle") + .withArgs( + getAddress(accounts[0].address), + zeroAddress, + accounts[0].address, + expectedId, + 1n + ); + await expect( + ethRegistry.read.ownerOf([expectedId]) + ).resolves.toEqualAddress(accounts[0].address); + }); + + it("supports locking names", async () => { + const { accounts, ethRegistry } = await loadFixture(deployEnsFixture); + + await registerName({ ethRegistry, label: "test2" }); + const expectedId = + fromHex(labelhash("test2"), "bigint") & + 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8n; + + await expect( + ethRegistry.read.ownerOf([expectedId]) + ).resolves.toEqualAddress(accounts[0].address); + await expect( + ethRegistry.read.ownerOf([expectedId | 3n]) + ).resolves.toEqualAddress(zeroAddress); + await ethRegistry.write.lock([expectedId, 0x3]); + await expect( + ethRegistry.read.ownerOf([expectedId | 3n]) + ).resolves.toEqualAddress(accounts[0].address); + await expect( + ethRegistry.read.ownerOf([expectedId]) + ).resolves.toEqualAddress(zeroAddress); + }); + + it("cannot unlock names", async () => { + const { accounts, ethRegistry } = await loadFixture(deployEnsFixture); + + await registerName({ + ethRegistry, + label: "test2", + subregistryLocked: true, + resolverLocked: true, + }); + const expectedId = + (fromHex(labelhash("test2"), "bigint") & + 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8n) | + 3n; + + await expect( + ethRegistry.read.ownerOf([expectedId]) + ).resolves.toEqualAddress(accounts[0].address); + await expect( + ethRegistry.read.ownerOf([expectedId ^ 3n]) + ).resolves.toEqualAddress(zeroAddress); + + await ethRegistry.write.lock([expectedId, 0x0]); + + await expect( + ethRegistry.read.ownerOf([expectedId]) + ).resolves.toEqualAddress(accounts[0].address); + await expect( + ethRegistry.read.ownerOf([expectedId ^ 3n]) + ).resolves.toEqualAddress(zeroAddress); + }); +}); diff --git a/contracts/test/Ens.t.ts b/contracts/test/Ens.t.ts index a50ef4d..a57916d 100644 --- a/contracts/test/Ens.t.ts +++ b/contracts/test/Ens.t.ts @@ -1,70 +1,7 @@ -import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers"; +import { loadFixture } from "@nomicfoundation/hardhat-toolbox-viem/network-helpers.js"; import { expect } from "chai"; -import { getAddress, keccak256, parseEventLogs, fromHex } from "viem"; -import { deployEnsFixture, registerName } from "./fixtures/deployEnsFixture"; -import { dnsEncodeName } from "./utils/utils"; - -describe("ETHRegistry", function () { - it("registers names", async () => { - const client = await hre.viem.getPublicClient(); - const walletClients = await hre.viem.getWalletClients(); - const { universalResolver, ethRegistry } = await loadFixture( - deployEnsFixture - ); - const tx = await registerName({ ethRegistry, label: "test2" }); - const receipt = await client.waitForTransactionReceipt({hash: tx}); - const logs = parseEventLogs({abi: ethRegistry.abi, logs: receipt.logs }) - const id = logs[0].args.id; - const expectedId = fromHex(keccak256("test2"), "bigint") & 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8n; - expect(id).to.equal(expectedId); - expect((await ethRegistry.read.ownerOf([id])).toLowerCase()).to.equal(walletClients[0].account.address); - }); - - it("registers locked names", async () => { - const client = await hre.viem.getPublicClient(); - const walletClients = await hre.viem.getWalletClients(); - const { universalResolver, ethRegistry } = await loadFixture( - deployEnsFixture - ); - const tx = await registerName({ ethRegistry, label: "test", subregistryLocked: true, resolverLocked: true }); - const receipt = await client.waitForTransactionReceipt({hash: tx}); - const logs = parseEventLogs({abi: ethRegistry.abi, logs: receipt.logs }) - const id = logs[0].args.id; - const expectedId = (fromHex(keccak256("test"), "bigint") & 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8n) | 3n; - expect(id).to.equal(expectedId); - expect((await ethRegistry.read.ownerOf([id])).toLowerCase()).to.equal(walletClients[0].account.address); - }); - - it("supports locking names", async () => { - const client = await hre.viem.getPublicClient(); - const walletClients = await hre.viem.getWalletClients(); - const { universalResolver, ethRegistry } = await loadFixture( - deployEnsFixture - ); - const id = fromHex(keccak256("test2"), "bigint") & 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8n; - await registerName({ ethRegistry, label: "test2" }); - expect((await ethRegistry.read.ownerOf([id])).toLowerCase()).to.equal(walletClients[0].account.address); - expect((await ethRegistry.read.ownerOf([id | 3n])).toLowerCase()).to.equal("0x0000000000000000000000000000000000000000"); - await ethRegistry.write.lock([id, 0x3]); - expect((await ethRegistry.read.ownerOf([id | 3n])).toLowerCase()).to.equal(walletClients[0].account.address); - expect((await ethRegistry.read.ownerOf([id])).toLowerCase()).to.equal("0x0000000000000000000000000000000000000000"); - }); - - it("cannot unlock names", async () => { - const client = await hre.viem.getPublicClient(); - const walletClients = await hre.viem.getWalletClients(); - const { universalResolver, ethRegistry } = await loadFixture( - deployEnsFixture - ); - const id = fromHex(keccak256("test2"), "bigint") & 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8n | 3n; - await registerName({ ethRegistry, label: "test2", subregistryLocked: true, resolverLocked: true }); - expect((await ethRegistry.read.ownerOf([id])).toLowerCase()).to.equal(walletClients[0].account.address); - expect((await ethRegistry.read.ownerOf([id ^ 3n])).toLowerCase()).to.equal("0x0000000000000000000000000000000000000000"); - await ethRegistry.write.lock([id, 0x0]); - expect((await ethRegistry.read.ownerOf([id])).toLowerCase()).to.equal(walletClients[0].account.address); - expect((await ethRegistry.read.ownerOf([id ^ 3n])).toLowerCase()).to.equal("0x0000000000000000000000000000000000000000"); - }); -}); +import { deployEnsFixture, registerName } from "./fixtures/deployEnsFixture.js"; +import { dnsEncodeName } from "./utils/utils.js"; describe("Ens", function () { it("returns eth registry for eth", async () => { @@ -73,8 +10,8 @@ describe("Ens", function () { ); const [fetchedEthRegistry, isExact] = await universalResolver.read.getRegistry([dnsEncodeName("eth")]); - expect(isExact).to.be.true; - expect(fetchedEthRegistry).to.equal(getAddress(ethRegistry.address)); + expect(isExact).toBe(true); + expect(fetchedEthRegistry).toEqualAddress(ethRegistry.address); }); it("returns eth registry for test.eth without user registry", async () => { @@ -85,7 +22,7 @@ describe("Ens", function () { const [registry, isExact] = await universalResolver.read.getRegistry([ dnsEncodeName("test.eth"), ]); - expect(isExact).to.be.false; - expect(registry).to.equal(getAddress(ethRegistry.address)); + expect(isExact).toBe(false); + expect(registry).toEqualAddress(ethRegistry.address); }); }); diff --git a/contracts/test/fixtures/deployEnsFixture.ts b/contracts/test/fixtures/deployEnsFixture.ts index 815c820..4061b63 100644 --- a/contracts/test/fixtures/deployEnsFixture.ts +++ b/contracts/test/fixtures/deployEnsFixture.ts @@ -1,31 +1,46 @@ import hre from "hardhat"; -import { Address, bytesToHex, keccak256, stringToHex, zeroAddress } from "viem"; -import { packetToBytes } from "../utils/utils"; +import { Address, bytesToHex, keccak256, stringToHex } from "viem"; +import { packetToBytes } from "../utils/utils.js"; export async function deployEnsFixture() { - const walletClients = await hre.viem.getWalletClients(); + const publicClient = await hre.viem.getPublicClient(); + const accounts = await hre.viem + .getWalletClients() + .then((clients) => clients.map((c) => c.account)); + const datastore = await hre.viem.deployContract("RegistryDatastore", []); - const rootRegistry = await hre.viem.deployContract("RootRegistry", [datastore.address]); - const ethRegistry = await hre.viem.deployContract("ETHRegistry", [datastore.address]); + const rootRegistry = await hre.viem.deployContract("RootRegistry", [ + datastore.address, + ]); + const ethRegistry = await hre.viem.deployContract("ETHRegistry", [ + datastore.address, + ]); const universalResolver = await hre.viem.deployContract("UniversalResolver", [ rootRegistry.address, ]); await rootRegistry.write.grantRole([ keccak256(stringToHex("SUBDOMAIN_ISSUER_ROLE")), - walletClients[0].account.address, + accounts[0].address, ]); await ethRegistry.write.grantRole([ keccak256(stringToHex("REGISTRAR_ROLE")), - walletClients[0].account.address, + accounts[0].address, ]); await rootRegistry.write.mint([ "eth", - walletClients[0].account.address, + accounts[0].address, ethRegistry.address, true, ]); - return { datastore, rootRegistry, ethRegistry, universalResolver }; + return { + publicClient, + accounts, + datastore, + rootRegistry, + ethRegistry, + universalResolver, + }; } export type EnsFixture = Awaited>; @@ -64,10 +79,11 @@ export const registerName = async ({ expiry?: bigint; owner?: Address; subregistry?: Address; - locked?: boolean; + subregistryLocked?: boolean; + resolverLocked?: boolean; }) => { const owner = owner_ ?? (await hre.viem.getWalletClients())[0].account.address; - const flags = (subregistryLocked ? 1n : 0n) | (resolverLocked ? 2n : 0n); - return await ethRegistry.write.register([label, owner, subregistry, flags, expiry]); + const flags = (subregistryLocked ? 1 : 0) | (resolverLocked ? 2 : 0); + return ethRegistry.write.register([label, owner, subregistry, flags, expiry]); }; diff --git a/contracts/tsconfig.json b/contracts/tsconfig.json index 3b5f8f0..b983f7d 100644 --- a/contracts/tsconfig.json +++ b/contracts/tsconfig.json @@ -1,27 +1,26 @@ { "compilerOptions": { - // Enable latest features - "lib": ["ESNext"], - "target": "ES2020", - "module": "NodeNext", + "target": "ES2022", + "module": "Node16", "moduleDetection": "force", - "jsx": "react-jsx", - "allowJs": true, - - // Bundler mode - "moduleResolution": "NodeNext", - "allowImportingTsExtensions": true, - "noEmit": true, - "esModuleInterop": true, - - // Best practices "strict": true, + "esModuleInterop": true, + "moduleResolution": "NodeNext", + "outDir": "build", "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, - - // Some stricter flags (disabled by default) - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false - } + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true + }, + "include": [ + "./tasks/**/*.ts", + "./tasks/**/*.cts", + "./test/**/*.ts", + "./artifacts/**/*.d.ts" + ], + "ts-node": { + "experimentalResolver": true, + "experimentalSpecifierResolution": "node", + "files": true + }, + "files": ["hardhat.config.cts"] } diff --git a/package.json b/package.json index aa6c881..1ea12dd 100644 --- a/package.json +++ b/package.json @@ -6,5 +6,8 @@ ], "devDependencies": { "typescript": "^5.4.5" + }, + "patchedDependencies": { + "@nomicfoundation/hardhat-viem@2.0.1": "patches/@nomicfoundation%2Fhardhat-viem@2.0.1.patch" } } diff --git a/patches/@nomicfoundation%2Fhardhat-viem@2.0.1.patch b/patches/@nomicfoundation%2Fhardhat-viem@2.0.1.patch new file mode 100644 index 0000000..3f0612f --- /dev/null +++ b/patches/@nomicfoundation%2Fhardhat-viem@2.0.1.patch @@ -0,0 +1,118 @@ +diff --git a/internal/clients.js b/node_modules/internal/clients.js +index 9466df2..e512716 100644 +--- a/internal/clients.js ++++ b/internal/clients.js +@@ -47,7 +47,8 @@ async function innerGetPublicClient(provider, chain, publicClientConfig) { + const parameters = { ...defaultParameters, ...publicClientConfig }; + const publicClient = viem.createPublicClient({ + chain, +- transport: viem.custom(provider), ++ transport: viem.custom(provider, { retryCount: 0 }), ++ ccipRead: false, + ...parameters, + }); + return publicClient; +@@ -80,7 +81,7 @@ async function innerGetWalletClients(provider, chain, accounts, walletClientConf + const walletClients = accounts.map((account) => viem.createWalletClient({ + chain, + account, +- transport: viem.custom(provider), ++ transport: viem.custom(provider, { retryCount: 0 }), + ...parameters, + })); + return walletClients; +@@ -123,7 +124,7 @@ async function innerGetTestClient(provider, chain, mode, testClientConfig) { + const testClient = viem.createTestClient({ + mode, + chain, +- transport: viem.custom(provider), ++ transport: viem.custom(provider, { retryCount: 0 }), + ...parameters, + }); + return testClient; +diff --git a/internal/tasks.js b/internal/tasks.js +index 411ea51..b2cb6ab 100644 +--- a/internal/tasks.js ++++ b/internal/tasks.js +@@ -105,6 +105,12 @@ declare module "hardhat/types/artifacts" { + .map((name) => `${name}: never;`) + .join("\n ")} + } ++ ++ interface ContractTypesMap { ++ ${Array.from(duplicateContractNames) ++ .map((name) => `${name}: never;`) ++ .join("\n ")} ++ } + } + `; + } +@@ -173,6 +179,7 @@ function generateArtifactsDefinition(contractTypeData) { + return `${AUTOGENERATED_FILE_PREFACE} + + import "hardhat/types/artifacts"; ++import type { GetContractReturnType } from "@nomicfoundation/hardhat-viem/types"; + + ${contractTypeData + .map((ctd) => `import { ${ctd.typeName} } from "./${ctd.contractName}";`) +@@ -187,6 +194,15 @@ declare module "hardhat/types/artifacts" { + .map((ctd) => `["${ctd.fqn}"]: ${ctd.typeName};`) + .join("\n ")} + } ++ ++ interface ContractTypesMap { ++ ${contractTypeData ++ .map((ctd) => `["${ctd.contractName}"]: GetContractReturnType<${ctd.typeName}["abi"]>;`) ++ .join("\n ")} ++ ${contractTypeData ++ .map((ctd) => `["${ctd.fqn}"]: GetContractReturnType<${ctd.typeName}["abi"]>;`) ++ .join("\n ")} ++ } + } + `; + } +diff --git a/internal/type-extensions.d.ts b/internal/type-extensions.d.ts +index 59bcafe..522bfd0 100644 +--- a/internal/type-extensions.d.ts ++++ b/internal/type-extensions.d.ts +@@ -1,18 +1,10 @@ + import type { Address, PublicClientConfig, WalletClientConfig, TestClientConfig } from "viem"; +-import type { PublicClient, TestClient, WalletClient, deployContract, sendDeploymentTransaction, getContractAt } from "../types"; ++import type { HardhatViemHelpers } from "../types"; + import "hardhat/types/runtime"; + import "hardhat/types/artifacts"; + declare module "hardhat/types/runtime" { + interface HardhatRuntimeEnvironment { +- viem: { +- getPublicClient(publicClientConfig?: Partial): Promise; +- getWalletClients(walletClientConfig?: Partial): Promise; +- getWalletClient(address: Address, walletClientConfig?: Partial): Promise; +- getTestClient(testClientConfig?: Partial): Promise; +- deployContract: typeof deployContract; +- sendDeploymentTransaction: typeof sendDeploymentTransaction; +- getContractAt: typeof getContractAt; +- }; ++ viem: HardhatViemHelpers; + } + } + declare module "hardhat/types/artifacts" { +diff --git a/types.d.ts b/types.d.ts +index 0ec3f21..e95ebbb 100644 +--- a/types.d.ts ++++ b/types.d.ts +@@ -35,5 +35,14 @@ export declare function sendDeploymentTransaction(contractNam + deploymentTransaction: GetTransactionReturnType; + }>; + export declare function getContractAt(contractName: ContractName, address: viemT.Address, config?: GetContractAtConfig): Promise; ++export interface HardhatViemHelpers { ++ getPublicClient(publicClientConfig?: Partial): Promise; ++ getWalletClients(walletClientConfig?: Partial): Promise; ++ getWalletClient(address: viemT.Address, walletClientConfig?: Partial): Promise; ++ getTestClient(testClientConfig?: Partial): Promise; ++ deployContract: typeof deployContract; ++ sendDeploymentTransaction: typeof sendDeploymentTransaction; ++ getContractAt: typeof getContractAt; ++} + export type { AbiParameterToPrimitiveType } from "abitype"; + //# sourceMappingURL=types.d.ts.map +\ No newline at end of file From 07d058ae097f8f12ddc96489d1d98e820bdb910a Mon Sep 17 00:00:00 2001 From: tate Date: Tue, 13 Aug 2024 11:17:54 +1000 Subject: [PATCH 04/10] gha --- .github/workflows/main.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/main.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..2e138d5 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,25 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + test: + name: Hardhat Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Use Bun 1.1.16 + uses: oven-sh/setup-bun@v1 + with: + bun-version: 1.1.16 + + - run: bun install --frozen-lockfile + + - run: cd ./contracts + + - name: Run tests + run: bun run test:hardhat From 05504814ae6dfad218f02ae79b0b0e8d27dd3fb6 Mon Sep 17 00:00:00 2001 From: tate Date: Tue, 13 Aug 2024 11:21:37 +1000 Subject: [PATCH 05/10] filter --- .github/workflows/main.yml | 4 +--- contracts/package.json | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2e138d5..4c8ded1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,7 +19,5 @@ jobs: - run: bun install --frozen-lockfile - - run: cd ./contracts - - name: Run tests - run: bun run test:hardhat + run: bun --filter contracts test:hardhat diff --git a/contracts/package.json b/contracts/package.json index b0f131a..bde690f 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -2,8 +2,8 @@ "name": "contracts", "type": "module", "scripts": { - "compile:hardhat": "NODE_OPTIONS=\"--experimental-loader ts-node/esm/transpile-only\" hardhat compile", - "test:hardhat": "NODE_OPTIONS=\"--experimental-loader ts-node/esm/transpile-only\" hardhat test", + "compile:hardhat": "NODE_OPTIONS='--experimental-loader ts-node/esm/transpile-only' hardhat compile", + "test:hardhat": "NODE_OPTIONS='--experimental-loader ts-node/esm/transpile-only' hardhat test", "test:forge": "echo \"Skipping forge test for now since there are no tests...\"", "test": "bun run test:forge && bun run test:hardhat" }, From 01a365edc70f165df3cafa80ef6adf6f0d6fb0f4 Mon Sep 17 00:00:00 2001 From: tate Date: Tue, 13 Aug 2024 11:23:36 +1000 Subject: [PATCH 06/10] foundry --- .github/workflows/main.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4c8ded1..877ba2b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,11 +7,14 @@ on: jobs: test: - name: Hardhat Tests + name: Contract tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + - name: Use Bun 1.1.16 uses: oven-sh/setup-bun@v1 with: From 5613f1972f4c494fbd4e6cb0e02cdc36fe34eecc Mon Sep 17 00:00:00 2001 From: tate Date: Tue, 13 Aug 2024 11:32:22 +1000 Subject: [PATCH 07/10] recursive submodules --- .github/workflows/main.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 877ba2b..f24240e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,6 +11,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + submodules: "recursive" - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 From 995bb622f6838713247cf1c63b52b9910f0f028e Mon Sep 17 00:00:00 2001 From: tate Date: Wed, 14 Aug 2024 09:38:19 +1000 Subject: [PATCH 08/10] RegistryDatastore tests --- .github/workflows/main.yml | 2 +- contracts/package.json | 2 +- contracts/src/registry/RegistryDatastore.sol | 2 +- contracts/test/RegistryDatastore.t.sol | 144 +++++++++++++++++++ 4 files changed, 147 insertions(+), 3 deletions(-) create mode 100644 contracts/test/RegistryDatastore.t.sol diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f24240e..d6daa6b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -25,4 +25,4 @@ jobs: - run: bun install --frozen-lockfile - name: Run tests - run: bun --filter contracts test:hardhat + run: bun --filter contracts test diff --git a/contracts/package.json b/contracts/package.json index bde690f..314b68e 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -4,7 +4,7 @@ "scripts": { "compile:hardhat": "NODE_OPTIONS='--experimental-loader ts-node/esm/transpile-only' hardhat compile", "test:hardhat": "NODE_OPTIONS='--experimental-loader ts-node/esm/transpile-only' hardhat test", - "test:forge": "echo \"Skipping forge test for now since there are no tests...\"", + "test:forge": "forge test", "test": "bun run test:forge && bun run test:hardhat" }, "devDependencies": { diff --git a/contracts/src/registry/RegistryDatastore.sol b/contracts/src/registry/RegistryDatastore.sol index c88ad84..da6e3ce 100644 --- a/contracts/src/registry/RegistryDatastore.sol +++ b/contracts/src/registry/RegistryDatastore.sol @@ -19,7 +19,7 @@ contract RegistryDatastore is IRegistryDatastore { } function getResolver(address registry, uint256 labelHash) public view returns(address resolver, uint96 flags) { - uint256 data = subregistries[registry][labelHash & LABEL_HASH_MASK]; + uint256 data = resolvers[registry][labelHash & LABEL_HASH_MASK]; resolver = address(uint160(data)); flags = uint96(data >> 160); } diff --git a/contracts/test/RegistryDatastore.t.sol b/contracts/test/RegistryDatastore.t.sol new file mode 100644 index 0000000..3c9d360 --- /dev/null +++ b/contracts/test/RegistryDatastore.t.sol @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.13; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +import "src/registry/RegistryDatastore.sol"; + +contract TestETHRegistry is Test { + uint256 LABEL_HASH_MASK = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000; + uint256 labelHash = uint256(keccak256("test")); + RegistryDatastore datastore; + + function setUp() public { + datastore = new RegistryDatastore(); + } + + function test_GetSetSubregistry_MsgSender() public { + // set subregistry with empty flags + datastore.setSubregistry(labelHash, address(this), 0); + + address subregistry; + uint96 flags; + + (subregistry, flags) = datastore.getSubregistry(labelHash); + vm.assertEq(subregistry, address(this)); + vm.assertEq(flags, 0); + + (subregistry, flags) = datastore.getSubregistry(address(this), labelHash); + vm.assertEq(subregistry, address(this)); + vm.assertEq(flags, 0); + } + + function test_GetSetSubregistry_OtherRegistry() public { + DummyRegistry r = new DummyRegistry(datastore); + r.setSubregistry(labelHash, address(this), 0); + + address subregistry; + uint96 flags; + + (subregistry, flags) = datastore.getSubregistry(labelHash); + vm.assertEq(subregistry, address(0)); + vm.assertEq(flags, 0); + + (subregistry, flags) = datastore.getSubregistry(address(r), labelHash); + vm.assertEq(subregistry, address(this)); + vm.assertEq(flags, 0); + } + + function test_GetSetSubregistry_32BitCustomFlags() public { + datastore.setSubregistry(labelHash, address(this), 0x80000001); + + address subregistry; + uint96 flags; + + (subregistry, flags) = datastore.getSubregistry(labelHash); + vm.assertEq(subregistry, address(this)); + vm.assertEq(flags, 0x80000001); + + // get with flags on labelhash + (subregistry, flags) = datastore.getSubregistry((labelHash & LABEL_HASH_MASK) | 0x80000001); + vm.assertEq(subregistry, address(this)); + vm.assertEq(flags, 0x80000001); + } + + function test_GetSetSubregistry_96BitCustomFlags() public { + datastore.setSubregistry(labelHash, address(this), 0x800000000000000000000001); + + (address subregistry, uint96 flags) = datastore.getSubregistry(labelHash); + vm.assertEq(subregistry, address(this)); + vm.assertEq(flags, 0x800000000000000000000001); + } + + function test_GetSetResolver_MsgSender() public { + datastore.setResolver(labelHash, address(this), 0); + + address resolver; + uint96 flags; + + (resolver, flags) = datastore.getResolver(labelHash); + vm.assertEq(resolver, address(this)); + vm.assertEq(flags, 0); + + (resolver, flags) = datastore.getResolver(address(this), labelHash); + vm.assertEq(resolver, address(this)); + vm.assertEq(flags, 0); + } + + function test_GetSetResolver_OtherRegistry() public { + DummyRegistry r = new DummyRegistry(datastore); + r.setResolver(labelHash, address(this), 0); + + address resolver; + uint96 flags; + + (resolver, flags) = datastore.getResolver(labelHash); + vm.assertEq(resolver, address(0)); + vm.assertEq(flags, 0); + + (resolver, flags) = datastore.getResolver(address(r), labelHash); + vm.assertEq(resolver, address(this)); + vm.assertEq(flags, 0); + } + + function test_GetSetResolver_32BitCustomFlags() public { + datastore.setResolver(labelHash, address(this), 0x80000001); + + address resolver; + uint96 flags; + + (resolver, flags) = datastore.getResolver(labelHash); + vm.assertEq(resolver, address(this)); + vm.assertEq(flags, 0x80000001); + + // get with flags on labelhash + (resolver, flags) = datastore.getResolver((labelHash & LABEL_HASH_MASK) | 0x80000001); + vm.assertEq(resolver, address(this)); + vm.assertEq(flags, 0x80000001); + } + + function test_GetSetResolver_96BitCustomFlags() public { + datastore.setResolver(labelHash, address(this), 0x800000000000000000000001); + + (address resolver, uint96 flags) = datastore.getResolver(labelHash); + vm.assertEq(resolver, address(this)); + vm.assertEq(flags, 0x800000000000000000000001); + } +} + +contract DummyRegistry { + RegistryDatastore datastore; + + constructor(RegistryDatastore _datastore) { + datastore = _datastore; + } + + function setSubregistry(uint256 labelHash, address subregistry, uint96 flags) public { + datastore.setSubregistry(labelHash, subregistry, flags); + } + + function setResolver(uint256 labelHash, address resolver, uint96 flags) public { + datastore.setResolver(labelHash, resolver, flags); + } +} \ No newline at end of file From 449659a6f17691e991feb0b070b559edfa1cb782 Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Wed, 14 Aug 2024 14:17:11 +0100 Subject: [PATCH 09/10] Rewrite ETHRegistry test in forge; forge fmt --- contracts/src/registry/BaseRegistry.sol | 27 +- contracts/src/registry/ERC1155Singleton.sol | 51 ++-- contracts/src/registry/ETHRegistry.sol | 68 ++--- contracts/src/registry/IERC1155Singleton.sol | 2 +- contracts/src/registry/IRegistry.sol | 2 +- contracts/src/registry/IRegistryDatastore.sol | 11 +- contracts/src/registry/RegistryDatastore.sol | 14 +- contracts/src/registry/RootRegistry.sol | 16 +- contracts/src/registry/UserRegistry.sol | 40 +-- contracts/src/utils/UniversalResolver.sol | 6 +- contracts/test/ETHRegistry.t.sol | 73 +++++ contracts/test/ETHRegistry.t.ts | 112 -------- contracts/test/RegistryDatastore.t.sol | 254 +++++++++--------- 13 files changed, 305 insertions(+), 371 deletions(-) create mode 100644 contracts/test/ETHRegistry.t.sol delete mode 100644 contracts/test/ETHRegistry.t.ts diff --git a/contracts/src/registry/BaseRegistry.sol b/contracts/src/registry/BaseRegistry.sol index d0d8ddc..aa904de 100644 --- a/contracts/src/registry/BaseRegistry.sol +++ b/contracts/src/registry/BaseRegistry.sol @@ -51,12 +51,15 @@ abstract contract BaseRegistry is IRegistry, ERC1155Singleton { /** * @dev See {IERC165-supportsInterface}. */ - function supportsInterface(bytes4 interfaceId) public view virtual override(ERC1155Singleton, IERC165) returns (bool) { - return - interfaceId == type(IERC1155).interfaceId || - interfaceId == type(IERC1155MetadataURI).interfaceId || - interfaceId == type(IRegistry).interfaceId || - super.supportsInterface(interfaceId); + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(ERC1155Singleton, IERC165) + returns (bool) + { + return interfaceId == type(IERC1155).interfaceId || interfaceId == type(IERC1155MetadataURI).interfaceId + || interfaceId == type(IRegistry).interfaceId || super.supportsInterface(interfaceId); } function _mint(uint256 tokenId, address owner, IRegistry registry, uint96 flags) internal { @@ -64,16 +67,18 @@ abstract contract BaseRegistry is IRegistry, ERC1155Singleton { datastore.setSubregistry(tokenId, address(registry), flags); } - /*********************** + /** + * * IRegistry functions * - ***********************/ - + * + */ + /** * @dev Fetches the registry for a subdomain of the current registry. * @param label The label to resolve. * @return The address of the registry for this subdomain, or `address(0)` if none exists. */ - function getSubregistry(string calldata label) external virtual view returns (IRegistry) { + function getSubregistry(string calldata label) external view virtual returns (IRegistry) { (address subregistry,) = datastore.getSubregistry(uint256(keccak256(bytes(label)))); return IRegistry(subregistry); } @@ -83,7 +88,7 @@ abstract contract BaseRegistry is IRegistry, ERC1155Singleton { * @param label The label to fetch a resolver for. * @return resolver The address of a resolver responsible for this name, or `address(0)` if none exists. */ - function getResolver(string calldata label) external virtual view returns (address resolver) { + function getResolver(string calldata label) external view virtual returns (address resolver) { (resolver,) = datastore.getResolver(uint256(keccak256(bytes(label)))); } } diff --git a/contracts/src/registry/ERC1155Singleton.sol b/contracts/src/registry/ERC1155Singleton.sol index 33824e0..821f1b5 100644 --- a/contracts/src/registry/ERC1155Singleton.sol +++ b/contracts/src/registry/ERC1155Singleton.sol @@ -19,12 +19,8 @@ abstract contract ERC1155Singleton is Context, ERC165, IERC1155Singleton, IERC11 using Arrays for uint256[]; using Arrays for address[]; - event Approval( - address indexed owner, - address indexed approved, - uint256 indexed tokenId - ); - + event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); + mapping(uint256 id => address) private _owners; mapping(address account => mapping(address operator => bool)) private _operatorApprovals; @@ -37,18 +33,16 @@ abstract contract ERC1155Singleton is Context, ERC165, IERC1155Singleton, IERC11 * @dev See {IERC165-supportsInterface}. */ function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { - return - interfaceId == type(IERC1155).interfaceId || - interfaceId == type(IERC1155Singleton).interfaceId || - interfaceId == type(IERC1155MetadataURI).interfaceId || - super.supportsInterface(interfaceId); + return interfaceId == type(IERC1155).interfaceId || interfaceId == type(IERC1155Singleton).interfaceId + || interfaceId == type(IERC1155MetadataURI).interfaceId || super.supportsInterface(interfaceId); } - /************************************************************************** + /** + * * ERC1155 methods - *************************************************************************/ - - function uri(uint256 /* id */) public view virtual returns (string memory); + * + */ + function uri(uint256 /* id */ ) public view virtual returns (string memory); /** * @dev See {IERC1155-balanceOf}. @@ -64,10 +58,12 @@ abstract contract ERC1155Singleton is Context, ERC165, IERC1155Singleton, IERC11 * * - `accounts` and `ids` must have the same length. */ - function balanceOfBatch( - address[] memory accounts, - uint256[] memory ids - ) public view virtual returns (uint256[] memory) { + function balanceOfBatch(address[] memory accounts, uint256[] memory ids) + public + view + virtual + returns (uint256[] memory) + { if (accounts.length != ids.length) { revert ERC1155InvalidArrayLength(ids.length, accounts.length); } @@ -148,11 +144,11 @@ abstract contract ERC1155Singleton is Context, ERC165, IERC1155Singleton, IERC11 uint256 id = ids.unsafeMemoryAccess(i); uint256 value = values.unsafeMemoryAccess(i); - if(value > 0) { + if (value > 0) { address owner = _owners[id]; - if(owner != from) { + if (owner != from) { revert ERC1155InsufficientBalance(from, 0, value, id); - } else if(value > 1) { + } else if (value > 1) { revert ERC1155InsufficientBalance(from, 1, value, id); } _owners[id] = to; @@ -320,7 +316,7 @@ abstract contract ERC1155Singleton is Context, ERC165, IERC1155Singleton, IERC11 } _updateWithAcceptanceCheck(from, address(0), ids, values, ""); } - + /** * @dev Approve `operator` to operate on all of `owner` tokens * @@ -341,10 +337,11 @@ abstract contract ERC1155Singleton is Context, ERC165, IERC1155Singleton, IERC11 /** * @dev Creates an array in memory with only one value for each of the elements provided. */ - function _asSingletonArrays( - uint256 element1, - uint256 element2 - ) private pure returns (uint256[] memory array1, uint256[] memory array2) { + function _asSingletonArrays(uint256 element1, uint256 element2) + private + pure + returns (uint256[] memory array1, uint256[] memory array2) + { /// @solidity memory-safe-assembly assembly { // Load the free memory pointer diff --git a/contracts/src/registry/ETHRegistry.sol b/contracts/src/registry/ETHRegistry.sol index f67e678..17b897d 100644 --- a/contracts/src/registry/ETHRegistry.sol +++ b/contracts/src/registry/ETHRegistry.sol @@ -20,19 +20,21 @@ contract ETHRegistry is BaseRegistry, AccessControl { error NameExpired(uint256 tokenId); error CannotReduceExpiration(uint64 oldExpiration, uint64 newExpiration); - constructor(IRegistryDatastore _datastore) - BaseRegistry(_datastore) - { + constructor(IRegistryDatastore _datastore) BaseRegistry(_datastore) { _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); } - function uri(uint256 /*tokenId*/) public override pure returns(string memory) { + function uri(uint256 /*tokenId*/ ) public pure override returns (string memory) { return ""; } - function ownerOf( - uint256 tokenId - ) public view virtual override(ERC1155Singleton, IERC1155Singleton) returns (address) { + function ownerOf(uint256 tokenId) + public + view + virtual + override(ERC1155Singleton, IERC1155Singleton) + returns (address) + { (, uint96 flags) = datastore.getSubregistry(tokenId); uint64 expires = uint64(flags); if (expires < block.timestamp) { @@ -41,31 +43,26 @@ contract ETHRegistry is BaseRegistry, AccessControl { return super.ownerOf(tokenId); } - function register( - string calldata label, - address owner, - IRegistry registry, - uint32 flags, - uint64 expires - ) public onlyRole(REGISTRAR_ROLE) returns(uint256 tokenId) { + function register(string calldata label, address owner, IRegistry registry, uint32 flags, uint64 expires) + public + onlyRole(REGISTRAR_ROLE) + returns (uint256 tokenId) + { flags &= FLAGS_MASK; tokenId = (uint256(keccak256(bytes(label))) & ~uint256(FLAGS_MASK)) | flags; - + (, uint96 oldFlags) = datastore.getSubregistry(tokenId); uint64 oldExpiry = uint64(oldFlags >> 32); if (oldExpiry >= block.timestamp) { revert NameAlreadyRegistered(label); } - + _mint(tokenId, owner, registry, uint96(flags) | (uint96(expires) << 32)); emit NewSubname(label); return tokenId; } - function renew( - uint256 tokenId, - uint64 expires - ) public onlyRole(REGISTRAR_ROLE) { + function renew(uint256 tokenId, uint64 expires) public onlyRole(REGISTRAR_ROLE) { (address subregistry, uint96 flags) = datastore.getSubregistry(tokenId); uint64 oldExpiration = uint64(flags >> 32); if (oldExpiration < block.timestamp) { @@ -77,7 +74,7 @@ contract ETHRegistry is BaseRegistry, AccessControl { datastore.setSubregistry(tokenId, subregistry, (flags & FLAGS_MASK) | (uint96(expires) << 32)); } - function nameData(uint256 tokenId) external view returns(uint64 expiry, uint32 flags) { + function nameData(uint256 tokenId) external view returns (uint64 expiry, uint32 flags) { (, uint96 _flags) = datastore.getSubregistry(tokenId); return (uint64(_flags >> 32), uint32(_flags)); } @@ -86,21 +83,21 @@ contract ETHRegistry is BaseRegistry, AccessControl { external onlyTokenOwner(tokenId) withSubregistryFlags(tokenId, FLAG_FLAGS_LOCKED, 0) + returns (uint256 newTokenId) { (address subregistry, uint96 oldFlags) = datastore.getSubregistry(tokenId); uint96 newFlags = oldFlags | (flags & FLAGS_MASK); - if(newFlags != oldFlags) { + if (newFlags != oldFlags) { address owner = ownerOf(tokenId); _burn(owner, tokenId, 1); - uint256 newTokenId = (tokenId & ~uint256(FLAGS_MASK)) | (newFlags & FLAGS_MASK); + newTokenId = (tokenId & ~uint256(FLAGS_MASK)) | (newFlags & FLAGS_MASK); _mint(newTokenId, owner, IRegistry(subregistry), newFlags); + } else { + newTokenId = tokenId; } } - function setSubregistry( - uint256 tokenId, - IRegistry registry - ) + function setSubregistry(uint256 tokenId, IRegistry registry) external onlyTokenOwner(tokenId) withSubregistryFlags(tokenId, FLAG_SUBREGISTRY_LOCKED, 0) @@ -109,10 +106,7 @@ contract ETHRegistry is BaseRegistry, AccessControl { datastore.setSubregistry(tokenId, address(registry), flags); } - function setResolver( - uint256 tokenId, - address resolver - ) + function setResolver(uint256 tokenId, address resolver) external onlyTokenOwner(tokenId) withSubregistryFlags(tokenId, FLAG_RESOLVER_LOCKED, 0) @@ -121,15 +115,11 @@ contract ETHRegistry is BaseRegistry, AccessControl { datastore.setResolver(tokenId, resolver, flags); } - function supportsInterface( - bytes4 interfaceId - ) public view override(BaseRegistry, AccessControl) returns (bool) { - return - interfaceId == type(IRegistry).interfaceId || - super.supportsInterface(interfaceId); + function supportsInterface(bytes4 interfaceId) public view override(BaseRegistry, AccessControl) returns (bool) { + return interfaceId == type(IRegistry).interfaceId || super.supportsInterface(interfaceId); } - function getSubregistry(string calldata label) external override virtual view returns (IRegistry) { + function getSubregistry(string calldata label) external view virtual override returns (IRegistry) { (address subregistry, uint96 flags) = datastore.getSubregistry(uint256(keccak256(bytes(label)))); uint64 expires = uint64(flags); if (expires >= block.timestamp) { @@ -138,7 +128,7 @@ contract ETHRegistry is BaseRegistry, AccessControl { return IRegistry(subregistry); } - function getResolver(string calldata label) external override virtual view returns (address) { + function getResolver(string calldata label) external view virtual override returns (address) { (address resolver, uint96 flags) = datastore.getResolver(uint256(keccak256(bytes(label)))); uint64 expires = uint64(flags); if (expires >= block.timestamp) { diff --git a/contracts/src/registry/IERC1155Singleton.sol b/contracts/src/registry/IERC1155Singleton.sol index 39cb03b..b0dc35a 100644 --- a/contracts/src/registry/IERC1155Singleton.sol +++ b/contracts/src/registry/IERC1155Singleton.sol @@ -5,4 +5,4 @@ import {IERC1155} from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; interface IERC1155Singleton is IERC1155 { function ownerOf(uint256 id) external view returns (address owner); -} \ No newline at end of file +} diff --git a/contracts/src/registry/IRegistry.sol b/contracts/src/registry/IRegistry.sol index bd7a242..a62e0b2 100644 --- a/contracts/src/registry/IRegistry.sol +++ b/contracts/src/registry/IRegistry.sol @@ -5,7 +5,7 @@ import {IERC1155Singleton} from "./IERC1155Singleton.sol"; interface IRegistry is IERC1155Singleton { event NewSubname(string label); - + /** * @dev Fetches the registry for a subdomain of the current registry. * @param label The label to resolve. diff --git a/contracts/src/registry/IRegistryDatastore.sol b/contracts/src/registry/IRegistryDatastore.sol index ea96240..06b38b4 100644 --- a/contracts/src/registry/IRegistryDatastore.sol +++ b/contracts/src/registry/IRegistryDatastore.sol @@ -12,10 +12,13 @@ interface IRegistryDatastore { event SubregistryUpdate(address indexed registry, uint256 indexed labelHash, address subregistry, uint96 flags); event ResolverUpdate(address indexed registry, uint256 indexed labelHash, address resolver, uint96 flags); - function getSubregistry(address registry, uint256 labelHash) external view returns(address subregistry, uint96 flags); - function getSubregistry(uint256 labelHash) external view returns(address subregistry, uint96 flags); - function getResolver(address registry, uint256 labelHash) external view returns(address resolver, uint96 flags); - function getResolver(uint256 labelHash) external view returns(address resolver, uint96 flags); + function getSubregistry(address registry, uint256 labelHash) + external + view + returns (address subregistry, uint96 flags); + function getSubregistry(uint256 labelHash) external view returns (address subregistry, uint96 flags); + function getResolver(address registry, uint256 labelHash) external view returns (address resolver, uint96 flags); + function getResolver(uint256 labelHash) external view returns (address resolver, uint96 flags); function setSubregistry(uint256 labelHash, address subregistry, uint96 flags) external; function setResolver(uint256 labelHash, address resolver, uint96 flags) external; } diff --git a/contracts/src/registry/RegistryDatastore.sol b/contracts/src/registry/RegistryDatastore.sol index da6e3ce..b042ae8 100644 --- a/contracts/src/registry/RegistryDatastore.sol +++ b/contracts/src/registry/RegistryDatastore.sol @@ -1,30 +1,34 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.13; -import { IRegistryDatastore } from './IRegistryDatastore.sol'; +import {IRegistryDatastore} from "./IRegistryDatastore.sol"; contract RegistryDatastore is IRegistryDatastore { uint256 LABEL_HASH_MASK = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000; mapping(address registry => mapping(uint256 labelHash => uint256)) internal subregistries; mapping(address registry => mapping(uint256 labelHash => uint256)) internal resolvers; - function getSubregistry(address registry, uint256 labelHash) public view returns(address subregistry, uint96 flags) { + function getSubregistry(address registry, uint256 labelHash) + public + view + returns (address subregistry, uint96 flags) + { uint256 data = subregistries[registry][labelHash & LABEL_HASH_MASK]; subregistry = address(uint160(data)); flags = uint96(data >> 160); } - function getSubregistry(uint256 labelHash) external view returns(address subregistry, uint96 flags) { + function getSubregistry(uint256 labelHash) external view returns (address subregistry, uint96 flags) { return getSubregistry(msg.sender, labelHash); } - function getResolver(address registry, uint256 labelHash) public view returns(address resolver, uint96 flags) { + function getResolver(address registry, uint256 labelHash) public view returns (address resolver, uint96 flags) { uint256 data = resolvers[registry][labelHash & LABEL_HASH_MASK]; resolver = address(uint160(data)); flags = uint96(data >> 160); } - function getResolver(uint256 labelHash) external view returns(address resolver, uint96 flags) { + function getResolver(uint256 labelHash) external view returns (address resolver, uint96 flags) { return getResolver(msg.sender, labelHash); } diff --git a/contracts/src/registry/RootRegistry.sol b/contracts/src/registry/RootRegistry.sol index c5a19f3..6f03995 100644 --- a/contracts/src/registry/RootRegistry.sol +++ b/contracts/src/registry/RootRegistry.sol @@ -18,7 +18,7 @@ contract RootRegistry is BaseRegistry, AccessControl { _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); } - function uri(uint256 /*id*/) public override pure returns (string memory) { + function uri(uint256 /*id*/ ) public pure override returns (string memory) { return ""; } @@ -31,7 +31,7 @@ contract RootRegistry is BaseRegistry, AccessControl { emit NewSubname(label); } - function burn(uint256 tokenId) + function burn(uint256 tokenId) external onlyRole(SUBDOMAIN_ISSUER_ROLE) withSubregistryFlags(tokenId, SUBREGISTRY_FLAGS_MASK, 0) @@ -41,10 +41,7 @@ contract RootRegistry is BaseRegistry, AccessControl { datastore.setSubregistry(tokenId, address(0), 0); } - function lock(uint256 tokenId) - external - onlyRole(SUBDOMAIN_ISSUER_ROLE) - { + function lock(uint256 tokenId) external onlyRole(SUBDOMAIN_ISSUER_ROLE) { (address subregistry, uint96 flags) = datastore.getSubregistry(tokenId); datastore.setSubregistry(tokenId, subregistry, flags & SUBREGISTRY_FLAG_LOCKED); } @@ -58,12 +55,7 @@ contract RootRegistry is BaseRegistry, AccessControl { datastore.setSubregistry(tokenId, address(registry), flags); } - function supportsInterface(bytes4 interfaceId) - public - view - override(BaseRegistry, AccessControl) - returns (bool) - { + function supportsInterface(bytes4 interfaceId) public view override(BaseRegistry, AccessControl) returns (bool) { return super.supportsInterface(interfaceId); } } diff --git a/contracts/src/registry/UserRegistry.sol b/contracts/src/registry/UserRegistry.sol index 264cb27..0922f9a 100644 --- a/contracts/src/registry/UserRegistry.sol +++ b/contracts/src/registry/UserRegistry.sol @@ -17,11 +17,7 @@ contract UserRegistry is BaseRegistry { IRegistry public parent; string public label; - constructor( - IRegistry _parent, - string memory _label, - IRegistryDatastore _datastore - ) BaseRegistry(_datastore) { + constructor(IRegistry _parent, string memory _label, IRegistryDatastore _datastore) BaseRegistry(_datastore) { parent = _parent; label = _label; } @@ -34,32 +30,23 @@ contract UserRegistry is BaseRegistry { _; } - function uri(uint256 /*id*/) public override pure returns (string memory) { + function uri(uint256 /*id*/ ) public pure override returns (string memory) { return ""; } - function mint( - string calldata _label, - address owner, - IRegistry registry, - uint96 flags - ) external onlyNameOwner { + function mint(string calldata _label, address owner, IRegistry registry, uint96 flags) external onlyNameOwner { uint256 tokenId = uint256(keccak256(bytes(_label))); _mint(tokenId, owner, registry, flags); emit NewSubname(label); } - function burn( - uint256 tokenId - ) external onlyNameOwner withSubregistryFlags(tokenId, SUBREGISTRY_FLAG_LOCKED, 0) { + function burn(uint256 tokenId) external onlyNameOwner withSubregistryFlags(tokenId, SUBREGISTRY_FLAG_LOCKED, 0) { address owner = ownerOf(tokenId); _burn(owner, tokenId, 1); datastore.setSubregistry(tokenId, address(0), 0); } - function locked( - uint256 tokenId - ) external view returns (bool) { + function locked(uint256 tokenId) external view returns (bool) { (, uint96 flags) = datastore.getSubregistry(tokenId); return flags & SUBREGISTRY_FLAG_LOCKED != 0; } @@ -69,19 +56,16 @@ contract UserRegistry is BaseRegistry { datastore.setSubregistry(tokenId, subregistry, flags & SUBREGISTRY_FLAG_LOCKED); } - function setSubregistry( - uint256 tokenId, - IRegistry registry - ) external onlyTokenOwner(tokenId) withSubregistryFlags(tokenId, SUBREGISTRY_FLAG_LOCKED, 0) { + function setSubregistry(uint256 tokenId, IRegistry registry) + external + onlyTokenOwner(tokenId) + withSubregistryFlags(tokenId, SUBREGISTRY_FLAG_LOCKED, 0) + { (, uint96 flags) = datastore.getSubregistry(tokenId); datastore.setSubregistry(tokenId, address(registry), flags); } - function supportsInterface( - bytes4 interfaceId - ) public view override returns (bool) { - return - interfaceId == type(IRegistry).interfaceId || - super.supportsInterface(interfaceId); + function supportsInterface(bytes4 interfaceId) public view override returns (bool) { + return interfaceId == type(IRegistry).interfaceId || super.supportsInterface(interfaceId); } } diff --git a/contracts/src/utils/UniversalResolver.sol b/contracts/src/utils/UniversalResolver.sol index a7a83b3..9bb718f 100644 --- a/contracts/src/utils/UniversalResolver.sol +++ b/contracts/src/utils/UniversalResolver.sol @@ -18,9 +18,7 @@ contract UniversalResolver { * @return reg A registry responsible for the name. * @return exact A boolean that is true if the registry is an exact match for `name`. */ - function getRegistry( - bytes calldata name - ) public view returns (IRegistry reg, bool exact) { + function getRegistry(bytes calldata name) public view returns (IRegistry reg, bool exact) { uint8 len = uint8(name[0]); if (len == 0) { return (root, true); @@ -43,7 +41,7 @@ contract UniversalResolver { * @return The resolver responsible for this name, or `address(0)` if none. */ function getResolver(bytes calldata name) public view returns (address) { - (IRegistry reg, ) = getRegistry(name); + (IRegistry reg,) = getRegistry(name); uint8 len = uint8(name[0]); string memory label = string(name[1:len + 1]); return reg.getResolver(label); diff --git a/contracts/test/ETHRegistry.t.sol b/contracts/test/ETHRegistry.t.sol new file mode 100644 index 0000000..b8ec19d --- /dev/null +++ b/contracts/test/ETHRegistry.t.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.13; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; + +import "src/registry/ETHRegistry.sol"; +import "src/registry/RegistryDatastore.sol"; + +contract TestETHRegistry is Test, ERC1155Holder { + event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value); + + RegistryDatastore datastore; + ETHRegistry registry; + + function setUp() public { + datastore = new RegistryDatastore(); + registry = new ETHRegistry(datastore); + registry.grantRole(registry.REGISTRAR_ROLE(), address(this)); + } + + function test_register_unlocked() public { + uint256 expectedId = + uint256(keccak256("test2") & 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8); + vm.expectEmit(true, true, true, true); + emit TransferSingle(address(this), address(0), address(this), expectedId, 1); + + uint256 tokenId = registry.register("test2", address(this), registry, 0, uint64(block.timestamp) + 86400); + vm.assertEq(tokenId, expectedId); + } + + function test_register_locked() public { + uint32 flags = registry.FLAG_SUBREGISTRY_LOCKED() | registry.FLAG_RESOLVER_LOCKED(); + uint256 expectedId = + uint256(keccak256("test2") & 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8) | flags; + vm.expectEmit(true, true, true, true); + emit TransferSingle(address(this), address(0), address(this), expectedId, 1); + + uint256 tokenId = registry.register("test2", address(this), registry, flags, uint64(block.timestamp) + 86400); + vm.assertEq(tokenId, expectedId); + } + + function test_lock_name() public { + uint32 flags = registry.FLAG_SUBREGISTRY_LOCKED() | registry.FLAG_RESOLVER_LOCKED(); + uint256 oldTokenId = registry.register("test2", address(this), registry, 0, uint64(block.timestamp) + 86400); + + vm.expectEmit(true, true, true, true); + emit TransferSingle(address(this), address(this), address(0), oldTokenId, 1); + uint256 expectedTokenId = oldTokenId | flags; + vm.expectEmit(true, true, true, true); + emit TransferSingle(address(this), address(0), address(this), expectedTokenId, 1); + + uint256 newTokenId = registry.lock(oldTokenId, flags); + vm.assertEq(newTokenId, expectedTokenId); + } + + function test_cannot_unlock_name() public { + uint32 flags = registry.FLAG_SUBREGISTRY_LOCKED() | registry.FLAG_RESOLVER_LOCKED(); + + uint256 oldTokenId = registry.register("test2", address(this), registry, flags, uint64(block.timestamp) + 86400); + uint256 newTokenId = registry.lock(oldTokenId, 0); + vm.assertEq(oldTokenId, newTokenId); + } + + function testFail_cannot_mint_duplicates() public { + uint32 flags = registry.FLAG_SUBREGISTRY_LOCKED() | registry.FLAG_RESOLVER_LOCKED(); + + registry.register("test2", address(this), registry, flags, uint64(block.timestamp) + 86400); + registry.register("test2", address(this), registry, 0, uint64(block.timestamp) + 86400); + } +} diff --git a/contracts/test/ETHRegistry.t.ts b/contracts/test/ETHRegistry.t.ts deleted file mode 100644 index 37e0cd2..0000000 --- a/contracts/test/ETHRegistry.t.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { loadFixture } from "@nomicfoundation/hardhat-toolbox-viem/network-helpers.js"; -import { expect } from "chai"; -import { fromHex, getAddress, labelhash, zeroAddress } from "viem"; -import { deployEnsFixture, registerName } from "./fixtures/deployEnsFixture.js"; - -describe("ETHRegistry", function () { - it("registers names", async () => { - const { accounts, ethRegistry } = await loadFixture(deployEnsFixture); - - const tx = await registerName({ ethRegistry, label: "test2" }); - const expectedId = - fromHex(labelhash("test2"), "bigint") & - 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8n; - - await expect(ethRegistry) - .transaction(tx) - .toEmitEvent("TransferSingle") - .withArgs( - getAddress(accounts[0].address), - zeroAddress, - accounts[0].address, - expectedId, - 1n - ); - await expect( - ethRegistry.read.ownerOf([expectedId]) - ).resolves.toEqualAddress(accounts[0].address); - }); - - it("registers locked names", async () => { - const { accounts, ethRegistry } = await loadFixture(deployEnsFixture); - - const tx = await registerName({ - ethRegistry, - label: "test2", - subregistryLocked: true, - resolverLocked: true, - }); - const expectedId = - (fromHex(labelhash("test2"), "bigint") & - 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8n) | - 3n; - - await expect(ethRegistry) - .transaction(tx) - .toEmitEvent("TransferSingle") - .withArgs( - getAddress(accounts[0].address), - zeroAddress, - accounts[0].address, - expectedId, - 1n - ); - await expect( - ethRegistry.read.ownerOf([expectedId]) - ).resolves.toEqualAddress(accounts[0].address); - }); - - it("supports locking names", async () => { - const { accounts, ethRegistry } = await loadFixture(deployEnsFixture); - - await registerName({ ethRegistry, label: "test2" }); - const expectedId = - fromHex(labelhash("test2"), "bigint") & - 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8n; - - await expect( - ethRegistry.read.ownerOf([expectedId]) - ).resolves.toEqualAddress(accounts[0].address); - await expect( - ethRegistry.read.ownerOf([expectedId | 3n]) - ).resolves.toEqualAddress(zeroAddress); - await ethRegistry.write.lock([expectedId, 0x3]); - await expect( - ethRegistry.read.ownerOf([expectedId | 3n]) - ).resolves.toEqualAddress(accounts[0].address); - await expect( - ethRegistry.read.ownerOf([expectedId]) - ).resolves.toEqualAddress(zeroAddress); - }); - - it("cannot unlock names", async () => { - const { accounts, ethRegistry } = await loadFixture(deployEnsFixture); - - await registerName({ - ethRegistry, - label: "test2", - subregistryLocked: true, - resolverLocked: true, - }); - const expectedId = - (fromHex(labelhash("test2"), "bigint") & - 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8n) | - 3n; - - await expect( - ethRegistry.read.ownerOf([expectedId]) - ).resolves.toEqualAddress(accounts[0].address); - await expect( - ethRegistry.read.ownerOf([expectedId ^ 3n]) - ).resolves.toEqualAddress(zeroAddress); - - await ethRegistry.write.lock([expectedId, 0x0]); - - await expect( - ethRegistry.read.ownerOf([expectedId]) - ).resolves.toEqualAddress(accounts[0].address); - await expect( - ethRegistry.read.ownerOf([expectedId ^ 3n]) - ).resolves.toEqualAddress(zeroAddress); - }); -}); diff --git a/contracts/test/RegistryDatastore.t.sol b/contracts/test/RegistryDatastore.t.sol index 3c9d360..8a8d3c0 100644 --- a/contracts/test/RegistryDatastore.t.sol +++ b/contracts/test/RegistryDatastore.t.sol @@ -7,138 +7,138 @@ import "forge-std/console.sol"; import "src/registry/RegistryDatastore.sol"; contract TestETHRegistry is Test { - uint256 LABEL_HASH_MASK = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000; - uint256 labelHash = uint256(keccak256("test")); - RegistryDatastore datastore; - - function setUp() public { - datastore = new RegistryDatastore(); - } - - function test_GetSetSubregistry_MsgSender() public { - // set subregistry with empty flags - datastore.setSubregistry(labelHash, address(this), 0); - - address subregistry; - uint96 flags; - - (subregistry, flags) = datastore.getSubregistry(labelHash); - vm.assertEq(subregistry, address(this)); - vm.assertEq(flags, 0); - - (subregistry, flags) = datastore.getSubregistry(address(this), labelHash); - vm.assertEq(subregistry, address(this)); - vm.assertEq(flags, 0); - } - - function test_GetSetSubregistry_OtherRegistry() public { - DummyRegistry r = new DummyRegistry(datastore); - r.setSubregistry(labelHash, address(this), 0); - - address subregistry; - uint96 flags; - - (subregistry, flags) = datastore.getSubregistry(labelHash); - vm.assertEq(subregistry, address(0)); - vm.assertEq(flags, 0); - - (subregistry, flags) = datastore.getSubregistry(address(r), labelHash); - vm.assertEq(subregistry, address(this)); - vm.assertEq(flags, 0); - } - - function test_GetSetSubregistry_32BitCustomFlags() public { - datastore.setSubregistry(labelHash, address(this), 0x80000001); - - address subregistry; - uint96 flags; - - (subregistry, flags) = datastore.getSubregistry(labelHash); - vm.assertEq(subregistry, address(this)); - vm.assertEq(flags, 0x80000001); - - // get with flags on labelhash - (subregistry, flags) = datastore.getSubregistry((labelHash & LABEL_HASH_MASK) | 0x80000001); - vm.assertEq(subregistry, address(this)); - vm.assertEq(flags, 0x80000001); - } - - function test_GetSetSubregistry_96BitCustomFlags() public { - datastore.setSubregistry(labelHash, address(this), 0x800000000000000000000001); - - (address subregistry, uint96 flags) = datastore.getSubregistry(labelHash); - vm.assertEq(subregistry, address(this)); - vm.assertEq(flags, 0x800000000000000000000001); - } + uint256 LABEL_HASH_MASK = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000; + uint256 labelHash = uint256(keccak256("test")); + RegistryDatastore datastore; + + function setUp() public { + datastore = new RegistryDatastore(); + } + + function test_GetSetSubregistry_MsgSender() public { + // set subregistry with empty flags + datastore.setSubregistry(labelHash, address(this), 0); + + address subregistry; + uint96 flags; + + (subregistry, flags) = datastore.getSubregistry(labelHash); + vm.assertEq(subregistry, address(this)); + vm.assertEq(flags, 0); + + (subregistry, flags) = datastore.getSubregistry(address(this), labelHash); + vm.assertEq(subregistry, address(this)); + vm.assertEq(flags, 0); + } + + function test_GetSetSubregistry_OtherRegistry() public { + DummyRegistry r = new DummyRegistry(datastore); + r.setSubregistry(labelHash, address(this), 0); + + address subregistry; + uint96 flags; + + (subregistry, flags) = datastore.getSubregistry(labelHash); + vm.assertEq(subregistry, address(0)); + vm.assertEq(flags, 0); + + (subregistry, flags) = datastore.getSubregistry(address(r), labelHash); + vm.assertEq(subregistry, address(this)); + vm.assertEq(flags, 0); + } + + function test_GetSetSubregistry_32BitCustomFlags() public { + datastore.setSubregistry(labelHash, address(this), 0x80000001); + + address subregistry; + uint96 flags; + + (subregistry, flags) = datastore.getSubregistry(labelHash); + vm.assertEq(subregistry, address(this)); + vm.assertEq(flags, 0x80000001); + + // get with flags on labelhash + (subregistry, flags) = datastore.getSubregistry((labelHash & LABEL_HASH_MASK) | 0x80000001); + vm.assertEq(subregistry, address(this)); + vm.assertEq(flags, 0x80000001); + } + + function test_GetSetSubregistry_96BitCustomFlags() public { + datastore.setSubregistry(labelHash, address(this), 0x800000000000000000000001); + + (address subregistry, uint96 flags) = datastore.getSubregistry(labelHash); + vm.assertEq(subregistry, address(this)); + vm.assertEq(flags, 0x800000000000000000000001); + } function test_GetSetResolver_MsgSender() public { - datastore.setResolver(labelHash, address(this), 0); - - address resolver; - uint96 flags; - - (resolver, flags) = datastore.getResolver(labelHash); - vm.assertEq(resolver, address(this)); - vm.assertEq(flags, 0); - - (resolver, flags) = datastore.getResolver(address(this), labelHash); - vm.assertEq(resolver, address(this)); - vm.assertEq(flags, 0); - } - - function test_GetSetResolver_OtherRegistry() public { - DummyRegistry r = new DummyRegistry(datastore); - r.setResolver(labelHash, address(this), 0); - - address resolver; - uint96 flags; - - (resolver, flags) = datastore.getResolver(labelHash); - vm.assertEq(resolver, address(0)); - vm.assertEq(flags, 0); - - (resolver, flags) = datastore.getResolver(address(r), labelHash); - vm.assertEq(resolver, address(this)); - vm.assertEq(flags, 0); - } - - function test_GetSetResolver_32BitCustomFlags() public { - datastore.setResolver(labelHash, address(this), 0x80000001); - - address resolver; - uint96 flags; - - (resolver, flags) = datastore.getResolver(labelHash); - vm.assertEq(resolver, address(this)); - vm.assertEq(flags, 0x80000001); - - // get with flags on labelhash - (resolver, flags) = datastore.getResolver((labelHash & LABEL_HASH_MASK) | 0x80000001); - vm.assertEq(resolver, address(this)); - vm.assertEq(flags, 0x80000001); - } - - function test_GetSetResolver_96BitCustomFlags() public { - datastore.setResolver(labelHash, address(this), 0x800000000000000000000001); - - (address resolver, uint96 flags) = datastore.getResolver(labelHash); - vm.assertEq(resolver, address(this)); - vm.assertEq(flags, 0x800000000000000000000001); - } + datastore.setResolver(labelHash, address(this), 0); + + address resolver; + uint96 flags; + + (resolver, flags) = datastore.getResolver(labelHash); + vm.assertEq(resolver, address(this)); + vm.assertEq(flags, 0); + + (resolver, flags) = datastore.getResolver(address(this), labelHash); + vm.assertEq(resolver, address(this)); + vm.assertEq(flags, 0); + } + + function test_GetSetResolver_OtherRegistry() public { + DummyRegistry r = new DummyRegistry(datastore); + r.setResolver(labelHash, address(this), 0); + + address resolver; + uint96 flags; + + (resolver, flags) = datastore.getResolver(labelHash); + vm.assertEq(resolver, address(0)); + vm.assertEq(flags, 0); + + (resolver, flags) = datastore.getResolver(address(r), labelHash); + vm.assertEq(resolver, address(this)); + vm.assertEq(flags, 0); + } + + function test_GetSetResolver_32BitCustomFlags() public { + datastore.setResolver(labelHash, address(this), 0x80000001); + + address resolver; + uint96 flags; + + (resolver, flags) = datastore.getResolver(labelHash); + vm.assertEq(resolver, address(this)); + vm.assertEq(flags, 0x80000001); + + // get with flags on labelhash + (resolver, flags) = datastore.getResolver((labelHash & LABEL_HASH_MASK) | 0x80000001); + vm.assertEq(resolver, address(this)); + vm.assertEq(flags, 0x80000001); + } + + function test_GetSetResolver_96BitCustomFlags() public { + datastore.setResolver(labelHash, address(this), 0x800000000000000000000001); + + (address resolver, uint96 flags) = datastore.getResolver(labelHash); + vm.assertEq(resolver, address(this)); + vm.assertEq(flags, 0x800000000000000000000001); + } } contract DummyRegistry { - RegistryDatastore datastore; + RegistryDatastore datastore; - constructor(RegistryDatastore _datastore) { - datastore = _datastore; - } + constructor(RegistryDatastore _datastore) { + datastore = _datastore; + } - function setSubregistry(uint256 labelHash, address subregistry, uint96 flags) public { - datastore.setSubregistry(labelHash, subregistry, flags); - } + function setSubregistry(uint256 labelHash, address subregistry, uint96 flags) public { + datastore.setSubregistry(labelHash, subregistry, flags); + } - function setResolver(uint256 labelHash, address resolver, uint96 flags) public { - datastore.setResolver(labelHash, resolver, flags); - } -} \ No newline at end of file + function setResolver(uint256 labelHash, address resolver, uint96 flags) public { + datastore.setResolver(labelHash, resolver, flags); + } +} From e1bae67f27c59c09943eb4f2bf211f141da82e15 Mon Sep 17 00:00:00 2001 From: tate Date: Thu, 15 Aug 2024 08:44:09 +1000 Subject: [PATCH 10/10] labelhashmask test fix --- contracts/test/RegistryDatastore.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/test/RegistryDatastore.t.sol b/contracts/test/RegistryDatastore.t.sol index 8a8d3c0..c9d510b 100644 --- a/contracts/test/RegistryDatastore.t.sol +++ b/contracts/test/RegistryDatastore.t.sol @@ -7,7 +7,7 @@ import "forge-std/console.sol"; import "src/registry/RegistryDatastore.sol"; contract TestETHRegistry is Test { - uint256 LABEL_HASH_MASK = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000; + uint256 LABEL_HASH_MASK = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000; uint256 labelHash = uint256(keccak256("test")); RegistryDatastore datastore;