You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
557 lines
18 KiB
557 lines
18 KiB
// SPDX-License-Identifier: MIT OR Apache-2.0
|
|
pragma solidity >=0.6.11;
|
|
pragma experimental ABIEncoderV2;
|
|
|
|
// ============ Internal Imports ============
|
|
import {Home} from "../Home.sol";
|
|
import {Version0} from "../Version0.sol";
|
|
import {XAppConnectionManager, TypeCasts} from "../XAppConnectionManager.sol";
|
|
import {IMessageRecipient} from "../../interfaces/IMessageRecipient.sol";
|
|
import {GovernanceMessage} from "./GovernanceMessage.sol";
|
|
// ============ External Imports ============
|
|
import {Initializable} from "@openzeppelin/contracts/proxy/Initializable.sol";
|
|
import {SafeMath} from "@openzeppelin/contracts/math/SafeMath.sol";
|
|
import {TypedMemView} from "@summa-tx/memview-sol/contracts/TypedMemView.sol";
|
|
|
|
contract GovernanceRouter is Version0, Initializable, IMessageRecipient {
|
|
// ============ Libraries ============
|
|
|
|
using SafeMath for uint256;
|
|
using TypedMemView for bytes;
|
|
using TypedMemView for bytes29;
|
|
using GovernanceMessage for bytes29;
|
|
|
|
// ============ Immutables ============
|
|
|
|
uint32 public immutable localDomain;
|
|
// number of seconds before recovery can be activated
|
|
uint256 public immutable recoveryTimelock;
|
|
|
|
// ============ Public Storage ============
|
|
|
|
// timestamp when recovery timelock expires; 0 if timelock has not been initiated
|
|
uint256 public recoveryActiveAt;
|
|
// the address of the recovery manager multisig
|
|
address public recoveryManager;
|
|
// the local entity empowered to call governance functions, set to 0x0 on non-Governor chains
|
|
address public governor;
|
|
// domain of Governor chain -- for accepting incoming messages from Governor
|
|
uint32 public governorDomain;
|
|
// xAppConnectionManager contract which stores Replica addresses
|
|
XAppConnectionManager public xAppConnectionManager;
|
|
// domain -> remote GovernanceRouter contract address
|
|
mapping(uint32 => bytes32) public routers;
|
|
// array of all domains with registered GovernanceRouter
|
|
uint32[] public domains;
|
|
|
|
// ============ Upgrade Gap ============
|
|
|
|
// gap for upgrade safety
|
|
uint256[43] private __GAP;
|
|
|
|
// ============ Events ============
|
|
|
|
/**
|
|
* @notice Emitted a remote GovernanceRouter address is added, removed, or changed
|
|
* @param domain the domain of the remote Router
|
|
* @param previousRouter the previously registered router; 0 if router is being added
|
|
* @param newRouter the new registered router; 0 if router is being removed
|
|
*/
|
|
event SetRouter(
|
|
uint32 indexed domain,
|
|
bytes32 previousRouter,
|
|
bytes32 newRouter
|
|
);
|
|
|
|
/**
|
|
* @notice Emitted when the Governor role is transferred
|
|
* @param previousGovernorDomain the domain of the previous Governor
|
|
* @param newGovernorDomain the domain of the new Governor
|
|
* @param previousGovernor the address of the previous Governor; 0 if the governor was remote
|
|
* @param newGovernor the address of the new Governor; 0 if the governor is remote
|
|
*/
|
|
event TransferGovernor(
|
|
uint32 previousGovernorDomain,
|
|
uint32 newGovernorDomain,
|
|
address indexed previousGovernor,
|
|
address indexed newGovernor
|
|
);
|
|
|
|
/**
|
|
* @notice Emitted when the RecoveryManager role is transferred
|
|
* @param previousRecoveryManager the address of the previous RecoveryManager
|
|
* @param newRecoveryManager the address of the new RecoveryManager
|
|
*/
|
|
event TransferRecoveryManager(
|
|
address indexed previousRecoveryManager,
|
|
address indexed newRecoveryManager
|
|
);
|
|
|
|
/**
|
|
* @notice Emitted when recovery state is initiated by the RecoveryManager
|
|
* @param recoveryManager the address of the current RecoveryManager who initiated the transition
|
|
* @param recoveryActiveAt the block at which recovery state will be active
|
|
*/
|
|
event InitiateRecovery(
|
|
address indexed recoveryManager,
|
|
uint256 recoveryActiveAt
|
|
);
|
|
|
|
/**
|
|
* @notice Emitted when recovery state is exited by the RecoveryManager
|
|
* @param recoveryManager the address of the current RecoveryManager who initiated the transition
|
|
*/
|
|
event ExitRecovery(address recoveryManager);
|
|
|
|
modifier typeAssert(bytes29 _view, GovernanceMessage.Types _type) {
|
|
_view.assertType(uint40(_type));
|
|
_;
|
|
}
|
|
|
|
// ============ Modifiers ============
|
|
|
|
modifier onlyReplica() {
|
|
require(xAppConnectionManager.isReplica(msg.sender), "!replica");
|
|
_;
|
|
}
|
|
|
|
modifier onlyGovernorRouter(uint32 _domain, bytes32 _address) {
|
|
require(_isGovernorRouter(_domain, _address), "!governorRouter");
|
|
_;
|
|
}
|
|
|
|
modifier onlyGovernor() {
|
|
require(msg.sender == governor, "! called by governor");
|
|
_;
|
|
}
|
|
|
|
modifier onlyRecoveryManager() {
|
|
require(msg.sender == recoveryManager, "! called by recovery manager");
|
|
_;
|
|
}
|
|
|
|
modifier onlyInRecovery() {
|
|
require(inRecovery(), "! in recovery");
|
|
_;
|
|
}
|
|
|
|
modifier onlyNotInRecovery() {
|
|
require(!inRecovery(), "in recovery");
|
|
_;
|
|
}
|
|
|
|
modifier onlyGovernorOrRecoveryManager() {
|
|
if (!inRecovery()) {
|
|
require(msg.sender == governor, "! called by governor");
|
|
} else {
|
|
require(
|
|
msg.sender == recoveryManager,
|
|
"! called by recovery manager"
|
|
);
|
|
}
|
|
_;
|
|
}
|
|
|
|
// ============ Constructor ============
|
|
|
|
constructor(uint32 _localDomain, uint256 _recoveryTimelock) {
|
|
localDomain = _localDomain;
|
|
recoveryTimelock = _recoveryTimelock;
|
|
}
|
|
|
|
// ============ Initializer ============
|
|
|
|
function initialize(
|
|
address _xAppConnectionManager,
|
|
address _recoveryManager
|
|
) public initializer {
|
|
// initialize governor
|
|
address _governorAddr = msg.sender;
|
|
bool _isLocalGovernor = true;
|
|
_transferGovernor(localDomain, _governorAddr, _isLocalGovernor);
|
|
// initialize recovery manager
|
|
recoveryManager = _recoveryManager;
|
|
// initialize XAppConnectionManager
|
|
setXAppConnectionManager(_xAppConnectionManager);
|
|
require(
|
|
xAppConnectionManager.localDomain() == localDomain,
|
|
"XAppConnectionManager bad domain"
|
|
);
|
|
}
|
|
|
|
// ============ External Functions ============
|
|
|
|
/**
|
|
* @notice Handle Optics messages
|
|
* For all non-Governor chains to handle messages
|
|
* sent from the Governor chain via Optics.
|
|
* Governor chain should never receive messages,
|
|
* because non-Governor chains are not able to send them
|
|
* @param _origin The domain (of the Governor Router)
|
|
* @param _sender The message sender (must be the Governor Router)
|
|
* @param _message The message
|
|
*/
|
|
function handle(
|
|
uint32 _origin,
|
|
bytes32 _sender,
|
|
bytes memory _message
|
|
) external override onlyReplica onlyGovernorRouter(_origin, _sender) {
|
|
bytes29 _msg = _message.ref(0);
|
|
if (_msg.isValidCall()) {
|
|
_handleCall(_msg.tryAsCall());
|
|
} else if (_msg.isValidTransferGovernor()) {
|
|
_handleTransferGovernor(_msg.tryAsTransferGovernor());
|
|
} else if (_msg.isValidSetRouter()) {
|
|
_handleSetRouter(_msg.tryAsSetRouter());
|
|
} else {
|
|
require(false, "!valid message type");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @notice Dispatch calls locally
|
|
* @param _calls The calls
|
|
*/
|
|
function callLocal(GovernanceMessage.Call[] calldata _calls)
|
|
external
|
|
onlyGovernorOrRecoveryManager
|
|
{
|
|
for (uint256 i = 0; i < _calls.length; i++) {
|
|
_dispatchCall(_calls[i]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @notice Dispatch calls on a remote chain via the remote GovernanceRouter
|
|
* @param _destination The domain of the remote chain
|
|
* @param _calls The calls
|
|
*/
|
|
function callRemote(
|
|
uint32 _destination,
|
|
GovernanceMessage.Call[] calldata _calls
|
|
) external onlyGovernor onlyNotInRecovery {
|
|
// ensure that destination chain has enrolled router
|
|
bytes32 _router = _mustHaveRouter(_destination);
|
|
// format call message
|
|
bytes memory _msg = GovernanceMessage.formatCalls(_calls);
|
|
// dispatch call message using Optics
|
|
Home(xAppConnectionManager.home()).dispatch(
|
|
_destination,
|
|
_router,
|
|
_msg
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @notice Transfer governorship
|
|
* @param _newDomain The domain of the new governor
|
|
* @param _newGovernor The address of the new governor
|
|
*/
|
|
function transferGovernor(uint32 _newDomain, address _newGovernor)
|
|
external
|
|
onlyGovernor
|
|
onlyNotInRecovery
|
|
{
|
|
bool _isLocalGovernor = _isLocalDomain(_newDomain);
|
|
// transfer the governor locally
|
|
_transferGovernor(_newDomain, _newGovernor, _isLocalGovernor);
|
|
// if the governor domain is local, we only need to change the governor address locally
|
|
// no need to message remote routers; they should already have the same domain set and governor = bytes32(0)
|
|
if (_isLocalGovernor) {
|
|
return;
|
|
}
|
|
// format transfer governor message
|
|
bytes memory _transferGovernorMessage = GovernanceMessage
|
|
.formatTransferGovernor(
|
|
_newDomain,
|
|
TypeCasts.addressToBytes32(_newGovernor)
|
|
);
|
|
// send transfer governor message to all remote routers
|
|
// note: this assumes that the Router is on the global GovernorDomain;
|
|
// this causes a process error when relinquishing governorship
|
|
// on a newly deployed domain which is not the GovernorDomain
|
|
_sendToAllRemoteRouters(_transferGovernorMessage);
|
|
}
|
|
|
|
/**
|
|
* @notice Transfer recovery manager role
|
|
* @dev callable by the recoveryManager at any time to transfer the role
|
|
* @param _newRecoveryManager The address of the new recovery manager
|
|
*/
|
|
function transferRecoveryManager(address _newRecoveryManager)
|
|
external
|
|
onlyRecoveryManager
|
|
{
|
|
emit TransferRecoveryManager(recoveryManager, _newRecoveryManager);
|
|
recoveryManager = _newRecoveryManager;
|
|
}
|
|
|
|
/**
|
|
* @notice Set the router address for a given domain and
|
|
* dispatch the change to all remote routers
|
|
* @param _domain The domain
|
|
* @param _router The address of the new router
|
|
*/
|
|
function setRouter(uint32 _domain, bytes32 _router)
|
|
external
|
|
onlyGovernor
|
|
onlyNotInRecovery
|
|
{
|
|
// set the router locally
|
|
_setRouter(_domain, _router);
|
|
// format message to set the router on all remote routers
|
|
bytes memory _setRouterMessage = GovernanceMessage.formatSetRouter(
|
|
_domain,
|
|
_router
|
|
);
|
|
|
|
_sendToAllRemoteRouters(_setRouterMessage);
|
|
}
|
|
|
|
/**
|
|
* @notice Set the router address *locally only*
|
|
* for the deployer to setup the router mapping locally
|
|
* before transferring governorship to the "true" governor
|
|
* @dev External helper for contract setup
|
|
* @param _domain The domain
|
|
* @param _router The new router
|
|
*/
|
|
function setRouterLocal(uint32 _domain, bytes32 _router)
|
|
external
|
|
onlyGovernorOrRecoveryManager
|
|
{
|
|
// set the router locally
|
|
_setRouter(_domain, _router);
|
|
}
|
|
|
|
/**
|
|
* @notice Set the address of the XAppConnectionManager
|
|
* @dev Domain/address validation helper
|
|
* @param _xAppConnectionManager The address of the new xAppConnectionManager
|
|
*/
|
|
function setXAppConnectionManager(address _xAppConnectionManager)
|
|
public
|
|
onlyGovernorOrRecoveryManager
|
|
{
|
|
xAppConnectionManager = XAppConnectionManager(_xAppConnectionManager);
|
|
}
|
|
|
|
/**
|
|
* @notice Initiate the recovery timelock
|
|
* @dev callable by the recovery manager
|
|
*/
|
|
function initiateRecoveryTimelock()
|
|
external
|
|
onlyNotInRecovery
|
|
onlyRecoveryManager
|
|
{
|
|
require(recoveryActiveAt == 0, "recovery already initiated");
|
|
// set the time that recovery will be active
|
|
recoveryActiveAt = block.timestamp.add(recoveryTimelock);
|
|
emit InitiateRecovery(recoveryManager, recoveryActiveAt);
|
|
}
|
|
|
|
/**
|
|
* @notice Exit recovery mode
|
|
* @dev callable by the recovery manager to end recovery mode
|
|
*/
|
|
function exitRecovery() external onlyRecoveryManager {
|
|
require(recoveryActiveAt != 0, "recovery not initiated");
|
|
delete recoveryActiveAt;
|
|
emit ExitRecovery(recoveryManager);
|
|
}
|
|
|
|
// ============ Public Functions ============
|
|
|
|
/**
|
|
* @notice Check if the contract is in recovery mode currently
|
|
* @return TRUE iff the contract is actively in recovery mode currently
|
|
*/
|
|
function inRecovery() public view returns (bool) {
|
|
uint256 _recoveryActiveAt = recoveryActiveAt;
|
|
bool _recoveryInitiated = _recoveryActiveAt != 0;
|
|
bool _recoveryActive = _recoveryActiveAt <= block.timestamp;
|
|
return _recoveryInitiated && _recoveryActive;
|
|
}
|
|
|
|
// ============ Internal Functions ============
|
|
|
|
/**
|
|
* @notice Handle message dispatching calls locally
|
|
* @param _msg The message
|
|
*/
|
|
function _handleCall(bytes29 _msg)
|
|
internal
|
|
typeAssert(_msg, GovernanceMessage.Types.Call)
|
|
{
|
|
GovernanceMessage.Call[] memory _calls = _msg.getCalls();
|
|
for (uint256 i = 0; i < _calls.length; i++) {
|
|
_dispatchCall(_calls[i]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @notice Handle message transferring governorship to a new Governor
|
|
* @param _msg The message
|
|
*/
|
|
function _handleTransferGovernor(bytes29 _msg)
|
|
internal
|
|
typeAssert(_msg, GovernanceMessage.Types.TransferGovernor)
|
|
{
|
|
uint32 _newDomain = _msg.domain();
|
|
address _newGovernor = TypeCasts.bytes32ToAddress(_msg.governor());
|
|
bool _isLocalGovernor = _isLocalDomain(_newDomain);
|
|
_transferGovernor(_newDomain, _newGovernor, _isLocalGovernor);
|
|
}
|
|
|
|
/**
|
|
* @notice Handle message setting the router address for a given domain
|
|
* @param _msg The message
|
|
*/
|
|
function _handleSetRouter(bytes29 _msg)
|
|
internal
|
|
typeAssert(_msg, GovernanceMessage.Types.SetRouter)
|
|
{
|
|
uint32 _domain = _msg.domain();
|
|
bytes32 _router = _msg.router();
|
|
_setRouter(_domain, _router);
|
|
}
|
|
|
|
/**
|
|
* @notice Dispatch message to all remote routers
|
|
* @param _msg The message
|
|
*/
|
|
function _sendToAllRemoteRouters(bytes memory _msg) internal {
|
|
Home _home = Home(xAppConnectionManager.home());
|
|
|
|
for (uint256 i = 0; i < domains.length; i++) {
|
|
if (domains[i] != uint32(0)) {
|
|
_home.dispatch(domains[i], routers[domains[i]], _msg);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @notice Dispatch call locally
|
|
* @param _call The call
|
|
* @return _ret
|
|
*/
|
|
function _dispatchCall(GovernanceMessage.Call memory _call)
|
|
internal
|
|
returns (bytes memory _ret)
|
|
{
|
|
address _toContract = TypeCasts.bytes32ToAddress(_call.to);
|
|
// attempt to dispatch using low-level call
|
|
bool _success;
|
|
(_success, _ret) = _toContract.call(_call.data);
|
|
// revert if the call failed
|
|
require(_success, "call failed");
|
|
}
|
|
|
|
/**
|
|
* @notice Transfer governorship within this contract's state
|
|
* @param _newDomain The domain of the new governor
|
|
* @param _newGovernor The address of the new governor
|
|
* @param _isLocalGovernor True if the newDomain is the localDomain
|
|
*/
|
|
function _transferGovernor(
|
|
uint32 _newDomain,
|
|
address _newGovernor,
|
|
bool _isLocalGovernor
|
|
) internal {
|
|
// require that the governor domain has a valid router
|
|
if (!_isLocalGovernor) {
|
|
_mustHaveRouter(_newDomain);
|
|
}
|
|
// Governor is 0x0 unless the governor is local
|
|
address _newGov = _isLocalGovernor ? _newGovernor : address(0);
|
|
// emit event before updating state variables
|
|
emit TransferGovernor(governorDomain, _newDomain, governor, _newGov);
|
|
// update state
|
|
governorDomain = _newDomain;
|
|
governor = _newGov;
|
|
}
|
|
|
|
/**
|
|
* @notice Set the router for a given domain
|
|
* @param _domain The domain
|
|
* @param _newRouter The new router
|
|
*/
|
|
function _setRouter(uint32 _domain, bytes32 _newRouter) internal {
|
|
bytes32 _previousRouter = routers[_domain];
|
|
// emit event at beginning in case return after remove
|
|
emit SetRouter(_domain, _previousRouter, _newRouter);
|
|
// if the router is being removed, remove the domain
|
|
if (_newRouter == bytes32(0)) {
|
|
_removeDomain(_domain);
|
|
return;
|
|
}
|
|
// if the router is being added, add the domain
|
|
if (_previousRouter == bytes32(0)) {
|
|
_addDomain(_domain);
|
|
}
|
|
// update state with new router
|
|
routers[_domain] = _newRouter;
|
|
}
|
|
|
|
/**
|
|
* @notice Add a domain that has a router
|
|
* @param _domain The domain
|
|
*/
|
|
function _addDomain(uint32 _domain) internal {
|
|
domains.push(_domain);
|
|
}
|
|
|
|
/**
|
|
* @notice Remove a domain and its associated router
|
|
* @param _domain The domain
|
|
*/
|
|
function _removeDomain(uint32 _domain) internal {
|
|
delete routers[_domain];
|
|
// find the index of the domain to remove & delete it from domains[]
|
|
for (uint256 i = 0; i < domains.length; i++) {
|
|
if (domains[i] == _domain) {
|
|
delete domains[i];
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @notice Determine if a given domain and address is the Governor Router
|
|
* @param _domain The domain
|
|
* @param _address The address of the domain's router
|
|
* @return _ret True if the given domain/address is the
|
|
* Governor Router.
|
|
*/
|
|
function _isGovernorRouter(uint32 _domain, bytes32 _address)
|
|
internal
|
|
view
|
|
returns (bool)
|
|
{
|
|
return _domain == governorDomain && _address == routers[_domain];
|
|
}
|
|
|
|
/**
|
|
* @notice Determine if a given domain is the local domain
|
|
* @param _domain The domain
|
|
* @return _ret - True if the given domain is the local domain
|
|
*/
|
|
function _isLocalDomain(uint32 _domain) internal view returns (bool) {
|
|
return _domain == localDomain;
|
|
}
|
|
|
|
/**
|
|
* @notice Require that a domain has a router and returns the router
|
|
* @param _domain The domain
|
|
* @return _router - The domain's router
|
|
*/
|
|
function _mustHaveRouter(uint32 _domain)
|
|
internal
|
|
view
|
|
returns (bytes32 _router)
|
|
{
|
|
_router = routers[_domain];
|
|
require(_router != bytes32(0), "!router");
|
|
}
|
|
}
|
|
|