feat: price dotNS names on a scarcity curve - #208
Conversation
Replace flat pricing with a geometric curve on base length: the price doubles for each character below nine and flattens to the base fee from nine upward. Every caller pays the curve; personhood gates access to the premium band rather than discounting it. Transfers re-price at the name's own length. Rename the escrow insurance fund to protocol fees, a non-refundable pot that only accumulates, and remove the dead refund-shortfall draw so refunds draw solely on the per-asset reserve.
CI Summary
Deploy ContractsDeployment FailedErrors Found:
Last 50 lines of output: |
…in state The protocol-fee conservation invariant mirrored the on-chain balance delta into its own tracker, so it compared the escrow balance against an echo of itself and could never catch a miscredit. The handler now accumulates the independently computed charge and quoted transfer fee, and a new invariant asserts those tracked inflows equal the on-chain protocol fees. Dead insurance-draw scaffolding, its recorded-log parsing, and stale insurance wording are removed to match the protocol-fee model.
…in state (#210) ## Description Follow-up to the scarcity-pricing change. The protocol-fee conservation invariant used to read the on-chain protocol-fee balance before and after each action and fold the difference into its own tracker. Because both sides came from the same source, the check compared the balance against an echo of itself and would have passed even if a caller had been charged the wrong amount. The handler now accumulates the fee it independently expects (the charge on a cross-paid registration, the quoted fee on a payable transfer), so the invariant proves the escrow credited exactly what the caller was asked to pay. A second invariant asserts that the tracked inflows sum to the on-chain protocol-fee balance, which holds because protocol fees only ever accrue. ## Type - [ ] Bug fix - [ ] Feature - [ ] Breaking change - [ ] Documentation - [ ] Chore - [ ] Refactor - [ ] Security ## Scope - [ ] Registration - [ ] Resolver - [ ] Store - [ ] Proof of Personhood - [ ] Deployment scripts - [x] Tests ## Related Issues Stacked on #208. ## Fixes ## Checklist ### Code - [x] Follows project style - [x] `forge build` passes - [x] `forge test` passes - [x] No new compiler warnings ### Testing - [ ] New tests added for changed behavior - [ ] Fuzz tests added where applicable - [x] Invariant tests verified ### Security - [x] No new `selfdestruct` or `delegatecall` - [x] Access control reviewed - [x] No storage layout conflicts (for upgradeable contracts) ### Documentation - [x] NatSpec updated on changed interfaces - [x] README updated if needed ### Breaking Changes - [x] No breaking changes - [ ] Breaking changes documented below **Breaking changes:** ## How to test ```bash forge test --mc DotnsNameEscrowInvariantTest ``` ## Notes Stacks on #208; review and merge that first. The new pricing-invariant suite is tracked separately in #209.
The base-fee setter rejects a value large enough to overflow the scarcity multiplier, so short-name pricing and transfers cannot be knocked offline by an oversized fee. Cross-payer registration charges the name's own curve price directly; the floor comparison that could never change the amount is removed. The transfer-fee event fires only when a fee is taken, so a zero-fee position rebind emits nothing. The escrow transfer-fee parameter and its registrar call sites read transferFee to match the value they carry, and the transfer-floor and fee-parameter documentation is completed.
The free name is claimed per wallet through the personhood gateway, which waives the price. The gateway lane accepts a name of any length and applies no public band gate; it only refuses the governance-reserved stems of five characters or fewer.
|
I would keep this PR on hold and have a broader discussion on this argument. This can be taken as a concrete proposal. |
re-gius
left a comment
There was a problem hiding this comment.
Requesting changes based on the design proposed in #209 (comment).
The curve itself is fine, and the work here isn't wasted. Under the agreed design the pricing function moves into a standalone economics contract that governance owns and can replace, and a modified version of this curve can become its first proposal.
|
On the standalone economics contract: I've put the case for keeping pricing in PopRules and adding commitment-to-version binding in #209 (comment) |
re-gius
left a comment
There was a problem hiding this comment.
Thanks for your effort and all these changes, it's much closer to what I was expecting. However, there are some fundamental design choices I would still change:
- v1 must reproduce today's on-chain prices: remove the scarcity pricing exponential model from this PR. Default deposit for v1 will be 10 DOT constant for all 9+ char names like today, while your scarcity model (and maybe some others) will be candidates for replacement in v2. We don't have any clarity on that yet.
- Bind the pricing version and enforce it: we may add
currentVersion()tocommitand make sure reveal requires a version check - Allow governance to upgrade the economics:
DotnsCostModelRegistry.sol:27 and :37 areonlyOwnerwith nooriginIsRoot()check. You can do what you did already for whitelisting.
Description
Prices every name by its length on one geometric curve instead of a flat fee. The price doubles for each character below nine and halves for each character above it, down to a floor F, so short scarce names cost the most and long abundant names bottom out at F rather than zero. A trailing two-digit number never changes the price, because it comes off before the length is measured.
Three bands share the curve. Names of nine characters or more are open to anyone at the curve price for the length. Names of six to eight characters are the premium band, registrable on the public paid path only by a verified person, who pays the curve for the length. Names of five characters or fewer are never sold on the curve; they enter circulation only through the reserved path, at zero base cost. Personhood only unlocks the premium band; the price there is the same curve everyone pays.
Names shorter than nine are closed on the public paid path by default. Governance opens them with a single switch (
shortNamesEnabled); while it is off, a paid registration below nine reverts. The switch gates the public paid path alone: the gateway free grant andregisterReserveddo not consult it.Registering under your own key locks a refundable deposit equal to the name's curve price. Fees, paid when someone else pays for a name or on a qualifying transfer, are non-refundable and gather in one protocol fee pot that only accumulates; it backs no refund and has no burn, sweep, or withdrawal. Moving a name re-prices it at its own length when the recipient cannot clear the band or the move is a personhood downgrade, so a scarce name cannot be handed cheaply to a party who could never have registered it.
The wei amounts live in a swappable cost model resolved through the protocol registry. Changing the curve deploys and registers a new model version; earlier versions stay priceable, and a commitment binds both a price ceiling (
maxPrice) and thepricingVersion, so the reveal prices at the committed version and a model change between commit and reveal cannot move the amount. Governance can also point the current version back at an earlier registered model.D and F are fixed for the life of a model; the model constructor enforces
0 < F <= Dand an anti-overflow ceiling, so the curve can never invert or price at zero. A stateful invariant proves the cost-model registry stays coherent under any sequence of registrations and version moves: every registered version keeps returning the same amount, and the current pointer always resolves to its registered model.The formula
nis the base length: the characters left once a trailing two-digit number is set aside.Dis the base fee, the price at nine characters;Fis the floor. Below nine, dropping a character doubles the price; from nine upward, adding a character halves it, until the floor F stops it reaching zero.Every length, at the launch seed D = 10 DOT, F = 0.1 DOT
Only names of nine characters or more are on public sale by default; base lengths below nine sit behind the short-name switch.
At base length 16 the curve value is D/128 = 0.078 DOT, below the floor, so every name of 16 characters or more settles at F. Base lengths of five and below sit further up the same curve but are never sold: governance issues reserved names through the reserved path at zero base cost.
Type
Scope
Related Issues
Transfer-fee cleanup that composes with soulbinding is tracked in #212.
Fixes
Closes #209.
Checklist
Code
forge buildpassesforge testpassesTesting
Security
selfdestructordelegatecallDocumentation
Breaking Changes
Breaking changes: the escrow fee pot is renamed from the insurance fund to protocol fees.
insuranceFund()becomesprotocolFees(),depositInsurancebecomesdepositProtocolFee, and theInsuranceDrawshortfall event and its top-up path are removed; refunds are backed by the per-asset reserve alone.PopRules.initializenow takes only the protocol registry: the base fee and floor no longer live onPopRulesbut seed aDotnsScarcityPricingmodel, registered in a newDotnsCostModelRegistryunder theCOST_MODELprotocol-registry key. Consumers reading the fee balance or the price must move to the new names.How to test
Notes
Governance is trusted for economic policy; the contract enforces only coherence (
0 < F <= D, no overflow, no zero price) at model construction. There is no on-chain band or timelock on D by design, so any rate or notice guarantee is the governance owner's. Fees accumulate in one pot with no withdrawal path; there is no burn or treasury routing in these contracts.