Alert Source Discuss
⚠️ Draft Standards Track: ERC

ERC-8121: Cross-Chain Function Calls via Hooks

A specification for cross-chain function calls using hooks with ERC-7930 interoperable addresses.

Authors Prem Makeig (@nxt3d)
Created 2025-12-12
Discussion Link https://ethereum-magicians.org/t/erc-8121-delegated-metadata-resolution-via-hooks/27424
Requires EIP-3668, EIP-7930

Abstract

This ERC introduces hooks, a specification for cross-chain function calls. A hook fully specifies what function to call, with what parameters, on which contract, on which chain. Hooks are particularly useful for redirecting metadata to known contracts with verifiable security properties, such as credential registries for Proof-of-Personhood (PoP) or Know-Your-Agent (KYA) for AI agent identity.

Motivation

Cross-chain reads require specifying exactly what function to call, on which contract, on which chain. Hooks provide a complete specification by combining a function selector, readable function call, return type, and ERC-7930 interoperable address. This enables secure resolution from known contracts with verifiable security properties, whether on the same chain or across chains.

Use Cases

  • Cross-Chain Metadata: Resolve metadata from contracts on other chains
  • Credential Resolution: Redirect a Proof-of-Person (PoP) or Know-Your-Customer (KYC) record to a trusted credential registry
  • Singleton Registries: Point to canonical registries with known security properties on any chain
  • Shared Metadata: Multiple contracts can reference the same metadata source across chains

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.

Overview

A hook is a fully specified function call containing a function selector, human-readable function call, return type, and an ERC-7930 interoperable address specifying both the target contract and chain. This makes hooks completely self-describing - any client can resolve them without external documentation.

Hook Function Signature

function hook(
    bytes4 functionSelector,
    string calldata functionCall,
    string calldata returnType,
    bytes calldata target
)
bytes4 constant HOOK_SELECTOR = 0x396b32a0;

Parameters

  • functionSelector: The 4-byte selector of the function to call, computed as bytes4(keccak256("functionName(type1,type2)"))
  • functionCall: A string representation of the function to call with its parameters (human-readable)
  • returnType: The return type in Solidity tuple notation for ABI decoding (e.g., (string), (uint256, bytes32), ((string, uint256[], bytes32)))
  • target: An ERC-7930 interoperable address specifying both the target contract and chain

Function Call Format

The functionCall parameter uses a Solidity-style syntax:

  • String parameters are enclosed in single quotes: 'value'
  • Bytes/hex parameters use the 0x prefix: 0x1234abcd
  • Numbers are written as literals: 42 or 1000000
  • Arrays use square brackets: [1, 2, 3] or ['a', 'b', 'c']
  • Structs/tuples use parentheses: ('alice', 42, true)

The target function’s return type is specified by the returnType parameter. Functions return ABI-encoded data which clients decode using the specified return type.

Example:

hook(0xc41a360a, "getOwner(42)", "(address)", 0x000100000101141234567890abcdef1234567890abcdef12345678)

Hook Encoding

Hooks can be encoded in two formats depending on the storage type:

Bytes Format

For systems that store bytes values, hooks MUST be ABI-encoded:

bytes4 constant HOOK_SELECTOR = 0x396b32a0;

// Target function selector: bytes4(keccak256("getContractMetadata(string)"))
bytes4 functionSelector = 0x1837de7f;

// ERC-7930 address: Ethereum mainnet (chain 1) contract
bytes memory target = hex"000100000101141234567890abcdef1234567890abcdef12345678";

bytes memory hookData = abi.encodeWithSelector(
    HOOK_SELECTOR,
    functionSelector,
    "getContractMetadata('kyc')",
    "(string)",  // return type
    target
);

// Store the hook as the value
originatingContract.setContractMetadata("kyc", hookData);
// Target function: function getContractMetadata(string) external view returns (bytes memory)

String Format

For systems that store string values, hooks MUST be formatted as shown below. The target is an ERC-7930 interoperable address.

hook(0xFunctionSelector, "functionCall()", "(returnType)", 0xERC7930Address)

Examples:

hook(0x5cc4350a, "getText('kyc')", "(string)", 0x000100000101141234567890abcdef1234567890abcdef12345678)
hook(0x5c60da1b, "getRecord(42)", "(string, uint256, bytes32)", 0x00010000010a141234567890abcdef1234567890abcdef12345678)
hook(0x1234abcd, "getData()", "((string, uint256[], address))", 0x000100000101141234567890abcdef1234567890abcdef12345678)

// First example: returns string, Ethereum mainnet (chain 1)
// Second example: returns tuple, Optimism (chain 10)
// Third example: returns struct (nested tuple), Ethereum mainnet

Detecting Hooks

Clients SHOULD be aware in advance which metadata keys may contain hooks. It is intentional that hook-enabled keys are known by clients beforehand, similar to how clients know to look for keys like "image" or "description".

For bytes values, hooks can be detected by checking if the value starts with the hook selector 0x396b32a0. For string values, hooks can be detected by checking if the value starts with hook(.

Resolving Hooks

When a client encounters a hook that it wants to use:

  1. Parse the hook to extract the functionSelector, functionCall, returnType, and target (ERC-7930 address)
  2. Verify the selector: Compute the expected selector from the function signature in functionCall and verify it matches functionSelector. Reject the hook if they don’t match.
  3. Parse the target: Decode the ERC-7930 address to extract the chain and contract address
  4. Verify the target (RECOMMENDED): Check that the target contract is known and trusted
  5. Parse the function call: Extract the function name and parameters from the string
  6. Enable ERC-3668: Clients MUST enable ERC-3668 offchain data retrieval before calling the target
  7. Call the target: Execute the function on the target contract and chain using the functionSelector
  8. Decode the result: ABI-decode the returned data using the returnType

Clients MAY choose NOT to resolve hooks if the target contract is not known to be secure and trustworthy. Some clients have ERC-3668 disabled by default, but clients MUST enable it before resolving the hook.

Example: Cross-Chain KYC Credential Resolution

A contract on Optimism can redirect its "kyc" metadata key to a trusted KYC provider contract on Ethereum mainnet:

Step 1: Store the hook in the originating contract (on Optimism)

bytes4 constant HOOK_SELECTOR = 0x396b32a0;

// Target function selector: bytes4(keccak256("getCredential(string)"))
bytes4 functionSelector = 0x1837de7f;

// KYCProvider on Ethereum mainnet (ERC-7930 format)
// Chain: Ethereum mainnet (chain 1), Address: 0x1234...5678
bytes memory target = hex"000100000101141234567890abcdef1234567890abcdef12345678";

// Create hook that calls getCredential('kyc: 0x76F1Ff...') on the KYC provider
bytes memory hookData = abi.encodeWithSelector(
    HOOK_SELECTOR,
    functionSelector,
    "getCredential('kyc: 0x76F1Ff0186DDb9461890bdb3094AF74A5F24a162')",
    "(string)",  // return type
    target
);

// Store the hook
originatingContract.setContractMetadata("kyc", hookData);

Step 2: Client resolves the hook

// Client reads metadata from originating contract (on Optimism)
const value = await originatingContract.getContractMetadata("kyc");

// Client detects this is a hook (starts with HOOK_SELECTOR)
if (value.startsWith("0x396b32a0")) {
    // Parse the hook (ABI decode after 4-byte selector)
    const { functionSelector, functionCall, returnType, target } = decodeHook(value);

    // Verify selector matches the function signature
    const expectedSelector = keccak256("getCredential(string)").slice(0, 10);
    if (functionSelector !== expectedSelector) {
        throw new Error("Selector mismatch");
    }

    // Decode ERC-7930 address to get chain and contract
    const { chainId, address } = decodeERC7930(target);
    // chainId = 1 (Ethereum mainnet)
    // address = 0x1234567890abcdef1234567890abcdef12345678

    // Verify target is trusted (implementation-specific)
    if (!isTrustedResolver(chainId, address)) {
        throw new Error("Untrusted resolver");
    }

    // Parse the function call string to get function name and args
    const { functionName, args } = parseFunctionCall(functionCall);
    // functionName = "getCredential"
    // args = ["kyc: 0x76F1Ff0186DDb9461890bdb3094AF74A5F24a162"]

    // Get provider for target chain and enable ERC-3668 (CCIP-Read)
    const targetProvider = getProviderForChain(chainId);
    const targetContract = new ethers.Contract(
        address,
        [`function ${functionName}(string) view returns (bytes)`],
        targetProvider.ccipReadEnabled(true)  // Enable CCIP-Read
    );

    // Resolve from target contract on Ethereum mainnet
    const resultBytes = await targetContract[functionName](...args);

    // ABI-decode using returnType: "(string)"
    const credential = ethers.utils.defaultAbiCoder.decode([returnType], resultBytes);
    // credential = "Maria Garcia /0x76F1Ff.../ ID: 146-DJH-6346-25294"
}

Rationale

Hooks provide a complete specification for cross-chain function calls by combining a function selector, human-readable function call, return type, and ERC-7930 interoperable address. This makes hooks entirely self-describing - any client can resolve them without external documentation or ABI files.

Why Include Both Function Selector and Function String?

Hooks include both a 4-byte function selector and a human-readable function call string. The selector provides type disambiguation (e.g., getData(bytes32) and getData(bytes) have different selectors, but 0x1234... in the string is ambiguous), while the string provides human readability. Clients can verify the selector matches the function signature, rejecting mismatches as errors or tampering.

Why Use ERC-7930 Interoperable Addresses?

ERC-7930 addresses include chain information, making hooks a complete cross-chain function call specification. A hook specifies exactly what function to call, with what parameters, on which contract, on which chain. This eliminates ambiguity and enables secure cross-chain reads when combined with ERC-3668.

Why Include the Return Type?

The returnType parameter allows clients to ABI-decode the result without external documentation. Target functions return bytes, which clients decode using the specified return type. This is not restrictive - any data (arrays, structs, multiple values) can be returned via ABI encoding. Applications can impose their own constraints (e.g., requiring (string) for metadata hooks), but hooks themselves support any return type.

Why Mandate ERC-3668?

ERC-3668 (CCIP-Read) is a powerful technology that enables both cross-chain and verified offchain resolution of metadata. However, because some clients disable ERC-3668 by default due to security considerations, hooks explicitly mandate ERC-3668 support. This gives clients the opportunity to enable ERC-3668 specifically for hook resolution without needing to have it enabled globally. By tying ERC-3668 to hooks, clients can make a deliberate choice to enable it when resolving from known, trusted contracts, while keeping it disabled for general use.

Backwards Compatibility

Hooks are backwards compatible; clients that are not aware of hooks will simply return the hook encoding as the raw value.

Security Considerations

Target Trust

The primary use of hooks is to resolve data from known contracts with verifiable security properties. Clients SHOULD:

  • Maintain a list of trusted target contract addresses or use a third-party registry
  • Fail when resolving from untrusted targets

Recursive Hooks

Implementations SHOULD limit the depth of hook resolution to prevent infinite loops where a hook resolves to another hook. A reasonable limit is 3-5 levels of indirection.

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Prem Makeig (@nxt3d), "ERC-8121: Cross-Chain Function Calls via Hooks [DRAFT]," Ethereum Improvement Proposals, no. 8121, December 2025. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-8121.