diff --git a/contracts/src/registry/BaseRegistry.sol b/contracts/src/registry/BaseRegistry.sol index aa904de..1acbbf1 100644 --- a/contracts/src/registry/BaseRegistry.sol +++ b/contracts/src/registry/BaseRegistry.sol @@ -62,11 +62,6 @@ abstract contract BaseRegistry is IRegistry, ERC1155Singleton { || interfaceId == type(IRegistry).interfaceId || super.supportsInterface(interfaceId); } - function _mint(uint256 tokenId, address owner, IRegistry registry, uint96 flags) internal { - _mint(owner, tokenId, 1, ""); - datastore.setSubregistry(tokenId, address(registry), flags); - } - /** * * IRegistry functions * diff --git a/contracts/src/registry/ETHRegistry.sol b/contracts/src/registry/ETHRegistry.sol index 17b897d..c723ae3 100644 --- a/contracts/src/registry/ETHRegistry.sol +++ b/contracts/src/registry/ETHRegistry.sol @@ -8,19 +8,16 @@ import {IERC1155Singleton} from "./IERC1155Singleton.sol"; import {IRegistry} from "./IRegistry.sol"; import {IRegistryDatastore} from "./IRegistryDatastore.sol"; import {BaseRegistry} from "./BaseRegistry.sol"; +import {LockableRegistry} from "./LockableRegistry.sol"; -contract ETHRegistry is BaseRegistry, AccessControl { +contract ETHRegistry is LockableRegistry, AccessControl { bytes32 public constant REGISTRAR_ROLE = keccak256("REGISTRAR_ROLE"); - 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(uint256 tokenId); error CannotReduceExpiration(uint64 oldExpiration, uint64 newExpiration); - constructor(IRegistryDatastore _datastore) BaseRegistry(_datastore) { + constructor(IRegistryDatastore _datastore) LockableRegistry(_datastore) { _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); } @@ -43,13 +40,13 @@ contract ETHRegistry is BaseRegistry, AccessControl { return super.ownerOf(tokenId); } - function register(string calldata label, address owner, IRegistry registry, uint32 flags, uint64 expires) + function register(string calldata label, address owner, IRegistry registry, uint96 flags, uint64 expires) public onlyRole(REGISTRAR_ROLE) returns (uint256 tokenId) { - flags &= FLAGS_MASK; tokenId = (uint256(keccak256(bytes(label))) & ~uint256(FLAGS_MASK)) | flags; + flags = (flags & FLAGS_MASK) | (uint96(expires) << 32); (, uint96 oldFlags) = datastore.getSubregistry(tokenId); uint64 oldExpiry = uint64(oldFlags >> 32); @@ -57,7 +54,8 @@ contract ETHRegistry is BaseRegistry, AccessControl { revert NameAlreadyRegistered(label); } - _mint(tokenId, owner, registry, uint96(flags) | (uint96(expires) << 32)); + _mint(owner, tokenId, 1, ""); + datastore.setSubregistry(tokenId, address(registry), flags); emit NewSubname(label); return tokenId; } @@ -79,42 +77,20 @@ contract ETHRegistry is BaseRegistry, AccessControl { return (uint64(_flags >> 32), uint32(_flags)); } - function lock(uint256 tokenId, uint32 flags) + function lock(uint256 tokenId, uint96 flags) 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) { + uint96 newFlags = _lock(tokenId, flags); + newTokenId = (tokenId & ~uint256(FLAGS_MASK)) | (newFlags & FLAGS_MASK); + if (tokenId != newTokenId) { address owner = ownerOf(tokenId); _burn(owner, tokenId, 1); - newTokenId = (tokenId & ~uint256(FLAGS_MASK)) | (newFlags & FLAGS_MASK); - _mint(newTokenId, owner, IRegistry(subregistry), newFlags); - } else { - newTokenId = tokenId; + _mint(owner, newTokenId, 1, ""); } } - function setSubregistry(uint256 tokenId, IRegistry registry) - external - onlyTokenOwner(tokenId) - withSubregistryFlags(tokenId, FLAG_SUBREGISTRY_LOCKED, 0) - { - (, 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) { return interfaceId == type(IRegistry).interfaceId || super.supportsInterface(interfaceId); } @@ -122,18 +98,20 @@ contract ETHRegistry is BaseRegistry, AccessControl { 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) { + if (expires <= block.timestamp) { return IRegistry(address(0)); } return IRegistry(subregistry); } function getResolver(string calldata label) external view virtual override returns (address) { - (address resolver, uint96 flags) = datastore.getResolver(uint256(keccak256(bytes(label)))); + (address subregistry, uint96 flags) = datastore.getSubregistry(uint256(keccak256(bytes(label)))); uint64 expires = uint64(flags); - if (expires >= block.timestamp) { + if (expires <= block.timestamp) { return address(0); } + + (address resolver, ) = datastore.getResolver(uint256(keccak256(bytes(label)))); return resolver; } } diff --git a/contracts/src/registry/LockableRegistry.sol b/contracts/src/registry/LockableRegistry.sol new file mode 100644 index 0000000..07d6a64 --- /dev/null +++ b/contracts/src/registry/LockableRegistry.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.13; + +import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; + +import {ERC1155Singleton} from "./ERC1155Singleton.sol"; +import {IERC1155Singleton} from "./IERC1155Singleton.sol"; +import {IRegistry} from "./IRegistry.sol"; +import {IRegistryDatastore} from "./IRegistryDatastore.sol"; +import {BaseRegistry} from "./BaseRegistry.sol"; + +abstract contract LockableRegistry is BaseRegistry { + uint96 public constant FLAGS_MASK = 0x7; + uint96 public constant FLAG_SUBREGISTRY_LOCKED = 0x1; + uint96 public constant FLAG_RESOLVER_LOCKED = 0x2; + uint96 public constant FLAG_FLAGS_LOCKED = 0x4; + + constructor(IRegistryDatastore _datastore) BaseRegistry(_datastore) { + } + + function _lock(uint256 tokenId, uint96 _flags) + internal + withSubregistryFlags(tokenId, FLAG_FLAGS_LOCKED, 0) + returns(uint96 newFlags) + { + (address subregistry, uint96 oldFlags) = datastore.getSubregistry(tokenId); + newFlags = oldFlags | (_flags & FLAGS_MASK); + if (newFlags != oldFlags) { + datastore.setSubregistry(tokenId, subregistry, newFlags); + } + } + + function setSubregistry(uint256 tokenId, IRegistry registry) + external + onlyTokenOwner(tokenId) + withSubregistryFlags(tokenId, FLAG_SUBREGISTRY_LOCKED, 0) + { + (, 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 flags(uint256 tokenId) external view returns(uint96) { + (, uint96 _flags) = datastore.getSubregistry(tokenId); + return _flags; + } +} diff --git a/contracts/src/registry/RootRegistry.sol b/contracts/src/registry/RootRegistry.sol index 6f03995..07ece49 100644 --- a/contracts/src/registry/RootRegistry.sol +++ b/contracts/src/registry/RootRegistry.sol @@ -7,14 +7,13 @@ import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import {IRegistry} from "./IRegistry.sol"; import {IRegistryDatastore} from "./IRegistryDatastore.sol"; +import {LockableRegistry} from "./LockableRegistry.sol"; import {BaseRegistry} from "./BaseRegistry.sol"; -contract RootRegistry is BaseRegistry, AccessControl { - bytes32 public constant SUBDOMAIN_ISSUER_ROLE = keccak256("SUBDOMAIN_ISSUER_ROLE"); - uint96 public constant SUBREGISTRY_FLAGS_MASK = 0x1; - uint96 public constant SUBREGISTRY_FLAG_LOCKED = 0x1; +contract RootRegistry is LockableRegistry, AccessControl { + bytes32 public constant TLD_ISSUER_ROLE = keccak256("TLD_ISSUER_ROLE"); - constructor(IRegistryDatastore _datastore) BaseRegistry(_datastore) { + constructor(IRegistryDatastore _datastore) LockableRegistry(_datastore) { _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); } @@ -22,37 +21,45 @@ contract RootRegistry is BaseRegistry, AccessControl { return ""; } - function mint(string calldata label, address owner, IRegistry registry, bool locked) + /** + * @dev Mints a new TLD. + * @param label The plaintext label for the TLD. + * @param owner The new owner of the TLD token. + * @param registry The address of the registry to use. + * @param flags Flags to set. + */ + function mint(string calldata label, address owner, IRegistry registry, uint96 flags) external - onlyRole(SUBDOMAIN_ISSUER_ROLE) + onlyRole(TLD_ISSUER_ROLE) + returns(uint256 tokenId) { - uint256 tokenId = uint256(keccak256(bytes(label))); - _mint(tokenId, owner, registry, locked ? SUBREGISTRY_FLAG_LOCKED : 0); + tokenId = uint256(keccak256(bytes(label))); + _mint(owner, tokenId, 1, ""); + datastore.setSubregistry(tokenId, address(registry), flags); emit NewSubname(label); } + /** + * @dev Burns a TLD. + * TLDs cannot be burned if any of their flags are set. + * @param tokenId The tokenID of the TLD to burn. + */ function burn(uint256 tokenId) external - onlyRole(SUBDOMAIN_ISSUER_ROLE) - withSubregistryFlags(tokenId, SUBREGISTRY_FLAGS_MASK, 0) + onlyTokenOwner(tokenId) + withSubregistryFlags(tokenId, FLAGS_MASK, 0) { address owner = ownerOf(tokenId); _burn(owner, tokenId, 1); datastore.setSubregistry(tokenId, address(0), 0); } - function lock(uint256 tokenId) external onlyRole(SUBDOMAIN_ISSUER_ROLE) { - (address subregistry, uint96 flags) = datastore.getSubregistry(tokenId); - datastore.setSubregistry(tokenId, subregistry, flags & SUBREGISTRY_FLAG_LOCKED); - } - - function setSubregistry(uint256 tokenId, IRegistry registry) + function lock(uint256 tokenId, uint96 flags) external onlyTokenOwner(tokenId) - withSubregistryFlags(tokenId, SUBREGISTRY_FLAGS_MASK, 0) + returns(uint96) { - (, uint96 flags) = datastore.getSubregistry(tokenId); - datastore.setSubregistry(tokenId, address(registry), flags); + return _lock(tokenId, flags); } function supportsInterface(bytes4 interfaceId) public view override(BaseRegistry, AccessControl) returns (bool) { diff --git a/contracts/src/registry/UserRegistry.sol b/contracts/src/registry/UserRegistry.sol index 0922f9a..f34e8c8 100644 --- a/contracts/src/registry/UserRegistry.sol +++ b/contracts/src/registry/UserRegistry.sol @@ -36,7 +36,8 @@ contract UserRegistry is BaseRegistry { function mint(string calldata _label, address owner, IRegistry registry, uint96 flags) external onlyNameOwner { uint256 tokenId = uint256(keccak256(bytes(_label))); - _mint(tokenId, owner, registry, flags); + _mint(owner, tokenId, 1, ""); + datastore.setSubregistry(tokenId, address(registry), flags); emit NewSubname(label); } diff --git a/contracts/test/ETHRegistry.t.sol b/contracts/test/ETHRegistry.t.sol index b8ec19d..57c5938 100644 --- a/contracts/test/ETHRegistry.t.sol +++ b/contracts/test/ETHRegistry.t.sol @@ -32,7 +32,7 @@ contract TestETHRegistry is Test, ERC1155Holder { } function test_register_locked() public { - uint32 flags = registry.FLAG_SUBREGISTRY_LOCKED() | registry.FLAG_RESOLVER_LOCKED(); + uint96 flags = registry.FLAG_SUBREGISTRY_LOCKED() | registry.FLAG_RESOLVER_LOCKED(); uint256 expectedId = uint256(keccak256("test2") & 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8) | flags; vm.expectEmit(true, true, true, true); @@ -43,7 +43,7 @@ contract TestETHRegistry is Test, ERC1155Holder { } function test_lock_name() public { - uint32 flags = registry.FLAG_SUBREGISTRY_LOCKED() | registry.FLAG_RESOLVER_LOCKED(); + uint96 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); @@ -57,7 +57,7 @@ contract TestETHRegistry is Test, ERC1155Holder { } function test_cannot_unlock_name() public { - uint32 flags = registry.FLAG_SUBREGISTRY_LOCKED() | registry.FLAG_RESOLVER_LOCKED(); + uint96 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); @@ -65,9 +65,39 @@ contract TestETHRegistry is Test, ERC1155Holder { } function testFail_cannot_mint_duplicates() public { - uint32 flags = registry.FLAG_SUBREGISTRY_LOCKED() | registry.FLAG_RESOLVER_LOCKED(); + uint96 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); } + + function test_set_subregistry() public { + uint256 tokenId = registry.register("test2", address(this), registry, 0, uint64(block.timestamp) + 86400); + registry.setSubregistry(tokenId, IRegistry(address(this))); + vm.assertEq(address(registry.getSubregistry("test2")), address(this)); + } + + function testFail_cannot_set_locked_subregistry() public { + uint96 flags = registry.FLAG_SUBREGISTRY_LOCKED(); + uint256 tokenId = registry.register("test2", address(this), registry, flags, uint64(block.timestamp) + 86400); + registry.setSubregistry(tokenId, IRegistry(address(this))); + } + + function test_set_resolver() public { + uint256 tokenId = registry.register("test2", address(this), registry, 0, uint64(block.timestamp) + 86400); + registry.setResolver(tokenId, address(this)); + vm.assertEq(address(registry.getResolver("test2")), address(this)); + } + + function testFail_cannot_set_locked_resolver() public { + uint96 flags = registry.FLAG_RESOLVER_LOCKED(); + uint256 tokenId = registry.register("test2", address(this), registry, flags, uint64(block.timestamp) + 86400); + registry.setResolver(tokenId, address(this)); + } + + function testFail_cannot_set_locked_flags() public { + uint96 flags = registry.FLAG_FLAGS_LOCKED(); + uint256 tokenId = registry.register("test2", address(this), registry, flags, uint64(block.timestamp) + 86400); + registry.lock(tokenId, registry.FLAG_RESOLVER_LOCKED()); + } } diff --git a/contracts/test/RootRegistry.t.sol b/contracts/test/RootRegistry.t.sol new file mode 100644 index 0000000..c6edad7 --- /dev/null +++ b/contracts/test/RootRegistry.t.sol @@ -0,0 +1,95 @@ +// 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/RootRegistry.sol"; +import "src/registry/RegistryDatastore.sol"; + +contract TestRootRegistry is Test, ERC1155Holder { + event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value); + + RegistryDatastore datastore; + RootRegistry registry; + + function setUp() public { + datastore = new RegistryDatastore(); + registry = new RootRegistry(datastore); + registry.grantRole(registry.TLD_ISSUER_ROLE(), address(this)); + } + + function test_register_unlocked() public { + uint256 expectedId = uint256(keccak256("test2")); + vm.expectEmit(true, true, true, true); + emit TransferSingle(address(this), address(0), address(this), expectedId, 1); + + uint256 tokenId = registry.mint("test2", address(this), registry, 0); + vm.assertEq(tokenId, expectedId); + uint96 flags = registry.flags(tokenId); + vm.assertEq(flags, 0); + } + + function test_register_locked() public { + uint96 flags = registry.FLAG_SUBREGISTRY_LOCKED() | registry.FLAG_RESOLVER_LOCKED(); + uint256 expectedId = uint256(keccak256("test2")); + vm.expectEmit(true, true, true, true); + emit TransferSingle(address(this), address(0), address(this), expectedId, 1); + + uint256 tokenId = registry.mint("test2", address(this), registry, flags); + vm.assertEq(tokenId, expectedId); + uint96 actualFlags = registry.flags(tokenId); + vm.assertEq(flags, actualFlags); + } + + function test_lock_name() public { + uint96 flags = registry.FLAG_SUBREGISTRY_LOCKED() | registry.FLAG_RESOLVER_LOCKED(); + uint256 tokenId = registry.mint("test2", address(this), registry, 0); + uint96 actualFlags = registry.lock(tokenId, flags); + vm.assertEq(flags, actualFlags); + uint96 actualFlags2 = registry.flags(tokenId); + vm.assertEq(flags, actualFlags2); + } + + function test_cannot_unlock_name() public { + uint96 flags = registry.FLAG_SUBREGISTRY_LOCKED() | registry.FLAG_RESOLVER_LOCKED(); + + uint256 tokenId = registry.mint("test2", address(this), registry, flags); + uint96 newFlags = registry.lock(tokenId, 0); + vm.assertEq(flags, newFlags); + uint96 newFlags2 = registry.flags(tokenId); + vm.assertEq(flags, newFlags2); + } + + function test_set_subregistry() public { + uint256 tokenId = registry.mint("test", address(this), registry, 0); + registry.setSubregistry(tokenId, IRegistry(address(this))); + vm.assertEq(address(registry.getSubregistry("test")), address(this)); + } + + function testFail_cannot_set_locked_subregistry() public { + uint96 flags = registry.FLAG_SUBREGISTRY_LOCKED(); + uint256 tokenId = registry.mint("test", address(this), registry, flags); + registry.setSubregistry(tokenId, IRegistry(address(this))); + } + + function test_set_resolver() public { + uint256 tokenId = registry.mint("test", address(this), registry, 0); + registry.setResolver(tokenId, address(this)); + vm.assertEq(address(registry.getResolver("test")), address(this)); + } + + function testFail_cannot_set_locked_resolver() public { + uint96 flags = registry.FLAG_RESOLVER_LOCKED(); + uint256 tokenId = registry.mint("test", address(this), registry, flags); + registry.setResolver(tokenId, address(this)); + } + + function testFail_cannot_set_locked_flags() public { + uint96 flags = registry.FLAG_FLAGS_LOCKED(); + uint256 tokenId = registry.mint("test", address(this), registry, flags); + registry.lock(tokenId, registry.FLAG_RESOLVER_LOCKED()); + } +} diff --git a/contracts/test/fixtures/deployEnsFixture.ts b/contracts/test/fixtures/deployEnsFixture.ts index 4061b63..3ba86da 100644 --- a/contracts/test/fixtures/deployEnsFixture.ts +++ b/contracts/test/fixtures/deployEnsFixture.ts @@ -19,7 +19,7 @@ export async function deployEnsFixture() { rootRegistry.address, ]); await rootRegistry.write.grantRole([ - keccak256(stringToHex("SUBDOMAIN_ISSUER_ROLE")), + keccak256(stringToHex("TLD_ISSUER_ROLE")), accounts[0].address, ]); await ethRegistry.write.grantRole([ @@ -30,7 +30,7 @@ export async function deployEnsFixture() { "eth", accounts[0].address, ethRegistry.address, - true, + 1n, ]); return { @@ -84,6 +84,6 @@ export const registerName = async ({ }) => { const owner = owner_ ?? (await hre.viem.getWalletClients())[0].account.address; - const flags = (subregistryLocked ? 1 : 0) | (resolverLocked ? 2 : 0); + const flags = (subregistryLocked ? 1n : 0n) | (resolverLocked ? 2n : 0n); return ethRegistry.write.register([label, owner, subregistry, flags, expiry]); };