diff --git a/contracts/GovernorBravoDelegate.sol b/contracts/GovernorBravoDelegate.sol new file mode 100644 index 0000000..e7f0b44 --- /dev/null +++ b/contracts/GovernorBravoDelegate.sol @@ -0,0 +1,393 @@ +pragma solidity ^0.7.0; +pragma experimental ABIEncoderV2; + +import { + GovernorBravoDelegateStorageV1, + GovernorBravoEvents, + TimelockInterface, + TokenInterface +} from "./GovernorBravoInterfaces.sol"; +import { SafeMath } from "./SafeMath.sol"; + +contract GovernorBravoDelegate is GovernorBravoDelegateStorageV1, GovernorBravoEvents { + /// @notice The name of this contract + string public constant name = "DSL Governor Bravo"; + + /// @notice The minimum setable proposal threshold + uint public constant MIN_PROPOSAL_THRESHOLD = 50000e18; // TODO - Update this + + /// @notice The maximum setable proposal threshold + uint public constant MAX_PROPOSAL_THRESHOLD = 100000e18; // TODO - Update this + + /// @notice The minimum setable voting period + uint public constant MIN_VOTING_PERIOD = 5760; // About 24 hours + + /// @notice The max setable voting period + uint public constant MAX_VOTING_PERIOD = 80640; // About 2 weeks + + /// @notice The min setable voting delay + uint public constant MIN_VOTING_DELAY = 1; + + /// @notice The max setable voting delay + uint public constant MAX_VOTING_DELAY = 40320; // About 1 week + + /// @notice The number of votes in support of a proposal required in order for a quorum to be reached and for a vote to succeed + uint public constant quorumVotes = 400000e18; // TODO - Update this + + /// @notice The maximum number of actions that can be included in a proposal + uint public constant proposalMaxOperations = 10; // 10 actions + + /// @notice The EIP-712 typehash for the contract's domain + bytes32 public constant DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)"); + + /// @notice The EIP-712 typehash for the ballot struct used by the contract + bytes32 public constant BALLOT_TYPEHASH = keccak256("Ballot(uint256 proposalId,uint8 support)"); + + /** + * @notice Used to initialize the contract during delegator contructor + * @param timelock_ The address of the Timelock + * @param token_ The address of the DSL Governance Token + * @param votingPeriod_ The initial voting period + * @param votingDelay_ The initial voting delay + * @param proposalThreshold_ The initial proposal threshold + */ + function initialize(address timelock_, address token_, uint votingPeriod_, uint votingDelay_, uint proposalThreshold_) public { + require(address(timelock) == address(0), "GovernorBravo::initialize: can only initialize once"); + require(msg.sender == admin, "GovernorBravo::initialize: admin only"); + require(timelock_ != address(0), "GovernorBravo::initialize: invalid timelock address"); + require(token_ != address(0), "GovernorBravo::initialize: invalid comp address"); + require(votingPeriod_ >= MIN_VOTING_PERIOD && votingPeriod_ <= MAX_VOTING_PERIOD, "GovernorBravo::initialize: invalid voting period"); + require(votingDelay_ >= MIN_VOTING_DELAY && votingDelay_ <= MAX_VOTING_DELAY, "GovernorBravo::initialize: invalid voting delay"); + require(proposalThreshold_ >= MIN_PROPOSAL_THRESHOLD && proposalThreshold_ <= MAX_PROPOSAL_THRESHOLD, "GovernorBravo::initialize: invalid proposal threshold"); + + timelock = TimelockInterface(timelock_); + token = TokenInterface(token_); + votingPeriod = votingPeriod_; + votingDelay = votingDelay_; + proposalThreshold = proposalThreshold_; + } + + /** + * @notice Function used to propose a new proposal. Sender must have delegates above the proposal threshold + * @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) { + require(token.getPriorVotes(msg.sender, SafeMath.sub(block.number, 1)) > proposalThreshold, "GovernorBravo::propose: proposer votes below proposal threshold"); + require(targets.length == values.length && targets.length == signatures.length && targets.length == calldatas.length, "GovernorBravo::propose: proposal function information arity mismatch"); + require(targets.length != 0, "GovernorBravo::propose: must provide actions"); + require(targets.length <= proposalMaxOperations, "GovernorBravo::propose: too many actions"); + + uint latestProposalId = latestProposalIds[msg.sender]; + if (latestProposalId != 0) { + ProposalState proposersLatestProposalState = state(latestProposalId); + require(proposersLatestProposalState != ProposalState.Active, "GovernorBravo::propose: one live proposal per proposer, found an already active proposal"); + require(proposersLatestProposalState != ProposalState.Pending, "GovernorBravo::propose: one live proposal per proposer, found an already pending proposal"); + } + + uint startBlock = SafeMath.add(block.number, votingDelay); + uint endBlock = SafeMath.add(startBlock, votingPeriod); + + proposalCount++; + // Proposal memory newProposal = Proposal({ + // id: proposalCount, + // proposer: msg.sender, + // eta: 0, + // targets: targets, + // values: values, + // signatures: signatures, + // calldatas: calldatas, + // startBlock: startBlock, + // endBlock: endBlock, + // forVotes: 0, + // againstVotes: 0, + // abstainVotes: 0, + // canceled: false, + // executed: false + // }); + + Proposal storage newProposal = proposals[proposalCount]; + + newProposal.id = proposalCount; + newProposal.proposer = msg.sender; + newProposal.targets = targets; + newProposal.values = values; + newProposal.signatures = signatures; + newProposal.calldatas = calldatas; + newProposal.startBlock = startBlock; + newProposal.endBlock = endBlock; + + latestProposalIds[newProposal.proposer] = proposalCount; + + emit ProposalCreated(proposalCount, msg.sender, targets, values, signatures, calldatas, startBlock, endBlock, description); + return proposalCount; + } + + /** + * @notice Queues a proposal of state succeeded + * @param proposalId The id of the proposal to queue + */ + function queue(uint proposalId) external { + require(state(proposalId) == ProposalState.Succeeded, "GovernorBravo::queue: proposal can only be queued if it is succeeded"); + Proposal storage proposal = proposals[proposalId]; + uint eta = SafeMath.add(block.timestamp, timelock.delay()); + for (uint i = 0; i < proposal.targets.length; i++) { + queueOrRevertInternal(proposal.targets[i], proposal.values[i], proposal.signatures[i], proposal.calldatas[i], eta); + } + proposal.eta = eta; + emit ProposalQueued(proposalId, eta); + } + + 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))), "GovernorBravo::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 { + require(state(proposalId) == ProposalState.Queued, "GovernorBravo::execute: proposal can only be executed if it is queued"); + Proposal 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 ProposalExecuted(proposalId); + } + + /** + * @notice Cancels a proposal only if sender is the proposer, or proposer delegates dropped below proposal threshold + * @param proposalId The id of the proposal to cancel + */ + function cancel(uint proposalId) external { + require(state(proposalId) != ProposalState.Executed, "GovernorBravo::cancel: cannot cancel executed proposal"); + + Proposal storage proposal = proposals[proposalId]; + require(msg.sender == proposal.proposer || token.getPriorVotes(proposal.proposer, SafeMath.sub(block.number, 1)) < proposalThreshold, "GovernorBravo::cancel: proposer above threshold"); + + 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 ProposalCanceled(proposalId); + } + + /** + * @notice Gets actions of a proposal + * @param proposalId the id of the proposal + * @return targets + * @return values + * @return signatures + * @return calldatas + */ + function getActions(uint proposalId) external view returns (address[] memory targets, uint[] memory values, string[] memory signatures, bytes[] memory calldatas) { + Proposal storage p = proposals[proposalId]; + return (p.targets, p.values, p.signatures, p.calldatas); + } + + /** + * @notice Gets the receipt for a voter on a given proposal + * @param proposalId the id of proposal + * @param voter The address of the voter + * @return The voting receipt + */ + function getReceipt(uint proposalId, address voter) external view returns (Receipt memory) { + return proposals[proposalId].receipts[voter]; + } + + + /** + * @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, "GovernorBravo::state: invalid proposal id"); + Proposal storage proposal = proposals[proposalId]; + if (proposal.canceled) { + return ProposalState.Canceled; + } else if (block.number <= proposal.startBlock) { + return ProposalState.Pending; + } else if (block.number <= proposal.endBlock) { + return ProposalState.Active; + } else if (proposal.forVotes <= proposal.againstVotes || proposal.forVotes < quorumVotes) { + return ProposalState.Defeated; + } else if (proposal.eta == 0) { + return ProposalState.Succeeded; + } else if (proposal.executed) { + return ProposalState.Executed; + } else if (block.timestamp >= SafeMath.add(proposal.eta, timelock.GRACE_PERIOD())) { + return ProposalState.Expired; + } else { + return ProposalState.Queued; + } + } + + /** + * @notice Cast a vote for a proposal + * @param proposalId The id of the proposal to vote on + * @param support The support value for the vote. 0=against, 1=for, 2=abstain + */ + function castVote(uint proposalId, uint8 support) external { + emit VoteCast(msg.sender, proposalId, support, castVoteInternal(msg.sender, proposalId, support), ""); + } + + /** + * @notice Cast a vote for a proposal with a reason + * @param proposalId The id of the proposal to vote on + * @param support The support value for the vote. 0=against, 1=for, 2=abstain + * @param reason The reason given for the vote by the voter + */ + function castVoteWithReason(uint proposalId, uint8 support, string calldata reason) external { + emit VoteCast(msg.sender, proposalId, support, castVoteInternal(msg.sender, proposalId, support), reason); + } + + /** + * @notice Cast a vote for a proposal by signature + * @dev External function that accepts EIP-712 signatures for voting on proposals. + */ + function castVoteBySig(uint proposalId, uint8 support, uint8 v, bytes32 r, bytes32 s) external { + bytes32 domainSeparator = keccak256(abi.encode(DOMAIN_TYPEHASH, keccak256(bytes(name)), getChainIdInternal(), address(this))); + bytes32 structHash = keccak256(abi.encode(BALLOT_TYPEHASH, proposalId, support)); + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + address signatory = ecrecover(digest, v, r, s); + require(signatory != address(0), "GovernorBravo::castVoteBySig: invalid signature"); + emit VoteCast(signatory, proposalId, support, castVoteInternal(signatory, proposalId, support), ""); + } + + /** + * @notice Internal function that caries out voting logic + * @param voter The voter that is casting their vote + * @param proposalId The id of the proposal to vote on + * @param support The support value for the vote. 0=against, 1=for, 2=abstain + * @return The number of votes cast + */ + function castVoteInternal(address voter, uint proposalId, uint8 support) internal returns (uint96) { + require(state(proposalId) == ProposalState.Active, "GovernorBravo::castVoteInternal: voting is closed"); + require(support <= 2, "GovernorBravo::castVoteInternal: invalid vote type"); + Proposal storage proposal = proposals[proposalId]; + Receipt storage receipt = proposal.receipts[voter]; + require(receipt.hasVoted == false, "GovernorBravo::castVoteInternal: voter already voted"); + uint96 votes = token.getPriorVotes(voter, proposal.startBlock); + + if (support == 0) { + proposal.againstVotes = SafeMath.add(proposal.againstVotes, votes); + } else if (support == 1) { + proposal.forVotes = SafeMath.add(proposal.forVotes, votes); + } else if (support == 2) { + proposal.abstainVotes = SafeMath.add(proposal.abstainVotes, votes); + } + + receipt.hasVoted = true; + receipt.support = support; + receipt.votes = votes; + + return votes; + } + + /** + * @notice Admin function for setting the voting delay + * @param newVotingDelay new voting delay, in blocks + */ + function _setVotingDelay(uint newVotingDelay) external { + require(msg.sender == admin, "GovernorBravo::_setVotingDelay: admin only"); + require(newVotingDelay >= MIN_VOTING_DELAY && newVotingDelay <= MAX_VOTING_DELAY, "GovernorBravo::_setVotingDelay: invalid voting delay"); + uint oldVotingDelay = votingDelay; + votingDelay = newVotingDelay; + + emit VotingDelaySet(oldVotingDelay,votingDelay); + } + + /** + * @notice Admin function for setting the voting period + * @param newVotingPeriod new voting period, in blocks + */ + function _setVotingPeriod(uint newVotingPeriod) external { + require(msg.sender == admin, "GovernorBravo::_setVotingPeriod: admin only"); + require(newVotingPeriod >= MIN_VOTING_PERIOD && newVotingPeriod <= MAX_VOTING_PERIOD, "GovernorBravo::_setVotingPeriod: invalid voting period"); + uint oldVotingPeriod = votingPeriod; + votingPeriod = newVotingPeriod; + + emit VotingPeriodSet(oldVotingPeriod, votingPeriod); + } + + /** + * @notice Admin function for setting the proposal threshold + * @dev newProposalThreshold must be greater than the hardcoded min + * @param newProposalThreshold new proposal threshold + */ + function _setProposalThreshold(uint newProposalThreshold) external { + require(msg.sender == admin, "GovernorBravo::_setProposalThreshold: admin only"); + require(newProposalThreshold >= MIN_PROPOSAL_THRESHOLD && newProposalThreshold <= MAX_PROPOSAL_THRESHOLD, "GovernorBravo::_setProposalThreshold: invalid proposal threshold"); + uint oldProposalThreshold = proposalThreshold; + proposalThreshold = newProposalThreshold; + + emit ProposalThresholdSet(oldProposalThreshold, proposalThreshold); + } + + /** + * @notice Initiate the GovernorBravo contract + * @dev Admin only. Accepts timelock admin + */ + function _initiate() external { + require(msg.sender == admin, "GovernorBravo::_initiate: admin only"); + require(proposalCount == 0, "GovernorBravo::_initiate: can only initiate once"); + timelock.acceptAdmin(); + } + + /** + * @notice Begins transfer of admin rights. The newPendingAdmin must call `_acceptAdmin` to finalize the transfer. + * @dev Admin function to begin change of admin. The newPendingAdmin must call `_acceptAdmin` to finalize the transfer. + * @param newPendingAdmin New pending admin. + */ + function _setPendingAdmin(address newPendingAdmin) external { + // Check caller = admin + require(msg.sender == admin, "GovernorBravo:_setPendingAdmin: admin only"); + + // Save current value, if any, for inclusion in log + address oldPendingAdmin = pendingAdmin; + + // Store pendingAdmin with value newPendingAdmin + pendingAdmin = newPendingAdmin; + + // Emit NewPendingAdmin(oldPendingAdmin, newPendingAdmin) + emit NewPendingAdmin(oldPendingAdmin, newPendingAdmin); + } + + /** + * @notice Accepts transfer of admin rights. msg.sender must be pendingAdmin + * @dev Admin function for pending admin to accept role and update admin + */ + function _acceptAdmin() external { + // Check caller is pendingAdmin and pendingAdmin ≠ address(0) + require(msg.sender == pendingAdmin && msg.sender != address(0), "GovernorBravo:_acceptAdmin: pending admin only"); + + // Save current values for inclusion in log + address oldAdmin = admin; + address oldPendingAdmin = pendingAdmin; + + // Store admin with value pendingAdmin + admin = pendingAdmin; + + // Clear the pending value + pendingAdmin = address(0); + + emit NewAdmin(oldAdmin, admin); + emit NewPendingAdmin(oldPendingAdmin, pendingAdmin); + } + + + function getChainIdInternal() internal pure returns (uint) { + uint chainId; + assembly { chainId := chainid() } + return chainId; + } + +} \ No newline at end of file diff --git a/contracts/GovernorBravoDelegator.sol b/contracts/GovernorBravoDelegator.sol new file mode 100644 index 0000000..f6aa05f --- /dev/null +++ b/contracts/GovernorBravoDelegator.sol @@ -0,0 +1,83 @@ +pragma solidity ^0.7.0; +pragma experimental ABIEncoderV2; + +import { GovernorBravoDelegatorStorage, GovernorBravoEvents } from "./GovernorBravoInterfaces.sol"; + +contract GovernorBravoDelegator is GovernorBravoDelegatorStorage, GovernorBravoEvents { + constructor( + address timelock_, + address admin_, + address token_, + address implementation_, + uint votingPeriod_, + uint votingDelay_, + uint proposalThreshold_ + ) { + // Admin set to msg.sender for initialization + admin = msg.sender; + + delegateTo( + implementation_, + abi.encodeWithSignature( + "initialize(address,address,uint256,uint256,uint256)", + timelock_, + token_, + votingPeriod_, + votingDelay_, + proposalThreshold_ + ) + ); + + _setImplementation(implementation_); + + admin = admin_; + } + + /** + * @notice Called by the admin to update the implementation of the delegator + * @param implementation_ The address of the new implementation for delegation + */ + function _setImplementation(address implementation_) public { + require(msg.sender == admin, "GovernorBravoDelegator::_setImplementation: admin only"); + require(implementation_ != address(0), "GovernorBravoDelegator::_setImplementation: invalid implementation address"); + + address oldImplementation = implementation; + implementation = implementation_; + + emit NewImplementation(oldImplementation, implementation); + } + + /** + * @notice Internal method to delegate execution to another contract + * @dev It returns to the external caller whatever the implementation returns or forwards reverts + * @param callee The contract to delegatecall + * @param data The raw data to delegatecall + */ + function delegateTo(address callee, bytes memory data) internal { + (bool success, bytes memory returnData) = callee.delegatecall(data); + assembly { + if eq(success, 0) { + revert(add(returnData, 0x20), returndatasize()) + } + } + } + + /** + * @dev Delegates execution to an implementation contract. + * It returns to the external caller whatever the implementation returns + * or forwards reverts. + */ + fallback () external payable { + // delegate all other functions to current implementation + (bool success, ) = implementation.delegatecall(msg.data); + + assembly { + let free_mem_ptr := mload(0x40) + returndatacopy(free_mem_ptr, 0, returndatasize()) + + switch success + case 0 { revert(free_mem_ptr, returndatasize()) } + default { return(free_mem_ptr, returndatasize()) } + } + } +} \ No newline at end of file diff --git a/contracts/GovernorBravoInterfaces.sol b/contracts/GovernorBravoInterfaces.sol new file mode 100644 index 0000000..4b9b337 --- /dev/null +++ b/contracts/GovernorBravoInterfaces.sol @@ -0,0 +1,180 @@ +pragma solidity ^0.7.0; +pragma experimental ABIEncoderV2; + +interface TimelockInterface { + function delay() external view returns (uint); + function GRACE_PERIOD() external view returns (uint); + function acceptAdmin() external; + function queuedTransactions(bytes32 hash) external view returns (bool); + function queueTransaction(address target, uint value, string calldata signature, bytes calldata data, uint eta) external returns (bytes32); + function cancelTransaction(address target, uint value, string calldata signature, bytes calldata data, uint eta) external; + function executeTransaction(address target, uint value, string calldata signature, bytes calldata data, uint eta) external payable returns (bytes memory); +} + +interface TokenInterface { + function getPriorVotes(address account, uint blockNumber) external view returns (uint96); +} + +contract GovernorBravoEvents { + /// @notice An event emitted when a new proposal is created + event ProposalCreated( + uint id, + address proposer, + address[] targets, + uint[] values, + string[] signatures, + bytes[] calldatas, + uint startBlock, + uint endBlock, + string description + ); + + /// @notice An event emitted when a vote has been cast on a proposal + /// @param voter The address which casted a vote + /// @param proposalId The proposal id which was voted on + /// @param support Support value for the vote. 0=against, 1=for, 2=abstain + /// @param votes Number of votes which were cast by the voter + /// @param reason The reason given for the vote by the voter + event VoteCast(address indexed voter, uint proposalId, uint8 support, uint votes, string reason); + + /// @notice An event emitted when a proposal has been canceled + event ProposalCanceled(uint id); + + /// @notice An event emitted when a proposal has been queued in the Timelock + event ProposalQueued(uint id, uint eta); + + /// @notice An event emitted when a proposal has been executed in the Timelock + event ProposalExecuted(uint id); + + /// @notice An event emitted when the voting delay is set + event VotingDelaySet(uint oldVotingDelay, uint newVotingDelay); + + /// @notice An event emitted when the voting period is set + event VotingPeriodSet(uint oldVotingPeriod, uint newVotingPeriod); + + /// @notice Emitted when implementation is changed + event NewImplementation(address oldImplementation, address newImplementation); + + /// @notice Emitted when proposal threshold is set + event ProposalThresholdSet(uint oldProposalThreshold, uint newProposalThreshold); + + /// @notice Emitted when pendingAdmin is changed + event NewPendingAdmin(address oldPendingAdmin, address newPendingAdmin); + + /// @notice Emitted when pendingAdmin is accepted, which means admin is updated + event NewAdmin(address oldAdmin, address newAdmin); +} + +contract GovernorBravoDelegatorStorage { + /// @notice Administrator for this contract + address public admin; + + /// @notice Pending administrator for this contract + address public pendingAdmin; + + /// @notice Active brains of Governor + address public implementation; +} + +/** + * @title Storage for Governor Bravo Delegate + * @notice For future upgrades, do not change GovernorBravoDelegateStorageV1. Create a new + * contract which implements GovernorBravoDelegateStorageV1 and following the naming convention + * GovernorBravoDelegateStorageVX. + */ +contract GovernorBravoDelegateStorageV1 is GovernorBravoDelegatorStorage { + /// @notice The delay before voting on a proposal may take place, once proposed, in blocks + uint public votingDelay; + + /// @notice The duration of voting on a proposal, in blocks + uint public votingPeriod; + + /// @notice The number of votes required in order for a voter to become a proposer + uint public proposalThreshold; + + /// @notice The total number of proposals + uint public proposalCount; + + /// @notice The address of the DSL Protocol Timelock + TimelockInterface public timelock; + + /// @notice The address of the DSL governance token + TokenInterface public token; + + /// @notice The official record of all proposals ever proposed + mapping (uint => Proposal) public proposals; + + /// @notice The latest proposal for each proposer + mapping (address => uint) public latestProposalIds; + + struct Proposal { + // Unique id for looking up a proposal + uint id; + + // Creator of the proposal + address proposer; + + // The timestamp that the proposal will be available for execution, set once the vote succeeds + uint eta; + + // the ordered list of target addresses for calls to be made + address[] targets; + + // The ordered list of values (i.e. msg.value) to be passed to the calls to be made + uint[] values; + + // The ordered list of function signatures to be called + string[] signatures; + + // The ordered list of calldata to be passed to each call + bytes[] calldatas; + + // The block at which voting begins: holders must delegate their votes prior to this block + uint startBlock; + + // The block at which voting ends: votes must be cast prior to this block + uint endBlock; + + // Current number of votes in favor of this proposal + uint forVotes; + + // Current number of votes in opposition to this proposal + uint againstVotes; + + // Current number of votes for abstaining for this proposal + uint abstainVotes; + + // Flag marking whether the proposal has been canceled + bool canceled; + + // Flag marking whether the proposal has been executed + bool executed; + + // Receipts of ballots for the entire set of voters + mapping (address => Receipt) receipts; + } + + /// @notice Ballot receipt record for a voter + struct Receipt { + // Whether or not a vote has been cast + bool hasVoted; + + // Whether or not the voter supports the proposal or abstains + uint8 support; + + // The number of votes the voter had, which were cast + uint96 votes; + } + + /// @notice Possible states that a proposal may be in + enum ProposalState { + Pending, + Active, + Canceled, + Defeated, + Succeeded, + Queued, + Expired, + Executed + } +} \ No newline at end of file diff --git a/contracts/SafeMath.sol b/contracts/SafeMath.sol new file mode 100644 index 0000000..92d2f45 --- /dev/null +++ b/contracts/SafeMath.sol @@ -0,0 +1,186 @@ +pragma solidity ^0.7.0; + +// From https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/math/Math.sol +// Subject to the MIT license. + +/** + * @dev Wrappers over Solidity's arithmetic operations with added overflow + * checks. + * + * Arithmetic operations in Solidity wrap on overflow. This can easily result + * in bugs, because programmers usually assume that an overflow raises an + * error, which is the standard behavior in high level programming languages. + * `SafeMath` restores this intuition by reverting the transaction when an + * operation overflows. + * + * Using this library instead of the unchecked operations eliminates an entire + * class of bugs, so it's recommended to use it always. + */ +library SafeMath { + /** + * @dev Returns the addition of two unsigned integers, reverting on overflow. + * + * Counterpart to Solidity's `+` operator. + * + * Requirements: + * - Addition cannot overflow. + */ + function add(uint256 a, uint256 b) internal pure returns (uint256) { + uint256 c = a + b; + require(c >= a, "SafeMath: addition overflow"); + + return c; + } + + /** + * @dev Returns the addition of two unsigned integers, reverting with custom message on overflow. + * + * Counterpart to Solidity's `+` operator. + * + * Requirements: + * - Addition cannot overflow. + */ + function add(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { + uint256 c = a + b; + require(c >= a, errorMessage); + + return c; + } + + /** + * @dev Returns the subtraction of two unsigned integers, reverting on underflow (when the result is negative). + * + * Counterpart to Solidity's `-` operator. + * + * Requirements: + * - Subtraction cannot underflow. + */ + function sub(uint256 a, uint256 b) internal pure returns (uint256) { + return sub(a, b, "SafeMath: subtraction underflow"); + } + + /** + * @dev Returns the subtraction of two unsigned integers, reverting with custom message on underflow (when the result is negative). + * + * Counterpart to Solidity's `-` operator. + * + * Requirements: + * - Subtraction cannot underflow. + */ + function sub(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { + require(b <= a, errorMessage); + uint256 c = a - b; + + return c; + } + + /** + * @dev Returns the multiplication of two unsigned integers, reverting on overflow. + * + * Counterpart to Solidity's `*` operator. + * + * Requirements: + * - Multiplication cannot overflow. + */ + function mul(uint256 a, uint256 b) internal pure returns (uint256) { + // Gas optimization: this is cheaper than requiring 'a' not being zero, but the + // benefit is lost if 'b' is also tested. + // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 + if (a == 0) { + return 0; + } + + uint256 c = a * b; + require(c / a == b, "SafeMath: multiplication overflow"); + + return c; + } + + /** + * @dev Returns the multiplication of two unsigned integers, reverting on overflow. + * + * Counterpart to Solidity's `*` operator. + * + * Requirements: + * - Multiplication cannot overflow. + */ + function mul(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { + // Gas optimization: this is cheaper than requiring 'a' not being zero, but the + // benefit is lost if 'b' is also tested. + // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 + if (a == 0) { + return 0; + } + + uint256 c = a * b; + require(c / a == b, errorMessage); + + return c; + } + + /** + * @dev Returns the integer division of two unsigned integers. + * Reverts on division by zero. The result is rounded towards zero. + * + * Counterpart to Solidity's `/` operator. Note: this function uses a + * `revert` opcode (which leaves remaining gas untouched) while Solidity + * uses an invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + */ + function div(uint256 a, uint256 b) internal pure returns (uint256) { + return div(a, b, "SafeMath: division by zero"); + } + + /** + * @dev Returns the integer division of two unsigned integers. + * Reverts with custom message on division by zero. The result is rounded towards zero. + * + * Counterpart to Solidity's `/` operator. Note: this function uses a + * `revert` opcode (which leaves remaining gas untouched) while Solidity + * uses an invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + */ + function div(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { + // Solidity only automatically asserts when dividing by 0 + require(b > 0, errorMessage); + uint256 c = a / b; + // assert(a == b * c + a % b); // There is no case in which this doesn't hold + + return c; + } + + /** + * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), + * Reverts when dividing by zero. + * + * Counterpart to Solidity's `%` operator. This function uses a `revert` + * opcode (which leaves remaining gas untouched) while Solidity uses an + * invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + */ + function mod(uint256 a, uint256 b) internal pure returns (uint256) { + return mod(a, b, "SafeMath: modulo by zero"); + } + + /** + * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), + * Reverts with custom message when dividing by zero. + * + * Counterpart to Solidity's `%` operator. This function uses a `revert` + * opcode (which leaves remaining gas untouched) while Solidity uses an + * invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + */ + function mod(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { + require(b != 0, errorMessage); + return a % b; + } +} \ No newline at end of file diff --git a/contracts/Timelock.sol b/contracts/Timelock.sol new file mode 100644 index 0000000..35d95a8 --- /dev/null +++ b/contracts/Timelock.sol @@ -0,0 +1,111 @@ +pragma solidity ^0.7.0; + +import "./SafeMath.sol"; + +contract Timelock { + using SafeMath for uint; + + event NewAdmin(address indexed newAdmin); + event NewPendingAdmin(address indexed newPendingAdmin); + event NewDelay(uint indexed newDelay); + event CancelTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint eta); + event ExecuteTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint eta); + event QueueTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint eta); + + uint public constant GRACE_PERIOD = 14 days; + uint public constant MINIMUM_DELAY = 2 days; + uint public constant MAXIMUM_DELAY = 30 days; + + address public admin; + address public pendingAdmin; + uint public delay; + + mapping (bytes32 => bool) public queuedTransactions; + + + constructor(address admin_, uint delay_) { + require(delay_ >= MINIMUM_DELAY, "Timelock::constructor: Delay must exceed minimum delay."); + require(delay_ <= MAXIMUM_DELAY, "Timelock::setDelay: Delay must not exceed maximum delay."); + + admin = admin_; + delay = delay_; + } + + fallback() external payable { } + + function setDelay(uint delay_) public { + require(msg.sender == address(this), "Timelock::setDelay: Call must come from Timelock."); + require(delay_ >= MINIMUM_DELAY, "Timelock::setDelay: Delay must exceed minimum delay."); + require(delay_ <= MAXIMUM_DELAY, "Timelock::setDelay: Delay must not exceed maximum delay."); + delay = delay_; + + emit NewDelay(delay); + } + + function acceptAdmin() public { + require(msg.sender == pendingAdmin, "Timelock::acceptAdmin: Call must come from pendingAdmin."); + admin = msg.sender; + pendingAdmin = address(0); + + emit NewAdmin(admin); + } + + function setPendingAdmin(address pendingAdmin_) public { + require(msg.sender == address(this), "Timelock::setPendingAdmin: Call must come from Timelock."); + pendingAdmin = pendingAdmin_; + + emit NewPendingAdmin(pendingAdmin); + } + + function queueTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) public returns (bytes32) { + require(msg.sender == admin, "Timelock::queueTransaction: Call must come from admin."); + require(eta >= getBlockTimestamp().add(delay), "Timelock::queueTransaction: Estimated execution block must satisfy delay."); + + bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); + queuedTransactions[txHash] = true; + + emit QueueTransaction(txHash, target, value, signature, data, eta); + return txHash; + } + + function cancelTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) public { + require(msg.sender == admin, "Timelock::cancelTransaction: Call must come from admin."); + + bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); + queuedTransactions[txHash] = false; + + emit CancelTransaction(txHash, target, value, signature, data, eta); + } + + function executeTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) public payable returns (bytes memory) { + require(msg.sender == admin, "Timelock::executeTransaction: Call must come from admin."); + + bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); + require(queuedTransactions[txHash], "Timelock::executeTransaction: Transaction hasn't been queued."); + require(getBlockTimestamp() >= eta, "Timelock::executeTransaction: Transaction hasn't surpassed time lock."); + require(getBlockTimestamp() <= eta.add(GRACE_PERIOD), "Timelock::executeTransaction: Transaction is stale."); + + queuedTransactions[txHash] = false; + + bytes memory callData; + + if (bytes(signature).length == 0) { + callData = data; + } else { + callData = abi.encodePacked(bytes4(keccak256(bytes(signature))), data); + } + + // solium-disable-next-line security/no-call-value + (bool success, bytes memory returnData) = target.call{value: value}(callData); + require(success, "Timelock::executeTransaction: Transaction execution reverted."); + + emit ExecuteTransaction(txHash, target, value, signature, data, eta); + + return returnData; + } + + function getBlockTimestamp() internal view returns (uint) { + // solium-disable-next-line security/no-block-members + return block.timestamp; + } +} \ No newline at end of file