PopRules
Inherits: Initializable, UUPSUpgradeable, OwnableUpgradeable, ERC165Upgradeable, IPopRules
Title: PopRules
Implements DotNS classification, flat NoStatus pricing, and base-name reservations.
Tier shape: 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.
NoStatus users pay a single flat deposit (startingPrice) per name; verified users pay
zero on registration.
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
startingPrice
Wei price for names with 9 characters and up.
uint256 public startingPrice
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
__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. Seeds startingPrice through
Note: function: updateStartingPrice.
function initialize(
uint256 _startingPrice,
IDotnsProtocolRegistry registry
)
public
initializer;
Parameters
| Name | Type | Description |
|---|---|---|
_startingPrice | uint256 | Base price in wei for NoStatus users. |
registry | IDotnsProtocolRegistry | Protocol-level address registry used to resolve sibling contracts. |
updateStartingPrice
Updates the spam-deterrent starting price for NoStatus pricing.
Owner-only; unauthorised callers trigger @custom:reverts
OwnableUnauthorizedAccount. newStartingPrice must be strictly positive, otherwise
Note:
reverts: PopError. The new value flows into _priceValidatedName on the next
pricing read; no redeploy. Emits @custom:emits StartingPriceUpdated with the prior
and new values.
function updateStartingPrice(uint256 newStartingPrice) public override onlyOwner;
Parameters
| Name | Type | Description |
|---|---|---|
newStartingPrice | uint256 | New base price in wei. |
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
| 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 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
| Name | Type | Description |
|---|---|---|
stem | string | The base label with no trailing digits. |
userAddress | address |
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
| 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 label 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 label 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 a spam
deterrent and is significant only for NoStatus users; verified users pay zero.
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
| 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. |
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
| 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. |
price
Calculates registration cost for a label.
Returns zero for any label shorter than 9 characters; lengths >= 9 pay the flat
startingPrice deposit. Ignores the caller's personhood status and reservation
state. Non-canonical labels trigger @custom:reverts PopError.
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. |
reachFee
Friction fee owed when account reaches into a label tier above its verification
level.
Non-zero only when account cannot meet the label's required PoP tier; the value is
the flat NoStatus deposit. Acts as cross-payer friction at registration time. Use
Note: function: transferFloor for transfer-time friction, which folds in the sender-tier-downgrade component as well. Non-canonical labels and labels with exactly one or more than two trailing digits trigger @custom:reverts PopError.
function reachFee(
string calldata name,
address account
)
external
view
override
returns (uint256 fee);
Parameters
| Name | Type | Description |
|---|---|---|
name | string | Domain label being acted on. |
account | address | Account whose verification reach is being measured. |
transferFloor
Transfer-time friction floor: the greater of the recipient-reach component and the sender-tier-downgrade component.
Returns the flat NoStatus deposit 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. Returns zero when neither condition holds. 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
| Name | Type | Description |
|---|---|---|
name | string | Domain label being transferred. |
from | address | Current holder of the name. |
to | address | Incoming holder of the name. |
_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 reachFee and priceWithCheck build on this so the tier-eligibility rule lives
in exactly one place and the two 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
function _priceValidatedName(uint256 namelength) internal view returns (uint256 priceValue);
_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
Strips trailing digits from a name.
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);
_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
| Name | Type | Description |
|---|---|---|
stem | string | The base label with no trailing digits. |
userAddress | address |
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
| Name | Type | Description |
|---|---|---|
name | string | Full label (with or without trailing digits). |
Returns
| Name | Type | Description |
|---|---|---|
stem | string | The 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
| Name | Type | Description |
|---|---|---|
stem | string | The 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
| Name | Type | Description |
|---|---|---|
stem | string | The base label 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 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;