paritytech / paritytech/dotns

[Bug]: Root-origin gates are transaction-wide, so any contract a Root call reaches inherits DotNS governance

Open
#300 0 comments 0 reactions 1 assignee View on GitHub

@GHkrishna is already working on this.

Since Sep 11, 2026.

P1 security type: bug
Dominant language
Solidity
Stars
4
Forks
2
Avg merge
1d 18h
Merged PRs (30d)
24

Description

Component

Other

Priority

P1

What happened?

Four contracts gate governance on SystemUtils.originIsRoot(): DotnsRegistrarController.sol:296, DotnsPopController.sol:936, PopRules.sol:90, DotnsNameWhitelist.sol:109.

originIsRoot reads the transaction origin, not the caller (upstream it is self.origin == Origin::Root from polkadot-sdk substrate/frame/revive/src/exec.rs:2431), with no frame comparison. The authority is therefore ambient for the whole call stack, so any code reached during a Root transaction holds full DotNS governance without holding any DotNS key. The trigger need not involve DotNS: any Root Revive.call into any Asset Hub contract is enough.

That grants the whitelist's ten onlyGovernance functions, setShortNamesEnabled, the whole PoP issuance surface, and registerReserved's Root branch, which skips both isGrantedTo and consume and passes 0 as the charge — any available label to any owner for free, including the 3-to-8 band the priced path is closed out of. PoP names are soulbound, so issuance is permanent without a registrar upgrade.

Expected behavior

Governance authority should require the Root dispatch to be aimed at DotNS, not merely that a Root transaction is somewhere on the stack.

DEPLOYMENTS.md:290 states the stronger guarantee today: "No signed account can do any of them, the contract owner included" and "no single key can grant a name; a grant costs a referendum", and neither holds while the gate is transaction-wide.

Reproduction

No path inside DotNS produces the trigger; it needs a Root dispatch reaching code governance does not control.

Additional context

Swapping to callerIsRoot() does not build: caller_is_origin is false across a UUPS delegatecall frame, so it returns false even on a direct one-hop Root dispatch. That is why originIsRoot was chosen, and upstream designates it for the proxy pattern.

R1: the code fix is not a launch blocker, since all four contracts are UUPS and the gate can be tightened by owner upgrade. Correcting DEPLOYMENTS.md:290 does block R1: shipping a guarantee we know to be false is worse than shipping the defect.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.