Skip to content
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

L2 Governance #1991

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
532 changes: 532 additions & 0 deletions contracts/contracts/governance/L2Governance.sol

Large diffs are not rendered by default.

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

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

contract L2Governor is TimelockController {
constructor(
uint256 minDelay,
address[] memory proposers,
address[] memory executors
) TimelockController(minDelay, proposers, executors) {}
}
295 changes: 295 additions & 0 deletions contracts/contracts/governance/MainnetGovernanceExecutor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
// 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 indexed 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 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(
shahthepro marked this conversation as resolved.
Show resolved Hide resolved
uint64[] calldata chainSelectors,
address[] calldata l2Governances
) external initializer {
uint256 len = chainSelectors.length;
if (len != l2Governances.length) {
revert InvalidInitializationArgLength();
}

for (uint256 i = 0; i < len; ++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 {
// Build the message
Client.EVM2AnyMessage memory message = _buildCCIPMessage(
commandSelector,
chainSelector,
proposalId,
maxGasLimit
);

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 Returns the CCIP fees that the contract needs to execute the command
* @param commandSelector Command to send
* @param chainSelector Destination chain
* @param proposalId L2 Proposal ID
* @param maxGasLimit Max Gas Limit to use
*/
function getCCIPFees(
bytes2 commandSelector,
uint64 chainSelector,
uint256 proposalId,
uint256 maxGasLimit
) external view returns (uint256) {
// Build the message
Client.EVM2AnyMessage memory message = _buildCCIPMessage(
commandSelector,
chainSelector,
proposalId,
maxGasLimit
);

return IRouterClient(ccipRouter).getFee(chainSelector, message);
}

/**
* @dev Builds the CCIP message for the given command
* @param commandSelector Command to send
* @param chainSelector Destination chain
* @param proposalId L2 Proposal ID
* @param maxGasLimit Max Gas Limit to use
*/
function _buildCCIPMessage(
bytes2 commandSelector,
uint64 chainSelector,
uint256 proposalId,
uint256 maxGasLimit
) internal view returns (Client.EVM2AnyMessage memory message) {
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
message = Client.EVM2AnyMessage({
receiver: abi.encode(config.l2Governance),
data: data,
tokenAmounts: new Client.EVMTokenAmount[](0),
extraArgs: extraArgs,
feeToken: address(0)
});
}

/**
* @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 payable 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 payable 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 {}
}
6 changes: 6 additions & 0 deletions contracts/contracts/interfaces/ICCIPRouter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface ICCIPRouter {
function getArmProxy() external view returns (address);
}
41 changes: 41 additions & 0 deletions contracts/contracts/interfaces/ITimelockController.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface ITimelockController {
function isOperation(bytes32 id) external view returns (bool);

function isOperationPending(bytes32 id) external view returns (bool);

function isOperationReady(bytes32 id) external view returns (bool);

function isOperationDone(bytes32 id) external view returns (bool);

function getMinDelay() external view returns (uint256 duration);

function hashOperationBatch(
address[] calldata targets,
uint256[] calldata values,
bytes[] calldata datas,
bytes32 predecessor,
bytes32 salt
) external pure returns (bytes32 hash);

function scheduleBatch(
address[] calldata targets,
uint256[] calldata values,
bytes[] calldata datas,
bytes32 predecessor,
bytes32 salt,
uint256 delay
) external;

function executeBatch(
address[] calldata targets,
uint256[] calldata values,
bytes[] calldata datas,
bytes32 predecessor,
bytes32 salt
) external payable;

function cancel(bytes32 id) external;
}
Loading
Loading