<!--
Blockchain Lab reading mirror.
Licence: CC0-1.0. This is the upstream licence, not a Blockchain Lab grant.
Canonical: https://github.com/ethereum/ERCs/blob/master/ERCS/erc-8107.md
Repository: https://github.com/ethereum/ERCs
Mirrored: 2026-10-10
The text below is the upstream file. The desk study is a different page and is not covered by this licence.
-->

---
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, the required path verification checks a single path against a minimum per-edge trust level; accumulating several marginal attestations is left to an OPTIONAL extension.

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.

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. The required `verifyPath` instead checks
one path, and every edge on it must meet `minEdgeTrust`. Marginal accumulation is
available only through the OPTIONAL `validateAgent` extension; 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. Short attestation expiries bound 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
* 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: Explicitly distrusted
   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` | Agent known to behave improperly | Explicitly excluded; voids trust paths containing this agent |
| `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
revocation is not permanent: a later attestation with a higher nonce restores trust from
`None` to `Marginal` or `Full`.

### 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; // address(0) once the wrapped name expires
      } catch {
         return address(0);
      }
   }

   return owner;
}
```

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

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
function resolveAgent(bytes32 node) public view returns (address) {
   address resolver = ens.resolver(node);
   if (resolver == address(0)) return address(0);

   try IAddrResolver(resolver).addr(node) returns (address payable agent) {
      return agent;
   } catch {
      return address(0);
   }
}
```

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
already the controller's authoritative statement of which address the name denotes.

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.

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.

#### 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 in which it is the trustor.

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 `Transfer` events on ENS names they trust and re-evaluate trust
when a trusted name changes hands.

Agents SHOULD use wrapped names, whose authority ends when their registration expires.
Gatekeepers SHOULD prefer wrapped names for gatekeeper and anchor nodes. An unwrapped
name keeps its authority after it expires; see Expired Unwrapped Names.

### 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`.

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

Approved operators MAY perform **relationship revocation** because its effect is bounded
to one `(trustorNode, trusteeNode)` pair and the scopes named in the call.

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 perform trustor-wide invalidation

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.

```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;
   try IERC1271(controller).isValidSignature(digest, signature) returns (bytes4 magic) {
      return magic == IERC1271.isValidSignature.selector;
   } catch {
      return false;
   }
}

/// @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 name whose registration has expired resolves to `address(0)` and therefore has
no signing authority. This applies to wrapped names only: an expired unwrapped name keeps
its registry owner, and with it its signing authority, until the name is re-registered
and reclaimed (see 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.

### 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 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; distrust goes through revokeTrust.
   ///      `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 Set explicit distrust (level None) for one scope
   /// @dev Caller MUST be the name controller (controllerOf) or an approved operator.
   ///      Prior trust is NOT required: a trustor may preemptively distrust an agent it
   ///      never trusted.
   /// @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 Revoke trust 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.
   /// @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 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 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 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);
}
```

### OPTIONAL Interface Extensions

The following functions are OPTIONAL. Implementations MAY include them but they are not required for compliance:

```solidity
/// @title ENS Trust Registry - OPTIONAL Extensions
/// @notice Convenience and on-chain-search functions. NOT required for ERC-8107 compliance.
/// @dev On-chain graph traversal is expensive and DoS-prone; prefer off-chain path
///      computation with `verifyPath`. Provided for completeness only.
interface ITrustRegistryExtended is ITrustRegistry {
   /// @notice Get agents trusted by a given agent (paginated)
   /// @dev OPTIONAL - useful for indexing but not required
   function getTrustees(
      bytes32 trustorNode,
      TrustLevel minLevel,
      bytes32 scope,
      uint256 offset,
      uint256 limit
   ) external view returns (bytes32[] memory trustees, uint256 total);

   /// @notice Get agents that trust a given agent (paginated)
   /// @dev OPTIONAL - useful for indexing but not required
   function getTrustors(
      bytes32 trusteeNode,
      TrustLevel minLevel,
      bytes32 scope,
      uint256 offset,
      uint256 limit
   ) external view returns (bytes32[] memory trustors, uint256 total);

   /// @notice Validate an agent through on-chain graph traversal
   /// @dev OPTIONAL - expensive, prefer off-chain computation with verifyPath
   /// @param validatorNode The validating agent's perspective
   /// @param targetNode The agent to validate
   /// @param params Validation parameters
   /// @param marginalThreshold Number of marginal attestations required (for accumulation)
   /// @param fullThreshold Number of full attestations required
   function validateAgent(
      bytes32 validatorNode,
      bytes32 targetNode,
      ValidationParams calldata params,
      uint8 marginalThreshold,
      uint8 fullThreshold
   ) external view returns (
      bool isValid,
      uint8 pathLength,
      uint8 marginalCount,
      uint8 fullCount
   );

   /// @notice Check if any trust path exists
   /// @dev OPTIONAL - expensive, prefer off-chain computation
   function pathExists(
      bytes32 fromNode,
      bytes32 toNode,
      uint8 maxDepth
   ) external view returns (bool exists, uint8 depth);

   /// @notice Validate participant without pre-computed path
   /// @dev OPTIONAL - expensive, prefer validateParticipantWithPath
   function validateParticipant(
      address coordinator,
      bytes32 coordinationType,
      bytes32 participantNode,
      uint8 marginalThreshold,
      uint8 fullThreshold
   ) 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` | `0x42b46ef5` |

An implementation that also provides the OPTIONAL extensions MUST return `true` for
`ITrustRegistryExtended` (`0x93e686a3`); one that does not MUST return `false`. This is
the mechanism by which a caller determines whether on-chain graph search is available
before attempting it.

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 fourteen functions
declared in this document, excluding the inherited `supportsInterface`, matching
Solidity's `type(ITrustRegistry).interfaceId`. `ITrustRegistryExtended`'s identifier
covers only the five extension functions, likewise excluding everything it inherits.

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.

### 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` (distrust 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 for an expired wrapped name

If valid:

* The trust record MUST be stored, keyed by `(trustorNode, trusteeNode, scope)`
* `getNonce(trustorNode)` MUST return the attestation's nonce
* `TrustSet` MUST be emitted

#### `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` sets explicit distrust. 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
* `TrustRevoked` MUST be emitted
* The relationship MUST remain in storage (not deleted) to preserve the explicit distrust

A prior trust relationship is **not** required. A trustor MAY distrust an agent it never
trusted, which blocks paths that would otherwise route through that edge. Callers that
want to distinguish withdrawing trust from preemptive distrust MUST read `getTrust`
first; the registry does not make that distinction.

A revocation at `bytes32(0)` voids the edge in every scope. A revocation 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).

Revocation 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`
* `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.

#### `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 fully quarantine a compromised or misbehaving counterparty, a trustor SHOULD revoke
it at universal scope **and** call `invalidateNonces` with a value above any nonce it has
ever signed. Revocation alone leaves a pre-signed universal attestation able to restore
trust.

An approved operator can perform the first step but not the second, so a delegated
quarantine requires the controller to complete it.

#### `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;
      }
   }

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

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

      // 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.

**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`. 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 distrust 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.

```solidity
function _edgeTrust(bytes32 from, bytes32 to, bytes32 scope)
   internal view returns (TrustLevel level, uint64 expiry)
{
   (TrustLevel u, uint64 uExp) = _getTrust(from, to, bytes32(0));
   if (scope == bytes32(0)) return (u, uExp);

   // 1. Universal distrust voids the edge in every scope
   if (u == TrustLevel.None) return (TrustLevel.None, uExp);

   // 2. Any stored scoped record is authoritative, expired or not
   (TrustLevel s, uint64 sExp) = _getTrust(from, to, scope);
   if (s != TrustLevel.Unknown) return (s, sExp);

   // 3. Otherwise universal applies
   return (u, uExp);
}
```

#### `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.

`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;

   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`
* `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.

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.

### 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 BatchLengthMismatch();
error EmptyBatch();
error BatchTrustorMismatch();
error BatchNonceNotIncreasing();
error EmptyScopeList();
error NonceJumpTooLarge(uint64 provided, uint64 max);
```

#### 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` |
| `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` |
| `removeIdentityGate` | 1 | no enabled gate | `GateNotFound` |
| `validateParticipantWithPath`, `validateParticipantAddress` | 1 | no enabled gate | `GateNotFound` |

### Recommended Reason Codes

For `TrustRevoked` 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 |

### 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, and universal distrust overrides every scope.

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

The `marginalThreshold` and `fullThreshold` parameters were designed for on-chain graph traversal with marginal accumulation logic. Since on-chain traversal is optional (expensive, DoS-prone), and the core primitive is `verifyPath`, we need only specify the minimum trust level each edge must have.

This simplification:

* Reduces parameter complexity
* Makes path verification straightforward
* Leaves accumulation semantics to optional extensions

For use cases requiring marginal accumulation, the optional `validateAgent` extension accepts threshold parameters.

### 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

Implementations are free to add `validateAgent` and `pathExists` as optional extensions; the Specification does not require them for compliance.

### 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)

### 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. Accumulation across several paths
is left to the OPTIONAL `validateAgent` extension, whose `marginalThreshold` and
`fullThreshold` parameters play the role of 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, so a controller can repoint
a trusted name at a new address. This is discussed under Security Considerations, and it
is the same exposure the standard already accepts for ENS name transfers.

### 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 Distrust?

Earlier drafts allowed `setTrust` to carry `TrustLevel.None`, which meant distrust 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 all distrust 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 preemptively distrust
an agent it never trusted — a genuine web of trust need that the earlier asymmetry made
impossible to express.

### 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, which is why the Semantics
section states the combined procedure and Security Considerations repeats it.

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

Every other delegable operation in this standard is bounded. A revocation 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 carrying out a
quarantine can revoke the relationships but cannot complete the nonce invalidation, so
the controller must sign that step. That is the correct place to require the controller.

### Why Is Universal Distrust Authoritative?

A trustor that revokes a counterparty as `COMPROMISED` means it everywhere. If universal
distrust applied only where no scoped record existed, every scoped grant the trustor had
ever made to that counterparty would keep admitting it, and the trustor would have to
find and revoke each one before the revocation took full effect.

Making universal `None` override every scope gives the trustor one call that holds in
every scope, including scopes it has forgotten and scoped attestations signed earlier
but submitted later. 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 revocation is complete.

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 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 for free: an expired wrapped name returns
`address(0)` and therefore has no signing authority.

### 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 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 ENS deployment, and with deployments that have no NameWrapper. Both wrapped and unwrapped names are supported.

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.

## 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
* Wrapped names attest through the unwrapped NameWrapper holder, an expired wrapped name has no authority, and approvals for a wrapped name are read from the NameWrapper rather than the registry
* 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 an expired wrapped name
* 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 revocation voids the edge in every scope, including scopes with a stored grant, and a later universal attestation restores the scoped path
* A revocation in a named scope applies to that scope only and leaves other scopes intact
* Revocation sets the stored expiry to zero, for `revokeTrust` and `revokeTrustBatch`
* Revocation requires no prior trust, and preemptive distrust in a scope blocks the universal fallback for that scope
* `revokeTrustBatch` distrusts every listed scope, including scopes that were never granted, leaves unlisted scopes untouched, and emits one `TrustRevoked` per listed scope

### Revocation durability

* An attestation signed before a revocation 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 distrust holds
* 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
* 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 revoked edge voids the path
* 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

### Interface detection

* `supportsInterface` returns `true` for `IERC165` and `ITrustRegistry`, and `false` for `ITrustRegistryExtended`, `0xffffffff`, and unrelated identifiers
* The identifiers published in Interface Detection match Solidity's computed values, guarding the spec's literals against interface drift
* `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
* `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

## Reference Implementation

| File | Contents |
|------|----------|
| [`contracts/ITrustRegistry.sol`](../assets/eip-8107/contracts/ITrustRegistry.sol) | Canonical types, errors, `ITrustRegistry`, and the optional `ITrustRegistryExtended` |
| [`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 the required `ITrustRegistry` surface only. The
optional extensions are declared in `ITrustRegistry.sol` but intentionally left
unimplemented, 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 multiple independent paths via the optional extensions

### Key Compromise

If an ENS name's controller is compromised:

**Mitigations:**

* Monitor for unexpected trust changes via `TrustSet` events
* 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 revoke the compromised node at universal scope, which voids its
  edges in every scope

Revocation by counterparties is the only complete remedy: a compromised trustor 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.

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
* Call `invalidateNonces` with a value above every nonce ever signed when quarantining a
  counterparty, in addition to revoking it at universal scope. This call requires the
  name controller; an approved operator cannot make 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 an ENS name is transferred:

* New owner inherits trust where they are the trustee
* New owner can manage trust where they are the trustor
* Old attestations signed by old owner remain valid until expiry

**Mitigations:**

* Use short expiries for high-stakes trust
* Monitor ENS `Transfer` events
* Re-evaluate trust after transfers

### 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.

**Mitigations:**

* Use short expiries so trust must be re-attested after a repoint
* Monitor `AddrChanged` events on nodes that appear in trust paths
* 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.

### Wrapped Names

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

* An expired wrapped name resolves to `address(0)` and loses signing authority
  immediately, without any action by counterparties
* 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

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 someone re-registers the name and reclaims it in the registry.
Until then, `controllerOf` returns that holder, who can still sign attestations, revoke
trust 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, and gains trustor authority once it reclaims the name in the registry. This
is the transfer exposure described under Identity Continuity, but with no `Transfer` by
the previous holder to signal it.

**Mitigations:**

* Use wrapped names: an expired wrapped name resolves to `address(0)` and loses
  authority immediately
* 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 revoke trust in 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. 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.

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.

### 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`)

**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).
