PopRules
Inherits: Initializable, UUPSUpgradeable, OwnableUpgradeable, ERC165Upgradeable, IPopRules
Title: PopRules
Implements dotNS classification, cost-model-driven pricing, and base-name reservations.
Tiers are set by base length, the length of a label's base name: the label with any
device suffix removed. Every label is measured as written, except a device name, whose
separator and allocated digits are not part of the name the candidate chose, so
joseph.42 measures six and joseph42 measures eight.
Base lengths <= 5 are governance-reserved, base lengths 6-8 require personhood, and base
lengths >= 9 are open to any caller as NoStatus. Devicehood applies to the separated form
alone: a digit suffix on an ordinary label says nothing about a proof.
Every caller pays the same amount for a given base length. The amount comes from the cost
model registered under DotnsConstants.COST_MODEL, which owns the curve; this contract
passes it only the base length and keeps the classification, reservation, and tier rules.
Personhood only unlocks the premium band. Base lengths below nine are closed to the public
paid path until Root sets shortNamesEnabled; the gateway and registerReserved do
not consult it.
Note: security-contact: admin@parity.io
Constants
MAX_RESERVATION_TIME
Maximum time a base name can be reserved.
uint256 public constant MAX_RESERVATION_TIME = 12 weeks
State Variables
reservations
Active reservations keyed by base name.
mapping(string baseName => Reservation reservation) public reservations
protocolRegistry
Protocol-level address registry for all dotNS contracts.
IDotnsProtocolRegistry public protocolRegistry
shortNamesEnabled
Whether the public paid path may register names shorter than nine characters. Closed by default; only governance opens it.
bool public shortNamesEnabled
__gap
uint256[50] private __gap
Functions
onlyRegistry
Restricts function to any registry-authorised controller.
modifier onlyRegistry() ;
constructor
Note: oz-upgrades-unsafe-allow: constructor
constructor() ;
initialize
Initialises the oracle (public entry point).
Runs once behind the proxy; subsequent calls trigger @custom:reverts
InvalidInitialization via the initializer modifier. Amounts come from the cost model
registered under DotnsConstants.COST_MODEL, so no price is seeded here.
function initialize(address initialOwner, IDotnsProtocolRegistry registry) public initializer;
Parameters
| Name | Type | Description |
|---|---|---|
initialOwner | address | Address that owns the contract once initialised. |
registry | IDotnsProtocolRegistry | Protocol-level address registry used to resolve sibling contracts. |
setShortNamesEnabled
Opens or closes the public market for names shorter than nine characters.
Restricted to a substrate Root origin; any other caller triggers @custom:reverts NotRoot. Short names are otherwise issued through the dotNS gateway pallet, so this flag is the Root-only lever that additionally admits them on the public paid path. While closed, which is the deploy default, @custom:function priceWithCheck and
Note: function: priceWithoutCheck trigger @custom:reverts PopError for a base length below nine, so no public caller buys a short name. The gateway free grant and the registrar's registerReserved path do not read this flag. Emits @custom:emits ShortNamesEnabledUpdated.
function setShortNamesEnabled(bool enabled) external override;
Parameters
| Name | Type | Description |
|---|---|---|
enabled | bool | Whether names shorter than nine characters may be bought. |
classifyName
Classifies a name into a required PoP tier per dotNS naming rules.
Pure; inputs are the label bytes only. Callers use the returned tier to decide which pricing and verification branch applies. A label that is neither a single lowercase ASCII DNS label nor a device name triggers @custom:reverts PopError. Trailing digits in an ordinary label count towards its length; only a device name's separator and suffix are removed before it is classified.
function classifyName(string calldata name)
external
pure
override
returns (PopStatus requirement, string memory message);
Parameters
| Name | Type | Description |
|---|---|---|
name | string | The name label being evaluated. |
Returns
| Name | Type | Description |
|---|---|---|
requirement | PopStatus | Required tier for registration. |
message | string | Explanation of the classification result. |
reserveBaseName
Creates or refreshes a reservation entry for a base name in the 6 to 8 band.
Authorised-controller entry point: only a controller in the registrar's controllers
set may call this, otherwise @custom:reverts NotRegistry. The gateway queue writes
through @custom:function reserveBaseNameForPop and the public commit-reveal flow
reads the slot rather than writing one, so this is the entry point for a sibling
controller. Its length window bounds the base name and does not name a tier:
Devicehood is decided by the separated label shape. The caller passes the base name; a
non-canonical label, a trailing digit, or a length outside [6, 8] triggers
Note: reverts: PopError. Cross-user collision on a live slot triggers @custom:reverts PopError so the caller cannot silently overwrite another user's reservation; same-user refresh and writes into an empty or expired slot emit @custom:emits BaseNameReserved.
function reserveBaseName(
string calldata baseName,
address userAddress
)
external
override
onlyRegistry;
Parameters
| Name | Type | Description |
|---|---|---|
baseName | string | The base name, with no trailing digits. |
userAddress | address |
isBaseName
Returns whether name can key a reservation: a base name with no trailing digits.
A device name always ends in two digits, so it never qualifies. Non-canonical labels trigger @custom:reverts PopError.
function isBaseName(string calldata baseName) external pure override returns (bool isBase);
Parameters
| Name | Type | Description |
|---|---|---|
baseName | string |
Returns
| Name | Type | Description |
|---|---|---|
isBase | bool | True when the label has no trailing digits. |
getBaseNameReservation
Retrieves reservation information for a base name.
Raw accessor: returns the stored slot regardless of expiry. Use
Notes:
-
function: isBaseNameReserved when live-window semantics are needed. Non-canonical labels trigger
-
reverts: PopError.
function getBaseNameReservation(string calldata baseName)
external
view
override
returns (address reservationOwner, uint64 expiryTimestamp);
Parameters
| Name | Type | Description |
|---|---|---|
baseName | string | The base name, without trailing digits. |
Returns
| Name | Type | Description |
|---|---|---|
reservationOwner | address | owner The address assigned to the reservation. |
expiryTimestamp | uint64 | expires UNIX timestamp when the reservation expires. |
isBaseNameReserved
Indicates whether a base name is currently reserved.
Applies the live-window predicate to the stored slot so an expired reservation reads as free. Non-canonical labels trigger @custom:reverts PopError.
function isBaseNameReserved(string calldata baseName)
external
view
override
returns (bool isReserved, address reservationOwner, uint64 expiryTimestamp);
Parameters
| Name | Type | Description |
|---|---|---|
baseName | string | The base name, without trailing digits. |
Returns
| Name | Type | Description |
|---|---|---|
isReserved | bool | reservedStatus True if a live reservation is active. |
reservationOwner | address | owner The reservation holder (zero when not reserved). |
expiryTimestamp | uint64 | expires UNIX timestamp when the reservation expires. |
priceWithCheck
Calculates price with PoP classification and reservation enforcement.
Reverting pricing path used by the commit-reveal controller. Price is the scarcity
curve for the label's base length and is charged to every caller, verified or not;
personhood only unlocks the premium band. Non-canonical
labels, a base name held live by another user, a governance-reserved label, or a
userAddress whose personhood tier does not meet the label's required tier each
trigger @custom:reverts PopError.
function priceWithCheck(
string calldata name,
address userAddress
)
external
view
override
returns (PriceWithMeta memory metadata);
Parameters
| Name | Type | Description |
|---|---|---|
name | string | Domain label. |
userAddress | address | Registering user for the given label. |
Returns
| Name | Type | Description |
|---|---|---|
metadata | PriceWithMeta | Price with PoP requirements and classification. |
priceWithCheckAtVersion
Calculates price at a specific cost-model version with PoP classification and reservation enforcement.
The versioned counterpart of @custom:function priceWithCheck: identical classification,
tier gating, and reservation rules, but the amount comes from the model registered for
pricingVersionValue rather than the current one. The commit-reveal controller prices
a reveal at the version bound into its commitment, so a model change between commit and
reveal does not move the amount. @custom:reverts UnknownVersion when the version was
never registered.
function priceWithCheckAtVersion(
string calldata name,
address userAddress,
uint256 pricingVersionValue
)
external
view
override
returns (PriceWithMeta memory metadata);
Parameters
| Name | Type | Description |
|---|---|---|
name | string | Domain label. |
userAddress | address | Registering user for the given label. |
pricingVersionValue | uint256 | Cost-model version to price against. |
Returns
| Name | Type | Description |
|---|---|---|
metadata | PriceWithMeta | Price with PoP requirements and classification. |
priceWithoutCheck
Calculates price with PoP classification and reservation metadata, without reverting on conflicts.
Non-reverting counterpart to priceWithCheck: surfaces the same fields, but reports
a Reserved status through metadata instead of reverting when the base name is
held by another user. Used by front-ends that need to present a price and eligibility
preview without forcing a transaction attempt. Governance-reserved names are not
rejected here either; the caller decides what to do. Non-canonical labels still
trigger @custom:reverts PopError because the input is malformed rather than just
contested.
function priceWithoutCheck(
string calldata name,
address userAddress
)
external
view
override
returns (PriceWithMeta memory metadata);
Parameters
| Name | Type | Description |
|---|---|---|
name | string | Domain label. |
userAddress | address | Registering user for the given label. |
Returns
| Name | Type | Description |
|---|---|---|
metadata | PriceWithMeta | Price with PoP requirements and classification. |
priceWithoutCheckAtVersion
Calculates price at a specific cost-model version with PoP classification and reservation metadata, without reverting on conflicts.
The versioned counterpart of @custom:function priceWithoutCheck: same non-reverting
preview behaviour, but the amount comes from the model registered for
pricingVersionValue. @custom:reverts UnknownVersion when the version was never
registered.
function priceWithoutCheckAtVersion(
string calldata name,
address userAddress,
uint256 pricingVersionValue
)
external
view
override
returns (PriceWithMeta memory metadata);
Parameters
| Name | Type | Description |
|---|---|---|
name | string | Domain label. |
userAddress | address | Registering user for the given label. |
pricingVersionValue | uint256 | Cost-model version to price against. |
Returns
| Name | Type | Description |
|---|---|---|
metadata | PriceWithMeta | Price with PoP requirements and classification. |
_priceWithCheck
Shared body for the reservation-enforcing pricing reads.
atVersion selects the amount source: the current model when false, the model for
pricingVersionValue when true. Classification, tier gating, and reservation rules are
the same on both paths, so they live here once.
function _priceWithCheck(
string calldata name,
address userAddress,
bool atVersion,
uint256 pricingVersionValue
)
internal
view
returns (PriceWithMeta memory metadata);
_priceWithoutCheck
Shared body for the non-reverting pricing reads.
Mirror of @custom:function _priceWithCheck for the front-end preview path: reports a
contested reservation through metadata rather than reverting. atVersion selects the
amount source in the same way.
function _priceWithoutCheck(
string calldata name,
address userAddress,
bool atVersion,
uint256 pricingVersionValue
)
internal
view
returns (PriceWithMeta memory metadata);
price
Calculates registration cost for a label.
Prices the label by its base length through the cost model registered under
DotnsConstants.COST_MODEL. Ignores the caller's personhood status and reservation
state. A non-canonical label triggers @custom:reverts PopError. An ordinary label is
priced as written; only a device name's allocated suffix is removed first.
function price(string calldata name) external view override returns (uint256);
Parameters
| Name | Type | Description |
|---|---|---|
name | string | Domain label to price. |
Returns
| Name | Type | Description |
|---|---|---|
<none> | uint256 | cost Registration cost in wei. |
pricingVersion
Returns the current cost-model version.
The current version held by the registry under DotnsConstants.COST_MODEL. The
commit-reveal controller binds it into a commitment and prices the reveal at that
version, so a model change between commit and reveal leaves the committed amount
unchanged. @custom:reverts PopError when no registry is configured.
function pricingVersion() external view override returns (uint256 modelVersion);
Returns
| Name | Type | Description |
|---|---|---|
modelVersion | uint256 | Identifier of the current cost model and its parameters. |
transferFloor
Transfer-time floor: the greater of the recipient-reach component and the sender-tier-downgrade component, each priced at the name's own length.
Re-prices the name at its own length on every move: returns the name's curve price when either (i) the recipient does not meet the label's required tier, or (ii) the recipient's personhood tier is strictly below the sender's, and zero when neither holds. Passing a name to a wallet that could never have registered it therefore costs the name's own curve price. The two components overlap on pure tier mismatches, so the function takes their maximum rather than their sum to avoid double-charging. Consumed by @custom:function DotnsRegistrar.quoteTransferFee. A label that is neither a single lowercase ASCII DNS label nor a device name triggers
Note: reverts: PopError.
function transferFloor(
string calldata name,
address from,
address to
)
external
view
override
returns (uint256 floor);
Parameters
| Name | Type | Description |
|---|---|---|
name | string | Domain label being transferred. |
from | address | Current holder of the name. |
to | address | Incoming holder of the name. |
Returns
| Name | Type | Description |
|---|---|---|
floor | uint256 | Transfer-time floor in wei: the name's own curve price, or zero. |
popStatusOf
Returns the personhood tier recorded for an account.
Reads the account's dotns-scoped tier from the personhood precompile and maps it to a
PopStatus. This is the direct account-tier read; the same tier otherwise surfaces
only as the userStatus field of a pricing query. Never returns Reserved, so the
result is one of NoStatus, Devicehood, or Personhood.
function popStatusOf(address account) external view override returns (PopStatus tier);
Parameters
| Name | Type | Description |
|---|---|---|
account | address | Address whose tier is read. |
Returns
| Name | Type | Description |
|---|---|---|
tier | PopStatus | The account's personhood tier. |
_personhoodTier
Reads account's dotns-scoped personhood tier from the alias-accounts
precompile and translates it into a PopStatus.
Single source of truth so callers cannot read the precompile directly and
drift on the status mapping. The precompile defines its statuses incrementally and
names them 0=None, 1=Lite, 2=Full; this maps 1 to Devicehood and 2 to Personhood.
Anything outside that range collapses to NoStatus so a future tier addition fails
closed instead of silently being treated as a higher level than it actually is.
function _personhoodTier(address account) private view returns (PopStatus);
_meetsReach
Single canonical "is userStatus at reach for required?" predicate.
Both priceWithCheck and transferFloor build on this so the tier-eligibility rule
lives in exactly one place and the callers cannot disagree about who clears a given label.
_personhoodTier never returns Reserved, so userStatus is in {NoStatus, Devicehood, Personhood} and the enum comparison reflects tier ordering directly. A Reserved
required (governance label) is unreachable by any verified user, so the comparison returns
false and the caller charges the friction fee, providing defence-in-depth if a Reserved
label ever enters circulation.
function _meetsReach(PopStatus required, PopStatus userStatus) private pure returns (bool);
_priceValidatedName
Amount for a base length at the current cost-model version.
The cost-model registry owns the curve; this contract passes it only the base length. The call is a view because it runs on the ERC721 transfer floor read through
Note: function: transferFloor.
function _priceValidatedName(uint256 baseLength) internal view returns (uint256 priceValue);
_priceValidatedNameAtVersion
Amount for a base length at a specific cost-model version.
Prices an in-flight registration at the version it committed to, so a model change between commit and reveal does not move its cost. @custom:reverts UnknownVersion (from the registry) when the version was never registered.
function _priceValidatedNameAtVersion(
uint256 pricingVersionValue,
uint256 baseLength
)
internal
view
returns (uint256 priceValue);
_costModelRegistry
Resolves the cost-model registry registered under DotnsConstants.COST_MODEL.
@custom:reverts PopError when no registry is configured, so a pricing read fails closed rather than resolving through the zero address.
function _costModelRegistry() private view returns (IDotnsCostModelRegistry registry);
_requireShortNamesOpen
Reverts a public paid registration of a base length below nine while the short-name market is closed.
The one gate both public price reads share. Base lengths of nine and above are always
open. @custom:reverts PopError when a base length below nine is priced while
shortNamesEnabled is false. The gateway and @custom:function registerReserved never
reach this, so neither is gated.
function _requireShortNamesOpen(uint256 baseLength) private view;
_baseNameEnd
Index one past name's base name: a device name without its suffix, or the whole
of any other label.
Only a device name has a suffix to remove. The gateway allocates those two digits to
tell apart people who chose the same stem, so removing them recovers what the
candidate actually picked. No such allocation stands behind the digits in an
ordinary label, where they are part of the name: web3 is a four-character word,
not web with a counter.
function _baseNameEnd(string calldata name) private pure returns (uint256 end);
_validatedBaseLength
The base length that pricing and classification both use to place a name in its band, which is the length of the name's base name.
Every label is measured as written, except a device name, whose allocated suffix is
not part of the name the candidate chose. So web3 and blink182 are measured
whole and no digit count is privileged or rejected.
function _validatedBaseLength(string calldata name) internal pure returns (uint256 baseLength);
_enforceReservationRules
Enforces base-name reservation rules.
function _enforceReservationRules(string calldata name, address userAddress) internal view;
Parameters
| Name | Type | Description |
|---|---|---|
name | string | Domain label. |
userAddress | address | Registering user. |
_isLive
Returns whether reservation is live at block.timestamp.
function _isLive(Reservation memory reservation) internal view returns (bool);
_countTrailingDigits
Counts trailing digits in a string.
function _countTrailingDigits(string calldata label)
internal
pure
returns (uint256 digitCount);
Parameters
| Name | Type | Description |
|---|---|---|
label | string | String to analyse. |
Returns
| Name | Type | Description |
|---|---|---|
digitCount | uint256 | Number of trailing digits. |
_stripDigits
Returns name's base name: a device name without its allocated suffix, or any
other label verbatim.
The reservation key. Because only a device name is shortened, joseph.42 contends
with joseph while joseph42 is an unrelated name and contends with nothing.
function _stripDigits(string calldata name) internal pure returns (string memory baseName);
Parameters
| Name | Type | Description |
|---|---|---|
name | string | Domain label. |
_classifyValidatedName
function _classifyValidatedName(string calldata name)
internal
pure
returns (PopStatus requirement, string memory message, uint256 baseLength);
_requireBaseName
Requires baseName to be a canonical DNS label, carrying no separator.
Reservation keys are base names, and a base name carries no separator, so a separator here is a caller error. A digit suffix passes this check, because a DNS label admits digits; the entry points that write a reservation reject one themselves.
Note: function: _requireLabel is the guard for whole labels.
function _requireBaseName(string calldata baseName) internal pure;
_requireLabel
Requires name to be a label dotNS can issue: a canonical DNS label, or a device
name carrying its separator.
The union is the full set of issuable labels, so a near miss such as alice.4 or
a.b.42 still reverts. @custom:function _requireBaseName is the stricter guard for
reservation keys, which never carry a separator.
function _requireLabel(string calldata name) internal pure;
supportsInterface
function supportsInterface(bytes4 interfaceId)
public
view
virtual
override
returns (bool supported);
_authorizeUpgrade
Function that should revert when msg.sender is not authorized to upgrade the contract.
Called by
{upgradeToAndCall}.
Normally, this function will use an xref:access.adoc[access control] modifier such as
{Ownable-onlyOwner}.
function _authorizeUpgrade(address) internal onlyOwner {}
function _authorizeUpgrade(address newImplementation) internal override onlyOwner;
version
Returns the release this network declares it runs, read live from the protocol registry so every dotNS contract reports one synchronised value.
Mirror of IDotnsProtocolRegistry.protocolVersion, kept under the historical
version() selector for ABI compatibility. It reports the network's declaration,
not this contract's build; per-contract identity is the codehash declared on the
registry.
function version() external view virtual returns (string memory versionString);
Returns
| Name | Type | Description |
|---|---|---|
versionString | string | Declared release as bare semver, empty when never declared. |
_onlyRegistry
Ensures the caller is any controller authorised on the registrar.
function _onlyRegistry() internal view;
reserveBaseNameForPop
Writes or refreshes a reservation for a base name.
Gateway-driven reservation path used by the PoP controller. Only a controller in the
registrar's controllers set may call this, otherwise @custom:reverts NotRegistry.
Does not apply the 6-8 base-length window that @custom:function reserveBaseName
enforces, but does require the input to be canonical with no trailing digits; a
non-canonical label or one with trailing digits triggers @custom:reverts PopError. If
the slot is already live and held by a different user, @custom:reverts PopError so the
caller's local bookkeeping and PopRules state stay in lockstep; if it is live for the
same user, expiry is refreshed to block.timestamp + MAX_RESERVATION_TIME. Emits
Note: emits: BaseNameReserved on every successful write.
function reserveBaseNameForPop(
string calldata baseName,
address userAddress
)
external
override
onlyRegistry;
Parameters
| Name | Type | Description |
|---|---|---|
baseName | string | The base name, with no trailing digits. |
userAddress | address |
stripDigits
Returns the base name of a label, its reservation key: a device name without its allocated suffix, or any other label unchanged.
Mirrors the normalisation applied before a reservation is written, so callers can
look up or release one by passing the whole label. Only a device name is shortened,
because only the gateway allocates the digits it carries: joseph.42 yields
joseph while joseph42 is an unrelated name and yields itself. Non-canonical
labels trigger @custom:reverts PopError.
function stripDigits(string calldata name)
external
pure
override
returns (string memory baseName);
Parameters
| Name | Type | Description |
|---|---|---|
name | string | Whole label, device name or otherwise. |
Returns
| Name | Type | Description |
|---|---|---|
baseName | string | The base name of name. |
releaseBaseName
Clears a reservation for a base name.
Only a controller in the registrar's controllers set may call this, otherwise
Note: reverts: NotRegistry. Non-canonical labels and labels with trailing digits trigger @custom:reverts PopError. Live reservations may only be cleared by the same controller that wrote them; another authorised controller attempting to clear a live slot triggers @custom:reverts PopError. Expired reservations may be cleared by any authorised controller as garbage collection. Used by the PoP controller when a reservation is claimed, relinquished, or a queue head promotion leaves the slot empty. Emits @custom:emits BaseNameReleased once the slot is cleared.
function releaseBaseName(string calldata baseName) external override onlyRegistry;
Parameters
| Name | Type | Description |
|---|---|---|
baseName | string | The base name whose reservation should be cleared (no trailing digits). |
releaseReservationForReclaim
Clears a reservation when the slot owner matches expectedOwner, allowing any
registrar-authorised controller (not only the stamping one) to release the slot.
Narrower than @custom:function releaseBaseName: callers must prove they know the
slot owner, so cross-controller release is gated on a positive match rather than on
caller identity. Intended for the public registrar controller's reclaim path, where
a prior occupant has handed the name back to escrow and the new registrant needs
the cross-flow guard cleared regardless of which controller originally stamped it.
Only a registrar-authorised controller may call this (@custom:reverts NotRegistry).
Non-canonical labels and labels with trailing digits trigger @custom:reverts PopError.
A live reservation
whose owner does not match expectedOwner triggers @custom:reverts PopError; expired
reservations are cleared regardless. Emits @custom:emits BaseNameReleased.
function releaseReservationForReclaim(
string calldata baseName,
address expectedOwner
)
external
override
onlyRegistry;
Parameters
| Name | Type | Description |
|---|---|---|
baseName | string | The base name whose reservation should be cleared (no trailing digits). |
expectedOwner | address | The address the caller expects to be the current reservation owner. |
_writeReservation
Internal single-source-of-truth writer for base-name reservations.
Routes both @custom:function reserveBaseName and @custom:function reserveBaseNameForPop
through one path so the cross-user collision semantics stay identical: a live slot held
by a different user @custom:reverts PopError, and any other case writes a fresh expiry
and emits @custom:emits BaseNameReserved. Same-owner re-reservations refresh the expiry
to block.timestamp + MAX_RESERVATION_TIME. Callers are responsible for validating
baseName is canonical and carries no trailing digits; this helper does no input
validation of its own so each public entry can layer additional eligibility checks.
function _writeReservation(string calldata baseName, address userAddress) internal;