Skip to content

ERC-7786

ERC-7786

Overview

ERC-7786 (Cross-Chain Messaging Gateway) is Ethereum's cross-chain messaging gateway standard, released in October 2024. This standard provides a unified interface for contracts to send and receive cross-chain messages containing arbitrary data, aiming to solve the current fragmentation problem in cross-chain communication protocols.

ERC-7786 is part of the Ethereum Foundation's Interoperability Working Group initiative, dedicated to creating a truly interoperable blockchain ecosystem.

Background Problem

Cross-Chain Protocol Fragmentation The current cross-chain communication ecosystem faces severe fragmentation: - LayerZero, Axelar, Wormhole, Hyperlane, and other protocols each operate independently - Each protocol has its own interface and workflow - Applications need to write adapter code for each protocol - Cannot easily switch or use multiple protocols simultaneously

Developer Dilemma

// Using LayerZero
function sendToLayerZero(...) {
    lzEndpoint.send(...);
}

// Using Axelar
function sendToAxelar(...) {
    axelarGateway.callContract(...);
}

// Using Wormhole
function sendToWormhole(...) {
    wormholeRelayer.sendPayloadToEvm(...);
}

// Each protocol requires different code!

Vendor Lock-in - Once a protocol is chosen, migration is difficult - Bound to a single protocol's limitations and risks - Cannot leverage the advantages of multiple protocols

ERC-7786 Solution

Unified Interface A standardized gateway interface that all cross-chain protocols can implement:

interface IGateway {
    function sendMessage(CrossChainMessage calldata message) external payable returns (bytes32 messageId);
    function receiveMessage(CrossChainMessage calldata message) external;
}

Adapter Pattern Existing protocols can implement ERC-7786 through adapters:

Application Contract -> ERC-7786 Gateway -> Adapter -> Underlying Protocol

Protocol Agnostic Applications can easily switch protocols or use multiple protocols simultaneously.

Core Concepts

CrossChainMessage Standard structure for cross-chain messages:

struct CrossChainMessage {
    address sender;              // Sender address
    uint256 sourceChainId;       // Source chain ID
    address recipient;           // Recipient address
    uint256 destinationChainId;  // Destination chain ID
    bytes payload;               // Message payload
    uint256 value;               // Native token amount
    Attribute[] attributes;      // Extensible attributes
}

Attribute An extensible key-value pair system:

struct Attribute {
    bytes32 key;    // Attribute type
    bytes value;    // Attribute data
}

Attribute examples: - Timeout duration - Retry strategy - Fee payment method - Security level - Execution conditions

Core Interface

Send Message

function sendMessage(
    CrossChainMessage calldata message
) external payable returns (bytes32 messageId);

Receive Message

function receiveMessage(
    CrossChainMessage calldata message
) external;

Query Functions

// Check gateway capabilities
function supportsAttribute(bytes32 attributeKey) external view returns (bool);

// Estimate fees
function estimateFee(CrossChainMessage calldata message) external view returns (uint256);

// Query message status
function getMessageStatus(bytes32 messageId) external view returns (MessageStatus);

Events

event MessageSent(
    bytes32 indexed messageId,
    address indexed sender,
    uint256 indexed destinationChainId,
    address recipient,
    bytes payload
);

event MessageReceived(
    bytes32 indexed messageId,
    address indexed recipient,
    uint256 indexed sourceChainId,
    address sender,
    bytes payload
);

Implementation Examples

Application Contract

contract CrossChainApp {
    IGateway public gateway;

    constructor(address _gateway) {
        gateway = IGateway(_gateway);
    }

    // Send cross-chain message
    function sendCrossChain(
        uint256 destinationChainId,
        address recipient,
        bytes calldata data
    ) external payable {
        CrossChainMessage memory message = CrossChainMessage({
            sender: address(this),
            sourceChainId: block.chainid,
            recipient: recipient,
            destinationChainId: destinationChainId,
            payload: data,
            value: 0,
            attributes: new Attribute[](0)
        });

        bytes32 messageId = gateway.sendMessage{value: msg.value}(message);
        emit CrossChainSent(messageId, destinationChainId, recipient);
    }

    // Receive cross-chain message
    function receiveMessage(
        CrossChainMessage calldata message
    ) external {
        require(msg.sender == address(gateway), "Only gateway");

        // Process message
        _handleMessage(message.payload);

        emit CrossChainReceived(message.sourceChainId, message.sender);
    }

    function _handleMessage(bytes calldata payload) internal {
        // Decode and process message
        // ...
    }
}

Gateway Adapter (LayerZero Example)

contract LayerZeroGateway is IGateway {
    ILayerZeroEndpoint public lzEndpoint;

    function sendMessage(
        CrossChainMessage calldata message
    ) external payable returns (bytes32) {
        // Convert ERC-7786 message to LayerZero format
        uint16 dstChainId = _chainIdToLZ(message.destinationChainId);
        bytes memory payload = abi.encode(message);

        // Send via LayerZero
        lzEndpoint.send{value: msg.value}(
            dstChainId,
            abi.encodePacked(message.recipient),
            payload,
            payable(msg.sender),
            address(0),
            bytes("")
        );

        return keccak256(abi.encode(message));
    }

    // LayerZero callback
    function lzReceive(
        uint16 _srcChainId,
        bytes calldata _srcAddress,
        uint64 _nonce,
        bytes calldata _payload
    ) external override {
        require(msg.sender == address(lzEndpoint), "Only endpoint");

        // Decode message
        CrossChainMessage memory message = abi.decode(_payload, (CrossChainMessage));

        // Forward to recipient
        ICrossChainReceiver(message.recipient).receiveMessage(message);

        emit MessageReceived(...);
    }
}

Attribute System

Standard Attributes ERC-7786 defines some recommended standard attributes:

Timeout Attribute

bytes32 constant TIMEOUT_KEY = keccak256("erc7786.timeout");

Attribute memory timeoutAttr = Attribute({
    key: TIMEOUT_KEY,
    value: abi.encode(block.timestamp + 1 hours)
});

Fee Payment Method

bytes32 constant FEE_PAYMENT_KEY = keccak256("erc7786.feePayment");

enum FeePayment {
    Source,        // Pay on source chain
    Destination,   // Pay on destination chain
    ThirdParty     // Third-party payment
}

Attribute memory feeAttr = Attribute({
    key: FEE_PAYMENT_KEY,
    value: abi.encode(FeePayment.Source)
});

Execution **Gas Limit**

bytes32 constant GAS_LIMIT_KEY = keccak256("erc7786.gasLimit");

Attribute memory gasAttr = Attribute({
    key: GAS_LIMIT_KEY,
    value: abi.encode(500000)
});

Custom Attributes Protocols can define their own attributes:

bytes32 constant CUSTOM_SECURITY_KEY = keccak256("myprotocol.security.level");

Attribute memory securityAttr = Attribute({
    key: CUSTOM_SECURITY_KEY,
    value: abi.encode("high")
});

Key Advantages

Interoperability - Applications write code once - Compatible with all protocols implementing ERC-7786 - Easily switch or combine multiple protocols

Reduced Vendor Lock-in

// Can easily switch protocols
gateway = new LayerZeroGateway();
// or
gateway = new AxelarGateway();
// or
gateway = new HyperlaneGateway();

// Application code needs no modification!

Simplified Development - Unified interface - Reduced learning costs - Less integration code - Standardized best practices

Protocol Competition - Protocols are interchangeable - Promotes innovation and optimization - Users choose the best solution

Extensibility - Attribute system supports new features - Backward compatible - Flexibly adapts to changing requirements

Use Cases

Cross-Chain *DeFi*

// Borrow on Ethereum, use on Arbitrum
function crossChainLend(
    uint256 amount,
    uint256 targetChainId,
    address borrowerOnTarget
) external {
    // Lock collateral on source chain
    _lockCollateral(amount);

    // Send cross-chain message to target chain
    CrossChainMessage memory message = CrossChainMessage({
        sender: address(this),
        sourceChainId: block.chainid,
        recipient: targetProtocol,
        destinationChainId: targetChainId,
        payload: abi.encodeWithSignature(
            "releaseLoan(address,uint256)",
            borrowerOnTarget,
            amount
        ),
        value: 0,
        attributes: new Attribute[](0)
    });

    gateway.sendMessage(message);
}

Cross-Chain *NFTs*

// Move NFTs between chains
function bridgeNFT(uint256 tokenId, uint256 targetChainId) external {
    require(ownerOf(tokenId) == msg.sender, "Not owner");

    // Burn on source chain
    _burn(tokenId);

    // Notify target chain to mint
    CrossChainMessage memory message = CrossChainMessage({
        sender: address(this),
        sourceChainId: block.chainid,
        recipient: mirrorContractOnTarget,
        destinationChainId: targetChainId,
        payload: abi.encode(msg.sender, tokenId, _metadata[tokenId]),
        value: 0,
        attributes: new Attribute[](0)
    });

    gateway.sendMessage(message);
}

Cross-Chain Governance

// Initiate proposal on main chain, execute on multiple chains
function executeMultiChainProposal(
    uint256[] calldata targetChains,
    bytes[] calldata payloads
) external onlyGovernance {
    for (uint i = 0; i < targetChains.length; i++) {
        CrossChainMessage memory message = CrossChainMessage({
            sender: address(this),
            sourceChainId: block.chainid,
            recipient: governanceExecutorOnTarget,
            destinationChainId: targetChains[i],
            payload: payloads[i],
            value: 0,
            attributes: new Attribute[](0)
        });

        gateway.sendMessage(message);
    }
}

Cross-Chain Identity Users verify identity on one chain and use it on other chains: - Single Sign-On (SSO) - Cross-chain credit scores - Unified membership systems

Relationship with Other Standards

Difference from ERC-7683 - ERC-7683: Cross-chain intents, focused on value transfers and swaps - ERC-7786: General message passing, supporting arbitrary data and operations

The two can complement each other: - ERC-7683 for cross-chain trades - ERC-7786 for cross-chain messages and state synchronization

Relationship with EIP-5164 EIP-5164 is also a cross-chain messaging standard, but: - EIP-5164 is older with fewer features - ERC-7786 is more complete, supporting the attribute system - ERC-7786 has broader protocol support

Implementation Challenges

Security - Message verification: Ensure message authenticity - Replay protection: Prevent duplicate message execution - Permission checks: Verify sender identity

Fee Management - Cross-chain fee calculation is complex - Multiple fee payment methods - Price volatility impact

Protocol Differences - Different protocols have different security models - Confirmation time differences - Inconsistent feature support

Error Handling - Rolling back failed messages - Timeout handling - User notifications

Adoption Status

Protocol Support The following protocols are implementing or considering ERC-7786: - Hyperlane - LayerZero - Wormhole - Axelar - Chainlink CCIP

Application Integration - Cross-chain DEXs - Multi-chain DAOs - Cross-chain games - Unified liquidity pools

Development Tools

*OpenZeppelin* Contracts**

import "@openzeppelin/contracts/crosschain/ERC7786Gateway.sol";

contract MyGateway is ERC7786Gateway {
    // Implement cross-chain logic
}

Testing Tools

import { testERC7786Compliance } from '@erc7786/test-suite';

describe('MyGateway', () => {
    it('should comply with ERC-7786', async () => {
        await testERC7786Compliance(gateway);
    });
});

Best Practices

Message Design - Keep payloads compact - Use efficient encoding - Include necessary context

Error Handling

function receiveMessage(CrossChainMessage calldata message) external {
    try this._handleMessage(message) {
        emit MessageSuccess(message.messageId);
    } catch Error(string memory reason) {
        emit MessageFailed(message.messageId, reason);
        // Optional: Store failed messages for retry
    }
}

Retry Mechanism

mapping(bytes32 => CrossChainMessage) public failedMessages;

function retryMessage(bytes32 messageId) external {
    CrossChainMessage memory message = failedMessages[messageId];
    gateway.sendMessage{value: msg.value}(message);
}

Future Outlook

Ecosystem Development - More protocols adopting ERC-7786 - Standard libraries and tools maturing - Wallet and block explorer support

Standard Evolution - New standard attributes - More complex message types - Integration with other standards

True Chain Abstraction ERC-7786 is a key component for achieving chain abstraction: - Users do not need to worry about the underlying chain - Applications run seamlessly across chains - Unified liquidity