EIP.tools

EIP.tools

⚠️ DraftStandards Track: ERC

ERC-8185: Off-Chain Entity Registry

A shared registry mapping off-chain entity identifiers to Ethereum addresses through pluggable verification

Authors
Created2026-03-04
Discussion Linkhttps://ethereum-magicians.org/t/erc-8185-off-chain-entity-registry-erc-8186-claimable-escrow/27899
Referenced by

Markdown

https://raw.githubusercontent.com/ethereum/ERCs/re...
Pull Request#1580PR open

EIP-GPT summary

Contents
AbstractMotivationSpecificationIdentifier SchemeNamespace LabelsNamespace KeyNormalizationDisplay FormatRegistry InterfaceVerifier InterfaceClaim SemanticsVerifier Administration SemanticsLinking SemanticsRevocation SemanticsNamespace GovernanceRationaleabi.encode over abi.encodePackedRelayed claimsNon-view verifyNo registrant transfer functionSingle-level alias indirectionVerifier interface simplicityNormalization off-chainDistinction from EASBackwards CompatibilityReference ImplementationSecurity ConsiderationsProof replayIdentifier collisionsOracle key compromiseRegistry administrationNormalization inconsistencyPost-revocation takeoverMalicious verifier reentrancyStale claims and identifier permanenceStale aliases after revocationNo registrant transferCopyright

Abstract

This ERC defines a standard interface for mapping off-chain entity identifiers — such as git repositories, DNS domains, and package registries — to Ethereum addresses, giving protocols and agents a single permissionless lookup for entities that otherwise have no canonical on-chain address. Identifiers are (namespace, string) pairs hashed to a single bytes32. Ownership is proven through per-namespace verifier contracts, enabling a migration path from oracle-signed proofs to zero-knowledge verification without changes to the registry.

Motivation

Protocols and AI agents increasingly need to resolve off-chain identifiers — git repositories, DNS domains, npm packages — to Ethereum addresses. An agent instructed to "fund the maintainers of react" or a grants protocol allocating to github:ethereum/go-ethereum both need the same thing: a single, permissionless, on-chain lookup.

Ethereum addresses humans well through ENS (ERC-137) and makes attestations about them through the Ethereum Attestation Service (EAS), but neither gives an off-chain entity a canonical Ethereum address. Absent a shared standard, each protocol that encounters this problem builds its own resolution system — Drips uses an application-specific oracle, funding platforms maintain their own registries, and ENS is designed for human-readable names rather than entity identifiers.

This fragmentation means every new protocol must either require entities to register before receiving funds or build a bespoke resolution backend. This ERC defines a shared primitive: a single registry contract per chain that any protocol or agent can read and any entity can register with. The registry holds no funds and charges no fees.

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 and RFC 8174.

Identifier Scheme

Every off-chain entity is identified by a (namespace, canonicalString) pair. The on-chain identifier is a single bytes32:

bytes32 id = keccak256(abi.encode(namespace, canonicalString))

Implementations MUST use abi.encode (length-prefixed) rather than abi.encodePacked to prevent collisions between namespace/string pairs that share a common packed concatenation (e.g. ("githu", "borg/repo") vs ("github", "org/repo")).

Examples:

NamespaceCanonical StringIdentifies
githuborg/repoA GitHub repository
dnsexample.comA DNS domain
npmpackage-nameAn npm package

Namespace Labels

Namespace labels MUST match [a-z0-9-]+ and be interpreted as lowercase ASCII. They are exact byte strings: github and GitHub are distinct, and the latter is non-compliant. Compliant implementations MUST reject namespaces that do not match this format.

Namespace Key

Implementations MUST derive a namespace lookup key as:

bytes32 namespaceKey = keccak256(bytes(namespace))

This key MUST be used for verifier registration, VerifierUpdated events, and all namespace-scoped storage. The full namespace string is retained in events and function parameters for human readability.

Normalization

Normalization rules for canonicalString — lowercase, trailing slash removal, Unicode handling — are namespace-specific and MUST be enforced by verifiers and frontends, not by the registry contract. The registry MUST hash whatever strings are passed to it without modification.

Display Format

It is RECOMMENDED that off-chain tools display identifiers as namespace:canonicalString (e.g. github:ethereum/go-ethereum). This format is not hashed on-chain; only the (namespace, canonicalString) pair passed to toId defines the bytes32 identifier.

Registry Interface

A compliant implementation MUST implement the following interface:

interface IOffChainEntityRegistry {
    // ----------------------------------------------------------------
    // Events
    // ----------------------------------------------------------------

    /// @notice MUST be emitted when an identifier is successfully claimed.
    event Claimed(
        bytes32 indexed id,
        string namespace,
        string canonicalString,
        address indexed owner,
        address verifier
    );

    /// @notice MUST be emitted when an owner voluntarily revokes their claim.
    event Revoked(
        bytes32 indexed id,
        string namespace,
        string canonicalString,
        address indexed previousOwner
    );

    /// @notice MUST be emitted when an alias is linked to a primary identifier.
    event Linked(bytes32 indexed aliasId, bytes32 indexed primaryId);

    /// @notice MUST be emitted when an alias is unlinked from its primary.
    event Unlinked(bytes32 indexed aliasId, bytes32 indexed primaryId);

    /// @notice MUST be emitted when a verifier is registered or replaced for a namespace.
    event VerifierUpdated(bytes32 indexed namespaceKey, string namespace, address verifier);

    // ----------------------------------------------------------------
    // Identifier helpers
    // ----------------------------------------------------------------

    /// @notice Computes the canonical identifier for a (namespace, canonicalString) pair.
    /// @return The keccak256(abi.encode(namespace, canonicalString)) hash.
    function toId(
        string calldata namespace,
        string calldata canonicalString
    ) external pure returns (bytes32);

    /// @notice Resolves an identifier to its canonical form.
    /// @dev If `id` is an alias, returns the primary identifier.
    ///      If `id` is not an alias, returns `id` unchanged.
    ///      MUST follow at most one level of alias indirection.
    function canonicalOf(bytes32 id) external view returns (bytes32);

    /// @notice Returns the registered owner of `id`, resolving through any alias.
    /// @return The owner address, or address(0) if unclaimed.
    function ownerOf(bytes32 id) external view returns (address);

    /// @notice Returns the verifier registered for a namespace.
    /// @return The verifier address, or address(0) if none is registered.
    function verifierOf(string calldata namespace) external view returns (address);

    // ----------------------------------------------------------------
    // Registration
    // ----------------------------------------------------------------

    /// @notice Claim ownership of an identifier on behalf of `owner` by
    ///         submitting a valid proof. Anyone MAY submit the transaction;
    ///         the proof MUST bind `owner`.
    /// @dev MUST revert if owner == address(0).
    ///      MUST revert if ownerOf(id) != address(0) (including aliases linked to an active primary).
    ///      MUST revert if no verifier is registered for the namespace.
    ///      MUST revert if the proof is invalid.
    ///      MUST record `owner` (not msg.sender) as the registered owner.
    ///      MUST emit Claimed with the verifier used on success.
    function claim(
        string calldata namespace,
        string calldata canonicalString,
        address owner,
        bytes calldata proof
    ) external;

    /// @notice Voluntarily revoke the caller's claim, returning the
    ///         identifier to unclaimed state.
    /// @dev MUST revert if called by anyone other than the current owner.
    ///      MUST revert if the identifier is an alias.
    ///      MUST emit Revoked on success.
    function revoke(
        string calldata namespace,
        string calldata canonicalString
    ) external;

    // ----------------------------------------------------------------
    // Linking
    // ----------------------------------------------------------------

    /// @notice Link one or more aliases to a primary identifier.
    /// @dev The caller MUST be the owner of both the primary and all alias identifiers.
    ///      The primary MUST NOT itself be an alias.
    ///      Each alias MUST NOT already be linked.
    ///      MUST emit Linked for each alias on success.
    function linkIds(
        bytes32 primaryId,
        bytes32[] calldata aliasIds
    ) external;

    /// @notice Unlink one or more aliases from their primary.
    /// @dev The caller MUST be the owner of the primary identifier.
    ///      Each alias MUST currently be linked to the specified primary.
    ///      MUST emit Unlinked for each alias on success.
    ///      After unlinking, each alias MUST be restored as an independently
    ///      owned identifier under the caller's address.
    function unlinkIds(
        bytes32 primaryId,
        bytes32[] calldata aliasIds
    ) external;

    // ----------------------------------------------------------------
    // Admin
    // ----------------------------------------------------------------

    /// @notice Register or replace the verifier for a namespace.
    /// @dev MUST revert if verifier is address(0).
    ///      Existing claims under the previous verifier MUST remain valid.
    ///      MUST emit VerifierUpdated on success.
    ///      Access control for this function is implementation-defined.
    function setVerifier(
        string calldata namespace,
        address verifier
    ) external;
}

Verifier Interface

The registry MUST delegate ownership verification to per-namespace verifier contracts. A compliant verifier MUST implement:

interface IVerifier {
    /// @notice Verify that `owner` owns the off-chain entity identified by `id`.
    /// @param id    The bytes32 identifier being claimed.
    /// @param owner The address being registered as owner. The proof MUST bind
    ///              this address — not the transaction sender — so that anyone
    ///              may submit the claim on the owner's behalf.
    /// @param proof Encoded proof data (format is verifier-specific).
    /// @return True if the proof is valid and `owner` owns the identifier.
    function verify(
        bytes32 id,
        address owner,
        bytes calldata proof
    ) external returns (bool);
}

Verifier implementations are intentionally unconstrained. The interface accommodates:

  • Oracle verifiers: A trusted backend confirms ownership off-chain and signs an EIP-712 proof.
  • ZK verifiers: Ownership is proven cryptographically on-chain.

A registry deployment MAY replace a verifier for any namespace at any time. Existing claims MUST NOT be invalidated by a verifier change.

Verifiers SHOULD return false for invalid proofs rather than reverting. The registry MUST treat a false return or a reverted verify call as an invalid proof.

Claim Semantics

  1. The registry MUST reject namespaces that do not match [a-z0-9-]+.
  2. The registry MUST compute the identifier as keccak256(abi.encode(namespace, canonicalString)).
  3. The registry MUST revert if owner == address(0).
  4. The registry MUST revert if ownerOf(id) != address(0).
  5. The registry MUST look up the verifier at namespaceKey = keccak256(bytes(namespace)) and revert if it is address(0).
  6. The registry MUST call IVerifier.verify(id, owner, proof) and revert if it returns false or reverts.
  7. If the identifier was previously a stale alias (from a revoked primary), the registry MUST clear the alias mapping and emit Unlinked(aliasId, stalePrimaryId) before recording the new owner, so that indexers reconstructing link state from Linked/Unlinked events remain consistent.
  8. The registry MUST record owner — not msg.sender — as the owner of the identifier.
  9. The registry MUST emit Claimed with the verifier address used for verification.

Verifier Administration Semantics

  1. The registry MUST reject namespaces that do not match [a-z0-9-]+.
  2. The registry MUST NOT set the verifier for a namespace to address(0).
  3. The registry MUST record the verifier at namespaceKey = keccak256(bytes(namespace)).
  4. The registry MUST emit VerifierUpdated on success.
  5. Existing ownership records MUST NOT be modified by a verifier change.

Linking Semantics

  1. Aliases MUST resolve through exactly one level of indirection. Chaining (an alias pointing to another alias) MUST NOT be allowed.
  2. When identifiers are linked, the alias's owner record is deleted. ownerOf(aliasId) MUST return the primary's owner.
  3. Each identifier's associated state remains separate. Linking only affects ownership resolution.

Revocation Semantics

  1. After revocation, ownerOf(id) MUST return address(0).
  2. Any aliases pointing to the revoked identifier become stale — they also resolve to address(0).
  3. Stale aliases MUST be automatically cleared (with Unlinked emitted) when someone claims that alias identifier in the future.

Namespace Governance

Namespace registration SHOULD be restricted to a curated set managed by the registry admin. Namespace labels SHOULD be short, stable, and lowercase ASCII ([a-z0-9-]+). Permissionless namespace registration invites squatting and low-quality entries. Implementations MAY migrate from an onlyOwner pattern to a governance contract as the namespace set stabilizes.

Rationale

abi.encode over abi.encodePacked

abi.encodePacked concatenates without length prefixes, so different namespace/string pairs can collide (see Identifier Scheme). abi.encode is unambiguous.

Relayed claims

claim takes an explicit owner parameter rather than registering msg.sender. Because the proof binds the owner address (see the IVerifier interface), any third party or relayer can safely submit the transaction and sponsor its gas — the submitter gains nothing, since only the owner the proof commits to can be registered. This lets entities that have never touched Ethereum be registered without holding ETH, and lets a contract wallet or multisig be set as owner without initiating the call itself.

Non-view verify

IVerifier.verify is deliberately declared non-view so that verifiers can be stateful — marking nullifiers, consuming one-time proofs, or rate-limiting claims — without an interface change. Stateless verifiers (such as the reference oracle verifier) simply implement it with view semantics.

No registrant transfer function

There is no function to transfer a registered identifier to another address. Changing the registrant requires revoking and re-claiming with a new proof, tying the registered address to proven ownership at claim time.

Single-level alias indirection

Allowing chains of aliases (alias → alias → primary) would require unbounded traversal in ownerOf, creating gas unpredictability and a potential griefing vector. One level of indirection is sufficient for the common case (linking a GitHub repo and a DNS domain owned by the same entity) while keeping resolution O(1).

Verifier interface simplicity

The IVerifier interface has a single function with no constraints on the proof format, accommodating oracle signatures, zero-knowledge proofs, and any future mechanism without interface changes.

Normalization off-chain

Embedding namespace-specific normalization rules in the registry would make it large, inflexible, and expensive. Verifiers and frontends share responsibility for canonicalization.

Distinction from EAS

Ethereum Attestation Service (EAS) supports arbitrary attestations about any subject. This ERC defines a single, chain-readable mapping from a standardized entity identifier to one owner address, optimized for permissionless lookup by protocols and agents.

Backwards Compatibility

This ERC introduces a new standard and does not modify any existing standards. It complements ENS (human-readable names) and EAS (attestations). An ENS name can resolve to an address returned by this registry. Protocols using application-specific registries can migrate without affecting their end-user interfaces.

Reference Implementation

A minimal reference implementation is provided in the assets directory:

The implementation has no external dependencies and is intentionally minimal. A more complete implementation with beacon proxy identity accounts, namespace-specific verifiers, and a TypeScript SDK is maintained outside this specification.

Security Considerations

Proof replay

EIP-712 domain separation (in oracle verifiers) binds proofs to a specific chain and registry deployment. Proofs include the owner address being registered and an expiry timestamp. Because claims are relayable, anyone can re-submit a proof within its TTL window — for example after a revocation — but only for the owner address the proof binds. This is accepted at the contract level; verifier implementations should use short expiry windows (e.g. 1 hour) and issue fresh proofs on each request.

Identifier collisions

The use of abi.encode (length-prefixed) prevents collisions between different namespace/string pairs. The keccak256 hash is 256 bits, making accidental collision negligible.

Oracle key compromise

A compromised oracle signing key allows claiming any unclaimed identifier in namespaces using that verifier, until the key is rotated. Signing keys should be held in an HSM with defined rotation procedures.

Registry administration

Whoever controls setVerifier can install a rubber-stamp verifier for any namespace and thereby claim every currently-unclaimed identifier in it. Existing claims are not affected by a verifier change — the specification requires that a verifier change never modifies or invalidates existing ownership records, which is the load-bearing safety property limiting the blast radius of an admin compromise to unclaimed identifiers. The setVerifier authority should be a multisig or governance contract, consistent with the Namespace Governance section.

Normalization inconsistency

If a verifier and a frontend disagree on either the namespace label or the canonical form of a string, the same off-chain entity could be registered under multiple identifiers or under the wrong namespace. Verifier implementations and registry frontends should enforce strict normalization and reject non-canonical inputs. Unicode homoglyphs and punycode variants should be normalized or rejected per namespace rules.

Post-revocation takeover

After revoke, ownerOf(id) returns address(0) and any party with a valid proof can claim the identifier. The previous owner has no on-chain reservation. Protocols sending funds should treat a recent revocation as elevated risk and may require confirmation that the new owner is the intended entity.

Malicious verifier reentrancy

The registry should call IVerifier.verify before updating ownership state (checks-effects-interactions), as the Claim Semantics ordering prescribes. A malicious verifier could attempt reentrancy during claim; production deployments should use a non-reentrant registry or trusted verifier contracts.

Stale claims and identifier permanence

Off-chain identifiers are not permanent — repositories can be renamed, domains can expire, packages can be unpublished. When an identifier's canonical string changes, the old and new bytes32 hashes are unrelated. The old identifier still resolves to the original owner; the new one resolves to address(0). This ERC does not define a challenge mechanism for stale claims.

An owner whose identifier has changed should claim the new identifier and link the old one as an alias via linkIds. Both identifiers then resolve to the same address.

Verifier implementations may enforce time-bounded claims requiring periodic re-verification, causing stale claims to expire naturally. A challenge or expiry mechanism is deferred to a future ERC.

Stale aliases after revocation

When a primary identifier is revoked without first unlinking its aliases, those aliases become stale (resolve to address(0)). This is by design — the stale alias is automatically cleared when someone claims that identifier in the future, with Unlinked(aliasId, stalePrimaryId) emitted at that point (as the Claim Semantics require) so indexers tracking link state from events do not diverge from on-chain state.

No registrant transfer

Wallet rotation requires a full revoke-and-reclaim cycle. This prevents unauthorized transfers at the cost of friction for legitimate wallet changes.

Copyright and related rights waived via CC0.

EIP.tools

EIP.tools

Search, read, and map Ethereum improvement proposals, ERCs, RIPs, and CAIPs from one focused interface.

Farcaster
by @apoorveth