diff --git a/.github/abi-contracts.txt b/.github/abi-contracts.txt index dc1201c1..cb6c1cdb 100644 --- a/.github/abi-contracts.txt +++ b/.github/abi-contracts.txt @@ -26,7 +26,6 @@ DotnsNameEscrow DotnsPopController DotnsPopResolver DotnsRoleManager -RootGatewayDispatcher DotnsNameWhitelist DotnsCostModelRegistry DotnsFlatPricing diff --git a/DEPLOYMENTS.md b/DEPLOYMENTS.md index 6d6c718a..766d6445 100644 --- a/DEPLOYMENTS.md +++ b/DEPLOYMENTS.md @@ -269,7 +269,6 @@ At minimum, confirm: - The forward, reverse, content, and Pop resolvers are present. - The escrow address is present. - StoreFactory and both store beacons are present. -- The RootGatewayDispatcher is present on environments that use the root-dispatch path. - The escrow's redeem window is non-zero. A zero leaves `release` reverting with `RedeemWindowNotConfigured` for every name on the deployment, so a holder who releases a name by accident has no chance to redeem it back. Any value the setter accepted is already at least `MIN_REDEEM_WINDOW` (1 day), so this check is only ever confirming that the window was configured at all, which is exactly what a proxy upgraded without seeding it would fail. ```bash diff --git a/DEPLOYMENT_CHECKLIST.md b/DEPLOYMENT_CHECKLIST.md index 95eeba68..b4351775 100644 --- a/DEPLOYMENT_CHECKLIST.md +++ b/DEPLOYMENT_CHECKLIST.md @@ -137,7 +137,6 @@ Confirm these keys are present: - [ ] `LabelStoreBeacon` - [ ] `UserStoreBeacon` - [ ] `Multicall3` -- [ ] `RootGatewayDispatcher` (only on chains that use the root-dispatch path) Done. ✅ diff --git a/KNOWN_ISSUES.md b/KNOWN_ISSUES.md index 5fec1d07..af995505 100644 --- a/KNOWN_ISSUES.md +++ b/KNOWN_ISSUES.md @@ -8,8 +8,7 @@ For the security and audit status of the codebase, see [SECURITY.md](./SECURITY. | :- | :---- | :--- | :------------ | | 1 | Deferred LabelStore deployment | Runtime | Runtime allows root-origin contract deployment | | 2 | Transfer fee is zero until the store is settled | Runtime (follows from 1) | The holder calls `claimLabelStore`, or 1 is resolved | -| 3 | Root origin is not propagated through delegatecalls | Runtime | Runtime propagates origin through delegatecalls | -| 4 | No standalone user-status mapping | Current implementation | A dedicated status mapping is added, if ever needed | +| 3 | No standalone user-status mapping | Current implementation | A dedicated status mapping is added, if ever needed | ## 1. Deferred LabelStore deployment @@ -35,19 +34,8 @@ The registrar derives the transfer-floor price by reading the label from the sen See [README → DotnsPopController](./README.md#early-testnet-quirk-labelstore-deployment). -## 3. Root origin is not propagated through delegatecalls -**Type:** runtime limitation. - -The substrate Root origin is not propagated through delegatecalls, so a UUPS implementation running inside its proxy's delegatecall frame cannot observe Root authority directly. Gateway calls therefore route through the non-proxy `RootGatewayDispatcher`, which is the direct callee of the Root runtime origin and forwards to the controller via a regular message call only after the Root check passes. - -**Workaround:** the `RootGatewayDispatcher` shim restores a frame in which the Root check is meaningful. - -**Resolution:** when the runtime propagates origin through delegatecalls, the controller can verify Root from its own frame and the dispatcher becomes unnecessary. - -See [README → RootGatewayDispatcher](./README.md#rootgatewaydispatcher). - -## 4. No standalone user-status mapping +## 3. No standalone user-status mapping **Type:** current implementation. diff --git a/README.md b/README.md index 6f8f3e7d..81fdeebf 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,7 @@ Commit-reveal controller for the public registration path. A caller first submit ### DotnsPopController -Dedicated controller for the Proof-of-Personhood gateway flow. Lives behind its own UUPS proxy with its own storage and is registered on the registrar via addController alongside the commit-reveal controller. Its gated entry points are callable only from the address resolved through the protocol registry under the POP_GATEWAY key, which is the RootGatewayDispatcher deployed against this controller; the dispatcher is the contract that actually proves substrate Root authority before forwarding here. +Dedicated controller for the Proof-of-Personhood gateway flow. Lives behind its own UUPS proxy with its own storage and is registered on the registrar via addController alongside the commit-reveal controller. Its gated entry points are callable only under a substrate Root origin, which the controller verifies itself by reading `originIsRoot` from the revive System precompile. Today the Pop gateway does not write a standalone user-status mapping. It materialises the PoP flow through gateway-issued labels, PoP resolver records, and reservation queue state; user tier checks for public pricing still come from the personhood precompile/context read. @@ -148,14 +148,6 @@ Pop-gateway issuances mint the name and persist its label, but LabelStore deploy Operational consequence for transfers: the registrar derives the transfer-floor price by reading the label from the sender's LabelStore. A gateway-issued name whose pending claim is not yet settled has no readable label on the sender side, so `_quoteTransferFee` returns zero regardless of the recipient's tier. Until the name is settled into a LabelStore, a downward transfer (for example PopFull to NoStatus) does not charge the cross-tier friction it would otherwise owe. Clients that consume gateway-issued names should treat settlement as a prerequisite for accurate transfer-time pricing, not just for label discovery. -### RootGatewayDispatcher - -Non-upgradeable shim that translates a substrate Root-origin dispatch into an EVM-observable authority on the PoP controller. The dispatcher is the direct callee of the Root runtime origin, asks the revive System precompile whether its caller is Root, and forwards the calldata to the controller via a regular message call only when that check passes. The forwarded call lands on the controller proxy with the dispatcher as the immediate caller, which the controller authorises against the address registered on the protocol registry under POP_GATEWAY. - -Hosting the Root check in a separate, non-proxy contract is what makes it work at all. The revive System precompile is only meaningful in the frame that is the direct callee of Root, and a UUPS implementation runs inside the proxy's delegatecall, so the controller cannot ask the precompile from its own frame. The dispatcher's target is immutable, set at construction to the controller proxy it serves, and the dispatcher holds no storage of its own and never delegatecalls, so it cannot be repurposed as an arbitrary-target proxy. Rotating the dispatcher is a single set call on the protocol registry; the controller picks up the new gateway on its next call without an upgrade. - -The dispatcher exists to work around a runtime limitation: the substrate Root origin is not propagated through delegatecalls, so a UUPS implementation running inside its proxy's delegatecall frame cannot observe Root authority directly. Routing gateway calls through the non-proxy dispatcher restores a frame in which the Root check is meaningful. When the runtime propagates origin through delegatecalls, the controller can verify Root from its own frame and the dispatcher becomes unnecessary. This is a runtime limitation, not a protocol design choice. - ### DotnsRegistrar ERC721-backed registrar that mints ownership of label IDs (labelhashes). Minting is restricted to every address in the controllers mapping; the mapping is owner-gated through addController and removeController. Every other contract in the system that needs to check "is this address authorised to drive name state?" consults this mapping rather than keeping a parallel list, which is what lets multiple controllers coexist on the same registrar without per-contract configuration changes. @@ -239,7 +231,7 @@ On-chain lookup table mapping well-known bytes32 keys (declared in DotnsConstant Without it, each contract would store direct addresses to every contract it calls. An upgrade that changes one address would require a separate owner transaction for every contract that references it. The protocol registry reduces this to one: update the key in the registry, and every caller picks up the new address on its next call. The indirection also means a governance-driven rotation of, say, the PoP controller does not break any consumer that has already been deployed. -The registered keys include REGISTRAR, CONTROLLER, REGISTRY, REVERSE_RESOLVER, RESOLVER, CONTENT_RESOLVER, POP_RULES, STORE_FACTORY, POP_CONTROLLER, POP_RESOLVER, NAME_ESCROW, MULTICALL3, and POP_GATEWAY. +The registered keys include REGISTRAR, CONTROLLER, REGISTRY, REVERSE_RESOLVER, RESOLVER, CONTENT_RESOLVER, POP_RULES, STORE_FACTORY, POP_CONTROLLER, POP_RESOLVER, NAME_ESCROW, and MULTICALL3. ### Multicall3 @@ -280,7 +272,6 @@ The protocol carries a handful of constraints worth knowing before deploying or - **Deferred LabelStore deployment** (runtime). See [DotnsPopController](#early-testnet-quirk-labelstore-deployment). - **Transfer fee is zero until the store is settled** (runtime). See [DotnsPopController](#early-testnet-quirk-labelstore-deployment). -- **Root origin is not propagated through delegatecalls** (runtime). See [RootGatewayDispatcher](#rootgatewaydispatcher). - **No standalone user-status mapping** (current implementation). See [DotnsPopController](#dotnspopcontroller). ## License diff --git a/RELEASE_ARTIFACTS.md b/RELEASE_ARTIFACTS.md index ce61038b..6f09af9c 100644 --- a/RELEASE_ARTIFACTS.md +++ b/RELEASE_ARTIFACTS.md @@ -86,7 +86,7 @@ To check the addresses against a chain: bun run deployments:verify --network paseo-assethub --rpc ``` -It reads the well-known keys from `DotnsConstants.sol`, resolves each through the protocol registry, and checks that every resolved address is one the manifest records and has code, that every recorded contract is pointed at by some key, and that `RootGatewayDispatcher.TARGET()` is the recorded `DotnsPopController`. The beacons are reported as unverifiable, since nothing in the registry points at them. +It reads the well-known keys from `DotnsConstants.sol`, resolves each through the protocol registry, and checks that every resolved address is one the manifest records and has code, that every recorded contract is pointed at by some key. The beacons are reported as unverifiable, since nothing in the registry points at them. It compares the two sides as sets, so it does not check that a given key holds the contract you would expect; that pairing is asserted when a deployment is wired. It reads a committed manifest rather than a published asset, so run it from a checkout at the tag. diff --git a/contracts/registrars/DotnsPopController.sol b/contracts/registrars/DotnsPopController.sol index 4226ee72..36c6464f 100644 --- a/contracts/registrars/DotnsPopController.sol +++ b/contracts/registrars/DotnsPopController.sol @@ -24,6 +24,7 @@ import {LabelUtils} from "../utils/LabelUtils.sol"; import {RegistrationUtils} from "../utils/RegistrationUtils.sol"; import {StringUtils} from "../utils/StringUtils.sol"; import {DotnsConstants} from "../utils/DotnsConstants.sol"; +import {SystemUtils} from "../utils/SystemUtils.sol"; /// @title DotnsPopController /// @notice Dedicated PoP controller orchestrating lite-person and full-person username @@ -91,27 +92,6 @@ contract DotnsPopController is /// state has been committed. uint256 private constant CHAT_KEY_LENGTH = 65; - /// @notice Selector for the typed @custom:function reserveLiteName overload. - /// @dev Hard-coded to disambiguate from the `(bytes)` overload at compile time. Must stay - /// in sync with the @custom:struct LiteRegistration field layout. - bytes4 private constant SELECTOR_RESERVE_LITE = - bytes4(keccak256("reserveLiteName((string,address,bytes))")); - - /// @notice Selector for the typed @custom:function reserveBaseName overload. - /// @dev `BaseReservation` is `(LiteRegistration, string)` and `LiteRegistration` is - /// `(string,address,bytes)`, hence the nested tuple in the canonical signature. - bytes4 private constant SELECTOR_RESERVE_BASE = - bytes4(keccak256("reserveBaseName(((string,address,bytes),string))")); - - /// @notice Selector for the typed reservation-only gateway primitive. - bytes4 private constant SELECTOR_RESERVE_BASE_ONLY = - bytes4(keccak256("reserveBaseNameOnly((address,string))")); - - /// @notice Selector for the typed @custom:function registerBaseName overload. - /// @dev `Link` is `(uint8,string,bytes)` because `LinkKind` is an enum. - bytes4 private constant SELECTOR_REGISTER_BASE = - bytes4(keccak256("registerBaseName((string,address,(uint8,string,bytes)))")); - /// @notice Protocol-level address registry for all DotNS contracts. IDotnsProtocolRegistry public protocolRegistry; @@ -155,17 +135,9 @@ contract DotnsPopController is /// @dev Reserved storage space to allow for layout changes in future upgrades. uint256[50] private __gap; - /// @notice Restricts calls to the address registered as the PoP gateway - /// on the protocol registry. - /// @dev Authority is delegated wholly to the registered gateway, which is - /// the Root gateway dispatcher. Any caller other than the registered - /// gateway is rejected with NotGateway. The Root-authority check - /// itself lives in the dispatcher because the revive System - /// precompile is only meaningful in the frame that is the direct - /// callee of Root, which is the dispatcher and never this UUPS - /// implementation. - modifier onlyGateway() { - _onlyGateway(); + /// @notice Restricts calls to a substrate Root origin. + modifier onlyRoot() { + _onlyRoot(); _; } @@ -199,17 +171,12 @@ contract DotnsPopController is } /// @inheritdoc IDotnsPopController - function reserveLiteName(LiteRegistration calldata params) external override onlyGateway { + function reserveLiteName(LiteRegistration calldata params) external override onlyRoot { _reserveLite(_popRules(), params); } /// @inheritdoc IDotnsPopController - function reserveLiteName(bytes calldata payload) external override onlyGateway { - _dispatchTyped(SELECTOR_RESERVE_LITE, payload); - } - - /// @inheritdoc IDotnsPopController - function reserveBaseName(BaseReservation calldata params) external override onlyGateway { + function reserveBaseName(BaseReservation calldata params) external override onlyRoot { IPopRules rules = _popRules(); bytes32 reservedHash; bool hasReservation = bytes(params.reservedBaseLabel).length != 0; @@ -227,16 +194,7 @@ contract DotnsPopController is } /// @inheritdoc IDotnsPopController - function reserveBaseName(bytes calldata payload) external override onlyGateway { - _dispatchTyped(SELECTOR_RESERVE_BASE, payload); - } - - /// @inheritdoc IDotnsPopController - function reserveBaseNameOnly(BaseNameReservation calldata params) - external - override - onlyGateway - { + function reserveBaseNameOnly(BaseNameReservation calldata params) external override onlyRoot { IPopRules rules = _popRules(); (bytes32 reservedHash,) = _validateReservableBaseLabel(rules, params.reservedBaseLabel); _advanceExpiredHead(reservedHash); @@ -244,11 +202,6 @@ contract DotnsPopController is _enqueueReservation(rules, reservedHash, params.reservedBaseLabel, params.user); } - /// @inheritdoc IDotnsPopController - function reserveBaseNameOnly(bytes calldata payload) external override onlyGateway { - _dispatchTyped(SELECTOR_RESERVE_BASE_ONLY, payload); - } - /// @notice Lite-only mint shared by @custom:function reserveLiteName and the lite leg /// of @custom:function reserveBaseName. /// @dev Gateway attestation is the authority for personhood on this path; the on-chain @@ -278,12 +231,7 @@ contract DotnsPopController is } /// @inheritdoc IDotnsPopController - function registerBaseName(bytes calldata payload) external override onlyGateway { - _dispatchTyped(SELECTOR_REGISTER_BASE, payload); - } - - /// @inheritdoc IDotnsPopController - function registerBaseName(FullRegistration calldata params) external override onlyGateway { + function registerBaseName(FullRegistration calldata params) external override onlyRoot { Link calldata link = params.link; address user = params.user; string calldata label = params.label; @@ -895,38 +843,13 @@ contract DotnsPopController is delete _reservedBaseLabel[labelhash]; } - /// @notice Internal check enforcing PoP-gateway-only access. - /// @dev Authorises a call when the caller matches the address registered - /// as the PoP gateway on the protocol registry. The dispatcher - /// registered there is responsible for proving substrate Root - /// authority via the revive System precompile; this contract trusts - /// that forwarded calls already carry that authority. Reverts with - /// NotGateway on failure, including when the registry key is unset. - function _onlyGateway() internal view { - address gw = protocolRegistry.get(DotnsConstants.POP_GATEWAY); - require(gw != address(0) && msg.sender == gw, NotGateway(msg.sender)); - } - - /// @notice Routes a raw cross-chain payload to the typed entrypoint identified by `selector`. - /// @dev Prepends `selector` to `payload` and `delegatecall`s `address(this)` so the typed - /// overload runs in the original call context, making the typed path the single source of - /// truth. The `bytes` payload from the cross-chain caller is already - /// `abi.encode(StructTuple)`, so concatenating `selector` with `payload` is exactly the - /// calldata the typed overload expects. Reverts bubble up byte-for-byte so the caller sees - /// the same error it would have seen on a direct typed call. The delegatecall target is - /// hard-coded to `address(this)` and `selector` is one of three module-private constants - /// pointing at this contract's own typed entrypoints, so storage context is preserved and - /// no external code can run in this contract's frame. @custom:function _onlyGateway runs - /// on both the outer bytes overload and the inner typed overload; both checks read the - /// same registry slot. - /// @custom:oz-upgrades-unsafe-allow delegatecall - function _dispatchTyped(bytes4 selector, bytes calldata payload) private { - (bool ok, bytes memory ret) = address(this).delegatecall(bytes.concat(selector, payload)); - if (!ok) { - assembly { - revert(add(ret, 32), mload(ret)) - } - } + /// @notice Internal check enforcing a substrate Root origin. + /// @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. The same + /// applies to anything reachable from an onlyRoot entrypoint. + function _onlyRoot() internal view { + require(SystemUtils.originIsRoot(), NotRoot()); } /// @inheritdoc UUPSUpgradeable diff --git a/contracts/registrars/IDotnsPopController.sol b/contracts/registrars/IDotnsPopController.sol index 8d08be49..75be380d 100644 --- a/contracts/registrars/IDotnsPopController.sol +++ b/contracts/registrars/IDotnsPopController.sol @@ -189,16 +189,11 @@ interface IDotnsPopController is IDotnsController { /// @param newHead Address now holding the head slot. event ReservationHeadAdvanced(bytes32 indexed labelhash, address indexed newHead); - /// @notice Thrown when a gated entrypoint is reached from an address that - /// is not the gateway registered on the protocol registry under - /// the PoP gateway key. - /// @dev The controller delegates substrate Root-authority verification to - /// the registered gateway, which is the Root gateway dispatcher, and - /// authorises calls solely against the address resolved from the - /// protocol registry. The caller parameter carries the immediate EVM - /// caller observed by this contract for off-chain diagnostics. - /// @param caller Immediate EVM caller observed by this contract. - error NotGateway(address caller); + /// @notice Thrown when a gated entrypoint is reached 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 supplied lite-person label does not match `NAMEXX`. error InvalidLiteLabel(); @@ -245,10 +240,9 @@ interface IDotnsPopController is IDotnsController { /// @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 via the registered PoP gateway (otherwise @custom:reverts NotGateway); - /// the gateway is responsible for asserting substrate Root authority before forwarding - /// here. The lite leg validates the dotted `stem.NN` shape and requires the flattened label - /// to classify as PopLite (otherwise @custom:reverts InvalidLiteLabel), and rejects a + /// @dev Callable only under a substrate Root origin (otherwise @custom:reverts NotRoot). The + /// lite leg validates the dotted `stem.NN` shape and requires the flattened label to classify + /// as PopLite (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; @@ -274,28 +268,9 @@ interface IDotnsPopController is IDotnsController { /// @param params Reservation request; see @custom:struct BaseReservation. function reserveBaseName(BaseReservation calldata params) external; - /// @notice Raw-payload variant of @custom:function reserveBaseName for cross-chain dispatch. - /// @dev `payload` is `abi.encode(BaseReservation({...}))`, the bare ABI-encoded struct - /// with NO function-selector prefix and NO leading bytes-length word. The contract - /// prepends the typed selector and `delegatecall`s itself so the typed entrypoint runs - /// in the original call context and remains the single source of truth, which means the - /// typed overload's full revert surface bubbles up byte-for-byte: gateway-only access - /// (otherwise @custom:reverts NotGateway), lite-label shape (otherwise - /// @custom:reverts InvalidLiteLabel), base-label shape (otherwise - /// @custom:reverts InvalidBaseLabel), already-registered base label (otherwise - /// @custom:reverts BaseNameAlreadyRegistered), duplicate-reservation guard (otherwise - /// @custom:reverts AlreadyReserved), and queue capacity (otherwise - /// @custom:reverts QueueFull). The success path likewise emits the same events as the - /// typed call: @custom:emits LiteNameReserved and @custom:emits NameRegistered on the lite - /// leg, plus @custom:emits ReservationQueued and any @custom:emits ReservationExpired - /// observed while advancing the queue head when the base-name leg runs. - /// Note: `abi.decode` ignores trailing bytes past the encoded struct, so off-chain - /// encoders MUST NOT assume strict length validation. - /// @param payload `abi.encode(BaseReservation)` produced by the cross-chain caller. - function reserveBaseName(bytes calldata payload) external; - /// @notice Enqueues only the full/base-name reservation for a user. - /// @dev Callable only via the registered PoP gateway. This is the second step of the split + /// @dev Callable only under a substrate 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, @@ -306,17 +281,11 @@ interface IDotnsPopController is IDotnsController { /// @param params Reservation request; see @custom:struct BaseNameReservation. function reserveBaseNameOnly(BaseNameReservation calldata params) external; - /// @notice Raw-payload variant of @custom:function reserveBaseNameOnly for cross-chain - /// dispatch. @param payload `abi.encode(BaseNameReservation)` produced by the cross-chain - /// caller. - function reserveBaseNameOnly(bytes calldata payload) external; - /// @notice Registers a lite-person username on behalf of the supplied /// user without touching the base-name reservation queue. - /// @dev Callable only via the registered PoP gateway (otherwise @custom:reverts NotGateway); - /// the gateway is responsible for asserting substrate Root authority before forwarding - /// here. The supplied label must satisfy the dotted `stem.NN` shape and the flattened label - /// must classify as PopLite (otherwise @custom:reverts InvalidLiteLabel); a supplied chat + /// @dev Callable only under a substrate Root origin (otherwise @custom:reverts NotRoot). The + /// supplied label must satisfy the dotted `stem.NN` shape and the flattened label must classify + /// as PopLite (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. On a warm-path mint /// @custom:emits LiteNameReserved and @custom:emits NameRegistered. On a cold-path @@ -327,30 +296,10 @@ interface IDotnsPopController is IDotnsController { /// @param params Registration request; see @custom:struct LiteRegistration. function reserveLiteName(LiteRegistration calldata params) external; - /// @notice Raw-payload variant of @custom:function reserveLiteName for cross-chain dispatch. - /// @dev `payload` is `abi.encode(LiteRegistration({...}))`, the bare ABI-encoded struct - /// with NO function-selector prefix and NO leading bytes-length word. The contract - /// prepends the typed selector and `delegatecall`s itself so the typed entrypoint runs - /// in the original call context and remains the single source of truth, so the typed - /// overload's revert surface bubbles up byte-for-byte: gateway-only access (otherwise - /// @custom:reverts NotGateway) and lite-label shape (otherwise - /// @custom:reverts InvalidLiteLabel). The success path emits the same events as the typed - /// call: @custom:emits LiteNameReserved and @custom:emits NameRegistered. - /// Note: `abi.decode` ignores trailing bytes past the encoded struct, so - /// off-chain encoders MUST NOT assume strict length validation; pad-only - /// junk past the tail is silently dropped (no state corruption; decoded - /// values are unchanged). - /// Worked example off-chain: - /// `bytes payload = abi.encode(LiteRegistration({liteLabel: "alice42", user: u, chatKey: - /// k}));` - /// @param payload `abi.encode(LiteRegistration)` produced by the cross-chain caller. - function reserveLiteName(bytes calldata payload) external; - /// @notice Registers a full-person username on behalf of the supplied user. - /// @dev Callable only via the registered PoP gateway (otherwise @custom:reverts NotGateway); - /// the gateway is responsible for asserting substrate Root authority before forwarding - /// here. The base label must satisfy the DNS-label shape and be a true base label with no - /// trailing digits (otherwise @custom:reverts InvalidBaseLabel), and the label must not + /// @dev Callable only under a substrate Root origin (otherwise @custom:reverts NotRoot). The + /// base label must satisfy the DNS-label shape and be a true base label with no trailing digits + /// (otherwise @custom:reverts InvalidBaseLabel), and the label must not /// classify as governance-reserved (otherwise @custom:reverts InvalidBaseLabel). The /// gateway also defers to PopRules as the single cross-flow authority: when PopRules /// carries a live base-name slot held by another user (stamped by the public commit-reveal @@ -381,24 +330,6 @@ interface IDotnsPopController is IDotnsController { /// @param params Registration request; see @custom:struct FullRegistration. function registerBaseName(FullRegistration calldata params) external; - /// @notice Raw-payload variant of @custom:function registerBaseName for cross-chain dispatch. - /// @dev `payload` is `abi.encode(FullRegistration({...}))`, the bare ABI-encoded struct - /// with NO function-selector prefix and NO leading bytes-length word. The contract - /// prepends the typed selector and `delegatecall`s itself so the typed entrypoint runs - /// in the original call context and remains the single source of truth, so the typed - /// overload's revert surface bubbles up byte-for-byte: gateway-only access (otherwise - /// @custom:reverts NotGateway), base-label shape (otherwise @custom:reverts InvalidBaseLabel), - /// lite-label shape on the `LiteUsername` branch (otherwise @custom:reverts InvalidLiteLabel), - /// and the standalone-mint holder guard (otherwise @custom:reverts NotHolder). The success - /// path emits the same events as the typed call: @custom:emits BaseNameClaimed on a claim - /// or @custom:emits StandaloneNameRegistered otherwise, @custom:emits LiteToFullLinked on - /// the `LiteUsername` branch, @custom:emits ReservationExpired for each entry reaped while - /// advancing the queue head, and always @custom:emits NameRegistered. - /// Note: `abi.decode` ignores trailing bytes past the encoded struct, so off-chain - /// encoders MUST NOT assume strict length validation. - /// @param payload `abi.encode(FullRegistration)` produced by the cross-chain caller. - function registerBaseName(bytes calldata payload) 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 diff --git a/contracts/registrars/RootGatewayDispatcher.sol b/contracts/registrars/RootGatewayDispatcher.sol deleted file mode 100644 index 0063afe8..00000000 --- a/contracts/registrars/RootGatewayDispatcher.sol +++ /dev/null @@ -1,77 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.34; - -import {ISystem} from "../external/revive/ISystem.sol"; -import {DotnsConstants} from "../utils/DotnsConstants.sol"; - -/// @title RootGatewayDispatcher -/// @notice Non-upgradeable shim that translates a substrate Root-origin -/// dispatch into an EVM-observable authority on the PoP controller -/// proxy. The dispatcher is the direct callee of the Root runtime -/// origin, asks the revive System precompile whether its caller is -/// Root, and forwards the calldata to the controller via a regular -/// message call when, and only when, that check passes. -/// @dev The Root-authority check must live in the frame that is the direct -/// callee of Root. A UUPS implementation runs inside the proxy's -/// delegatecall, so the controller cannot ask the precompile from its -/// own frame. The dispatcher hosts that check in a non-proxy contract -/// and converts the result into the immediate-caller predicate the -/// controller checks on the forwarded call. -/// -/// Lifecycle: -/// - Deployed once per controller proxy with its target bound to that proxy -/// address. -/// - Registered on the protocol registry under the PoP gateway key; the -/// controller resolves this key on every gated call. -/// - The PoP gateway pallet sends Root-origin dispatches at the dispatcher's -/// address rather than the controller's. -/// -/// Security: -/// - The target is immutable, so a deployed dispatcher can only ever forward -/// to the controller it was constructed against. Rotating the controller -/// proxy means deploying a new dispatcher and pointing the gateway pallet -/// at it. -/// - The dispatcher holds no storage and never delegatecalls, so it cannot -/// be used as an arbitrary-target proxy. -/// - The fallback is non-payable: gated controller entrypoints are -/// non-payable, and rejecting value transfers at the dispatcher boundary -/// keeps the forwarded call shape identical to a direct controller call. -/// @custom:security-contact admin@parity.io -contract RootGatewayDispatcher { - /// @notice Thrown when the immediate substrate origin is not Root. - /// @dev The revive System precompile returns false rather than reverting - /// on a non-Root origin, so the gate has to surface its own error. - error NotRoot(); - - /// @notice Controller proxy address this dispatcher forwards to. - /// @dev Set once at construction and never reassigned. - address public immutable TARGET; - - /// @param target_ Address of the PoP controller proxy. - constructor(address target_) { - TARGET = target_; - } - - /// @notice Verifies Root authority through the revive System precompile - /// and forwards the raw calldata to the controller proxy via a - /// regular message call. - /// @dev The precompile check is evaluated in this contract's frame, which - /// is the direct callee of Root, so the precompile resolves the - /// origin walk successfully. The forwarded call lands on the - /// controller proxy with this contract as the immediate caller, - /// which the controller authorises against the gateway address - /// registered on the protocol registry. - fallback() external { - require(ISystem(DotnsConstants.REVIVE_SYSTEM).callerIsRoot(), NotRoot()); - - (bool ok, bytes memory ret) = TARGET.call(msg.data); - if (!ok) { - assembly { - revert(add(ret, 32), mload(ret)) - } - } - assembly { - return(add(ret, 32), mload(ret)) - } - } -} diff --git a/contracts/utils/DotnsConstants.sol b/contracts/utils/DotnsConstants.sol index 1a902655..1f500f66 100644 --- a/contracts/utils/DotnsConstants.sol +++ b/contracts/utils/DotnsConstants.sol @@ -14,8 +14,8 @@ library DotnsConstants { /// that opts the precompile in. /// @dev Mirrors the upstream `SYSTEM_ADDR` constant in /// `substrate/frame/revive/uapi/sol/ISystem.sol`. Consumed by - /// `DotnsPopController` to authenticate Root-origin dispatches via - /// `ISystem.callerIsRoot()`. + /// `DotnsPopController` and `DotnsNameWhitelist` 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 @@ -186,18 +186,6 @@ library DotnsConstants { /// forge-lint: disable-next-line(unsafe-typecast) bytes32 internal constant CREATE3_FACTORY = bytes32("create3Factory"); - /// @notice Well-known key for the address authorised to invoke the PoP - /// controller's gated entrypoints. - /// @dev Role: substrate Root-origin shim. Resolves to the Root gateway - /// dispatcher deployed against the PoP controller. The dispatcher - /// verifies Root authority through the revive System precompile in - /// its own frame and forwards calldata to the controller via a - /// regular message call; the controller authorises against this - /// registry key, so rotating the dispatcher is a single write on - /// the protocol registry. - /// forge-lint: disable-next-line(unsafe-typecast) - bytes32 internal constant POP_GATEWAY = bytes32("popGateway"); - /// @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. Both the public and PoP diff --git a/deployments/paseo-assethub/420420417.json b/deployments/paseo-assethub/420420417.json index 36d39e61..ae75b387 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":"0xfe5A45f7fD58D1A6FE09455DB799405b1dcE9411","DotnsPopResolver":"0xDaC984884EcA8Fc44011f1D6C49B27828390A72B","DotnsProtocolRegistry":"0xD19e3D0C97CF501125a04A97405e3e6592fa846E","DotnsRegistrar":"0x4f06E818Ba3d987704fd91cf3d868E4b019106Ab","DotnsRegistrarController":"0xBdaA01bD1bA67d709F2b1fF286Da0d854977EA30","DotnsRegistry":"0xf34054fd76BbF85f216cf9908226D5f0A72E50CA","DotnsResolver":"0xbd1165E549DF96F083c0A16f61590927bC187009","DotnsReverseResolver":"0xee3883d7eB60Ee9BCD7F3bcD8f2f05302A9Cc035","LabelStoreBeacon":"0xb57Ebc2e7085616d4906D1fE49af1cE13f7dffeF","Multicall3":"0xB4468000abD87D3c56cbFBd153161223D7b109e5","PopRules":"0x747B456bE03aec0b42bd85C51513730FBD45DA31","RootGatewayDispatcher":"0xa889CCA3Fb4B07b98a11cc54C10f13dDA20bc3db","StoreFactory":"0x709A027F446a9e2a4BB9cb9a9c754435b19e32B7","UserStoreBeacon":"0xb7C995601679840d36F37E86DB2d7dF30797eC5C","_seed":"0x0000000000000000000000000000000000000000"} +{"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":"0xb57Ebc2e7085616d4906D1fE49af1cE13f7dffeF","Multicall3":"0xB4468000abD87D3c56cbFBd153161223D7b109e5","PopRules":"0x747B456bE03aec0b42bd85C51513730FBD45DA31","StoreFactory":"0x709A027F446a9e2a4BB9cb9a9c754435b19e32B7","UserStoreBeacon":"0xb7C995601679840d36F37E86DB2d7dF30797eC5C","_seed":"0x0000000000000000000000000000000000000000"} diff --git a/scripts/deploy/DeployPopSystem.s.sol b/scripts/deploy/DeployPopSystem.s.sol index 4e3ae214..56d41408 100644 --- a/scripts/deploy/DeployPopSystem.s.sol +++ b/scripts/deploy/DeployPopSystem.s.sol @@ -30,7 +30,6 @@ contract DeployPopSystem is BaseDeployer { _deployPopResolver(owner, protocolRegistry); address popController = _deployPopController(owner, protocolRegistry); - _deployGatewayDispatcher(owner, popController); _deployPopLens(owner, protocolRegistry); saveDeployments(); @@ -71,35 +70,9 @@ contract DeployPopSystem is BaseDeployer { ); } - /// @notice Deploys the Root gateway dispatcher bound to the PoP - /// controller proxy and records it on the manifest for the - /// wire-up stage to register on the protocol registry. - /// @dev The dispatcher's target is immutable and must be set to the - /// controller proxy at construction. Registry registration is the - /// wire-up stage's job, following the same pattern as every other - /// protocol address, so this script only deploys and logs. - /// @param owner Broadcasting account. - /// @param popController Address of the controller proxy from the previous - /// deploy step. - /// @return dispatcher Address of the deployed Root gateway dispatcher. - function _deployGatewayDispatcher( - address owner, - address popController - ) - internal - returns (address dispatcher) - { - dispatcher = _broadcastDeployCreate3( - owner, - "RootGatewayDispatcher.sol:RootGatewayDispatcher", - abi.encode(popController), - "RootGatewayDispatcher" - ); - } - /// @notice Deploys the read-only PoP lens bound to the protocol registry and records it on /// the manifest for the wire-up stage to register. - /// @dev A plain CREATE3 deployment, like the dispatcher: the lens holds no state beyond the + /// @dev A plain CREATE3 deployment: the lens holds no state beyond the /// registry it resolves siblings through, so it needs no proxy. Registry registration is /// the wire-up stage's job. /// @param owner Broadcasting account. diff --git a/scripts/deploy/WireDeployments.s.sol b/scripts/deploy/WireDeployments.s.sol index cdbc4959..ac50b392 100644 --- a/scripts/deploy/WireDeployments.s.sol +++ b/scripts/deploy/WireDeployments.s.sol @@ -52,7 +52,6 @@ contract WireDeployments is BaseDeployer { address popResolver; address popController; address popLens; - address rootGatewayDispatcher; } function run() external { @@ -92,7 +91,6 @@ contract WireDeployments is BaseDeployer { addr.popResolver = _readAddress("DotnsPopResolver"); addr.popController = _readAddress("DotnsPopController"); addr.popLens = _readAddress("DotnsPopLens"); - addr.rootGatewayDispatcher = _readAddress("RootGatewayDispatcher"); } function _authoriseControllers(address owner, Addresses memory addr) internal { @@ -124,7 +122,6 @@ contract WireDeployments is BaseDeployer { registry.set(DotnsConstants.POP_CONTROLLER, addr.popController); registry.set(DotnsConstants.POP_RESOLVER, addr.popResolver); registry.set(DotnsConstants.POP_LENS, addr.popLens); - registry.set(DotnsConstants.POP_GATEWAY, addr.rootGatewayDispatcher); vm.stopBroadcast(); console.log("Protocol registry keys set"); } @@ -222,10 +219,6 @@ contract WireDeployments is BaseDeployer { ); require(registry.get(DotnsConstants.POP_RESOLVER) == addr.popResolver, "Key: popResolver"); require(registry.get(DotnsConstants.POP_LENS) == addr.popLens, "Key: popLens"); - require( - registry.get(DotnsConstants.POP_GATEWAY) == addr.rootGatewayDispatcher, - "Key: popGateway" - ); require( DotnsRegistrar(addr.registrar).controllers(IDotnsController(addr.registrarController)), diff --git a/scripts/js/release-metadata.mjs b/scripts/js/release-metadata.mjs index b4aa2d5e..59d42d99 100644 --- a/scripts/js/release-metadata.mjs +++ b/scripts/js/release-metadata.mjs @@ -337,21 +337,6 @@ function verify(args) { } } - // The hop the truapi host walks after reading DotnsGateway.DispatcherAddress. - const dispatcher = contracts.RootGatewayDispatcher; - if (dispatcher) { - const target = cast(["call", dispatcher, "TARGET()(address)", "--rpc-url", rpc]); - const expected = contracts.DotnsPopController; - if (!expected || target.toLowerCase() !== expected.toLowerCase()) { - problems.push( - `RootGatewayDispatcher.TARGET() is ${target}, manifest DotnsPopController is ${ - expected ?? "absent" - }`, - ); - } else { - console.log(` ok RootGatewayDispatcher.TARGET() ${target}`); - } - } for (const label of UNVERIFIABLE) { if (contracts[label]) { diff --git a/test/base/BaseDotns.t.sol b/test/base/BaseDotns.t.sol index 25651709..f30dd48d 100644 --- a/test/base/BaseDotns.t.sol +++ b/test/base/BaseDotns.t.sol @@ -19,7 +19,6 @@ import { } from "../../contracts/registrars/DotnsPopController.sol"; import {DotnsPopLens} from "../../contracts/registrars/DotnsPopLens.sol"; import {IDotnsPopLens} from "../../contracts/registrars/IDotnsPopLens.sol"; -import {RootGatewayDispatcher} from "../../contracts/registrars/RootGatewayDispatcher.sol"; import {IDotnsController} from "../../contracts/registrars/IDotnsController.sol"; import {DotnsRegistry} from "../../contracts/registry/DotnsRegistry.sol"; import {DotnsResolver} from "../../contracts/resolvers/DotnsResolver.sol"; @@ -104,45 +103,22 @@ abstract contract BaseDotns is Test { /// @notice Deployed PoP lens instance (read-only view over PoP identity data). DotnsPopLens public dotnsPopLens; - /// @notice Test stand-in for the Root gateway dispatcher. - /// @dev Registered on the protocol registry under the PoP gateway key - /// during setUp. Tests that exercise gated PoP entrypoints prank as - /// this address, mirroring how the dispatcher's forwarded call - /// appears to the controller in production. - address public popGateway; - /// @notice Selector for the typed reserveLiteName entrypoint. bytes4 internal constant SELECTOR_RESERVE_LITE_TYPED = bytes4(keccak256("reserveLiteName((string,address,bytes))")); - /// @notice Selector for the bytes-encoded reserveLiteName entrypoint. - bytes4 internal constant SELECTOR_RESERVE_LITE_BYTES = - bytes4(keccak256("reserveLiteName(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 bytes-encoded reserveBaseName entrypoint. - bytes4 internal constant SELECTOR_RESERVE_BASE_BYTES = - bytes4(keccak256("reserveBaseName(bytes)")); - /// @notice Selector for the typed reserveBaseNameOnly entrypoint. bytes4 internal constant SELECTOR_RESERVE_BASE_ONLY_TYPED = bytes4(keccak256("reserveBaseNameOnly((address,string))")); - /// @notice Selector for the bytes-encoded reserveBaseNameOnly entrypoint. - bytes4 internal constant SELECTOR_RESERVE_BASE_ONLY_BYTES = - bytes4(keccak256("reserveBaseNameOnly(bytes)")); - /// @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 bytes-encoded registerBaseName entrypoint. - bytes4 internal constant SELECTOR_REGISTER_BASE_BYTES = - bytes4(keccak256("registerBaseName(bytes)")); - /// @notice Default reservation duration used by the PoP controller. uint64 public constant DEFAULT_RESERVATION_DURATION = 7 days; @@ -180,8 +156,6 @@ abstract contract BaseDotns is Test { // baselength 7 with 2 trailing digits classifies as PopLite. /// @notice PoP lite classification label fixture A. string internal constant LITE_LABEL_A = "aliceli01"; - /// @notice Dotted form of @custom:constant LITE_LABEL_A used by gateway helpers. - string internal constant LITE_LABEL_A_DOTTED = "aliceli.01"; /// @notice PoP lite classification label fixture B. string internal constant LITE_LABEL_B = "alicoli02"; /// @notice PoP lite classification label fixture C. @@ -324,9 +298,6 @@ abstract contract BaseDotns is Test { dotnsPopController = DotnsPopController(dotnsPopControllerAddress); vm.label(dotnsPopControllerAddress, "DotnsPopController"); - popGateway = address(new RootGatewayDispatcher(dotnsPopControllerAddress)); - vm.label(popGateway, "RootGatewayDispatcher"); - dotnsRegistrar.addController(IDotnsController(dotnsPopControllerAddress)); address dotnsNameEscrowAddress = Upgrades.deployUUPSProxy( @@ -355,9 +326,6 @@ abstract contract BaseDotns is Test { protocolRegistry.set(DotnsConstants.POP_RESOLVER, dotnsPopResolverAddress); protocolRegistry.set(DotnsConstants.POP_CONTROLLER, dotnsPopControllerAddress); protocolRegistry.set(DotnsConstants.NAME_ESCROW, dotnsNameEscrowAddress); - // Stand-in for the Root gateway dispatcher. Dedicated dispatcher - // coverage lives in test/unit/registrar/RootGatewayDispatcher.t.sol. - protocolRegistry.set(DotnsConstants.POP_GATEWAY, popGateway); // Deploy the read-only lens last, once every sibling key it resolves // (POP_CONTROLLER, REGISTRAR, STORE_FACTORY, POP_RESOLVER, POP_RULES) is @@ -377,16 +345,6 @@ abstract contract BaseDotns is Test { ); } - /// @notice Mocks revive's System precompile callerIsRoot result. - /// @param returnValue Value to return from `callerIsRoot`. - function _mockCallerIsRoot(bool returnValue) internal { - vm.mockCall( - DotnsConstants.REVIVE_SYSTEM, - abi.encodeWithSelector(ISystem.callerIsRoot.selector), - abi.encode(returnValue) - ); - } - /// @notice Mocks revive's System precompile originIsRoot result. /// @param returnValue Value to return from `originIsRoot`. function _mockOriginIsRoot(bool returnValue) internal { @@ -496,17 +454,16 @@ abstract contract BaseDotns is Test { dotnsRegistrarController.setRole(DotnsConstants.WHITELIST_OPERATOR_ROLE, account, false); } - /// @notice Drives a PoP reservation from the registered gateway address and settles - /// the resulting pending claim from the user's signed origin. + /// @notice Drives a PoP reservation under a Root origin and settles the resulting + /// pending claim from the user's signed origin. /// @dev Single canonical helper for PoP-gateway reservations across unit and fuzz - /// test suites. Pranks from the gateway stand-in installed during setUp, which - /// mirrors how the Root gateway dispatcher appears to the controller in production. + /// test suites. Calls the controller under a mocked Root origin. /// The auto-settle deploys the user's `LabelStore` and writes the stashed label so /// subsequent gateway mints for the same user take the warm path and assertions /// 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 _gatewayReserveBaseName or @custom:function _gatewayReserveLiteName + /// @custom:function _rootReserveBaseName or @custom:function _rootReserveLiteName /// directly. function _reservePop( address user, @@ -516,7 +473,7 @@ abstract contract BaseDotns is Test { ) internal { - _gatewayReserveBaseName( + _rootReserveBaseName( IDotnsPopController.BaseReservation({ lite: IDotnsPopController.LiteRegistration({ liteLabel: _toGatewayLiteLabel(liteLabel), user: user, chatKey: chatKey @@ -530,62 +487,41 @@ abstract contract BaseDotns is Test { } } - /// @notice Dispatches the typed `reserveLiteName` call through the gateway stand-in. - function _gatewayReserveLiteName(IDotnsPopController.LiteRegistration memory params) internal { + /// @notice Dispatches the typed `reserveLiteName` call under a mocked Root origin. + function _rootReserveLiteName(IDotnsPopController.LiteRegistration memory params) internal { params.liteLabel = _toGatewayLiteLabel(params.liteLabel); _dispatchFromRoot(abi.encodeWithSelector(SELECTOR_RESERVE_LITE_TYPED, params)); } - /// @notice Dispatches a pre-encoded `reserveLiteName` payload through the gateway. - function _gatewayReserveLiteName(bytes memory payload) internal { - _dispatchFromRoot(abi.encodeWithSelector(SELECTOR_RESERVE_LITE_BYTES, payload)); - } - - /// @notice Dispatches the typed `reserveBaseName` call through the gateway stand-in. - function _gatewayReserveBaseName(IDotnsPopController.BaseReservation memory params) internal { + /// @notice Dispatches the typed `reserveBaseName` call under a mocked Root origin. + function _rootReserveBaseName(IDotnsPopController.BaseReservation memory params) internal { params.lite.liteLabel = _toGatewayLiteLabel(params.lite.liteLabel); _dispatchFromRoot(abi.encodeWithSelector(SELECTOR_RESERVE_BASE_TYPED, params)); } - /// @notice Dispatches a pre-encoded `reserveBaseName` payload through the gateway. - function _gatewayReserveBaseName(bytes memory payload) internal { - _dispatchFromRoot(abi.encodeWithSelector(SELECTOR_RESERVE_BASE_BYTES, payload)); - } - - /// @notice Dispatches the typed `reserveBaseNameOnly` call through the gateway stand-in. - function _gatewayReserveBaseNameOnly(IDotnsPopController.BaseNameReservation memory params) + /// @notice Dispatches the typed `reserveBaseNameOnly` call under a mocked Root origin. + function _rootReserveBaseNameOnly(IDotnsPopController.BaseNameReservation memory params) internal { _dispatchFromRoot(abi.encodeWithSelector(SELECTOR_RESERVE_BASE_ONLY_TYPED, params)); } - /// @notice Dispatches a pre-encoded `reserveBaseNameOnly` payload through the gateway. - function _gatewayReserveBaseNameOnly(bytes memory payload) internal { - _dispatchFromRoot(abi.encodeWithSelector(SELECTOR_RESERVE_BASE_ONLY_BYTES, payload)); - } - - /// @notice Dispatches the typed `registerBaseName` call through the gateway stand-in. + /// @notice Dispatches the typed `registerBaseName` call under a mocked Root origin. /// @dev Normalises any LiteUsername link to its dotted form before dispatch. - function _gatewayRegisterBaseName(IDotnsPopController.FullRegistration memory params) internal { + function _rootRegisterBaseName(IDotnsPopController.FullRegistration memory params) internal { if (params.link.kind == IDotnsPopController.LinkKind.LiteUsername) { params.link.liteLabel = _toGatewayLiteLabel(params.link.liteLabel); } _dispatchFromRoot(abi.encodeWithSelector(SELECTOR_REGISTER_BASE_TYPED, params)); } - /// @notice Dispatches a pre-encoded `registerBaseName` payload through the gateway. - function _gatewayRegisterBaseName(bytes memory payload) internal { - _dispatchFromRoot(abi.encodeWithSelector(SELECTOR_REGISTER_BASE_BYTES, payload)); - } - - /// @notice Forwards `payload` to the gateway stand-in while pretending the call - /// originated from the root account. - /// @dev Reverts with the inner error data when the forwarded call fails, so + /// @notice Calls `payload` on the PoP controller under a mocked Root origin. + /// @dev Reverts with the inner error data when the call fails, so /// `vm.expectRevert` assertions remain meaningful at the test level. function _dispatchFromRoot(bytes memory payload) internal returns (bytes memory ret) { - _mockCallerIsRoot(true); + _mockOriginIsRoot(true); - (bool ok, bytes memory data) = popGateway.call(payload); + (bool ok, bytes memory data) = address(dotnsPopController).call(payload); if (!ok) { assembly { revert(add(data, 32), mload(data)) diff --git a/test/fuzz/registrar/DotnsPopControllerFuzz.t.sol b/test/fuzz/registrar/DotnsPopControllerFuzz.t.sol index bd48f03e..65530f83 100644 --- a/test/fuzz/registrar/DotnsPopControllerFuzz.t.sol +++ b/test/fuzz/registrar/DotnsPopControllerFuzz.t.sol @@ -10,7 +10,6 @@ import {IPopRules} from "../../../contracts/pop/IPopRules.sol"; import {ILabelStore} from "../../../contracts/store/ILabelStore.sol"; import {StringUtils} from "../../../contracts/utils/StringUtils.sol"; import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; -import {Vm} from "forge-std/Vm.sol"; /// @title DotnsPopControllerFuzz /// @notice Property-based tests for @custom:contract DotnsPopController. @@ -108,197 +107,6 @@ contract DotnsPopControllerFuzz is BaseDotns { } } - function testFuzz_reserveLiteName_overloads_equivalent(uint8 suffix, bytes1 keySeed) public { - suffix = uint8(bound(uint256(suffix), 0, 99)); - string memory digits = _twoDigitDecimal(uint256(suffix)); - // The gateway-facing entry point accepts the dotted `stem.digits` shape; the - // on-chain stored label is the dot-stripped flat form. - string memory liteLabelDotted = string.concat("dualli.", digits); - string memory liteLabelFlat = string.concat("dualli", digits); - bytes memory chatKey = _validChatKey(keySeed); - - _grantPopLite(ed); - - IDotnsPopController.LiteRegistration memory params = IDotnsPopController.LiteRegistration({ - liteLabel: liteLabelDotted, user: ed, chatKey: chatKey - }); - - bytes32 node = _nodeOf(liteLabelFlat); - // Snapshot the world once and run both dispatch paths from the same starting - // state; post-state and full event log must match for the typed and bytes - // overloads to be observably equivalent. - uint256 baseline = vm.snapshotState(); - - vm.recordLogs(); - vm.prank(popGateway); - dotnsPopController.reserveLiteName(params); - Vm.Log[] memory typedLogs = vm.getRecordedLogs(); - address typedOwner = IERC721(address(dotnsRegistrar)).ownerOf(uint256(node)); - bytes memory typedKey = dotnsPopResolver.chatKey(node); - - vm.revertToState(baseline); - - vm.recordLogs(); - vm.prank(popGateway); - dotnsPopController.reserveLiteName(abi.encode(params)); - Vm.Log[] memory bytesLogs = vm.getRecordedLogs(); - - assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(node)), typedOwner); - assertEq(dotnsPopResolver.chatKey(node), typedKey); - _assertLogsEqual(typedLogs, bytesLogs); - } - - function testFuzz_reserveBaseName_overloads_equivalent( - uint8 suffix, - bytes1 keySeed, - bool useReservation - ) - public - { - suffix = uint8(bound(uint256(suffix), 0, 99)); - string memory digits = _twoDigitDecimal(uint256(suffix)); - // Gateway-facing dotted shape; flat form drives the on-chain node lookup. - string memory liteLabelDotted = string.concat("dualbs.", digits); - string memory liteLabelFlat = string.concat("dualbs", digits); - bytes memory chatKey = _validChatKey(keySeed); - // `useReservation` toggles between the lite-only branch and the lite-plus- - // reservation branch so both legs of the entrypoint are exercised. - string memory reservedBase = useReservation ? BASE_LABEL_A : ""; - - // PopFull covers both legs: the lite label requires PopLite-or-Full and - // the base label (PopFull-classified) requires PopFull. - _grantPopFull(ed); - - IDotnsPopController.BaseReservation memory params = IDotnsPopController.BaseReservation({ - lite: IDotnsPopController.LiteRegistration({ - liteLabel: liteLabelDotted, user: ed, chatKey: chatKey - }), - reservedBaseLabel: reservedBase - }); - - bytes32 liteNode = _nodeOf(liteLabelFlat); - bytes32 reservedHash = useReservation ? keccak256(bytes(reservedBase)) : bytes32(0); - uint256 baseline = vm.snapshotState(); - - vm.recordLogs(); - vm.prank(popGateway); - dotnsPopController.reserveBaseName(params); - Vm.Log[] memory typedLogs = vm.getRecordedLogs(); - address typedOwner = IERC721(address(dotnsRegistrar)).ownerOf(uint256(liteNode)); - bytes memory typedKey = dotnsPopResolver.chatKey(liteNode); - IDotnsPopController.UserReservation memory typedUserRes = - dotnsPopController.userReservation(ed); - (uint64 typedHead, uint64 typedTail) = useReservation - ? dotnsPopController.reservationMeta(reservedHash) - : (uint64(0), uint64(0)); - - vm.revertToState(baseline); - - vm.recordLogs(); - vm.prank(popGateway); - dotnsPopController.reserveBaseName(abi.encode(params)); - Vm.Log[] memory bytesLogs = vm.getRecordedLogs(); - - assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(liteNode)), typedOwner); - assertEq(dotnsPopResolver.chatKey(liteNode), typedKey); - IDotnsPopController.UserReservation memory bytesUserRes = - dotnsPopController.userReservation(ed); - assertEq(bytesUserRes.labelhash, typedUserRes.labelhash); - assertEq(uint256(bytesUserRes.index), uint256(typedUserRes.index)); - if (useReservation) { - (uint64 bytesHead, uint64 bytesTail) = dotnsPopController.reservationMeta(reservedHash); - assertEq(bytesHead, typedHead); - assertEq(bytesTail, typedTail); - } - _assertLogsEqual(typedLogs, bytesLogs); - } - - function testFuzz_registerBaseName_overloads_equivalent( - uint8 suffix, - bytes1 keySeed, - bool useLiteLink - ) - public - { - suffix = uint8(bound(uint256(suffix), 0, 99)); - // Stem `longnamebob` (stem length above the PopLite ceiling) plus a 2-digit suffix - // classifies as NoStatus, the legitimate inhabitant of the base-name path. The - // `useLiteLink` toggle picks between the `None` (fresh chat key) and `LiteUsername` - // (inherit from prior lite) branches. - string memory baseLabel = string.concat("longnamebob", _twoDigitDecimal(uint256(suffix))); - bytes memory chatKey = _validChatKey(keySeed); - - _grantPopLite(ed); - - // Pre-register a lite label so the LiteUsername-link branch has a - // node to inherit a chat key from. Skipped for the None branch so - // the two branches stay isolated under fuzzing. - IDotnsPopController.Link memory link; - if (useLiteLink) { - vm.prank(popGateway); - dotnsPopController.reserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A_DOTTED, user: ed, chatKey: chatKey - }) - ); - vm.prank(ed); - dotnsPopController.settlePendingClaims(ed, type(uint256).max); - link = IDotnsPopController.Link({ - kind: IDotnsPopController.LinkKind.LiteUsername, - liteLabel: LITE_LABEL_A_DOTTED, - chatKey: "" - }); - } else { - link = IDotnsPopController.Link({ - kind: IDotnsPopController.LinkKind.None, liteLabel: "", chatKey: chatKey - }); - } - - IDotnsPopController.FullRegistration memory params = - IDotnsPopController.FullRegistration({label: baseLabel, user: ed, link: link}); - - bytes32 baseNode = _nodeOf(baseLabel); - uint256 baseline = vm.snapshotState(); - - vm.recordLogs(); - vm.prank(popGateway); - dotnsPopController.registerBaseName(params); - Vm.Log[] memory typedLogs = vm.getRecordedLogs(); - address typedOwner = IERC721(address(dotnsRegistrar)).ownerOf(uint256(baseNode)); - bytes memory typedKey = dotnsPopResolver.chatKey(baseNode); - bytes32 typedLiteLink = dotnsPopResolver.liteLink(baseNode); - - vm.revertToState(baseline); - - vm.recordLogs(); - vm.prank(popGateway); - dotnsPopController.registerBaseName(abi.encode(params)); - Vm.Log[] memory bytesLogs = vm.getRecordedLogs(); - - assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(baseNode)), typedOwner); - assertEq(dotnsPopResolver.chatKey(baseNode), typedKey); - assertEq(dotnsPopResolver.liteLink(baseNode), typedLiteLink); - // Event equivalence catches axis-classification drift between the two - // paths (e.g. one path emitting `BaseNameClaimed` while the other - // emits `StandaloneNameRegistered` would be a regression). - _assertLogsEqual(typedLogs, bytesLogs); - } - - /// @notice Assert that two recorded log arrays are element-wise identical. - /// @dev Compares count, ordering, emitter, topics and unindexed payload; any divergence - /// fails the test. - function _assertLogsEqual(Vm.Log[] memory a, Vm.Log[] memory b) internal { - assertEq(a.length, b.length, "log count mismatch"); - for (uint256 i = 0; i < a.length; ++i) { - assertEq(a[i].emitter, b[i].emitter, "log emitter mismatch"); - assertEq(a[i].topics.length, b[i].topics.length, "log topic count mismatch"); - for (uint256 t = 0; t < a[i].topics.length; ++t) { - assertEq(a[i].topics[t], b[i].topics[t], "log topic mismatch"); - } - assertEq(keccak256(a[i].data), keccak256(b[i].data), "log data mismatch"); - } - } - function testFuzz_isReservedForClaim_tracks_duration_boundary( uint64 duration, uint64 elapsed @@ -344,7 +152,7 @@ contract DotnsPopControllerFuzz is BaseDotns { bytes memory chatKey = _validChatKey(keySeed); _grantPopLite(ed); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({liteLabel: label, user: ed, chatKey: chatKey}) ); @@ -370,7 +178,7 @@ contract DotnsPopControllerFuzz is BaseDotns { bytes memory chatKey = _validChatKey(keySeed); _grantPopLite(ed); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({liteLabel: label, user: ed, chatKey: chatKey}) ); @@ -402,7 +210,7 @@ contract DotnsPopControllerFuzz is BaseDotns { dotnsPopController.setReservationDuration(duration); _grantPopLite(ed); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0x77) }) diff --git a/test/intergration/PopLifecycleFlow.t.sol b/test/intergration/PopLifecycleFlow.t.sol index 95ef722c..e94cd1ba 100644 --- a/test/intergration/PopLifecycleFlow.t.sol +++ b/test/intergration/PopLifecycleFlow.t.sol @@ -102,7 +102,7 @@ contract PopLifecycleFlow is BaseDotns { function test_cold_gateway_reserve_then_user_settles_pending_claim() public { _grantPopFull(ed); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL, user: ed, chatKey: CHAT_KEY }) @@ -136,7 +136,7 @@ contract PopLifecycleFlow is BaseDotns { function test_reserve_settle_reserve_cycle_for_same_user() public { _grantPopFull(ed); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL, user: ed, chatKey: CHAT_KEY }) @@ -160,7 +160,7 @@ contract PopLifecycleFlow is BaseDotns { string memory secondLabel = "aliceli02"; bytes memory secondKey = hex"04beefcafedeadbeefcafedeadbeefcafedeadbeefcafedeadbeefcafedeadbeefcafedeadbeefcafedeadbeefcafedeadbeefcafedeadbeefcafedeadbeefcafe"; - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: secondLabel, user: ed, chatKey: secondKey }) @@ -179,7 +179,7 @@ contract PopLifecycleFlow is BaseDotns { function test_transfer_of_token_with_live_pending_claim_does_not_move_claim() public { _grantPopFull(ed); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL, user: ed, chatKey: CHAT_KEY }) @@ -213,7 +213,7 @@ contract PopLifecycleFlow is BaseDotns { function test_lapsed_pending_claim_settles_and_deploys_store() public { _grantPopFull(ed); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL, user: ed, chatKey: CHAT_KEY }) @@ -238,7 +238,7 @@ contract PopLifecycleFlow is BaseDotns { function test_lite_via_gateway_then_full_via_public_after_upgrade() public { _grantPopLite(ed); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL, user: ed, chatKey: CHAT_KEY }) @@ -268,7 +268,7 @@ contract PopLifecycleFlow is BaseDotns { // PopFull superset) and the full-person claim. _grantPopFull(user); _reservePop(user, LITE_LABEL, CHAT_KEY, FULL_LABEL); - _gatewayRegisterBaseName( + _rootRegisterBaseName( IDotnsPopController.FullRegistration({ label: FULL_LABEL, user: user, link: _linkWithLite(LITE_LABEL) }) diff --git a/test/intergration/StoreIntegration.t.sol b/test/intergration/StoreIntegration.t.sol index c1ba451a..3150dd51 100644 --- a/test/intergration/StoreIntegration.t.sol +++ b/test/intergration/StoreIntegration.t.sol @@ -61,7 +61,7 @@ contract StoreIntegrationTest is BaseDotns { chatKey[i] = bytes1(uint8(i + 1)); } - _gatewayRegisterBaseName( + _rootRegisterBaseName( IDotnsPopController.FullRegistration({label: base, user: ed, link: _linkFresh(chatKey)}) ); diff --git a/test/invariant/registrar/PopControllerHandler.t.sol b/test/invariant/registrar/PopControllerHandler.t.sol index 9e25be52..87f16836 100644 --- a/test/invariant/registrar/PopControllerHandler.t.sol +++ b/test/invariant/registrar/PopControllerHandler.t.sol @@ -156,18 +156,8 @@ contract PopControllerHandler is Test { /// @notice Reserves a lite label for an actor, optionally enqueueing on a /// base label. /// @dev Swallows known-good reverts (QueueFull, AlreadyReserved, ERC721 - /// collision) so the runner keeps exploring. `useBytes` chooses - /// between the typed and bytes overloads so existing invariants run - /// against mixed dispatch paths. The dispatch path should affect call - /// shape only, never resulting state. - function reserve( - uint256 actorIndex, - uint256 baseIndex, - bool attachReservation, - bool useBytes - ) - external - { + /// 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]); @@ -180,7 +170,7 @@ contract PopControllerHandler is Test { reservedBaseLabel: reservedBase }); - if (_callReserveBaseName(params, useBytes)) { + if (_callReserveBaseName(params)) { if (attachReservation) _track(keccak256(bytes(reservedBase))); bytes32 node = LabelUtils.namehashUnder(TLD_NODE, LabelUtils.labelhashMemory(liteLabel)); mintedLiteTokenIds.push(uint256(node)); @@ -193,9 +183,8 @@ 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 - /// exploring. `useBytes` selects the dispatch path for both the lite /// leg and the full register leg. - function claim(uint256 actorIndex, uint256 baseIndex, bool useBytes) external { + function claim(uint256 actorIndex, uint256 baseIndex) external { address actor = _actor(actorIndex); string memory baseLabel = _baseLabel(baseIndex); @@ -212,7 +201,7 @@ contract PopControllerHandler is Test { }), reservedBaseLabel: "" }); - if (!_callReserveBaseName(liteParams, useBytes)) return; + if (!_callReserveBaseName(liteParams)) return; _trackPendingActor(actor); // The lite leg stashed a pending claim. Settle it now so the base @@ -226,7 +215,7 @@ contract PopControllerHandler is Test { }); IDotnsPopController.FullRegistration memory fullParams = IDotnsPopController.FullRegistration({label: baseLabel, user: actor, link: link}); - if (!_callRegisterBaseName(fullParams, useBytes)) return; + if (!_callRegisterBaseName(fullParams)) return; bytes32 liteLabelhash = LabelUtils.labelhashMemory(liteLabel); bytes32 fullNode = LabelUtils.namehashUnder(TLD_NODE, LabelUtils.labelhashMemory(baseLabel)); @@ -242,15 +231,7 @@ contract PopControllerHandler is Test { /// @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. - /// `useBytes` selects the dispatch path for the register call. - function reLink( - uint256 actorIndex, - uint256 baseIndex, - uint256 liteIndex, - bool useBytes - ) - external - { + function reLink(uint256 actorIndex, uint256 baseIndex, uint256 liteIndex) external { uint256 liteCount = priorLiteLabels.length; if (liteCount == 0) return; @@ -267,7 +248,7 @@ contract PopControllerHandler is Test { }); IDotnsPopController.FullRegistration memory params = IDotnsPopController.FullRegistration({label: baseLabel, user: actor, link: link}); - if (!_callRegisterBaseName(params, useBytes)) return; + if (!_callRegisterBaseName(params)) return; bytes32 liteLabelhash = LabelUtils.labelhashMemory(liteLabel); bytes32 fullNode = LabelUtils.namehashUnder(TLD_NODE, LabelUtils.labelhashMemory(baseLabel)); @@ -319,26 +300,15 @@ contract PopControllerHandler is Test { try CONTROLLER.settlePendingClaims(actor, type(uint256).max) {} catch {} } - /// @notice Calls `reserveBaseName` through the typed or bytes overload. + /// @notice Calls `reserveBaseName`. /// @dev Returns true on success and false on revert so the caller's - /// bookkeeping (ghost arrays) stays consistent with on-chain state - /// regardless of dispatch path. + /// bookkeeping (ghost arrays) stays consistent with on-chain state. /// @return ok Whether the underlying call succeeded. - function _callReserveBaseName( - IDotnsPopController.BaseReservation memory params, - bool useBytes - ) + function _callReserveBaseName(IDotnsPopController.BaseReservation memory params) internal returns (bool ok) { - _mockCallerIsRoot(true); - if (useBytes) { - try CONTROLLER.reserveBaseName(abi.encode(params)) { - return true; - } catch { - return false; - } - } + _mockOriginIsRoot(true); try CONTROLLER.reserveBaseName(params) { return true; } catch { @@ -346,24 +316,13 @@ contract PopControllerHandler is Test { } } - /// @notice Mirror of `_callReserveBaseName` for the `registerBaseName` - /// overloads. + /// @notice Mirror of `_callReserveBaseName` for `registerBaseName`. /// @return ok Whether the underlying call succeeded. - function _callRegisterBaseName( - IDotnsPopController.FullRegistration memory params, - bool useBytes - ) + function _callRegisterBaseName(IDotnsPopController.FullRegistration memory params) internal returns (bool ok) { - _mockCallerIsRoot(true); - if (useBytes) { - try CONTROLLER.registerBaseName(abi.encode(params)) { - return true; - } catch { - return false; - } - } + _mockOriginIsRoot(true); try CONTROLLER.registerBaseName(params) { return true; } catch { @@ -371,11 +330,11 @@ contract PopControllerHandler is Test { } } - /// @notice Mocks the revive `callerIsRoot()` query to return `returnValue`. - function _mockCallerIsRoot(bool returnValue) internal { + /// @notice Mocks the revive `originIsRoot()` query to return `returnValue`. + function _mockOriginIsRoot(bool returnValue) internal { vm.mockCall( DotnsConstants.REVIVE_SYSTEM, - abi.encodeWithSelector(ISystem.callerIsRoot.selector), + abi.encodeWithSelector(ISystem.originIsRoot.selector), abi.encode(returnValue) ); } diff --git a/test/unit/registrar/DotnsPopController.t.sol b/test/unit/registrar/DotnsPopController.t.sol index bb08c952..85a0dff3 100644 --- a/test/unit/registrar/DotnsPopController.t.sol +++ b/test/unit/registrar/DotnsPopController.t.sol @@ -36,10 +36,8 @@ contract DotnsPopControllerTests is BaseDotns { } function test_reserveBaseName_reverts_when_origin_is_not_root() public { - _mockCallerIsRoot(false); - vm.expectRevert( - abi.encodeWithSelector(IDotnsPopController.NotGateway.selector, address(this)) - ); + _mockOriginIsRoot(false); + vm.expectRevert(IDotnsPopController.NotRoot.selector); dotnsPopController.reserveBaseName( IDotnsPopController.BaseReservation({ lite: IDotnsPopController.LiteRegistration({ @@ -70,7 +68,7 @@ contract DotnsPopControllerTests is BaseDotns { IDotnsPopController.Link memory link = _linkWithLite(LITE_LABEL_A); vm.recordLogs(); - _gatewayRegisterBaseName( + _rootRegisterBaseName( IDotnsPopController.FullRegistration({label: BASE_LABEL_A, user: ed, link: link}) ); Vm.Log[] memory logs = vm.getRecordedLogs(); @@ -93,7 +91,7 @@ contract DotnsPopControllerTests is BaseDotns { IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0xcf)); vm.recordLogs(); - _gatewayRegisterBaseName( + _rootRegisterBaseName( IDotnsPopController.FullRegistration({label: BASE_LABEL_C, user: ed, link: link}) ); Vm.Log[] memory logs = vm.getRecordedLogs(); @@ -109,7 +107,7 @@ contract DotnsPopControllerTests is BaseDotns { _reservePop(ed, LITE_LABEL_A, liteChatKey, BASE_LABEL_A); IDotnsPopController.Link memory link = _linkWithLite(LITE_LABEL_A); - _gatewayRegisterBaseName( + _rootRegisterBaseName( IDotnsPopController.FullRegistration({label: BASE_LABEL_A, user: ed, link: link}) ); @@ -127,7 +125,7 @@ contract DotnsPopControllerTests is BaseDotns { _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), BASE_LABEL_A); IDotnsPopController.Link memory link = _linkWithLite(LITE_LABEL_A); - _gatewayRegisterBaseName( + _rootRegisterBaseName( IDotnsPopController.FullRegistration({label: BASE_LABEL_A, user: ed, link: link}) ); @@ -144,7 +142,7 @@ contract DotnsPopControllerTests is BaseDotns { IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0x02)); - _gatewayRegisterBaseName( + _rootRegisterBaseName( IDotnsPopController.FullRegistration({label: BASE_LABEL_B, user: ed, link: link}) ); @@ -160,7 +158,7 @@ contract DotnsPopControllerTests is BaseDotns { IDotnsPopController.Link memory link = _linkWithLite(LITE_LABEL_A); vm.recordLogs(); - _gatewayRegisterBaseName( + _rootRegisterBaseName( IDotnsPopController.FullRegistration({label: BASE_LABEL_B, user: ed, link: link}) ); Vm.Log[] memory logs = vm.getRecordedLogs(); @@ -180,10 +178,8 @@ contract DotnsPopControllerTests is BaseDotns { function test_registerBaseName_reverts_when_origin_is_not_root() public { IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0xaa)); - _mockCallerIsRoot(false); - vm.expectRevert( - abi.encodeWithSelector(IDotnsPopController.NotGateway.selector, address(this)) - ); + _mockOriginIsRoot(false); + vm.expectRevert(IDotnsPopController.NotRoot.selector); dotnsPopController.registerBaseName( IDotnsPopController.FullRegistration({label: BASE_LABEL_A, user: ed, link: link}) ); @@ -299,7 +295,7 @@ contract DotnsPopControllerTests is BaseDotns { // `_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); - _gatewayRegisterBaseName( + _rootRegisterBaseName( IDotnsPopController.FullRegistration({label: baseStem, user: ed, link: link}) ); @@ -323,7 +319,7 @@ contract DotnsPopControllerTests is BaseDotns { vm.warp(block.timestamp + dotnsPopController.reservationDuration() + 1); // Anyone can call. Pinning this prevents a future patch from silently - // adding `onlyGateway` and breaking permissionless garbage collection. + // adding `onlyRoot` and breaking permissionless garbage collection. address stranger = makeAddr("stranger"); vm.prank(stranger); dotnsPopController.expireReservation(BASE_LABEL_A); @@ -359,7 +355,7 @@ contract DotnsPopControllerTests is BaseDotns { // classifies as PopFull and ed is PopFull; the isLitePersonLabel // guard then rejects the zero trailing digits. vm.expectRevert(IDotnsPopController.InvalidLiteLabel.selector); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({liteLabel: "aliceli", user: ed, chatKey: ""}) ); @@ -368,7 +364,7 @@ contract DotnsPopControllerTests is BaseDotns { // PopRules.priceWithCheck before reaching the controller's own // isSingleLabel check. vm.expectPartialRevert(IPopRules.PopError.selector); - _gatewayRegisterBaseName( + _rootRegisterBaseName( IDotnsPopController.FullRegistration({label: "not.valid", user: ed, link: link}) ); } @@ -380,7 +376,7 @@ contract DotnsPopControllerTests is BaseDotns { // and shares the lite's stem, so both tokens coexist on the registrar. _grantPopFull(tiago); IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0xbb)); - _gatewayRegisterBaseName( + _rootRegisterBaseName( IDotnsPopController.FullRegistration({label: "aliceli", user: tiago, link: link}) ); @@ -453,7 +449,7 @@ contract DotnsPopControllerTests is BaseDotns { IDotnsRegistrar.NameNotAvailable.selector, uint256(_nodeOf(LITE_LABEL_A)) ) ); - _gatewayReserveBaseName( + _rootReserveBaseName( IDotnsPopController.BaseReservation({ lite: IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL_A, user: tiago, chatKey: _validChatKey(0xbb) @@ -466,7 +462,7 @@ contract DotnsPopControllerTests is BaseDotns { function test_public_register_after_pop_full_mint_reverts_at_registrar() public { // "longnamebob01" is classification-NoStatus, so ed keeps default status. IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0xcf)); - _gatewayRegisterBaseName( + _rootRegisterBaseName( IDotnsPopController.FullRegistration({label: "longnamebob01", user: ed, link: link}) ); @@ -501,7 +497,7 @@ contract DotnsPopControllerTests is BaseDotns { function test_owner_of_pop_minted_name_can_create_subname() public { _grantPopFull(ed); IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0xcf)); - _gatewayRegisterBaseName( + _rootRegisterBaseName( IDotnsPopController.FullRegistration({label: BASE_LABEL_A, user: ed, link: link}) ); @@ -519,7 +515,7 @@ contract DotnsPopControllerTests is BaseDotns { function test_non_owner_cannot_create_subname_under_pop_minted_name() public { _grantPopFull(ed); IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0xcf)); - _gatewayRegisterBaseName( + _rootRegisterBaseName( IDotnsPopController.FullRegistration({label: BASE_LABEL_A, user: ed, link: link}) ); @@ -567,7 +563,7 @@ contract DotnsPopControllerTests is BaseDotns { _reservePop(ed, LITE_LABEL_A, _validChatKey(0xaa), "longnamebob"); IDotnsPopController.Link memory link = _linkWithLite(LITE_LABEL_A); - _gatewayRegisterBaseName( + _rootRegisterBaseName( IDotnsPopController.FullRegistration({label: "longnamebob", user: ed, link: link}) ); @@ -610,7 +606,7 @@ contract DotnsPopControllerTests is BaseDotns { // would lock every two-digit variant of the stem for the full reservation window. _grantPopFull(ed); _reservePop(ed, LITE_LABEL_A, _validChatKey(0xaa), "longnamebob"); - _gatewayRegisterBaseName( + _rootRegisterBaseName( IDotnsPopController.FullRegistration({ label: "longnamebob", user: ed, link: _linkWithLite(LITE_LABEL_A) }) @@ -626,7 +622,7 @@ contract DotnsPopControllerTests is BaseDotns { _reservePop(ed, LITE_LABEL_A, _validChatKey(0xaa), "longnamebob"); IDotnsPopController.Link memory link = _linkWithLite(LITE_LABEL_A); - _gatewayRegisterBaseName( + _rootRegisterBaseName( IDotnsPopController.FullRegistration({label: "longnamebob", user: ed, link: link}) ); // Now the stem is clear on PopRules, so tiago can register the @@ -646,16 +642,12 @@ contract DotnsPopControllerTests is BaseDotns { assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(_nodeOf("longnamebob01"))), tiago); } - function test_controller_authorised_but_not_gateway_cannot_enter_pop_flow() public { + function test_registered_controller_without_root_origin_cannot_enter_pop_flow() public { // The public commit-reveal controller is already a registered controller. // Even from that origin, the Root-gate must reject the call. - _mockCallerIsRoot(false); + _mockOriginIsRoot(false); vm.prank(address(dotnsRegistrarController)); - vm.expectRevert( - abi.encodeWithSelector( - IDotnsPopController.NotGateway.selector, address(dotnsRegistrarController) - ) - ); + vm.expectRevert(IDotnsPopController.NotRoot.selector); dotnsPopController.reserveBaseName( IDotnsPopController.BaseReservation({ lite: IDotnsPopController.LiteRegistration({ @@ -712,7 +704,7 @@ contract DotnsPopControllerTests is BaseDotns { _grantPopFull(tiago); IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0xcf)); - _gatewayRegisterBaseName( + _rootRegisterBaseName( IDotnsPopController.FullRegistration({label: BASE_LABEL_A, user: tiago, link: link}) ); @@ -723,7 +715,7 @@ contract DotnsPopControllerTests is BaseDotns { _grantPopFull(ed); IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0xcf)); - _gatewayRegisterBaseName( + _rootRegisterBaseName( IDotnsPopController.FullRegistration({label: BASE_LABEL_A, user: ed, link: link}) ); @@ -735,7 +727,7 @@ contract DotnsPopControllerTests is BaseDotns { _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), BASE_LABEL_A); IDotnsPopController.Link memory link = _linkWithLite(LITE_LABEL_A); - _gatewayRegisterBaseName( + _rootRegisterBaseName( IDotnsPopController.FullRegistration({label: BASE_LABEL_A, user: ed, link: link}) ); @@ -749,14 +741,14 @@ contract DotnsPopControllerTests is BaseDotns { _grantPopFull(tiago); IDotnsPopController.Link memory strangerLink = _linkFresh(_validChatKey(0xbb)); vm.expectPartialRevert(IDotnsPopController.NotHolder.selector); - _gatewayRegisterBaseName( + _rootRegisterBaseName( IDotnsPopController.FullRegistration({ label: BASE_LABEL_A, user: tiago, link: strangerLink }) ); // A's reservation is intact; A claims successfully. IDotnsPopController.Link memory claimLink = _linkWithLite(LITE_LABEL_A); - _gatewayRegisterBaseName( + _rootRegisterBaseName( IDotnsPopController.FullRegistration({label: BASE_LABEL_A, user: ed, link: claimLink}) ); @@ -771,7 +763,7 @@ contract DotnsPopControllerTests is BaseDotns { // which the PoP controller's governance guard rejects. vm.expectRevert(IDotnsPopController.InvalidBaseLabel.selector); - _gatewayRegisterBaseName( + _rootRegisterBaseName( IDotnsPopController.FullRegistration({label: "alice", user: ed, link: link}) ); } @@ -783,7 +775,7 @@ contract DotnsPopControllerTests is BaseDotns { // which classifies as `Reserved for Governance` and is rejected by the // PoP controller's governance guard. vm.expectRevert(IDotnsPopController.InvalidBaseLabel.selector); - _gatewayReserveBaseName( + _rootReserveBaseName( IDotnsPopController.BaseReservation({ lite: IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) @@ -799,7 +791,7 @@ contract DotnsPopControllerTests is BaseDotns { uint256 controllerBalanceBefore = address(dotnsPopController).balance; IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0xaa)); - _gatewayRegisterBaseName( + _rootRegisterBaseName( IDotnsPopController.FullRegistration({label: BASE_LABEL_A, user: ed, link: link}) ); // No native token moves on the PoP path. @@ -816,7 +808,7 @@ contract DotnsPopControllerTests is BaseDotns { address fresh = makeAddr("freshLite"); _grantPopLite(fresh); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: "freshli01", user: fresh, chatKey: _validChatKey(0xcc) }) @@ -829,7 +821,7 @@ contract DotnsPopControllerTests is BaseDotns { _grantPopFull(ed); vm.expectRevert(IDotnsPopController.InvalidLiteLabel.selector); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: "alice", user: ed, chatKey: _validChatKey(0xaa) }) @@ -840,7 +832,7 @@ contract DotnsPopControllerTests is BaseDotns { _grantPopFull(ed); vm.expectRevert(IDotnsPopController.InvalidLiteLabel.selector); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: "aliceli.001", user: ed, chatKey: _validChatKey(0xaa) }) @@ -853,7 +845,7 @@ contract DotnsPopControllerTests is BaseDotns { // `abcd.12` flattens to `abcd12`: base length 4 classifies as Reserved (governance), so the // gateway lite path still rejects it even though the dotted format is valid. vm.expectRevert(IDotnsPopController.InvalidLiteLabel.selector); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: "abcd.12", user: ed, chatKey: _validChatKey(0xaa) }) @@ -865,7 +857,7 @@ contract DotnsPopControllerTests is BaseDotns { // `andrewsays.01` flattens to `andrewsays01`: base length 10 classifies as NoStatus, which // the gateway may issue as a lite username regardless of stem length. - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: "andrewsays.01", user: ed, chatKey: _validChatKey(0xaa) }) @@ -875,10 +867,8 @@ contract DotnsPopControllerTests is BaseDotns { } function test_reserveLiteName_reverts_when_origin_is_not_root() public { - _mockCallerIsRoot(false); - vm.expectRevert( - abi.encodeWithSelector(IDotnsPopController.NotGateway.selector, address(this)) - ); + _mockOriginIsRoot(false); + vm.expectRevert(IDotnsPopController.NotRoot.selector); dotnsPopController.reserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) @@ -889,7 +879,7 @@ contract DotnsPopControllerTests is BaseDotns { function test_reserveBaseName_lite_and_base_legs_both_succeed_in_one_call() public { _grantPopFull(ed); - _gatewayReserveBaseName( + _rootReserveBaseName( IDotnsPopController.BaseReservation({ lite: IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) @@ -908,7 +898,7 @@ contract DotnsPopControllerTests is BaseDotns { function test_split_gateway_flow_mints_lite_then_reserves_base() public { _grantPopFull(ed); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) }) @@ -917,7 +907,7 @@ contract DotnsPopControllerTests is BaseDotns { assertEq(IERC721(address(dotnsRegistrar)).ownerOf(uint256(_nodeOf(LITE_LABEL_A))), ed); assertFalse(dotnsRegistrar.exists(uint256(_nodeOf(BASE_LABEL_A)))); - _gatewayReserveBaseNameOnly( + _rootReserveBaseNameOnly( IDotnsPopController.BaseNameReservation({user: ed, reservedBaseLabel: BASE_LABEL_A}) ); @@ -927,11 +917,9 @@ contract DotnsPopControllerTests is BaseDotns { assertFalse(dotnsRegistrar.exists(uint256(_nodeOf(BASE_LABEL_A)))); } - function test_reserveBaseNameOnly_reverts_for_non_gateway() public { - _mockCallerIsRoot(false); - vm.expectRevert( - abi.encodeWithSelector(IDotnsPopController.NotGateway.selector, address(this)) - ); + function test_reserveBaseNameOnly_reverts_when_origin_is_not_root() public { + _mockOriginIsRoot(false); + vm.expectRevert(IDotnsPopController.NotRoot.selector); dotnsPopController.reserveBaseNameOnly( IDotnsPopController.BaseNameReservation({user: ed, reservedBaseLabel: BASE_LABEL_A}) ); @@ -939,12 +927,12 @@ contract DotnsPopControllerTests is BaseDotns { function test_reserveBaseNameOnly_reverts_for_reserved_or_suffixed_labels() public { vm.expectRevert(IDotnsPopController.InvalidBaseLabel.selector); - _gatewayReserveBaseNameOnly( + _rootReserveBaseNameOnly( IDotnsPopController.BaseNameReservation({user: ed, reservedBaseLabel: "alice"}) ); vm.expectRevert(IDotnsPopController.InvalidBaseLabel.selector); - _gatewayReserveBaseNameOnly( + _rootReserveBaseNameOnly( IDotnsPopController.BaseNameReservation({user: ed, reservedBaseLabel: "longnamebob01"}) ); } @@ -955,20 +943,20 @@ contract DotnsPopControllerTests is BaseDotns { // front rather than discovered to be unusable at claim time. _grantPopFull(ed); _reservePop(ed, LITE_LABEL_A, _validChatKey(0xaa), "longnamebob"); - _gatewayRegisterBaseName( + _rootRegisterBaseName( IDotnsPopController.FullRegistration({ label: "longnamebob", user: ed, link: _linkWithLite(LITE_LABEL_A) }) ); vm.expectRevert(IDotnsPopController.BaseNameAlreadyRegistered.selector); - _gatewayReserveBaseNameOnly( + _rootReserveBaseNameOnly( IDotnsPopController.BaseNameReservation({user: tiago, reservedBaseLabel: "longnamebob"}) ); } function test_reserveBaseNameOnly_does_not_mint_lite_or_base_name() public { - _gatewayReserveBaseNameOnly( + _rootReserveBaseNameOnly( IDotnsPopController.BaseNameReservation({user: ed, reservedBaseLabel: BASE_LABEL_A}) ); @@ -981,10 +969,10 @@ contract DotnsPopControllerTests is BaseDotns { } function test_reserveBaseNameOnly_same_user_can_replace_prior_reservation() public { - _gatewayReserveBaseNameOnly( + _rootReserveBaseNameOnly( IDotnsPopController.BaseNameReservation({user: ed, reservedBaseLabel: BASE_LABEL_A}) ); - _gatewayReserveBaseNameOnly( + _rootReserveBaseNameOnly( IDotnsPopController.BaseNameReservation({user: ed, reservedBaseLabel: BASE_LABEL_B}) ); @@ -1002,7 +990,7 @@ contract DotnsPopControllerTests is BaseDotns { // stashed label. The settled name lands in the beneficiary's store, and the settlement // event records the third party as the settler. _grantPopFull(ed); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) }) @@ -1027,7 +1015,7 @@ contract DotnsPopControllerTests is BaseDotns { function test_user_settles_own_pending_claim_after_gateway_mint() public { _grantPopFull(ed); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) }) @@ -1046,7 +1034,7 @@ contract DotnsPopControllerTests is BaseDotns { function test_claimLabelStore_settles_callers_own_pending_claim() public { _grantPopFull(ed); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) }) @@ -1068,7 +1056,7 @@ contract DotnsPopControllerTests is BaseDotns { function test_settle_deploys_store_when_user_has_none() public { _grantPopFull(ed); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) }) @@ -1097,7 +1085,7 @@ contract DotnsPopControllerTests is BaseDotns { // Classification runs first; empty string fails canonical label check // in PopRules before reaching the PoP controller's own shape guard. vm.expectPartialRevert(IPopRules.PopError.selector); - _gatewayRegisterBaseName( + _rootRegisterBaseName( IDotnsPopController.FullRegistration({label: "", user: ed, link: link}) ); } @@ -1107,7 +1095,7 @@ contract DotnsPopControllerTests is BaseDotns { bytes memory chatKey = _validChatKey(0x42); - _gatewayReserveBaseName( + _rootReserveBaseName( IDotnsPopController.BaseReservation({ lite: IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL_A, user: ed, chatKey: chatKey @@ -1123,64 +1111,6 @@ contract DotnsPopControllerTests is BaseDotns { assertEq(dotnsPopResolver.chatKey(node), chatKey); } - function testFuzz_bytes_overloads_reject_non_root_origin(uint8 which) public { - // `which` selects which of the three bytes overloads to invoke; the - // `onlyGateway` modifier must reject a non-Root origin on each. - // Single fuzz replaces three near-identical unit tests. - which = uint8(bound(uint256(which), 0, 2)); - - _mockCallerIsRoot(false); - vm.expectRevert( - abi.encodeWithSelector(IDotnsPopController.NotGateway.selector, address(this)) - ); - - if (which == 0) { - dotnsPopController.reserveLiteName( - abi.encode( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) - }) - ) - ); - } else if (which == 1) { - dotnsPopController.reserveBaseName( - abi.encode( - IDotnsPopController.BaseReservation({ - lite: IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0xaa) - }), - reservedBaseLabel: "" - }) - ) - ); - } else { - dotnsPopController.registerBaseName( - abi.encode( - IDotnsPopController.FullRegistration({ - label: BASE_LABEL_A, - user: ed, - link: IDotnsPopController.Link({ - kind: IDotnsPopController.LinkKind.None, - liteLabel: "", - chatKey: _validChatKey(0xaa) - }) - }) - ) - ); - } - } - - function test_reserveLiteName_bytes_reverts_on_malformed_payload() public { - // Truncated payload cannot be ABI-decoded into the target struct, so the - // typed entrypoint reverts inside `abi.decode` (panic-style, no return - // data); `_dispatchTyped` re-throws via assembly so the outer call - // surfaces the same empty revert. Asserting "any revert" is intentional; - // locking the exact error string would couple the test to solc internals. - bytes memory truncated = hex"deadbeef"; - vm.expectRevert(); - _gatewayReserveLiteName(truncated); - } - function test_revert_setReservationDuration_below_minimum() public { // The setter enforces a floor so a single owner call cannot retroactively // expire every live queue and pending-claim entry. @@ -1204,7 +1134,7 @@ contract DotnsPopControllerTests is BaseDotns { _grantPopFull(ed); bytes memory chatKey = _validChatKey(0x01); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL_A, user: ed, chatKey: chatKey }) @@ -1227,7 +1157,7 @@ contract DotnsPopControllerTests is BaseDotns { _grantPopFull(ed); bytes memory chatKey = _validChatKey(0x07); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL_A, user: ed, chatKey: chatKey }) @@ -1252,7 +1182,7 @@ contract DotnsPopControllerTests is BaseDotns { _grantPopFull(ed); bytes memory chatKey = _validChatKey(0x03); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL_A, user: ed, chatKey: chatKey }) @@ -1286,17 +1216,17 @@ contract DotnsPopControllerTests is BaseDotns { // block gas limit. Settling with a limit below the queue length reports the residue and a // follow-up call clears it. _grantPopFull(ed); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0x05) }) ); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL_B, user: ed, chatKey: _validChatKey(0x06) }) ); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL_C, user: ed, chatKey: _validChatKey(0x07) }) @@ -1323,7 +1253,7 @@ contract DotnsPopControllerTests is BaseDotns { // the label is written, the queue empties, the beneficiary leaves the enumeration set, and // the settler is recorded on the event. _grantPopFull(ed); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0x02) }) @@ -1358,7 +1288,7 @@ contract DotnsPopControllerTests is BaseDotns { // reservation duration and settling writes the label into the store rather than // discarding it. _grantPopFull(ed); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0x04) }) @@ -1384,12 +1314,12 @@ contract DotnsPopControllerTests is BaseDotns { // accumulating deferred names instead of reverting; a single signed-origin // settlement writes them all at once. _grantPopFull(ed); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0x05) }) ); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL_B, user: ed, chatKey: _validChatKey(0x06) }) @@ -1430,7 +1360,7 @@ contract DotnsPopControllerTests is BaseDotns { // 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); - _gatewayReserveBaseName( + _rootReserveBaseName( IDotnsPopController.BaseReservation({ lite: IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0x31) @@ -1441,7 +1371,7 @@ contract DotnsPopControllerTests is BaseDotns { assertEq(storeFactory.getLabelStore(ed), address(0)); assertEq(dotnsPopController.pendingClaimCountOf(ed), 1); - _gatewayRegisterBaseName( + _rootRegisterBaseName( IDotnsPopController.FullRegistration({ label: BASE_LABEL_A, user: ed, link: _linkWithLite(LITE_LABEL_A) }) @@ -1474,17 +1404,17 @@ contract DotnsPopControllerTests is BaseDotns { _grantPopFull(tiago); _grantPopFull(leonardo); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0x01) }) ); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL_B, user: tiago, chatKey: _validChatKey(0x02) }) ); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL_C, user: leonardo, chatKey: _validChatKey(0x03) }) @@ -1511,7 +1441,7 @@ contract DotnsPopControllerTests is BaseDotns { function test_pendingClaimUsers_returns_empty_when_offset_past_count() public { _grantPopFull(ed); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0x01) }) @@ -1525,7 +1455,7 @@ contract DotnsPopControllerTests is BaseDotns { // 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); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0x11) }) @@ -1551,7 +1481,7 @@ contract DotnsPopControllerTests is BaseDotns { // stash is a no-op and does not disturb another user's pending claim. _grantPopFull(ed); bytes memory chatKey = _validChatKey(0x12); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL_A, user: ed, chatKey: chatKey }) @@ -1576,17 +1506,17 @@ contract DotnsPopControllerTests is BaseDotns { _grantPopFull(ed); _grantPopFull(tiago); _grantPopFull(leonardo); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0x01) }) ); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL_B, user: tiago, chatKey: _validChatKey(0x02) }) ); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL_C, user: leonardo, chatKey: _validChatKey(0x03) }) @@ -1603,7 +1533,7 @@ contract DotnsPopControllerTests is BaseDotns { function test_settle_with_empty_chat_key_skips_resolver_write() public { _grantPopFull(ed); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({liteLabel: LITE_LABEL_A, user: ed, chatKey: ""}) ); @@ -1621,7 +1551,7 @@ contract DotnsPopControllerTests is BaseDotns { function test_gatewayReserve_warm_user_after_settle_writes_directly_without_stashing() public { _grantPopFull(ed); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0x21) }) @@ -1633,7 +1563,7 @@ contract DotnsPopControllerTests is BaseDotns { assertTrue(store != address(0)); bytes memory secondChatKey = _validChatKey(0x22); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL_B, user: ed, chatKey: secondChatKey }) @@ -1694,7 +1624,7 @@ contract DotnsPopControllerTests is BaseDotns { IDotnsPopController.Link memory link = _linkFresh(_validChatKey(0xbb)); vm.expectPartialRevert(IDotnsPopController.NotHolder.selector); - _gatewayRegisterBaseName( + _rootRegisterBaseName( IDotnsPopController.FullRegistration({label: BASE_LABEL_A, user: tiago, link: link}) ); } @@ -1705,14 +1635,14 @@ contract DotnsPopControllerTests is BaseDotns { // returns empty lists and zero counts. _grantPopFull(ed); _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), ""); - _gatewayRegisterBaseName( + _rootRegisterBaseName( IDotnsPopController.FullRegistration({ label: BASE_LABEL_A, user: ed, link: _linkFresh(_validChatKey(0x02)) }) ); _grantPopFull(leonardo); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL_C, user: leonardo, chatKey: _validChatKey(0x03) }) @@ -1753,7 +1683,7 @@ contract DotnsPopControllerTests is BaseDotns { function test_liteNamesOf_pagination_slices_and_clamps() public { _grantPopFull(ed); _reservePop(ed, LITE_LABEL_A, _validChatKey(0x01), ""); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL_B, user: ed, chatKey: _validChatKey(0x02) }) @@ -1798,7 +1728,7 @@ contract DotnsPopControllerTests is BaseDotns { _grantPopFull(ed); bytes memory liteChatKey = _validChatKey(0xaa); _reservePop(ed, LITE_LABEL_A, liteChatKey, BASE_LABEL_A); - _gatewayRegisterBaseName( + _rootRegisterBaseName( IDotnsPopController.FullRegistration({ label: BASE_LABEL_A, user: ed, link: _linkWithLite(LITE_LABEL_A) }) @@ -1848,7 +1778,7 @@ contract DotnsPopControllerTests is BaseDotns { // 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); - _gatewayReserveLiteName( + _rootReserveLiteName( IDotnsPopController.LiteRegistration({ liteLabel: LITE_LABEL_C, user: leonardo, chatKey: _validChatKey(0x01) }) diff --git a/test/unit/registrar/RootGatewayDispatcher.t.sol b/test/unit/registrar/RootGatewayDispatcher.t.sol deleted file mode 100644 index f7f08423..00000000 --- a/test/unit/registrar/RootGatewayDispatcher.t.sol +++ /dev/null @@ -1,164 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.34; - -import {BaseDotns} from "../../base/BaseDotns.t.sol"; -import {IDotnsPopController} from "../../../contracts/registrars/IDotnsPopController.sol"; -import {RootGatewayDispatcher} from "../../../contracts/registrars/RootGatewayDispatcher.sol"; -import {DotnsConstants} from "../../../contracts/utils/DotnsConstants.sol"; - -/// @title RootGatewayDispatcherTests -/// @notice Unit coverage for the non-upgradeable shim that converts revive -/// Root-origin dispatches into the immediate-caller predicate the -/// controller authorises against. -contract RootGatewayDispatcherTests is BaseDotns { - /// @notice Selector of the typed `reserveLiteName(LiteRegistration)` - /// overload, computed explicitly so calldata construction does not - /// collide with the sibling `(bytes)` overload that the cross-chain - /// payload entrypoint uses. - bytes4 internal constant _RESERVE_LITE_TYPED_SELECTOR = - bytes4(keccak256("reserveLiteName((string,address,bytes))")); - - /// @notice Dispatcher under test, deployed in `setUp` and bound to the - /// live controller proxy. - RootGatewayDispatcher internal dispatcher; - - /// @notice Deploys the dispatcher bound to the already-deployed controller - /// proxy and rebinds the protocol registry's gateway slot so the - /// precompile path is exercised end-to-end. Defaults the System - /// precompile to "not Root" so each test opts in explicitly. - function setUp() public override { - super.setUp(); - - dispatcher = new RootGatewayDispatcher(address(dotnsPopController)); - vm.prank(owner); - protocolRegistry.set(DotnsConstants.POP_GATEWAY, address(dispatcher)); - - _mockCallerIsRoot(false); - } - - function test_dispatcher_target_is_immutable_and_points_to_controller() public view { - assertEq(dispatcher.TARGET(), address(dotnsPopController)); - } - - function test_dispatcher_reverts_when_caller_is_not_root() public { - _grantPopFull(ed); - bytes memory payload = abi.encodeWithSelector( - _RESERVE_LITE_TYPED_SELECTOR, - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0x01) - }) - ); - - vm.expectRevert(RootGatewayDispatcher.NotRoot.selector); - // `expectRevert` asserts the failure shape; the return tuple is - // intentionally discarded. - // solhint-disable-next-line no-unused-vars - (bool ok,) = address(dispatcher).call(payload); - ok; - } - - function test_dispatcher_rejects_value_transfers() public { - _mockCallerIsRoot(true); - vm.deal(address(this), 1 ether); - - // Non-payable fallback rejects any non-zero value transfer before the - // precompile check runs; the revert carries Solidity's default empty - // payload, not NotRoot, so we just assert the call fails. - (bool ok,) = address(dispatcher).call{value: 1 wei}(""); - assertFalse(ok); - } - - function test_dispatcher_forwards_to_controller_when_root() public { - _mockCallerIsRoot(true); - _grantPopFull(ed); - - bytes memory payload = abi.encodeWithSelector( - _RESERVE_LITE_TYPED_SELECTOR, - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A_DOTTED, user: ed, chatKey: _validChatKey(0x01) - }) - ); - - (bool ok,) = address(dispatcher).call(payload); - assertTrue(ok); - - bytes32 node = _nodeOf(LITE_LABEL_A); - assertEq(dotnsRegistry.owner(node), ed); - } - - function test_dispatcher_bubbles_controller_revert_data() public { - _mockCallerIsRoot(true); - // The flat lite-label `LITE_LABEL_A` does not satisfy the gateway-facing - // `stem.digits` shape, so the controller reverts with InvalidLiteLabel. - // The dispatcher must surface that revert verbatim rather than masking it - // as NotRoot. - bytes memory payload = abi.encodeWithSelector( - _RESERVE_LITE_TYPED_SELECTOR, - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0x01) - }) - ); - - (bool ok, bytes memory ret) = address(dispatcher).call(payload); - assertFalse(ok); - // The controller's revert payload propagates: it is not the - // dispatcher's NotRoot selector. - bytes4 sel; - assembly { - sel := mload(add(ret, 32)) - } - assertTrue(sel != RootGatewayDispatcher.NotRoot.selector); - } - - function test_controller_authorises_call_from_dispatcher_address() public { - // Dispatcher path: the precompile reports non-Root from inside the - // controller's proxy implementation frame, but the dispatcher acting - // as the immediate caller carries the call through the controller's - // gateway check. - _grantPopFull(ed); - - vm.prank(address(dispatcher)); - dotnsPopController.reserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A_DOTTED, user: ed, chatKey: _validChatKey(0x01) - }) - ); - - bytes32 node = _nodeOf(LITE_LABEL_A); - assertEq(dotnsRegistry.owner(node), ed); - } - - function test_controller_rejects_unknown_msg_sender_when_not_root() public { - // Caller is neither the registered gateway nor a Root-origin - // dispatch, so the gateway check rejects it. - vm.expectRevert( - abi.encodeWithSelector(IDotnsPopController.NotGateway.selector, address(this)) - ); - dotnsPopController.reserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0x01) - }) - ); - } - - function test_rotating_pop_gateway_key_revokes_old_dispatcher() public { - address newGateway = address(0xBEEF); - - vm.prank(owner); - protocolRegistry.set(DotnsConstants.POP_GATEWAY, newGateway); - - assertEq(protocolRegistry.get(DotnsConstants.POP_GATEWAY), newGateway); - - // Old dispatcher no longer authorised. - _grantPopFull(ed); - vm.prank(address(dispatcher)); - vm.expectRevert( - abi.encodeWithSelector(IDotnsPopController.NotGateway.selector, address(dispatcher)) - ); - dotnsPopController.reserveLiteName( - IDotnsPopController.LiteRegistration({ - liteLabel: LITE_LABEL_A, user: ed, chatKey: _validChatKey(0x01) - }) - ); - } -}