Exploring the Openzeppelin CrossChain Functionality

What is the new CrossChain support and how can you use it.

For the first time Openzeppelin Contracts have added CrossChain Support. In particular the following chains are currently supported:

  • Polygon: One of the most popular sidechains right now. We've discussed it previously here.
  • Optimism: A Layer 2 chain based on optimistic rollups. We discussed the tech previously here.
  • Arbitrum: A different Layer 2 chain also based on optimistic rollups. 
  • AMB: The Arbitrary Message Bridge is a general tool one can use to relay any data between two chains.
Son of a bitch Meme

What are the Difficulties with CrossChain?

So what exactly are the things to consider in CrossChain communication?

  • Why even send data to begin with? Well you might have governance-owned contract on Ethereum mainnet for example controlled by a native ERC-20. And you want this contract to be able to change fundamental things, for example upgrade another contract on the child chain. Then you need a way to allow the contract from the mainnet to send data to the child in a secure way.
  • The sender problem: One difficulty of sending cross-chain messages is determining the sender address, because from the contract's perspective msg.sender will actually be a child system address. What exactly msg.sender will be of course depends on the child chain, but it won't be the actual sender from the root chain.
  • The access control problem: Another issue with CrossChain is the potential for an address existing twice, so be aware that a contract with identical address can exist on child and root chain.
  • Signature Double-Use: And if you allow for any signatures in your contract, they might be double used. This is why you should always double check the chain id or better use EIP-712 which handles all the complexities with secure signatures.

How the OZ CrossChain Support works

Openzeppelin has added contracts to support the usage in Polygon, Optimism, Arbitrum and AMB. There is a master interface CrossChainEnabled.sol which all four implementations make use of. And there is a new AccessControlCrossChain.sol to allow for secure access control via roles but with CrossChain support. An overview of it all can be seen here.

Each chain implementation contains a different mechanism to retrieve the original CrossChain sender which roughly looks like this:

function processMessageFromRoot(
    uint256, /* stateId */
    address rootMessageSender,
    bytes calldata data
)
AMB_Bridge(bridge).messageSender()
LibArbitrumL2.crossChainSender(LibArbitrumL2.ARBSYS)
Optimism_Bridge(messenger).xDomainMessageSender()

For the full details, check out the contract code here.

How to use it - Polygon Example

Let's take a deeper dive in how you would actually use this with the example of Polygon.

We'll create contracts on the root and child chain with secure access control.

1. Creating the Root Contract

So first let's create a contract that we will deploy on the root blockchain. In the normal case this would be the Ethereum mainnet. We can inherit from the FxBaseRootTunnel.sol contract and pass check point and root addresses depending on the network:

  • GOERLI_CHECKPOINT_MANAGER = 0x2890bA17EfE978480615e330ecB65333b880928e
  • GOERLI_FX_ROOT = 0x3d1d3E34f7fB6D26245E6640E1c50710eFFf15bA
  • MAINNET_CHECKPOINT_MANAGER = 0x86E4Dc95c7FBdBf52e33D563BbDB00823894C287
  • MAINNET_FX_ROOT = 0xfe5e5D361b2ad62c541bAb87C45a0B9B018389a2

It will give you two internal functions to work with

  1. _processMessageFromChild: Override this to respond to messages sent from the child contract.
  2. _sendMessageToChild: Call this to send a message to the child.
import {FxBaseRootTunnel} from
    "fx-portal/contracts/tunnel/FxBaseRootTunnel.sol";

// see left for full addresses
address constant GOERLI_CP_MANAGER = 0x2890bA17EfE978480615e...;
address constant GOERLI_FX_ROOT = 0x3d1d3E34f7fB6D26245E6640...;

contract PolygonRoot is FxBaseRootTunnel {
    bytes public latestData;

    constructor()
        FxBaseRootTunnel(GOERLI_CP_MANAGER, GOERLI_FX_ROOT) {
    }

    function _processMessageFromChild(
        bytes memory data
    ) internal override {
        latestData = data;
    }

    function sendMessageToChild(bytes memory message) public {
        _sendMessageToChild(message);
    }
}
import {CrossChainEnabledPolygonChild} from
    "oz/contracts/crosschain/polygon/CrossChainEnabledPolygonChild.sol";
import {AccessControlCrossChain} from
    "oz/contracts/access/AccessControlCrossChain.sol";

address constant MUMBAI_FX_CHILD = 0xCf73231F...; // see right

contract PolygonChild is
    CrossChainEnabledPolygonChild,
    AccessControlCrossChain
{
    event MessageSent(bytes message);
    uint256 public myNumber = 12;

    constructor(
        address rootParent
    ) CrossChainEnabledPolygonChild(MUMBAI_FX_CHILD) {
        _grantRole(
            _crossChainRoleAlias(DEFAULT_ADMIN_ROLE),
            rootParent
        );
    }

    function setNumberForParentChain(
        uint256 newNumber
    ) external onlyRole(DEFAULT_ADMIN_ROLE) {
        myNumber = newNumber;
    }

    function _sendMessageToRoot(
        bytes memory message
    ) internal {
        emit MessageSent(message);
    }
}

2. Creating the Child Contract

And then we can create the child contract that we will deploy on the child blockchain. In our case this will be the Polygon network. We can inherit from the Openzeppelin CrossChainEnabledPolygonChild.sol contract and pass FX Portal Child contract depending on the network:

  • MUMBAI_FX_CHILD = 0xCf73231F28B7331BBe3124B907840A94851f9f11
  • MAINNET_FX_CHILD = 0x8397259c983751DAf40400790063935a11afa28a

And now we can also make use of the AccessControlCrossChain.sol from Openzeppelin. Just inherit from it in the contract and we'll get the usual access control functions along with a new _crossChainRoleAlias function.

In our example upon deployment we will pass the previously deployed root contract address here and immediately grant it the admin role, but since this is actually a crosschain communication, it works a little differently:

  1. Of course the onlyRole modifier cannot just check the msg.sender, so instead it uses the CrossChainEnabled.sol interface to determine the actual CrossChain sender.
  2. And to further prevent access from contracts in the child chain with the same address as in the root chain, we need to distinguish between senders from msg.sender directly or from CrossChain. For that we can grant a specific role using _crossChainRoleAlias.

And then let's add a test function setNumberForParentChain which only the CrossChain root is allowed to call.

And just for completeness, if you wanted to send a message back to the root, in Polygon you could do so by emitting the MessageSent event.

3. Get Encoded Data Helper

This is completely optional, but for our testing you could add an extra function like this:

function getEncodedSetNumberData(uint256 newNumber) external pure returns (bytes memory) {
    return abi.encodeWithSelector(PolygonChild.setNumberForParentChain.selector, newNumber);
}

Basically it will return the encoded data if you wanted to call setNumberForParentChain. This data is what you would need to send along in the root contract via sendMessageToChild. Of course in most setups you would implement this just using Web3.js or whatever frontend framework you're using.

4. Testing the CrossChain Transfer on Remix

Okay so now let's actually deploy this to the testnet. We are using

  • the Goerli network as root, the latest Ethereum testnet
  • and Mumbai, the Polygon testnet

But the flow would be identical for mainnet, just using different addresses.

So we can just copy the full code here into Remix:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.13;

import {CrossChainEnabledPolygonChild} from "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/crosschain/polygon/CrossChainEnabledPolygonChild.sol";
import {AccessControlCrossChain} from "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/AccessControlCrossChain.sol";

import {FxBaseRootTunnel} from "https://github.com/fx-portal/contracts/blob/main/contracts/tunnel/FxBaseRootTunnel.sol";

address constant MUMBAI_FX_CHILD = 0xCf73231F28B7331BBe3124B907840A94851f9f11;
address constant GOERLI_CHECKPOINT_MANAGER = 0x2890bA17EfE978480615e330ecB65333b880928e;
address constant GOERLI_FX_ROOT = 0x3d1d3E34f7fB6D26245E6640E1c50710eFFf15bA;

address constant MAINNET_FX_CHILD = 0x8397259c983751DAf40400790063935a11afa28a;
address constant MAINNET_CHECKPOINT_MANAGER = 0x86E4Dc95c7FBdBf52e33D563BbDB00823894C287;
address constant MAINNET_FX_ROOT = 0xfe5e5D361b2ad62c541bAb87C45a0B9B018389a2;

contract PolygonChild is CrossChainEnabledPolygonChild, AccessControlCrossChain {
    uint256 public myNumber;

    constructor(address rootParent) CrossChainEnabledPolygonChild(MUMBAI_FX_CHILD) {
        _grantRole(_crossChainRoleAlias(DEFAULT_ADMIN_ROLE), rootParent);
        myNumber = 12;
    }

    function setNumberForParentChain(uint256 newNumber) external onlyRole(DEFAULT_ADMIN_ROLE) {
        myNumber = newNumber;
    }

    function getEncodedSetNumberData(uint256 newNumber) external pure returns (bytes memory) {
        return abi.encodeWithSelector(PolygonChild.setNumberForParentChain.selector, newNumber);
    }
}

contract PolygonRoot is FxBaseRootTunnel {
    bytes public latestData;

    constructor() FxBaseRootTunnel(GOERLI_CHECKPOINT_MANAGER, GOERLI_FX_ROOT) {}

    function _processMessageFromChild(bytes memory data) internal override {
        latestData = data;
    }

    function sendMessageToChild(bytes memory message) public {
        _sendMessageToChild(message);
    }
}

And now you can

  1. Switch MetaMask to Goerli and deploy the PolygonRoot.
  2. Switch MetaMask to Mumbai and copy the address and deploy PolygonChild for the constructor input.
  3. Switch MetaMask to Goerli  and call setFxChildTunnel on PolygonRoot passing the child address.
  4. Now encode the data you want to send. For example to set the number for setNumberForParentChain as 42, the encoded data would be: 0x21148d91000000000000000000000000000000000000000000000000000000000000002a. The easiest way to get this is via getEncodedSetNumberData.
  5. Call sendMessageToChild and pass along the encoded data. This will initiate the CrossChain transfer. It might take a while.
  6. Wait.... meanwhile you can double check the events from the FX_CHILD here or simply the transfers of the zero address here.
Mumbai Zero
Mumbai FX Child

If that looks confusing to you, then that's no surprise. The zero address is a special system address in Polygon which is used to commit the CrossChain transfers. And the transfers also end up in the events list of the child.

In my tests it took anywhere between 2 to 25 minutes until the CrossChain transfer on Polygon was completed.

And that's it!

If you did everything correctly, you can switch back to Mumbai and read the newly set number which should have changed to 42.

Sponge Bob 42

Markus Waas

Solidity Developer

More great blog posts from Markus Waas

© 2024 Solidity Dev Studio. All rights reserved.

This website is powered by Scrivito, the next generation React CMS.