Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 27 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,41 +32,43 @@ If a run fails partway, re-run it from the Actions tab; the draft is updated rat

## Economics

dotNS uses a single tunable constant, written **D** throughout the protocol. D is the starting price used by PopRules and equals ten DOT at launch; governance can adjust it under the same gate as the upgrade authority. D is the only money quantity the protocol charges; everything else is a composition of D with zero.
Every price comes from one number. The base fee D is set in the native token and equals ten DOT at launch. A name's price depends only on its base length, the character count once a trailing number is set aside:

D plays two distinct roles. As a **deposit** it is the refundable lock a NoStatus user posts to register a NoStatus-tier label; the deposit is bound to the name, not to the depositor, so it travels with the NFT on every transfer and only unlocks when the current holder releases the name back to escrow. Transferring a funded name forfeits the deposit to the new holder, who inherits the locked D and the right to release later. As a **friction** charge it is the non-refundable amount a sender pays on a cross-tier downward or reach-floor transfer. The two flows are economically distinct: the deposit gates a count of names (one D per NoStatus name in existence), the friction gates the rate of tier laundering.
price(n) = D · 2^(9 − n) for a base length n below nine, and D for n of nine or more.

### Registration matrix
The price doubles for each character below nine and flattens to D from nine upward. Short names are scarce, so they cost more, and the curve follows how the count of available names falls as they shorten. A trailing number never changes the price, because it comes off before the length is measured, so `andrew` and `andrew01` both cost 8D. A name carries no digits or exactly two; any other trailing-digit count is rejected.

The public controller computes the registration charge as the greater of the owner-side price and the payer-to-owner downward friction; it does not add the two together. The single charge becomes a refundable deposit on a direct NoStatus registration and becomes non-refundable reserve funding on a cross-payer registration.
| Base length | Price |
|---|---|
| 9 or more | D |
| 8 | 2D |
| 7 | 4D |
| 6 | 8D |
| 5 | 16D |
| 4 | 32D |
| 3 | 64D |

| Owner tier | Reserved (stem ≤5) | PopFull-tier (stem 6-8, no digits) | PopLite-tier (stem 6-8, two digits) | NoStatus-tier (stem ≥9) |
|---|---|---|---|---|
| **NoStatus user** | rejected | rejected | rejected | direct: pays D into deposit |
| **PopLite user** | rejected | rejected | gateway-only; free | free |
| **PopFull user** | rejected | free | gateway-only; free | free |
| **Whitelisted address** | free | free | free | free |
### Bands and who pays

Cross-payer registrations pay the greater of the owner-side price and the transfer-floor amount into the reserve. Reserved labels remain forbidden on the cross-payer path because the owner-side gate still rejects them. Whitelist registrations go through the same commit-reveal pipeline as the public path.
Three bands share the one curve. Names of nine characters or more are open to anyone at the flat fee D. Names of six to eight characters are the premium band: only a verified person may register there, and they pay the curve for the length, 8D, 4D or 2D. Personhood buys access to the premium band, not a discount inside it. Names of five characters or fewer are reserved to governance; no user registers them, and governance releases them itself at the price their length sets, into the treasury it already controls.

### Transfer matrix
Every caller pays the same curve for a given length. Each wallet gets one free name through the personhood gateway, the unpriced lane: the gateway waives the price and issues that grant per wallet, and the name can be any length. The bands above are the public curve; the gateway lane does not apply them and only refuses the governance-reserved stems of five characters or fewer. Governance releasing a reserved name charges itself that length's price, which settles in a circle into its own treasury and nets nothing.

The registrar consults PopRules for the transfer floor. A transfer pays D whenever the recipient's tier is strictly below the sender's, or whenever the recipient cannot reach the label's required tier. A stale PopFull-tier name landing with a PopLite holder, for example, can still owe friction even when the holder-to-holder move otherwise looks same-tier. Same-tier and upward transfers between holders of the label's own class are free of friction.
### Deposits and protocol fees

The deposit, when present, is bound to the name and rides with it on every transfer. The escrow position is rebound to the new holder rather than refunded; only releasing the name back to escrow ever unlocks the locked D. Transferring a funded name is therefore a real forfeiture: the sender hands the locked deposit over to the recipient along with the NFT.
Registering a name under your own key locks a refundable deposit equal to the name's price. The deposit is bound to the name rather than the depositor, so it travels with the token on every transfer and unlocks only when the current holder releases the name back to escrow. Holding many names ties up capital on the same curve, and the cost rises with scarcity because the price does.

| Sender → Recipient | Friction (to insurance) | Deposit movement |
|---|---|---|
| NoStatus → NoStatus (same tier) | 0 | Travels with the name; position rebinds to recipient |
| NoStatus → PopLite or PopFull | 0 | Travels with the name; position rebinds to recipient |
| PopLite → NoStatus | D | Any inherited deposit travels with the name |
| PopLite → PopLite (same) | 0 | Any inherited deposit stays bound to the name |
| PopLite → PopFull (upward) | 0 | Any inherited deposit stays bound to the name |
| PopFull → NoStatus | D | Any inherited deposit travels with the name |
| PopFull → PopLite (downward) | D | Any inherited deposit stays bound to the name |
| PopFull → PopFull (same) | 0 | Any inherited deposit stays bound to the name |
A fee is non-refundable. Two things pay a fee instead of a deposit: a name someone else pays for, and a transfer. Fees flow into one protocol fee pot that only accumulates. Governance can route the pot to one of a fixed pair, a burn or the on-chain treasury, and to nowhere else. A holder's own deposit is their money held in trust and is never swept into fees.

The friction is constant and additive across downward hops. Every step that crosses a tier boundary downward charges D independently, so routing a name through intermediary tiers never costs less than the equivalent direct transfer. Laundering pays at least as much as the route it tries to avoid. Because the deposit follows the NFT, a NoStatus user cannot recover their D by handing the name to a fresh address and registering again; the only path back to D is releasing the current name into escrow. This binds Sybil cost to one D per live NoStatus name in existence, independent of how often names change hands.
### Transfers re-price at the name's own length

Moving a name re-prices it from scratch at its own length on every move. Passing a six-character name to a wallet that could never have registered it costs 8D, the name's own price, not the flat D floor. `andrew` and `andrew01` re-derive to the same 8D. The exit price equals what the name was worth to acquire, so there is no cheap way to hand a scarce name to a party who could not have earned it. A move between parties who both clear the name's band costs nothing, and the fee, when one is owed, settles into protocol fees.

The deposit, when present, is bound to the name and rides with it on every transfer. The escrow position is rebound to the new holder rather than refunded; only releasing the name back to escrow ever unlocks the locked deposit. Transferring a funded name hands the locked deposit to the recipient along with the token.

### What governance controls

Everything hangs off D. Governance can move D, but only inside a fixed band and by at most a set multiple per vote, so it can neither drop the price to zero, which would free every short name, nor raise it to an extractive level, and any change is legible several votes ahead. Governance sets how long the interval on the free grant runs, and chooses where fees go, a burn or the treasury, from that fixed pair. Nothing in that surface lets governance seize, reassign or destroy a name anyone already holds.

### Refund and cooldown model

Expand Down
73 changes: 29 additions & 44 deletions contracts/escrow/DotnsNameEscrow.sol
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,10 @@ contract DotnsNameEscrow is
/// @notice Reverse lookup into `_releasedTokens` (one-based) for O(1) remove-by-swap.
mapping(uint256 tokenId => uint256 indexPlusOne) private _releasedIndexPlusOne;

/// @notice Cumulative balance of cross-tier fees held against unreleased shortfalls.
/// @dev 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;
/// @notice Cumulative balance of non-refundable protocol fees; only accumulates.
/// @dev Credited by cross-paid registration fees and transfer fees. Never debited: protocol
/// fees do not back refunds, which draw solely on the per-asset reserve.
uint256 public protocolFees;

/// @notice Pull-payment ledger storing each recipient's claimable refund balance.
/// @dev Per-recipient isolation ensures a failing or reentrant receiver cannot block other
Expand Down Expand Up @@ -205,8 +204,8 @@ contract DotnsNameEscrow is
ReleasePosition storage position = _positions[params.tokenId];

// Use `recipient` as the "is this slot funded?" sentinel so zero-amount
// positions (seeded by free PopFull / PopLite registrations) still count
// as funded and cannot be re-seeded with a different recipient.
// positions (seeded by cross-paid registrations, which pay a fee rather than a deposit)
// still count as present and cannot be re-seeded with a different recipient.
require(position.recipient == address(0), PositionAlreadyFunded(params.tokenId));
require(!position.released, AlreadyReleased(params.tokenId));

Expand All @@ -228,15 +227,15 @@ contract DotnsNameEscrow is
}

/// @inheritdoc IDotnsNameEscrow
function depositInsurance(InsuranceDepositParams calldata params)
function depositProtocolFee(ProtocolFeeDepositParams calldata params)
external
payable
override
onlyController
{
require(msg.value > 0, InvalidAmount());

insuranceFund += msg.value;
protocolFees += msg.value;

emit CrossTierFeePaid(
params.tokenId,
Expand Down Expand Up @@ -265,7 +264,7 @@ contract DotnsNameEscrow is

address priorRecipient = position.recipient;

uint256 fee = params.reachFloor;
uint256 fee = params.transferFee;
require(msg.value >= fee, InsufficientValue());

// Deposits follow the NFT, not the depositor. When the position is funded the locked
Expand All @@ -276,20 +275,19 @@ contract DotnsNameEscrow is
position.recipient = params.to;
}

if (fee > 0) {
insuranceFund += fee;
}

charged = fee;

emit CrossTierFeePaid(
params.tokenId,
params.payer,
params.to,
fee,
/* isRegistration */
false
);
if (fee > 0) {
protocolFees += fee;
emit CrossTierFeePaid(
params.tokenId,
params.payer,
params.to,
fee,
/* isRegistration */
false
);
}

uint256 overpayment = msg.value - fee;
if (overpayment > 0) {
Expand All @@ -305,8 +303,8 @@ contract DotnsNameEscrow is

ReleasePosition storage position = _positions[tokenId];
// Recipient is the canonical "is this position present?" sentinel; zero-amount positions
// seeded for free PopFull / PopLite registrations are still releasable so every minted
// name has a reachable lifecycle.
// seeded for cross-paid registrations are still releasable so every minted name has a
// reachable lifecycle.
require(position.recipient != address(0), DepositNotConfigured(tokenId));
require(!position.released, AlreadyReleased(tokenId));

Expand Down Expand Up @@ -359,30 +357,17 @@ contract DotnsNameEscrow is
// `position.recipient == msg.sender` was just enforced above, so reuse the local in place
// of an extra warm SLOAD.
address recipient = msg.sender;
uint256 reserved = tokenReserved[asset];

uint256 fromRefundable;
uint256 fromInsurance;
if (reserved >= owed) {
fromRefundable = owed;
// fromInsurance is already 0 from default initialization.
} else {
fromRefundable = reserved;
fromInsurance = owed - reserved;
require(
insuranceFund >= fromInsurance,
InsufficientFunds(tokenId, owed, reserved + insuranceFund)
);
}

// The per-asset reserve backs every refundable deposit; protocol fees are non-refundable
// and never cover a refund.
require(
tokenReserved[asset] >= owed, InsufficientFunds(tokenId, owed, tokenReserved[asset])
);

// Effects: mutate state only after all checks have passed.
position.claimed = true;
position.amount = 0;
tokenReserved[asset] -= fromRefundable;
if (fromInsurance > 0) {
insuranceFund -= fromInsurance;
emit InsuranceDraw(tokenId, fromInsurance);
}
tokenReserved[asset] -= owed;

_pendingWithdrawals[recipient] += owed;

Expand Down
Loading
Loading