Skip to content

L2 Governance #1991

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 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
498 changes: 498 additions & 0 deletions contracts/contracts/governance/L2Governance.sol

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions contracts/contracts/governance/L2Governor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import { TimelockController } from "@openzeppelin/contracts/governance/TimelockController.sol";

contract L2Governor is TimelockController {
constructor(address[] memory proposers, address[] memory executors)
TimelockController(86400, proposers, executors)
{}
}
279 changes: 279 additions & 0 deletions contracts/contracts/governance/MainnetGovernanceExecutor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import { Governable } from "./Governable.sol";
import { QUEUE_PROPOSAL_COMMAND, CANCEL_PROPOSAL_COMMAND } from "./L2Governance.sol";
import { Initializable } from "../utils/Initializable.sol";

import { ARBITRUM_ONE_SELECTOR } from "../utils/CCIPChainSelectors.sol";

import { Client } from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import { IRouterClient } from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";

contract MainnetGovernanceExecutor is Governable, Initializable {
/***************************************
Events
****************************************/
/**
* @dev Emitted whenever a command is forwarded to CCIP Router
*/
event CommandSentToCCIPRouter(
uint64 indexed chainSelector,
bytes32 messageId,
bytes2 commandSelector,
uint256 proposalId
);
/**
* @dev Emitted when a Chain Config is added
*/
event ChainConfigAdded(
uint64 indexed chainSelector,
address indexed l2Governance
);
/**
* @dev Emitted when a Chain Config is removed
*/
event ChainConfigRemoved(uint64 indexed chainSelector);

/***************************************
Errors
****************************************/
error UnsupportedChain(uint64 chainSelector);
error InsufficientBalanceForFees(uint256 feesRequired);
error DuplicateChainConfig(uint64 chainSelector);
error InvalidGovernanceCommand(bytes2 command);
error InvalidInitializationArgLength();
error InvalidGovernanceAddress();

/***************************************
Storage
****************************************/
address public immutable ccipRouter;

struct ChainConfig {
bool isSupported;
address l2Governance;
}
/**
* @dev All supported chains
*/
mapping(uint64 => ChainConfig) public chainConfig;

constructor(address _ccipRouter) {
ccipRouter = _ccipRouter;
}

function initialize(
uint64[] calldata chainSelectors,
address[] calldata l2Governances
) public initializer {
if (chainSelectors.length != l2Governances.length) {
revert InvalidInitializationArgLength();
}

for (uint256 i = 0; i < chainSelectors.length; ++i) {
_addChainConfig(chainSelectors[i], l2Governances[i]);
}
}

/***************************************
CCIP
****************************************/
/**
* @dev Send a command to queue/cancel a L2 Proposal through CCIP Router
* @param commandSelector Command to send
* @param chainSelector Destination chain
* @param proposalId L2 Proposal ID
* @param maxGasLimit Max Gas Limit to use
*/
function _sendCommandToL2(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this necessary since you also have queueL2Proposal and cancelL2Proposal and for now L2Governance only accepts these two message types?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume you mean the sendCommandToL2 and not this internal function? I can get rid of that, yeah

Copy link

@pandadefi pandadefi Feb 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, commented at the wrong place.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have removed that sendCommandToL2. Also, removed the InvalidGovernanceCommand check and error since those are not user-input now

bytes2 commandSelector,
uint64 chainSelector,
uint256 proposalId,
uint256 maxGasLimit
) internal {
// Ensure it's a valid command
if (
commandSelector != QUEUE_PROPOSAL_COMMAND &&
commandSelector != CANCEL_PROPOSAL_COMMAND
) {
revert InvalidGovernanceCommand(commandSelector);
}

ChainConfig memory config = chainConfig[chainSelector];

// Ensure it's a supported chain
if (!config.isSupported) {
revert UnsupportedChain(chainSelector);
}

// Build the command data
bytes memory data = abi.encode(
// Command Selector
commandSelector,
// Encoded Command Data
abi.encode(proposalId)
);

bytes memory extraArgs = hex"";

// Set gas limit if needed
if (maxGasLimit > 0) {
extraArgs = Client._argsToBytes(
// Set gas limit
Client.EVMExtraArgsV1({ gasLimit: maxGasLimit })
);
}

// Build the message
Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
receiver: abi.encode(config.l2Governance),
data: data,
tokenAmounts: new Client.EVMTokenAmount[](0),
extraArgs: extraArgs,
feeToken: address(0)
});

IRouterClient router = IRouterClient(ccipRouter);

// Compute fees
uint256 fees = router.getFee(chainSelector, message);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might want to add a view function that returns the result of getFee to query before sending a command to make sure you have enough ETH in the contract for execution.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have added a getCCIPFees method that takes the same params as _sendCommandToL2 but returns the fee amount


// Ensure the contract has enough balance to pay the fees
if (fees > address(this).balance) {
revert InsufficientBalanceForFees(fees);
}

// Forward to CCIP Router
// slither-disable-next-line arbitrary-send-eth
bytes32 messageId = router.ccipSend{ value: fees }(
chainSelector,
message
);

emit CommandSentToCCIPRouter(
chainSelector,
messageId,
commandSelector,
proposalId
);
}

/**
* @dev Send a command to queue/cancel a L2 Proposal through CCIP Router.
* Has to come through Governance
* @param commandSelector Command to send
* @param chainSelector Destination chain
* @param proposalId L2 Proposal ID
* @param maxGasLimit Max Gas Limit to use
*/
function sendCommandToL2(
bytes2 commandSelector,
uint64 chainSelector,
uint256 proposalId,
uint256 maxGasLimit
) external onlyGovernor {
_sendCommandToL2(
commandSelector,
chainSelector,
proposalId,
maxGasLimit
);
}

/**
* @dev Send a command to queue a L2 Proposal through CCIP Router.
* Has to come through Governance
* @param chainSelector Destination chain
* @param proposalId L2 Proposal ID
* @param maxGasLimit Max Gas Limit to use
*/
function queueL2Proposal(
uint64 chainSelector,
uint256 proposalId,
uint256 maxGasLimit
) external onlyGovernor {
_sendCommandToL2(
QUEUE_PROPOSAL_COMMAND,
chainSelector,
proposalId,
maxGasLimit
);
}

/**
* @dev Send a command to cancel a L2 Proposal through CCIP Router.
* Has to come through Governance
* @param chainSelector Destination chain
* @param proposalId L2 Proposal ID
* @param maxGasLimit Max Gas Limit to use
*/
function cancelL2Proposal(
uint64 chainSelector,
uint256 proposalId,
uint256 maxGasLimit
) external onlyGovernor {
_sendCommandToL2(
CANCEL_PROPOSAL_COMMAND,
chainSelector,
proposalId,
maxGasLimit
);
}

/***************************************
Configuration
****************************************/
/**
* @dev Add a L2 Chain to forward commands to.
* Has to go through Governance
* @param chainSelector New timelock address
* @param l2Governance New timelock address
*/
function addChainConfig(uint64 chainSelector, address l2Governance)
external
onlyGovernor
{
_addChainConfig(chainSelector, l2Governance);
}

function _addChainConfig(uint64 chainSelector, address l2Governance)
internal
{
if (chainConfig[chainSelector].isSupported) {
revert DuplicateChainConfig(chainSelector);
}

if (l2Governance == address(0)) {
revert InvalidGovernanceAddress();
}

chainConfig[chainSelector] = ChainConfig({
isSupported: true,
l2Governance: l2Governance
});

emit ChainConfigAdded(chainSelector, l2Governance);
}

/**
* @dev Remove a supported L2 chain.
* Has to go through Governance
* @param chainSelector New timelock address
*/
function removeChainConfig(uint64 chainSelector) external onlyGovernor {
if (!chainConfig[chainSelector].isSupported) {
revert UnsupportedChain(chainSelector);
}

chainConfig[chainSelector] = ChainConfig({
isSupported: false,
l2Governance: address(0)
});

emit ChainConfigRemoved(chainSelector);
}

// Accept ETH
receive() external payable {}
}
16 changes: 16 additions & 0 deletions contracts/contracts/proxies/Proxies.sol
Original file line number Diff line number Diff line change
Expand Up @@ -202,3 +202,19 @@ contract OETHBuybackProxy is InitializeGovernedUpgradeabilityProxy {
contract BridgedWOETHProxy is InitializeGovernedUpgradeabilityProxy {

}

/**
* @notice L2GovernanceProxy delegates calls to L2Governance implementation
*/
contract L2GovernanceProxy is InitializeGovernedUpgradeabilityProxy {

}

/**
* @notice MainnetGovernanceExecutorProxy delegates calls to MainnetGovernanceExecutor implementation
*/
contract MainnetGovernanceExecutorProxy is
InitializeGovernedUpgradeabilityProxy
{

}
6 changes: 6 additions & 0 deletions contracts/contracts/utils/CCIPChainSelectors.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// Ref: https://docs.chain.link/ccip/supported-networks/v1_2_0/mainnet#arbitrum-mainnet
uint64 constant MAINNET_SELECTOR = 5009297550715157269;
uint64 constant ARBITRUM_ONE_SELECTOR = 4949039107694359620;
31 changes: 31 additions & 0 deletions contracts/deploy/085_deploy_l2_governance_proxies.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const { isFork } = require("../test/helpers");
const { deployWithConfirmation } = require("../utils/deploy");

const deployName = "085_deploy_l2_governance_proxies";

const main = async (hre) => {
console.log(`Running ${deployName} deployment on ${hre.network.name}...`);

if (hre.network.name == "arbitrumOne") {
// Deploy L2 Governor on Arbitrum One
const l2GovernanceProxy = await deployWithConfirmation("L2GovernanceProxy");
console.log("L2GovernanceProxy address:", l2GovernanceProxy.address);
} else if (hre.network.name == "mainnet") {
// Deploy Governance Executor on Mainnet
const mainnetGovernanceExecutorProxy = await deployWithConfirmation(
"MainnetGovernanceExecutorProxy"
);
console.log(
"MainnetGovernanceExecutorProxy address:",
mainnetGovernanceExecutorProxy.address
);
}

console.log(`${deployName} deploy done.`);
};

main.id = deployName;
main.skip = !(isFork || ["arbitrumOne", "mainnet"].includes(hre.network.name));
main.tags = ["arbitrum", "mainnet"];

module.exports = main;
Loading