Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/rare-bushes-march.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`GovernorCrosschain`: Governor module that facilitates the execute of crosschain operations through CrosschainRemoteExecutors and ERC-7786 gateways.
90 changes: 90 additions & 0 deletions contracts/crosschain/CrosschainRemoteController.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.26;

import {IERC7786GatewaySource} from "../interfaces/draft-IERC7786.sol";
import {InteroperableAddress} from "../utils/draft-InteroperableAddress.sol";
import {Bytes} from "../utils/Bytes.sol";
import {ERC7786Recipient} from "./ERC7786Recipient.sol";

import {Mode} from "../account/utils/draft-ERC7579Utils.sol";

abstract contract CrosschainRemoteController {
using Bytes for bytes;
using InteroperableAddress for bytes;

struct ExecutorDetails {
address gateway;
bytes executor;
}
mapping(bytes chain => ExecutorDetails) private _remoteExecutors;

/**
* @dev Emitted when a new remote executor is registered.
*
* Note: the `executor` argument is a full InteroperableAddress (chain ref + address).
*/
event RemoteExecutorRegistered(address gateway, bytes executor);

/**
* @dev Reverted when trying to register a link for a chain that is already registered.
*
* Note: the `chain` argument is a "chain-only" InteroperableAddress (empty address).
*/
error RemoteExecutorAlreadyRegistered(bytes chain);

/**
* @dev Reverted when trying to send a crosschain instruction to a chain with no registered executor.
*/
error NoExecutorRegistered(bytes chain);

/// @dev Send crosschain instruction to a the canonical remote executor of a given chain.
function _crosschainExecute(bytes memory chain, Mode mode, bytes memory executionCalldata) internal virtual {
(address gateway, bytes memory executor) = getRemoteExecutor(chain);
require(gateway != address(0), NoExecutorRegistered(chain));
_crosschainExecute(gateway, executor, mode, executionCalldata);
}

/// @dev Send crosschain instruction to an arbitrary remote executor via an arbitrary ERC-7786 gateway.
function _crosschainExecute(
address gateway,
bytes memory executor,
Mode mode,
bytes memory executionCalldata
) internal virtual {
IERC7786GatewaySource(gateway).sendMessage(executor, abi.encodePacked(mode, executionCalldata), new bytes[](0));
}

/**
* @dev Returns the ERC-7786 gateway used for sending and receiving cross-chain messages to a given chain.
*
* Note: The `chain` parameter is a "chain-only" InteroperableAddress (empty address) and the `counterpart` returns
* the full InteroperableAddress (chain ref + address) that is on `chain`.
*/
function getRemoteExecutor(
bytes memory chain
) public view virtual returns (address gateway, bytes memory executor) {
ExecutorDetails storage self = _remoteExecutors[chain];
return (self.gateway, self.executor);
}

/**
* @dev Internal setter to change the ERC-7786 gateway and executor for a given chain. Called at construction.
*
* Note: The `executor` parameter is the full InteroperableAddress (chain ref + address).
*/
function _registerRemoteExecutor(address gateway, bytes memory executor, bool allowOverride) internal virtual {
// Sanity check, this should revert if gateway is not an ERC-7786 implementation. Note that since
// supportsAttribute returns data, an EOA would fail that test (nothing returned).
IERC7786GatewaySource(gateway).supportsAttribute(bytes4(0));

(bytes2 chainType, bytes memory chainReference, ) = executor.parseV1();
bytes memory chain = InteroperableAddress.formatV1(chainType, chainReference, hex"");
if (allowOverride || _remoteExecutors[chain].gateway == address(0)) {
_remoteExecutors[chain] = ExecutorDetails(gateway, executor);
emit RemoteExecutorRegistered(gateway, executor);
} else {
revert RemoteExecutorAlreadyRegistered(chain);
}
}
}
75 changes: 75 additions & 0 deletions contracts/crosschain/CrosschainRemoteExecutor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.26;

import {IERC7786GatewaySource} from "../interfaces/draft-IERC7786.sol";
import {ERC7786Recipient} from "./ERC7786Recipient.sol";
import {ERC7579Utils, Mode, CallType, ExecType} from "../account/utils/draft-ERC7579Utils.sol";
import {Bytes} from "../utils/Bytes.sol";

contract CrosschainRemoteExecutor is ERC7786Recipient {
using Bytes for bytes;
using ERC7579Utils for *;

address private _gateway;
bytes private _controller;

event CrosschainControllerSet(address gateway, bytes controller);
error AccessRestricted();

constructor(address initialGateway, bytes memory initialController) {
_setup(initialGateway, initialController);
}

function gateway() public view virtual returns (address) {
return _gateway;
}

function controller() public view virtual returns (bytes memory) {
return _controller;
}

function reconfigure(address newGateway, bytes memory newController) public virtual {
require(msg.sender == address(this), AccessRestricted());
_setup(newGateway, newController);
}

function _setup(address gateway_, bytes memory controller_) internal virtual {
// Sanity check, this should revert if gateway is not an ERC-7786 implementation. Note that since
// supportsAttribute returns data, an EOA would fail that test (nothing returned).
IERC7786GatewaySource(gateway_).supportsAttribute(bytes4(0));

_gateway = gateway_;
_controller = controller_;

emit CrosschainControllerSet(gateway_, controller_);
}

/// @inheritdoc ERC7786Recipient
function _isAuthorizedGateway(
address instance,
bytes calldata sender
) internal view virtual override returns (bool) {
return gateway() == instance && controller().equal(sender);
}

/// @inheritdoc ERC7786Recipient
function _processMessage(
address /*gateway*/,
bytes32 /*receiveId*/,
bytes calldata /*sender*/,
bytes calldata payload
) internal virtual override {
// split payload
(CallType callType, ExecType execType, , ) = Mode.wrap(bytes32(payload[0x00:0x20])).decodeMode();
bytes calldata executionCalldata = payload[0x20:];

if (callType == ERC7579Utils.CALLTYPE_SINGLE) {
executionCalldata.execSingle(execType);
} else if (callType == ERC7579Utils.CALLTYPE_BATCH) {
executionCalldata.execBatch(execType);
} else if (callType == ERC7579Utils.CALLTYPE_DELEGATECALL) {
executionCalldata.execDelegateCall(execType);
} else revert ERC7579Utils.ERC7579UnsupportedCallType(callType);
}
}
22 changes: 13 additions & 9 deletions contracts/governance/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,19 @@ Timelock extensions add a delay for governance decisions to be executed. The wor

Other extensions can customize the behavior or interface in multiple ways.

* {GovernorStorage}: Stores the proposal details onchain and provides enumerability of the proposals. This can be useful for some L2 chains where storage is cheap compared to calldata.
* {GovernorCrosschain}: Helps with the execution of crosschain operation using {CrosschainRemoteExector} and ERC-7786 gateways.

* {GovernorSettings}: Manages some of the settings (voting delay, voting period duration, and proposal threshold) in a way that can be updated through a governance proposal, without requiring an upgrade.
* {GovernorNoncesKeyed}: An extension of {Governor} with support for keyed nonces in addition to traditional nonces when voting by signature.

* {GovernorPreventLateQuorum}: Ensures there is a minimum voting period after quorum is reached as a security protection against large voters.

* {GovernorProposalGuardian}: Adds a proposal guardian that can cancel proposals at any stage in their lifecycle--this permission is passed on to the proposers if the guardian is not set.

* {GovernorSuperQuorum}: Extension of {Governor} with a super quorum. Proposals that meet the super quorum (and have a majority of for votes) advance to the `Succeeded` state before the proposal deadline.
* {GovernorSettings}: Manages some of the settings (voting delay, voting period duration, and proposal threshold) in a way that can be updated through a governance proposal, without requiring an upgrade.

* {GovernorNoncesKeyed}: An extension of {Governor} with support for keyed nonces in addition to traditional nonces when voting by signature.
* {GovernorStorage}: Stores the proposal details onchain and provides enumerability of the proposals. This can be useful for some L2 chains where storage is cheap compared to calldata.

* {GovernorSuperQuorum}: Extension of {Governor} with a super quorum. Proposals that meet the super quorum (and have a majority of for votes) advance to the `Succeeded` state before the proposal deadline.

In addition to modules and extensions, the core contract requires a few virtual functions to be implemented to your particular specifications:

Expand Down Expand Up @@ -92,17 +94,19 @@ NOTE: Functions of the `Governor` contract do not include access control. If you

{{GovernorTimelockCompound}}

{{GovernorSettings}}
{{GovernorCrosschain}}

{{GovernorPreventLateQuorum}}
{{GovernorNoncesKeyed}}

{{GovernorStorage}}
{{GovernorPreventLateQuorum}}

{{GovernorProposalGuardian}}

{{GovernorSuperQuorum}}
{{GovernorSettings}}

{{GovernorNoncesKeyed}}
{{GovernorStorage}}

{{GovernorSuperQuorum}}

== Utils

Expand Down
34 changes: 34 additions & 0 deletions contracts/governance/extensions/GovernorCrosschain.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.24;

import {Governor} from "../Governor.sol";
import {Mode} from "../../account/utils/draft-ERC7579Utils.sol";
import {CrosschainRemoteController} from "../../crosschain/CrosschainRemoteController.sol";

/// @dev Extension of {Governor} for cross-chain governance through ERC-7786 gateways and {CrosschainRemoteExecutors}.
abstract contract GovernorCrosschain is Governor, CrosschainRemoteController {
/// @dev Send crosschain instruction to a the canonical remote executor of a given chain.
function relayCrosschain(
bytes memory chain,
Mode mode,
bytes memory executionCalldata
) public virtual onlyGovernance {
_crosschainExecute(chain, mode, executionCalldata);
}

/// @dev Send crosschain instruction to an arbitrary remote executor via an arbitrary ERC-7786 gateway.
function relayCrosschain(
address gateway,
bytes memory executor,
Mode mode,
bytes memory executionCalldata
) public virtual onlyGovernance {
_crosschainExecute(gateway, executor, mode, executionCalldata);
}

/// @dev Register the canonical remote executor for a given chain.
function registerRemoteExecutor(address gateway, bytes memory executor) public virtual onlyGovernance {
_registerRemoteExecutor(gateway, executor, true);
}
}
20 changes: 20 additions & 0 deletions contracts/mocks/governance/GovernorCrosschain.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.24;

import {Governor} from "../../governance/Governor.sol";
import {GovernorSettings} from "../../governance/extensions/GovernorSettings.sol";
import {GovernorCountingSimple} from "../../governance/extensions/GovernorCountingSimple.sol";
import {GovernorVotesQuorumFraction} from "../../governance/extensions/GovernorVotesQuorumFraction.sol";
import {GovernorCrosschain} from "../../governance/extensions/GovernorCrosschain.sol";

abstract contract GovernorCrosschainMock is
GovernorSettings,
GovernorVotesQuorumFraction,
GovernorCountingSimple,
GovernorCrosschain
{
function proposalThreshold() public view override(Governor, GovernorSettings) returns (uint256) {
return super.proposalThreshold();
}
}
Loading
Loading