Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 19 additions & 12 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,21 +242,28 @@ secret). CI supplies it via the `MAINNET_RPC_URL` repository secret.
## Current status

- `GitcoinGovernorWithGuardian` and its two custom extensions are written.
- **Gitcoin Governor Charlie is deployed and verified on mainnet** at
`0xef41CbD211076E8b1901e214Bf751d404cf06638` (block `25_776_742`, transaction
`0xaa524bf06153ec3fde533f207d0f843fea26661774db6fe7113a67423b9caafb`). Its configuration is 1.5
million GTC quorum, 14,400-block voting delay, 40,320-block voting period, 150,000 GTC proposal
threshold, 7,200-block vote extension, and Proposal Guardian
`0x5743E35477363241300FcEdc2F5eB0195F300817`.
- The Governor **deploy script** (`DeployGitcoinGovernorWithGuardian[Mainnet].s.sol`) and the
**upgrade proposal script** (`ProposeGovernorUpgrade[Mainnet].s.sol`) are in place. Both carry
`TODO`s to confirm with stakeholders before running (Governor name, vote extension, proposal
guardian; new Governor address, proposer, proposal text).
**upgrade proposal script** (`ProposeGovernorUpgrade[Mainnet].s.sol`) are in place. The proposal
concrete now points at the deployed Governor and still carries `TODO`s for the proposer and final
proposal text.
- The **mainnet fork integration suite** (`test/*.integration.t.sol`) is in place: it deploys the
new Governor with the real deploy script, submits the upgrade proposal with the real proposal
script, and exercises the upgrade lifecycle, post-upgrade governance, quorum behavior
(settable + late-quorum), and the Proposal Guardian. Shared helpers live in `test/helpers/`;
each suite has a `…MainnetScript` provenance concrete, with room for a `…MainnetDeployed`
concrete after the real deployment. Proposals are voted through by an electorate of **real
delegates** whose live weights are read from the fork in `setUp`. The suite pins `FORK_BLOCK`
in `test/helpers/GitcoinGovernorUpgradeTestBase.sol` — when bumping it, re-verify the
`PROPOSER` delegate still clears the proposal threshold and the electorate still clears quorum
(`setUp` asserts both weights loudly, and quorum-boundary tests assert their own weight
preconditions).
the four Governor suites have both `…MainnetScript` and `…MainnetDeployed` provenance concretes.
The former fork from before deployment and run the production deploy script; the latter fork from
the first block after deployment and bind to the production bytecode. Proposals are voted through
by an electorate of **real delegates** whose live weights are read from the fork in `setUp`. The
suite pins both `GOVERNOR_PRE_DEPLOYMENT_BLOCK` and `GOVERNOR_POST_DEPLOYMENT_BLOCK` in
`test/helpers/GitcoinGovernorUpgradeTestBase.sol`; when bumping either, re-verify the `PROPOSER`
delegate still clears the proposal threshold and the electorate still clears quorum (`setUp`
asserts both weights loudly, and quorum-boundary tests assert their own weight preconditions).
- The Franchiser contracts are in place as the `lib/franchiser-expiry` submodule (ScopeLift fork,
`repo-updates` branch), and all four **Franchiser script pairs** are written:
`DeployFranchiser[Mainnet]`, `ProposeFranchiserDelegation[Mainnet]`,
Expand All @@ -273,8 +280,8 @@ secret). CI supplies it via the `MAINNET_RPC_URL` repository secret.
proposals already snapshotted), expiry sweeps (permissionless, candidate filtering, weight
persists until swept), and the scripts' validation reverts. Shared helpers live in
`test/helpers/PostUpgradeFranchiserTestBase.sol`.
- Up next: confirm the outstanding `TODO`s with Gitcoin stakeholders, deploy the new Governor,
and run the upgrade proposal (see [Deliverables](#deliverables)).
- Up next: confirm the upgrade proposal's proposer and final text with Gitcoin stakeholders, then
run the upgrade proposal (see [Deliverables](#deliverables)).
- CI runs `forge build`, `forge test`, and `scopelint check`. Coverage and Slither jobs are scaffolded
but commented out in `.github/workflows/ci.yml`.

Expand Down
47 changes: 31 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,20 @@ scopelint check # verify formatting and conventions (also run in CI)

`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, the initial proposal guardian, and the Governor name carry `TODO`s to confirm with
stakeholders before deploying.
token and Compound Timelock addresses (fixed since 2021), plus the finalized governance parameters.
The upgraded Governor is named "Gitcoin Governor Charlie," uses a 1.5 million GTC quorum, a 14,400-
block voting delay (approximately 48 hours), the existing 40,320-block voting period and 150,000 GTC
proposal threshold, and a 7,200-block late-quorum voting window (approximately 24 hours). Proposal
Guardian authority is initially assigned to `0x5743E35477363241300FcEdc2F5eB0195F300817`.

Gitcoin Governor Charlie was deployed and verified on Ethereum mainnet on August 17, 2026:

| Item | Value |
| --- | --- |
| Governor | [`0xef41CbD211076E8b1901e214Bf751d404cf06638`](https://etherscan.io/address/0xef41CbD211076E8b1901e214Bf751d404cf06638#code) |
| Deployment transaction | [`0xaa524bf06153ec3fde533f207d0f843fea26661774db6fe7113a67423b9caafb`](https://etherscan.io/tx/0xaa524bf06153ec3fde533f207d0f843fea26661774db6fe7113a67423b9caafb) |
| Deployment block | `25,776,742` |
| Deployer | `0xba41C0652c89dDa91041Fb6ad58576784c9f28F6` |

Dry-run first to simulate the deployment and print the transaction it would send, and review that
before broadcasting:
Expand Down Expand Up @@ -90,9 +100,8 @@ Governor as its pending admin (`setPendingAdmin`), and the new Governor claims t
same Timelock, that the old Governor is the Timelock's current admin, and that the proposer's
voting weight meets the proposal threshold.

The new Governor's address, the proposer, and the final proposal text carry `TODO`s. The script
reverts until the first two are set — the proposal cannot be submitted before the new Governor is
deployed and a proposer is confirmed.
The deployed Governor address is fixed in the concrete. The proposer and final proposal text still
carry `TODO`s, and the script reverts until a proposer is confirmed.

Dry-run first to simulate the proposal and review the transaction it would send:

Expand Down Expand Up @@ -211,11 +220,12 @@ forge script script/RecallExpiredFranchisersMainnet.s.sol:RecallExpiredFranchise

## Testing

The integration tests (`test/*.integration.t.sol`) run against a fork of Ethereum mainnet pinned
to a fixed block, and simulate the entire upgrade the way it will actually happen: the real deploy
script deploys the new Governor onto the fork, the real proposal script submits the upgrade
proposal to the currently active Governor, and delegates vote it through to execution — after
which the suites exercise the upgraded Governor in place:
The integration tests (`test/*.integration.t.sol`) run against forks of Ethereum mainnet pinned to
fixed blocks. The `…MainnetScript` provenance forks from before the real deployment and runs the
production deploy script. The `…MainnetDeployed` provenance forks from the first block after the
deployment and binds to Gitcoin Governor Charlie's live mainnet bytecode. Both provenances then
submit the upgrade proposal to the currently active Governor and vote it through to execution before
exercising the upgraded Governor in place:

- `GovernorUpgradeProposal` — the upgrade proposal's lifecycle on the active Governor: passing it
hands the Timelock to the new Governor; defeating it leaves the current Governor in control.
Expand Down Expand Up @@ -245,10 +255,15 @@ the real deploy script and drive the operations scripts end-to-end:
guard protecting live positions.

Each suite is written against an abstract base that leaves *how the system comes into being* to a
small concrete contract at the bottom of the file. Today each file has a `…MainnetScript` concrete
that deploys via the real deploy scripts; once the new Governor and the Franchiser system are live
on mainnet, `…MainnetDeployed` concretes pointing at the deployed addresses can rerun the same
suites as a post-deployment acceptance check.
small concrete contract at the bottom of the file. The four Governor suites have both
`…MainnetScript` and `…MainnetDeployed` concretes. Run the deployed-bytecode acceptance suites with:

```sh
forge test --match-contract '.*MainnetDeployed'
```

The Franchiser suites currently retain their `…MainnetScript` provenance; deployed concretes will
be added once the Franchiser system is live on mainnet.

## License

Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

37 changes: 15 additions & 22 deletions script/DeployGitcoinGovernorWithGuardianMainnet.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,43 +6,36 @@ import {DeployGitcoinGovernorWithGuardian} from "script/DeployGitcoinGovernorWit
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.
/// @dev The proposal threshold and voting period mirror the active "GTC Governor Bravo"
/// (0x9D4C63565D5618310271bF3F3c01b2954C1D1639). The quorum, voting delay, late-quorum vote
/// extension, Proposal Guardian, and name are configured specifically for this upgrade.
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";
// Sets the EIP-712 domain name used when signing votes.
string constant GOVERNOR_NAME = "Gitcoin Governor Charlie";

// 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;
// Governance parameters confirmed for the upgrade. The voting period and proposal threshold
// remain unchanged from the existing Governor.
uint256 constant INITIAL_QUORUM = 1_500_000e18;
uint48 constant INITIAL_VOTING_DELAY = 14_400;
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;
// Guarantees approximately 24 hours between a proposal reaching quorum and voting ending,
// assuming 12-second blocks.
uint48 constant INITIAL_VOTE_EXTENSION = 7200;

// 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;
// Gitcoin's designated Proposal Guardian.
address constant INITIAL_PROPOSAL_GUARDIAN = 0x5743E35477363241300FcEdc2F5eB0195F300817;

// 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();
DeployGitcoinGovernorWithGuardian.run();
}

function _getDeploymentParams() internal pure override returns (DeploymentParams memory) {
Expand Down
6 changes: 2 additions & 4 deletions script/ProposeGovernorUpgradeMainnet.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,9 @@ import {ProposeGovernorUpgrade} from "script/ProposeGovernorUpgrade.s.sol";
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.
// Gitcoin Governor Charlie, deployed to mainnet on August 17, 2026.
GitcoinGovernorWithGuardian constant NEW_GOVERNOR =
GitcoinGovernorWithGuardian(payable(address(0)));
GitcoinGovernorWithGuardian(payable(0xef41CbD211076E8b1901e214Bf751d404cf06638));

// 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
Expand Down
22 changes: 15 additions & 7 deletions test/GovernorUpgradeProposal.integration.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,17 @@ import {GitcoinGovernorUpgradeTestBase} from "test/helpers/GitcoinGovernorUpgrad
// upgrade proposal — submitted to the old Governor by the real proposal script — is walked
// through passing, failing, and post-upgrade outcomes for control of the Timelock.
abstract contract GovernorUpgradeProposalTest is GitcoinGovernorUpgradeTestBase {
function test_DeploysTheNewGovernorWithTheMainnetConfiguration() external view {
assertEq(governor.name(), "GTC Governor Bravo");
function test_NewGovernorHasTheMainnetConfiguration() external view {
assertEq(governor.name(), "Gitcoin Governor Charlie");
assertEq(address(governor.token()), address(GTC_TOKEN));
assertEq(governor.timelock(), address(TIMELOCK));
// These values mirror the active Governor, read from mainnet when the tests were written.
assertEq(governor.votingDelay(), 13_140);
assertEq(governor.votingDelay(), 14_400);
assertEq(governor.votingPeriod(), 40_320);
// The proposal threshold and voting period remain unchanged from the active Governor.
assertEq(governor.proposalThreshold(), 150_000e18);
assertEq(governor.quorum(block.number), QUORUM);
assertEq(governor.lateQuorumVoteExtension(), VOTE_EXTENSION);
// The placeholder guardian from the mainnet deploy config; update this assertion when the
// security-council guardian address is confirmed.
assertEq(governor.proposalGuardian(), address(TIMELOCK));
assertEq(governor.proposalGuardian(), PROPOSAL_GUARDIAN);
assertEq(
governor.COUNTING_MODE(), "support=bravo,fractional&quorum=for,abstain&params=fractional"
);
Expand Down Expand Up @@ -157,3 +155,13 @@ contract GovernorUpgradeProposalMainnetScript is GovernorUpgradeProposalTest {
return _deployGovernorWithMainnetScript();
}
}

contract GovernorUpgradeProposalMainnetDeployed is GovernorUpgradeProposalTest {
function _setUpNetwork() internal override {
_createMainnetGovernorPostDeploymentFork();
}

function _fetchOrDeploySystem() internal view override returns (GitcoinGovernorWithGuardian) {
return _fetchDeployedGovernor();
}
}
2 changes: 1 addition & 1 deletion test/PostUpgradeFranchiserDelegation.integration.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ abstract contract PostUpgradeFranchiserDelegationTest is PostUpgradeFranchiserTe
uint256 _initialWeight = GTC_TOKEN.getCurrentVotes(LEFTERIS);
if (_initialWeight == 0) {
revert(
"Test scaffolding: lefteris.eth has no delegated weight at FORK_BLOCK; pick a delegate "
"Test scaffolding: lefteris.eth has no delegated weight at the pinned fork block; pick a delegate "
"with existing weight for the stacking scenario"
);
}
Expand Down
10 changes: 10 additions & 0 deletions test/PostUpgradeGovernance.integration.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -299,3 +299,13 @@ contract PostUpgradeGovernanceMainnetScript is PostUpgradeGovernanceTest {
return _deployGovernorWithMainnetScript();
}
}

contract PostUpgradeGovernanceMainnetDeployed is PostUpgradeGovernanceTest {
function _setUpNetwork() internal override {
_createMainnetGovernorPostDeploymentFork();
}

function _fetchOrDeploySystem() internal view override returns (GitcoinGovernorWithGuardian) {
return _fetchDeployedGovernor();
}
}
10 changes: 10 additions & 0 deletions test/PostUpgradeProposalGuardian.integration.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -223,3 +223,13 @@ contract PostUpgradeProposalGuardianMainnetScript is PostUpgradeProposalGuardian
return _deployGovernorWithMainnetScript();
}
}

contract PostUpgradeProposalGuardianMainnetDeployed is PostUpgradeProposalGuardianTest {
function _setUpNetwork() internal override {
_createMainnetGovernorPostDeploymentFork();
}

function _fetchOrDeploySystem() internal view override returns (GitcoinGovernorWithGuardian) {
return _fetchDeployedGovernor();
}
}
32 changes: 20 additions & 12 deletions test/PostUpgradeQuorumBehavior.integration.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,18 @@ abstract contract PostUpgradeQuorumBehaviorTest is GitcoinGovernorPostUpgradeTes
"rebalance the raised-quorum scenarios"
);
}
if (_votingWeightOf(KEV) < LOWERED_QUORUM) {
revert("kev.eth no longer clears the lowered quorum; rebalance the lowered-quorum scenario");
}
if (_votingWeightOf(KEV) >= QUORUM) {
revert(
"kev.eth now single-handedly clears the original quorum; "
"rebalance the lowered-quorum scenario"
);
}
uint256 _blocWeight = _votingWeightOf(PROPOSER) + _votingWeightOf(ANON_GNOSIS_SAFE)
+ _votingWeightOf(EVENT_HORIZON);
if (_blocWeight < LOWERED_QUORUM) {
revert(
"The sniping bloc no longer clears the lowered quorum; rebalance the late-quorum scenario"
);
}
if (_blocWeight >= QUORUM) {
revert(
"The sniping bloc now clears the original quorum; rebalance the lowered-quorum scenario"
);
}
if (_votingWeightOf(KEV) <= _blocWeight) {
revert("kev.eth no longer out-weighs the sniping bloc; rebalance the late-quorum scenario");
}
Expand Down Expand Up @@ -103,14 +99,16 @@ abstract contract PostUpgradeQuorumBehaviorTest is GitcoinGovernorPostUpgradeTes
function test_ProposalMeetingOnlyTheLoweredQuorumSucceedsAndExecutes() external {
_setQuorumViaProposal(LOWERED_QUORUM);

// kev.eth alone clears the lowered quorum but would have fallen short of the original one
// (guarded in setUp).
// The minority bloc clears the lowered quorum but falls short of the original one (guarded in
// setUp).
address _receiver = makeAddr("receiver");
ProposalDetails memory _proposal =
_buildGtcSendProposal(_receiver, 1000e18, "Send GTC under the lowered quorum");
_submitProposal(_proposal);
_jumpToProposalActive(_proposal.id);
_castVote(KEV, _proposal.id, FOR);
_castVote(PROPOSER, _proposal.id, FOR);
_castVote(ANON_GNOSIS_SAFE, _proposal.id, FOR);
_castVote(EVENT_HORIZON, _proposal.id, FOR);
_jumpPastProposalDeadline(_proposal.id);

assertEq(governor.state(_proposal.id), IGovernor.ProposalState.Succeeded);
Expand Down Expand Up @@ -248,3 +246,13 @@ contract PostUpgradeQuorumBehaviorMainnetScript is PostUpgradeQuorumBehaviorTest
return _deployGovernorWithMainnetScript();
}
}

contract PostUpgradeQuorumBehaviorMainnetDeployed is PostUpgradeQuorumBehaviorTest {
function _setUpNetwork() internal override {
_createMainnetGovernorPostDeploymentFork();
}

function _fetchOrDeploySystem() internal view override returns (GitcoinGovernorWithGuardian) {
return _fetchDeployedGovernor();
}
}
Loading
Loading