EIP Draft: Multi-chain Governance

I didn’t know about CAIPs! But what we’re talking about it pretty EVM-specific; I think it fits well here.

1 Like

Thanks everyone for narrowing the scope. Focusing on a standard for remote execution makes a lot of sense.

The risk now seems to be the fact that such standard could apply to ‘arbitrary’ remote execution, while we all started with the governance use case in mind.

Unless you guys think it won’t affect the design space that much, it might be useful to restrain this standard to governance applications to start. That can help address questions like:

  • multiple callers/remote/branch? Frankly, I think we don’t need that in the case of governance, as only one designated ‘branch’ contract should be able to receive, validate and execute proposals. Agree with @anna-carroll here
  • naming being the most difficult in programming (esp. protocol programming), I actually quite liked the GovernonRoot/GovernorBranch proposal at the beginning of the EIP - however I understand it could be messing out with proposal creation and voting, something we’re not interested in. Not really fond of caller

My suggestion might be completely off. If it’s easy enough to abstract that to arbitrary remote execution, then let’s go for it!

1 Like

I think we can have our cake and eat it too! That being said, I agree @mintcloud. Let’s analyze it with our governance hats on so that we can nail that use case. Once we’re happy with it we can put our protocol hats on and see how well it works for other use cases.

Something I realized over the weekend was that the sender of the message can’t necessarily guarantee who the recipient is. The Router could include the Remote’s chainId and address in the message, but anyone can decide to execute that message. It’s not up to the Router. And, for a bridge implementations like Nomad, the recipient is implicit in the domain code.

Instead, it’s the Remote that must be aware of the Router. The remote must validate the message sender. This is evidenced by both the Nomad Governance Router and the OpenZeppelin Cross-Chain Aware contract implementation.

The Nomad GovernanceRouter has a handle function that receives remote calls. Note the onlyGovernorRouter modifier:

    function handle(
        uint32 _origin,
        uint32, // _nonce (unused)
        bytes32 _sender,
        bytes memory _message
    ) external override onlyReplica onlyGovernorRouter(_origin, _sender);

The new OpenZeppelin cross chain contracts are implemented as being “cross chain aware”, in the sense that they are aware of a cross chain sender, and can authorize accordingly:

    function _crossChainSender() internal view virtual override onlyCrossChain returns (address) {
        return _sender;

    function processMessageFromRoot(
        address rootMessageSender,
        bytes calldata data
    ) external override nonReentrant {
        _sender = rootMessageSender;
        Address.functionDelegateCall(address(this), data, "cross-chain execution failed");
        _sender = DEFAULT_SENDER;

The OZ cross-chain plumbing is a little more low-level, but it illustrates how they are providing easy access to the cross-chain origin so that the receiver can authorize the call.

Finally, I do want to mention the Curve Gauges. While we’re thinking about this in terms of governance, it’s very clear that protocols will need this for their interactions as well. The Curve sidechain Gauges are a great example of this. They’ve cut and paste gauge code for each of Arbitrum, Polygon, xDai, and others, then replaced a small piece of the internals to send a message across the relevant bridge. If they had had a remote execution abstraction they could have re-used so much more code.

So with that being said, I would tweak the Remote Exec outline above to instead say that the Remote should be aware of the router, and have accessors for the router / chainid.

Recipient Validates Sender

Yes, the message receiving contract should be “aware” of the address of the message sending contract.

Note the same functionality in Zodiac Nomad module - it calls the authorized sender the “Controller”:

  /// Address of the remote controller which is authorized
  /// to initiate execTransactions on the module from a remote domain.
  address public controller;
  /// Domain of the controller which is authorized to send messages to the module.
  /// Domains are unique identifiers within Nomad for a domain (chain, L1, L2, sidechain, rollup, etc).
  uint32 public controllerDomain;

when the Zodiac module receives messages from Nomad, it validates that the message comes from the Controller:

function handle(
    uint32 _origin,
    uint32, // _nonce (unused)
    bytes32 _sender,
    bytes memory _message
  ) external onlyValid(msg.sender, _origin, _sender) {

where onlyValid in turn calls

require(isController(_senderAddr, _origin), "Unauthorized controller");

Cross-Chain Owner

You can think of the Controller like the cross-chain version of an Owner. Instead of using onlyOwner which checks that

msg.sender == owner

we instead have to check that

origin == controllerDomain && 
sender == controllerAddress

Again, with the (domain, address) tuple being the unique identifier in the cross-chain world - address is no longer sufficient.

Naming, Again

I want to note that Router is a naming convention that @prestwich and I started using for cross-chain application contracts which are isomorphic in nature. That is, these cross-chain applications (xApps) have the same code on every chain; they must contain the logic for both sending and receiving messages.

Nomad’s Bridge and core Governance xApps use the Router pattern. They contain both message sending & message receiving logic, and the code is deployed the same everywhere.

The Zodiac Nomad module does not follow this pattern. It only implements receiving logic.

The contract you’ve described as the “Router” only implements sending logic - as such, I’d gently discourage using the term Router for that.

1 Like

I’m just joining in, and I have difficulties following your language. In particular, I don’t understand which contracts are on which chains. I’m also not sure I undersand the:

My understand is, that when doing a cross-chain operation, such as executing a governance call, there is:

  • A caller (C), on chain #1
  • A receiver (R), on chain #2.

Am I correct in understanding that

  1. the router is on chain #1, its being called bu the caller (C), and it triggers “something”
  2. the remote is on chain #2, it receives cross chain messages, calls the receiver (R) and includes mechanisms so that R can figure out who C is?

In that sense, Router-Remote would form a bridge.

Or maybe I got it wrong, and Router and Remote are both on chain #1, with remote being the bridge entry-point and router being a discovery mechanism.

For the record, I believe that the main issue in that space comes from the different bridges having such different interfaces. IMO, the “low level” bridges should be very simple, with very few feature (they should not care about replay tickets for example), and all the features should be built in the userspace.

Here is an example of such a bridge, that can be be specialized for full duplex communication between polygon root chain mainnet and child chain

If such bridges were standard and widely available, we could build routers allow chainId lookup, and remote execution on any chain (for which a bridge is known)

1 Like

Yes, exactly! You nailed it.

Looking at your code, the IBridge contract would be roughly analogous to the “Router”:

interface IBridge {
    function sendMessage(address target, bytes memory data) external;

It’s just a matter of defining additional responsibilities for the receiver. By defining the receiver behaviour we can unify an interface across Nomad, the Polygon Bridge, and others.

I’m going to whip up a new EIP draft that captures what we’ve discussed so far, then create a new topic so that we can start a fresh conversation.

1 Like

The draft of the EIP for Cross-Chain Execution has now been opened as a PR. Once an EIP number is assigned I’ll create a fresh discussion thread for us to continue fine-tuning the spec.

I’ve added everyone in this thread as a contributor; but if you don’t wish to be included please dm me.

New Thread: EIP-5164: Cross-Chain Execution