Repository navigation
Implement a script to submit the Governor upgrade proposal #5
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
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| // SPDX-License-Identifier: AGPL-3.0-only | ||
| pragma solidity ^0.8.35; | ||
|
|
||
| import {Script} from "forge-std/Script.sol"; | ||
| import {console2} from "forge-std/console2.sol"; | ||
| import {ICompoundTimelock} from "@openzeppelin/contracts/vendor/compound/ICompoundTimelock.sol"; | ||
| import {GitcoinGovernorWithGuardian} from "src/GitcoinGovernorWithGuardian.sol"; | ||
| import {IGovernorBravo} from "src/interfaces/IGovernorBravo.sol"; | ||
|
|
||
| /// @notice Abstract base that holds the mechanics of proposing the Governor upgrade: a proposal, | ||
| /// submitted to the old (currently active) Governor, that transfers admin control of the DAO's | ||
| /// Timelock to the new `GitcoinGovernorWithGuardian`. The proposal carries two actions: | ||
| /// | ||
| /// 1. `timelock.setPendingAdmin(newGovernor)` — the Timelock, executing the passed proposal, | ||
| /// names the new Governor as its pending admin. | ||
| /// 2. `newGovernor.__acceptAdmin()` — the new Governor claims the admin role from the Timelock. | ||
| /// | ||
| /// A concrete contract supplies the configuration for a specific proposal by implementing | ||
| /// `_getProposalParams`. | ||
| abstract contract ProposeGovernorUpgrade is Script { | ||
| struct ProposalParams { | ||
| IGovernorBravo oldGovernor; | ||
| GitcoinGovernorWithGuardian newGovernor; | ||
| address proposer; | ||
| string description; | ||
| } | ||
|
|
||
| uint256 public proposalId; | ||
| bool internal isLogging = true; | ||
|
|
||
| function run() public virtual { | ||
| ProposalParams memory _params = _getProposalParams(); | ||
| _revertIfProposalParamsAreInvalid(_params); | ||
|
|
||
| (address[] memory _targets, uint256[] memory _values, bytes[] memory _calldatas) = | ||
| _buildProposalActions(_params); | ||
|
|
||
| _log("Proposing the Governor upgrade with:"); | ||
| _log(string.concat(" oldGovernor: ", vm.toString(address(_params.oldGovernor)))); | ||
| _log(string.concat(" newGovernor: ", vm.toString(address(_params.newGovernor)))); | ||
| _log(string.concat(" timelock: ", vm.toString(_params.oldGovernor.timelock()))); | ||
| _log(string.concat(" proposer: ", vm.toString(_params.proposer))); | ||
| _log(string.concat(" description: ", _params.description)); | ||
|
|
||
| vm.startBroadcast(_params.proposer); | ||
| // BROADCAST: submit the upgrade proposal to the old Governor | ||
| _log("[1/1] Submitting the upgrade proposal to the old Governor"); | ||
| proposalId = _params.oldGovernor.propose(_targets, _values, _calldatas, _params.description); | ||
| vm.stopBroadcast(); | ||
|
|
||
| _log(string.concat("Upgrade proposal submitted with id ", vm.toString(proposalId))); | ||
| } | ||
|
|
||
| function disableLogging() public { | ||
| isLogging = false; | ||
| } | ||
|
|
||
| function _getProposalParams() internal view virtual returns (ProposalParams memory); | ||
|
|
||
| function _buildProposalActions(ProposalParams memory _params) | ||
| internal | ||
| view | ||
| returns (address[] memory _targets, uint256[] memory _values, bytes[] memory _calldatas) | ||
| { | ||
| _targets = new address[](2); | ||
| _values = new uint256[](2); | ||
| _calldatas = new bytes[](2); | ||
|
|
||
| _targets[0] = _params.oldGovernor.timelock(); | ||
| _calldatas[0] = | ||
| abi.encodeCall(ICompoundTimelock.setPendingAdmin, (address(_params.newGovernor))); | ||
|
|
||
| _targets[1] = address(_params.newGovernor); | ||
| _calldatas[1] = abi.encodeCall(_params.newGovernor.__acceptAdmin, ()); | ||
| } | ||
|
|
||
| function _log(string memory _msg) internal view { | ||
| if (isLogging) { | ||
| console2.log(_msg); | ||
| } | ||
| } | ||
|
|
||
| function _revertIfProposalParamsAreInvalid(ProposalParams memory _params) internal view { | ||
| if (address(_params.oldGovernor) == address(0)) { | ||
| revert( | ||
| "ProposeGovernorUpgrade: oldGovernor is the zero address; " | ||
| "set it to the address of the currently active Governor" | ||
| ); | ||
| } | ||
| if (address(_params.newGovernor) == address(0)) { | ||
| revert( | ||
| "ProposeGovernorUpgrade: newGovernor is the zero address; " | ||
| "set it to the address of the deployed GitcoinGovernorWithGuardian" | ||
| ); | ||
| } | ||
| if (_params.proposer == address(0)) { | ||
| revert( | ||
| "ProposeGovernorUpgrade: proposer is the zero address; " | ||
| "set it to the delegate submitting the proposal" | ||
| ); | ||
| } | ||
| if (bytes(_params.description).length == 0) { | ||
| revert( | ||
| "ProposeGovernorUpgrade: description is empty; " | ||
| "set it to the text of the upgrade proposal" | ||
| ); | ||
| } | ||
|
|
||
| address _timelock = _params.oldGovernor.timelock(); | ||
| if (_params.newGovernor.timelock() != _timelock) { | ||
| revert( | ||
| string.concat( | ||
| "ProposeGovernorUpgrade: the new governor's timelock is ", | ||
| vm.toString(_params.newGovernor.timelock()), | ||
| " but the old governor's timelock is ", | ||
| vm.toString(_timelock), | ||
| "; both governors must be wired to the same timelock" | ||
| ) | ||
| ); | ||
| } | ||
| if (ICompoundTimelock(payable(_timelock)).admin() != address(_params.oldGovernor)) { | ||
| revert( | ||
| string.concat( | ||
| "ProposeGovernorUpgrade: the timelock's admin is ", | ||
| vm.toString(ICompoundTimelock(payable(_timelock)).admin()), | ||
| " but expected the old governor ", | ||
| vm.toString(address(_params.oldGovernor)), | ||
| "; the upgrade proposal must be submitted to the governor that controls the timelock" | ||
| ) | ||
| ); | ||
| } | ||
| uint256 _proposerVotes = _params.oldGovernor.getVotes(_params.proposer, block.number - 1); | ||
| if (_proposerVotes < _params.oldGovernor.proposalThreshold()) { | ||
| revert( | ||
| string.concat( | ||
| "ProposeGovernorUpgrade: the proposer's voting weight of ", | ||
| vm.toString(_proposerVotes), | ||
| " is below the old governor's proposal threshold of ", | ||
| vm.toString(_params.oldGovernor.proposalThreshold()), | ||
| "; the proposer must hold or be delegated at least the threshold" | ||
| ) | ||
| ); | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| // SPDX-License-Identifier: AGPL-3.0-only | ||
| pragma solidity ^0.8.35; | ||
|
|
||
| import {GitcoinGovernorWithGuardian} from "src/GitcoinGovernorWithGuardian.sol"; | ||
| import {IGovernorBravo} from "src/interfaces/IGovernorBravo.sol"; | ||
| import {ProposeGovernorUpgrade} from "script/ProposeGovernorUpgrade.s.sol"; | ||
|
|
||
| /// @notice Mainnet configuration for the Governor upgrade proposal, submitted to the active | ||
| /// "GTC Governor Bravo" to transfer Timelock control to the new `GitcoinGovernorWithGuardian`. | ||
| contract ProposeGovernorUpgradeMainnet is ProposeGovernorUpgrade { | ||
| IGovernorBravo constant OLD_GOVERNOR = IGovernorBravo(0x9D4C63565D5618310271bF3F3c01b2954C1D1639); | ||
|
|
||
| // TODO: Set to the GitcoinGovernorWithGuardian address once it is deployed to mainnet. The zero | ||
| // address makes this script revert until then, so the proposal cannot be submitted before the | ||
| // new Governor exists. | ||
| GitcoinGovernorWithGuardian constant NEW_GOVERNOR = | ||
| GitcoinGovernorWithGuardian(payable(address(0))); | ||
|
|
||
| // TODO: Set to the delegate who will submit the proposal. They must hold or be delegated voting | ||
| // weight of at least the old Governor's proposal threshold. The zero address makes this script | ||
| // revert until a proposer is confirmed. | ||
| address constant PROPOSER = address(0); | ||
|
|
||
| // TODO: Finalize the proposal text with Gitcoin stakeholders before proposing. This description | ||
| // is stored on-chain (hashed) and displayed by governance UIs like Tally. | ||
| string constant DESCRIPTION = "Upgrade the Gitcoin Governor: transfer Timelock admin rights from" | ||
| " the current Governor to the new GitcoinGovernorWithGuardian, which adds a proposal guardian," | ||
| " late-quorum protection, and a DAO-settable quorum."; | ||
|
|
||
| // Boilerplate override so this file declares the public `run()` that scopelint's script rule | ||
| // looks for; the mechanics all live in the base. | ||
| function run() public override { | ||
| super.run(); | ||
| } | ||
|
|
||
| function _getProposalParams() internal pure override returns (ProposalParams memory) { | ||
| return ProposalParams({ | ||
| oldGovernor: OLD_GOVERNOR, | ||
| newGovernor: NEW_GOVERNOR, | ||
| proposer: PROPOSER, | ||
| description: DESCRIPTION | ||
| }); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| // SPDX-License-Identifier: AGPL-3.0-only | ||
| pragma solidity ^0.8.35; | ||
|
|
||
| import {IGovernor} from "@openzeppelin/contracts/governance/IGovernor.sol"; | ||
|
|
||
| /// @title IGovernorBravo | ||
| /// @author [ScopeLift](https://scopelift.co) | ||
| /// @notice The subset of the ABI of Gitcoin's active Governor ("GTC Governor Bravo") that this | ||
| /// project uses to propose, vote on, and execute the proposal transferring Timelock control to the | ||
| /// upgraded Governor. The active Governor was built on OpenZeppelin Contracts v4.8.0 during the | ||
| /// DAO's 2023 upgrade. | ||
| /// @dev The v4.8.0 Governor's `ProposalState` enum has an identical layout to the v5 one, so this | ||
| /// interface reuses `IGovernor.ProposalState` from the pinned OpenZeppelin v5 library rather than | ||
| /// redeclaring it. | ||
| interface IGovernorBravo { | ||
| /// @notice The address of the Compound Timelock through which this Governor executes passed | ||
| /// proposals, and whose admin it is. | ||
| function timelock() external view returns (address); | ||
|
|
||
| /// @notice The number of votes required to become a proposer. | ||
| function proposalThreshold() external view returns (uint256); | ||
|
|
||
| /// @notice The voting weight an account held at a past block. | ||
| /// @param _account The address whose voting weight is queried. | ||
| /// @param _blockNumber The block number at which the weight is read. | ||
| /// @return The number of votes the account held at the given block. | ||
| function getVotes(address _account, uint256 _blockNumber) external view returns (uint256); | ||
|
|
||
| /// @notice The current lifecycle state of a proposal. | ||
| /// @param _proposalId The identifier of the proposal to query. | ||
| /// @return The proposal's current state. | ||
| function state(uint256 _proposalId) external view returns (IGovernor.ProposalState); | ||
|
|
||
| /// @notice The block at which a proposal's voting weight is snapshotted, i.e. the block after | ||
| /// which voting opens. | ||
| /// @param _proposalId The identifier of the proposal to query. | ||
| /// @return The proposal's snapshot block number. | ||
| function proposalSnapshot(uint256 _proposalId) external view returns (uint256); | ||
|
|
||
| /// @notice The block at which a proposal's voting period ends. | ||
| /// @param _proposalId The identifier of the proposal to query. | ||
| /// @return The proposal's deadline block number. | ||
| function proposalDeadline(uint256 _proposalId) external view returns (uint256); | ||
|
|
||
| /// @notice The timestamp at which a queued proposal becomes executable in the Timelock. | ||
| /// @param _proposalId The identifier of the proposal to query. | ||
| /// @return The proposal's eta timestamp, or zero if it is not queued. | ||
| function proposalEta(uint256 _proposalId) external view returns (uint256); | ||
|
|
||
| /// @notice Creates a new proposal. The caller must hold voting weight of at least | ||
| /// `proposalThreshold` at the previous block. | ||
| /// @param _targets The addresses the proposal's actions call. | ||
| /// @param _values The ETH values sent with each action. | ||
| /// @param _calldatas The calldata each action is called with. | ||
| /// @param _description A human-readable description of the proposal. | ||
| /// @return The identifier of the newly created proposal. | ||
| function propose( | ||
| address[] memory _targets, | ||
| uint256[] memory _values, | ||
| bytes[] memory _calldatas, | ||
| string memory _description | ||
| ) external returns (uint256); | ||
|
|
||
| /// @notice Casts a vote on a proposal. | ||
| /// @param _proposalId The identifier of the proposal voted on. | ||
| /// @param _support The vote type: 0 = Against, 1 = For, 2 = Abstain. | ||
| /// @return The voting weight cast. | ||
| function castVote(uint256 _proposalId, uint8 _support) external returns (uint256); | ||
|
|
||
| /// @notice Queues a succeeded proposal's actions in the Timelock. | ||
| /// @param _targets The addresses the proposal's actions call. | ||
| /// @param _values The ETH values sent with each action. | ||
| /// @param _calldatas The calldata each action is called with. | ||
| /// @param _descriptionHash The keccak256 hash of the proposal's description. | ||
| /// @return The identifier of the queued proposal. | ||
| function queue( | ||
| address[] memory _targets, | ||
| uint256[] memory _values, | ||
| bytes[] memory _calldatas, | ||
| bytes32 _descriptionHash | ||
| ) external returns (uint256); | ||
|
|
||
| /// @notice Executes a queued proposal once its Timelock eta has passed. | ||
| /// @param _targets The addresses the proposal's actions call. | ||
| /// @param _values The ETH values sent with each action. | ||
| /// @param _calldatas The calldata each action is called with. | ||
| /// @param _descriptionHash The keccak256 hash of the proposal's description. | ||
| /// @return The identifier of the executed proposal. | ||
| function execute( | ||
| address[] memory _targets, | ||
| uint256[] memory _values, | ||
| bytes[] memory _calldatas, | ||
| bytes32 _descriptionHash | ||
| ) external payable returns (uint256); | ||
| } |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.