DotnsNameEscrow
Inherits: Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuardTransient, ERC165Upgradeable, IERC721Receiver, IDotnsNameEscrow
Title: Dotns Name Escrow
Holds refundable deposits for registered names and manages the release/reclaim lifecycle. @custom: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-withdraw delay, not the long-lived deposit lock
and not the reclaim boundary (see redeemWindow), 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
MAX_REDEEM_WINDOW
Upper bound on the configurable redeem window.
The redeem window is a different quantity from the cooldown: it is the period after
release in which only the previous holder may act, and it gates reclaim rather than
withdrawal. The bound limits how long policy can hold a released name out of
circulation, and keeps the cast to uint64 in release well below saturation.
uint256 public constant MAX_REDEEM_WINDOW = 30 days
State Variables
protocolRegistry
The protocol registry for resolving sibling contract addresses.
IDotnsProtocolRegistry public protocolRegistry
cooldown
Delay after release before the deposit withdrawal may be credited.
Forces a delay between release and withdraw. It does not bound reclaim: the
release-to-reclaim boundary is redeemWindow, a separate and longer quantity. Also
supplies the per-entry clock for time-locked refund credits, which is why raising it
would slow every refund path and not just the deposit one.
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
redeemWindow
Period after release during which only the previous holder may act.
Distinct from cooldown. Inside this window the holder may redeem the name back
and nobody else may take it (available reports false); once it elapses reclaim
becomes permissionless and any unwithdrawn deposit is credited to the recipient
rather than stranded. Appended after the pre-existing variables and paid for out of
__gap, so the layout of everything above is untouched.
uint256 public redeemWindow
__gap
Reserved storage space to allow for layout changes in the future.
Reduced from 50 to 49 when redeemWindow was appended above, keeping the total
storage footprint of this contract unchanged.
uint256[49] 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. redeemWindowSeconds is forwarded to @custom:function
updateRedeemWindow, which rejects a zero value (@custom:reverts InvalidRedeemWindow)
and any value above @custom:constant MAX_REDEEM_WINDOW (@custom:reverts
RedeemWindowTooLong), and emits @custom:emits RedeemWindowUpdated.
function initialize(
IDotnsProtocolRegistry registry,
uint256 cooldownSeconds,
uint256 redeemWindowSeconds
)
external
initializer;
Parameters
| Name | Type | Description |
|---|---|---|
registry | IDotnsProtocolRegistry | Protocol registry used to resolve registrar and controller addresses. |
cooldownSeconds | uint256 | Delay after release before the deposit withdrawal may be credited. |
redeemWindowSeconds | uint256 | Period after release in which only the previous holder may act. |
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;
updateRedeemWindow
Updates the redeem window for future releases.
Owner-only. Affects only releases recorded after this call; positions already released
keep the redeemableUntil snapshot taken at their release time. newRedeemWindow must
be non-zero, otherwise @custom:reverts InvalidRedeemWindow, and must not exceed the
contract's MAX_REDEEM_WINDOW upper bound, otherwise @custom:reverts
RedeemWindowTooLong; the bound limits how long policy can hold a released name out of
circulation and protects the uint64 cast in release from truncation. Emits
Note:
emits: RedeemWindowUpdated with the prior and new values.
This is also the post-upgrade seeding hook: pair it with upgradeToAndCall so an
upgraded proxy never runs with an unseeded window.
function updateRedeemWindow(uint256 newRedeemWindow) 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
| Name | Type | Description |
|---|---|---|
count | uint256 | Number 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
| Name | Type | Description |
|---|---|---|
asset | address | Asset 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
| Name | Type | Description |
|---|---|---|
start | uint256 | Start index into the released-token set. |
limit | uint256 | Maximum 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
| Name | Type | Description |
|---|---|---|
recipient | address | Address 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
| Name | Type | Description |
|---|---|---|
charged | uint256 | Amount 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.
Release stamps two independent clocks. withdrawAvailableAt (release + cooldown)
opens the deposit withdrawal; redeemableUntil (release + redeemWindow) closes the
holder's exclusive redeem phase and opens permissionless reclaim. Both are snapshots
so later policy changes never move an in-flight position. A release attempted while
redeemWindow is unseeded triggers @custom:reverts InvalidRedeemWindow.
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;
_settleDeposit
Moves a position's outstanding deposit onto the recipient's pull-payment balance.
Shared by @custom:function withdraw, where the recipient pulls the deposit themselves,
and by @custom:function reclaim, where a third party takes the name and the deposit is
settled on the departing holder's behalf. Both credit the same ledger and neither
transfers value, so the accounting is identical and lives here once. Draws from the
per-asset tokenReserved pool first and tops up from insuranceFund on shortfall;
Note: reverts: InsufficientFunds when even the combined balance cannot cover the amount owed. Emits @custom:emits RefundWithdrawn, and @custom:emits InsuranceDraw whenever the insurance fund contributes. A zero-amount position is a no-op: it writes nothing and emits nothing, which keeps the free-registration lifecycle free of meaningless ledger entries and events.
function _settleDeposit(
ReleasePosition storage position,
uint256 tokenId,
address recipient
)
private;
Parameters
| Name | Type | Description |
|---|---|---|
position | ReleasePosition | Storage pointer to the position being settled. |
tokenId | uint256 | |
recipient | address | Address credited with the deposit. Always the position recipient. |
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
| Name | Type | Description |
|---|---|---|
amount | uint256 | Native 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
| Name | Type | Description |
|---|---|---|
amount | uint256 | Native 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
| Name | Type | Description |
|---|---|---|
entryId | uint256 | Identifier of the entry to claim. |
Returns
| Name | Type | Description |
|---|---|---|
amount | uint256 | Native 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
| Name | Type | Description |
|---|---|---|
entryIds | uint256[] | List of entry identifiers to claim. |
Returns
| Name | Type | Description |
|---|---|---|
totalAmount | uint256 | Sum 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 token whose redeem window has elapsed 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 released with redeemableUntil reached, otherwise @custom:reverts
NotReclaimable. Emits @custom:emits NameReclaimed once custody is transferred.
Reclaim does not require the deposit to have been withdrawn first. If the position
still holds value, this call settles it: the amount is debited from tokenReserved
(topping up from the insurance fund on shortfall, @custom:reverts InsufficientFunds if
even the combined balance is short) and credited to the previous recipient's
pull-payment balance, claimable through @custom:function claimWithdrawal with no
deadline. That is what keeps a name recyclable when its previous holder never returns:
the value follows them, the name does not wait for them. Emits @custom:emits
RefundWithdrawn on settlement, and @custom:emits InsuranceDraw when the insurance fund
tops up a shortfall.
function reclaim(
uint256 tokenId,
address newOwner
)
external
override
onlyController
nonReentrant;
Parameters
| Name | Type | Description |
|---|---|---|
tokenId | uint256 | |
newOwner | address | Address of the new registrant taking over the name. |
redeem
Returns a released token to its previous holder during the redeem window.
The undo for an accidental release, and the reason the redeem window exists. Only the
position recipient may call this (@custom:reverts NotRefundRecipient otherwise), the
position must be released and not yet withdrawn, and block.timestamp must still be
below redeemableUntil; a position failing any of those is not redeemable and
Note: reverts: NotRedeemable. No value moves. The position keeps its recipient, asset and amount, so the deposit stays locked exactly as it was before the release and the name returns to its pre-release state, releasable again later on a fresh pair of clocks. Excluding withdrawn positions is deliberate: a holder who has already pulled the deposit would otherwise recover the name without it being deposit-backed, breaking the one-deposit- per-live-name bound. The choice is therefore exclusive — take the value back, or take the name back. Emits @custom:emits NameRedeemed once custody returns.
function redeem(uint256 tokenId) external override nonReentrant;
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
| Name | Type | Description |
|---|---|---|
versionString | string | Current 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;