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
3 changes: 0 additions & 3 deletions .github/workflows/4naly3er.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,6 @@ jobs:
with:
python-version: "3.12"

- name: Apply OZ payable patches
run: bash scripts/shell/apply-oz-patches.sh

- env:
FOUNDRY_DISABLE_NIGHTLY_WARNING: "1"
run: forge install && forge build --build-info
Expand Down
5 changes: 2 additions & 3 deletions .github/workflows/contract-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,8 @@ jobs:
# Forge just recompiles whatever the content hashes say is dirty, so a partial
# match via `restore-keys` still saves most of a cold `via_ir` build. This has
# to sit after `bun install`: its postinstall (`setup.bash`) force-reclones
# `lib/`'s pinned dependencies and reapplies the OZ patches on top, so hashing
# `lib/` any earlier would key the cache off content these steps are about to
# replace.
# `lib/`'s pinned dependencies, so hashing `lib/` any earlier would key the
# cache off content these steps are about to replace.
#
# Forge also drops persisted fuzz/invariant/test failure state under
# `cache/fuzz`, `cache/invariant`, and `cache/test-failures`, and replays it
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/deploy-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,6 @@ jobs:
curl -sSL "https://github.com/rust-lang/mdBook/releases/download/v${MDBOOK_VERSION}/mdbook-v${MDBOOK_VERSION}-x86_64-unknown-linux-gnu.tar.gz" | tar -xz -C /usr/local/bin

- run: forge install
- name: Apply OZ payable patches
run: bash scripts/shell/apply-oz-patches.sh
- run: forge build

- name: Generate docs
Expand Down
39 changes: 3 additions & 36 deletions .github/workflows/gas-report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@ name: Gas Report
# job is gated on `github.event.action == 'closed'` and does NOT check
# out PR-head code, so no untrusted code executes under that context.
#
# The matrix `generate` shard that pulls the PR head's `patches/` runs
# under the `pull_request` trigger (not `pull_request_target`), so its
# checkout of PR-head code does not see repository secrets.
#
# Adding other `pull_request_target` event types or any step that
# checks out PR-head code under that trigger would let a malicious PR
# exfiltrate repo secrets. Re-review carefully if widened.
Expand Down Expand Up @@ -89,45 +85,16 @@ jobs:
with:
python-version: "3.12"

# Sidecar checkout of the PR head's `patches/` and apply script. The
# master-branch matrix shard is checked out at `master`, which does not yet
# carry the OZ payable patches needed for the registrar transferFrom
# overrides. Sourcing them from the PR head keeps the two shards
# building against the same vendored OZ tree. Removed once the patches
# land on master.
- name: Sidecar checkout for OZ patches
if: matrix.target.id == 'master'
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
path: pr-head-sidecar
sparse-checkout: |
patches
scripts/shell/apply-oz-patches.sh
sparse-checkout-cone-mode: false

- name: Apply OZ patches (master shard only)
if: matrix.target.id == 'master'
run: |
rm -rf patches
cp -R pr-head-sidecar/patches ./patches
mkdir -p scripts/shell
cp pr-head-sidecar/scripts/shell/apply-oz-patches.sh scripts/shell/apply-oz-patches.sh
chmod +x scripts/shell/apply-oz-patches.sh
bash scripts/shell/apply-oz-patches.sh

- run: bun install

# `cache/` and `out/` are forge's own incremental-compile state: it hashes each
# source file and only recompiles what changed since the hash it last saw. A
# restored cache from an unrelated commit is always safe to build on top of.
# Forge just recompiles whatever the content hashes say is dirty, so a partial
# match via `restore-keys` still saves most of a cold `via_ir` build. This has
# to sit after `bun install`, not just after the master shard's patch step
# above: `bun install`'s postinstall (`setup.bash`) force-reclones `lib/`'s
# pinned dependencies for both shards and reapplies the OZ patches on top,
# so hashing `lib/` any earlier would key the cache off content that step
# is about to replace.
# to sit after `bun install`: its postinstall (`setup.bash`) force-reclones
# `lib/`'s pinned dependencies for both shards, so hashing `lib/` any earlier
# would key the cache off content that step is about to replace.
#
# Forge also drops persisted fuzz/invariant/test failure state under
# `cache/fuzz`, `cache/invariant`, and `cache/test-failures`, and replays it
Expand Down
5 changes: 2 additions & 3 deletions .github/workflows/push_checking.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,8 @@ jobs:
# Forge just recompiles whatever the content hashes say is dirty, so a partial
# match via `restore-keys` still saves most of a cold `via_ir` build. This has
# to sit after `bun install`: its postinstall (`setup.bash`) force-reclones
# `lib/`'s pinned dependencies and reapplies the OZ patches on top, so hashing
# `lib/` any earlier would key the cache off content these steps are about to
# replace.
# `lib/`'s pinned dependencies, so hashing `lib/` any earlier would key the
# cache off content these steps are about to replace.
#
# Forge also drops persisted fuzz/invariant/test failure state under
# `cache/fuzz`, `cache/invariant`, and `cache/test-failures`, and replays it
Expand Down
3 changes: 0 additions & 3 deletions .github/workflows/slither.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,6 @@ jobs:
with:
python-version: "3.12"

- name: Apply OZ payable patches
run: bash scripts/shell/apply-oz-patches.sh

- env:
FOUNDRY_DISABLE_NIGHTLY_WARNING: "1"
run: forge install && forge build --build-info
Expand Down
24 changes: 12 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,29 +81,29 @@ Names of five characters or fewer are never sold on the public path, which rejec

### Worked examples

The path decides whether the amount is a refundable deposit, a non-refundable fee, or nothing at all. Every amount below is the flat launch deposit of 10 DOT.
The path decides whether the amount is a refundable deposit or nothing at all. Every amount below is the flat launch deposit of 10 DOT.

| Name | Base length | Path | Amount | Held as |
|---|---|---|---|---|
| `gavinwood` | 9 | public, own key | 10 DOT | refundable deposit |
| `gavinwood` | 9 | public, someone else pays | 10 DOT | protocol fee |
| `gavinwood` | 9 | public, someone else pays | 10 DOT | refundable deposit, owned by the name's owner |
| `andrewsays` | 10 | public, own key | 10 DOT | refundable deposit |
| `andrew` | 6 | public, own key (switch on, proof of personhood) | 10 DOT | refundable deposit |
| `alicebob42` | 10 | public, own key | 10 DOT | refundable deposit |
| any six-to-eight name | 6 to 8 | gateway path | none | no deposit, no fee |
| `andrew`, moved to a wallet that cannot clear its band | 6 | transfer | 10 DOT | protocol fee |
| `andrew`, moved to a wallet that cannot clear its band | 6 | transfer | none | free; the deposit moves with the name |

### Deposits and protocol fees
### Deposits

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 to you, so it travels with the token on every transfer and unlocks only when the current holder releases the name back to escrow.

A name someone else pays for, and a transfer, pay a non-refundable fee instead. When a third party pays for another wallet's registration, the charge is the same owner-side price, with no separate friction added; it routes to a single protocol fee pot, and the owner's escrow slot is seeded with a zero amount so the release lifecycle stays reachable. A gateway grant carries neither a deposit nor a fee. The protocol fee pot only ever grows: it backs no refunds, and nothing burns, sweeps, or withdraws from it. Refunds draw solely on the separate per-asset reserve that deposits fund. A holder's own deposit is their money held in trust and is never moved into fees.
A name someone else pays for locks the same deposit, owned by the name's owner: a third party who pays for another wallet's registration gives that owner the deposit, which the owner collects on release. A gateway grant carries no deposit. Refunds draw solely on the per-asset reserve that deposits fund, and a holder's deposit is their money held in trust. The escrow also holds a protocol fee balance from the transfer fees earlier releases charged. Nothing credits it, and only a Root call, `withdrawProtocolFees`, moves it out, to an address governance chooses; deposits, pending withdrawals and refund entries stay out of its reach.

### Transfers re-price at the name's own length
### Transfers are free

Publicly registered names transfer freely and charge the name's own price, but only in two cases: the recipient cannot clear the name's band, or the move is a personhood downgrade, where the recipient's tier is lower than the sender's. Passing a six-character name to a wallet that could never have registered it costs the name's own price, so there is no cheap way to hand a band-gated name to a party who could not have earned it. A move between two wallets that both clear the band, and a move to the same address, cost nothing. The fee, when one is owed, settles into the protocol fee pot. The deposit, when present, rides with the name: the escrow position rebinds to the new holder rather than refunding, and only releasing the name back to escrow unlocks the locked deposit.
Every transferable name moves for free, whatever its length and the holders' tiers, and a transfer that carries value reverts. The bands apply at registration only: once registered, a name can go to any wallet. The deposit, when present, rides with the name: the escrow position rebinds to the new holder rather than refunding, and only releasing the name back to escrow unlocks the locked deposit.

Names minted through the gateway pallet are soulbound: they stay bound to the person who earned them and cannot be transferred at all. Any transfer of a gateway-issued name reverts, and quoting a transfer fee for one reverts rather than returning a price. Everything else about the name works normally, so its owner still sets records, issues subnames, and manages the name.
Names minted through the gateway pallet are soulbound: they stay bound to the person who earned them and cannot be transferred at all. Any transfer of a gateway-issued name reverts. Everything else about the name works normally, so its owner still sets records, issues subnames, and manages the name.

A bare transfer does not reset the name's records. Registration and reclaim from escrow both point the registry record back at the default reverse resolver, but a direct ERC-721 `transferFrom` never calls the registry, so a name sold privately arrives still pointing at the seller's resolver. A buyer should overwrite the records they care about rather than assume they start clean.

Expand All @@ -125,7 +125,7 @@ Both are stamped at release time, so a governance change never moves the clocks
- `redeem` returns the token and moves no value. The position keeps its recipient, asset and amount, so the deposit stays locked and the name lands back in its exact pre-release state, releasable again later on a fresh pair of clocks. This is the undo for an accidental release.
- `withdraw` credits the deposit and forfeits the right to redeem. A holder paid for the name cannot also take it back, otherwise they would hold a name no deposit backs.

Once `redeemableUntil` passes, `reclaim` is permissionless through the ordinary commit-reveal path, whether or not the previous holder ever withdrew. If the position still holds value, reclaim settles it: the amount is credited to the previous holder's pull-payment balance and stays claimable with no deadline. The value follows the departing holder; the name does not wait for them. Cross-paid registrations and grant-backed reserved registrations seed zero-amount positions, so those names have nothing to withdraw and nothing to settle, but they move through release, redeem and reclaim like any other name.
Once `redeemableUntil` passes, `reclaim` is permissionless through the ordinary commit-reveal path, whether or not the previous holder ever withdrew. If the position still holds value, reclaim settles it: the amount is credited to the previous holder's pull-payment balance and stays claimable with no deadline. The value follows the departing holder; the name does not wait for them. Grant-backed reserved registrations seed zero-amount positions, so those names have nothing to withdraw and nothing to settle, but they move through release, redeem and reclaim like any other name.

Gateway-issued (soulbound) names never enter this lifecycle. Releasing a name transfers it into escrow custody, and a soulbound name reverts on any transfer, so it holds no position and has no release, redeem, or reclaim path. It stays with the person who earned it.

Expand All @@ -135,11 +135,11 @@ A pricing model's values are fixed for its life: the flat model's deposit, or th

### What governance controls

Governance chooses a model's values when it deploys one, and the contracts hold them coherent and nothing more: a flat deposit above zero; for the scarcity candidate, D and F above zero, F no greater than D, and D within a ceiling that keeps the multiplication below nine characters from overflowing. Beyond these checks there is no cap on how high or low the amounts go and no limit on how fast they move, so any rate limit or advance notice comes from the governance process rather than from these contracts. Governance also opens or closes the short-name market with the switch, tunes the release cooldown within its one-hour bound, sets the redeem window between 1 and 30 days, and sets the gateway's reservation duration. None of these controls lets governance seize, reassign, or destroy a name anyone already holds.
Governance chooses a model's values when it deploys one, and the contracts hold them coherent and nothing more: a flat deposit above zero; for the scarcity candidate, D and F above zero, F no greater than D, and D within a ceiling that keeps the multiplication below nine characters from overflowing. Beyond these checks there is no cap on how high or low the amounts go and no limit on how fast they move, so any rate limit or advance notice comes from the governance process rather than from these contracts. Governance also opens or closes the short-name market with the switch, tunes the release cooldown within its one-hour bound, sets the redeem window between 1 and 30 days, and sets the gateway's reservation duration. Root can also move the protocol fee balance out of the escrow. None of these controls lets governance seize, reassign, or destroy a name anyone already holds.

### Refund ledgers

The escrow keeps two separate pull-payment ledgers. One has no cooldown and serves as the fallback when a registration overpayment cannot be pushed back to the sender inline. The other gives every credit its own cooldown clock and holds transfer-fee overpayments. A deposit unlocked by releasing a funded name lands on the no-cooldown ledger; its delay comes instead from the position's own `withdrawAvailableAt` stamp, so once `withdraw` lands the credit is immediately pullable. Clients enumerate pending refunds through the escrow's public views, which page under a fixed cap so discovery stays bounded.
The escrow keeps two separate pull-payment ledgers. One has no cooldown and serves as the fallback when a registration overpayment cannot be pushed back to the sender inline. The other gives every entry its own cooldown clock and holds the transfer-fee overpayments earlier releases credited; nothing adds to it. A deposit unlocked by releasing a funded name lands on the no-cooldown ledger; its delay comes instead from the position's own `withdrawAvailableAt` stamp, so once `withdraw` lands the credit is immediately pullable. Clients enumerate pending refunds through the escrow's public views, which page under a fixed cap so discovery stays bounded.

Clients that need the exact moment a released name becomes registrable should read `redeemableUntil` from `getReleasePosition` rather than polling `available`.

Expand Down Expand Up @@ -231,7 +231,7 @@ A separated label reaches the chain only through the gateway path: the public pa

Tier assignment is read on every pricing call, not stored: PopRules queries the alias-accounts personhood precompile at DotnsConstants.PERSONHOOD with the dotns context (bytes32("dotns")), and translates the returned status byte into a PopStatus (0=NoStatus, 1=Devicehood, 2=Personhood; the precompile itself names them None, Lite and Full). Unknown tier bytes collapse to NoStatus, so a future precompile addition fails closed rather than silently being treated as a higher tier. There is no on-chain self-attestation; users obtain personhood off-chain through the People-chain ring proof and the alias-accounts pallet propagates the result via XCM.

Classification is not the same thing as effective holder context. A long label such as andrewsays is always a NoStatus-tier label by shape, but it may be held by a Personhood, Devicehood, or NoStatus account. Consumers that need to know what rules apply to that live name should combine three reads: classify the label, query the registrar owner, then query the owner's dotns-context PoP status through the precompile (`PopRules.popStatusOf`). The escrow position is the economic qualifier: if the token has an active release position with a non-zero amount, it came through the refundable NoStatus deposit path; if no such deposit exists, a verified holder can own the same long label without it being deposit-backed. In other words, andrewsays does not become a Personhood-tier label when a Personhood account owns it, but the owner can still hold personhood for transfer pricing, reverse resolution, and UI display.
Classification is not the same thing as effective holder context. A long label such as andrewsays is always a NoStatus-tier label by shape, but it may be held by a Personhood, Devicehood, or NoStatus account. Consumers that need to know what rules apply to that live name should combine three reads: classify the label, query the registrar owner, then query the owner's dotns-context PoP status through the precompile (`PopRules.popStatusOf`). The escrow position is the economic qualifier: if the token has an active release position with a non-zero amount, it came through the paid public path, whoever paid; if no such deposit exists, the name was issued for free, so a verified holder can own the same long label without it being deposit-backed. In other words, andrewsays does not become a Personhood-tier label when a Personhood account owns it, but the owner can still hold personhood for reverse resolution and UI display. Any transferable name moves for free whatever its holder's tier.

Name grants are the exception path for users or organisations that need to register without satisfying the live PoP tier check. dotNS still does not accept self-attestation: the contracts only consume PoP status from the personhood precompile, and a user cannot set or prove their own status inside dotNS. Instead, `DotnsNameWhitelist` binds a specific label to a specific beneficiary, and registerReserved mints it at zero base cost, bypassing the PoP pricing gate while still using the normal commit-reveal and availability checks. Only governance issues a grant: every admin action on the whitelist requires a substrate Root dispatch, so no key can grant a name, the contract owner included. A Root dispatch can also mint a reserved name directly, without a grant. That covers the whitelist path; the registrar's controller set and the protocol registry's keys are still owner-managed, so an owner retains other routes to the same outcome.

Expand Down
Loading
Loading