Contract Name:
MarketUpdateProposer
Contract Source Code:
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.15;
import "./../ITimelock.sol";
/**
* @title MarketUpdateProposer
* @notice This contract allows for the creation of proposals that can be executed by the timelock
* @dev This contract is used to propose market updates
* Few important points to note:
* 1) The marketAdmin can propose updates. The marketAdmin can be set by the governor. marketAdmin will be a multi-sig.
* 2) Here governor is the main-governor-timelock. This terminology(using governor as variable for timelock) is for
* consistency with Configurator.sol.
* 3) If marketAdmin/multi-sig is compromised, the new marketAdmin can be set by the governor.
* 4) While the marketAdmin/multi-sig is compromised, the new marketAdmin can propose updates. But those updates will be
* sent to timelock and can be paused by the marketAdminPauseGuardian Configurator and CometProxyAdmin.
* 5) The proposalGuardian can also be used for the same purpose and can cancel the proposal.
*
*
*/
contract MarketUpdateProposer {
struct MarketUpdateProposal {
/// @notice Unique id for looking up a proposal
uint id;
/// @notice Creator of the proposal
address proposer;
/// @notice The timestamp that the proposal will be available for execution, set once the vote succeeds
uint eta;
/// @notice the ordered list of target addresses for calls to be made
address[] targets;
/// @notice The ordered list of values (i.e. msg.value) to be passed to the calls to be made
uint[] values;
/// @notice The ordered list of function signatures to be called
string[] signatures;
/// @notice The ordered list of calldata to be passed to each call
bytes[] calldatas;
/// @notice Flag marking whether the proposal has been canceled
bool canceled;
/// @notice Flag marking whether the proposal has been executed
bool executed;
}
enum ProposalState {
Canceled,
Queued,
Executed,
Expired
}
address public governor;
address public proposalGuardian;
address public marketAdmin;
ITimelock public timelock;
/// @notice The official record of all proposals ever proposed
mapping(uint => MarketUpdateProposal) public proposals;
/// @notice The total number of proposals
uint public proposalCount;
/// @notice The initial proposal ID, set when the contract is deployed
uint public constant INITIAL_PROPOSAL_ID = 0;
/// @notice The maximum number of actions that can be included in a proposal
uint public constant PROPOSAL_MAX_OPERATIONS = 20; // 20 actions
/// @notice An event emitted when a new proposal is created
event MarketUpdateProposalCreated(uint id, address proposer, address[] targets, uint[] values, string[] signatures, bytes[] calldatas, string description);
event MarketUpdateProposalExecuted(uint id);
event MarketUpdateProposalCancelled(uint id);
event SetProposalGuardian(address indexed oldProposalGuardian, address indexed newProposalGuardian);
event SetMarketAdmin(address indexed oldAdmin, address indexed newAdmin);
event SetGovernor(address indexed oldGovernor, address indexed newGovernor);
error Unauthorized();
error InvalidAddress();
constructor(address governor_, address marketAdmin_, address proposalGuardian_, ITimelock timelock_) public {
if (governor_ == address(0) || marketAdmin_ == address(0) || address(timelock_) == address(0)) revert InvalidAddress();
governor = governor_;
marketAdmin = marketAdmin_;
proposalGuardian = proposalGuardian_;
timelock = timelock_;
}
/**
* @notice Transfers the governor rights to a new address
* @dev Can only be called by the governor. Reverts with Unauthorized if the caller is not the governor.
* Emits an event with the old and new governor addresses.
* @param newGovernor The address of the new governor.
*/
function setGovernor(address newGovernor) external {
if (msg.sender != governor) revert Unauthorized();
if (address(newGovernor) == address(0)) revert InvalidAddress();
address oldGovernor = governor;
governor = newGovernor;
emit SetGovernor(oldGovernor, newGovernor);
}
/**
* @notice Sets a new proposalGuardian.
* @dev Can only be called by the governor. Reverts with Unauthorized if the caller is not the owner.
* Emits an event with the old and new proposalGuardian addresses.
* Note that there is no enforced zero address check on `newProposalGuardian` as it may be a deliberate choice
* to assign the zero address in certain scenarios. This design allows flexibility if the zero address
* is intended to represent a specific state, such as temporarily disabling the proposalGuardian.
* @param newProposalGuardian The address of the new market admin proposalGuardian.
*/
function setProposalGuardian(address newProposalGuardian) external {
if (msg.sender != governor) revert Unauthorized();
address oldProposalGuardian = proposalGuardian;
proposalGuardian = newProposalGuardian;
emit SetProposalGuardian(oldProposalGuardian, newProposalGuardian);
}
/**
* @notice Sets a new market admin.
* @dev Can only be called by the governor. Reverts with Unauthorized if the caller is not the governor.
* Emits an event with the old and new market admin addresses.
* Note that there is no enforced zero address check on `newMarketAdmin` as it may be a deliberate choice
* to assign the zero address in certain scenarios. This design allows flexibility if the zero address
* is intended to represent a specific state, such as temporarily disabling the market admin role.
* @param newMarketAdmin The address of the new market admin.
*/
function setMarketAdmin(address newMarketAdmin) external {
if (msg.sender != governor) revert Unauthorized();
address oldMarketAdmin = marketAdmin;
marketAdmin = newMarketAdmin;
emit SetMarketAdmin(oldMarketAdmin, newMarketAdmin);
}
/**
* @notice Function used to propose a new proposal for market updates
* @dev Can only be called by the market admin. Reverts with Unauthorized if the caller is not the market admin
* The function requires the provided arrays to have the same length and at least one action
* Emits a MarketUpdateProposalCreated event with the proposal details
* @param targets Target addresses for proposal calls
* @param values Eth values for proposal calls
* @param signatures Function signatures for proposal calls
* @param calldatas Calldatas for proposal calls
* @param description String description of the proposal
* @return Proposal id of new proposal
*/
function propose(address[] memory targets, uint[] memory values, string[] memory signatures, bytes[] memory calldatas, string memory description) public returns (uint) {
if (msg.sender != marketAdmin) revert Unauthorized();
require(targets.length == values.length && targets.length == signatures.length && targets.length == calldatas.length, "MarketUpdateProposer::propose: proposal function information arity mismatch");
require(targets.length != 0, "MarketUpdateProposer::propose: must provide actions");
require(targets.length <= PROPOSAL_MAX_OPERATIONS, "MarketUpdateProposer::propose: too many actions");
proposalCount++;
uint newProposalID = proposalCount;
MarketUpdateProposal storage newProposal = proposals[newProposalID];
require(newProposal.id == 0, "MarketUpdateProposer::propose: ProposalID collision");
uint eta = block.timestamp + timelock.delay();
newProposal.id = newProposalID;
newProposal.proposer = msg.sender;
newProposal.eta = eta;
newProposal.targets = targets;
newProposal.values = values;
newProposal.signatures = signatures;
newProposal.calldatas = calldatas;
newProposal.canceled = false;
newProposal.executed = false;
emit MarketUpdateProposalCreated(newProposal.id, msg.sender, targets, values, signatures, calldatas, description);
for (uint i = 0; i < newProposal.targets.length; i++) {
queueOrRevertInternal(newProposal.targets[i], newProposal.values[i], newProposal.signatures[i], newProposal.calldatas[i], eta);
}
return newProposal.id;
}
function queueOrRevertInternal(address target, uint value, string memory signature, bytes memory data, uint eta) internal {
require(!timelock.queuedTransactions(keccak256(abi.encode(target, value, signature, data, eta))), "MarketUpdateProposer::queueOrRevertInternal: identical proposal action already queued at eta");
timelock.queueTransaction(target, value, signature, data, eta);
}
/**
* @notice Executes a queued proposal if eta has passed
* @param proposalId The id of the proposal to execute
*/
function execute(uint proposalId) external payable {
if (msg.sender != marketAdmin) revert Unauthorized();
require(state(proposalId) == ProposalState.Queued, "MarketUpdateProposer::execute: proposal can only be executed if it is queued");
MarketUpdateProposal storage proposal = proposals[proposalId];
proposal.executed = true;
for (uint i = 0; i < proposal.targets.length; i++) {
timelock.executeTransaction{value: proposal.values[i]}(proposal.targets[i], proposal.values[i], proposal.signatures[i], proposal.calldatas[i], proposal.eta);
}
emit MarketUpdateProposalExecuted(proposalId);
}
/**
* @notice Cancels a proposal only if sender is the proposer, proposalGuardian, or marketAdmin, and the proposal is not already executed
* @param proposalId The id of the proposal to cancel
*/
function cancel(uint proposalId) external {
if (msg.sender != governor && msg.sender != proposalGuardian && msg.sender != marketAdmin) revert Unauthorized();
require(state(proposalId) != ProposalState.Executed, "MarketUpdateProposer::cancel: cannot cancel executed proposal");
MarketUpdateProposal storage proposal = proposals[proposalId];
proposal.canceled = true;
for (uint i = 0; i < proposal.targets.length; i++) {
timelock.cancelTransaction(proposal.targets[i], proposal.values[i], proposal.signatures[i], proposal.calldatas[i], proposal.eta);
}
emit MarketUpdateProposalCancelled(proposalId);
}
/**
* @notice Gets the state of a proposal
* @param proposalId The id of the proposal
* @return Proposal state
*/
function state(uint proposalId) public view returns (ProposalState) {
require(proposalCount >= proposalId && proposalId > INITIAL_PROPOSAL_ID, "MarketUpdateProposer::state: invalid proposal id");
MarketUpdateProposal storage proposal = proposals[proposalId];
if (proposal.canceled) {
return ProposalState.Canceled;
} else if (proposal.executed) {
return ProposalState.Executed;
} else if (block.timestamp >= (proposal.eta + timelock.GRACE_PERIOD())) {
return ProposalState.Expired;
} else {
return ProposalState.Queued;
}
}
/**
* @notice Get details of a proposal by its id
* @param proposalId The id of the proposal
* @return id The id of the proposal
* @return proposer The address of the proposer
* @return eta The estimated time at which the proposal can be executed
* @return targets targets of the proposal actions
* @return values ETH values of the proposal actions
* @return signatures signatures of the proposal actions
* @return calldatas calldatas of the proposal actions
* @return canceled boolean indicating whether the proposal has been canceled
* @return executed boolean indicating whether the proposal has been executed
*/
function getProposal(uint proposalId) public view
returns (
uint,
address,
uint,
address[] memory,
uint[] memory,
string[] memory,
bytes[] memory,
bool,
bool
)
{
MarketUpdateProposal storage proposal = proposals[proposalId];
return (
proposal.id,
proposal.proposer,
proposal.eta,
proposal.targets,
proposal.values,
proposal.signatures,
proposal.calldatas,
proposal.canceled,
proposal.executed
);
}
}
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.15;
/**
* @dev Interface for interacting with a Timelock
*/
interface ITimelock {
/// @notice Event emitted when a pending admin accepts admin position
event NewAdmin(address indexed newAdmin);
/// @notice Event emitted when new pending admin is set by the timelock
event NewPendingAdmin(address indexed newPendingAdmin);
/// @notice Event emitted when Timelock sets new delay value
event NewDelay(uint indexed newDelay);
/// @notice Event emitted when admin cancels an enqueued transaction
event CancelTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint eta);
/// @notice Event emitted when admin executes an enqueued transaction
event ExecuteTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint eta);
/// @notice Event emitted when admin enqueues a transaction
event QueueTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint eta);
/// @notice The length of time, once the delay has passed, in which a transaction can be executed before it becomes stale
function GRACE_PERIOD() virtual external view returns (uint);
/// @notice The minimum value that the `delay` variable can be set to
function MINIMUM_DELAY() virtual external view returns (uint);
/// @notice The maximum value that the `delay` variable can be set to
function MAXIMUM_DELAY() virtual external view returns (uint);
/// @notice Address that has admin privileges
function admin() virtual external view returns (address);
/// @notice The address that may become the new admin by calling `acceptAdmin()`
function pendingAdmin() virtual external view returns (address);
/**
* @notice Set the pending admin
* @param pendingAdmin_ New pending admin address
*/
function setPendingAdmin(address pendingAdmin_) virtual external;
/**
* @notice Accept the position of admin (if caller is the current pendingAdmin)
*/
function acceptAdmin() virtual external;
/// @notice Duration that a transaction must be queued before it can be executed
function delay() virtual external view returns (uint);
/**
* @notice Set the delay value
* @param delay New delay value
*/
function setDelay(uint delay) virtual external;
/// @notice Mapping of transaction hashes to whether that transaction is currently enqueued
function queuedTransactions(bytes32 txHash) virtual external returns (bool);
/**
* @notice Enque a transaction
* @param target Address that the transaction is targeted at
* @param value Value to send to target address
* @param signature Function signature to call on target address
* @param data Calldata for function called on target address
* @param eta Timestamp of when the transaction can be executed
* @return txHash of the enqueued transaction
*/
function queueTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) virtual external returns (bytes32);
/**
* @notice Cancel an enqueued transaction
* @param target Address that the transaction is targeted at
* @param value Value of the transaction to cancel
* @param signature Function signature of the transaction to cancel
* @param data Calldata for the transaction to cancel
* @param eta Timestamp of the transaction to cancel
*/
function cancelTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) virtual external;
/**
* @notice Execute an enqueued transaction
* @param target Target address of the transaction to execute
* @param value Value of the transaction to execute
* @param signature Function signature of the transaction to execute
* @param data Calldata for the transaction to execute
* @param eta Timestamp of the transaction to execute
* @return bytes returned from executing transaction
*/
function executeTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) virtual external payable returns (bytes memory);
}