---
eip: 8107
title: ENS Trust Registry for Agent Coordination
description: Web of trust validation using ENS names for ERC-8001 multi-party coordination
author: Kwame Bryan (@KBryan)
discussions-to: https://ethereum-magicians.org/t/erc-ens-trust-registry-for-agent-coordination/27200
status: Review
type: Standards Track
category: ERC
created: 2025-12-16
requires: 137, 165, 712, 1271, 5267, 8001
---

## Abstract

This ERC defines a **Trust Registry** that enables agents to establish and query transitive trust relationships using ENS names as identifiers. Trust is expressed at four levels (Unknown, None, Marginal, Full) and propagates along certification paths, adopting the trust levels and path model of the OpenPGP web of trust described in [RFC 9580](https://www.rfc-editor.org/rfc/rfc9580.html), which obsoletes RFC 4880. Unlike OpenPGP, path verification checks a single path against a minimum per-edge trust level and does not accumulate marginal attestations.

The registry serves as the **trust and delegation module** anticipated by [ERC-8001](./eip-8001.md), enabling coordinators to gate participation based on trust graph proximity. An agent is considered valid from a coordinator's perspective if a verified trust path exists from the gate's gatekeeper to the agent that satisfies the gate's parameters, and the gatekeeper has not denied any agent on that path.

The registry keeps two independent judgements per relationship. An **endorsement** (a trust level) is the trustor's own vouching for the trustee, and withdrawing it removes only that edge. A **denial** is the trustor's refusal to rely on the trustee at all, which overrides endorsements by other agents within the trustor's own trust decisions. Endorsements are signed by the name controller and may be withdrawn by its approved operators; denials are managed by the name controller alone.

This standard specifies trust attestation structures, the path verification algorithm, ENS integration semantics, and [ERC-8001](./eip-8001.md) coordination hooks.

## Motivation

[ERC-8001](./eip-8001.md) defines minimal primitives for multi-party agent coordination and defers everything above them, including reputation, to modules. Its Security Considerations name the gap directly:

> "Equivocation: A participant can sign conflicting intents. Mitigate with module-level slashing or reputation."

and its Motivation sets the boundary:

> "Privacy, thresholds, bonding, and cross-chain are left to modules."

This ERC provides that trust and delegation module. Before coordinating, agents need answers to:

1. **"Should I include this agent in my coordination?"** — Participant selection
2. **"Can I trust this agent's judgement about other agents?"** — Transitive trust
3. **"How do I update trust based on coordination outcomes?"** — Trust maintenance

### Why Web of Trust?

The web of trust model, standardised for OpenPGP in RFC 9580 (which obsoletes RFC 4880) and proven over 25+ years of deployment, solves the bootstrap problem: how do you establish trust with unknown agents without a centralised registrar?

| OpenPGP Concept | This Standard |
|---------------|---------------|
| Public key | ENS name |
| Key signing | Trust attestation |
| Owner trust levels | `TrustLevel` enum |
| Key validity | Agent validity for coordination |
| Certification path | Trust chain through agents |

This standard adopts OpenPGP's trust levels and certification paths, but not its
validity calculation. OpenPGP treats a key as valid once it carries one fully trusted
signature or enough marginally trusted ones. `verifyPath` instead checks one path, and
every edge on it must meet `minEdgeTrust`. This standard does not define marginal
accumulation; see "Divergence from OpenPGP".

### Why ENS?

ENS provides a battle-tested, finalised identity layer:

- **Stable identifiers** that survive key rotation
- **Ownership semantics** via `owner()` and `isApprovedForAll()`
- **Human readable** names (`alice.agents.eth` not `0x742d...`)
- **Subdomain delegation** for protocol-issued agent identities

Using ENS avoids dependency on draft identity standards while remaining compatible with future standards through adapter patterns.

**Deployment note**: This standard requires access to an ENS registry. On Ethereum mainnet, use the canonical ENS deployment. On other networks, use network-specific ENS deployments or bridges. Implementations also take the network's NameWrapper address, or the zero address where no NameWrapper is deployed. CCIP-Read is a client-side mechanism and cannot be used for on-chain validation, so agents relying on on-chain identity gates need an on-chain resolver.

### Identity Continuity

ENS names are the identity. When an ENS name is transferred, the new owner inherits existing trust relationships where that name is the trustee, and can manage trust where that name is the trustor. Gates name their gatekeeper and anchors the same way, so a gate follows those names rather than their controllers. Short attestation expiries bound part of the exposure this creates; the Specification states the requirements under Identity Continuity.

## Specification

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) and [RFC 8174](https://www.rfc-editor.org/rfc/rfc8174).

### Overview

This ERC specifies:

* Trust levels and their semantics
* Denial, independent of trust levels
* ENS-indexed trust attestation structures with scope as key
* [EIP-712](./eip-712.md) typed data for signing attestations
* [ERC-1271](./eip-1271.md) support for contract controllers
* The `ITrustRegistry` interface, discoverable via [ERC-165](./eip-165.md)
* Path verification algorithm
* [ERC-8001](./eip-8001.md) integration hooks

### Trust Levels

Implementations MUST use the canonical enum:

```solidity
enum TrustLevel {
   Unknown,   // 0: No trust relationship established
   None,      // 1: Endorsement withdrawn
   Marginal,  // 2: Partial trust — accepted on an edge when minEdgeTrust <= Marginal
   Full       // 3: Complete trust — satisfies any minEdgeTrust
}
```

**Semantic definitions:**

| Level | Meaning | Validation Contribution |
|-------|---------|------------------------|
| `Unknown` | Default state; no data about agent | Cannot contribute to validation |
| `None` | The trustor has withdrawn its endorsement | Voids an edge whose effective level it is. Does not affect paths that do not use that edge |
| `Marginal` | Agent generally trustworthy | Contributes to validation when `minEdgeTrust <= Marginal` |
| `Full` | Agent's judgement equals own verification | Always contributes to validation |

**Level transitions:**

Attestations grant trust; revocations withdraw it. Each level has exactly one way to
reach it:

* `Marginal` and `Full` are reached only by a valid attestation with a higher nonce
* `None` is reached only by `revokeTrust` or `revokeTrustBatch`
* `Unknown` is the default and cannot be reassigned; a relationship, once created, is
  never removed from storage

`setTrust` therefore MUST reject attestations whose level is `Unknown` or `None`, and
withdrawal is not permanent: a later attestation with a higher nonce restores trust from
`None` to `Marginal` or `Full`.

No level transition changes the denial flag, and changing the denial flag changes no
level (see Denial).

### Denial

The registry represents three distinct intentions of a trustor `A` towards a trustee `B`
in a scope `s`:

| Intention | Meaning | Operation |
|-----------|---------|-----------|
| Endorsement | `A` trusts `B` | `setTrust`, `setTrustBatch` (level `Marginal` or `Full`) |
| Withdrawal | `A` no longer endorses `B` itself | `revokeTrust`, `revokeTrustBatch` (level `None`) |
| Denial | `A` refuses to rely on `B`, even when other agents endorse `B` | `setDenial`, cleared by `clearDenial` |

Each trust record therefore holds a boolean **denial flag** alongside its level. The
flag and the level are independent:

* `setTrust`, `setTrustBatch`, `revokeTrust` and `revokeTrustBatch` MUST NOT change the
  denial flag
* `setDenial` and `clearDenial` MUST NOT change the level, the expiry, or any other
  stored part of the record

Withdrawal does not imply denial, and denial does not imply withdrawal:

```text
Withdraw(A, B, s) ⇏ Deny(A, B, s)
Deny(A, B, s)     ⇏ Withdraw(A, B, s)
```

Denial is not subject to scope fallback. Writing `Deny(A, B, s)` for the flag stored at
scope `s`, the **effective denial** in scope `s` is:

```text
EffectiveDeny(A, B, s) = Deny(A, B, bytes32(0)) ∨ Deny(A, B, s)
```

At universal scope this reduces to `Deny(A, B, bytes32(0))`. A scoped flag that is
`false` never overrides a universal flag that is `true`. Path verification applies
effective denial to every edge and, from the validator, to every later node on the path
(see Validator denial).

Clearing a denial reveals the stored endorsement exactly as it stands: `Full` or
`Marginal` applies again, `None` and `Unknown` still void the edge, and a scoped denial
still applies after a universal one is cleared. An attestation submitted while a denial
is in force updates the stored endorsement but does not clear the denial.

Because a clear reveals whatever is stored when it executes, `clearDenial` takes the
trustor's nonce as the caller observed it during review, and reverts if any attestation
has landed since. That check cannot stop an attestation signed earlier from landing
after the clear, where it applies immediately. Closing that case depends on raising the
nonce floor before the review (see `clearDenial`).

### ENS Integration

The Trust Registry uses ENS namehashes as agent identifiers.

```solidity
// ENS namehash computation (per ERC-137)
bytes32 node = keccak256(abi.encodePacked(
   keccak256(abi.encodePacked(bytes32(0), keccak256("eth"))),
   keccak256("alice")
));
// node = namehash("alice.eth")
```

#### External Interfaces

The registry reads three external contracts:

```solidity
interface IENS {
   function owner(bytes32 node) external view returns (address);
   function resolver(bytes32 node) external view returns (address);
   function isApprovedForAll(address owner, address operator) external view returns (bool);
}

/// @dev NameWrapper is an ERC-1155; the token holder is the real name controller
interface INameWrapper {
   function ownerOf(uint256 id) external view returns (address);
   function isApprovedForAll(address owner, address operator) external view returns (bool);
}

interface IAddrResolver {
   function addr(bytes32 node) external view returns (address payable);
}
```

#### Name Controller Resolution

`ens.owner(node)` returns the registry controller. For **wrapped** names that controller
is the NameWrapper contract rather than the party actually controlling the name.
NameWrapper does not implement [ERC-1271](./eip-1271.md), so treating it as the signing
authority would leave every wrapped name unable to attest.

Implementations MUST resolve the controller as follows:

```solidity
function controllerOf(bytes32 node) internal view returns (address) {
   address owner = ens.owner(node);
   if (owner == address(0)) return address(0);

   // Wrapped name: the ERC-1155 holder is the real controller
   if (nameWrapper != address(0) && owner == nameWrapper) {
      try INameWrapper(nameWrapper).ownerOf(uint256(node)) returns (address wrapped) {
         return wrapped; // see Wrapped Name Lifecycle
      } catch {
         return address(0);
      }
   }

   return owner;
}
```

Wherever this standard refers to "the ENS name owner", it means `controllerOf(node)`.

Authority follows ownership as the registry and the NameWrapper report it, not ENS
management permissions. `controllerOf` does not consult `canModifyName`, fuses, or the
`.eth` registrar's expiry.

#### Wrapped Name Lifecycle

The NameWrapper's `ownerOf` reports a holder until the name's NameWrapper expiry has
passed. Once it has, all fuses reset. `ownerOf` then returns the zero address only if
`PARENT_CANNOT_CONTROL` was burned, so `controllerOf` behaves as follows:

| Name | Before NameWrapper expiry | After NameWrapper expiry |
|------|---------------------------|--------------------------|
| Wrapped `.eth` second-level name | holder, including during the 90-day grace period | zero address |
| Emancipated subname (`PARENT_CANNOT_CONTROL` burned) | holder | zero address |
| Unemancipated wrapped subname | holder | holder; fuses reset |

An unwrapped name has no NameWrapper expiry: `controllerOf` returns its registry owner
before and after the registrar expiry (see Expired Unwrapped Names).

For a wrapped `.eth` second-level name, the NameWrapper sets its expiry to the registrar
expiry plus the 90-day grace period when the name is wrapped, and a renewal through the
NameWrapper (`NameWrapper.renew`, which the 2023 wrapped ETHRegistrarController calls)
normally sets it again. A renewal that calls the BaseRegistrar directly, as the current
ETHRegistrarController and the 2019 ETHRegistrarController do, extends the registrar
expiry but not the NameWrapper expiry. The holder then loses control when the old
NameWrapper expiry passes, although the name is still registered and no one else can
register it. A later renewal through the NameWrapper can set the NameWrapper expiry from
the registrar expiry again, and the same holder then regains control. Neither kind of
renewal emits a NameWrapper event, so whether a wrapped name has a controller is read
from state (`controllerOf`, or the NameWrapper expiry from `getData`), not inferred from
renewals. Where this standard refers to the end of a wrapped name's grace period, it
means its NameWrapper expiry.

For the 90 days before its NameWrapper expiry, the NameWrapper treats a wrapped `.eth`
second-level name as in its grace period. It refuses every management operation on the
name (`canModifyName` returns false), but `ownerOf` still returns the holder, and the
holder keeps every power this standard grants. Normally this is the registrar grace
period, during which no one else can register the name.

Emancipated subnames have no grace period. An unemancipated subname never loses its
holder through expiry. Its NameWrapper expiry is often zero, and its parent can reassign
it at any time (see Parent-Controlled Subnames).

An endorsement (`Marginal` or `Full`) issued while its trustor, a `.eth` second-level
name, is past its registrar expiry is the one most likely to outlive the registration
that issued it, since an unrenewed name passes to its next registrant with every record
it set. The contract accepts such an endorsement under the same authority rule as every
other operation. The recommendations below guide signers and coordinators. They do not
change what the contract accepts or what the path-validation functions return. For such
names, wrapped or unwrapped:

* A trustor that signs or sets an endorsement after its registrar expiry SHOULD give it
  an `expiry` no later than the end of the grace period, and SHOULD issue it again after
  renewing.
* A coordinator that relies on such an endorsement SHOULD apply two separate checks:
  * **Continuity: has the name been registered again?** The coordinator SHOULD treat
    the endorsement as ineligible once the BaseRegistrar has emitted `NameRegistered`
    for the trustor's label after the endorsement's `TrustSet` event. That event is
    the latest `TrustSet` for the record the path uses: the trustor, trustee and scope
    that scope fallback selects for the validation scope (see Scope fallback
    semantics), not the latest record for the pair in any scope. `NameRenewed` does
    not end it, however many renewals follow. Both events are indexed by the
    labelhash, so the coordinator needs the trustor's label, checked against the node. A
    later `nameExpires` alone does not show a renewal, because a registration after the
    grace period also produces one. The coordinator orders the `TrustSet` event against
    these events by block number, transaction index and log index, not by block
    timestamp, because events in the same block share a timestamp. A registration by the
    same holder also ends the endorsement, which the trustor then issues again. The
    check concerns registration, not control: a transfer within one registration emits
    no `NameRegistered` (see Monitoring Control Changes). A coordinator that cannot
    establish the label, or every such event from the `TrustSet` event to the block at
    which it decides, SHOULD NOT rely on the endorsement. This check is not limited to
    endorsements issued past the trustor's expiry: a coordinator applies it to every
    attestation a `.eth` second-level name sets or receives, whenever it was issued (see
    below).
  * **Liveness, for a wrapped trustor: does the name have a controller now?** The
    coordinator reads `controllerOf(node)` (in practice the NameWrapper's `ownerOf`) or
    the NameWrapper expiry (`getData(node)`) when it makes its decision, and SHOULD NOT
    rely on the endorsement while the name has no controller. A `NameRenewed` event is
    not evidence of liveness: a renewal that bypasses the NameWrapper emits it without
    extending the NameWrapper expiry.
* A coordinator that relies on such an endorsement SHOULD validate with `enforceExpiry`
  set, monitor the trustor's control, its registrar expiry and, for a wrapped trustor,
  its NameWrapper expiry, and re-evaluate its gates when control changes (see Monitoring
  Control Changes).

An expiry on the name's own endorsements does not bound a path through it. The next
registrant inherits every attestation naming the name as trustee, and can set new
endorsements from it at once. Once the name is re-registered, a path from an earlier
trustor through the name reaches any party the new registrant endorses, however the
earlier holder capped its own endorsements. Only the attestation that points to the name
bounds such a path. For an attestation whose trustee is a `.eth` second-level name,
wrapped or unwrapped:

* A trustor SHOULD give it an `expiry` no later than the end of the trustee's grace
  period, and SHOULD issue it again after the trustee renews. An attestation signed
  before the trustee's registrar expiry, with the 90-day maximum recommended under
  Identity Continuity, already meets this.
* A coordinator SHOULD apply the continuity check above to every `.eth` second-level
  name on the path, as trustee as well as trustor, and SHOULD validate with
  `enforceExpiry` set. It treats an attestation pointing to the name as ineligible once
  the name has been registered again after the attestation's `TrustSet` event. This
  bounds the path where the trustor did not cap the expiry, and spares the trustor a new
  signature after each renewal.
* For an attestation whose trustee is an emancipated subname, a trustor SHOULD give it
  an `expiry` no later than the subname's NameWrapper expiry, after which the parent can
  create the subname again for another holder (see Parent-Controlled Subnames).
* A coordinator SHOULD apply the same check to every name on the path below a `.eth`
  second-level name. It treats such a name as registered again whenever its
  second-level ancestor is, since the ancestor's next registrant can reassign it, and
  whenever the name, or an ancestor of it below the second-level name, has been wrapped
  again after the attestation's `TrustSet` event. A name has been wrapped again when the
  NameWrapper emits `NameWrapped` for its node, which is indexed. The NameWrapper emits
  it whenever a name becomes wrapped, including when a parent creates an emancipated
  subname again after its NameWrapper expiry. It does not emit it when a parent
  reassigns a subname that still has a holder in the NameWrapper, which is a change of
  control (see Parent-Controlled Subnames). A holder that unwraps its subname and wraps
  it again also emits it, and its trustors then sign again. The coordinator orders the
  event as above. Finding the ancestors needs the name, checked against the node, as the
  check above needs the label.
* A registration by the same holder ends these attestations too, not only the
  holder's own endorsements. A holder that lets its name pass its grace period and
  registers it again therefore needs every trustor that endorsed it to sign again. A
  trustor SHOULD watch its trustees' registrations as well as their expiries.

Implementations MUST pin the `nameWrapper` address at deployment, and MUST NOT allow it
to be changed afterwards: a registry pointed at a hostile contract in that slot would let
it claim control of every wrapped name. Implementations deployed on networks with no
NameWrapper deployment MUST set `nameWrapper` to the zero address, which disables
unwrapping.

#### Agent Address Resolution

[ERC-8001](./eip-8001.md) identifies participants by address (`address[] participants`),
while this registry identifies agents by ENS namehash. The binding between the two is
the node's forward address record ([ERC-137](./eip-137.md)):

```solidity
/// @dev Gas forwarded to a node's resolver
uint256 internal constant RESOLVE_GAS = 50_000;

function resolveAgent(bytes32 node) public view returns (address) {
   address resolver = ens.resolver(node);
   if (resolver == address(0)) return address(0);

   // Low-level, gas-capped, bounded-copy call: a hostile resolver can neither make
   // resolution revert nor consume more than RESOLVE_GAS
   bytes memory data = abi.encodeWithSelector(IAddrResolver.addr.selector, node);
   bool ok;
   uint256 word;
   uint256 size;
   assembly ("memory-safe") {
      ok := staticcall(RESOLVE_GAS, resolver, add(data, 0x20), mload(data), 0, 0x20)
      size := returndatasize()
      word := mload(0)
   }
   if (!ok || size < 0x20 || word >> 160 != 0) return address(0);
   return address(uint160(word));
}
```

An address `a` is bound to node `n` if and only if `resolveAgent(n) == a` and
`a != address(0)`. No separate registration step is required: the `addr` record is the
controller's statement of which address the name denotes. That statement can outlive the
controller that made it, because ENS does not clear the record when a name expires or is
re-registered. After a re-registration the record keeps naming the previous holder's
address until the new controller changes it, and a wrapped name keeps its record after
its NameWrapper expiry passes, while nobody controls it. `validateParticipantAddress`
therefore also requires the node to have a controller, which covers the second case but
not the first (see Resolver Trust).

Resolution is a **verification** primitive, not a search. Callers supply the node as the
terminal element of a `TrustPath` and the registry confirms that it resolves to the
participant address. This standard does not use reverse resolution, which is
self-asserted and would require on-chain string handling to verify.

`resolveAgent` MUST NOT revert because of the resolver's behaviour. It MUST return the
zero address when the node has no resolver, or when the call to `addr` reverts, runs out
of gas, returns fewer than 32 bytes, or returns a first word with any of its upper 96
bits set. Return data beyond the first 32 bytes MUST be ignored. Implementations MUST
bound the gas forwarded to the resolver; the reference implementation forwards 50,000.

Nodes served by an off-chain resolver (CCIP-Read) cannot be resolved during on-chain
validation: `addr` reverts and `resolveAgent` returns the zero address. Agents that need
to pass on-chain identity gates MUST publish an on-chain `addr` record, served by a
resolver that answers `addr` within the gas bound.

#### Identity Continuity

The ENS name, not the key behind it, is the identity. When a name is transferred, the new
controller inherits every attestation naming it as trustee, and gains authority over
every attestation and every denial in which it is the trustor.

The same holds for gates. A gate's `gatekeeperNode` and `requiredAnchors` identify ENS
names, not their controllers. When control of one of those names changes, its role in
the gate passes to the new controller, and the gate configuration does not change.
Coordinators SHOULD monitor control changes for the gatekeeper and anchor nodes of their
gates, and SHOULD re-evaluate the affected gates when one occurs. See Gatekeeper and
Anchor Names.

Attestations covering high-stakes scopes SHOULD carry short expiries (RECOMMENDED: 90
days maximum), so that a transfer cannot carry inherited trust forward indefinitely.
Agents SHOULD monitor control changes on ENS names they trust, and re-evaluate trust
when a trusted name changes hands. No single event stream reports every change of
control; see Monitoring Control Changes.

Agents SHOULD use wrapped `.eth` second-level names or emancipated subnames, whose
authority ends when their NameWrapper expiry passes. For a `.eth` second-level name that
is normally the end of the 90-day grace period. A renewal that bypasses the NameWrapper
leaves the NameWrapper expiry behind, so the name can lose its controller while still
registered, until a renewal through the NameWrapper restores it. See Wrapped Name
Lifecycle. Gatekeepers SHOULD prefer such names for gatekeeper and anchor nodes. An
unwrapped name keeps its authority after it expires, and an unemancipated wrapped subname
keeps it indefinitely; see Wrapped Name Lifecycle and Expired Unwrapped Names. A name
keeps this expiry only while it stays wrapped: unless `CANNOT_UNWRAP` is burned, its
holder can unwrap it, after which it follows the rules for unwrapped names. Wrapping also
does not survive re-registration: the current `.eth` registrar controller registers names
unwrapped, so a name that lapses while wrapped follows the rules for unwrapped names
under its next registrant unless that registrant wraps it. Coordinators SHOULD therefore
check whether a name is wrapped when they verify a path or gate, not rely on its status
when an endorsement was issued. NameWrapper fuses can also stop a name changing hands, at
a long-term cost; see Name Locking with Fuses.

### Signature Authority

Trust attestations MUST be signed by an address with signing authority for the ENS name.

**Signing authority** is limited to:

* The ENS name controller (`controllerOf(node)`, which unwraps NameWrapper names), OR
* For contract controllers: any signer the contract validates via [ERC-1271](./eip-1271.md)

**Transaction submission** is governed by the following authority model. "Approved
operator" means an address for which `isApprovedForAll` returns true on whichever
contract holds the name, per `canSubmitRevocation`. A per-token approval (`approve` on
the NameWrapper or the `.eth` registrar) is not an operator approval and grants no
authority under this standard, although it carries powers in ENS itself (see below). An
operator approval belongs to the holder that granted it, so it no longer applies once
the name changes hands.

| Operation | Controller | Approved operator | Any address |
|-----------|:----------:|:-----------------:|:-----------:|
| `setTrust`, `setTrustBatch` | signs | no | MAY submit a valid signature |
| `revokeTrust`, `revokeTrustBatch` | MAY call | MAY call | no |
| `setDenial`, `clearDenial` | MAY call | **MUST NOT** | no |
| `invalidateNonces` | MAY call | **MUST NOT** | no |
| `setIdentityGate`, `removeIdentityGate` | keyed by `msg.sender`; no ENS authority involved | | |

Approved operators MAY perform **withdrawal** because its effect is bounded to the
trustor's own endorsement of one trustee, in the scopes named in the call. A withdrawal
removes an edge; it cannot stop the trustee being reached through other agents.

Approved operators MUST NOT call `setDenial` or `clearDenial`.
A denial overrides every other agent's endorsement within the trustor's own trust
decisions, so setting one decides whose judgement the trustor accepts, and clearing one
can readmit an agent the controller excluded. That is the same authority as signing an
attestation, and it stays with the controller. This standard adds no separate
delegation mechanism for it: a controller that wants to delegate denial can use a
contract controller.

Approved operators MUST NOT call `invalidateNonces`. Its blast radius is trustor-wide:
raising the nonce floor invalidates every attestation the trustor has signed but not yet
submitted, for every trustee and every scope. `isApprovedForAll` is a broad, long-lived
approval granted for ENS name management, and treating it as authority to void an
agent's entire outstanding attestation set is an escalation the name owner did not
knowingly grant.

This separation ensures:

* Attestations are cryptographically bound to the name controller
* Routine revocation and transaction submission can be delegated (relayers, operators)
* Approvals cannot be used to forge signatures
* Approvals cannot be used to set or clear denials
* Approvals cannot be used to perform trustor-wide invalidation

These restrictions are only as strong as the ENS ownership model beneath them. In the
ENS registry, an address approved through `isApprovedForAll` can change a name's owner,
and in the NameWrapper it can transfer the name's token unless `CANNOT_TRANSFER` is
burned. A per-token approval on the `.eth` registrar is as strong for an unwrapped name:
its holder can transfer the registration, or call `reclaim` to set the registry owner.
Such an operator can make itself the controller, and with it acquire every
controller-only power above. It can also migrate the name to ENSv2, naming itself the
new owner there, which ends all authority over the name under this standard (see ENSv2
Compatibility and Migration). The restrictions stop an approval from being used as
controller authority in place; they do not stop an operator with transfer authority
from taking control of the name. Trustors SHOULD grant `isApprovedForAll`, and
per-token approvals on the `.eth` registrar, only to parties they would trust with
control of the name. A per-token approval on the NameWrapper cannot transfer the name,
but it lets its holder extend the expiry of the name's subnames, which prolongs the
authority of an emancipated subname's holder. If `CANNOT_APPROVE` is burned, that
approval also survives a transfer of the name.

Implementations MUST attempt ECDSA recovery before [ERC-1271](./eip-1271.md), and MUST
reject malleable (high-s) signatures. A controller that has code is not necessarily a
contract account: an EOA with an [EIP-7702](./eip-7702.md) delegation has code, and its
delegate need not implement `isValidSignature`, so routing every controller with code
straight to [ERC-1271](./eip-1271.md) would reject that EOA's valid signatures.

An [ERC-1271](./eip-1271.md) call that reverts, returns fewer than 32 bytes, or returns a
first word other than the magic value `0x1626ba7e` left-aligned with zero padding MUST be
treated as a failed signature, so `setTrust` reverts with `InvalidSignature` rather than
with the controller's revert data or an empty revert.

```solidity
/// @dev Verify signature - signing authority is the name controller only
function verifySignature(
   bytes32 node,
   bytes32 digest,
   bytes calldata signature
) internal view returns (bool) {
   address controller = controllerOf(node);
   if (controller == address(0)) return false;

   // ECDSA first: covers plain EOAs and EIP-7702 delegated EOAs
   (address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(digest, signature);
   if (err == ECDSA.RecoverError.NoError && recovered == controller) return true;

   // ERC-1271 fallback for any controller with code
   if (controller.code.length == 0) return false;
   // Low-level call with a returndata length check: a high-level call would revert
   // with empty data (not InvalidSignature) when the controller's code returns
   // nothing, e.g. an EIP-7702 delegate with an empty fallback
   return SignatureChecker.isValidERC1271SignatureNow(controller, digest, signature);
}

/// @dev Check if caller can submit a revokeTrust transaction
function canSubmitRevocation(bytes32 node, address caller) internal view returns (bool) {
   address controller = controllerOf(node);
   if (controller == address(0)) return false;
   if (caller == controller) return true;

   // Approvals live on whichever contract actually holds the name
   if (nameWrapper != address(0) && ens.owner(node) == nameWrapper) {
      return INameWrapper(nameWrapper).isApprovedForAll(controller, caller);
   }

   return ens.isApprovedForAll(controller, caller);
}
```

A wrapped `.eth` second-level name loses its signing authority when its NameWrapper
expiry passes, normally the end of its grace period (see Wrapped Name Lifecycle), and an
emancipated subname when its expiry passes. Both then resolve to `address(0)`. An
unemancipated wrapped subname keeps its holder after it expires, and an expired unwrapped
name keeps its registry owner until the name is re-registered or cleared (see Wrapped
Name Lifecycle and Expired Unwrapped Names).

### EIP-712 Domain

Implementations MUST use the following [EIP-712](./eip-712.md) domain:

```text
EIP712Domain(
   name: "TrustRegistry",
   version: "1",
   chainId: block.chainid,
   verifyingContract: address(this)
)
```

Implementations SHOULD expose the domain via [ERC-5267](./eip-5267.md).

### Primary Types

```solidity
struct TrustAttestation {
   bytes32 trustorNode;       // ENS namehash of trustor
   bytes32 trusteeNode;       // ENS namehash of trustee
   TrustLevel level;          // Trust level assigned
   bytes32 scope;             // Scope restriction; bytes32(0) = universal
   uint64 expiry;             // Unix timestamp; 0 = no expiry
   uint64 nonce;              // Per-trustor monotonic nonce
}

struct ValidationParams {
   uint8 maxPathLength;       // Maximum trust chain depth (1-10)
   TrustLevel minEdgeTrust;   // Minimum trust level required on each edge
   bytes32 scope;             // Scope to verify against; bytes32(0) = universal
   bool enforceExpiry;        // Check expiry on all chain elements
   bytes32[] requiredAnchors; // Path MUST traverse at least one anchor; empty = no requirement
}

struct TrustPath {
   bytes32[] nodes;           // [validator, ...intermediaries..., target]
}
```

**Path length definition**: Path length is the number of edges (trust relationships) in the path. A direct trust relationship has path length 1. A path `[A, B, C]` has length 2.

#### Default Validation Parameters

When not specified, implementations SHOULD use:

```solidity
ValidationParams({
   maxPathLength: 5,
   minEdgeTrust: TrustLevel.Marginal,
   scope: bytes32(0),
   enforceExpiry: true,
   requiredAnchors: new bytes32[](0)
})
```

#### Validation Parameters Constraints

Implementations MUST reject `ValidationParams` where:

| Condition | Error |
|-----------|-------|
| `maxPathLength == 0` or `maxPathLength > 10` | `InvalidMaxPathLength` |
| `minEdgeTrust == TrustLevel.Unknown` or `minEdgeTrust == TrustLevel.None` | `InvalidMinEdgeTrust` |
| `requiredAnchors.length > 10` | `TooManyRequiredAnchors` |

Both `verifyPath` and `setIdentityGate` MUST apply these constraints, through a shared
routine, before the parameters are used. The Gate Constraints below are not part of that
routine.

#### Gate Constraints

`setIdentityGate` MUST also reject the following parameters, which no path can satisfy:

| Condition | Error |
|-----------|-------|
| `requiredAnchors.length > 0` and `maxPathLength < 2` | `UnsatisfiableAnchors` |

Only intermediary nodes satisfy an anchor (see Anchor semantics), and a path of one edge
has none, so such a gate would reject every participant. `setIdentityGate` applies this
check after the Validation Parameters Constraints. `verifyPath` MUST NOT apply it: given
these parameters directly, it returns `false`, as it does for any path that misses its
anchors. This is the only unsatisfiable configuration `setIdentityGate` rejects. Others,
such as a gate whose only anchor is its own gatekeeper, are accepted and reject every
participant.

### Typed Data Hashes

```solidity
bytes32 constant TRUST_ATTESTATION_TYPEHASH = keccak256(
   "TrustAttestation(bytes32 trustorNode,bytes32 trusteeNode,uint8 level,bytes32 scope,uint64 expiry,uint64 nonce)"
);

function _structHash(TrustAttestation calldata att) internal pure returns (bytes32) {
   return keccak256(abi.encode(
      TRUST_ATTESTATION_TYPEHASH,
      att.trustorNode,
      att.trusteeNode,
      uint8(att.level),
      att.scope,
      att.expiry,
      att.nonce
   ));
}
```

The signed digest is the [EIP-712](./eip-712.md) typed data hash of `_structHash(att)`
under the domain above.

### Interface

Implementations MUST expose the following interface:

```solidity
interface ITrustRegistry is IERC165 {
   // ═══════════════════════════════════════════════════════════════════
   // Events
   // ═══════════════════════════════════════════════════════════════════

   /// @notice Emitted when trust is set or updated
   /// @dev `nonce` is the attestation's nonce, so indexers can order attestations
   ///      from logs alone
   event TrustSet(
      bytes32 indexed trustorNode,
      bytes32 indexed trusteeNode,
      TrustLevel level,
      bytes32 indexed scope,
      uint64 expiry,
      uint64 nonce
   );

   /// @notice Emitted when trust is explicitly revoked
   event TrustRevoked(
      bytes32 indexed trustorNode,
      bytes32 indexed trusteeNode,
      bytes32 indexed scope,
      bytes32 reasonCode
   );

   /// @notice Emitted when a denial flag changes
   /// @dev Emitted only on a change, so the latest event per
   ///      (trustorNode, trusteeNode, scope) reconstructs the stored flag
   event DenialSet(
      bytes32 indexed trustorNode,
      bytes32 indexed trusteeNode,
      bytes32 indexed scope,
      bool denied,
      bytes32 reasonCode
   );

   /// @notice Emitted when a trustor invalidates outstanding attestations
   event NoncesInvalidated(bytes32 indexed trustorNode, uint64 newNonce);

   /// @notice Emitted when an identity gate is configured
   /// @dev Carries the complete ValidationParams so indexers can reconstruct gate
   ///      configuration from logs alone
   event IdentityGateSet(
      address indexed coordinator,
      bytes32 indexed coordinationType,
      bytes32 indexed gatekeeperNode,
      uint8 maxPathLength,
      TrustLevel minEdgeTrust,
      bytes32 scope,
      bool enforceExpiry,
      bytes32[] requiredAnchors
   );

   /// @notice Emitted when an identity gate is removed
   event IdentityGateRemoved(address indexed coordinator, bytes32 indexed coordinationType);

   // ═══════════════════════════════════════════════════════════════════
   // Trust Management
   // ═══════════════════════════════════════════════════════════════════

   /// @notice Set trust level for another agent in a specific scope
   /// @dev Signature MUST be from the name controller (controllerOf), or validate via
   ///      ERC-1271 when that controller is a contract.
   ///      `level` MUST be Marginal or Full; withdrawal goes through revokeTrust.
   ///      MUST NOT change the denial flag.
   ///      `nonce` MUST exceed the current nonce by at most 2**32.
   /// @param attestation The trust attestation
   /// @param signature EIP-712 signature from the trustor's name controller (controllerOf)
   function setTrust(
      TrustAttestation calldata attestation,
      bytes calldata signature
   ) external;

   /// @notice Batch set multiple trust relationships
   /// @dev All attestations MUST share the same trustorNode, and each MUST be signed by
   ///      the trustor's name controller (controllerOf).
   /// @param attestations Array of trust attestations
   /// @param signatures Corresponding signatures
   function setTrustBatch(
      TrustAttestation[] calldata attestations,
      bytes[] calldata signatures
   ) external;

   /// @notice Withdraw the trustor's endorsement (level None) in one scope
   /// @dev Caller MUST be the name controller (controllerOf) or an approved operator.
   ///      Prior trust is NOT required: a trustor may preemptively withdraw in a scope
   ///      it never granted. MUST NOT change the denial flag.
   /// @param trustorNode The trustor's ENS namehash
   /// @param trusteeNode The agent to revoke trust from
   /// @param scope The scope to revoke trust in
   /// @param reasonCode Reason code for revocation
   function revokeTrust(
      bytes32 trustorNode,
      bytes32 trusteeNode,
      bytes32 scope,
      bytes32 reasonCode
   ) external;

   /// @notice Withdraw the trustor's endorsement across several scopes in one transaction
   /// @dev Caller MUST be the name controller (controllerOf) or an approved operator.
   ///      Scopes are supplied by the caller; this standard does not enumerate them
   ///      on-chain. MUST NOT change the denial flag.
   /// @param trustorNode The trustor's ENS namehash
   /// @param trusteeNode The agent to revoke trust from
   /// @param scopes The scopes to revoke trust in
   /// @param reasonCode Reason code for revocation
   function revokeTrustBatch(
      bytes32 trustorNode,
      bytes32 trusteeNode,
      bytes32[] calldata scopes,
      bytes32 reasonCode
   ) external;

   /// @notice Deny an agent within the trustor's own decisions in one scope
   /// @dev Caller MUST be the name controller (controllerOf); approved operators MUST
   ///      NOT call this. A denied agent fails every edge from the trustor, and every
   ///      path the trustor evaluates as validator, whatever its endorsements.
   ///      MUST NOT change the trust level, expiry or any other stored field.
   ///      Denying an agent already denied in that scope succeeds with no event.
   /// @param trustorNode The trustor's ENS namehash
   /// @param trusteeNode The agent to deny
   /// @param scope The scope of the denial; bytes32(0) applies in every scope
   /// @param reasonCode Reason code for the denial
   function setDenial(
      bytes32 trustorNode,
      bytes32 trusteeNode,
      bytes32 scope,
      bytes32 reasonCode
   ) external;

   /// @notice Clear the trustor's denial of an agent in one scope, provided no
   ///         attestation by the trustor has landed since the caller reviewed it
   /// @dev Caller MUST be the name controller (controllerOf); approved operators MUST
   ///      NOT call this. MUST revert with NonceChanged unless
   ///      getNonce(trustorNode) == expectedNonce, so endorsements written after the
   ///      caller's review cannot take effect through the clear.
   ///      It cannot stop an attestation signed earlier from landing after the
   ///      clear: callers SHOULD raise the floor with invalidateNonces before
   ///      reading expectedNonce, and SHOULD pause signing until the clear
   ///      has executed.
   ///      MUST NOT change the trust level, expiry or any other stored field.
   ///      Clearing a denial that is not set succeeds with no event.
   /// @param trustorNode The trustor's ENS namehash
   /// @param trusteeNode The agent to reinstate
   /// @param scope The scope of the denial to clear
   /// @param expectedNonce The trustor's nonce at the time of review
   /// @param reasonCode Reason code for the change
   function clearDenial(
      bytes32 trustorNode,
      bytes32 trusteeNode,
      bytes32 scope,
      uint64 expectedNonce,
      bytes32 reasonCode
   ) external;

   /// @notice Invalidate every outstanding attestation below a nonce
   /// @dev Caller MUST be the name controller; approved operators MUST NOT call this.
   ///      It voids every outstanding attestation across all trustees and scopes.
   ///      Revocation alone does NOT invalidate attestations the trustor already signed
   ///      but has not yet submitted; this does.
   /// @param trustorNode The trustor's ENS namehash
   /// @param newNonce The new nonce floor; MUST exceed the current nonce by at most 2**32
   function invalidateNonces(bytes32 trustorNode, uint64 newNonce) external;

   /// @notice Get trust record between two agents in a specific scope
   /// @param trustorNode The trusting agent
   /// @param trusteeNode The trusted agent
   /// @param scope The trust scope (bytes32(0) for universal)
   /// @return level Current trust level
   /// @return expiry Expiration timestamp (0 = never)
   function getTrust(
      bytes32 trustorNode,
      bytes32 trusteeNode,
      bytes32 scope
   ) external view returns (TrustLevel level, uint64 expiry);

   /// @notice Get the denial flag stored for exactly one scope
   /// @dev Returns the stored flag, not the effective denial: a universal denial is
   ///      not reflected in the result for a named scope. The effective denial is
   ///      getDenial(A, B, bytes32(0)) || getDenial(A, B, scope)
   /// @param trustorNode The trusting agent
   /// @param trusteeNode The denied agent
   /// @param scope The scope (bytes32(0) for universal)
   /// @return Whether the flag is set for that scope
   function getDenial(
      bytes32 trustorNode,
      bytes32 trusteeNode,
      bytes32 scope
   ) external view returns (bool);

   /// @notice Get current nonce for a trustor
   /// @param trustorNode The agent's ENS namehash
   /// @return Current nonce value
   function getNonce(bytes32 trustorNode) external view returns (uint64);

   // ═══════════════════════════════════════════════════════════════════
   // Path Verification
   // ═══════════════════════════════════════════════════════════════════

   /// @notice Verify a pre-computed trust path
   /// @dev Returns true only if every edge check, the validator denial check
   ///      AND the requiredAnchors constraint are satisfied. There is no partial
   ///      success.
   /// @param path The trust path to verify
   /// @param params Validation parameters
   /// @return valid Whether the path satisfies all validation requirements
   function verifyPath(
      TrustPath calldata path,
      ValidationParams calldata params
   ) external view returns (bool valid);

   // ═══════════════════════════════════════════════════════════════════
   // ERC-8001 Integration
   // ═══════════════════════════════════════════════════════════════════

   /// @notice Set the identity gate for one of the caller's coordination types
   /// @dev Stored under `(msg.sender, coordinationType)`. Callers own their own
   ///      namespace, so no cross-caller authorisation is needed and well-known
   ///      coordination type constants cannot be squatted.
   /// @param coordinationType The ERC-8001 coordination type
   /// @param gatekeeperNode Agent whose trust graph gates entry
   /// @param params Validation parameters for the gate
   function setIdentityGate(
      bytes32 coordinationType,
      bytes32 gatekeeperNode,
      ValidationParams calldata params
   ) external;

   /// @notice Remove the caller's identity gate for a coordination type
   /// @param coordinationType The ERC-8001 coordination type
   function removeIdentityGate(bytes32 coordinationType) external;

   /// @notice Get identity gate configuration
   /// @param coordinator The address that registered the gate
   /// @param coordinationType The ERC-8001 coordination type
   /// @return gatekeeperNode The gatekeeper agent
   /// @return params Validation parameters
   /// @return enabled Whether the gate is active
   function getIdentityGate(
      address coordinator,
      bytes32 coordinationType
   ) external view returns (
      bytes32 gatekeeperNode,
      ValidationParams memory params,
      bool enabled
   );

   /// @notice Resolve the agent address bound to an ENS node
   /// @param node The agent's ENS namehash
   /// @return agent The node's forward address record, or address(0) if unset
   function resolveAgent(bytes32 node) external view returns (address agent);

   /// @notice Validate a participant identified by ENS node
   /// @param coordinator The address that registered the gate
   /// @param coordinationType The ERC-8001 coordination type
   /// @param participantNode The agent being gated
   /// @param path Pre-computed trust path from gatekeeper to participantNode
   /// @return isValid Whether participant passes the gate
   function validateParticipantWithPath(
      address coordinator,
      bytes32 coordinationType,
      bytes32 participantNode,
      TrustPath calldata path
   ) external view returns (bool isValid);

   /// @notice Validate an ERC-8001 participant identified by address
   /// @dev The terminal node of `path` MUST resolve to `participant`. This is the
   ///      hook an ERC-8001 coordinator calls for each entry in `participants`.
   /// @param coordinator The address that registered the gate
   /// @param coordinationType The ERC-8001 coordination type
   /// @param participant The participant address taken from the ERC-8001 intent
   /// @param path Pre-computed trust path from gatekeeper to the participant's node
   /// @return isValid Whether participant passes the gate
   function validateParticipantAddress(
      address coordinator,
      bytes32 coordinationType,
      address participant,
      TrustPath calldata path
   ) external view returns (bool isValid);
}
```

### Interface Detection

Implementations MUST implement [ERC-165](./eip-165.md) and MUST return `true` from
`supportsInterface` for:

| Interface | Identifier |
|-----------|------------|
| `IERC165` | `0x01ffc9a7` |
| `ITrustRegistry` | `0x94f0ee34` |

Per [ERC-165](./eip-165.md), `supportsInterface` MUST return `false` for `0xffffffff`
and MUST use at most 30,000 gas.

`ITrustRegistry`'s identifier is the XOR of the selectors of the seventeen functions
declared in this document, excluding the inherited `supportsInterface`, matching
Solidity's `type(ITrustRegistry).interfaceId`.

Adding, removing, or changing the signature of any function in `ITrustRegistry` changes
its identifier. Implementations tracking a draft revision of this standard SHOULD
recompute rather than hard-code the constant.

Earlier drafts had two other identifiers, and implementations of this revision MUST NOT
report `true` for either:

* `0x42b46ef5`, for drafts with no denial. The functions it covers are still present,
  but their path verification semantics differ
* `0x31f35e83`, for drafts in which `setDenial(bytes32,bytes32,bytes32,bool,bytes32)`
  both set and cleared a denial, with no check on what the clear would reveal

Earlier drafts also declared an optional `ITrustRegistryExtended`, identified by
`0x93e686a3`. This revision does not define it, and assigns that identifier no meaning.
See Deployments of Earlier Drafts.

### Semantics

#### `setTrust`

`setTrust` MUST revert, in the order given under Revert Order, if:

* `attestation.trustorNode == attestation.trusteeNode` (self-trust prohibited)
* `attestation.level` is `Unknown` or `None` (withdrawal is expressed by revocation, not attestation)
* `attestation.expiry != 0 && attestation.expiry <= block.timestamp`
* The ENS name for `trustorNode` does not exist (`ens.owner(trustorNode)` is the zero address)
* `attestation.nonce <= getNonce(attestation.trustorNode)`
* `attestation.nonce > getNonce(attestation.trustorNode) + 2**32`, with the same
  overflow-safe bound as `invalidateNonces`. The error is
  `NonceJumpTooLarge(attestation.nonce, bound)`
* The signature does not verify per the Signature Authority section, including when
  `controllerOf` returns the zero address, as it does for a wrapped `.eth` name past its
  NameWrapper expiry

If valid:

* The trust level and expiry MUST be stored, keyed by `(trustorNode, trusteeNode, scope)`
* The denial flag for that key MUST NOT change
* `getNonce(trustorNode)` MUST return the attestation's nonce
* `TrustSet` MUST be emitted

An attestation is accepted while the trustee is denied. It updates the stored
endorsement, which takes effect only when the denial is cleared.

#### `setTrustBatch`

`setTrustBatch` MUST revert, in the order given under Revert Order, if:

* `attestations.length != signatures.length`
* `attestations` is empty
* Any attestation has a different `trustorNode` than the first attestation
* Nonces within the batch are not strictly increasing
* Any individual attestation would fail `setTrust` validation

The batch-level conditions apply to the whole batch before any attestation is checked
individually.

#### `revokeTrust`

`revokeTrust` withdraws the trustor's endorsement. It is the only way to reach
`TrustLevel.None`.

`revokeTrust` MUST revert if:

* Caller is not the name controller or an approved operator for `trustorNode`

If valid:

* Trust level MUST be set to `None`
* The stored expiry MUST be set to `0`, so `getTrust` returns `(None, 0)` for every
  revoked record
* The denial flag MUST NOT change
* `TrustRevoked` MUST be emitted
* The relationship MUST remain in storage (not deleted) to preserve the withdrawal

A prior trust relationship is **not** required. A trustor MAY withdraw in a scope it never
granted. Such a preemptive withdrawal is a tombstone: it blocks the universal fallback in
that scope, and so blocks paths that would otherwise route through the edge there.
Callers that want to distinguish withdrawing an existing endorsement from a preemptive
tombstone MUST read `getTrust` first; the registry does not make that distinction.

A withdrawal at `bytes32(0)` voids the trustor's edge to the trustee in every scope,
overriding positive scoped records for the same pair. A withdrawal in any other scope
affects that scope only. In both cases only the named record is written, so `getTrust`
for another scope still returns that scope's stored record; the override is applied
during path verification (see Scope fallback semantics).

Withdrawal removes an edge and nothing else. It does not affect paths that do not use
that edge: if other agents the trustor relies on still endorse the trustee, paths
through them remain valid, including paths the trustor evaluates as validator. A
universal withdrawal concerns one trustor and trustee pair; it is not a denylist. A
trustor that refuses to rely on the trustee at all MUST use `setDenial`. The reason
code does not change this: a withdrawal recorded as `COMPROMISED` is still only a
withdrawal.

Withdrawal does **not** invalidate attestations the trustor has already signed but not
yet submitted. See `revokeTrustBatch` and `invalidateNonces`.

#### `revokeTrustBatch`

`revokeTrustBatch` applies `revokeTrust` to several scopes for the same
`(trustorNode, trusteeNode)` pair in a single transaction.

`revokeTrustBatch` MUST revert if:

* Caller is not the name controller or an approved operator for `trustorNode`
* `scopes` is empty

If valid, for each listed scope:

* Trust level MUST be set to `None`
* The stored expiry MUST be set to `0`
* The denial flag MUST NOT change
* `TrustRevoked` MUST be emitted
* The relationship MUST remain in storage

Because trust is keyed by `(trustorNode, trusteeNode, scope)`, a trustor that has
granted trust in several scopes must name each one. Implementations MUST NOT enumerate
scopes on-chain; callers derive the list from `TrustSet` logs, which is the same
off-chain indexing this standard already assumes for path computation.

#### `setDenial`

`setDenial` denies `trusteeNode` within the trustor's decisions in `scope`.

`setDenial` MUST revert, in the order given under Revert Order, if:

* Caller is not the name controller for `trustorNode` (approved operators are **not**
  sufficient; see Signature Authority). This includes a name whose `controllerOf` is the
  zero address
* `trustorNode == trusteeNode`

If valid and the stored flag is `false`:

* The stored flag for `(trustorNode, trusteeNode, scope)` MUST be set to `true`
* The trust level, expiry and every other stored part of the record MUST NOT change
* `DenialSet` MUST be emitted with `denied = true`

If the stored flag is already `true`, `setDenial` MUST succeed without changing storage
and MUST NOT emit `DenialSet`. `setDenial` takes no nonce: denying is never blocked by
other activity on the trustor's name.

#### `clearDenial`

`clearDenial` clears the trustor's denial of `trusteeNode` in `scope`, on condition that
the endorsements it would reveal are the ones the caller reviewed.

`clearDenial` MUST revert, in the order given under Revert Order, if:

* Caller is not the name controller for `trustorNode`, as for `setDenial`
* `trustorNode == trusteeNode`
* `getNonce(trustorNode) != expectedNonce`, with `NonceChanged(expectedNonce, current)`

The nonce check applies whether or not the flag is set. Every attestation that is
stored raises the trustor's nonce, as does `invalidateNonces`, so an unchanged nonce
means no endorsement by the trustor, for any trustee and in any scope, has been written
since the caller read it. Withdrawals do not raise the nonce, but can only reduce trust.

If valid and the stored flag is `true`:

* The stored flag for `(trustorNode, trusteeNode, scope)` MUST be set to `false`
* The trust level, expiry and every other stored part of the record MUST NOT change
* `DenialSet` MUST be emitted with `denied = false`

If the stored flag is already `false`, `clearDenial` MUST succeed without changing
storage and MUST NOT emit `DenialSet`. Every `DenialSet` event from either function
therefore records a change, and the latest event for a key gives its stored flag.

The nonce check stops an attestation that lands *before* the clear from taking effect
through it. It cannot stop one that lands *after*: an attestation signed above the
current nonce stays submittable, and once the denial is cleared it applies as soon as it
is stored. A caller SHOULD therefore clear in this order:

1. If the trustor may have signed attestations that are not yet stored, call
   `invalidateNonces` with a floor above every nonce the trustor has signed
2. After that call has executed, read `getNonce(trustorNode)`, review the stored
   endorsements, and pass the nonce read to `clearDenial`

After step 1 no attestation signed earlier can be stored, either before the clear or
after it, so the endorsements reviewed are the ones the clear reveals. Attestations
still in flight then fail with `NonceTooLow` rather than making the clear revert. The
nonce check still catches an attestation the trustor signs after step 1 if it is stored
before the clear. One signed after step 1 and held until after the clear passes the
check and applies once stored. That is fresh authorisation by the controller, not a
stale signature, so no further mechanism is specified. A caller that must not let
unreviewed attestations take effect right after reinstatement SHOULD pause or serialise
the trustor's signing from step 1 until the clear has executed, especially when
different components handle signing and recovery. If `clearDenial` reverts with
`NonceChanged`, the caller SHOULD review again before retrying with the new nonce.

Prior trust is not required: a trustor MAY deny an agent it never endorsed. A universal
denial applies in every scope, and no scoped flag can lift it. Denial has no expiry; it
holds until the controller clears it.

#### `getDenial`

`getDenial` MUST return the denial flag stored for exactly `(trustorNode, trusteeNode,
scope)`. It MUST NOT return the effective denial: after a universal denial,
`getDenial` for a named scope returns that scope's own flag. This matches `getTrust`,
which also returns stored data, and lets clients inspect each scope's flag.

Callers derive the effective denial from two reads:

```text
effectiveDenial(A, B, s) = getDenial(A, B, bytes32(0)) || getDenial(A, B, s)
```

A scoped flag that is `false` never overrides a universal flag that is `true`. To lift
an effective denial in scope `s`, both the universal flag and the flag for `s` have to be
cleared.

#### `invalidateNonces`

Nonces are the only ordering this standard has between a signed attestation and a later
revocation. An attestation carries no signing timestamp, so an attestation signed at
nonce `N` remains submittable until the trustor's nonce reaches `N`, regardless of what
has been revoked in the meantime.

`invalidateNonces` lets a trustor raise that floor.

`invalidateNonces` MUST revert if:

* Caller is not the name controller for `trustorNode` (approved operators are **not**
  sufficient; see Signature Authority)
* `newNonce <= getNonce(trustorNode)`
* `newNonce > getNonce(trustorNode) + 2**32`, evaluated without overflow: when that sum
  exceeds `type(uint64).max`, the bound is `type(uint64).max`. The error is
  `NonceJumpTooLarge(newNonce, bound)`

If valid:

* `getNonce(trustorNode)` MUST return `newNonce`
* `NoncesInvalidated` MUST be emitted

Every attestation signed with a nonce at or below `newNonce` becomes permanently
unusable, whichever trustee or scope it names.

Trustors SHOULD issue nonces sequentially (`getNonce(trustorNode) + 1`) so that the
highest outstanding nonce is always known. Signing far ahead of the current nonce
creates attestations that stay submittable indefinitely; `setTrust` rejects an
attestation more than 2^32 above the current nonce.

To quarantine a compromised or misbehaving counterparty, a trustor SHOULD:

1. **Set denial** at universal scope. The counterparty then fails every edge from the
   trustor and every path the trustor evaluates as validator, so gates whose gatekeeper
   is the trustor reject it in every evaluation from then on, even while other agents
   still vouch for it. Gates whose gatekeeper is any other agent are unaffected.
2. **Invalidate outstanding nonces**, with a floor above every nonce the trustor has
   signed. This is trustor-wide: it voids the trustor's outstanding attestations for
   every trustee and scope, not only the counterparty's. Attestations signed with a
   nonce above the chosen floor remain submittable.
3. **Investigate and recover**: once step 2 has executed, read the trustor's nonce,
   review the stored endorsements, withdraw those that should not return, then call
   `clearDenial` with the nonce read if the counterparty is reinstated.

The denial does not depend on step 2: a pre-signed attestation submitted later updates
the stored endorsement but cannot lift the denial. Step 2 matters for recovery, since
clearing the denial reveals whatever endorsement is stored at that time, including one
that a stray signature wrote while the denial was in force. The nonce check in
`clearDenial` depends on step 2. Without it, the check rejects a clear when an
attestation signed earlier lands first, but not when that attestation is held back and
submitted after the clear, where it applies immediately. Reading the nonce after step 2
closes both orderings and leaves the check to cover only attestations the trustor signs
after step 2.

Every step requires the name controller. An approved operator can withdraw endorsements,
but cannot deny, clear a denial, or invalidate nonces.

This procedure assumes the trustor's own controller is sound. If the trustor's
controller is itself compromised, denial is not sufficient, because the same controller
can clear it; see Key Compromise.

#### `verifyPath` — Path Verification Algorithm

`verifyPath` validates a pre-computed trust path.

`verifyPath` returns a single verdict. It MUST return `false` unless **every**
requirement is satisfied, including the `requiredAnchors` constraint. A path whose
edges all verify but which does not traverse a required anchor is not valid, and
implementations MUST NOT report it as such.

**Parameter validation:** `verifyPath` MUST reject `ValidationParams` that violate the
Validation Parameters Constraints **before** using them, through the same routine
`setIdentityGate` uses. This is a correctness requirement, not a courtesy: with
`minEdgeTrust == TrustLevel.Unknown`, the per-edge comparison `level < minEdgeTrust` can
never be true, so an unvalidated call would report a path with no trust at all as valid.

`validateParticipantWithPath` and `validateParticipantAddress` read parameters from a
stored gate rather than from the caller. Those parameters were validated when
`setIdentityGate` stored them, so those functions MAY skip revalidation, as the
pseudocode below does by passing `validateParams = false`; they MUST NOT accept
parameters from any other source.

**Algorithm:**

The external functions are thin wrappers around internal helpers that take `memory`
arguments: `_verifyPath`, `_getTrust` (the stored record, as `getTrust` returns it) and
`_getIdentityGate` (the stored gate, as `getIdentityGate` returns it). The
`validateParams` flag is `false` only for parameters read from a stored gate.

```solidity
function verifyPath(
   TrustPath calldata path,
   ValidationParams calldata params
) external view returns (bool valid) {
   return _verifyPath(path, params, true);
}

function _verifyPath(
   TrustPath memory path,
   ValidationParams memory params,
   bool validateParams
) internal view returns (bool) {
   // Parameters are validated BEFORE they are used. Skipping this would let
   // minEdgeTrust == Unknown make every edge comparison vacuously pass.
   if (validateParams) requireValidParams(params);

   // Path must have at least 2 nodes (validator and target)
   if (path.nodes.length < 2) return false;

   // Path length constraint (edges = nodes - 1)
   if (path.nodes.length - 1 > params.maxPathLength) return false;

   // Nodes MUST be distinct; a repeated node inflates length without adding trust.
   // maxPathLength caps this at 11 nodes, so the quadratic scan is bounded.
   for (uint256 i = 0; i < path.nodes.length; i++) {
      for (uint256 j = i + 1; j < path.nodes.length; j++) {
         if (path.nodes[i] == path.nodes[j]) return false;
      }
   }

   // The validator's denial overrides any indirect endorsement.
   // k = 1 is already covered by the first edge check.
   bytes32 validator = path.nodes[0];
   for (uint256 k = 2; k < path.nodes.length; ++k) {
      (, , bool validatorDenies) = _edgeTrust(validator, path.nodes[k], params.scope);
      if (validatorDenies) return false;
   }

   // Track anchor satisfaction
   bool foundAnchor = params.requiredAnchors.length == 0;

   // Verify each edge
   for (uint256 i = 0; i < path.nodes.length - 1; i++) {
      // Universal None, then a stored scoped record, then universal trust
      (TrustLevel level, uint64 expiry, bool denied) =
         _edgeTrust(path.nodes[i], path.nodes[i + 1], params.scope);

      // Denial voids the edge whatever its endorsement
      if (denied) return false;

      // Edge must meet minimum trust level
      if (level < params.minEdgeTrust) return false;

      // None explicitly voids (even if minEdgeTrust is somehow None)
      if (level == TrustLevel.None) return false;

      // Expiry check
      if (params.enforceExpiry && expiry != 0 && expiry <= block.timestamp) {
         return false;
      }

      // Anchor check (intermediate nodes only, not first or last)
      if (!foundAnchor && i > 0) {
         for (uint256 j = 0; j < params.requiredAnchors.length; j++) {
            if (path.nodes[i] == params.requiredAnchors[j]) {
               foundAnchor = true;
               break;
            }
         }
      }
   }

   // Anchors are part of the verdict, not a separate advisory signal
   return foundAnchor;
}
```

**Node uniqueness:** A path MUST NOT contain the same node twice. Repeated nodes cannot
manufacture trust, since every edge is still checked, but they inflate path length and
serve no purpose. `maxPathLength` bounds a path at 11 nodes, so the duplicate scan is at
most 55 comparisons.

**Edge denial:** `verifyPath` MUST return `false` if, for any edge `nodes[i] → nodes[i+1]`,
`EffectiveDeny(nodes[i], nodes[i+1], params.scope)` holds (see Denial). This applies
whatever endorsement the edge stores, whether or not `enforceExpiry` is set, and
whatever `minEdgeTrust` is.

**Validator denial:** `verifyPath` MUST return `false` if, for any node `nodes[k]` with
`k >= 2`, `EffectiveDeny(nodes[0], nodes[k], params.scope)` holds. For `k = 1` the edge
denial check already applies.

The validator check is per validator, not global. It applies only to paths evaluated
with the denying agent as `nodes[0]`. A denial held by any other node on the path voids
the path only through the edge check on that node's own outgoing edge. The validator
check reads denial only: the validator's trust levels for nodes beyond the first,
including `None` and `Unknown`, do not void the path.

**Formal statement.** Write a path as `P = [P0, …, Pn]` with `n ≥ 1`, write `s` for
`params.scope`, write `L(A, B, s)` and `x(A, B, s)` for the level and expiry chosen by the
Scope fallback semantics, and write `D(A, B, s)` for `EffectiveDeny(A, B, s)`. A path
that has distinct nodes, satisfies `n ≤ maxPathLength`, and meets the anchor
requirement is valid if and only if both of the following hold:

```text
Endorsement:  ∀ i ∈ [0, n):  L(Pi, Pi+1, s) ≥ minEdgeTrust
                             ∧ ¬(enforceExpiry ∧ x(Pi, Pi+1, s) ≠ 0 ∧ x(Pi, Pi+1, s) ≤ now)

Denial:       ∀ i ∈ [0, n):  ¬D(Pi, Pi+1, s)
              ∀ k ∈ [2, n]:  ¬D(P0, Pk, s)
```

Together the two denial lines give `¬D(P0, Pk, s)` for every `k ∈ [1, n]`. The two checks
read different state under different authority. The endorsement check reads levels,
which are written by signed attestations and by withdrawals that approved operators may
submit. The denial check reads denial flags, which only the name controller can write.
A withdrawal therefore changes only the endorsement check, and only for the edge it
names.

**Anchor semantics:** Only intermediary nodes (indices `1` through
`nodes.length - 2`) can satisfy `requiredAnchors`. The validator and the target
are excluded, so a direct edge `[A, B]` can never satisfy a non-empty
`requiredAnchors`. For the same reason `setIdentityGate` rejects a gate that requires
anchors but allows only one edge (see Gate Constraints). Callers that want anchors to
be advisory MUST pass an empty `requiredAnchors` array rather than inspecting a partial
result.

**Scope fallback semantics:**

"Universal" below means the record stored at `bytes32(0)`. When `params.scope` is
`bytes32(0)`, the universal record applies. Otherwise, implementations MUST determine
each edge's trust as follows:

1. If the universal level is `None`, the edge is `None`: universal withdrawal voids the
   edge in every scope
2. Otherwise, if a record is stored at `params.scope` (its level is not `Unknown`), that
   record applies, whether or not it has expired
3. Otherwise, the universal record applies

An expired scoped record therefore never falls back to universal trust: with
`enforceExpiry` set, the edge fails.

Denial does not use these fallback rules. `_edgeTrust` returns the effective denial
alongside the level and expiry, read from the same records. The reference
implementation packs the level, expiry, write time and denial flag of each record into
one storage slot, so the edge denial check needs no storage read beyond those the
fallback rules already make. The validator denial check reuses the same function for
the pair `(nodes[0], nodes[k])` and ignores its level and expiry.

```solidity
function _edgeTrust(bytes32 from, bytes32 to, bytes32 scope)
   internal view returns (TrustLevel level, uint64 expiry, bool denied)
{
   TrustRecord storage u = _trust[from][to][bytes32(0)];

   // Universal denial holds in every scope, so no scoped record can lift it
   if (scope == bytes32(0) || u.denied) return (u.level, u.expiry, u.denied);

   TrustRecord storage s = _trust[from][to][scope];

   // 1. Universal None voids the endorsement in every scope
   if (u.level == TrustLevel.None) return (TrustLevel.None, u.expiry, s.denied);

   // 2. Any stored scoped record is authoritative, expired or not
   if (s.level != TrustLevel.Unknown) return (s.level, s.expiry, s.denied);

   // 3. Otherwise universal applies
   return (u.level, u.expiry, s.denied);
}
```

`TrustRecord` and `_trust` are the reference implementation's storage. Implementations
MAY store records differently, provided `_edgeTrust` returns the same values.

#### `setIdentityGate`

Gates are stored under the key `(msg.sender, coordinationType)`. A caller can only
create, replace, or remove gates within its own namespace, so no cross-caller
authorisation check is required and two callers can never collide on the same
`coordinationType`.

`setIdentityGate` MUST revert if `params` violates the Validation Parameters
Constraints or the Gate Constraints, in the order given under Revert Order.

`setIdentityGate` MUST NOT require the caller to control `gatekeeperNode`. Naming a
gatekeeper only reads that agent's public attestations; it neither modifies them nor
makes any claim on the gatekeeper's behalf.

If valid:

* The gate MUST be stored at `(msg.sender, coordinationType)` and marked enabled
* `IdentityGateSet` MUST be emitted

#### `removeIdentityGate`

`removeIdentityGate` MUST revert with `GateNotFound` if no enabled gate exists at
`(msg.sender, coordinationType)`.

If valid:

* The gate MUST be disabled
* `IdentityGateRemoved` MUST be emitted

#### `validateParticipantWithPath`

This function gates [ERC-8001](./eip-8001.md) coordination participation for a participant
identified by ENS node.

The caller names the agent being gated. Implementations MUST bind the result to that
agent by requiring the path to terminate at `participantNode`; a path that merely
originates at the gatekeeper proves nothing about the participant.

```solidity
function validateParticipantWithPath(
   address coordinator,
   bytes32 coordinationType,
   bytes32 participantNode,
   TrustPath calldata path
) external view returns (bool isValid) {
   (bytes32 gatekeeperNode, ValidationParams memory params, bool enabled) =
               _getIdentityGate(coordinator, coordinationType);

   // No gate = misconfiguration, not open participation
   if (!enabled) revert GateNotFound(coordinationType);

   if (path.nodes.length < 2) return false;

   // Path MUST start at the gatekeeper...
   if (path.nodes[0] != gatekeeperNode) return false;

   // ...and MUST terminate at the participant being gated
   if (path.nodes[path.nodes.length - 1] != participantNode) return false;

   return _verifyPath(path, params, false);
}
```

`validateParticipantWithPath` MUST return `false` if:

* `path.nodes.length < 2`
* `path.nodes[0] != gatekeeperNode`
* `path.nodes[path.nodes.length - 1] != participantNode`
* `verifyPath(path, params)` returns `false`

It MUST revert with `GateNotFound` when no gate is enabled at
`(coordinator, coordinationType)`, before any other check. An unconfigured gate is a
misconfiguration, not open participation; see "Why Do Unconfigured Gates Revert?".

#### `validateParticipantAddress`

This is the hook an [ERC-8001](./eip-8001.md) coordinator calls for each entry in an
intent's `participants` array. It differs from `validateParticipantWithPath` only in
how the participant is identified: by address rather than by node.

```solidity
function validateParticipantAddress(
   address coordinator,
   bytes32 coordinationType,
   address participant,
   TrustPath calldata path
) external view returns (bool isValid) {
   (bytes32 gatekeeperNode, ValidationParams memory params, bool enabled) =
               _getIdentityGate(coordinator, coordinationType);

   // No gate = misconfiguration, not open participation
   if (!enabled) revert GateNotFound(coordinationType);

   if (participant == address(0)) return false;
   if (path.nodes.length < 2) return false;
   if (path.nodes[0] != gatekeeperNode) return false;

   // The terminal node MUST be bound to the participant address
   if (resolveAgent(path.nodes[path.nodes.length - 1]) != participant) return false;

   // A name nobody controls cannot vouch for its addr record, which may be stale
   if (controllerOf(path.nodes[path.nodes.length - 1]) == address(0)) return false;

   return _verifyPath(path, params, false);
}
```

`validateParticipantAddress` MUST return `false` if:

* `participant == address(0)`
* `path.nodes.length < 2`
* `path.nodes[0] != gatekeeperNode`
* `resolveAgent(path.nodes[path.nodes.length - 1]) != participant`
* `controllerOf(path.nodes[path.nodes.length - 1]) == address(0)`
* `verifyPath(path, params)` returns `false`

It MUST revert with `GateNotFound` when no gate is enabled at
`(coordinator, coordinationType)`, before any other check.

Because an unresolvable node yields `address(0)`, the `participant == address(0)` check
also prevents a node with no `addr` record from matching a zero participant address.

The controller check fails closed. ENS does not clear an `addr` record when its name
loses its controller, so the record of a wrapped name past its NameWrapper expiry, or of
an emancipated subname past its expiry, still resolves, although nobody can act for the
name to confirm or correct it. Such a node is not admitted by address. The check reads
`controllerOf` only, not expiry or fuses, so it does not reject an expired unwrapped name
or a name in its grace period, and a migrated name whose legacy resolver is kept still
passes, because `controllerOf` returns the Graveyard. A wrapped name that lost its
controller this way passes again once a renewal through the NameWrapper restores its
holder (see Wrapped Name Lifecycle). `validateParticipantWithPath` identifies the
participant by node and does not consult control.

A coordinator gating a full [ERC-8001](./eip-8001.md) intent calls this once per entry in
`participants`, supplying one pre-computed path per participant.

The result describes the registry's state when it is evaluated. A denial, withdrawal,
expiry or change of control affects later evaluations only, so a participant admitted
when it accepted an intent stays admitted unless the coordinator calls the hook again. A
coordinator whose execution relies on its participants still passing the gate SHOULD
evaluate the gate again in the transaction that executes, for example in
`executeCoordination`, with the paths supplied in its `executionData`. That evaluation
reads registry state only. It cannot apply the continuity check under Wrapped Name
Lifecycle, which reads BaseRegistrar and registry events that no transaction can see. A
coordinator that relies on that check SHOULD perform it off-chain on the paths it will
supply, as close to execution as it can, and SHOULD NOT treat the on-chain result as
covering it.

### Errors

Implementations MUST revert with these errors:

```solidity
error SelfTrustProhibited();
error InvalidAttestationLevel(TrustLevel level);
error NonceTooLow(uint64 provided, uint64 required);
error AttestationExpired(uint64 expiry, uint64 currentTime);
error InvalidSignature();
error NotAuthorized(bytes32 node, address actor);
error ENSNameNotFound(bytes32 node);
error GateNotFound(bytes32 coordinationType);
error InvalidMaxPathLength(uint8 provided);
error InvalidMinEdgeTrust(TrustLevel provided);
error TooManyRequiredAnchors(uint256 provided);
error UnsatisfiableAnchors(uint8 maxPathLength);
error BatchLengthMismatch();
error EmptyBatch();
error BatchTrustorMismatch();
error BatchNonceNotIncreasing();
error EmptyScopeList();
error NonceJumpTooLarge(uint64 provided, uint64 max);
error NonceChanged(uint64 expected, uint64 current);
```

`NonceTooLow.required` is `getNonce(trustorNode) + 1`, saturating at
`type(uint64).max`. Once a trustor's floor reaches `type(uint64).max` no nonce is
accepted, and both `setTrust` and `invalidateNonces` revert with
`NonceTooLow(provided, type(uint64).max)` rather than an arithmetic panic.

#### Revert Order

Implementations MUST evaluate conditions in the order listed and MUST revert with the
first error that applies.

| Function | Order | Condition | Error |
|----------|:-----:|-----------|-------|
| `setTrust` | 1 | `trustorNode == trusteeNode` | `SelfTrustProhibited` |
| `setTrust` | 2 | `level` is `Unknown` or `None` | `InvalidAttestationLevel` |
| `setTrust` | 3 | `expiry != 0 && expiry <= block.timestamp` | `AttestationExpired` |
| `setTrust` | 4 | `ens.owner(trustorNode) == address(0)` | `ENSNameNotFound` |
| `setTrust` | 5 | `nonce <= getNonce(trustorNode)` | `NonceTooLow` |
| `setTrust` | 6 | `nonce > getNonce(trustorNode) + 2**32`, capped at `type(uint64).max` | `NonceJumpTooLarge` |
| `setTrust` | 7 | signature fails, including `controllerOf` returning zero for an expired wrapped name | `InvalidSignature` |
| `setTrustBatch` | 1 | lengths differ | `BatchLengthMismatch` |
| `setTrustBatch` | 2 | empty array | `EmptyBatch` |
| `setTrustBatch` | 3 | any `trustorNode` differs from the first | `BatchTrustorMismatch` |
| `setTrustBatch` | 4 | nonces not strictly increasing | `BatchNonceNotIncreasing` |
| `setTrustBatch` | 5 | per item, the `setTrust` checks in order | as above |
| `revokeTrust` | 1 | caller not controller or approved operator | `NotAuthorized` |
| `revokeTrustBatch` | 1 | caller not controller or approved operator | `NotAuthorized` |
| `revokeTrustBatch` | 2 | `scopes` empty | `EmptyScopeList` |
| `setDenial` | 1 | caller not controller | `NotAuthorized` |
| `setDenial` | 2 | `trustorNode == trusteeNode` | `SelfTrustProhibited` |
| `clearDenial` | 1 | caller not controller | `NotAuthorized` |
| `clearDenial` | 2 | `trustorNode == trusteeNode` | `SelfTrustProhibited` |
| `clearDenial` | 3 | `getNonce(trustorNode) != expectedNonce` | `NonceChanged` |
| `invalidateNonces` | 1 | caller not controller | `NotAuthorized` |
| `invalidateNonces` | 2 | `newNonce <= getNonce(trustorNode)` | `NonceTooLow` |
| `invalidateNonces` | 3 | `newNonce > getNonce(trustorNode) + 2**32`, capped at `type(uint64).max` | `NonceJumpTooLarge` |
| `setIdentityGate`, `verifyPath` | 1–3 | the Validation Parameters Constraints, in table order | `InvalidMaxPathLength`, `InvalidMinEdgeTrust`, `TooManyRequiredAnchors` |
| `setIdentityGate` | 4 | the Gate Constraints: `requiredAnchors.length > 0 && maxPathLength < 2` | `UnsatisfiableAnchors` |
| `removeIdentityGate` | 1 | no enabled gate | `GateNotFound` |
| `validateParticipantWithPath`, `validateParticipantAddress` | 1 | no enabled gate | `GateNotFound` |

### Recommended Reason Codes

For `TrustRevoked` and `DenialSet` events, the following reason codes are RECOMMENDED:

| Reason Code | Value | Meaning |
|-------------|-------|---------|
| Unspecified | `bytes32(0)` | No specific reason |
| Misbehaviour | `keccak256("MISBEHAVIOR")` | Agent acted improperly |
| Compromised | `keccak256("COMPROMISED")` | Key or account compromised |
| Inactive | `keccak256("INACTIVE")` | Agent no longer active |
| Transfer | `keccak256("TRANSFER")` | ENS name transferred |

Reason codes are informational and do not change the effect of an operation. Every
withdrawal sets the level to `None` and nothing else: a withdrawal recorded as
`COMPROMISED` does not deny the agent, and paths that avoid the withdrawn edge stay
valid. Where exclusion is intended, it MUST be expressed with `setDenial`.
`clearDenial` MAY use `bytes32(0)`.

### Recommended Scopes

For interoperability, the following scope values are RECOMMENDED:

| Scope | Value | Use Case |
|-------|-------|----------|
| Universal | `bytes32(0)` | Trust applies to all contexts |
| DeFi | `keccak256("DEFI")` | DeFi coordination |
| Gaming | `keccak256("GAMING")` | Gaming/metaverse |
| MEV | `keccak256("MEV")` | MEV protection |
| Commerce | `keccak256("COMMERCE")` | Agentic commerce |

### Recommended Coordination Types

For [ERC-8001](./eip-8001.md) identity gates:

| Coordination Type | Value |
|-------------------|-------|
| MEV Coordination | `keccak256("MEV_COORDINATION")` |
| DeFi Yield | `keccak256("DEFI_YIELD")` |
| Gaming Match | `keccak256("GAMING_MATCH")` |
| Commerce Escrow | `keccak256("COMMERCE_ESCROW")` |

## Rationale

### Why ENS Instead of a New Identity System?

ENS is finalised [ERC-137](./eip-137.md), battle-tested, and widely adopted. Creating a new identity system would:

* Add dependency on draft standards
* Fragment the identity ecosystem
* Require new adoption efforts

ENS provides everything needed: stable identifiers, ownership semantics, and extensibility.

### Relationship to Token-Based Agent Identity Registries

Some agent identity registries represent each agent as an [ERC-721](./eip-721.md) token,
and attach reputation and validation records to that token rather than to a name. This
registry does not depend on any of them, but it can consume such an identity through an
adapter. For example, a protocol can issue an ENS subname for each agent token, keep the
subname's controller in step with the token's holder, and point its `addr` record at the
agent's address. The agent then attests, is attested to, and passes identity gates
through that name exactly as any other ENS-named agent does.

Keeping the dependency in an adapter means `requires` lists only finalised standards, and
a change to a registry that is still in draft cannot change the behaviour of this one.

### Why Scope as Storage Key?

A trustor may have different trust levels for the same trustee in different contexts. For example:

* Trust `bob.eth` fully for DeFi coordination
* Trust `bob.eth` marginally for gaming

Making scope part of the storage key `(trustorNode, trusteeNode, scope)` enables this naturally. Universal trust `bytes32(0)` serves as a fallback when scoped trust is not specified. A universal withdrawal overrides every scope for its trustor and trustee pair, and a universal denial applies in every scope.

### Why minEdgeTrust Instead of Marginal/Full Thresholds?

Earlier drafts had `marginalThreshold` and `fullThreshold` parameters, designed for on-chain graph traversal with marginal accumulation. This standard has no on-chain traversal (see "Why verifyPath Only (No On-Chain Search)?"), and its core primitive, `verifyPath`, checks one path, so it needs only the minimum trust level each edge must have.

This simplification:

* Reduces parameter complexity
* Makes path verification straightforward
* Gives every path a single verdict that any implementation reproduces

This standard does not define accumulation. An application that wants it can verify several paths with `verifyPath` and combine the verdicts under its own rule, which is outside this standard.

### Why Separate Signing Authority from Transaction Submission?

ENS approvals (`isApprovedForAll`) are designed for operators to manage names on behalf of controllers. However, allowing approved operators to forge attestation signatures would break the cryptographic binding between attestations and name controllers.

By restricting signing authority to the name controller (or [ERC-1271](./eip-1271.md) for contract controllers) while allowing operators to submit transactions like `revokeTrust`, we preserve:

* Cryptographic integrity of attestations
* Operational flexibility for name management
* Clear security boundaries

### Why verifyPath Only (No On-Chain Search)?

On-chain graph traversal is expensive and creates DoS vectors:

* Branching factor can explode with user-controlled adjacency lists
* Gas costs are unpredictable
* Attackers can bloat trustee lists

By requiring pre-computed paths, this standard:

* Keeps on-chain verification O(path length)
* Pushes search complexity to off-chain indexers where it belongs
* Enables predictable gas costs

This standard defines no on-chain search, enumeration or trust-aggregation function.
Earlier drafts declared such functions as an optional extension. They were removed
because their results were not specified precisely enough to be interoperable (see
Deployments of Earlier Drafts).

*Non-normative note:* a later standard could define graph discovery or trust
aggregation as a separate extension with its own interface identifier. To be
interoperable it would have to state how each function applies scope fallback, expiry,
withdrawal, edge denial and validator denial, as `verifyPath` does.

### Why Four Trust Levels?

The four-level model (Unknown, None, Marginal, Full) is proven by GnuPG's 25+ years of use. Finer granularity adds complexity without clear benefit; coarser granularity loses important distinctions.

With `minEdgeTrust`, applications can choose their security posture:

* `minEdgeTrust: Full` — Only fully trusted paths
* `minEdgeTrust: Marginal` — Accept marginal trust (default)

Denial is a flag beside the level, not a fifth level. A level answers how far the
trustor vouches for an agent; denial answers whether the trustor will rely on the agent
at all. The two must vary independently: a trustor needs to deny an agent without losing
the endorsement it would restore afterwards, and to withdraw an endorsement without
denying. A single ordered enum cannot hold both answers at once, and a fifth value
would also change the ordering that `minEdgeTrust` comparisons rely on.

### Divergence from OpenPGP

OpenPGP keeps two quantities apart. Validity says whether a key belongs to its claimed
holder, and is computed from certifications. Owner trust says how far a user relies on a
key holder's certifications, and each user assigns it privately. This standard keeps
one quantity, the level on an edge. Where the trustee is an intermediary, the level acts
as owner trust: it decides whether the path may continue through that agent. Where the
trustee is the target, it acts as validity. The binding between a name and its
controller, which OpenPGP validity establishes for keys, comes from ENS ownership rather
than from attestations.

The required `verifyPath` also performs no marginal accumulation. In OpenPGP a key
becomes valid with one fully trusted certification or several marginally trusted ones
(GnuPG requires three by default). Here a single path is checked and every edge must
reach `minEdgeTrust`, so marginal edges never combine. This standard does not define
accumulation across several paths, and has no counterpart to GnuPG's thresholds.

Both choices keep on-chain verification to one verdict per path at O(path length) cost.

### Why Required Anchors?

Sybil attacks are the primary threat to web of trust systems. Required anchors force trust paths to traverse established community nodes (DAOs, protocols, auditors), transforming Sybil resistance from application-layer advice into protocol-level enforcement.

### Why Are Gates Keyed by Caller Address?

`coordinationType` in [ERC-8001](./eip-8001.md) is a proposer-chosen identifier such as
`keccak256("MEV_SANDWICH_COORD_V1")`. It carries no namespace of its own, so two
unrelated applications can and will choose the same value. A gate registry keyed on
`coordinationType` alone would hand whichever application registered first permanent
control of that identifier for everybody else, and the Recommended Coordination Types
in this document would be the first values squatted.

Keying gates by `(msg.sender, coordinationType)` gives every caller a namespace it
inherently owns. Squatting becomes impossible, registering a gate requires no ENS name
at all, and well-known coordination type constants stay safe to publish.

The alternative of keying by `(gatekeeperNode, coordinationType)` was rejected because
it conflates two distinct roles: the party that *sets a policy* and the party whose
trust graph the policy *reads*. A coordinator should be able to gate on any agent's
public attestations without that agent's involvement.

### Why Do Unconfigured Gates Revert?

The integrator supplies both halves of the gate key, `(coordinator, coordinationType)`.
A mistyped coordination type, or a coordinator address taken from the wrong deployment,
finds no gate. If an unconfigured gate were open, that mistake would admit every
participant while behaving exactly like a working gate, and nothing on-chain would show
it until an untrusted agent had already joined.

Reverting with `GateNotFound` makes the misconfiguration fail at the first call.
Coordinators that want open participation for some coordination types lose nothing:
they do not call the hook for those types. Calling it is a statement that a gate is
expected.

Returning `false` was rejected. It also fails closed, but a missing gate would then be
indistinguishable from a participant who is not trusted, so the coordinator and the
participant would both be told the participant was at fault. A revert names the real
problem and carries the coordination type that was looked up.

The cost is that the hook can no longer be called unconditionally as a generic module.
A coordinator that gates only some coordination types has to know which ones, and after
`removeIdentityGate` further calls for that type revert until the coordinator stops
making them. Coordinators that need to branch can read `getIdentityGate(...).enabled`
first.

### Why Forward Resolution Instead of a Binding Registry?

The `addr` record is already the name controller's authoritative statement of which
address a name denotes. Reusing it means agents that are usable today are usable here
with no registration step, no additional storage, and no second source of truth to keep
synchronised.

Reverse resolution was rejected: reverse records are self-asserted, verifying one
requires a forward-resolution round trip, and doing so on-chain means string handling
and namehash computation over untrusted input.

The trade-off is that resolution follows the `addr` record, not the controller. A
controller can repoint a trusted name at a new address. The record can also outlive the
controller that set it, because ENS does not clear it when a name expires or is
re-registered. After a re-registration the record keeps naming the previous holder's
address, and address-based gates keep admitting that address, until the new registrant
changes the record or sets it when registering. A lapsed holder of an unwrapped name can
also still change the record. Requiring the terminal node to have a controller removes
the case of a wrapped name nobody controls, but not these. See Resolver Trust and
Expired Unwrapped Names.

### Why Does Resolution Cap Gas and Never Revert?

A participant chooses its own resolver, and an [ERC-8001](./eip-8001.md) coordinator
typically validates every participant of an intent in one call. If resolution could
revert, or could consume 63/64 of the remaining gas, one participant could abort
validation for all the others. Capping the forwarded gas and treating every malformed
answer as "no address" makes a hostile resolver cost the caller at most the cap and fail
closed for that participant only.

The 50,000 cap is well above what deployed resolvers need. Measured at mainnet block
26,173,410 with all touched state cold, `addr(bytes32)` needed 3,669 gas on the 2020
PublicResolver, 5,654 on the current PublicResolver, 8,726 on the `base.eth` resolver and
18,752 on the most expensive resolver sampled, which forwards through several contracts.
The cap is not a protocol constant: gas repricing could change these figures, which is
why the bound itself is left to implementations.

[ERC-1271](./eip-1271.md) checks are not gas-capped. They run only when an attestation is
submitted, and a batch carries a single trustor, so a controller that consumes gas can
only waste gas for whoever submits its own attestations. Validation never verifies
signatures.

### Why a Single Per-Trustor Nonce?

A single monotonic nonce per trustor serialises that agent's attestations: signatures
must be submitted in ascending order, and a signature is permanently stranded if a
higher-nonced one lands first. Three alternatives avoid some or all of that cost, and
each was considered separately.

**Unordered nonce bitmap.** Any unused nonce is accepted, so ordering stops mattering.
There is also no floor to raise, so `invalidateNonces` cannot exist. A trustor
responding to a compromise would have to enumerate and burn every outstanding nonce
individually. That requires knowing them, which is exactly what a compromised agent does
not know.

**Ordered nonces per `(trustor, trustee, scope)`.** Each relationship has its own floor,
so issuing for one relationship never strands another. Invalidation remains possible,
but only one record at a time. Quarantining a counterparty means raising a floor for
every scope the trustor ever used with it, enumerated off-chain as for
`revokeTrustBatch`, and trustor-wide invalidation means doing that for every trustee.
Signers also have to track one counter per relationship rather than one per identity.

**Global floor plus per-trustee floor.** An attestation would be valid only above both
the trustor's global nonce and a floor kept for its `(trustor, trustee)` pair. This does
allow targeted quarantine: a trustor could void the outstanding attestations naming one
counterparty without touching any other. Its costs are:

* One extra storage read in `setTrust`. Path verification is unaffected, since it reads
  stored relationships rather than nonces.
* A second nonce dimension that every signer has to track and every attestation has to
  clear.
* A larger interface. The per-trustee floor needs its own setter and getter, which
  changes the `ITrustRegistry` interface identifier.
* No benefit when the trustor's own key is compromised, since the attacker controls the
  per-trustee floors as well as the global one.

The hybrid was not adopted because the case it improves is narrow. Targeted quarantine
helps only when a trustor has outstanding signatures naming the quarantined counterparty
and, at the same time, outstanding signatures naming others that it wants to keep.
Sequential issuance, which this standard recommends, keeps that window small, and the
cost of a trustor-wide floor within it is re-signing the attestations still in flight.
That does not justify a second nonce space for every signer and a larger interface for
every implementation.

The serialisation cost is therefore the price of a single trustor-wide floor that one
call can raise. It is a reasonable trade for a trust registry, where attestations
are infrequent and correct revocation matters more than issuance throughput. Agents
needing high issuance throughput should batch with `setTrustBatch`, which consumes a
contiguous nonce range in one transaction.

### Why Attestations Grant and Revocations Withdraw?

Earlier drafts allowed `setTrust` to carry `TrustLevel.None`, which meant `None` could
be reached two ways: by a signed attestation, or by `revokeTrust`. The two paths had
different authorisation models (signature versus caller), emitted different events, and
disagreed on whether prior trust was required.

Restricting attestations to `Marginal` and `Full`, and routing every withdrawal through
revocation, gives each level exactly one path, one event, and one authorisation rule.
Revocation no longer requires prior trust, so a trustor can also withdraw preemptively
in a scope it never granted, leaving a tombstone that blocks the universal fallback
there.

### Why Explicit Nonce Invalidation?

Revocation cannot, on its own, undo an attestation that has been signed but not yet
submitted. Attestations carry no signing timestamp, so the only ordering available
between "signed" and "revoked" is the trustor's nonce, and a revocation does not know
what nonces the trustor has already signed.

Advancing the nonce by one during `revokeTrust` would be security theatre: an
attestation signed with nonce 10 while the counter sits at 0 survives any small bump.
Only the trustor knows the highest nonce it has issued, so only the trustor can set a
floor that actually invalidates its outstanding signatures.

`invalidateNonces` is therefore a separate, explicit operation. The cost is that
quarantining a counterparty is two calls rather than one, a denial and an invalidation,
which is why the Semantics section states the combined procedure and Security
Considerations repeats it.

### Why Is `invalidateNonces` Controller-Only?

The only operation this standard lets an approved operator perform is bounded. A
withdrawal affects one `(trustorNode, trusteeNode)` pair and the scopes named in the
call, so delegating it to an approved operator lets an ops key handle routine incident
response without being able to cause harm the controller cannot easily undo.

`invalidateNonces` is not bounded. Raising the nonce floor permanently voids every
attestation the trustor has signed but not yet submitted, across all trustees and all
scopes, and no counterparty can restore them. ENS `isApprovedForAll` is a coarse,
long-lived approval granted so an operator can manage a name, and holders frequently
grant it to marketplaces and management tools. Reading that approval as authority to
void an agent's entire outstanding attestation set would give those tools a power their
grantor never contemplated.

Restricting the operation to the controller costs one thing: an operator responding to
an incident can withdraw endorsements but cannot complete a quarantine, which needs the
controller for both the denial and the nonce invalidation. That is the correct place to
require the controller.

### Why Is Universal Withdrawal Authoritative?

A trustor that withdraws its endorsement at universal scope means it everywhere. If
universal `None` applied only where no scoped record existed, every scoped grant the
trustor had ever made to that trustee would keep its edge alive, and the trustor would
have to find and withdraw each one before the withdrawal took full effect.

Making universal `None` override every scope for the same pair gives the trustor one
call that holds in every scope. It covers scopes the trustor has overlooked, and scoped
attestations signed earlier but submitted later, which would otherwise silently restore
the withdrawn relationship. The override is confined to that one trustor and trustee
pair: it removes the trustor's own edge and leaves paths through other agents alone. The
cost is one extra storage read per edge when verifying in a named scope: a cold storage
read plus its key hashing, about 2,800 gas per edge in the reference implementation, or
about 28,000 gas on a 10-edge path. `verifyPath` is normally called in a `view` context
or once per participant, which makes that cost acceptable for a guarantee that
withdrawal is complete.

The override lasts only while the universal record is `None`. A later universal
attestation replaces it, and every positive scoped record for the pair applies again,
including scoped grants made long before the withdrawal. A trustor re-endorsing at
universal scope SHOULD first review, or withdraw, the scoped grants it does not want to
return.

The rule is applied during verification rather than written into each scoped record, so
`getTrust` stays literal: it returns what is stored for the scope asked about.

A stored scoped record that has expired does not fall back to universal trust. Falling
back would let a lapsed narrow grant silently widen into whatever universal trust the
trustor holds, so the edge fails instead.

### Why Separate Withdrawal from Denial?

A trustor can mean two different things when it stops vouching for an agent. It may
simply no longer wish to endorse the agent itself, or it may refuse to rely on the agent
at all. An earlier draft expressed both with `None`: a validator's `None` for any node
voided every path that validator evaluated through that node. That had two defects.

* **No neutral withdrawal.** `Unknown` cannot be restored, so `revokeTrust` was the only
  way to withdraw an endorsement before it expired, and every withdrawal also excluded
  the agent from the trustor's paths through other agents.
* **Operator escalation.** Approved operators may submit withdrawals, because a
  withdrawal is bounded to one edge. Once `None` excluded the agent, the same
  delegation let an operator decide whose judgement the trustor accepted, a power that
  is not bounded to the edge it writes.

This standard keeps the two apart. `None` withdraws an edge and nothing else, and stays
delegable. Denial is a separate flag, written only by the name controller, that does
exclude. Each operation is observable on its own, through `TrustRevoked` and
`DenialSet`, and through `getTrust` and `getDenial`.

### Why Does a Validator's Denial Override Indirect Paths?

Trust can be transitive, but a denial the validator has expressed remains authoritative
within that validator's own trust decision.

Without the validator check, denial would only remove an edge. Suppose Alice trusts Bob,
Bob trusts Carol, and Alice has denied Carol. The path `[Alice, Carol]` fails, because
its only edge is denied. The path `[Alice, Bob, Carol]` checks only the edges Alice to
Bob and Bob to Carol. Both hold, so the path would verify, and Bob's endorsement would
override Alice's refusal inside Alice's own trust decision. That contradicts what a
denial means.

The check covers intermediaries as well as the target. An intermediary's place on a path
is to vouch for the next node, so the path relies on its judgement (see Divergence from
OpenPGP). A validator that has denied an agent has rejected that agent's judgement. A
path that depends on it is no more acceptable than a path that ends at that agent.

Only the validator's denial counts. Checking every pair of nodes on the path would cost
O(n²) storage reads rather than O(n). It would also let third parties veto other
validators' decisions: any agent placed on a path could void it by denying a node it was
never asked to vouch for. Each agent's denial therefore governs its own outgoing edges,
and its own paths as validator.

OpenPGP has no exact counterpart. Revoking a certification there resembles withdrawal.
Assigning a key an owner trust of "never" stops its certifications counting, which
resembles denial of an intermediary, but it leaves the key itself able to be valid
through other certifications. Denial here also rejects the agent as a target.

The edge check reads no additional storage slot, because the flag shares a slot with the
level and expiry that the edge check already reads; in the reference implementation it
adds a few hundred gas per edge. The validator check costs one `_edgeTrust` call per
node from index 2 onwards, at most 9 on a 10-edge path. Measured in the reference
implementation (solc 0.8.28, optimizer off) with every storage slot cold, a 10-edge path
with no denials costs 146,265 gas in a named scope and 92,033 gas at universal scope,
against 92,673 and 64,126 gas for the same contract with the edge and validator denial
checks removed. Nearly all of the difference is the validator check, about 6,000 gas per
node in a named scope, where each lookup reads the universal and the scoped record cold,
and about 3,000 at universal scope, where it reads one.

### Why Does Clearing a Denial Check the Trustor's Nonce?

Endorsement and denial are stored independently, so an attestation can land while a
denial is in force and is revealed when the denial is cleared. Without a check, a
controller that reviews the stored endorsement and then clears the denial could be
front-run by a relayer submitting another valid attestation, which the clear would then
activate unreviewed.

The check uses the trustor's nonce rather than an expected endorsement. Clearing a
universal denial reactivates the trustee's records in every scope, which a single
expected record cannot describe and an exhaustive list cannot bound. Every stored
attestation raises the nonce, so one comparison covers every scope at the cost of one
storage read. The cost is false positives: any attestation by the trustor, for any
trustee, makes a pending clear revert, and the controller reviews again. Clears are rare,
and this fails closed.

The check closes the front-run, not the back-run. An attestation signed before the review
and held back until after the clear passes the check, because it has not landed yet, and
then applies immediately. Only raising the nonce floor above every signature issued
closes that case, which is why `clearDenial` directs callers to read the nonce after
`invalidateNonces`. Done in that order, attestations still in flight fail instead of
causing false positives. Attestations the controller signs after raising the floor are
fresh authorisations, and pausing signing until the clear executes is left to the
caller. The floor is guidance rather than part of `clearDenial`: folding it in would
void the trustor's outstanding attestations for every other trustee whenever one denial
is cleared, and a trustor with nothing outstanding has no need of it.

The check covers only the trustor's own endorsements. When the trustor is the
validator, clearing a denial also readmits paths that reach the trustee through other
agents, whose endorsements the trustor neither controls nor can pin.

Denying takes no nonce. It is the emergency operation, and it must not fail because an
attestation landed first. For the same reason the guard is a separate function rather
than an optional argument: an unguarded clear would be the easier call, and the
protection would go unused.

### Why Is Denial Controller-Only?

Withdrawal changes one edge. Denial changes which agents' endorsements the trustor
accepts, as validator and on every edge from it, and clearing a denial readmits an
agent the controller excluded. Both directions carry the authority of the trustor's
judgement, the same authority as signing an attestation, so both stay with the
controller.

Denial reuses the existing controller check rather than adding a role, a signature type
or a delegation registry. A controller that needs to delegate it can use a contract
controller, which decides for itself who may act. The limit described under Signature
Authority still applies: an operator that can transfer the name can become the
controller.

### Why Batch Revocation Instead of Revoke-All?

Universal revocation already voids an edge in every scope during path verification.
`revokeTrustBatch` covers the remaining case: withdrawing trust in several named scopes
while keeping trust elsewhere, with `getTrust` reporting `None` in each of them.

A blanket primitive that wrote `None` into every scope would have to enumerate them
on-chain, or shadow individual records with a revocation epoch that `getTrust` would no
longer report literally. Batch revocation keeps scope enumeration off-chain, where this
standard already puts path search. A trustor's client reads its own `TrustSet` logs,
derives the scope list, and submits one transaction. On-chain cost stays proportional to
the scopes actually named.

### Why Unwrap NameWrapper Names?

`ens.owner(node)` returns the NameWrapper contract for wrapped names. NameWrapper does
not implement [ERC-1271](./eip-1271.md), so a registry that used the raw registry owner as
the signing authority would reject every attestation from a wrapped name, and wrapped
names are a large and growing share of `.eth` registrations.

Unwrapping via `ownerOf(uint256(node))` recovers the actual controller and preserves the
[ERC-1271](./eip-1271.md) path for controllers that are themselves contracts. It also
inherits NameWrapper's expiry behaviour: a name that loses its holder at expiry loses its
signing authority at the same moment (see Wrapped Name Lifecycle).

### Why Does Authority Follow Ownership Rather Than Management Permission?

During a `.eth` name's grace period the NameWrapper reports the holder as owner but
refuses every management operation. Requiring `canModifyName` as well as ownership
would remove the name's authority for those 90 days. This standard does not do that:

* The name has not changed hands, and cannot: the NameWrapper refuses to transfer a
  `.eth` name from the start of its grace period. No one else can register it during the
  grace period, and a renewal keeps it with the holder, so the authority still belongs
  to the party who established the name's trust relationships. A renewal that bypasses
  the NameWrapper keeps the registration with the holder but not the NameWrapper expiry.
  The holder can then lose control until a renewal through the NameWrapper restores it.
  No one else gains control in between (see Wrapped Name Lifecycle).
* Removing authority would also remove the emergency controls. A holder who loses
  management permission would no longer be able to withdraw an endorsement, deny a
  compromised agent, or invalidate outstanding signatures. Keeping only those controls
  would split the authority model in two for one lifecycle state, and the split would
  not be clean: `clearDenial` is one of those controls, and it can itself restore trust.
* The check would cover only part of the risk. An unwrapped name keeps its registry
  owner long after it expires, and the registry cannot detect that expiry (see Expired
  Unwrapped Names). An unemancipated subname keeps `canModifyName` after expiry.
* It would add a NameWrapper function to the interfaces this standard depends on.

New endorsements during the grace period are a separate question. If the name is not
renewed they pass to its next registrant. That is not a new kind of exposure: an
endorsement set before an ordinary transfer survives the transfer in the same way (see
Identity Continuity). The contract therefore accepts them, so that one authority rule
applies to every operation. The Specification bounds them through the expiry field
attestations already carry, with recommendations on each side (see Wrapped Name
Lifecycle):

* **Trustor side.** A trustor that endorses after its registrar expiry caps the
  endorsement's `expiry` at the end of its grace period, and endorses again after
  renewing.
* **Coordinator side.** A coordinator treats an attestation as ineligible once a `.eth`
  second-level name it involves, as trustor or trustee or as the ancestor of either, has
  been registered again after the attestation was set, or a name below that second-level
  name, in any of those roles, has been wrapped again. Renewals do not end it; a new
  registration does, even by the same holder, so every trustor of that name signs
  again. It does not rely on a wrapped
  trustor's endorsement while the trustor has no controller, validates with
  `enforceExpiry` set, and re-evaluates its gates when control changes.

These are recommendations to signers and coordinators. They do not change on-chain
acceptance or path validity, and none of them needs a change to the interface.

Binding each record to the registration that issued it would let the registry itself
reject records from an earlier registration of the same name. ENSv1 state does not
support that: recording the registrar expiry is not enough, because a renewal and a
later registration both leave a later expiry, and telling them apart takes the
BaseRegistrar's events. A binding would also change the storage layout and the
interface, and would work only for `.eth` second-level names, because the registry
cannot derive a labelhash from a namehash. This standard leaves it to an ENSv2-native
successor. Under ENSv1, trust inherited through re-registration is bounded by short
attestation expiries, by the coordinator's registration check under Wrapped Name
Lifecycle, and by monitoring control changes (see Identity Continuity).

### Why Pin the NameWrapper?

The `nameWrapper` slot decides who controls every wrapped name. If it could be changed,
whoever changed it could point it at a contract whose `ownerOf` returns an address of
their choosing, and from then on sign attestations, revoke trust, set denials and
invalidate nonces for every wrapped name in the registry. Pinning the address at deployment removes that
takeover path entirely.

The cost falls on a successor wrapper, and only on names that move to it. Names wrapped
in the pinned NameWrapper keep working unchanged. A name wrapped in a successor makes
`ens.owner(node)` return the successor contract. That address is not the pinned wrapper,
so the registry treats it as a raw controller. Because it does not implement
[ERC-1271](./eip-1271.md), that name cannot attest until a registry that recognises the
successor is deployed. Unwrapped names are unaffected.

Supporting a successor therefore means deploying a new registry. Trust state does not
carry over. Stored relationships live in the old registry, and signatures cannot be
replayed against the new one, because `verifyingContract` is part of the
[EIP-712](./eip-712.md) domain. A successor registry can bootstrap its graph by
re-attestation: trustors sign fresh attestations against the new domain, which their
clients can derive from their own `TrustSet` logs.

A governance-updatable slot was rejected because it moves the takeover path rather than
removing it. Whoever holds the update right, whether an owner key, a multisig or a DAO,
gains latent control of every wrapped name, and that is a stronger trust assumption than
ENS itself asks for. A redeployment is a visible, opt-in migration. A slot update would
be a silent change of authority over names whose controllers never agreed to it.

## Backwards Compatibility

This ERC introduces new functionality and does not modify existing standards.

**ENS Compatibility**: Uses standard ENS interfaces only — `owner`, `resolver`, and `isApprovedForAll` on the registry, `addr` on the resolver ([ERC-137](./eip-137.md)), and `ownerOf`/`isApprovedForAll` on the NameWrapper. Works with any deployment that exposes these ENSv1 interfaces, and with deployments that have no NameWrapper. Both wrapped and unwrapped names are supported. ENSv2 is covered under ENSv2 Compatibility and Migration.

On-chain validation does not rely on CCIP-Read. Agents whose names are served by an off-chain resolver can still hold and issue trust, but cannot be validated by address, since `addr` is not readable on-chain for those names.

**ERC-8001 Compatibility**: Designed as a module. [ERC-8001](./eip-8001.md) coordinators can optionally integrate identity gates, calling `validateParticipantAddress` once per entry in an intent's `participants` array. Coordinators call the hook only for coordination types they have gated; for any other type it reverts with `GateNotFound`. No change to [ERC-8001](./eip-8001.md) is required, and coordinators that ignore this standard are unaffected.

**Wallet Compatibility**: Uses [EIP-712](./eip-712.md) signatures, compatible with all major wallets. Supports [ERC-1271](./eip-1271.md) for contract wallets and smart accounts.

### Deployments of Earlier Drafts

This revision is not backwards compatible with registries built from earlier drafts,
although every earlier function keeps its signature. It adds `setDenial`, `clearDenial`,
`getDenial` and `DenialSet`, which changes the `ITrustRegistry` identifier from
`0x42b46ef5` to `0x94f0ee34`. It also changes path verification: a path that verified under an earlier
draft fails under this revision when any of its edges is denied, or when its validator
denies any node on it. Earlier drafts had no way to express that exclusion, so
applications that used withdrawal for it have to re-express it as denial. The record
layout also gains a field.

A later draft, identified by `0x31f35e83`, had a single
`setDenial(bytes32,bytes32,bytes32,bool,bytes32)` that both set and cleared denials. This
revision replaces it with `setDenial(bytes32,bytes32,bytes32,bytes32)`, which only
denies, and `clearDenial`, which checks the trustor's nonce, and adds `NonceChanged`.
Registries built from that draft have the same storage layout and path semantics, and
differ only in how a denial is cleared.

Registries built from earlier drafts that report the same identifier, `0x94f0ee34`,
have the same storage layout, but can differ in behaviour that the identifier does not
reveal:

* In drafts merged before 16 October 2026, `validateParticipantAddress` does not
  require the terminal node to have a controller. They admit the stale `addr` record of
  a wrapped name past its NameWrapper expiry. Coordinators using such a registry SHOULD
  check themselves that the terminal node has a controller, for a wrapped name by
  reading the NameWrapper's `ownerOf`.
* All of them accept a gate that requires anchors but allows only one edge, which this
  revision rejects (see Gate Constraints). Such a gate rejects every participant, so
  the difference fails closed.

Neither `supportsInterface` nor the [EIP-712](./eip-712.md) domain tells these drafts
apart from this revision: all of them report `0x94f0ee34` and the domain version `"1"`.
Both are self-reported, so neither could prove behaviour in any case. A consumer that
depends on the behaviour of this revision identifies the registry by its deployment
address and verified source, not by either query.

For a registry behind an upgradeable proxy, the address and the source verified once do
not fix its behaviour. Whoever can upgrade it can change any rule this standard
specifies, including which ENS registry and NameWrapper it reads, which is the power
"Why Pin the NameWrapper?" removes from the `nameWrapper` slot. A consumer of such a
registry identifies its current implementation and the holder of its upgrade authority,
and monitors both.

Earlier drafts also declared an optional `ITrustRegistryExtended` interface, identified
by `0x93e686a3`, with `getTrustees`, `getTrustors`, `validateAgent`, `pathExists` and
`validateParticipant`. This revision removes it. Those functions did not state how they
applied expiry, withdrawal, denial or scope fallback, and `pathExists` took no
`ValidationParams`, so two implementations could return different results for the same
state. The reference implementation has never implemented `ITrustRegistryExtended` or
reported its identifier (its first version declared functions of some of these names,
with other signatures, in its required interface), and no deployment that reports the
identifier is known. This standard assigns `0x93e686a3` no meaning: a registry that
reports it is not thereby claiming any behaviour defined here. Removing the extension
changes neither `ITrustRegistry` nor its identifier.

Deployed registries cannot adopt this revision in place unless they are upgradeable. An
application moving to it SHOULD:

* Deploy a registry implementing this revision, and point consumers at it. Consumers
  that hold the registry address immutably have to be redeployed as well
* Update any hard-coded `ITrustRegistry` identifier to `0x94f0ee34`
* Recreate trust state with fresh attestations, since signatures are bound to the old
  registry's [EIP-712](./eip-712.md) domain, then recreate identity gates. A withdrawal
  that was meant to exclude an agent SHOULD be recreated as a denial
* Index `DenialSet` alongside `TrustSet` and `TrustRevoked`, and include denial in
  off-chain path search
* Re-run [ERC-8001](./eip-8001.md) participation tests against the new registry,
  including cases where an agent is withdrawn by the gatekeeper but endorsed by others

### ENSv2 Compatibility and Migration

This ERC specifies trust authority and address resolution using ENSv1 registry
interfaces and a NameWrapper address pinned at deployment. It does not specify native
ENSv2 controller resolution, role-based signing authority, or address resolution.

The descriptions of ENSv2 contracts in this section are informative, not normative. They
record the published ENSv2 migration process as deployed on Sepolia, checked against
that deployment at block 11,906,480 (13 October 2026). ENSv2 is not deployed on Ethereum
mainnet. Its mainnet contracts, migration process and timing may differ, and nothing in
this section describes how they will behave. The controller-resolution rules of this
standard do not depend on it: `controllerOf` returns whatever the ENSv1 registry and the
pinned NameWrapper report, whichever contract that is. The recommendations below apply
wherever names migrate in the way described.

Under that process, a migrated name's ENSv1 token is
transferred to the ENSv2 Graveyard contract. An unwrapped or unlocked wrapped name is
unwrapped to the Graveyard, and a locked name stays wrapped with the Graveyard as its
holder. Either way, `controllerOf` returns the Graveyard after migration. The Graveyard
does not implement [ERC-1271](./eip-1271.md), so neither the name's new ENSv2 owner, nor
its former controller, nor any approved operator can manage trust for that name through
this ERC. Migration clears the ENSv1 resolver, except for a locked name with
`CANNOT_SET_RESOLVER` burned, which keeps its legacy resolver. An approved operator can
migrate a name and choose the new ENSv2 owner.

Migration does not remove existing trust relationships from the Trust Registry. The
required path verification algorithm evaluates stored relationships and does not
require each path node to retain an active ENSv1 controller. Previously established
trust paths can therefore remain valid after a name migrates, subject to their trust
levels, expiry and gate constraints. Address-based validation still fails where the
terminal node's resolver was cleared.

A migrated name can no longer withdraw endorsements it has issued, set denials, or clear
them. Migration freezes both its outgoing endorsements and its denials: they stay in
storage and keep applying, and the former controller loses the authority to change
them. Outgoing endorsements persist until they expire, and those with `expiry == 0`
persist indefinitely. A migrated gatekeeper therefore continues to admit participants
through stored paths, cannot exclude further agents, and cannot readmit agents it has
denied. Before migrating, agents SHOULD withdraw endorsements they may later need to
withdraw, set or clear denials so that the frozen state is the one they intend, and
gatekeepers SHOULD reconfigure dependent gates. Conversely, attestations a migrated name
signed but never submitted can no longer be submitted, because its signatures no longer
verify.

Subnames of a migrated parent keep their controller and resolver until any party calls
the Graveyard's permissionless `clear` function. `clear` then hands unwrapped subnames
and unemancipated wrapped subnames to the Graveyard and clears their resolvers, which
freezes their trust state as described above. It reverts for an emancipated subname that
has not expired. Protocols that issue agent subnames SHOULD NOT migrate the parent name
while those subnames are in use with this standard.

On that deployment, `clear` also applies to `.eth` second-level names that were never
migrated. Once such a name is past its grace period, `clear` re-registers it in ENSv1 to
the Graveyard and clears its resolver. From then on `controllerOf` returns the
Graveyard, which ends the lapsed holder's authority over an expired unwrapped name (see
Expired Unwrapped Names) and freezes the name's trust state. `clear` reverts for a name
that is active or in its grace period. On that deployment ENSv1 registration is also
closed: at the block above, the BaseRegistrar's only controllers are the Graveyard and a
renewal controller with no registration function, so a name that lapses in ENSv1 cannot
be re-registered there by a new owner. Whether and when ENSv1 registration closes on
mainnet is not established. A new owner in ENSv2 gains no authority under this standard.

Coordinators SHOULD monitor migration of gatekeeper, anchor and participant names, and
re-evaluate the affected gates. Applications using address-based validation SHOULD
account for the loss or retention of legacy resolver records. For high-assurance
coordination, time-limited attestations and explicit gate reconfiguration are
RECOMMENDED.

New names registered only through ENSv2 are not directly supported by this ERC's ENSv1
interfaces. Supporting ENSv2 requires a separate integration that defines its
controller, signing and resolution semantics. A successor Trust Registry may provide
such support without changing this ERC's interface. Trust state does not migrate
automatically between registries. Existing signatures are bound to their original
verifying contract through [EIP-712](./eip-712.md), so establishing trust in a successor
registry requires fresh attestations. This is the successor-registry pattern described
for a successor wrapper under "Why Pin the NameWrapper?".

## Test Cases

A test suite covering the normative requirements of this document is provided in
[`tests/TrustRegistry.t.sol`](../assets/eip-8107/tests/TrustRegistry.t.sol). It exercises:

### Attestation and authority

* `setTrust` rejects self-trust, stale nonces, already-expired attestations, and signatures from any address other than the name controller
* `setTrust` rejects attestations whose level is `Unknown` or `None`, and a later attestation restores trust after a revocation
* Any address can submit a valid signature, so relayed attestations succeed
* An ENS operator approval does not confer signing authority
* Contract controllers are accepted through [ERC-1271](./eip-1271.md), and reject signers they do not recognise
* An EOA with an EIP-7702 delegation attests by ECDSA even when its delegate has no `isValidSignature`, and through ERC-1271 when its delegate validates a non-ECDSA credential
* A malleable (high-s) signature is rejected even though it recovers the controller
* An [ERC-1271](./eip-1271.md) controller that returns empty, short or dirty data, reverts with large data, or consumes all forwarded gas yields `InvalidSignature`, including an EIP-7702 delegate with an empty fallback; trailing data after a valid magic word is ignored
* Wrapped names attest through the unwrapped NameWrapper holder, and approvals for a wrapped name are read from the NameWrapper rather than the registry; a per-token approval grants nothing
* Only the pinned NameWrapper is unwrapped: a name held by any other wrapper contract resolves to that contract as a raw controller, so its token holder cannot attest
* A pinned NameWrapper whose `ownerOf` reverts yields no controller, so the name cannot attest
* Nobody can revoke on behalf of an unregistered name or a wrapped `.eth` name past its grace period
* A wrapped `.eth` second-level name keeps every power, including new attestations, withdrawal, denial, clearing and nonce invalidation, through its grace period while the NameWrapper refuses management. Its authority ends one second after the grace period, its stored records persist, and the next holder controls them
* On a path `G → T → M`, an edge into `T` set while `T` was registered still carries the path after another party re-registers `T`, to any agent the new registrant endorses, through all three verification entry points. For a wrapped `T`, validation by node returns the same verdicts after a renewal as after such a re-registration, which is why the continuity checks under Wrapped Name Lifecycle fall to the coordinator
* An emancipated subname loses its authority one second after its expiry; an unemancipated subname keeps it after expiry, including with a NameWrapper expiry of zero
* An expired unwrapped name keeps its authority, documenting the behaviour described under Expired Unwrapped Names
* A parent that reassigns a subname hands the new controller the subname's authority and the trust placed in it, documenting the behaviour described under Parent-Controlled Subnames
* `TrustSet` carries the attestation's nonce, for single attestations and for each item in a batch
* Domain separation prevents an attestation from replaying onto a second registry
* Every row of the Revert Order table has a test that violates that row and every later row, and asserts the row's exact error and arguments

### Scope

* `(trustor, trustee, scope)` records are independent
* Path verification prefers scoped trust and falls back to universal only when no scoped record exists; an expired scoped record does not fall back
* A universal withdrawal voids the edge in every scope, including scopes with a stored grant, and a later universal attestation restores the scoped path
* A withdrawal in a named scope applies to that scope only and leaves other scopes intact
* Withdrawal sets the stored expiry to zero, for `revokeTrust` and `revokeTrustBatch`
* Withdrawal requires no prior trust, and a preemptive withdrawal in a scope blocks the universal fallback for that scope
* `revokeTrustBatch` withdraws in every listed scope, including scopes that were never granted, leaves unlisted scopes untouched, and emits one `TrustRevoked` per listed scope

### Withdrawal

* An approved operator's withdrawal of `G → X` leaves `G → A → X` valid, through all three verification entry points: withdrawal does not deny
* A withdrawn intermediary still carries paths that avoid the withdrawn edge
* Universal withdrawal overrides positive scoped records for the same pair, and scoped withdrawal leaves other scopes alone
* Withdrawing the only edge into a required anchor invalidates the anchored path, even where an unanchored path remains

### Denial

* A validator's denial of the target or of any intermediary voids the path, at every index, even when every edge verifies and the denied edge stores `Full`
* An intermediary's denial of its own successor voids that edge
* A non-validator's denial of a non-adjacent node, or a denial held off the path, does not void the path
* Universal denial applies in every scope, including over stored scoped grants; scoped denial applies only in its own scope; a cleared or unset scoped flag never lifts a universal denial
* Denial holds with `enforceExpiry` off and with either `minEdgeTrust`
* `setTrust`, `setTrustBatch`, `revokeTrust` and `revokeTrustBatch` never change the denial flag, and `setDenial` and `clearDenial` change no other bit of the stored record, checked against the raw storage word
* Clearing a denial reveals the stored `Full`, `Marginal`, `None` or `Unknown` exactly, and clearing a universal denial leaves a scoped denial in force
* For every combination of universal and scoped flags, `getDenial(A, B, bytes32(0)) || getDenial(A, B, s)` matches the verdict of path verification in scope `s`
* `setDenial` and `clearDenial` are controller-only: approved operators (on the registry or the NameWrapper), strangers, the NameWrapper itself and names without a controller are rejected, and a transfer moves the authority to the new controller
* Denying an agent already denied, or clearing a flag that is not set, succeeds and emits nothing, and `DenialSet` logs alone reconstruct every stored flag
* `clearDenial` reverts with `NonceChanged` when an attestation lands after review, including a front-run pre-signed attestation, one in another scope when clearing a universal denial, one for another trustee, and a nonce invalidation; the denial stays in force, and a clear at the new nonce then succeeds
* A withdrawal after review does not block `clearDenial`, and the clear reveals the withdrawal; a stale nonce reverts even when the flag is not set
* Without a raised floor, an attestation signed before review and submitted after the clear applies immediately, documenting the back-run the guard cannot stop; with the floor raised before the nonce is read, that attestation fails with `NonceTooLow` before and after the clear, and the reviewed endorsement stays in force
* `getDenial` returns the stored flag for the exact scope, not the effective denial
* A fuzzed sequence of endorsements, withdrawals, denials and clears over two scopes matches a direct model of the specification for `getTrust`, `getDenial`, the edge check and the validator check

### Revocation durability and quarantine

* An attestation signed before a withdrawal remains submittable and restores trust, which is the hazard described under Pre-Signed Attestations
* `invalidateNonces` closes it: after raising the floor, the pre-signed attestation is rejected and the withdrawal holds
* An attestation submitted while a denial is in force updates the stored endorsement but does not admit the agent, and clearing the denial reveals it
* Signatures at or below a new floor are rejected, while a signature above it still lands
* The floor is trustor-wide, applying to every trustee and scope, and still permits fresh attestations above it
* Neither a single attestation nor a single `invalidateNonces` call can raise the floor by more than 2^32; the largest allowed jump succeeds and can be repeated, and near `type(uint64).max` the bound caps rather than wrapping
* At a floor of `type(uint64).max`, `setTrust`, `setTrustBatch` and `invalidateNonces` revert with `NonceTooLow` whose `required` saturates at `type(uint64).max`; the last nonce is usable exactly once
* Approved operators cannot call `invalidateNonces`

### Path verification

* Direct and transitive paths, missing edges, paths shorter than two nodes, and paths exceeding `maxPathLength`
* `minEdgeTrust` rejects marginal edges when `Full` is required
* Expiry is enforced when `enforceExpiry` is set and ignored when it is not
* A withdrawn edge voids the path
* `verifyPath`, `validateParticipantWithPath` and `validateParticipantAddress` reach the same verdict in every withdrawal and denial case above
* Paths containing a repeated node are rejected, even when every individual edge verifies
* `requiredAnchors` is part of the verdict: a path whose edges all verify but which traverses no anchor is invalid, and neither the validator nor the target can satisfy an anchor
* `ValidationParams` outside the stated constraints are rejected

### ENSv2 migration

Migration is modelled with a mock that holds the name and does not implement `isValidSignature`, once as the unwrapped registry owner and once as the NameWrapper holder.

* `setTrust` from a migrated name reverts with `InvalidSignature`, including for an attestation signed before migration
* `revokeTrust`, `setDenial`, `clearDenial` and `invalidateNonces` for a migrated name revert with `NotAuthorized`, for both the former controller and a former approved operator, and the stored endorsement and denial persist
* A stored path through a migrated intermediary or gatekeeper still verifies, through `verifyPath` and both gate functions, and a denial recorded before migration still applies
* `validateParticipantAddress` fails when a migrated terminal node's resolver is cleared, and passes when the legacy resolver is retained

The reference tests model the NameWrapper with a mock whose `ownerOf` follows the deployed
contract's expiry rules, with the NameWrapper expiry set at the end of the grace period.
The lifecycle cases above, and the migration and `clear` behaviour described under ENSv2
Compatibility and Migration, were also checked against the deployed contracts: the ENSv1
cases on an Ethereum mainnet fork, and the ENSv2 cases on a Sepolia fork of the ENSv2
deployment at block 11,906,480, since ENSv2 is not yet deployed on mainnet. On the
mainnet fork, wrapped names were renewed through each path a renewal can take: the 2023
wrapped ETHRegistrarController, which renews through `NameWrapper.renew`; the current and the
2019 ETHRegistrarController, which call the BaseRegistrar directly and leave the
NameWrapper expiry behind, with the effects described under Wrapped Name Lifecycle and
on `validateParticipantAddress`; and `NameWrapper.renew` called directly, which only its
controllers may call. Those fork tests are not part of the reference suite because they
need network access.

### Interface detection

* `supportsInterface` returns `true` for `IERC165` and `ITrustRegistry`, and `false` for `0xffffffff` and unrelated identifiers. The reference implementation also returns `false` for `0x93e686a3`, the identifier of the withdrawn `ITrustRegistryExtended`
* The identifiers published in Interface Detection match Solidity's computed values, guarding the spec's literals against interface drift
* The earlier identifiers `0x42b46ef5` and `0x31f35e83` are not reported; the first differs from the current one by exactly the `setDenial`, `clearDenial` and `getDenial` selectors, and the second by exactly the replacement of the boolean `setDenial` with `setDenial` and `clearDenial`
* `supportsInterface` stays within the 30,000 gas budget

### Identity gates and ERC-8001 integration

* Gates are namespaced per coordinator: two coordinators registering the same coordination type hold independent gates, and neither can modify or remove the other's
* Registering a gate does not require controlling the named gatekeeper
* Both validation functions revert with `GateNotFound` when no gate is enabled, including when the gate is read under the wrong coordinator address and after it has been removed
* Both validation functions reject paths shorter than two nodes
* `validateParticipantWithPath` rejects paths that do not start at the gatekeeper or do not terminate at the named participant
* A gate with a non-empty `requiredAnchors` rejects a direct path from its gatekeeper
* `setIdentityGate` rejects a non-empty `requiredAnchors` with `maxPathLength` 1 with `UnsatisfiableAnchors` and leaves any existing gate unchanged. It accepts every other configuration the Validation Parameters Constraints accept, including `maxPathLength` 1 without anchors and `maxPathLength` 2 with anchors, where a path through an anchor passes and one that misses it fails. `verifyPath` given the same rejected parameters returns `false` rather than reverting
* `validateParticipantAddress` rejects a participant address the terminal node does not resolve to, the zero address, nodes with no `addr` record, and nodes behind an off-chain resolver
* After a re-registration that leaves the `addr` record untouched, `validateParticipantAddress` admits the previous holder's address and not the new registrant's until the new registrant sets the record, and then only the new registrant's; a registration that sets the record in the same transaction leaves no such window
* A wrapped terminal node past its NameWrapper expiry, whose `addr` record still resolves, fails `validateParticipantAddress` because it has no controller, while `validateParticipantWithPath` still passes; once a NameWrapper renewal restores the holder, it passes again
* A lapsed unwrapped holder that repoints the `addr` record after its grace period moves the address `validateParticipantAddress` admits
* `resolveAgent` returns the zero address, without reverting, for a resolver with no code and for empty, short, dirty or reverting responses; it reads only the first word of an oversized response; a resolver that consumes gas costs the caller at most the cap, and cannot stop a coordinator that validates several participants in one call from validating the others

## Reference Implementation

| File | Contents |
|------|----------|
| [`contracts/ITrustRegistry.sol`](../assets/eip-8107/contracts/ITrustRegistry.sol) | Canonical types, errors and `ITrustRegistry` |
| [`contracts/TrustRegistry.sol`](../assets/eip-8107/contracts/TrustRegistry.sol) | Reference implementation of the required interface |
| [`tests/TrustRegistry.t.sol`](../assets/eip-8107/tests/TrustRegistry.t.sol) | Test suite covering the normative requirements above |

`TrustRegistry.sol` implements `ITrustRegistry` and nothing beyond it, since this
standard pushes path search to off-chain indexers.

## Security Considerations

### Sybil Attacks

An attacker can create many ENS names and establish mutual trust between them.

**Protocol-level mitigations:**

* **Required anchors**: `ValidationParams.requiredAnchors` forces paths through established community nodes
* **Short path limits**: `maxPathLength: 2` requires close proximity to validators
* **High trust requirement**: `minEdgeTrust: Full` rejects marginal trust paths

**Application-level mitigations:**

* Weight trust by ENS name age or registration cost
* Implement additional stake requirements
* Monitor trust graphs for anomalous patterns off-chain

### Trust Graph Manipulation

Attackers may attempt to position themselves in many trust paths.

**Mitigations:**

* Monitor trust graphs for anomalous patterns off-chain
* Use `minEdgeTrust: Full` for high-value coordination
* Require several independent paths, each checked with `verifyPath`, in application logic

### Key Compromise

If an ENS name's controller is compromised:

**Mitigations:**

* Monitor for unexpected trust changes via `TrustSet` events
* Monitor control changes on trusted, gatekeeper and anchor names, since an attacker
  holding the controller may also move the name (see Monitoring Control Changes)
* Use short expiries (90 days maximum recommended for high-stakes)
* ENS name controllers can be rotated, and a wrapped name's controller can be recovered
  through the NameWrapper
* Affected agents can deny the compromised node at universal scope. This voids their
  edges to it in every scope and excludes it from every path they evaluate as validator,
  including paths through agents that still vouch for it. Gates whose gatekeeper has
  denied the node therefore reject it in every evaluation after the denial (see
  `validateParticipantAddress` on evaluating again at execution). Other gatekeepers are
  unaffected until they deny it themselves
* Withdrawing an endorsement, even at universal scope and with the `COMPROMISED` reason
  code, removes only the withdrawing agent's own edge. Paths through other agents that
  still endorse the node stay valid

Two cases need different responses. When a **counterparty** is compromised, the
trustor's own controller is sound, so the trustor denies the counterparty and follows
the quarantine procedure under `invalidateNonces`. When the **trustor's own controller**
is compromised, denial is not sufficient: the attacker holds the same controller and can
clear any denial, withdraw endorsements, and sign new attestations. A compromised
trustor also cannot undo attestations the attacker has already signed, because the
attacker controls the same nonce space. Rotating the controller stops new attestations
but does not retract existing ones, nor restore denials the attacker cleared. The
complete remedy is denial by counterparties: other agents deny the compromised name,
which the attacker cannot clear, since denial is stored under each counterparty's own
name.

Neither an attestation nor an `invalidateNonces` call can raise the nonce floor by more
than 2^32. Without a bound, a single call with `type(uint64).max`, or a single
attestation signed with that nonce, would stop the name from ever attesting again. Because
the nonce belongs to the name rather than the key, that lockout would pass to every
future controller, so an attacker holding a compromised key could make it permanent.
2^32 leaves ample headroom for any real quarantine, and a trustor that has signed further
ahead can repeat the call.

### Pre-Signed Attestations

An attestation is submittable by anyone holding the signature, at any time, until the
trustor's nonce passes it. It carries no signing timestamp, so revoking trust does not
retract signatures the trustor issued earlier:

1. Alice signs an attestation granting `bob.eth` full trust at nonce 10, and hands it to
   a relayer that has not yet submitted it
2. Alice revokes trust in `bob.eth`
3. Anyone submits the nonce-10 attestation, restoring full trust

**Mitigations:**

* Issue nonces sequentially so the highest outstanding nonce is always known
* When quarantining a counterparty, deny it at universal scope and call
  `invalidateNonces` with a value above every nonce ever signed. A pre-signed attestation
  that lands while the denial is in force updates the stored endorsement but does not
  admit the counterparty; raising the floor keeps such an attestation from being
  stored, and later revealed when the denial is cleared. Both calls require the name controller; an approved
  operator can make neither
* Before reviewing a denial for clearing, raise the nonce floor above every nonce signed,
  then read the nonce passed to `clearDenial`. The nonce check makes the clear revert
  when an attestation lands first, and the floor stops one signed earlier from landing
  after the clear, where the nonce check cannot reach it
* Prefer short `expiry` values, which bound how long a stray signature stays usable even
  if the nonce floor is never raised

Note that `invalidateNonces` is trustor-wide: raising the floor invalidates that
trustor's outstanding attestations for every trustee and scope, not only the one being
quarantined. This is a deliberate trade-off in favour of failing closed. It reaches only
attestations that are signed but not yet submitted: relationships already stored are
untouched and continue to verify as before.

### ENS Name Transfer

When control of an ENS name changes:

* New owner inherits trust where they are the trustee
* New owner can manage trust where they are the trustor
* Attestations the old owner stored remain in force until they expire or are withdrawn
* Attestations the old owner signed but did not submit verify only against the current
  controller. They fail after the transfer, and become submittable again if control
  returns to the signer while their nonces are still unused
* Where the name is a gate's gatekeeper or one of its anchors, the new owner takes over
  that role (see Gatekeeper and Anchor Names)

**Mitigations:**

* Use short expiries for high-stakes trust
* Monitor control changes as described under Monitoring Control Changes
* Re-evaluate trust, and the gates that name the node, after any change of control

### Gatekeeper and Anchor Names

A gate stores its gatekeeper and its anchors as namehashes. A change of control over one
of those names moves its role in the gate to the new controller, and nothing in the gate
records the change.

**Gatekeeper.** A new controller of `gatekeeperNode` can issue direct attestations from
that identity. Where the gate requires no anchor, one qualifying attestation from the
gatekeeper to an agent creates a valid direct path, so the new controller can admit any
agent it chooses. Gates with a non-empty `requiredAnchors` reject direct paths, because
neither the validator nor the target can satisfy an anchor. Those gates remain bound by
their path constraints: the new controller can admit an agent only through a path that
traverses an anchor and meets every other parameter. In either case the new controller
can also deny, which excludes agents from every path the gate evaluates (see Validator
denial), and can clear any denial a previous controller set, readmitting agents the gate
had excluded.

**Anchor.** A new controller of an anchor node inherits every attestation naming the
anchor as trustee, and can attest from the anchor to any agent. Paths through the anchor
stay valid for as long as the attestations leading to it do. Time-limited inbound
attestations bound how long an anchor stays reachable. The anchor configuration itself
never expires, and attestations with `expiry == 0` remain valid until the trustor
replaces or revokes them, so expiry does not bound the exposure in general.

**Mitigations:**

* Monitor control changes for every gatekeeper and anchor node (see Monitoring Control
  Changes), and re-evaluate or reconfigure the affected gates
* Use a non-empty `requiredAnchors` where a change of gatekeeper control would be costly
* Give attestations leading to anchors an expiry
* Evaluate locking gatekeeper and anchor names with fuses, weighing the trade-off
  described under Name Locking with Fuses

### Monitoring Control Changes

Several provisions of this standard ask agents and coordinators to notice when control
of a name changes. No single event reports every such change. How a change appears
depends on how the name is held:

* The registry emits `NewOwner` from `setSubnodeOwner`. Registration, reclaiming a
  `.eth` name in the registry, and a parent reassigning a subname all go through it
* The registry emits `Transfer` from `setOwner`, when a registry owner hands a name to
  another address
* For unwrapped `.eth` second-level names, the BaseRegistrar's [ERC-721](./eip-721.md)
  transfer events record changes of registrant. Transferring a registration does not
  change the registry owner until the new registrant reclaims the name
* For wrapped names, the NameWrapper's [ERC-1155](./eip-1155.md) `TransferSingle` and
  `TransferBatch` events record changes of the token holder

* For wrapped names, control can also be lost with no transfer and no event. The holder
  loses control when the name's NameWrapper expiry passes. After a renewal that left the
  NameWrapper expiry behind, this happens while the name is still registered (see
  Wrapped Name Lifecycle)
* Control lost that way returns to the same holder, again with no transfer, when a
  renewal through the NameWrapper sets its NameWrapper expiry again. The NameWrapper
  emits no event for the renewal, and the BaseRegistrar's `NameRenewed` looks the same
  as for a renewal that left the NameWrapper expiry unchanged

After any relevant event, monitors are RECOMMENDED to re-check the name's effective
controller (`controllerOf`), its fuses and its expiry (for a wrapped name, its
NameWrapper expiry from `getData` as well as its registrar expiry), rather than relying
on any single event stream. Expiry emits no event at all, so a wrapped name also needs
re-checking when its NameWrapper expiry passes.

### Resolver Trust

Agent address resolution reads the node's `addr` record, which the name controller can
change at any time. A controller can therefore repoint a trusted name at a different
address, and any coordination gated on that name will admit the new address without any
trust attestation changing.

The `addr` record can also be stale, naming an address that the name's current
controller never chose. ENS does not clear the record when a name expires or changes
hands. After a re-registration it keeps naming the previous holder's address until the
new registrant changes it, so `validateParticipantAddress` keeps admitting the previous
holder for the name in the meantime. A wrapped name past its NameWrapper expiry keeps its
record although nobody controls the name. Monitoring `AddrChanged` catches neither case,
because the record does not change. `validateParticipantAddress` rejects a terminal node
that has no controller, which covers the second case. It cannot detect the first, because
the new registrant is a controller. A registration that sets the `addr` record in the
same transaction, which the current `.eth` registrar controller supports, leaves no stale
window.

**Mitigations:**

* Use short expiries so trust must be re-attested after a repoint
* Monitor `AddrChanged` events on nodes that appear in trust paths
* Monitor control of those nodes as well (see Monitoring Control Changes), and treat a
  node's `addr` record as unconfirmed after a change of control until the new controller
  has set it
* Treat a name's address record as exactly as trustworthy as the name itself

A malicious or buggy resolver can also return arbitrary addresses. Resolvers are chosen
by the name controller, so this is contained by the same trust decision: trusting a node
means trusting whatever resolver that node points at.

Nodes served by an off-chain resolver (CCIP-Read) cannot be resolved on-chain;
`resolveAgent` returns `address(0)` and address-identified validation fails closed.

A resolver can also try to disrupt validation rather than misdirect it, by reverting,
returning malformed data, or consuming gas. Resolution is gas-capped and never reverts
(see Agent Address Resolution), so such a resolver fails closed for its own node and
costs the caller at most the cap. Callers that validate many participants in one call
SHOULD budget for that cap on each participant.

A controller contract can likewise consume all gas forwarded to `isValidSignature`.
This only affects the party that submits that controller's attestations, which SHOULD
simulate a submission before relaying it.

### Wrapped Names

For wrapped names the controller is the NameWrapper token holder, recovered via
`ownerOf(uint256(node))`. Two consequences follow:

* A wrapped `.eth` second-level name resolves to `address(0)` and loses signing
  authority when its NameWrapper expiry passes, normally the end of its grace period (see
  Wrapped Name Lifecycle), and an emancipated subname at its expiry, without any action
  by counterparties. An unemancipated subname keeps its holder after it expires (see
  Wrapped Name Lifecycle)
* NameWrapper fuses can make a name's control non-transferable or burn its ability to
  change the resolver, which agents can treat as a positive trust signal, but this
  standard does not inspect fuses. Locking a name has a long-term cost; see Name Locking
  with Fuses

The NameWrapper address is pinned at deployment (see Name Controller Resolution). A
registry pointed at a hostile contract in that slot would let it claim control of every
wrapped name.

### Expired Unwrapped Names

When an unwrapped `.eth` second-level name expires, `ens.owner(node)` keeps returning
the lapsed holder until the name is re-registered or, where the ENSv2 Graveyard is
deployed, cleared by it (see ENSv2 Compatibility and Migration). Until then,
`controllerOf` returns that holder, who can still sign attestations, revoke trust, set
or clear denials and invalidate nonces for the name.

The registry cannot detect this. It receives only the namehash, while `.eth` expiry is
recorded by the BaseRegistrar against the labelhash, which cannot be derived from the
namehash.

When the name is re-registered, the new registrant inherits every attestation naming it
as trustee. Registration through the `.eth` registrar controller updates the registry
in the same transaction, so the new registrant gains trustor authority at once. This is
the transfer exposure described under Identity Continuity, but without any action by
the previous holder. The new registration appears as described under Monitoring Control
Changes.

The lapsed holder's reach also extends to address-based validation. Until the name is
re-registered, the lapsed holder is still the registry owner, so the resolver still
accepts its changes, and after the grace period it can repoint the `addr` record and
with it the address that `validateParticipantAddress` admits for the name. When the name
is re-registered, the `addr` record the previous holder left keeps naming that holder's
address until the new registrant changes it. Address-based gates keep admitting the
previous holder in the meantime, whether or not the name was wrapped before it lapsed
(see Resolver Trust). This re-registration window lasts as long as ENSv1 registration is
open. On the Sepolia ENSv2 deployment described under ENSv2 Compatibility and Migration,
ENSv1 registration is closed and `clear` hands a lapsed name to the Graveyard and clears
its resolver; whether mainnet will follow the same process is not established.

**Mitigations:**

* Use wrapped `.eth` second-level names or emancipated subnames, which resolve to
  `address(0)` and lose authority once their NameWrapper expiry passes
* Use short attestation expiries, which bound how long trust in a lapsed name survives
* Trustors and gatekeepers monitor the expiry of the unwrapped names they trust
  off-chain, and withdraw or deny names that lapse

### Parent-Controlled Subnames

Protocol-issued subnames such as `alice.agents.eth` are controlled by their parent as
well as their holder. An unwrapped parent, or a wrapped parent whose subname does not
have `PARENT_CANNOT_CONTROL` burned, can reassign the subname at any time. Such a
subname does not lose its holder when it expires, and it outlives its parent's
registration: whoever registers the parent next can reassign it. The new
controller then inherits every attestation naming the subname as trustee and gains
authority over every attestation in which it is the trustor, exactly as on a transfer,
but without the holder's consent.

The same applies when such a subname is a gatekeeper or an anchor: the parent can hand
its role in every gate that names it to a controller of its choosing. A reassignment
emits a registry `NewOwner` event (see Monitoring Control Changes).

`PARENT_CANNOT_CONTROL` protects an emancipated subname only until its NameWrapper
expiry. After that, a parent can create the subname again for another holder, who
inherits its attestations in the same way.

The continuity check under Wrapped Name Lifecycle treats a subname as registered again
whenever its `.eth` second-level ancestor is registered again, or the subname or an
ancestor of it below that second-level name is wrapped again. This bounds both cases for
coordinators that apply it.

Trustors SHOULD treat such a subname as no more trustworthy than its parent, and SHOULD
prefer subnames with `PARENT_CANNOT_CONTROL` burned. This standard does not inspect
fuses, so trustors and gatekeepers check them off-chain. `PARENT_CANNOT_CONTROL` stops
the parent reassigning the subname, but does not stop the holder transferring it; see
Name Locking with Fuses.

### Name Locking with Fuses

NameWrapper fuses can stop a wrapped name changing hands. `PARENT_CANNOT_CONTROL`,
`CANNOT_UNWRAP` and `CANNOT_TRANSFER` together prevent reassignment by the parent and
transfer by the holder while they are in effect. `CANNOT_UNWRAP` must be burned before
any other owner-controlled fuse can be. Wrapped `.eth` second-level names have
`PARENT_CANNOT_CONTROL` burned automatically when wrapped. Every fuse resets when the
name's NameWrapper expiry passes, which for a `.eth` second-level name is normally the
end of its grace period (see Wrapped Name Lifecycle). An emancipated name then loses its
holder. An unemancipated name keeps its holder but loses its fuses, so expiry still needs
monitoring.

Locking has a cost. Under ENS's published migration process, as deployed on Sepolia,
locked names with `CANNOT_TRANSFER` burned cannot migrate to ENSv2, because migration
transfers the name to a migration controller (see ENSv2 Compatibility and Migration).
Locking an identity is therefore a long-term commitment. Applications should weigh that
commitment against the protection it gives before locking gatekeeper, anchor or agent
names. This standard does not inspect fuses and does not require them.

### ENSv2 Migration

A name that migrates to ENSv2 can no longer attest, revoke, set or clear denials, or
invalidate nonces through this ERC, while the relationships it issued and received, and
the denials it recorded, stay in force. A migrated gatekeeper keeps admitting
participants through stored paths, cannot exclude further agents, and cannot readmit
agents it denied. Subnames of a migrated parent can lose their controller and resolver. See ENSv2
Compatibility and Migration under Backwards Compatibility.

### Identity Gate Namespacing

Gates are keyed by `(coordinator, coordinationType)`. A caller can only affect its own
namespace, so no party can squat, overwrite, or remove another's gate.

Consumers therefore have to name the coordinator whose policy they intend to apply.
Reading a gate under the wrong coordinator address, or under a mistyped coordination
type, finds no enabled gate, and both validation functions revert with `GateNotFound`
rather than returning a verdict. A misconfigured integration fails loudly at its first
call instead of silently admitting every participant. See "Why Do Unconfigured Gates
Revert?".

### Replay Protection

[EIP-712](./eip-712.md) domain binding prevents cross-contract replay. Monotonic nonces prevent replay within the same contract. The `chainId` in the domain prevents cross-chain replay.

Monotonic nonces stop an attestation from being applied twice, but they do not stop one
from being applied *late*. See Pre-Signed Attestations.

### Stale Trust

Trust relationships may become stale if agents don't update them.

**Mitigations:**

* Use `enforceExpiry: true` in validation parameters
* Set reasonable `expiry` values on attestations (90 days maximum for high-stakes)
* Monitor `TrustSet` event timestamps off-chain

### Off-Chain Path Computation

This standard assumes off-chain indexers compute trust paths. Malicious indexers could:

* Return suboptimal paths
* Omit valid paths
* Return invalid paths (caught by `verifyPath`)

An indexer that follows only consecutive trusted edges also returns paths through nodes
the validator has denied, which `verifyPath` rejects under Validator denial, and paths
over denied edges. Indexers therefore have to follow `DenialSet` as well as `TrustSet`
and `TrustRevoked`. Indexers SHOULD apply every rule of the path verification
algorithm during search, evaluated from the validator's perspective, so that the paths
they return verify.

**Mitigations:**

* Users can run their own indexers
* Multiple independent indexers provide redundancy
* Invalid paths are always rejected on-chain

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).
