EIP Draft: Multi-chain Governance

I completely agree! The need for a standard is what is driving this proposal. A common spec will encourage an ecosystem of governance interfaces, analytics, aggregators, and whatever else people can dream up. Tally and other DAO aggregators will be able to scale with us into our multi-chain future. Let’s keep this conversation going.

Based on what you said, it sounds like the Gnosis Safe has a lot of modules that will make implementing the spec pretty straightforward. Having many reusable parts allows developers to move much more quickly. However: what is the “whole” that these parts form? If we view it like a vehicle, then we see that Gnosis has a great engine and nice wheels but what is the driving experience? How are the engine and wheels controlled? This is what we need to determine. This is the “aggregator” module you mention.

Finding the Abstraction

The specification needs to capture the right level of abstraction; it needs to be narrow in scope such that it is useful but also composable.

To me there are two important features to multi-chain governance:

1. Proposals include state changes across multiple chains
2. Token holders across multiple L2s and chains can vote on the proposals

Given that we don’t fully agree on how to tackle voting, let’s put voting to the side for the moment and focus on the first point: how can we standardize multi-chain proposals. This is where we have the most commonality right now. No matter how you vote, code needs to be agreed upon and executed on multiple chains.

Multi-Chain Proposals

At a bare minimum we should have a common interface to introspect multi-chain state change proposals. A user of the spec must be able to:

  1. Easily find proposals
  2. View the state changes
  3. Know whether a proposal has been executed

All signalling should be done through events and on-chain data (this is an EIP after all).

Find Proposals

Proposal data should not be stored on-chain. There could be many state changes in a proposal, which would make storage prohibitively expensive. Instead, we can identify proposals using a content hash: the contents of the proposal is hashed to form a unique and verifiable identifier. Let’s call this identifier the proposal hash.

Depending on the implementation, proposal data includes state change data as well as consensus data. However, being a multi-chain system, execution and consensus may occur on different chains. We should separate that data. Let’s introduce another content hash for the state change data called the state change hash.

We now have:

stateChangeHash = hash( stateChangeData )
proposalHash = hash( stateChangeHash, consensusData )

To make this data available off-chain we’re going to need two events.

Let’s emit the first event from a contract we will call the StateChangeOrigin:

interface StateChangeOrigin {
    event StateChangeCreated( stateChangeHash, ...stateChangeData );
}

This event includes the computed stateChangeHash as an indexed topic to make proposal discovery easier.

The second event captures the consensus data. By its nature, consensus must occur in a single place, so let’s call the second contract the ConsensusRoot

interface ConsensusRoot {
    event ProposalCreated( proposalHash, stateChangeHash, ...consensusData);
}

The ProposalCreated event emits the proposal hash as an indexed topic as well.

By listening for events from a group of StateChangeOrigin and ConsensusRoot contracts a viewer will be able to put together the whole picture of the proposal. These contracts may or may not live on the same chain, or they could even be the same contract!

View State Changes

Users must be able to see what the state changes are. We need to standardize the data format for multi-chain calls. What do we need to know for a call? At a minimum, we need to know:

  • The chainId on which the call is occurring
  • The caller who is the one doing the calling
  • The target of the call
  • The calldata for the call

We can define the “state change data” as being an array of structs of the above:

struct Call {
    uint chainId;
    address caller;
    address target;
    bytes callData;
}

interface StateChangeOrigin {
    event StateChangeCreated( bytes32 indexed stateChangeHash, Call[] calls );
}

Now users can see what changes a proposal is going to make. For now, let’s hand-wave the details of stateChangeHash = hash(calls).

Know the Proposal State

Users need to know whether a proposal was successfully executed. This data should be available both on-chain and off-chain through events.

Given that a proposal can be executed on multiple chains, we’ll need to track execution per caller.

The Caller contract is the one that executes the proposal, so it must provide an event and on-chain accessor:

interface Caller {
    event Executed(bytes32 indexed proposalHash);

    function wasExecuted(bytes32 proposalHash) external view returns (bool);
}

The Executed event must be emitted when the execution occurs. The wasExecuted function allows on-chain contracts to determine if a proposal passed, and behave accordingly.

Summary

Let’s bring the above all together:

struct Call {
    uint chainId;
    address caller;
    address target;
    bytes callData;
}

interface StateChangeOrigin {
    event StateChangeCreated(
        bytes32 indexed stateChangeHash,
        Call[] calls
    );
}

interface ConsensusRoot {
    event ProposalCreated(
        bytes32 indexed proposalHash,
        bytes32 indexed stateChangeHash,
        bytes consensusData
    );
}

interface Caller {
    event Executed(bytes32 indexed proposalHash);

    function wasExecuted(bytes32 proposalHash) external view returns (bool);
}

These interfaces will allow a third party to:

  • Find proposals (via indexing)
  • View the state changes (by interpreting encoded event data)
  • Know whether a proposal has been executed (by looking at multi-chain callers)

Open Questions

  • Is this enough for a multi-chain proposal MVP? Does this spec need more? Is it too much?
  • what details are missing?
    • Add value and delegateCall on the Call struct?
    • Add bytes extraData on the event StateChangeOrigin for extensibility?
  • Is this spec flexible enough to support multiple voting EIPs?

Note on Gas

You can ballpark the gas usage using evm.codes. The most expensive part will be the hashing of calldata, so let’s calculate the hashing costs:

  • Estimate 6 words per Call struct
  • Estimate 6 calls per Proposal
  • Estimate each proposal has 4 words of consensus data

Total bytes per proposal: (6*6+4) * 32 = 1280

Plug that into the SHA3 opcode and it costs 393 gas (worst-case). A cold SLOAD is 2100, so it’s cheaper than loading from storage.

Summary

This pared-down version of the EIP keeps the proposal standard, but is flexible enough for implementations to tackle consensus their own way.

What do you all think of this? Is this more narrow scope a better starting point?

1 Like