diff --git a/.env.example b/.env.example index ad1339a3..736308a1 100644 --- a/.env.example +++ b/.env.example @@ -42,6 +42,12 @@ WHITELIST_OPERATOR=0xd908e5a6c88e9263f8fd0756bd0b77916008bb72 # RPC_URL=https://rpc.example.org RPC_URL=paseo_local +# Required. Bare TLD label the protocol registry is initialised with, without the +# leading dot (for example `dot`, `paseo`, or `test`). Distinct networks can share +# a chain id, so the TLD is set explicitly per deploy rather than derived. The +# deploy aborts if this is unset or empty, and no setter can correct it afterwards. +DOTNS_TLD=dot + # Optional. Manifest subdirectory under `deployments/`. Set it when two chains # share a chain id (for example a previewnet and a next environment both reach # 420420417 through the local adapter) so each keeps its own manifest. Defaults diff --git a/.github/workflows/deploy-contracts.yml b/.github/workflows/deploy-contracts.yml index edcb5317..52e4184c 100644 --- a/.github/workflows/deploy-contracts.yml +++ b/.github/workflows/deploy-contracts.yml @@ -32,6 +32,10 @@ jobs: env: ACCOUNT_NAME: anvil-default-7 ACCOUNT_PASSWORD: anvil-default-password + # Required by the deploy scripts (BaseDeployer.tldLabel). The TLD is passed + # to DotnsProtocolRegistry.initialize as init calldata, which does not affect + # any CREATE3 address, so the reproduction still lands the canonical set. + DOTNS_TLD: dot WHITELIST_OPERATOR: "0x14dC79964da2C08b23698B3D3cc7Ca32193d9955" FACTORY_DEPLOYER: "0xd498F7BC5bB3cBdd0068c3deEbbd814b69C3F164" PINNED_FACTORY: "0x8533c79E058c5a6489CAFeCA86dc600E029D75f5" diff --git a/contracts/escrow/DotnsNameEscrow.sol b/contracts/escrow/DotnsNameEscrow.sol index e7cd5071..cc9e5102 100644 --- a/contracts/escrow/DotnsNameEscrow.sol +++ b/contracts/escrow/DotnsNameEscrow.sol @@ -17,8 +17,8 @@ import {IDotnsProtocolRegistry} from "../registry/IDotnsProtocolRegistry.sol"; import {DotnsConstants} from "../utils/DotnsConstants.sol"; /// @title Dotns Name Escrow -/// @notice Holds refundable deposits for .dot names and manages the release/reclaim lifecycle. -/// @custom:security-contact admin@parity.io +/// @notice Holds refundable deposits for registered names and manages the release/reclaim +/// lifecycle. @custom:security-contact admin@parity.io contract DotnsNameEscrow is Initializable, UUPSUpgradeable, diff --git a/contracts/escrow/IDotnsNameEscrow.sol b/contracts/escrow/IDotnsNameEscrow.sol index 77e88af2..69623dc2 100644 --- a/contracts/escrow/IDotnsNameEscrow.sol +++ b/contracts/escrow/IDotnsNameEscrow.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.34; /// @title Dotns Name Escrow Interface -/// @notice Escrows refundable deposits for .dot registrations and manages the release lifecycle. +/// @notice Escrows refundable deposits for registered names and manages the release lifecycle. /// @custom:security-contact admin@parity.io interface IDotnsNameEscrow { /// @notice Parameters for recording a deposit position. diff --git a/contracts/registrars/DotnsPopController.sol b/contracts/registrars/DotnsPopController.sol index 444a6fb2..7baf162d 100644 --- a/contracts/registrars/DotnsPopController.sol +++ b/contracts/registrars/DotnsPopController.sol @@ -430,7 +430,7 @@ contract DotnsPopController is returns (address) { bytes32 labelhash = LabelUtils.labelhashMemory(label); - bytes32 node = LabelUtils.namehash(labelhash); + bytes32 node = LabelUtils.namehashUnder(protocolRegistry.tldNode(), labelhash); if (store == address(0)) { store = factory.deployLabelStoreFor(user); } @@ -651,7 +651,7 @@ contract DotnsPopController is /// @param label Bare DNS label (no TLD); the TLD is appended on write. function _writeRecord(address store, bytes32 node, string memory label) internal { if (ILabelStore(store).isLocked(node)) return; - ILabelStore(store).storeLabel(node, string.concat(label, DotnsConstants.TLD)); + ILabelStore(store).storeLabel(node, string.concat(label, protocolRegistry.tld())); } /// @notice Appends a deferred binding for `user` and adds them to the enumeration set. @@ -796,22 +796,22 @@ contract DotnsPopController is /// @notice Validates a lite-person `NAMEXX` label and derives `(labelhash, node)`. function _validateLiteLabel(string memory liteLabel) internal - pure + view returns (bytes32 labelhash, bytes32 node) { require(liteLabel.isLitePersonLabelMemory(), InvalidLiteLabel()); labelhash = LabelUtils.labelhashMemory(liteLabel); - node = LabelUtils.namehash(labelhash); + node = LabelUtils.namehashUnder(protocolRegistry.tldNode(), labelhash); } /// @notice Validates a base (full-person) DNS label and derives `(labelhash, node)`. function _validateBaseLabel(string calldata baseLabel) internal - pure + view returns (bytes32 labelhash, bytes32 node) { require(baseLabel.isSingleLabel(), InvalidBaseLabel()); - (labelhash, node) = LabelUtils.deriveNode(baseLabel); + (labelhash, node) = LabelUtils.deriveNode(protocolRegistry.tldNode(), baseLabel); } /// @notice Reverts when a non-empty chat key is not exactly `CHAT_KEY_LENGTH` bytes. diff --git a/contracts/registrars/DotnsRegistrar.sol b/contracts/registrars/DotnsRegistrar.sol index 11b0069e..98a80791 100644 --- a/contracts/registrars/DotnsRegistrar.sol +++ b/contracts/registrars/DotnsRegistrar.sol @@ -136,7 +136,7 @@ contract DotnsRegistrar is function labelOf(uint256 tokenId) external view override returns (string memory) { address holder = _ownerOf(tokenId); if (holder == address(0)) return ""; - return LabelUtils.stripDotTld(_readLabel(tokenId, holder)); + return LabelUtils.stripTld(protocolRegistry.tld(), _readLabel(tokenId, holder)); } /// @inheritdoc IDotnsRegistrar @@ -352,7 +352,7 @@ contract DotnsRegistrar is /// the registry would have already broken every other call site). function _writeOwnerLabel(address owner, uint256 tokenId, string calldata label) private { _storeFactory() - .writeLabel(owner, bytes32(tokenId), string.concat(label, DotnsConstants.TLD)); + .writeLabel(owner, bytes32(tokenId), string.concat(label, protocolRegistry.tld())); } /// @notice Quotes the friction fee required for a transfer. @@ -404,8 +404,11 @@ contract DotnsRegistrar is // No label means there is no label-derived price to charge against; treat as a zero-fee // move (typical of gateway-cold PoP mints that have not yet claimed a `LabelStore`). if (bytes(fullName).length == 0) return (0, 0); - string memory label = LabelUtils.stripDotTld(fullName); - if (bytes(label).length == 0) return (0, 0); + // A stored full name always carries the registry TLD suffix, so an empty strip means the + // name is malformed for this registry (a wrong or missing suffix); fail loudly rather than + // mis-pricing the move as zero-fee. + string memory label = LabelUtils.stripTld(registry.tld(), fullName); + require(bytes(label).length != 0, InvalidLabel()); reachFloor = IPopRules(registry.get(DotnsConstants.POP_RULES)).transferFloor(label, from, to); diff --git a/contracts/registrars/DotnsRegistrarController.sol b/contracts/registrars/DotnsRegistrarController.sol index d5fade58..9a56f6e0 100644 --- a/contracts/registrars/DotnsRegistrarController.sol +++ b/contracts/registrars/DotnsRegistrarController.sol @@ -22,12 +22,12 @@ import {RegistrationUtils} from "../utils/RegistrationUtils.sol"; import {StoreUtils} from "../utils/StoreUtils.sol"; /// @title Dotns Registrar Controller -/// @notice Allocates .dot labels using a commit reveal scheme. +/// @notice Allocates top-level labels using a commit reveal scheme. /// @dev Orchestrates allocation, PoP validation, pricing enforcement, forward registry /// wiring, default reverse resolution, and immutable store writing. /// /// Tokenisation: the minted ERC721 tokenId is `uint256(node)`, where -/// `node = namehash(DOT_NODE, labelhash)`. The registry stores a sentinel owner +/// `node = namehash(tldNode, labelhash)`. The registry stores a sentinel owner /// (`address(0)`) for tokenised nodes and derives ownership from the ERC721 registrar for /// authorisation. /// @custom:security-contact admin@parity.io @@ -324,12 +324,12 @@ contract DotnsRegistrarController is /// the policy minimum" from "shape-valid but already minted". function _validatedLabelNode(string calldata label) internal - pure + view returns (bytes32 labelhash, bytes32 node) { require(label.isSingleLabel(), InvalidLabel()); require(bytes(label).length >= 3, LabelTooShort(label)); - (labelhash, node) = LabelUtils.deriveNode(label); + (labelhash, node) = LabelUtils.deriveNode(protocolRegistry.tldNode(), label); } function _requireAvailableLabel(string calldata label) @@ -395,13 +395,13 @@ contract DotnsRegistrarController is // @custom:function register) so the registry's `ownerOf` check sees the new holder. IStoreFactory factory = IStoreFactory(protocolRegistry.get(DotnsConstants.STORE_FACTORY)); - string memory fullName = string.concat(registration.label, DotnsConstants.TLD); + string memory fullName = string.concat(registration.label, protocolRegistry.tld()); labelStore = factory.writeLabel(registration.owner, node, fullName); } if (setReverseRecord) { reverse.setReverseName( - registration.owner, string.concat(registration.label, DotnsConstants.TLD) + registration.owner, string.concat(registration.label, protocolRegistry.tld()) ); } diff --git a/contracts/registrars/IDotnsRegistrarController.sol b/contracts/registrars/IDotnsRegistrarController.sol index 7bd1121d..dacaf6d8 100644 --- a/contracts/registrars/IDotnsRegistrarController.sol +++ b/contracts/registrars/IDotnsRegistrarController.sol @@ -5,7 +5,7 @@ import {IDotnsController} from "./IDotnsController.sol"; import {IPopRules} from "../pop/IPopRules.sol"; /// @title Dotns Registrar Controller -/// @notice Interface for registering .dot labels using a commit reveal scheme. +/// @notice Interface for registering top-level labels using a commit reveal scheme. /// @dev Defines allocation only; forward resolution, reverse lookup, pricing mechanics, PoP /// validation, and store writing are handled by external contracts. Users commit a hash of /// registration parameters and, after a minimum delay, reveal the same parameters to register. diff --git a/contracts/registry/DotnsProtocolRegistry.sol b/contracts/registry/DotnsProtocolRegistry.sol index 46dd697f..3d08753f 100644 --- a/contracts/registry/DotnsProtocolRegistry.sol +++ b/contracts/registry/DotnsProtocolRegistry.sol @@ -8,13 +8,18 @@ import { } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import {IDotnsProtocolRegistry} from "./IDotnsProtocolRegistry.sol"; +import {LabelUtils} from "../utils/LabelUtils.sol"; +import {StringUtils} from "../utils/StringUtils.sol"; /// @title Dotns Protocol Registry /// @author Parity -/// @notice Upgradeable address registry for all DotNS protocol contracts. +/// @notice Upgradeable address registry for all DotNS protocol contracts, and the authority for +/// the network's top-level domain. /// @dev Single source of truth for sibling-contract lookups. All siblings resolve each other via /// well-known `bytes32` constants in `DotnsConstants` rather than holding direct addresses, -/// so an upgrade or rewire only mutates this contract. +/// so an upgrade or rewire only mutates this contract. The TLD node and suffix are set once +/// at initialisation and read live by every consumer, so a network runs one TLD without +/// recompiling its contracts. /// @custom:security-contact admin@parity.io contract DotnsProtocolRegistry is Initializable, @@ -22,6 +27,8 @@ contract DotnsProtocolRegistry is OwnableUpgradeable, IDotnsProtocolRegistry { + using StringUtils for string; + /// @notice Address stored for each well-known protocol key. mapping(bytes32 key => address addr) private _addresses; @@ -30,6 +37,12 @@ contract DotnsProtocolRegistry is /// multiple keys without being treated as deregistered when only one key is rewired. mapping(address addr => uint256 refcount) private _registeredRefcount; + /// @notice Namehash of the TLD node, `namehash(0, keccak256(bytes(tldLabel)))`. + bytes32 private _tldNode; + + /// @notice TLD suffix including the leading dot, e.g. `.dot`. + string private _tld; + uint256[50] private __gap; /// @custom:oz-upgrades-unsafe-allow constructor @@ -37,11 +50,19 @@ contract DotnsProtocolRegistry is _disableInitializers(); } - /// @notice Initialises the protocol registry. + /// @notice Initialises the protocol registry and fixes the network's TLD. /// @dev Callable exactly once via `Initializable`, otherwise - /// @custom:reverts InvalidInitialization. Sets the deployer as owner. - function initialize() external initializer { + /// @custom:reverts InvalidInitialization. Sets the deployer as owner. `tldLabel` is the + /// bare label without a dot (e.g. `dot`, `paseo`); it must be a single DNS label, + /// otherwise @custom:reverts InvalidTld. The TLD is fixed here because changing it after + /// names exist would reroot every node. + /// @param tldLabel Bare TLD label, without the leading dot. + function initialize(string calldata tldLabel) external initializer { __Ownable_init(msg.sender); + + require(tldLabel.isSingleLabel(), InvalidTld()); + _tldNode = LabelUtils.namehashUnder(bytes32(0), LabelUtils.labelhash(tldLabel)); + _tld = string.concat(".", tldLabel); } /// @inheritdoc IDotnsProtocolRegistry @@ -70,6 +91,16 @@ contract DotnsProtocolRegistry is return addr != address(0) && _registeredRefcount[addr] > 0; } + /// @inheritdoc IDotnsProtocolRegistry + function tldNode() external view override returns (bytes32 node) { + return _tldNode; + } + + /// @inheritdoc IDotnsProtocolRegistry + function tld() external view override returns (string memory suffix) { + return _tld; + } + /// @notice Returns implementation version. /// @return versionString Current version string. function version() external pure virtual returns (string memory versionString) { diff --git a/contracts/registry/DotnsRegistry.sol b/contracts/registry/DotnsRegistry.sol index b2e535e5..0d50a4ae 100644 --- a/contracts/registry/DotnsRegistry.sol +++ b/contracts/registry/DotnsRegistry.sol @@ -101,12 +101,13 @@ contract DotnsRegistry is Initializable, UUPSUpgradeable, OwnableUpgradeable, ID if (newOwner != previousOwner) { string memory fullName = - string.concat(subLabel, ".", parentLabel, DotnsConstants.TLD); + string.concat(subLabel, ".", parentLabel, protocolRegistry.tld()); _writeSubnodeToStore(newOwner, subnode, fullName); } } else { records[subnode] = Record({owner: newOwner, resolver: reverseResolver, exists: true}); - string memory fullName = string.concat(subLabel, ".", parentLabel, DotnsConstants.TLD); + string memory fullName = + string.concat(subLabel, ".", parentLabel, protocolRegistry.tld()); _writeSubnodeToStore(newOwner, subnode, fullName); } @@ -209,15 +210,16 @@ contract DotnsRegistry is Initializable, UUPSUpgradeable, OwnableUpgradeable, ID factory.writeLabel(storeOwner, node, fullName); } - /// @notice Computes the namehash of `parentLabel` rooted at the configured TLD. + /// @notice Computes the namehash of `parentLabel` rooted at the network's TLD node. /// @dev Walks the label right-to-left in calldata using memory-safe assembly to avoid the /// cost of slicing into intermediate `bytes` and to keep gas linear in the label depth. - function _parentNamehash(string calldata parentLabel) internal pure returns (bytes32 node) { + /// Reads the TLD node from the protocol registry, so it is a view rather than pure. + function _parentNamehash(string calldata parentLabel) internal view returns (bytes32 node) { bytes calldata labels = bytes(parentLabel); uint256 end = labels.length; require(end != 0, ParentLabelMismatch()); - node = DotnsConstants.DOT_NODE; + node = protocolRegistry.tldNode(); while (true) { uint256 start = end; diff --git a/contracts/registry/IDotnsProtocolRegistry.sol b/contracts/registry/IDotnsProtocolRegistry.sol index f0f4c5e7..55208cc8 100644 --- a/contracts/registry/IDotnsProtocolRegistry.sol +++ b/contracts/registry/IDotnsProtocolRegistry.sol @@ -6,7 +6,8 @@ pragma solidity ^0.8.34; /// @notice Interface for the DotNS protocol-level address registry. /// @dev Single source of truth for sibling lookups. Contracts resolve each other via well-known /// `bytes32` constants in `DotnsConstants` so an upgrade or rewire only mutates the -/// registry, never the consumers. +/// registry, never the consumers. The registry also holds the network's top-level domain, +/// so every consumer reads one TLD rather than compiling its own. /// @custom:security-contact admin@parity.io interface IDotnsProtocolRegistry { /// @notice Emitted when a protocol address is set or updated. @@ -15,6 +16,9 @@ interface IDotnsProtocolRegistry { /// @notice Thrown when a zero address is provided where one is not allowed. error ZeroAddress(); + /// @notice Thrown when the TLD label supplied at initialisation is not a single DNS label. + error InvalidTld(); + /// @notice Returns the address stored for a given key. /// @dev Returns `address(0)` when the key is unset; callers must validate when non-zero is /// required. @@ -35,4 +39,13 @@ interface IDotnsProtocolRegistry { /// registered return true. Treats `address(0)` as never registered regardless of /// refcount. function isRegisteredAddress(address addr) external view returns (bool registered); + + /// @notice Returns the namehash of the network's TLD node. + /// @dev `namehash(0, keccak256(bytes(tldLabel)))`, fixed at initialisation. Consumers use it + /// as the root parent when deriving a name's node. + function tldNode() external view returns (bytes32 node); + + /// @notice Returns the network's TLD suffix, including the leading dot (e.g. `.dot`). + /// @dev Fixed at initialisation. Consumers append it when rendering a label as a full name. + function tld() external view returns (string memory suffix); } diff --git a/contracts/registry/IDotnsRegistry.sol b/contracts/registry/IDotnsRegistry.sol index d8288731..7979744f 100644 --- a/contracts/registry/IDotnsRegistry.sol +++ b/contracts/registry/IDotnsRegistry.sol @@ -10,7 +10,7 @@ pragma solidity ^0.8.34; interface IDotnsRegistry { /// @notice Record describing a subnode creation request. /// @param subLabel Human readable subnode label e.g "alice". - /// @param parentLabel Canonical parent name without the `.dot` suffix e.g. bob or child.bob. + /// @param parentLabel Canonical parent name without the TLD suffix e.g. bob or child.bob. /// @param owner Address to assign as owner of the created subnode. struct SubnodeRecord { bytes32 parentNode; @@ -60,7 +60,7 @@ interface IDotnsRegistry { /// @notice Record describing a subnode resolver update request. /// @param subLabel Human-readable subnode label e.g "alice". - /// @param parentLabel Canonical parent name without `.dot` suffix e.g bob or child.bob. + /// @param parentLabel Canonical parent name without the TLD suffix e.g bob or child.bob. /// @param resolver Resolver contract address (zero clears). struct SubnodeResolverRecord { bytes32 parentNode; diff --git a/contracts/resolvers/DotnsReverseResolver.sol b/contracts/resolvers/DotnsReverseResolver.sol index 48763b45..3d3beeff 100644 --- a/contracts/resolvers/DotnsReverseResolver.sol +++ b/contracts/resolvers/DotnsReverseResolver.sol @@ -16,7 +16,7 @@ import {DotnsConstants} from "../utils/DotnsConstants.sol"; import {LabelUtils} from "../utils/LabelUtils.sol"; /// @title Dotns Reverse Resolver -/// @notice Resolves an address to its associated .dot name. +/// @notice Resolves an address to its associated name under the network TLD. /// @dev Writes are gated on a fixed writer address resolved from the protocol /// registry (the registrar or its controller), not on node ownership. /// Reverse records bind to an EOA rather than a registry node, so authority @@ -72,12 +72,12 @@ contract DotnsReverseResolver is /// @inheritdoc IDotnsReverseResolver function claimReverseRecord(string calldata label) external override { bytes32 labelhash = LabelUtils.labelhash(label); - uint256 tokenId = uint256(LabelUtils.namehash(labelhash)); + uint256 tokenId = uint256(LabelUtils.namehashUnder(protocolRegistry.tldNode(), labelhash)); IERC721 registrar = IERC721(protocolRegistry.get(DotnsConstants.REGISTRAR)); require(registrar.ownerOf(tokenId) == msg.sender, NotNameOwner(msg.sender, tokenId)); - string memory fullName = string.concat(label, DotnsConstants.TLD); + string memory fullName = string.concat(label, protocolRegistry.tld()); reverseNames[msg.sender] = fullName; emit ReverseNameSet(msg.sender, fullName); } @@ -87,13 +87,13 @@ contract DotnsReverseResolver is string memory stored = reverseNames[addr]; if (bytes(stored).length == 0) return ""; - // Strip the `.dot` suffix and validate against current ownership so a transferred-away + // Strip the TLD suffix and validate against current ownership so a transferred-away // name never resolves under a stale reverse record. - string memory label = LabelUtils.stripDotTld(stored); + string memory label = LabelUtils.stripTld(protocolRegistry.tld(), stored); if (bytes(label).length == 0) return ""; bytes32 labelhash = LabelUtils.labelhashMemory(label); - uint256 tokenId = uint256(LabelUtils.namehash(labelhash)); + uint256 tokenId = uint256(LabelUtils.namehashUnder(protocolRegistry.tldNode(), labelhash)); IERC721 registrar = IERC721(protocolRegistry.get(DotnsConstants.REGISTRAR)); try registrar.ownerOf(tokenId) returns (address currentOwner) { diff --git a/contracts/resolvers/IDotnsReverseResolver.sol b/contracts/resolvers/IDotnsReverseResolver.sol index d1e87925..3920624b 100644 --- a/contracts/resolvers/IDotnsReverseResolver.sol +++ b/contracts/resolvers/IDotnsReverseResolver.sol @@ -32,13 +32,13 @@ interface IDotnsReverseResolver { /// @param name The human-readable name associated with the address. function setReverseName(address addr, string calldata name) external; - /// @notice Self-service claim: associates `msg.sender` with `