DotnsNameEscrow

Git Source

Inherits: Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuardTransient, ERC165Upgradeable, IERC721Receiver, IDotnsNameEscrow

Title: Dotns Name Escrow

Holds refundable deposits for .dot names and manages the release/reclaim lifecycle.

Note: security-contact: admin@parity.io

Constants

MAX_RELEASED_PAGE_SIZE

Maximum page size for releasedTokens pagination.

uint256 public constant MAX_RELEASED_PAGE_SIZE = 200

MAX_REFUND_PAGE_SIZE

Maximum page size for pendingRefunds pagination and batch claims.

uint256 public constant MAX_REFUND_PAGE_SIZE = 200

MAX_COOLDOWN

Upper bound on the configurable release-cooldown.

The cooldown gates only the release-to-reclaim window, not the long-lived deposit lock, so it is intentionally kept short. Capping at one hour also keeps the cast to uint64 well below the saturation point at every plausible block timestamp.

uint256 public constant MAX_COOLDOWN = 1 hours

State Variables

protocolRegistry

The protocol registry for resolving sibling contract addresses.

IDotnsProtocolRegistry public protocolRegistry

cooldown

Cooldown period after release during which refunds can be claimed but not yet reclaimed.

Forces a delay between release and reclaim so the original payer has an uncontested window to pull their refund before the controller hands the name out again.

uint256 public cooldown

tokenReserved

Total amount of a specific asset reserved across all positions.

Keyed by asset so future ERC20 support can track per-token liabilities independently; address(0) represents the native token and is the only asset currently accepted.

mapping(address asset => uint256 amount) public tokenReserved

_positions

Per-token escrow position storing recipient, amount, lifecycle flags and cooldown.

mapping(uint256 tokenId => ReleasePosition position) private _positions

_releasedTokens

Ordered set of tokens currently in escrow custody, used for paginated enumeration.

uint256[] private _releasedTokens

_releasedIndexPlusOne

Reverse lookup into _releasedTokens (one-based) for O(1) remove-by-swap.

mapping(uint256 tokenId => uint256 indexPlusOne) private _releasedIndexPlusOne

insuranceFund

Cumulative balance of cross-tier fees held against unreleased shortfalls.

Credited by cross-tier registration deposits, reach-floor friction, and transfer-fee deltas; debited only when withdraw needs to top up a refund that exceeds the asset's reserved balance.

uint256 public insuranceFund

_pendingWithdrawals

Pull-payment ledger storing each recipient's claimable refund balance.

Per-recipient isolation ensures a failing or reentrant receiver cannot block other users' withdrawals. Used as the fallback path for registration overpayments whose direct push back to msg.sender failed (because the caller is a contract that rejects incoming value).

mapping(address recipient => uint256 amount) private _pendingWithdrawals

_refundEntries

Time-locked refund ledger keyed by entryId.

Every credit allocates a fresh entryId so per-entry cooldowns are independent and drip-feed credits cannot reset an existing entry's clock.

mapping(uint256 entryId => RefundEntry entry) private _refundEntries

_entriesByRecipient

Per-recipient list of pending entryIds for paginated enumeration and batch claim.

mapping(address recipient => uint256[] entryIds) private _entriesByRecipient

_entryIndexPlusOne

Reverse lookup into _entriesByRecipient (one-based) for O(1) remove-by-swap.

mapping(uint256 entryId => uint256 indexPlusOne) private _entryIndexPlusOne

_nextEntryId

Monotonic counter assigning entryIds to new refund credits.

uint256 private _nextEntryId

__gap

Reserved storage space to allow for layout changes in the future.

uint256[50] private __gap

Functions

onlyController

Restricts calls to the configured registrar controller.

modifier onlyController() ;

onlyRegistrar

Restricts calls to the configured registrar from the protocol registry.

modifier onlyRegistrar() ;

constructor

Note: oz-upgrades-unsafe-allow: constructor

constructor() ;

initialize

Initialises the name escrow.

Runs once behind the proxy; subsequent calls trigger @custom:reverts InvalidInitialization via the initializer modifier. registry must be non-zero, otherwise @custom:reverts InvalidAsset; cooldownSeconds is forwarded to

Note: function: updateCooldown, which rejects a zero value (@custom:reverts InvalidCooldown) and any value above @custom:constant MAX_COOLDOWN (@custom:reverts CooldownTooLong), and emits @custom:emits CooldownUpdated as part of seeding the initial cooldown.

function initialize(
    IDotnsProtocolRegistry registry,
    uint256 cooldownSeconds
)
    external
    initializer;

Parameters

NameTypeDescription
registryIDotnsProtocolRegistryProtocol registry used to resolve registrar and controller addresses.
cooldownSecondsuint256Refund cooldown after release.

updateCooldown

Updates the cooldown duration for future releases.

Owner-only. Affects only releases recorded after this call; positions already released keep the withdrawAvailableAt snapshot taken at their release time. newCooldown must be non-zero, otherwise @custom:reverts InvalidCooldown, and must not exceed the contract's MAX_COOLDOWN upper bound, otherwise @custom:reverts CooldownTooLong; the bound keeps the release-to-reclaim window short and protects the uint64 cast in release from truncation. Emits @custom:emits CooldownUpdated with the prior and new values.

function updateCooldown(uint256 newCooldown) public override onlyOwner;

getReleasePosition

Returns the escrow state for a token.

function getReleasePosition(uint256 tokenId)
    external
    view
    override
    returns (ReleasePosition memory position);

releasedTokenCount

Returns the number of tokens currently held by escrow pending reclaim or withdrawal.

function releasedTokenCount() external view override returns (uint256 count);

Returns

NameTypeDescription
countuint256Number of released tokens not yet reclaimed.

reserves

Returns total amount of assets liabilities reserved for withdrawals.

function reserves(address asset) external view returns (uint256 amount);

Parameters

NameTypeDescription
assetaddressAsset address. address(0) denotes native token.

releasedTokens

Returns a bounded paginated slice of released token identifiers.

limit must be non-zero and at most MAX_RELEASED_PAGE_SIZE, otherwise

Note: reverts: InvalidPageSize.

function releasedTokens(
    uint256 start,
    uint256 limit
)
    external
    view
    override
    returns (uint256[] memory tokenIds);

Parameters

NameTypeDescription
startuint256Start index into the released-token set.
limituint256Maximum number of token identifiers to return.

deposit

Records an asset deposit position for a token.

Only the configured controller may call this, otherwise @custom:reverts NotController. params.amount must equal msg.value, otherwise @custom:reverts InvalidAmount; a zero amount is accepted so cross-payer and free-tier registrations can still seed a position that the release lifecycle can advance. Only native deposits are accepted today, so a non-zero params.asset triggers @custom:reverts AssetNotSupported, and a zero params.recipient triggers @custom:reverts InvalidRecipient. The slot for params.tokenId must be empty (sentinel: position.recipient == address(0)): a previously funded position triggers @custom:reverts PositionAlreadyFunded, and a position already in the released phase triggers @custom:reverts AlreadyReleased. Emits @custom:emits NativeDepositRecorded once the deposit is booked.

function deposit(DepositParams calldata params) external payable override onlyController;

creditOverpayment

Credits msg.value to recipient's pull-payment ledger so the caller can later pull the balance with @custom:func claimWithdrawal.

Only the configured controller may call this, otherwise @custom:reverts NotController. recipient must be non-zero (@custom:reverts InvalidRecipient) and msg.value must be non-zero (@custom:reverts InvalidAmount). Used by the registrar controller to refund overpayment without pushing native value into a potentially reverting contract receiver. Emits @custom:emits OverpaymentRefunded once the credit lands.

function creditOverpayment(address recipient) external payable override onlyController;

Parameters

NameTypeDescription
recipientaddressAddress whose pending balance should grow by msg.value.

depositInsurance

Records a cross-tier registration fee into the insurance fund.

Only the configured controller may call this, otherwise @custom:reverts NotController. msg.value must be non-zero, otherwise @custom:reverts InvalidAmount. Emits

Note: emits: CrossTierFeePaid with isRegistration = true once the fee is booked.

function depositInsurance(InsuranceDepositParams calldata params)
    external
    payable
    override
    onlyController;

chargeTransferFee

Charges transfer friction and rebinds the token's escrow position to the new holder.

Only the configured registrar may call this, otherwise @custom:reverts NotRegistrar. When a fee is owed, the attached value must cover it or @custom:reverts InsufficientValue. Whenever a position exists for the token and the NFT is leaving its prior recipient, the position recipient is rebound to the new holder so the deposit (when funded) and the lifecycle marker (when zero-amount) both follow the NFT. The escrow does not refund anyone at transfer time; the only path back to the locked deposit is for the current holder to release into escrow and wait the cooldown. Emits @custom:emits CrossTierFeePaid (non-registration) when a non-zero fee is credited to insurance, and credits any surplus value to the payer on the time-locked refund ledger via @custom:emits RefundCredited.

function chargeTransferFee(ChargeTransferFeeParams calldata params)
    external
    payable
    override
    onlyRegistrar
    returns (uint256 charged);

Returns

NameTypeDescription
chargeduint256Amount actually credited to insurance.

release

Releases a token into escrow and starts the withdrawal cooldown.

First step of the phased lifecycle. The caller must be the current NFT holder and the current position recipient (the field is rebound to the holder on every transfer that moves the name off the prior recipient), otherwise @custom:reverts NotRefundRecipient. Approved operators cannot release on behalf of the holder because the recipient field is keyed to the holder, not to any approval set, which keeps the deposit refund tied to the on-chain owner. The slot for tokenId must already hold a position (sentinel: position.recipient != address(0)); an unseeded slot triggers @custom:reverts DepositNotConfigured, and a position already in the released phase triggers

Note: reverts: AlreadyReleased. Zero-amount positions are still releasable so every minted name has a reachable lifecycle. The escrow must additionally be approved to move the NFT, otherwise @custom:reverts EscrowNotApproved. Emits @custom:emits NameReleased once the NFT is moved into custody.

function release(uint256 tokenId) external override nonReentrant;

withdraw

Credits the refundable deposit for a released token to the recipient's pending balance.

Second step of the phased lifecycle. The position must already be released (@custom:reverts NotReleased otherwise) and not yet claimed (@custom:reverts AlreadyClaimed on re-entry). Only the current position recipient (the address that released the name, which mirrored the NFT holder at that moment) may call this, otherwise @custom:reverts NotRefundRecipient, and block.timestamp must have reached withdrawAvailableAt, otherwise @custom:reverts WithdrawalTooEarly. Draws from the per-asset tokenReserved pool first and falls back to the shared insurance fund on shortfall; if even the combined balance is short, @custom:reverts InsufficientFunds. Funds are not transferred here, only credited to the pull-payment ledger. Emits

Note: emits: RefundWithdrawn once the credit lands, and @custom:emits InsuranceDraw whenever the insurance fund tops up a shortfall.

function withdraw(uint256 tokenId) external override nonReentrant;

claimWithdrawal

Pulls the caller's accumulated pending refund balance.

Final step of the phased lifecycle. Pull-payment isolation: each recipient owns an independent ledger entry, so a failing or reentrant receiver cannot block other users' withdrawals. The caller must have a positive pending balance, otherwise

Note: reverts: NoPendingWithdrawal; a failing native transfer triggers

function claimWithdrawal() external override nonReentrant returns (uint256 amount);

Returns

NameTypeDescription
amountuint256Native amount transferred to the caller.

pendingWithdrawal

Returns the pending refund balance owed to recipient.

function pendingWithdrawal(address recipient) external view override returns (uint256 amount);

Returns

NameTypeDescription
amountuint256Native amount currently credited to recipient and pullable via claimWithdrawal.

claimRefund

Pulls a single time-locked refund entry.

Caller must be the entry's recipient (@custom:reverts NotRefundRecipient otherwise), the entry must exist (@custom:reverts NoSuchRefundEntry on a deleted or unknown id), and block.timestamp must have reached availableAt (@custom:reverts RefundLocked otherwise). The entry is deleted before the transfer; a failing native transfer triggers @custom:reverts RefundFailed. Emits @custom:emits RefundClaimed.

function claimRefund(uint256 entryId) external override nonReentrant returns (uint256 amount);

Parameters

NameTypeDescription
entryIduint256Identifier of the entry to claim.

Returns

NameTypeDescription
amountuint256Native amount transferred to the caller.

claimRefundsBatch

Pulls multiple time-locked refund entries in one call.

Atomic: any invalid entry in the batch (wrong recipient, missing, or locked) reverts the entire call. The batch size is bounded by MAX_REFUND_PAGE_SIZE (@custom:reverts InvalidPageSize otherwise). Aggregates the per-entry amounts and transfers once; on transfer failure @custom:reverts RefundFailed. Emits

Note: emits: RefundClaimed once per entry.

function claimRefundsBatch(uint256[] calldata entryIds)
    external
    override
    nonReentrant
    returns (uint256 totalAmount);

Parameters

NameTypeDescription
entryIdsuint256[]List of entry identifiers to claim.

Returns

NameTypeDescription
totalAmountuint256Sum of the credited amounts transferred to the caller.

pendingRefundCount

Returns the number of pending refund entries owed to recipient.

function pendingRefundCount(address recipient) external view override returns (uint256 count);

pendingRefundIds

Returns up to limit pending refund entry ids for recipient, starting at offset.

Limit must be in (0, MAX_REFUND_PAGE_SIZE], otherwise @custom:reverts InvalidPageSize.

function pendingRefundIds(
    address recipient,
    uint256 offset,
    uint256 limit
)
    external
    view
    override
    returns (uint256[] memory entryIds);

pendingRefunds

Returns up to limit pending refund entries for recipient, paired with their entry ids.

Limit must be in (0, MAX_REFUND_PAGE_SIZE], otherwise @custom:reverts InvalidPageSize.

function pendingRefunds(
    address recipient,
    uint256 offset,
    uint256 limit
)
    external
    view
    override
    returns (uint256[] memory entryIds, RefundEntry[] memory entries);

refundEntry

Returns a single refund entry by id, or a zero-filled struct if the id is unknown.

function refundEntry(uint256 entryId)
    external
    view
    override
    returns (RefundEntry memory entry);

_creditRefund

Internal helper: allocate a new entryId and credit a refund to recipient.

Assigns the next monotonic entryId, stores the entry, appends to the recipient's enumeration array, and emits @custom:emits RefundCredited. The cooldown is read from the configured cooldown storage value; @custom:constant MAX_COOLDOWN bounds it so the cast to uint64 cannot truncate for any plausible block timestamp.

function _creditRefund(
    address recipient,
    uint256 amount,
    uint256 tokenId
)
    internal
    returns (uint256 entryId);

_removeRefundEntry

Internal helper: delete a refund entry and swap-pop its slot in the recipient's enumeration array.

function _removeRefundEntry(uint256 entryId, address recipient) internal;

reclaim

Transfers a released-and-claimed token from escrow custody to a new owner.

Hands the NFT back to the controller for re-registration. Only the configured controller may call this, otherwise @custom:reverts NotController, and the position must be both released and claimed, otherwise @custom:reverts NotReclaimable. Emits

Note: emits: NameReclaimed once custody is transferred.

function reclaim(
    uint256 tokenId,
    address newOwner
)
    external
    override
    onlyController
    nonReentrant;

Parameters

NameTypeDescription
tokenIduint256
newOwneraddressAddress of the new registrant taking over the name.

onERC721Received

Whenever an {IERC721} tokenId token is transferred to this contract via {IERC721-safeTransferFrom} by operator from from, this function is called. It must return its Solidity selector to confirm the token transfer. If any other value is returned or the interface is not implemented by the recipient, the transfer will be reverted. The selector can be obtained in Solidity with IERC721Receiver.onERC721Received.selector.

function onERC721Received(
    address,
    address,
    uint256 tokenId,
    bytes calldata
)
    external
    view
    override
    returns (bytes4 selector);

supportsInterface

function supportsInterface(bytes4 interfaceId) public view override returns (bool supported);

version

Returns implementation version.

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

Returns

NameTypeDescription
versionStringstringCurrent version string.

_registrar

Returns the configured registrar from the protocol registry.

function _registrar() internal view returns (IDotnsRegistrar registrar);

_onlyController

Restricts calls to the configured controller from the protocol registry.

function _onlyController() internal view;

_onlyRegistrar

Restricts calls to the configured registrar from the protocol registry.

function _onlyRegistrar() internal view;

_addReleasedToken

Adds a token to the released-token set if absent.

function _addReleasedToken(uint256 tokenId) internal;

_removeReleasedToken

Removes a token from the released-token set if present.

function _removeReleasedToken(uint256 tokenId) internal;

_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;