Skip to content

Commit

Permalink
Merge pull request #4001 from connext/conductor
Browse files Browse the repository at this point in the history
Conductor
  • Loading branch information
LayneHaber authored Apr 22, 2023
2 parents 0125f0b + af1faa6 commit 387e177
Show file tree
Hide file tree
Showing 9 changed files with 1,817 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
[
"constructor(address)",
"error Conductor_dequeue__notQueued(bytes32)",
"error Conductor_execute__callFailed()",
"error Conductor_execute__cannotBypass(bytes4,address)",
"error Conductor_execute__notElapsed(bytes32)",
"error Conductor_queue__alreadyQueued(bytes32)",
"error Conductor_renounceOwnership__prohibited()",
"error ProposedOwnable__onlyOwner_notOwner()",
"error ProposedOwnable__onlyProposed_notProposedOwner()",
"error ProposedOwnable__ownershipDelayElapsed_delayNotElapsed()",
"error ProposedOwnable__proposeNewOwner_invalidProposal()",
"error ProposedOwnable__proposeNewOwner_noOwnershipChange()",
"error ProposedOwnable__renounceOwnership_invalidProposal()",
"error ProposedOwnable__renounceOwnership_noProposal()",
"event BypassAdded(address,bytes4)",
"event BypassDequeued(address,bytes4)",
"event Dequeued(bytes32 indexed,bytes[])",
"event Executed(bytes32 indexed,bytes[])",
"event OwnershipProposed(address indexed)",
"event OwnershipTransferred(address indexed,address indexed)",
"event Queued(bytes32 indexed,uint256,bytes[])",
"function acceptProposedOwner()",
"function bypassDelay(bytes32) view returns (bool)",
"function delay() view returns (uint256)",
"function dequeue(bytes[])",
"function dequeueBypass(address,bytes4,uint8)",
"function execute(bytes[]) payable",
"function executeBypass(address,bytes4,uint8)",
"function executeWithBypass(bytes[]) payable",
"function owner() view returns (address)",
"function proposals(bytes32) view returns (uint256)",
"function proposeNewOwner(address)",
"function proposed() view returns (address)",
"function proposedTimestamp() view returns (uint256)",
"function queue(bytes[])",
"function queueBypass(address,bytes4,uint8)",
"function renounceOwnership() view",
"function renounced() view returns (bool)"
]
250 changes: 250 additions & 0 deletions packages/deployments/contracts/contracts/messaging/Conductor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity 0.8.17;

import {ProposedOwnable} from "../shared/ProposedOwnable.sol";
import {ExcessivelySafeCall} from "../shared/libraries/ExcessivelySafeCall.sol";

/**
* @title Conductor
* @author Connext Labs
* @notice A contract that can queue and execute transactions.
*
* This contract is intended to be used as an owner, to allow for timelocks
* over arbitrary admin configuration and contract deployment.
*
* @dev Target/selector combos can bypass the timelock if added to a whitelist.
* However, adding these values requires a timelock (use `multisend` to add).
*
* @dev Inherits a default delay fof 7 days from ProposedOwnable
*/
contract Conductor is ProposedOwnable {
// ============ Structs ============

/**
* @notice A struct that represents a transaction to eventually execute
* @param to The address to send the transaction to
* @param value The amount of wei to send with the transaction
* @param data The data to send with the transaction
*/
struct Transaction {
address to;
uint256 value;
bytes data;
}

// ============ Events ============

/**
* @notice Emitted when a target/selector combo is added to the bypass whitelist
* @param target The address of the target contract
* @param selector The selector of the function to bypass
*/
event BypassAdded(address target, bytes4 selector);

/**
* @notice Emitted when a target/selector combo is removed from the bypass whitelist
* @param target The address of the target contract
* @param selector The selector of the function to bypass
*/
event BypassRemoved(address target, bytes4 selector);

/**
* @notice Emitted when a set of transactions are queued
* @param key The keccak256 hash of the transactions
* @param elapse The timestamp at which the transactions can be executed
* @param transactions The transactions that were queued
*/
event Queued(bytes32 indexed key, uint256 elapse, bytes[] transactions);

/**
* @notice Emitted when a set of transactions are removed from the queue
* @param key The keccak256 hash of the transactions
* @param transactions The transactions that were dequeued
*/
event Dequeued(bytes32 indexed key, bytes[] transactions);

/**
* @notice Emitted when a set of transactions are executed
* @param key The keccak256 hash of the transactions
* @param transactions The transactions that were executed
*/
event Executed(bytes32 indexed key, bytes[] transactions);

// ============ Errors ============

error Conductor_onlyConductor__notConductor(address sender);
error Conductor_renounceOwnership__prohibited();
error Conductor_queue__alreadyQueued(bytes32 key);
error Conductor_dequeue__notQueued(bytes32 key);
error Conductor_execute__notElapsed(bytes32 key);
error Conductor_execute__cannotBypass(bytes4 selector, address target);
error Conductor_execute__callFailed();

// ============ Storage ============

/**
* @notice A mapping of target/selector combos to whether they can bypass the
* timelock.
* @dev Keyed by keccak256(abi.encodePacked(target, selector))
*/
mapping(bytes32 => bool) public bypassDelay;

/**
* @notice A mapping of active transaction sets to execute
* @dev Keyed by keccak256(abi.encode(transactions))
*/
mapping(bytes32 => uint256) public proposals;

// ============ Modifiers ============
modifier onlyConductor() {
if (msg.sender != address(this)) {
revert Conductor_onlyConductor__notConductor(msg.sender);
}
_;
}

// ============ Constructor ============
constructor(address _owner) ProposedOwnable() {
_setOwner(_owner);
}

// ============ Fallback ============
/**
* @notice Allows this contract to receive ETH
*/
receive() external payable {}

// ============ Admin ============

/**
* @notice Adds a target/selector combo to the bypass whitelist
* @dev This function is intended to be called via queue / execute flow to enforce the
* same timelock on the bypass whitelist as on the execution of transactions.
* @param _target The address of the target contract
* @param _selector The selector of the function to bypass
*/
function addBypass(address _target, bytes4 _selector) public onlyConductor {
bypassDelay[keccak256(abi.encodePacked(_target, _selector))] = true;
emit BypassAdded(_target, _selector);
}

/**
* @notice Removes a target/selector combo from the bypass whitelist
* @dev This function is intended to be called via queue / execute flow to enforce the
* same timelock on the bypass whitelist as on the execution of transactions.
* @param _target The address of the target contract
* @param _selector The selector of the function to bypass
*/
function removeBypass(address _target, bytes4 _selector) public onlyConductor {
delete bypassDelay[keccak256(abi.encodePacked(_target, _selector))];
emit BypassRemoved(_target, _selector);
}

/**
* @notice Renouncing ownership of this contract is blocked
*/
function renounceOwnership() public view override onlyOwner {
revert Conductor_renounceOwnership__prohibited();
}

// ============ Public ============

/**
* @notice Stores a proposed set of transactions to execute post-timelock
* @dev _transactions is an array of abi-encoded Transaction types
* @param _transactions The transactions to queue
*/
function queue(bytes[] memory _transactions) public onlyOwner {
bytes32 key = keccak256(abi.encode(_transactions));
if (proposals[key] != 0) {
revert Conductor_queue__alreadyQueued(key);
}
uint256 elapse = block.timestamp + delay();
proposals[key] = elapse;
emit Queued(key, elapse, _transactions);
}

/**
* @notice Removes a set of transactions from the queue
* @param _transactions The transactions to dequeue
*/
function dequeue(bytes[] memory _transactions) public onlyOwner {
bytes32 key = keccak256(abi.encode(_transactions));
if (proposals[key] == 0) {
revert Conductor_dequeue__notQueued(key);
}
delete proposals[key];
emit Dequeued(key, _transactions);
}

/**
* @notice Executes a set of transactions after the timelock has elapsed
* @param _transactions The transactions to execute
*/
function execute(bytes[] memory _transactions) public payable onlyOwner {
bytes32 key = keccak256(abi.encode(_transactions));
if (block.timestamp < proposals[key]) {
revert Conductor_execute__notElapsed(key);
}
delete proposals[key];
_execute(_transactions, false);
emit Executed(key, _transactions);
}

/**
* @notice Executes a set of transactions without checking the timelock
* @dev `_execute` enforces that the target/selector combo can bypass
* @param _transactions The transactions to execute
*/
function executeWithBypass(bytes[] memory _transactions) public payable onlyOwner {
bytes32 key = keccak256(abi.encode(_transactions));
_execute(_transactions, true);
emit Executed(key, _transactions);
}

// ============ Private ============

/**
* @notice Executes a set of transactions
* @param _transactions The transactions to execute
* @param _bypass Whether to bypass the timelock
*/
function _execute(bytes[] memory _transactions, bool _bypass) internal {
uint256 reserve = 10_000;
// Check if all transactions can be bypassed
for (uint256 i; i < _transactions.length; i++) {
Transaction memory transaction = abi.decode(_transactions[i], (Transaction));
bytes4 selector = _getSelector(transaction.data);
bytes32 key = keccak256(abi.encodePacked(transaction.to, selector));
if (_bypass) {
// Make sure selector / addr pair can be bypassed
if (!bypassDelay[key]) {
revert Conductor_execute__cannotBypass(selector, transaction.to);
}
}
// NOTE: no need to assert proposals, because it will be checked via
// `execute` function if used

// Execute transaction
(bool success, ) = ExcessivelySafeCall.excessivelySafeCall(
transaction.to,
gasleft() - reserve,
transaction.value,
0,
transaction.data
);
if (!success) {
revert Conductor_execute__callFailed();
}
}
}

/**
* @notice Gets selector from calldata (first 4 bytes)
*/
function _getSelector(bytes memory data) internal pure returns (bytes4 selector) {
assembly {
selector := mload(add(data, 32))
}
}
}
Loading

0 comments on commit 387e177

Please sign in to comment.