From bc0218d4223494409d2d5b62a8d6299f38a87b00 Mon Sep 17 00:00:00 2001 From: giuseppere Date: Mon, 21 Sep 2026 11:09:15 +0200 Subject: [PATCH 01/22] chore: replace the code owners with the people who can approve The file named @waytwotall and @sphamjoli. @sphamjoli has read access and has left the organisation, so they cannot approve a pull request, which left one person able to satisfy a code-owner review on the whole repository. That is about to matter. The "main" ruleset is active but its target list is empty, so it currently matches no branches and master is unprotected. Once its targets are set, every pull request needs a code-owner approval, and with the old list that meant @waytwotall or nothing. Replaces them with @re-gius and @GHkrishna, both of whom can approve. Two owners rather than one, because a code owner cannot approve their own pull request, so a single owner cannot merge anything they wrote. Co-Authored-By: Claude Opus 5 (1M context) --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6b635390f..b4950d95a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @waytwotall @sphamjoli +* @re-gius @GHkrishna From 3fc0638b4f245c8f4c18af52f121d6dc0567fa0c Mon Sep 17 00:00:00 2001 From: giuseppere Date: Mon, 21 Sep 2026 13:53:12 +0200 Subject: [PATCH 02/22] chore: own the repository as a team, not as two names The team outlives its membership: people joining and leaving @paritytech/dotns is a team-settings change, and this file stops needing a pull request every time that happens, which is exactly how it went stale last time. Inert until the team is granted write on this repository: CODEOWNERS ignores teams without access, so that grant lands before this merges. Co-Authored-By: Claude Fable 5 --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b4950d95a..1de1318fe 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @re-gius @GHkrishna +* @paritytech/dotns From 6bbd97d80d834cf0980404870aec7ac686344c1f Mon Sep 17 00:00:00 2001 From: giuseppere Date: Tue, 22 Sep 2026 09:39:59 +0200 Subject: [PATCH 03/22] Upgrade testnet manifests --- deployments/paseo-assethub/420420417.json | 2 +- deployments/previewnet/420420417.json | 1 + deployments/previewnet/README.md | 18 ++++++++++++++++++ scripts/js/release-metadata.mjs | 18 ++++++++++++++++++ 4 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 deployments/previewnet/420420417.json create mode 100644 deployments/previewnet/README.md diff --git a/deployments/paseo-assethub/420420417.json b/deployments/paseo-assethub/420420417.json index bbb35225e..968901c10 100644 --- a/deployments/paseo-assethub/420420417.json +++ b/deployments/paseo-assethub/420420417.json @@ -1 +1 @@ -{"Create3Factory":"0x8533c79E058c5a6489CAFeCA86dc600E029D75f5","DotnsContentResolver":"0x7F74D7CD50f5a834270E2ad395a01b01891AB37d","DotnsCostModelRegistry":"0x8bfd1f0957e73716732e725802f13830B5682da4","DotnsFlatPricing":"0xD839B281dF72Df44fF275305E72cAEEc0fDAA648","DotnsNameEscrow":"0x4881Afb78e7C908cAe818168B926229D93376520","DotnsNameWhitelist":"0x420166cD67Ca0233094E492a4BbA67045eD7C38C","DotnsPopController":"0xCC932348606cc1f3318cADeC5A5Cd2CA447f8a4b","DotnsPopLens":"0xAE374b07c7e6f473CBa21d57e36AC15C631Abc51","DotnsPopResolver":"0xDaC984884EcA8Fc44011f1D6C49B27828390A72B","DotnsProtocolRegistry":"0xD19e3D0C97CF501125a04A97405e3e6592fa846E","DotnsRegistrar":"0x4f06E818Ba3d987704fd91cf3d868E4b019106Ab","DotnsRegistrarController":"0xBdaA01bD1bA67d709F2b1fF286Da0d854977EA30","DotnsRegistry":"0xf34054fd76BbF85f216cf9908226D5f0A72E50CA","DotnsResolver":"0xbd1165E549DF96F083c0A16f61590927bC187009","DotnsReverseResolver":"0xee3883d7eB60Ee9BCD7F3bcD8f2f05302A9Cc035","LabelStoreBeacon":"0xb57Ebc2e7085616d4906D1fE49af1cE13f7dffeF","Multicall3":"0xB4468000abD87D3c56cbFBd153161223D7b109e5","PopRules":"0x747B456bE03aec0b42bd85C51513730FBD45DA31","StoreFactory":"0x709A027F446a9e2a4BB9cb9a9c754435b19e32B7","UserStoreBeacon":"0xb7C995601679840d36F37E86DB2d7dF30797eC5C","_seed":"0x0000000000000000000000000000000000000000"} +{"Create3Factory":"0x8533c79E058c5a6489CAFeCA86dc600E029D75f5","DotnsContentResolver":"0x7F74D7CD50f5a834270E2ad395a01b01891AB37d","DotnsCostModelRegistry":"0x8bfd1f0957e73716732e725802f13830B5682da4","DotnsFlatPricing":"0xD839B281dF72Df44fF275305E72cAEEc0fDAA648","DotnsNameEscrow":"0x4881Afb78e7C908cAe818168B926229D93376520","DotnsNameWhitelist":"0x420166cD67Ca0233094E492a4BbA67045eD7C38C","DotnsPopController":"0xCC932348606cc1f3318cADeC5A5Cd2CA447f8a4b","DotnsPopLens":"0xAE374b07c7e6f473CBa21d57e36AC15C631Abc51","DotnsPopResolver":"0xDaC984884EcA8Fc44011f1D6C49B27828390A72B","DotnsProtocolRegistry":"0xD19e3D0C97CF501125a04A97405e3e6592fa846E","DotnsRegistrar":"0x4f06E818Ba3d987704fd91cf3d868E4b019106Ab","DotnsRegistrarController":"0xBdaA01bD1bA67d709F2b1fF286Da0d854977EA30","DotnsRegistry":"0xf34054fd76BbF85f216cf9908226D5f0A72E50CA","DotnsResolver":"0xbd1165E549DF96F083c0A16f61590927bC187009","DotnsReverseResolver":"0xee3883d7eB60Ee9BCD7F3bcD8f2f05302A9Cc035","LabelStoreBeacon":"0x2227d9807F5A71332Aaa0640643030f2A3bf84cD","LabelStoreBeaconLegacy":"0xb57Ebc2e7085616d4906D1fE49af1cE13f7dffeF","Multicall3":"0xB4468000abD87D3c56cbFBd153161223D7b109e5","PopRules":"0x747B456bE03aec0b42bd85C51513730FBD45DA31","StoreFactory":"0x99605a926FcB40aB520F659c6505E5ff862771f6","StoreFactoryLegacy":"0x709A027F446a9e2a4BB9cb9a9c754435b19e32B7","UserStoreBeacon":"0x3d1Ca165f7A5e387C2df02DB2FadD3149c1C72ad","UserStoreBeaconLegacy":"0xb7C995601679840d36F37E86DB2d7dF30797eC5C","_seed":"0x0000000000000000000000000000000000000000"} diff --git a/deployments/previewnet/420420417.json b/deployments/previewnet/420420417.json new file mode 100644 index 000000000..2f36c9126 --- /dev/null +++ b/deployments/previewnet/420420417.json @@ -0,0 +1 @@ +{"Create3Factory":"0x8533c79E058c5a6489CAFeCA86dc600E029D75f5","DotnsContentResolver":"0x7F74D7CD50f5a834270E2ad395a01b01891AB37d","DotnsCostModelRegistry":"0x8bfd1f0957e73716732e725802f13830B5682da4","DotnsFlatPricing":"0xD839B281dF72Df44fF275305E72cAEEc0fDAA648","DotnsNameEscrow":"0x4881Afb78e7C908cAe818168B926229D93376520","DotnsNameWhitelist":"0x420166cD67Ca0233094E492a4BbA67045eD7C38C","DotnsPopController":"0xCC932348606cc1f3318cADeC5A5Cd2CA447f8a4b","DotnsPopLens":"0xfe5A45f7fD58D1A6FE09455DB799405b1dcE9411","DotnsPopResolver":"0xDaC984884EcA8Fc44011f1D6C49B27828390A72B","DotnsProtocolRegistry":"0xD19e3D0C97CF501125a04A97405e3e6592fa846E","DotnsRegistrar":"0x4f06E818Ba3d987704fd91cf3d868E4b019106Ab","DotnsRegistrarController":"0xBdaA01bD1bA67d709F2b1fF286Da0d854977EA30","DotnsRegistry":"0xf34054fd76BbF85f216cf9908226D5f0A72E50CA","DotnsResolver":"0xbd1165E549DF96F083c0A16f61590927bC187009","DotnsReverseResolver":"0xee3883d7eB60Ee9BCD7F3bcD8f2f05302A9Cc035","LabelStoreBeacon":"0x2227d9807F5A71332Aaa0640643030f2A3bf84cD","Multicall3":"0xB4468000abD87D3c56cbFBd153161223D7b109e5","PopRules":"0x747B456bE03aec0b42bd85C51513730FBD45DA31","StoreFactory":"0x99605a926FcB40aB520F659c6505E5ff862771f6","UserStoreBeacon":"0x3d1Ca165f7A5e387C2df02DB2FadD3149c1C72ad"} diff --git a/deployments/previewnet/README.md b/deployments/previewnet/README.md new file mode 100644 index 000000000..420ca9599 --- /dev/null +++ b/deployments/previewnet/README.md @@ -0,0 +1,18 @@ +# previewnet + +The Product Preview Network's Asset Hub (`wss://previewnet.substrate.dev/asset-hub`, +ETH RPC `https://previewnet.substrate.dev/eth-rpc`), TLD `.dot`. Deployed from a v0.8.0 +genesis, so every address matches the fresh-deploy set in `deployments/expected.json`. + +The chain id is `420420417`, the same as `paseo-assethub`: pallet-revive testnets share it, +so the network folder, not the chain id, is what identifies a deployment. The two manifests +differ where their histories do: a network upgraded in place carries `*Legacy` entries for +addresses its upgrades superseded, and can hold contracts deployed before the current set. + +This network is wiped and redeployed periodically; verify against the chain before relying +on this file after a wipe: + +```bash +node scripts/js/release-metadata.mjs verify --network previewnet \ + --rpc https://previewnet.substrate.dev/eth-rpc --tag v0.8.0 +``` diff --git a/scripts/js/release-metadata.mjs b/scripts/js/release-metadata.mjs index 0f48deb63..5fd2975b3 100644 --- a/scripts/js/release-metadata.mjs +++ b/scripts/js/release-metadata.mjs @@ -637,6 +637,15 @@ function verify(args) { rpc, ]); if (declared === ZERO_HASH) { + // A key that is tolerated unset is also tolerated undeclared. Such keys are outside + // the declared release surface, so the declaration pass never writes a codehash for + // them; a network whose genesis predates the current policy can still have one set, + // and that combination (set, no code identity) is expected there. + if (UNSET_TOLERATED[key]) { + resolved.add(address.toLowerCase()); + console.log(` skip ${key} ${address} (${label}: no declaration, ${UNSET_TOLERATED[key]})`); + continue; + } problems.push(`key '${key}' (${label}) has no declared codehash`); continue; } @@ -669,6 +678,15 @@ function verify(args) { const notExpected = new Set(unpointed); for (const [address, label] of byAddress) { if (!resolved.has(address) && !notExpected.has(address)) { + // A `*Legacy` entry records an address that became stale or unused after an in-place + // upgrade: when a contract moves to a new address, the outgoing one keeps its manifest + // entry under the `Legacy` suffix instead of being erased, because live state can keep + // depending on it after nothing points at it. Unkeyed by design, so the reverse check + // skips it instead of reading it as an orphan. + if (label.endsWith("Legacy")) { + console.log(` skip ${label} ${contracts[label]} (superseded deployment, kept for the beacon upgrade path)`); + continue; + } problems.push(`${label} ${contracts[label]} is in the manifest but no key points at it`); } } From 2dd93fb5c610eed16666db0f507791c91447286d Mon Sep 17 00:00:00 2001 From: giuseppere Date: Tue, 22 Sep 2026 09:45:55 +0200 Subject: [PATCH 04/22] Revert "Upgrade testnet manifests" Pushed to master directly by mistake; it belongs in a reviewed PR. This reverts commit 6bbd97d80d834cf0980404870aec7ac686344c1f. --- deployments/paseo-assethub/420420417.json | 2 +- deployments/previewnet/420420417.json | 1 - deployments/previewnet/README.md | 18 ------------------ scripts/js/release-metadata.mjs | 18 ------------------ 4 files changed, 1 insertion(+), 38 deletions(-) delete mode 100644 deployments/previewnet/420420417.json delete mode 100644 deployments/previewnet/README.md diff --git a/deployments/paseo-assethub/420420417.json b/deployments/paseo-assethub/420420417.json index 968901c10..bbb35225e 100644 --- a/deployments/paseo-assethub/420420417.json +++ b/deployments/paseo-assethub/420420417.json @@ -1 +1 @@ -{"Create3Factory":"0x8533c79E058c5a6489CAFeCA86dc600E029D75f5","DotnsContentResolver":"0x7F74D7CD50f5a834270E2ad395a01b01891AB37d","DotnsCostModelRegistry":"0x8bfd1f0957e73716732e725802f13830B5682da4","DotnsFlatPricing":"0xD839B281dF72Df44fF275305E72cAEEc0fDAA648","DotnsNameEscrow":"0x4881Afb78e7C908cAe818168B926229D93376520","DotnsNameWhitelist":"0x420166cD67Ca0233094E492a4BbA67045eD7C38C","DotnsPopController":"0xCC932348606cc1f3318cADeC5A5Cd2CA447f8a4b","DotnsPopLens":"0xAE374b07c7e6f473CBa21d57e36AC15C631Abc51","DotnsPopResolver":"0xDaC984884EcA8Fc44011f1D6C49B27828390A72B","DotnsProtocolRegistry":"0xD19e3D0C97CF501125a04A97405e3e6592fa846E","DotnsRegistrar":"0x4f06E818Ba3d987704fd91cf3d868E4b019106Ab","DotnsRegistrarController":"0xBdaA01bD1bA67d709F2b1fF286Da0d854977EA30","DotnsRegistry":"0xf34054fd76BbF85f216cf9908226D5f0A72E50CA","DotnsResolver":"0xbd1165E549DF96F083c0A16f61590927bC187009","DotnsReverseResolver":"0xee3883d7eB60Ee9BCD7F3bcD8f2f05302A9Cc035","LabelStoreBeacon":"0x2227d9807F5A71332Aaa0640643030f2A3bf84cD","LabelStoreBeaconLegacy":"0xb57Ebc2e7085616d4906D1fE49af1cE13f7dffeF","Multicall3":"0xB4468000abD87D3c56cbFBd153161223D7b109e5","PopRules":"0x747B456bE03aec0b42bd85C51513730FBD45DA31","StoreFactory":"0x99605a926FcB40aB520F659c6505E5ff862771f6","StoreFactoryLegacy":"0x709A027F446a9e2a4BB9cb9a9c754435b19e32B7","UserStoreBeacon":"0x3d1Ca165f7A5e387C2df02DB2FadD3149c1C72ad","UserStoreBeaconLegacy":"0xb7C995601679840d36F37E86DB2d7dF30797eC5C","_seed":"0x0000000000000000000000000000000000000000"} +{"Create3Factory":"0x8533c79E058c5a6489CAFeCA86dc600E029D75f5","DotnsContentResolver":"0x7F74D7CD50f5a834270E2ad395a01b01891AB37d","DotnsCostModelRegistry":"0x8bfd1f0957e73716732e725802f13830B5682da4","DotnsFlatPricing":"0xD839B281dF72Df44fF275305E72cAEEc0fDAA648","DotnsNameEscrow":"0x4881Afb78e7C908cAe818168B926229D93376520","DotnsNameWhitelist":"0x420166cD67Ca0233094E492a4BbA67045eD7C38C","DotnsPopController":"0xCC932348606cc1f3318cADeC5A5Cd2CA447f8a4b","DotnsPopLens":"0xAE374b07c7e6f473CBa21d57e36AC15C631Abc51","DotnsPopResolver":"0xDaC984884EcA8Fc44011f1D6C49B27828390A72B","DotnsProtocolRegistry":"0xD19e3D0C97CF501125a04A97405e3e6592fa846E","DotnsRegistrar":"0x4f06E818Ba3d987704fd91cf3d868E4b019106Ab","DotnsRegistrarController":"0xBdaA01bD1bA67d709F2b1fF286Da0d854977EA30","DotnsRegistry":"0xf34054fd76BbF85f216cf9908226D5f0A72E50CA","DotnsResolver":"0xbd1165E549DF96F083c0A16f61590927bC187009","DotnsReverseResolver":"0xee3883d7eB60Ee9BCD7F3bcD8f2f05302A9Cc035","LabelStoreBeacon":"0xb57Ebc2e7085616d4906D1fE49af1cE13f7dffeF","Multicall3":"0xB4468000abD87D3c56cbFBd153161223D7b109e5","PopRules":"0x747B456bE03aec0b42bd85C51513730FBD45DA31","StoreFactory":"0x709A027F446a9e2a4BB9cb9a9c754435b19e32B7","UserStoreBeacon":"0xb7C995601679840d36F37E86DB2d7dF30797eC5C","_seed":"0x0000000000000000000000000000000000000000"} diff --git a/deployments/previewnet/420420417.json b/deployments/previewnet/420420417.json deleted file mode 100644 index 2f36c9126..000000000 --- a/deployments/previewnet/420420417.json +++ /dev/null @@ -1 +0,0 @@ -{"Create3Factory":"0x8533c79E058c5a6489CAFeCA86dc600E029D75f5","DotnsContentResolver":"0x7F74D7CD50f5a834270E2ad395a01b01891AB37d","DotnsCostModelRegistry":"0x8bfd1f0957e73716732e725802f13830B5682da4","DotnsFlatPricing":"0xD839B281dF72Df44fF275305E72cAEEc0fDAA648","DotnsNameEscrow":"0x4881Afb78e7C908cAe818168B926229D93376520","DotnsNameWhitelist":"0x420166cD67Ca0233094E492a4BbA67045eD7C38C","DotnsPopController":"0xCC932348606cc1f3318cADeC5A5Cd2CA447f8a4b","DotnsPopLens":"0xfe5A45f7fD58D1A6FE09455DB799405b1dcE9411","DotnsPopResolver":"0xDaC984884EcA8Fc44011f1D6C49B27828390A72B","DotnsProtocolRegistry":"0xD19e3D0C97CF501125a04A97405e3e6592fa846E","DotnsRegistrar":"0x4f06E818Ba3d987704fd91cf3d868E4b019106Ab","DotnsRegistrarController":"0xBdaA01bD1bA67d709F2b1fF286Da0d854977EA30","DotnsRegistry":"0xf34054fd76BbF85f216cf9908226D5f0A72E50CA","DotnsResolver":"0xbd1165E549DF96F083c0A16f61590927bC187009","DotnsReverseResolver":"0xee3883d7eB60Ee9BCD7F3bcD8f2f05302A9Cc035","LabelStoreBeacon":"0x2227d9807F5A71332Aaa0640643030f2A3bf84cD","Multicall3":"0xB4468000abD87D3c56cbFBd153161223D7b109e5","PopRules":"0x747B456bE03aec0b42bd85C51513730FBD45DA31","StoreFactory":"0x99605a926FcB40aB520F659c6505E5ff862771f6","UserStoreBeacon":"0x3d1Ca165f7A5e387C2df02DB2FadD3149c1C72ad"} diff --git a/deployments/previewnet/README.md b/deployments/previewnet/README.md deleted file mode 100644 index 420ca9599..000000000 --- a/deployments/previewnet/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# previewnet - -The Product Preview Network's Asset Hub (`wss://previewnet.substrate.dev/asset-hub`, -ETH RPC `https://previewnet.substrate.dev/eth-rpc`), TLD `.dot`. Deployed from a v0.8.0 -genesis, so every address matches the fresh-deploy set in `deployments/expected.json`. - -The chain id is `420420417`, the same as `paseo-assethub`: pallet-revive testnets share it, -so the network folder, not the chain id, is what identifies a deployment. The two manifests -differ where their histories do: a network upgraded in place carries `*Legacy` entries for -addresses its upgrades superseded, and can hold contracts deployed before the current set. - -This network is wiped and redeployed periodically; verify against the chain before relying -on this file after a wipe: - -```bash -node scripts/js/release-metadata.mjs verify --network previewnet \ - --rpc https://previewnet.substrate.dev/eth-rpc --tag v0.8.0 -``` diff --git a/scripts/js/release-metadata.mjs b/scripts/js/release-metadata.mjs index 5fd2975b3..0f48deb63 100644 --- a/scripts/js/release-metadata.mjs +++ b/scripts/js/release-metadata.mjs @@ -637,15 +637,6 @@ function verify(args) { rpc, ]); if (declared === ZERO_HASH) { - // A key that is tolerated unset is also tolerated undeclared. Such keys are outside - // the declared release surface, so the declaration pass never writes a codehash for - // them; a network whose genesis predates the current policy can still have one set, - // and that combination (set, no code identity) is expected there. - if (UNSET_TOLERATED[key]) { - resolved.add(address.toLowerCase()); - console.log(` skip ${key} ${address} (${label}: no declaration, ${UNSET_TOLERATED[key]})`); - continue; - } problems.push(`key '${key}' (${label}) has no declared codehash`); continue; } @@ -678,15 +669,6 @@ function verify(args) { const notExpected = new Set(unpointed); for (const [address, label] of byAddress) { if (!resolved.has(address) && !notExpected.has(address)) { - // A `*Legacy` entry records an address that became stale or unused after an in-place - // upgrade: when a contract moves to a new address, the outgoing one keeps its manifest - // entry under the `Legacy` suffix instead of being erased, because live state can keep - // depending on it after nothing points at it. Unkeyed by design, so the reverse check - // skips it instead of reading it as an orphan. - if (label.endsWith("Legacy")) { - console.log(` skip ${label} ${contracts[label]} (superseded deployment, kept for the beacon upgrade path)`); - continue; - } problems.push(`${label} ${contracts[label]} is in the manifest but no key points at it`); } } From 027e763130f3b2262bb9843a3d8ccfd0f4e47a97 Mon Sep 17 00:00:00 2001 From: Giuseppe Re Date: Tue, 22 Sep 2026 11:21:31 +0200 Subject: [PATCH 05/22] chore: record the live testnet manifests (#315) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description Records what the two live testnets actually run. - `paseo-assethub` is updated to the post-v0.8.0 in-place-upgrade state: the store factory lives behind a proxy at `0x99605a92…71f6`, its beacons moved with it, and the outgoing factory and beacons stay recorded under `*Legacy` keys because the migrated stores remain on them (see #312). - `previewnet` gets its first committed manifest, read from its chain. It runs an unmodified v0.8.0 genesis, so every address equals `deployments/expected.json`. A README explains the chain id shared with `paseo-assethub` and the `*Legacy` convention. - `release-metadata.mjs verify` learns two general rules: a `*Legacy` manifest entry is a superseded deployment that holds no registry key by design, and a key tolerated unset is also tolerated undeclared where an older genesis set it. ## Type - [ ] Bug fix - [ ] Feature - [ ] Breaking change - [ ] Documentation - [x] Chore - [ ] Refactor - [ ] Security ## Scope - [ ] Registration - [ ] Resolver - [ ] Store - [ ] Proof of Personhood - [x] Deployment scripts - [ ] Tests ## Related Issues ## Fixes ## Checklist ### Code - [x] Follows project style - [x] `forge build` passes - [x] `forge test` passes - [x] No new compiler warnings ### Testing - [x] New tests added for changed behavior - [ ] Fuzz tests added where applicable - [ ] Invariant tests verified ### Security - [x] No new `selfdestruct` or `delegatecall` - [x] Access control reviewed - [x] No storage layout conflicts (for upgradeable contracts) ### Documentation - [ ] 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 node scripts/js/release-metadata.mjs validate node scripts/js/release-metadata.mjs verify --network paseo-assethub \ --rpc https://eth-rpc-paseo-next.polkadot.io --tag v0.8.0 node scripts/js/release-metadata.mjs verify --network previewnet \ --rpc https://previewnet.substrate.dev/eth-rpc --tag v0.8.0 ``` Both verifies end with "matches the chain". ## Notes Manifests are records of live networks; the Paseo one diverges from expected.json where its history does (Legacy entries, a lens deployed before the current set), and the previewnet one matches it exactly --- deployments/paseo-assethub/420420417.json | 2 +- deployments/previewnet/420420417.json | 1 + deployments/previewnet/README.md | 18 ++++++++++++++++++ scripts/js/release-metadata.mjs | 18 ++++++++++++++++++ 4 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 deployments/previewnet/420420417.json create mode 100644 deployments/previewnet/README.md diff --git a/deployments/paseo-assethub/420420417.json b/deployments/paseo-assethub/420420417.json index bbb35225e..968901c10 100644 --- a/deployments/paseo-assethub/420420417.json +++ b/deployments/paseo-assethub/420420417.json @@ -1 +1 @@ -{"Create3Factory":"0x8533c79E058c5a6489CAFeCA86dc600E029D75f5","DotnsContentResolver":"0x7F74D7CD50f5a834270E2ad395a01b01891AB37d","DotnsCostModelRegistry":"0x8bfd1f0957e73716732e725802f13830B5682da4","DotnsFlatPricing":"0xD839B281dF72Df44fF275305E72cAEEc0fDAA648","DotnsNameEscrow":"0x4881Afb78e7C908cAe818168B926229D93376520","DotnsNameWhitelist":"0x420166cD67Ca0233094E492a4BbA67045eD7C38C","DotnsPopController":"0xCC932348606cc1f3318cADeC5A5Cd2CA447f8a4b","DotnsPopLens":"0xAE374b07c7e6f473CBa21d57e36AC15C631Abc51","DotnsPopResolver":"0xDaC984884EcA8Fc44011f1D6C49B27828390A72B","DotnsProtocolRegistry":"0xD19e3D0C97CF501125a04A97405e3e6592fa846E","DotnsRegistrar":"0x4f06E818Ba3d987704fd91cf3d868E4b019106Ab","DotnsRegistrarController":"0xBdaA01bD1bA67d709F2b1fF286Da0d854977EA30","DotnsRegistry":"0xf34054fd76BbF85f216cf9908226D5f0A72E50CA","DotnsResolver":"0xbd1165E549DF96F083c0A16f61590927bC187009","DotnsReverseResolver":"0xee3883d7eB60Ee9BCD7F3bcD8f2f05302A9Cc035","LabelStoreBeacon":"0xb57Ebc2e7085616d4906D1fE49af1cE13f7dffeF","Multicall3":"0xB4468000abD87D3c56cbFBd153161223D7b109e5","PopRules":"0x747B456bE03aec0b42bd85C51513730FBD45DA31","StoreFactory":"0x709A027F446a9e2a4BB9cb9a9c754435b19e32B7","UserStoreBeacon":"0xb7C995601679840d36F37E86DB2d7dF30797eC5C","_seed":"0x0000000000000000000000000000000000000000"} +{"Create3Factory":"0x8533c79E058c5a6489CAFeCA86dc600E029D75f5","DotnsContentResolver":"0x7F74D7CD50f5a834270E2ad395a01b01891AB37d","DotnsCostModelRegistry":"0x8bfd1f0957e73716732e725802f13830B5682da4","DotnsFlatPricing":"0xD839B281dF72Df44fF275305E72cAEEc0fDAA648","DotnsNameEscrow":"0x4881Afb78e7C908cAe818168B926229D93376520","DotnsNameWhitelist":"0x420166cD67Ca0233094E492a4BbA67045eD7C38C","DotnsPopController":"0xCC932348606cc1f3318cADeC5A5Cd2CA447f8a4b","DotnsPopLens":"0xAE374b07c7e6f473CBa21d57e36AC15C631Abc51","DotnsPopResolver":"0xDaC984884EcA8Fc44011f1D6C49B27828390A72B","DotnsProtocolRegistry":"0xD19e3D0C97CF501125a04A97405e3e6592fa846E","DotnsRegistrar":"0x4f06E818Ba3d987704fd91cf3d868E4b019106Ab","DotnsRegistrarController":"0xBdaA01bD1bA67d709F2b1fF286Da0d854977EA30","DotnsRegistry":"0xf34054fd76BbF85f216cf9908226D5f0A72E50CA","DotnsResolver":"0xbd1165E549DF96F083c0A16f61590927bC187009","DotnsReverseResolver":"0xee3883d7eB60Ee9BCD7F3bcD8f2f05302A9Cc035","LabelStoreBeacon":"0x2227d9807F5A71332Aaa0640643030f2A3bf84cD","LabelStoreBeaconLegacy":"0xb57Ebc2e7085616d4906D1fE49af1cE13f7dffeF","Multicall3":"0xB4468000abD87D3c56cbFBd153161223D7b109e5","PopRules":"0x747B456bE03aec0b42bd85C51513730FBD45DA31","StoreFactory":"0x99605a926FcB40aB520F659c6505E5ff862771f6","StoreFactoryLegacy":"0x709A027F446a9e2a4BB9cb9a9c754435b19e32B7","UserStoreBeacon":"0x3d1Ca165f7A5e387C2df02DB2FadD3149c1C72ad","UserStoreBeaconLegacy":"0xb7C995601679840d36F37E86DB2d7dF30797eC5C","_seed":"0x0000000000000000000000000000000000000000"} diff --git a/deployments/previewnet/420420417.json b/deployments/previewnet/420420417.json new file mode 100644 index 000000000..2f36c9126 --- /dev/null +++ b/deployments/previewnet/420420417.json @@ -0,0 +1 @@ +{"Create3Factory":"0x8533c79E058c5a6489CAFeCA86dc600E029D75f5","DotnsContentResolver":"0x7F74D7CD50f5a834270E2ad395a01b01891AB37d","DotnsCostModelRegistry":"0x8bfd1f0957e73716732e725802f13830B5682da4","DotnsFlatPricing":"0xD839B281dF72Df44fF275305E72cAEEc0fDAA648","DotnsNameEscrow":"0x4881Afb78e7C908cAe818168B926229D93376520","DotnsNameWhitelist":"0x420166cD67Ca0233094E492a4BbA67045eD7C38C","DotnsPopController":"0xCC932348606cc1f3318cADeC5A5Cd2CA447f8a4b","DotnsPopLens":"0xfe5A45f7fD58D1A6FE09455DB799405b1dcE9411","DotnsPopResolver":"0xDaC984884EcA8Fc44011f1D6C49B27828390A72B","DotnsProtocolRegistry":"0xD19e3D0C97CF501125a04A97405e3e6592fa846E","DotnsRegistrar":"0x4f06E818Ba3d987704fd91cf3d868E4b019106Ab","DotnsRegistrarController":"0xBdaA01bD1bA67d709F2b1fF286Da0d854977EA30","DotnsRegistry":"0xf34054fd76BbF85f216cf9908226D5f0A72E50CA","DotnsResolver":"0xbd1165E549DF96F083c0A16f61590927bC187009","DotnsReverseResolver":"0xee3883d7eB60Ee9BCD7F3bcD8f2f05302A9Cc035","LabelStoreBeacon":"0x2227d9807F5A71332Aaa0640643030f2A3bf84cD","Multicall3":"0xB4468000abD87D3c56cbFBd153161223D7b109e5","PopRules":"0x747B456bE03aec0b42bd85C51513730FBD45DA31","StoreFactory":"0x99605a926FcB40aB520F659c6505E5ff862771f6","UserStoreBeacon":"0x3d1Ca165f7A5e387C2df02DB2FadD3149c1C72ad"} diff --git a/deployments/previewnet/README.md b/deployments/previewnet/README.md new file mode 100644 index 000000000..420ca9599 --- /dev/null +++ b/deployments/previewnet/README.md @@ -0,0 +1,18 @@ +# previewnet + +The Product Preview Network's Asset Hub (`wss://previewnet.substrate.dev/asset-hub`, +ETH RPC `https://previewnet.substrate.dev/eth-rpc`), TLD `.dot`. Deployed from a v0.8.0 +genesis, so every address matches the fresh-deploy set in `deployments/expected.json`. + +The chain id is `420420417`, the same as `paseo-assethub`: pallet-revive testnets share it, +so the network folder, not the chain id, is what identifies a deployment. The two manifests +differ where their histories do: a network upgraded in place carries `*Legacy` entries for +addresses its upgrades superseded, and can hold contracts deployed before the current set. + +This network is wiped and redeployed periodically; verify against the chain before relying +on this file after a wipe: + +```bash +node scripts/js/release-metadata.mjs verify --network previewnet \ + --rpc https://previewnet.substrate.dev/eth-rpc --tag v0.8.0 +``` diff --git a/scripts/js/release-metadata.mjs b/scripts/js/release-metadata.mjs index 0f48deb63..5fd2975b3 100644 --- a/scripts/js/release-metadata.mjs +++ b/scripts/js/release-metadata.mjs @@ -637,6 +637,15 @@ function verify(args) { rpc, ]); if (declared === ZERO_HASH) { + // A key that is tolerated unset is also tolerated undeclared. Such keys are outside + // the declared release surface, so the declaration pass never writes a codehash for + // them; a network whose genesis predates the current policy can still have one set, + // and that combination (set, no code identity) is expected there. + if (UNSET_TOLERATED[key]) { + resolved.add(address.toLowerCase()); + console.log(` skip ${key} ${address} (${label}: no declaration, ${UNSET_TOLERATED[key]})`); + continue; + } problems.push(`key '${key}' (${label}) has no declared codehash`); continue; } @@ -669,6 +678,15 @@ function verify(args) { const notExpected = new Set(unpointed); for (const [address, label] of byAddress) { if (!resolved.has(address) && !notExpected.has(address)) { + // A `*Legacy` entry records an address that became stale or unused after an in-place + // upgrade: when a contract moves to a new address, the outgoing one keeps its manifest + // entry under the `Legacy` suffix instead of being erased, because live state can keep + // depending on it after nothing points at it. Unkeyed by design, so the reverse check + // skips it instead of reading it as an orphan. + if (label.endsWith("Legacy")) { + console.log(` skip ${label} ${contracts[label]} (superseded deployment, kept for the beacon upgrade path)`); + continue; + } problems.push(`${label} ${contracts[label]} is in the manifest but no key points at it`); } } From 14f55a67dda1a3083567ec862df31f056f9886fd Mon Sep 17 00:00:00 2001 From: Krishna Waske Date: Wed, 23 Sep 2026 11:23:11 +0530 Subject: [PATCH 06/22] fix(security): minor security bugs (#307) ## Description Three low severity findings from the internal audit, grouped together. **Subname paths now have a length limit.** `isNamePath` checked that each dot separated segment was at most 63 octets but never checked the path as a whole, so a caller could build an arbitrarily long `parentLabel` out of legal segments. `setSubnodeOwner` stores the resulting full name in the new owner's `LabelStore`, and that row cannot be deleted, so the text sat there permanently and made every enumeration that reads it back more expensive. The path is now capped at 255 octets, which is the DNS wire format ceiling. A path over that limit reverts with `ParentLabelMismatch`, same as a malformed one. Nothing live comes close: real parent paths are one or two labels. **CI now enforces the live manifest rule.** `CONTRIBUTING.md` says a code PR must not edit `deployments//.json`, because those files are records of what is actually deployed and they ship straight into the `deployments.json` release asset. The CREATE3 parity gates compare `deployments/expected.json` instead, so nothing was checking the live files. A new job in `release-metadata.yml` fails a PR that touches one, unless the PR carries the `deployment-record` label, which is what a real deploy uses. **`setOwner` now mentions subnames.** The comment on `setOwner` warned that a name sold directly keeps the seller's resolver pointer until the buyer overwrites it. That is true for the node itself but not for anything under it: a subname stores its own owner and `isAuthorised` returns on that before it looks at the registrar, so the seller keeps write access to every subname they created until the buyer reassigns each one. Someone acting on the old wording would clean up the node and think the name was clear. Added to both the implementation and the interface. ## Type - [x] Bug fix - [ ] Feature - [ ] Breaking change - [x] Documentation - [ ] Chore - [ ] Refactor - [x] Security ## Scope - [x] Registration - [ ] Resolver - [x] Store - [ ] Proof of Personhood - [x] Deployment scripts - [x] Tests ## Related Issues Internal security audit, low severity findings. ## Fixes ## Checklist ### Code - [x] Follows project style - [ ] `forge build` passes - [ ] `forge test` passes - [ ] No new compiler warnings ### Testing - [x] 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 ## How to test ```bash forge test --mt test_accepts_a_path_at_the_ceiling forge test --mt test_rejects_a_path_one_octet_over_the_ceiling forge test --mt test_subnode_parent_path_over_the_octet_ceiling_is_rejected ``` ## Notes The length cap makes the injection cheaper to live with, it does not close it. Anyone holding a node can still name someone else as a subnode owner and push an undeletable row into their store. What changed is that the row is now bounded at roughly 320 octets instead of being unbounded, so the cost of reading it back is in the same range as a genuine row. Closing it properly means one of three things( they are design decisions though tbh): require the caller to be the subnode's new owner on the persist branch, give `LabelStore` an owner callable removal, or bound the scan in `DotnsPopLens` with a resumable cursor. The `deployment-record` label has to exist in the repo before the new CI job is useful, otherwise a legitimate deploy PR has no way through. The audit also flagged that the release workflows hold deployment keys with no environment gate. That one needs a GitHub environment set up before the workflow change means anything, so it is not inc in this PR. --------- Signed-off-by: GHkrishna --- .github/workflows/release-metadata.yml | 43 ++++++++++++++++++++++++++ CONTRIBUTING.md | 4 +-- DEPLOYMENTS.md | 2 +- README.md | 2 +- contracts/registry/DotnsRegistry.sol | 6 ++++ contracts/registry/IDotnsRegistry.sol | 32 ++++++++++++++++--- contracts/utils/StringUtils.sol | 35 ++++++++++++++++++--- test/unit/utils/StringUtils.t.sol | 28 +++++++++++++++++ 8 files changed, 138 insertions(+), 14 deletions(-) diff --git a/.github/workflows/release-metadata.yml b/.github/workflows/release-metadata.yml index d0ff5577d..837cc133a 100644 --- a/.github/workflows/release-metadata.yml +++ b/.github/workflows/release-metadata.yml @@ -7,6 +7,13 @@ name: Release Metadata # resolved against sources. on: pull_request: + # Includes labeled and unlabeled so adding `deployment-record` re-runs the manifest guard + # below. Listing types at all replaces the default set, so the defaults are repeated here. + types: [opened, edited, synchronize, reopened, labeled, unlabeled, ready_for_review] + # A pull request touching none of the paths below never starts this workflow, so no job in + # it reports a conclusion on that pull request. Harmless while these are optional checks, + # and the thing to settle before marking any of them required: a required check that never + # reports blocks the merge. paths: - "deployments/**" - ".github/abi-contracts.txt" @@ -29,3 +36,39 @@ jobs: - name: Validate deployment manifests and the contract list run: bun scripts/js/release-metadata.mjs validate + + # Live network manifests record what is deployed on a real chain and ship verbatim into the + # deployments.json release asset, so they are written by a deploy and never by a code change + # (CONTRIBUTING.md). The CREATE3 parity gates compare deployments/expected.json rather than + # these files, so this job is where that rule is enforced. A pull request that records a real + # deploy carries the `deployment-record` label and is exempt. + # Runs on every pull request rather than being skipped for a labelled one: a skipped job + # reports no conclusion, so making this a required check would leave a correctly labelled + # deploy pull request unable to merge. The label is read inside the step instead. + live-manifests-unchanged: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Reject edits to a live network manifest + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + IS_DEPLOY_RECORD: ${{ contains(github.event.pull_request.labels.*.name, 'deployment-record') }} + run: | + changed=$(git diff --name-only "$BASE_SHA"...HEAD -- 'deployments/*/*.json') + if [ -z "$changed" ]; then + echo "No live network manifest was edited." + exit 0 + fi + if [ "$IS_DEPLOY_RECORD" = "true" ]; then + echo "Live network manifest edited under the 'deployment-record' label:" + printf ' %s\n' $changed + exit 0 + fi + echo "::error::A code pull request must not edit a live network manifest." + printf ' %s\n' $changed + echo "Update deployments/expected.json instead; the live file is written by a deploy." + echo "If this pull request records a real deploy, add the 'deployment-record' label." + exit 1 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 150ae8b2d..12c1b6897 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -135,7 +135,7 @@ Any new contract address that other contracts need to read must be looked up thr If you are adding a new contract category, add a `bytes32` key for it in `DotnsConstants.sol`, wire it up in `WireDeployments.s.sol` (including its entry in `_registryEntries`, so its code identity is declared and verified with the rest), and list the contract and its interface in `.github/abi-contracts.txt` so their ABIs ship in the release artifact. Read it the same way every existing contract does. Give the contract the standard `version()` mirror — a `view` that returns `protocolRegistry.protocolVersion()`, copied from any existing contract — and never a hardcoded version constant: what a network runs is declared once, on the protocol registry, every contract reports that one value, and per-contract identity is the declared codehash the deploy pipeline writes, not a self-report compiled into the bytecode. -A change that moves or adds an address (a new salt, a new contract, a contract restructured behind a proxy) must update `deployments/expected.json` in the same PR — that diff is where review sees the move — and must NOT touch any `deployments//.json`. Those are records of live networks, updated only by a real deploy on that network; editing one from a code PR publishes an address nothing is deployed at. The expected set diverging from a network's manifest is normal and means a redeploy or migration is owed on that network — see "Network manifests and the expected set" in `DEPLOYMENTS.md`. +A change that moves or adds an address (a new salt, a new contract, a contract restructured behind a proxy) must update `deployments/expected.json` in the same PR — that diff is where review sees the move — and must NOT touch any `deployments//.json`. Those are records of live networks, updated only by a real deploy on that network; editing one from a code PR publishes an address nothing is deployed at. The expected set diverging from a network's manifest is normal and means a redeploy or migration is owed on that network — see "Network manifests and the expected set" in `DEPLOYMENTS.md`. CI enforces this: `release-metadata.yml` fails a pull request that edits a live manifest unless the pull request carries the `deployment-record` label, which is how a real deploy records its addresses. Bad — the registrar address is frozen at construction, so rotating it needs an upgrade: @@ -277,7 +277,7 @@ forge test --no-match-path 'test/fork/**' 2. Delete the paired fork test under `test/fork/`. 3. Delete every `*Old.sol` and `I*Old.sol` referenced only by the upgrade script. 4. Delete temporary forge artefacts: `broadcast/') lines.append("") lines.append("") @@ -55,7 +55,7 @@ def generate_html(findings): lines.append('
') lines.append('
') - lines.append('

DotNS 4naly3er Report

') + lines.append('

dotNS 4naly3er Report

') lines.append(f'

Generated {timestamp} | {total} findings

') lines.append("
") diff --git a/scripts/reports/coverage-report.py b/scripts/reports/coverage-report.py index 8b3684817..2f77d5cbd 100644 --- a/scripts/reports/coverage-report.py +++ b/scripts/reports/coverage-report.py @@ -89,7 +89,7 @@ def generate_html(files, total_line_coverage): lines = [] lines.append('') lines.append('') - lines.append("DotNS Coverage Report") + lines.append("dotNS Coverage Report") lines.append('') lines.append("") lines.append("") @@ -97,7 +97,7 @@ def generate_html(files, total_line_coverage): lines.append('
') lines.append('
') - lines.append('

DotNS Coverage Report

') + lines.append('

dotNS Coverage Report

') lines.append(f'

Generated {timestamp} | {len(files)} files

') lines.append("
") diff --git a/scripts/reports/gas-report.py b/scripts/reports/gas-report.py index 94492b556..cd6882da3 100644 --- a/scripts/reports/gas-report.py +++ b/scripts/reports/gas-report.py @@ -135,7 +135,7 @@ def generate_html(master_contracts, current_contracts): lines = [] lines.append('') lines.append('') - lines.append("DotNS Gas Report") + lines.append("dotNS Gas Report") lines.append('') lines.append("") lines.append("") @@ -143,7 +143,7 @@ def generate_html(master_contracts, current_contracts): lines.append('
') lines.append('
') - lines.append('

DotNS Gas Report

') + lines.append('

dotNS Gas Report

') lines.append(f'

Generated {timestamp} | {len(all_contract_names)} contracts

') lines.append("
") diff --git a/scripts/reports/slither-report.py b/scripts/reports/slither-report.py index 2deac7e28..034310fb1 100644 --- a/scripts/reports/slither-report.py +++ b/scripts/reports/slither-report.py @@ -48,7 +48,7 @@ def generate_html(findings): lines = [] lines.append('') lines.append('') - lines.append("DotNS Slither Report") + lines.append("dotNS Slither Report") lines.append('') lines.append("") lines.append("") @@ -56,7 +56,7 @@ def generate_html(findings): lines.append('
') lines.append('
') - lines.append('

DotNS Slither Report

') + lines.append('

dotNS Slither Report

') lines.append(f'

Generated {timestamp} | {total} findings

') lines.append("
") diff --git a/scripts/reports/test-report.py b/scripts/reports/test-report.py index 81205b7bd..f7846e108 100644 --- a/scripts/reports/test-report.py +++ b/scripts/reports/test-report.py @@ -85,7 +85,7 @@ def generate_html(suites): lines = [] lines.append('') lines.append('') - lines.append("DotNS Test Report") + lines.append("dotNS Test Report") lines.append('') lines.append("") lines.append("") @@ -93,7 +93,7 @@ def generate_html(suites): lines.append('
') lines.append('
') - lines.append('

DotNS Test Report

') + lines.append('

dotNS Test Report

') lines.append(f'

Generated {timestamp} | {len(suites)} suites

') lines.append("
") diff --git a/test/base/BaseDotns.t.sol b/test/base/BaseDotns.t.sol index 2d771ca59..ad7454b20 100644 --- a/test/base/BaseDotns.t.sol +++ b/test/base/BaseDotns.t.sol @@ -40,8 +40,8 @@ import {IPersonhood} from "../../contracts/external/personhood/IPersonhood.sol"; import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol"; /// @title BaseDotns -/// @notice Common Foundry test base for deploying a DotNS stack behind UUPS proxies. -/// @dev Deploys and wires the core DotNS contracts used by test suites: +/// @notice Common Foundry test base for deploying a dotNS stack behind UUPS proxies. +/// @dev Deploys and wires the core dotNS contracts used by test suites: /// - StoreFactory: per-user Store instances used for immutable registration writes /// - DotnsRegistrar: ERC721-backed registrar used to allocate label ownership /// - DotnsRegistry: forward registry used to set subnode ownership under .dot @@ -78,7 +78,7 @@ abstract contract BaseDotns is Test { /// @notice Deployed cost-model registry resolved by PopRules under `COST_MODEL`. DotnsCostModelRegistry public costModelRegistry; - /// @notice Deployed DotNS registrar instance. + /// @notice Deployed dotNS registrar instance. DotnsRegistrar public dotnsRegistrar; /// @notice Deployed registrar controller instance. @@ -96,30 +96,42 @@ abstract contract BaseDotns is Test { /// @notice Deployed reverse resolver instance. DotnsReverseResolver public dotnsReverseResolver; - /// @notice Deployed PoP resolver instance (chat keys + lite links). + /// @notice Deployed PoP resolver instance (chat keys + device-name links). DotnsPopResolver public dotnsPopResolver; - /// @notice Deployed PoP controller instance (gateway-driven lite/full issuance). + /// @notice Deployed PoP controller instance (gateway-driven device-name and personhood-name + /// issuance). DotnsPopController public dotnsPopController; /// @notice Deployed PoP lens instance (read-only view over PoP identity data). DotnsPopLens public dotnsPopLens; - /// @notice Selector for the typed reserveLiteName entrypoint. - bytes4 internal constant SELECTOR_RESERVE_LITE_TYPED = - bytes4(keccak256("reserveLiteName((string,address,bytes))")); + /// @notice Selector for the typed issueDeviceName entrypoint. + bytes4 internal constant SELECTOR_ISSUE_DEVICE_NAME = + bytes4(keccak256("issueDeviceName((string,address,bytes))")); - /// @notice Selector for the typed reserveBaseName entrypoint. - bytes4 internal constant SELECTOR_RESERVE_BASE_TYPED = - bytes4(keccak256("reserveBaseName(((string,address,bytes),string))")); + /// @notice Selector for the typed issueDeviceNameWithReservation entrypoint. + bytes4 internal constant SELECTOR_ISSUE_DEVICE_NAME_WITH_RESERVATION = + bytes4(keccak256("issueDeviceNameWithReservation(((string,address,bytes),string))")); - /// @notice Selector for the typed reserveBaseNameOnly entrypoint. - bytes4 internal constant SELECTOR_RESERVE_BASE_ONLY_TYPED = - bytes4(keccak256("reserveBaseNameOnly((address,string))")); + /// @notice Selector for the typed reservePersonhoodName entrypoint. + bytes4 internal constant SELECTOR_RESERVE_PERSONHOOD_NAME = + bytes4(keccak256("reservePersonhoodName((address,string))")); - /// @notice Selector for the typed registerBaseName entrypoint. - bytes4 internal constant SELECTOR_REGISTER_BASE_TYPED = - bytes4(keccak256("registerBaseName((string,address,(uint8,string,bytes)))")); + /// @notice Selector for the typed issuePersonhoodName entrypoint. + bytes4 internal constant SELECTOR_ISSUE_PERSONHOOD_NAME = + bytes4(keccak256("issuePersonhoodName((string,address,(uint8,string,bytes)))")); + + /// @notice Selector of `reserveLiteName((string,address,bytes))`. + /// @dev A literal: the legacy selectors never change while the legacy interface exists, so a + /// signature edit fails the pin in the legacy suite. + bytes4 internal constant SELECTOR_LEGACY_RESERVE_LITE_NAME = 0x220015c0; + + /// @notice Selector of `reserveBaseName(((string,address,bytes),string))`. + bytes4 internal constant SELECTOR_LEGACY_RESERVE_BASE_NAME = 0xd89f1bce; + + /// @notice Selector of `registerBaseName((string,address,(uint8,string,bytes)))`. + bytes4 internal constant SELECTOR_LEGACY_REGISTER_BASE_NAME = 0x7c4b376b; /// @notice Default reservation duration used by the PoP controller. uint64 public constant DEFAULT_RESERVATION_DURATION = 7 days; @@ -157,24 +169,32 @@ abstract contract BaseDotns is Test { bytes32 public constant ZERO_HASH = bytes32(0); // Classification-valid labels for the PoP path (post PopRules enforcement). - // A stem of 7 behind a two-digit suffix classifies as PopLite. These carry the gateway's + // A stem of 7 behind a two-digit suffix classifies as Devicehood. These carry the gateway's // separator, which reaches the chain only through the PoP path: the public path rejects a // separator, so a dotted label is always gateway-issued. - /// @notice PoP lite classification label fixture A. - string internal constant LITE_LABEL_A = "michael.01"; - /// @notice PoP lite classification label fixture B. - string internal constant LITE_LABEL_B = "matthew.02"; - /// @notice PoP lite classification label fixture C. - string internal constant LITE_LABEL_C = "william.03"; - /// @notice PoP lite classification label fixture D. - string internal constant LITE_LABEL_D = "richard.04"; - - // baselength 8 with no trailing digits classifies as PopFull. - /// @notice PoP full classification label fixture A. + /// @notice PoP device-name classification label fixture A. + string internal constant DEVICE_LABEL_A = "michael.01"; + /// @notice PoP device-name classification label fixture B. + string internal constant DEVICE_LABEL_B = "matthew.02"; + /// @notice PoP device-name classification label fixture C. + string internal constant DEVICE_LABEL_C = "william.03"; + /// @notice PoP device-name classification label fixture D. + string internal constant DEVICE_LABEL_D = "richard.04"; + + // Base length 8 with no trailing digits classifies as Personhood. + /// @notice Personhood-name fixture A: a single name, as a person would choose it. + string internal constant PERSONHOOD_LABEL_A = "victoria"; + /// @notice Personhood-name fixture B. + string internal constant PERSONHOOD_LABEL_B = "isabella"; + /// @notice Personhood-name fixture C. + string internal constant PERSONHOOD_LABEL_C = "caroline"; + + // Ordinary labels for suites outside PoP: 8 letters, no digits and no device suffix. + /// @notice Plain label fixture A, for suites that register or grant a label outside PoP. string internal constant BASE_LABEL_A = "alicebob"; - /// @notice PoP full classification label fixture B. + /// @notice Plain label fixture B. string internal constant BASE_LABEL_B = "wonderla"; - /// @notice PoP full classification label fixture C. + /// @notice Plain label fixture C. string internal constant BASE_LABEL_C = "carolboy"; // Measured whole at 11, which is 9 or more, so these classify as NoStatus. @@ -355,7 +375,7 @@ abstract contract BaseDotns is Test { vm.stopPrank(); vm.warp(block.timestamp + 365 days); // Default every account to `None` (NoStatus) on the personhood - // precompile. Per-account `_grantPopFull`/`_grantPopLite` calls install + // precompile. Per-account `_grantPersonhood`/`_grantDevicehood` calls install // more specific mocks that override this selector-only stub. vm.mockCall( DotnsConstants.PERSONHOOD, @@ -425,14 +445,14 @@ abstract contract BaseDotns is Test { node = LabelUtils.namehashUnder(_tldNode(), LabelUtils.labelhashMemory(label)); } - /// @notice Computes the hierarchical subnode for a lite label `.`. - /// @dev A lite username is `stem` beneath the numeric container `suffix.tld`, so its node is + /// @notice Computes the hierarchical subnode for a device name `.`. + /// @dev A device name is `stem` beneath the numeric container `suffix.tld`, so its node is /// `namehash(namehash(tldNode, keccak(suffix)), keccak(stem))`, not a hash of the whole - /// label. Mirrors @custom:function DotnsPopController._liteSubnode. - /// @param liteLabel Lite label, e.g. `michael.01`. + /// label. Mirrors @custom:function DotnsPopController._deviceSubnode. + /// @param deviceLabel Device name, e.g. `michael.01`. /// @return node The subnode identifier. - function _liteNodeOf(string memory liteLabel) internal pure returns (bytes32 node) { - (string memory stem, string memory suffix) = StringUtils.splitLiteLabel(liteLabel); + function _deviceNodeOf(string memory deviceLabel) internal pure returns (bytes32 node) { + (string memory stem, string memory suffix) = StringUtils.splitDeviceLabel(deviceLabel); bytes32 parentNode = LabelUtils.namehashUnder(_tldNode(), LabelUtils.labelhashMemory(suffix)); node = LabelUtils.namehashUnder(parentNode, LabelUtils.labelhashMemory(stem)); @@ -452,13 +472,14 @@ abstract contract BaseDotns is Test { /// @notice Mocks the personhood precompile so it returns `tier` for `who` /// under the dotns context. /// @dev Single source of truth for tier mocking. Tier numbering matches - /// @custom:contract IPersonhood: 0=None, 1=Lite, 2=Full. `contextAlias` is non-zero + /// @custom:contract IPersonhood, which names them 0=None, 1=Lite, 2=Full; + /// 1 is `Devicehood` and 2 is `Personhood` here. `contextAlias` is non-zero /// whenever `tier != 0` so callers that read it (cross-context /// identity tests) still see a deterministic value. function _setUserPopStatus(address who, IPopRules.PopStatus tier) internal { uint8 status; - if (tier == IPopRules.PopStatus.PopFull) status = 2; - else if (tier == IPopRules.PopStatus.PopLite) status = 1; + if (tier == IPopRules.PopStatus.Personhood) status = 2; + else if (tier == IPopRules.PopStatus.Devicehood) status = 1; // PopStatus.Reserved is a label classification, never a user tier; map // anything else to None. bytes32 contextAlias = status == 0 ? bytes32(0) : keccak256(abi.encode(who, status)); @@ -471,14 +492,14 @@ abstract contract BaseDotns is Test { ); } - /// @notice Grants PopFull status to `who` via the personhood precompile mock. - function _grantPopFull(address who) internal { - _setUserPopStatus(who, IPopRules.PopStatus.PopFull); + /// @notice Grants Personhood status to `who` via the personhood precompile mock. + function _grantPersonhood(address who) internal { + _setUserPopStatus(who, IPopRules.PopStatus.Personhood); } - /// @notice Grants PopLite status to `who` via the personhood precompile mock. - function _grantPopLite(address who) internal { - _setUserPopStatus(who, IPopRules.PopStatus.PopLite); + /// @notice Grants Devicehood status to `who` via the personhood precompile mock. + function _grantDevicehood(address who) internal { + _setUserPopStatus(who, IPopRules.PopStatus.Devicehood); } /// @notice Resets `who` back to `None` on the personhood precompile mock. @@ -522,22 +543,22 @@ abstract contract BaseDotns is Test { /// against the resolver and store hold. Chat keys are persisted eagerly on the PoP /// resolver at reserve time regardless of settlement. Tests that want to observe /// cold-path semantics (label stashed, no store deployed) must call - /// @custom:function _rootReserveBaseName or @custom:function _rootReserveLiteName - /// directly. + /// @custom:function _rootIssueDeviceNameWithReservation or + /// @custom:function _rootIssueDeviceName directly. function _reservePop( address user, - string memory liteLabel, + string memory deviceLabel, bytes memory chatKey, - string memory reservedBaseLabel + string memory reservedLabel ) internal { - _rootReserveBaseName( - IDotnsPopController.BaseReservation({ - lite: IDotnsPopController.LiteRegistration({ - liteLabel: liteLabel, user: user, chatKey: chatKey + _rootIssueDeviceNameWithReservation( + IDotnsPopController.DeviceNameIssuanceWithReservation({ + issuance: IDotnsPopController.DeviceNameIssuance({ + label: deviceLabel, user: user, chatKey: chatKey }), - reservedBaseLabel: reservedBaseLabel + reservedLabel: reservedLabel }) ); if (dotnsPopController.pendingClaimCountOf(user) != 0) { @@ -546,28 +567,59 @@ abstract contract BaseDotns is Test { } } - /// @notice Dispatches the typed `reserveLiteName` call under a mocked Root origin. - function _rootReserveLiteName(IDotnsPopController.LiteRegistration memory params) internal { - _dispatchFromRoot(abi.encodeWithSelector(SELECTOR_RESERVE_LITE_TYPED, params)); + /// @notice Dispatches the typed `issueDeviceName` call under a mocked Root origin. + function _rootIssueDeviceName(IDotnsPopController.DeviceNameIssuance memory params) internal { + _dispatchFromRoot(abi.encodeWithSelector(SELECTOR_ISSUE_DEVICE_NAME, params)); } /// @notice Dispatches the typed `reserveBaseName` call under a mocked Root origin. - function _rootReserveBaseName(IDotnsPopController.BaseReservation memory params) internal { - _dispatchFromRoot(abi.encodeWithSelector(SELECTOR_RESERVE_BASE_TYPED, params)); + function _rootIssueDeviceNameWithReservation( + IDotnsPopController.DeviceNameIssuanceWithReservation memory params + ) + internal + { + _dispatchFromRoot( + abi.encodeWithSelector(SELECTOR_ISSUE_DEVICE_NAME_WITH_RESERVATION, params) + ); } - /// @notice Dispatches the typed `reserveBaseNameOnly` call under a mocked Root origin. - function _rootReserveBaseNameOnly(IDotnsPopController.BaseNameReservation memory params) + /// @notice Dispatches the typed `reservePersonhoodName` call under a mocked Root origin. + function _rootReservePersonhoodName(IDotnsPopController.PersonhoodNameReservation memory params) internal { - _dispatchFromRoot(abi.encodeWithSelector(SELECTOR_RESERVE_BASE_ONLY_TYPED, params)); + _dispatchFromRoot(abi.encodeWithSelector(SELECTOR_RESERVE_PERSONHOOD_NAME, params)); } - /// @notice Dispatches the typed `registerBaseName` call under a mocked Root origin. + /// @notice Dispatches the typed `issuePersonhoodName` call under a mocked Root origin. /// @dev Dispatches the label as written. The gateway sends what People Chain holds, so a - /// helper that reshaped it would hide whether a fixture is a valid lite label. - function _rootRegisterBaseName(IDotnsPopController.FullRegistration memory params) internal { - _dispatchFromRoot(abi.encodeWithSelector(SELECTOR_REGISTER_BASE_TYPED, params)); + /// helper that reshaped it would hide whether a fixture is a valid device name. + function _rootIssuePersonhoodName(IDotnsPopController.PersonhoodNameIssuance memory params) + internal + { + _dispatchFromRoot(abi.encodeWithSelector(SELECTOR_ISSUE_PERSONHOOD_NAME, params)); + } + + /// @notice Dispatches the legacy `reserveLiteName` call under a mocked Root origin. + function _rootLegacyReserveLiteName(IDotnsPopController.DeviceNameIssuance memory params) + internal + { + _dispatchFromRoot(abi.encodeWithSelector(SELECTOR_LEGACY_RESERVE_LITE_NAME, params)); + } + + /// @notice Dispatches the legacy `reserveBaseName` call under a mocked Root origin. + function _rootLegacyReserveBaseName( + IDotnsPopController.DeviceNameIssuanceWithReservation memory params + ) + internal + { + _dispatchFromRoot(abi.encodeWithSelector(SELECTOR_LEGACY_RESERVE_BASE_NAME, params)); + } + + /// @notice Dispatches the legacy `registerBaseName` call under a mocked Root origin. + function _rootLegacyRegisterBaseName(IDotnsPopController.PersonhoodNameIssuance memory params) + internal + { + _dispatchFromRoot(abi.encodeWithSelector(SELECTOR_LEGACY_REGISTER_BASE_NAME, params)); } /// @notice Calls `payload` on the PoP controller under a mocked Root origin. @@ -591,25 +643,25 @@ abstract contract BaseDotns is Test { return data; } - /// @notice Constructs a `Link` that inherits the chat key from a prior lite label. - function _linkWithLite(string memory liteLabel) + /// @notice Constructs a `Link` that inherits the chat key from a prior device name. + function _linkWithDeviceName(string memory deviceLabel) internal pure returns (IDotnsPopController.Link memory) { return IDotnsPopController.Link({ - kind: IDotnsPopController.LinkKind.LiteUsername, liteLabel: liteLabel, chatKey: "" + kind: IDotnsPopController.LinkKind.DeviceName, deviceLabel: deviceLabel, chatKey: "" }); } - /// @notice Constructs a `Link` carrying a fresh chat key (no lite inheritance). + /// @notice Constructs a `Link` carrying a fresh chat key (no device-name inheritance). function _linkFresh(bytes memory chatKey) internal pure returns (IDotnsPopController.Link memory) { return IDotnsPopController.Link({ - kind: IDotnsPopController.LinkKind.None, liteLabel: "", chatKey: chatKey + kind: IDotnsPopController.LinkKind.None, deviceLabel: "", chatKey: chatKey }); } @@ -717,7 +769,7 @@ abstract contract BaseDotns is Test { /// @notice Registers `label` for `labelOwner` under the requested PoP status and /// returns its node. - /// @dev For NoStatus, no status is set on the oracle. For PopLite/PopFull, status is set + /// @dev For NoStatus, no status is set on the oracle. For Devicehood/Personhood, status is set /// for `(labelOwner, label)` before commit-reveal. /// @param label The label to register (without the `.dot` suffix). /// @param labelOwner The address that will own the registered label. diff --git a/test/fuzz/pop/PopFuzz.t.sol b/test/fuzz/pop/PopFuzz.t.sol index 6d6a83256..92d8d0ab3 100644 --- a/test/fuzz/pop/PopFuzz.t.sol +++ b/test/fuzz/pop/PopFuzz.t.sol @@ -8,34 +8,34 @@ import {IPopRules} from "../../../contracts/pop/IPopRules.sol"; /// @notice Property-based tests for @custom:contract PopRules classification, reservation and /// pricing. contract PopRulesFuzzTest is BaseDotns { - function testFuzz_popfull_user_can_access_poplite(uint256 seed, uint256 length) public { + function testFuzz_personhood_user_can_access_devicehood(uint256 seed, uint256 length) public { length = bound(length, 6, 8); - // PopLite is the gateway's separated form, so the stem carries the separator; `length` - // bounds the stem, which is what sets the band. + // Devicehood is the gateway's separated form, so the label carries the separator; + // `length` bounds the stem, which is what sets the band. string memory nameLabel = string(abi.encodePacked(_makeAlpha(seed, length), ".01")); - _grantPopFull(ed); + _grantPersonhood(ed); IPopRules.PriceWithMeta memory priceMetadata = popRules.priceWithCheck(nameLabel, ed); - assertEq(uint256(priceMetadata.status), uint256(IPopRules.PopStatus.PopLite)); - assertEq(uint256(priceMetadata.userStatus), uint256(IPopRules.PopStatus.PopFull)); + assertEq(uint256(priceMetadata.status), uint256(IPopRules.PopStatus.Devicehood)); + assertEq(uint256(priceMetadata.userStatus), uint256(IPopRules.PopStatus.Personhood)); } - function testFuzz_popfull_user_can_access_nostatus(uint256 seed, uint256 length) public { + function testFuzz_personhood_user_can_access_nostatus(uint256 seed, uint256 length) public { length = bound(length, 9, 14); // Measured whole, so the suffix only lengthens it and the band stays NoStatus. string memory nameLabel = string(abi.encodePacked(_makeAlpha(seed, length), "01")); - _grantPopFull(ed); + _grantPersonhood(ed); IPopRules.PriceWithMeta memory priceMetadata = popRules.priceWithCheck(nameLabel, ed); assertEq(uint256(priceMetadata.status), uint256(IPopRules.PopStatus.NoStatus)); - assertEq(uint256(priceMetadata.userStatus), uint256(IPopRules.PopStatus.PopFull)); + assertEq(uint256(priceMetadata.userStatus), uint256(IPopRules.PopStatus.Personhood)); } - function testFuzz_nostatus_user_cannot_access_popfull(uint256 seed) public { + function testFuzz_nostatus_user_cannot_access_personhood(uint256 seed) public { string memory nameLabel = _makeAlpha(seed, 8); _grantNoStatus(ed); @@ -103,7 +103,7 @@ contract PopRulesFuzzTest is BaseDotns { assertEq(priceMetadata.price, popRules.price(nameLabel)); } - function testFuzz_expired_reservation_rolls_forward_to_next_lite_registrant(uint256 seed) + function testFuzz_expired_reservation_rolls_forward_to_next_device_registrant(uint256 seed) public { string memory baseName = _makeAlpha(seed, 6); @@ -135,7 +135,7 @@ contract PopRulesFuzzTest is BaseDotns { baseName[0] = bytes1(uint8(baseName[0]) - 32); string memory mixedCaseName = string(abi.encodePacked(string(baseName), "01")); - _grantPopLite(ed); + _grantDevicehood(ed); vm.expectPartialRevert(IPopRules.PopError.selector); popRules.priceWithCheck(mixedCaseName, ed); diff --git a/test/fuzz/registrar/DotnsPopControllerFuzz.t.sol b/test/fuzz/registrar/DotnsPopControllerFuzz.t.sol index 996939001..c2f967ea9 100644 --- a/test/fuzz/registrar/DotnsPopControllerFuzz.t.sol +++ b/test/fuzz/registrar/DotnsPopControllerFuzz.t.sol @@ -26,20 +26,22 @@ contract DotnsPopControllerFuzz is BaseDotns { return StringUtils.uintToString(value); } - function testFuzz_reserveBaseName_accepts_any_two_digit_suffix(uint8 suffix) public { + function testFuzz_issueDeviceNameWithReservation_accepts_any_two_digit_suffix(uint8 suffix) + public + { suffix = uint8(bound(uint256(suffix), 0, 99)); string memory label = string.concat("joseph", ".", _twoDigitDecimal(uint256(suffix))); - // Lite-tier label requires ed to hold PopLite (or PopFull) status so the + // Device-name label requires ed to hold Devicehood (or Personhood) status so the // `priceWithCheck` guard inside the controller accepts the reservation. - _grantPopLite(ed); + _grantDevicehood(ed); _reservePop(ed, label, "", ""); - assertEq(dotnsRegistry.owner(_liteNodeOf(label)), ed); + assertEq(dotnsRegistry.owner(_deviceNodeOf(label)), ed); } - function testFuzz_reserveBaseName_persists_chat_key_exact_bytes( + function testFuzz_issueDeviceNameWithReservation_persists_chat_key_exact_bytes( uint8 suffix, bytes1 keySeed, bool useKey @@ -49,12 +51,12 @@ contract DotnsPopControllerFuzz is BaseDotns { suffix = uint8(bound(uint256(suffix), 0, 99)); string memory label = string.concat("joseph", ".", _twoDigitDecimal(uint256(suffix))); - _grantPopLite(ed); + _grantDevicehood(ed); bytes memory chatKey = useKey ? _validChatKey(keySeed) : bytes(""); _reservePop(ed, label, chatKey, ""); - bytes32 node = _liteNodeOf(label); + bytes32 node = _deviceNodeOf(label); if (chatKey.length == 0) { assertEq(dotnsPopResolver.chatKey(node).length, 0); } else { @@ -62,24 +64,24 @@ contract DotnsPopControllerFuzz is BaseDotns { } } - /// @dev The reservation covers the stem, and the stem is what a public registrant contends - /// for: a digit-suffixed spelling is measured as written and so is an unrelated name - /// the reservation never sees. + /// @dev The reservation covers the base name, and the base name is what a public registrant + /// contends for: a digit-suffixed spelling is measured as written and so is an unrelated + /// name the reservation never sees. function testFuzz_public_register_respects_popRules_reservation(bool reserveFirst) public { - string memory stem = "longnamebob"; + string memory baseName = "longnamebob"; if (reserveFirst) { // The reserved base label `longnamebob` classifies as NoStatus (11 characters); the - // lite label has to be PopLite-eligible. Tiago needs PopFull status so both + // device name has to be Devicehood-eligible. Tiago needs Personhood status so both // `priceWithCheck` calls inside `reserveBaseName` succeed. - _grantPopFull(tiago); - _reservePop(tiago, LITE_LABEL_A, "", stem); + _grantPersonhood(tiago); + _reservePop(tiago, DEVICE_LABEL_A, "", baseName); } - bytes32 secret = keccak256(abi.encodePacked(stem, ed, block.timestamp)); + bytes32 secret = keccak256(abi.encodePacked(baseName, ed, block.timestamp)); IDotnsRegistrarController.Registration memory registration = IDotnsRegistrarController.Registration({ - label: stem, + label: baseName, owner: ed, secret: secret, reserved: true, @@ -95,16 +97,17 @@ contract DotnsPopControllerFuzz is BaseDotns { if (reserveFirst) { vm.expectRevert( abi.encodeWithSelector( - IPopRules.PopError.selector, "Base name reserved for original Lite registrant" + IPopRules.PopError.selector, + "Reserved for a device-name holder's personhood claim" ) ); vm.prank(ed); dotnsRegistrarController.register{value: 1 ether}(registration); } else { - uint256 cost = popRules.priceWithCheck(stem, ed).price; + uint256 cost = popRules.priceWithCheck(baseName, ed).price; vm.prank(ed); dotnsRegistrarController.register{value: cost}(registration); - assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(_nodeOf(stem))), ed); + assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(_nodeOf(baseName))), ed); } } @@ -124,15 +127,15 @@ contract DotnsPopControllerFuzz is BaseDotns { vm.prank(owner); dotnsPopController.setReservationDuration(duration); - // Reserving `BASE_LABEL_A` (PopFull classification) alongside the - // lite leg requires ed to hold PopFull status so both `priceWithCheck` + // Reserving `PERSONHOOD_LABEL_A` (Personhood classification) alongside the + // the issuance requires ed to hold Personhood status so both `priceWithCheck` // calls succeed. - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, "", BASE_LABEL_A); + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, "", PERSONHOOD_LABEL_A); vm.warp(block.timestamp + uint256(elapsed)); - (bool reserved, address holder) = dotnsPopController.isReservedForClaim(BASE_LABEL_A); + (bool reserved, address holder) = dotnsPopController.isReservedForClaim(PERSONHOOD_LABEL_A); if (uint256(elapsed) <= uint256(duration)) { assertTrue(reserved); assertEq(holder, ed); @@ -152,9 +155,9 @@ contract DotnsPopControllerFuzz is BaseDotns { string memory label = string.concat("joseph", ".", _twoDigitDecimal(uint256(suffix))); bytes memory chatKey = _validChatKey(keySeed); - _grantPopLite(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({liteLabel: label, user: ed, chatKey: chatKey}) + _grantDevicehood(ed); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({label: label, user: ed, chatKey: chatKey}) ); IDotnsPopController.PendingClaim[] memory pending = @@ -164,7 +167,7 @@ contract DotnsPopControllerFuzz is BaseDotns { assertEq(storeFactory.getLabelStore(ed), address(0)); // Chat key is persisted eagerly on the resolver at reserve time, even though // the LabelStore write is deferred to settlement on the cold path. - bytes32 node = _liteNodeOf(label); + bytes32 node = _deviceNodeOf(label); assertEq(dotnsPopResolver.chatKey(node), chatKey); } @@ -178,15 +181,15 @@ contract DotnsPopControllerFuzz is BaseDotns { string memory label = string.concat("joseph", ".", _twoDigitDecimal(uint256(suffix))); bytes memory chatKey = _validChatKey(keySeed); - _grantPopLite(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({liteLabel: label, user: ed, chatKey: chatKey}) + _grantDevicehood(ed); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({label: label, user: ed, chatKey: chatKey}) ); vm.prank(ed); dotnsPopController.settlePendingClaims(ed, type(uint256).max); - bytes32 node = _liteNodeOf(label); + bytes32 node = _deviceNodeOf(label); address store = storeFactory.getLabelStore(ed); assertTrue(store != address(0)); assertEq(ILabelStore(store).getLabel(node), string.concat(label, protocolRegistry.tld())); @@ -210,10 +213,10 @@ contract DotnsPopControllerFuzz is BaseDotns { vm.prank(owner); dotnsPopController.setReservationDuration(duration); - _grantPopLite(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0x77) + _grantDevicehood(ed); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0x77) }) ); @@ -221,18 +224,18 @@ contract DotnsPopControllerFuzz is BaseDotns { vm.warp(uint256(mintedAt) + uint256(elapsed)); // Stores always settle: settlement writes the label into the store whether or not the - // reservation deadline has passed, so age never strands a claim. + // claim is older than `reservationDuration`, so age never strands a claim. vm.prank(ed); (uint256 settledCount, bool moreRemaining) = dotnsPopController.settlePendingClaims(ed, type(uint256).max); assertEq(settledCount, 1); assertFalse(moreRemaining); - bytes32 node = _liteNodeOf(LITE_LABEL_A); + bytes32 node = _deviceNodeOf(DEVICE_LABEL_A); address store = storeFactory.getLabelStore(ed); assertTrue(store != address(0)); assertEq( - ILabelStore(store).getLabel(node), string.concat(LITE_LABEL_A, protocolRegistry.tld()) + ILabelStore(store).getLabel(node), string.concat(DEVICE_LABEL_A, protocolRegistry.tld()) ); assertEq(dotnsPopController.pendingClaimCountOf(ed), 0); } diff --git a/test/fuzz/registrar/DotnsRegistrarControllerFuzz.t.sol b/test/fuzz/registrar/DotnsRegistrarControllerFuzz.t.sol index e43eb3d50..227bf4c52 100644 --- a/test/fuzz/registrar/DotnsRegistrarControllerFuzz.t.sol +++ b/test/fuzz/registrar/DotnsRegistrarControllerFuzz.t.sol @@ -57,9 +57,9 @@ contract DotnsRegistrarControllerFuzzTest is BaseDotns { function testFuzz_register_refunds_overpayment_inline(uint256 extra, uint256 salt) public { address registrant = tiago; - string memory nameLabel = _labelPopFullPriced(bound(salt, 0, 64)); + string memory nameLabel = _labelPersonhoodPriced(bound(salt, 0, 64)); - _grantPopFull(registrant); + _grantPersonhood(registrant); IDotnsRegistrarController.Registration memory registration = _commitFor(nameLabel, registrant, false); @@ -104,8 +104,8 @@ contract DotnsRegistrarControllerFuzzTest is BaseDotns { address payer = leonardo; string memory nameLabel = _labelPopfull(bound(salt, 0, 64)); - _grantPopFull(nameOwner); - _grantPopFull(payer); + _grantPersonhood(nameOwner); + _grantPersonhood(payer); IDotnsRegistrarController.Registration memory registration = _commitFor(nameLabel, nameOwner, true, payer); @@ -151,7 +151,7 @@ contract DotnsRegistrarControllerFuzzTest is BaseDotns { address recipient = leonardo; string memory nameLabel = _labelPopfull(bound(salt, 0, 64)); - _grantPopFull(sender); + _grantPersonhood(sender); IDotnsRegistrarController.Registration memory registration = _commitFor(nameLabel, sender, true); @@ -185,7 +185,7 @@ contract DotnsRegistrarControllerFuzzTest is BaseDotns { uint256 primarySalt = bound(salt, 0, 63); string memory primaryName = _labelPopfull(primarySalt); - _grantPopFull(nameOwner); + _grantPersonhood(nameOwner); IDotnsRegistrarController.Registration memory primaryRegistration = _commitFor(primaryName, nameOwner, true); @@ -198,7 +198,7 @@ contract DotnsRegistrarControllerFuzzTest is BaseDotns { string memory giftedName = _labelNoStatusPriced(primarySalt + 1); - _grantPopFull(payer); + _grantPersonhood(payer); IDotnsRegistrarController.Registration memory giftedRegistration = _commitFor(giftedName, nameOwner, true, payer); @@ -299,7 +299,7 @@ contract DotnsRegistrarControllerFuzzTest is BaseDotns { address recipient = leonardo; string memory nameLabel = _labelPopfull(bound(salt, 0, 64)); - _grantPopFull(sender); + _grantPersonhood(sender); IDotnsRegistrarController.Registration memory registration = _commitFor(nameLabel, sender, true); @@ -366,7 +366,7 @@ contract DotnsRegistrarControllerFuzzTest is BaseDotns { vm.warp(block.timestamp + dotnsRegistrarController.minCommitmentAge() + 1); } - /// @notice Generate a label that classifies as PopFull (baselength 8, no trailing digits). + /// @notice Generate a label that classifies as Personhood (baselength 8, no trailing digits). function _labelPopfull(uint256 salt) internal pure returns (string memory label) { return string(abi.encodePacked("popful", _uintToAlphaFixed(salt, 2))); } @@ -377,10 +377,10 @@ contract DotnsRegistrarControllerFuzzTest is BaseDotns { return string(abi.encodePacked("nostatu", _uintToAlphaFixed(salt, 2), "01")); } - /// @notice Generate an 8-character PopFull-tier label, measured whole, that prices above - /// the floor so the amount is non-zero. A public fixture cannot be PopLite: that is + /// @notice Generate an 8-character Personhood-tier label, measured whole, that prices above + /// the floor so the amount is non-zero. A public fixture cannot be Devicehood: that is /// the gateway's separated form and this path rejects a separator. - function _labelPopFullPriced(uint256 salt) internal pure returns (string memory label) { + function _labelPersonhoodPriced(uint256 salt) internal pure returns (string memory label) { return string(abi.encodePacked("free", _uintToAlphaFixed(salt, 2), "01")); } diff --git a/test/intergration/BasicDotns.reverts.t.sol b/test/intergration/BasicDotns.reverts.t.sol index e436bbf77..6a18e88ca 100644 --- a/test/intergration/BasicDotns.reverts.t.sol +++ b/test/intergration/BasicDotns.reverts.t.sol @@ -11,18 +11,18 @@ import {IDotnsRegistrarController} from "../../contracts/registrars/IDotnsRegist /// @notice Integration coverage for revert paths protecting registration, subnames, /// and resolver writes. contract BasicDotnsIntegrationReverts is BaseDotns { - /// @notice PopFull-classified label fixture exercised by the revert cases. + /// @notice Personhood-classified label fixture exercised by the revert cases. string internal constant NAME_POPFULL = "waytalls"; /// @notice Sample CIDv1 content hash used as a record value. bytes internal constant CID_A = hex"e30101701220aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - function test_revert_poplite_cannot_register_popfull_required() public { + function test_revert_devicehood_cannot_register_personhood_required() public { string memory nameLabel = NAME_POPFULL; address registrant = ed; - _grantPopLite(registrant); + _grantDevicehood(registrant); vm.startPrank(registrant); bytes32 secret = keccak256(abi.encodePacked(nameLabel, registrant, block.timestamp)); @@ -41,11 +41,7 @@ contract BasicDotnsIntegrationReverts is BaseDotns { vm.warp(block.timestamp + dotnsRegistrarController.minCommitmentAge() + 1); - vm.expectRevert( - abi.encodeWithSelector( - IPopRules.PopError.selector, "Requires Full personhood verification" - ) - ); + vm.expectRevert(abi.encodeWithSelector(IPopRules.PopError.selector, "Requires personhood")); dotnsRegistrarController.register(registration); vm.stopPrank(); } @@ -54,7 +50,7 @@ contract BasicDotnsIntegrationReverts is BaseDotns { address parentOwner = ed; address attacker = tiago; - _grantPopFull(parentOwner); + _grantPersonhood(parentOwner); _commitAndRegister(NAME_POPFULL, parentOwner, true); bytes32 parentNode = _namehash(dotNode, keccak256(bytes(NAME_POPFULL))); @@ -76,7 +72,7 @@ contract BasicDotnsIntegrationReverts is BaseDotns { function test_parent_can_reassign_existing_subdomain() public { address parentOwner = ed; - _grantPopFull(parentOwner); + _grantPersonhood(parentOwner); _commitAndRegister(NAME_POPFULL, parentOwner, true); bytes32 parentNode = _namehash(dotNode, keccak256(bytes(NAME_POPFULL))); @@ -104,7 +100,7 @@ contract BasicDotnsIntegrationReverts is BaseDotns { address parentOwner = ed; address attacker = tiago; - _grantPopFull(parentOwner); + _grantPersonhood(parentOwner); _commitAndRegister(NAME_POPFULL, parentOwner, true); bytes32 node = _namehash(dotNode, keccak256(bytes(NAME_POPFULL))); diff --git a/test/intergration/BasicDotns.t.sol b/test/intergration/BasicDotns.t.sol index ac068afd4..31fefc7e3 100644 --- a/test/intergration/BasicDotns.t.sol +++ b/test/intergration/BasicDotns.t.sol @@ -10,9 +10,9 @@ import {ILabelStore} from "../../contracts/store/ILabelStore.sol"; /// @notice End-to-end happy-path integration coverage for registration, /// records, subnames, and ERC721 transfers across all PoP tiers. contract BasicDotnsIntegration is BaseDotns { - /// @notice PopFull-classified label fixture used by the integration flow. + /// @notice Personhood-classified label fixture used by the integration flow. string internal constant NAME_POPFULL = "waytalls"; - /// @notice PopLite-classified label fixture used by the integration flow. + /// @notice Devicehood-classified label fixture used by the integration flow. string internal constant NAME_POPLITE = "way2tall01"; /// @notice NoStatus-classified label fixture used by the integration flow. string internal constant NAME_NOSTATUS = "kitesurfing01"; @@ -37,8 +37,8 @@ contract BasicDotnsIntegration is BaseDotns { string transferRecipientSub; } - function test_popfull_end_to_end() public { - _grantPopFull(ed); + function test_personhood_end_to_end() public { + _grantPersonhood(ed); _flowEndToEnd( FlowParams({ @@ -55,8 +55,8 @@ contract BasicDotnsIntegration is BaseDotns { ); } - function test_poplite_end_to_end() public { - _grantPopLite(leonardo); + function test_devicehood_end_to_end() public { + _grantDevicehood(leonardo); _flowEndToEnd( FlowParams({ @@ -222,7 +222,7 @@ contract BasicDotnsIntegration is BaseDotns { _commitAndRegister(NAME_NOSTATUS, victim, false); assertEq(dotnsReverseResolver.nameOf(victim), ""); - _grantPopFull(victim); + _grantPersonhood(victim); string memory victimPrimary = "victimname01"; _commitAndRegister(victimPrimary, victim, true); diff --git a/test/intergration/PopLifecycleFlow.t.sol b/test/intergration/PopLifecycleFlow.t.sol index 8c6daf07f..ec436751f 100644 --- a/test/intergration/PopLifecycleFlow.t.sol +++ b/test/intergration/PopLifecycleFlow.t.sol @@ -11,15 +11,13 @@ import {ILabelStore} from "../../contracts/store/ILabelStore.sol"; import {LabelUtils} from "../../contracts/utils/LabelUtils.sol"; /// @title PopLifecycleFlow -/// @notice Integration coverage for a PoP-gateway-minted full username across +/// @notice Integration coverage for a PoP-gateway-minted personhood name across /// its full on-chain lifecycle: reservation, claim, record writes, /// subname issuance, transfer, and cross-contract lookup paths a -/// downstream consumer walks starting from the lite username string. +/// downstream consumer walks starting from the device name string. contract PopLifecycleFlow is BaseDotns { - /// @notice Lite label fixture. Baselength 7 with 2 trailing digits classifies as PopLite. - string internal constant LITE_LABEL = "michael.01"; - /// @notice Full label fixture. Baselength 9 with no trailing digits classifies as PopFull. - string internal constant FULL_LABEL = "alicefull"; + /// @notice Device name fixture. Baselength 7 with 2 trailing digits classifies as Devicehood. + string internal constant DEVICE_LABEL = "michael.01"; /// @notice Subname label used for the subnode portion of the flow. string internal constant SUB_LABEL = "app"; /// @notice 65-byte canonical chat-key payload (secp256k1 uncompressed shape). @@ -32,94 +30,102 @@ contract PopLifecycleFlow is BaseDotns { bytes internal constant CONTENT_HASH_B = hex"e30101701220bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; - function test_recover_full_username_from_lite_label() public { - _mintLiteThenClaimFull(ed); + function test_recover_personhood_name_from_device_name() public { + _issueDeviceThenClaimPersonhood(ed); - bytes32 liteLabelhash = LabelUtils.labelhashMemory(LITE_LABEL); - bytes32 fullNode = dotnsPopResolver.fullClaim(liteLabelhash); - assertTrue(fullNode != bytes32(0), "lite label has no full claim"); + bytes32 deviceLabelhash = LabelUtils.labelhashMemory(DEVICE_LABEL); + bytes32 personhoodNode = dotnsPopResolver.personhoodNodeOf(deviceLabelhash); + assertTrue(personhoodNode != bytes32(0), "device name has no personhood claim"); - address owner = IERC721(address(dotnsRegistrar)).ownerOf(uint256(fullNode)); + address owner = IERC721(address(dotnsRegistrar)).ownerOf(uint256(personhoodNode)); assertEq(owner, ed); - assertEq(dotnsRegistrar.labelOf(uint256(fullNode)), FULL_LABEL); - assertEq(dotnsPopResolver.chatKey(fullNode), CHAT_KEY); + assertEq(dotnsRegistrar.labelOf(uint256(personhoodNode)), PERSONHOOD_LABEL_A); + assertEq(dotnsPopResolver.chatKey(personhoodNode), CHAT_KEY); // Same label mirrored in the owner's Store under the canonical store key. ILabelStore ownerStore = ILabelStore(storeFactory.getLabelStore(owner)); - assertEq(ownerStore.getLabel(fullNode), string.concat(FULL_LABEL, protocolRegistry.tld())); + assertEq( + ownerStore.getLabel(personhoodNode), + string.concat(PERSONHOOD_LABEL_A, protocolRegistry.tld()) + ); } - function test_pop_full_name_is_soulbound_but_fully_usable() public { - _mintLiteThenClaimFull(ed); + function test_personhood_name_is_soulbound_but_fully_usable() public { + _issueDeviceThenClaimPersonhood(ed); - bytes32 fullNode = _nodeOf(FULL_LABEL); - uint256 fullTokenId = uint256(fullNode); - bytes32 liteLabelhash = LabelUtils.labelhashMemory(LITE_LABEL); + bytes32 personhoodNode = _nodeOf(PERSONHOOD_LABEL_A); + uint256 personhoodTokenId = uint256(personhoodNode); + bytes32 deviceLabelhash = LabelUtils.labelhashMemory(DEVICE_LABEL); - assertEq(IERC721(address(dotnsRegistrar)).ownerOf(fullTokenId), ed); - assertEq(dotnsRegistry.owner(fullNode), ed); - assertEq(dotnsRegistrar.labelOf(fullTokenId), FULL_LABEL); - assertEq(dotnsPopResolver.chatKey(fullNode), CHAT_KEY); - assertEq(dotnsPopResolver.liteLink(fullNode), liteLabelhash); - assertEq(dotnsPopResolver.fullClaim(liteLabelhash), fullNode); - assertTrue(dotnsRegistrar.isSoulbound(fullTokenId)); + assertEq(IERC721(address(dotnsRegistrar)).ownerOf(personhoodTokenId), ed); + assertEq(dotnsRegistry.owner(personhoodNode), ed); + assertEq(dotnsRegistrar.labelOf(personhoodTokenId), PERSONHOOD_LABEL_A); + assertEq(dotnsPopResolver.chatKey(personhoodNode), CHAT_KEY); + assertEq(dotnsPopResolver.deviceLabelhashOf(personhoodNode), deviceLabelhash); + assertEq(dotnsPopResolver.personhoodNodeOf(deviceLabelhash), personhoodNode); + assertTrue(dotnsRegistrar.isSoulbound(personhoodTokenId)); // The name is fully usable by its owner: records and subnames work. vm.prank(ed); - dotnsContentResolver.setContenthash(fullNode, CONTENT_HASH_A); - assertEq(dotnsContentResolver.contenthash(fullNode), CONTENT_HASH_A); + dotnsContentResolver.setContenthash(personhoodNode, CONTENT_HASH_A); + assertEq(dotnsContentResolver.contenthash(personhoodNode), CONTENT_HASH_A); - bytes32 subnode = _setSubnode(ed, fullNode, SUB_LABEL, FULL_LABEL, leonardo); + bytes32 subnode = _setSubnode(ed, personhoodNode, SUB_LABEL, PERSONHOOD_LABEL_A, leonardo); assertEq(dotnsRegistry.owner(subnode), leonardo); // It is soulbound: quoting a transfer and attempting one both revert, and ownership // does not move. - vm.expectRevert(abi.encodeWithSelector(IDotnsRegistrar.NameSoulbound.selector, fullTokenId)); - dotnsRegistrar.quoteTransferFee(fullTokenId, tiago); + vm.expectRevert( + abi.encodeWithSelector(IDotnsRegistrar.NameSoulbound.selector, personhoodTokenId) + ); + dotnsRegistrar.quoteTransferFee(personhoodTokenId, tiago); - vm.expectRevert(abi.encodeWithSelector(IDotnsRegistrar.NameSoulbound.selector, fullTokenId)); + vm.expectRevert( + abi.encodeWithSelector(IDotnsRegistrar.NameSoulbound.selector, personhoodTokenId) + ); vm.prank(ed); - dotnsRegistrar.transferFrom(ed, tiago, fullTokenId); + dotnsRegistrar.transferFrom(ed, tiago, personhoodTokenId); - assertEq(IERC721(address(dotnsRegistrar)).ownerOf(fullTokenId), ed); - assertEq(dotnsRegistry.owner(fullNode), ed); + assertEq(IERC721(address(dotnsRegistrar)).ownerOf(personhoodTokenId), ed); + assertEq(dotnsRegistry.owner(personhoodNode), ed); // PoP-layer records and the owner's continued control are untouched by the blocked move. - assertEq(dotnsPopResolver.chatKey(fullNode), CHAT_KEY); - assertEq(dotnsPopResolver.fullClaim(liteLabelhash), fullNode); - assertEq(dotnsContentResolver.contenthash(fullNode), CONTENT_HASH_A); + assertEq(dotnsPopResolver.chatKey(personhoodNode), CHAT_KEY); + assertEq(dotnsPopResolver.personhoodNodeOf(deviceLabelhash), personhoodNode); + assertEq(dotnsContentResolver.contenthash(personhoodNode), CONTENT_HASH_A); assertEq(dotnsRegistry.owner(subnode), leonardo); vm.prank(ed); - dotnsContentResolver.setContenthash(fullNode, CONTENT_HASH_B); - assertEq(dotnsContentResolver.contenthash(fullNode), CONTENT_HASH_B); + dotnsContentResolver.setContenthash(personhoodNode, CONTENT_HASH_B); + assertEq(dotnsContentResolver.contenthash(personhoodNode), CONTENT_HASH_B); - bytes32 reassignedSubnode = _setSubnode(ed, fullNode, SUB_LABEL, FULL_LABEL, tiago); + bytes32 reassignedSubnode = + _setSubnode(ed, personhoodNode, SUB_LABEL, PERSONHOOD_LABEL_A, tiago); assertEq(reassignedSubnode, subnode); assertEq(dotnsRegistry.owner(subnode), tiago); - // The lite name is a subname owned in the registry, not a transferable token. - assertEq(dotnsRegistry.owner(_liteNodeOf(LITE_LABEL)), ed); - assertFalse(dotnsRegistrar.exists(uint256(_liteNodeOf(LITE_LABEL)))); + // The device name is a subname owned in the registry, not a transferable token. + assertEq(dotnsRegistry.owner(_deviceNodeOf(DEVICE_LABEL)), ed); + assertFalse(dotnsRegistrar.exists(uint256(_deviceNodeOf(DEVICE_LABEL)))); } function test_cold_gateway_reserve_then_user_settles_pending_claim() public { - _grantPopFull(ed); + _grantPersonhood(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL, user: ed, chatKey: CHAT_KEY + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL, user: ed, chatKey: CHAT_KEY }) ); - bytes32 liteNode = _liteNodeOf(LITE_LABEL); - assertEq(dotnsRegistry.owner(liteNode), ed); - assertEq(dotnsRegistry.owner(liteNode), ed); + bytes32 deviceNode = _deviceNodeOf(DEVICE_LABEL); + assertEq(dotnsRegistry.owner(deviceNode), ed); + assertEq(dotnsRegistry.owner(deviceNode), ed); assertEq(storeFactory.getLabelStore(ed), address(0)); // Chat key is persisted eagerly on the resolver at reserve time; only the // LabelStore write is deferred for cold-path users. - assertEq(dotnsPopResolver.chatKey(liteNode), CHAT_KEY); + assertEq(dotnsPopResolver.chatKey(deviceNode), CHAT_KEY); IDotnsPopController.PendingClaim[] memory pending = dotnsPopController.pendingClaims(ed, 0, type(uint256).max); - assertEq(pending[0].label, LITE_LABEL); + assertEq(pending[0].label, DEVICE_LABEL); assertGt(pending[0].mintedAt, 0); vm.prank(ed); @@ -128,18 +134,19 @@ contract PopLifecycleFlow is BaseDotns { address store = storeFactory.getLabelStore(ed); assertTrue(store != address(0)); assertEq( - ILabelStore(store).getLabel(liteNode), string.concat(LITE_LABEL, protocolRegistry.tld()) + ILabelStore(store).getLabel(deviceNode), + string.concat(DEVICE_LABEL, protocolRegistry.tld()) ); - assertEq(dotnsPopResolver.chatKey(liteNode), CHAT_KEY); + assertEq(dotnsPopResolver.chatKey(deviceNode), CHAT_KEY); assertEq(dotnsPopController.pendingClaimCountOf(ed), 0); assertEq(dotnsPopController.pendingClaimUserCount(), 0); } function test_reserve_settle_reserve_cycle_for_same_user() public { - _grantPopFull(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL, user: ed, chatKey: CHAT_KEY + _grantPersonhood(ed); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL, user: ed, chatKey: CHAT_KEY }) ); uint64 firstMintedAt = dotnsPopController.pendingClaims(ed, 0, 1)[0].mintedAt; @@ -154,67 +161,67 @@ contract PopLifecycleFlow is BaseDotns { address store = storeFactory.getLabelStore(ed); assertEq( - ILabelStore(store).getLabel(_liteNodeOf(LITE_LABEL)), - string.concat(LITE_LABEL, protocolRegistry.tld()) + ILabelStore(store).getLabel(_deviceNodeOf(DEVICE_LABEL)), + string.concat(DEVICE_LABEL, protocolRegistry.tld()) ); string memory secondLabel = "michael.02"; bytes memory secondKey = hex"04beefcafedeadbeefcafedeadbeefcafedeadbeefcafedeadbeefcafedeadbeefcafedeadbeefcafedeadbeefcafedeadbeefcafedeadbeefcafedeadbeefcafe"; - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: secondLabel, user: ed, chatKey: secondKey + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: secondLabel, user: ed, chatKey: secondKey }) ); // The user is warm now, so the second reservation writes straight into the store. assertEq( - ILabelStore(store).getLabel(_liteNodeOf(secondLabel)), + ILabelStore(store).getLabel(_deviceNodeOf(secondLabel)), string.concat(secondLabel, protocolRegistry.tld()) ); - assertEq(dotnsPopResolver.chatKey(_liteNodeOf(secondLabel)), secondKey); + assertEq(dotnsPopResolver.chatKey(_deviceNodeOf(secondLabel)), secondKey); assertGt(firstMintedAt, 0); assertEq(dotnsPopController.pendingClaimCountOf(ed), 0); assertEq(dotnsPopController.pendingClaimUserCount(), 0); } function test_gateway_name_with_live_pending_claim_is_soulbound_and_settles_for_owner() public { - _grantPopFull(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL, user: ed, chatKey: CHAT_KEY + _grantPersonhood(ed); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL, user: ed, chatKey: CHAT_KEY }) ); - // The lite name is a subname owned in the registry, not a transferable ERC-721 token, so it - // cannot be moved out of the beneficiary's wallet before settlement: there is no token to - // transfer and the owner holds no reassignment primitive. This is the path the issue + // The device name is a subname owned in the registry, not a transferable ERC-721 token, so + // it cannot be moved out of the beneficiary's wallet before settlement: there is no token + // to transfer and the owner holds no reassignment primitive. This is the path the issue // closes: a pre-claim transfer previously escaped tier pricing entirely. - bytes32 liteNode = _liteNodeOf(LITE_LABEL); - assertEq(dotnsRegistry.owner(liteNode), ed); - assertFalse(dotnsRegistrar.exists(uint256(liteNode))); + bytes32 deviceNode = _deviceNodeOf(DEVICE_LABEL); + assertEq(dotnsRegistry.owner(deviceNode), ed); + assertFalse(dotnsRegistrar.exists(uint256(deviceNode))); // The pending claim is keyed by the original user and still settles into their store. IDotnsPopController.PendingClaim[] memory pending = dotnsPopController.pendingClaims(ed, 0, type(uint256).max); - assertEq(pending[0].label, LITE_LABEL); + assertEq(pending[0].label, DEVICE_LABEL); assertGt(pending[0].mintedAt, 0); vm.prank(ed); dotnsPopController.settlePendingClaims(ed, type(uint256).max); address edStore = storeFactory.getLabelStore(ed); assertTrue(edStore != address(0)); - bytes32 node = _liteNodeOf(LITE_LABEL); + bytes32 node = _deviceNodeOf(DEVICE_LABEL); assertEq( - ILabelStore(edStore).getLabel(node), string.concat(LITE_LABEL, protocolRegistry.tld()) + ILabelStore(edStore).getLabel(node), string.concat(DEVICE_LABEL, protocolRegistry.tld()) ); } function test_lapsed_pending_claim_settles_and_deploys_store() public { - _grantPopFull(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL, user: ed, chatKey: CHAT_KEY + _grantPersonhood(ed); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL, user: ed, chatKey: CHAT_KEY }) ); @@ -222,54 +229,55 @@ contract PopLifecycleFlow is BaseDotns { // Permissionless settlement from a stranger address: age never drops the claim, so the // store is deployed for the beneficiary and the label is written and readable. - bytes32 liteNode = _liteNodeOf(LITE_LABEL); + bytes32 deviceNode = _deviceNodeOf(DEVICE_LABEL); vm.prank(makeAddr("settler")); dotnsPopController.settlePendingClaims(ed, type(uint256).max); address store = storeFactory.getLabelStore(ed); assertTrue(store != address(0)); assertEq( - ILabelStore(store).getLabel(liteNode), string.concat(LITE_LABEL, protocolRegistry.tld()) + ILabelStore(store).getLabel(deviceNode), + string.concat(DEVICE_LABEL, protocolRegistry.tld()) ); assertEq(dotnsPopController.pendingClaimCountOf(ed), 0); assertEq(dotnsPopController.pendingClaimUserCount(), 0); } - function test_lite_via_gateway_then_full_via_public_after_upgrade() public { - _grantPopLite(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL, user: ed, chatKey: CHAT_KEY + function test_device_name_via_gateway_then_personhood_name_via_public_after_upgrade() public { + _grantDevicehood(ed); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL, user: ed, chatKey: CHAT_KEY }) ); - bytes32 liteNode = _liteNodeOf(LITE_LABEL); - assertEq(dotnsRegistry.owner(liteNode), ed); + bytes32 deviceNode = _deviceNodeOf(DEVICE_LABEL); + assertEq(dotnsRegistry.owner(deviceNode), ed); - _grantPopFull(ed); + _grantPersonhood(ed); string memory popfullLabel = "alicedef"; _commitAndRegister(popfullLabel, ed, false); - bytes32 fullNode = _nodeOf(popfullLabel); - assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(fullNode)), ed); - assertEq(dotnsRegistry.owner(liteNode), ed); + bytes32 personhoodNode = _nodeOf(popfullLabel); + assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(personhoodNode)), ed); + assertEq(dotnsRegistry.owner(deviceNode), ed); } - /// @notice Mints the lite label for `user` then claims the full label against it. - /// @dev Grants PopFull up front so both the lite reservation leg and the - /// reservedBaseLabel leg pass `priceWithCheck`. - function _mintLiteThenClaimFull(address user) internal { - // The reservedBaseLabel leg of reserveBaseName now runs - // priceWithCheck on FULL_LABEL too, and FULL_LABEL classifies as - // PopFull. Granting PopFull up front satisfies classification/tier - // on both the lite leg (PopLite classification, admitted by the - // PopFull superset) and the full-person claim. - _grantPopFull(user); - _reservePop(user, LITE_LABEL, CHAT_KEY, FULL_LABEL); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({ - label: FULL_LABEL, user: user, link: _linkWithLite(LITE_LABEL) + /// @notice Mints the device name for `user` then claims the personhood label against it. + /// @dev Grants Personhood up front so both the device-name reservation leg and the + /// reservedLabel leg pass `priceWithCheck`. + function _issueDeviceThenClaimPersonhood(address user) internal { + // The reservedLabel leg of reserveBaseName now runs + // priceWithCheck on PERSONHOOD_LABEL_A too, and PERSONHOOD_LABEL_A classifies as + // Personhood. Granting Personhood up front satisfies classification/tier + // on both the device-name issuance (Devicehood classification, admitted by the + // Personhood superset) and the personhood claim. + _grantPersonhood(user); + _reservePop(user, DEVICE_LABEL, CHAT_KEY, PERSONHOOD_LABEL_A); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: user, link: _linkWithDeviceName(DEVICE_LABEL) }) ); } diff --git a/test/intergration/StoreIntegration.t.sol b/test/intergration/StoreIntegration.t.sol index 3150dd517..ece1f0d33 100644 --- a/test/intergration/StoreIntegration.t.sol +++ b/test/intergration/StoreIntegration.t.sol @@ -51,7 +51,7 @@ contract StoreIntegrationTest is BaseDotns { } function test_pop_controller_registration_writes_label_store() public { - _grantPopFull(ed); + _grantPersonhood(ed); string memory base = BASE_LABEL_A; // DotnsPopResolver.setChatKey requires exactly 65 bytes (uncompressed ECDSA pubkey). @@ -61,8 +61,10 @@ contract StoreIntegrationTest is BaseDotns { chatKey[i] = bytes1(uint8(i + 1)); } - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: base, user: ed, link: _linkFresh(chatKey)}) + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: base, user: ed, link: _linkFresh(chatKey) + }) ); vm.prank(ed); diff --git a/test/invariant/escrow/DotnsNameEscrowInvariant.t.sol b/test/invariant/escrow/DotnsNameEscrowInvariant.t.sol index e81a2bd1b..99752fcef 100644 --- a/test/invariant/escrow/DotnsNameEscrowInvariant.t.sol +++ b/test/invariant/escrow/DotnsNameEscrowInvariant.t.sol @@ -5,7 +5,7 @@ import {BaseDotns} from "../../base/BaseDotns.t.sol"; import {IDotnsNameEscrow} from "../../../contracts/escrow/IDotnsNameEscrow.sol"; import {EscrowHandler} from "./EscrowHandler.t.sol"; -/// @title Dotns Name Escrow Invariant Suite +/// @title dotNS Name Escrow Invariant Suite /// @notice Asserts solvency, custody, and recipient-locking properties of the name escrow /// across randomised registration, release, withdraw, claim, and transfer flows. contract DotnsNameEscrowInvariantTest is BaseDotns { diff --git a/test/invariant/escrow/EscrowHandler.t.sol b/test/invariant/escrow/EscrowHandler.t.sol index 752da2722..3fd67404b 100644 --- a/test/invariant/escrow/EscrowHandler.t.sol +++ b/test/invariant/escrow/EscrowHandler.t.sol @@ -162,7 +162,7 @@ contract EscrowHandler is Test { /// randomly aligns or splits their PoP statuses, and dispatches the controller /// `register()` call from the payer. Routes to the deposit, depositProtocolFee, /// or skip branch depending on the resulting tier prices. Revert-safe: if the - /// computed price is zero on both sides (PoPLite/PoPFull no-cost path) the call + /// computed price is zero on both sides (Devicehood/Personhood no-cost path) the call /// still completes but ghost state is only updated where state actually changed. /// @param ownerSeed Seed selecting the registrant. /// @param payerSeed Seed selecting the payer. @@ -174,21 +174,21 @@ contract EscrowHandler is Test { address ownerAddr = _pickDifferentActor(payer, payerSeed); if (ownerAddr == address(0)) return; - // Tier-status assignment: 0 = both NoStatus, 1 = elevate payer to PopFull, - // 2 = elevate owner to PopLite, 3 = elevate both (no fee differential). + // Tier-status assignment: 0 = both NoStatus, 1 = elevate payer to Personhood, + // 2 = elevate owner to Devicehood, 3 = elevate both (no fee differential). uint256 mode = bound(statusSeed, 0, 3); if (mode == 0) { _mockPersonhoodTier(payer, IPopRules.PopStatus.NoStatus); _mockPersonhoodTier(ownerAddr, IPopRules.PopStatus.NoStatus); } else if (mode == 1) { - _mockPersonhoodTier(payer, IPopRules.PopStatus.PopFull); + _mockPersonhoodTier(payer, IPopRules.PopStatus.Personhood); _mockPersonhoodTier(ownerAddr, IPopRules.PopStatus.NoStatus); } else if (mode == 2) { _mockPersonhoodTier(payer, IPopRules.PopStatus.NoStatus); - _mockPersonhoodTier(ownerAddr, IPopRules.PopStatus.PopLite); + _mockPersonhoodTier(ownerAddr, IPopRules.PopStatus.Devicehood); } else if (mode == 3) { - _mockPersonhoodTier(payer, IPopRules.PopStatus.PopFull); - _mockPersonhoodTier(ownerAddr, IPopRules.PopStatus.PopFull); + _mockPersonhoodTier(payer, IPopRules.PopStatus.Personhood); + _mockPersonhoodTier(ownerAddr, IPopRules.PopStatus.Personhood); } string memory label = _generateUniqueLabel(); @@ -450,7 +450,7 @@ contract EscrowHandler is Test { } /// @notice Sets a random PoP status for an actor mid-run. - /// @dev Bound: 0 = NoStatus, 1 = PopLite, 2 = PopFull. Allows the fuzzer to flip + /// @dev Bound: 0 = NoStatus, 1 = Devicehood, 2 = Personhood. Allows the fuzzer to flip /// tiers between handler calls so subsequent registrations and transfers exercise /// every routing branch. /// @param actorSeed Seed selecting the actor whose status flips. @@ -469,8 +469,8 @@ contract EscrowHandler is Test { /// @param tier Verification tier to report for `account`. function _mockPersonhoodTier(address account, IPopRules.PopStatus tier) internal { uint8 statusByte; - if (tier == IPopRules.PopStatus.PopFull) statusByte = 2; - else if (tier == IPopRules.PopStatus.PopLite) statusByte = 1; + if (tier == IPopRules.PopStatus.Personhood) statusByte = 2; + else if (tier == IPopRules.PopStatus.Devicehood) statusByte = 1; bytes32 contextAlias = statusByte == 0 ? bytes32(0) : keccak256(abi.encode(account, statusByte)); diff --git a/test/invariant/registrar/DotnsPopControllerInvariant.t.sol b/test/invariant/registrar/DotnsPopControllerInvariant.t.sol index 1bb895c38..8251b98bf 100644 --- a/test/invariant/registrar/DotnsPopControllerInvariant.t.sol +++ b/test/invariant/registrar/DotnsPopControllerInvariant.t.sol @@ -63,7 +63,7 @@ contract DotnsPopControllerInvariant is BaseDotns { /// warp, relink and the two settlement paths) fill no list of their own and are left /// to the campaign. function _seedCoverage() internal { - // A reserve, then a claim on the same base label, so a lite name and a full-person name + // A reserve, then a claim on the same base label, so a device name and a personhood name // both exist and the actor's store is settled. handler.reserve(0, 0, true); handler.claim(0, 0); @@ -71,10 +71,10 @@ contract DotnsPopControllerInvariant is BaseDotns { // An odd base index takes a label of the public path's own, which the gateway cannot // have claimed; the transfer then picks the public token that registration minted. handler.publicRegister(1, 1); - handler.attemptTransfer(handler.mintedLiteTokenCount(), 0); + handler.attemptTransfer(handler.mintedDeviceTokenCount(), 0); // Sub-labels come from a seed and most are not valid DNS labels, so walk seeds until - // one is accepted under a full-person parent. + // one is accepted under a personhood parent. for (uint256 seed; seed < 64 && handler.subnodeCreatedCount() == 0; ++seed) { handler.createSubname(seed, seed, seed); } @@ -91,7 +91,7 @@ contract DotnsPopControllerInvariant is BaseDotns { } /// @notice A user-created subname never collides with a name the gateway issued. - /// @dev A gateway full-person name is a second-level node and a gateway lite name is a subname + /// @dev A gateway personhood name is a second-level node and a gateway device name is a subname /// of its numeric container; a subname a user builds under a name they own must land on /// neither, or a user could reach a gateway-issued node. function invariant_subnames_never_reach_a_gateway_node() public view { @@ -102,7 +102,8 @@ contract DotnsPopControllerInvariant is BaseDotns { bytes32 subnode = handler.subnodesCreated(i); for (uint256 j = 0; j < gatewayCount; j++) { string memory label = handler.gatewayLabelsSeen(j); - bytes32 gatewayNode = _carriesSeparator(label) ? _liteNodeOf(label) : _nodeOf(label); + bytes32 gatewayNode = + _carriesSeparator(label) ? _deviceNodeOf(label) : _nodeOf(label); assertTrue(subnode != gatewayNode, "subnode collided with a gateway name"); } } @@ -149,11 +150,11 @@ contract DotnsPopControllerInvariant is BaseDotns { string memory label = handler.gatewayLabelsSeen(i); assertFalse(handler.isPublicLabel(label), "gateway label taken publicly"); if (_carriesSeparator(label)) { - // A lite name is a subname owned in the registry, non-transferable because there is - // no token behind it, not through the soulbound flag. - bytes32 node = _liteNodeOf(label); - assertTrue(dotnsRegistry.owner(node) != address(0), "gateway lite name unowned"); - assertFalse(dotnsRegistrar.exists(uint256(node)), "gateway lite name is a token"); + // A device name is a subname owned in the registry, non-transferable because there + // is no token behind it, not through the soulbound flag. + bytes32 node = _deviceNodeOf(label); + assertTrue(dotnsRegistry.owner(node) != address(0), "gateway device name unowned"); + assertFalse(dotnsRegistrar.exists(uint256(node)), "gateway device name is a token"); } else { assertTrue(dotnsRegistrar.isSoulbound(uint256(_nodeOf(label))), "gateway name free"); } @@ -236,56 +237,72 @@ contract DotnsPopControllerInvariant is BaseDotns { } } - /// @notice For every historic (liteLabelhash, fullNode) pair the resolver's + /// @notice For every historic (deviceLabelhash, personhoodNode) pair the resolver's /// forward and reverse indexes either still round-trip to each /// other or have both been cleared by a later overwrite. A partial /// overwrite, where one side still points at a stale partner, is /// the corruption signature this invariant guards against. - function invariant_fullClaim_liteLink_are_inverse() public view { + function invariant_personhoodLink_deviceLink_are_inverse() public view { uint256 n = handler.claimedCount(); for (uint256 i = 0; i < n; i++) { - bytes32 liteLabelhash = handler.claimedLiteLabelhashes(i); - bytes32 fullNode = handler.claimedFullNodes(i); + bytes32 deviceLabelhash = handler.claimedDeviceLabelhashes(i); + bytes32 personhoodNode = handler.claimedPersonhoodNodes(i); - bytes32 currentFullForLite = dotnsPopResolver.fullClaim(liteLabelhash); - bytes32 currentLiteForFull = dotnsPopResolver.liteLink(fullNode); + bytes32 currentPersonhoodForDevice = dotnsPopResolver.personhoodNodeOf(deviceLabelhash); + bytes32 currentDeviceForPersonhood = dotnsPopResolver.deviceLabelhashOf(personhoodNode); // Either the pair is still live on both sides, or both sides // have been cleared. Anything else is a partial overwrite. - if (currentFullForLite == fullNode) { - assertEq(currentLiteForFull, liteLabelhash, "live fullClaim but liteLink drifted"); - } else if (currentLiteForFull == liteLabelhash) { - assertEq(currentFullForLite, fullNode, "live liteLink but fullClaim drifted"); + if (currentPersonhoodForDevice == personhoodNode) { + assertEq( + currentDeviceForPersonhood, + deviceLabelhash, + "live personhoodNodeOf but deviceLabelhashOf drifted" + ); + } else if (currentDeviceForPersonhood == deviceLabelhash) { + assertEq( + currentPersonhoodForDevice, + personhoodNode, + "live deviceLabelhashOf but personhoodNodeOf drifted" + ); } // Else: both sides were overwritten. Covered by the stale // invariants below. } } - /// @notice No stale `liteLink`: for every touched fullNode, a non-zero - /// liteLink value round-trips through `fullClaim` back to the same - /// fullNode. A drifting liteLink is the corruption footprint this + /// @notice No stale `deviceLabelhashOf`: for every touched personhoodNode, a non-zero + /// device labelhash round-trips through `personhoodNodeOf` back to the same + /// personhoodNode. A drifting device labelhash is the corruption footprint this /// invariant guards against. - function invariant_no_stale_liteLink() public view { + function invariant_no_stale_deviceLabelhash() public view { uint256 n = handler.claimedCount(); for (uint256 i = 0; i < n; i++) { - bytes32 fullNode = handler.claimedFullNodes(i); - bytes32 currentLite = dotnsPopResolver.liteLink(fullNode); - if (currentLite == bytes32(0)) continue; - assertEq(dotnsPopResolver.fullClaim(currentLite), fullNode, "stale liteLink"); + bytes32 personhoodNode = handler.claimedPersonhoodNodes(i); + bytes32 currentDevice = dotnsPopResolver.deviceLabelhashOf(personhoodNode); + if (currentDevice == bytes32(0)) continue; + assertEq( + dotnsPopResolver.personhoodNodeOf(currentDevice), + personhoodNode, + "stale deviceLabelhashOf" + ); } } - /// @notice No stale `fullClaim`: symmetric to `invariant_no_stale_liteLink`, - /// every claimed liteLabelhash with a non-zero fullClaim round-trips - /// through `liteLink` back to the same liteLabelhash. - function invariant_no_stale_fullClaim() public view { + /// @notice No stale `personhoodNodeOf`: symmetric to `invariant_no_stale_deviceLabelhash`, + /// every claimed deviceLabelhash with a non-zero personhood node round-trips + /// through `deviceLabelhashOf` back to the same deviceLabelhash. + function invariant_no_stale_personhoodNode() public view { uint256 n = handler.claimedCount(); for (uint256 i = 0; i < n; i++) { - bytes32 liteLabelhash = handler.claimedLiteLabelhashes(i); - bytes32 currentFull = dotnsPopResolver.fullClaim(liteLabelhash); - if (currentFull == bytes32(0)) continue; - assertEq(dotnsPopResolver.liteLink(currentFull), liteLabelhash, "stale fullClaim"); + bytes32 deviceLabelhash = handler.claimedDeviceLabelhashes(i); + bytes32 currentPersonhood = dotnsPopResolver.personhoodNodeOf(deviceLabelhash); + if (currentPersonhood == bytes32(0)) continue; + assertEq( + dotnsPopResolver.deviceLabelhashOf(currentPersonhood), + deviceLabelhash, + "stale personhoodNodeOf" + ); } } @@ -349,22 +366,22 @@ contract DotnsPopControllerInvariant is BaseDotns { assertEq(page.length, count, "count != enumeration length"); } - /// @notice Settlement writes labels and never strands a minted name. A full-person name is a - /// token whose settled label reads back from the registrar; a lite name is a registry + /// @notice Settlement writes labels and never strands a minted name. A personhood name is a + /// token whose settled label reads back from the registrar; a device name is a registry /// subname whose settled label lives in the owner's store. Either is settled or still /// staged in the owner's pending queue, so a minted name is never left in neither /// place. - /// @dev The stranded case the old model allowed, a lapsed entry swept out of the queue with - /// nothing written, is now unreachable: settlement always writes the label regardless of - /// the reservation deadline. A deployed store and a pending claim are mutually exclusive - /// in this suite, so a lite name whose owner holds a store has necessarily been settled. + /// @dev A pending entry is never dropped without its label being written: settlement always + /// writes the label, whatever the claim's age. A deployed store and a pending claim are + /// mutually exclusive in this suite, so a device name whose owner holds a store has + /// necessarily been settled. function invariant_settled_names_written_and_never_stranded() public view { - uint256 n = handler.mintedLiteTokenCount(); + uint256 n = handler.mintedDeviceTokenCount(); for (uint256 i = 0; i < n; i++) { - uint256 id = handler.mintedLiteTokenIds(i); + uint256 id = handler.mintedDeviceTokenIds(i); if (dotnsRegistrar.exists(id)) { - // Full-person name: a tokenised second-level name. A settled name reads its label + // Personhood name: a tokenised second-level name. A settled name reads its label // back from the registrar; otherwise it must still be staged in its owner's queue. if (bytes(dotnsRegistrar.labelOf(id)).length != 0) continue; assertTrue( @@ -374,28 +391,28 @@ contract DotnsPopControllerInvariant is BaseDotns { continue; } - // Lite name: a registry subname, not a token. It is owned in the registry, and settled - // into the owner's store or still staged in pending. Store and pending are mutually - // exclusive here, so a deployed store means the label was written. + // Device name: a registry subname, not a token. It is owned in the registry, and + // settled into the owner's store or still staged in pending. Store and pending are + // mutually exclusive here, so a deployed store means the label was written. bytes32 node = bytes32(id); address nameOwner = dotnsRegistry.owner(node); - assertTrue(nameOwner != address(0), "lite name lost its registry owner"); + assertTrue(nameOwner != address(0), "device name lost its registry owner"); if (IStoreFactory(address(storeFactory)).getLabelStore(nameOwner) != address(0)) { continue; } - assertTrue(_stagedInPending(nameOwner, node), "lite name neither settled nor staged"); + assertTrue(_stagedInPending(nameOwner, node), "device name neither settled nor staged"); } } /// @notice Whether `owner` holds a pending claim whose label derives to `node`. - /// @dev A lite label carries the separator and derives to its subnode; any other label derives + /// @dev A device name carries the separator and derives to its subnode; any other label derives /// to a second-level node under the TLD. function _stagedInPending(address owner, bytes32 node) internal view returns (bool staged) { IDotnsPopController.PendingClaim[] memory pending = dotnsPopController.pendingClaims(owner, 0, type(uint256).max); for (uint256 j = 0; j < pending.length; j++) { bytes32 pendingNode = _carriesSeparator(pending[j].label) - ? _liteNodeOf(pending[j].label) + ? _deviceNodeOf(pending[j].label) : _nodeOf(pending[j].label); if (pendingNode == node) return true; } diff --git a/test/invariant/registrar/DotnsRegistrarControllerInvariant.t.sol b/test/invariant/registrar/DotnsRegistrarControllerInvariant.t.sol index 70b4ae56e..45467623d 100644 --- a/test/invariant/registrar/DotnsRegistrarControllerInvariant.t.sol +++ b/test/invariant/registrar/DotnsRegistrarControllerInvariant.t.sol @@ -31,15 +31,15 @@ contract DotnsRegistrarControllerInvariantTest is BaseDotns { vm.deal(address(handler), 1000 ether); - handler.addActor(ed, IPopRules.PopStatus.PopFull); - handler.addActor(leonardo, IPopRules.PopStatus.PopLite); + handler.addActor(ed, IPopRules.PopStatus.Personhood); + handler.addActor(leonardo, IPopRules.PopStatus.Devicehood); handler.addActor(tiago, IPopRules.PopStatus.NoStatus); // Additional actors for richer multi-user transfer chains. address alice = _createUser("alice"); address bob = _createUser("bob"); - handler.addActor(alice, IPopRules.PopStatus.PopFull); - handler.addActor(bob, IPopRules.PopStatus.PopLite); + handler.addActor(alice, IPopRules.PopStatus.Personhood); + handler.addActor(bob, IPopRules.PopStatus.Devicehood); targetContract(address(handler)); diff --git a/test/invariant/registrar/DotnsRegistrarReservedGrantInvariant.t.sol b/test/invariant/registrar/DotnsRegistrarReservedGrantInvariant.t.sol index 78621251f..aff8791de 100644 --- a/test/invariant/registrar/DotnsRegistrarReservedGrantInvariant.t.sol +++ b/test/invariant/registrar/DotnsRegistrarReservedGrantInvariant.t.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.34; import {BaseDotns} from "../../base/BaseDotns.t.sol"; import {ReservedGrantHandler} from "./ReservedGrantHandler.t.sol"; -/// @title Dotns Reserved Grant Invariant Suite +/// @title dotNS Reserved Grant Invariant Suite /// @notice Asserts the properties the grant-gated reserved path exists to provide, across any /// reachable interleaving of grants, relayed mints, ungranted attempts and double spends: /// only granted labels mint, they mint to the beneficiary rather than the submitter, a diff --git a/test/invariant/registrar/DotnsRegistrarSoulboundInvariant.t.sol b/test/invariant/registrar/DotnsRegistrarSoulboundInvariant.t.sol index 014fe288f..5f7495bc1 100644 --- a/test/invariant/registrar/DotnsRegistrarSoulboundInvariant.t.sol +++ b/test/invariant/registrar/DotnsRegistrarSoulboundInvariant.t.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.34; import {BaseDotns} from "../../base/BaseDotns.t.sol"; import {SoulboundHandler} from "./SoulboundHandler.t.sol"; -/// @title Dotns Registrar Soulbound Invariant Suite +/// @title dotNS Registrar Soulbound Invariant Suite /// @notice Asserts that PoP-gateway (soulbound) names never change owner across any reachable /// sequence of mints and transfer attempts, and that the campaign actually exercises /// soulbound tokens rather than passing vacuously. diff --git a/test/invariant/registrar/PopControllerHandler.t.sol b/test/invariant/registrar/PopControllerHandler.t.sol index 32819c7c3..8ebc2c613 100644 --- a/test/invariant/registrar/PopControllerHandler.t.sol +++ b/test/invariant/registrar/PopControllerHandler.t.sol @@ -24,7 +24,7 @@ import {IPersonhood} from "../../../contracts/external/personhood/IPersonhood.so /// @notice Bounded random-action handler for @custom:contract DotnsPopController invariant tests. /// @dev Cycles through an actor set and a fixed base-label set so the fuzzer /// explores combinations deterministically. Tracks every labelhash that has -/// hosted a reservation, every minted lite token, and every successful +/// hosted a reservation, every minted device name, and every successful /// claim so invariants can iterate over just what exists. contract PopControllerHandler is Test { /// @notice The PoP controller under test. @@ -35,7 +35,7 @@ contract PopControllerHandler is Test { DotnsRegistrar public immutable REGISTRAR; /// @notice Pricing and classification, read to quote a public registration. IPopRules public immutable POP_RULES; - /// @notice The hierarchical registry, where a lite name lives as a subname of its container. + /// @notice The hierarchical registry, where a device name lives as a subname of its container. IDotnsRegistry public immutable REGISTRY; /// @notice Node hash of the suite's TLD, injected from the deployed protocol registry. /// @dev Keeps the handler rooted at the same TLD the protocol under test uses, without a @@ -53,32 +53,32 @@ contract PopControllerHandler is Test { bytes32[] public reservedLabelsSeen; /// @notice Dedup set for `reservedLabelsSeen` to keep iteration cheap. mapping(bytes32 labelhash => bool) internal _tracked; - /// @notice Monotonic per-actor counter feeding the lite-label suffix so + /// @notice Monotonic per-actor counter feeding the device-name suffix so /// each generated label is unique inside an actor's namespace. - mapping(address actor => uint64 suffix) internal _liteSuffix; + mapping(address actor => uint64 suffix) internal _deviceSuffix; - /// @notice Lite tokens minted through the handler (one push per successful - /// reserve, plus the lite and full nodes pushed on claim and the - /// full node pushed on reLink). Used by the labelOf-non-empty + /// @notice Device names minted through the handler (one push per successful + /// reserve, plus the device and personhood nodes pushed on claim and the + /// personhood node pushed on reLink). Used by the labelOf-non-empty /// invariant to enumerate the token space without scanning the /// full uint256 id range. - uint256[] public mintedLiteTokenIds; - - /// @notice Full nodes minted through successful claims, captured alongside - /// the lite labelhash they were linked against. Used by the - /// fullClaim/liteLink inverse invariant: for each entry, - /// fullClaim(liteHash) == node. - bytes32[] public claimedFullNodes; - /// @notice Lite labelhashes paired index-for-index with `claimedFullNodes`. - bytes32[] public claimedLiteLabelhashes; - - /// @notice Prior lite labels reserved by the handler. Used by reLink - /// actions so the fuzzer can re-use an existing lite label + uint256[] public mintedDeviceTokenIds; + + /// @notice Personhood nodes minted through successful claims, captured alongside + /// the device-name labelhash they were linked against. Used by the + /// personhoodNodeOf/deviceLabelhashOf inverse invariant: for each entry, + /// personhoodNodeOf(deviceLabelhash) == node. + bytes32[] public claimedPersonhoodNodes; + /// @notice Device-name labelhashes paired index-for-index with `claimedPersonhoodNodes`. + bytes32[] public claimedDeviceLabelhashes; + + /// @notice Prior device names reserved by the handler. Used by reLink + /// actions so the fuzzer can re-use an existing device name /// against a fresh base claim, driving the resolver overwrite /// paths under repeated `(baseLabel, actor)` reuse. Kept as /// the raw string because the controller re-hashes internally /// on every call. - string[] public priorLiteLabels; + string[] public priorDeviceLabels; /// @notice Every label the gateway has issued, deduplicated. /// @dev Provenance is written once at mint, so this set only grows. The monotonicity @@ -125,7 +125,7 @@ contract PopControllerHandler is Test { mapping(address actor => bool) internal _pendingActorTracked; /// @notice Seeds the actor pool, base-label set, and personhood mocks so - /// every action call admits both lite and base classifications. + /// every action call admits both device-name and base classifications. /// @param controller_ The PoP controller under test. /// @param publicController_ The public commit-reveal controller. /// @param registrar_ The ERC-721 registrar both controllers mint through. @@ -149,19 +149,19 @@ contract PopControllerHandler is Test { REGISTRY = registry_; TLD_NODE = tldNode_; actors = actors_; - // baselength 8, no trailing digits: PopFull classification. + // baselength 8, no trailing digits: Personhood classification. baseLabels.push("alicebob"); // length 12, no trailing digits: NoStatus classification. A reservable base label - // keys the reservation queue, which only ever holds a digit-free stem. + // keys the reservation queue, which only ever holds a digit-free base name. baseLabels.push("wonderlandxy"); // baselength 10, no trailing digits: NoStatus classification. baseLabels.push("carolcarol"); - // Every actor needs PopFull status on the personhood precompile so the + // Every actor needs Personhood status on the personhood precompile so the // classification/tier guard in PopRules.priceWithCheck admits every - // label the handler can generate: PopLite lite labels (PopFull is a - // superset of PopLite), PopFull base labels, and NoStatus base labels - // (which merely require userStatus != PopLite). + // label the handler can generate: Devicehood device names (Personhood is a + // superset of Devicehood), Personhood base labels, and NoStatus base labels + // (which merely require userStatus != Devicehood). for (uint256 i = 0; i < actors_.length; i++) { _mockPersonhoodTier(actors_[i], 2); } @@ -178,7 +178,7 @@ contract PopControllerHandler is Test { /// @notice Mocks the personhood precompile so `account` reports the given /// status byte for the protocol's personhood context. /// @dev Status byte mirrors the precompile's wire format: 0 = NoStatus, - /// 1 = PopLite, 2 = PopFull. A zero status clears the context alias. + /// 1 = Devicehood, 2 = Personhood. A zero status clears the context alias. function _mockPersonhoodTier(address account, uint8 statusByte) internal { bytes32 contextAlias = statusByte == 0 ? bytes32(0) : keccak256(abi.encode(account, statusByte)); @@ -212,19 +212,19 @@ contract PopControllerHandler is Test { } /// @notice Number of token ids the handler has ever recorded as minted. - function mintedLiteTokenCount() external view returns (uint256) { - return mintedLiteTokenIds.length; + function mintedDeviceTokenCount() external view returns (uint256) { + return mintedDeviceTokenIds.length; } - /// @notice Number of successful (lite, full) claim pairs the handler has + /// @notice Number of successful (device, personhood) claim pairs the handler has /// recorded. function claimedCount() external view returns (uint256) { - return claimedFullNodes.length; + return claimedPersonhoodNodes.length; } - /// @notice Number of prior lite labels available for reLink replay. - function priorLiteLabelCount() external view returns (uint256) { - return priorLiteLabels.length; + /// @notice Number of prior device names available for reLink replay. + function priorDeviceLabelCount() external view returns (uint256) { + return priorDeviceLabels.length; } /// @notice Number of actors the handler has ever seen with a pending claim. @@ -232,30 +232,31 @@ contract PopControllerHandler is Test { return pendingClaimActorsSeen.length; } - /// @notice Reserves a lite label for an actor, optionally enqueueing on a + /// @notice Reserves a device name for an actor, optionally enqueueing on a /// base label. /// @dev Swallows known-good reverts (QueueFull, AlreadyReserved, ERC721 /// collision) so the runner keeps exploring. function reserve(uint256 actorIndex, uint256 baseIndex, bool attachReservation) external { address actor = _actor(actorIndex); - _liteSuffix[actor]++; - string memory liteLabel = _buildLiteLabel("rsv", actor, _liteSuffix[actor]); + _deviceSuffix[actor]++; + string memory deviceLabel = _buildDeviceLabel("rsv", actor, _deviceSuffix[actor]); string memory reservedBase = attachReservation ? _baseLabel(baseIndex) : ""; - IDotnsPopController.BaseReservation memory params = IDotnsPopController.BaseReservation({ - lite: IDotnsPopController.LiteRegistration({ - liteLabel: liteLabel, user: actor, chatKey: "" - }), - reservedBaseLabel: reservedBase - }); + IDotnsPopController.DeviceNameIssuanceWithReservation memory params = + IDotnsPopController.DeviceNameIssuanceWithReservation({ + issuance: IDotnsPopController.DeviceNameIssuance({ + label: deviceLabel, user: actor, chatKey: "" + }), + reservedLabel: reservedBase + }); - if (_callReserveBaseName(params)) { + if (_callIssueDeviceNameWithReservation(params)) { if (attachReservation) _track(keccak256(bytes(reservedBase))); - // A lite name is a subname beneath its numeric container, not a token, so record its + // A device name is a subname beneath its numeric container, not a token, so record its // subnode rather than the whole-label hash. - mintedLiteTokenIds.push(uint256(SubnodeUtils.liteSubnodeOf(TLD_NODE, liteLabel))); - priorLiteLabels.push(liteLabel); - _trackGatewayLabel(liteLabel); + mintedDeviceTokenIds.push(uint256(SubnodeUtils.deviceSubnodeOf(TLD_NODE, deviceLabel))); + priorDeviceLabels.push(deviceLabel); + _trackGatewayLabel(deviceLabel); _trackPendingActor(actor); } } @@ -264,7 +265,7 @@ contract PopControllerHandler is Test { /// head of the queue for the picked base label. /// @dev Missing preconditions (wrong actor, expired head, empty queue) /// surface as a revert and are swallowed so the runner keeps - /// leg and the full register leg. + /// leg and the personhood issuance leg. function claim(uint256 actorIndex, uint256 baseIndex) external { address actor = _actor(actorIndex); string memory baseLabel = _baseLabel(baseIndex); @@ -273,73 +274,77 @@ contract PopControllerHandler is Test { if (reservation.labelhash == bytes32(0)) return; if (reservation.labelhash != keccak256(bytes(baseLabel))) return; - _liteSuffix[actor]++; - string memory liteLabel = _buildLiteLabel("clm", actor, _liteSuffix[actor]); + _deviceSuffix[actor]++; + string memory deviceLabel = _buildDeviceLabel("clm", actor, _deviceSuffix[actor]); - IDotnsPopController.BaseReservation memory liteParams = IDotnsPopController.BaseReservation({ - lite: IDotnsPopController.LiteRegistration({ - liteLabel: liteLabel, user: actor, chatKey: "" - }), - reservedBaseLabel: "" - }); - if (!_callReserveBaseName(liteParams)) return; - // Recorded here rather than after the full leg below: the subname exists from this point, - // and a full leg that reverts would otherwise leave it outside every invariant's reach. A - // lite name is a subname beneath its numeric container, not a token, so record its subnode. - mintedLiteTokenIds.push(uint256(SubnodeUtils.liteSubnodeOf(TLD_NODE, liteLabel))); - priorLiteLabels.push(liteLabel); - _trackGatewayLabel(liteLabel); + IDotnsPopController.DeviceNameIssuanceWithReservation memory deviceParams = + IDotnsPopController.DeviceNameIssuanceWithReservation({ + issuance: IDotnsPopController.DeviceNameIssuance({ + label: deviceLabel, user: actor, chatKey: "" + }), + reservedLabel: "" + }); + if (!_callIssueDeviceNameWithReservation(deviceParams)) return; + // Recorded here rather than after the personhood leg below: the subname exists from this + // point, and a personhood leg that reverts would otherwise leave it outside every + // invariant's reach. A device name is a subname beneath its numeric container, not a token, + // so record its subnode. + mintedDeviceTokenIds.push(uint256(SubnodeUtils.deviceSubnodeOf(TLD_NODE, deviceLabel))); + priorDeviceLabels.push(deviceLabel); + _trackGatewayLabel(deviceLabel); _trackPendingActor(actor); - // The lite leg stashed a pending claim. Settle it now so the base + // The the issuance stashed a pending claim. Settle it now so the base // registration below takes the warm path; the pending-claim mechanism // forbids a second stash for the same user. vm.prank(actor); try CONTROLLER.settlePendingClaims(actor, type(uint256).max) {} catch {} IDotnsPopController.Link memory link = IDotnsPopController.Link({ - kind: IDotnsPopController.LinkKind.LiteUsername, liteLabel: liteLabel, chatKey: "" + kind: IDotnsPopController.LinkKind.DeviceName, deviceLabel: deviceLabel, chatKey: "" }); - IDotnsPopController.FullRegistration memory fullParams = - IDotnsPopController.FullRegistration({label: baseLabel, user: actor, link: link}); - if (!_callRegisterBaseName(fullParams)) return; - - bytes32 fullNode = LabelUtils.namehashUnder(TLD_NODE, LabelUtils.labelhashMemory(baseLabel)); - claimedLiteLabelhashes.push(LabelUtils.labelhashMemory(liteLabel)); - claimedFullNodes.push(fullNode); - mintedLiteTokenIds.push(uint256(fullNode)); + IDotnsPopController.PersonhoodNameIssuance memory personhoodParams = + IDotnsPopController.PersonhoodNameIssuance({label: baseLabel, user: actor, link: link}); + if (!_callIssuePersonhoodName(personhoodParams)) return; + + bytes32 personhoodNode = + LabelUtils.namehashUnder(TLD_NODE, LabelUtils.labelhashMemory(baseLabel)); + claimedDeviceLabelhashes.push(LabelUtils.labelhashMemory(deviceLabel)); + claimedPersonhoodNodes.push(personhoodNode); + mintedDeviceTokenIds.push(uint256(personhoodNode)); _trackGatewayLabel(baseLabel); } - /// @notice Re-registers an already-used lite label against a fresh - /// base-label claim so the same liteHash maps to a new fullNode. + /// @notice Re-registers an already-used device name against a fresh + /// base-label claim so the same deviceLabelhash maps to a new personhoodNode. /// @dev Drives the resolver overwrite paths. When the handler /// re-uses the same (baseLabel, actor) pair later it also exercises - /// the symmetric case: same fullNode mapped to a new liteHash. - function reLink(uint256 actorIndex, uint256 baseIndex, uint256 liteIndex) external { - uint256 liteCount = priorLiteLabels.length; - if (liteCount == 0) return; + /// the symmetric case: same personhoodNode mapped to a new deviceLabelhash. + function reLink(uint256 actorIndex, uint256 baseIndex, uint256 deviceIndex) external { + uint256 deviceCount = priorDeviceLabels.length; + if (deviceCount == 0) return; address actor = _actor(actorIndex); string memory baseLabel = _baseLabel(baseIndex); - string memory liteLabel = priorLiteLabels[liteIndex % liteCount]; + string memory deviceLabel = priorDeviceLabels[deviceIndex % deviceCount]; IDotnsPopController.UserReservation memory reservation = CONTROLLER.userReservation(actor); if (reservation.labelhash == bytes32(0)) return; if (reservation.labelhash != keccak256(bytes(baseLabel))) return; IDotnsPopController.Link memory link = IDotnsPopController.Link({ - kind: IDotnsPopController.LinkKind.LiteUsername, liteLabel: liteLabel, chatKey: "" + kind: IDotnsPopController.LinkKind.DeviceName, deviceLabel: deviceLabel, chatKey: "" }); - IDotnsPopController.FullRegistration memory params = - IDotnsPopController.FullRegistration({label: baseLabel, user: actor, link: link}); - if (!_callRegisterBaseName(params)) return; - - bytes32 liteLabelhash = LabelUtils.labelhashMemory(liteLabel); - bytes32 fullNode = LabelUtils.namehashUnder(TLD_NODE, LabelUtils.labelhashMemory(baseLabel)); - claimedLiteLabelhashes.push(liteLabelhash); - claimedFullNodes.push(fullNode); - mintedLiteTokenIds.push(uint256(fullNode)); + IDotnsPopController.PersonhoodNameIssuance memory params = + IDotnsPopController.PersonhoodNameIssuance({label: baseLabel, user: actor, link: link}); + if (!_callIssuePersonhoodName(params)) return; + + bytes32 deviceLabelhash = LabelUtils.labelhashMemory(deviceLabel); + bytes32 personhoodNode = + LabelUtils.namehashUnder(TLD_NODE, LabelUtils.labelhashMemory(baseLabel)); + claimedDeviceLabelhashes.push(deviceLabelhash); + claimedPersonhoodNodes.push(personhoodNode); + mintedDeviceTokenIds.push(uint256(personhoodNode)); _trackGatewayLabel(baseLabel); } @@ -394,7 +399,7 @@ contract PopControllerHandler is Test { address registrant = publicActors[actorIndex % publicActors.length]; // Alternate between a contested base label and one of the public path's own. The // contested half exercises the race; the fresh half keeps the public side populated - // even in a run where the gateway holds every shared stem, so the provenance + // even in a run where the gateway holds every shared base name, so the provenance // invariant always has a public label to check. Ten characters classifies NoStatus. string memory label = baseIndex % 2 == 0 ? _baseLabel(baseIndex) @@ -406,7 +411,7 @@ contract PopControllerHandler is Test { ) { price = quote.price; } catch { - // A stem the gateway queue holds for someone else, or a tier the registrant does + // A base name the gateway queue holds for someone else, or a tier the registrant does // not meet, is priced nowhere. Both are ordinary contention. return; } @@ -449,13 +454,13 @@ contract PopControllerHandler is Test { /// registered one moves once the fee is paid. The recipient is a public actor either /// way, so a moved name never deposits a `LabelStore` on a gateway actor. function attemptTransfer(uint256 tokenIndex, uint256 toIndex) external { - uint256 gatewayCount = mintedLiteTokenIds.length; + uint256 gatewayCount = mintedDeviceTokenIds.length; uint256 total = gatewayCount + publicTokenIds.length; if (total == 0) return; uint256 pick = tokenIndex % total; uint256 tokenId = - pick < gatewayCount ? mintedLiteTokenIds[pick] : publicTokenIds[pick - gatewayCount]; + pick < gatewayCount ? mintedDeviceTokenIds[pick] : publicTokenIds[pick - gatewayCount]; if (!REGISTRAR.exists(tokenId)) return; address from = REGISTRAR.ownerOf(tokenId); @@ -480,10 +485,10 @@ contract PopControllerHandler is Test { /// @notice Creates an arbitrary subname under a gateway-issued name. /// @dev Interleaves subname creation with gateway mints so no subnode can quietly land on an - /// issued name. A lite parent is skipped: this derives the parent node by hashing the - /// whole label under the TLD, which is not a node the gateway minted for a lite name, so - /// the existence check returns early. The subnode owner comes from `publicActors` so a - /// subname never deposits a `LabelStore` on a gateway actor. + /// issued name. A device-name parent is skipped: this derives the parent node by hashing + /// the whole label under the TLD, which is not a node the gateway minted for a device + /// name, so the existence check returns early. The subnode owner comes from `publicActors` + /// so a subname never deposits a `LabelStore` on a gateway actor. function createSubname(uint256 parentIndex, uint256 subLabelSeed, uint256 toIndex) external { uint256 n = gatewayLabelsSeen.length; if (n == 0) return; @@ -551,12 +556,14 @@ contract PopControllerHandler is Test { /// @dev Returns true on success and false on revert so the caller's /// bookkeeping (ghost arrays) stays consistent with on-chain state. /// @return ok Whether the underlying call succeeded. - function _callReserveBaseName(IDotnsPopController.BaseReservation memory params) + function _callIssueDeviceNameWithReservation( + IDotnsPopController.DeviceNameIssuanceWithReservation memory params + ) internal returns (bool ok) { _mockOriginIsRoot(true); - try CONTROLLER.reserveBaseName(params) { + try CONTROLLER.issueDeviceNameWithReservation(params) { ok = true; } catch { ok = false; @@ -567,14 +574,14 @@ contract PopControllerHandler is Test { _mockOriginIsRoot(false); } - /// @notice Mirror of `_callReserveBaseName` for `registerBaseName`. + /// @notice Mirror of `_callIssueDeviceNameWithReservation` for `issuePersonhoodName`. /// @return ok Whether the underlying call succeeded. - function _callRegisterBaseName(IDotnsPopController.FullRegistration memory params) + function _callIssuePersonhoodName(IDotnsPopController.PersonhoodNameIssuance memory params) internal returns (bool ok) { _mockOriginIsRoot(true); - try CONTROLLER.registerBaseName(params) { + try CONTROLLER.issuePersonhoodName(params) { ok = true; } catch { ok = false; @@ -591,15 +598,15 @@ contract PopControllerHandler is Test { ); } - /// @notice Builds a classification-valid PoP lite label. + /// @notice Builds a classification-valid PoP device name. /// @dev Shape: `<4 letters from actor>.<2 digits>`, the separated form the gateway - /// accepts. The stem is 7 characters, which classifies as PopLite under PopRules, and + /// accepts. The stem is 7 characters, which classifies as Devicehood under PopRules, and /// the separator and digits are the suffix. Tag disambiguates the reserve vs claim call /// sites so neither collides with the other in the ERC721 namespace. The letter block - /// is derived from the actor address via keccak so each actor lives in its own lite + /// is derived from the actor address via keccak so each actor lives in its own device-name /// namespace. Suffix wraps modulo 100 so the label keeps exactly two digits; /// collisions past 100 reuses are swallowed by the caller's try/catch. - function _buildLiteLabel( + function _buildDeviceLabel( string memory tag, address actor, uint64 suffix diff --git a/test/invariant/registrar/RegistrarControllerHandler.t.sol b/test/invariant/registrar/RegistrarControllerHandler.t.sol index bd81e23b4..81706442d 100644 --- a/test/invariant/registrar/RegistrarControllerHandler.t.sol +++ b/test/invariant/registrar/RegistrarControllerHandler.t.sol @@ -166,13 +166,13 @@ contract RegistrarControllerHandler is Test { } /// @notice Mocks the personhood precompile so it reports `tier` for `account`. - /// @dev Mirrors the `BaseDotns._setUserPopStatus` mapping (PopFull = 2, - /// PopLite = 1, anything else = 0) so the handler stays consistent + /// @dev Mirrors the `BaseDotns._setUserPopStatus` mapping (Personhood = 2, + /// Devicehood = 1, anything else = 0) so the handler stays consistent /// with the test base. function _mockPersonhoodTier(address account, IPopRules.PopStatus tier) internal { uint8 statusByte; - if (tier == IPopRules.PopStatus.PopFull) statusByte = 2; - else if (tier == IPopRules.PopStatus.PopLite) statusByte = 1; + if (tier == IPopRules.PopStatus.Personhood) statusByte = 2; + else if (tier == IPopRules.PopStatus.Devicehood) statusByte = 1; bytes32 contextAlias = statusByte == 0 ? bytes32(0) : keccak256(abi.encode(account, statusByte)); diff --git a/test/invariant/registry/DotnsRegistryInvariant.t.sol b/test/invariant/registry/DotnsRegistryInvariant.t.sol index 3d90c3d20..28f73787e 100644 --- a/test/invariant/registry/DotnsRegistryInvariant.t.sol +++ b/test/invariant/registry/DotnsRegistryInvariant.t.sol @@ -6,7 +6,7 @@ import {IDotnsRegistry} from "../../../contracts/registry/IDotnsRegistry.sol"; import {IPopRules} from "../../../contracts/pop/IPopRules.sol"; import {RegistryHandler} from "./RegistryHandler.t.sol"; -/// @title Dotns Registry Invariant Suite +/// @title dotNS Registry Invariant Suite /// @notice Asserts authorisation, persistence, and parent-reassignment properties of the /// hierarchical registry across randomised registration, subnode creation, and /// transfer flows. @@ -29,12 +29,12 @@ contract DotnsRegistryInvariantTest is BaseDotns { vm.deal(address(handler), 1000 ether); - handler.addActor(ed, IPopRules.PopStatus.PopFull); - handler.addActor(leonardo, IPopRules.PopStatus.PopLite); + handler.addActor(ed, IPopRules.PopStatus.Personhood); + handler.addActor(leonardo, IPopRules.PopStatus.Devicehood); handler.addActor(tiago, IPopRules.PopStatus.NoStatus); address alice = _createUser("alice"); - handler.addActor(alice, IPopRules.PopStatus.PopFull); + handler.addActor(alice, IPopRules.PopStatus.Personhood); targetContract(address(handler)); diff --git a/test/invariant/registry/RegistryHandler.t.sol b/test/invariant/registry/RegistryHandler.t.sol index 5d95f71e1..91fa7e26e 100644 --- a/test/invariant/registry/RegistryHandler.t.sol +++ b/test/invariant/registry/RegistryHandler.t.sol @@ -122,8 +122,8 @@ contract RegistryHandler is Test { /// @param tier Verification tier to report for `account`. function _mockPersonhoodTier(address account, IPopRules.PopStatus tier) internal { uint8 statusByte; - if (tier == IPopRules.PopStatus.PopFull) statusByte = 2; - else if (tier == IPopRules.PopStatus.PopLite) statusByte = 1; + if (tier == IPopRules.PopStatus.Personhood) statusByte = 2; + else if (tier == IPopRules.PopStatus.Devicehood) statusByte = 1; bytes32 contextAlias = statusByte == 0 ? bytes32(0) : keccak256(abi.encode(account, statusByte)); diff --git a/test/unit/escrow/DotnsNameEscrow.t.sol b/test/unit/escrow/DotnsNameEscrow.t.sol index 87227ca7b..2fe4feb4a 100644 --- a/test/unit/escrow/DotnsNameEscrow.t.sol +++ b/test/unit/escrow/DotnsNameEscrow.t.sol @@ -34,8 +34,8 @@ contract GhostNft is ERC721 { /// @custom:contract DotnsNameEscrow, plus the pull-payment and solvency guarantees. contract DotnsNameEscrowTest is BaseDotns { /// @notice Default label used across most tests. - /// @dev Nine-character stem that classifies as NoStatus; the flat deposit equals - /// BASE_DEPOSIT. + /// @dev Eleven characters measured as written, which classifies as NoStatus; the flat deposit + /// equals BASE_DEPOSIT. string internal constant LABEL = "labelnine01"; /// @notice Register `label` for `nameOwner` under the NoStatus PoP tier and return its tokenId. @@ -219,7 +219,7 @@ contract DotnsNameEscrowTest is BaseDotns { function test_self_registration_seeds_funded_position() public { string memory popLabel = "selfregis"; - bytes32 node = _register(popLabel, ed, IPopRules.PopStatus.PopFull); + bytes32 node = _register(popLabel, ed, IPopRules.PopStatus.Personhood); uint256 tokenId = uint256(node); vm.prank(ed); @@ -410,9 +410,9 @@ contract DotnsNameEscrowTest is BaseDotns { function test_funded_position_rebinds_to_new_holder_on_transfer() public { string memory label = BASE_LABEL_A; - _grantPopFull(ed); - _grantPopFull(leonardo); - _register(label, ed, IPopRules.PopStatus.PopFull); + _grantPersonhood(ed); + _grantPersonhood(leonardo); + _register(label, ed, IPopRules.PopStatus.Personhood); uint256 tokenId = _tokenIdForLabel(label); IDotnsNameEscrow.ReleasePosition memory before = dotnsNameEscrow.getReleasePosition(tokenId); @@ -456,8 +456,8 @@ contract DotnsNameEscrowTest is BaseDotns { function test_downgrade_transfer_pays_name_price() public { string memory label = "lights01"; - _grantPopFull(ed); - _grantPopLite(leonardo); + _grantPersonhood(ed); + _grantDevicehood(leonardo); _commitAndRegister(label, ed, false); @@ -484,17 +484,17 @@ contract DotnsNameEscrowTest is BaseDotns { // Downward cross-tier transfer of a funded NoStatus name: the friction fee settles to // protocol fees and the deposit travels with the NFT. There is no transfer-time refund: // `position.recipient` rebinds to the new holder, the locked deposit follows, and only - // the new holder can later release into escrow. Promoting `ed` to PopFull before the + // the new holder can later release into escrow. Promoting `ed` to Personhood before the // transfer forces `PopRules.transferFloor` to return the base fee D while the position // still carries the original `BASE_DEPOSIT` deposit, so both legs of `chargeTransferFee` // run in one call. uint256 tokenId = _registerNoStatus(LABEL, ed); - _grantPopFull(ed); + _grantPersonhood(ed); uint256 startingPrice = popRules.price(LABEL); uint256 quotedFee = dotnsRegistrar.quoteTransferFee(tokenId, leonardo); - assertEq(quotedFee, startingPrice, "PopFull holder downgrading to NoStatus pays D"); + assertEq(quotedFee, startingPrice, "Personhood holder downgrading to NoStatus pays D"); uint256 reservesBefore = dotnsNameEscrow.reserves(address(0)); uint256 protocolFeesBefore = dotnsNameEscrow.protocolFees(); @@ -761,14 +761,14 @@ contract DotnsNameEscrowTest is BaseDotns { /// @notice Verified payer sponsoring a NoStatus owner on a NoStatus-tier label pays /// exactly D under the max-not-sum rule. /// @dev `priced.price` equals D because the owner is NoStatus, and `transferFloor` returns - /// D because the payer's PopFull tier downgrades into the owner's NoStatus tier. The + /// D because the payer's Personhood tier downgrades into the owner's NoStatus tier. The /// controller charges `max(priced.price, friction) = D` and routes the entire charge /// into protocol fees; the owner-side refundable position is seeded with zero /// amount, so reserves must not move. function test_cross_payer_verified_sponsors_nostatus_pays_only_D() public { string memory label = "crosspayr01"; - _grantPopFull(leonardo); + _grantPersonhood(leonardo); // ed left at default NoStatus tier. uint256 priorProtocolFees = dotnsNameEscrow.protocolFees(); @@ -810,8 +810,8 @@ contract DotnsNameEscrowTest is BaseDotns { function test_cross_payer_pays_the_curve_into_fees() public { string memory label = BASE_LABEL_A; - _grantPopFull(leonardo); - _grantPopFull(ed); + _grantPersonhood(leonardo); + _grantPersonhood(ed); uint256 ownPrice = popRules.price(label); uint256 priorProtocolFees = dotnsNameEscrow.protocolFees(); @@ -845,12 +845,12 @@ contract DotnsNameEscrowTest is BaseDotns { /// it, so `max(ownerPrice, friction)` collapses to the owner price. The whole charge /// routes to protocol fees with no refundable deposit. function test_cross_payer_downgrade_charges_only_owner_price() public { - // LITE_LABEL_A carries the gateway's separator, which the public path this test drives + // DEVICE_LABEL_A carries the gateway's separator, which the public path this test drives // rejects, so the label here is a flat one in the same tier. string memory label = "michael01"; - _grantPopFull(leonardo); - _grantPopLite(ed); + _grantPersonhood(leonardo); + _grantDevicehood(ed); uint256 ownPrice = popRules.price(label); uint256 priorProtocolFees = dotnsNameEscrow.protocolFees(); diff --git a/test/unit/escrow/DotnsNameEscrowRedeem.t.sol b/test/unit/escrow/DotnsNameEscrowRedeem.t.sol index 4f182c3c2..a479125b7 100644 --- a/test/unit/escrow/DotnsNameEscrowRedeem.t.sol +++ b/test/unit/escrow/DotnsNameEscrowRedeem.t.sol @@ -20,8 +20,8 @@ contract DotnsNameEscrowRedeemTest is BaseDotns { /// @notice 14-char label classifying as NoStatus, so registration seeds a funded position. string internal constant FUNDED_LABEL = "redeemlabela01"; - /// @notice 6-char digit-free label classifying as PopFull, registered on the cross-payer path - /// so its deposit position carries a zero amount. + /// @notice 6-char digit-free label classifying as Personhood, registered on the cross-payer + /// path so its deposit position carries a zero amount. string internal constant FREE_LABEL = "redeem"; /// @notice Register `label` for `nameOwner` at `status` and return its tokenId. @@ -322,7 +322,8 @@ contract DotnsNameEscrowRedeemTest is BaseDotns { /// right to recover their own name for no consideration whatsoever. `withdraw` is also a /// step a holder has every reason to make before a name can be recycled. function test_zero_amount_withdrawal_does_not_forfeit_the_redeem_right() public { - uint256 tokenId = _registerCrossPayer(FREE_LABEL, ed, leonardo, IPopRules.PopStatus.PopFull); + uint256 tokenId = + _registerCrossPayer(FREE_LABEL, ed, leonardo, IPopRules.PopStatus.Personhood); assertEq(_positionOf(tokenId).amount, 0, "this case needs a zero-amount position"); _approveAndRelease(tokenId, ed); @@ -439,7 +440,8 @@ contract DotnsNameEscrowRedeemTest is BaseDotns { /// Under a `released && claimed` reclaim gate that would make the name permanently /// unregisterable. function test_zero_amount_release_becomes_reclaimable_without_any_withdrawal() public { - uint256 tokenId = _registerCrossPayer(FREE_LABEL, ed, leonardo, IPopRules.PopStatus.PopFull); + uint256 tokenId = + _registerCrossPayer(FREE_LABEL, ed, leonardo, IPopRules.PopStatus.Personhood); assertEq( _positionOf(tokenId).amount, diff --git a/test/unit/pop/PopRules.t.sol b/test/unit/pop/PopRules.t.sol index ee8599a6b..131402c5f 100644 --- a/test/unit/pop/PopRules.t.sol +++ b/test/unit/pop/PopRules.t.sol @@ -23,20 +23,20 @@ contract PopRulesTests is BaseDotns { assertEq(classificationMessage, "Reserved for Governance"); } - function test_classify_poplite() public view { + function test_classify_devicehood() public view { (IPopRules.PopStatus classificationStatus, string memory classificationMessage) = popRules.classifyName("lights.01"); - assertEq(uint256(classificationStatus), uint256(IPopRules.PopStatus.PopLite)); - assertEq(classificationMessage, "Requires Lite personhood verification"); + assertEq(uint256(classificationStatus), uint256(IPopRules.PopStatus.Devicehood)); + assertEq(classificationMessage, "Requires devicehood"); } - function test_classify_popfull() public view { + function test_classify_personhood() public view { (IPopRules.PopStatus classificationStatus, string memory classificationMessage) = popRules.classifyName("alicebob"); - assertEq(uint256(classificationStatus), uint256(IPopRules.PopStatus.PopFull)); - assertEq(classificationMessage, "Requires Full personhood verification"); + assertEq(uint256(classificationStatus), uint256(IPopRules.PopStatus.Personhood)); + assertEq(classificationMessage, "Requires personhood"); } function test_classify_nostatus() public view { @@ -59,10 +59,10 @@ contract PopRulesTests is BaseDotns { /// these is measured as written: `andrew1` is seven characters and `andrew123` is nine. function test_classify_accepts_any_flat_digit_count() public view { (IPopRules.PopStatus oneDigit,) = popRules.classifyName("andrew1"); - assertEq(uint256(oneDigit), uint256(IPopRules.PopStatus.PopFull)); + assertEq(uint256(oneDigit), uint256(IPopRules.PopStatus.Personhood)); (IPopRules.PopStatus twoDigits,) = popRules.classifyName("andrew01"); - assertEq(uint256(twoDigits), uint256(IPopRules.PopStatus.PopFull)); + assertEq(uint256(twoDigits), uint256(IPopRules.PopStatus.Personhood)); (IPopRules.PopStatus threeDigits,) = popRules.classifyName("andrew123"); assertEq(uint256(threeDigits), uint256(IPopRules.PopStatus.NoStatus)); @@ -75,32 +75,36 @@ contract PopRulesTests is BaseDotns { assertEq(uint256(webThree), uint256(IPopRules.PopStatus.Reserved)); (IPopRules.PopStatus interior,) = popRules.classifyName("micha3l"); - assertEq(uint256(interior), uint256(IPopRules.PopStatus.PopFull)); + assertEq(uint256(interior), uint256(IPopRules.PopStatus.Personhood)); } - /// @dev A lite label's suffix is fixed by its shape, so a wrong digit count fails the shape + /// @dev A device name's suffix is fixed by its shape, so a wrong digit count fails the shape /// check rather than the count check and never reaches classification. - function test_classify_reverts_for_a_lite_suffix_of_the_wrong_length() public { + function test_classify_reverts_for_a_device_suffix_of_the_wrong_length() public { vm.expectRevert( abi.encodeWithSelector( IPopRules.PopError.selector, - "Name must be a lowercase ASCII DNS label or a lite label" + "Name must be a lowercase ASCII DNS label or a device name" ) ); popRules.classifyName("andrew.1"); } - /// @dev The gateway's suffix is not part of the name the candidate chose, so a lite label + /// @dev The gateway's suffix is not part of the name the candidate chose, so a device name /// prices as its stem, while its flat spelling is a different, longer name. The flat /// launch model charges one deposit at every length, so the rule is only observable on /// a length-sensitive curve; this switches to the scarcity model to see it. - function test_a_lite_suffix_prices_as_its_stem() public { + function test_a_device_suffix_prices_as_its_stem() public { vm.startPrank(owner); costModelRegistry.register(IDotnsPricing(address(scarcityPricing))); costModelRegistry.setCurrentVersion(scarcityPricing.version()); vm.stopPrank(); - assertEq(popRules.price("andrew.01"), popRules.price("andrew"), "lite prices as its stem"); + assertEq( + popRules.price("andrew.01"), + popRules.price("andrew"), + "a device name prices as its stem" + ); assertTrue( popRules.price("andrew01") != popRules.price("andrew"), "the flat spelling is a longer name" @@ -108,14 +112,14 @@ contract PopRulesTests is BaseDotns { } function test_verified_person_pays_the_deposit_for_premium() public { - _grantPopFull(ed); + _grantPersonhood(ed); assertEq(popRules.priceWithCheck("alicebob", ed).price, BASE_DEPOSIT); assertEq(popRules.priceWithCheck("lights", ed).price, BASE_DEPOSIT); } function test_transfer_reprices_at_own_length() public { - _grantPopFull(leonardo); + _grantPersonhood(leonardo); assertEq(popRules.transferFloor("lights", leonardo, tiago), BASE_DEPOSIT); assertEq(popRules.transferFloor("lights", leonardo, leonardo), 0); @@ -128,40 +132,36 @@ contract PopRulesTests is BaseDotns { popRules.priceWithCheck("hello", ed); } - function test_price_with_check_revert_full_needed() public { - vm.expectRevert( - abi.encodeWithSelector( - IPopRules.PopError.selector, "Requires Full personhood verification" - ) - ); + function test_price_with_check_revert_personhood_needed() public { + vm.expectRevert(abi.encodeWithSelector(IPopRules.PopError.selector, "Requires personhood")); popRules.priceWithCheck("alicebob", ed); } - function test_popfull_user_can_access_poplite_name() public { - _grantPopFull(ed); + function test_personhood_user_can_access_devicehood_name() public { + _grantPersonhood(ed); IPopRules.PriceWithMeta memory priceMetadata = popRules.priceWithCheck("lights.01", ed); - assertEq(uint256(priceMetadata.status), uint256(IPopRules.PopStatus.PopLite)); - assertEq(uint256(priceMetadata.userStatus), uint256(IPopRules.PopStatus.PopFull)); + assertEq(uint256(priceMetadata.status), uint256(IPopRules.PopStatus.Devicehood)); + assertEq(uint256(priceMetadata.userStatus), uint256(IPopRules.PopStatus.Personhood)); assertEq(priceMetadata.price, BASE_DEPOSIT); } - function test_poplite_user_can_access_nostatus_name() public { - _grantPopLite(ed); + function test_devicehood_user_can_access_nostatus_name() public { + _grantDevicehood(ed); IPopRules.PriceWithMeta memory priceMetadata = popRules.priceWithCheck("longnamehere.01", ed); assertEq(uint256(priceMetadata.status), uint256(IPopRules.PopStatus.NoStatus)); - assertEq(uint256(priceMetadata.userStatus), uint256(IPopRules.PopStatus.PopLite)); + assertEq(uint256(priceMetadata.userStatus), uint256(IPopRules.PopStatus.Devicehood)); assertEq(priceMetadata.price, BASE_DEPOSIT); } function test_base_reservation_blocks_others() public { // Authorise this test contract as a registrar controller so it may call - // reserveBaseName (gated by DotnsRegistrar.controllers). Passes the stem - // directly per the stems-only public boundary. + // reserveBaseName (gated by DotnsRegistrar.controllers). Passes the base name + // directly, as the public boundary takes base names only. _authoriseTestAsController(); popRules.reserveBaseName("lights", leonardo); @@ -175,7 +175,7 @@ contract PopRulesTests is BaseDotns { vm.expectRevert( abi.encodeWithSelector( - IPopRules.PopError.selector, "Base name reserved for original Lite registrant" + IPopRules.PopError.selector, "Reserved for a device-name holder's personhood claim" ) ); popRules.priceWithCheck("lights", tiago); @@ -194,7 +194,7 @@ contract PopRulesTests is BaseDotns { function test_short_names_closed_reverts_direct_path() public { _setShortNames(false); - _grantPopFull(ed); + _grantPersonhood(ed); vm.expectRevert( abi.encodeWithSelector(IPopRules.PopError.selector, "Short names are not for sale") ); @@ -211,14 +211,14 @@ contract PopRulesTests is BaseDotns { function test_open_band_priced_while_short_names_closed() public { _setShortNames(false); - _grantPopLite(ed); + _grantDevicehood(ed); // longnamehere is 12 characters, so the switch never gates it. assertEq(popRules.priceWithCheck("longnamehere", ed).price, BASE_DEPOSIT); } function test_enabling_short_names_opens_the_market() public { _setShortNames(false); - _grantPopFull(ed); + _grantPersonhood(ed); _setShortNames(true); assertEq(popRules.priceWithCheck("alicebob", ed).price, BASE_DEPOSIT); } @@ -268,10 +268,10 @@ contract PopRulesTests is BaseDotns { } function test_transferFloor_matches_model() public { - // A PopFull sender handing a NoStatus name to a NoStatus recipient pays the name's own + // A Personhood sender handing a NoStatus name to a NoStatus recipient pays the name's own // price, which is the registered model's amount for its base length. string memory label = "longnamehere"; - _grantPopFull(ed); + _grantPersonhood(ed); uint256 floor = popRules.transferFloor(label, ed, leonardo); assertEq(floor, costModelRegistry.priceForBaseLength(bytes(label).length)); } diff --git a/test/unit/pop/PopRulesClassification.t.sol b/test/unit/pop/PopRulesClassification.t.sol index 2e39dc3bf..af45f2b11 100644 --- a/test/unit/pop/PopRulesClassification.t.sol +++ b/test/unit/pop/PopRulesClassification.t.sol @@ -12,9 +12,9 @@ import {IPopRules} from "../../../contracts/pop/IPopRules.sol"; /// and reads no storage, so a tier is a function of the label alone. Anything that needs a /// cost model or a reservation belongs in the proxy-backed suite instead. contract PopRulesClassificationTests is Test { - /// @notice Guard message for a string that is neither a DNS label nor a lite label. + /// @notice Guard message for a string that is neither a DNS label nor a device name. string internal constant SHAPE_ERROR = - "Name must be a lowercase ASCII DNS label or a lite label"; + "Name must be a lowercase ASCII DNS label or a device name"; PopRules internal rules; @@ -31,24 +31,24 @@ contract PopRulesClassificationTests is Test { vm.expectRevert(abi.encodeWithSelector(IPopRules.PopError.selector, reason)); } - /// @dev Base length is the stem, so a two-digit suffix does not count towards the band - /// whether or not it carries the gateway's separator. - function test_classify_measures_a_lite_label_by_its_stem() public view { + /// @dev Base length is the length of the base name, so a two-digit suffix does not count + /// towards the band whether or not it carries the gateway's separator. + function test_classify_measures_a_device_name_by_its_stem() public view { _assertTier("alice.42", IPopRules.PopStatus.Reserved); - _assertTier("joseph.42", IPopRules.PopStatus.PopLite); - _assertTier("benjamin.42", IPopRules.PopStatus.PopLite); + _assertTier("joseph.42", IPopRules.PopStatus.Devicehood); + _assertTier("benjamin.42", IPopRules.PopStatus.Devicehood); _assertTier("elizabeth.42", IPopRules.PopStatus.NoStatus); } /// @notice The separator is meaning, not presentation: it is what makes a name an identity. - /// @dev A lite label is measured by the stem the candidate chose, because the gateway + /// @dev A device name is measured by the stem the candidate chose, because the gateway /// allocated the digits. An ordinary label is measured as written. So the two spellings - /// are different names in different bands, and only the separated one is PopLite. + /// are different names in different bands, and only the separated one is Devicehood. function test_classify_differs_with_and_without_the_separator() public view { - _assertTier("joseph.42", IPopRules.PopStatus.PopLite); - _assertTier("joseph42", IPopRules.PopStatus.PopFull); + _assertTier("joseph.42", IPopRules.PopStatus.Devicehood); + _assertTier("joseph42", IPopRules.PopStatus.Personhood); - _assertTier("michael.01", IPopRules.PopStatus.PopLite); + _assertTier("michael.01", IPopRules.PopStatus.Devicehood); _assertTier("michael01", IPopRules.PopStatus.NoStatus); _assertTier("elizabeth.42", IPopRules.PopStatus.NoStatus); @@ -56,28 +56,33 @@ contract PopRulesClassificationTests is Test { } /// @notice The regression test for deriving the digit count by subtraction. - /// @dev `bytes(name).length - baseLength` is 3 for a lite label, never 2, so a subtraction - /// based check classifies every lite name in the 6-8 band as PopFull. These rows catch - /// it, which is why they assert PopLite rather than merely "not Reserved". - function test_classify_puts_a_lite_label_in_the_lite_tier_not_the_full_tier() public view { + /// @dev `bytes(name).length - baseLength` is 3 for a device name, never 2, so a subtraction + /// based check classifies every device name in the 6-8 band as Personhood. These rows + /// catch it, which is why they assert Devicehood rather than merely "not Reserved". + function test_classify_puts_a_device_name_in_the_devicehood_tier_not_the_personhood_tier() + public + view + { (IPopRules.PopStatus sixChar,) = rules.classifyName("joseph.42"); - assertEq(uint256(sixChar), uint256(IPopRules.PopStatus.PopLite), "stem 6 is PopLite"); + assertEq(uint256(sixChar), uint256(IPopRules.PopStatus.Devicehood), "stem 6 is Devicehood"); (IPopRules.PopStatus eightChar,) = rules.classifyName("benjamin.42"); - assertEq(uint256(eightChar), uint256(IPopRules.PopStatus.PopLite), "stem 8 is PopLite"); + assertEq( + uint256(eightChar), uint256(IPopRules.PopStatus.Devicehood), "stem 8 is Devicehood" + ); } function test_classify_measures_a_label_without_a_suffix_whole() public view { _assertTier("alice", IPopRules.PopStatus.Reserved); - _assertTier("joseph", IPopRules.PopStatus.PopFull); - _assertTier("benjamin", IPopRules.PopStatus.PopFull); + _assertTier("joseph", IPopRules.PopStatus.Personhood); + _assertTier("benjamin", IPopRules.PopStatus.Personhood); _assertTier("elizabeth", IPopRules.PopStatus.NoStatus); } /// @dev An ordinary name carrying digits stays registrable, measured as written. function test_classify_accepts_an_ordinary_flat_digit_suffix() public view { _assertTier("longnamebob01", IPopRules.PopStatus.NoStatus); - _assertTier("lights01", IPopRules.PopStatus.PopFull); + _assertTier("lights01", IPopRules.PopStatus.Personhood); } /// @dev No digit count is privileged or rejected, and none is stripped: a name ending in @@ -85,15 +90,15 @@ contract PopRulesClassificationTests is Test { function test_classify_accepts_a_flat_suffix_of_any_length() public view { _assertTier("iamtherealbob0", IPopRules.PopStatus.NoStatus); _assertTier("elizabeth12345", IPopRules.PopStatus.NoStatus); - _assertTier("blink182", IPopRules.PopStatus.PopFull); + _assertTier("blink182", IPopRules.PopStatus.Personhood); _assertTier("web3", IPopRules.PopStatus.Reserved); } - /// @dev A lite label's suffix is fixed by its shape, so a wrong count fails the shape check + /// @dev A device name's suffix is fixed by its shape, so a wrong count fails the shape check /// before the count check can see it. - /// @dev A separator is legal only on a lite label, so a string carrying one that misses - /// the shape in any way is neither a DNS label nor a lite label. - function test_classify_reverts_for_a_malformed_lite_label() public { + /// @dev A separator is legal only on a device name, so a string carrying one that misses + /// the shape in any way is neither a DNS label nor a device name. + function test_classify_reverts_for_a_malformed_device_name() public { _expectRevert(SHAPE_ERROR); rules.classifyName("joseph.4"); @@ -108,7 +113,7 @@ contract PopRulesClassificationTests is Test { rules.classifyName("jos3ph.42"); } - /// @dev Only a lite label is shortened, so `joseph.42` contends with a reservation on + /// @dev Only a device name is shortened, so `joseph.42` contends with a reservation on /// `joseph` while `joseph42` is an unrelated name that contends with nothing. function test_stripDigits_shortens_only_the_separated_form() public view { assertEq(rules.stripDigits("joseph.42"), "joseph"); @@ -116,13 +121,13 @@ contract PopRulesClassificationTests is Test { assertEq(rules.stripDigits("blink182"), "blink182"); } - /// @dev A stem returned as `joseph.` would fail `isSingleLabelMemory` in the registrar + /// @dev A base name returned as `joseph.` would fail `isSingleLabelMemory` in the registrar /// controller and silently skip the reclaim branch's reservation release. function test_stripDigits_leaves_no_trailing_separator() public view { - string memory stem = rules.stripDigits("joseph.42"); - bytes memory raw = bytes(stem); - assertTrue(raw.length > 0, "stem is not empty"); - assertTrue(raw[raw.length - 1] != ".", "stem carries no trailing separator"); + string memory baseName = rules.stripDigits("joseph.42"); + bytes memory raw = bytes(baseName); + assertTrue(raw.length > 0, "base name is not empty"); + assertTrue(raw[raw.length - 1] != ".", "base name carries no trailing separator"); } function test_stripDigits_returns_a_suffixless_label_verbatim() public view { @@ -130,8 +135,8 @@ contract PopRulesClassificationTests is Test { } /// @dev Must answer the question rather than revert, since the controller asks it of - /// labels that may be lite. - function test_isBaseName_answers_false_for_a_lite_label() public view { + /// labels that may be device-name. + function test_isBaseName_answers_false_for_a_device_name() public view { assertFalse(rules.isBaseName("joseph.42")); assertTrue(rules.isBaseName("elizabeth")); } diff --git a/test/unit/registrar/DotnsPopController.t.sol b/test/unit/registrar/DotnsPopController.t.sol index 787820977..1706ff3b6 100644 --- a/test/unit/registrar/DotnsPopController.t.sol +++ b/test/unit/registrar/DotnsPopController.t.sol @@ -22,16 +22,16 @@ import {Vm} from "forge-std/Vm.sol"; /// tests assert specific behaviours that do not benefit from input /// variation. contract DotnsPopControllerTests is BaseDotns { - function test_reserveBaseName_mints_and_wires_registry_and_resolver() public { - // Lite path requires PopLite tier and a classification-valid lite label. - _grantPopFull(ed); + function test_issueDeviceNameWithReservation_mints_and_wires_registry_and_resolver() public { + // Device-name path requires Devicehood tier and a classification-valid device name. + _grantPersonhood(ed); bytes memory chatKey = _validChatKey(0x01); - _reservePop(ed, LITE_LABEL_A, chatKey, ""); + _reservePop(ed, DEVICE_LABEL_A, chatKey, ""); - // A lite username is a subnode under its numeric container, not a tokenised name, so its + // A device name is a subnode under its numeric container, not a tokenised name, so its // ownership lives in the registry record rather than the registrar's ERC-721 ledger. - bytes32 node = _liteNodeOf(LITE_LABEL_A); + bytes32 node = _deviceNodeOf(DEVICE_LABEL_A); assertEq(dotnsRegistry.owner(node), ed); assertEq(dotnsPopResolver.chatKey(node), chatKey); @@ -51,9 +51,9 @@ contract DotnsPopControllerTests is BaseDotns { /// does not re-mint it. /// @dev A re-mint would revert on the already-registered container, so the second reservation /// succeeding, with the container owner unchanged, is proof it took the reuse path. - function test_second_lite_stem_reuses_the_container() public { - _grantPopFull(ed); - _grantPopFull(leonardo); + function test_second_device_stem_reuses_the_container() public { + _grantPersonhood(ed); + _grantPersonhood(leonardo); _reservePop(ed, "michael.01", _validChatKey(0x01), ""); bytes32 containerNode = _nodeOf("01"); @@ -62,177 +62,199 @@ contract DotnsPopControllerTests is BaseDotns { _reservePop(leonardo, "matthew.01", _validChatKey(0x02), ""); assertEq(dotnsRegistry.owner(containerNode), containerOwner, "container not re-owned"); - assertEq(dotnsRegistry.owner(_liteNodeOf("michael.01")), ed, "first stem owned by ed"); + assertEq(dotnsRegistry.owner(_deviceNodeOf("michael.01")), ed, "first stem owned by ed"); assertEq( - dotnsRegistry.owner(_liteNodeOf("matthew.01")), + dotnsRegistry.owner(_deviceNodeOf("matthew.01")), leonardo, "second stem owned by leonardo" ); } - function test_reserveBaseName_reverts_when_origin_is_not_root() public { + function test_issueDeviceNameWithReservation_reverts_when_origin_is_not_root() public { _mockOriginIsRoot(false); vm.expectRevert(IDotnsPopController.NotRoot.selector); - dotnsPopController.reserveBaseName( - IDotnsPopController.BaseReservation({ - lite: IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: "" + dotnsPopController.issueDeviceNameWithReservation( + IDotnsPopController.DeviceNameIssuanceWithReservation({ + issuance: IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: "" }), - reservedBaseLabel: "" + reservedLabel: "" }) ); } - function test_reserveBaseName_enqueues_when_reserved_label_provided() public { + function test_issueDeviceNameWithReservation_enqueues_when_reserved_label_provided() public { // `PopRules.priceWithCheck` admits only the live reservation holder on // a given base stem, so the queue is single-occupant. Multi-occupant // queue coverage lives in the invariant suite. - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0xaa), BASE_LABEL_A); + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0xaa), PERSONHOOD_LABEL_A); - (bool reserved, address holder) = dotnsPopController.isReservedForClaim(BASE_LABEL_A); + (bool reserved, address holder) = dotnsPopController.isReservedForClaim(PERSONHOOD_LABEL_A); assertTrue(reserved); assertEq(holder, ed); } - function test_registerBaseName_claim_emits_claim_event_and_not_standalone() public { - // ed gets a different PopFull-classified base label via standalone mint. - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), BASE_LABEL_A); + function test_issuePersonhoodName_claim_emits_reservation_claimed() public { + // ed gets a different Personhood-classified base label via standalone mint. + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), PERSONHOOD_LABEL_A); - IDotnsPopController.Link memory link = _linkWithLite(LITE_LABEL_A); + IDotnsPopController.Link memory link = _linkWithDeviceName(DEVICE_LABEL_A); vm.recordLogs(); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: BASE_LABEL_A, user: ed, link: link}) + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: link + }) ); Vm.Log[] memory logs = vm.getRecordedLogs(); - _assertEventEmittedOnce(logs, keccak256("BaseNameClaimed(bytes32,address,string)")); - _assertEventNotEmitted(logs, keccak256("StandaloneNameRegistered(bytes32,address,string)")); + _assertEventEmittedOnce(logs, keccak256("ReservationClaimed(bytes32,address)")); + _assertEventEmittedOnce(logs, keccak256("PersonhoodNameIssued(bytes32,address,string)")); - (bool reserved,) = dotnsPopController.isReservedForClaim(BASE_LABEL_A); + (bool reserved,) = dotnsPopController.isReservedForClaim(PERSONHOOD_LABEL_A); assertFalse(reserved); } - function test_registerBaseName_standalone_emits_standalone_event_and_not_claim() public { - _grantPopFull(tiago); - _reservePop(tiago, LITE_LABEL_B, _validChatKey(0x01), BASE_LABEL_A); + function test_issuePersonhoodName_standalone_emits_no_reservation_claimed() public { + _grantPersonhood(tiago); + _reservePop(tiago, DEVICE_LABEL_B, _validChatKey(0x01), PERSONHOOD_LABEL_A); // `priceWithCheck` admits only the live reservation holder on a given // base stem, so the standalone mint runs against a separate, free stem // owned by a different user; this asserts neither user sees state leak // from the other. - _grantPopFull(ed); + _grantPersonhood(ed); IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0xcf)); vm.recordLogs(); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: BASE_LABEL_C, user: ed, link: link}) + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_C, user: ed, link: link + }) ); Vm.Log[] memory logs = vm.getRecordedLogs(); - _assertEventEmittedOnce(logs, keccak256("StandaloneNameRegistered(bytes32,address,string)")); - _assertEventNotEmitted(logs, keccak256("BaseNameClaimed(bytes32,address,string)")); + _assertEventEmittedOnce(logs, keccak256("PersonhoodNameIssued(bytes32,address,string)")); + _assertEventNotEmitted(logs, keccak256("ReservationClaimed(bytes32,address)")); } - function test_registerBaseName_claim_inherits_chat_key_from_lite_node() public { - // Promote ed to PopFull so the standalone PopFull-classified mint passes. - _grantPopFull(ed); - bytes memory liteChatKey = _validChatKey(0xaa); - _reservePop(ed, LITE_LABEL_A, liteChatKey, BASE_LABEL_A); + function test_issuePersonhoodName_claim_inherits_chat_key_from_device_node() public { + // Promote ed to Personhood so the standalone Personhood-classified mint passes. + _grantPersonhood(ed); + bytes memory deviceChatKey = _validChatKey(0xaa); + _reservePop(ed, DEVICE_LABEL_A, deviceChatKey, PERSONHOOD_LABEL_A); - IDotnsPopController.Link memory link = _linkWithLite(LITE_LABEL_A); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: BASE_LABEL_A, user: ed, link: link}) + IDotnsPopController.Link memory link = _linkWithDeviceName(DEVICE_LABEL_A); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: link + }) ); - bytes32 fullNode = _nodeOf(BASE_LABEL_A); - assertEq(dotnsPopResolver.chatKey(fullNode), liteChatKey); - assertEq(dotnsPopResolver.liteLink(fullNode), keccak256(bytes(LITE_LABEL_A))); + bytes32 personhoodNode = _nodeOf(PERSONHOOD_LABEL_A); + assertEq(dotnsPopResolver.chatKey(personhoodNode), deviceChatKey); + assertEq( + dotnsPopResolver.deviceLabelhashOf(personhoodNode), keccak256(bytes(DEVICE_LABEL_A)) + ); } - function test_registerBaseName_claim_wipes_entire_queue() public { + function test_issuePersonhoodName_claim_wipes_entire_queue() public { // `priceWithCheck` admits only the live reservation holder on a given // base stem, so the queue stays single-occupant; after the holder // claims, the stem is free for a fresh reservation from any user. - _grantPopFull(ed); - _grantPopFull(tiago); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), BASE_LABEL_A); - - IDotnsPopController.Link memory link = _linkWithLite(LITE_LABEL_A); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: BASE_LABEL_A, user: ed, link: link}) + _grantPersonhood(ed); + _grantPersonhood(tiago); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), PERSONHOOD_LABEL_A); + + IDotnsPopController.Link memory link = _linkWithDeviceName(DEVICE_LABEL_A); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: link + }) ); - _reservePop(tiago, LITE_LABEL_D, _validChatKey(0x04), BASE_LABEL_B); - (, address wonderHolder) = dotnsPopController.isReservedForClaim(BASE_LABEL_B); + _reservePop(tiago, DEVICE_LABEL_D, _validChatKey(0x04), PERSONHOOD_LABEL_B); + (, address wonderHolder) = dotnsPopController.isReservedForClaim(PERSONHOOD_LABEL_B); assertEq(wonderHolder, tiago); } - function test_registerBaseName_standalone_auto_relinquishes_users_other_reservation() public { - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), BASE_LABEL_A); + function test_issuePersonhoodName_standalone_auto_relinquishes_users_other_reservation() + public + { + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), PERSONHOOD_LABEL_A); - _grantPopFull(ed); + _grantPersonhood(ed); IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0x02)); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: BASE_LABEL_B, user: ed, link: link}) + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_B, user: ed, link: link + }) ); - (bool reserved,) = dotnsPopController.isReservedForClaim(BASE_LABEL_A); + (bool reserved,) = dotnsPopController.isReservedForClaim(PERSONHOOD_LABEL_A); assertFalse(reserved); } - function test_registerBaseName_standalone_with_lite_link_silently_relinquishes() public { - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), BASE_LABEL_A); + function test_issuePersonhoodName_standalone_with_device_link_relinquishes_the_other_reservation() + public + { + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), PERSONHOOD_LABEL_A); - _grantPopFull(ed); - IDotnsPopController.Link memory link = _linkWithLite(LITE_LABEL_A); + _grantPersonhood(ed); + IDotnsPopController.Link memory link = _linkWithDeviceName(DEVICE_LABEL_A); vm.recordLogs(); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: BASE_LABEL_B, user: ed, link: link}) + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_B, user: ed, link: link + }) ); Vm.Log[] memory logs = vm.getRecordedLogs(); - _assertEventEmittedOnce(logs, keccak256("StandaloneNameRegistered(bytes32,address,string)")); - _assertEventEmittedOnce(logs, keccak256("LiteToFullLinked(bytes32,bytes32)")); - _assertEventNotEmitted(logs, keccak256("BaseNameClaimed(bytes32,address,string)")); - _assertEventNotEmitted(logs, keccak256("ReservationRelinquished(bytes32,address)")); + _assertEventEmittedOnce(logs, keccak256("PersonhoodNameIssued(bytes32,address,string)")); + _assertEventEmittedOnce(logs, keccak256("DeviceNameLinked(bytes32,bytes32)")); + _assertEventNotEmitted(logs, keccak256("ReservationClaimed(bytes32,address)")); + _assertEventEmittedOnce(logs, keccak256("ReservationRelinquished(bytes32,address)")); - bytes32 fullNode = _nodeOf(BASE_LABEL_B); - assertEq(dotnsPopResolver.liteLink(fullNode), keccak256(bytes(LITE_LABEL_A))); + bytes32 personhoodNode = _nodeOf(PERSONHOOD_LABEL_B); + assertEq( + dotnsPopResolver.deviceLabelhashOf(personhoodNode), keccak256(bytes(DEVICE_LABEL_A)) + ); - (bool reserved,) = dotnsPopController.isReservedForClaim(BASE_LABEL_A); + (bool reserved,) = dotnsPopController.isReservedForClaim(PERSONHOOD_LABEL_A); assertFalse(reserved); } - function test_registerBaseName_reverts_when_origin_is_not_root() public { + function test_issuePersonhoodName_reverts_when_origin_is_not_root() public { IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0xaa)); _mockOriginIsRoot(false); vm.expectRevert(IDotnsPopController.NotRoot.selector); - dotnsPopController.registerBaseName( - IDotnsPopController.FullRegistration({label: BASE_LABEL_A, user: ed, link: link}) + dotnsPopController.issuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: link + }) ); } function test_relinquishReservation_promotes_next_waiter_when_head_leaves() public { - _grantPopFull(ed); - _grantPopFull(tiago); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), BASE_LABEL_A); + _grantPersonhood(ed); + _grantPersonhood(tiago); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), PERSONHOOD_LABEL_A); vm.prank(ed); dotnsPopController.relinquishReservation(); - (bool empty,) = dotnsPopController.isReservedForClaim(BASE_LABEL_A); + (bool empty,) = dotnsPopController.isReservedForClaim(PERSONHOOD_LABEL_A); assertFalse(empty); - _reservePop(tiago, LITE_LABEL_B, _validChatKey(0x02), BASE_LABEL_A); - (bool reserved, address holder) = dotnsPopController.isReservedForClaim(BASE_LABEL_A); + _reservePop(tiago, DEVICE_LABEL_B, _validChatKey(0x02), PERSONHOOD_LABEL_A); + (bool reserved, address holder) = dotnsPopController.isReservedForClaim(PERSONHOOD_LABEL_A); assertTrue(reserved); assertEq(holder, tiago); } @@ -246,12 +268,12 @@ contract DotnsPopControllerTests is BaseDotns { } function test_setReservationDuration_shortening_retroactively_expires_live_entries() public { - _grantPopFull(ed); + _grantPersonhood(ed); // Enqueue alice under the default duration (7 days). - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), BASE_LABEL_A); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), PERSONHOOD_LABEL_A); (bool liveBefore, address holderBefore) = - dotnsPopController.isReservedForClaim(BASE_LABEL_A); + dotnsPopController.isReservedForClaim(PERSONHOOD_LABEL_A); assertTrue(liveBefore); assertEq(holderBefore, ed); // Warp 2 days forward (still well within the original 7-day window). @@ -261,7 +283,7 @@ contract DotnsPopControllerTests is BaseDotns { vm.prank(owner); dotnsPopController.setReservationDuration(1 days); - (bool liveAfter,) = dotnsPopController.isReservedForClaim(BASE_LABEL_A); + (bool liveAfter,) = dotnsPopController.isReservedForClaim(PERSONHOOD_LABEL_A); assertFalse(liveAfter); } @@ -275,34 +297,34 @@ contract DotnsPopControllerTests is BaseDotns { // Two independent reads must agree: the per-user reservation pointer // (`userReservation`) and the per-base claim view (`isReservedForClaim`). // Both change atomically when the same user re-reserves on a new stem. - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), BASE_LABEL_A); + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), PERSONHOOD_LABEL_A); IDotnsPopController.UserReservation memory firstReservation = dotnsPopController.userReservation(ed); - assertEq(firstReservation.labelhash, keccak256(bytes(BASE_LABEL_A))); + assertEq(firstReservation.labelhash, keccak256(bytes(PERSONHOOD_LABEL_A))); // Second reservation by the same user on a different stem drops the // first slot and installs the new one. - _reservePop(ed, LITE_LABEL_B, _validChatKey(0x02), BASE_LABEL_B); + _reservePop(ed, DEVICE_LABEL_B, _validChatKey(0x02), PERSONHOOD_LABEL_B); IDotnsPopController.UserReservation memory secondReservation = dotnsPopController.userReservation(ed); - assertEq(secondReservation.labelhash, keccak256(bytes(BASE_LABEL_B))); + assertEq(secondReservation.labelhash, keccak256(bytes(PERSONHOOD_LABEL_B))); - (bool firstReserved,) = dotnsPopController.isReservedForClaim(BASE_LABEL_A); + (bool firstReserved,) = dotnsPopController.isReservedForClaim(PERSONHOOD_LABEL_A); assertFalse(firstReserved); (bool secondReserved, address secondHolder) = - dotnsPopController.isReservedForClaim(BASE_LABEL_B); + dotnsPopController.isReservedForClaim(PERSONHOOD_LABEL_B); assertTrue(secondReserved); assertEq(secondHolder, ed); } function test_reEnqueue_after_own_expiry_promotes_same_user_to_head() public { - string memory baseStem = BASE_LABEL_A; - _grantPopFull(ed); + string memory baseStem = PERSONHOOD_LABEL_A; + _grantPersonhood(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), baseStem); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), baseStem); // Warp past the reservation window and fire the GC so ed's pointer gets // cleared by `_advanceExpiredHead`. If the expiry path forgets the // per-user pointer, the second reserve call below hits `AlreadyReserved` @@ -312,8 +334,8 @@ contract DotnsPopControllerTests is BaseDotns { (bool expiredReserved,) = dotnsPopController.isReservedForClaim(baseStem); assertFalse(expiredReserved); - // Same user reserves the same stem again with a fresh lite label. - _reservePop(ed, LITE_LABEL_B, _validChatKey(0x02), baseStem); + // Same user reserves the same stem again with a fresh device name. + _reservePop(ed, DEVICE_LABEL_B, _validChatKey(0x02), baseStem); (bool nowReserved, address holder) = dotnsPopController.isReservedForClaim(baseStem); assertTrue(nowReserved); @@ -321,17 +343,17 @@ contract DotnsPopControllerTests is BaseDotns { } function test_claim_then_reEnqueue_on_same_stem_resets_cleanly() public { - string memory baseStem = BASE_LABEL_A; - _grantPopFull(ed); - _grantPopFull(tiago); + string memory baseStem = PERSONHOOD_LABEL_A; + _grantPersonhood(ed); + _grantPersonhood(tiago); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), baseStem); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), baseStem); // Claim wipes the queue via `_clearQueue` which must also drop // `_reservedBaseLabel[labelhash]` and release the PopRules slot. Missing // any one of those lets the next reservation inherit stale state. - IDotnsPopController.Link memory link = _linkWithLite(LITE_LABEL_A); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: baseStem, user: ed, link: link}) + IDotnsPopController.Link memory link = _linkWithDeviceName(DEVICE_LABEL_A); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({label: baseStem, user: ed, link: link}) ); (bool oldSlot, address oldHolder) = dotnsPopController.isReservedForClaim(baseStem); @@ -339,41 +361,42 @@ contract DotnsPopControllerTests is BaseDotns { assertEq(oldHolder, address(0)); // Fresh stem, different user. If the previous queue leaked, this enqueue // would either revert or land the wrong head address on PopRules. - _reservePop(tiago, LITE_LABEL_B, _validChatKey(0x02), BASE_LABEL_B); - (bool newSlot, address newHolder) = dotnsPopController.isReservedForClaim(BASE_LABEL_B); + _reservePop(tiago, DEVICE_LABEL_B, _validChatKey(0x02), PERSONHOOD_LABEL_B); + (bool newSlot, address newHolder) = + dotnsPopController.isReservedForClaim(PERSONHOOD_LABEL_B); assertTrue(newSlot); assertEq(newHolder, tiago); - (address popHolder,) = popRules.getBaseNameReservation(BASE_LABEL_B); + (address popHolder,) = popRules.getBaseNameReservation(PERSONHOOD_LABEL_B); assertEq(popHolder, tiago); } function test_expireReservation_is_permissionless() public { - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), BASE_LABEL_A); + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), PERSONHOOD_LABEL_A); vm.warp(block.timestamp + dotnsPopController.reservationDuration() + 1); // Anyone can call. Pinning this prevents a future patch from silently // adding `onlyRoot` and breaking permissionless garbage collection. address stranger = makeAddr("stranger"); vm.prank(stranger); - dotnsPopController.expireReservation(BASE_LABEL_A); + dotnsPopController.expireReservation(PERSONHOOD_LABEL_A); - (bool reserved,) = dotnsPopController.isReservedForClaim(BASE_LABEL_A); + (bool reserved,) = dotnsPopController.isReservedForClaim(PERSONHOOD_LABEL_A); assertFalse(reserved); } function test_head_expires_clears_slot_for_next_reserver() public { - string memory baseStem = BASE_LABEL_A; - _grantPopFull(ed); - _grantPopFull(leonardo); + string memory baseStem = PERSONHOOD_LABEL_A; + _grantPersonhood(ed); + _grantPersonhood(leonardo); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), baseStem); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), baseStem); vm.warp(block.timestamp + dotnsPopController.reservationDuration() + 1); dotnsPopController.expireReservation(baseStem); - _reservePop(leonardo, LITE_LABEL_C, _validChatKey(0x03), baseStem); + _reservePop(leonardo, DEVICE_LABEL_C, _validChatKey(0x03), baseStem); (bool reserved, address holder) = dotnsPopController.isReservedForClaim(baseStem); assertTrue(reserved); @@ -384,51 +407,51 @@ contract DotnsPopControllerTests is BaseDotns { } function test_entry_point_format_rejections() public { - _grantPopFull(ed); + _grantPersonhood(ed); // The shape check runs before any pricing, so "michael" is rejected for carrying no // separator rather than for anything about its tier. - vm.expectRevert(IDotnsPopController.InvalidLiteLabel.selector); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({liteLabel: "michael", user: ed, chatKey: ""}) + vm.expectRevert(IDotnsPopController.InvalidDeviceLabel.selector); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({label: "michael", user: ed, chatKey: ""}) ); IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0xaa)); - vm.expectRevert(IDotnsPopController.InvalidBaseLabel.selector); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: "not.valid", user: ed, link: link}) + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({label: "not.valid", user: ed, link: link}) ); } - function test_same_stem_lite_and_base_occupy_distinct_registrar_tokens() public { - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0xaa), ""); - // "michael" (baselength 7, no trailing digits) classifies as PopFull - // and shares the lite's stem, so both tokens coexist on the registrar. - _grantPopFull(tiago); + function test_same_stem_device_and_personhood_names_occupy_distinct_nodes() public { + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0xaa), ""); + // "michael" (baselength 7, no trailing digits) classifies as Personhood + // and shares the device name's stem, so both tokens coexist on the registrar. + _grantPersonhood(tiago); IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0xbb)); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: "michael", user: tiago, link: link}) + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({label: "michael", user: tiago, link: link}) ); - assertEq(dotnsRegistry.owner(_liteNodeOf(LITE_LABEL_A)), ed); + assertEq(dotnsRegistry.owner(_deviceNodeOf(DEVICE_LABEL_A)), ed); assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(_nodeOf("michael"))), tiago); } function test_both_controllers_can_mint_on_shared_registrar() public { - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), ""); + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), ""); _commitAndRegister("longnamebob01", tiago, true); - assertEq(dotnsRegistry.owner(_liteNodeOf(LITE_LABEL_A)), ed); + assertEq(dotnsRegistry.owner(_deviceNodeOf(DEVICE_LABEL_A)), ed); assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(_nodeOf("longnamebob01"))), tiago); } function test_gateway_reserved_name_rejects_public_register_by_other_user() public { - _grantPopFull(tiago); + _grantPersonhood(tiago); // The gateway reserves the stem, and the stem is what the reservation blocks: a public // registrant contends for `longnamebob` itself. - _reservePop(tiago, LITE_LABEL_A, _validChatKey(0x11), "longnamebob"); + _reservePop(tiago, DEVICE_LABEL_A, _validChatKey(0x11), "longnamebob"); (address holder,) = popRules.getBaseNameReservation("longnamebob"); assertEq(holder, tiago); @@ -455,7 +478,7 @@ contract DotnsPopControllerTests is BaseDotns { vm.expectRevert( abi.encodeWithSelector( - IPopRules.PopError.selector, "Base name reserved for original Lite registrant" + IPopRules.PopError.selector, "Reserved for a device-name holder's personhood claim" ) ); vm.prank(ed); @@ -463,8 +486,8 @@ contract DotnsPopControllerTests is BaseDotns { } function test_gateway_reserved_name_allows_holder_to_register_via_public() public { - _grantPopFull(tiago); - _reservePop(tiago, LITE_LABEL_A, _validChatKey(0x11), "longnamebob"); + _grantPersonhood(tiago); + _reservePop(tiago, DEVICE_LABEL_A, _validChatKey(0x11), "longnamebob"); _commitAndRegister("longnamebob", tiago, true); assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(_nodeOf("longnamebob"))), tiago); @@ -475,50 +498,50 @@ contract DotnsPopControllerTests is BaseDotns { /// shares no stem with it and a stranger may take it. This is the guarantee that /// replaces the old cross-flow, where the two spellings collided. function test_gateway_reservation_does_not_cover_a_digit_suffixed_name() public { - _grantPopFull(tiago); - _reservePop(tiago, LITE_LABEL_A, _validChatKey(0x11), "longnamebob"); + _grantPersonhood(tiago); + _reservePop(tiago, DEVICE_LABEL_A, _validChatKey(0x11), "longnamebob"); - _grantPopFull(ed); + _grantPersonhood(ed); _commitAndRegister("longnamebob01", ed, true); assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(_nodeOf("longnamebob01"))), ed); } - function test_second_pop_lite_mint_of_same_label_reverts() public { - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0xaa), ""); + function test_second_device_name_issuance_of_same_label_reverts() public { + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0xaa), ""); - // Re-issuing a lite name is rejected at the controller before any registry write, so a + // Re-issuing a device name is rejected at the controller before any registry write, so a // duplicate dispatch cannot rehome the identity or overwrite its records. - _grantPopFull(tiago); - vm.expectRevert(IDotnsPopController.LiteNameAlreadyIssued.selector); - _rootReserveBaseName( - IDotnsPopController.BaseReservation({ - lite: IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: tiago, chatKey: _validChatKey(0xbb) + _grantPersonhood(tiago); + vm.expectRevert(IDotnsPopController.DeviceNameAlreadyIssued.selector); + _rootIssueDeviceNameWithReservation( + IDotnsPopController.DeviceNameIssuanceWithReservation({ + issuance: IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: tiago, chatKey: _validChatKey(0xbb) }), - reservedBaseLabel: "" + reservedLabel: "" }) ); } - /// @notice A lite name's owner can host a subname beneath it. - /// @dev A lite name is itself a subname the owner controls in the registry, so the owner holds - /// the parent authority @custom:function IDotnsRegistry.setSubnodeOwner requires and can - /// graft their own subnames beneath it, such as a device name `phone.michael.01`. - function test_lite_name_owner_can_host_a_subname() public { - _grantPopLite(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0xb4) + /// @notice A device name's owner can host a subname beneath it. + /// @dev A device name is itself a subname the owner controls in the registry, so the owner + /// holds the parent authority @custom:function IDotnsRegistry.setSubnodeOwner requires and + /// can graft their own subnames beneath it, such as a device name `phone.michael.01`. + function test_device_name_owner_can_host_a_subname() public { + _grantDevicehood(ed); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0xb4) }) ); - // A lite name is a subname the owner controls, so they can host their own subnames beneath - // it, such as a device name `phone.michael.01`. + // A device name is a subname the owner controls, so they can host their own subnames + // beneath it, such as a device name `phone.michael.01`. IDotnsRegistry.SubnodeRecord memory subnodeRecord = IDotnsRegistry.SubnodeRecord({ - parentNode: _liteNodeOf(LITE_LABEL_A), + parentNode: _deviceNodeOf(DEVICE_LABEL_A), subLabel: "phone", - parentLabel: LITE_LABEL_A, + parentLabel: DEVICE_LABEL_A, owner: ed, persist: true }); @@ -534,10 +557,10 @@ contract DotnsPopControllerTests is BaseDotns { /// inside the same call, so the assertion that it still reads false is what proves the /// failed mint took the provenance write with it. A public name that answered /// `isPopIssued` would pass for a person. - function test_pop_full_mint_after_public_register_reverts_and_writes_no_provenance() public { + function test_personhood_mint_after_public_register_reverts_and_writes_no_provenance() public { string memory label = "longnamebobx"; - _grantPopFull(tiago); + _grantPersonhood(tiago); _commitAndRegister(label, tiago, true); IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0xd1)); @@ -546,20 +569,22 @@ contract DotnsPopControllerTests is BaseDotns { IDotnsRegistrar.NameNotAvailable.selector, uint256(_nodeOf(label)) ) ); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: label, user: ed, link: link}) + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({label: label, user: ed, link: link}) ); assertFalse(dotnsPopController.isPopIssued(label), "provenance survived a failed mint"); assertFalse(dotnsRegistrar.isSoulbound(uint256(_nodeOf(label))), "public name locked"); } - function test_public_register_after_pop_full_mint_reverts_at_registrar() public { - // "longnamebobx" is classification-NoStatus, so ed keeps default status. A full-person + function test_public_register_after_personhood_mint_reverts_at_registrar() public { + // "longnamebobx" is classification-NoStatus, so ed keeps default status. A personhood // label carries no digit suffix. IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0xcf)); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: "longnamebobx", user: ed, link: link}) + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: "longnamebobx", user: ed, link: link + }) ); assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(_nodeOf("longnamebobx"))), ed); @@ -591,17 +616,19 @@ contract DotnsPopControllerTests is BaseDotns { } function test_owner_of_pop_minted_name_can_create_subname() public { - _grantPopFull(ed); + _grantPersonhood(ed); IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0xcf)); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: BASE_LABEL_A, user: ed, link: link}) + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: link + }) ); - bytes32 parentNode = _nodeOf(BASE_LABEL_A); + bytes32 parentNode = _nodeOf(PERSONHOOD_LABEL_A); IDotnsRegistry.SubnodeRecord memory subnodeRecord = IDotnsRegistry.SubnodeRecord({ parentNode: parentNode, subLabel: "app", - parentLabel: BASE_LABEL_A, + parentLabel: PERSONHOOD_LABEL_A, owner: leonardo, persist: true }); @@ -613,17 +640,19 @@ contract DotnsPopControllerTests is BaseDotns { } function test_non_owner_cannot_create_subname_under_pop_minted_name() public { - _grantPopFull(ed); + _grantPersonhood(ed); IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0xcf)); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: BASE_LABEL_A, user: ed, link: link}) + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: link + }) ); - bytes32 parentNode = _nodeOf(BASE_LABEL_A); + bytes32 parentNode = _nodeOf(PERSONHOOD_LABEL_A); IDotnsRegistry.SubnodeRecord memory subnodeRecord = IDotnsRegistry.SubnodeRecord({ parentNode: parentNode, subLabel: "app", - parentLabel: BASE_LABEL_A, + parentLabel: PERSONHOOD_LABEL_A, owner: tiago, persist: true }); @@ -637,16 +666,16 @@ contract DotnsPopControllerTests is BaseDotns { // A name minted through the public commit-reveal flow already has an owner, so a PoP // reservation over it could never be redeemed. The guard rejects it at reserve time // rather than admitting it and only failing at claim, which would have locked every - // lite name built on that stem for the full reservation window. + // device name built on that stem for the full reservation window. _commitAndRegister("longnamebob", ed, true); - _grantPopFull(tiago); - vm.expectRevert(IDotnsPopController.BaseNameAlreadyRegistered.selector); - _reservePop(tiago, LITE_LABEL_A, _validChatKey(0xaa), "longnamebob"); + _grantPersonhood(tiago); + vm.expectRevert(IDotnsPopController.PersonhoodNameUnavailable.selector); + _reservePop(tiago, DEVICE_LABEL_A, _validChatKey(0xaa), "longnamebob"); - // The guard runs before the lite mint, so the whole call aborts and no lite name is - // minted for the candidate. - assertFalse(dotnsRegistrar.exists(uint256(_nodeOf(LITE_LABEL_A)))); + // The guard runs before the device-name mint, so the whole call aborts and no device name + // is minted for the candidate. + assertFalse(dotnsRegistrar.exists(uint256(_nodeOf(DEVICE_LABEL_A)))); // No reservation was recorded, so the stem family stays open to other candidates. (bool reserved,) = dotnsPopController.isReservedForClaim("longnamebob"); @@ -654,8 +683,8 @@ contract DotnsPopControllerTests is BaseDotns { } function test_enqueue_becomesHead_writes_popRules_reservation() public { - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0xaa), "longnamebob"); + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0xaa), "longnamebob"); (address holder, uint64 expires) = popRules.getBaseNameReservation("longnamebob"); assertEq(holder, ed); @@ -663,12 +692,12 @@ contract DotnsPopControllerTests is BaseDotns { } function test_claim_releases_popRules_slot() public { - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0xaa), "longnamebob"); + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0xaa), "longnamebob"); - IDotnsPopController.Link memory link = _linkWithLite(LITE_LABEL_A); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: "longnamebob", user: ed, link: link}) + IDotnsPopController.Link memory link = _linkWithDeviceName(DEVICE_LABEL_A); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({label: "longnamebob", user: ed, link: link}) ); (address holder,) = popRules.getBaseNameReservation("longnamebob"); @@ -676,8 +705,8 @@ contract DotnsPopControllerTests is BaseDotns { } function test_relinquish_last_releases_popRules_slot() public { - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0xaa), "longnamebob"); + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0xaa), "longnamebob"); vm.prank(ed); dotnsPopController.relinquishReservation(); @@ -687,8 +716,8 @@ contract DotnsPopControllerTests is BaseDotns { } function test_advanceExpiredHead_last_expire_releases_popRules_slot() public { - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0xaa), "longnamebob"); + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0xaa), "longnamebob"); vm.warp(block.timestamp + dotnsPopController.reservationDuration() + 1); dotnsPopController.expireReservation("longnamebob"); @@ -697,44 +726,47 @@ contract DotnsPopControllerTests is BaseDotns { assertEq(holder, address(0)); } - function test_reserveBaseName_reverts_for_digit_suffixed_reserved_base_label() public { - _grantPopFull(ed); - vm.expectRevert(IDotnsPopController.InvalidBaseLabel.selector); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0xaa), "longnamebob01"); + function test_issueDeviceNameWithReservation_reverts_for_digit_suffixed_reserved_label() + public + { + _grantPersonhood(ed); + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0xaa), "longnamebob01"); } - function test_reserveBaseName_reverts_when_reserved_label_already_registered() public { - // George worked example: ed reserves and then claims the base name, which frees the - // stem slot on PopRules. A later lite candidate must not be able to queue a reservation - // over the now-registered name: the queue keys by stem, so an unclaimable reservation - // would hold that stem for the full reservation window and block every lite name built - // on it. - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0xaa), "longnamebob"); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({ - label: "longnamebob", user: ed, link: _linkWithLite(LITE_LABEL_A) + function test_issueDeviceNameWithReservation_reverts_when_reserved_label_already_registered() + public + { + // George worked example: ed reserves and then claims the base name, which frees the stem + // slot on PopRules. A later device-name candidate must not be able to queue a reservation + // over the now-registered name: the queue keys by stem, so an unclaimable reservation would + // hold that stem for the full reservation window and block every device name built on it. + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0xaa), "longnamebob"); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: "longnamebob", user: ed, link: _linkWithDeviceName(DEVICE_LABEL_A) }) ); - _grantPopFull(tiago); - vm.expectRevert(IDotnsPopController.BaseNameAlreadyRegistered.selector); - _reservePop(tiago, LITE_LABEL_B, _validChatKey(0xbb), "longnamebob"); + _grantPersonhood(tiago); + vm.expectRevert(IDotnsPopController.PersonhoodNameUnavailable.selector); + _reservePop(tiago, DEVICE_LABEL_B, _validChatKey(0xbb), "longnamebob"); } /// @dev Claiming the stem takes the name, so nothing is left for a stranger to register. /// What the claim releases is the reservation slot on PopRules, which is what this /// asserts; a digit-suffixed spelling would prove nothing, being an unrelated name. function test_claim_clears_the_reservation_slot() public { - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0xaa), "longnamebob"); + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0xaa), "longnamebob"); (address beforeClaim,) = popRules.getBaseNameReservation("longnamebob"); assertEq(beforeClaim, ed, "reserved for the claimant"); - IDotnsPopController.Link memory link = _linkWithLite(LITE_LABEL_A); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: "longnamebob", user: ed, link: link}) + IDotnsPopController.Link memory link = _linkWithDeviceName(DEVICE_LABEL_A); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({label: "longnamebob", user: ed, link: link}) ); (address afterClaim,) = popRules.getBaseNameReservation("longnamebob"); @@ -743,13 +775,13 @@ contract DotnsPopControllerTests is BaseDotns { } function test_public_stranger_can_mint_after_reservation_expires() public { - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0xaa), "longnamebob"); + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0xaa), "longnamebob"); vm.warp(block.timestamp + dotnsPopController.reservationDuration() + 1); dotnsPopController.expireReservation("longnamebob"); - _grantPopFull(tiago); + _grantPersonhood(tiago); _commitAndRegister("longnamebob", tiago, true); assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(_nodeOf("longnamebob"))), tiago); } @@ -760,12 +792,12 @@ contract DotnsPopControllerTests is BaseDotns { _mockOriginIsRoot(false); vm.prank(address(dotnsRegistrarController)); vm.expectRevert(IDotnsPopController.NotRoot.selector); - dotnsPopController.reserveBaseName( - IDotnsPopController.BaseReservation({ - lite: IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: "" + dotnsPopController.issueDeviceNameWithReservation( + IDotnsPopController.DeviceNameIssuanceWithReservation({ + issuance: IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: "" }), - reservedBaseLabel: "longnamebob" + reservedLabel: "longnamebob" }) ); } @@ -806,259 +838,270 @@ contract DotnsPopControllerTests is BaseDotns { assertFalse(reservedAfter); } - function test_registerBaseName_standalone_succeeds_when_head_is_expired() public { - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), BASE_LABEL_A); + function test_issuePersonhoodName_standalone_succeeds_when_head_is_expired() public { + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), PERSONHOOD_LABEL_A); // Walk past both the controller and PopRules expiry windows so // nothing blocks tiago's priceWithCheck. vm.warp(block.timestamp + popRules.MAX_RESERVATION_TIME() + 1); - _grantPopFull(tiago); + _grantPersonhood(tiago); IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0xcf)); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: BASE_LABEL_A, user: tiago, link: link}) + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: tiago, link: link + }) ); - assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(_nodeOf(BASE_LABEL_A))), tiago); + assertEq( + IERC721(address(dotnsRegistrar)).ownerOf(uint256(_nodeOf(PERSONHOOD_LABEL_A))), tiago + ); } - function test_registerBaseName_standalone_succeeds_when_queue_empty() public { - _grantPopFull(ed); + function test_issuePersonhoodName_standalone_succeeds_when_queue_empty() public { + _grantPersonhood(ed); IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0xcf)); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: BASE_LABEL_A, user: ed, link: link}) + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: link + }) ); - assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(_nodeOf(BASE_LABEL_A))), ed); + assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(_nodeOf(PERSONHOOD_LABEL_A))), ed); } - function test_registerBaseName_claim_path_bypasses_standalone_holder_guard() public { - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), BASE_LABEL_A); + function test_issuePersonhoodName_claim_path_bypasses_standalone_holder_guard() public { + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), PERSONHOOD_LABEL_A); - IDotnsPopController.Link memory link = _linkWithLite(LITE_LABEL_A); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: BASE_LABEL_A, user: ed, link: link}) + IDotnsPopController.Link memory link = _linkWithDeviceName(DEVICE_LABEL_A); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: link + }) ); - assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(_nodeOf(BASE_LABEL_A))), ed); + assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(_nodeOf(PERSONHOOD_LABEL_A))), ed); } - function test_registerBaseName_guard_blocks_stranger_and_preserves_claim() public { - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), BASE_LABEL_A); + function test_issuePersonhoodName_guard_blocks_stranger_and_preserves_claim() public { + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), PERSONHOOD_LABEL_A); - _grantPopFull(tiago); + _grantPersonhood(tiago); IDotnsPopController.Link memory strangerLink = _linkFresh(_validChatKey(0xbb)); vm.expectPartialRevert(IDotnsPopController.NotHolder.selector); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({ - label: BASE_LABEL_A, user: tiago, link: strangerLink + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: tiago, link: strangerLink }) ); // A's reservation is intact; A claims successfully. - IDotnsPopController.Link memory claimLink = _linkWithLite(LITE_LABEL_A); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: BASE_LABEL_A, user: ed, link: claimLink}) + IDotnsPopController.Link memory claimLink = _linkWithDeviceName(DEVICE_LABEL_A); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: claimLink + }) ); - assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(_nodeOf(BASE_LABEL_A))), ed); + assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(_nodeOf(PERSONHOOD_LABEL_A))), ed); } - function test_registerBaseName_reverts_for_governance_length_name() public { - _grantPopFull(ed); + function test_issuePersonhoodName_reverts_for_governance_length_name() public { + _grantPersonhood(ed); IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0xaa)); // Stem "alice" has baselength 5; classifies as `Reserved for Governance`, // which the PoP controller's governance guard rejects. - vm.expectRevert(IDotnsPopController.InvalidBaseLabel.selector); + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: "alice", user: ed, link: link}) + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({label: "alice", user: ed, link: link}) ); } - function test_reserveBaseName_reserved_label_classification_reverts() public { - _grantPopFull(ed); + function test_issueDeviceNameWithReservation_reserved_label_classification_reverts() public { + _grantPersonhood(ed); - // Lite leg uses a valid lite label; reserved leg uses a <=5-char stem, + // The issuance uses a valid device name; reserved leg uses a <=5-char stem, // which classifies as `Reserved for Governance` and is rejected by the // PoP controller's governance guard. - vm.expectRevert(IDotnsPopController.InvalidBaseLabel.selector); - _rootReserveBaseName( - IDotnsPopController.BaseReservation({ - lite: IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); + _rootIssueDeviceNameWithReservation( + IDotnsPopController.DeviceNameIssuanceWithReservation({ + issuance: IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) }), - reservedBaseLabel: "alice" + reservedLabel: "alice" }) ); } - function test_registerBaseName_popFull_user_on_popFull_label_succeeds() public { - _grantPopFull(ed); + function test_issuePersonhoodName_personhood_user_on_personhood_label_succeeds() public { + _grantPersonhood(ed); uint256 controllerBalanceBefore = address(dotnsPopController).balance; IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0xaa)); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: BASE_LABEL_A, user: ed, link: link}) + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: link + }) ); // No native token moves on the PoP path. assertEq(address(dotnsPopController).balance, controllerBalanceBefore); - assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(_nodeOf(BASE_LABEL_A))), ed); + assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(_nodeOf(PERSONHOOD_LABEL_A))), ed); } - function test_reserveLiteName_succeeds_regardless_of_base_reservation() public { - string memory baseStem = BASE_LABEL_A; + function test_issueDeviceName_succeeds_regardless_of_stem_reservation() public { + string memory baseStem = PERSONHOOD_LABEL_A; // Occupy the base stem with a live reservation. - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), baseStem); - // A different user calls reserveLiteName on a different lite label. - address fresh = makeAddr("freshLite"); - _grantPopLite(fresh); - - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: "stephen.01", user: fresh, chatKey: _validChatKey(0xcc) + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), baseStem); + // A different user calls issueDeviceName on a different device name. + address fresh = makeAddr("freshDevice"); + _grantDevicehood(fresh); + + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: "stephen.01", user: fresh, chatKey: _validChatKey(0xcc) }) ); - assertEq(dotnsRegistry.owner(_liteNodeOf("stephen.01")), fresh); + assertEq(dotnsRegistry.owner(_deviceNodeOf("stephen.01")), fresh); } - function test_reserveLiteName_reverts_for_non_lite_format() public { - _grantPopFull(ed); + function test_issueDeviceName_reverts_for_non_device_format() public { + _grantPersonhood(ed); - vm.expectRevert(IDotnsPopController.InvalidLiteLabel.selector); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: "alice", user: ed, chatKey: _validChatKey(0xaa) + vm.expectRevert(IDotnsPopController.InvalidDeviceLabel.selector); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: "alice", user: ed, chatKey: _validChatKey(0xaa) }) ); } - function test_reserveLiteName_reverts_when_suffix_is_not_exactly_two_digits() public { - _grantPopFull(ed); + function test_issueDeviceName_reverts_when_suffix_is_not_exactly_two_digits() public { + _grantPersonhood(ed); - vm.expectRevert(IDotnsPopController.InvalidLiteLabel.selector); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: "michael.001", user: ed, chatKey: _validChatKey(0xaa) + vm.expectRevert(IDotnsPopController.InvalidDeviceLabel.selector); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: "michael.001", user: ed, chatKey: _validChatKey(0xaa) }) ); } - function test_reserveLiteName_reverts_when_the_stem_is_governance_reserved() public { - _grantPopFull(ed); + function test_issueDeviceName_reverts_when_the_stem_is_governance_reserved() public { + _grantPersonhood(ed); - // `abcd.12` is a well-formed lite label whose four-letter stem classifies as Reserved, + // `abcd.12` is a well-formed device name whose four-letter stem classifies as Reserved, // so classification is what rejects it rather than the shape. - vm.expectRevert(IDotnsPopController.InvalidLiteLabel.selector); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: "abcd.12", user: ed, chatKey: _validChatKey(0xaa) + vm.expectRevert(IDotnsPopController.InvalidDeviceLabel.selector); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: "abcd.12", user: ed, chatKey: _validChatKey(0xaa) }) ); } - function test_reserveLiteName_succeeds_for_long_stem() public { - _grantPopLite(ed); + function test_issueDeviceName_succeeds_for_long_stem() public { + _grantDevicehood(ed); // `andrewsays.01` has a stem of 10, which classifies as NoStatus. The gateway may issue - // it as a lite username regardless of stem length. - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: "andrewsays.01", user: ed, chatKey: _validChatKey(0xaa) + // it as a device name regardless of stem length. + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: "andrewsays.01", user: ed, chatKey: _validChatKey(0xaa) }) ); - assertEq(dotnsRegistry.owner(_liteNodeOf("andrewsays.01")), ed); + assertEq(dotnsRegistry.owner(_deviceNodeOf("andrewsays.01")), ed); } - /// @notice A public registration is not an identity and appears in neither listing. - /// @dev Both listings require provenance, so a name the gateway never minted is absent from - /// both however it is spelled. Without that, `joseph42` would read as a full-person - /// identity purely because it is a single label. - function test_lens_omits_a_public_registration_from_both_listings() public { - _grantPopFull(ed); + /// @notice A public registration is not an identity and is not listed. + /// @dev The listing requires provenance, so a name the gateway never minted is absent however + /// it is spelled. Without that, `joseph42` would read as a personhood name purely because + /// it is a single label. + function test_lens_omits_a_public_registration() public { + _grantPersonhood(ed); _commitAndRegister("joseph42", ed, true); assertFalse(dotnsPopController.isPopIssued("joseph42"), "the gateway did not mint it"); - assertEq(dotnsPopLens.fullNamesOf(ed, 0, 10).length, 0, "not a full-person identity"); - assertEq(dotnsPopLens.liteNamesOf(ed, 0, 10).length, 0, "nor a lite one"); - assertEq(dotnsPopLens.fullNameCountOf(ed), 0); - assertEq(dotnsPopLens.liteNameCountOf(ed), 0); + assertEq(dotnsPopLens.namesOf(ed, 0, 10).length, 0, "not an identity"); + assertEq(dotnsPopLens.nameCountOf(ed), 0); } - /// @notice A full-person gateway name lists as full, not lite. - /// @dev `isPopIssued` covers every name the controller mints, lite and full alike, so it - /// cannot say which kind a name is. The separator does that. Keying the lite listing on - /// provenance alone would move every full-person identity into it. - function test_lens_lists_a_full_person_name_as_full() public { - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), ""); + /// @notice Device names and personhood names come back in one listing. + /// @dev `isPopIssued` covers every name the controller mints, of both kinds, and the label + /// shape tells them apart: a device name carries its separator. + function test_lens_lists_device_and_personhood_names_together() public { + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), ""); - IDotnsPopController.Link memory link = _linkWithLite(LITE_LABEL_A); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: BASE_LABEL_A, user: ed, link: link}) + IDotnsPopController.Link memory link = _linkWithDeviceName(DEVICE_LABEL_A); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: link + }) ); - assertTrue(dotnsPopController.isPopIssued(BASE_LABEL_A), "both kinds carry provenance"); - assertTrue(dotnsPopController.isPopIssued(LITE_LABEL_A), "both kinds carry provenance"); - - IDotnsPopLens.Name[] memory full = dotnsPopLens.fullNamesOf(ed, 0, 10); - assertEq(full.length, 1, "the full-person name is in the full listing"); - assertEq(full[0].label, BASE_LABEL_A); + assertTrue( + dotnsPopController.isPopIssued(PERSONHOOD_LABEL_A), "both kinds carry provenance" + ); + assertTrue(dotnsPopController.isPopIssued(DEVICE_LABEL_A), "both kinds carry provenance"); - IDotnsPopLens.Name[] memory lite = dotnsPopLens.liteNamesOf(ed, 0, 10); - assertEq(lite.length, 1, "and the lite name is in the lite listing"); - assertEq(lite[0].label, LITE_LABEL_A); + IDotnsPopLens.Name[] memory names = dotnsPopLens.namesOf(ed, 0, 10); + assertEq(names.length, 2, "both names are listed"); + assertTrue(_namesContainNode(names, _nodeOf(PERSONHOOD_LABEL_A)), "the personhood name"); + assertTrue(_namesContainNode(names, _deviceNodeOf(DEVICE_LABEL_A)), "and the device name"); + assertEq(dotnsPopLens.nameCountOf(ed), 2); } - /// @notice A full-person name is letters only, the same rule a lite stem follows. + /// @notice A personhood name is letters only, the same rule a device-name stem follows. /// @dev The hyphen and interior-digit cases are the ones a trailing-digit check misses: /// `alice-bob` and `micha3l` are valid DNS labels and would otherwise be issued as - /// identities, while being impossible as lite stems. All three revert with this + /// identities, while being impossible as device-name stems. All three revert with this /// interface's own error. - function test_registerBaseName_rejects_a_label_that_is_not_letters_only() public { + function test_issuePersonhoodName_rejects_a_label_that_is_not_letters_only() public { IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0xa1)); - vm.expectRevert(IDotnsPopController.InvalidBaseLabel.selector); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: "alice-bob", user: ed, link: link}) + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({label: "alice-bob", user: ed, link: link}) ); - vm.expectRevert(IDotnsPopController.InvalidBaseLabel.selector); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: "micha3l", user: ed, link: link}) + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({label: "micha3l", user: ed, link: link}) ); - vm.expectRevert(IDotnsPopController.InvalidBaseLabel.selector); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: "Joseph", user: ed, link: link}) + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({label: "Joseph", user: ed, link: link}) ); } - /// @dev The reservation entrypoints share `_validateBaseLabel`, so a hyphen is rejected + /// @dev The reservation entrypoints share `_validatePersonhoodLabel`, so a hyphen is rejected /// there too rather than resolving to an empty queue. function test_reservation_entrypoints_reject_a_label_that_is_not_letters_only() public { - _grantPopFull(ed); + _grantPersonhood(ed); - vm.expectRevert(IDotnsPopController.InvalidBaseLabel.selector); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0xa2), "alice-bob"); + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0xa2), "alice-bob"); - vm.expectRevert(IDotnsPopController.InvalidBaseLabel.selector); + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); dotnsPopController.expireReservation("alice-bob"); - vm.expectRevert(IDotnsPopController.InvalidBaseLabel.selector); + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); dotnsPopController.isReservedForClaim("micha3l"); - vm.expectRevert(IDotnsPopController.InvalidBaseLabel.selector); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0xa3), "Joseph"); + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0xa3), "Joseph"); } /// @notice The controller answers its own ERC-165 id and nothing else. @@ -1074,16 +1117,16 @@ contract DotnsPopControllerTests is BaseDotns { /// @dev The node is the hash of the whole string, so it is not the node a subname path /// would produce for the same text. Pinning both is what makes "keep the dot" /// concrete rather than cosmetic. - function test_reserveLiteName_stores_the_label_with_its_separator() public { - _grantPopLite(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: "michael.01", user: ed, chatKey: _validChatKey(0xaa) + function test_issueDeviceName_stores_the_label_with_its_separator() public { + _grantDevicehood(ed); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: "michael.01", user: ed, chatKey: _validChatKey(0xaa) }) ); // The name is a subname under its numeric container, owned in the registry record. - bytes32 subnamePathNode = _liteNodeOf("michael.01"); + bytes32 subnamePathNode = _deviceNodeOf("michael.01"); assertEq(dotnsRegistry.owner(subnamePathNode), ed); // The whole-label reading is a different node and is never minted as a token. @@ -1093,12 +1136,12 @@ contract DotnsPopControllerTests is BaseDotns { } function test_isPopIssued_is_set_at_mint() public { - _grantPopLite(ed); + _grantDevicehood(ed); assertFalse(dotnsPopController.isPopIssued("michael.01"), "not issued before the mint"); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: "michael.01", user: ed, chatKey: _validChatKey(0xaa) + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: "michael.01", user: ed, chatKey: _validChatKey(0xaa) }) ); @@ -1117,11 +1160,11 @@ contract DotnsPopControllerTests is BaseDotns { /// name stashed as a pending claim must already answer true. function test_isPopIssued_holds_across_cold_path_settlement() public { address cold = makeAddr("coldClaimant"); - _grantPopLite(cold); + _grantDevicehood(cold); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: "william.03", user: cold, chatKey: _validChatKey(0xbb) + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: "william.03", user: cold, chatKey: _validChatKey(0xbb) }) ); @@ -1134,134 +1177,139 @@ contract DotnsPopControllerTests is BaseDotns { assertTrue(dotnsPopController.isPopIssued("william.03"), "still true once settled"); } - /// @dev Provenance covers every name this controller mints, not only the lite ones, because - /// it records who issued the name rather than which tier it sits in. - function test_isPopIssued_covers_full_person_names() public { - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), ""); + /// @dev Provenance covers every name this controller mints, not only the device-name ones, + /// because it records who issued the name rather than which tier it sits in. + function test_isPopIssued_covers_personhood_names() public { + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), ""); - IDotnsPopController.Link memory link = _linkWithLite(LITE_LABEL_A); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: BASE_LABEL_A, user: ed, link: link}) + IDotnsPopController.Link memory link = _linkWithDeviceName(DEVICE_LABEL_A); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: link + }) ); - assertTrue(dotnsPopController.isPopIssued(BASE_LABEL_A)); + assertTrue(dotnsPopController.isPopIssued(PERSONHOOD_LABEL_A)); } - function test_reserveLiteName_reverts_when_origin_is_not_root() public { + function test_issueDeviceName_reverts_when_origin_is_not_root() public { _mockOriginIsRoot(false); vm.expectRevert(IDotnsPopController.NotRoot.selector); - dotnsPopController.reserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) + dotnsPopController.issueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) }) ); } - function test_reserveBaseName_lite_and_base_legs_both_succeed_in_one_call() public { - _grantPopFull(ed); + function test_issueDeviceNameWithReservation_issuance_and_reservation_both_succeed_in_one_call() + public + { + _grantPersonhood(ed); - _rootReserveBaseName( - IDotnsPopController.BaseReservation({ - lite: IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) + _rootIssueDeviceNameWithReservation( + IDotnsPopController.DeviceNameIssuanceWithReservation({ + issuance: IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) }), - reservedBaseLabel: BASE_LABEL_A + reservedLabel: PERSONHOOD_LABEL_A }) ); - assertEq(dotnsRegistry.owner(_liteNodeOf(LITE_LABEL_A)), ed); + assertEq(dotnsRegistry.owner(_deviceNodeOf(DEVICE_LABEL_A)), ed); - (bool reserved, address holder) = dotnsPopController.isReservedForClaim(BASE_LABEL_A); + (bool reserved, address holder) = dotnsPopController.isReservedForClaim(PERSONHOOD_LABEL_A); assertTrue(reserved); assertEq(holder, ed); } - function test_split_gateway_flow_mints_lite_then_reserves_base() public { - _grantPopFull(ed); + function test_split_gateway_flow_issues_device_name_then_reserves_personhood_name() public { + _grantPersonhood(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) }) ); - assertEq(dotnsRegistry.owner(_liteNodeOf(LITE_LABEL_A)), ed); - assertFalse(dotnsRegistrar.exists(uint256(_nodeOf(BASE_LABEL_A)))); + assertEq(dotnsRegistry.owner(_deviceNodeOf(DEVICE_LABEL_A)), ed); + assertFalse(dotnsRegistrar.exists(uint256(_nodeOf(PERSONHOOD_LABEL_A)))); - _rootReserveBaseNameOnly( - IDotnsPopController.BaseNameReservation({user: ed, reservedBaseLabel: BASE_LABEL_A}) + _rootReservePersonhoodName( + IDotnsPopController.PersonhoodNameReservation({user: ed, label: PERSONHOOD_LABEL_A}) ); - (bool reserved, address holder) = dotnsPopController.isReservedForClaim(BASE_LABEL_A); + (bool reserved, address holder) = dotnsPopController.isReservedForClaim(PERSONHOOD_LABEL_A); assertTrue(reserved); assertEq(holder, ed); - assertFalse(dotnsRegistrar.exists(uint256(_nodeOf(BASE_LABEL_A)))); + assertFalse(dotnsRegistrar.exists(uint256(_nodeOf(PERSONHOOD_LABEL_A)))); } - function test_reserveBaseNameOnly_reverts_when_origin_is_not_root() public { + function test_reservePersonhoodName_reverts_when_origin_is_not_root() public { _mockOriginIsRoot(false); vm.expectRevert(IDotnsPopController.NotRoot.selector); - dotnsPopController.reserveBaseNameOnly( - IDotnsPopController.BaseNameReservation({user: ed, reservedBaseLabel: BASE_LABEL_A}) + dotnsPopController.reservePersonhoodName( + IDotnsPopController.PersonhoodNameReservation({user: ed, label: PERSONHOOD_LABEL_A}) ); } - function test_reserveBaseNameOnly_reverts_for_reserved_or_suffixed_labels() public { - vm.expectRevert(IDotnsPopController.InvalidBaseLabel.selector); - _rootReserveBaseNameOnly( - IDotnsPopController.BaseNameReservation({user: ed, reservedBaseLabel: "alice"}) + function test_reservePersonhoodName_reverts_for_reserved_or_suffixed_labels() public { + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); + _rootReservePersonhoodName( + IDotnsPopController.PersonhoodNameReservation({user: ed, label: "alice"}) ); - vm.expectRevert(IDotnsPopController.InvalidBaseLabel.selector); - _rootReserveBaseNameOnly( - IDotnsPopController.BaseNameReservation({user: ed, reservedBaseLabel: "longnamebob01"}) + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); + _rootReservePersonhoodName( + IDotnsPopController.PersonhoodNameReservation({user: ed, label: "longnamebob01"}) ); } - function test_reserveBaseNameOnly_reverts_when_label_already_registered() public { + function test_reservePersonhoodName_reverts_when_label_already_registered() public { // The standalone reservation entrypoint shares the same guard: a base name that already // has an owner on the registrar can never be redeemed, so the reservation is rejected up // front rather than discovered to be unusable at claim time. - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0xaa), "longnamebob"); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({ - label: "longnamebob", user: ed, link: _linkWithLite(LITE_LABEL_A) + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0xaa), "longnamebob"); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: "longnamebob", user: ed, link: _linkWithDeviceName(DEVICE_LABEL_A) }) ); - vm.expectRevert(IDotnsPopController.BaseNameAlreadyRegistered.selector); - _rootReserveBaseNameOnly( - IDotnsPopController.BaseNameReservation({user: tiago, reservedBaseLabel: "longnamebob"}) + vm.expectRevert(IDotnsPopController.PersonhoodNameUnavailable.selector); + _rootReservePersonhoodName( + IDotnsPopController.PersonhoodNameReservation({user: tiago, label: "longnamebob"}) ); } - function test_reserveBaseNameOnly_does_not_mint_lite_or_base_name() public { - _rootReserveBaseNameOnly( - IDotnsPopController.BaseNameReservation({user: ed, reservedBaseLabel: BASE_LABEL_A}) + function test_reservePersonhoodName_does_not_issue_a_name() public { + _rootReservePersonhoodName( + IDotnsPopController.PersonhoodNameReservation({user: ed, label: PERSONHOOD_LABEL_A}) ); - assertFalse(dotnsRegistrar.exists(uint256(_nodeOf(LITE_LABEL_A)))); - assertFalse(dotnsRegistrar.exists(uint256(_nodeOf(BASE_LABEL_A)))); + assertFalse(dotnsRegistrar.exists(uint256(_nodeOf(DEVICE_LABEL_A)))); + assertFalse(dotnsRegistrar.exists(uint256(_nodeOf(PERSONHOOD_LABEL_A)))); - (bool reserved, address holder) = dotnsPopController.isReservedForClaim(BASE_LABEL_A); + (bool reserved, address holder) = dotnsPopController.isReservedForClaim(PERSONHOOD_LABEL_A); assertTrue(reserved); assertEq(holder, ed); } - function test_reserveBaseNameOnly_same_user_can_replace_prior_reservation() public { - _rootReserveBaseNameOnly( - IDotnsPopController.BaseNameReservation({user: ed, reservedBaseLabel: BASE_LABEL_A}) + function test_reservePersonhoodName_same_user_can_replace_prior_reservation() public { + _rootReservePersonhoodName( + IDotnsPopController.PersonhoodNameReservation({user: ed, label: PERSONHOOD_LABEL_A}) ); - _rootReserveBaseNameOnly( - IDotnsPopController.BaseNameReservation({user: ed, reservedBaseLabel: BASE_LABEL_B}) + _rootReservePersonhoodName( + IDotnsPopController.PersonhoodNameReservation({user: ed, label: PERSONHOOD_LABEL_B}) ); - (bool firstReserved,) = dotnsPopController.isReservedForClaim(BASE_LABEL_A); + (bool firstReserved,) = dotnsPopController.isReservedForClaim(PERSONHOOD_LABEL_A); assertFalse(firstReserved); - (bool secondReserved, address holder) = dotnsPopController.isReservedForClaim(BASE_LABEL_B); + (bool secondReserved, address holder) = + dotnsPopController.isReservedForClaim(PERSONHOOD_LABEL_B); assertTrue(secondReserved); assertEq(holder, ed); } @@ -1271,14 +1319,14 @@ contract DotnsPopControllerTests is BaseDotns { // gateway can settle a user's pending claim, deploying the user's store and writing the // stashed label. The settled name lands in the beneficiary's store, and the settlement // event records the third party as the settler. - _grantPopFull(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) + _grantPersonhood(ed); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) }) ); - bytes32 labelhash = keccak256(bytes(LITE_LABEL_A)); + bytes32 labelhash = keccak256(bytes(DEVICE_LABEL_A)); address expectedStore = vm.computeCreateAddress(address(storeFactory), vm.getNonce(address(storeFactory))); @@ -1290,17 +1338,17 @@ contract DotnsPopControllerTests is BaseDotns { address store = storeFactory.getLabelStore(ed); assertEq(store, expectedStore); assertEq( - ILabelStore(store).getLabel(_liteNodeOf(LITE_LABEL_A)), - string.concat(LITE_LABEL_A, ".dot") + ILabelStore(store).getLabel(_deviceNodeOf(DEVICE_LABEL_A)), + string.concat(DEVICE_LABEL_A, ".dot") ); assertEq(dotnsPopController.pendingClaimCountOf(ed), 0); } function test_user_settles_own_pending_claim_after_gateway_mint() public { - _grantPopFull(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) + _grantPersonhood(ed); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) }) ); @@ -1310,17 +1358,17 @@ contract DotnsPopControllerTests is BaseDotns { address store = storeFactory.getLabelStore(ed); assertTrue(store != address(0)); assertEq( - ILabelStore(store).getLabel(_liteNodeOf(LITE_LABEL_A)), - string.concat(LITE_LABEL_A, ".dot") + ILabelStore(store).getLabel(_deviceNodeOf(DEVICE_LABEL_A)), + string.concat(DEVICE_LABEL_A, ".dot") ); assertEq(dotnsPopController.pendingClaimCountOf(ed), 0); } function test_claimLabelStore_settles_callers_own_pending_claim() public { - _grantPopFull(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) + _grantPersonhood(ed); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) }) ); @@ -1331,22 +1379,22 @@ contract DotnsPopControllerTests is BaseDotns { address store = storeFactory.getLabelStore(ed); assertTrue(store != address(0)); assertEq( - ILabelStore(store).getLabel(_liteNodeOf(LITE_LABEL_A)), - string.concat(LITE_LABEL_A, protocolRegistry.tld()) + ILabelStore(store).getLabel(_deviceNodeOf(DEVICE_LABEL_A)), + string.concat(DEVICE_LABEL_A, protocolRegistry.tld()) ); assertEq(dotnsPopController.pendingClaimCountOf(ed), 0); } function test_settle_deploys_store_when_user_has_none() public { - _grantPopFull(ed); + _grantPersonhood(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) }) ); - assertEq(dotnsPopController.pendingClaims(ed, 0, 1)[0].label, LITE_LABEL_A); + assertEq(dotnsPopController.pendingClaims(ed, 0, 1)[0].label, DEVICE_LABEL_A); assertEq(storeFactory.getLabelStore(ed), address(0)); (uint256 settledCount, bool moreRemaining) = @@ -1357,40 +1405,40 @@ contract DotnsPopControllerTests is BaseDotns { address store = storeFactory.getLabelStore(ed); assertTrue(store != address(0)); assertEq( - ILabelStore(store).getLabel(_liteNodeOf(LITE_LABEL_A)), - string.concat(LITE_LABEL_A, ".dot") + ILabelStore(store).getLabel(_deviceNodeOf(DEVICE_LABEL_A)), + string.concat(DEVICE_LABEL_A, ".dot") ); assertEq(dotnsPopController.pendingClaimCountOf(ed), 0); } - function test_registerBaseName_zero_length_label_reverts() public { - _grantPopFull(ed); + function test_issuePersonhoodName_zero_length_label_reverts() public { + _grantPersonhood(ed); IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0xaa)); - vm.expectRevert(IDotnsPopController.InvalidBaseLabel.selector); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: "", user: ed, link: link}) + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({label: "", user: ed, link: link}) ); } - function test_reserveBaseName_accepts_65_byte_chat_key() public { - _grantPopFull(ed); + function test_issueDeviceNameWithReservation_accepts_65_byte_chat_key() public { + _grantPersonhood(ed); bytes memory chatKey = _validChatKey(0x42); - _rootReserveBaseName( - IDotnsPopController.BaseReservation({ - lite: IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: chatKey + _rootIssueDeviceNameWithReservation( + IDotnsPopController.DeviceNameIssuanceWithReservation({ + issuance: IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: chatKey }), - reservedBaseLabel: "" + reservedLabel: "" }) ); vm.prank(ed); dotnsPopController.settlePendingClaims(ed, type(uint256).max); - bytes32 node = _liteNodeOf(LITE_LABEL_A); + bytes32 node = _deviceNodeOf(DEVICE_LABEL_A); assertEq(dotnsPopResolver.chatKey(node), chatKey); } @@ -1414,16 +1462,16 @@ contract DotnsPopControllerTests is BaseDotns { } function test_gatewayReserve_stashes_pending_claim_when_user_has_no_label_store() public { - _grantPopFull(ed); + _grantPersonhood(ed); bytes memory chatKey = _validChatKey(0x01); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: chatKey + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: chatKey }) ); - bytes32 node = _liteNodeOf(LITE_LABEL_A); + bytes32 node = _deviceNodeOf(DEVICE_LABEL_A); assertEq(dotnsRegistry.owner(node), ed); assertEq(storeFactory.getLabelStore(ed), address(0)); // Chat key is now persisted eagerly on the resolver at reserve time, even when @@ -1432,17 +1480,17 @@ contract DotnsPopControllerTests is BaseDotns { IDotnsPopController.PendingClaim[] memory pending = dotnsPopController.pendingClaims(ed, 0, type(uint256).max); - assertEq(pending[0].label, LITE_LABEL_A); + assertEq(pending[0].label, DEVICE_LABEL_A); assertGt(pending[0].mintedAt, 0); } function test_settle_deploys_store_and_writes_label_and_chat_key() public { - _grantPopFull(ed); + _grantPersonhood(ed); bytes memory chatKey = _validChatKey(0x07); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: chatKey + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: chatKey }) ); @@ -1452,9 +1500,9 @@ contract DotnsPopControllerTests is BaseDotns { address store = storeFactory.getLabelStore(ed); assertTrue(store != address(0)); - bytes32 node = _liteNodeOf(LITE_LABEL_A); + bytes32 node = _deviceNodeOf(DEVICE_LABEL_A); assertEq( - ILabelStore(store).getLabel(node), string.concat(LITE_LABEL_A, protocolRegistry.tld()) + ILabelStore(store).getLabel(node), string.concat(DEVICE_LABEL_A, protocolRegistry.tld()) ); assertEq(dotnsPopResolver.chatKey(node), chatKey); @@ -1462,16 +1510,16 @@ contract DotnsPopControllerTests is BaseDotns { } function test_settle_emits_settled_and_name_registered() public { - _grantPopFull(ed); + _grantPersonhood(ed); bytes memory chatKey = _validChatKey(0x03); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: chatKey + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: chatKey }) ); - bytes32 labelhash = keccak256(bytes(LITE_LABEL_A)); + bytes32 labelhash = keccak256(bytes(DEVICE_LABEL_A)); address expectedStore = vm.computeCreateAddress(address(storeFactory), vm.getNonce(address(storeFactory))); @@ -1479,7 +1527,7 @@ contract DotnsPopControllerTests is BaseDotns { vm.expectEmit(true, true, false, true, address(dotnsPopController)); emit IDotnsPopController.PendingClaimSettled(ed, labelhash, expectedStore, ed); vm.expectEmit(true, true, true, true, address(dotnsPopController)); - emit IDotnsPopController.NameRegistered(LITE_LABEL_A, labelhash, ed, expectedStore); + emit IDotnsPopController.NameRegistered(DEVICE_LABEL_A, labelhash, ed, expectedStore); dotnsPopController.settlePendingClaims(ed, type(uint256).max); } @@ -1498,20 +1546,20 @@ contract DotnsPopControllerTests is BaseDotns { // A large queue is drained in bounded batches so a single settlement can never exceed the // block gas limit. Settling with a limit below the queue length reports the residue and a // follow-up call clears it. - _grantPopFull(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0x05) + _grantPersonhood(ed); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0x05) }) ); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_B, user: ed, chatKey: _validChatKey(0x06) + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_B, user: ed, chatKey: _validChatKey(0x06) }) ); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_C, user: ed, chatKey: _validChatKey(0x07) + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_C, user: ed, chatKey: _validChatKey(0x07) }) ); assertEq(dotnsPopController.pendingClaimCountOf(ed), 3); @@ -1530,21 +1578,21 @@ contract DotnsPopControllerTests is BaseDotns { assertEq(dotnsPopController.pendingClaimCountOf(ed), 0); } - function test_settle_on_expiry_by_third_party_writes_label() public { - // Age never gates settlement: a claim warped past its reservation deadline still settles + function test_settle_after_reservation_duration_by_third_party_writes_label() public { + // Age never gates settlement: a claim warped past `reservationDuration` still settles // in full. A third party drives the settlement, the store is deployed for the beneficiary, // the label is written, the queue empties, the beneficiary leaves the enumeration set, and // the settler is recorded on the event. - _grantPopFull(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0x02) + _grantPersonhood(ed); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0x02) }) ); vm.warp(block.timestamp + DEFAULT_RESERVATION_DURATION + 1); - bytes32 labelhash = keccak256(bytes(LITE_LABEL_A)); + bytes32 labelhash = keccak256(bytes(DEVICE_LABEL_A)); address expectedStore = vm.computeCreateAddress(address(storeFactory), vm.getNonce(address(storeFactory))); @@ -1559,8 +1607,8 @@ contract DotnsPopControllerTests is BaseDotns { address store = storeFactory.getLabelStore(ed); assertEq(store, expectedStore); assertEq( - ILabelStore(store).getLabel(_liteNodeOf(LITE_LABEL_A)), - string.concat(LITE_LABEL_A, protocolRegistry.tld()) + ILabelStore(store).getLabel(_deviceNodeOf(DEVICE_LABEL_A)), + string.concat(DEVICE_LABEL_A, protocolRegistry.tld()) ); assertEq(dotnsPopController.pendingClaimCountOf(ed), 0); assertEq(dotnsPopController.pendingClaimUserCount(), 0); @@ -1570,10 +1618,10 @@ contract DotnsPopControllerTests is BaseDotns { // The old model dropped lapsed entries; stores now always settle. Warping past the // reservation duration and settling writes the label into the store rather than // discarding it. - _grantPopFull(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0x04) + _grantPersonhood(ed); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0x04) }) ); @@ -1585,34 +1633,34 @@ contract DotnsPopControllerTests is BaseDotns { address store = storeFactory.getLabelStore(ed); assertTrue(store != address(0)); assertEq( - ILabelStore(store).getLabel(_liteNodeOf(LITE_LABEL_A)), - string.concat(LITE_LABEL_A, protocolRegistry.tld()) + ILabelStore(store).getLabel(_deviceNodeOf(DEVICE_LABEL_A)), + string.concat(DEVICE_LABEL_A, protocolRegistry.tld()) ); assertEq(dotnsPopController.pendingClaimCountOf(ed), 0); assertEq(dotnsPopController.pendingClaimUserCount(), 0); } - function test_reserveLiteName_piles_second_pending_claim_when_caller_has_no_store() public { + function test_issueDeviceName_piles_second_pending_claim_when_caller_has_no_store() public { // The Root gateway origin cannot deploy a LabelStore, so a store-less user keeps // accumulating deferred names instead of reverting; a single signed-origin // settlement writes them all at once. - _grantPopFull(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0x05) + _grantPersonhood(ed); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0x05) }) ); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_B, user: ed, chatKey: _validChatKey(0x06) + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_B, user: ed, chatKey: _validChatKey(0x06) }) ); IDotnsPopController.PendingClaim[] memory pending = dotnsPopController.pendingClaims(ed, 0, type(uint256).max); assertEq(pending.length, 2); - assertEq(pending[0].label, LITE_LABEL_A); - assertEq(pending[1].label, LITE_LABEL_B); + assertEq(pending[0].label, DEVICE_LABEL_A); + assertEq(pending[1].label, DEVICE_LABEL_B); assertEq(dotnsPopController.pendingClaimUserCount(), 1); vm.prank(ed); @@ -1621,12 +1669,12 @@ contract DotnsPopControllerTests is BaseDotns { address store = storeFactory.getLabelStore(ed); assertTrue(store != address(0)); assertEq( - ILabelStore(store).getLabel(_liteNodeOf(LITE_LABEL_A)), - string.concat(LITE_LABEL_A, protocolRegistry.tld()) + ILabelStore(store).getLabel(_deviceNodeOf(DEVICE_LABEL_A)), + string.concat(DEVICE_LABEL_A, protocolRegistry.tld()) ); assertEq( - ILabelStore(store).getLabel(_liteNodeOf(LITE_LABEL_B)), - string.concat(LITE_LABEL_B, protocolRegistry.tld()) + ILabelStore(store).getLabel(_deviceNodeOf(DEVICE_LABEL_B)), + string.concat(DEVICE_LABEL_B, protocolRegistry.tld()) ); assertEq(dotnsPopController.pendingClaimCountOf(ed), 0); assertEq(dotnsPopController.pendingClaimUserCount(), 0); @@ -1637,34 +1685,34 @@ contract DotnsPopControllerTests is BaseDotns { assertEq(dotnsPopController.pendingClaimCountOf(ed), 0); } - function test_registerBaseName_claim_by_store_less_full_person_piles_then_settles() public { - // Regression: a store-less full person reserves a lite name plus a base reservation - // (the lite leg stashes a deferred claim because Root cannot deploy the store), then - // claims the base name. The base mint stashes a second deferred claim instead of + function test_issuePersonhoodName_claim_by_store_less_personhood_piles_then_settles() public { + // Regression: a store-less personhood holder reserves a device name plus a base reservation + // (the device-name issuance stashes a deferred claim because Root cannot deploy the store), + // then claims the base name. The base mint stashes a second deferred claim instead of // reverting; one signed-origin settlement deploys the store and settles both. - _grantPopFull(ed); - _rootReserveBaseName( - IDotnsPopController.BaseReservation({ - lite: IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0x31) + _grantPersonhood(ed); + _rootIssueDeviceNameWithReservation( + IDotnsPopController.DeviceNameIssuanceWithReservation({ + issuance: IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0x31) }), - reservedBaseLabel: BASE_LABEL_A + reservedLabel: PERSONHOOD_LABEL_A }) ); assertEq(storeFactory.getLabelStore(ed), address(0)); assertEq(dotnsPopController.pendingClaimCountOf(ed), 1); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({ - label: BASE_LABEL_A, user: ed, link: _linkWithLite(LITE_LABEL_A) + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: _linkWithDeviceName(DEVICE_LABEL_A) }) ); IDotnsPopController.PendingClaim[] memory pending = dotnsPopController.pendingClaims(ed, 0, type(uint256).max); assertEq(pending.length, 2); - assertEq(pending[0].label, LITE_LABEL_A); - assertEq(pending[1].label, BASE_LABEL_A); + assertEq(pending[0].label, DEVICE_LABEL_A); + assertEq(pending[1].label, PERSONHOOD_LABEL_A); vm.prank(ed); dotnsPopController.settlePendingClaims(ed, type(uint256).max); @@ -1672,34 +1720,34 @@ contract DotnsPopControllerTests is BaseDotns { address store = storeFactory.getLabelStore(ed); assertTrue(store != address(0)); assertEq( - ILabelStore(store).getLabel(_liteNodeOf(LITE_LABEL_A)), - string.concat(LITE_LABEL_A, protocolRegistry.tld()) + ILabelStore(store).getLabel(_deviceNodeOf(DEVICE_LABEL_A)), + string.concat(DEVICE_LABEL_A, protocolRegistry.tld()) ); assertEq( - ILabelStore(store).getLabel(_nodeOf(BASE_LABEL_A)), - string.concat(BASE_LABEL_A, protocolRegistry.tld()) + ILabelStore(store).getLabel(_nodeOf(PERSONHOOD_LABEL_A)), + string.concat(PERSONHOOD_LABEL_A, protocolRegistry.tld()) ); assertEq(dotnsPopController.pendingClaimCountOf(ed), 0); } function test_pendingClaimUsers_enumeration_mirrors_stash_and_settle() public { - _grantPopFull(ed); - _grantPopFull(tiago); - _grantPopFull(leonardo); + _grantPersonhood(ed); + _grantPersonhood(tiago); + _grantPersonhood(leonardo); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0x01) + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0x01) }) ); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_B, user: tiago, chatKey: _validChatKey(0x02) + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_B, user: tiago, chatKey: _validChatKey(0x02) }) ); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_C, user: leonardo, chatKey: _validChatKey(0x03) + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_C, user: leonardo, chatKey: _validChatKey(0x03) }) ); @@ -1723,10 +1771,10 @@ contract DotnsPopControllerTests is BaseDotns { } function test_pendingClaimUsers_returns_empty_when_offset_past_count() public { - _grantPopFull(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0x01) + _grantPersonhood(ed); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0x01) }) ); @@ -1734,13 +1782,13 @@ contract DotnsPopControllerTests is BaseDotns { assertEq(empty.length, 0); } - function test_settle_at_exact_expiry_boundary_writes_label() public { - // Age is irrelevant to settlement: at the exact reservation deadline the claim still - // settles and writes its label rather than being treated as forfeit. - _grantPopFull(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0x11) + function test_settle_at_exactly_reservation_duration_writes_label() public { + // Age is irrelevant to settlement: exactly `reservationDuration` after the mint the claim + // still settles and writes its label rather than being treated as forfeit. + _grantPersonhood(ed); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0x11) }) ); @@ -1753,8 +1801,8 @@ contract DotnsPopControllerTests is BaseDotns { address store = storeFactory.getLabelStore(ed); assertTrue(store != address(0)); assertEq( - ILabelStore(store).getLabel(_liteNodeOf(LITE_LABEL_A)), - string.concat(LITE_LABEL_A, protocolRegistry.tld()) + ILabelStore(store).getLabel(_deviceNodeOf(DEVICE_LABEL_A)), + string.concat(DEVICE_LABEL_A, protocolRegistry.tld()) ); assertEq(dotnsPopController.pendingClaimCountOf(ed), 0); } @@ -1762,11 +1810,11 @@ contract DotnsPopControllerTests is BaseDotns { function test_settle_is_keyed_by_user_arg_other_stash_untouched() public { // Settlement targets the `user` argument, not the caller: settling for a user with no // stash is a no-op and does not disturb another user's pending claim. - _grantPopFull(ed); + _grantPersonhood(ed); bytes memory chatKey = _validChatKey(0x12); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: chatKey + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: chatKey }) ); @@ -1778,7 +1826,7 @@ contract DotnsPopControllerTests is BaseDotns { IDotnsPopController.PendingClaim[] memory pending = dotnsPopController.pendingClaims(ed, 0, type(uint256).max); - assertEq(pending[0].label, LITE_LABEL_A); + assertEq(pending[0].label, DEVICE_LABEL_A); assertGt(pending[0].mintedAt, 0); assertEq(storeFactory.getLabelStore(ed), address(0)); assertEq(storeFactory.getLabelStore(tiago), address(0)); @@ -1786,22 +1834,22 @@ contract DotnsPopControllerTests is BaseDotns { } function test_pendingClaimUsers_pagination_boundary_cases() public { - _grantPopFull(ed); - _grantPopFull(tiago); - _grantPopFull(leonardo); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0x01) + _grantPersonhood(ed); + _grantPersonhood(tiago); + _grantPersonhood(leonardo); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0x01) }) ); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_B, user: tiago, chatKey: _validChatKey(0x02) + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_B, user: tiago, chatKey: _validChatKey(0x02) }) ); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_C, user: leonardo, chatKey: _validChatKey(0x03) + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_C, user: leonardo, chatKey: _validChatKey(0x03) }) ); @@ -1815,28 +1863,28 @@ contract DotnsPopControllerTests is BaseDotns { } function test_settle_with_empty_chat_key_skips_resolver_write() public { - _grantPopFull(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({liteLabel: LITE_LABEL_A, user: ed, chatKey: ""}) + _grantPersonhood(ed); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({label: DEVICE_LABEL_A, user: ed, chatKey: ""}) ); vm.prank(ed); dotnsPopController.settlePendingClaims(ed, type(uint256).max); - bytes32 node = _liteNodeOf(LITE_LABEL_A); + bytes32 node = _deviceNodeOf(DEVICE_LABEL_A); address store = storeFactory.getLabelStore(ed); assertTrue(store != address(0)); assertEq( - ILabelStore(store).getLabel(node), string.concat(LITE_LABEL_A, protocolRegistry.tld()) + ILabelStore(store).getLabel(node), string.concat(DEVICE_LABEL_A, protocolRegistry.tld()) ); assertEq(dotnsPopResolver.chatKey(node).length, 0); } function test_gatewayReserve_warm_user_after_settle_writes_directly_without_stashing() public { - _grantPopFull(ed); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0x21) + _grantPersonhood(ed); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0x21) }) ); @@ -1846,15 +1894,15 @@ contract DotnsPopControllerTests is BaseDotns { assertTrue(store != address(0)); bytes memory secondChatKey = _validChatKey(0x22); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_B, user: ed, chatKey: secondChatKey + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_B, user: ed, chatKey: secondChatKey }) ); - bytes32 node = _liteNodeOf(LITE_LABEL_B); + bytes32 node = _deviceNodeOf(DEVICE_LABEL_B); assertEq( - ILabelStore(store).getLabel(node), string.concat(LITE_LABEL_B, protocolRegistry.tld()) + ILabelStore(store).getLabel(node), string.concat(DEVICE_LABEL_B, protocolRegistry.tld()) ); assertEq(dotnsPopResolver.chatKey(node), secondChatKey); assertEq(dotnsPopController.pendingClaimCountOf(ed), 0); @@ -1864,12 +1912,12 @@ contract DotnsPopControllerTests is BaseDotns { function test_advanceExpiredHead_promotes_waiter_and_resyncs_popRules() public { string memory stem = "longnamebob"; uint64 duration = dotnsPopController.reservationDuration(); - _grantPopFull(ed); - _grantPopFull(tiago); + _grantPersonhood(ed); + _grantPersonhood(tiago); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), stem); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), stem); vm.warp(block.timestamp + uint256(duration) / 2); - _reservePop(tiago, LITE_LABEL_B, _validChatKey(0x02), stem); + _reservePop(tiago, DEVICE_LABEL_B, _validChatKey(0x02), stem); bytes32 labelhash = keccak256(bytes(stem)); (uint64 head, uint64 tail) = dotnsPopController.reservationMeta(labelhash); @@ -1900,191 +1948,237 @@ contract DotnsPopControllerTests is BaseDotns { } function test_multiWaiter_standaloneGuard_rejects_non_head_user() public { - _grantPopFull(ed); - _grantPopFull(tiago); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), BASE_LABEL_A); - _reservePop(tiago, LITE_LABEL_B, _validChatKey(0x02), BASE_LABEL_A); + _grantPersonhood(ed); + _grantPersonhood(tiago); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), PERSONHOOD_LABEL_A); + _reservePop(tiago, DEVICE_LABEL_B, _validChatKey(0x02), PERSONHOOD_LABEL_A); IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0xbb)); vm.expectPartialRevert(IDotnsPopController.NotHolder.selector); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({label: BASE_LABEL_A, user: tiago, link: link}) + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: tiago, link: link + }) ); } - function test_liteNamesOf_and_fullNamesOf_split_issued_names_by_shape() public { - // Settled names read back from the store; a pending gateway name reads from the queue with - // a live deadline; the two shapes never cross into each other's list; an untouched account - // returns empty lists and zero counts. - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), ""); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({ - label: BASE_LABEL_A, user: ed, link: _linkFresh(_validChatKey(0x02)) + function test_namesOf_lists_issued_names_with_settlement_state() public { + // Settled names read back from the store; a pending gateway name reads from the queue; an + // untouched account returns an empty list and a zero count. + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), ""); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: _linkFresh(_validChatKey(0x02)) }) ); - _grantPopFull(leonardo); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_C, user: leonardo, chatKey: _validChatKey(0x03) + _grantPersonhood(leonardo); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_C, user: leonardo, chatKey: _validChatKey(0x03) }) ); - IDotnsPopLens.Name[] memory edLite = dotnsPopLens.liteNamesOf(ed, 0, type(uint256).max); - assertEq(edLite.length, 1); - assertEq(edLite[0].node, _liteNodeOf(LITE_LABEL_A)); - assertEq(edLite[0].label, LITE_LABEL_A); - assertTrue(edLite[0].settled); - assertEq(edLite[0].deadline, 0); - - IDotnsPopLens.Name[] memory edFull = dotnsPopLens.fullNamesOf(ed, 0, type(uint256).max); - assertEq(edFull.length, 1); - assertEq(edFull[0].node, _nodeOf(BASE_LABEL_A)); - assertEq(edFull[0].label, BASE_LABEL_A); - assertTrue(edFull[0].settled); - - assertEq(dotnsPopLens.liteNameCountOf(ed), 1); - assertEq(dotnsPopLens.fullNameCountOf(ed), 1); - - IDotnsPopLens.Name[] memory leoLite = - dotnsPopLens.liteNamesOf(leonardo, 0, type(uint256).max); - assertEq(leoLite.length, 1); - assertEq(leoLite[0].node, _liteNodeOf(LITE_LABEL_C)); - assertEq(leoLite[0].label, LITE_LABEL_C); - assertFalse(leoLite[0].settled); - assertGt(leoLite[0].deadline, 0); - assertEq(dotnsPopLens.liteNameCountOf(leonardo), 1); - assertEq(dotnsPopLens.fullNameCountOf(leonardo), 0); - - assertEq(dotnsPopLens.liteNamesOf(tiago, 0, type(uint256).max).length, 0); - assertEq(dotnsPopLens.fullNamesOf(tiago, 0, type(uint256).max).length, 0); - assertEq(dotnsPopLens.liteNameCountOf(tiago), 0); - assertEq(dotnsPopLens.fullNameCountOf(tiago), 0); - } - - function test_liteNamesOf_pagination_slices_and_clamps() public { - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), ""); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_B, user: ed, chatKey: _validChatKey(0x02) + IDotnsPopLens.Name[] memory edNames = dotnsPopLens.namesOf(ed, 0, type(uint256).max); + assertEq(edNames.length, 2); + IDotnsPopLens.Name memory edDevice = _nameWithNode(edNames, _deviceNodeOf(DEVICE_LABEL_A)); + assertEq(edDevice.label, DEVICE_LABEL_A); + assertTrue(edDevice.settled); + IDotnsPopLens.Name memory edPersonhood = _nameWithNode(edNames, _nodeOf(PERSONHOOD_LABEL_A)); + assertEq(edPersonhood.label, PERSONHOOD_LABEL_A); + assertTrue(edPersonhood.settled); + assertEq(dotnsPopLens.nameCountOf(ed), 2); + + IDotnsPopLens.Name[] memory leoNames = dotnsPopLens.namesOf(leonardo, 0, type(uint256).max); + assertEq(leoNames.length, 1); + assertEq(leoNames[0].node, _deviceNodeOf(DEVICE_LABEL_C)); + assertEq(leoNames[0].label, DEVICE_LABEL_C); + assertFalse(leoNames[0].settled); + assertEq(dotnsPopLens.nameCountOf(leonardo), 1); + + assertEq(dotnsPopLens.namesOf(tiago, 0, type(uint256).max).length, 0); + assertEq(dotnsPopLens.nameCountOf(tiago), 0); + } + + function test_namesOf_pagination_slices_and_clamps() public { + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), ""); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_B, user: ed, chatKey: _validChatKey(0x02) }) ); - assertEq(dotnsPopLens.liteNameCountOf(ed), 2); + assertEq(dotnsPopLens.nameCountOf(ed), 2); - IDotnsPopLens.Name[] memory first = dotnsPopLens.liteNamesOf(ed, 0, 1); + IDotnsPopLens.Name[] memory first = dotnsPopLens.namesOf(ed, 0, 1); assertEq(first.length, 1); - IDotnsPopLens.Name[] memory second = dotnsPopLens.liteNamesOf(ed, 1, 1); + IDotnsPopLens.Name[] memory second = dotnsPopLens.namesOf(ed, 1, 1); assertEq(second.length, 1); assertTrue(first[0].node != second[0].node); - assertEq(dotnsPopLens.liteNamesOf(ed, 2, 1).length, 0); + assertEq(dotnsPopLens.namesOf(ed, 2, 1).length, 0); // A limit above the internal page ceiling is clamped rather than reverting; the account // holds fewer names than the ceiling, so the full set still comes back. IDotnsPopLens.Name[] memory clamped = - dotnsPopLens.liteNamesOf(ed, 0, DotnsConstants.MAX_PAGE_SIZE + 1); + dotnsPopLens.namesOf(ed, 0, DotnsConstants.MAX_PAGE_SIZE + 1); assertEq(clamped.length, 2); } function test_name_listings_exclude_names_owned_by_others() public { // The listing re-checks registrar ownership per entry, so a name owned by another account // never surfaces in this account's list. - _grantPopFull(ed); - _grantPopFull(tiago); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), ""); - _reservePop(tiago, LITE_LABEL_C, _validChatKey(0x02), ""); + _grantPersonhood(ed); + _grantPersonhood(tiago); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), ""); + _reservePop(tiago, DEVICE_LABEL_C, _validChatKey(0x02), ""); - IDotnsPopLens.Name[] memory edLite = dotnsPopLens.liteNamesOf(ed, 0, type(uint256).max); - assertEq(edLite.length, 1); - assertFalse(_namesContainNode(edLite, _liteNodeOf(LITE_LABEL_C))); + IDotnsPopLens.Name[] memory edDevice = dotnsPopLens.namesOf(ed, 0, type(uint256).max); + assertEq(edDevice.length, 1); + assertFalse(_namesContainNode(edDevice, _deviceNodeOf(DEVICE_LABEL_C))); - IDotnsPopLens.Name[] memory tiagoLite = - dotnsPopLens.liteNamesOf(tiago, 0, type(uint256).max); - assertEq(tiagoLite.length, 1); - assertFalse(_namesContainNode(tiagoLite, _liteNodeOf(LITE_LABEL_A))); + IDotnsPopLens.Name[] memory tiagoDevice = dotnsPopLens.namesOf(tiago, 0, type(uint256).max); + assertEq(tiagoDevice.length, 1); + assertFalse(_namesContainNode(tiagoDevice, _deviceNodeOf(DEVICE_LABEL_A))); } function test_nameDetail_and_nameDetailByNode_report_record() public { - _grantPopFull(ed); - bytes memory liteChatKey = _validChatKey(0xaa); - _reservePop(ed, LITE_LABEL_A, liteChatKey, BASE_LABEL_A); - _rootRegisterBaseName( - IDotnsPopController.FullRegistration({ - label: BASE_LABEL_A, user: ed, link: _linkWithLite(LITE_LABEL_A) - }) - ); - - bytes32 fullNode = _nodeOf(BASE_LABEL_A); - IDotnsPopLens.NameDetail memory full = dotnsPopLens.nameDetail(BASE_LABEL_A); - assertEq(full.node, fullNode); - assertEq(full.label, BASE_LABEL_A); - assertEq(full.owner, ed); - assertTrue(full.exists); - assertTrue(full.settled); - assertTrue(full.tier == IPopRules.PopStatus.PopFull); - assertEq(full.chatKey, liteChatKey); - assertEq(full.liteLink, keccak256(bytes(LITE_LABEL_A))); - // A base label is never a lite labelhash, so no promoted node is keyed under it. - assertEq(full.fullClaim, bytes32(0)); - - // Holding the lite label lets nameDetail recover the promoted full node. - IDotnsPopLens.NameDetail memory lite = dotnsPopLens.nameDetail(LITE_LABEL_A); - assertEq(lite.fullClaim, fullNode); - - // A settled lite name that was never promoted carries no full claim, and the by-node - // overload leaves it zero. - _grantPopFull(leonardo); - _reservePop(leonardo, LITE_LABEL_C, _validChatKey(0xbb), ""); + _grantPersonhood(ed); + bytes memory deviceChatKey = _validChatKey(0xaa); + _reservePop(ed, DEVICE_LABEL_A, deviceChatKey, PERSONHOOD_LABEL_A); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: _linkWithDeviceName(DEVICE_LABEL_A) + }) + ); + + bytes32 personhoodNode = _nodeOf(PERSONHOOD_LABEL_A); + IDotnsPopLens.NameDetail memory personhood = dotnsPopLens.nameDetail(PERSONHOOD_LABEL_A); + assertEq(personhood.node, personhoodNode); + assertEq(personhood.label, PERSONHOOD_LABEL_A); + assertEq(personhood.owner, ed); + assertTrue(personhood.exists); + assertTrue(personhood.settled); + assertTrue(personhood.requiredTier == IPopRules.PopStatus.Personhood); + assertEq(personhood.chatKey, deviceChatKey); + assertEq(personhood.deviceLabelhash, keccak256(bytes(DEVICE_LABEL_A))); + // A base label is never a device-name labelhash, so no promoted node is keyed under it. + assertEq(personhood.personhoodNode, bytes32(0)); + + // Holding the device name lets nameDetail recover the promoted personhood node. + IDotnsPopLens.NameDetail memory device = dotnsPopLens.nameDetail(DEVICE_LABEL_A); + assertEq(device.personhoodNode, personhoodNode); + + // A settled device name that was never promoted carries no personhood claim, and the + // by-node overload leaves it zero. + _grantPersonhood(leonardo); + _reservePop(leonardo, DEVICE_LABEL_C, _validChatKey(0xbb), ""); IDotnsPopLens.NameDetail memory coldByNode = - dotnsPopLens.nameDetailByNode(_liteNodeOf(LITE_LABEL_C)); + dotnsPopLens.nameDetailByNode(_deviceNodeOf(DEVICE_LABEL_C)); assertTrue(coldByNode.exists); - assertEq(coldByNode.fullClaim, bytes32(0)); + assertEq(coldByNode.personhoodNode, bytes32(0)); // Unknown name and node never revert and return a zeroed record. IDotnsPopLens.NameDetail memory unknownName = dotnsPopLens.nameDetail("nothingxx"); assertFalse(unknownName.exists); assertEq(unknownName.owner, address(0)); assertEq(bytes(unknownName.label).length, 0); - assertEq(unknownName.fullClaim, bytes32(0)); + assertEq(unknownName.personhoodNode, bytes32(0)); IDotnsPopLens.NameDetail memory unknownNode = dotnsPopLens.nameDetailByNode(bytes32(uint256(0xdead))); assertFalse(unknownNode.exists); assertEq(unknownNode.owner, address(0)); - assertEq(unknownNode.fullClaim, bytes32(0)); + assertEq(unknownNode.personhoodNode, bytes32(0)); } - /// @notice `nameDetail` classifies a cold-path lite name before it settles, not only after. + /// @notice `nameDetail` classifies a cold-path device name before it settles, not only after. /// @dev A pending subname has no label recoverable from its node, so `nameDetail` classifies /// the caller-supplied label. Before this was wired the tier read `NoStatus` until settlement /// wrote the label into the store. - function test_nameDetail_classifies_a_cold_lite_name_before_and_after_settlement() public { - address fresh = makeAddr("coldlite"); - _grantPopLite(fresh); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: fresh, chatKey: _validChatKey(0x01) + function test_nameDetail_classifies_a_cold_device_name_before_and_after_settlement() public { + address fresh = makeAddr("colddevice"); + _grantDevicehood(fresh); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: fresh, chatKey: _validChatKey(0x01) }) ); // Cold path: the claim is staged and no store is deployed yet. assertEq(storeFactory.getLabelStore(fresh), address(0), "cold user has no store yet"); - IDotnsPopLens.NameDetail memory pending = dotnsPopLens.nameDetail(LITE_LABEL_A); + IDotnsPopLens.NameDetail memory pending = dotnsPopLens.nameDetail(DEVICE_LABEL_A); assertTrue(pending.exists, "subname owned before settlement"); assertFalse(pending.settled, "not settled yet"); - assertEq(pending.label, LITE_LABEL_A, "caller label supplied before settlement"); - assertTrue(pending.tier == IPopRules.PopStatus.PopLite, "classified as lite before settle"); + assertEq(pending.label, DEVICE_LABEL_A, "caller label supplied before settlement"); + assertTrue( + pending.requiredTier == IPopRules.PopStatus.Devicehood, + "classified as devicehood before settle" + ); vm.prank(fresh); dotnsPopController.settlePendingClaims(fresh, type(uint256).max); - IDotnsPopLens.NameDetail memory settled = dotnsPopLens.nameDetail(LITE_LABEL_A); + IDotnsPopLens.NameDetail memory settled = dotnsPopLens.nameDetail(DEVICE_LABEL_A); + assertTrue(settled.settled, "settled after draining the queue"); + assertEq(settled.label, DEVICE_LABEL_A, "label recovered from the store after settlement"); + assertTrue( + settled.requiredTier == IPopRules.PopStatus.Devicehood, + "still devicehood after settlement" + ); + } + + /// @notice `nameDetail` reports an unsettled personhood name's label and tier. + /// @dev The registrar reads a tokenised name's label from the holder's `LabelStore`, which a + /// store-less holder does not have yet, so the lens falls back to the caller's label. + function test_nameDetail_reports_an_unsettled_personhood_name() public { + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: _linkFresh("") + }) + ); + assertEq(dotnsPopController.pendingClaimCountOf(ed), 1, "the claim is pending"); + + IDotnsPopLens.NameDetail memory detail = dotnsPopLens.nameDetail(PERSONHOOD_LABEL_A); + assertTrue(detail.exists, "minted before settlement"); + assertFalse(detail.settled, "not settled yet"); + assertEq(detail.label, PERSONHOOD_LABEL_A, "caller label supplied before settlement"); + assertTrue( + detail.requiredTier == IPopRules.PopStatus.Personhood, + "classified as personhood before settlement" + ); + } + + /// @notice `nameDetailByNode` leaves an unsettled personhood name's label empty until + /// settlement makes it recoverable from the node. + function test_nameDetailByNode_recovers_a_personhood_label_only_after_settlement() public { + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: _linkFresh("") + }) + ); + bytes32 node = _nodeOf(PERSONHOOD_LABEL_A); + + IDotnsPopLens.NameDetail memory pending = dotnsPopLens.nameDetailByNode(node); + assertTrue(pending.exists, "minted before settlement"); + assertFalse(pending.settled, "not settled yet"); + assertEq(pending.label, "", "no label recoverable from the node before settlement"); + assertTrue( + pending.requiredTier == IPopRules.PopStatus.NoStatus, "unclassified without a label" + ); + + vm.prank(ed); + dotnsPopController.settlePendingClaims(ed, type(uint256).max); + + IDotnsPopLens.NameDetail memory settled = dotnsPopLens.nameDetailByNode(node); assertTrue(settled.settled, "settled after draining the queue"); - assertEq(settled.label, LITE_LABEL_A, "label recovered from the store after settlement"); - assertTrue(settled.tier == IPopRules.PopStatus.PopLite, "still lite after settlement"); + assertEq(settled.label, PERSONHOOD_LABEL_A, "label recovered after settlement"); + assertTrue( + settled.requiredTier == IPopRules.PopStatus.Personhood, + "classified as personhood after settlement" + ); } /// @notice A store row whose node key is not its own text's node is neither counted nor listed. @@ -2093,14 +2187,14 @@ contract DotnsPopControllerTests is BaseDotns { /// are /// derived together), so this injects one directly to pin the guard. function test_lens_ignores_a_store_row_whose_node_does_not_match_its_text() public { - _grantPopFull(ed); - _grantPopFull(leonardo); + _grantPersonhood(ed); + _grantPersonhood(leonardo); - // ed holds a legitimate lite name, which deploys ed's store and counts once. - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), ""); - assertEq(dotnsPopLens.liteNameCountOf(ed), 1, "one legitimate lite name"); + // ed holds a legitimate device name, which deploys ed's store and counts once. + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), ""); + assertEq(dotnsPopLens.nameCountOf(ed), 1, "one legitimate device name"); - // A second lite name, issued to leonardo, so its text reads as PoP-issued. + // A second device name, issued to leonardo, so its text reads as PoP-issued. _reservePop(leonardo, "another.01", _validChatKey(0x02), ""); // ed owns a storeless subname beneath the numeric container. Only a registered controller @@ -2127,19 +2221,23 @@ contract DotnsPopControllerTests is BaseDotns { // The forged row is owned by ed and its text is PoP-issued, but its node does not match the // text, so the listing excludes it and the count stays one. - assertEq(dotnsPopLens.liteNameCountOf(ed), 1, "forged row excluded from the count"); - IDotnsPopLens.Name[] memory lite = dotnsPopLens.liteNamesOf(ed, 0, type(uint256).max); - assertEq(lite.length, 1, "forged row excluded from the listing"); - assertEq(lite[0].node, _liteNodeOf(LITE_LABEL_A), "only the legitimate lite name is listed"); + assertEq(dotnsPopLens.nameCountOf(ed), 1, "forged row excluded from the count"); + IDotnsPopLens.Name[] memory names = dotnsPopLens.namesOf(ed, 0, type(uint256).max); + assertEq(names.length, 1, "forged row excluded from the listing"); + assertEq( + names[0].node, + _deviceNodeOf(DEVICE_LABEL_A), + "only the legitimate device name is listed" + ); } function test_profileOf_reports_store_pending_and_reservation() public { // A store-less user with a staged claim, a settled user holding a reservation, and an // untouched account each report distinct profile facts. - _grantPopFull(leonardo); - _rootReserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_C, user: leonardo, chatKey: _validChatKey(0x01) + _grantPersonhood(leonardo); + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_C, user: leonardo, chatKey: _validChatKey(0x01) }) ); IDotnsPopLens.PopProfile memory cold = dotnsPopLens.profileOf(leonardo); @@ -2147,12 +2245,12 @@ contract DotnsPopControllerTests is BaseDotns { assertEq(cold.pendingClaimCount, 1); assertEq(cold.reservationLabelhash, bytes32(0)); - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x02), BASE_LABEL_A); + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x02), PERSONHOOD_LABEL_A); IDotnsPopLens.PopProfile memory warm = dotnsPopLens.profileOf(ed); assertTrue(warm.hasLabelStore); assertEq(warm.pendingClaimCount, 0); - assertEq(warm.reservationLabelhash, keccak256(bytes(BASE_LABEL_A))); + assertEq(warm.reservationLabelhash, keccak256(bytes(PERSONHOOD_LABEL_A))); IDotnsPopLens.PopProfile memory empty = dotnsPopLens.profileOf(tiago); assertFalse(empty.hasLabelStore); @@ -2161,17 +2259,32 @@ contract DotnsPopControllerTests is BaseDotns { } function test_reservedBaseLabelOf_returns_label_or_empty() public { - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), BASE_LABEL_A); + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), PERSONHOOD_LABEL_A); assertEq( - dotnsPopController.reservedBaseLabelOf(keccak256(bytes(BASE_LABEL_A))), BASE_LABEL_A + dotnsPopController.reservedLabelOf(keccak256(bytes(PERSONHOOD_LABEL_A))), + PERSONHOOD_LABEL_A ); assertEq( - bytes(dotnsPopController.reservedBaseLabelOf(keccak256(bytes("unknownbase")))).length, 0 + bytes(dotnsPopController.reservedLabelOf(keccak256(bytes("unknownbase")))).length, 0 ); } + function _nameWithNode( + IDotnsPopLens.Name[] memory names, + bytes32 node + ) + internal + pure + returns (IDotnsPopLens.Name memory) + { + for (uint256 i; i < names.length; ++i) { + if (names[i].node == node) return names[i]; + } + revert("no name with that node"); + } + function _namesContainNode( IDotnsPopLens.Name[] memory names, bytes32 node diff --git a/test/unit/registrar/DotnsPopControllerLegacy.t.sol b/test/unit/registrar/DotnsPopControllerLegacy.t.sol new file mode 100644 index 000000000..de5406c18 --- /dev/null +++ b/test/unit/registrar/DotnsPopControllerLegacy.t.sol @@ -0,0 +1,654 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.34; + +import {BaseDotns} from "../../base/BaseDotns.t.sol"; +import {IDotnsPopController} from "../../../contracts/registrars/IDotnsPopController.sol"; +import { + IDotnsPopControllerLegacy +} from "../../../contracts/registrars/IDotnsPopControllerLegacy.sol"; +import {Vm} from "forge-std/Vm.sol"; + +/// @title DotnsPopControllerLegacyTests +/// @notice The legacy entrypoints, and the reservation-queue events. +/// @dev The legacy selectors and error selectors are pinned as literals, because the legacy +/// interface exists to keep them unchanged. Each legacy entrypoint is checked against its +/// replacement from the same state: identical events and state on success, identical revert +/// data on every shared error. Every `InvalidLiteLabel` / `InvalidBaseLabel` revert reachable +/// from a legacy entrypoint is exercised from both the legacy entrypoint and its replacement, +/// which reverts with the new error instead. The shape check inside `_validateDeviceLabel` is +/// preceded by the same check at each of its callers, so it is reached through them and has no +/// test of its own. +contract DotnsPopControllerLegacyTests is BaseDotns { + // Selectors and interfaces + + function test_legacy_selectors_are_pinned() public pure { + assertEq( + IDotnsPopControllerLegacy.reserveLiteName.selector, SELECTOR_LEGACY_RESERVE_LITE_NAME + ); + assertEq( + IDotnsPopControllerLegacy.reserveBaseName.selector, SELECTOR_LEGACY_RESERVE_BASE_NAME + ); + assertEq( + IDotnsPopControllerLegacy.registerBaseName.selector, SELECTOR_LEGACY_REGISTER_BASE_NAME + ); + } + + function test_legacy_error_selectors_are_pinned() public pure { + assertEq(IDotnsPopControllerLegacy.InvalidLiteLabel.selector, bytes4(0x5c6c2eea)); + assertEq(IDotnsPopControllerLegacy.InvalidBaseLabel.selector, bytes4(0x39e67d31)); + } + + function test_new_selectors_differ_from_the_legacy_ones() public pure { + assertTrue( + IDotnsPopController.issueDeviceName.selector != SELECTOR_LEGACY_RESERVE_LITE_NAME + ); + assertTrue( + IDotnsPopController.issueDeviceNameWithReservation.selector + != SELECTOR_LEGACY_RESERVE_BASE_NAME + ); + assertTrue( + IDotnsPopController.issuePersonhoodName.selector != SELECTOR_LEGACY_REGISTER_BASE_NAME + ); + } + + function test_supports_both_interfaces() public view { + assertTrue( + dotnsPopController.supportsInterface(type(IDotnsPopController).interfaceId), + "current interface" + ); + assertTrue( + dotnsPopController.supportsInterface(type(IDotnsPopControllerLegacy).interfaceId), + "legacy interface" + ); + } + + // Legacy entrypoints have the same effect as their replacements + + function test_reserveLiteName_matches_issueDeviceName() public { + _rootReservePersonhoodName( + IDotnsPopController.PersonhoodNameReservation({user: ed, label: PERSONHOOD_LABEL_A}) + ); + IDotnsPopController.DeviceNameIssuance memory issuance = + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0x01) + }); + + _assertSameEffect( + abi.encodeCall(IDotnsPopControllerLegacy.reserveLiteName, (issuance)), + abi.encodeCall(IDotnsPopController.issueDeviceName, (issuance)), + ed, + DEVICE_LABEL_A, + PERSONHOOD_LABEL_A + ); + } + + function test_reserveBaseName_matches_issueDeviceNameWithReservation() public { + _rootReservePersonhoodName( + IDotnsPopController.PersonhoodNameReservation({user: tiago, label: PERSONHOOD_LABEL_A}) + ); + _rootReservePersonhoodName( + IDotnsPopController.PersonhoodNameReservation({user: ed, label: PERSONHOOD_LABEL_B}) + ); + IDotnsPopController.DeviceNameIssuanceWithReservation memory params = + IDotnsPopController.DeviceNameIssuanceWithReservation({ + issuance: IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0x01) + }), + reservedLabel: PERSONHOOD_LABEL_A + }); + + _assertSameEffect( + abi.encodeCall(IDotnsPopControllerLegacy.reserveBaseName, (params)), + abi.encodeCall(IDotnsPopController.issueDeviceNameWithReservation, (params)), + ed, + DEVICE_LABEL_A, + PERSONHOOD_LABEL_A + ); + } + + function test_registerBaseName_matches_issuePersonhoodName_on_a_linked_claim() public { + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), PERSONHOOD_LABEL_A); + _rootReservePersonhoodName( + IDotnsPopController.PersonhoodNameReservation({user: tiago, label: PERSONHOOD_LABEL_A}) + ); + IDotnsPopController.PersonhoodNameIssuance memory params = + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: _linkWithDeviceName(DEVICE_LABEL_A) + }); + + _assertSameEffect( + abi.encodeCall(IDotnsPopControllerLegacy.registerBaseName, (params)), + abi.encodeCall(IDotnsPopController.issuePersonhoodName, (params)), + ed, + DEVICE_LABEL_A, + PERSONHOOD_LABEL_A + ); + } + + function test_registerBaseName_matches_issuePersonhoodName_without_a_reservation() public { + IDotnsPopController.PersonhoodNameIssuance memory params = + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: _linkFresh(_validChatKey(0x02)) + }); + + _assertSameEffect( + abi.encodeCall(IDotnsPopControllerLegacy.registerBaseName, (params)), + abi.encodeCall(IDotnsPopController.issuePersonhoodName, (params)), + ed, + DEVICE_LABEL_A, + PERSONHOOD_LABEL_A + ); + } + + function test_legacy_reserveLiteName_issues_a_device_name() public { + vm.recordLogs(); + _rootLegacyReserveLiteName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: _validChatKey(0x01) + }) + ); + Vm.Log[] memory logs = vm.getRecordedLogs(); + + assertEq(dotnsRegistry.owner(_deviceNodeOf(DEVICE_LABEL_A)), ed); + assertTrue(dotnsPopController.isPopIssued(DEVICE_LABEL_A)); + assertEq(dotnsPopResolver.chatKey(_deviceNodeOf(DEVICE_LABEL_A)), _validChatKey(0x01)); + assertEq(_countEvents(logs, keccak256("DeviceNameIssued(bytes32,address,string)")), 1); + } + + function test_legacy_reserveBaseName_issues_and_reserves() public { + _rootLegacyReserveBaseName( + IDotnsPopController.DeviceNameIssuanceWithReservation({ + issuance: IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: "" + }), + reservedLabel: PERSONHOOD_LABEL_A + }) + ); + + assertEq(dotnsRegistry.owner(_deviceNodeOf(DEVICE_LABEL_A)), ed); + assertEq( + dotnsPopController.userReservation(ed).labelhash, + keccak256(bytes(PERSONHOOD_LABEL_A)), + "the reservation is queued" + ); + (bool reserved, address holder) = dotnsPopController.isReservedForClaim(PERSONHOOD_LABEL_A); + assertTrue(reserved); + assertEq(holder, ed); + } + + function test_legacy_registerBaseName_issues_a_personhood_name() public { + vm.recordLogs(); + _rootLegacyRegisterBaseName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: _linkFresh(_validChatKey(0x02)) + }) + ); + Vm.Log[] memory logs = vm.getRecordedLogs(); + + assertEq(dotnsRegistry.owner(_nodeOf(PERSONHOOD_LABEL_A)), ed); + assertTrue(dotnsPopController.isPopIssued(PERSONHOOD_LABEL_A)); + assertEq(_countEvents(logs, keccak256("PersonhoodNameIssued(bytes32,address,string)")), 1); + } + + // Label errors: legacy entrypoints revert with the legacy errors, replacements with the new + // ones + + /// @notice `_issueDeviceName`, shape check: a label with no separator. + function test_device_name_shape_error_on_both_paths() public { + IDotnsPopController.DeviceNameIssuance memory issuance = + IDotnsPopController.DeviceNameIssuance({label: "michael", user: ed, chatKey: ""}); + + vm.expectRevert(IDotnsPopControllerLegacy.InvalidLiteLabel.selector); + _rootLegacyReserveLiteName(issuance); + vm.expectRevert(IDotnsPopController.InvalidDeviceLabel.selector); + _rootIssueDeviceName(issuance); + + IDotnsPopController.DeviceNameIssuanceWithReservation memory combined = + IDotnsPopController.DeviceNameIssuanceWithReservation({ + issuance: issuance, reservedLabel: "" + }); + vm.expectRevert(IDotnsPopControllerLegacy.InvalidLiteLabel.selector); + _rootLegacyReserveBaseName(combined); + vm.expectRevert(IDotnsPopController.InvalidDeviceLabel.selector); + _rootIssueDeviceNameWithReservation(combined); + } + + /// @notice `_issueDeviceName`, classification check: a stem short enough to be reserved. + function test_device_name_reserved_stem_error_on_both_paths() public { + IDotnsPopController.DeviceNameIssuance memory issuance = + IDotnsPopController.DeviceNameIssuance({label: "abc.01", user: ed, chatKey: ""}); + + vm.expectRevert(IDotnsPopControllerLegacy.InvalidLiteLabel.selector); + _rootLegacyReserveLiteName(issuance); + vm.expectRevert(IDotnsPopController.InvalidDeviceLabel.selector); + _rootIssueDeviceName(issuance); + } + + /// @notice `_issuePersonhoodName`, classification check: a governance-reserved length. + function test_personhood_name_reserved_length_error_on_both_paths() public { + IDotnsPopController.PersonhoodNameIssuance memory issuance = + IDotnsPopController.PersonhoodNameIssuance({ + label: "abcde", user: ed, link: _linkFresh("") + }); + + vm.expectRevert(IDotnsPopControllerLegacy.InvalidBaseLabel.selector); + _rootLegacyRegisterBaseName(issuance); + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); + _rootIssuePersonhoodName(issuance); + } + + /// @notice `_issuePersonhoodName`, device-link shape check. + function test_device_link_shape_error_on_both_paths() public { + IDotnsPopController.PersonhoodNameIssuance memory issuance = + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: _linkWithDeviceName("michael") + }); + + vm.expectRevert(IDotnsPopControllerLegacy.InvalidLiteLabel.selector); + _rootLegacyRegisterBaseName(issuance); + vm.expectRevert(IDotnsPopController.InvalidDeviceLabel.selector); + _rootIssuePersonhoodName(issuance); + } + + /// @notice `_validatePersonhoodLabel`, shape check, from the personhood issuance and from the + /// reservation of the combined entrypoint. + function test_personhood_name_shape_error_on_both_paths() public { + IDotnsPopController.PersonhoodNameIssuance memory issuance = + IDotnsPopController.PersonhoodNameIssuance({ + label: "alice1", user: ed, link: _linkFresh("") + }); + vm.expectRevert(IDotnsPopControllerLegacy.InvalidBaseLabel.selector); + _rootLegacyRegisterBaseName(issuance); + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); + _rootIssuePersonhoodName(issuance); + + IDotnsPopController.DeviceNameIssuanceWithReservation memory combined = + _combined(DEVICE_LABEL_A, "alice1"); + vm.expectRevert(IDotnsPopControllerLegacy.InvalidBaseLabel.selector); + _rootLegacyReserveBaseName(combined); + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); + _rootIssueDeviceNameWithReservation(combined); + } + + /// @notice `_validatePersonhoodLabel` from the callers that are not legacy entrypoints: always + /// the new error. + function test_personhood_name_shape_error_on_non_legacy_callers() public { + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); + dotnsPopController.expireReservation("alice1"); + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); + dotnsPopController.isReservedForClaim("alice1"); + } + + /// @notice `_validateReservablePersonhoodLabel`, classification check: a reserved length. + function test_reservable_label_classification_error_on_both_paths() public { + IDotnsPopController.DeviceNameIssuanceWithReservation memory combined = + _combined(DEVICE_LABEL_A, "abcde"); + vm.expectRevert(IDotnsPopControllerLegacy.InvalidBaseLabel.selector); + _rootLegacyReserveBaseName(combined); + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); + _rootIssueDeviceNameWithReservation(combined); + + vm.expectRevert(IDotnsPopController.InvalidPersonhoodLabel.selector); + _rootReservePersonhoodName( + IDotnsPopController.PersonhoodNameReservation({user: ed, label: "abcde"}) + ); + } + + // Errors the legacy entrypoints share with their replacements + + function test_non_root_callers_are_rejected_on_both_paths() public { + IDotnsPopController.DeviceNameIssuance memory issuance = + IDotnsPopController.DeviceNameIssuance({label: DEVICE_LABEL_A, user: ed, chatKey: ""}); + bytes memory expected = abi.encodeWithSelector(IDotnsPopController.NotRoot.selector); + + vm.expectRevert(expected); + IDotnsPopControllerLegacy(address(dotnsPopController)).reserveLiteName(issuance); + vm.expectRevert(expected); + dotnsPopController.issueDeviceName(issuance); + } + + function test_invalid_chat_key_error_on_both_paths() public { + IDotnsPopController.DeviceNameIssuance memory issuance = + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: ed, chatKey: hex"01" + }); + bytes memory expected = + abi.encodeWithSelector(IDotnsPopController.InvalidChatKey.selector, uint256(1)); + _assertBothRevertWith( + abi.encodeCall(IDotnsPopControllerLegacy.reserveLiteName, (issuance)), + abi.encodeCall(IDotnsPopController.issueDeviceName, (issuance)), + expected + ); + + IDotnsPopController.PersonhoodNameIssuance memory personhood = + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: _linkFresh(hex"01") + }); + _assertBothRevertWith( + abi.encodeCall(IDotnsPopControllerLegacy.registerBaseName, (personhood)), + abi.encodeCall(IDotnsPopController.issuePersonhoodName, (personhood)), + expected + ); + } + + function test_second_device_name_issuance_error_on_both_paths() public { + IDotnsPopController.DeviceNameIssuance memory issuance = + IDotnsPopController.DeviceNameIssuance({label: DEVICE_LABEL_A, user: ed, chatKey: ""}); + _rootIssueDeviceName(issuance); + + bytes memory expected = + abi.encodeWithSelector(IDotnsPopController.DeviceNameAlreadyIssued.selector); + _assertBothRevertWith( + abi.encodeCall(IDotnsPopControllerLegacy.reserveLiteName, (issuance)), + abi.encodeCall(IDotnsPopController.issueDeviceName, (issuance)), + expected + ); + IDotnsPopController.DeviceNameIssuanceWithReservation memory combined = + _combined(DEVICE_LABEL_A, PERSONHOOD_LABEL_A); + _assertBothRevertWith( + abi.encodeCall(IDotnsPopControllerLegacy.reserveBaseName, (combined)), + abi.encodeCall(IDotnsPopController.issueDeviceNameWithReservation, (combined)), + expected + ); + } + + function test_device_link_the_user_does_not_own_error_on_both_paths() public { + _rootIssueDeviceName( + IDotnsPopController.DeviceNameIssuance({ + label: DEVICE_LABEL_A, user: tiago, chatKey: "" + }) + ); + IDotnsPopController.PersonhoodNameIssuance memory params = + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: _linkWithDeviceName(DEVICE_LABEL_A) + }); + + _assertBothRevertWith( + abi.encodeCall(IDotnsPopControllerLegacy.registerBaseName, (params)), + abi.encodeCall(IDotnsPopController.issuePersonhoodName, (params)), + abi.encodeWithSelector( + IDotnsPopController.DeviceNameNotOwned.selector, + ed, + keccak256(bytes(DEVICE_LABEL_A)) + ) + ); + } + + function test_registered_reserved_label_error_on_both_paths() public { + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: tiago, link: _linkFresh("") + }) + ); + IDotnsPopController.DeviceNameIssuanceWithReservation memory combined = + _combined(DEVICE_LABEL_A, PERSONHOOD_LABEL_A); + + _assertBothRevertWith( + abi.encodeCall(IDotnsPopControllerLegacy.reserveBaseName, (combined)), + abi.encodeCall(IDotnsPopController.issueDeviceNameWithReservation, (combined)), + abi.encodeWithSelector(IDotnsPopController.PersonhoodNameUnavailable.selector) + ); + } + + function test_label_held_by_another_waiter_error_on_both_paths() public { + _rootReservePersonhoodName( + IDotnsPopController.PersonhoodNameReservation({user: tiago, label: PERSONHOOD_LABEL_A}) + ); + IDotnsPopController.PersonhoodNameIssuance memory params = + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: _linkFresh("") + }); + + _assertBothRevertWith( + abi.encodeCall(IDotnsPopControllerLegacy.registerBaseName, (params)), + abi.encodeCall(IDotnsPopController.issuePersonhoodName, (params)), + abi.encodeWithSelector( + IDotnsPopController.NotHolder.selector, ed, keccak256(bytes(PERSONHOOD_LABEL_A)) + ) + ); + } + + function test_full_queue_error_on_both_paths() public { + for (uint256 i; i < dotnsPopController.MAX_RESERVATION_QUEUE(); ++i) { + _rootReservePersonhoodName( + IDotnsPopController.PersonhoodNameReservation({ + user: makeAddr(string.concat("waiter", vm.toString(i))), + label: PERSONHOOD_LABEL_A + }) + ); + } + IDotnsPopController.DeviceNameIssuanceWithReservation memory combined = + _combined(DEVICE_LABEL_A, PERSONHOOD_LABEL_A); + + _assertBothRevertWith( + abi.encodeCall(IDotnsPopControllerLegacy.reserveBaseName, (combined)), + abi.encodeCall(IDotnsPopController.issueDeviceNameWithReservation, (combined)), + abi.encodeWithSelector( + IDotnsPopController.QueueFull.selector, keccak256(bytes(PERSONHOOD_LABEL_A)) + ) + ); + } + + // Every reservation-queue exit emits an event + + function test_claim_emits_claimed_and_evicts_every_other_waiter() public { + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), PERSONHOOD_LABEL_A); + _rootReservePersonhoodName( + IDotnsPopController.PersonhoodNameReservation({user: tiago, label: PERSONHOOD_LABEL_A}) + ); + _rootReservePersonhoodName( + IDotnsPopController.PersonhoodNameReservation({ + user: leonardo, label: PERSONHOOD_LABEL_A + }) + ); + + vm.recordLogs(); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: _linkFresh(_validChatKey(0x02)) + }) + ); + Vm.Log[] memory logs = vm.getRecordedLogs(); + + bytes32 claimed = keccak256("ReservationClaimed(bytes32,address)"); + bytes32 evicted = keccak256("ReservationEvicted(bytes32,address)"); + assertEq(_countEventsFor(logs, claimed, ed), 1, "the claimant's claim"); + assertEq(_countEventsFor(logs, evicted, tiago), 1, "first waiter evicted"); + assertEq(_countEventsFor(logs, evicted, leonardo), 1, "second waiter evicted"); + assertEq(_countEventsFor(logs, evicted, ed), 0, "the claimant is not evicted"); + assertEq(dotnsPopController.userReservation(tiago).labelhash, bytes32(0)); + assertEq(dotnsPopController.userReservation(leonardo).labelhash, bytes32(0)); + } + + function test_standalone_issuance_relinquishes_the_users_other_reservation() public { + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), PERSONHOOD_LABEL_A); + + vm.recordLogs(); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_B, user: ed, link: _linkFresh(_validChatKey(0x02)) + }) + ); + Vm.Log[] memory logs = vm.getRecordedLogs(); + + bytes32 relinquished = keccak256("ReservationRelinquished(bytes32,address)"); + assertEq(_countEventsFor(logs, relinquished, ed), 1, "the dropped entry is recorded"); + assertEq(_countEvents(logs, keccak256("ReservationClaimed(bytes32,address)")), 0); + assertEq(dotnsPopController.userReservation(ed).labelhash, bytes32(0)); + } + + function test_re_reservation_relinquishes_the_previous_entry() public { + _rootReservePersonhoodName( + IDotnsPopController.PersonhoodNameReservation({user: ed, label: PERSONHOOD_LABEL_A}) + ); + + vm.recordLogs(); + _rootReservePersonhoodName( + IDotnsPopController.PersonhoodNameReservation({user: ed, label: PERSONHOOD_LABEL_B}) + ); + Vm.Log[] memory logs = vm.getRecordedLogs(); + + bytes32 relinquished = keccak256("ReservationRelinquished(bytes32,address)"); + assertEq(_countEventsFor(logs, relinquished, ed), 1); + assertEq( + dotnsPopController.userReservation(ed).labelhash, keccak256(bytes(PERSONHOOD_LABEL_B)) + ); + } + + function test_relinquishReservation_emits_exactly_once() public { + _rootReservePersonhoodName( + IDotnsPopController.PersonhoodNameReservation({user: ed, label: PERSONHOOD_LABEL_A}) + ); + + vm.recordLogs(); + vm.prank(ed); + dotnsPopController.relinquishReservation(); + Vm.Log[] memory logs = vm.getRecordedLogs(); + + assertEq(_countEvents(logs, keccak256("ReservationRelinquished(bytes32,address)")), 1); + } + + function test_issuance_without_a_reservation_emits_no_relinquish() public { + vm.recordLogs(); + _rootIssuePersonhoodName( + IDotnsPopController.PersonhoodNameIssuance({ + label: PERSONHOOD_LABEL_A, user: ed, link: _linkFresh(_validChatKey(0x02)) + }) + ); + Vm.Log[] memory logs = vm.getRecordedLogs(); + + assertEq(_countEvents(logs, keccak256("ReservationRelinquished(bytes32,address)")), 0); + } + + // Helpers + + function _combined( + string memory deviceLabel, + string memory reservedLabel + ) + internal + view + returns (IDotnsPopController.DeviceNameIssuanceWithReservation memory) + { + return IDotnsPopController.DeviceNameIssuanceWithReservation({ + issuance: IDotnsPopController.DeviceNameIssuance({ + label: deviceLabel, user: ed, chatKey: "" + }), + reservedLabel: reservedLabel + }); + } + + /// @dev Runs `legacyPayload` and `replacementPayload` from the same state and requires the same + /// events, in order and byte for byte, and the same observable state afterwards. + function _assertSameEffect( + bytes memory legacyPayload, + bytes memory replacementPayload, + address user, + string memory deviceLabel, + string memory personhoodLabel + ) + internal + { + uint256 snapshot = vm.snapshotState(); + vm.recordLogs(); + _dispatchFromRoot(legacyPayload); + Vm.Log[] memory legacyLogs = vm.getRecordedLogs(); + bytes memory legacyState = _observableState(user, deviceLabel, personhoodLabel); + + vm.revertToState(snapshot); + vm.recordLogs(); + _dispatchFromRoot(replacementPayload); + Vm.Log[] memory replacementLogs = vm.getRecordedLogs(); + + assertGt(legacyLogs.length, 0, "the call emits events"); + assertEq(legacyLogs.length, replacementLogs.length, "same number of events"); + for (uint256 i; i < legacyLogs.length; ++i) { + assertEq(legacyLogs[i].emitter, replacementLogs[i].emitter, "same emitter"); + assertEq( + abi.encode(legacyLogs[i].topics), + abi.encode(replacementLogs[i].topics), + "same topics" + ); + assertEq(legacyLogs[i].data, replacementLogs[i].data, "same data"); + } + assertEq(_observableState(user, deviceLabel, personhoodLabel), legacyState, "same state"); + } + + /// @dev Records, reservation and pending claims the gateway entrypoints write for `user`. + function _observableState( + address user, + string memory deviceLabel, + string memory personhoodLabel + ) + internal + view + returns (bytes memory) + { + bytes32 deviceNode = _deviceNodeOf(deviceLabel); + bytes32 personhoodNode = _nodeOf(personhoodLabel); + (bool reserved, address holder) = dotnsPopController.isReservedForClaim(personhoodLabel); + return bytes.concat( + abi.encode( + dotnsRegistry.owner(deviceNode), + dotnsRegistry.owner(personhoodNode), + dotnsPopController.isPopIssued(deviceLabel), + dotnsPopController.isPopIssued(personhoodLabel), + dotnsPopResolver.chatKey(deviceNode), + dotnsPopResolver.chatKey(personhoodNode) + ), + abi.encode( + dotnsPopResolver.deviceLabelhashOf(personhoodNode), + dotnsPopResolver.personhoodNodeOf(keccak256(bytes(deviceLabel))), + dotnsPopController.userReservation(user), + reserved, + holder, + dotnsPopController.pendingClaimCountOf(user) + ) + ); + } + + /// @dev Requires both payloads to revert under Root with exactly `expected`. + function _assertBothRevertWith( + bytes memory legacyPayload, + bytes memory replacementPayload, + bytes memory expected + ) + internal + { + assertEq(_rootRevertData(legacyPayload), expected, "legacy entrypoint"); + assertEq(_rootRevertData(replacementPayload), expected, "replacement"); + } + + function _rootRevertData(bytes memory payload) internal returns (bytes memory data) { + _mockOriginIsRoot(true); + bool ok; + (ok, data) = address(dotnsPopController).call(payload); + _mockOriginIsRoot(false); + assertFalse(ok, "the call reverts"); + } + + function _countEvents(Vm.Log[] memory logs, bytes32 sig) internal pure returns (uint256 count) { + for (uint256 i; i < logs.length; ++i) { + if (logs[i].topics.length != 0 && logs[i].topics[0] == sig) ++count; + } + } + + /// @dev Counts `sig` events whose second indexed topic is `user`; every reservation event + /// indexes the labelhash first and the user second. + function _countEventsFor( + Vm.Log[] memory logs, + bytes32 sig, + address user + ) + internal + pure + returns (uint256 count) + { + for (uint256 i; i < logs.length; ++i) { + if ( + logs[i].topics.length > 2 && logs[i].topics[0] == sig + && logs[i].topics[2] == bytes32(uint256(uint160(user))) + ) ++count; + } + } +} diff --git a/test/unit/registrar/DotnsRegistrar.t.sol b/test/unit/registrar/DotnsRegistrar.t.sol index 6092e1374..ed11cfb11 100644 --- a/test/unit/registrar/DotnsRegistrar.t.sol +++ b/test/unit/registrar/DotnsRegistrar.t.sol @@ -363,9 +363,9 @@ contract DotnsRegistrarTests is BaseDotns { function test_transfer_reverts_when_fee_required_but_no_value_attached() public { string memory label = "feerequired01"; - // Grant Full to ed so the mint succeeds, but leave leonardo at NoStatus so the + // Grant personhood to ed so the mint succeeds, but leave leonardo at NoStatus so the // transfer floor charges a non-zero downgrade fee. - _register(label, ed, IPopRules.PopStatus.PopFull); + _register(label, ed, IPopRules.PopStatus.Personhood); uint256 tokenId = _tokenIdForLabel(label); uint256 fee = dotnsRegistrar.quoteTransferFee(tokenId, leonardo); @@ -444,8 +444,8 @@ contract DotnsRegistrarTests is BaseDotns { assertTrue(dotnsRegistrar.supportsInterface(type(IERC721).interfaceId)); } - /// @notice Mints a name as if through the PoP gateway: the caller is the address registered - /// under `POP_CONTROLLER`, and the gateway path passes an empty label. + /// @notice Mints a name as if through the dotNS gateway pallet: the caller is the address + /// registered under `POP_CONTROLLER`, and the gateway path passes an empty label. function _mintSoulbound( string memory label, address nameOwner diff --git a/test/unit/registrar/DotnsRegistrarController.t.sol b/test/unit/registrar/DotnsRegistrarController.t.sol index 0fa85c112..d66ec3f93 100644 --- a/test/unit/registrar/DotnsRegistrarController.t.sol +++ b/test/unit/registrar/DotnsRegistrarController.t.sol @@ -146,7 +146,7 @@ contract DotnsRegistrarControllerTest is BaseDotns { function test_register_reverts_at_exact_expiry_boundary() public { string memory nameLabel = "alicebobx"; address nameOwner = ed; - _grantPopFull(nameOwner); + _grantPersonhood(nameOwner); IDotnsRegistrarController.Registration memory registration = IDotnsRegistrarController.Registration({ @@ -178,11 +178,11 @@ contract DotnsRegistrarControllerTest is BaseDotns { vm.stopPrank(); } - function test_register_popfull_wires_all_records() public { + function test_register_personhood_wires_all_records() public { string memory nameLabel = "web2summit"; address nameOwner = ed; - _grantPopFull(nameOwner); + _grantPersonhood(nameOwner); vm.prank(owner); storeFactory.deployLabelStoreFor(nameOwner); @@ -222,13 +222,13 @@ contract DotnsRegistrarControllerTest is BaseDotns { } /// @notice A two-digit second-level name is unreachable, which is what keeps a subname - /// from spelling a lite name. + /// from spelling a device name. /// @dev `joseph.42` reads either as one label or as `joseph` beneath `42`, and the second /// reading needs `42` to exist. No production controller entry point can create it: /// this path requires three characters, and both gateway paths are letters only. An /// owner-authorised controller calling the registrar directly is not bound by either, /// so the invariant holds over entry points rather than over the registrar. Pinned - /// here because it rests on a length floor that reads as unrelated to lite names. + /// here because it rests on a length floor that reads as unrelated to device names. /// `register` is called directly rather than through `_commitAndRegister`, which /// quotes `priceWithCheck` first and would revert on the reserved tier instead. No /// commitment is needed: the label is validated before one is consumed. @@ -250,21 +250,21 @@ contract DotnsRegistrarControllerTest is BaseDotns { dotnsRegistrarController.register(registration); } - /// @notice A public registration reserves no stem. + /// @notice A public registration reserves no base name. /// @dev PopRules' reservation slot belongs to the gateway queue, and `register` writes none - /// of its own. Pinned here because a stem the public path reserved silently would + /// of its own. Pinned here because a base name the public path reserved silently would /// block the gateway's own registrant for the whole reservation window. function test_public_register_reserves_no_base_name() public { string memory nameLabel = "lights01"; address nameOwner = ed; - _grantPopFull(nameOwner); + _grantPersonhood(nameOwner); _commitAndRegister(nameLabel, nameOwner, true); assertEq(dotnsRegistrar.ownerOf(_tokenIdForLabel(nameLabel)), nameOwner); (bool isReserved,,) = popRules.isBaseNameReserved(popRules.stripDigits(nameLabel)); - assertFalse(isReserved, "no stem reserved by a public registration"); + assertFalse(isReserved, "no base name reserved by a public registration"); } function test_register_does_not_overwrite_third_party_reverse_record() public { @@ -516,7 +516,7 @@ contract DotnsRegistrarControllerTest is BaseDotns { function test_transfer_writes_label_and_creates_store() public { string memory nameLabel = "alicetransfer01"; - _register(nameLabel, ed, IPopRules.PopStatus.PopFull); + _register(nameLabel, ed, IPopRules.PopStatus.Personhood); assertEq(storeFactory.getLabelStore(leonardo), address(0)); @@ -540,7 +540,7 @@ contract DotnsRegistrarControllerTest is BaseDotns { function test_transfer_back_skips_locked_entry() public { string memory nameLabel = "carolreturn01"; - _register(nameLabel, ed, IPopRules.PopStatus.PopFull); + _register(nameLabel, ed, IPopRules.PopStatus.Personhood); uint256 tokenId = _tokenIdForLabel(nameLabel); @@ -562,7 +562,7 @@ contract DotnsRegistrarControllerTest is BaseDotns { function test_transfer_clears_primary_reverse_name_when_current_name_is_moved() public { string memory nameLabel = "primarymove01"; - _register(nameLabel, ed, IPopRules.PopStatus.PopFull); + _register(nameLabel, ed, IPopRules.PopStatus.Personhood); uint256 tokenId = _tokenIdForLabel(nameLabel); assertEq(dotnsReverseResolver.nameOf(ed), "primarymove01.dot"); @@ -580,7 +580,7 @@ contract DotnsRegistrarControllerTest is BaseDotns { assertEq(storeFactory.getLabelStore(address(0)), address(0)); - _register(nameLabel, tiago, IPopRules.PopStatus.PopFull); + _register(nameLabel, tiago, IPopRules.PopStatus.Personhood); ILabelStore tiagoStore = ILabelStore(storeFactory.getLabelStore(tiago)); uint256 tokenId = _tokenIdForLabel(nameLabel); @@ -592,7 +592,7 @@ contract DotnsRegistrarControllerTest is BaseDotns { function test_safe_transfer_writes_to_store() public { string memory nameLabel = "safexfer01"; - _register(nameLabel, ed, IPopRules.PopStatus.PopFull); + _register(nameLabel, ed, IPopRules.PopStatus.Personhood); uint256 tokenId = _tokenIdForLabel(nameLabel); @@ -614,7 +614,7 @@ contract DotnsRegistrarControllerTest is BaseDotns { address payer = ed; address ownerAddr = leonardo; - _grantPopFull(payer); + _grantPersonhood(payer); bytes32 secret = keccak256(abi.encodePacked(popfullLabel, ownerAddr, block.timestamp)); IDotnsRegistrarController.Registration memory registration = @@ -644,7 +644,7 @@ contract DotnsRegistrarControllerTest is BaseDotns { IPopRules.OwnerStatusInsufficient.selector, popfullLabel, IPopRules.PopStatus.NoStatus, - IPopRules.PopStatus.PopFull + IPopRules.PopStatus.Personhood ) ); dotnsRegistrarController.register{value: charge}(registration); @@ -653,7 +653,7 @@ contract DotnsRegistrarControllerTest is BaseDotns { function test_transfer_via_approved_operator_writes_to_store() public { string memory nameLabel = "opxfer01"; - _register(nameLabel, ed, IPopRules.PopStatus.PopFull); + _register(nameLabel, ed, IPopRules.PopStatus.Personhood); uint256 tokenId = _tokenIdForLabel(nameLabel); @@ -1096,9 +1096,10 @@ contract DotnsRegistrarControllerTest is BaseDotns { // Regression: the paid path's governance-reserved rejection is unchanged by the grant gate. /// @dev A reserved-tier label (base length five or fewer) is refused on the paid path whoever - /// pays. The cross-payer branch distinguishes a governance-reserved label from a stem held by - /// another user, so it reverts `GovernanceReserved` rather than `NameReserved`. Reserved-tier - /// labels reach circulation only through `registerReserved`, and a grant does not change that. + /// pays. The cross-payer branch distinguishes a governance-reserved label from a base name + /// held by another user: this one reverts `GovernanceReserved`, and `NameReserved` is kept for + /// the held base name. Reserved-tier labels reach circulation only through `registerReserved`, + /// and a grant does not change that. function test_register_rejects_a_governance_reserved_label_for_a_cross_payer() public { string memory nameLabel = "alice"; diff --git a/test/unit/registrar/DotnsRegistrarControllerLifecycle.t.sol b/test/unit/registrar/DotnsRegistrarControllerLifecycle.t.sol index 5c00e657d..e58e8c649 100644 --- a/test/unit/registrar/DotnsRegistrarControllerLifecycle.t.sol +++ b/test/unit/registrar/DotnsRegistrarControllerLifecycle.t.sol @@ -20,15 +20,15 @@ import {ReentrantOverpaymentAttacker} from "../../helpers/ReentrantOverpaymentAt /// and reentrancy guarding. contract DotnsRegistrarControllerLifecycleTest is BaseDotns { /// @dev The subject is the reclaim path, not the tier: release to escrow, withdraw, let the - /// redeem window elapse, then a new owner reclaims. PopFull because `lights01` is - /// measured whole at eight characters and no public label reaches PopLite. + /// redeem window elapse, then a new owner reclaims. Personhood because `lights01` is + /// measured whole at eight characters and no public label reaches Devicehood. function test_register_reclaim_succeeds_for_new_owner() public { string memory label = "lights01"; address originalOwner = ed; address newOwner = leonardo; - _grantPopFull(originalOwner); - _grantPopFull(newOwner); + _grantPersonhood(originalOwner); + _grantPersonhood(newOwner); _commitAndRegister(label, originalOwner, true); @@ -69,10 +69,10 @@ contract DotnsRegistrarControllerLifecycleTest is BaseDotns { dotnsRegistrarController.register{value: registrationPrice}(registration); assertEq(dotnsRegistrar.ownerOf(tokenId), newOwner); - // A public registration reserves no stem, so reclaim has no slot to take over and none + // A public registration reserves no base name, so reclaim has no slot to take over and none // to garbage-collect. (bool isReserved,,) = popRules.isBaseNameReserved(popRules.stripDigits(label)); - assertFalse(isReserved, "no stem reserved by either registration"); + assertFalse(isReserved, "no base name reserved by either registration"); } function test_register_cross_payer_charges_max_not_sum_of_price_and_reach() public { @@ -80,7 +80,7 @@ contract DotnsRegistrarControllerLifecycleTest is BaseDotns { address payer = leonardo; address nameOwner = ed; - _grantPopFull(payer); + _grantPersonhood(payer); _grantNoStatus(nameOwner); uint256 ownerPrice = popRules.priceWithoutCheck(label, nameOwner).price; @@ -124,7 +124,7 @@ contract DotnsRegistrarControllerLifecycleTest is BaseDotns { address payer = leonardo; address nameOwner = ed; - _grantPopFull(payer); + _grantPersonhood(payer); _grantNoStatus(nameOwner); uint256 ownerPrice = popRules.priceWithoutCheck(label, nameOwner).price; @@ -200,7 +200,7 @@ contract DotnsRegistrarControllerLifecycleTest is BaseDotns { function test_register_creates_funded_position_for_self_registration() public { string memory label = BASE_LABEL_A; address nameOwner = ed; - _grantPopFull(nameOwner); + _grantPersonhood(nameOwner); IDotnsRegistrarController.Registration memory registration = IDotnsRegistrarController.Registration({ @@ -347,7 +347,7 @@ contract DotnsRegistrarControllerLifecycleTest is BaseDotns { string memory firstLabel = "primary01"; string memory secondLabel = "secondary01"; - _grantPopLite(ed); + _grantDevicehood(ed); _commitAndRegister(firstLabel, ed, true); assertEq(dotnsReverseResolver.nameOf(ed), string.concat(firstLabel, ".dot")); diff --git a/test/unit/registry/DotnsProtocolRegistryDeclarations.t.sol b/test/unit/registry/DotnsProtocolRegistryDeclarations.t.sol index bd02417dd..ea0c81f61 100644 --- a/test/unit/registry/DotnsProtocolRegistryDeclarations.t.sol +++ b/test/unit/registry/DotnsProtocolRegistryDeclarations.t.sol @@ -169,7 +169,7 @@ contract DotnsProtocolRegistryDeclarationTests is BaseDotns { assertEq(registry.expectedCodehash(DotnsConstants.REGISTRAR), bytes32(0)); } - /// @notice `version()` on every DotNS contract mirrors the registry's declared release, so + /// @notice `version()` on every dotNS contract mirrors the registry's declared release, so /// the whole network answers with one synchronised value: empty before the first /// declaration, the declared tag after, with no per-contract bookkeeping. Sampled /// across every mirror the fixture deploys, including a claimed user store. diff --git a/test/unit/registry/DotnsRegistry.t.sol b/test/unit/registry/DotnsRegistry.t.sol index 3d29cb57a..5082d4337 100644 --- a/test/unit/registry/DotnsRegistry.t.sol +++ b/test/unit/registry/DotnsRegistry.t.sol @@ -41,7 +41,7 @@ contract DotnsRegistryTests is BaseDotns { } /// @notice A separator is never valid in a subname label. - /// @dev This is what reserves the dotted-label space to the PoP gateway. Loosen it and a + /// @dev This is what reserves the dotted-label space to the gateway path. Loosen it and a /// subname could be minted whose label equals a person's, putting two claimants on one /// node. Both subnode entry points are checked, since either would open the space. function test_subname_label_carrying_a_separator_is_rejected() public { @@ -75,7 +75,7 @@ contract DotnsRegistryTests is BaseDotns { } /// @notice A subname created outside the gateway carries no person provenance. - /// @dev The gateway issues a lite name as the subname `michael` under `01` and records it in + /// @dev The gateway issues a device name as the subname `michael` under `01` and records it in /// `_popIssued`. This test creates the same subname directly, without the gateway, and /// shows it reads back as not `isPopIssued`: provenance, not the name text, is what marks /// a gateway-issued person. No production controller entry point can create the parent here: @@ -378,7 +378,7 @@ contract DotnsRegistryTests is BaseDotns { function test_revert_subnode_owner_with_parent_label_mismatch() public { string memory parentLabel = "actualparent01"; - bytes32 parentNode = _register(parentLabel, ed, IPopRules.PopStatus.PopFull); + bytes32 parentNode = _register(parentLabel, ed, IPopRules.PopStatus.Personhood); IDotnsRegistry.SubnodeRecord memory subnodeRecord = IDotnsRegistry.SubnodeRecord({ parentNode: parentNode, @@ -616,8 +616,8 @@ contract DotnsRegistryTests is BaseDotns { function test_same_sublabel_under_different_parents_owned_by_same_address() public { string memory parentLabelA = "alphaomega"; string memory parentLabelB = "bravobro"; - bytes32 parentNodeA = _register(parentLabelA, owner, IPopRules.PopStatus.PopFull); - bytes32 parentNodeB = _register(parentLabelB, owner, IPopRules.PopStatus.PopFull); + bytes32 parentNodeA = _register(parentLabelA, owner, IPopRules.PopStatus.Personhood); + bytes32 parentNodeB = _register(parentLabelB, owner, IPopRules.PopStatus.Personhood); string memory subLabel = "app"; diff --git a/test/unit/resolver/DotnsPopResolver.t.sol b/test/unit/resolver/DotnsPopResolver.t.sol index b3cc5d033..8aee730ab 100644 --- a/test/unit/resolver/DotnsPopResolver.t.sol +++ b/test/unit/resolver/DotnsPopResolver.t.sol @@ -31,26 +31,26 @@ contract DotnsPopResolverTests is BaseDotns { dotnsPopResolver.setChatKey(_nodeOf("alice42"), _validChatKey(0x04)); } - function test_setLiteLink_writes_and_emits() public { - bytes32 fullNode = _nodeOf("alice"); - bytes32 liteLabelhash = keccak256(bytes("alice42")); + function test_setDeviceLink_writes_and_emits() public { + bytes32 personhoodNode = _nodeOf("alice"); + bytes32 deviceLabelhash = keccak256(bytes("alice42")); vm.prank(address(dotnsPopController)); vm.expectEmit(true, true, false, false); - emit IDotnsPopResolver.LiteLinkUpdated(fullNode, liteLabelhash); - dotnsPopResolver.setLiteLink(fullNode, liteLabelhash); + emit IDotnsPopResolver.DeviceLinkUpdated(personhoodNode, deviceLabelhash); + dotnsPopResolver.setDeviceLink(personhoodNode, deviceLabelhash); // Both directions must be populated by a single write: forward - // (full => lite) and reverse (lite => full). The reverse index is what - // downstream consumers (Nova) use to answer "given this lite username, - // which full name did they claim?". - assertEq(dotnsPopResolver.liteLink(fullNode), liteLabelhash); - assertEq(dotnsPopResolver.fullClaim(liteLabelhash), fullNode); + // (personhood => device) and reverse (device => personhood). The reverse index is what + // downstream consumers (Nova) use to answer "given this device name, + // which personhood name did they claim?". + assertEq(dotnsPopResolver.deviceLabelhashOf(personhoodNode), deviceLabelhash); + assertEq(dotnsPopResolver.personhoodNodeOf(deviceLabelhash), personhoodNode); } - function test_setLiteLink_reverts_for_unauthorised_caller() public { + function test_setDeviceLink_reverts_for_unauthorised_caller() public { vm.prank(ed); vm.expectRevert(abi.encodeWithSelector(IDotnsPopResolver.NotPopController.selector, ed)); - dotnsPopResolver.setLiteLink(_nodeOf("alice"), keccak256(bytes("alice42"))); + dotnsPopResolver.setDeviceLink(_nodeOf("alice"), keccak256(bytes("alice42"))); } function test_setChatKey_accepts_zero_node_as_passthrough() public { @@ -67,11 +67,11 @@ contract DotnsPopResolverTests is BaseDotns { assertEq(dotnsPopResolver.chatKey(bytes32(0)), key); } - function test_setLiteLink_accepts_zero_inputs_as_passthrough() public { + function test_setDeviceLink_accepts_zero_inputs_as_passthrough() public { vm.prank(address(dotnsPopController)); - dotnsPopResolver.setLiteLink(bytes32(0), bytes32(0)); - assertEq(dotnsPopResolver.liteLink(bytes32(0)), bytes32(0)); - assertEq(dotnsPopResolver.fullClaim(bytes32(0)), bytes32(0)); + dotnsPopResolver.setDeviceLink(bytes32(0), bytes32(0)); + assertEq(dotnsPopResolver.deviceLabelhashOf(bytes32(0)), bytes32(0)); + assertEq(dotnsPopResolver.personhoodNodeOf(bytes32(0)), bytes32(0)); } function test_rotating_pop_controller_changes_authorised_writer() public { @@ -174,156 +174,156 @@ contract DotnsPopResolverTests is BaseDotns { dotnsPopResolver.setChatKey(_nodeOf("alice42"), key); } - function test_setLiteLink_same_full_node_relink_clears_old_reverse() public { - bytes32 fullA = _nodeOf("alice"); - bytes32 liteX = keccak256(bytes("alice42")); - bytes32 liteY = keccak256(bytes("alice99")); + function test_setDeviceLink_same_personhood_node_relink_clears_old_reverse() public { + bytes32 personhoodA = _nodeOf("alice"); + bytes32 deviceX = keccak256(bytes("alice42")); + bytes32 deviceY = keccak256(bytes("alice99")); vm.startPrank(address(dotnsPopController)); - dotnsPopResolver.setLiteLink(fullA, liteX); - dotnsPopResolver.setLiteLink(fullA, liteY); + dotnsPopResolver.setDeviceLink(personhoodA, deviceX); + dotnsPopResolver.setDeviceLink(personhoodA, deviceY); vm.stopPrank(); // Old reverse must be cleared so downstream consumers stop resolving - // `liteX` to `fullA`. - assertEq(dotnsPopResolver.fullClaim(liteX), bytes32(0)); + // `deviceX` to `personhoodA`. + assertEq(dotnsPopResolver.personhoodNodeOf(deviceX), bytes32(0)); // New pair round-trips cleanly. - assertEq(dotnsPopResolver.liteLink(fullA), liteY); - assertEq(dotnsPopResolver.fullClaim(liteY), fullA); + assertEq(dotnsPopResolver.deviceLabelhashOf(personhoodA), deviceY); + assertEq(dotnsPopResolver.personhoodNodeOf(deviceY), personhoodA); } - function test_setLiteLink_same_lite_relink_clears_old_forward() public { - bytes32 fullA = _nodeOf("alice"); - bytes32 fullB = _nodeOf("bob"); - bytes32 liteX = keccak256(bytes("alice42")); + function test_setDeviceLink_same_device_relink_clears_old_forward() public { + bytes32 personhoodA = _nodeOf("alice"); + bytes32 personhoodB = _nodeOf("bob"); + bytes32 deviceX = keccak256(bytes("alice42")); vm.startPrank(address(dotnsPopController)); - dotnsPopResolver.setLiteLink(fullA, liteX); - dotnsPopResolver.setLiteLink(fullB, liteX); + dotnsPopResolver.setDeviceLink(personhoodA, deviceX); + dotnsPopResolver.setDeviceLink(personhoodB, deviceX); vm.stopPrank(); - // Old forward must be cleared so `fullA` no longer claims `liteX`. - assertEq(dotnsPopResolver.liteLink(fullA), bytes32(0)); + // Old forward must be cleared so `personhoodA` no longer claims `deviceX`. + assertEq(dotnsPopResolver.deviceLabelhashOf(personhoodA), bytes32(0)); // New pair round-trips. - assertEq(dotnsPopResolver.liteLink(fullB), liteX); - assertEq(dotnsPopResolver.fullClaim(liteX), fullB); + assertEq(dotnsPopResolver.deviceLabelhashOf(personhoodB), deviceX); + assertEq(dotnsPopResolver.personhoodNodeOf(deviceX), personhoodB); } - function test_setLiteLink_idempotent_relink_keeps_both_indices() public { - bytes32 fullA = _nodeOf("alice"); - bytes32 liteX = keccak256(bytes("alice42")); + function test_setDeviceLink_idempotent_relink_keeps_both_indices() public { + bytes32 personhoodA = _nodeOf("alice"); + bytes32 deviceX = keccak256(bytes("alice42")); vm.startPrank(address(dotnsPopController)); - dotnsPopResolver.setLiteLink(fullA, liteX); - dotnsPopResolver.setLiteLink(fullA, liteX); + dotnsPopResolver.setDeviceLink(personhoodA, deviceX); + dotnsPopResolver.setDeviceLink(personhoodA, deviceX); vm.stopPrank(); // Writing the same pair twice must not accidentally delete either - // side: the `oldLite == liteLabelhash` and `oldFull == fullNode` + // side: the `oldDevice == deviceLabelhash` and `oldPersonhood == personhoodNode` // guards in the implementation are the things under test here. - assertEq(dotnsPopResolver.liteLink(fullA), liteX); - assertEq(dotnsPopResolver.fullClaim(liteX), fullA); + assertEq(dotnsPopResolver.deviceLabelhashOf(personhoodA), deviceX); + assertEq(dotnsPopResolver.personhoodNodeOf(deviceX), personhoodA); } - function test_setLiteLink_chain_returns_to_original_without_drift() public { - bytes32 fullA = _nodeOf("alice"); - bytes32 liteX = keccak256(bytes("alice42")); - bytes32 liteY = keccak256(bytes("alice99")); + function test_setDeviceLink_chain_returns_to_original_without_drift() public { + bytes32 personhoodA = _nodeOf("alice"); + bytes32 deviceX = keccak256(bytes("alice42")); + bytes32 deviceY = keccak256(bytes("alice99")); vm.startPrank(address(dotnsPopController)); - dotnsPopResolver.setLiteLink(fullA, liteX); - dotnsPopResolver.setLiteLink(fullA, liteY); - dotnsPopResolver.setLiteLink(fullA, liteX); + dotnsPopResolver.setDeviceLink(personhoodA, deviceX); + dotnsPopResolver.setDeviceLink(personhoodA, deviceY); + dotnsPopResolver.setDeviceLink(personhoodA, deviceX); vm.stopPrank(); // After A -> X -> Y -> X, only the (A, X) pair survives. - assertEq(dotnsPopResolver.liteLink(fullA), liteX); - assertEq(dotnsPopResolver.fullClaim(liteX), fullA); - assertEq(dotnsPopResolver.fullClaim(liteY), bytes32(0)); + assertEq(dotnsPopResolver.deviceLabelhashOf(personhoodA), deviceX); + assertEq(dotnsPopResolver.personhoodNodeOf(deviceX), personhoodA); + assertEq(dotnsPopResolver.personhoodNodeOf(deviceY), bytes32(0)); } - function test_setLiteLink_cross_chain_no_drift() public { - bytes32 fullA = _nodeOf("alice"); - bytes32 fullB = _nodeOf("bob"); - bytes32 liteX = keccak256(bytes("alice42")); + function test_setDeviceLink_cross_chain_no_drift() public { + bytes32 personhoodA = _nodeOf("alice"); + bytes32 personhoodB = _nodeOf("bob"); + bytes32 deviceX = keccak256(bytes("alice42")); vm.startPrank(address(dotnsPopController)); - dotnsPopResolver.setLiteLink(fullA, liteX); - dotnsPopResolver.setLiteLink(fullB, liteX); + dotnsPopResolver.setDeviceLink(personhoodA, deviceX); + dotnsPopResolver.setDeviceLink(personhoodB, deviceX); vm.stopPrank(); // A must no longer appear as a claimant of anything, only B. - assertEq(dotnsPopResolver.liteLink(fullA), bytes32(0)); - assertEq(dotnsPopResolver.liteLink(fullB), liteX); - assertEq(dotnsPopResolver.fullClaim(liteX), fullB); + assertEq(dotnsPopResolver.deviceLabelhashOf(personhoodA), bytes32(0)); + assertEq(dotnsPopResolver.deviceLabelhashOf(personhoodB), deviceX); + assertEq(dotnsPopResolver.personhoodNodeOf(deviceX), personhoodB); } - function test_setLiteLink_quadrangle_clears_both_stale_inverses() public { - bytes32 fullA = _nodeOf("alice"); - bytes32 fullB = _nodeOf("bob"); - bytes32 liteX = keccak256(bytes("alice42")); - bytes32 liteY = keccak256(bytes("bob42")); + function test_setDeviceLink_quadrangle_clears_both_stale_inverses() public { + bytes32 personhoodA = _nodeOf("alice"); + bytes32 personhoodB = _nodeOf("bob"); + bytes32 deviceX = keccak256(bytes("alice42")); + bytes32 deviceY = keccak256(bytes("bob42")); vm.startPrank(address(dotnsPopController)); - dotnsPopResolver.setLiteLink(fullA, liteX); - dotnsPopResolver.setLiteLink(fullB, liteY); - dotnsPopResolver.setLiteLink(fullA, liteY); + dotnsPopResolver.setDeviceLink(personhoodA, deviceX); + dotnsPopResolver.setDeviceLink(personhoodB, deviceY); + dotnsPopResolver.setDeviceLink(personhoodA, deviceY); vm.stopPrank(); // After (A,X), (B,Y), (A,Y): only (A,Y) remains. B's forward link // and X's reverse link must both be cleared. - assertEq(dotnsPopResolver.liteLink(fullA), liteY); - assertEq(dotnsPopResolver.liteLink(fullB), bytes32(0)); - assertEq(dotnsPopResolver.fullClaim(liteY), fullA); - assertEq(dotnsPopResolver.fullClaim(liteX), bytes32(0)); + assertEq(dotnsPopResolver.deviceLabelhashOf(personhoodA), deviceY); + assertEq(dotnsPopResolver.deviceLabelhashOf(personhoodB), bytes32(0)); + assertEq(dotnsPopResolver.personhoodNodeOf(deviceY), personhoodA); + assertEq(dotnsPopResolver.personhoodNodeOf(deviceX), bytes32(0)); } - function test_setLiteLink_with_zero_lite_is_passthrough() public { - bytes32 fullA = _nodeOf("alice"); + function test_setDeviceLink_with_zero_device_is_passthrough() public { + bytes32 personhoodA = _nodeOf("alice"); // Pin current behaviour for the zero-hash edge: the setter does not - // revert on a zero `liteLabelhash` and writes both indices at the + // revert on a zero `deviceLabelhash` and writes both indices at the // zero key. Any future validator that rejects zero inputs lands // here as a failing assertion. vm.prank(address(dotnsPopController)); - dotnsPopResolver.setLiteLink(fullA, bytes32(0)); + dotnsPopResolver.setDeviceLink(personhoodA, bytes32(0)); - assertEq(dotnsPopResolver.liteLink(fullA), bytes32(0)); - assertEq(dotnsPopResolver.fullClaim(bytes32(0)), fullA); + assertEq(dotnsPopResolver.deviceLabelhashOf(personhoodA), bytes32(0)); + assertEq(dotnsPopResolver.personhoodNodeOf(bytes32(0)), personhoodA); } - function test_setLiteLink_long_chain_invariant_holds_at_every_step() public { - // Ten sequential re-links of the same `fullNode` to fresh lite + function test_setDeviceLink_long_chain_invariant_holds_at_every_step() public { + // Ten sequential re-links of the same `personhoodNode` to fresh device-name // labelhashes. At each step the forward and reverse indices must - // round-trip for the current pair, and the previous lite's reverse + // round-trip for the current pair, and the previous device name's reverse // entry must have been cleared. - bytes32 fullA = _nodeOf("alice"); - bytes32 previousLite = bytes32(0); + bytes32 personhoodA = _nodeOf("alice"); + bytes32 previousDevice = bytes32(0); vm.startPrank(address(dotnsPopController)); for (uint256 i = 1; i <= 10; i++) { - bytes32 currentLite = keccak256(abi.encodePacked("alice", i)); - dotnsPopResolver.setLiteLink(fullA, currentLite); + bytes32 currentDevice = keccak256(abi.encodePacked("alice", i)); + dotnsPopResolver.setDeviceLink(personhoodA, currentDevice); // Current pair round-trips. - assertEq(dotnsPopResolver.liteLink(fullA), currentLite); - assertEq(dotnsPopResolver.fullClaim(currentLite), fullA); + assertEq(dotnsPopResolver.deviceLabelhashOf(personhoodA), currentDevice); + assertEq(dotnsPopResolver.personhoodNodeOf(currentDevice), personhoodA); // Previous reverse entry was nulled. - if (previousLite != bytes32(0)) { - assertEq(dotnsPopResolver.fullClaim(previousLite), bytes32(0)); + if (previousDevice != bytes32(0)) { + assertEq(dotnsPopResolver.personhoodNodeOf(previousDevice), bytes32(0)); } - previousLite = currentLite; + previousDevice = currentDevice; } vm.stopPrank(); } - function test_setLiteLink_old_lite_reads_zero_after_relink() public { - // Integration-shaped assertion: a consumer that cached the old lite - // hash and later queries `fullClaim` must see `bytes32(0)`, not a - // stale fullNode. - bytes32 fullA = _nodeOf("alice"); - bytes32 liteX = keccak256(bytes("alice42")); - bytes32 liteY = keccak256(bytes("alice99")); + function test_setDeviceLink_old_device_reads_zero_after_relink() public { + // Integration-shaped assertion: a consumer that cached the old device-name + // hash and later queries `personhoodNodeOf` must see `bytes32(0)`, not a + // stale personhoodNode. + bytes32 personhoodA = _nodeOf("alice"); + bytes32 deviceX = keccak256(bytes("alice42")); + bytes32 deviceY = keccak256(bytes("alice99")); vm.startPrank(address(dotnsPopController)); - dotnsPopResolver.setLiteLink(fullA, liteX); - dotnsPopResolver.setLiteLink(fullA, liteY); + dotnsPopResolver.setDeviceLink(personhoodA, deviceX); + dotnsPopResolver.setDeviceLink(personhoodA, deviceY); vm.stopPrank(); - assertEq(dotnsPopResolver.fullClaim(liteX), bytes32(0)); + assertEq(dotnsPopResolver.personhoodNodeOf(deviceX), bytes32(0)); } // 65-byte chat-key helper now lives on BaseDotns as `_validChatKey`. } diff --git a/test/unit/resolver/DotnsReverseResolver.t.sol b/test/unit/resolver/DotnsReverseResolver.t.sol index 2205f46c7..55d0b576a 100644 --- a/test/unit/resolver/DotnsReverseResolver.t.sol +++ b/test/unit/resolver/DotnsReverseResolver.t.sol @@ -11,10 +11,10 @@ import {IDotnsReverseResolver} from "../../../contracts/resolvers/IDotnsReverseR /// @custom:function nameOf read. contract DotnsReverseResolverTests is BaseDotns { /// @notice Label fixture used by tests that need a labelled, transferable name. - /// @dev Stem of nine characters with a two-digit suffix; classifies as NoStatus. + /// @dev Eleven characters measured as written, digits included; classifies as NoStatus. string internal constant CLAIM_LABEL = "claimuser01"; /// @notice Secondary label fixture used by overwrite and transfer tests. - /// @dev Stem of nine characters with a two-digit suffix; classifies as NoStatus. + /// @dev Twelve characters measured as written, digits included; classifies as NoStatus. string internal constant ALT_LABEL = "secondname01"; function test_nameof_returns_empty_when_unset() public view { @@ -116,18 +116,18 @@ contract DotnsReverseResolverTests is BaseDotns { assertEq(dotnsReverseResolver.nameOf(leonardo), string.concat(CLAIM_LABEL, ".dot")); } - /// @notice A lite username owner can claim and reverse-resolve it. - /// @dev A lite name is a registry subname, not a token, so the claim reads ownership through + /// @notice A device name owner can claim and reverse-resolve it. + /// @dev A device name is a registry subname, not a token, so the claim reads ownership through /// the registry at the stem-under-container node rather than from the registrar's ERC-721 /// ledger, and the reverse read resolves the same node. - function test_claim_and_nameof_for_a_lite_username() public { - _grantPopFull(ed); - _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), ""); + function test_claim_and_nameof_for_a_device_name() public { + _grantPersonhood(ed); + _reservePop(ed, DEVICE_LABEL_A, _validChatKey(0x01), ""); vm.prank(ed); - dotnsReverseResolver.claimReverseRecord(LITE_LABEL_A); + dotnsReverseResolver.claimReverseRecord(DEVICE_LABEL_A); - assertEq(dotnsReverseResolver.nameOf(ed), string.concat(LITE_LABEL_A, ".dot")); + assertEq(dotnsReverseResolver.nameOf(ed), string.concat(DEVICE_LABEL_A, ".dot")); } function test_nameof_fails_closed_when_caller_no_longer_owns_stored_name() public { diff --git a/test/unit/utils/StringUtils.t.sol b/test/unit/utils/StringUtils.t.sol index 9222a2063..15ad10615 100644 --- a/test/unit/utils/StringUtils.t.sol +++ b/test/unit/utils/StringUtils.t.sol @@ -17,16 +17,16 @@ contract StringUtilsHarness { return value.isSingleLabel(); } - function isPersonLabel(string calldata value) external pure returns (bool) { - return value.isPersonLabel(); + function isPersonhoodLabel(string calldata value) external pure returns (bool) { + return value.isPersonhoodLabel(); } - function isLitePersonLabel(string calldata value) external pure returns (bool) { - return value.isLitePersonLabel(); + function isDeviceLabel(string calldata value) external pure returns (bool) { + return value.isDeviceLabel(); } - function isLitePersonLabelMemory(string calldata value) external pure returns (bool) { - return StringUtils.isLitePersonLabelMemory(value); + function isDeviceLabelMemory(string calldata value) external pure returns (bool) { + return StringUtils.isDeviceLabelMemory(value); } function isNamePath(string calldata value) external pure returns (bool) { @@ -35,8 +35,8 @@ contract StringUtilsHarness { } /// @title StringUtilsTests -/// @notice Unit tests for the lite-label shape predicate and the letters-only name predicate. -/// @dev A lite label is the only shape in DotNS permitted to carry a separator, and its stem +/// @notice Unit tests for the device-name shape predicate and the letters-only name predicate. +/// @dev A device name is the only shape in dotNS permitted to carry a separator, and its stem /// is letters only, which is stricter than a DNS label. A digit suffix is not exclusive: /// an ordinary label may end in digits. These tests are the definition of that boundary. contract StringUtilsTests is Test { @@ -56,87 +56,87 @@ contract StringUtilsTests is Test { } /// @notice Asserts both data locations agree, then that they agree with `expected`. - function _assertLite(string memory value, bool expected) internal view { - bool fromCalldata = utils.isLitePersonLabel(value); - bool fromMemory = utils.isLitePersonLabelMemory(value); + function _assertDeviceLabel(string memory value, bool expected) internal view { + bool fromCalldata = utils.isDeviceLabel(value); + bool fromMemory = utils.isDeviceLabelMemory(value); assertEq(fromCalldata, fromMemory, "calldata and memory variants disagree"); assertEq(fromCalldata, expected, value); } - /// @notice The same letters-only rule a lite stem uses, applied to a whole label. - /// @dev Mirrors the gateway pallet's `is_valid_person`, so a full-person label admits no + /// @notice The same letters-only rule a device-name stem uses, applied to a whole label. + /// @dev Mirrors the gateway pallet's `is_valid_person`, so a personhood name admits no /// digits and no hyphens. No floor on length: that is the governance-reserved band. - function test_isPersonLabel_accepts_letters_only() public view { - assertTrue(utils.isPersonLabel("alicebob")); - assertTrue(utils.isPersonLabel("a")); - assertTrue(utils.isPersonLabel(_stem(63))); + function test_isPersonhoodLabel_accepts_letters_only() public view { + assertTrue(utils.isPersonhoodLabel("alicebob")); + assertTrue(utils.isPersonhoodLabel("a")); + assertTrue(utils.isPersonhoodLabel(_stem(63))); - assertFalse(utils.isPersonLabel("alice-bob"), "no hyphens"); - assertFalse(utils.isPersonLabel("micha3l"), "no interior digits"); - assertFalse(utils.isPersonLabel("alicebob01"), "no digit suffix"); - assertFalse(utils.isPersonLabel("Alicebob"), "no uppercase"); - assertFalse(utils.isPersonLabel(""), "not empty"); - assertFalse(utils.isPersonLabel(_stem(64)), "bounded at 63 octets"); + assertFalse(utils.isPersonhoodLabel("alice-bob"), "no hyphens"); + assertFalse(utils.isPersonhoodLabel("micha3l"), "no interior digits"); + assertFalse(utils.isPersonhoodLabel("alicebob01"), "no digit suffix"); + assertFalse(utils.isPersonhoodLabel("Alicebob"), "no uppercase"); + assertFalse(utils.isPersonhoodLabel(""), "not empty"); + assertFalse(utils.isPersonhoodLabel(_stem(64)), "bounded at 63 octets"); } - function test_isLitePersonLabel_accepts_the_dotted_shape() public view { - _assertLite("joseph.42", true); - _assertLite("joseph.00", true); - _assertLite("elizabeth.42", true); - _assertLite(string.concat(_stem(63), ".42"), true); + function test_isDeviceLabel_accepts_the_dotted_shape() public view { + _assertDeviceLabel("joseph.42", true); + _assertDeviceLabel("joseph.00", true); + _assertDeviceLabel("elizabeth.42", true); + _assertDeviceLabel(string.concat(_stem(63), ".42"), true); } /// @dev The shape puts no floor on the stem: how short a name may be is policy, held by - /// PopRules as the governance-reserved band, so a short stem is a well-formed lite + /// PopRules as the governance-reserved band, so a short stem is a well-formed device-name /// label that classification then rejects. - function test_isLitePersonLabel_puts_no_floor_on_the_stem() public view { - _assertLite("a.42", true); - _assertLite("josep.42", true); - _assertLite(string.concat(_stem(1), ".42"), true); + function test_isDeviceLabel_puts_no_floor_on_the_stem() public view { + _assertDeviceLabel("a.42", true); + _assertDeviceLabel("josep.42", true); + _assertDeviceLabel(string.concat(_stem(1), ".42"), true); } /// @dev The stem is the name a person chose, which People Chain restricts to lowercase /// letters, so it is stricter than a DNS label: no digits, no hyphens, no uppercase. - function test_isLitePersonLabel_requires_a_letters_only_stem() public view { - _assertLite("alice-bob.99", false); - _assertLite("josep4.42", false); - _assertLite("jos3ph.42", false); - _assertLite("joseph1.42", false); - _assertLite("Joseph.42", false); + function test_isDeviceLabel_requires_a_letters_only_stem() public view { + _assertDeviceLabel("alice-bob.99", false); + _assertDeviceLabel("josep4.42", false); + _assertDeviceLabel("jos3ph.42", false); + _assertDeviceLabel("joseph1.42", false); + _assertDeviceLabel("Joseph.42", false); } - function test_isLitePersonLabel_rejects_a_missing_or_repeated_separator() public view { - _assertLite("joseph42", false); - _assertLite("jos.eph.42", false); - _assertLite(".42", false); - _assertLite("joseph.", false); + function test_isDeviceLabel_rejects_a_missing_or_repeated_separator() public view { + _assertDeviceLabel("joseph42", false); + _assertDeviceLabel("jos.eph.42", false); + _assertDeviceLabel(".42", false); + _assertDeviceLabel("joseph.", false); } - function test_isLitePersonLabel_rejects_any_suffix_but_two_digits() public view { - _assertLite("joseph.4", false); - _assertLite("joseph.123", false); - _assertLite("joseph.4x", false); - _assertLite("joseph.x4", false); + function test_isDeviceLabel_rejects_any_suffix_but_two_digits() public view { + _assertDeviceLabel("joseph.4", false); + _assertDeviceLabel("joseph.123", false); + _assertDeviceLabel("joseph.4x", false); + _assertDeviceLabel("joseph.x4", false); } - function test_isLitePersonLabel_rejects_a_non_canonical_stem() public view { - _assertLite("jos_eph.42", false); - _assertLite("-joseph.42", false); - _assertLite("joseph-.42", false); - _assertLite(string.concat(_stem(64), ".42"), false); + function test_isDeviceLabel_rejects_a_non_canonical_stem() public view { + _assertDeviceLabel("jos_eph.42", false); + _assertDeviceLabel("-joseph.42", false); + _assertDeviceLabel("joseph-.42", false); + _assertDeviceLabel(string.concat(_stem(64), ".42"), false); } - function test_isLitePersonLabel_rejects_the_empty_string() public view { - _assertLite("", false); + function test_isDeviceLabel_rejects_the_empty_string() public view { + _assertDeviceLabel("", false); } - /// @dev The bound moved from the whole label to the stem, so a lite label runs three octets + /// @dev The bound moved from the whole label to the stem, so a device name runs three octets /// past the single-label limit. Pinned because it is a deliberate widening. - function test_isLitePersonLabel_bounds_the_stem_not_the_whole_label() public view { + function test_isDeviceLabel_bounds_the_stem_not_the_whole_label() public view { string memory longest = string.concat(_stem(63), ".42"); - assertEq(bytes(longest).length, 66, "longest lite label is 66 octets"); - _assertLite(longest, true); - assertFalse(utils.isSingleLabel(longest), "a lite label is not a single DNS label"); + assertEq(bytes(longest).length, 66, "longest device name is 66 octets"); + _assertDeviceLabel(longest, true); + assertFalse(utils.isSingleLabel(longest), "a device name is not a single DNS label"); } /// @notice A path of exactly @custom:constant StringUtils.MAX_NAME_PATH_OCTETS octets passes. From bbcd7377dd75bd874e6d5110bac5a5abf08cd1c3 Mon Sep 17 00:00:00 2001 From: Giuseppe Re Date: Fri, 2 Oct 2026 13:51:06 +0200 Subject: [PATCH 13/22] ci(release): publish only from tag pushes, fix Usage and Deployments in release notes (#325) ## Description Fixes the release-pipeline problems hit while publishing v1.0.0. **Dispatch runs could not publish.** The `release tags` ruleset lets only the dotns team create `v*` tags. A `workflow_dispatch` run built and tested everything, created the draft, and then [failed](https://github.com/paritytech/dotns/actions/runs/36723620824) at "Publish release": publishing a draft is the moment GitHub creates its tag, and the workflow token is not allowed to (`Cannot create ref due to creations being restricted`). Both publish workflows now run only on a tag push: - `workflow_dispatch` is removed from `publish-release.yml` and `publish-prerelease.yml`. - "Resolve release tag" reads the tag that started the run. The dispatch-only guard against an existing tag, and its token, are gone. - The concurrency group keys on `github.ref_name`, which is now always the tag. **Release body fixes** - The Usage snippet imported `Store.json`, which no release ships. It now imports `LabelStore.json` (both workflows). - The Deployments section implied the listed networks run the release being published. It now says a network can still run an earlier release until it is upgraded, and points to how to tell: the protocol registry's `protocolVersion()` Docs are also fixed accordingly. ## Type - [x] Bug fix - [ ] Feature - [ ] Breaking change - [x] Documentation - [ ] Chore - [ ] Refactor - [ ] Security ## Scope - [ ] Registration - [ ] Resolver - [ ] Store - [ ] Proof of Personhood - [ ] Deployment scripts - [ ] Tests ## Related Issues ## Fixes Issues during the v1.0.0 release process. ## Checklist ### Code - [x] Follows project style - [ ] `forge build` passes - [ ] `forge test` passes - [ ] No new compiler warnings ### Testing - [ ] New tests added for changed behavior - [ ] Fuzz tests added where applicable - [ ] Invariant tests verified ### Security - [x] No new `selfdestruct` or `delegatecall` - [x] Access control reviewed - [x] No storage layout conflicts (for upgradeable contracts) ### Documentation - [ ] 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 You could test it by triggering a new pre-release. ## Notes - Repo setting to update after merge: the `releases` environment still allows `master` as a deployment ref, which only the dispatch path used. Remove it so the environment matches RELEASE_ARTIFACTS. - The v1.0.0 release notes were corrected by hand for the same two body issues; this PR stops later releases from repeating them. --- .github/workflows/publish-prerelease.yml | 72 +++++++++++----------- .github/workflows/publish-release.yml | 77 +++++++++++++----------- README.md | 8 +-- RELEASE_ARTIFACTS.md | 4 +- scripts/js/release-metadata.mjs | 2 + 5 files changed, 85 insertions(+), 78 deletions(-) diff --git a/.github/workflows/publish-prerelease.yml b/.github/workflows/publish-prerelease.yml index 35d759bad..51d7caf82 100644 --- a/.github/workflows/publish-prerelease.yml +++ b/.github/workflows/publish-prerelease.yml @@ -1,30 +1,47 @@ name: Publish Beta Package -# Creating a `v*` tag is restricted to the dotns team by the `release tags` ruleset, and the -# job below pauses on the `releases` environment for a reviewer, so a release takes two -# distinct human actions: cutting the tag, and approving the run it starts. +# Creating a `v*` tag is restricted to the dotns team by the `release tags` ruleset, the +# `guard` job refuses a tag whose commit is not on master, and the gated job pauses on the +# `releases` environment for a reviewer. A release therefore ships reviewed master code and +# takes two distinct human actions: cutting the tag, and approving the run it starts. on: push: tags: - "v[0-9]+.[0-9]+.[0-9]+-*" - workflow_dispatch: - inputs: - version: - description: "Pre-release version, e.g. v0.5.5-rc1. The tag is created from the selected branch." - required: true - type: string permissions: contents: write -# Two runs for the same version would race to attach assets to the same draft. On a -# dispatch `github.ref_name` is the branch, so key on the requested version instead. +# Two runs for the same version would race to attach assets to the same draft. concurrency: - group: ${{ github.workflow }}-${{ inputs.version || github.ref_name }} + group: ${{ github.workflow }}-${{ github.ref_name }} cancel-in-progress: false jobs: + # Runs first, with no environment and read-only access: a tag that does not point at a + # commit on master stops here, before a reviewer is asked to approve and before any + # secret is read. master only takes pull requests, which need an approving review, so + # this keeps every release on reviewed code. + guard: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Require the tagged commit to be on master + run: | + if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/master; then + echo "::error::$GITHUB_REF_NAME points at $GITHUB_SHA, which is not on master. Tag a commit on master." + exit 1 + fi + echo "$GITHUB_REF_NAME points at $GITHUB_SHA, which is on master." + beta-release: + needs: guard runs-on: ubuntu-latest # Gated: this job reads the genesis owner key, which lives only on the `releases` # environment, so every run waits for a reviewer's approval there. @@ -34,19 +51,12 @@ jobs: env: DOTNS_TLDS: testnet paseo steps: - # On workflow_dispatch the tag does not exist yet; the release step creates it - # from the branch this run was started on. Read through an env var rather than - # interpolating the input into the script. + # The run is started by pushing the tag, so the tag already exists and is the release's + # identity. Only the tag push triggers this workflow: the `release tags` ruleset keeps + # the workflow's token from creating a `v*` tag, so a run without one could not publish. - name: Resolve release tag - env: - GH_TOKEN: ${{ github.token }} - INPUT_VERSION: ${{ inputs.version }} run: | - if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then - TAG="$INPUT_VERSION" - else - TAG="$GITHUB_REF_NAME" - fi + TAG="$GITHUB_REF_NAME" # The suffix is restricted to characters GitHub keeps verbatim in an asset # name; a space, for instance, is rewritten to a dot and would fail the # asset check after a full build. Segmented like run.sh's semver check @@ -56,16 +66,8 @@ jobs: echo "::error::Pre-release version must look like v1.2.3-rc1, got '$TAG'." exit 1 fi - # A release created for an existing tag is cut at that tag's commit: GitHub - # ignores target_commitish when the tag is already there. The ABIs would come - # from this branch while the release pointed somewhere else. - if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ] \ - && gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$TAG" >/dev/null 2>&1; then - echo "::error::Tag $TAG already exists; use a different version." - exit 1 - fi echo "RELEASE_TAG=$TAG" >> "$GITHUB_ENV" - echo "Releasing $TAG from $GITHUB_REF_NAME." + echo "Releasing $TAG." - name: Reject a pre-published release env: @@ -238,7 +240,7 @@ jobs: ### Usage ```ts - import StoreAbi from "./abis/Store.json"; + import LabelStoreAbi from "./abis/LabelStore.json"; import RegistrarAbi from "./abis/DotnsRegistrar.json"; ``` ENDOFBODY @@ -291,10 +293,8 @@ jobs: - name: Create draft pre-release with artifacts uses: softprops/action-gh-release@v2 with: - # Explicit because on workflow_dispatch there is no tag to infer; the action - # creates it at this run's commit. + # The tag that started this run. tag_name: ${{ env.RELEASE_TAG }} - target_commitish: ${{ github.sha }} files: | dotns-abis-*.zip release/abis/*.json diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 1fb0af90a..b55a8f8d4 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -1,30 +1,47 @@ name: Publish Release Package -# Creating a `v*` tag is restricted to the dotns team by the `release tags` ruleset, and the -# job below pauses on the `releases` environment for a reviewer, so a release takes two -# distinct human actions: cutting the tag, and approving the run it starts. +# Creating a `v*` tag is restricted to the dotns team by the `release tags` ruleset, the +# `guard` job refuses a tag whose commit is not on master, and the gated job pauses on the +# `releases` environment for a reviewer. A release therefore ships reviewed master code and +# takes two distinct human actions: cutting the tag, and approving the run it starts. on: push: tags: - "v[0-9]+.[0-9]+.[0-9]+" - workflow_dispatch: - inputs: - version: - description: "Version to release, e.g. v0.5.5. The tag is created from the selected branch." - required: true - type: string permissions: contents: write -# Two runs for the same version would race to attach assets to the same draft. On a -# dispatch `github.ref_name` is the branch, so key on the requested version instead. +# Two runs for the same version would race to attach assets to the same draft. concurrency: - group: ${{ github.workflow }}-${{ inputs.version || github.ref_name }} + group: ${{ github.workflow }}-${{ github.ref_name }} cancel-in-progress: false jobs: + # Runs first, with no environment and read-only access: a tag that does not point at a + # commit on master stops here, before a reviewer is asked to approve and before any + # secret is read. master only takes pull requests, which need an approving review, so + # this keeps every release on reviewed code. + guard: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Require the tagged commit to be on master + run: | + if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/master; then + echo "::error::$GITHUB_REF_NAME points at $GITHUB_SHA, which is not on master. Tag a commit on master." + exit 1 + fi + echo "$GITHUB_REF_NAME points at $GITHUB_SHA, which is on master." + release: + needs: guard runs-on: ubuntu-latest # Gated: this job reads the genesis owner key, which lives only on the `releases` # environment, so every run waits for a reviewer's approval there. @@ -34,33 +51,18 @@ jobs: env: DOTNS_TLDS: testnet paseo steps: - # On workflow_dispatch the tag does not exist yet; the release step creates it - # from the branch this run was started on. Read through an env var rather than - # interpolating the input into the script. + # The run is started by pushing the tag, so the tag already exists and is the release's + # identity. Only the tag push triggers this workflow: the `release tags` ruleset keeps + # the workflow's token from creating a `v*` tag, so a run without one could not publish. - name: Resolve release tag - env: - GH_TOKEN: ${{ github.token }} - INPUT_VERSION: ${{ inputs.version }} run: | - if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then - TAG="$INPUT_VERSION" - else - TAG="$GITHUB_REF_NAME" - fi + TAG="$GITHUB_REF_NAME" if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "::error::Version must look like v1.2.3, got '$TAG'." exit 1 fi - # A release created for an existing tag is cut at that tag's commit: GitHub - # ignores target_commitish when the tag is already there. The ABIs would come - # from this branch while the release pointed somewhere else. - if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ] \ - && gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$TAG" >/dev/null 2>&1; then - echo "::error::Tag $TAG already exists; use a different version." - exit 1 - fi echo "RELEASE_TAG=$TAG" >> "$GITHUB_ENV" - echo "Releasing $TAG from $GITHUB_REF_NAME." + echo "Releasing $TAG." - name: Reject a pre-published release env: @@ -230,7 +232,7 @@ jobs: ### Usage ```ts - import StoreAbi from "./abis/Store.json"; + import LabelStoreAbi from "./abis/LabelStore.json"; import RegistrarAbi from "./abis/DotnsRegistrar.json"; ``` ENDOFBODY @@ -251,6 +253,11 @@ jobs: echo "|---------|---------:|" jq -r '.networks | to_entries[] | "| \(.key) | \(.value.chainId) |"' release/deployments.json echo "" + echo "A network can still run an earlier release until it is upgraded to this one. Its protocol" + echo "registry's \`protocolVersion()\` returns the release it runs as bare semver (\`1.0.0\`, no \`v\`)," + echo "or an empty string when the network has never declared one. \`deployments:verify --tag\`" + echo "checks the chain against a release ([Verifying a release]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/blob/$TAG/RELEASE_ARTIFACTS.md#verifying-a-release))." + echo "" echo "- **Addresses:** [deployments.json]($ASSET_BASE/deployments.json)" echo "- **Release contents:** [release-manifest.json]($ASSET_BASE/release-manifest.json)" echo "- **Code identity:** [codehashes.json]($ASSET_BASE/codehashes.json) (stripped-metadata build hashes of this release's contracts; input to upgrade checks)" @@ -307,10 +314,8 @@ jobs: - name: Create draft release with artifacts uses: softprops/action-gh-release@v2 with: - # Explicit because on workflow_dispatch there is no tag to infer; the action - # creates it at this run's commit. + # The tag that started this run. tag_name: ${{ env.RELEASE_TAG }} - target_commitish: ${{ github.sha }} files: | dotns-abis-*.zip release/abis/*.json diff --git a/README.md b/README.md index 1b30b0452..9a0d59333 100644 --- a/README.md +++ b/README.md @@ -37,13 +37,13 @@ Deployment notes are in [DEPLOYMENTS.md](./DEPLOYMENTS.md). Network addresses ar A release publishes the contract ABIs and the deployed addresses as GitHub release assets, described in [RELEASE_ARTIFACTS.md](./RELEASE_ARTIFACTS.md). It does not deploy anything; deploying contracts to a network is a separate process, described in [DEPLOYMENTS.md](./DEPLOYMENTS.md). -Run **Publish Release Package** from the Actions tab, pick the branch to release from, and enter the version (`v0.5.5`). The workflow does the rest: it builds, tests, extracts the ABIs listed in [.github/abi-contracts.txt](./.github/abi-contracts.txt), generates the address and manifest files, creates the release as a draft with every asset attached, verifies the set against what the build produced, and only then publishes. Pushing a matching tag runs the same workflow, so `git tag v0.5.5 && git push origin v0.5.5` remains equivalent. +A release starts from its tag. A member of the dotns team tags the commit to release and pushes the tag: `git tag v0.5.5 && git push origin v0.5.5`. Only the team can create `v*` tags, which the `release tags` ruleset enforces, so the workflow cannot create one itself. The tagged commit must be on `master`, which only takes pull requests that need an approving review; the workflow refuses any other tag before asking for approval. The push starts **Publish Release Package**, which waits for a reviewer's approval on the `releases` environment and then does the rest: it builds, tests, extracts the ABIs listed in [.github/abi-contracts.txt](./.github/abi-contracts.txt), generates the address and manifest files, creates the release as a draft with every asset attached, verifies the set against what the build produced, and only then publishes. -Pre-releases use **Publish Beta Package** with a suffixed version, `v0.5.5-rc1`. The version is the release identity; the `version` field in `package.json` is unrelated and nothing reads it. +Pre-releases work the same way with a suffixed tag, `v0.5.5-rc1`, whose push starts **Publish Beta Package**. The tag is the release identity; the `version` field in `package.json` is unrelated and nothing reads it. -Do not create releases through the GitHub UI's release form, or with `gh release create`. Both publish immediately, and because this repository has immutable releases enabled, a published release can no longer accept assets: only its title and notes stay editable. A release made that way carries no ABIs at all. The workflow rejects an already-published version before building, so the mistake fails in seconds rather than silently shipping an empty release. +Do not create releases through the GitHub UI's release form, or with `gh release create`. Both publish immediately, and because this repository has immutable releases enabled, a published release can no longer accept assets: only its title and notes stay editable. A release made that way carries no ABIs at all. The workflow rejects an already-published version before building, so the mistake fails in seconds and no empty release ships. -If a run fails partway, re-run it from the Actions tab; the draft is updated rather than duplicated. One case needs a manual step: the upload replaces an asset of the same name but never removes others, so if the contract list changed since the failed run, the draft still carries the assets it no longer expects and the verification step will keep refusing to publish. Delete the draft and re-run. If the version has already been published, use a different one, since its assets cannot be changed. +If a run fails partway, re-run it from its page in the Actions tab; the draft is updated in place. A re-run builds the same commit, because a pushed `v*` tag stays where it is: the `release tags` ruleset blocks deleting or moving one for everyone outside the dotns team, and the team does not move a pushed release tag either. So a re-run only helps when the failure is outside the code (a flaky step, an expired approval); a failure that needs a code fix needs a new version. When you abandon a version that way, delete its draft from the Releases page so it does not linger unpublished. One case needs a manual step: the upload replaces an asset of the same name but never removes others, so if the contract list changed since the failed run, the draft still carries the assets it no longer expects and the verification step will keep refusing to publish. Delete the draft and re-run. If the version has already been published, use a different one, since its assets cannot be changed. ## Economics diff --git a/RELEASE_ARTIFACTS.md b/RELEASE_ARTIFACTS.md index dd544d13a..b80046fe7 100644 --- a/RELEASE_ARTIFACTS.md +++ b/RELEASE_ARTIFACTS.md @@ -129,8 +129,8 @@ key that owns every contract in it. The environment holds: pair right after the toolchain is installed and fails the run on a mismatch, so a mistyped key dies before anything is built. - Required reviewers: the dotns team. Every release run pauses for one approval. -- Deployment refs: `master` (for `workflow_dispatch`) and `v[0-9]*` tags. A dispatch started - from any other branch stops at the environment gate. +- Deployment refs: `v[0-9]*` tags. Both workflows run only on a tag push, so a run from any + other ref stops at the environment gate. Creating a `v*` tag is itself restricted to the dotns team by the `release tags` ruleset, so a release takes two distinct human actions: cutting the tag, and approving the run it starts. diff --git a/scripts/js/release-metadata.mjs b/scripts/js/release-metadata.mjs index acaf49fb6..c5692c5b7 100644 --- a/scripts/js/release-metadata.mjs +++ b/scripts/js/release-metadata.mjs @@ -837,6 +837,8 @@ function verify(args) { ]).replace(/^"|"$/g, ""); if (declaredVersion === tag) { console.log(` ok protocolVersion ${declaredVersion}`); + } else if (declaredVersion === "") { + problems.push(`chain declares no protocol version, expected '${tag}'`); } else { problems.push(`chain declares protocol version '${declaredVersion}', expected '${tag}'`); } From 62a8c2c413a2d5aac603fd83c3f50f25b0fe1637 Mon Sep 17 00:00:00 2001 From: giuseppere Date: Mon, 5 Oct 2026 15:43:19 +0200 Subject: [PATCH 14/22] chore(upgrades): replace the v0.8.0 campaign with v0.8.0 snapshots from chain Removes the v0.7.0 to v0.8.0 campaign (snapshots, store-factory migrator, ownership rotation, ten upgrade scripts and fork tests, runbook); it stays in history under upgrades/paseo-nv2-v0.8.0. Adds snapshots of the four contracts this campaign upgrades (DotnsRegistry, PopRules, DotnsPopResolver, DotnsPopController), copied from upgrades/paseo-nv2-v0.8.0 with the code-changed files of their import closure. verify-snapshots.sh reproduces all five deployed implementations on Paseo, DotnsRegistrar included. LayoutCompatibility covers the four. Co-Authored-By: Claude Opus 5.5 (1M context) --- contracts/escrow/DotnsNameEscrowOld.sol | 819 ------------------ contracts/pop/IDotnsCostModelRegistryOld.sol | 87 -- contracts/pop/IDotnsPricingOld.sol | 32 - contracts/pop/IPopRulesOld.sol | 355 ++++++++ contracts/pop/PopRulesOld.sol | 98 ++- .../registrars/DotnsPopControllerOld.sol | 175 ++-- .../DotnsRegistrarControllerOld.sol | 484 ----------- contracts/registrars/DotnsRegistrarOld.sol | 130 +-- .../registrars/IDotnsPopControllerOld.sol | 528 +++++++++++ contracts/registrars/IDotnsRegistrarOld.sol | 190 ---- .../registry/DotnsProtocolRegistryOld.sol | 112 --- contracts/registry/DotnsRegistryOld.sol | 93 +- .../registry/IDotnsProtocolRegistryOld.sol | 51 -- contracts/registry/IDotnsRegistryOld.sol | 164 ---- .../resolvers/DotnsContentResolverOld.sol | 152 ---- contracts/resolvers/DotnsPopResolverOld.sol | 50 +- contracts/resolvers/DotnsResolverOld.sol | 100 --- .../resolvers/DotnsReverseResolverOld.sol | 146 ---- contracts/resolvers/IDotnsPopResolverOld.sol | 86 ++ contracts/store/IStoreFactoryOld.sol | 157 ---- contracts/store/StoreFactoryMigrator.sol | 366 -------- contracts/utils/DotnsConstantsOld.sol | 192 ---- contracts/utils/RegistrationUtilsOld.sol | 97 --- contracts/utils/StoreUtilsOld.sol | 60 -- contracts/utils/StringUtilsOld.sol | 295 +++++++ contracts/utils/SubnodeUtilsOld.sol | 39 +- contracts/utils/SystemUtilsOld.sol | 21 - contracts/whitelist/DotnsNameWhitelistOld.sol | 512 ----------- docs/PASEO-V080-RUNBOOK.md | 287 ------ scripts/deploy/MigrateStoreFactory.s.sol | 273 ------ scripts/deploy/RotateOwnership.s.sol | 286 ------ scripts/deploy/UpgradeContentResolver.s.sol | 76 -- scripts/deploy/UpgradeNameEscrow.s.sol | 75 -- scripts/deploy/UpgradeNameWhitelist.s.sol | 75 -- scripts/deploy/UpgradePopController.s.sol | 75 -- scripts/deploy/UpgradePopResolver.s.sol | 75 -- scripts/deploy/UpgradeProtocolRegistry.s.sol | 77 -- scripts/deploy/UpgradeRegistrar.s.sol | 75 -- .../deploy/UpgradeRegistrarController.s.sol | 78 -- scripts/deploy/UpgradeResolver.s.sol | 75 -- scripts/deploy/UpgradeReverseResolver.s.sol | 76 -- test/fork/MigrateStoreFactory.t.sol | 211 ----- test/fork/RotateOwnership.t.sol | 153 ---- test/fork/UpgradeContentResolver.t.sol | 70 -- test/fork/UpgradeNameEscrow.t.sol | 71 -- test/fork/UpgradeNameWhitelist.t.sol | 67 -- test/fork/UpgradePopController.t.sol | 66 -- test/fork/UpgradePopResolver.t.sol | 70 -- test/fork/UpgradeProtocolRegistry.t.sol | 87 -- test/fork/UpgradeRegistrar.t.sol | 73 -- test/fork/UpgradeRegistrarController.t.sol | 76 -- test/fork/UpgradeResolver.t.sol | 70 -- test/fork/UpgradeReverseResolver.t.sol | 70 -- test/unit/store/StoreFactoryMigrator.t.sol | 203 ----- test/unit/upgrade/LayoutCompatibility.t.sol | 99 +-- 55 files changed, 1603 insertions(+), 6977 deletions(-) delete mode 100644 contracts/escrow/DotnsNameEscrowOld.sol delete mode 100644 contracts/pop/IDotnsCostModelRegistryOld.sol delete mode 100644 contracts/pop/IDotnsPricingOld.sol create mode 100644 contracts/pop/IPopRulesOld.sol delete mode 100644 contracts/registrars/DotnsRegistrarControllerOld.sol create mode 100644 contracts/registrars/IDotnsPopControllerOld.sol delete mode 100644 contracts/registrars/IDotnsRegistrarOld.sol delete mode 100644 contracts/registry/DotnsProtocolRegistryOld.sol delete mode 100644 contracts/registry/IDotnsProtocolRegistryOld.sol delete mode 100644 contracts/registry/IDotnsRegistryOld.sol delete mode 100644 contracts/resolvers/DotnsContentResolverOld.sol delete mode 100644 contracts/resolvers/DotnsResolverOld.sol delete mode 100644 contracts/resolvers/DotnsReverseResolverOld.sol create mode 100644 contracts/resolvers/IDotnsPopResolverOld.sol delete mode 100644 contracts/store/IStoreFactoryOld.sol delete mode 100644 contracts/store/StoreFactoryMigrator.sol delete mode 100644 contracts/utils/DotnsConstantsOld.sol delete mode 100644 contracts/utils/RegistrationUtilsOld.sol delete mode 100644 contracts/utils/StoreUtilsOld.sol create mode 100644 contracts/utils/StringUtilsOld.sol delete mode 100644 contracts/utils/SystemUtilsOld.sol delete mode 100644 contracts/whitelist/DotnsNameWhitelistOld.sol delete mode 100644 docs/PASEO-V080-RUNBOOK.md delete mode 100644 scripts/deploy/MigrateStoreFactory.s.sol delete mode 100644 scripts/deploy/RotateOwnership.s.sol delete mode 100644 scripts/deploy/UpgradeContentResolver.s.sol delete mode 100644 scripts/deploy/UpgradeNameEscrow.s.sol delete mode 100644 scripts/deploy/UpgradeNameWhitelist.s.sol delete mode 100644 scripts/deploy/UpgradePopController.s.sol delete mode 100644 scripts/deploy/UpgradePopResolver.s.sol delete mode 100644 scripts/deploy/UpgradeProtocolRegistry.s.sol delete mode 100644 scripts/deploy/UpgradeRegistrar.s.sol delete mode 100644 scripts/deploy/UpgradeRegistrarController.s.sol delete mode 100644 scripts/deploy/UpgradeResolver.s.sol delete mode 100644 scripts/deploy/UpgradeReverseResolver.s.sol delete mode 100644 test/fork/MigrateStoreFactory.t.sol delete mode 100644 test/fork/RotateOwnership.t.sol delete mode 100644 test/fork/UpgradeContentResolver.t.sol delete mode 100644 test/fork/UpgradeNameEscrow.t.sol delete mode 100644 test/fork/UpgradeNameWhitelist.t.sol delete mode 100644 test/fork/UpgradePopController.t.sol delete mode 100644 test/fork/UpgradePopResolver.t.sol delete mode 100644 test/fork/UpgradeProtocolRegistry.t.sol delete mode 100644 test/fork/UpgradeRegistrar.t.sol delete mode 100644 test/fork/UpgradeRegistrarController.t.sol delete mode 100644 test/fork/UpgradeResolver.t.sol delete mode 100644 test/fork/UpgradeReverseResolver.t.sol delete mode 100644 test/unit/store/StoreFactoryMigrator.t.sol diff --git a/contracts/escrow/DotnsNameEscrowOld.sol b/contracts/escrow/DotnsNameEscrowOld.sol deleted file mode 100644 index cfc70c71e..000000000 --- a/contracts/escrow/DotnsNameEscrowOld.sol +++ /dev/null @@ -1,819 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.34; - -import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import { - OwnableUpgradeable -} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; -import {ReentrancyGuardTransient} from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol"; -import { - ERC165Upgradeable -} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; -import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; -import {IDotnsNameEscrow} from "./IDotnsNameEscrow.sol"; -import {IDotnsRegistrarOld} from "../registrars/IDotnsRegistrarOld.sol"; -import {IDotnsProtocolRegistryOld} from "../registry/IDotnsProtocolRegistryOld.sol"; -import {DotnsConstantsOld} from "../utils/DotnsConstantsOld.sol"; - -/// @title Dotns Name Escrow -/// @notice Holds refundable deposits for registered names and manages the release/reclaim -/// lifecycle. @custom:security-contact admin@parity.io -contract DotnsNameEscrowOld is - Initializable, - UUPSUpgradeable, - OwnableUpgradeable, - ReentrancyGuardTransient, - ERC165Upgradeable, - IERC721Receiver, - IDotnsNameEscrow -{ - /// @notice Maximum page size for releasedTokens pagination. - uint256 public constant MAX_RELEASED_PAGE_SIZE = 200; - - /// @notice Maximum page size for pendingRefunds pagination and batch claims. - uint256 public constant MAX_REFUND_PAGE_SIZE = 200; - - /// @notice Upper bound on the configurable release-cooldown. - /// @dev 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; - - /// @notice Upper bound on the configurable redeem window. - /// @dev 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; - - /// @notice Lower bound on the configurable redeem window. - /// @dev A window short enough to elapse before its holder can plausibly notice the release - /// offers no protection at all, and one of zero length turns every release into an - /// immediate hand-off to whoever is watching. The floor keeps the window long enough to - /// span a holder being asleep or away for a day, so the guarantee survives any setting - /// the owner is able to choose. - uint256 public constant MIN_REDEEM_WINDOW = 1 days; - - /// @notice The protocol registry for resolving sibling contract addresses. - IDotnsProtocolRegistryOld public protocolRegistry; - - /// @notice Delay after release before the deposit withdrawal may be credited. - /// @dev 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; - - /// @notice Total amount of a specific asset reserved across all positions. - /// @dev 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; - - /// @notice Per-token escrow position storing recipient, amount, lifecycle flags and cooldown. - mapping(uint256 tokenId => ReleasePosition position) private _positions; - - /// @notice Ordered set of tokens currently in escrow custody, used for paginated enumeration. - uint256[] private _releasedTokens; - - /// @notice Reverse lookup into `_releasedTokens` (one-based) for O(1) remove-by-swap. - mapping(uint256 tokenId => uint256 indexPlusOne) private _releasedIndexPlusOne; - - /// @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 - /// 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; - - /// @notice Time-locked refund ledger keyed by entryId. - /// @dev 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; - - /// @notice Per-recipient list of pending entryIds for paginated enumeration and batch claim. - mapping(address recipient => uint256[] entryIds) private _entriesByRecipient; - - /// @notice Reverse lookup into `_entriesByRecipient` (one-based) for O(1) remove-by-swap. - mapping(uint256 entryId => uint256 indexPlusOne) private _entryIndexPlusOne; - - /// @notice Monotonic counter assigning entryIds to new refund credits. - uint256 private _nextEntryId; - - /// @notice Period after release during which only the previous holder may act. - /// @dev 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. - uint256 public redeemWindow; - - /// @dev Reserved storage space to allow for layout changes in future upgrades. A variable - /// appended above must shrink this array by the same number of slots, so an upgrade never - /// moves the slots of anything already stored. - uint256[50] private __gap; - - /// @notice Restricts calls to the configured registrar controller. - modifier onlyController() { - _onlyController(); - _; - } - - /// @notice Restricts calls to the configured registrar from the protocol registry. - modifier onlyRegistrar() { - _onlyRegistrar(); - _; - } - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - _disableInitializers(); - } - - /// @notice Initialises the name escrow. - /// @dev 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 - /// @custom: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 any value below @custom:constant MIN_REDEEM_WINDOW - /// (@custom:reverts RedeemWindowTooShort) or above @custom:constant MAX_REDEEM_WINDOW - /// (@custom:reverts RedeemWindowTooLong), and emits @custom:emits RedeemWindowUpdated. - /// @param registry Protocol registry used to resolve registrar and controller addresses. - /// @param cooldownSeconds Delay after release before the deposit withdrawal may be credited. - /// @param redeemWindowSeconds Period after release in which only the previous holder may act. - function initialize( - IDotnsProtocolRegistryOld registry, - uint256 cooldownSeconds, - uint256 redeemWindowSeconds - ) - external - initializer - { - require(address(registry) != address(0), InvalidAsset()); - - __Ownable_init(msg.sender); - __ERC165_init(); - - protocolRegistry = registry; - updateCooldown(cooldownSeconds); - updateRedeemWindow(redeemWindowSeconds); - } - - /// @inheritdoc IDotnsNameEscrow - function updateCooldown(uint256 newCooldown) public override onlyOwner { - require(newCooldown != 0, InvalidCooldown()); - require(newCooldown <= MAX_COOLDOWN, CooldownTooLong(newCooldown, MAX_COOLDOWN)); - - uint256 currentCooldown = cooldown; - cooldown = newCooldown; - - emit CooldownUpdated(currentCooldown, newCooldown); - } - - /// @inheritdoc IDotnsNameEscrow - function updateRedeemWindow(uint256 newRedeemWindow) public override onlyOwner { - require( - newRedeemWindow >= MIN_REDEEM_WINDOW, - RedeemWindowTooShort(newRedeemWindow, MIN_REDEEM_WINDOW) - ); - require( - newRedeemWindow <= MAX_REDEEM_WINDOW, - RedeemWindowTooLong(newRedeemWindow, MAX_REDEEM_WINDOW) - ); - - uint256 currentRedeemWindow = redeemWindow; - redeemWindow = newRedeemWindow; - - emit RedeemWindowUpdated(currentRedeemWindow, newRedeemWindow); - } - - /// @inheritdoc IDotnsNameEscrow - function getReleasePosition(uint256 tokenId) - external - view - override - returns (ReleasePosition memory position) - { - position = _positions[tokenId]; - } - - /// @inheritdoc IDotnsNameEscrow - function releasedTokenCount() external view override returns (uint256 count) { - count = _releasedTokens.length; - } - - /// @inheritdoc IDotnsNameEscrow - function reserves(address asset) external view returns (uint256 amount) { - amount = tokenReserved[asset]; - } - - /// @inheritdoc IDotnsNameEscrow - function releasedTokens( - uint256 start, - uint256 limit - ) - external - view - override - returns (uint256[] memory tokenIds) - { - require(limit != 0 && limit <= MAX_RELEASED_PAGE_SIZE, InvalidPageSize(limit)); - - uint256 length = _releasedTokens.length; - if (start >= length) return new uint256[](0); - - uint256 end = start + limit; - if (end > length) end = length; - - tokenIds = new uint256[](end - start); - uint256 outIndex = 0; - for (uint256 i = start; i < end; ++i) { - tokenIds[outIndex] = _releasedTokens[i]; - ++outIndex; - } - } - - /// @inheritdoc IDotnsNameEscrow - function deposit(DepositParams calldata params) external payable override onlyController { - // Reject mismatched amount/msg.value so callers cannot under-fund a position. - require(msg.value == params.amount, InvalidAmount()); - // Only native deposits are currently supported; ERC20 support can be added in a - // future upgrade by relaxing this check and routing transfers via SafeERC20. - require(params.asset == address(0), AssetNotSupported(params.asset)); - require(params.recipient != address(0), InvalidRecipient()); - - ReleasePosition storage position = _positions[params.tokenId]; - - // Use `recipient` as the "is this slot funded?" sentinel so zero-amount - // 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)); - - position.asset = params.asset; - position.amount = params.amount; - position.recipient = params.recipient; - - tokenReserved[position.asset] += params.amount; - - emit NativeDepositRecorded(params.tokenId, params.amount); - } - - /// @inheritdoc IDotnsNameEscrow - function creditOverpayment(address recipient) external payable override onlyController { - require(recipient != address(0), InvalidRecipient()); - require(msg.value != 0, InvalidAmount()); - _pendingWithdrawals[recipient] += msg.value; - emit OverpaymentRefunded(recipient, msg.value); - } - - /// @inheritdoc IDotnsNameEscrow - function depositProtocolFee(ProtocolFeeDepositParams calldata params) - external - payable - override - onlyController - { - require(msg.value > 0, InvalidAmount()); - - protocolFees += msg.value; - - emit CrossTierFeePaid( - params.tokenId, - params.payer, - params.recipient, - msg.value, - /* isRegistration */ - true - ); - } - - /// @inheritdoc IDotnsNameEscrow - function chargeTransferFee(ChargeTransferFeeParams calldata params) - external - payable - override - onlyRegistrar - returns (uint256 charged) - { - ReleasePosition storage position = _positions[params.tokenId]; - // Released positions are mid-lifecycle in escrow custody and must not be rebound; the - // recipient is the original releaser who will claim the refund. The canonical transfer - // path never reaches here for released tokens (`_update` short-circuits on escrow-touching - // transfers) but the guard hardens the contract against a divergent registrar. - require(!position.released, AlreadyReleased(params.tokenId)); - - address priorRecipient = position.recipient; - - uint256 fee = params.transferFee; - require(msg.value >= fee, InsufficientValue()); - - // Deposits follow the NFT, not the depositor. When the position is funded the locked - // deposit travels with the name; when it is a zero-amount lifecycle marker the marker - // travels with it. In both cases the position is rebound to the new holder so only - // the current holder can later release into escrow and claim the refund. - if (priorRecipient != address(0) && params.to != priorRecipient) { - position.recipient = params.to; - } - - charged = fee; - - if (fee > 0) { - protocolFees += fee; - emit CrossTierFeePaid( - params.tokenId, - params.payer, - params.to, - fee, - /* isRegistration */ - false - ); - } - - uint256 overpayment = msg.value - fee; - if (overpayment > 0) { - _creditRefund(params.payer, overpayment, params.tokenId); - } - } - - /// @inheritdoc IDotnsNameEscrow - function release(uint256 tokenId) external override nonReentrant { - IDotnsRegistrarOld registrar = _registrar(); - - address currentOwner = registrar.ownerOf(tokenId); - - ReleasePosition storage position = _positions[tokenId]; - // Recipient is the canonical "is this position present?" sentinel; zero-amount positions - // 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)); - - // Position recipient mirrors the current NFT holder (rebound on every transfer), so the - // holder gate collapses to a single equality check. Approved operators cannot release on - // behalf of the holder because the recipient field is keyed to the holder, not to any - // approval set; this keeps the deposit refund flow tied to the on-chain owner. - require( - msg.sender == currentOwner && msg.sender == position.recipient, - NotRefundRecipient(msg.sender, tokenId) - ); - - bool approvedForEscrow = registrar.getApproved(tokenId) == address(this) - || registrar.isApprovedForAll(currentOwner, address(this)); - - require(approvedForEscrow, EscrowNotApproved(tokenId)); - - // Fail closed on an unseeded window rather than stamping `redeemableUntil` at the current - // timestamp, which would collapse the holder's exclusive redeem phase to zero length and - // open permissionless reclaim the instant the name is released. Only reachable on a proxy - // upgraded without pairing the upgrade with `updateRedeemWindow`. - uint256 currentRedeemWindow = redeemWindow; - require(currentRedeemWindow != 0, RedeemWindowNotConfigured()); - - // Snapshot the position fields once into stack locals so the trailing event emit reuses - // them without three extra warm SLOADs after the state mutation. Both casts to `uint64` are - // safe because `cooldown` and `redeemWindow` are bounded by @custom:constant MAX_COOLDOWN - // and @custom:constant MAX_REDEEM_WINDOW respectively. - address asset = position.asset; - uint256 amount = position.amount; - // forge-lint: disable-next-line(unsafe-typecast) - uint64 availableAt = uint64(block.timestamp + cooldown); - // forge-lint: disable-next-line(unsafe-typecast) - uint64 redeemUntil = uint64(block.timestamp + currentRedeemWindow); - - position.withdrawAvailableAt = availableAt; - position.redeemableUntil = redeemUntil; - position.released = true; - - registrar.safeTransferFrom(currentOwner, address(this), tokenId); - - _addReleasedToken(tokenId); - - emit NameReleased(tokenId, msg.sender, asset, amount, availableAt, redeemUntil); - } - - /// @inheritdoc IDotnsNameEscrow - function withdraw(uint256 tokenId) external override nonReentrant { - ReleasePosition storage position = _positions[tokenId]; - - require(position.released, NotReleased(tokenId)); - require(!position.claimed, AlreadyClaimed(tokenId)); - require(position.recipient == msg.sender, NotRefundRecipient(msg.sender, tokenId)); - require( - block.timestamp >= position.withdrawAvailableAt, - WithdrawalTooEarly(tokenId, position.withdrawAvailableAt, block.timestamp) - ); - - _settleDeposit(position, tokenId, msg.sender); - } - - /// @notice Moves a position's outstanding deposit onto the recipient's pull-payment balance. - /// @dev 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. The per-asset - /// `tokenReserved` pool backs the refund in full, and @custom:reverts InsufficientFunds - /// when it cannot cover the amount owed. Emits @custom:emits RefundWithdrawn. - /// 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. - /// @param position Storage pointer to the position being settled. - /// @param recipient Address credited with the deposit. Always the position recipient. - function _settleDeposit( - ReleasePosition storage position, - uint256 tokenId, - address recipient - ) - private - { - uint256 owed = position.amount; - address asset = position.asset; - - // Nothing to settle: return before touching `claimed`. That flag is what `redeem` reads to - // decide whether the holder has already been paid for the name, so setting it here would - // make a zero-amount `withdraw`, which pays nothing and emits nothing, silently forfeit - // the holder's right to recover their own name for no consideration at all. Free PopFull - // and PopLite registrations seed exactly these positions, and `withdraw` is the step the - // old contract required before a name could be recycled, so that is a path holders will - // take. - if (owed == 0) return; - - // Effects: from here the deposit really is being handed over, so the flag is set. - position.claimed = true; - - // 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]) - ); - - position.amount = 0; - tokenReserved[asset] -= owed; - - _pendingWithdrawals[recipient] += owed; - - emit RefundWithdrawn(tokenId, recipient, asset, owed); - } - - /// @inheritdoc IDotnsNameEscrow - function claimWithdrawal() external override nonReentrant returns (uint256 amount) { - amount = _pendingWithdrawals[msg.sender]; - require(amount > 0, NoPendingWithdrawal()); - - // Effects before interaction. - _pendingWithdrawals[msg.sender] = 0; - - (bool ok,) = payable(msg.sender).call{value: amount}(""); - // tokenId is not meaningful here since a single pending balance can aggregate - // multiple positions; surface 0 to keep the existing error shape. - require(ok, RefundFailed(0)); - - emit WithdrawalClaimed(msg.sender, amount); - } - - /// @inheritdoc IDotnsNameEscrow - function pendingWithdrawal(address recipient) external view override returns (uint256 amount) { - amount = _pendingWithdrawals[recipient]; - } - - /// @inheritdoc IDotnsNameEscrow - function claimRefund(uint256 entryId) external override nonReentrant returns (uint256 amount) { - // Storage pointer over memory copy: only the fields we actually need are SLOAD-ed. - RefundEntry storage entry = _refundEntries[entryId]; - amount = entry.amount; - // Check existence first so a deleted (already-claimed or unknown) entry surfaces a clear - // `NoSuchRefundEntry` rather than the recipient-mismatch revert that would otherwise fire - // against the zero-address sentinel. - require(amount > 0, NoSuchRefundEntry(entryId)); - uint256 entryTokenId = entry.tokenId; - require(entry.recipient == msg.sender, NotRefundRecipient(msg.sender, entryTokenId)); - require(block.timestamp >= entry.availableAt, RefundLocked(entryId, entry.availableAt)); - - _removeRefundEntry(entryId, msg.sender); - - (bool ok,) = payable(msg.sender).call{value: amount}(""); - require(ok, RefundFailed(entryTokenId)); - - emit RefundClaimed(msg.sender, entryId, amount); - } - - /// @inheritdoc IDotnsNameEscrow - function claimRefundsBatch(uint256[] calldata entryIds) - external - override - nonReentrant - returns (uint256 totalAmount) - { - uint256 length = entryIds.length; - require(length > 0 && length <= MAX_REFUND_PAGE_SIZE, InvalidPageSize(length)); - - for (uint256 i; i < length; ++i) { - uint256 entryId = entryIds[i]; - RefundEntry storage entry = _refundEntries[entryId]; - uint256 amount = entry.amount; - require(amount > 0, NoSuchRefundEntry(entryId)); - require(entry.recipient == msg.sender, NotRefundRecipient(msg.sender, entry.tokenId)); - require(block.timestamp >= entry.availableAt, RefundLocked(entryId, entry.availableAt)); - - totalAmount += amount; - _removeRefundEntry(entryId, msg.sender); - - emit RefundClaimed(msg.sender, entryId, amount); - } - - (bool ok,) = payable(msg.sender).call{value: totalAmount}(""); - require(ok, RefundFailed(0)); - } - - /// @inheritdoc IDotnsNameEscrow - function pendingRefundCount(address recipient) external view override returns (uint256 count) { - count = _entriesByRecipient[recipient].length; - } - - /// @inheritdoc IDotnsNameEscrow - function pendingRefundIds( - address recipient, - uint256 offset, - uint256 limit - ) - external - view - override - returns (uint256[] memory entryIds) - { - require(limit > 0 && limit <= MAX_REFUND_PAGE_SIZE, InvalidPageSize(limit)); - - uint256[] storage all = _entriesByRecipient[recipient]; - uint256 total = all.length; - if (offset >= total) return new uint256[](0); - - uint256 end = offset + limit; - if (end > total) end = total; - - entryIds = new uint256[](end - offset); - for (uint256 i = 0; i < entryIds.length; ++i) { - entryIds[i] = all[offset + i]; - } - } - - /// @inheritdoc IDotnsNameEscrow - function pendingRefunds( - address recipient, - uint256 offset, - uint256 limit - ) - external - view - override - returns (uint256[] memory entryIds, RefundEntry[] memory entries) - { - require(limit > 0 && limit <= MAX_REFUND_PAGE_SIZE, InvalidPageSize(limit)); - - uint256[] storage all = _entriesByRecipient[recipient]; - uint256 total = all.length; - if (offset >= total) { - return (new uint256[](0), new RefundEntry[](0)); - } - - uint256 end = offset + limit; - if (end > total) end = total; - - uint256 count = end - offset; - entryIds = new uint256[](count); - entries = new RefundEntry[](count); - for (uint256 i = 0; i < count; ++i) { - uint256 entryId = all[offset + i]; - entryIds[i] = entryId; - entries[i] = _refundEntries[entryId]; - } - } - - /// @inheritdoc IDotnsNameEscrow - function refundEntry(uint256 entryId) - external - view - override - returns (RefundEntry memory entry) - { - entry = _refundEntries[entryId]; - } - - /// @notice Internal helper: allocate a new entryId and credit a refund to `recipient`. - /// @dev 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) - { - require(recipient != address(0), InvalidRecipient()); - require(amount > 0, InvalidAmount()); - - entryId = ++_nextEntryId; - // forge-lint: disable-next-line(unsafe-typecast) - uint64 availableAt = uint64(block.timestamp + cooldown); - - _refundEntries[entryId] = RefundEntry({ - recipient: recipient, amount: amount, availableAt: availableAt, tokenId: tokenId - }); - - uint256[] storage list = _entriesByRecipient[recipient]; - list.push(entryId); - _entryIndexPlusOne[entryId] = list.length; - - emit RefundCredited(recipient, entryId, amount, availableAt, tokenId); - } - - /// @notice Internal helper: delete a refund entry and swap-pop its slot in the recipient's - /// enumeration array. - function _removeRefundEntry(uint256 entryId, address recipient) internal { - uint256 indexPlusOne = _entryIndexPlusOne[entryId]; - // Caller is expected to have validated existence already; defensive check kept cheap. - if (indexPlusOne == 0) return; - - uint256 index = indexPlusOne - 1; - uint256[] storage list = _entriesByRecipient[recipient]; - uint256 lastIndex = list.length - 1; - - if (index != lastIndex) { - uint256 movedEntryId = list[lastIndex]; - list[index] = movedEntryId; - _entryIndexPlusOne[movedEntryId] = indexPlusOne; - } - list.pop(); - - delete _entryIndexPlusOne[entryId]; - delete _refundEntries[entryId]; - } - - /// @inheritdoc IDotnsNameEscrow - function reclaim( - uint256 tokenId, - address newOwner - ) - external - override - onlyController - nonReentrant - { - require(isReclaimable(tokenId), NotReclaimable(tokenId)); - - ReleasePosition storage position = _positions[tokenId]; - address previousRecipient = position.recipient; - - // Settle before deleting: the departing holder keeps their claim on the deposit even though - // they are losing the name. `_settleDeposit` is a no-op for a zero-amount position and for - // one already withdrawn, so the common paths cost nothing extra. - _settleDeposit(position, tokenId, previousRecipient); - - delete _positions[tokenId]; - _removeReleasedToken(tokenId); - - _registrar().safeTransferFrom(address(this), newOwner, tokenId); - - emit NameReclaimed(tokenId, previousRecipient, newOwner); - } - - /// @inheritdoc IDotnsNameEscrow - /// @dev `public` rather than `external` so `reclaim` can gate on it without a self-call, which - /// is what keeps the condition in one place instead of two. - function isReclaimable(uint256 tokenId) public view override returns (bool reclaimable) { - ReleasePosition storage position = _positions[tokenId]; - - // The gate is the elapsed redeem window, not the `claimed` flag. Gating on `claimed` would - // make recyclability depend on the previous holder choosing to withdraw, which strands the - // name whenever they have no reason to: a zero-amount position has nothing to collect, so - // "never withdraws" is the default rather than the exception. The window bounds the wait - // instead, and reclaim settles any unwithdrawn value rather than holding it hostage. - // - // Lifecycle state only. `reclaim` also settles the deposit, which can in principle revert - // `InsufficientFunds` when the reserved balance cannot cover the amount owed, so a true - // answer here is a claim about the window rather than a guarantee - // that the call is funded. The two coincide because `tokenReserved` is by construction the - // exact sum of live position amounts: only `deposit` credits it, and only `_settleDeposit` - // debits it, by exactly the amount it zeroes. `invariant_reserves_match_positions` holds - // that construction, and `invariant_reclaimable_positions_are_fundable` asserts the - // implication directly, so a change that broke the coincidence would fail the suite rather - // than surface as a name advertised and then unregisterable. - reclaimable = position.released && block.timestamp >= position.redeemableUntil; - } - - /// @inheritdoc IDotnsNameEscrow - function redeem(uint256 tokenId) external override nonReentrant { - ReleasePosition storage position = _positions[tokenId]; - - require(position.recipient == msg.sender, NotRefundRecipient(msg.sender, tokenId)); - // One error for the whole state predicate: unreleased, already withdrawn, or past the - // window are all simply "not redeemable" from the caller's point of view, and collapsing - // them avoids leaking a three-way state machine into the revert surface. - require( - position.released && !position.claimed && block.timestamp < position.redeemableUntil, - NotRedeemable(tokenId) - ); - - // Restore the pre-release state and nothing more. Recipient, asset and amount are left - // untouched so the deposit stays locked against the name; clearing the clocks means a later - // release starts a fresh pair rather than inheriting stale deadlines. - position.released = false; - position.withdrawAvailableAt = 0; - position.redeemableUntil = 0; - - _removeReleasedToken(tokenId); - - _registrar().safeTransferFrom(address(this), msg.sender, tokenId); - - emit NameRedeemed(tokenId, msg.sender); - } - - /// @inheritdoc IERC721Receiver - function onERC721Received( - address, - address, - uint256 tokenId, - bytes calldata - ) - external - view - override - returns (bytes4 selector) - { - require(msg.sender == address(_registrar()), NotAcceptedTransfer(msg.sender)); - // Only accept transfers that this contract itself initiated via `release`. A holder calling - // `registrar.safeTransferFrom(holder, escrow, tokenId)` directly would otherwise land the - // NFT in custody with no `released` position, leaving the token (and any prior deposit) - // permanently unreachable through `withdraw` / `reclaim`. - require(_positions[tokenId].released, UnsolicitedDeposit(tokenId)); - selector = IERC721Receiver.onERC721Received.selector; - } - - /// @inheritdoc ERC165Upgradeable - function supportsInterface(bytes4 interfaceId) public view override returns (bool supported) { - supported = interfaceId == type(IDotnsNameEscrow).interfaceId - || interfaceId == type(IERC721Receiver).interfaceId - || super.supportsInterface(interfaceId); - } - - /// @notice Returns implementation version. - /// @return versionString Current version string. - function version() external pure virtual returns (string memory versionString) { - versionString = "1.0.0"; - } - - /// @notice Returns the configured registrar from the protocol registry. - function _registrar() internal view returns (IDotnsRegistrarOld registrar) { - registrar = IDotnsRegistrarOld(protocolRegistry.get(DotnsConstantsOld.REGISTRAR)); - } - - /// @notice Restricts calls to the configured controller from the protocol registry. - function _onlyController() internal view { - address controller = protocolRegistry.get(DotnsConstantsOld.CONTROLLER); - require(msg.sender == controller, NotController(msg.sender)); - } - - /// @notice Restricts calls to the configured registrar from the protocol registry. - function _onlyRegistrar() internal view { - address registrar = protocolRegistry.get(DotnsConstantsOld.REGISTRAR); - require(msg.sender == registrar, NotRegistrar(msg.sender)); - } - - /// @notice Adds a token to the released-token set if absent. - function _addReleasedToken(uint256 tokenId) internal { - if (_releasedIndexPlusOne[tokenId] != 0) return; - - _releasedTokens.push(tokenId); - _releasedIndexPlusOne[tokenId] = _releasedTokens.length; - } - - /// @notice Removes a token from the released-token set if present. - function _removeReleasedToken(uint256 tokenId) internal { - uint256 indexPlusOne = _releasedIndexPlusOne[tokenId]; - if (indexPlusOne == 0) return; - - uint256 index = indexPlusOne - 1; - uint256 lastIndex = _releasedTokens.length - 1; - - if (index != lastIndex) { - uint256 lastTokenId = _releasedTokens[lastIndex]; - _releasedTokens[index] = lastTokenId; - _releasedIndexPlusOne[lastTokenId] = index + 1; - } - - _releasedTokens.pop(); - delete _releasedIndexPlusOne[tokenId]; - } - - /// @inheritdoc UUPSUpgradeable - function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} -} diff --git a/contracts/pop/IDotnsCostModelRegistryOld.sol b/contracts/pop/IDotnsCostModelRegistryOld.sol deleted file mode 100644 index 5d1d689a0..000000000 --- a/contracts/pop/IDotnsCostModelRegistryOld.sol +++ /dev/null @@ -1,87 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.34; - -import {IDotnsPricingOld} from "./IDotnsPricingOld.sol"; - -/// @title DotNS Cost Model Registry -/// @notice Holds every cost model the protocol has run and names the current one. -/// @dev The address registered under `DotnsConstantsOld.COST_MODEL` points here, set once and never -/// repointed. Changing the live curve registers a new model, which adds its version and moves -/// the current pointer. Prior models stay live and priceable by version, so a registration -/// committed against an earlier curve settles at the amount it committed to. -/// @custom:security-contact admin@parity.io -interface IDotnsCostModelRegistryOld { - /// @notice Emitted when a model is registered and becomes current. - /// @param version The model's version identifier. - /// @param model The model address now serving that version. - event CostModelRegistered(uint256 indexed version, address indexed model); - - /// @notice Emitted when the current version is pointed at an already-registered model. - /// @param version The version now serving fresh pricing. - event CurrentModelSet(uint256 indexed version); - - /// @notice Thrown when registering a model whose version is already held. - /// @param version The version already registered. - error AlreadyRegistered(uint256 version); - - /// @notice Thrown when pricing against a version that was never registered. - /// @param version The version with no registered model. - error UnknownVersion(uint256 version); - - /// @notice Thrown when registering a model whose version is zero, which is the sentinel for - /// an unregistered version and so cannot name a real model. - error ZeroVersion(); - - /// @notice Thrown when a registration reveals at a different version than it committed to. - /// @dev Raised where a commit-reveal flow binds a version at commit and checks it at reveal, so - /// the version a name prices at cannot move after the commitment is made. - /// @param committed The version bound when the commitment was made. - /// @param revealed The version supplied at reveal. - error PricingVersionMismatch(uint256 committed, uint256 revealed); - - /// @notice Registers a model and makes it current. - /// @dev Owner-only. Keys the model by its own `version`, so a version can be registered once; - /// a repeat triggers @custom:reverts AlreadyRegistered. Moves the current pointer to the - /// new version and emits @custom:emits CostModelRegistered. - /// @param model The cost model to register. - function register(IDotnsPricingOld model) external; - - /// @notice Points the current version at an already-registered model. - /// @dev Owner-only. Reverts to a previously registered version without redeploying it, so - /// governance can roll fresh pricing back to an earlier curve. @custom:reverts - /// UnknownVersion when no model is registered for `version`. Emits @custom:emits - /// CurrentModelSet. - /// @param version The already-registered version to make current. - function setCurrentVersion(uint256 version) external; - - /// @notice Returns the model registered for a version, or the zero address when none. - /// @param version The version to look up. - /// @return model The model registered for that version. - function modelOf(uint256 version) external view returns (IDotnsPricingOld model); - - /// @notice Returns the version currently serving fresh pricing. - /// @return version The current version identifier. - function currentVersion() external view returns (uint256 version); - - /// @notice Returns the current model. - /// @return model The model serving the current version. - function current() external view returns (IDotnsPricingOld model); - - /// @notice Prices a base length at the current version. - /// @param baseLength Digit-stripped length of the label being priced. - /// @return weiPrice Registration cost in wei at the current version. - function priceForBaseLength(uint256 baseLength) external view returns (uint256 weiPrice); - - /// @notice Prices a base length at a specific version. - /// @dev @custom:reverts UnknownVersion when no model is registered for `version`. - /// @param version The version to price against. - /// @param baseLength Digit-stripped length of the label being priced. - /// @return weiPrice Registration cost in wei at that version. - function priceForBaseLengthAtVersion( - uint256 version, - uint256 baseLength - ) - external - view - returns (uint256 weiPrice); -} diff --git a/contracts/pop/IDotnsPricingOld.sol b/contracts/pop/IDotnsPricingOld.sol deleted file mode 100644 index 39b61094b..000000000 --- a/contracts/pop/IDotnsPricingOld.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.34; - -/// @title DotNS Pricing Cost Model -/// @notice Prices a registration from the base length of its label alone. -/// @dev The seam between name policy and the wei amount a registration costs. `PopRulesOld` and the -/// public commit-reveal controller keep the classification, reservation, and tier rules; the -/// model owns only the amount for a given base length, so the curve can be swapped by -/// registering a new model under `DotnsConstantsOld.COST_MODEL` without touching either. Only -/// the base length crosses the seam: the model reads no personhood band or `PopStatus`. The public -/// controller prices NoStatus deposits through this same path, so the model carries no PoP -/// name. -/// @custom:security-contact admin@parity.io -interface IDotnsPricingOld { - /// @notice Thrown when a model constructor parameter breaks a pricing invariant. - /// @param reason Human-readable explanation of the failed invariant. - error PricingError(string reason); - - /// @notice Returns the registration cost in wei for a label of the given base length. - /// @dev Pure amount lookup: the caller supplies the digit-stripped base length and the model - /// returns the curve value for it. Runs on the ERC721 transfer floor read, so it stays a - /// view with no state writes. - /// @param baseLength Digit-stripped length of the label being priced. - /// @return weiPrice Registration cost in wei for that base length. - function priceForBaseLength(uint256 baseLength) external view returns (uint256 weiPrice); - - /// @notice Returns a stable identifier for this model and its parameters. - /// @dev Changes when the model shape or its parameters change, so clients and telemetry can - /// tell one live curve from another. Not consulted on the pricing path. - /// @return modelVersion Identifier derived from the model form and its parameters. - function version() external view returns (uint256 modelVersion); -} diff --git a/contracts/pop/IPopRulesOld.sol b/contracts/pop/IPopRulesOld.sol new file mode 100644 index 000000000..cbe512ce7 --- /dev/null +++ b/contracts/pop/IPopRulesOld.sol @@ -0,0 +1,355 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.34; + +/// @title Proof of Personhood Rules for Dotns +/// @notice Proof of personhood interface defining Dotns price calculation, PoP-tier requirements, +/// and base-name reservation rules. +/// @dev Classifies labels into the PoP tier required for registration and exposes reservation +/// metadata. Every label is measured as written, except a gateway lite label, whose +/// separator and allocated digits are removed first. Base length <= 5 is reserved for +/// governance; 6-8 requires PopFull; >= 9 is open to every caller as NoStatus. PopLite is +/// the separated form alone, so digits in an ordinary label carry no personhood meaning. +/// Reservations are keyed by that same stem, so `joseph` and `joseph.42` share a slot while +/// `joseph42` is an unrelated name. +/// +/// Amounts come from the cost model registered under `DotnsConstants.COST_MODEL`, which owns +/// the curve; only the base length crosses that seam. Every caller pays the same amount for a +/// given length; personhood only unlocks the premium band. +/// @custom:security-contact admin@parity.io +interface IPopRulesOld { + /// @notice Proof-of-Personhood eligibility tier. + /// @dev `NoStatus` is the default for unverified users; `PopLite` and `PopFull` are the two + /// personhood tiers; `Reserved` covers both governance-held names and base stems held by + /// another user through the reservation table. + enum PopStatus { + NoStatus, + PopLite, + PopFull, + Reserved + } + + /// @notice Emitted when a base name receives a reservation. + /// @param baseName The digit-stripped label receiving the reservation. + /// @param owner Address obtaining the reservation right. + /// @param expires UNIX timestamp when the reservation expires. + event BaseNameReserved(string indexed baseName, address indexed owner, uint64 expires); + + /// @notice Emitted when the public market for names shorter than nine characters is opened or + /// closed. + /// @dev Set by the Root-gated @custom:function setShortNamesEnabled. + /// @param enabled Whether names shorter than nine characters may now be bought. + event ShortNamesEnabledUpdated(bool enabled); + + /// @notice Thrown when a name violates PoP-tier or reservation requirements. + /// @param reason Human-readable explanation of the failure condition. + error PopError(string reason); + + /// @notice Thrown when @custom:function setShortNamesEnabled is called without a substrate + /// Root origin. + /// @dev Carries no caller parameter: a Root origin has no account to report, and reading + /// `msg.sender` under one traps. + error NotRoot(); + + /// @notice Thrown when a caller is not an authorised controller on the registrar. + error NotRegistry(); + + /// @notice Thrown when registering a name whose base stem is held as a live reservation by + /// another user. + /// @param label Caller-supplied label whose stem is reserved. + error NameReserved(string label); + + /// @notice Thrown when registering a label that classifies as governance-reserved at the + /// protocol level. + /// @dev Distinct from @custom:reverts NameReserved so off-chain consumers can tell "wait for + /// the holder to relinquish" apart from "this label is permanently held by governance". + /// @param label Caller-supplied label that classifies as governance-reserved. + error GovernanceReserved(string label); + + /// @notice Thrown on the cross-payer path when the owner's recorded PoP tier does not meet the + /// label's required tier. The direct path's `priceWithCheck` covers this same condition via its + /// own revert. + /// @param label Label whose tier requirement was unmet. + /// @param userStatus Owner's recorded tier. + /// @param required Required tier for the label. + error OwnerStatusInsufficient(string label, PopStatus userStatus, PopStatus required); + + /// @notice Bundle returned from metadata-aware pricing queries. + /// @param price Registration cost from the current cost model for the label's base length. + /// @param status Required PoP tier for this name. + /// @param userStatus Current PoP status recorded for the querying user. + /// @param message Human-readable classification description. + struct PriceWithMeta { + uint256 price; + PopStatus status; + PopStatus userStatus; + string message; + } + + /// @notice Reservation metadata for a base name (digits removed). + /// @param owner Address holding exclusive claim rights during the reservation window. + /// @param expires UNIX timestamp when the reservation expires. + /// @param controller Address that wrote the reservation; the only address permitted to release + /// it before expiry. + struct Reservation { + address owner; + uint64 expires; + address controller; + } + + /// @notice Classifies a name into a required PoP tier per DotNS naming rules. + /// @dev Pure; inputs are the label bytes only. Callers use the returned tier to decide which + /// pricing and verification branch applies. A label that is neither a single lowercase + /// ASCII DNS label nor a lite label triggers @custom:reverts PopError; a trailing-digit + /// suffix of any length is accepted and classified by the length it leaves. + /// @param name The name label being evaluated. + /// @return requirement Required tier for registration. + /// @return message Explanation of the classification result. + function classifyName(string calldata name) + external + pure + returns (PopStatus requirement, string memory message); + + /// @notice Opens or closes the public market for names shorter than nine characters. + /// @dev Restricted to a substrate Root origin; any other caller triggers @custom:reverts + /// NotRoot. Short names are otherwise issued through the PoP gateway, so this flag is the + /// Root-only lever that additionally admits them on the public paid path. + /// While closed, which is the deploy default, @custom:function priceWithCheck and + /// @custom:function priceWithoutCheck trigger @custom:reverts PopError for a base length + /// below nine, so no public caller buys a short name. The gateway free grant and the + /// registrar's registerReserved path do not read this flag. Emits @custom:emits + /// ShortNamesEnabledUpdated. + /// @param enabled Whether names shorter than nine characters may be bought. + function setShortNamesEnabled(bool enabled) external; + + /// @notice Returns the personhood tier recorded for an account. + /// @dev Reads the account's dotns-scoped tier from the personhood precompile and maps it to a + /// `PopStatus`. This is the direct account-tier read; the same tier otherwise surfaces + /// only as the `userStatus` field of a pricing query. Never returns `Reserved`, so the + /// result is one of `NoStatus`, `PopLite`, or `PopFull`. + /// @param account Address whose tier is read. + /// @return tier The account's personhood tier. + function personhoodOf(address account) external view returns (PopStatus tier); + + /// @notice Creates or refreshes a reservation entry for a stem in the 6 to 8 band. + /// @dev Authorised-controller entry point: only a controller in the registrar's `controllers` + /// set may call this, otherwise @custom:reverts NotRegistry. The gateway queue writes + /// through @custom:function reserveBaseNameForPop and the public commit-reveal flow + /// reads the slot rather than writing one, so this is the entry point for a sibling + /// controller. Its length window bounds the stem and does not name a tier: PopLite is + /// decided by the separated label shape. The caller passes the already-stripped stem; a + /// non-canonical label, a trailing digit, or a length outside `[6, 8]` triggers + /// @custom:reverts PopError. Cross-user collision on a live slot triggers @custom:reverts + /// PopError so the caller cannot silently overwrite another user's reservation; same-user + /// refresh and writes into an empty or expired slot emit @custom:emits BaseNameReserved. + /// @param stem The base label with no trailing digits. + /// @param user The address receiving reservation rights. + function reserveBaseName(string calldata stem, address user) external; + + /// @notice Emitted when a base-name reservation is cleared. + /// @param baseName The base label whose reservation was released. + event BaseNameReleased(string indexed baseName); + + /// @notice Writes or refreshes a reservation for a bare base-name stem. + /// @dev Gateway-driven reservation path used by the PoP controller. Only a controller in the + /// registrar's `controllers` set may call this, otherwise @custom:reverts NotRegistry. + /// Does not apply the lite-format length window that @custom:function reserveBaseName + /// enforces, but does require the input to be canonical and stem-shaped (no trailing + /// digits); a non-canonical or non-stem label triggers @custom:reverts PopError. If the + /// slot is already live and held by a different user, @custom:reverts PopError so the + /// caller's local bookkeeping and PopRulesOld state stay in lockstep; if it is live for the + /// same user, expiry is refreshed to `block.timestamp + MAX_RESERVATION_TIME`. Emits + /// @custom:emits BaseNameReserved on every successful write. + /// @param stem The base label with no trailing digits. + /// @param user The address receiving reservation rights. + function reserveBaseNameForPop(string calldata stem, address user) external; + + /// @notice Clears a reservation for a base-name stem. + /// @dev Only a controller in the registrar's `controllers` set may call this, otherwise + /// @custom:reverts NotRegistry. Non-canonical or non-stem labels trigger + /// @custom:reverts PopError. Live reservations may only be cleared by the same controller + /// that wrote them; another authorised controller attempting to clear a live slot triggers + /// @custom:reverts PopError. Expired reservations may be cleared by any authorised + /// controller as garbage collection. Used by the PoP controller when a reservation is + /// claimed, relinquished, or a queue head promotion leaves the slot empty. Emits + /// @custom:emits BaseNameReleased once the slot is cleared. + /// @param stem The base label whose reservation should be cleared (no trailing digits). + function releaseBaseName(string calldata stem) external; + + /// @notice Clears a reservation when the slot owner matches `expectedOwner`, allowing any + /// registrar-authorised controller (not only the stamping one) to release the slot. + /// @dev Narrower than @custom:function releaseBaseName: callers must prove they know the + /// slot owner, so cross-controller release is gated on a positive match rather than on + /// caller identity. Intended for the public registrar controller's reclaim path, where + /// a prior occupant has handed the name back to escrow and the new registrant needs + /// the cross-flow guard cleared regardless of which controller originally stamped it. + /// Only a registrar-authorised controller may call this (@custom:reverts NotRegistry). + /// Non-canonical or non-stem labels trigger @custom:reverts PopError. A live reservation + /// whose owner does not match `expectedOwner` triggers @custom:reverts PopError; expired + /// reservations are cleared regardless. Emits @custom:emits BaseNameReleased. + /// @param stem The base label whose reservation should be cleared (no trailing digits). + /// @param expectedOwner The address the caller expects to be the current reservation owner. + function releaseReservationForReclaim(string calldata stem, address expectedOwner) external; + + /// @notice Retrieves reservation information for a base name. + /// @dev Raw accessor: returns the stored slot regardless of expiry. Use + /// @custom:function isBaseNameReserved + /// when live-window semantics are needed. Non-canonical labels trigger + /// @custom:reverts PopError. + /// @param baseName The base label without trailing digits. + /// @return owner The address assigned to the reservation. + /// @return expires UNIX timestamp when the reservation expires. + function getBaseNameReservation(string calldata baseName) + external + view + returns (address owner, uint64 expires); + + /// @notice Returns the reservation stem of a label: a lite label without its allocated + /// suffix, or any other label unchanged. + /// @dev Mirrors the normalisation applied before a reservation is written, so callers can + /// look up or release one by passing the full label. Only a lite label is shortened, + /// because only the gateway allocates the digits it carries: `joseph.42` yields + /// `joseph` while `joseph42` is an unrelated name and yields itself. Non-canonical + /// labels trigger @custom:reverts PopError. + /// @param name Full label, lite or otherwise. + /// @return stem The reservation stem of `name`. + function stripDigits(string calldata name) external pure returns (string memory stem); + + /// @notice Indicates whether a base name is currently reserved. + /// @dev Applies the live-window predicate to the stored slot so an expired reservation reads + /// as free. Non-canonical labels trigger @custom:reverts PopError. + /// @param baseName The base label without trailing digits. + /// @return reservedStatus True if a live reservation is active. + /// @return owner The reservation holder (zero when not reserved). + /// @return expires UNIX timestamp when the reservation expires. + function isBaseNameReserved(string calldata baseName) + external + view + returns (bool reservedStatus, address owner, uint64 expires); + + /// @notice Calculates price with PoP classification and reservation enforcement. + /// @dev Reverting pricing path used by the commit-reveal controller. Price is the scarcity + /// curve for the label's base length and is charged to every caller, verified or not; + /// personhood only unlocks the premium band. Non-canonical + /// labels, a base stem held live by another user, a governance-reserved label, or a + /// `userAddress` whose personhood tier does not meet the label's required tier each + /// trigger @custom:reverts PopError. + /// @param name Domain label. + /// @param userAddress Registering user for the given label. + /// @return metadata Price with PoP requirements and classification. + function priceWithCheck( + string calldata name, + address userAddress + ) + external + view + returns (PriceWithMeta memory metadata); + + /// @notice Calculates price at a specific cost-model version with PoP classification and + /// reservation enforcement. + /// @dev The versioned counterpart of @custom:function priceWithCheck: identical classification, + /// tier gating, and reservation rules, but the amount comes from the model registered for + /// `pricingVersionValue` rather than the current one. The commit-reveal controller prices + /// a reveal at the version bound into its commitment, so a model change between commit and + /// reveal does not move the amount. @custom:reverts UnknownVersion when the version was + /// never registered. + /// @param name Domain label. + /// @param userAddress Registering user for the given label. + /// @param pricingVersionValue Cost-model version to price against. + /// @return metadata Price with PoP requirements and classification. + function priceWithCheckAtVersion( + string calldata name, + address userAddress, + uint256 pricingVersionValue + ) + external + view + returns (PriceWithMeta memory metadata); + + /// @notice Calculates price with PoP classification and reservation metadata, without + /// reverting on conflicts. + /// @dev Non-reverting counterpart to `priceWithCheck`: surfaces the same fields, but reports + /// a `Reserved` status through `metadata` instead of reverting when the base stem is + /// held by another user. Used by front-ends that need to present a price and eligibility + /// preview without forcing a transaction attempt. Governance-reserved names are not + /// rejected here either; the caller decides what to do. Non-canonical labels still + /// trigger @custom:reverts PopError because the input is malformed rather than just + /// contested. + /// @param name Domain label. + /// @param userAddress Registering user for the given label. + /// @return metadata Price with PoP requirements and classification. + function priceWithoutCheck( + string calldata name, + address userAddress + ) + external + view + returns (PriceWithMeta memory metadata); + + /// @notice Calculates price at a specific cost-model version with PoP classification and + /// reservation metadata, without reverting on conflicts. + /// @dev The versioned counterpart of @custom:function priceWithoutCheck: same non-reverting + /// preview behaviour, but the amount comes from the model registered for + /// `pricingVersionValue`. @custom:reverts UnknownVersion when the version was never + /// registered. + /// @param name Domain label. + /// @param userAddress Registering user for the given label. + /// @param pricingVersionValue Cost-model version to price against. + /// @return metadata Price with PoP requirements and classification. + function priceWithoutCheckAtVersion( + string calldata name, + address userAddress, + uint256 pricingVersionValue + ) + external + view + returns (PriceWithMeta memory metadata); + + /// @notice Transfer-time floor: the greater of the recipient-reach component and the + /// sender-tier-downgrade component, each priced at the name's own length. + /// @dev Re-prices the name at its own length on every move: returns the name's curve price when + /// either (i) the recipient does not meet the label's required tier, or (ii) the + /// recipient's personhood tier is strictly below the sender's, and zero when neither + /// holds. Passing a name to a wallet that could never have registered it therefore costs + /// the name's own curve price. The two components overlap on pure + /// tier mismatches, so the function takes their maximum rather than their sum to avoid + /// double-charging. Consumed by @custom:function DotnsRegistrarOld.quoteTransferFee. + /// A label that is neither a single lowercase ASCII DNS label nor a lite label triggers + /// @custom:reverts PopError. + /// @param name Domain label being transferred. + /// @param from Current holder of the name. + /// @param to Incoming holder of the name. + /// @return floor Transfer-time floor in wei: the name's own curve price, or zero. + function transferFloor( + string calldata name, + address from, + address to + ) + external + view + returns (uint256 floor); + + /// @notice Returns whether `name` is a base name under PoP rules. + /// @dev A base name has no trailing digits, so it is what a reservation may be keyed by. A + /// lite label always ends in two, so it is never a base name. Non-canonical labels + /// trigger @custom:reverts PopError. + /// @param name The label to check. + /// @return isBase True when the label has no trailing digits. + function isBaseName(string calldata name) external pure returns (bool isBase); + + /// @notice Calculates registration cost for a label. + /// @dev Prices the label by its base length through the cost model registered under + /// `DotnsConstants.COST_MODEL`. Ignores the caller's personhood status and reservation + /// state. A non-canonical label triggers @custom:reverts PopError. An ordinary label is + /// priced as written; only a lite label's allocated suffix is removed first. + /// @param name Domain label to price. + /// @return cost Registration cost in wei. + function price(string calldata name) external view returns (uint256 cost); + + /// @notice Returns the current cost-model version. + /// @dev The current version held by the registry under `DotnsConstants.COST_MODEL`. The + /// commit-reveal controller binds it into a commitment and prices the reveal at that + /// version, so a model change between commit and reveal leaves the committed amount + /// unchanged. @custom:reverts PopError when no registry is configured. + /// @return modelVersion Identifier of the current cost model and its parameters. + function pricingVersion() external view returns (uint256 modelVersion); +} diff --git a/contracts/pop/PopRulesOld.sol b/contracts/pop/PopRulesOld.sol index de528afd4..49bc3bfbe 100644 --- a/contracts/pop/PopRulesOld.sol +++ b/contracts/pop/PopRulesOld.sol @@ -9,14 +9,14 @@ import { import { ERC165Upgradeable } from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; -import {StringUtils} from "../utils/StringUtils.sol"; -import {SystemUtilsOld} from "../utils/SystemUtilsOld.sol"; -import {IPopRules} from "./IPopRules.sol"; -import {IDotnsCostModelRegistryOld} from "./IDotnsCostModelRegistryOld.sol"; -import {IDotnsProtocolRegistryOld} from "../registry/IDotnsProtocolRegistryOld.sol"; +import {StringUtilsOld} from "../utils/StringUtilsOld.sol"; +import {SystemUtils} from "../utils/SystemUtils.sol"; +import {IPopRulesOld} from "./IPopRulesOld.sol"; +import {IDotnsCostModelRegistry} from "./IDotnsCostModelRegistry.sol"; +import {IDotnsProtocolRegistry} from "../registry/IDotnsProtocolRegistry.sol"; import {IDotnsController} from "../registrars/IDotnsController.sol"; import {DotnsRegistrarOld} from "../registrars/DotnsRegistrarOld.sol"; -import {DotnsConstantsOld} from "../utils/DotnsConstantsOld.sol"; +import {DotnsConstants} from "../utils/DotnsConstants.sol"; import {IPersonhood} from "../external/personhood/IPersonhood.sol"; /// @title PopRulesOld @@ -28,7 +28,7 @@ import {IPersonhood} from "../external/personhood/IPersonhood.sol"; /// lengths >= 9 are open to any caller as NoStatus. PopLite is the separated form alone: a /// digit suffix on an ordinary label says nothing about personhood. /// Every caller pays the same amount for a given base length. The amount comes from the cost -/// model registered under `DotnsConstantsOld.COST_MODEL`, which owns the curve; this contract +/// model registered under `DotnsConstants.COST_MODEL`, which owns the curve; this contract /// passes it only the base length and keeps the classification, reservation, and tier rules. /// Personhood only unlocks the premium band. Base lengths below nine are closed to the public /// paid path until Root sets `shortNamesEnabled`; the gateway and registerReserved do @@ -39,9 +39,9 @@ contract PopRulesOld is UUPSUpgradeable, OwnableUpgradeable, ERC165Upgradeable, - IPopRules + IPopRulesOld { - using StringUtils for *; + using StringUtilsOld for *; /// @notice Active reservations keyed by stem. mapping(string baseName => Reservation reservation) public reservations; @@ -50,7 +50,7 @@ contract PopRulesOld is uint256 public constant MAX_RESERVATION_TIME = 12 weeks; /// @notice Protocol-level address registry for all DotNS contracts. - IDotnsProtocolRegistryOld public protocolRegistry; + IDotnsProtocolRegistry public protocolRegistry; /// @notice Whether the public paid path may register names shorter than nine characters. /// Closed by default; only governance opens it. @@ -73,25 +73,26 @@ contract PopRulesOld is /// @notice Initialises the oracle (public entry point). /// @dev Runs once behind the proxy; subsequent calls trigger @custom:reverts /// InvalidInitialization via the `initializer` modifier. Amounts come from the cost model - /// registered under `DotnsConstantsOld.COST_MODEL`, so no price is seeded here. + /// registered under `DotnsConstants.COST_MODEL`, so no price is seeded here. + /// @param initialOwner Address that owns the contract once initialised. /// @param registry Protocol-level address registry used to resolve sibling contracts. - function initialize(IDotnsProtocolRegistryOld registry) public initializer { - __Ownable_init(msg.sender); + function initialize(address initialOwner, IDotnsProtocolRegistry registry) public initializer { + __Ownable_init(initialOwner); __ERC165_init(); protocolRegistry = registry; } - /// @inheritdoc IPopRules + /// @inheritdoc IPopRulesOld function setShortNamesEnabled(bool enabled) external override { // Opening the short-name band to the public path is a governance decision, so it is gated // on a substrate Root origin rather than the owner. `msg.sender` is deliberately not read: // a Root origin has no account behind it, so reading it would trap. - require(SystemUtilsOld.originIsRoot(), NotRoot()); + require(SystemUtils.originIsRoot(), NotRoot()); shortNamesEnabled = enabled; emit ShortNamesEnabledUpdated(enabled); } - /// @inheritdoc IPopRules + /// @inheritdoc IPopRulesOld function classifyName(string calldata name) external pure @@ -102,7 +103,7 @@ contract PopRulesOld is (requirement, message,) = _classifyValidatedName(name); } - /// @inheritdoc IPopRules + /// @inheritdoc IPopRulesOld function reserveBaseName( string calldata stem, address userAddress @@ -120,14 +121,14 @@ contract PopRulesOld is _writeReservation(stem, userAddress); } - /// @inheritdoc IPopRules + /// @inheritdoc IPopRulesOld function isBaseName(string calldata baseName) external pure override returns (bool isBase) { _requireLabel(baseName); uint256 digits = _countTrailingDigits(baseName); return digits == 0; } - /// @inheritdoc IPopRules + /// @inheritdoc IPopRulesOld function getBaseNameReservation(string calldata baseName) external view @@ -139,7 +140,7 @@ contract PopRulesOld is return (reserved.owner, reserved.expires); } - /// @inheritdoc IPopRules + /// @inheritdoc IPopRulesOld function isBaseNameReserved(string calldata baseName) external view @@ -151,7 +152,7 @@ contract PopRulesOld is return (_isLive(reservation), reservation.owner, reservation.expires); } - /// @inheritdoc IPopRules + /// @inheritdoc IPopRulesOld function priceWithCheck( string calldata name, address userAddress @@ -164,7 +165,7 @@ contract PopRulesOld is return _priceWithCheck(name, userAddress, false, 0); } - /// @inheritdoc IPopRules + /// @inheritdoc IPopRulesOld function priceWithCheckAtVersion( string calldata name, address userAddress, @@ -178,7 +179,7 @@ contract PopRulesOld is return _priceWithCheck(name, userAddress, true, pricingVersionValue); } - /// @inheritdoc IPopRules + /// @inheritdoc IPopRulesOld function priceWithoutCheck( string calldata name, address userAddress @@ -191,7 +192,7 @@ contract PopRulesOld is return _priceWithoutCheck(name, userAddress, false, 0); } - /// @inheritdoc IPopRules + /// @inheritdoc IPopRulesOld function priceWithoutCheckAtVersion( string calldata name, address userAddress, @@ -273,24 +274,24 @@ contract PopRulesOld is if (_isLive(reservation) && reservation.owner != userAddress) { metadata.message = "Base name reserved for original Lite registrant"; - metadata.status = IPopRules.PopStatus.Reserved; + metadata.status = IPopRulesOld.PopStatus.Reserved; } return metadata; } - /// @inheritdoc IPopRules + /// @inheritdoc IPopRulesOld function price(string calldata name) external view override returns (uint256) { _requireLabel(name); return _priceValidatedName(_validatedBaseLength(name)); } - /// @inheritdoc IPopRules + /// @inheritdoc IPopRulesOld function pricingVersion() external view override returns (uint256 modelVersion) { return _costModelRegistry().currentVersion(); } - /// @inheritdoc IPopRules + /// @inheritdoc IPopRulesOld function transferFloor( string calldata name, address from, @@ -317,7 +318,7 @@ contract PopRulesOld is return reachComponent > downgradeComponent ? reachComponent : downgradeComponent; } - /// @inheritdoc IPopRules + /// @inheritdoc IPopRulesOld function personhoodOf(address account) external view override returns (PopStatus tier) { return _personhoodTier(account); } @@ -330,8 +331,8 @@ contract PopRulesOld is /// collapses to `NoStatus` so a future tier addition fails closed instead of /// silently being treated as a higher level than it actually is. function _personhoodTier(address account) private view returns (PopStatus) { - IPersonhood.PersonhoodInfo memory info = IPersonhood(DotnsConstantsOld.PERSONHOOD) - .personhoodStatus(account, DotnsConstantsOld.PERSONHOOD_CONTEXT); + IPersonhood.PersonhoodInfo memory info = IPersonhood(DotnsConstants.PERSONHOOD) + .personhoodStatus(account, DotnsConstants.PERSONHOOD_CONTEXT); if (info.status == 2) return PopStatus.PopFull; if (info.status == 1) return PopStatus.PopLite; return PopStatus.NoStatus; @@ -372,13 +373,13 @@ contract PopRulesOld is return _costModelRegistry().priceForBaseLengthAtVersion(pricingVersionValue, baseLength); } - /// @notice Resolves the cost-model registry registered under `DotnsConstantsOld.COST_MODEL`. + /// @notice Resolves the cost-model registry registered under `DotnsConstants.COST_MODEL`. /// @dev @custom:reverts PopError when no registry is configured, so a pricing read fails closed /// rather than resolving through the zero address. - function _costModelRegistry() private view returns (IDotnsCostModelRegistryOld registry) { - address configured = protocolRegistry.get(DotnsConstantsOld.COST_MODEL); + function _costModelRegistry() private view returns (IDotnsCostModelRegistry registry) { + address configured = protocolRegistry.get(DotnsConstants.COST_MODEL); require(configured != address(0), PopError("Cost model not configured")); - return IDotnsCostModelRegistryOld(configured); + return IDotnsCostModelRegistry(configured); } /// @notice Reverts a public paid registration of a base length below nine while the short-name @@ -401,7 +402,7 @@ contract PopRulesOld is function _stemEnd(string calldata name) private pure returns (uint256 stemEnd) { stemEnd = bytes(name).length; if (!name.isLitePersonLabel()) return stemEnd; - return stemEnd - StringUtils.LITE_SUFFIX_DIGITS - 1; + return stemEnd - StringUtilsOld.LITE_SUFFIX_DIGITS - 1; } /// @notice The base length that pricing and classification both use to place a name in its @@ -525,25 +526,30 @@ contract PopRulesOld is override returns (bool supported) { - return interfaceId == type(IPopRules).interfaceId || super.supportsInterface(interfaceId); + return interfaceId == type(IPopRulesOld).interfaceId || super.supportsInterface(interfaceId); } /// @inheritdoc UUPSUpgradeable function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} - /// @notice Returns implementation version. - function version() external pure virtual returns (string memory versionString) { - versionString = "1.0.0"; + /// @notice Returns the release this network declares it runs, read live from the protocol + /// registry so every DotNS contract reports one synchronised value. + /// @dev Mirror of `IDotnsProtocolRegistry.protocolVersion`, kept under the historical + /// `version()` selector for ABI compatibility. It reports the network's declaration, + /// not this contract's build; per-contract identity is the codehash declared on the + /// registry. + /// @return versionString Declared release as bare semver, empty when never declared. + function version() external view virtual returns (string memory versionString) { + versionString = protocolRegistry.protocolVersion(); } /// @notice Ensures the caller is any controller authorised on the registrar. function _onlyRegistry() internal view { - DotnsRegistrarOld registrar = - DotnsRegistrarOld(protocolRegistry.get(DotnsConstantsOld.REGISTRAR)); + DotnsRegistrarOld registrar = DotnsRegistrarOld(protocolRegistry.get(DotnsConstants.REGISTRAR)); require(registrar.controllers(IDotnsController(msg.sender)), NotRegistry()); } - /// @inheritdoc IPopRules + /// @inheritdoc IPopRulesOld function reserveBaseNameForPop( string calldata stem, address userAddress @@ -560,13 +566,13 @@ contract PopRulesOld is _writeReservation(stem, userAddress); } - /// @inheritdoc IPopRules + /// @inheritdoc IPopRulesOld function stripDigits(string calldata name) external pure override returns (string memory stem) { _requireLabel(name); return _stripDigits(name); } - /// @inheritdoc IPopRules + /// @inheritdoc IPopRulesOld function releaseBaseName(string calldata stem) external override onlyRegistry { _requireStem(stem); require( @@ -588,7 +594,7 @@ contract PopRulesOld is emit BaseNameReleased(stem); } - /// @inheritdoc IPopRules + /// @inheritdoc IPopRulesOld function releaseReservationForReclaim( string calldata stem, address expectedOwner diff --git a/contracts/registrars/DotnsPopControllerOld.sol b/contracts/registrars/DotnsPopControllerOld.sol index d6bca3a2f..ff14e642c 100644 --- a/contracts/registrars/DotnsPopControllerOld.sol +++ b/contracts/registrars/DotnsPopControllerOld.sol @@ -13,20 +13,20 @@ import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -import {IDotnsPopController} from "./IDotnsPopController.sol"; -import {IDotnsRegistrarOld} from "./IDotnsRegistrarOld.sol"; -import {IDotnsProtocolRegistryOld} from "../registry/IDotnsProtocolRegistryOld.sol"; -import {IDotnsPopResolver} from "../resolvers/IDotnsPopResolver.sol"; -import {IPopRules} from "../pop/IPopRules.sol"; -import {IStoreFactoryOld} from "../store/IStoreFactoryOld.sol"; +import {IDotnsPopControllerOld} from "./IDotnsPopControllerOld.sol"; +import {IDotnsRegistrar} from "./IDotnsRegistrar.sol"; +import {IDotnsProtocolRegistry} from "../registry/IDotnsProtocolRegistry.sol"; +import {IDotnsPopResolverOld} from "../resolvers/IDotnsPopResolverOld.sol"; +import {IPopRulesOld} from "../pop/IPopRulesOld.sol"; +import {IStoreFactory} from "../store/IStoreFactory.sol"; import {ILabelStore} from "../store/ILabelStore.sol"; import {LabelUtils} from "../utils/LabelUtils.sol"; -import {RegistrationUtilsOld} from "../utils/RegistrationUtilsOld.sol"; +import {RegistrationUtils} from "../utils/RegistrationUtils.sol"; import {SubnodeUtilsOld} from "../utils/SubnodeUtilsOld.sol"; -import {IDotnsRegistryOld} from "../registry/IDotnsRegistryOld.sol"; -import {StringUtils} from "../utils/StringUtils.sol"; -import {DotnsConstantsOld} from "../utils/DotnsConstantsOld.sol"; -import {SystemUtilsOld} from "../utils/SystemUtilsOld.sol"; +import {IDotnsRegistry} from "../registry/IDotnsRegistry.sol"; +import {StringUtilsOld} from "../utils/StringUtilsOld.sol"; +import {DotnsConstants} from "../utils/DotnsConstants.sol"; +import {SystemUtils} from "../utils/SystemUtils.sol"; /// @title DotnsPopControllerOld /// @notice Dedicated PoP controller orchestrating lite-person and full-person username @@ -38,7 +38,7 @@ import {SystemUtilsOld} from "../utils/SystemUtilsOld.sol"; /// Enforcement: /// Personhood is attested off-chain by the gateway before the call reaches this /// contract, so the on-chain personhood precompile is not re-queried on the gateway path. -/// Every base-label mint path still calls @custom:function IPopRules.classifyName to reject +/// Every base-label mint path still calls @custom:function IPopRulesOld.classifyName to reject /// governance-reserved labels (@custom:reverts InvalidBaseLabel on the base path, /// @custom:reverts InvalidLiteLabel on the lite path). The lite leg accepts any two-digit lite /// label whose stem is not governance-reserved, regardless of stem length. Native-token pricing @@ -56,19 +56,19 @@ import {SystemUtilsOld} from "../utils/SystemUtilsOld.sol"; /// contend for the same label. This holds of labels the contracts minted, not of an arbitrary /// string: a subname stored under a digit-only parent reads the same way, which is why /// provenance is published through @custom:function isPopIssued rather than inferred. -/// (2) Base-name reservations are synchronised into `IPopRules`. The head of this -/// controller's reservation queue is written through `IPopRules.reserveBaseNameForPop` on -/// every head transition; the slot is cleared through `IPopRules.releaseBaseName` when the +/// (2) Base-name reservations are synchronised into `IPopRulesOld`. The head of this +/// controller's reservation queue is written through `IPopRulesOld.reserveBaseNameForPop` on +/// every head transition; the slot is cleared through `IPopRulesOld.releaseBaseName` when the /// queue empties (claim, final relinquish, final expiry). The public commit-reveal -/// controller routes through `IPopRules.priceWithCheck`, which rejects any registration +/// controller routes through `IPopRulesOld.priceWithCheck`, which rejects any registration /// targeting a base-name stem reserved for another user, so the public flow respects /// gateway reservations without ever importing this contract. PopRulesOld is the single /// cross-flow authority; the queue here is the intra-PoP ordering layer on top of it. /// /// Shared primitives: labelhash / namehash via @custom:contract LabelUtils; the mint + -/// forward-registry + store-write triad via @custom:contract RegistrationUtilsOld; chat-key and +/// forward-registry + store-write triad via @custom:contract RegistrationUtils; chat-key and /// lite-to-full link persistence via -/// @custom:contract IDotnsPopResolver. Keeping per-name records on the resolver preserves the +/// @custom:contract IDotnsPopResolverOld. Keeping per-name records on the resolver preserves the /// "Store = labels only" invariant. /// @custom:security-contact admin@parity.io contract DotnsPopControllerOld is @@ -76,9 +76,9 @@ contract DotnsPopControllerOld is UUPSUpgradeable, OwnableUpgradeable, ERC165Upgradeable, - IDotnsPopController + IDotnsPopControllerOld { - using StringUtils for *; + using StringUtilsOld for *; using EnumerableSet for EnumerableSet.AddressSet; /// @notice Upper bound for the number of simultaneously queued reservations per label. @@ -91,13 +91,13 @@ contract DotnsPopControllerOld is uint64 public constant MIN_RESERVATION_DURATION = 1 hours; /// @notice Required byte length for a non-empty chat key. - /// @dev Mirrors @custom:contract IDotnsPopResolver `InvalidChatKeyLength` so the controller + /// @dev Mirrors @custom:contract IDotnsPopResolverOld `InvalidChatKeyLength` so the controller /// can fail closed before the mint instead of bubbling the resolver's revert after partial /// state has been committed. uint256 private constant CHAT_KEY_LENGTH = 65; /// @notice Protocol-level address registry for all DotNS contracts. - IDotnsProtocolRegistryOld public protocolRegistry; + IDotnsProtocolRegistry public protocolRegistry; /// @notice Per-label queue metadata (head/tail pointers). mapping(bytes32 labelhash => ReservationQueueMeta meta) internal _reservationMeta; @@ -165,8 +165,10 @@ contract DotnsPopControllerOld is /// call outside an active initialiser scope reverts with @custom:reverts NotInitializing. /// Emits @custom:emits ReservationDurationSet so indexers observe the initial value /// through the same event the setter uses later. + /// @param initialOwner Address that owns the contract once initialised. function initialize( - IDotnsProtocolRegistryOld registry, + address initialOwner, + IDotnsProtocolRegistry registry, uint64 reservationDuration_ ) external @@ -176,26 +178,26 @@ contract DotnsPopControllerOld is reservationDuration_ >= MIN_RESERVATION_DURATION, ReservationDurationTooLow(reservationDuration_) ); - __Ownable_init(msg.sender); + __Ownable_init(initialOwner); __ERC165_init(); protocolRegistry = registry; reservationDuration = reservationDuration_; emit ReservationDurationSet(reservationDuration_); } - /// @inheritdoc IDotnsPopController + /// @inheritdoc IDotnsPopControllerOld function isPopIssued(string calldata label) external view override returns (bool issued) { return _popIssued[label]; } - /// @inheritdoc IDotnsPopController + /// @inheritdoc IDotnsPopControllerOld function reserveLiteName(LiteRegistration calldata params) external override onlyRoot { _reserveLite(_popRules(), params); } - /// @inheritdoc IDotnsPopController + /// @inheritdoc IDotnsPopControllerOld function reserveBaseName(BaseReservation calldata params) external override onlyRoot { - IPopRules rules = _popRules(); + IPopRulesOld rules = _popRules(); bytes32 reservedHash; bool hasReservation = bytes(params.reservedBaseLabel).length != 0; if (hasReservation) { @@ -211,9 +213,9 @@ contract DotnsPopControllerOld is } } - /// @inheritdoc IDotnsPopController + /// @inheritdoc IDotnsPopControllerOld function reserveBaseNameOnly(BaseNameReservation calldata params) external override onlyRoot { - IPopRules rules = _popRules(); + IPopRulesOld rules = _popRules(); (bytes32 reservedHash,) = _validateReservableBaseLabel(rules, params.reservedBaseLabel); _advanceExpiredHead(reservedHash); _removeUserFromQueue(params.user); @@ -231,15 +233,15 @@ contract DotnsPopControllerOld is /// Takes the @custom:struct LiteRegistration struct directly so both call sites pass the same /// payload shape: the typed entrypoint forwards its own `params`, the `reserveBaseName` /// entrypoint forwards `params.lite`. - function _reserveLite(IPopRules rules, LiteRegistration calldata params) internal { + function _reserveLite(IPopRulesOld rules, LiteRegistration calldata params) internal { require(params.liteLabel.isLitePersonLabel(), InvalidLiteLabel()); _requireValidChatKey(params.chatKey); - (IPopRules.PopStatus required,) = rules.classifyName(params.liteLabel); + (IPopRulesOld.PopStatus required,) = rules.classifyName(params.liteLabel); // The shape check fixes the suffix, so classification lands on PopLite (stem 6-8), // NoStatus (stem 9 or more), or Reserved (stem 5 or fewer). Accept the first two; a // stem short enough to be governance-reserved is not issued from this path. - require(required != IPopRules.PopStatus.Reserved, InvalidLiteLabel()); + require(required != IPopRulesOld.PopStatus.Reserved, InvalidLiteLabel()); (bytes32 labelhash, bytes32 node) = _validateLiteLabel(params.liteLabel); _completeGatewayRegistration( @@ -249,7 +251,7 @@ contract DotnsPopControllerOld is emit LiteNameReserved(labelhash, params.user, params.liteLabel); } - /// @inheritdoc IDotnsPopController + /// @inheritdoc IDotnsPopControllerOld function registerBaseName(FullRegistration calldata params) external override onlyRoot { Link calldata link = params.link; address user = params.user; @@ -257,10 +259,10 @@ contract DotnsPopControllerOld is (bytes32 labelhash, bytes32 node) = _validateBaseLabel(label); - IPopRules rules = _popRules(); - (IPopRules.PopStatus required,) = rules.classifyName(label); + IPopRulesOld rules = _popRules(); + (IPopRulesOld.PopStatus required,) = rules.classifyName(label); require( - required != IPopRules.PopStatus.Reserved && required != IPopRules.PopStatus.PopLite, + required != IPopRulesOld.PopStatus.Reserved && required != IPopRulesOld.PopStatus.PopLite, InvalidBaseLabel() ); @@ -325,13 +327,13 @@ contract DotnsPopControllerOld is } } - /// @inheritdoc IDotnsPopController + /// @inheritdoc IDotnsPopControllerOld function expireReservation(string calldata reservedBaseLabel) external override { (bytes32 labelhash,) = _validateBaseLabel(reservedBaseLabel); _advanceExpiredHead(labelhash); } - /// @inheritdoc IDotnsPopController + /// @inheritdoc IDotnsPopControllerOld function relinquishReservation() external override { UserReservation memory userRes = _userReservations[msg.sender]; require(userRes.labelhash != bytes32(0), NoActiveReservation(msg.sender)); @@ -339,12 +341,12 @@ contract DotnsPopControllerOld is emit ReservationRelinquished(userRes.labelhash, msg.sender); } - /// @inheritdoc IDotnsPopController + /// @inheritdoc IDotnsPopControllerOld function claimLabelStore() external override returns (bool moreRemaining) { - (, moreRemaining) = _settlePending(msg.sender, DotnsConstantsOld.MAX_PAGE_SIZE); + (, moreRemaining) = _settlePending(msg.sender, DotnsConstants.MAX_PAGE_SIZE); } - /// @inheritdoc IDotnsPopController + /// @inheritdoc IDotnsPopControllerOld function settlePendingClaims( address user, uint256 limit @@ -367,7 +369,7 @@ contract DotnsPopControllerOld is internal returns (uint256 settledCount, bool moreRemaining) { - IStoreFactoryOld factory = _storeFactory(); + IStoreFactory factory = _storeFactory(); address store = factory.getLabelStore(user); PendingClaim[] storage queue = _pendingClaimQueue[user]; @@ -396,7 +398,7 @@ contract DotnsPopControllerOld is /// empty queue never leaves a fresh store behind with nothing in it. Returns the (possibly /// newly deployed) store so the caller threads it through the remaining entries. function _settlePendingLabel( - IStoreFactoryOld factory, + IStoreFactory factory, address store, address user, string memory label @@ -419,7 +421,7 @@ contract DotnsPopControllerOld is return store; } - /// @inheritdoc IDotnsPopController + /// @inheritdoc IDotnsPopControllerOld function isReservedForClaim(string calldata reservedBaseLabel) external view @@ -437,14 +439,14 @@ contract DotnsPopControllerOld is return (true, head.owner); } - /// @inheritdoc IDotnsPopController + /// @inheritdoc IDotnsPopControllerOld function setReservationDuration(uint64 duration) external override onlyOwner { require(duration >= MIN_RESERVATION_DURATION, ReservationDurationTooLow(duration)); reservationDuration = duration; emit ReservationDurationSet(duration); } - /// @inheritdoc IDotnsPopController + /// @inheritdoc IDotnsPopControllerOld function reservationMeta(bytes32 labelhash) external view @@ -455,7 +457,7 @@ contract DotnsPopControllerOld is return (meta.head, meta.tail); } - /// @inheritdoc IDotnsPopController + /// @inheritdoc IDotnsPopControllerOld function reservationEntry( bytes32 labelhash, uint64 index @@ -469,7 +471,7 @@ contract DotnsPopControllerOld is return (entry.owner, entry.joinedAt); } - /// @inheritdoc IDotnsPopController + /// @inheritdoc IDotnsPopControllerOld function userReservation(address user) external view @@ -479,7 +481,7 @@ contract DotnsPopControllerOld is return _userReservations[user]; } - /// @inheritdoc IDotnsPopController + /// @inheritdoc IDotnsPopControllerOld function pendingClaims( address user, uint256 offset, @@ -496,7 +498,7 @@ contract DotnsPopControllerOld is uint256 available = total - offset; uint256 count = limit < available ? limit : available; - if (count > DotnsConstantsOld.MAX_PAGE_SIZE) count = DotnsConstantsOld.MAX_PAGE_SIZE; + if (count > DotnsConstants.MAX_PAGE_SIZE) count = DotnsConstants.MAX_PAGE_SIZE; claims = new PendingClaim[](count); for (uint256 i; i < count; ++i) { @@ -504,17 +506,17 @@ contract DotnsPopControllerOld is } } - /// @inheritdoc IDotnsPopController + /// @inheritdoc IDotnsPopControllerOld function pendingClaimCountOf(address user) external view override returns (uint256 count) { return _pendingClaimQueue[user].length; } - /// @inheritdoc IDotnsPopController + /// @inheritdoc IDotnsPopControllerOld function pendingClaimUserCount() external view override returns (uint256 count) { return _pendingClaimUsers.length(); } - /// @inheritdoc IDotnsPopController + /// @inheritdoc IDotnsPopControllerOld function pendingClaimUsers( uint256 offset, uint256 limit @@ -529,7 +531,7 @@ contract DotnsPopControllerOld is uint256 available = total - offset; uint256 count = limit < available ? limit : available; - if (count > DotnsConstantsOld.MAX_PAGE_SIZE) count = DotnsConstantsOld.MAX_PAGE_SIZE; + if (count > DotnsConstants.MAX_PAGE_SIZE) count = DotnsConstants.MAX_PAGE_SIZE; users = new address[](count); for (uint256 i; i < count; ++i) { @@ -537,7 +539,7 @@ contract DotnsPopControllerOld is } } - /// @inheritdoc IDotnsPopController + /// @inheritdoc IDotnsPopControllerOld function reservedBaseLabelOf(bytes32 labelhash) external view @@ -554,25 +556,30 @@ contract DotnsPopControllerOld is override(ERC165Upgradeable, IERC165) returns (bool) { - return interfaceId == type(IDotnsPopController).interfaceId + return interfaceId == type(IDotnsPopControllerOld).interfaceId || super.supportsInterface(interfaceId); } - /// @notice Returns implementation version. - /// @return versionString Current version string. - function version() external pure virtual returns (string memory versionString) { - versionString = "1.0.0"; + /// @notice Returns the release this network declares it runs, read live from the protocol + /// registry so every DotNS contract reports one synchronised value. + /// @dev Mirror of `IDotnsProtocolRegistry.protocolVersion`, kept under the historical + /// `version()` selector for ABI compatibility. It reports the network's declaration, + /// not this contract's build; per-contract identity is the codehash declared on the + /// registry. + /// @return versionString Declared release as bare semver, empty when never declared. + function version() external view virtual returns (string memory versionString) { + versionString = protocolRegistry.protocolVersion(); } /// @notice Mints a name, wires forward registry, persists PoP-flow records (chat key, /// lite link) on the PoP resolver, and either writes the label into the owner's /// existing `LabelStore` or stashes a pending claim when the owner has none yet. /// @dev The mint + forward-registry pair is delegated to - /// @custom:function RegistrationUtilsOld.registerAndStore so this flow and the public + /// @custom:function RegistrationUtils.registerAndStore so this flow and the public /// commit-reveal flow share exactly one implementation of that sequence. The label is /// passed empty so the registrar does not deploy a `LabelStore`; the mint origin cannot /// run the `LabelStore` constructor. PoP-flow per-name records - /// (chat key, lite link) are persisted eagerly on @custom:contract IDotnsPopResolver + /// (chat key, lite link) are persisted eagerly on @custom:contract IDotnsPopResolverOld /// here, before the label is written, so the resolver carries the full identity record /// from mint time regardless of whether the owner already has a `LabelStore`. The Store /// stays labels-only. Warm path emits @custom:emits NameRegistered immediately; the @@ -613,8 +620,8 @@ contract DotnsPopControllerOld is }) ); } else { - RegistrationUtilsOld.registerAndStore( - RegistrationUtilsOld.RegistrationContext({ + RegistrationUtils.registerAndStore( + RegistrationUtils.RegistrationContext({ protocolRegistry: protocolRegistry, user: user, label: "", @@ -625,7 +632,7 @@ contract DotnsPopControllerOld is } if (chatKeyBytes.length != 0 || liteLabelhash != bytes32(0)) { - IDotnsPopResolver resolver = _popResolver(); + IDotnsPopResolverOld resolver = _popResolver(); if (chatKeyBytes.length != 0) { resolver.setChatKey(node, chatKeyBytes); } @@ -685,7 +692,7 @@ contract DotnsPopControllerOld is /// its existing `priceWithCheck` guard. Subsequent waiters only live in the local queue /// until they are promoted. function _enqueueReservation( - IPopRules rules, + IPopRulesOld rules, bytes32 labelhash, string memory baseLabel, address user @@ -821,7 +828,7 @@ contract DotnsPopControllerOld is /// @notice Validates a base (full-person) label and derives `(labelhash, node)`. /// @dev Letters only, so this is stricter than a DNS label: a hyphen or an interior digit - /// is rejected here even though @custom:function StringUtils.isSingleLabel would admit it. + /// is rejected here even though @custom:function StringUtilsOld.isSingleLabel would admit it. function _validateBaseLabel(string calldata baseLabel) internal view @@ -845,7 +852,7 @@ contract DotnsPopControllerOld is /// on it, for the full reservation window. `exists` (owner set) mirrors exactly what makes the /// eventual claim's mint revert, so a label that passes here is one a claim can still register. function _validateReservableBaseLabel( - IPopRules rules, + IPopRulesOld rules, string calldata baseLabel ) internal @@ -854,9 +861,9 @@ contract DotnsPopControllerOld is { (labelhash, node) = _validateBaseLabel(baseLabel); - (IPopRules.PopStatus required,) = rules.classifyName(baseLabel); + (IPopRulesOld.PopStatus required,) = rules.classifyName(baseLabel); require( - required != IPopRules.PopStatus.Reserved && rules.isBaseName(baseLabel), + required != IPopRulesOld.PopStatus.Reserved && rules.isBaseName(baseLabel), InvalidBaseLabel() ); require(!_registrar().exists(uint256(node)), BaseNameAlreadyRegistered()); @@ -872,28 +879,28 @@ contract DotnsPopControllerOld is } /// @notice Resolves the PoP resolver via the protocol registry. - function _popResolver() internal view returns (IDotnsPopResolver) { - return IDotnsPopResolver(protocolRegistry.get(DotnsConstantsOld.POP_RESOLVER)); + function _popResolver() internal view returns (IDotnsPopResolverOld) { + return IDotnsPopResolverOld(protocolRegistry.get(DotnsConstants.POP_RESOLVER)); } /// @notice Resolves the PopRulesOld contract via the protocol registry. - function _popRules() internal view returns (IPopRules) { - return IPopRules(protocolRegistry.get(DotnsConstantsOld.POP_RULES)); + function _popRules() internal view returns (IPopRulesOld) { + return IPopRulesOld(protocolRegistry.get(DotnsConstants.POP_RULES)); } /// @notice Resolves the Store factory via the protocol registry. - function _storeFactory() internal view returns (IStoreFactoryOld) { - return IStoreFactoryOld(protocolRegistry.get(DotnsConstantsOld.STORE_FACTORY)); + function _storeFactory() internal view returns (IStoreFactory) { + return IStoreFactory(protocolRegistry.get(DotnsConstants.STORE_FACTORY)); } /// @notice Resolves the registrar via the protocol registry. - function _registrar() internal view returns (IDotnsRegistrarOld) { - return IDotnsRegistrarOld(protocolRegistry.get(DotnsConstantsOld.REGISTRAR)); + function _registrar() internal view returns (IDotnsRegistrar) { + return IDotnsRegistrar(protocolRegistry.get(DotnsConstants.REGISTRAR)); } /// @notice Resolves the registry via the protocol registry. - function _registry() internal view returns (IDotnsRegistryOld) { - return IDotnsRegistryOld(protocolRegistry.get(DotnsConstantsOld.REGISTRY)); + function _registry() internal view returns (IDotnsRegistry) { + return IDotnsRegistry(protocolRegistry.get(DotnsConstants.REGISTRY)); } /// @notice Writes the new head of the queue into PopRulesOld so the public commit-reveal flow @@ -904,7 +911,7 @@ contract DotnsPopControllerOld is /// `reserveBaseNameForPop`. function _syncPopRulesToHead(bytes32 labelhash, address newHead) internal { string memory baseLabel = _reservedBaseLabel[labelhash]; - IPopRules rules = _popRules(); + IPopRulesOld rules = _popRules(); rules.releaseBaseName(baseLabel); rules.reserveBaseNameForPop(baseLabel, newHead); emit ReservationHeadAdvanced(labelhash, newHead); @@ -920,7 +927,7 @@ contract DotnsPopControllerOld is } /// @notice Internal check enforcing a Root origin. - /// @dev Authorises a call when @custom:function SystemUtilsOld.originIsRoot is true, and + /// @dev Authorises a call when @custom:function SystemUtils.originIsRoot is true, and /// reverts with NotRoot otherwise. `msg.sender` is deliberately not consulted: a /// Root origin has no account behind it, so reading `msg.sender` traps. That holds /// for this frame and any delegatecall sharing it; a nested call sees the calling @@ -932,7 +939,7 @@ contract DotnsPopControllerOld is /// pass. Every call out of this contract goes to a protocol contract resolved /// through the registry. function _onlyRoot() internal view { - require(SystemUtilsOld.originIsRoot(), NotRoot()); + require(SystemUtils.originIsRoot(), NotRoot()); } /// @inheritdoc UUPSUpgradeable diff --git a/contracts/registrars/DotnsRegistrarControllerOld.sol b/contracts/registrars/DotnsRegistrarControllerOld.sol deleted file mode 100644 index afaeae211..000000000 --- a/contracts/registrars/DotnsRegistrarControllerOld.sol +++ /dev/null @@ -1,484 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.34; - -import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import { - OwnableUpgradeable -} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; -import { - ERC165Upgradeable -} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; -import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; -import {ReentrancyGuardTransient} from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol"; - -import {IDotnsRegistrarOld} from "./IDotnsRegistrarOld.sol"; -import {IDotnsReverseResolver} from "../resolvers/IDotnsReverseResolver.sol"; -import {IPopRules} from "../pop/IPopRules.sol"; -import {IDotnsCostModelRegistryOld} from "../pop/IDotnsCostModelRegistryOld.sol"; -import {StringUtils} from "../utils/StringUtils.sol"; -import {IDotnsRegistrarController} from "./IDotnsRegistrarController.sol"; -import {IDotnsNameEscrow} from "../escrow/IDotnsNameEscrow.sol"; -import {IDotnsNameWhitelist} from "../whitelist/IDotnsNameWhitelist.sol"; -import {IStoreFactoryOld} from "../store/IStoreFactoryOld.sol"; -import {IDotnsProtocolRegistryOld} from "../registry/IDotnsProtocolRegistryOld.sol"; -import {IDotnsRegistryOld} from "../registry/IDotnsRegistryOld.sol"; -import {DotnsConstantsOld} from "../utils/DotnsConstantsOld.sol"; -import {LabelUtils} from "../utils/LabelUtils.sol"; -import {RegistrationUtilsOld} from "../utils/RegistrationUtilsOld.sol"; -import {StoreUtilsOld} from "../utils/StoreUtilsOld.sol"; -import {SystemUtilsOld} from "../utils/SystemUtilsOld.sol"; - -/// @title Dotns Registrar Controller -/// @notice Allocates top-level labels using a commit reveal scheme. -/// @dev Orchestrates allocation, PoP validation, pricing enforcement, forward registry -/// wiring, default reverse resolution, and immutable store writing. -/// -/// Tokenisation: the minted ERC721 tokenId is `uint256(node)`, where -/// `node = namehash(tldNode, labelhash)`. The registry stores a sentinel owner -/// (`address(0)`) for tokenised nodes and derives ownership from the ERC721 registrar for -/// authorisation. -/// @custom:security-contact admin@parity.io -contract DotnsRegistrarControllerOld is - Initializable, - UUPSUpgradeable, - OwnableUpgradeable, - ERC165Upgradeable, - ReentrancyGuardTransient, - IDotnsRegistrarController -{ - using StringUtils for *; - using StoreUtilsOld for IStoreFactoryOld; - - /// @notice One role's membership and its admin role. - /// @dev Member of the reserved AccessControl namespace, unused because gating is Root only. - /// @param hasRole Whether an account holds the role. - /// @param adminRole Admin role that manages the role. - struct RoleData { - mapping(address account => bool) hasRole; - bytes32 adminRole; - } - - /// @notice Reserved OpenZeppelin access-control namespace held at its ERC-7201 slot. - /// @dev Declared and left unused so the namespace stays present in the layout. Its slot derives - /// from the label, disjoint from the sequential slots below, so it consumes none of them. - /// @param _roles Role data keyed by role identifier. - /// @custom:storage-location erc7201:openzeppelin.storage.AccessControl - struct AccessControlStorage { - mapping(bytes32 role => RoleData) _roles; - } - - /// @notice Upper bound for commitment validity to cap storage griefing risk. - uint256 public constant MAX_ALLOWED_COMMITMENT_AGE = 7 days; - - /// @notice Minimum age a commitment must reach before reveal. - uint256 public minCommitmentAge; - - /// @notice Maximum age after which a commitment expires. - uint256 public maxCommitmentAge; - - /// @notice Stores Mapping of commitment hashes to timestamp committed. - mapping(bytes32 hash => uint256 timestamp) public commitments; - - /// @notice Cost-model version stamped on a commitment at commit time. - /// @dev Recorded from the registry's current version when `commit` runs, so the reveal can bind - /// a registration to the version that was current then. A caller cannot commit against an - /// arbitrary earlier, cheaper version: the reveal rejects a `pricingVersion` that differs - /// from this stamp. - mapping(bytes32 hash => uint256 version) public committedPricingVersion; - - /// @dev Reserved slot held so the sequential storage layout stays fixed across the in-place - /// upgrade. Unused: name eligibility lives in @custom:contract DotnsNameWhitelistOld. - /// @custom:oz-renamed-from whiteList - mapping(address account => bool retained) private __whiteListSlot; - - /// @notice Protocol-level address registry for all DotNS contracts. - IDotnsProtocolRegistryOld public protocolRegistry; - - /// @dev Reserved storage space to allow for layout changes in the future. The retained - /// whitelist slot above holds one slot, so the gap holds 49 to keep the footprint fixed. - uint256[49] private __gap; - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - _disableInitializers(); - } - - /// @notice Initialises the registrar controller. - /// @dev Callable once through the UUPS proxy; direct calls on the implementation revert - /// with @custom:reverts InvalidInitialization, and any nested call outside an active - /// initialiser scope reverts with @custom:reverts NotInitializing. Validates the - /// commitment window bounds: `minAge` must be strictly positive (otherwise - /// @custom:reverts MinCommitmentAgeZero) so a reveal cannot land in the same block as - /// its commit; `maxAge` must exceed `minAge` (otherwise - /// @custom:reverts MaxCommitmentAgeTooLow) and must stay within - /// `MAX_ALLOWED_COMMITMENT_AGE` (otherwise @custom:reverts MaxCommitmentAgeTooHigh) before - /// wiring the protocol registry. - function initialize( - IDotnsProtocolRegistryOld registry, - uint256 minAge, - uint256 maxAge - ) - external - initializer - { - __ERC165_init(); - __Ownable_init(msg.sender); - - require(minAge > 0, MinCommitmentAgeZero()); - require(maxAge > minAge, MaxCommitmentAgeTooLow()); - require(maxAge <= MAX_ALLOWED_COMMITMENT_AGE, MaxCommitmentAgeTooHigh()); - - protocolRegistry = registry; - - minCommitmentAge = minAge; - maxCommitmentAge = maxAge; - } - - /// @inheritdoc IDotnsRegistrarController - function available(string calldata label) public view override returns (bool) { - bytes32 node; - (, node) = _validatedLabelNode(label); - IDotnsRegistrarOld registrar = - IDotnsRegistrarOld(protocolRegistry.get(DotnsConstantsOld.REGISTRAR)); - return registrar.available(uint256(node)); - } - - /// @inheritdoc IDotnsRegistrarController - function makeCommitment(Registration calldata registration) - public - pure - override - returns (bytes32 commitment) - { - commitment = keccak256( - abi.encode( - registration.label, - registration.owner, - registration.secret, - registration.reserved, - registration.maxPrice, - registration.pricingVersion - ) - ); - } - - /// @inheritdoc IDotnsRegistrarController - function commit(bytes32 commitment) external override { - uint256 prior = commitments[commitment]; - require( - prior == 0 || prior + maxCommitmentAge <= block.timestamp, - UnexpiredCommitmentExists(commitment) - ); - - commitments[commitment] = block.timestamp; - committedPricingVersion[commitment] = _currentPricingVersion(); - emit NameCommitted(commitment); - } - - /// @notice Reads the cost model's current version through the protocol registry. - /// @dev Resolved at commit time so the stamp binds the version live then, not at reveal. - /// @return pricingVersion The current cost-model version. - function _currentPricingVersion() internal view returns (uint256 pricingVersion) { - return IDotnsCostModelRegistryOld(protocolRegistry.get(DotnsConstantsOld.COST_MODEL)) - .currentVersion(); - } - - /// @inheritdoc IDotnsRegistrarController - function register(Registration calldata registration) external payable override nonReentrant { - (IDotnsRegistrarOld registrar, bytes32 labelhash, bytes32 node) = - _requireAvailableLabel(registration.label); - _consumeCommitment(registration); - - address escrow = _escrow(); - IPopRules rules = IPopRules(protocolRegistry.get(DotnsConstantsOld.POP_RULES)); - - uint256 tokenId = uint256(node); - bool isReclaim = registrar.exists(tokenId); - - string memory stem = rules.stripDigits(registration.label); - bool stemCanonical = stem.isSingleLabelMemory(); - // Reclaim hands the name back from a prior occupant who may hold a sibling-controller's - // stem reservation, which is garbage once the name moves on, so clear it. Non-reclaim - // paths intentionally leave an existing reservation in place: the slot belongs to the - // sibling controller that wrote it (e.g. the PoP queue head stamp), and clearing it from - // here would brick that controller's release and advance paths. - if (stemCanonical && isReclaim) { - (address reservationOwner,) = rules.getBaseNameReservation(stem); - address expectedOwner = - IDotnsNameEscrow(payable(escrow)).getReleasePosition(tokenId).recipient; - if (reservationOwner != address(0) && reservationOwner == expectedOwner) { - rules.releaseReservationForReclaim(stem, expectedOwner); - } - } - - bool isDirect = msg.sender == registration.owner; - IPopRules.PriceWithMeta memory priced; - if (isDirect) { - priced = rules.priceWithCheckAtVersion( - registration.label, registration.owner, registration.pricingVersion - ); - } else { - priced = rules.priceWithoutCheckAtVersion( - registration.label, registration.owner, registration.pricingVersion - ); - if (priced.status == IPopRules.PopStatus.Reserved) { - (IPopRules.PopStatus required,) = rules.classifyName(registration.label); - if (required == IPopRules.PopStatus.Reserved) { - revert IPopRules.GovernanceReserved(registration.label); - } - revert IPopRules.NameReserved(registration.label); - } - require( - priced.userStatus >= priced.status, - IPopRules.OwnerStatusInsufficient( - registration.label, priced.userStatus, priced.status - ) - ); - } - - uint256 totalCharged = priced.price; - require( - totalCharged <= registration.maxPrice, - PriceExceedsMax(registration.label, totalCharged, registration.maxPrice) - ); - require(msg.value >= totalCharged, InsufficientValue()); - - IDotnsReverseResolver reverse; - bool setReverseRecord; - if (registration.reserved && isDirect) { - reverse = - IDotnsReverseResolver(protocolRegistry.get(DotnsConstantsOld.REVERSE_RESOLVER)); - setReverseRecord = bytes(reverse.nameOf(registration.owner)).length == 0; - } - - _completeRegistration( - registration, labelhash, node, priced.price, setReverseRecord, reverse, isReclaim - ); - - if (isReclaim) { - IDotnsNameEscrow(payable(escrow)).reclaim(tokenId, registration.owner); - // Reclaim hands the NFT to the new holder; rewrite the registry record so the prior - // owner's resolver pointer cannot follow the name. Must run after `escrow.reclaim` - // so the registry's `ownerOf` check sees the new holder, not the escrow. - IDotnsRegistryOld(protocolRegistry.get(DotnsConstantsOld.REGISTRY)) - .setOwner(node, registration.owner); - } - - _settleEscrow(escrow, tokenId, registration.owner, isDirect, totalCharged); - - if (msg.value > totalCharged) { - uint256 refund = msg.value - totalCharged; - (bool ok,) = payable(msg.sender).call{value: refund}(""); - if (ok) { - emit OverpaymentRefunded(msg.sender, refund); - } else { - IDotnsNameEscrow(payable(escrow)).creditOverpayment{value: refund}(msg.sender); - } - } - } - - /// @notice Settles every escrow side-effect of a successful registration. - /// @dev Extracted to keep `register` under the stack-depth ceiling. On a direct - /// registration the full `chargeAmount` lands in the refundable deposit position - /// keyed to `nameOwner`. On a cross-payer registration the deposit position is - /// seeded with a zero amount so the release lifecycle stays reachable, and the same - /// `chargeAmount` routes to the protocol fee pot via `depositProtocolFee` keyed to - /// `msg.sender` as the payer. - function _settleEscrow( - address escrow, - uint256 tokenId, - address nameOwner, - bool isDirect, - uint256 chargeAmount - ) - internal - { - uint256 depositAmount = isDirect ? chargeAmount : 0; - IDotnsNameEscrow(payable(escrow)).deposit{value: depositAmount}( - IDotnsNameEscrow.DepositParams({ - tokenId: tokenId, asset: address(0), amount: depositAmount, recipient: nameOwner - }) - ); - - if (!isDirect && chargeAmount > 0) { - IDotnsNameEscrow(payable(escrow)).depositProtocolFee{value: chargeAmount}( - IDotnsNameEscrow.ProtocolFeeDepositParams({ - tokenId: tokenId, payer: msg.sender, recipient: nameOwner - }) - ); - } - } - - /// @inheritdoc IDotnsRegistrarController - function registerReserved(Registration calldata registration) external override nonReentrant { - // Read Root once, up front. Everything below must stay callable under a substrate Root - // origin, which has no account, so no branch may read `msg.sender`: the grant is checked - // against `registration.owner`, the commitment is keyed on its own hash, and the mint - // targets the owner. - bool isRoot = SystemUtilsOld.originIsRoot(); - IDotnsNameWhitelist whitelist; - if (!isRoot) { - whitelist = _nameWhitelist(); - require( - whitelist.isGrantedTo(registration.label, registration.owner), - NameNotGranted(registration.label, registration.owner) - ); - } - - (, bytes32 labelhash, bytes32 node) = _requireAvailableLabel(registration.label); - _consumeCommitment(registration); - - // Spend the grant before minting so a grant in the wrong state fails before any name is - // issued. Root skips it: a governance mint must not consume a grant held by someone else. - // - // A consequence worth knowing: if Root mints a label that is `Claimed` or `Reserved` on - // the whitelist, that record survives the mint. The beneficiary's own `registerReserved` - // then fails `NameNotAvailable`, and the node stays in the whitelist's active set until - // governance calls `revokeName`. Nothing is lost, but the grant is stranded. - if (!isRoot) { - whitelist.consume(registration.label, registration.owner); - } - - // No reverse record. `setReverseName` overwrites unconditionally, and the gate above lets - // anyone submit for the beneficiary, so writing here would let a third party relabel - // another address. The owner claims their own record through `claimReverseRecord`, which - // checks ownership and writes only their own key. - _completeRegistration( - registration, labelhash, node, 0, false, IDotnsReverseResolver(address(0)), false - ); - } - - /// @inheritdoc IERC165 - function supportsInterface(bytes4 interfaceId) - public - view - override(ERC165Upgradeable, IERC165) - returns (bool) - { - return interfaceId == type(IDotnsRegistrarController).interfaceId - || super.supportsInterface(interfaceId); - } - - /// @notice Validates label shape and derives `(labelhash, node)`. - /// @dev Delegates hashing to @custom:contract LabelUtils so the assembly sequence lives in - /// exactly one place across the codebase. Error ownership stays on this interface: shape - /// violations revert with `InvalidLabel()`; labels below the minimum length revert with - /// `LabelTooShort(label)` so off-chain consumers can distinguish "shape-valid but below - /// the policy minimum" from "shape-valid but already minted". - function _validatedLabelNode(string calldata label) - internal - view - returns (bytes32 labelhash, bytes32 node) - { - require(label.isSingleLabel(), InvalidLabel()); - require(bytes(label).length >= 3, LabelTooShort(label)); - (labelhash, node) = LabelUtils.deriveNode(protocolRegistry.tldNode(), label); - } - - function _requireAvailableLabel(string calldata label) - internal - view - returns (IDotnsRegistrarOld registrar, bytes32 labelhash, bytes32 node) - { - (labelhash, node) = _validatedLabelNode(label); - registrar = IDotnsRegistrarOld(protocolRegistry.get(DotnsConstantsOld.REGISTRAR)); - require(registrar.available(uint256(node)), NameNotAvailable(label)); - } - - function _consumeCommitment(Registration calldata registration) internal { - bytes32 commitment = makeCommitment(registration); - uint256 committedAt = commitments[commitment]; - - require(committedAt != 0, CommitmentNotFound(commitment)); - require( - committedAt + minCommitmentAge <= block.timestamp, - CommitmentTooNew(commitment, committedAt + minCommitmentAge, block.timestamp) - ); - require( - committedAt + maxCommitmentAge > block.timestamp, - CommitmentTooOld(commitment, committedAt + maxCommitmentAge, block.timestamp) - ); - - uint256 stamped = committedPricingVersion[commitment]; - require( - registration.pricingVersion == stamped, - IDotnsCostModelRegistryOld.PricingVersionMismatch(stamped, registration.pricingVersion) - ); - - delete commitments[commitment]; - delete committedPricingVersion[commitment]; - } - - /// @notice Completes a commit-reveal registration: mints (or skips when reclaiming), - /// wires forward registry, optionally sets the reverse record, and writes the owner's - /// Store. - /// @dev On a fresh mint the triad of mint + forward-registry + store-write is delegated - /// to @custom:function RegistrationUtilsOld.registerAndStore, the single canonical - /// implementation shared across every DotNS registration flow. On a reclaim the mint step is - /// skipped (the - /// escrow has already moved custody) and only the registry wiring and store write run. - /// Reverse-record setting and the priced-registration event stay here because they are - /// commit-reveal-specific policy. - function _completeRegistration( - Registration calldata registration, - bytes32 labelhash, - bytes32 node, - uint256 baseCost, - bool setReverseRecord, - IDotnsReverseResolver reverse, - bool isReclaim - ) - internal - { - address labelStore; - if (!isReclaim) { - labelStore = RegistrationUtilsOld.registerAndStore( - RegistrationUtilsOld.RegistrationContext({ - protocolRegistry: protocolRegistry, - user: registration.owner, - label: registration.label, - labelhash: labelhash, - node: node - }) - ); - } else { - // Registry reset on reclaim is deferred until after `escrow.reclaim` runs (see - // @custom:function register) so the registry's `ownerOf` check sees the new holder. - IStoreFactoryOld factory = - IStoreFactoryOld(protocolRegistry.get(DotnsConstantsOld.STORE_FACTORY)); - string memory fullName = string.concat(registration.label, protocolRegistry.tld()); - labelStore = factory.writeLabel(registration.owner, node, fullName); - } - - if (setReverseRecord) { - reverse.setReverseName( - registration.owner, string.concat(registration.label, protocolRegistry.tld()) - ); - } - - emit NameRegistered(registration.label, labelhash, registration.owner, baseCost, labelStore); - } - - /// @notice Returns the configured name escrow from the protocol registry. - function _escrow() internal view returns (address escrow) { - escrow = protocolRegistry.get(DotnsConstantsOld.NAME_ESCROW); - require(escrow != address(0), EscrowNotConfigured()); - } - - /// @notice Returns implementation version. - /// @return versionString Current version string. - function version() external pure virtual returns (string memory versionString) { - versionString = "1.0.0"; - } - - /// @notice Returns the configured name whitelist from the protocol registry. - function _nameWhitelist() internal view returns (IDotnsNameWhitelist whitelist) { - address configured = protocolRegistry.get(DotnsConstantsOld.NAME_WHITELIST); - require(configured != address(0), WhitelistNotConfigured()); - whitelist = IDotnsNameWhitelist(configured); - } - - /// @inheritdoc UUPSUpgradeable - function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} -} diff --git a/contracts/registrars/DotnsRegistrarOld.sol b/contracts/registrars/DotnsRegistrarOld.sol index d0391ca7d..3b35a888a 100644 --- a/contracts/registrars/DotnsRegistrarOld.sol +++ b/contracts/registrars/DotnsRegistrarOld.sol @@ -10,23 +10,23 @@ import { ERC721Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; -import {IDotnsRegistrarOld} from "./IDotnsRegistrarOld.sol"; +import {IDotnsRegistrar} from "./IDotnsRegistrar.sol"; import {IDotnsController} from "./IDotnsController.sol"; -import {IDotnsProtocolRegistryOld} from "../registry/IDotnsProtocolRegistryOld.sol"; +import {IDotnsProtocolRegistry} from "../registry/IDotnsProtocolRegistry.sol"; -import {IStoreFactoryOld} from "../store/IStoreFactoryOld.sol"; +import {IStoreFactory} from "../store/IStoreFactory.sol"; import {ILabelStore} from "../store/ILabelStore.sol"; -import {StoreUtilsOld} from "../utils/StoreUtilsOld.sol"; +import {StoreUtils} from "../utils/StoreUtils.sol"; import {LabelUtils} from "../utils/LabelUtils.sol"; -import {StringUtils} from "../utils/StringUtils.sol"; +import {StringUtilsOld} from "../utils/StringUtilsOld.sol"; import {IDotnsNameEscrow} from "../escrow/IDotnsNameEscrow.sol"; -import {IPopRules} from "../pop/IPopRules.sol"; -import {DotnsConstantsOld} from "../utils/DotnsConstantsOld.sol"; +import {IPopRulesOld} from "../pop/IPopRulesOld.sol"; +import {DotnsConstants} from "../utils/DotnsConstants.sol"; /// @title Dotns Registrar /// @notice ERC721-backed registrar implementing permanent name ownership. /// @dev Deliberately policy-free on pricing, reservations, and PoP gating; those live in the -/// controllers and @custom:contract IPopRules. The registrar owns transferability itself: publicly +/// controllers and @custom:contract IPopRulesOld. The registrar owns transferability itself: publicly /// registered names transfer freely, while names minted through the PoP gateway are soulbound and /// revert on transfer. The `_update` hook enforces both the soulbound gate and the fee-on-transfer /// settlement that consults the escrow. @@ -36,10 +36,10 @@ contract DotnsRegistrarOld is UUPSUpgradeable, OwnableUpgradeable, ERC721Upgradeable, - IDotnsRegistrarOld + IDotnsRegistrar { - using StoreUtilsOld for IStoreFactoryOld; - using StringUtils for *; + using StoreUtils for IStoreFactory; + using StringUtilsOld for *; /// @notice Mapping of authorised controllers. /// @dev Controllers may call `register`. Keyed by the shared baseline @custom:contract @@ -52,11 +52,11 @@ contract DotnsRegistrarOld is /// @notice Protocol-level address registry for all DotNS contracts. /// @dev Used to resolve sibling contract addresses (store factory, controller, registry) /// without storing individual references. - IDotnsProtocolRegistryOld public protocolRegistry; + IDotnsProtocolRegistry public protocolRegistry; /// @notice Marks a token as soulbound: minted through the PoP gateway and non-transferable. /// @dev Set at mint by @custom:function register when the caller is the address registered - /// under `DotnsConstantsOld.POP_CONTROLLER`. Write-once and never cleared: a name's soulbound + /// under `DotnsConstants.POP_CONTROLLER`. Write-once and never cleared: a name's soulbound /// state is fixed at registration. Read by the `_update` transfer gate and by /// @custom:function quoteTransferFee. mapping(uint256 tokenId => bool soulbound) private _soulbound; @@ -82,38 +82,40 @@ contract DotnsRegistrarOld is /// proxy; direct calls on the implementation revert with @custom:reverts InvalidInitialization /// because `_disableInitializers` runs in the constructor, and any nested call outside an /// active initialiser scope reverts with @custom:reverts NotInitializing. + /// @param initialOwner Address that owns the contract once initialised. function initialize( + address initialOwner, string calldata name, string calldata symbol, - IDotnsProtocolRegistryOld registry + IDotnsProtocolRegistry registry ) external initializer { require(address(registry) != address(0), ProtocolRegistryRequired()); - __Ownable_init(msg.sender); + __Ownable_init(initialOwner); __ERC721_init(name, symbol); protocolRegistry = registry; } - /// @inheritdoc IDotnsRegistrarOld + /// @inheritdoc IDotnsRegistrar function addController(IDotnsController controller) external onlyOwner { controllers[controller] = true; emit ControllerAdded(controller); } - /// @inheritdoc IDotnsRegistrarOld + /// @inheritdoc IDotnsRegistrar function removeController(IDotnsController controller) external onlyOwner { controllers[controller] = false; emit ControllerRemoved(controller); } - /// @inheritdoc IDotnsRegistrarOld + /// @inheritdoc IDotnsRegistrar function available(uint256 id) public view override returns (bool isAvailable) { address holder = _ownerOf(id); if (holder == address(0)) return true; - address escrow = protocolRegistry.get(DotnsConstantsOld.NAME_ESCROW); + address escrow = protocolRegistry.get(DotnsConstants.NAME_ESCROW); if (holder != escrow) return false; // Escrow custody on its own does not mean registrable: a released name inside its redeem @@ -123,7 +125,7 @@ contract DotnsRegistrarOld is return IDotnsNameEscrow(payable(escrow)).isReclaimable(id); } - /// @inheritdoc IDotnsRegistrarOld + /// @inheritdoc IDotnsRegistrar function register( uint256 id, address owner, @@ -138,10 +140,10 @@ contract DotnsRegistrarOld is // fresh-mint branch; the escrow-held branch must use the reclaim path and is rejected // here with the typed error so callers do not see OZ's `ERC721InvalidSender(0)`. require(!_exists(id), NameNotAvailable(id)); - require(owner != protocolRegistry.get(DotnsConstantsOld.NAME_ESCROW), InvalidOwner()); + require(owner != protocolRegistry.get(DotnsConstants.NAME_ESCROW), InvalidOwner()); // Empty labels are an intentional gateway-cold path (substrate Root cannot deploy a // `LabelStore` under `pallet-revive`, so the controller stashes a pending claim and the - // user settles via @custom:function IDotnsPopController.claimLabelStore later). Non-empty + // user settles via @custom:function IDotnsPopControllerOld.claimLabelStore later). Non-empty // labels must still be canonical so the transfer-floor lookup in `_quoteTransferFee` // cannot brick the token by reverting on a malformed stem. require(bytes(label).length == 0 || label.isSingleLabel(), InvalidLabel()); @@ -150,20 +152,20 @@ contract DotnsRegistrarOld is // canonical PoP controller mints soulbound names, so a compromised or buggy peer controller // cannot lock a public name and the PoP controller cannot mint an unlocked one. Written // only on the true branch to leave the public path free of a redundant zero write. - bool soulbound = msg.sender == protocolRegistry.get(DotnsConstantsOld.POP_CONTROLLER); + bool soulbound = msg.sender == protocolRegistry.get(DotnsConstants.POP_CONTROLLER); if (soulbound) _soulbound[id] = true; if (bytes(label).length != 0) _writeOwnerLabel(owner, id, label); emit NameRegistered(id, owner, soulbound); } - /// @inheritdoc IDotnsRegistrarOld + /// @inheritdoc IDotnsRegistrar function labelOf(uint256 tokenId) external view override returns (string memory) { address holder = _ownerOf(tokenId); if (holder == address(0)) return ""; return LabelUtils.stripTld(protocolRegistry.tld(), _readLabel(tokenId, holder)); } - /// @inheritdoc IDotnsRegistrarOld + /// @inheritdoc IDotnsRegistrar function quoteTransferFee( uint256 tokenId, address to @@ -183,7 +185,7 @@ contract DotnsRegistrarOld is (,, requiredFee) = _quoteTransferFee(from, to, tokenId); } - /// @inheritdoc IDotnsRegistrarOld + /// @inheritdoc IDotnsRegistrar function transferFrom( address from, address to, @@ -191,12 +193,12 @@ contract DotnsRegistrarOld is ) public payable - override(ERC721Upgradeable, IDotnsRegistrarOld) + override(ERC721Upgradeable, IDotnsRegistrar) { super.transferFrom(from, to, tokenId); } - /// @inheritdoc IDotnsRegistrarOld + /// @inheritdoc IDotnsRegistrar function safeTransferFrom( address from, address to, @@ -204,12 +206,12 @@ contract DotnsRegistrarOld is ) public payable - override(ERC721Upgradeable, IDotnsRegistrarOld) + override(ERC721Upgradeable, IDotnsRegistrar) { super.safeTransferFrom(from, to, tokenId, ""); } - /// @inheritdoc IDotnsRegistrarOld + /// @inheritdoc IDotnsRegistrar function safeTransferFrom( address from, address to, @@ -218,23 +220,28 @@ contract DotnsRegistrarOld is ) public payable - override(ERC721Upgradeable, IDotnsRegistrarOld) + override(ERC721Upgradeable, IDotnsRegistrar) { super.safeTransferFrom(from, to, tokenId, data); } - /// @notice Returns implementation version. - /// @return versionString Current version string. - function version() external pure virtual returns (string memory versionString) { - versionString = "1.0.0"; + /// @notice Returns the release this network declares it runs, read live from the protocol + /// registry so every DotNS contract reports one synchronised value. + /// @dev Mirror of `IDotnsProtocolRegistry.protocolVersion`, kept under the historical + /// `version()` selector for ABI compatibility. It reports the network's declaration, + /// not this contract's build; per-contract identity is the codehash declared on the + /// registry. + /// @return versionString Declared release as bare semver, empty when never declared. + function version() external view virtual returns (string memory versionString) { + versionString = protocolRegistry.protocolVersion(); } - /// @inheritdoc IDotnsRegistrarOld + /// @inheritdoc IDotnsRegistrar function exists(uint256 tokenId) external view override returns (bool tokenExists) { tokenExists = _exists(tokenId); } - /// @inheritdoc IDotnsRegistrarOld + /// @inheritdoc IDotnsRegistrar function isSoulbound(uint256 tokenId) external view override returns (bool soulbound) { soulbound = _soulbound[tokenId]; } @@ -286,10 +293,13 @@ contract DotnsRegistrarOld is // Resolve every registry-sourced dependency once and thread it into the helpers so a // single transfer pays one external lookup per key rather than three. - IDotnsProtocolRegistryOld registry = protocolRegistry; - address escrow = registry.get(DotnsConstantsOld.NAME_ESCROW); + IDotnsProtocolRegistry registry = protocolRegistry; + address escrow = registry.get(DotnsConstants.NAME_ESCROW); require(escrow != address(0), EscrowNotConfigured()); - IStoreFactoryOld factory = IStoreFactoryOld(registry.get(DotnsConstantsOld.STORE_FACTORY)); + // `release` is the only caller that moves a name into custody, so any other sender is a + // deposit the escrow holds no position for. + require(to != escrow || msg.sender == escrow, UnsolicitedEscrowDeposit(tokenId)); + IStoreFactory factory = IStoreFactory(registry.get(DotnsConstants.STORE_FACTORY)); bool isEscrowTouching = to == escrow || from == escrow; // Skip mirroring on escrow-touching paths: release deposits the NFT into custody where @@ -333,7 +343,7 @@ contract DotnsRegistrarOld is /// @notice Mirrors the sender's label entry into the recipient's `LabelStore`. function _syncRecipientStore( - IStoreFactoryOld factory, + IStoreFactory factory, address to, address from, uint256 tokenId @@ -345,16 +355,20 @@ contract DotnsRegistrarOld is // Defensive: the sender holds no label entry for the token. Gateway mints reach this // only at mint time, and a gateway name is soulbound so it never transfers; a public // name always carries a label. Nothing to mirror, so do not deploy a recipient store; - // downstream writes are demand-deploy through `StoreUtilsOld.ensureLabelStore`. + // downstream writes are demand-deploy through `StoreUtils.ensureLabelStore`. return; } - factory.writeLabel(to, bytes32(tokenId), fullName); + // A slot holding a different string was written by someone else, and `storeLabel` has no + // delete, so mirroring nothing would hand over a name `_quoteTransferFeeFor` rejects on + // every onward transfer. A matching entry is still a no-op, so a transfer back to a prior + // owner passes. + factory.writeNewLabel(to, bytes32(tokenId), fullName); } /// @notice Reads the full name (`label.tld`) for `tokenId` from `holder`'s `LabelStore` using /// a caller-supplied factory. function _readLabelFor( - IStoreFactoryOld factory, + IStoreFactory factory, uint256 tokenId, address holder ) @@ -383,17 +397,17 @@ contract DotnsRegistrarOld is /// @notice Resolves the configured name escrow address from the protocol registry. function _escrow() private view returns (address escrow) { - escrow = protocolRegistry.get(DotnsConstantsOld.NAME_ESCROW); + escrow = protocolRegistry.get(DotnsConstants.NAME_ESCROW); } /// @notice Resolves the configured PoP rules contract from the protocol registry. - function _popRules() private view returns (IPopRules rules) { - rules = IPopRules(protocolRegistry.get(DotnsConstantsOld.POP_RULES)); + function _popRules() private view returns (IPopRulesOld rules) { + rules = IPopRulesOld(protocolRegistry.get(DotnsConstants.POP_RULES)); } /// @notice Resolves the configured store factory from the protocol registry. - function _storeFactory() private view returns (IStoreFactoryOld factory) { - factory = IStoreFactoryOld(protocolRegistry.get(DotnsConstantsOld.STORE_FACTORY)); + function _storeFactory() private view returns (IStoreFactory factory) { + factory = IStoreFactory(protocolRegistry.get(DotnsConstants.STORE_FACTORY)); } /// @notice Writes the canonical full name into `owner`'s `LabelStore` keyed by @@ -403,13 +417,13 @@ contract DotnsRegistrarOld is /// the registry would have already broken every other call site). function _writeOwnerLabel(address owner, uint256 tokenId, string calldata label) private { _storeFactory() - .writeLabel(owner, bytes32(tokenId), string.concat(label, protocolRegistry.tld())); + .writeNewLabel(owner, bytes32(tokenId), string.concat(label, protocolRegistry.tld())); } /// @notice Quotes the friction fee required for a transfer. /// @dev Required fee is the name's own price returned by @custom:function - /// PopRulesOld.transferFloor. It is paid by the sender on every downward or cross-reach - /// transfer and settles to the + /// PopRulesOld.transferFloor. It is paid by the sender on every downward or cross-reach transfer + /// and settles to the /// protocol fee pot. Any prior deposit travels with the NFT: the escrow rebinds the position to /// the new holder rather than refunding the sender, so transferring a funded name forfeits the /// locked deposit to the recipient. Self-transfers and escrow-touching transfers return zero. @@ -424,12 +438,12 @@ contract DotnsRegistrarOld is { if (from == to) return (address(0), 0, 0); - IDotnsProtocolRegistryOld registry = protocolRegistry; - escrow = registry.get(DotnsConstantsOld.NAME_ESCROW); + IDotnsProtocolRegistry registry = protocolRegistry; + escrow = registry.get(DotnsConstants.NAME_ESCROW); require(escrow != address(0), EscrowNotConfigured()); bool isEscrowTouching = to == escrow || from == escrow; - IStoreFactoryOld factory = IStoreFactoryOld(registry.get(DotnsConstantsOld.STORE_FACTORY)); + IStoreFactory factory = IStoreFactory(registry.get(DotnsConstants.STORE_FACTORY)); (transferFee, requiredFee) = _quoteTransferFeeFor(registry, factory, isEscrowTouching, from, to, tokenId); } @@ -439,8 +453,8 @@ contract DotnsRegistrarOld is /// escrow-touching move or when the sender holds no label entry; otherwise reads the canonical /// label and delegates to @custom:function PopRulesOld.transferFloor. function _quoteTransferFeeFor( - IDotnsProtocolRegistryOld registry, - IStoreFactoryOld factory, + IDotnsProtocolRegistry registry, + IStoreFactory factory, bool isEscrowTouching, address from, address to, @@ -464,7 +478,7 @@ contract DotnsRegistrarOld is require(bytes(label).length != 0, InvalidLabel()); transferFee = - IPopRules(registry.get(DotnsConstantsOld.POP_RULES)).transferFloor(label, from, to); + IPopRulesOld(registry.get(DotnsConstants.POP_RULES)).transferFloor(label, from, to); requiredFee = transferFee; } diff --git a/contracts/registrars/IDotnsPopControllerOld.sol b/contracts/registrars/IDotnsPopControllerOld.sol new file mode 100644 index 000000000..418ff0ea9 --- /dev/null +++ b/contracts/registrars/IDotnsPopControllerOld.sol @@ -0,0 +1,528 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.34; + +import {IDotnsController} from "./IDotnsController.sol"; + +/// @title IDotnsPopControllerOld +/// @notice Interface for the dedicated PoP controller orchestrating lite-person and full-person +/// username issuance on behalf of the PoP gateway. +/// @dev Deliberately disjoint from @custom:contract IDotnsRegistrarController. The two +/// controllers coexist on @custom:contract DotnsRegistrarOld via its multi-controller affordance +/// and neither imports the other. A full-person username collides through the registrar's ERC721 +/// availability check (first-to-mint wins); a lite username is not a token, so it collides through +/// @custom:function IDotnsRegistry.recordExists at its stem-under-container node +/// (@custom:reverts LiteNameAlreadyIssued). Reservation queuing for `reservedBaseLabel` +/// mirrors its live head into PopRulesOld, so a queued stem also blocks the public +/// commit-reveal flow, which reads that slot when it prices a name. +/// +/// Label formats: +/// Lite-person usernames (first argument to @custom:function reserveBaseName and the +/// `liteLabel` of a `LinkKind.LiteUsername` link) are a stem of lowercase ASCII letters, a +/// separator, then exactly two digits (e.g. `joseph.42`) per +/// @custom:function StringUtilsOld.isLitePersonLabel. The stem is stricter than a DNS label +/// because the name a person chooses is restricted to letters; a stem short enough to +/// be governance-reserved is rejected by classification, not by the shape. The label is stored in +/// the form the gateway sends, which is the canonical form of the name, so nothing here +/// normalises it. +/// Full-person usernames (the `label` of @custom:function registerBaseName and the +/// optional `reservedBaseLabel` of @custom:function reserveBaseName) are lowercase ASCII +/// letters only, per @custom:function StringUtilsOld.isPersonLabel (e.g. `alice`). That is the +/// same rule a lite stem follows and is stricter than a DNS label: no hyphens and no interior +/// digits, because a full-person name is also a name a person chose. A separator marks a lite +/// label and is rejected everywhere else, so only the gateway can create a dotted name; a +/// digit suffix on its own is not exclusive, since a public label may carry one directly. +/// Cross-flow priority on the base stem is arbitrated by +/// @custom:function IPopRulesOld.reserveBaseNameForPop. +/// @custom:security-contact admin@parity.io +interface IDotnsPopControllerOld is IDotnsController { + /// @notice Discriminant for the `Link` union supplied to `registerBaseName`. + /// @dev Selects the chat-key source for the full-person username. Orthogonal to whether + /// the registration is a claim or standalone; that is derived from on-chain reservation + /// state. `None` means the caller supplies a fresh chat key in `link.chatKey`. + /// `LiteUsername` means the full-person username is linked to a prior lite-person + /// username (`link.liteLabel`) and inherits its chat key. + enum LinkKind { + None, + LiteUsername + } + + /// @notice Tagged union selecting the chat-key source for a full-person registration. + /// @param liteLabel Lite-person `stem.NN` label (only read when `kind == LiteUsername`). + /// @param chatKey Chat key bytes (only read when `kind == None`). + struct Link { + LinkKind kind; + string liteLabel; + bytes chatKey; + } + + /// @notice Per-user reservation pointer: which queue the user sits in and where. + /// @param labelhash Non-zero when the user holds a live reservation; zero otherwise. + /// @param index Monotonic queue index, meaningful only when `labelhash` is non-zero. + struct UserReservation { + bytes32 labelhash; + uint64 index; + } + + /// @notice Reservation queue entry: a user and the timestamp they joined the queue. + /// @dev Packs into a single storage slot (20 + 8 bytes). + struct ReservationEntry { + address owner; + uint64 joinedAt; + } + + /// @notice Metadata describing the occupied range of a reservation queue. + /// @dev Uses monotonically increasing indices. Active entries occupy `[head, tail)`; + /// `length = tail - head`. Slots past `head` are deleted as the head advances so + /// garbage never accumulates. + struct ReservationQueueMeta { + uint64 head; + uint64 tail; + } + + /// @notice Deferred per-user binding of a freshly minted name to its `LabelStore`. + /// @dev Recorded by the gateway path when the user has no `LabelStore`. The binding later + /// settles via @custom:function settlePendingClaims, which deploys the store from a signed + /// origin and writes the stashed label. PoP-resolver records (chat key, lite link) are + /// persisted eagerly at mint time on @custom:contract IDotnsPopResolverOld, not at settlement, + /// so the resolver carries the full identity record regardless of whether the user has + /// settled their Store. A user accumulates one entry per deferred name: the Root gateway path + /// cannot deploy a `LabelStore` (contract creation is forbidden from the Root origin), so it + /// keeps stashing entries until a signed-origin @custom:function settlePendingClaims deploys + /// the store and settles the entries. Each entry's deadline is measured from its own + /// `mintedAt` against `reservationDuration`. + /// @param label Bare label without the TLD, which is appended at settlement time. A lite + /// claim carries its separator, so this is not always a single DNS label. + /// @param mintedAt Timestamp of the originating mint. + struct PendingClaim { + string label; + uint64 mintedAt; + } + + /// @notice Lite-person registration payload. + /// @dev Single struct so the gateway can ABI-encode one tuple as the cross-chain payload + /// and the contract decodes it directly out of `msg.data`. All fields are required; + /// `chatKey` may be empty bytes to skip the resolver write. + /// @param liteLabel Lite-person `stem.NN` label being minted. + /// @param user Beneficiary account on this chain. + /// @param chatKey Chat-key bytes persisted on the PoP resolver. Empty leaves the slot unset. + struct LiteRegistration { + string liteLabel; + address user; + bytes chatKey; + } + + /// @notice Lite-person registration combined with an optional base-name reservation. + /// @dev `BaseReservation` is a @custom:struct LiteRegistration plus a base-label reservation + /// slot, expressed as composition rather than duplicated fields so internal helpers can + /// consume the lite leg via `params.lite` without unpacking. The lite leg always runs; + /// the reservation leg only runs when `reservedBaseLabel` is non-empty. + /// @param lite Lite-person registration request; see LiteRegistration. + /// @param reservedBaseLabel Base label to enqueue for a later full-person claim. Empty + /// string skips the reservation leg. + struct BaseReservation { + LiteRegistration lite; + string reservedBaseLabel; + } + + /// @notice Base-name reservation payload for the split gateway flow. + /// @dev This is the reservation-only primitive. The lite username mint is handled by + /// @custom:function reserveLiteName, and LabelStore settlement is handled by + /// @custom:function settlePendingClaims. + /// @param user Beneficiary account that will hold the reservation. + /// @param reservedBaseLabel Base label to enqueue for a later full-person claim. + struct BaseNameReservation { + address user; + string reservedBaseLabel; + } + + /// @notice Full-person registration payload. + /// @param label Base DNS label being minted. + /// @param user Beneficiary account on this chain. + /// @param link Chat-key source for the new entry; see @custom:struct Link. + struct FullRegistration { + string label; + address user; + Link link; + } + + /// @notice Emitted when a lite-person username is registered via the PoP gateway. + event LiteNameReserved(bytes32 indexed labelhash, address indexed user, string label); + + /// @notice Emitted when a full-person username is claimed out of an existing reservation. + event BaseNameClaimed(bytes32 indexed labelhash, address indexed user, string label); + + /// @notice Emitted when a standalone full-person username is registered via the PoP gateway. + event StandaloneNameRegistered(bytes32 indexed labelhash, address indexed user, string label); + + /// @notice Emitted when a reservation entry is added to the queue for a base name. + /// @param position Position in the queue at the time of joining (0 = active holder). + event ReservationQueued( + bytes32 indexed reservedLabelhash, address indexed user, uint64 position + ); + + /// @notice Emitted when a reservation entry is removed due to expiry. + event ReservationExpired(bytes32 indexed reservedLabelhash, address indexed user); + + /// @notice Emitted when a user voluntarily relinquishes their reservation. + event ReservationRelinquished(bytes32 indexed reservedLabelhash, address indexed user); + + /// @notice Emitted when a full-person username is linked to a lite-person username. + event LiteToFullLinked(bytes32 indexed fullLabelhash, bytes32 indexed liteLabelhash); + + /// @notice Emitted when the reservation duration is updated. + event ReservationDurationSet(uint64 duration); + + /// @notice Emitted when a name is successfully registered via the PoP controller. + /// @param store The Store instance used to persist the immutable registration record. + event NameRegistered( + string indexed label, bytes32 indexed labelhash, address indexed owner, address store + ); + + /// @notice Emitted when a gateway-path mint defers its `LabelStore` write into the + /// pending-claim mapping because the user has no store yet. + event PendingClaimStashed(address indexed user, bytes32 indexed labelhash, string label); + + /// @notice Emitted when a pending claim is written into a `LabelStore`. + /// @dev Fires once per settled entry from @custom:function settlePendingClaims. `settledBy` + /// is the caller: it equals `user` for a self-settlement and is any other address for a + /// third-party settlement, so consumers can tell the two apart from the log alone. + /// @param user Account the settled name belongs to. + /// @param labelhash Labelhash of the settled name. + /// @param store The `LabelStore` the label was written into. + /// @param settledBy Caller that performed and paid for the settlement. + event PendingClaimSettled( + address indexed user, bytes32 indexed labelhash, address store, address indexed settledBy + ); + + /// @notice Emitted when a reservation queue's head transitions to a new user, either via + /// expiry of the prior head or via the explicit relinquish path. + /// @param labelhash Base-label hash whose queue head changed. + /// @param newHead Address now holding the head slot. + event ReservationHeadAdvanced(bytes32 indexed labelhash, address indexed newHead); + + /// @notice Thrown when a gated entrypoint is reached without a Root origin. + /// @dev Carries no caller parameter: a Root origin has no account to report, + /// and reading `msg.sender` under one traps. + error NotRoot(); + + /// @notice Thrown when a supplied lite-person label does not match `stem.NN`. + error InvalidLiteLabel(); + + /// @notice Thrown when a supplied base label is not a canonical DNS label. + error InvalidBaseLabel(); + + /// @notice Thrown when a reserved base label already has an owner on the registrar, so the + /// queued reservation could never be redeemed at mint time. + error BaseNameAlreadyRegistered(); + + /// @notice Thrown when a lite username is issued again while its subname already exists. + /// @dev A lite name is issued once; re-issuing it would rehome the identity to a new owner and + /// overwrite its records, so an existing subname is rejected rather than reassigned. + error LiteNameAlreadyIssued(); + + /// @notice Thrown when a supplied chat key is non-empty and not exactly 65 bytes long. + /// @dev Mirrors the resolver's `InvalidChatKeyLength` so the controller surfaces a + /// controller-local error before the mint runs. + /// @param length Caller-supplied chat key length, in bytes. + error InvalidChatKey(uint256 length); + + /// @notice Thrown when a user tries to claim or relinquish a reservation that they do not hold. + error NoActiveReservation(address user); + + /// @notice Thrown when a reservation queue has reached its capacity. + error QueueFull(bytes32 labelhash); + + /// @notice Thrown when attempting to enqueue a user who already has an active reservation. + error AlreadyReserved(address user, bytes32 labelhash); + + /// @notice Thrown when someone tries to mint a base label in standalone mode while another user + /// holds the live head-of-queue reservation. + error NotHolder(address user, bytes32 labelhash); + + /// @notice Thrown when a lite-link inheritance does not match the registrar-side owner + /// of the lite label. + /// @dev Prevents identity hijack by ensuring the registrant on the full-name leg actually + /// holds the prior lite identity whose chat key is being inherited. + /// @param user Registrant supplied by the gateway. + /// @param liteLabelhash Lite label whose ownership did not match. + error LiteLabelNotOwnedByUser(address user, bytes32 liteLabelhash); + + /// @notice Thrown when @custom:function setReservationDuration is called with a value below + /// the protocol minimum. + /// @param duration Caller-supplied duration, in seconds. + error ReservationDurationTooLow(uint64 duration); + + /// @notice Registers a lite-person username on behalf of the supplied user + /// and optionally enqueues a reservation for a base name they intend to + /// claim as a full person later. + /// @dev Callable only under a Root origin (otherwise @custom:reverts NotRoot). The + /// lite leg validates the `stem.NN` shape and requires the label to classify outside the + /// governance-reserved tier (otherwise @custom:reverts InvalidLiteLabel), and rejects a + /// supplied chat key whose length is neither zero nor `CHAT_KEY_LENGTH` + /// (otherwise @custom:reverts InvalidChatKey). On a warm-path mint (user already has a + /// `LabelStore`) it @custom:emits LiteNameReserved and @custom:emits NameRegistered; + /// on a cold-path mint it @custom:emits LiteNameReserved and + /// @custom:emits PendingClaimStashed, with @custom:emits NameRegistered deferred to + /// @custom:function settlePendingClaims when the claim settles. The base-name leg only runs + /// when `reservedBaseLabel` is non-empty: it requires a letters-only person label, which is + /// therefore also a true base label (otherwise @custom:reverts InvalidBaseLabel), and + /// with no owner on the registrar (otherwise @custom:reverts BaseNameAlreadyRegistered), + /// since a name that already has an owner could never be claimed. This validation runs + /// before both the lite mint and any queue mutation, so an already-registered + /// `reservedBaseLabel` aborts the whole call and the candidate receives no lite username + /// either; callers should validate the reserved label before attesting rather than relying + /// on this revert. It then advances the + /// head past expired entries (@custom:emits ReservationExpired for each one), + /// removes the user from any prior queue position so a single user holds at most one live + /// reservation across all labels, and enqueues a fresh entry + /// (@custom:emits ReservationQueued). The enqueue rejects with @custom:reverts + /// AlreadyReserved when the user already holds a reservation that was not cleared by the + /// prior removal and with @custom:reverts QueueFull when the per-label queue has reached + /// `MAX_RESERVATION_QUEUE`. Cross-chain callers pass the ABI-encoded reservation tuple as + /// the call's payload, which Solidity decodes directly. + /// @param params Reservation request; see @custom:struct BaseReservation. + function reserveBaseName(BaseReservation calldata params) external; + + /// @notice Enqueues only the full/base-name reservation for a user. + /// @dev Callable only under a Root origin (otherwise @custom:reverts NotRoot). + /// This is the second step of the split + /// gateway flow: @custom:function reserveLiteName mints the lite username first, then this + /// function reserves the full/base label in a separate transaction so proof-size stays below + /// per-call limits. Reverts with @custom:reverts InvalidBaseLabel when the label is empty, + /// is not lowercase ASCII letters (so a hyphen or any digit rejects it), or is + /// governance-reserved, and with + /// @custom:reverts BaseNameAlreadyRegistered when the label already has an owner on the + /// registrar and so could never be claimed. The caller remains agnostic about + /// backend batching; it simply exposes a small retryable primitive. + /// @param params Reservation request; see @custom:struct BaseNameReservation. + function reserveBaseNameOnly(BaseNameReservation calldata params) external; + + /// @notice Registers a lite-person username on behalf of the supplied + /// user without touching the base-name reservation queue. + /// @dev Callable only under a Root origin (otherwise @custom:reverts NotRoot). The + /// supplied label must satisfy the `stem.NN` shape and must classify outside the + /// governance-reserved tier (otherwise @custom:reverts InvalidLiteLabel); a supplied chat + /// key whose length is neither zero nor `CHAT_KEY_LENGTH` reverts + /// @custom:reverts InvalidChatKey before mint and resolver writes run. A username that has + /// already been issued reverts @custom:reverts LiteNameAlreadyIssued. On a warm-path mint + /// @custom:emits LiteNameReserved and @custom:emits NameRegistered. On a cold-path + /// mint @custom:emits LiteNameReserved and @custom:emits PendingClaimStashed, with + /// @custom:emits NameRegistered deferred to @custom:function settlePendingClaims when the + /// claim settles. Cross-chain callers pass the ABI-encoded lite-registration tuple as the + /// call's payload, which Solidity decodes directly. + /// @param params Registration request; see @custom:struct LiteRegistration. + function reserveLiteName(LiteRegistration calldata params) external; + + /// @notice Whether this controller issued `label` as a PoP identity. + /// @dev Keyed by text, so it answers about a name rather than about a node. A lite label is + /// issued as a subname (`joseph` beneath its numeric container `42`) and a full-person label as + /// a second-level name, so a caller holding a node must check that the node is the one `label` + /// resolves to under those rules before reading this answer as being about what it holds; node + /// identity is what names the object. Set at mint and never cleared, so it is unaffected by a + /// name later becoming transferable; the soulbound flag is a transfer rule and cannot stand in + /// for it. + /// @param label Bare label without the TLD, for example `joseph.42`. + /// @return issued True when this controller issued `label`. + function isPopIssued(string calldata label) external view returns (bool issued); + + /// @notice Registers a full-person username on behalf of the supplied user. + /// @dev Callable only under a Root origin (otherwise @custom:reverts NotRoot). The + /// base label must be a letters-only person label, and therefore a true base label, + /// (otherwise @custom:reverts InvalidBaseLabel), and the label must not + /// classify as governance-reserved (otherwise @custom:reverts InvalidBaseLabel). The + /// gateway also defers to PopRulesOld as the single cross-flow authority: when PopRulesOld + /// carries a live base-name slot held by another user (this controller's prior queue head, + /// or a sibling controller's write), the call reverts @custom:reverts NotHolder before any + /// queue mutation. Two orthogonal axes drive the state machine. The reservation + /// axis treats the user as claiming if and only if they hold the live head-of-queue + /// reservation on the base label: a claim wipes the entire queue, releases the PopRulesOld + /// slot, and @custom:emits BaseNameClaimed; a non-claim silently relinquishes any + /// pending entry the user holds and @custom:emits StandaloneNameRegistered. Advancing + /// the queue head past expired entries @custom:emits ReservationExpired for each + /// one. The chat-key axis selects whether a fresh key is persisted on the resolver or the + /// new entry inherits its key from a prior lite-person username. The fresh-key branch + /// rejects a chat key whose length is neither zero nor `CHAT_KEY_LENGTH` (otherwise + /// @custom:reverts InvalidChatKey). The `LiteUsername` branch validates the lite label's + /// `stem.NN` shape (otherwise @custom:reverts InvalidLiteLabel), requires the registrant to + /// own the lite token (otherwise @custom:reverts LiteLabelNotOwnedByUser), reads the lite + /// node's chat key from the resolver and copies it across; if the lite node carries no chat + /// key the inherited value is empty and the full node's chat-key write is silently skipped + /// (the `LiteToFullLinked` event still fires). @custom:emits LiteToFullLinked + /// alongside the registration event. On a warm-path mint the event order is + /// @custom:emits NameRegistered first (from the inner mint), then + /// @custom:emits BaseNameClaimed or @custom:emits StandaloneNameRegistered, then + /// @custom:emits LiteToFullLinked when applicable. On a cold-path mint + /// @custom:emits PendingClaimStashed replaces the initial @custom:emits NameRegistered; + /// the deferred @custom:emits NameRegistered fires later from @custom:function + /// settlePendingClaims. Cross-chain callers pass the ABI-encoded full-registration tuple as + /// the call's payload, which Solidity decodes directly. + /// @param params Registration request; see @custom:struct FullRegistration. + function registerBaseName(FullRegistration calldata params) external; + + /// @notice Permissionlessly removes expired entries from the head of a reservation queue. + /// @dev Permissionless on purpose: anyone (typically a UI or a bot) can poke a stale queue + /// so the next live head takes over without waiting for the next gateway call. Validates + /// `reservedBaseLabel` as a letters-only person label (otherwise + /// @custom:reverts InvalidBaseLabel) + /// and @custom:emits ReservationExpired for every expired entry reaped from the + /// head. A label carrying a digit or a hyphen is not a person label and + /// @custom:reverts InvalidBaseLabel, as does a lite label, since a separator is not one + /// either. Only a letters-only label reaches the queue, and one that was never reserved + /// resolves to an empty queue so the call is a no-op. + function expireReservation(string calldata reservedBaseLabel) external; + + /// @notice Lets the caller voluntarily drop their own active reservation. + /// @dev Reverts with @custom:reverts NoActiveReservation when the caller holds no live + /// reservation. On success the caller's entry is removed from its queue and + /// @custom:emits ReservationRelinquished is emitted; if the removed entry was the queue + /// head, head advancement may additionally @custom:emits ReservationExpired for any + /// stale entries reaped behind it. + function relinquishReservation() external; + + /// @notice Returns whether a label currently has a live reservation at the queue head. + /// @dev Validates `reservedBaseLabel` as a letters-only person label (otherwise + /// @custom:reverts InvalidBaseLabel) before inspecting the queue. + function isReservedForClaim(string calldata reservedBaseLabel) + external + view + returns (bool reserved, address holder); + + /// @notice Updates the reservation duration used to decide when queue entries expire. + /// @dev Owner-gated (otherwise @custom:reverts OwnableUnauthorizedAccount); emits + /// @custom:emits ReservationDurationSet on success. + function setReservationDuration(uint64 duration) external; + + /// @notice Returns the queue metadata (`head`, `tail`) for `labelhash`. + /// @dev Read-only accessor over the per-label reservation queue. `head == tail` means + /// the queue is empty; active entries occupy `[head, tail)`. Exposed on the interface + /// because invariant tests and off-chain consumers use it to enumerate + /// live queue state without scanning storage. + /// @param labelhash Keccak-256 of the base label whose queue is being read. + /// @return head Index of the live queue head. + /// @return tail Index one past the last queued entry. + function reservationMeta(bytes32 labelhash) external view returns (uint64 head, uint64 tail); + + /// @notice Returns the queue entry at `index` for `labelhash`. + /// @dev Sparse storage: a zero `entryOwner` means the slot was relinquished, expired and + /// reaped, or never written. Callers pair this with @custom:function reservationMeta to walk + /// the live window `[head, tail)`. + /// @param labelhash Keccak-256 of the base label whose queue is being read. + /// @param index Queue index to look up. + /// @return entryOwner Owner of the slot (zero if empty/relinquished). + /// @return joinedAt Timestamp the entry was enqueued (only meaningful when + /// `entryOwner != address(0)`). + function reservationEntry( + bytes32 labelhash, + uint64 index + ) + external + view + returns (address entryOwner, uint64 joinedAt); + + /// @notice Returns `user`'s current reservation pointer. + /// @dev A zero `labelhash` on the returned struct means the user holds no reservation; + /// `index` is meaningful only when `labelhash` is non-zero. + /// @param user Account whose reservation pointer is being read. + /// @return reservation Per-user reservation pointer; see @custom:struct UserReservation. + function userReservation(address user) + external + view + returns (UserReservation memory reservation); + + /// @notice Returns the base label a reservation queue is keyed under. + /// @dev Reverse lookup from the `bytes32` queue key to its label string, so a consumer that + /// observed a queue by labelhash (for example from a reservation event) can recover the + /// human-readable label without holding its preimage. Returns an empty string when no + /// reservation was ever enqueued under `labelhash`. + /// @param labelhash Keccak-256 of the base label. + /// @return baseLabel The base label string, or empty when unknown. + function reservedBaseLabelOf(bytes32 labelhash) external view returns (string memory baseLabel); + + /// @notice Returns the window, in seconds, after which a queue or pending-claim entry lapses. + /// @dev Governance-configurable via @custom:function setReservationDuration. Read by the lens + /// to compute each pending claim's settlement deadline. + /// @return duration Reservation duration in seconds. + function reservationDuration() external view returns (uint64 duration); + + /// @notice Settles up to `limit` of a user's pending claims, writing each stashed label into + /// the user's `LabelStore` and deploying that store when the user has none yet. + /// @dev Permissionless: any caller may settle any user's claims and bears the full cost, + /// including the `LabelStore` storage deposit, which is charged to the + /// transaction signer. Settlement is never destructive: the name is already minted, so this + /// only completes the deferred label write. Each settled entry is removed from the queue and + /// the user leaves the pending-claim enumeration set once their queue empties. At most + /// `limit` entries are processed so a large queue cannot exceed the block gas limit; + /// `moreRemaining` reports whether entries are left for a follow-up call, and a `limit` of + /// zero settles nothing. Writes are idempotent on an already-locked store slot, so a claim + /// whose label was independently written settles harmlessly. Emits + /// @custom:emits PendingClaimSettled and @custom:emits NameRegistered per settled entry, with + /// `settledBy` set to the caller so a third-party settlement is distinguishable from a + /// self-settlement. + /// @param user Account whose pending claims are settled. + /// @param limit Maximum number of entries to settle in this call. + /// @return settledCount Number of entries settled. + /// @return moreRemaining Whether the user still holds unsettled entries. + function settlePendingClaims( + address user, + uint256 limit + ) + external + returns (uint256 settledCount, bool moreRemaining); + + /// @notice Settles the caller's own pending claims into their `LabelStore`. + /// @dev Convenience for a user settling their own store: equivalent to + /// @custom:function settlePendingClaims with `msg.sender` and a bounded batch. The caller + /// deploys and pays for their store on the first write. Settles at most one bounded batch so + /// the call cannot exceed the block gas limit; `moreRemaining` reports whether the caller + /// still holds unsettled entries, in which case they call again. Emits the same + /// @custom:emits PendingClaimSettled and @custom:emits NameRegistered as + /// @custom:function settlePendingClaims. + /// @return moreRemaining Whether the caller still holds unsettled entries. + function claimLabelStore() external returns (bool moreRemaining); + + /// @notice Returns a paginated slice of a user's pending claims in queue order. + /// @dev An empty array means the user has no pending claims at `offset`. Each entry carries + /// its `mintedAt`; the settlement deadline is `mintedAt + reservationDuration`. An `offset` + /// past the end returns an empty array rather than reverting, and a page holds at most + /// `DotnsConstants.MAX_PAGE_SIZE` entries. + /// @param user Account whose pending claims are read. + /// @param offset Start index into the queue. + /// @param limit Maximum entries to return. + /// @return claims Page of the user's pending claims; see @custom:struct PendingClaim. + function pendingClaims( + address user, + uint256 offset, + uint256 limit + ) + external + view + returns (PendingClaim[] memory claims); + + /// @notice Returns the number of pending claims currently staged for `user`. + /// @param user Account whose pending claims are counted. + /// @return count Number of staged pending claims. + function pendingClaimCountOf(address user) external view returns (uint256 count); + + /// @notice Returns the number of users with at least one live pending claim. + /// @dev Exact live count, not an all-time tally: fully settled users are removed from the + /// enumeration set so off-chain consumers can page through every stalled user without + /// filtering. + /// @return count Number of users currently holding a pending claim. + function pendingClaimUserCount() external view returns (uint256 count); + + /// @notice Returns a paginated slice of users with at least one live pending claim. + /// @dev Pair with @custom:function pendingClaims to read each user's stashed entries. + /// Ordering is not chronological; callers MUST NOT assume `mintedAt` is monotonic + /// across the slice. Returns an empty array when `offset` is past the live count, and a page + /// holds at most `DotnsConstants.MAX_PAGE_SIZE` entries. + /// @param offset Start index. + /// @param limit Maximum entries to return. + /// @return users Slice of users currently holding a pending claim. + function pendingClaimUsers( + uint256 offset, + uint256 limit + ) + external + view + returns (address[] memory users); +} diff --git a/contracts/registrars/IDotnsRegistrarOld.sol b/contracts/registrars/IDotnsRegistrarOld.sol deleted file mode 100644 index d6b120146..000000000 --- a/contracts/registrars/IDotnsRegistrarOld.sol +++ /dev/null @@ -1,190 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.34; - -import {IERC721} from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; -import {IDotnsController} from "./IDotnsController.sol"; - -/// @title Dotns Registrar -/// @notice ERC721-backed ownership for DotNS names with controller-gated registration. -/// @dev Intentionally minimal and policy-free. Provides ERC721 ownership for registered name -/// token IDs and controller-gated registration; pricing, PoP enforcement, and flow-specific -/// policy live in the controllers. -/// @custom:security-contact admin@parity.io -interface IDotnsRegistrarOld is IERC721 { - /// @notice Thrown when a name is already registered. - error NameNotAvailable(uint256 tokenId); - - /// @notice Thrown when the caller is not an authorised controller. - error NotController(address caller); - - /// @notice Thrown when the protocol registry has no escrow address configured. - error EscrowNotConfigured(); - - /// @notice Thrown when a standard ERC721 transfer is attempted but the recipient - /// tier requires a non-zero transfer fee and the caller forwarded no `msg.value`. - error TransferFeeRequired(uint256 tokenId, address to, uint256 requiredFee); - - /// @notice Thrown when @custom:function initialize is called with the zero address as - /// the protocol registry. - error ProtocolRegistryRequired(); - - /// @notice Thrown when a mint, burn, or self-transfer carries `msg.value`. None of those - /// paths forward value onward, so attached value would be permanently trapped. - error UnexpectedValue(); - - /// @notice Thrown when @custom:function register is called with the escrow address as - /// `owner`, which would mint directly into escrow custody with no @custom:struct - /// ReleasePosition recorded. - error InvalidOwner(); - - /// @notice Thrown when @custom:function register receives an empty or non-canonical - /// label. - error InvalidLabel(); - - /// @notice Thrown when a transfer or a transfer-fee quote targets a soulbound name. - /// @dev Soulbound names are minted through the PoP gateway and are permanently - /// non-transferable. Raised by the `_update` transfer gate and by - /// @custom:function quoteTransferFee. - error NameSoulbound(uint256 tokenId); - - /// @notice Emitted when a name is registered. - /// @param id The token id (namehash node) that was minted. - /// @param owner The address that received the name. - /// @param soulbound True when the name is soulbound: PoP-gateway minted and non-transferable. - /// Lets indexers classify registrations without a per-token @custom:function isSoulbound read. - event NameRegistered(uint256 indexed id, address indexed owner, bool soulbound); - - /// @notice Emitted when a controller is added. - /// @dev Typed as the shared baseline @custom:contract IDotnsController so the commit-reveal - /// controller and the PoP controller (and any future controller) all fit the same signature - /// without - /// the registrar depending on any specific controller interface. - event ControllerAdded(IDotnsController indexed controller); - - /// @notice Emitted when a controller is removed. - event ControllerRemoved(IDotnsController indexed controller); - - /// @notice Returns whether a registration call may proceed for `id`. - /// @dev Signals two distinct paths to the controller. Returns `true` when the owner slot is - /// empty (a fresh @custom:function register call may mint) OR when the current owner is - /// the configured escrow and the released position's redeem window has elapsed (the - /// controller must then route through @custom:function IDotnsNameEscrow.reclaim instead of - /// @custom:function register, because `register` calls `_mint` which rejects existing - /// tokens). All other holders return `false`. The controller distinguishes the two `true` - /// cases via @custom:function exists. - /// Escrow custody inside the redeem window returns `false`: that window belongs to the - /// previous holder, who may still @custom:function IDotnsNameEscrow.redeem the name, and - /// reclaim would revert until it elapses. Clients wanting the exact moment a released name - /// becomes registrable should read `redeemableUntil` from - /// @custom:function IDotnsNameEscrow.getReleasePosition. - function available(uint256 id) external view returns (bool isAvailable); - - /// @notice Registers a name permanently. - /// @dev Permanence is by construction: there is no `expire`, `renew`, or `release` path on - /// the registrar. Custody only moves via ERC721 transfers (which the registrar polices via - /// the fee-on-transfer hook) or via escrow reclaim. Restricted to authorised controllers - /// (otherwise @custom:reverts NotController) and rejects ids that are not available - /// (otherwise @custom:reverts NameNotAvailable). Emits @custom:emits NameRegistered on - /// success. - /// @dev When the caller is the address registered under `DotnsConstantsOld.POP_CONTROLLER`, the - /// name is marked soulbound and becomes permanently non-transferable (see - /// @custom:function isSoulbound). Provenance is read from the protocol registry at mint time, - /// so no other authorised controller can mint a soulbound name and the PoP controller cannot - /// mint an unlocked one. Public registrations from any other controller stay transferable. - /// @param label The human-readable label string (e.g. "alice"). - function register(uint256 id, address owner, string calldata label) external; - - /// @notice Returns whether a token is soulbound (PoP-gateway minted and non-transferable). - /// @dev Durable on-chain marker set once at mint by @custom:function register and never - /// cleared. A `true` result means every transfer overload reverts with - /// @custom:reverts NameSoulbound and @custom:function quoteTransferFee reverts likewise. - /// @param tokenId The name's token id. - /// @return soulbound True when the name was minted through the PoP gateway. - function isSoulbound(uint256 tokenId) external view returns (bool soulbound); - - /// @notice Returns whether a given token id has been minted. - function exists(uint256 tokenId) external view returns (bool tokenExists); - - /// @notice Adds an authorised controller. - /// @dev Typed against the baseline `IDotnsController` (not a concrete subtype) so a single - /// authorisation surface accepts every controller flavour (commit-reveal, PoP gateway, - /// future variants) without per-flavour setters. Owner-gated (otherwise - /// @custom:reverts OwnableUnauthorizedAccount); emits @custom:emits ControllerAdded on - /// success. - function addController(IDotnsController controller) external; - - /// @notice Removes an authorised controller. - /// @dev Mirrors the @custom:function addController baseline-typed signature so any registered - /// controller can - /// be revoked through the same entry point. Owner-gated (otherwise - /// @custom:reverts OwnableUnauthorizedAccount); emits @custom:emits ControllerRemoved on - /// success. - function removeController(IDotnsController controller) external; - - /// @notice Returns whether `controller` is currently authorised to call - /// @custom:function register. - /// @param controller Candidate controller. - /// @return authorised True when `controller` was added via @custom:function addController and - /// has not been removed. - function controllers(IDotnsController controller) external view returns (bool authorised); - - /// @notice Returns the human-readable label a token was registered with. - /// @dev Canonical state source for the label string; any client that holds a node or - /// tokenId can resolve the original label in one view call without scanning registration - /// events. Returns the empty string when the token does not exist. - function labelOf(uint256 tokenId) external view returns (string memory label); - - /// @notice Quotes the additional native fee required to transfer a token to `to`. - /// @dev Returns the fee from @custom:function PopRulesOld.transferFloor: the name's own price - /// as the maximum of (i) the reach component charged when the recipient does not meet - /// the label's required tier and (ii) the downgrade component charged when the - /// recipient tier is strictly below the sender tier. Self-transfers and - /// escrow-touching transfers (release into escrow, reclaim out of escrow) return - /// zero. A token whose sender has no stored label also returns zero because there - /// is no label-derived price to charge against. Soulbound names are non-transferable - /// and have no transfer price, so a soulbound `tokenId` reverts with - /// @custom:reverts NameSoulbound rather than returning zero. Rejects a zero `to` with - /// @custom:reverts ERC721InvalidReceiver, an unminted `tokenId` with - /// @custom:reverts ERC721NonexistentToken via the underlying `ownerOf`, and requires - /// the protocol registry to have an escrow configured (otherwise - /// @custom:reverts EscrowNotConfigured). Returns zero when the protocol registry - /// has no `STORE_FACTORY` configured because no label-derived price is reachable. - function quoteTransferFee( - uint256 tokenId, - address to - ) - external - view - returns (uint256 requiredFee); - - /// @inheritdoc IERC721 - /// @dev The registrar's `_update` hook consults @custom:function PopRulesOld.transferFloor - /// to compute the required transfer fee; if the caller does not forward at least that - /// amount as `msg.value`, the transfer reverts with @custom:reverts TransferFeeRequired. - /// A soulbound name is non-transferable and reverts with @custom:reverts NameSoulbound. - /// The `payable` modifier on every transfer overload exists so the fee can be forwarded - /// in the same call. - function safeTransferFrom( - address from, - address to, - uint256 tokenId, - bytes calldata data - ) - external - payable - override; - - /// @inheritdoc IERC721 - /// @dev Subject to the same fee-on-transfer gate as the four-argument overload; reverts with - /// @custom:reverts TransferFeeRequired when the recipient owes a non-zero transfer fee and - /// the caller has not forwarded it as `msg.value`, and with @custom:reverts NameSoulbound when - /// the token is soulbound. - function safeTransferFrom(address from, address to, uint256 tokenId) external payable override; - - /// @inheritdoc IERC721 - /// @dev Subject to the same fee-on-transfer gate as the safe overloads; reverts with - /// @custom:reverts TransferFeeRequired when the recipient owes a non-zero transfer fee and - /// the caller has not forwarded it as `msg.value`, and with @custom:reverts NameSoulbound when - /// the token is soulbound. - function transferFrom(address from, address to, uint256 tokenId) external payable override; -} diff --git a/contracts/registry/DotnsProtocolRegistryOld.sol b/contracts/registry/DotnsProtocolRegistryOld.sol deleted file mode 100644 index 35f91d2ee..000000000 --- a/contracts/registry/DotnsProtocolRegistryOld.sol +++ /dev/null @@ -1,112 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.34; - -import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import { - OwnableUpgradeable -} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; - -import {IDotnsProtocolRegistryOld} from "./IDotnsProtocolRegistryOld.sol"; -import {LabelUtils} from "../utils/LabelUtils.sol"; -import {StringUtils} from "../utils/StringUtils.sol"; - -/// @title Dotns Protocol Registry -/// @author Parity -/// @notice Upgradeable address registry for all DotNS protocol contracts, and the authority for -/// the network's top-level domain. -/// @dev Single source of truth for sibling-contract lookups. All siblings resolve each other via -/// well-known `bytes32` constants in `DotnsConstantsOld` rather than holding direct addresses, -/// so an upgrade or rewire only mutates this contract. The TLD node and suffix are set once -/// at initialisation and read live by every consumer, so a network runs one TLD without -/// recompiling its contracts. -/// @custom:security-contact admin@parity.io -contract DotnsProtocolRegistryOld is - Initializable, - UUPSUpgradeable, - OwnableUpgradeable, - IDotnsProtocolRegistryOld -{ - using StringUtils for string; - - /// @notice Address stored for each well-known protocol key. - mapping(bytes32 key => address addr) private _addresses; - - /// @notice Reference count per address, incremented for every key it is registered under. - /// @dev Lets `isRegisteredAddress` answer in O(1) and survive a contract being mapped to - /// multiple keys without being treated as deregistered when only one key is rewired. - mapping(address addr => uint256 refcount) private _registeredRefcount; - - /// @notice Namehash of the TLD node, `namehash(0, keccak256(bytes(tldLabel)))`. - bytes32 private _tldNode; - - /// @notice TLD suffix including the leading dot, e.g. `.dot`. - string private _tld; - - uint256[50] private __gap; - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - _disableInitializers(); - } - - /// @notice Initialises the protocol registry and fixes the network's TLD. - /// @dev Callable exactly once via `Initializable`, otherwise - /// @custom:reverts InvalidInitialization. Sets the deployer as owner. `tldLabel` is the - /// bare label without a dot (e.g. `dot`, `paseo`); it must be a single DNS label, - /// otherwise @custom:reverts InvalidTld. The TLD is fixed here because changing it after - /// names exist would reroot every node. - /// @param tldLabel Bare TLD label, without the leading dot. - function initialize(string calldata tldLabel) external initializer { - __Ownable_init(msg.sender); - - require(tldLabel.isSingleLabel(), InvalidTld()); - _tldNode = LabelUtils.namehashUnder(bytes32(0), LabelUtils.labelhash(tldLabel)); - _tld = string.concat(".", tldLabel); - } - - /// @inheritdoc IDotnsProtocolRegistryOld - function get(bytes32 key) external view override returns (address addr) { - return _addresses[key]; - } - - /// @inheritdoc IDotnsProtocolRegistryOld - function set(bytes32 key, address addr) external override onlyOwner { - require(addr != address(0), ZeroAddress()); - - address previousAddress = _addresses[key]; - if (previousAddress == addr) return; - - if (previousAddress != address(0)) { - --_registeredRefcount[previousAddress]; - } - ++_registeredRefcount[addr]; - - _addresses[key] = addr; - emit AddressUpdated(key, addr); - } - - /// @inheritdoc IDotnsProtocolRegistryOld - function isRegisteredAddress(address addr) external view override returns (bool registered) { - return addr != address(0) && _registeredRefcount[addr] > 0; - } - - /// @inheritdoc IDotnsProtocolRegistryOld - function tldNode() external view override returns (bytes32 node) { - return _tldNode; - } - - /// @inheritdoc IDotnsProtocolRegistryOld - function tld() external view override returns (string memory suffix) { - return _tld; - } - - /// @notice Returns implementation version. - /// @return versionString Current version string. - function version() external pure virtual returns (string memory versionString) { - versionString = "1.0.0"; - } - - /// @inheritdoc UUPSUpgradeable - function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} -} diff --git a/contracts/registry/DotnsRegistryOld.sol b/contracts/registry/DotnsRegistryOld.sol index 0acef37cc..0d4d7fd5b 100644 --- a/contracts/registry/DotnsRegistryOld.sol +++ b/contracts/registry/DotnsRegistryOld.sol @@ -6,31 +6,31 @@ import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/U import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; -import {IDotnsRegistryOld} from "./IDotnsRegistryOld.sol"; +import {IDotnsRegistry} from "./IDotnsRegistry.sol"; import {IDotnsController} from "../registrars/IDotnsController.sol"; -import {IDotnsRegistrarOld} from "../registrars/IDotnsRegistrarOld.sol"; -import {IStoreFactoryOld} from "../store/IStoreFactoryOld.sol"; -import {StoreUtilsOld} from "../utils/StoreUtilsOld.sol"; +import {IDotnsRegistrar} from "../registrars/IDotnsRegistrar.sol"; +import {IStoreFactory} from "../store/IStoreFactory.sol"; +import {StoreUtils} from "../utils/StoreUtils.sol"; import {LabelUtils} from "../utils/LabelUtils.sol"; -import {IDotnsProtocolRegistryOld} from "./IDotnsProtocolRegistryOld.sol"; -import {StringUtils} from "../utils/StringUtils.sol"; -import {DotnsConstantsOld} from "../utils/DotnsConstantsOld.sol"; +import {IDotnsProtocolRegistry} from "./IDotnsProtocolRegistry.sol"; +import {StringUtilsOld} from "../utils/StringUtilsOld.sol"; +import {DotnsConstants} from "../utils/DotnsConstants.sol"; /// @title Dotns Registry /// @author Parity /// @notice Upgradeable on-chain registry for hierarchical name ownership and resolution. /// @dev Tokenised second-level nodes store `owner == address(0)` as a sentinel and defer to -/// `IDotnsRegistrarOld.ownerOf`; subnodes carry an explicit owner address in `records`. +/// `IDotnsRegistrar.ownerOf`; subnodes carry an explicit owner address in `records`. /// @custom:security-contact admin@parity.io -contract DotnsRegistryOld is Initializable, UUPSUpgradeable, OwnableUpgradeable, IDotnsRegistryOld { - using StoreUtilsOld for IStoreFactoryOld; - using StringUtils for *; +contract DotnsRegistryOld is Initializable, UUPSUpgradeable, OwnableUpgradeable, IDotnsRegistry { + using StoreUtils for IStoreFactory; + using StringUtilsOld for *; /// @notice Mapping of node identifiers to records. mapping(bytes32 node => Record record) private records; /// @notice Protocol-level address registry for all DotNS contracts. - IDotnsProtocolRegistryOld public protocolRegistry; + IDotnsProtocolRegistry public protocolRegistry; uint256[50] private __gap; @@ -55,15 +55,22 @@ contract DotnsRegistryOld is Initializable, UUPSUpgradeable, OwnableUpgradeable, /// @dev Callable exactly once via `Initializable`, otherwise /// @custom:reverts InvalidInitialization. `registry` must be non-zero, otherwise /// @custom:reverts NotAllowed. + /// @param initialOwner Address that owns the contract once initialised. /// @param registry Protocol-level address registry used to resolve sibling contracts. - function initialize(IDotnsProtocolRegistryOld registry) external initializer { - __Ownable_init(msg.sender); + function initialize( + address initialOwner, + IDotnsProtocolRegistry registry + ) + external + initializer + { + __Ownable_init(initialOwner); require(address(registry) != address(0), NotAllowed()); protocolRegistry = registry; } - /// @inheritdoc IDotnsRegistryOld + /// @inheritdoc IDotnsRegistry function setSubnodeOwner(SubnodeRecord calldata record) external override @@ -73,6 +80,13 @@ contract DotnsRegistryOld is Initializable, UUPSUpgradeable, OwnableUpgradeable, address newOwner = record.owner; require(newOwner != address(0), NotAllowed()); + // Deferring the store write (persist == false) is a controller workflow: the deferred label + // is backfilled later by an authorised writer, and only a registered controller drives that + // path. Every other caller must persist eagerly rather than strand an unindexed label. + if (!record.persist) { + _onlyRegistrarController(); + } + bytes32 parentNode = record.parentNode; string calldata subLabel = record.subLabel; string calldata parentLabel = record.parentLabel; @@ -84,7 +98,7 @@ contract DotnsRegistryOld is Initializable, UUPSUpgradeable, OwnableUpgradeable, subnode = LabelUtils.namehashUnder(parentNode, labelhash); Record storage existing = records[subnode]; - address reverseResolver = protocolRegistry.get(DotnsConstantsOld.REVERSE_RESOLVER); + address reverseResolver = protocolRegistry.get(DotnsConstants.REVERSE_RESOLVER); if (existing.exists) { address previousOwner = existing.owner; // Reset the resolver pointer on reassignment so the prior subnode owner's resolver @@ -116,11 +130,10 @@ contract DotnsRegistryOld is Initializable, UUPSUpgradeable, OwnableUpgradeable, emit NewOwner(parentNode, labelhash, newOwner); } - /// @inheritdoc IDotnsRegistryOld + /// @inheritdoc IDotnsRegistry function setOwner(bytes32 node, address newOwner) external override onlyRegistrarController { require(newOwner != address(0), NotAllowed()); - IDotnsRegistrarOld registrar = - IDotnsRegistrarOld(protocolRegistry.get(DotnsConstantsOld.REGISTRAR)); + IDotnsRegistrar registrar = IDotnsRegistrar(protocolRegistry.get(DotnsConstants.REGISTRAR)); require(registrar.ownerOf(uint256(node)) == newOwner, NotAuthorised()); // The resolver pointer is reset to the default reverse resolver on every call to this @@ -131,20 +144,20 @@ contract DotnsRegistryOld is Initializable, UUPSUpgradeable, OwnableUpgradeable, // Owner remains the zero sentinel so reads delegate to the registrar's ERC-721 holder. records[node] = Record({ owner: address(0), - resolver: protocolRegistry.get(DotnsConstantsOld.REVERSE_RESOLVER), + resolver: protocolRegistry.get(DotnsConstants.REVERSE_RESOLVER), exists: true }); emit NodeTransferred(node, newOwner); } - /// @inheritdoc IDotnsRegistryOld + /// @inheritdoc IDotnsRegistry function setResolver(bytes32 node, address newResolver) external override authorised(node) { records[node].resolver = newResolver; emit NewResolver(node, newResolver); } - /// @inheritdoc IDotnsRegistryOld + /// @inheritdoc IDotnsRegistry function setSubnodeResolver(SubnodeResolverRecord calldata record) external override @@ -165,7 +178,7 @@ contract DotnsRegistryOld is Initializable, UUPSUpgradeable, OwnableUpgradeable, emit NewResolver(subnode, record.resolver); } - /// @inheritdoc IDotnsRegistryOld + /// @inheritdoc IDotnsRegistry function owner(bytes32 node) external view override returns (address) { Record storage record = records[node]; // Read `owner` first: a non-zero stored owner proves the record exists and is a subnode, @@ -174,22 +187,21 @@ contract DotnsRegistryOld is Initializable, UUPSUpgradeable, OwnableUpgradeable, address storedOwner = record.owner; if (storedOwner != address(0)) return storedOwner; if (!record.exists) return address(0); - IDotnsRegistrarOld registrar = - IDotnsRegistrarOld(protocolRegistry.get(DotnsConstantsOld.REGISTRAR)); + IDotnsRegistrar registrar = IDotnsRegistrar(protocolRegistry.get(DotnsConstants.REGISTRAR)); return registrar.ownerOf(uint256(node)); } - /// @inheritdoc IDotnsRegistryOld + /// @inheritdoc IDotnsRegistry function resolver(bytes32 node) external view override returns (address) { return records[node].resolver; } - /// @inheritdoc IDotnsRegistryOld + /// @inheritdoc IDotnsRegistry function recordExists(bytes32 node) external view override returns (bool) { return records[node].exists; } - /// @inheritdoc IDotnsRegistryOld + /// @inheritdoc IDotnsRegistry function isAuthorised( bytes32 node, address account @@ -212,9 +224,8 @@ contract DotnsRegistryOld is Initializable, UUPSUpgradeable, OwnableUpgradeable, ) internal { - IStoreFactoryOld factory = - IStoreFactoryOld(protocolRegistry.get(DotnsConstantsOld.STORE_FACTORY)); - factory.writeLabel(storeOwner, node, fullName); + IStoreFactory factory = IStoreFactory(protocolRegistry.get(DotnsConstants.STORE_FACTORY)); + factory.writeNewLabel(storeOwner, node, fullName); } /// @notice Computes the namehash of `parentLabel` rooted at the network's TLD node. @@ -277,8 +288,7 @@ contract DotnsRegistryOld is Initializable, UUPSUpgradeable, OwnableUpgradeable, if (!record.exists) return false; - IDotnsRegistrarOld registrar = - IDotnsRegistrarOld(protocolRegistry.get(DotnsConstantsOld.REGISTRAR)); + IDotnsRegistrar registrar = IDotnsRegistrar(protocolRegistry.get(DotnsConstants.REGISTRAR)); uint256 tokenId = uint256(node); address tokenOwner = registrar.ownerOf(tokenId); if (account == tokenOwner) return true; @@ -294,17 +304,20 @@ contract DotnsRegistryOld is Initializable, UUPSUpgradeable, OwnableUpgradeable, /// in one place and lets commit-reveal and PoP controllers coexist without registry /// reconfiguration on each addition. function _onlyRegistrarController() internal view { - IDotnsRegistrarOld registrar = - IDotnsRegistrarOld(protocolRegistry.get(DotnsConstantsOld.REGISTRAR)); + IDotnsRegistrar registrar = IDotnsRegistrar(protocolRegistry.get(DotnsConstants.REGISTRAR)); require(registrar.controllers(IDotnsController(msg.sender)), NotAuthorised()); } - /// @notice Returns implementation version. - /// @return versionString Current version string. - function version() external pure virtual returns (string memory versionString) { - versionString = "1.0.0"; + /// @notice Returns the release this network declares it runs, read live from the protocol + /// registry so every DotNS contract reports one synchronised value. + /// @dev Mirror of `IDotnsProtocolRegistry.protocolVersion`, kept under the historical + /// `version()` selector for ABI compatibility. It reports the network's declaration, + /// not this contract's build; per-contract identity is the codehash declared on the + /// registry. + /// @return versionString Declared release as bare semver, empty when never declared. + function version() external view virtual returns (string memory versionString) { + versionString = protocolRegistry.protocolVersion(); } - /// @inheritdoc UUPSUpgradeable function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} } diff --git a/contracts/registry/IDotnsProtocolRegistryOld.sol b/contracts/registry/IDotnsProtocolRegistryOld.sol deleted file mode 100644 index 16c4eb847..000000000 --- a/contracts/registry/IDotnsProtocolRegistryOld.sol +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.34; - -/// @title IDotnsProtocolRegistryOld -/// @author Parity -/// @notice Interface for the DotNS protocol-level address registry. -/// @dev Single source of truth for sibling lookups. Contracts resolve each other via well-known -/// `bytes32` constants in `DotnsConstantsOld` so an upgrade or rewire only mutates the -/// registry, never the consumers. The registry also holds the network's top-level domain, -/// so every consumer reads one TLD rather than compiling its own. -/// @custom:security-contact admin@parity.io -interface IDotnsProtocolRegistryOld { - /// @notice Emitted when a protocol address is set or updated. - event AddressUpdated(bytes32 indexed key, address indexed addr); - - /// @notice Thrown when a zero address is provided where one is not allowed. - error ZeroAddress(); - - /// @notice Thrown when the TLD label supplied at initialisation is not a single DNS label. - error InvalidTld(); - - /// @notice Returns the address stored for a given key. - /// @dev Returns `address(0)` when the key is unset; callers must validate when non-zero is - /// required. - function get(bytes32 key) external view returns (address addr); - - /// @notice Sets or updates the address for a given key. - /// @dev Owner-restricted, otherwise @custom:reverts OwnableUnauthorizedAccount. `addr` - /// must be non-zero, otherwise @custom:reverts ZeroAddress. Idempotent when the new - /// value matches the stored one (no event emitted in that case). Maintains a - /// per-address refcount so the same contract can occupy multiple keys without losing - /// its registered status until every key is rewired. Emits - /// @custom:emits AddressUpdated on each effective change. - function set(bytes32 key, address addr) external; - - /// @notice Returns true iff `addr` is currently registered under at least one well-known key. - /// @dev O(1) refcount-backed lookup. Canonical peer-trust check consumed by `LabelStore` - /// writes and `StoreFactory` deploys; only addresses governance has actively - /// registered return true. Treats `address(0)` as never registered regardless of - /// refcount. - function isRegisteredAddress(address addr) external view returns (bool registered); - - /// @notice Returns the namehash of the network's TLD node. - /// @dev `namehash(0, keccak256(bytes(tldLabel)))`, fixed at initialisation. Consumers use it - /// as the root parent when deriving a name's node. - function tldNode() external view returns (bytes32 node); - - /// @notice Returns the network's TLD suffix, including the leading dot (e.g. `.dot`). - /// @dev Fixed at initialisation. Consumers append it when rendering a label as a full name. - function tld() external view returns (string memory suffix); -} diff --git a/contracts/registry/IDotnsRegistryOld.sol b/contracts/registry/IDotnsRegistryOld.sol deleted file mode 100644 index 892ae3edb..000000000 --- a/contracts/registry/IDotnsRegistryOld.sol +++ /dev/null @@ -1,164 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.34; - -/// @title IDotnsRegistryOld -/// @author Parity -/// @notice Minimal on-chain registry for hierarchical name ownership and resolution. -/// @dev Tokenised second-level nodes are owned through the registrar (ERC-721); subnodes are -/// owned directly by the address stored in `Record.owner`. -/// @custom:security-contact admin@parity.io -interface IDotnsRegistryOld { - /// @notice Record describing a subnode creation request. - /// @param subLabel Human readable subnode label e.g "alice". - /// @param parentLabel Canonical parent name without the TLD suffix e.g. bob or child.bob. - /// @param owner Address to assign as owner of the created subnode. - /// @param persist Whether to index the subnode into the owner's `LabelStore`, deploying it on - /// demand. When false the ownership and resolver record is still written, but the store - /// is left untouched. The store write is gated to protocol store writers (the registrar - /// and its controllers), not the name owner, so a deferring writer indexes the label - /// itself by deploying the owner's store and writing to it. The registry writes the - /// store only on creation or on a reassignment to a new owner, so a later same-owner re-call - /// with `persist` true does not backfill it; the authorised writer backfills it - /// directly. - struct SubnodeRecord { - bytes32 parentNode; - string subLabel; - string parentLabel; - address owner; - bool persist; - } - - /// @notice Record describing the state of a node. - /// @param owner Address that owns the node, or address(0) sentinel for tokenised nodes. - /// @param resolver Address of the resolver associated with the node. - /// @param exists Whether the node has been explicitly created. - struct Record { - address owner; - address resolver; - bool exists; - } - - /// @notice Emitted when a new subnode owner is set. - /// @param node Parent node. - /// @param label Labelhash of the created subnode. - event NewOwner(bytes32 indexed node, bytes32 indexed label, address owner); - - /// @notice Emitted when ownership of a node is transferred. - event NodeTransferred(bytes32 indexed node, address owner); - - /// @notice Emitted when a resolver is set or updated. - event NewResolver(bytes32 indexed node, address resolver); - - /// @notice Thrown when an invalid (zero) address is provided. - error NotAllowed(); - - /// @notice Thrown when the caller is not authorised. - error NotAuthorised(); - - /// @notice Thrown when the caller is not the registry controller. - error NotRegistryController(); - - /// @notice Thrown when attempting to create a node that already exists. - error NodeAlreadyOwned(bytes32 node); - - /// @notice Thrown when a sublabel is not a canonical lowercase ASCII DNS label. - error InvalidLabel(); - - /// @notice Thrown when the supplied parent label does not match the parent node. - error ParentLabelMismatch(); - - /// @notice Record describing a subnode resolver update request. - /// @param subLabel Human-readable subnode label e.g "alice". - /// @param parentLabel Canonical parent name without the TLD suffix e.g bob or child.bob. - /// @param resolver Resolver contract address (zero clears). - struct SubnodeResolverRecord { - bytes32 parentNode; - string subLabel; - string parentLabel; - address resolver; - } - - /// @notice Creates or reassigns a subnode and assigns its owner. - /// @dev Callable only by the current owner of `record.parentNode`, otherwise - /// @custom:reverts NotAuthorised. The new owner address must be non-zero, otherwise - /// @custom:reverts NotAllowed. `record.subLabel` must be a single canonical DNS label - /// (otherwise @custom:reverts InvalidLabel) and `record.parentLabel` must be a name - /// path whose namehash matches `record.parentNode` (otherwise - /// @custom:reverts ParentLabelMismatch). Subnodes are parent-sovereign: the current - /// `record.parentNode` owner may reassign or rotate a subnode's resolver at any time - /// without the prior subnode owner's consent. On reassignment the resolver pointer is - /// reset to the protocol-registered default reverse resolver so a prior subnode - /// owner's resolver cannot be inherited by the next holder (records on other resolver - /// contracts are keyed by node and are not cleared by this function; downstream - /// consumers should gate resolver reads on current ownership). Indexes the subnode - /// under the new owner's `LabelStore` keyed by the namehashed `subnode` so off-chain - /// consumers can enumerate names per address. Indexing into the owner's `LabelStore` is - /// governed by `record.persist` (see @custom:struct SubnodeRecord); the ownership and - /// resolver record is written either way. Emits @custom:emits NewOwner on each successful - /// assignment. - function setSubnodeOwner(SubnodeRecord calldata record) external returns (bytes32 subnode); - - /// @notice Sets the resolver for an existing subnode. - /// @dev Callable only by the current owner of `record.parentNode`, otherwise - /// @custom:reverts NotAuthorised. The subnode owner can still update the resolver - /// directly via `setResolver`. Both entry points emit the same `NewResolver(subnode, - /// ...)` event, so the parent can silently override a subnode owner's chosen resolver: - /// this is the parent-sovereign hierarchy applied to resolution. Off-chain consumers - /// that surface trust signals to subnode owners should treat any resolver rotation as - /// a re-attestation prompt. `record.subLabel` must be a single canonical DNS label - /// (otherwise @custom:reverts InvalidLabel) and `record.parentLabel` must be a name - /// path whose namehash matches `record.parentNode` (otherwise - /// @custom:reverts ParentLabelMismatch). The resulting subnode must already exist, - /// otherwise @custom:reverts NotAuthorised. Emits @custom:emits NewResolver on - /// success. - function setSubnodeResolver(SubnodeResolverRecord calldata record) external; - - /// @notice Creates or resets a node record for a tokenised base registration. - /// @dev Restricted to the registrar's controllers, otherwise @custom:reverts NotAuthorised. - /// `newOwner` must be non-zero (otherwise @custom:reverts NotAllowed) and must match - /// the ERC-721 owner reported by the registrar (otherwise - /// @custom:reverts NotAuthorised). The function is callable both on a fresh - /// registration and on every reclaim from escrow: each call rewrites - /// `records[node].resolver` to the protocol-registered default reverse resolver so a - /// prior owner's resolver pointer (and the records keyed under it) cannot be inherited - /// by the next holder across that recycle. A secondary-market ERC-721 `transferFrom` does - /// not call the registry, so a name sold directly keeps the seller's resolver pointer - /// until the buyer overwrites it. Stores `owner = address(0)` as a sentinel so reads - /// delegate to `IDotnsRegistrarOld.ownerOf` and ERC-721 transfers remain authoritative. - /// Emits - /// @custom:emits NodeTransferred on success. - function setOwner(bytes32 node, address newOwner) external; - - /// @notice Sets or clears the resolver for a node. - /// @dev Callable only by the current node owner, otherwise @custom:reverts NotAuthorised. - /// For tokenised nodes, authorisation falls back to ERC-721 owner / approved / - /// operator-for-all via the registrar. The registry does not validate - /// `resolverAddr` against any interface or code presence; off-chain consumers must - /// verify resolver shape before trusting reads. Emits @custom:emits NewResolver on - /// success. - /// @param resolverAddr Resolver contract address (zero clears). - function setResolver(bytes32 node, address resolverAddr) external; - - /// @notice Returns the owner of a node. - /// @dev For tokenised nodes the stored owner is the zero sentinel; the implementation falls - /// back to `IDotnsRegistrarOld.ownerOf(uint256(node))`. - function owner(bytes32 node) external view returns (address); - - /// @notice Returns the resolver of a node. - function resolver(bytes32 node) external view returns (address); - - /// @notice Returns whether a node exists. - function recordExists(bytes32 node) external view returns (bool); - - /// @notice Returns whether `account` is authorised to manage `node`. - /// @dev For subnodes, authority is the explicit stored owner. For tokenised nodes it is the - /// ERC-721 owner, an address approved for the token, or an operator approved for all of - /// the owner's tokens via the registrar. This is the canonical authorisation check the - /// registry enforces on owner-gated entry points; sibling contracts may consult it so a - /// single registrar-level approval delegates management across the protocol. Returns - /// false for a node that does not exist. - /// @param node Node identifier. - /// @param account Address whose authority is being checked. - /// @return authorisedFlag True when `account` may manage `node`. - function isAuthorised(bytes32 node, address account) external view returns (bool authorisedFlag); -} diff --git a/contracts/resolvers/DotnsContentResolverOld.sol b/contracts/resolvers/DotnsContentResolverOld.sol deleted file mode 100644 index 2a59ba308..000000000 --- a/contracts/resolvers/DotnsContentResolverOld.sol +++ /dev/null @@ -1,152 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.34; - -import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import { - OwnableUpgradeable -} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; -import { - ERC165Upgradeable -} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; - -import {IDotnsRegistryOld} from "../registry/IDotnsRegistryOld.sol"; -import {IDotnsContentResolver} from "./IDotnsContentResolver.sol"; -import {IDotnsProtocolRegistryOld} from "../registry/IDotnsProtocolRegistryOld.sol"; -import {DotnsConstantsOld} from "../utils/DotnsConstantsOld.sol"; - -/// @title Dotns Content Resolver -/// @notice Implements `IDotnsContentResolver` interface with content hash, text records, and -/// operator approvals. -/// @dev Writes are gated on the registry's authorisation for the node (owner or registrar-level -/// approval) or on a resolver-local operator the owner has approved, rather than on a -/// privileged writer address. Content records are user-managed metadata, so write authority -/// follows the node owner across transfers and honours the same delegates the registry -/// recognises. -/// @custom:security-contact admin@parity.io -contract DotnsContentResolverOld is - Initializable, - UUPSUpgradeable, - OwnableUpgradeable, - ERC165Upgradeable, - IDotnsContentResolver -{ - /// @notice Stores all content hash mappings - mapping(bytes32 node => bytes contentHash) private contenthashes; - - /// @notice Stores all text records - mapping(bytes32 node => mapping(string key => string value)) private textRecords; - - /// @notice Store all approval mapping - mapping(address owner => mapping(address operator => bool approved)) private operators; - - /// @notice Protocol-level address registry for all DotNS contracts. - IDotnsProtocolRegistryOld public protocolRegistry; - - /// @dev Reserved storage space to allow for layout changes in the future. - // forge-lint: disable-next-line(mixed-case-variable) - uint256[50] private __gap; - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - _disableInitializers(); - } - - /// @notice Initialises the content resolver. - /// @dev Runs once through the UUPS proxy; a repeat call reverts with - /// @custom:reverts InvalidInitialization. Emits @custom:emits OwnershipTransferred when - /// `msg.sender` is recorded as the initial owner and @custom:emits Initialized once - /// setup completes. - /// @param registry Protocol-level address registry used to resolve sibling contracts. - function initialize(IDotnsProtocolRegistryOld registry) external initializer { - __Ownable_init(msg.sender); - __ERC165_init(); - protocolRegistry = registry; - } - - /// @inheritdoc IDotnsContentResolver - function setContenthash(bytes32 node, bytes calldata hash) external override { - _requireNodeOwnerOrOperator(node); - contenthashes[node] = hash; - emit ContentHashUpdated(node, hash); - } - - /// @inheritdoc IDotnsContentResolver - function contenthash(bytes32 node) external view override returns (bytes memory hash) { - return contenthashes[node]; - } - - /// @inheritdoc IDotnsContentResolver - function setText(bytes32 node, string calldata key, string calldata value) external override { - _requireNodeOwnerOrOperator(node); - textRecords[node][key] = value; - emit TextUpdated(node, key, value); - } - - /// @inheritdoc IDotnsContentResolver - function text( - bytes32 node, - string calldata key - ) - external - view - override - returns (string memory value) - { - return textRecords[node][key]; - } - - /// @inheritdoc IDotnsContentResolver - function setApprovalForAll(address operator, bool approved) external override { - operators[msg.sender][operator] = approved; - emit ApprovalForAll(msg.sender, operator, approved); - } - - /// @inheritdoc IDotnsContentResolver - function isApprovedForAll( - address owner, - address operator - ) - external - view - override - returns (bool) - { - return operators[owner][operator]; - } - - /// @notice Ensures the caller may write records for `node`. - /// @dev Authority is granted to the node owner, to a resolver-local operator the owner has - /// approved for all of their records, or to any address the registry deems authorised - /// for the node. Delegating through the registry means a single registrar-level - /// approval (ERC-721 owner / approved / operator-for-all) also confers record-write - /// authority, while the resolver-local operator mapping remains a narrower record-only - /// delegation that grants no power over ownership or transfers. The cheap owner and - /// local-operator checks run before the cross-contract registry call. - /// @param node Node identifier. - function _requireNodeOwnerOrOperator(bytes32 node) internal view { - IDotnsRegistryOld _registry = - IDotnsRegistryOld(protocolRegistry.get(DotnsConstantsOld.REGISTRY)); - address nodeOwner = _registry.owner(node); - require( - msg.sender == nodeOwner || operators[nodeOwner][msg.sender] - || _registry.isAuthorised(node, msg.sender), - NotAuthorised(node, msg.sender) - ); - } - - /// @notice Returns implementation version. - /// @return versionString Current version string. - function version() external pure virtual returns (string memory versionString) { - versionString = "1.0.0"; - } - - /// @inheritdoc ERC165Upgradeable - function supportsInterface(bytes4 interfaceId) public view override returns (bool) { - return interfaceId == type(IDotnsContentResolver).interfaceId - || super.supportsInterface(interfaceId); - } - - /// @inheritdoc UUPSUpgradeable - function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} -} diff --git a/contracts/resolvers/DotnsPopResolverOld.sol b/contracts/resolvers/DotnsPopResolverOld.sol index 490a92472..21b92fbf8 100644 --- a/contracts/resolvers/DotnsPopResolverOld.sol +++ b/contracts/resolvers/DotnsPopResolverOld.sol @@ -10,9 +10,9 @@ import { ERC165Upgradeable } from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; -import {IDotnsPopResolver} from "./IDotnsPopResolver.sol"; -import {IDotnsProtocolRegistryOld} from "../registry/IDotnsProtocolRegistryOld.sol"; -import {DotnsConstantsOld} from "../utils/DotnsConstantsOld.sol"; +import {IDotnsPopResolverOld} from "./IDotnsPopResolverOld.sol"; +import {IDotnsProtocolRegistry} from "../registry/IDotnsProtocolRegistry.sol"; +import {DotnsConstants} from "../utils/DotnsConstants.sol"; /// @title DotnsPopResolverOld /// @notice Per-node resolver holding records produced by the PoP username flow. @@ -26,10 +26,10 @@ contract DotnsPopResolverOld is UUPSUpgradeable, OwnableUpgradeable, ERC165Upgradeable, - IDotnsPopResolver + IDotnsPopResolverOld { /// @notice Protocol-level address registry used to resolve the authorised writer. - IDotnsProtocolRegistryOld public protocolRegistry; + IDotnsProtocolRegistry public protocolRegistry; /// @notice Stored chat-key bytes keyed by node. mapping(bytes32 node => bytes chatKey) private _chatKeys; @@ -67,14 +67,21 @@ contract DotnsPopResolverOld is /// setup needs because the authorised writer is resolved dynamically through /// `POP_CONTROLLER`. Emits @custom:emits OwnershipTransferred when `msg.sender` is /// recorded as the initial owner and @custom:emits Initialized once setup completes. + /// @param initialOwner Address that owns the contract once initialised. /// @param registry Protocol-level address registry used for writer resolution. - function initialize(IDotnsProtocolRegistryOld registry) external initializer { - __Ownable_init(msg.sender); + function initialize( + address initialOwner, + IDotnsProtocolRegistry registry + ) + external + initializer + { + __Ownable_init(initialOwner); __ERC165_init(); protocolRegistry = registry; } - /// @inheritdoc IDotnsPopResolver + /// @inheritdoc IDotnsPopResolverOld function setChatKey( bytes32 node, bytes calldata chatKeyBytes @@ -88,7 +95,7 @@ contract DotnsPopResolverOld is emit ChatKeyUpdated(node, chatKeyBytes); } - /// @inheritdoc IDotnsPopResolver + /// @inheritdoc IDotnsPopResolverOld function setLiteLink( bytes32 fullNode, bytes32 liteLabelhash @@ -110,39 +117,42 @@ contract DotnsPopResolverOld is emit LiteLinkUpdated(fullNode, liteLabelhash); } - /// @inheritdoc IDotnsPopResolver + /// @inheritdoc IDotnsPopResolverOld function chatKey(bytes32 node) external view override returns (bytes memory) { return _chatKeys[node]; } - /// @inheritdoc IDotnsPopResolver + /// @inheritdoc IDotnsPopResolverOld function liteLink(bytes32 fullNode) external view override returns (bytes32) { return _liteLinks[fullNode]; } - /// @inheritdoc IDotnsPopResolver + /// @inheritdoc IDotnsPopResolverOld function fullClaim(bytes32 liteLabelhash) external view override returns (bytes32) { return _fullClaims[liteLabelhash]; } - /// @notice Returns implementation version. - /// @dev Bumped on every upgrade. Used by deployment scripts as a - /// post-upgrade assertion target. - /// @return versionString Current version string. - function version() external pure virtual returns (string memory versionString) { - versionString = "1.0.0"; + /// @notice Returns the release this network declares it runs, read live from the protocol + /// registry so every DotNS contract reports one synchronised value. + /// @dev Mirror of `IDotnsProtocolRegistry.protocolVersion`, kept under the historical + /// `version()` selector for ABI compatibility. It reports the network's declaration, + /// not this contract's build; per-contract identity is the codehash declared on the + /// registry. + /// @return versionString Declared release as bare semver, empty when never declared. + function version() external view virtual returns (string memory versionString) { + versionString = protocolRegistry.protocolVersion(); } /// @inheritdoc ERC165Upgradeable function supportsInterface(bytes4 interfaceId) public view override returns (bool) { return - interfaceId == type(IDotnsPopResolver).interfaceId + interfaceId == type(IDotnsPopResolverOld).interfaceId || super.supportsInterface(interfaceId); } /// @notice Internal check enforcing PoP-controller-only access. function _onlyPopController() internal view { - address popController = protocolRegistry.get(DotnsConstantsOld.POP_CONTROLLER); + address popController = protocolRegistry.get(DotnsConstants.POP_CONTROLLER); require(msg.sender == popController, NotPopController(msg.sender)); } diff --git a/contracts/resolvers/DotnsResolverOld.sol b/contracts/resolvers/DotnsResolverOld.sol deleted file mode 100644 index 59661989e..000000000 --- a/contracts/resolvers/DotnsResolverOld.sol +++ /dev/null @@ -1,100 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.34; - -import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import { - OwnableUpgradeable -} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; -import { - ERC165Upgradeable -} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; - -import {IDotnsResolver} from "./IDotnsResolver.sol"; -import {IDotnsRegistryOld} from "../registry/IDotnsRegistryOld.sol"; -import {IDotnsProtocolRegistryOld} from "../registry/IDotnsProtocolRegistryOld.sol"; -import {DotnsConstantsOld} from "../utils/DotnsConstantsOld.sol"; - -/// @title Dotns Resolver -/// @notice Stores forward-resolution address records for DotNS nodes -/// @dev Writes are gated on node ownership in the forward registry, not on a -/// privileged writer address. Address records describe where a name points -/// and only the current node owner has the authority to set that target. -/// @custom:security-contact admin@parity.io -contract DotnsResolverOld is - Initializable, - UUPSUpgradeable, - OwnableUpgradeable, - ERC165Upgradeable, - IDotnsResolver -{ - /// @notice Protocol-level address registry for all DotNS contracts. - IDotnsProtocolRegistryOld public protocolRegistry; - - /// @dev Reserved storage space to allow for layout changes in the future. - // forge-lint: disable-next-line(mixed-case-variable) - uint256[50] private __gap; - - /// @notice Node => resolved address. - mapping(bytes32 node => address owner) private addresses; - - /// @notice Restricts access to the owner of `node` as recorded in the registry. - /// @param node Node identifier. - modifier onlyNodeOwner(bytes32 node) { - _onlyNodeOwner(node); - _; - } - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - _disableInitializers(); - } - - /// @notice Initialises the resolver. - /// @dev Runs once through the UUPS proxy; a repeat call reverts with - /// @custom:reverts InvalidInitialization. Emits @custom:emits OwnershipTransferred when - /// `msg.sender` is recorded as the initial owner and @custom:emits Initialized once - /// setup completes. - /// @param registry Protocol-level address registry used to resolve sibling contracts. - function initialize(IDotnsProtocolRegistryOld registry) external initializer { - __Ownable_init(msg.sender); - __ERC165_init(); - protocolRegistry = registry; - } - - /// @inheritdoc IDotnsResolver - function setAddress(bytes32 node, address value) external override onlyNodeOwner(node) { - addresses[node] = value; - emit AddressSet(node, value); - } - - /// @inheritdoc IDotnsResolver - function addressOf(bytes32 node) external view override returns (address value) { - return addresses[node]; - } - - /// @inheritdoc ERC165Upgradeable - function supportsInterface(bytes4 interfaceId) public view override returns (bool) { - return - interfaceId == type(IDotnsResolver).interfaceId || super.supportsInterface(interfaceId); - } - - /// @notice Internal ownership check for a registry node. - /// @dev Resolves the registry lazily through `protocolRegistry` so a registry - /// upgrade or rewire is picked up automatically without a resolver upgrade. - /// @param node Node identifier. - function _onlyNodeOwner(bytes32 node) internal view { - IDotnsRegistryOld _registry = - IDotnsRegistryOld(protocolRegistry.get(DotnsConstantsOld.REGISTRY)); - require(_registry.owner(node) == msg.sender, NotAuthorised(node, msg.sender)); - } - - /// @notice Returns implementation version. - /// @return versionString Current version string. - function version() external pure virtual returns (string memory versionString) { - versionString = "1.0.0"; - } - - /// @inheritdoc UUPSUpgradeable - function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} -} diff --git a/contracts/resolvers/DotnsReverseResolverOld.sol b/contracts/resolvers/DotnsReverseResolverOld.sol deleted file mode 100644 index 11fbeb698..000000000 --- a/contracts/resolvers/DotnsReverseResolverOld.sol +++ /dev/null @@ -1,146 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.34; - -import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import { - OwnableUpgradeable -} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; -import { - ERC165Upgradeable -} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; -import {IDotnsReverseResolver} from "./IDotnsReverseResolver.sol"; -import {IDotnsProtocolRegistryOld} from "../registry/IDotnsProtocolRegistryOld.sol"; -import {IDotnsRegistryOld} from "../registry/IDotnsRegistryOld.sol"; -import {DotnsConstantsOld} from "../utils/DotnsConstantsOld.sol"; -import {LabelUtils} from "../utils/LabelUtils.sol"; -import {SubnodeUtilsOld} from "../utils/SubnodeUtilsOld.sol"; -import {StringUtils} from "../utils/StringUtils.sol"; - -/// @title Dotns Reverse Resolver -/// @notice Resolves an address to its associated name under the network TLD. -/// @dev Writes are gated on a fixed writer address resolved from the protocol -/// registry (the registrar or its controller), not on node ownership. -/// Reverse records bind to an EOA rather than a registry node, so authority -/// is delegated to the contract that mints names on the user's behalf. -/// @custom:security-contact admin@parity.io -contract DotnsReverseResolverOld is - Initializable, - UUPSUpgradeable, - OwnableUpgradeable, - ERC165Upgradeable, - IDotnsReverseResolver -{ - /// @dev Mapping from address to its reverse name. An empty string indicates - /// that no reverse name is set. - mapping(address owner => string name) private reverseNames; - - /// @notice Protocol-level address registry for all DotNS contracts. - IDotnsProtocolRegistryOld public protocolRegistry; - - /// @dev Reserved storage space to allow for layout changes in the future. - // forge-lint: disable-next-line(mixed-case-variable) - uint256[50] private __gap; - - /// @notice Restricts access to the configured registrar. - modifier onlyRegistrar() { - _onlyRegistrar(); - _; - } - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - _disableInitializers(); - } - - /// @notice Initialises the reverse resolver. - /// @dev May only be called once per proxy; a repeat call reverts with - /// @custom:reverts InvalidInitialization. Emits @custom:emits OwnershipTransferred when - /// `msg.sender` is recorded as the initial owner and @custom:emits Initialized once - /// setup completes. - /// @param registry Protocol-level address registry used to resolve sibling contracts. - function initialize(IDotnsProtocolRegistryOld registry) external initializer { - __Ownable_init(msg.sender); - __ERC165_init(); - protocolRegistry = registry; - } - - /// @inheritdoc IDotnsReverseResolver - function setReverseName(address addr, string calldata name) external override onlyRegistrar { - reverseNames[addr] = name; - emit ReverseNameSet(addr, name); - } - - /// @inheritdoc IDotnsReverseResolver - function claimReverseRecord(string calldata label) external override { - bytes32 node = _nodeOf(label); - require(_registry().owner(node) == msg.sender, NotNameOwner(msg.sender, uint256(node))); - - string memory fullName = string.concat(label, protocolRegistry.tld()); - reverseNames[msg.sender] = fullName; - emit ReverseNameSet(msg.sender, fullName); - } - - /// @inheritdoc IDotnsReverseResolver - function nameOf(address addr) external view override returns (string memory name) { - string memory stored = reverseNames[addr]; - if (bytes(stored).length == 0) return ""; - - // Strip the TLD suffix and validate against current ownership so a transferred-away - // name never resolves under a stale reverse record. - string memory label = LabelUtils.stripTld(protocolRegistry.tld(), stored); - if (bytes(label).length == 0) return ""; - - if (_registry().owner(_nodeOf(label)) != addr) return ""; - return stored; - } - - /// @notice Resolves the node a name maps to, whether tokenised or a lite subname. - /// @dev A lite name is `stem` beneath its numeric container, so it hashes as a subnode; any - /// other name hashes as a second-level label under the TLD. Ownership of either is read - /// through the registry, which delegates a tokenised name to the registrar and holds a - /// subname directly. - /// @param label Bare label without the TLD, e.g. `alice` or `alice.01`. - /// @return node The node the name resolves to. - function _nodeOf(string memory label) internal view returns (bytes32 node) { - bytes32 tldNode = protocolRegistry.tldNode(); - if (StringUtils.isLitePersonLabelMemory(label)) { - return SubnodeUtilsOld.liteSubnodeOf(tldNode, label); - } - node = LabelUtils.namehashUnder(tldNode, LabelUtils.labelhashMemory(label)); - } - - /// @notice Resolves the registry via the protocol registry. - function _registry() internal view returns (IDotnsRegistryOld) { - return IDotnsRegistryOld(protocolRegistry.get(DotnsConstantsOld.REGISTRY)); - } - - /// @inheritdoc ERC165Upgradeable - function supportsInterface(bytes4 interfaceId) - public - view - override(ERC165Upgradeable) - returns (bool supported) - { - return interfaceId == type(IDotnsReverseResolver).interfaceId - || super.supportsInterface(interfaceId); - } - - /// @notice Internal check enforcing registrar-only access. - function _onlyRegistrar() internal view { - address controller = protocolRegistry.get(DotnsConstantsOld.CONTROLLER); - address registrar = protocolRegistry.get(DotnsConstantsOld.REGISTRAR); - require( - msg.sender == controller || msg.sender == registrar, NotRegistrarController(msg.sender) - ); - } - - /// @notice Returns implementation version. - /// @return versionString Current version string. - function version() external pure virtual returns (string memory versionString) { - versionString = "1.0.0"; - } - - /// @inheritdoc UUPSUpgradeable - function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} -} diff --git a/contracts/resolvers/IDotnsPopResolverOld.sol b/contracts/resolvers/IDotnsPopResolverOld.sol new file mode 100644 index 000000000..f75aaa025 --- /dev/null +++ b/contracts/resolvers/IDotnsPopResolverOld.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.34; + +/// @title IDotnsPopResolverOld +/// @notice Resolver for per-name records produced by the PoP username flow. +/// @dev Holds three record kinds: +/// - Chat key: ECDH public-key bytes used for end-to-end encrypted messaging. +/// - Lite link: for a full-person node, the labelhash of the lite-person +/// username it was minted from (when the link was made). +/// - Full claim: reverse index mapping a lite labelhash to the full-person +/// node it was promoted to. Mirrors `liteLink` on every write so a +/// caller that holds a lite labelhash can resolve the full-person node +/// without scanning events. +/// +/// Lives separately from the per-user `LabelStore` so that the store can remain +/// a labels-only, protocol-write / user-read surface, and follows the project's +/// resolver-per-record-category convention used by @custom:contract IDotnsContentResolver and +/// @custom:contract IDotnsReverseResolver. +/// +/// Write authorisation is delegated to the address registered as +/// `DotnsProtocolRegistry.POP_CONTROLLER` at call time, so rotating the PoP +/// controller is a single `set` on the protocol registry with no resolver +/// upgrade required. +/// @custom:security-contact admin@parity.io +interface IDotnsPopResolverOld { + /// @notice Emitted when a node's chat key is set or updated. + /// @param node The node whose chat key was written. + /// @param chatKey The new chat key bytes. + event ChatKeyUpdated(bytes32 indexed node, bytes chatKey); + + /// @notice Emitted when a full-person node's lite link is set or updated. + /// @param fullNode The full-person node carrying the link. + /// @param liteLabelhash The labelhash of the linked lite-person username. + event LiteLinkUpdated(bytes32 indexed fullNode, bytes32 indexed liteLabelhash); + + /// @notice Thrown when the caller is not the authorised PoP controller. + /// @param caller The address that attempted the write. + error NotPopController(address caller); + + /// @notice Thrown when the provided chat key does not match the expected 65-byte length. + /// @param length The length of the payload that was rejected. + error InvalidChatKeyLength(uint256 length); + + /// @notice Sets the chat key for `node`. + /// @dev Callable only by the address registered under `DotnsProtocolRegistry.POP_CONTROLLER`, + /// otherwise @custom:reverts NotPopController. Overwrites any previous value. The payload + /// must be exactly 65 bytes: the uncompressed secp256k1 public key encoding (1 prefix + /// byte followed by the 32-byte X and 32-byte Y affine coordinates); any other length + /// reverts with @custom:reverts InvalidChatKeyLength. Emits @custom:emits ChatKeyUpdated + /// on every successful write. + /// @param node The node whose chat key is being written. + /// @param chatKey ECDH public key bytes (pallet-side type is `[u8; 65]`). + function setChatKey(bytes32 node, bytes calldata chatKey) external; + + /// @notice Sets the lite-person link for a full-person `node`. + /// @dev Callable only by the authorised PoP controller, otherwise + /// @custom:reverts NotPopController. Overwrites any previous link. When overwriting, the + /// stale inverse entry is nulled so both the forward (`liteLink`) and reverse + /// (`fullClaim`) indices remain consistent: re-linking the same `fullNode` to a new + /// `liteLabelhash` clears `fullClaim(oldLite)`, and re-linking the same `liteLabelhash` + /// to a new `fullNode` clears `liteLink(oldFull)`. The invariant + /// `fullClaim(liteLink(node)) == node` always holds after the call. Emits + /// @custom:emits LiteLinkUpdated on every successful write. + /// @param fullNode The full-person node carrying the link. + /// @param liteLabelhash The labelhash of the linked lite-person username. + function setLiteLink(bytes32 fullNode, bytes32 liteLabelhash) external; + + /// @notice Returns the chat key associated with a node. + /// @param node The node to query. + /// @return chatKey The stored chat key bytes, or empty if unset. + function chatKey(bytes32 node) external view returns (bytes memory chatKey); + + /// @notice Returns the lite-person labelhash linked to a full-person node. + /// @param fullNode The full-person node to query. + /// @return liteLabelhash The linked lite-person labelhash, or zero if unset. + function liteLink(bytes32 fullNode) external view returns (bytes32 liteLabelhash); + + /// @notice Returns the full-person node a given lite label has claimed. + /// @dev Reverse of @custom:function liteLink. Written by the same `setLiteLink` call so the + /// two directions stay in lockstep. Returns zero when the lite label + /// has never been linked to a full claim. + /// @param liteLabelhash The labelhash of the lite-person username to query. + /// @return fullNode The full-person node claimed from this lite label, or + /// zero if unset. + function fullClaim(bytes32 liteLabelhash) external view returns (bytes32 fullNode); +} diff --git a/contracts/store/IStoreFactoryOld.sol b/contracts/store/IStoreFactoryOld.sol deleted file mode 100644 index 690925391..000000000 --- a/contracts/store/IStoreFactoryOld.sol +++ /dev/null @@ -1,157 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.34; - -/// @title IStoreFactoryOld -/// @notice Interface for the DotNS per-user store factory. -/// @dev Owns two `UpgradeableBeacon` instances; one for `LabelStore` (protocol-managed), -/// one for `UserStore` (user-claimed). Each user may acquire at most one of each, -/// forever. There is no transfer, no redeploy, no additional store type. -/// @custom:security-contact admin@parity.io -interface IStoreFactoryOld { - /// @notice Emitted when a `LabelStore` beacon-proxy is deployed for `user`. - /// @param user The user the store is bound to. - /// @param store The deployed store address. - event LabelStoreDeployed(address indexed user, address indexed store); - - /// @notice Emitted when `user` claims their `UserStore` beacon-proxy. - /// @param user The user the store is bound to. - /// @param store The deployed store address. - event UserStoreClaimed(address indexed user, address indexed store); - - /// @notice Emitted when the `LabelStore` implementation behind the label beacon is upgraded. - /// @param newImplementation The new implementation address. - event LabelStoreImplementationUpgraded(address indexed newImplementation); - - /// @notice Emitted when the `UserStore` implementation behind the user beacon is upgraded. - /// @param newImplementation The new implementation address. - event UserStoreImplementationUpgraded(address indexed newImplementation); - - /// @notice Thrown when attempting to deploy or claim a store that already exists. - /// @param user The user for whom the store exists. - /// @param existingStore The already-deployed store address. - error AlreadyDeployed(address user, address existingStore); - - /// @notice Thrown when a zero user address is supplied. - /// @param user The invalid user argument. - error InvalidUser(address user); - - /// @notice Thrown when a zero protocol registry address is supplied to the constructor. - /// @param protocolRegistry The invalid registry argument. - error InvalidProtocolRegistry(address protocolRegistry); - - /// @notice Thrown when a zero implementation address is supplied to the constructor or an - /// upgrade. @param implementation The invalid implementation argument. - error InvalidImplementation(address implementation); - - /// @notice Thrown when an unauthorised address attempts to deploy a label store. - /// @param caller The unauthorised msg.sender. - error NotAuthorised(address caller); - - /// @notice Thrown when a freshly deployed proxy does not report the expected owner. - error ImplementationBindingMismatch(); - - /// @notice Returns the `UpgradeableBeacon` address backing all `LabelStore` proxies. - /// @return beacon Address of the beacon contract. - function labelStoreBeacon() external view returns (address beacon); - - /// @notice Returns the `UpgradeableBeacon` address backing all `UserStore` proxies. - /// @return beacon Address of the beacon contract. - function userStoreBeacon() external view returns (address beacon); - - /// @notice Returns the protocol registry address used for writer authorisation. - /// @return registry Address of the protocol registry. - function protocolRegistry() external view returns (address registry); - - /// @notice Deploys a `LabelStore` beacon-proxy bound to `user`. - /// @dev Callable by the factory owner or a component named in - /// @custom:function StoreAuth.isStoreWriter; any other caller - /// @custom:reverts NotAuthorised. `user` must be non-zero, - /// otherwise @custom:reverts InvalidUser. The user must not already have a - /// `LabelStore`, otherwise @custom:reverts AlreadyDeployed. After deployment the - /// freshly initialised proxy must report `user` as its owner, otherwise - /// @custom:reverts ImplementationBindingMismatch. Emits - /// @custom:emits LabelStoreDeployed on success. - /// @param user The user the store is bound to forever. - /// @return store The deployed store address. - function deployLabelStoreFor(address user) external returns (address store); - - /// @notice Returns the `LabelStore` address bound to `user`, or the zero address if none. - /// @param user The user to look up. - /// @return store The bound store address, or zero. - function getLabelStore(address user) external view returns (address store); - - /// @notice Returns the total number of `LabelStore` proxies ever deployed. - /// @return count Length of the deployment list. - function getLabelStoreCount() external view returns (uint256 count); - - /// @notice Paginated enumeration over every `LabelStore` proxy ever deployed. - /// @dev Insertion order of `deployLabelStoreFor` calls. `offset >= getLabelStoreCount()` - /// returns an empty array; result length is `min(limit, count - offset)`. - /// @param offset Start index. - /// @param limit Maximum entries to return. - /// @return stores Slice of label-store addresses. - function getLabelStores( - uint256 offset, - uint256 limit - ) - external - view - returns (address[] memory stores); - - /// @notice Upgrades the `LabelStore` implementation for every existing and future proxy. - /// @dev Callable by the factory owner only, otherwise - /// @custom:reverts OwnableUnauthorizedAccount. `newImplementation` must be non-zero, - /// otherwise @custom:reverts InvalidImplementation. The candidate is sentinel-probed by - /// calling `ILabelStore.protocolRegistry` on it before the beacon is rotated; if the - /// address does not implement that selector the probe reverts and the upgrade does not - /// land (deliberate fail-fast guard, no named error). Delegates to - /// `UpgradeableBeacon.upgradeTo` and emits - /// @custom:emits LabelStoreImplementationUpgraded on success. - /// @param newImplementation The new implementation address. - function upgradeLabelStoreImplementation(address newImplementation) external; - - /// @notice Caller claims their `UserStore` beacon-proxy. - /// @dev Self-claim only; `_owner` on the resulting store is always `msg.sender`, - /// regardless of who pays gas. One store per caller, forever: a caller who already - /// has a `UserStore` @custom:reverts AlreadyDeployed. After deployment the freshly - /// initialised proxy must report `msg.sender` as its owner, otherwise - /// @custom:reverts ImplementationBindingMismatch. Emits - /// @custom:emits UserStoreClaimed on success. - /// @return store The deployed store address. - function claimUserStore() external returns (address store); - - /// @notice Returns the `UserStore` address bound to `user`, or the zero address if none. - /// @param user The user to look up. - /// @return store The bound store address, or zero. - function getUserStore(address user) external view returns (address store); - - /// @notice Returns the total number of `UserStore` proxies ever claimed. - /// @return count Length of the claim list. - function getUserStoreCount() external view returns (uint256 count); - - /// @notice Paginated enumeration over every `UserStore` proxy ever claimed. - /// @dev Insertion order of `claimUserStore` calls. `offset >= getUserStoreCount()` - /// returns an empty array; result length is `min(limit, count - offset)`. - /// @param offset Start index. - /// @param limit Maximum entries to return. - /// @return stores Slice of user-store addresses. - function getUserStores( - uint256 offset, - uint256 limit - ) - external - view - returns (address[] memory stores); - - /// @notice Upgrades the `UserStore` implementation for every existing and future proxy. - /// @dev Callable by the factory owner only, otherwise - /// @custom:reverts OwnableUnauthorizedAccount. `newImplementation` must be non-zero, - /// otherwise @custom:reverts InvalidImplementation. The candidate is sentinel-probed by - /// calling `IUserStore.getKeyCount` on it before the beacon is rotated; if the address - /// does not implement that selector the probe reverts and the upgrade does not land - /// (deliberate fail-fast guard, no named error). Delegates to - /// `UpgradeableBeacon.upgradeTo` and emits - /// @custom:emits UserStoreImplementationUpgraded on success. - /// @param newImplementation The new implementation address. - function upgradeUserStoreImplementation(address newImplementation) external; -} diff --git a/contracts/store/StoreFactoryMigrator.sol b/contracts/store/StoreFactoryMigrator.sol deleted file mode 100644 index d3e0e8123..000000000 --- a/contracts/store/StoreFactoryMigrator.sol +++ /dev/null @@ -1,366 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.34; - -import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import { - OwnableUpgradeable -} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; -import {BeaconProxy} from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; -import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; - -import {IStoreFactory} from "./IStoreFactory.sol"; -import {IDotnsStore} from "./IDotnsStore.sol"; -import {ILabelStore} from "./ILabelStore.sol"; -import {IUserStore} from "./IUserStore.sol"; -import {LabelStore} from "./LabelStore.sol"; -import {UserStore} from "./UserStore.sol"; -import {IDotnsProtocolRegistry} from "../registry/IDotnsProtocolRegistry.sol"; -import {StoreAuth} from "../utils/StoreAuth.sol"; - -/// @title StoreFactoryMigrator -/// @notice The shipped `StoreFactory`, with the per-user bindings reachable and a paged import -/// that copies them from the factory a network used before this one. -/// @dev PR-scoped migration tooling, upgraded into the proxy for the import transactions and -/// upgraded straight back out. It exists because a `StoreFactory` cannot be moved: the -/// bindings are proxy storage and the shipped contract offers no way to write one except by -/// deploying a new store, so a network that re-points `STORE_FACTORY` at a fresh factory -/// starts with an empty directory and hands every existing user a second, empty store the -/// next time one is needed. -/// -/// The contract body is the shipped factory verbatim, with four fields widened from -/// `private` to `internal` and `importStores` added. Keeping it a copy rather than a -/// subclass or a set of raw slot writes is what makes the layout identical by construction: -/// the upgrade in and the upgrade back both diff against a layout that cannot have drifted. -/// -/// What it does not do: the imported stores stay on the beacons the old factory minted, and -/// those beacons answer to the old factory. Their implementations remain upgradeable there, -/// by the same owner, and are not reachable from this factory's beacons. A `BeaconProxy` -/// holds its beacon address in an immutable, so no migration can change that. -/// @custom:security-contact admin@parity.io -contract StoreFactoryMigrator is Initializable, UUPSUpgradeable, OwnableUpgradeable, IStoreFactory { - /// @notice Beacon backing every `LabelStore` proxy. - /// @dev Public getter name is interface-constrained by @custom:contract IStoreFactory. - address public override labelStoreBeacon; - - /// @notice Beacon backing every `UserStore` proxy. - /// @dev Public getter name is interface-constrained by @custom:contract IStoreFactory. - address public override userStoreBeacon; - - /// @notice Protocol registry used to authorise `deployLabelStoreFor` callers. - /// @dev Public getter name is interface-constrained by @custom:contract IStoreFactory. - address public override protocolRegistry; - - /// @dev user => their permanent `LabelStore`. Set once per user, forever. - mapping(address user => address store) internal _labelStores; - - /// @dev user => their permanent `UserStore`. Set once per user, forever. - mapping(address user => address store) internal _userStores; - - /// @dev Insertion-order list of every `LabelStore` proxy ever deployed. Append-only. - address[] internal _labelStoreList; - - /// @dev Insertion-order list of every `UserStore` proxy ever claimed. Append-only. - address[] internal _userStoreList; - - /// @dev Reserved storage space to allow for layout changes in future upgrades. - uint256[50] private __gap; - - /// @notice A binding was adopted from the previous factory. - /// @param user Address the store belongs to. - /// @param store The `LabelStore` now bound to `user` on this factory. - event StoresImported(address indexed user, address indexed store); - - /// @notice The old factory's store list does not have the length the factory reports. - /// @dev Reading the count and the list are two calls, so they can disagree: a truncated - /// enumeration would import a prefix and leave the rest unbound, and unbound users are - /// handed an empty store on their next registration rather than the one they have. - /// @param expected Count the old factory reports. - /// @param actual Number of entries actually seen. - error ImportCountMismatch(uint256 expected, uint256 actual); - - /// @notice A store's owner is not the user the old factory has it bound to. - /// @dev The store list and the per-user mapping are separate state. Importing on the store's - /// word alone would let a store whose owner no longer matches the mapping bind a user - /// the old factory does not consider its holder. - /// @param user Owner the store reports. - /// @param store The store in the old factory's list. - error ImportBindingMismatch(address user, address store); - - /// @notice A page of zero bindings was requested. - /// @dev A zero limit would import nothing while looking like progress, so it is a caller bug - /// worth naming rather than a no-op worth allowing. - error ImportPageEmpty(); - - /// @notice Restricts `deployLabelStoreFor` to the owner or a component named in - /// @custom:function StoreAuth.isStoreWriter. - modifier onlyOwnerOrProtocol() { - _onlyOwnerOrProtocol(); - _; - } - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - _disableInitializers(); - } - - /// @notice Initialises the factory together with both store implementations and beacons. - /// @dev Callable exactly once via `Initializable`, otherwise - /// @custom:reverts InvalidInitialization. A single initialiser call wires everything: - /// - Deploys a fresh `LabelStore` implementation. - /// - Deploys a fresh `UserStore` implementation. - /// - Constructs both `UpgradeableBeacon` instances, owned by `address(this)`, which - /// under the proxy is the proxy itself, so `upgrade*Implementation` can delegate to - /// `beacon.upgradeTo` and the beacons outlive any implementation swap. - /// The implementations are deployed here rather than accepted as parameters, so the call - /// carries no ordering dependency on a prior deploy and exposes no argument through which - /// a mismatched implementation could reach a beacon. `protocolRegistry_` must be - /// non-zero, otherwise @custom:reverts InvalidProtocolRegistry. - /// @param initialOwner Account that owns this factory and can upgrade it and the store - /// implementations. - /// @param protocolRegistry_ The protocol registry for writer auth on label stores. - function initialize(address initialOwner, address protocolRegistry_) external initializer { - __Ownable_init(initialOwner); - - require(protocolRegistry_ != address(0), InvalidProtocolRegistry(protocolRegistry_)); - // Probing the registry rejects a wrong address here rather than at the first store deploy, - // and is what catches the two address arguments being passed the wrong way round. - IDotnsProtocolRegistry(protocolRegistry_).isRegisteredAddress(address(0)); - - protocolRegistry = protocolRegistry_; - labelStoreBeacon = address(new UpgradeableBeacon(address(new LabelStore()), address(this))); - userStoreBeacon = address(new UpgradeableBeacon(address(new UserStore()), address(this))); - } - - /// @inheritdoc IStoreFactory - function deployLabelStoreFor(address user) - external - override - onlyOwnerOrProtocol - returns (address store) - { - require(user != address(0), InvalidUser(user)); - require(_labelStores[user] == address(0), AlreadyDeployed(user, _labelStores[user])); - - bytes memory initData = abi.encodeCall(ILabelStore.initialize, (user, protocolRegistry)); - store = address(new BeaconProxy(labelStoreBeacon, initData)); - require(IDotnsStore(store).owner() == user, ImplementationBindingMismatch()); - _labelStores[user] = store; - _labelStoreList.push(store); - - emit LabelStoreDeployed(user, store); - } - - /// @inheritdoc IStoreFactory - function getLabelStore(address user) external view override returns (address store) { - return _labelStores[user]; - } - - /// @inheritdoc IStoreFactory - function getLabelStoreCount() external view override returns (uint256 count) { - return _labelStoreList.length; - } - - /// @inheritdoc IStoreFactory - function getLabelStores( - uint256 offset, - uint256 limit - ) - external - view - override - returns (address[] memory stores) - { - stores = _paginateAddresses(_labelStoreList, offset, limit); - } - - /// @inheritdoc IStoreFactory - function upgradeLabelStoreImplementation(address newImplementation) - external - override - onlyOwner - { - require(newImplementation != address(0), InvalidImplementation(newImplementation)); - ILabelStore(newImplementation).protocolRegistry(); - UpgradeableBeacon(labelStoreBeacon).upgradeTo(newImplementation); - emit LabelStoreImplementationUpgraded(newImplementation); - } - - /// @inheritdoc IStoreFactory - function claimUserStore() external override returns (address store) { - require( - _userStores[msg.sender] == address(0), - AlreadyDeployed(msg.sender, _userStores[msg.sender]) - ); - - bytes memory initData = - abi.encodeCall(IUserStore.initialize, (msg.sender, protocolRegistry)); - store = address(new BeaconProxy(userStoreBeacon, initData)); - require(IDotnsStore(store).owner() == msg.sender, ImplementationBindingMismatch()); - _userStores[msg.sender] = store; - _userStoreList.push(store); - - emit UserStoreClaimed(msg.sender, store); - } - - /// @inheritdoc IStoreFactory - function getUserStore(address user) external view override returns (address store) { - return _userStores[user]; - } - - /// @inheritdoc IStoreFactory - function getUserStoreCount() external view override returns (uint256 count) { - return _userStoreList.length; - } - - /// @inheritdoc IStoreFactory - function getUserStores( - uint256 offset, - uint256 limit - ) - external - view - override - returns (address[] memory stores) - { - stores = _paginateAddresses(_userStoreList, offset, limit); - } - - /// @inheritdoc IStoreFactory - function upgradeUserStoreImplementation(address newImplementation) external override onlyOwner { - require(newImplementation != address(0), InvalidImplementation(newImplementation)); - IUserStore(newImplementation).protocolRegistry(); - UpgradeableBeacon(userStoreBeacon).upgradeTo(newImplementation); - emit UserStoreImplementationUpgraded(newImplementation); - } - - /// @notice Returns the release this network declares it runs, read live from the protocol - /// registry so every DotNS contract reports one synchronised value. - /// @dev Mirror of `IDotnsProtocolRegistry.protocolVersion`, kept under the historical - /// `version()` selector for ABI compatibility. It reports the network's declaration, - /// not this contract's build; per-contract identity is the codehash declared on the - /// registry. - /// @return versionString Declared release as bare semver, empty when never declared. - function version() external view virtual returns (string memory versionString) { - versionString = IDotnsProtocolRegistry(protocolRegistry).protocolVersion(); - } - - /// @inheritdoc UUPSUpgradeable - function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} - - /// @notice Internal authorisation check deferred from the `onlyOwnerOrProtocol` modifier. - function _onlyOwnerOrProtocol() internal view { - if (msg.sender == owner()) return; - require(StoreAuth.isStoreWriter(protocolRegistry, msg.sender), NotAuthorised(msg.sender)); - } - - /// @notice Copies one page of `oldFactory`'s per-user `LabelStore` bindings into this factory. - /// @dev Owner-only, and reads everything it needs from `oldFactory` itself: the count, the - /// store list, and each store's owner. Nothing is supplied by the caller but the page - /// bounds, so there is no window between an operator reading the set and this executing. - /// That window is not hypothetical: the count on the network being migrated moved while - /// this was being written, and a list captured a moment early imports every entry it - /// holds, rewires, and leaves the newest holder to be handed a second empty store on - /// their next registration. - /// - /// Paged because of where this runs. pallet-revive meters transactions in more - /// dimensions than gas, and a single call importing all 63 live bindings exceeded what - /// one block admits on Paseo Asset Hub Next: estimation died mid-loop around the 44th - /// store, at any gas limit, with the weight exhaustion surfacing as an empty revert. - /// Nothing off-chain models that ceiling, so the import is sized to stay far under it - /// instead of proven against it. - /// - /// Each binding is checked back through `getLabelStore` before it is written. A store's - /// `owner` is the user it was deployed for, and the factory's mapping is the authority - /// on that pairing; requiring the two to agree rejects a store whose owner has been - /// changed out from under the mapping, which is the one shape that would bind a user to - /// a store the old factory does not consider theirs. - /// - /// A pair this factory already holds is skipped, so an interrupted import is finished by - /// running the same pages again and a page replayed whole is a no-op. Rebinding stays - /// impossible: a user bound to a DIFFERENT store is rejected, because bindings are - /// permanent here as everywhere else in the factory. - /// - /// The page that reaches the end of the list asserts the count equality; until then the - /// factory legitimately holds a prefix and no key points at it. - /// - /// `UserStore` bindings are deliberately not imported. They are claimed by users - /// themselves and the network being migrated from has none; a migration that does have - /// them needs this extended, not reused. - /// @param oldFactory Factory whose bindings are being adopted. - /// @param offset Index into the old factory's store list where this page starts. - /// @param limit Maximum number of bindings this page carries. Must be non-zero. - function importStores(address oldFactory, uint256 offset, uint256 limit) external onlyOwner { - require(oldFactory != address(0), InvalidUser(oldFactory)); - require(limit != 0, ImportPageEmpty()); - - uint256 total = IStoreFactory(oldFactory).getLabelStoreCount(); - address[] memory stores = IStoreFactory(oldFactory).getLabelStores(offset, limit); - // The shipped pagination truncates at the end of the list, so a short page is only legal - // on the last one. Anything else is a truncated read, and importing it would leave a gap - // the closing count check could not attribute. - uint256 expected = offset >= total ? 0 : (total - offset < limit ? total - offset : limit); - require(stores.length == expected, ImportCountMismatch(expected, stores.length)); - - for (uint256 i; i < stores.length; ++i) { - address store = stores[i]; - require(store != address(0), InvalidImplementation(store)); - - address user = IDotnsStore(store).owner(); - require(user != address(0), InvalidUser(user)); - require( - IStoreFactory(oldFactory).getLabelStore(user) == store, - ImportBindingMismatch(user, store) - ); - - address existing = _labelStores[user]; - if (existing == store) { - // Carried by an earlier run of this same page. Skipping it is what makes an - // interrupted import finishable by replaying pages. - continue; - } - require(existing == address(0), AlreadyDeployed(user, existing)); - - _labelStores[user] = store; - _labelStoreList.push(store); - - emit StoresImported(user, store); - } - - // The page that reaches the end of the list closes the books: every binding the old - // factory reports must now be held here, once. A mismatch means a store appeared in the - // list twice, or the list disagreed with the mapping. - if (offset + stores.length == total) { - require( - _labelStoreList.length == total, ImportCountMismatch(total, _labelStoreList.length) - ); - } - } - - /// @notice Shared pagination helper used by `getLabelStores` and `getUserStores`. - /// @dev Single canonical slicer so both enumerations bound-check and copy identically. - /// @param source Storage array to slice. - /// @param offset Start index. - /// @param limit Maximum entries to return. - /// @return slice Result slice; empty when `offset >= source.length`. - function _paginateAddresses( - address[] storage source, - uint256 offset, - uint256 limit - ) - internal - view - returns (address[] memory slice) - { - uint256 total = source.length; - if (offset >= total) return new address[](0); - - uint256 available = total - offset; - uint256 count = limit < available ? limit : available; - - slice = new address[](count); - for (uint256 i; i < count; ++i) { - slice[i] = source[offset + i]; - } - } -} diff --git a/contracts/utils/DotnsConstantsOld.sol b/contracts/utils/DotnsConstantsOld.sol deleted file mode 100644 index e6bd98606..000000000 --- a/contracts/utils/DotnsConstantsOld.sol +++ /dev/null @@ -1,192 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.34; - -/// @title DotNS Constants -/// @notice Protocol-level invariants shared across DotNS contracts. -/// @dev Centralises the well-known protocol-registry keys that every contract uses to discover -/// its siblings (registrar, controller, registry, resolvers, etc.). Each key is a -/// role address resolved at call time, so rotating an implementation is a -/// single `set` on the protocol registry without redeploying consumers. The TLD is not a -/// constant here: it is set per network on the protocol registry and read by every consumer. -/// @custom:security-contact admin@parity.io -library DotnsConstantsOld { - /// @notice Address of revive's System precompile, exposed by every revive runtime - /// that opts the precompile in. - /// @dev Mirrors the upstream `SYSTEM_ADDR` constant in - /// `substrate/frame/revive/uapi/sol/ISystem.sol`. Consumed by - /// `DotnsPopControllerOld` and `DotnsNameWhitelistOld` to authenticate - /// Root-origin dispatches via `ISystem.originIsRoot()`. - address internal constant REVIVE_SYSTEM = address(0x0900); - - /// @notice Address of the Proof-of-Personhood precompile backed by the - /// alias-accounts pallet on Asset Hub. - /// @dev Consumed by `PopRulesOld` to read each account's personhood tier - /// (`None` / `Lite` / `Full`) and the dotns-scoped `contextAlias`. - address internal constant PERSONHOOD = address(0x000000000000000000000000000000000a010000); - - /// @notice Application identifier passed to @custom:function IPersonhood.personhoodStatus. - /// @dev Fixed per project so the same person receives a stable, dotns-only - /// `contextAlias` and no cross-application linkability is exposed. - /// forge-lint: disable-next-line(unsafe-typecast) - bytes32 internal constant PERSONHOOD_CONTEXT = bytes32("dotns"); - - /// @notice Launch deposit passed into the `DotnsFlatPricing` constructor. - /// @dev 10 DOT under revive's 18-decimal Asset Hub convention. A new amount is a fresh model - /// deployment registered under @custom:constant COST_MODEL, so this constant seeds the - /// model rather than being read afterwards. Single source of truth for deploy scripts and - /// tests so the seed cannot drift between call sites. - uint256 internal constant BASE_DEPOSIT = 10 ether; - - /// @notice Price floor F passed into the `DotnsScarcityPricing` candidate's constructor. - /// @dev Below `BASE_DEPOSIT` so that curve falls above nine characters. Seeds the candidate - /// constructor; a new floor is a fresh model deployment. - uint256 internal constant MIN_PRICE = 0.1 ether; - - /// @notice Well-known key for the cost model pricing registrations by base length. - /// @dev Role: single authority for the wei amount a registration costs. `PopRulesOld` resolves - /// it here on every pricing read, so swapping the model is one `set` on the protocol - /// registry without redeploying `PopRulesOld` or its consumers. - /// forge-lint: disable-next-line(unsafe-typecast) - bytes32 internal constant COST_MODEL = bytes32("costModel"); - - /// @notice Default release cooldown seeded on `DotnsNameEscrowOld.initialize`. - /// @dev Single source of truth for deploy scripts and tests so the value cannot drift between - /// call sites. Bounded on-chain by `DotnsNameEscrowOld.MAX_COOLDOWN`. Live deployments - /// rotate the runtime value via `updateCooldown` rather than rebuilding consumers. - uint256 internal constant ESCROW_COOLDOWN = 15 minutes; - - /// @notice Default redeem window seeded on `DotnsNameEscrowOld.initialize`. - /// @dev Single source of truth for deploy scripts and tests. Bounded on-chain by - /// `DotnsNameEscrowOld.MAX_REDEEM_WINDOW`. Live deployments rotate the runtime value via - /// `updateRedeemWindow`. - uint256 internal constant ESCROW_REDEEM_WINDOW = 1 days; - /// @notice Maximum entries a paginated view returns in a single page. - /// @dev Shared ceiling for paginated reads: a view clamps its returned array to this figure, - /// and callers page through larger sets with `offset`. - uint256 internal constant MAX_PAGE_SIZE = 200; - - /// @notice Default per-name live-claim cap the name whitelist starts with. - /// @dev Governance retunes it on the whitelist within `WHITELIST_MAX_CLAIMANTS_LIMIT`. - uint16 internal constant WHITELIST_DEFAULT_MAX_CLAIMANTS = 64; - - /// @notice Default claim-reason byte cap the name whitelist starts with. - /// @dev Governance retunes it on the whitelist within `WHITELIST_MAX_REASON_LIMIT`. - uint256 internal constant WHITELIST_DEFAULT_MAX_REASON_BYTES = 256; - - /// @notice Upper bound on the whitelist live-claim cap. Caps the claim clear-loop below the - /// block gas limit. - uint16 internal constant WHITELIST_MAX_CLAIMANTS_LIMIT = 128; - - /// @notice Upper bound on the whitelist reason byte cap. - uint256 internal constant WHITELIST_MAX_REASON_LIMIT = 256; - - /// @notice Default cap on labels granted in one `grantNames` call. - /// @dev Governance retunes it on the whitelist within `WHITELIST_MAX_GRANT_BATCH_LIMIT`. - uint16 internal constant WHITELIST_DEFAULT_MAX_GRANT_BATCH = 100; - - /// @notice Upper bound on the `grantNames` batch cap. Bounds one call below the block gas - /// limit. - uint16 internal constant WHITELIST_MAX_GRANT_BATCH_LIMIT = 256; - - /// @notice Well-known key for the ERC721 registrar backing name ownership. - /// @dev Role: token-of-record for registered names. Mints, burns, and tracks the - /// `tokenId => label` mapping consumed by the forward registry on - /// transfer. - /// forge-lint: disable-next-line(unsafe-typecast) - bytes32 internal constant REGISTRAR = bytes32("registrar"); - - /// @notice Well-known key for the registrar controller orchestrating commit-reveal - /// registration. @dev Role: commit-reveal entry point for the public registration flow. - /// Calls `register` on the registrar after pricing and validation. - /// forge-lint: disable-next-line(unsafe-typecast) - bytes32 internal constant CONTROLLER = bytes32("controller"); - - /// @notice Well-known key for the forward registry storing node ownership and resolver. - /// @dev Role: source of truth for `(node => owner, resolver)`. Read by every - /// resolver gate that defers authority to the node owner. - /// forge-lint: disable-next-line(unsafe-typecast) - bytes32 internal constant REGISTRY = bytes32("registry"); - - /// @notice Well-known key for the reverse resolver for address-to-name mapping. - /// @dev Role: stores `address => name` reverse records. Writer is the - /// registrar/controller, not the address holder. - /// forge-lint: disable-next-line(unsafe-typecast) - bytes32 internal constant REVERSE_RESOLVER = bytes32("reverseResolver"); - - /// @notice Well-known key for the PoP oracle enforcing eligibility and pricing. - /// @dev Role: arbiter of PoP cross-flow priority and pricing. Consulted by - /// both the public commit-reveal controller and the PoP controller. - /// forge-lint: disable-next-line(unsafe-typecast) - bytes32 internal constant POP_RULES = bytes32("popRules"); - - /// @notice Well-known key for the factory deploying per-user Store instances. - /// @dev Role: deploy-on-demand provisioning of user `LabelStore` proxies and - /// authorisation gate for protocol writes into them. - /// forge-lint: disable-next-line(unsafe-typecast) - bytes32 internal constant STORE_FACTORY = bytes32("storeFactory"); - - /// @notice Well-known key for the forward resolver storing address records. - /// @dev Role: `node => address` records. Writes gated on node ownership. - /// forge-lint: disable-next-line(unsafe-typecast) - bytes32 internal constant RESOLVER = bytes32("resolver"); - - /// @notice Well-known key for the content resolver storing content hashes and text records. - /// @dev Role: `node => contenthash`/`text` records and ERC721-style operator - /// approvals. Writes gated on node ownership or operator approval. - /// forge-lint: disable-next-line(unsafe-typecast) - bytes32 internal constant CONTENT_RESOLVER = bytes32("contentResolver"); - - /// @notice Well-known key for the dedicated PoP controller orchestrating lite/full-person - /// username issuance on behalf of the PoP gateway. - /// @dev Kept distinct from `CONTROLLER` (commit-reveal public controller) so the - /// two can coexist per `DotnsRegistrarOld`'s multi-controller affordance. - /// forge-lint: disable-next-line(unsafe-typecast) - bytes32 internal constant POP_CONTROLLER = bytes32("popController"); - - /// @notice Well-known key for the PoP resolver holding per-name records produced - /// by the PoP username flow (chat keys, lite => full links). - /// @dev Role: `node => chatKey` and bidirectional `lite <=> full` link index. - /// Writer is the `POP_CONTROLLER`, not the node owner. - /// forge-lint: disable-next-line(unsafe-typecast) - bytes32 internal constant POP_RESOLVER = bytes32("popResolver"); - - /// @notice Well-known key for the read-only lens over PoP identity data. - /// @dev Role: off-chain query surface. Composes the account name listings, the per-name - /// record, and the account summary from the controller, registrar, store factory, PoP - /// resolver, and PopRulesOld. Holds no authority and is consumed by clients, not by other - /// contracts. - /// forge-lint: disable-next-line(unsafe-typecast) - bytes32 internal constant POP_LENS = bytes32("popLens"); - - /// @notice Well-known key for the name escrow holding refundable deposits and - /// driving the release lifecycle for registered names. - /// @dev Role: custodial vault for registration deposits and the state machine - /// that drives the name release lifecycle. - /// forge-lint: disable-next-line(unsafe-typecast) - bytes32 internal constant NAME_ESCROW = bytes32("nameEscrow"); - - /// @notice Well-known key for the generic Multicall3 batching helper. - /// @dev Role: unauthorised arbitrary-target multicall utility used by - /// clients and tooling. Target contracts still enforce their own - /// permissions and observe Multicall3 as `msg.sender`. - /// forge-lint: disable-next-line(unsafe-typecast) - bytes32 internal constant MULTICALL3 = bytes32("multicall3"); - - /// @notice Well-known key for the CREATE3 factory backing the deterministic - /// deploy pipeline. - /// @dev Role: permissionless CREATE3 deployer. The first deploy stage - /// bootstraps the factory, records it under this key, and every later - /// stage resolves it from here, so deterministic addresses never depend - /// on an environment variable. - /// forge-lint: disable-next-line(unsafe-typecast) - bytes32 internal constant CREATE3_FACTORY = bytes32("create3Factory"); - - /// @notice Well-known key for the pre-launch name whitelist that binds a label to the - /// one address permitted to register it. - /// @dev Role: authority for label-bound registration grants. The public controller resolves - /// it here and reads it on the reserved path, requiring a grant naming the intended owner - /// unless the dispatch is Root, and consuming the grant on a successful mint. The PoP - /// controller does not consult it. - /// forge-lint: disable-next-line(unsafe-typecast) - bytes32 internal constant NAME_WHITELIST = bytes32("nameWhitelist"); -} diff --git a/contracts/utils/RegistrationUtilsOld.sol b/contracts/utils/RegistrationUtilsOld.sol deleted file mode 100644 index cd9760192..000000000 --- a/contracts/utils/RegistrationUtilsOld.sol +++ /dev/null @@ -1,97 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.34; - -import {IDotnsRegistrarOld} from "../registrars/IDotnsRegistrarOld.sol"; -import {IDotnsRegistryOld} from "../registry/IDotnsRegistryOld.sol"; -import {IDotnsProtocolRegistryOld} from "../registry/IDotnsProtocolRegistryOld.sol"; -import {IStoreFactoryOld} from "../store/IStoreFactoryOld.sol"; -import {StoreUtilsOld} from "./StoreUtilsOld.sol"; -import {DotnsConstantsOld} from "./DotnsConstantsOld.sol"; - -/// @title DotNS Registration Utilities Library -/// @notice Single canonical implementation of the "mint + forward-registry + store-write" -/// triad used by every DotNS registration flow. -/// @dev Exists so that every controller (public commit-reveal, PoP gateway, future -/// privileged flows) calls the same sequence. Without this library each controller -/// re-implements the sequence, and the implementations drift. -/// @dev Scope: this library is deliberately minimal. It only performs the steps -/// that every registration flow needs regardless of policy: -/// 1. Mint the ERC721 name token on the base registrar. -/// 2. Write the forward registry entry (node => owner + default resolver). -/// 3. Resolve the per-user `LabelStore` (deploying on demand) so the caller -/// can pass it back. The registrar writes the labels-only store entry -/// internally. -/// Flow-specific concerns (pricing, reverse-record setting, chat-key persistence, -/// reservation queue mutation) stay inside the calling controller. -/// @custom:security-contact admin@parity.io -library RegistrationUtilsOld { - using StoreUtilsOld for IStoreFactoryOld; - - /// @notice Inputs describing a single name registration. - /// @dev Passed as a struct so callers do not have to thread a growing positional - /// argument list, and so future additions (e.g. a subname parent node) can - /// be made additively without breaking call sites. - /// @param protocolRegistry The protocol-level address registry for sibling lookups. - /// @param user Address receiving the name. - /// @param label Human-readable label (without the TLD). - /// @param labelhash `keccak256(bytes(label))`. - /// @param node `namehash(tldNode, labelhash)`. - struct RegistrationContext { - IDotnsProtocolRegistryOld protocolRegistry; - address user; - string label; - bytes32 labelhash; - bytes32 node; - } - - /// @notice Resolved sibling contracts for a registration call. - /// @dev Held as a struct internally so the helper can pass a single value to the - /// downstream steps rather than three separate locals. Never returned to - /// callers; kept in memory for the lifetime of one `registerAndStore`. - struct Siblings { - IDotnsRegistrarOld registrar; - IDotnsRegistryOld registry; - IStoreFactoryOld storeFactory; - } - - /// @notice Performs the canonical mint + forward-registry + store-write sequence. - /// @dev Callable by any authorised controller. Emits no events; each controller - /// emits its own flow-level event after this returns, so behavioural drift - /// between flows stays contained at the emission layer rather than at the - /// underlying state-transition layer. Store authorisation is resolved against the - /// protocol registry on every write (@custom:function StoreAuth.isStoreWriter), so no - /// per-store allowlist bookkeeping is needed here. - /// @dev The registrar writes the `LabelStore` entry directly inside `register` - /// so this helper deliberately does not call `StoreUtilsOld.writeLabel`. - /// Doing it twice would deploy or touch the store on every flow and - /// could conflict with the registrar's locked-entry semantics. - /// @param context Registration inputs. See @custom:struct RegistrationContext. - /// @return labelStore The resolved or newly deployed `LabelStore` address for `context.user`. - function registerAndStore(RegistrationContext memory context) - internal - returns (address labelStore) - { - Siblings memory siblings = _resolveSiblings(context.protocolRegistry); - - siblings.registrar.register(uint256(context.node), context.user, context.label); - siblings.registry.setOwner(context.node, context.user); - - labelStore = siblings.storeFactory.getLabelStore(context.user); - } - - /// @notice Resolves sibling contracts via the protocol registry. - /// @dev Exists so that resolution is one round-trip through a single helper and - /// not duplicated inline at every call site. If protocol-registry key - /// conventions change, the change lands here. - function _resolveSiblings(IDotnsProtocolRegistryOld protocolRegistry) - private - view - returns (Siblings memory siblings) - { - siblings = Siblings({ - registrar: IDotnsRegistrarOld(protocolRegistry.get(DotnsConstantsOld.REGISTRAR)), - registry: IDotnsRegistryOld(protocolRegistry.get(DotnsConstantsOld.REGISTRY)), - storeFactory: IStoreFactoryOld(protocolRegistry.get(DotnsConstantsOld.STORE_FACTORY)) - }); - } -} diff --git a/contracts/utils/StoreUtilsOld.sol b/contracts/utils/StoreUtilsOld.sol deleted file mode 100644 index 2e1bbb3b1..000000000 --- a/contracts/utils/StoreUtilsOld.sol +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.34; - -import {ILabelStore} from "../store/ILabelStore.sol"; -import {IStoreFactoryOld} from "../store/IStoreFactoryOld.sol"; - -/// @title DotNS Store Utilities Library -/// @notice Canonical helpers for protocol writes into per-user `LabelStore` instances. -/// @dev One auth rule, one write path. Every DotNS consumer (controller, registrar, -/// registry, PoP controller) funnels label writes through `writeLabel` so -/// authorisation and deploy-on-first-use semantics are identical across flows. -/// @custom:security-contact admin@parity.io -library StoreUtilsOld { - /// @notice Returns the `LabelStore` for `user`, deploying one via the factory if absent. - /// @dev Deploy-on-demand: a user's store is created on their first protocol write so - /// unused accounts never pay the deployment cost. The deploy path is gated by the - /// factory, so callers that are not the factory owner and not a store writer - /// @custom:reverts NotAuthorised when a deployment is required. - /// @param factory The store factory. - /// @param user The user whose label store is being resolved. - /// @return store The resolved or newly deployed store address. - function ensureLabelStore( - IStoreFactoryOld factory, - address user - ) - internal - returns (address store) - { - store = factory.getLabelStore(user); - if (store == address(0)) { - store = factory.deployLabelStoreFor(user); - } - } - - /// @notice Writes `label` under `labelhash` for `user`, deploying their `LabelStore` if needed. - /// @dev Idempotent on locked entries: once a label is locked the call is a no-op rather - /// than a revert, so retried protocol flows (e.g. an ERC721 transfer back to a prior - /// owner) pass through without failing on the existing lock. Inherits the factory's - /// writer authorisation: callers that are not the factory owner and not - /// a store writer @custom:reverts NotAuthorised when the user has no store yet. - /// @param factory The store factory. - /// @param user The label store owner. - /// @param labelhash The labelhash key. - /// @param label The label string (typically the full name, e.g. "alice.dot"). - /// @return store The resolved or newly deployed store address. - function writeLabel( - IStoreFactoryOld factory, - address user, - bytes32 labelhash, - string memory label - ) - internal - returns (address store) - { - store = ensureLabelStore(factory, user); - if (!ILabelStore(store).isLocked(labelhash)) { - ILabelStore(store).storeLabel(labelhash, label); - } - } -} diff --git a/contracts/utils/StringUtilsOld.sol b/contracts/utils/StringUtilsOld.sol new file mode 100644 index 000000000..1ffafbe2e --- /dev/null +++ b/contracts/utils/StringUtilsOld.sol @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.34; + +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; + +/// @title String Utilities Library +/// @notice Provides string manipulation utilities for DotNS contracts. +/// @dev Extends OpenZeppelin's Strings library with additional UTF-8 and conversion helpers. +/// @custom:security-contact admin@parity.io +library StringUtilsOld { + using Strings for uint256; + using Strings for int256; + using Strings for address; + + /// @notice Number of digits in a lite-person PoP label's suffix. + /// @dev The count the gateway emits, and an exact requirement here: the separator sits at a + /// fixed offset from the end, so a one or three digit suffix is rejected. The gateway + /// reads its own minimum, so widening it there does not widen this. + uint256 internal constant LITE_SUFFIX_DIGITS = 2; + + /// @notice Maximum number of octets in a single DNS label. + /// @dev RFC 1035 caps each label at 63 octets. Enforced inside @custom:function _isDnsLabel so + /// every public validator (@custom:function isSingleLabel, @custom:function isNamePath, + /// @custom:function isLitePersonLabel) inherits the bound and oversized labels never + /// reach the registrar. A lite label bounds its stem rather than the whole string, so + /// it reaches `MAX_DNS_LABEL_OCTETS + LITE_SUFFIX_DIGITS + 1` octets. Its stem is + /// bounded here but checked in @custom:function _isLitePersonLabel, which is stricter + /// on charset than a DNS label: letters only. + uint256 internal constant MAX_DNS_LABEL_OCTETS = 63; + + /// @notice ASCII full stop separating a lite label's stem from its digit suffix. + /// @dev A lite label is the only label shape in DotNS that carries a separator; + /// @custom:function _isDnsLabel rejects it everywhere else. + bytes1 internal constant LABEL_SEPARATOR = 0x2e; + + /// @notice Computes the character length of a UTF-8 encoded string. + /// @dev Counts Unicode code points, not bytes. Handles multi-byte UTF-8 sequences: + /// - 1 byte: 0x00-0x7F (ASCII) + /// - 2 bytes: 0xC0-0xDF + /// - 3 bytes: 0xE0-0xEF + /// - 4 bytes: 0xF0-0xF7 + /// - 5 bytes: 0xF8-0xFB (rare, outside Unicode standard) + /// - 6 bytes: 0xFC-0xFD (rare, outside Unicode standard) + /// @param value The UTF-8 encoded string to measure. + /// @return len The number of Unicode characters in the string. + function strlen(string memory value) internal pure returns (uint256 len) { + uint256 i = 0; + uint256 bytelength = bytes(value).length; + for (len = 0; i < bytelength; len++) { + bytes1 byteValue = bytes(value)[i]; + if (byteValue < 0x80) i += 1; + else if (byteValue < 0xE0) i += 2; + else if (byteValue < 0xF0) i += 3; + else if (byteValue < 0xF8) i += 4; + else if (byteValue < 0xFC) i += 5; + else i += 6; + } + } + + /// @notice Validates that `s` is a single canonical DNS label. + /// @dev Lowercase ASCII letters, digits, and hyphen only; hyphen may not be the first or last + /// character; length must be in `(0, MAX_DNS_LABEL_OCTETS]`. No dots allowed; use + /// @custom:function isNamePath for dotted forms. Mirrors the label rules enforced at the + /// registrar. + /// @param value Candidate label. + /// @return isValid True if `value` is a canonical DNS label. + function isSingleLabel(string calldata value) internal pure returns (bool isValid) { + bytes memory label = bytes(value); + return _isDnsLabel(label, 0, label.length); + } + + /// @notice Memory-location helper for @custom:function isSingleLabel, used where the + /// candidate label is produced by an upstream string transformation (e.g. the output of + /// @custom:function stripDigits) so callers do not need a calldata round-trip. + /// @param value Candidate label held in memory. + /// @return isValid True if `value` is a canonical DNS label. + function isSingleLabelMemory(string memory value) internal pure returns (bool isValid) { + bytes memory label = bytes(value); + return _isDnsLabel(label, 0, label.length); + } + + /// @notice Validates the lite-person PoP label format: `.`. + /// @dev A lite-person label is a stem of lowercase ASCII letters, one + /// @custom:constant LABEL_SEPARATOR, then exactly + /// @custom:constant LITE_SUFFIX_DIGITS digits (e.g. `joseph.42`). Letters only, + /// because the stem is the name a person chose, which is restricted to letters. How short + /// a stem may be is policy rather than format, so it is left to the governance-reserved band in + /// @custom:function IPopRulesOld.classifyName. + /// It is the only label shape in DotNS permitted to carry a separator, which is what + /// reserves the dotted space to the gateway. A digit suffix is not exclusive: an + /// ordinary label may end in digits, but it is measured as written and so classifies by + /// its full length. + /// Cross-flow priority is arbitrated on the stem, not on the whole label: a lite + /// label's stem is reserved as a base name through + /// @custom:function IPopRulesOld.reserveBaseNameForPop, so `joseph.42` contends with + /// `joseph`. There is no flat spelling of a lite label for it to contend with. + /// @param value Candidate label. + /// @return isValid True if `value` is a stem of lowercase ASCII letters followed by a + /// separator and exactly @custom:constant LITE_SUFFIX_DIGITS digits. + function isLitePersonLabel(string calldata value) internal pure returns (bool isValid) { + return _isLitePersonLabel(bytes(value)); + } + + /// @notice Memory-location helper for @custom:function isLitePersonLabel. + /// @dev For callers holding the label in memory rather than calldata: the controller reads + /// it back from a struct before deriving the node, and the lens reads it out of a + /// store. Same predicate, different data location. + /// @param value Candidate label held in memory. + /// @return isValid True if `value` is a stem of lowercase ASCII letters followed by a + /// separator and exactly @custom:constant LITE_SUFFIX_DIGITS digits. + function isLitePersonLabelMemory(string memory value) internal pure returns (bool isValid) { + return _isLitePersonLabel(bytes(value)); + } + + function _isLitePersonLabel(bytes memory raw) private pure returns (bool isValid) { + uint256 length = raw.length; + // One stem letter, the separator, then the digits is the shortest accepted shape. The + // stem is not bounded below here: how short a name may be is policy, and PopRulesOld + // already holds it as the governance-reserved band. Enforcing a minimum here would + // duplicate the governance-reserved band and drift from it. + if (length < LITE_SUFFIX_DIGITS + 2) return false; + + // Fixing the separator's position is what enforces the exact digit count: a third + // digit, a missing separator and a trailing separator all land a non-separator byte + // here. The letters-only stem then admits no second separator, so exactly one is + // possible without scanning for it. + uint256 separator = length - LITE_SUFFIX_DIGITS - 1; + if (raw[separator] != LABEL_SEPARATOR) return false; + + // The stem is a name a person chose, so it follows the same letters-only rule as a + // full-person label. Uppercase is excluded for the reason it is everywhere else here: + // a single name must have a single spelling, and so a single node. + if (!_isPersonLabel(raw, 0, separator)) return false; + + for (uint256 i = separator + 1; i < length; ++i) { + bytes1 char = raw[i]; + if (char < bytes1(0x30) || char > bytes1(0x39)) return false; + } + + return true; + } + + /// @notice Validates that `value` is a name a person chose: lowercase ASCII letters only. + /// @dev Matches the gateway's full-person label rule, which admits no digits + /// and no hyphens, so a label outside this shape cannot have been issued. Stricter + /// than @custom:function isSingleLabel, and it is the same rule + /// @custom:function isLitePersonLabel applies to a lite stem. How short a name may be + /// is policy rather than format, so no floor is applied here. + /// @param value Candidate label. + /// @return isValid True if every octet of `value` is a lowercase ASCII letter. + function isPersonLabel(string calldata value) internal pure returns (bool isValid) { + bytes memory raw = bytes(value); + return _isPersonLabel(raw, 0, raw.length); + } + + function _isPersonLabel( + bytes memory raw, + uint256 start, + uint256 end + ) + private + pure + returns (bool isValid) + { + if (end <= start) return false; + if (end - start > MAX_DNS_LABEL_OCTETS) return false; + + for (uint256 i = start; i < end; ++i) { + bytes1 char = raw[i]; + if (char < bytes1(0x61) || char > bytes1(0x7a)) return false; + } + + return true; + } + + /// @notice Splits a lite-person label `.` into its stem and digit suffix. + /// @dev Splits at the first @custom:constant LABEL_SEPARATOR. A lite label carries exactly one + /// separator, so the caller is expected to have run @custom:function + /// isLitePersonLabelMemory first; a label with no separator returns the whole input as the stem + /// and an empty + /// suffix, which the caller's later label checks reject. + /// @param value Lite label held in memory, for example `alice.01`. + /// @return stem The label before the separator, for example `alice`. + /// @return suffix The digit suffix after the separator, for example `01`. + function splitLiteLabel(string memory value) + internal + pure + returns (string memory stem, string memory suffix) + { + bytes memory raw = bytes(value); + uint256 length = raw.length; + + uint256 separator = length; + for (uint256 i; i < length; ++i) { + if (raw[i] == LABEL_SEPARATOR) { + separator = i; + break; + } + } + + bytes memory stemBytes = new bytes(separator); + for (uint256 i; i < separator; ++i) { + stemBytes[i] = raw[i]; + } + + uint256 suffixLength = separator == length ? 0 : length - separator - 1; + bytes memory suffixBytes = new bytes(suffixLength); + for (uint256 i; i < suffixLength; ++i) { + suffixBytes[i] = raw[separator + 1 + i]; + } + + stem = string(stemBytes); + suffix = string(suffixBytes); + } + + /// @notice Validates that `s` is a dot-separated path of canonical DNS labels. + /// @dev Each segment between dots must satisfy @custom:function isSingleLabel. Empty + /// segments (leading, trailing, or consecutive dots) fail. Used when + /// callers submit multi-label paths (e.g. `alice.dot`) rather than + /// bare labels. + /// @param value Candidate name path. + /// @return isValid True if every dot-separated segment is a canonical DNS label. + function isNamePath(string calldata value) internal pure returns (bool isValid) { + bytes memory path = bytes(value); + uint256 length = path.length; + if (length == 0) return false; + + uint256 start; + for (uint256 i = 0; i < length; ++i) { + if (path[i] != bytes1(0x2e)) continue; + if (!_isDnsLabel(path, start, i)) return false; + start = i + 1; + } + + return _isDnsLabel(path, start, length); + } + + function _isDnsLabel( + bytes memory label, + uint256 start, + uint256 end + ) + private + pure + returns (bool isValid) + { + if (end <= start) return false; + if (end - start > MAX_DNS_LABEL_OCTETS) return false; + if (label[start] == bytes1(0x2d) || label[end - 1] == bytes1(0x2d)) { + return false; + } + + for (uint256 i = start; i < end; ++i) { + bytes1 char = label[i]; + bool isLowercase = char >= 0x61 && char <= 0x7a; + bool isDigit = char >= 0x30 && char <= 0x39; + if (!(isLowercase || isDigit || char == bytes1(0x2d))) return false; + } + + return true; + } + + /// @notice Converts a uint256 to its decimal string representation. + /// @dev Wraps OpenZeppelin's Strings.toString(). + /// @param value The unsigned integer to convert. + /// @return The decimal string representation. + function uintToString(uint256 value) internal pure returns (string memory) { + return value.toString(); + } + + /// @notice Converts an address to its checksummed hexadecimal string representation. + /// @dev Wraps OpenZeppelin's Strings.toHexString(). Returns lowercase hex with "0x" prefix. + /// @param account The address to convert. + /// @return The hexadecimal string representation (42 characters including "0x"). + function addressToHex(address account) internal pure returns (string memory) { + return account.toHexString(); + } + + /// @notice Converts a bytes32 value to a string, treating it as a null-terminated ASCII string. + /// @dev Reads bytes until the first null byte (0x00) or end of bytes32. + /// Useful for converting short strings stored in bytes32 back to string type. + /// @param _bytes32 The bytes32 value containing a null-terminated ASCII string. + /// @return The extracted string (up to 32 characters). + function bytes32ToString(bytes32 _bytes32) internal pure returns (string memory) { + uint8 i = 0; + while (i < 32 && _bytes32[i] != 0) { + i++; + } + bytes memory bytesArray = new bytes(i); + for (uint8 j = 0; j < i; j++) { + bytesArray[j] = _bytes32[j]; + } + return string(bytesArray); + } +} diff --git a/contracts/utils/SubnodeUtilsOld.sol b/contracts/utils/SubnodeUtilsOld.sol index 2f380731d..73a9a9394 100644 --- a/contracts/utils/SubnodeUtilsOld.sol +++ b/contracts/utils/SubnodeUtilsOld.sol @@ -1,25 +1,24 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.34; -import {IDotnsRegistryOld} from "../registry/IDotnsRegistryOld.sol"; -import {IDotnsRegistrarOld} from "../registrars/IDotnsRegistrarOld.sol"; -import {IDotnsProtocolRegistryOld} from "../registry/IDotnsProtocolRegistryOld.sol"; +import {IDotnsRegistry} from "../registry/IDotnsRegistry.sol"; +import {IDotnsRegistrar} from "../registrars/IDotnsRegistrar.sol"; +import {IDotnsProtocolRegistry} from "../registry/IDotnsProtocolRegistry.sol"; import {LabelUtils} from "./LabelUtils.sol"; -import {StringUtils} from "./StringUtils.sol"; -import {DotnsConstantsOld} from "./DotnsConstantsOld.sol"; +import {StringUtilsOld} from "./StringUtilsOld.sol"; +import {DotnsConstants} from "./DotnsConstants.sol"; /// @title DotNS Subnode Utilities Library /// @notice General-purpose helpers for registering names that live as subnodes of another name, /// rather than as tokenised second-level registrations. /// @dev A subname has no token: its ownership lives in the registry record, not in the registrar's -/// ERC-721 ledger. So it is registered through @custom:function -/// IDotnsRegistryOld.setSubnodeOwner here, rather than through the tokenised mint triad of -/// @custom:contract RegistrationUtilsOld. +/// ERC-721 ledger. So it is registered through @custom:function IDotnsRegistry.setSubnodeOwner +/// here, rather than through the tokenised mint triad of @custom:contract RegistrationUtils. /// @custom:security-contact admin@parity.io library SubnodeUtilsOld { /// @notice Inputs describing a single subname registration. /// @dev Passed as a struct so call sites name each field rather than thread a positional - /// argument list, mirroring @custom:struct RegistrationUtilsOld.RegistrationContext. + /// argument list, mirroring @custom:struct RegistrationUtils.RegistrationContext. /// @param protocolRegistry Protocol-level address registry used to resolve the registry and /// TLD. /// @param parentLabel Second-level parent label, e.g. `01`. @@ -27,7 +26,7 @@ library SubnodeUtilsOld { /// @param owner Address to record as the subname owner. /// @param persist Whether the registry should index the subnode into the owner's `LabelStore`. struct SubnameContext { - IDotnsProtocolRegistryOld protocolRegistry; + IDotnsProtocolRegistry protocolRegistry; string parentLabel; string subLabel; address owner; @@ -60,7 +59,7 @@ library SubnodeUtilsOld { /// separator first. /// @dev The single place a lite label is turned into a node, shared by the write path and every /// reader of a lite name so the issuer and its readers agree on where a lite name lives. - /// Callers gate on @custom:function StringUtils.isLitePersonLabelMemory beforehand, so the + /// Callers gate on @custom:function StringUtilsOld.isLitePersonLabelMemory beforehand, so the /// label is known to carry the separator this splits on. /// @param tldNode The TLD node. /// @param liteLabel Lite label, e.g. `alice.01`. @@ -73,7 +72,7 @@ library SubnodeUtilsOld { pure returns (bytes32 subnode) { - (string memory stem, string memory suffix) = StringUtils.splitLiteLabel(liteLabel); + (string memory stem, string memory suffix) = StringUtilsOld.splitLiteLabel(liteLabel); subnode = subnodeOf(tldNode, suffix, stem); } @@ -81,13 +80,13 @@ library SubnodeUtilsOld { /// if it does not exist yet. /// @dev Derives the parent node `parentLabel.tld`; when no name is registered there yet it is /// minted through the registrar with the calling contract as owner, so the caller holds - /// the parent authority @custom:function IDotnsRegistryOld.setSubnodeOwner requires. The + /// the parent authority @custom:function IDotnsRegistry.setSubnodeOwner requires. The /// calling contract must therefore be a registrar controller, otherwise the registrar /// @custom:reverts NotController. When a name already exists at the parent it must be /// owned by the caller, otherwise @custom:reverts NotAuthorised, so a name someone else holds /// is /// never treated as the caller's parent. Ownership of the subname is then recorded through - /// @custom:function IDotnsRegistryOld.setSubnodeOwner. `persist` is forwarded to the + /// @custom:function IDotnsRegistry.setSubnodeOwner. `persist` is forwarded to the /// registry: when false the ownership and resolver record is written but the owner's /// `LabelStore` is /// not, and the caller writes the label into the store separately. @@ -101,16 +100,14 @@ library SubnodeUtilsOld { /// @param context Subname registration inputs. See @custom:struct SubnameContext. /// @return subnode Namehash of the registered subname. function registerSubname(SubnameContext memory context) internal returns (bytes32 subnode) { - IDotnsProtocolRegistryOld protocolRegistry = context.protocolRegistry; + IDotnsProtocolRegistry protocolRegistry = context.protocolRegistry; bytes32 parentNode = LabelUtils.namehashUnder( protocolRegistry.tldNode(), LabelUtils.labelhashMemory(context.parentLabel) ); - IDotnsRegistrarOld registrar = - IDotnsRegistrarOld(protocolRegistry.get(DotnsConstantsOld.REGISTRAR)); - IDotnsRegistryOld registry = - IDotnsRegistryOld(protocolRegistry.get(DotnsConstantsOld.REGISTRY)); + IDotnsRegistrar registrar = IDotnsRegistrar(protocolRegistry.get(DotnsConstants.REGISTRAR)); + IDotnsRegistry registry = IDotnsRegistry(protocolRegistry.get(DotnsConstants.REGISTRY)); // Mint the parent on first use, owned by the caller, and pass an empty label so no // `LabelStore` is written for it. When it already exists it must belong to the caller, @@ -121,11 +118,11 @@ library SubnodeUtilsOld { registrar.register(uint256(parentNode), address(this), ""); registry.setOwner(parentNode, address(this)); } else { - require(registry.owner(parentNode) == address(this), IDotnsRegistryOld.NotAuthorised()); + require(registry.owner(parentNode) == address(this), IDotnsRegistry.NotAuthorised()); } subnode = registry.setSubnodeOwner( - IDotnsRegistryOld.SubnodeRecord({ + IDotnsRegistry.SubnodeRecord({ parentNode: parentNode, subLabel: context.subLabel, parentLabel: context.parentLabel, diff --git a/contracts/utils/SystemUtilsOld.sol b/contracts/utils/SystemUtilsOld.sol deleted file mode 100644 index 131e6d3ff..000000000 --- a/contracts/utils/SystemUtilsOld.sol +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.34; - -import {ISystem} from "../external/revive/ISystem.sol"; -import {DotnsConstantsOld} from "./DotnsConstantsOld.sol"; - -/// @title SystemUtilsOld -/// @notice Shared access to revive's System precompile for DotNS contracts. -/// @dev Canonical wrapper around `ISystem` at `DotnsConstantsOld.REVIVE_SYSTEM`, so the precompile -/// address and interface are wired in one place rather than duplicated per consumer. -/// @custom:security-contact admin@parity.io -library SystemUtilsOld { - /// @notice Returns whether the transaction-level origin is substrate Root. - /// @dev Reads the stack origin through `ISystem.originIsRoot`, which holds through a UUPS - /// proxy's delegatecall frame where `callerIsRoot` returns false, and returns false - /// rather than reverting on a non-Root origin. - /// @return root True when the transaction origin is Root. - function originIsRoot() internal view returns (bool root) { - return ISystem(DotnsConstantsOld.REVIVE_SYSTEM).originIsRoot(); - } -} diff --git a/contracts/whitelist/DotnsNameWhitelistOld.sol b/contracts/whitelist/DotnsNameWhitelistOld.sol deleted file mode 100644 index 3bcfa248d..000000000 --- a/contracts/whitelist/DotnsNameWhitelistOld.sol +++ /dev/null @@ -1,512 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.34; - -import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import { - OwnableUpgradeable -} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; -import { - ERC165Upgradeable -} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; -import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; - -import {IDotnsNameWhitelist} from "./IDotnsNameWhitelist.sol"; -import {IDotnsProtocolRegistryOld} from "../registry/IDotnsProtocolRegistryOld.sol"; -import {LabelUtils} from "../utils/LabelUtils.sol"; -import {StringUtils} from "../utils/StringUtils.sol"; -import {DotnsConstantsOld} from "../utils/DotnsConstantsOld.sol"; -import {SystemUtilsOld} from "../utils/SystemUtilsOld.sol"; - -/// @title DotnsNameWhitelistOld -/// @notice Pre-launch name whitelist. A name is Open until governance reserves it or a claim is -/// accepted for it. Several beneficiaries may claim the same Open name, each with a -/// reason, and governance accepts one as the winner. -/// @dev Lives behind its own UUPS proxy with its own storage. Callers pass bare labels only; the -/// contract derives the node from the label and the TLD in the protocol registry, so a -/// caller cannot supply a mismatched hash. Claims are keyed by the beneficiary `user`, not -/// the submitter, so a relayer or a cross-chain sovereign account can submit on a user's -/// behalf and the name binds to that user. All state is on-chain and queryable through views; -/// no event indexing is required. A name holds at most `maxClaimants` live claims, which -/// bounds the loop that clears them on resolution. Resolving a name deletes its claims, -/// refunding their storage deposit, so only reserved or won names persist. The entire admin -/// surface is substrate Root: `SystemUtilsOld.originIsRoot` is true through the proxy's -/// delegatecall frame, and no gate reads `msg.sender`, so Root's lack of an address is not a -/// problem. No signed account grants, revokes, reserves, or retunes a cap; the owner's -/// authority is upgrade only. The public and PoP controllers hold only the `consume` hook. -/// Entries are keyed by the node under the active TLD, which the deployment holds immutable -/// for the whitelist's lifetime. -/// @custom:security-contact admin@parity.io -contract DotnsNameWhitelistOld is - Initializable, - UUPSUpgradeable, - OwnableUpgradeable, - ERC165Upgradeable, - IDotnsNameWhitelist -{ - using StringUtils for string; - using EnumerableSet for EnumerableSet.AddressSet; - using EnumerableSet for EnumerableSet.Bytes32Set; - - /// @notice One role's membership and its admin role. - /// @dev Member of the reserved AccessControl namespace, unused because gating is Root only. - /// @param hasRole Whether an account holds the role. - /// @param adminRole Admin role that manages the role. - struct RoleData { - mapping(address account => bool) hasRole; - bytes32 adminRole; - } - - /// @notice Reserved OpenZeppelin access-control namespace held at its ERC-7201 slot. - /// @dev Declared and left unused so the namespace stays present in the layout. Its slot derives - /// from the label, disjoint from the sequential slots below, so it consumes none of them. - /// @param _roles Role data keyed by role identifier. - /// @custom:storage-location erc7201:openzeppelin.storage.AccessControl - struct AccessControlStorage { - mapping(bytes32 role => RoleData) _roles; - } - - /// @notice Protocol-level address registry for all DotNS contracts. - IDotnsProtocolRegistryOld public protocolRegistry; - - /// @notice Live-claim cap per name, tunable by governance within - /// `DotnsConstantsOld.WHITELIST_MAX_CLAIMANTS_LIMIT`. - uint16 public maxClaimants; - - /// @notice Cap on labels per `grantNames` call, tunable by governance within - /// `DotnsConstantsOld.WHITELIST_MAX_GRANT_BATCH_LIMIT`. - uint16 public maxGrantBatch; - - /// @notice Reason byte cap, tunable by governance within - /// `DotnsConstantsOld.WHITELIST_MAX_REASON_LIMIT`. - uint256 public maxReasonBytes; - - /// @notice Resolved state per name. - mapping(bytes32 node => NameRecord record) private _names; - - /// @notice Claims per name, keyed by beneficiary. - mapping(bytes32 node => mapping(address user => Claim claim)) private _claims; - - /// @notice Beneficiaries with a live claim per name. - mapping(bytes32 node => EnumerableSet.AddressSet claimants) private _claimants; - - /// @notice Names holding reserved, claimed or claim-holding state, kept enumerable for review. - EnumerableSet.Bytes32Set private _activeNodes; - - /// @notice Timestamp requests start being accepted. - uint64 private _requestOpen; - - /// @notice Timestamp requests stop being accepted. - uint64 private _requestClose; - - /// @dev Reserved storage space to allow for layout changes in the future. - uint256[50] private __gap; - - /// @notice Restricts a call to a substrate Root dispatch. - /// @dev The whole admin surface is governance-only: no key grants, revokes, reserves, or - /// retunes a cap. The owner's authority is deployment and upgrade, not allocation, so no - /// signed account can hand out a name. `msg.sender` is never read here, which is also what - /// keeps every gated entry point callable under a Root origin, since Root has no account. - modifier onlyGovernance() { - _onlyGovernance(); - _; - } - - /// @notice Restricts a call to a registrar controller resolved through the registry. - modifier onlyController() { - require( - msg.sender == protocolRegistry.get(DotnsConstantsOld.CONTROLLER) - || msg.sender == protocolRegistry.get(DotnsConstantsOld.POP_CONTROLLER), - NotController(msg.sender) - ); - _; - } - - /// @notice Internal check enforcing the substrate Root gate. - function _onlyGovernance() internal view { - require(SystemUtilsOld.originIsRoot(), NotGovernance()); - } - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - _disableInitializers(); - } - - /// @notice Initialises the whitelist. - /// @dev Callable once through the UUPS proxy; direct calls on the implementation - /// @custom:reverts InvalidInitialization. Sets the deployer as owner and wires the - /// protocol registry the node derivation reads the TLD from. - /// @param registry Protocol registry all DotNS contracts resolve through. - function initialize(IDotnsProtocolRegistryOld registry) external initializer { - __ERC165_init(); - __Ownable_init(msg.sender); - protocolRegistry = registry; - maxClaimants = DotnsConstantsOld.WHITELIST_DEFAULT_MAX_CLAIMANTS; - maxGrantBatch = DotnsConstantsOld.WHITELIST_DEFAULT_MAX_GRANT_BATCH; - maxReasonBytes = DotnsConstantsOld.WHITELIST_DEFAULT_MAX_REASON_BYTES; - } - - /// @inheritdoc IDotnsNameWhitelist - function setMaxClaimants(uint16 newMax) external override onlyGovernance { - require( - newMax > 0 && newMax <= DotnsConstantsOld.WHITELIST_MAX_CLAIMANTS_LIMIT, - MaxClaimantsOutOfRange() - ); - maxClaimants = newMax; - emit MaxClaimantsSet(newMax); - } - - /// @inheritdoc IDotnsNameWhitelist - function setMaxReasonBytes(uint256 newMax) external override onlyGovernance { - require( - newMax > 0 && newMax <= DotnsConstantsOld.WHITELIST_MAX_REASON_LIMIT, - MaxReasonBytesOutOfRange() - ); - maxReasonBytes = newMax; - emit MaxReasonBytesSet(newMax); - } - - /// @inheritdoc IDotnsNameWhitelist - function setMaxGrantBatch(uint16 newMax) external override onlyGovernance { - require( - newMax > 0 && newMax <= DotnsConstantsOld.WHITELIST_MAX_GRANT_BATCH_LIMIT, - MaxGrantBatchOutOfRange() - ); - maxGrantBatch = newMax; - emit MaxGrantBatchSet(newMax); - } - - /// @inheritdoc IDotnsNameWhitelist - function requestName( - string calldata label, - string calldata reason, - address user - ) - external - override - { - require(_isWindowOpen(), WindowClosed()); - require(user != address(0), ZeroUser()); - require(bytes(reason).length <= maxReasonBytes, ReasonTooLong()); - require(label.isSingleLabel(), InvalidLabel()); - - bytes32 node = _nodeOf(label); - require(_names[node].status == NameStatus.Open, NameNotOpen(node)); - require(_claims[node][user].status == ClaimStatus.None, AlreadyClaimed(node, user)); - require(_claimants[node].length() < maxClaimants, TooManyClaimants(node)); - - _claims[node][user] = Claim({ - user: user, - status: ClaimStatus.Requested, - requestedAt: uint64(block.timestamp), - submitter: msg.sender, - reason: reason - }); - _claimants[node].add(user); - _activate(node, label); - emit NameRequested(node, user, label, reason); - } - - /// @inheritdoc IDotnsNameWhitelist - function accept(string calldata label, address user) external override onlyGovernance { - bytes32 node = _nodeOf(label); - require(_claims[node][user].status == ClaimStatus.Requested, NotRequested(node, user)); - emit NameAccepted(node, user, label); - _settle(node, user, label); - } - - /// @inheritdoc IDotnsNameWhitelist - function reject(string calldata label, address user) external override onlyGovernance { - bytes32 node = _nodeOf(label); - Claim storage claim = _claims[node][user]; - require(claim.status == ClaimStatus.Requested, NotRequested(node, user)); - // Free the claimant slot either way. Keep a sticky Rejected record only for a self-filed - // claim, so the beneficiary cannot re-request; a claim filed on their behalf is - // deleted and never binds them. - _claimants[node].remove(user); - if (claim.submitter == user) { - claim.status = ClaimStatus.Rejected; - } else { - delete _claims[node][user]; - } - emit NameRejected(node, user, label); - _deactivate(node); - } - - /// @inheritdoc IDotnsNameWhitelist - function grantName(string calldata label, address user) external override onlyGovernance { - _grant(label, user); - } - - /// @inheritdoc IDotnsNameWhitelist - function grantNames(string[] calldata labels, address user) external override onlyGovernance { - require(labels.length <= maxGrantBatch, TooManyLabels()); - for (uint256 i = 0; i < labels.length; i++) { - _grant(labels[i], user); - } - } - - /// @inheritdoc IDotnsNameWhitelist - function revokeName(string calldata label) external override onlyGovernance { - bytes32 node = _nodeOf(label); - NameRecord storage record = _names[node]; - require( - record.status == NameStatus.Claimed || _claimants[node].length() != 0, - NothingToRevoke(node) - ); - address winner = record.winner; - _clearClaimants(node, address(0), label); - record.status = NameStatus.Open; - record.winner = address(0); - emit NameRevoked(node, winner, label); - _deactivate(node); - } - - /// @inheritdoc IDotnsNameWhitelist - function setReserved(string calldata label, bool reserved) external override onlyGovernance { - require(label.isSingleLabel(), InvalidLabel()); - bytes32 node = _nodeOf(label); - NameRecord storage record = _names[node]; - if (reserved) { - require(record.status == NameStatus.Open, NameNotOpen(node)); - // Clear any pending claims the way a grant does, so a permissionless requestName - // cannot force governance to revokeName before it can reserve. - _clearClaimants(node, address(0), label); - record.status = NameStatus.Reserved; - _activate(node, label); - emit NameReserved(node, label); - } else { - require(record.status == NameStatus.Reserved, NotReserved(node)); - record.status = NameStatus.Open; - emit NameUnreserved(node, label); - _deactivate(node); - } - } - - /// @inheritdoc IDotnsNameWhitelist - function consume(string calldata label, address registrant) external override onlyController { - bytes32 node = _nodeOf(label); - NameRecord storage record = _names[node]; - require( - record.status == NameStatus.Claimed && record.winner == registrant, - NotWinner(registrant, node) - ); - record.status = NameStatus.Open; - record.winner = address(0); - emit NameConsumed(node, registrant, label); - _deactivate(node); - } - - /// @inheritdoc IDotnsNameWhitelist - function setWindow(uint64 startsIn, uint64 duration) external override onlyGovernance { - require(duration > 0, BadWindow()); - uint64 openAt = uint64(block.timestamp) + startsIn; - uint64 closeAt = openAt + duration; - _requestOpen = openAt; - _requestClose = closeAt; - emit WindowSet(openAt, closeAt); - } - - /// @inheritdoc IDotnsNameWhitelist - function statusOf(string calldata label) external view override returns (NameStatus status) { - return _names[_nodeOf(label)].status; - } - - /// @inheritdoc IDotnsNameWhitelist - function isReserved(string calldata label) external view override returns (bool reserved) { - return _names[_nodeOf(label)].status == NameStatus.Reserved; - } - - /// @inheritdoc IDotnsNameWhitelist - function granteeOf(string calldata label) external view override returns (address winner) { - NameRecord storage record = _names[_nodeOf(label)]; - return record.status == NameStatus.Claimed ? record.winner : address(0); - } - - /// @inheritdoc IDotnsNameWhitelist - function isGrantedTo( - string calldata label, - address account - ) - external - view - override - returns (bool granted) - { - NameRecord storage record = _names[_nodeOf(label)]; - return - account != address(0) && record.status == NameStatus.Claimed && record.winner == account; - } - - /// @inheritdoc IDotnsNameWhitelist - function claimOf( - string calldata label, - address user - ) - external - view - override - returns (Claim memory claim) - { - return _claims[_nodeOf(label)][user]; - } - - /// @inheritdoc IDotnsNameWhitelist - function claimantCount(string calldata label) external view override returns (uint256 count) { - return _claimants[_nodeOf(label)].length(); - } - - /// @inheritdoc IDotnsNameWhitelist - function claims( - string calldata label, - uint256 offset, - uint256 limit - ) - external - view - override - returns (Claim[] memory page) - { - bytes32 node = _nodeOf(label); - EnumerableSet.AddressSet storage set = _claimants[node]; - uint256 total = set.length(); - if (offset >= total) { - return new Claim[](0); - } - uint256 available = total - offset; - uint256 count = limit < available ? limit : available; - page = new Claim[](count); - for (uint256 i; i < count; ++i) { - page[i] = _claims[node][set.at(offset + i)]; - } - } - - /// @inheritdoc IDotnsNameWhitelist - function nameCount() external view override returns (uint256 count) { - return _activeNodes.length(); - } - - /// @inheritdoc IDotnsNameWhitelist - function names( - uint256 offset, - uint256 limit - ) - external - view - override - returns (NameView[] memory page) - { - uint256 total = _activeNodes.length(); - if (offset >= total) { - return new NameView[](0); - } - uint256 available = total - offset; - uint256 count = limit < available ? limit : available; - page = new NameView[](count); - for (uint256 i; i < count; ++i) { - bytes32 node = _activeNodes.at(offset + i); - NameRecord storage record = _names[node]; - page[i] = NameView({ - node: node, label: record.label, status: record.status, winner: record.winner - }); - } - } - - /// @inheritdoc IDotnsNameWhitelist - function window() external view override returns (uint64 openAt, uint64 closeAt) { - return (_requestOpen, _requestClose); - } - - /// @inheritdoc IDotnsNameWhitelist - function isWindowOpen() external view override returns (bool open) { - return _isWindowOpen(); - } - - /// @inheritdoc ERC165Upgradeable - function supportsInterface(bytes4 interfaceId) - public - view - override(ERC165Upgradeable) - returns (bool supported) - { - return interfaceId == type(IDotnsNameWhitelist).interfaceId - || super.supportsInterface(interfaceId); - } - - /// @notice Grants `label` to `user` directly, clearing any pending claims. - /// @param label Bare label to grant. - /// @param user Beneficiary the name binds to. - function _grant(string calldata label, address user) internal { - require(user != address(0), ZeroUser()); - require(label.isSingleLabel(), InvalidLabel()); - bytes32 node = _nodeOf(label); - require(_names[node].status == NameStatus.Open, NameNotOpen(node)); - emit NameAccepted(node, user, label); - _settle(node, user, label); - } - - /// @notice Marks a name claimed for `winner` and clears its claims, rejecting the losers. - /// @param node Namehash of the label under the active TLD. - /// @param winner Beneficiary the name binds to. - /// @param label Bare label, stored for review. - function _settle(bytes32 node, address winner, string calldata label) internal { - NameRecord storage record = _names[node]; - record.status = NameStatus.Claimed; - record.winner = winner; - _activate(node, label); - _clearClaimants(node, winner, label); - } - - /// @notice Deletes every claim on a name, rejecting each claimant that is not `winner`. - /// @param node Namehash of the label under the active TLD. - /// @param winner Claimant spared a rejection event; the zero address rejects every claimant. - /// @param label Bare label emitted with each rejection. - function _clearClaimants(bytes32 node, address winner, string calldata label) internal { - address[] memory current = _claimants[node].values(); - for (uint256 i; i < current.length; ++i) { - address claimant = current[i]; - delete _claims[node][claimant]; - _claimants[node].remove(claimant); - if (claimant != winner) { - emit NameRejected(node, claimant, label); - } - } - } - - /// @notice Records a name as active and stores its label the first time it is seen. - /// @param node Namehash of the label under the active TLD. - /// @param label Bare label stored on first activation. - function _activate(bytes32 node, string calldata label) internal { - NameRecord storage record = _names[node]; - if (bytes(record.label).length == 0) { - record.label = label; - } - _activeNodes.add(node); - } - - /// @notice Drops a name from the active set once it is Open with no claims. - /// @param node Namehash of the label under the active TLD. - function _deactivate(bytes32 node) internal { - NameRecord storage record = _names[node]; - if (record.status == NameStatus.Open && _claimants[node].length() == 0) { - _activeNodes.remove(node); - delete record.label; - } - } - - /// @notice Derives the namehash of `label` under the active TLD read from the registry. - /// @param label Bare label to hash. - /// @return node Namehash of the label under the active TLD. - function _nodeOf(string calldata label) internal view returns (bytes32 node) { - (, node) = LabelUtils.deriveNode(protocolRegistry.tldNode(), label); - } - - /// @notice Returns whether the current time is within the open window. - /// @return open True when the current time is within the window. - function _isWindowOpen() internal view returns (bool open) { - return block.timestamp >= _requestOpen && block.timestamp < _requestClose; - } - - /// @inheritdoc UUPSUpgradeable - function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} -} diff --git a/docs/PASEO-V080-RUNBOOK.md b/docs/PASEO-V080-RUNBOOK.md deleted file mode 100644 index b106b21a7..000000000 --- a/docs/PASEO-V080-RUNBOOK.md +++ /dev/null @@ -1,287 +0,0 @@ -# Paseo Asset Hub Next: broadcast order for the v0.8.0 in-place upgrade - -Every step is one `scripts/deploy/upgrade.sh` invocation under the deployment owner, and the -order is not a preference. Broadcasts run through CI, one label per step; see "Broadcasting -through the review PR" below. Running locally instead needs the key material, which nobody -holds, so the label path is not a convenience but the mechanism. Three dependencies make it the only order that works: - -- **The protocol registry goes first.** Every upgraded contract's `version()` reads - `protocolRegistry.protocolVersion()`, and `MigrateStoreFactory` calls `setExpectedCodehash`. - Neither entrypoint exists on the implementation the network starts on, so anything that runs - before the registry swap either reverts or reads a contract that cannot answer. -- **The store migration goes after the registry and before the declaration.** It rewires the - `storeFactory` key, and the declaration records the codehash of whatever that key resolves to. - Declaring first would record the old factory and then quietly stop being true. -- **The declaration goes last.** It is a claim about the whole deployment. Made early, an - abandoned run leaves a false claim standing; made last, the same abandoned run leaves the - previous declaration, which clients read as an older network. - -Nothing in the scripts enforces this. Each is independently runnable by design, so the ordering -lives here and in the fork tests that reproduce it. - -## Before anything is broadcast - -```bash -RPC_URL=https://eth-rpc-paseo-next.polkadot.io scripts/shell/verify-snapshots.sh - -PASEO_FORK_RPC=https://eth-rpc-paseo-next.polkadot.io \ - RPC_URL=https://eth-rpc-paseo-next.polkadot.io scripts/shell/fork-tests.sh -``` - -The second form needs no Docker. `bun run test:fork` brings up the local ETH-RPC adapter, which -only translates for the same node the hosted endpoint already fronts, so both exercise the same -state. Use whichever is available; the suite is 15 tests and takes under a minute either way. - -The first is the check that matters most and takes seconds. Every `*Old.sol` snapshot must -reproduce the implementation currently deployed; a snapshot that has drifted makes every layout -diff meaningless while leaving the build green. Re-run it on the day, not from an earlier run: -these proxies are upgraded in place, so a swap landing in between changes the answer. - -Confirm the broadcaster owns the proxies. All of them, and the deployed store factory, answer to -the same account; each script asserts this before it swaps anything, so a wrong signer fails fast -instead of reverting inside an upgrade call. - -## Order - -| # | Script | Why here | -| --- | --- | --- | -| 0 | `RotateOwnership` | Moves every contract to a fresh key before anything else is broadcast. | -| 1 | `UpgradeProtocolRegistry` | Adds `protocolVersion` and `setExpectedCodehash`, which steps 2 to 14 depend on. | -| 2 | `UpgradeRegistry` | Adds the deferred-write gate. Reads `registrar.controllers` live. | -| 3 | `UpgradeRegistrar` | Holds the controller authorisations the gate reads. | -| 4 | `UpgradeRegistrarController` | Carries the retained slot that keeps `protocolRegistry` readable. | -| 5 | `UpgradePopController` | | -| 6 | `UpgradePopRules` | | -| 7 | `UpgradeNameEscrow` | Custodies deposits; check `redeemWindow()` is non-zero afterwards. | -| 8 | `UpgradeNameWhitelist` | | -| 9 | `UpgradeResolver` | | -| 10 | `UpgradeReverseResolver` | | -| 11 | `UpgradeContentResolver` | | -| 12 | `UpgradePopResolver` | | -| 13 | `MigrateStoreFactory` | Deploys the replacement, imports the bindings, rewires the key. | -| 14 | `DeclareRelease` | Declares every codehash, then the release. Last, always. | - -Steps 5 to 12 have no dependency on each other and can go in any order among themselves. - -## Broadcasting through the review PR - -The workflow (`.github/workflows/paseo-upgrade-step.yml`) lives only on this branch and is -triggered by labels on the open review PR into master, because a dispatch button only exists for -workflows on the default branch and nothing that signs with the owner key goes there. One step: - -1. Add the label `run: