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!
To use ERC-5164 to send messages your contract code will need to:
- On the sending chain, send a message to the MessageDispatcher
dispatchMessage
ordispatchMessageBatch
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.
- A smart contract on the sending chain calls
dispatchMessage
ordispatchMessageBatch
on the MessageDispatcher.. - 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.
- 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
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 messageto
: address of the contract that will receive the messagedata
: message that you want to be executed on L2
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 messagemessages
: 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;
}
MessageDispatcherOptimism _messageDispatcher = 0x3F3623aB84a86410096f53051b82aA41773A4480;
address _greeter = 0x19c8f7B8BA7a151d6825924446A596b6084a36ae;
_messageDispatcher.dispatchMessage(
420,
_greeter,
abi.encodeCall(Greeter.setGreeting, ("Hello from L1"))
);
Code:
Arbitrum requires an EOA to submit a bridge transaction. The Ethereum to Arbitrum ERC-5164 MessageDispatcher dispatchMessage
implementation is therefore split into two actions:
- Message to MessageDispatcher
dispatchMessage
is fingerprinted and stored along with theirmessageId
. - 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);
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
Once the message has been bridged it will be executed by the MessageExecutor contract.
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);
Network | Contract | Address |
---|---|---|
Ethereum | EthereumToOptimismDispatcher.sol | 0xa8f85bAB964D7e6bE938B54Bf4b29A247A88CD9d |
Optimism | EthereumToOptimismExecutor | 0x890a87E71E731342a6d10e7628bd1F0733ce3296 |
Network | Contract | Address |
---|---|---|
Ethereum Goerli | EthereumToArbitrumDispatcher.sol | 0xBc244773f71a2f897fAB5D5953AA052B8ff68670 |
Arbitrum Goerli | EthereumToArbitrumExecutor | 0xe7Ab52219631882f778120c1f19D6086ED390bE1 |
Arbitrum Goerli | Greeter | 0xA181dE5454daa63115e4A2f626E9268Cc812FcC1 |
Network | Contract | Address |
---|---|---|
Ethereum Goerli | EthereumToOptimismDispatcher.sol | 0x81F4056FFFa1C1fA870de40BC45c752260E3aD13 |
Optimism Goerli | EthereumToOptimismExecutor | 0xc5165406dB791549f0D2423D1483c1EA10A3A206 |
Optimism Goerli | Greeter | 0x50281C11B6a18d0613F507fD2E7a1ADd712De7D8 |
Network | Contract | Address |
---|---|---|
Ethereum Goerli | EthereumToPolygonDispatcher | 0xBA8d8a0554dFd7F7CCf3cEB47a88d711e6a65F5b |
Polygon Mumbai | EthereumToPolygonExecutor | 0x784fFd1E27FA32804bD0a170dc7A277399AbD361 |
Polygon Mumbai | Greeter | 0x3b73dCeC4447DDB1303F9b766BbBeB87aFAf22a3 |
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
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
Run the following command to compile the contracts:
yarn compile
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
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
You can use the following commands to deploy on testnet.
yarn deploy:arbitrumGoerli
yarn deploy:optimismGoerli
yarn deploy:mumbai
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
.
yarn bridge:arbitrumGoerli
It takes about 15 minutes for the message to be bridged to Arbitrum Goerli.
Network | Message | Transaction hash |
---|---|---|
Ethereum Goerli | dispatchMessage | 0xfdb983cad74d5d95c2ffdbb38cde50fefbe78280416bbe44de35485c213909d5 |
Ethereum Goerli | processMessage | 0x4effcda5e729a2943a86bd1317a784644123388bb4fd7ea207e70ec3a360ab60 |
Arbitrum Goerli | executeMessage | 0x0883252887d34a4a545a20e252e55c712807d1707438cf6e8503a99a32357024 |
yarn bridge:optimismGoerli
It takes about 5 minutes for the message to be bridged to Optimism Goerli.
Network | Message | Transaction hash |
---|---|---|
Ethereum Goerli | dispatchMessage | 0xdaf3b8210294dc2414beefa14e56f47f638510031c4487443c58fd6a92c8f386 |
Optimism Goerli | executeMessage | https://goerli-optimism.etherscan.io/tx/0xa83813646e7978cea4f27b57688ce30e3622b135ca6c18489d0c8fa3ee297c5b |
yarn bridge:mumbai
It takes about 30 minutes for the message to be bridged to Mumbai.
Network | Message | Transaction hash |
---|---|---|
Ethereum Goerli | dispatchMessage | 0x856355f3df4f94bae2075abbce57163af95637ae9c65bbe231f170d9cdf251c9 |
Polygon Mumbai | executeMessage | 0x78aff3ff10b43169ce468bf88da79560724ea292290c336cd84a43fdd8441c52 |
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