diff --git a/README.md b/README.md index c0dcf95..c9e7243 100644 --- a/README.md +++ b/README.md @@ -37,14 +37,42 @@ scopelint check # verify formatting and conventions (also run in CI) - `AGENTS.md` — project context, architecture, and conventions for contributors and coding agents. - `foundry.toml` — Foundry build profiles and formatting configuration. -Deployment and governance-proposal scripts (`script/`) and the test suite (`test/`) are still being -built. +- `script/` — deployment scripts (see [Scripts](#scripts)). The Governor deploy script is in place; + governance-proposal and Franchiser scripts are still being built. + +The test suite (`test/`) is still being built. ## Scripts -> 🚧 **Under development.** The deployment and governance-proposal scripts — Governor adoption, -> Franchiser deployment, and Franchiser delegation — are not yet available. Usage instructions will -> be documented here as they land. +### Deploy the upgraded Governor + +`script/DeployGitcoinGovernorWithGuardian.s.sol` holds the reusable deployment mechanics, and +`script/DeployGitcoinGovernorWithGuardianMainnet.s.sol` supplies the mainnet configuration: the GTC +token and Compound Timelock addresses (fixed since 2021), plus governance parameters that mirror the +active "GTC Governor Bravo" so the upgrade preserves current behavior. The new late-quorum vote +extension and the Governor name carry `TODO`s to confirm with stakeholders before deploying. + +Dry-run first to simulate the deployment and print the transaction it would send, and review that +before broadcasting: + +```sh +forge script script/DeployGitcoinGovernorWithGuardianMainnet.s.sol:DeployGitcoinGovernorWithGuardianMainnet \ + --rpc-url "$ETH_RPC_URL" +``` + +Then broadcast and verify (using an encrypted keystore account set up with `cast wallet import`): + +```sh +forge script script/DeployGitcoinGovernorWithGuardianMainnet.s.sol:DeployGitcoinGovernorWithGuardianMainnet \ + --rpc-url "$ETH_RPC_URL" \ + --account deployer \ + --broadcast \ + --verify +``` + +> 🚧 **Still under development.** The governance-proposal scripts — Governor adoption, Franchiser +> deployment, and Franchiser delegation — are not yet available. Usage instructions will be +> documented here as they land. ## License diff --git a/script/DeployGitcoinGovernorWithGuardian.s.sol b/script/DeployGitcoinGovernorWithGuardian.s.sol new file mode 100644 index 0000000..4ac0bc4 --- /dev/null +++ b/script/DeployGitcoinGovernorWithGuardian.s.sol @@ -0,0 +1,113 @@ +// 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 {IComp} from "src/interfaces/IComp.sol"; + +/// @notice Abstract base that holds the mechanics of deploying a `GitcoinGovernorWithGuardian`. +/// A concrete contract supplies the configuration for a specific deployment by implementing +/// `_getDeploymentParams`. +abstract contract DeployGitcoinGovernorWithGuardian is Script { + struct DeploymentParams { + string name; + uint256 initialQuorum; + uint48 initialVoteExtension; + uint48 initialVotingDelay; + uint32 initialVotingPeriod; + uint256 initialProposalThreshold; + IComp token; + ICompoundTimelock timelock; + } + + GitcoinGovernorWithGuardian public governor; + bool internal isLogging = true; + + function run() public virtual { + DeploymentParams memory _params = _getDeploymentParams(); + _revertIfDeploymentParamsAreInvalid(_params); + + _log("Deploying GitcoinGovernorWithGuardian with:"); + _log(string.concat(" name: ", _params.name)); + _log(string.concat(" initialQuorum: ", vm.toString(_params.initialQuorum))); + _log(string.concat(" initialVoteExtension: ", vm.toString(_params.initialVoteExtension))); + _log(string.concat(" initialVotingDelay: ", vm.toString(_params.initialVotingDelay))); + _log(string.concat(" initialVotingPeriod: ", vm.toString(_params.initialVotingPeriod))); + _log( + string.concat(" initialProposalThreshold: ", vm.toString(_params.initialProposalThreshold)) + ); + _log(string.concat(" token: ", vm.toString(address(_params.token)))); + _log(string.concat(" timelock: ", vm.toString(address(_params.timelock)))); + + vm.startBroadcast(); + // BROADCAST: deploy the GitcoinGovernorWithGuardian + _log("[1/1] Deploying GitcoinGovernorWithGuardian"); + governor = new GitcoinGovernorWithGuardian( + _params.name, + _params.initialQuorum, + _params.initialVoteExtension, + _params.initialVotingDelay, + _params.initialVotingPeriod, + _params.initialProposalThreshold, + address(_params.token), + _params.timelock + ); + vm.stopBroadcast(); + + _log(string.concat("GitcoinGovernorWithGuardian deployed at ", vm.toString(address(governor)))); + + _revertIfDeploymentIsInvalid(_params); + } + + function disableLogging() public { + isLogging = false; + } + + function _getDeploymentParams() internal view virtual returns (DeploymentParams memory); + + function _log(string memory _msg) internal view { + if (isLogging) { + console2.log(_msg); + } + } + + function _revertIfDeploymentParamsAreInvalid(DeploymentParams memory _params) internal pure { + if (address(_params.token) == address(0)) { + revert( + "DeployGitcoinGovernorWithGuardian: token is the zero address; " + "set it to the address of the COMP-style GTC token" + ); + } + if (address(_params.timelock) == address(0)) { + revert( + "DeployGitcoinGovernorWithGuardian: timelock is the zero address; " + "set it to the address of Gitcoin's Compound Timelock" + ); + } + } + + function _revertIfDeploymentIsInvalid(DeploymentParams memory _params) internal view { + if (address(governor.token()) != address(_params.token)) { + revert( + string.concat( + "DeployGitcoinGovernorWithGuardian: deployed governor token is ", + vm.toString(address(governor.token())), + " but expected ", + vm.toString(address(_params.token)) + ) + ); + } + if (governor.timelock() != address(_params.timelock)) { + revert( + string.concat( + "DeployGitcoinGovernorWithGuardian: deployed governor timelock is ", + vm.toString(governor.timelock()), + " but expected ", + vm.toString(address(_params.timelock)) + ) + ); + } + } +} diff --git a/script/DeployGitcoinGovernorWithGuardianMainnet.s.sol b/script/DeployGitcoinGovernorWithGuardianMainnet.s.sol new file mode 100644 index 0000000..420a9a3 --- /dev/null +++ b/script/DeployGitcoinGovernorWithGuardianMainnet.s.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.35; + +// `run()` is inherited from the abstract base; scopelint's per-file `script` rule does not resolve +// the inherited entrypoint, so this concrete config opts out of that check. +// scopelint: ignore-script-file + +import {ICompoundTimelock} from "@openzeppelin/contracts/vendor/compound/ICompoundTimelock.sol"; +import {DeployGitcoinGovernorWithGuardian} from "script/DeployGitcoinGovernorWithGuardian.s.sol"; +import {IComp} from "src/interfaces/IComp.sol"; + +/// @notice Mainnet deployment configuration for the upgraded Gitcoin Governor. +/// @dev The governance parameters mirror the active "GTC Governor Bravo" +/// (0x9D4C63565D5618310271bF3F3c01b2954C1D1639) so the upgrade preserves current behavior. The +/// late-quorum vote extension is new to this Governor and has no precedent to carry over. +contract DeployGitcoinGovernorWithGuardianMainnet is DeployGitcoinGovernorWithGuardian { + // TODO: Confirm the Governor name with Gitcoin stakeholders before deploying. It sets the EIP-712 + // domain separator used when signing votes; defaulted here to the active Governor's name for + // continuity. + string constant GOVERNOR_NAME = "GTC Governor Bravo"; + + // Fixed values that can't change + IComp constant GTC_TOKEN = IComp(0xDe30da39c46104798bB5aA3fe8B9e0e1F348163F); + ICompoundTimelock constant TIMELOCK = + ICompoundTimelock(payable(0x57a8865cfB1eCEf7253c27da6B4BC3dAEE5Be518)); + + // Match the values from the existing Governor + uint256 constant INITIAL_QUORUM = 2_500_000e18; + uint48 constant INITIAL_VOTING_DELAY = 13_140; + uint32 constant INITIAL_VOTING_PERIOD = 40_320; + uint256 constant INITIAL_PROPOSAL_THRESHOLD = 150_000e18; + + // TODO: Validate the late-quorum vote extension with Gitcoin stakeholders before deploying. This + // is a new parameter with no precedent in the active Governor; defaulted here to ~2 days (14,400 + // blocks at ~12s/block), the minimum window guaranteed between a proposal reaching quorum and its + // voting period ending. + uint48 constant INITIAL_VOTE_EXTENSION = 14_400; + + function _getDeploymentParams() internal pure override returns (DeploymentParams memory) { + return DeploymentParams({ + name: GOVERNOR_NAME, + initialQuorum: INITIAL_QUORUM, + initialVoteExtension: INITIAL_VOTE_EXTENSION, + initialVotingDelay: INITIAL_VOTING_DELAY, + initialVotingPeriod: INITIAL_VOTING_PERIOD, + initialProposalThreshold: INITIAL_PROPOSAL_THRESHOLD, + token: GTC_TOKEN, + timelock: TIMELOCK + }); + } +} diff --git a/src/GitcoinGovernorWithGuardian.sol b/src/GitcoinGovernorWithGuardian.sol index 2981455..0ba36da 100644 --- a/src/GitcoinGovernorWithGuardian.sol +++ b/src/GitcoinGovernorWithGuardian.sol @@ -45,12 +45,18 @@ contract GitcoinGovernorWithGuardian is /// @param _name Name of the governor instance (used in building the EIP-712 domain separator). /// @param _initialQuorum The deployment value for the proposal quorum value this Governor will /// enforce. + /// @param _initialVoteExtension The deployment value for the late quorum voting extension + /// period. The number of blocks that is required to pass since the moment a proposal reaches + /// quorum until its voting period ends. If necessary the voting period will be extended beyond + /// the one set during proposal creation. /// @param _initialVotingDelay The deployment value for the voting delay this Governor will /// enforce. /// @param _initialVotingPeriod The deployment value for the voting period this Governor will /// enforce. /// @param _initialProposalThreshold The deployment value for the number of GTC required to submit /// a proposal this Governor will enforce. + /// @param _token The address of the COMP-style legacy governance token used to source voting + /// weight. /// @param _timelockAddress The address of Gitcoin's Timelock address. constructor( string memory _name, @@ -59,12 +65,14 @@ contract GitcoinGovernorWithGuardian is uint48 _initialVotingDelay, uint32 _initialVotingPeriod, uint256 _initialProposalThreshold, + address _token, ICompoundTimelock _timelockAddress ) Governor(_name) GovernorSettableFixedQuorum(_initialQuorum) GovernorPreventLateQuorum(_initialVoteExtension) GovernorSettings(_initialVotingDelay, _initialVotingPeriod, _initialProposalThreshold) + GovernorVotesComp(_token) GovernorTimelockCompound(_timelockAddress) {} diff --git a/src/extensions/GovernorVotesComp.sol b/src/extensions/GovernorVotesComp.sol index 6f3876e..e680abd 100644 --- a/src/extensions/GovernorVotesComp.sol +++ b/src/extensions/GovernorVotesComp.sol @@ -10,7 +10,19 @@ import {IComp} from "src/interfaces/IComp.sol"; /// @notice Modified GovernorVotes contract that supports legacy COMP-style tokens. abstract contract GovernorVotesComp is Governor { /// @notice The legacy IComp token from which voting weight is sourced. - IComp public token; + IComp private immutable TOKEN; + + /// @param _token The address of the COMP-style legacy governance token used to source voting + /// weight. + constructor(address _token) { + TOKEN = IComp(_token); + } + + /// @notice Returns the legacy IComp token from which voting weight is sourced. + /// @return The IComp token used to source voting weight. + function token() public view virtual returns (IComp) { + return TOKEN; + } /// @notice This function implements the clock interface as specified in ERC-6372. /// @dev Returns the current clock value used for governance voting. @@ -46,6 +58,6 @@ abstract contract GovernorVotesComp is Governor { override returns (uint256) { - return token.getPriorVotes(_account, _timepoint); + return TOKEN.getPriorVotes(_account, _timepoint); } }