Skip to content

Cross-Chain treasury management proposal

This guide walks through the process of creating and executing a cross-chain governance proposal to mint W tokens to both the Optimism and Arbitrum treasuries. In this tutorial, we'll cover how to create a proposal on the hub chain (Ethereum Mainnet), cast votes from spoke chains (Optimism and Arbitrum), aggregate votes, and execute the proposal.

Create a Proposal

The first step is to create a proposal on the hub chain, which in this case is Ethereum Mainnet. The proposal will contain instructions to mint 10 W tokens to the Optimism treasury and 15 ETH to the Arbitrum treasury.

In the following code snippet, we initialize the proposal with two transactions, each targeting the Hub's Message Dispatcher contract. These transactions will relay the governance actions to the respective spoke chains via Wormhole.

Key actions:

  • Define the proposal targets (two transactions to the Message Dispatcher)
  • Set values for each transaction (in this case, both are 0 as we're not transferring any native ETH)
  • Encode the calldata for minting 10 W tokens on Optimism and sending 15 ETH to Arbitrum
  • Finally, we submit the proposal to the HubGovernor contract
HubGovernor governor = HubGovernor(GOVERNOR_ADDRESS);
// Prepare proposal details
address[] memory targets = new address[](2);
targets[0] = HUB_MESSAGE_DISPATCHER_ADDRESS;
targets[1] = HUB_MESSAGE_DISPATCHER_ADDRESS;
uint256[] memory values = new uint256[](2);
values[0] = 0;
values[1] = 0;
bytes[] memory calldatas = new bytes[](2);
// Prepare message for Optimism to mint 10 W tokens
// bytes created using abi.encodeWithSignature("mint(address,uint256)", 0xB0fFa8000886e57F86dd5264b9582b2Ad87b2b91, 10e18)
calldatas[0] = abi.encodeWithSignature(
    "dispatch(bytes)", 
    abi.encode(
        OPTIMISM_WORMHOLE_CHAIN_ID,
        [OPTIMISM_WORMHOLE_TREASURY_ADDRESS],
        [uint256(10 ether)],
        [hex"0x40c10f19000000000000000000000000b0ffa8000886e57f86dd5264b9582b2ad87b2b910000000000000000000000000000000000000000000000008ac7230489e8000000000000000000000000000000000000000000000000000000000000"] 
    )
);
// Prepare message for Arbitrum to receive 15 ETH
calldatas[1] = abi.encodeWithSignature(
    "dispatch(bytes)", 
    abi.encode(
        ARBITRUM_WORMHOLE_CHAIN_ID,
        [ARBITRUM_WORMHOLE_TREASURY_ADDRESS],
        [uint256(15 ether)],
        [hex"0x40c10f19000000000000000000000000b0ffa8000886e57f86dd5264b9582b2ad87b2b910000000000000000000000000000000000000000000000008ac7230489e8000000000000000000000000000000000000000000000000000000000000"] 
    )
);
string memory description = "Mint 10 W to Optimism treasury and 10 W to Arbitrum treasury via Wormhole";
// Create the proposal
uint256 proposalId = governor.propose(
    targets, values, calldatas, description
)
Parameters

GOVERNOR_ADDRESS address

The address of the HubGovernor contract on Ethereum Mainnet.


targets address[]

An array that specifies the addresses that will receive the proposal's actions. Here, both are set to the HUB_MESSAGE_DISPATCHER_ADDRESS.


values uint256[]

An array containing the value of each transaction (in Wei). In this case, both are set to zero because no ETH is being transferred.


calldatas bytes[]

The calldata for the proposal. These are encoded contract calls containing cross-chain dispatch instructions for minting tokens and sending ETH. The calldata specifies minting 10 W tokens to the Optimism treasury and sending 15 ETH to the Arbitrum treasury.


description string

A description of the proposal, outlining the intent to mint tokens to Optimism and send ETH to Arbitrum.

Returns

proposalId uint256

The ID of the newly created proposal on the hub chain.

Vote on the Proposal via Spoke

Once the proposal is created on the hub chain, stakeholders can cast their votes on the spoke chains. This snippet demonstrates how to connect to a spoke chain and cast a vote for the proposal. The voting power (weight) is calculated based on each stakeholder's token holdings on the spoke chain.

Key actions:

  • Connect to the SpokeVoteAggregator contract on the spoke chain. This contract aggregates votes from the spoke chains and relays them to the hub chain
  • Cast a vote in support of the proposal
// Connect to the SpokeVoteAggregator contract of the desired chain
SpokeVoteAggregator voteAggregator = SpokeVoteAggregator(VOTE_AGGREGATOR_ADDRESS);
// Cast a vote
uint8 support = 1; // 1 for supporting, 0 for opposing
uint256 weight = voteAggregator.castVote(proposalId, support);
Parameters

VOTE_AGGREGATOR_ADDRESS address

The address of the SpokeVoteAggregator contract on the spoke chain (Optimism or Arbitrum).


proposalId uint256

The ID of the proposal created on the hub chain, which is being voted on.


support uint8

The vote being cast (1 for supporting the proposal, 0 for opposing).

Returns

weight uint256

The weight of the vote, determined by the voter’s token holdings on the spoke chain.

Vote Aggregation

In the background process, votes cast on the spoke chains are aggregated and sent back to the hub chain for final tallying. This is typically handled off-chain by a "crank turner" service, which periodically queries the vote status and updates the hub chain.

Key actions:

  • Aggregate votes from different chains and submit them to the hub chain for tallying
// Aggregate votes sent to Hub (this would typically be done by a "crank turner" off-chain)
hubVotePool.crossChainVote(queryResponseRaw, signatures);
Parameters

queryResponseRaw bytes

The raw vote data from the spoke chains.


signatures bytes

Cryptographic signatures that verify the validity of the votes from the spoke chains.

Execute Proposal and Dispatch Cross-Chain Messages

After the proposal passes and the votes are tallied, the next step is to execute the proposal. The HubGovernor contract will dispatch the cross-chain messages to the spoke chains, where the respective treasuries will receive the tokens.

Key actions:

  • Execute the proposal after the voting period ends and the proposal passes
  • The execute function finalizes the proposal execution by dispatching the cross-chain governance actions. The descriptionHash ensures that the executed proposal matches the one that was voted on.
HubGovernor governor = HubGovernor(GOVERNOR_ADDRESS);
// Standard timelock execution
governor.execute(targets, values, calldatas, descriptionHash);
Parameters

governor HubGovernor

The HubGovernor contract instance.


targets address[]

An array containing the target addresses for the proposal’s transactions (in this case, the HUB_MESSAGE_DISPATCHER_ADDRESS for both).


values uint256[]

An array of values (in Wei) associated with each transaction (both are zero in this case).


calldatas bytes[]

The encoded transaction data to dispatch the governance actions (e.g., minting tokens and transferring ETH).


descriptionHash bytes32

A hash of the proposal’s description, used to verify the proposal before execution.

Returns

No direct return, but executing this function finalizes the cross-chain governance actions by dispatching the encoded messages via Wormhole to the spoke chains.

Once the proposal is executed, the encoded messages will be dispatched via Wormhole to the spoke chains, where the Optimism and Arbitrum treasuries will receive their respective funds.