degencodebeast / ERC5164

EIP-5164 implementation

Home Page:https://eips.ethereum.org/EIPS/eip-5164

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

ERC-5164

EIP-5164 specifies how smart contracts on one chain can message contracts on another. Transport layers, such as bridges, will have their own EIP-5164 implementations. This repository includes implementations for: Ethereum to Polygon, Ethereum to Optimism, and Ethereum to Arbitrum. All three use the 'native' bridge solutions.

The EIP is currently in the Review stage: https://eips.ethereum.org/EIPS/eip-5164

Feedback and PR are welcome!

How to use

To use ERC-5164 to send messages your contract code will need to:

  • On the sending chain, send a message to the MessageDispatcher dispatchMessage or dispatchMessageBatch function
  • Listen for messages from the corresponding MessageExecutor(s) on the receiving chain.

The listener will need to be able to unpack the original sender address (it's appended to calldata). We recommend inheriting from the included ExecutorAware.sol contract.

Note

For most bridges, you only have to call dispatchMessage or dispatchMessageBatch to have messages executed by the MessageExecutor. However, Arbitrum requires an EOA to process the dispatch. We will review this below.

How it works

  1. A smart contract on the sending chain calls dispatchMessage or dispatchMessageBatch on the MessageDispatcher..
  2. The corresponding MessageExecutor(s) on the receiving chain will execute the message or batch of Message structs. The address of the original dispatcher on the sending chain is appended to the message data.
  3. Any smart contract can receive messages from a MessageExecutor, but they should use the original dispatcher address for authentication.

Note: this specification does not require messages to be executed in order

Dispatching

Dispatch a message

To dispatch a message from Ethereum to the L2 of your choice, you have to interact with the ISingleMessageDispatcher contract and call the following function.

/**
 * @notice Dispatch a message to the receiving chain.
 * @dev Must compute and return an ID uniquely identifying the message.
 * @dev Must emit the `MessageDispatched` event when successfully dispatched.
 * @param toChainId ID of the receiving chain
 * @param to Address on the receiving chain that will receive `data`
 * @param data Data dispatched to the receiving chain
 * @return bytes32 ID uniquely identifying the message
 */
function dispatchMessage(
  uint256 toChainId,
  address to,
  bytes calldata data
) external returns (bytes32);
  • toChainId: id of the chain to which you want to dispatch the message
  • to: address of the contract that will receive the message
  • data: message that you want to be executed on L2

Dispatch a batch messages

To dispatch a batch of messages from Ethereum to the L2 of your choice, you have to interact with the IBatchedMessageDispatcher contract and call the following function.

/**
 * @notice Dispatch `messages` to the receiving chain.
 * @dev Must compute and return an ID uniquely identifying the `messages`.
 * @dev Must emit the `MessageBatchDispatched` event when successfully dispatched.
 * @param toChainId ID of the receiving chain
 * @param messages Array of Message dispatched
 * @return bytes32 ID uniquely identifying the `messages`
 */
function dispatchMessageBatch(uint256 toChainId, MessageLib.Message[] calldata messages)
  external
  returns (bytes32);
  • toChainId: id of the chain to which you want to dispatch the message
  • messages: array of Message that you want to be executed on L2
/**
 * @notice Message data structure
 * @param to Address that will be dispatched on the receiving chain
 * @param data Data that will be sent to the `to` address
 */
struct Message {
  address to;
  bytes data;
}

Example

MessageDispatcherOptimism _messageDispatcher = 0x3F3623aB84a86410096f53051b82aA41773A4480;
address _greeter = 0x19c8f7B8BA7a151d6825924446A596b6084a36ae;

_messageDispatcher.dispatchMessage(
  420,
  _greeter,
  abi.encodeCall(Greeter.setGreeting, ("Hello from L1"))
);

Code:

Arbitrum Dispatch

Arbitrum requires an EOA to submit a bridge transaction. The Ethereum to Arbitrum ERC-5164 MessageDispatcher dispatchMessage implementation is therefore split into two actions:

  1. Message to MessageDispatcher dispatchMessage is fingerprinted and stored along with their messageId.
  2. Anyone may call MessageDispatcher processMessage to send a previously fingerprinted dispatched message.

The processMessage function requires the same transaction parameters as the Arbitrum bridge. The Arbitrum SDK is needed to properly estimate the gas required to execute the message on L2.

/**
 * @notice Process message that has been dispatched.
 * @dev The transaction hash must match the one stored in the `dispatched` mapping.
 * @dev `_from` is passed as `callValueRefundAddress` cause this address can cancel the retryably ticket.
 * @dev We store `_message` in memory to avoid a stack too deep error.
 * @param _messageId ID of the message to process
 * @param _from Address who dispatched the `_data`
 * @param _to Address that will receive the message
 * @param _data Data that was dispatched
 * @param _refundAddress Address that will receive the `excessFeeRefund` amount if any
 * @param _gasLimit Maximum amount of gas required for the `_messages` to be executed
 * @param _maxSubmissionCost Max gas deducted from user's L2 balance to cover base submission fee
 * @param _gasPriceBid Gas price bid for L2 execution
 * @return uint256 Id of the retryable ticket that was created
 */
function processMessage(
  bytes32 messageId,
  address from,
  address to,
  bytes calldata data,
  address refundAddress,
  uint256 gasLimit,
  uint256 maxSubmissionCost,
  uint256 gasPriceBid
) external payable returns (uint256);

Arbitrum Dispatch Example

  const greeterAddress = await getContractAddress('Greeter', ARBITRUM_GOERLI_CHAIN_ID, 'Forge');

  const greeting = 'Hello from L1';
  const messageData = new Interface(['function setGreeting(string)']).encodeFunctionData(
    'setGreeting',
    [greeting],
  );

  const nextNonce = (await messageDispatcherArbitrum.nonce()).add(1);

  const encodedMessageId = keccak256(
    defaultAbiCoder.encode(
      ['uint256', 'address', 'address', 'bytes'],
      [nextNonce, deployer, greeterAddress, messageData],
    ),
  );

  const executeMessageData = new Interface([
    'function executeMessage(address,bytes,bytes32,uint256,address)',
  ]).encodeFunctionData('executeMessage', [
    greeterAddress,
    messageData,
    encodedMessageId,
    GOERLI_CHAIN_ID,
    deployer,
  ]);

...

  const { deposit, gasLimit, maxSubmissionCost } = await l1ToL2MessageGasEstimate.estimateAll(
    {
      from: messageDispatcherArbitrumAddress,
      to: messageExecutorAddress,
      l2CallValue: BigNumber.from(0),
      excessFeeRefundAddress: deployer,
      callValueRefundAddress: deployer,
      data: executeMessageData,
    },
    baseFee,
    l1Provider,
  );

  await messageDispatcherArbitrum.dispatchMessage(
    ARBITRUM_GOERLI_CHAIN_ID,
    greeterAddress,
    messageData,
  );

...

await messageDispatcherArbitrum.processMessage(
    messageId,
    deployer,
    greeterAddress,
    messageData,
    deployer,
    gasLimit,
    maxSubmissionCost,
    gasPriceBid,
    {
      value: deposit,
    },
  );

Code: script/bridge/BridgeToArbitrumGoerli.ts

Execution

Execute message

Once the message has been bridged it will be executed by the MessageExecutor contract.

Authenticate messages

To ensure that the messages originate from the MessageExecutor contract, your contracts can inherit from the ExecutorAware abstract contract.

It makes use of EIP-2771 to authenticate the message forwarder (i.e. the MessageExecutor) and has helper functions to extract from the calldata the original sender and the messageId of the dispatched message.

/**
 * @notice Check which executor this contract trust.
 * @param _executor Address to check
 */
function isTrustedExecutor(address _executor) public view returns (bool);

/**
  * @notice Retrieve messageId from message data.
  * @return _msgDataMessageId ID uniquely identifying the message that was executed
  */
function _messageId() internal pure returns (bytes32 _msgDataMessageId)

/**
  * @notice Retrieve fromChainId from message data.
  * @return _msgDataFromChainId ID of the chain that dispatched the messages
  */
function _fromChainId() internal pure returns (uint256 _msgDataFromChainId);

/**
 * @notice Retrieve signer address from message data.
 * @return _signer Address of the signer
 */
function _msgSender() internal view returns (address payable _signer);

Deployed Contracts

Mainnet

Ethereum -> Optimism

Network Contract Address
Ethereum EthereumToOptimismDispatcher.sol 0xa8f85bAB964D7e6bE938B54Bf4b29A247A88CD9d
Optimism EthereumToOptimismExecutor 0x890a87E71E731342a6d10e7628bd1F0733ce3296

Testnet

Ethereum Goerli -> Arbitrum Goerli

Network Contract Address
Ethereum Goerli EthereumToArbitrumDispatcher.sol 0xBc244773f71a2f897fAB5D5953AA052B8ff68670
Arbitrum Goerli EthereumToArbitrumExecutor 0xe7Ab52219631882f778120c1f19D6086ED390bE1
Arbitrum Goerli Greeter 0xA181dE5454daa63115e4A2f626E9268Cc812FcC1

Ethereum Goerli -> Optimism Goerli

Network Contract Address
Ethereum Goerli EthereumToOptimismDispatcher.sol 0x81F4056FFFa1C1fA870de40BC45c752260E3aD13
Optimism Goerli EthereumToOptimismExecutor 0xc5165406dB791549f0D2423D1483c1EA10A3A206
Optimism Goerli Greeter 0x50281C11B6a18d0613F507fD2E7a1ADd712De7D8

Ethereum Goerli -> Polygon Mumbai

Network Contract Address
Ethereum Goerli EthereumToPolygonDispatcher 0xBA8d8a0554dFd7F7CCf3cEB47a88d711e6a65F5b
Polygon Mumbai EthereumToPolygonExecutor 0x784fFd1E27FA32804bD0a170dc7A277399AbD361
Polygon Mumbai Greeter 0x3b73dCeC4447DDB1303F9b766BbBeB87aFAf22a3

Development

Installation

You may have to install the following tools to use this repository:

  • Yarn to handle dependencies
  • Foundry to compile and test contracts
  • direnv to handle environment variables
  • lcov to generate the code coverage report

Install dependencies:

yarn

Env

Copy .envrc.example and write down the env variables needed to run this project.

cp .envrc.example .envrc

Once your env variables are setup, load them with:

direnv allow

Compile

Run the following command to compile the contracts:

yarn compile

Test

We use Hardhat to run Arbitrum fork tests. All other tests are being written in Solidity and make use of Forge Standard Library.

To run Forge unit and fork tests:

yarn test

To run Arbitrum fork tests, use the following commands:

  • Fork tests to dispatch messages from Ethereum to Arbitrum:

    yarn fork:startDispatchMessageBatchArbitrumMainnet
    
  • Fork tests to execute messages on Arbitrum:

    yarn fork:startExecuteMessageBatchArbitrumMainnet
    

Coverage

Forge is used for coverage, run it with:

yarn coverage

You can then consult the report by opening coverage/index.html:

open coverage/index.html

Deployment

You can use the following commands to deploy on testnet.

Ethereum Goerli to Arbitrum Goerli bridge

yarn deploy:arbitrumGoerli

Ethereum Goerli to Optimism Goerli bridge

yarn deploy:optimismGoerli

Ethereum Goerli to Polygon Mumbai bridge

yarn deploy:mumbai

Bridging

You can use the following commands to bridge from Ethereum to a layer 2 of your choice.

It will set the greeting message in the Greeter contract to Hello from L1 instead of Hello from L2.

Ethereum Goerli to Arbitrum Goerli

yarn bridge:arbitrumGoerli

It takes about 15 minutes for the message to be bridged to Arbitrum Goerli.

Example transaction
Network Message Transaction hash
Ethereum Goerli dispatchMessage 0xfdb983cad74d5d95c2ffdbb38cde50fefbe78280416bbe44de35485c213909d5
Ethereum Goerli processMessage 0x4effcda5e729a2943a86bd1317a784644123388bb4fd7ea207e70ec3a360ab60
Arbitrum Goerli executeMessage 0x0883252887d34a4a545a20e252e55c712807d1707438cf6e8503a99a32357024

Ethereum Goerli to Optimism Goerli

yarn bridge:optimismGoerli

It takes about 5 minutes for the message to be bridged to Optimism Goerli.

Example transaction
Network Message Transaction hash
Ethereum Goerli dispatchMessage 0xdaf3b8210294dc2414beefa14e56f47f638510031c4487443c58fd6a92c8f386
Optimism Goerli executeMessage https://goerli-optimism.etherscan.io/tx/0xa83813646e7978cea4f27b57688ce30e3622b135ca6c18489d0c8fa3ee297c5b

Ethereum Goerli to Polygon Mumbai

yarn bridge:mumbai

It takes about 30 minutes for the message to be bridged to Mumbai.

Example transaction
Network Message Transaction hash
Ethereum Goerli dispatchMessage 0x856355f3df4f94bae2075abbce57163af95637ae9c65bbe231f170d9cdf251c9
Polygon Mumbai executeMessage 0x78aff3ff10b43169ce468bf88da79560724ea292290c336cd84a43fdd8441c52

Code quality

Prettier is used to format TypeScript and Solidity code. Use it by running:

yarn format

Solhint is used to lint Solidity files. Run it with:

yarn hint

TypeChain is used to generates types for Hardhat scripts and tests. Generate them by running:

yarn typechain

About

EIP-5164 implementation

https://eips.ethereum.org/EIPS/eip-5164


Languages

Language:Solidity 82.9%Language:TypeScript 17.0%Language:Shell 0.0%