ERC-8121 - Cross-Chain Function Calls via Hooks

Created 2025-12-12
Status Draft
Category ERC
Type Standards Track
Authors
Requires

Abstract

This ERC introduces hooks for cross-chain function calls. A hook fully specifies what function to call, with what parameters, on which contract, on which chain. Hooks require clients to use ERC-3668 to resolve, enabling cross-chain and off-chain verifiable data resolution. 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

There is a need to resolve data cross-chain, such as credentials like Proof-of-Personhood (PoP), for example from a dedicated identity chain. There is also a need to save these cross-chain function calls onchain, and there is currently no existing standard for saving this type of function call as a string or bytes value onchain, in a maximally human-readable way. It should also be possible for a hook to be included in plain text, for example a markdown file intended to be consumed by AI agents. Hooks allow for a specific function, contract, and chain to be specified in a human-readable way. One of the most important features of hooks is that it allows clients to evaluate whether or not they trust the target contract, for example to resolve a PoP or KYC credential, before calling the function.

Use Cases

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 an optional function selector, function signature with explicit types, human-readable function call with values, return type, and an ERC-7930 interoperable address specifying both the target contract and chain. The function selector acts as a checksum to verify the function signature, ensuring type safety and preventing ambiguity. This makes hooks completely self-describing - any client can resolve them without external documentation.

Hook Function Signature

Hooks can be encoded with or without an optional function selector. When the selector is omitted, functionSignature is the first parameter.

With optional function selector:

function hook(
    bytes4 functionSelector,
    string calldata functionSignature,
    string calldata functionCall,
    string calldata returnType,
    bytes calldata target
)

Without function selector:

function hook(
    string calldata functionSignature,
    string calldata functionCall,
    string calldata returnType,
    bytes calldata target
)
bytes4 constant HOOK_SELECTOR_WITH_SELECTOR = 0x037f43ed;  // When functionSelector is included
bytes4 constant HOOK_SELECTOR_WITHOUT_SELECTOR = 0x6113bfa3; // When functionSelector is omitted

Parameters

Function Call Format

The functionCall parameter uses a Solidity-style syntax:

Examples:

// Example with optional function selector (acts as checksum)
hook(0xc41a360a, "getOwner(uint256)", "getOwner(42)", "(address)", 0x000100000101141234567890abcdef1234567890abcdef12345678)

// Example with function selector omitted
hook("getOwner(uint256)", "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. Use different hook selectors depending on whether the optional function selector is included:

With optional function selector:

// Hook selector: first 4 bytes of keccak256("hook(bytes4,string,string,string,bytes)")
bytes4 constant HOOK_SELECTOR_WITH_SELECTOR = 0x037f43ed;

// Function signature with explicit types
string memory functionSignature = "getContractMetadata(string)";

// Optional function selector as checksum (computed from signature)
bytes4 functionSelector = bytes4(keccak256(functionSignature)); // 0x1837de7f

// Function call with values
string memory functionCall = "getContractMetadata('kyc')";

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

bytes memory hookData = abi.encodeWithSelector(
    HOOK_SELECTOR_WITH_SELECTOR,
    functionSelector,
    functionSignature,
    functionCall,
    "(bytes)",  // return type
    target
);

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

Without function selector:

// Hook selector: first 4 bytes of keccak256("hook(string,string,string,bytes)")
bytes4 constant HOOK_SELECTOR_WITHOUT_SELECTOR = 0x6113bfa3;

// Function signature with explicit types
string memory functionSignature = "getContractMetadata(string)";

// Function call with values
string memory functionCall = "getContractMetadata('kyc')";

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

bytes memory hookData = abi.encodeWithSelector(
    HOOK_SELECTOR_WITHOUT_SELECTOR,
    functionSignature,  // First parameter when selector is omitted
    functionCall,
    "(bytes)",  // 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.

With optional function selector:

hook(0x9e574b14, "getContractMetadata(string)", "getContractMetadata('kyc')", "(bytes)", 0x000100000101141234567890abcdef1234567890abcdef12345678)

Without function selector:

hook("getContractMetadata(string)", "getContractMetadata('kyc')", "(bytes)", 0x000100000101141234567890abcdef1234567890abcdef12345678)

Parsers MUST detect the format by checking if the first parameter after hook( starts with 0x followed by 8 hexadecimal characters (an optional function selector) or not.

Examples:

// Example 1: simple struct parameter
hook(0xabcdef12, "getData((string,uint256))", "getData(('alice', 42))", "(bytes)", 0x000100000101141234567890abcdef1234567890abcdef12345678)

// Example 2: struct parameter returning struct
hook(0x12345678, "getCredential((string,uint256,bytes32))", "getCredential(('kyc', 12345, 0xabcd1234...))", "((string,address,uint256))", 0x000100000101141234567890abcdef1234567890abcdef12345678)

// Example 3: nested struct (struct containing a struct)
hook(0x9abcdef0, "getAgent((string,uint256,(address,bool,string)))", "getAgent(('alice', 42, (0x1234..., true, 'verified')))", "((string,uint256,(address,bool)))", 0x000100000101141234567890abcdef1234567890abcdef12345678)

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 either hook selector 0x037f43ed (with optional function selector) or 0x6113bfa3 (without function selector). For string values, hooks can be detected by checking if the value starts with hook(.

Resolving Hooks (Read Operations)

When a client encounters a hook that it wants to use for a read operation:

  1. Detect hook format: For bytes format, check if the value starts with 0x037f43ed (with selector) or 0x6113bfa3 (without selector). For string format, parse the first parameter after hook( to determine if it's a selector (starts with 0x + 8 hex chars) or not.
  2. Parse the hook: Extract the functionSelector (if present), functionSignature, functionCall, returnType, and target (ERC-7930 address). If the selector is omitted, functionSignature is the first parameter.
  3. Verify the selector (if provided): If functionSelector is present, compute the expected selector from functionSignature as bytes4(keccak256(functionSignature)) and verify it matches functionSelector. Reject the hook if they don't match. This verification ensures type safety and prevents ambiguity with structs or overloaded functions. If the selector is omitted, compute it from functionSignature for use in the function call.
  4. Parse the target: Decode the ERC-7930 address to extract the chain and contract address
  5. Verify the target (RECOMMENDED): Check that the target contract is known and trusted
  6. Parse the function call: Extract the function name and parameter values from functionCall. Use functionSignature to determine the parameter types for ABI encoding.
  7. Enable ERC-3668: Clients MUST enable ERC-3668 offchain data retrieval before calling the target
  8. Call the target: Execute the function on the target contract and chain. Use the provided functionSelector if available, otherwise compute it from functionSignature as bytes4(keccak256(functionSignature)). ABI-encode the parameters according to functionSignature.
  9. Get the result: Retrieve the return value from the function call.

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.

Write Operations

Write operations are also possible with hooks and follow the same flow as read operations, except that the transaction needs to be signed and submitted to the blockchain. The hook specifies the function to call, parameters, target contract, and chain, but instead of reading the result, the transaction is signed and broadcast to the network for inclusion in a block.

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_WITHOUT_SELECTOR = 0x6113bfa3;

// Function signature with explicit types
string memory functionSignature = "getCredential(string)";

// Function call with values
string memory functionCall = "getCredential('kyc: 0x76F1Ff0186DDb9461890bdb3094AF74A5F24a162')";

// 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_WITHOUT_SELECTOR,
    functionSignature,  // First parameter when selector is omitted
    functionCall,
    "(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)
const hasSelector = value.startsWith("0x037f43ed");
if (value.startsWith("0x037f43ed") || value.startsWith("0x6113bfa3")) {
    // Parse the hook (ABI decode after 4-byte selector)
    let functionSelector, functionSignature, functionCall, returnType, target;
    if (hasSelector) {
        ({ functionSelector, functionSignature, functionCall, returnType, target } = decodeHook(value));
    } else {
        ({ functionSignature, functionCall, returnType, target } = decodeHook(value));
        functionSelector = null;
    }

    // Verify selector matches the function signature (checksum verification, if provided)
    const computedSelector = keccak256(functionSignature).slice(0, 10);
    if (functionSelector) {
        if (functionSelector !== computedSelector) {
            throw new Error("Selector mismatch - function signature verification failed");
        }
    }
    // Use computed selector if not provided
    const selectorToUse = functionSelector || computedSelector;

    // 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 parameter values
    const { functionName, args } = parseFunctionCall(functionCall);
    // functionName = "getCredential"
    // args = ["kyc: 0x76F1Ff0186DDb9461890bdb3094AF74A5F24a162"]

    // Use functionSignature to determine parameter types for ABI encoding
    // functionSignature = "getCredential(string)"

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

    // Resolve from target contract on Ethereum mainnet
    // ABI-encode parameters according to functionSignature
    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, including ERC-7930 interoperable address. This makes hooks entirely self-describing - any client can resolve them without external documentation or ABI files. For use cases including resolving credentials from known registries including PoP (Proof of Personhood) and KYC (Know Your Customer) credentials, the client needs to make sure the source of the credential is trustworthy and verified. Hooks allow clients to jump from a user's metadata record, for example, to a KYC credential from a known credential issuer.

Why Include Both Function Selector and Function String?

Hooks include both an optional 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 predict the return values without consulting documentation. It is also possible to predict if the return data is compatible with intended metadata. For example, if a bytes value redirects using hooks, that return value may need to also be bytes, according to the specific metadata standard (not specified here). 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:

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

Copyright and related rights waived via CC0.