--- eip: 8185 title: Off-Chain Entity Registry description: A shared registry mapping off-chain entity identifiers to Ethereum addresses through pluggable verification author: Carl Barrdahl (@carlbarrdahl) discussions-to: https://ethereum-magicians.org/t/erc-8185-off-chain-entity-registry-erc-8186-claimable-escrow/27899 status: Draft type: Standards Track category: ERC created: 2026-03-04 --- ## 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](./eip-137.md)) 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`: ```solidity 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: | Namespace | Canonical String | Identifies | | --------- | ---------------- | ------------------- | | `github` | `org/repo` | A GitHub repository | | `dns` | `example.com` | A DNS domain | | `npm` | `package-name` | An 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: ```solidity 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: ```solidity 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: ```solidity 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](./eip-712.md) 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: - [`OffChainEntityRegistry.sol`](../assets/eip-8185/OffChainEntityRegistry.sol) — registry with claim, revoke, link, unlink - [`OracleVerifier.sol`](../assets/eip-8185/OracleVerifier.sol) — example [EIP-712](./eip-712.md) oracle verifier 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](./eip-712.md) 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 Copyright and related rights waived via [CC0](../LICENSE.md).