From 8ee67f91f1a5bc880519195329a9895cbef157eb Mon Sep 17 00:00:00 2001 From: Ben DiFrancesco Date: Fri, 3 Jul 2026 12:28:17 -0400 Subject: [PATCH] Add initial proposal guardian to the Governor's constructor --- README.md | 3 ++- .../DeployGitcoinGovernorWithGuardian.s.sol | 24 ++++++++++++++++++- ...oyGitcoinGovernorWithGuardianMainnet.s.sol | 10 +++++++- src/GitcoinGovernorWithGuardian.sol | 11 +++++++-- 4 files changed, 43 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c9e7243..4470cb7 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,8 @@ The test suite (`test/`) is still being built. `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. +extension, the initial proposal guardian, 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: diff --git a/script/DeployGitcoinGovernorWithGuardian.s.sol b/script/DeployGitcoinGovernorWithGuardian.s.sol index 4ac0bc4..b8293a5 100644 --- a/script/DeployGitcoinGovernorWithGuardian.s.sol +++ b/script/DeployGitcoinGovernorWithGuardian.s.sol @@ -20,6 +20,7 @@ abstract contract DeployGitcoinGovernorWithGuardian is Script { uint256 initialProposalThreshold; IComp token; ICompoundTimelock timelock; + address initialProposalGuardian; } GitcoinGovernorWithGuardian public governor; @@ -40,6 +41,9 @@ abstract contract DeployGitcoinGovernorWithGuardian is Script { ); _log(string.concat(" token: ", vm.toString(address(_params.token)))); _log(string.concat(" timelock: ", vm.toString(address(_params.timelock)))); + _log( + string.concat(" initialProposalGuardian: ", vm.toString(_params.initialProposalGuardian)) + ); vm.startBroadcast(); // BROADCAST: deploy the GitcoinGovernorWithGuardian @@ -52,7 +56,8 @@ abstract contract DeployGitcoinGovernorWithGuardian is Script { _params.initialVotingPeriod, _params.initialProposalThreshold, address(_params.token), - _params.timelock + _params.timelock, + _params.initialProposalGuardian ); vm.stopBroadcast(); @@ -86,6 +91,13 @@ abstract contract DeployGitcoinGovernorWithGuardian is Script { "set it to the address of Gitcoin's Compound Timelock" ); } + if (_params.initialProposalGuardian == address(0)) { + revert( + "DeployGitcoinGovernorWithGuardian: initialProposalGuardian is the zero address; " + "deploying without a guardian would let every proposer cancel their own proposals at " + "any lifecycle stage, so set it to the address that should hold cancel authority" + ); + } } function _revertIfDeploymentIsInvalid(DeploymentParams memory _params) internal view { @@ -109,5 +121,15 @@ abstract contract DeployGitcoinGovernorWithGuardian is Script { ) ); } + if (governor.proposalGuardian() != _params.initialProposalGuardian) { + revert( + string.concat( + "DeployGitcoinGovernorWithGuardian: deployed governor proposal guardian is ", + vm.toString(governor.proposalGuardian()), + " but expected ", + vm.toString(_params.initialProposalGuardian) + ) + ); + } } } diff --git a/script/DeployGitcoinGovernorWithGuardianMainnet.s.sol b/script/DeployGitcoinGovernorWithGuardianMainnet.s.sol index 420a9a3..46a3c52 100644 --- a/script/DeployGitcoinGovernorWithGuardianMainnet.s.sol +++ b/script/DeployGitcoinGovernorWithGuardianMainnet.s.sol @@ -36,6 +36,13 @@ contract DeployGitcoinGovernorWithGuardianMainnet is DeployGitcoinGovernorWithGu // voting period ending. uint48 constant INITIAL_VOTE_EXTENSION = 14_400; + // TODO: Confirm the proposal guardian address with Gitcoin stakeholders before deploying — + // presumably the DAO's security-council multisig. Defaulted here to the Timelock, i.e. the DAO + // itself, so that until a dedicated guardian is chosen, cancel authority rests with governance + // rather than with no one (an unset guardian would let every proposer cancel their own proposals + // at any lifecycle stage). + address constant INITIAL_PROPOSAL_GUARDIAN = 0x57a8865cfB1eCEf7253c27da6B4BC3dAEE5Be518; + function _getDeploymentParams() internal pure override returns (DeploymentParams memory) { return DeploymentParams({ name: GOVERNOR_NAME, @@ -45,7 +52,8 @@ contract DeployGitcoinGovernorWithGuardianMainnet is DeployGitcoinGovernorWithGu initialVotingPeriod: INITIAL_VOTING_PERIOD, initialProposalThreshold: INITIAL_PROPOSAL_THRESHOLD, token: GTC_TOKEN, - timelock: TIMELOCK + timelock: TIMELOCK, + initialProposalGuardian: INITIAL_PROPOSAL_GUARDIAN }); } } diff --git a/src/GitcoinGovernorWithGuardian.sol b/src/GitcoinGovernorWithGuardian.sol index 0ba36da..de9b074 100644 --- a/src/GitcoinGovernorWithGuardian.sol +++ b/src/GitcoinGovernorWithGuardian.sol @@ -58,6 +58,10 @@ contract GitcoinGovernorWithGuardian is /// @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. + /// @param _initialProposalGuardian The deployment value for the proposal guardian, the address + /// empowered to cancel proposals at any point in their lifecycle before execution. Set at + /// deployment so the Governor is never live without a guardian; the DAO can replace it later via + /// a governance proposal calling `setProposalGuardian`. constructor( string memory _name, uint256 _initialQuorum, @@ -66,7 +70,8 @@ contract GitcoinGovernorWithGuardian is uint32 _initialVotingPeriod, uint256 _initialProposalThreshold, address _token, - ICompoundTimelock _timelockAddress + ICompoundTimelock _timelockAddress, + address _initialProposalGuardian ) Governor(_name) GovernorSettableFixedQuorum(_initialQuorum) @@ -74,7 +79,9 @@ contract GitcoinGovernorWithGuardian is GovernorSettings(_initialVotingDelay, _initialVotingPeriod, _initialProposalThreshold) GovernorVotesComp(_token) GovernorTimelockCompound(_timelockAddress) - {} + { + _setProposalGuardian(_initialProposalGuardian); + } /// @inheritdoc GovernorSettings /// @dev We override this function to resolve ambiguity between inherited contracts.