PopRules

Git Source

Inherits: Initializable, UUPSUpgradeable, OwnableUpgradeable, ERC165Upgradeable, IPopRules

Title: PopRules

Implements DotNS classification, cost-model-driven pricing, and base-name reservations.

Tiers: base lengths <= 5 are governance-reserved, base lengths 6-8 require PopFull (or PopLite when carrying exactly two trailing digits, for gateway-issued lite names), base lengths >= 9 are open to any caller as NoStatus when they carry zero or exactly two trailing digits. A one-digit suffix and more than two trailing digits are invalid. 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 governance 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 digit-stripped 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(IDotnsProtocolRegistry registry) public initializer;

Parameters

NameTypeDescription
registryIDotnsProtocolRegistryProtocol-level address registry used to resolve sibling contracts.

setShortNamesEnabled

Opens or closes the public market for names shorter than nine characters.

Owner-only; unauthorised callers trigger @custom:reverts OwnableUnauthorizedAccount. 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 onlyOwner;

Parameters

NameTypeDescription
enabledboolWhether 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. Non-canonical labels (anything other than a single lowercase ASCII DNS label) and labels with exactly one or more than two trailing digits both trigger @custom:reverts PopError.

function classifyName(string calldata name)
    external
    pure
    override
    returns (PopStatus requirement, string memory message);

Parameters

NameTypeDescription
namestringThe name label being evaluated.

Returns

NameTypeDescription
requirementPopStatusRequired tier for registration.
messagestringExplanation of the classification result.

reserveBaseName

Creates or refreshes a reservation entry for a PopLite-eligible stem.

Commit-reveal reservation path. Only an authorised controller on the registrar may call this, otherwise @custom:reverts NotRegistry. The caller passes the already-stripped stem; the contract enforces stem shape (no trailing digits) and PopLite-eligibility (length in [6, 8]), and a non-canonical label or a label outside that shape 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 stem,
    address userAddress
)
    external
    override
    onlyRegistry;

Parameters

NameTypeDescription
stemstringThe base label with no trailing digits.
userAddressaddress

isBaseName

Returns whether name is a base name under PoP rules.

A base name has no trailing digits; lite-person labels always have exactly two trailing digits, so the two spaces are disjoint. Non-canonical labels trigger

Note: reverts: PopError.

function isBaseName(string calldata baseName) external pure override returns (bool isBase);

Parameters

NameTypeDescription
baseNamestring

Returns

NameTypeDescription
isBaseboolTrue 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

NameTypeDescription
baseNamestringThe base label without trailing digits.

Returns

NameTypeDescription
reservationOwneraddressowner The address assigned to the reservation.
expiryTimestampuint64expires 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

NameTypeDescription
baseNamestringThe base label without trailing digits.

Returns

NameTypeDescription
isReservedboolreservedStatus True if a live reservation is active.
reservationOwneraddressowner The reservation holder (zero when not reserved).
expiryTimestampuint64expires 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 stem 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

NameTypeDescription
namestringDomain label.
userAddressaddressRegistering user for the given label.

Returns

NameTypeDescription
metadataPriceWithMetaPrice 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

NameTypeDescription
namestringDomain label.
userAddressaddressRegistering user for the given label.
pricingVersionValueuint256Cost-model version to price against.

Returns

NameTypeDescription
metadataPriceWithMetaPrice 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 stem 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

NameTypeDescription
namestringDomain label.
userAddressaddressRegistering user for the given label.

Returns

NameTypeDescription
metadataPriceWithMetaPrice 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

NameTypeDescription
namestringDomain label.
userAddressaddressRegistering user for the given label.
pricingVersionValueuint256Cost-model version to price against.

Returns

NameTypeDescription
metadataPriceWithMetaPrice 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 label whose trailing-digit suffix is neither zero nor exactly two, and any non-canonical label, trigger @custom:reverts PopError.

function price(string calldata name) external view override returns (uint256);

Parameters

NameTypeDescription
namestringDomain label to price.

Returns

NameTypeDescription
<none>uint256cost 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

NameTypeDescription
modelVersionuint256Identifier 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. Non-canonical labels and labels with exactly one or more than two trailing digits trigger @custom:reverts PopError.

function transferFloor(
    string calldata name,
    address from,
    address to
)
    external
    view
    override
    returns (uint256 floor);

Parameters

NameTypeDescription
namestringDomain label being transferred.
fromaddressCurrent holder of the name.
toaddressIncoming holder of the name.

Returns

NameTypeDescription
flooruint256Transfer-time floor in wei: the name's own curve price, or zero.

personhoodOf

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, PopLite, or PopFull.

function personhoodOf(address account) external view override returns (PopStatus tier);

Parameters

NameTypeDescription
accountaddressAddress whose tier is read.

Returns

NameTypeDescription
tierPopStatusThe 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. Tiers are defined incrementally on the precompile side: 0=None, 1=Lite, 2=Full. 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, PopLite, PopFull} 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;

_validatedBaseLength

Validates the digit suffix and returns the base length that pricing and classification both use to place a name in its band.

A name carries no digit suffix or exactly two digits; any other count triggers

Note: reverts: PopError, so a longer suffix cannot slip a name into a shorter band.

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

NameTypeDescription
namestringDomain label.
userAddressaddressRegistering 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

NameTypeDescription
labelstringString to analyse.

Returns

NameTypeDescription
digitCountuint256Number of trailing digits.

_stripDigits

Strips trailing digits from a name.

function _stripDigits(string calldata name) internal pure returns (string memory baseName);

Parameters

NameTypeDescription
namestringDomain label.

_classifyValidatedName

function _classifyValidatedName(string calldata name)
    internal
    pure
    returns (PopStatus requirement, string memory message, uint256 baseLength);

_requireCanonicalLabel

function _requireCanonicalLabel(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 implementation version.

function version() external pure virtual returns (string memory versionString);

_onlyRegistry

Ensures the caller is any controller authorised on the registrar.

function _onlyRegistry() internal view;

reserveBaseNameForPop

Writes or refreshes a reservation for a bare base-name stem.

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 lite-format length window that @custom:function reserveBaseName enforces, but does require the input to be canonical and stem-shaped (no trailing digits); a non-canonical or non-stem label 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 stem,
    address userAddress
)
    external
    override
    onlyRegistry;

Parameters

NameTypeDescription
stemstringThe base label with no trailing digits.
userAddressaddress

stripDigits

Returns the bare stem of a label, i.e. the label with any trailing ASCII digits removed.

Mirrors the normalisation that @custom:function reserveBaseName applies before writing a reservation, so callers can look up or release a reservation by passing the full label without re-implementing the digit-stripping rule. Non-canonical labels trigger @custom:reverts PopError.

function stripDigits(string calldata name) external pure override returns (string memory stem);

Parameters

NameTypeDescription
namestringFull label (with or without trailing digits).

Returns

NameTypeDescription
stemstringThe label with trailing digits removed.

releaseBaseName

Clears a reservation for a base-name stem.

Only a controller in the registrar's controllers set may call this, otherwise

Notes:

  • reverts: NotRegistry. Non-canonical or non-stem labels trigger

  • emits: BaseNameReleased once the slot is cleared.

function releaseBaseName(string calldata stem) external override onlyRegistry;

Parameters

NameTypeDescription
stemstringThe base label 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 or non-stem labels 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 stem,
    address expectedOwner
)
    external
    override
    onlyRegistry;

Parameters

NameTypeDescription
stemstringThe base label whose reservation should be cleared (no trailing digits).
expectedOwneraddressThe address the caller expects to be the current reservation owner.

_writeReservation

Internal single-source-of-truth writer for stem 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 stem is canonical and stem-shaped (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 stem, address userAddress) internal;