You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Feat]: Name reservation contract with a bounded request window #203
Add a dedicated contract that records a reservation from a username to a claimant address, accepts claims only inside a governance-set window, and exposes the reservation for every consumer to read. This replaces the address whitelist as the way reserved names are held. Part of #202.
Proposal
Store a reservation per label: claimant address, status (requested, approved, revoked), and timestamps. PopRules already proves the shape with its reservations mapping of base name to {owner, expires}; decide whether to reuse that mapping or hold state in the new contract and have the rules layer read it.
Request window: requestOpen and requestClose, set by governance. Claims submitted outside the window revert; after close the contract accepts no new requests.
Lifecycle: submit a claim, approve, revoke. Approve and revoke are restricted to the governance track and operators (see the governance-track sub-issue).
Views and events: reservationOf(label) returns (address claimant, uint8 status, uint64 requestedAt) and isReserved(label), plus events on submit, approve and revoke, so the PoP gateway and other off-chain consumers can index without scanning logs.
Register the contract in the protocol registry under a new key such as NAME_RESERVATION, so no consumer hard-codes its address.
Acceptance criteria
A claim can be submitted only while the window is open; submissions outside it revert.
Governance can set and change the open and close times.
reservationOf and isReserved are public and return current state.
Submit, approve and revoke each emit an event.
The contract is resolvable through the protocol registry.
Tests and the paired fork test cover the window boundaries and each lifecycle transition.
Scope: this issue (#203) for the contract, #204 for registration enforcement, under the #202 tracking issue. Governance authority follows #205 and #238.
Purpose
A dedicated contract holds pre-launch name reservations. Both registrar controllers check it before they mint a name. It binds a label to the one address allowed to register it, tracks each name's status of requested, accepted or rejected, and lets anyone read the full list on-chain. The controllers only read it and store nothing themselves.
Contract
DotnsNameWhitelist runs behind its own UUPS proxy and inherits DotnsRoleManager, which gives it the owner and operator-role checks. Entries are keyed by node, the namehash of the label under the active TLD. Callers never pass a hash. Every function takes the bare label, and the contract derives the node from the label and the TLD in the protocol registry, the same way the controllers do, so a wrong hash cannot get in.
Each entry stores the grantee, when it was requested, when it was decided, the status, and the label kept so the list can be read on-chain. GrantStatus is None, Requested, Accepted or Rejected. None is the zero value, so a missing entry does not look like a live one. Only Accepted reserves a name.
Lifecycle and authority
A user calls requestName to request a label for themselves. An operator or the owner then calls accept or reject. Operators can also grant names directly with grantName, or in bulk with the admin-only grantNames. revokeName clears any entry. The owner is governance. Operators hold WHITELIST_OPERATOR_ROLE, which the owner grants and revokes, and the owner can do anything an operator can.
Governance: Root and the Whitelist pallet
Governance calls the contract by sending pallet_revive::call at it. A contract call accepts only Root or a signed account, so governance comes in as Root. Root has no address, and reading msg.sender under Root reverts, so the governance-only functions, upgrades and role changes, check ISystem(REVIVE_SYSTEM).originIsRoot() under #238. We use originIsRoot, not callerIsRoot, because it reads the transaction's origin and still returns true through the UUPS proxy's delegatecall, where callerIsRoot returns false.
There are two ways to produce that Root origin. Upgrades and registry changes go through the standard Root referendum track: slow, with the highest approval bar. That fits actions that can replace an implementation or change the registry. Operator changes and setting the request window go through the substrate Whitelist pallet on a short track. A vouching body such as the Fellowship whitelists the exact pallet_revive::call hash, then a fast referendum on the Whitelisted Caller track dispatches it as Root. The whitelisted call is single-use, and the pallet re-checks it at dispatch, so a revoke cancels a pending call. This lets operator changes happen within a day, still by a token-holder vote and still as Root, without the multi-week track.
A custom governance origin cannot call the contract directly, because pallet_revive takes only Root or a signed account. That is why the fast path goes through the Whitelist pallet, which re-dispatches the call as Root. This works whether or not the Asset Hub Migration has moved OpenGov onto Polkadot Asset Hub: called locally, or over XCM from the Relay, the contract still sees Root.
Operators are ordinary signed accounts with a fixed address, so their day-to-day accept, reject and grant calls need no referendum. The split is deliberate. The dangerous actions have no private key that can trigger them, only governance can. Operators hold a key, but they can only accept, reject, grant and revoke, and governance can undo any of it.
Registration enforcement
Both controllers check the whitelist everywhere they mint a name: the public path, the PoP lite mint and the PoP full mint. Before minting label L for registrant R, a controller reads the accepted grantee. If the label is granted to someone else, it refuses. Otherwise it checks L against the shared lite or full rules, mints, and if R is the grantee it calls consume, which removes the grant. A grant stays until R registers, and consume is the only way a controller can remove one. The whitelist reserves exact labels and PopRules reserves stems, so a PoP full mint must pass both.
Central rules
One helper, NameRules, holds the lite and full name rules, and both controllers call it. Today those checks, label shape and classification, are copied in both controllers. The whitelist does not classify names itself.
Open items
The request window (requestOpen, requestClose, set by governance) is required by this issue and not built yet. It also stops anyone spamming requestName to fill storage. revokeName deletes the entry today; we should decide whether to keep it as a Revoked status so the history stays. NameRules should be a library, not a contract. The names need to match this issue, which asks for a NAME_RESERVATION key and reservationOf/isReserved.
Interface
// SPDX-License-Identifier: MITpragma solidity^0.8.34;
/// @title IDotnsNameWhitelist/// @notice Interface for the pre-launch name whitelist that binds a name to the single address/// permitted to register it, tracking each name from request to decision./// @dev The contract never accepts a caller-supplied hash. Every entry point takes the bare/// label and derives the node itself from the TLD held in the protocol registry, the same/// derivation the controllers use, so a malformed or mismatched hash cannot be smuggled in./// Every entry keeps its bare label, request and decision timestamps, and status, and the/// node set is enumerable, so the whole whitelist is reviewable on-chain and by event log./// Operator appointment and removal, and upgrades, are owner-gated through/// @custom:contract DotnsRoleManager./// @custom:security-contact admin@parity.iointerfaceIDotnsNameWhitelist {
/// @notice Lifecycle status of a whitelist entry./// @dev `None` is the zero-value default of an absent entry, so a missing node reads as `None`/// rather than as a live status. `Accepted` is the only status the controllers admit for/// registration; `Requested` and `Rejected` do not reserve the name.enum GrantStatus {
None,
Requested,
Accepted,
Rejected
}
/// @notice A whitelist entry and its request-to-decision lifecycle./// @dev `grantee`, `requestedAt` and `status` co-locate in one storage slot (20 + 8 + 1/// bytes); `decidedAt` spills to the next; the dynamic `label` is stored separately./// @param grantee Address permitted to register the name once accepted./// @param requestedAt Timestamp the entry was requested./// @param status Lifecycle status; see GrantStatus./// @param decidedAt Timestamp the entry was accepted or rejected; zero while `Requested`./// @param label Bare label, kept for on-chain review.struct Grant {
address grantee;
uint64 requestedAt;
GrantStatus status;
uint64 decidedAt;
string label;
}
/// @notice Emitted when a name is requested./// @param node Namehash of the label under the active TLD./// @param grantee Address that requested the name./// @param label Bare label requested.event NameRequested(bytes32indexednode, addressindexedgrantee, stringlabel);
/// @notice Emitted when a request is accepted, including an operator direct grant./// @param node Namehash of the label under the active TLD./// @param grantee Address permitted to register the name./// @param label Bare label accepted.event NameAccepted(bytes32indexednode, addressindexedgrantee, stringlabel);
/// @notice Emitted when a request is rejected./// @param node Namehash of the label under the active TLD./// @param grantee Address whose request was rejected./// @param label Bare label rejected.event NameRejected(bytes32indexednode, addressindexedgrantee, stringlabel);
/// @notice Emitted when an entry is cleared./// @param node Namehash of the label under the active TLD./// @param grantee Address whose entry was cleared./// @param label Bare label cleared.event NameRevoked(bytes32indexednode, addressindexedgrantee, stringlabel);
/// @notice Emitted when a grantee registers their name and the entry is consumed./// @param node Namehash of the label under the active TLD./// @param grantee Address that registered the name./// @param label Bare label consumed.event NameConsumed(bytes32indexednode, addressindexedgrantee, stringlabel);
/// @notice Thrown when a grant is issued to the zero address.error ZeroGrantee();
/// @notice Thrown when a label is not a canonical single DNS label.error InvalidLabel();
/// @notice Thrown when requesting or granting a name that already has a live entry./// @param node Namehash of the label under the active TLD.error AlreadyExists(bytes32node);
/// @notice Thrown when accepting or rejecting a name that is not in the `Requested` status./// @param node Namehash of the label under the active TLD.error NotRequested(bytes32node);
/// @notice Thrown when clearing a name that holds no entry./// @param node Namehash of the label under the active TLD.error NotGranted(bytes32node);
/// @notice Thrown when `consume` is called by any address other than a registrar controller./// @param caller Rejected caller.error NotController(addresscaller);
/// @notice Thrown when `consume` is called for a name not accepted for the registrant./// @param registrant Address attempting to register the name./// @param node Namehash of the label under the active TLD.error NotGrantee(addressregistrant, bytes32node);
/// @notice Requests `label` for the caller./// @dev Records a `Requested` entry bound to the caller. Reverts with/// @custom:reverts AlreadyExists when the name already has a live entry and with/// @custom:reverts InvalidLabel when `label` is not a canonical single label. Emits/// @custom:emits NameRequested./// @param label Bare label to request.function requestName(stringcalldatalabel) external;
/// @notice Accepts the pending request on `label`./// @dev Restricted to an operator or the owner. Moves a `Requested` entry to `Accepted` and/// stamps the decision. Reverts with @custom:reverts NotRequested when the name is not/// pending. Emits @custom:emits NameAccepted./// @param label Bare label to accept.function accept(stringcalldatalabel) external;
/// @notice Rejects the pending request on `label`./// @dev Restricted to an operator or the owner. Moves a `Requested` entry to `Rejected` and/// stamps the decision; the entry is kept for review. Reverts with/// @custom:reverts NotRequested when the name is not pending. Emits/// @custom:emits NameRejected./// @param label Bare label to reject.function reject(stringcalldatalabel) external;
/// @notice Grants `label` to `grantee` directly, without a prior request./// @dev Restricted to an operator or the owner. Writes an `Accepted` entry with the request/// and decision timestamps set to now, for provisioning names to a chosen address./// Reverts with @custom:reverts AlreadyExists when the name already has a live entry,/// with @custom:reverts ZeroGrantee on a zero grantee, and with/// @custom:reverts InvalidLabel when `label` is not a canonical single label. Emits/// @custom:emits NameAccepted./// @param label Bare label to grant./// @param grantee Address permitted to register the name.function grantName(stringcalldatalabel, addressgrantee) external;
/// @notice Grants several labels to one `grantee` directly./// @dev Restricted to an operator or the owner. Applies the same rules as/// @custom:function grantName to each entry./// @param labels Bare labels to grant./// @param grantee Address permitted to register each name.function grantNames(string[] calldatalabels, addressgrantee) external;
/// @notice Clears the entry on `label`, whatever its status./// @dev Restricted to an operator or the owner. Reverts with @custom:reverts NotGranted when/// the name holds no entry. Emits @custom:emits NameRevoked./// @param label Bare label to clear.function revokeName(stringcalldatalabel) external;
/// @notice Removes the accepted grant on `label` as `registrant` registers it./// @dev Restricted to the registrar controllers resolved through the protocol registry, so/// the entry is consumed exactly when its grantee registers the name. Reverts with/// @custom:reverts NotController for any other caller and @custom:reverts NotGrantee when/// `label` is not accepted for `registrant`. Emits @custom:emits NameConsumed./// @param label Bare label being registered./// @param registrant Address registering the name.function consume(stringcalldatalabel, addressregistrant) external;
/// @notice Returns the address `label` is accepted for, or the zero address otherwise./// @dev Non-zero only for an `Accepted` entry, so a pending or rejected name does not reserve./// @param label Bare label to look up./// @return grantee Address permitted to register the name.function granteeOf(stringcalldatalabel) externalviewreturns (addressgrantee);
/// @notice Returns whether `account` holds an accepted grant for `label`./// @dev The pair check the controllers use to admit a registrant. False for the zero address./// @param label Bare label to look up./// @param account Address to test against the grant./// @return granted True when `account` is the accepted grantee.function isGrantedTo(
stringcalldatalabel,
addressaccount
)
externalviewreturns (boolgranted);
/// @notice Returns the full entry for `label`, including status and timestamps./// @param label Bare label to look up./// @return grant The stored entry; a zeroed struct with `None` status when absent.function grantOf(stringcalldatalabel) externalviewreturns (Grant memorygrant);
/// @notice Returns the number of entries, of any status./// @return count Entry count.function grantCount() externalviewreturns (uint256count);
/// @notice Returns a page of entries for review./// @dev Reads the canonical offset and limit window. An `offset` at or beyond/// @custom:function grantCount returns an empty page; `limit` is clamped to the/// remaining entries. Iteration order is not stable across revokes./// @param offset Index of the first entry to return./// @param limit Maximum number of entries to return./// @return page Entries in the window.function grants(uint256offset, uint256limit) externalviewreturns (Grant[] memorypage);
}
Activity
re-gius commentedon Aug 12, 2026
EDIT: changed to P1 - see discussion in #202
sphamjoli commentedon Aug 18, 2026
Design: pre-launch name whitelist
Scope: this issue (#203) for the contract, #204 for registration enforcement, under the #202 tracking issue. Governance authority follows #205 and #238.
Purpose
A dedicated contract holds pre-launch name reservations. Both registrar controllers check it before they mint a name. It binds a label to the one address allowed to register it, tracks each name's status of requested, accepted or rejected, and lets anyone read the full list on-chain. The controllers only read it and store nothing themselves.
Contract
DotnsNameWhitelistruns behind its own UUPS proxy and inheritsDotnsRoleManager, which gives it the owner and operator-role checks. Entries are keyed by node, the namehash of the label under the active TLD. Callers never pass a hash. Every function takes the bare label, and the contract derives the node from the label and the TLD in the protocol registry, the same way the controllers do, so a wrong hash cannot get in.Each entry stores the grantee, when it was requested, when it was decided, the status, and the label kept so the list can be read on-chain.
GrantStatusisNone,Requested,AcceptedorRejected.Noneis the zero value, so a missing entry does not look like a live one. OnlyAcceptedreserves a name.Lifecycle and authority
A user calls
requestNameto request a label for themselves. An operator or the owner then callsacceptorreject. Operators can also grant names directly withgrantName, or in bulk with the admin-onlygrantNames.revokeNameclears any entry. The owner is governance. Operators holdWHITELIST_OPERATOR_ROLE, which the owner grants and revokes, and the owner can do anything an operator can.Governance: Root and the Whitelist pallet
Governance calls the contract by sending
pallet_revive::callat it. A contract call accepts only Root or a signed account, so governance comes in as Root. Root has no address, and readingmsg.senderunder Root reverts, so the governance-only functions, upgrades and role changes, checkISystem(REVIVE_SYSTEM).originIsRoot()under #238. We useoriginIsRoot, notcallerIsRoot, because it reads the transaction's origin and still returns true through the UUPS proxy's delegatecall, wherecallerIsRootreturns false.There are two ways to produce that Root origin. Upgrades and registry changes go through the standard Root referendum track: slow, with the highest approval bar. That fits actions that can replace an implementation or change the registry. Operator changes and setting the request window go through the substrate Whitelist pallet on a short track. A vouching body such as the Fellowship whitelists the exact
pallet_revive::callhash, then a fast referendum on the Whitelisted Caller track dispatches it as Root. The whitelisted call is single-use, and the pallet re-checks it at dispatch, so a revoke cancels a pending call. This lets operator changes happen within a day, still by a token-holder vote and still as Root, without the multi-week track.A custom governance origin cannot call the contract directly, because
pallet_revivetakes only Root or a signed account. That is why the fast path goes through the Whitelist pallet, which re-dispatches the call as Root. This works whether or not the Asset Hub Migration has moved OpenGov onto Polkadot Asset Hub: called locally, or over XCM from the Relay, the contract still sees Root.Operators are ordinary signed accounts with a fixed address, so their day-to-day
accept,rejectandgrantcalls need no referendum. The split is deliberate. The dangerous actions have no private key that can trigger them, only governance can. Operators hold a key, but they can only accept, reject, grant and revoke, and governance can undo any of it.Registration enforcement
Both controllers check the whitelist everywhere they mint a name: the public path, the PoP lite mint and the PoP full mint. Before minting label L for registrant R, a controller reads the accepted grantee. If the label is granted to someone else, it refuses. Otherwise it checks L against the shared lite or full rules, mints, and if R is the grantee it calls
consume, which removes the grant. A grant stays until R registers, andconsumeis the only way a controller can remove one. The whitelist reserves exact labels andPopRulesreserves stems, so a PoP full mint must pass both.Central rules
One helper,
NameRules, holds the lite and full name rules, and both controllers call it. Today those checks, label shape and classification, are copied in both controllers. The whitelist does not classify names itself.Open items
The request window (
requestOpen,requestClose, set by governance) is required by this issue and not built yet. It also stops anyone spammingrequestNameto fill storage.revokeNamedeletes the entry today; we should decide whether to keep it as aRevokedstatus so the history stays.NameRulesshould be a library, not a contract. The names need to match this issue, which asks for aNAME_RESERVATIONkey andreservationOf/isReserved.Interface