Skip to content
Open
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
53 changes: 38 additions & 15 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,10 @@ secret). CI supplies it via the `MAINNET_RPC_URL` repository secret.
is reserved for the claims a test makes about the system under test; checks on test assumptions
or scaffolding (setUp fork state, helper lifecycle checkpoints, scenario preconditions) instead
revert with a developer-aimed message naming the broken assumption, so a failure reads as
"repair the test setup," not "a behavior regressed."
"repair the test setup," not "a behavior regressed." Fuzz-run caps for the fork suites go on
each provenance concrete as contract-level `/// forge-config:` lines: forge silently ignores
inline config on test functions inherited from an abstract suite, so a function-level cap there
does nothing and the fuzz test falls back to the profile's run count (5,000 under `ci`).
- **Keep docs current.** `README.md` is intentionally lightweight and reflects the project's
in-progress status. As scripts, tests, and contracts mature, update the README in the **same change**
that introduces them — document a script's usage when the script lands, and drop the "under
Expand All @@ -252,36 +255,56 @@ secret). CI supplies it via the `MAINNET_RPC_URL` repository secret.
**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 upgrade proposal is on-chain.** It was submitted to the old Governor directly by kev.eth,
not through the proposal script, in block `26_041_897` (transaction
`0xb589c7fcb916860feefeef54da017800787e48952fc739b6d1ab50b34ca77e89`), with id
`0xefb5ffee46ab14020294f926e242a2fa79f096de6577be73d69429207a464489`. Voting runs from block
`26_055_037` to `26_095_357`. Its two actions match the proposal script's exactly; its
description is the stakeholders' own 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/`;
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).
(settable + late-quorum), and the Proposal Guardian. Shared helpers live in `test/helpers/`.
Three provenance concretes exist, chosen through the `_setUpNetwork`, `_fetchOrDeploySystem`,
and `_fetchOrSubmitUpgradeProposal` hooks:
- `…MainnetScript` forks from before deployment, runs the production deploy script, and submits
the upgrade proposal with the proposal script.
- `…MainnetDeployed` (the four Governor suites only) forks from the first block after
deployment, binds to the production bytecode, and submits the upgrade proposal with the
proposal script.
- `…MainnetProposed` forks from the first block after the upgrade proposal's submission, binds
to the production bytecode, and adopts the live proposal. Its id and description are read from
the old Governor's `ProposalCreated` event via `vm.eth_getLogs`. Its actions come from the
tests' independent `_upgradeProposalDetails` mirror, so the id matching proves the live
proposal carries exactly the expected actions.

Proposals are voted through by an electorate of **real delegates** whose live weights are read
from the fork in `setUp`. The suite pins `GOVERNOR_PRE_DEPLOYMENT_BLOCK`,
`GOVERNOR_POST_DEPLOYMENT_BLOCK`, and `UPGRADE_PROPOSAL_POST_SUBMISSION_BLOCK` (alongside
`UPGRADE_PROPOSAL_SUBMISSION_BLOCK` and `SUBMITTED_UPGRADE_PROPOSAL_ID`) in
`test/helpers/GitcoinGovernorUpgradeTestBase.sol`; when bumping a fork block, 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]`,
`ProposeFranchiserRecall[Mainnet]`, and `RecallExpiredFranchisers[Mainnet]`. The deploy script
dry-runs clean against a mainnet fork; the proposal and sweep concretes carry `TODO`s (factory
and new-Governor addresses, proposer, per-round delegations) and revert until those are set.
- The **Franchiser fork integration suites** (`test/PostUpgradeFranchiser*.integration.t.sol`)
are in place: each runs the full Governor upgrade in `setUp` (the production sequence), deploys
the Franchiser system with the real deploy script via a `_fetchOrDeployFranchiser` provenance
hook, and drives the operations scripts through constructor-injected test configs in
are in place, each with `…MainnetScript` and `…MainnetProposed` concretes. Each runs the full
Governor upgrade in `setUp` (the production sequence), deploys the Franchiser system with the
real deploy script via a `_fetchOrDeployFranchiser` provenance hook, and drives the operations
scripts through constructor-injected test configs in
`test/helpers/`. Coverage spans delegation rounds (fresh/existing delegatees, top-ups and
expiration overwrites, zero-amount adjustments, defeats), early recalls (including
sub-delegation clawback and the in-flight-snapshot property — a recall cannot strip weight from
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 upgrade proposal's proposer and final text with Gitcoin stakeholders, then
run the upgrade proposal (see [Deliverables](#deliverables)).
- Up next: the DAO votes on the upgrade proposal; once it executes, Franchiser adoption follows
(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
31 changes: 21 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,11 +221,19 @@ forge script script/RecallExpiredFranchisersMainnet.s.sol:RecallExpiredFranchise
## Testing

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:
fixed blocks, under three provenances:

- `…MainnetScript` forks from before the real deployment, runs the production deploy script, and
submits the upgrade proposal with the proposal script.
- `…MainnetDeployed` forks from the first block after the deployment, binds to Gitcoin Governor
Charlie's live mainnet bytecode, and submits the upgrade proposal with the proposal script.
- `…MainnetProposed` forks from the first block after the upgrade proposal was submitted on-chain
(in block 26,041,897), binds to the live Governor, and adopts the live proposal. Its id and
description are read from the chain, while the actions it must carry come from the tests' own
independent copy, so this provenance verifies the exact proposal delegates are voting on.

Each provenance then votes the upgrade proposal through to execution on the currently active
Governor 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 @@ -254,16 +262,19 @@ the real deploy script and drive the operations scripts end-to-end:
on-chain candidate filtering, the weight that persists until a sweep actually runs, and the
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. The four Governor suites have both
`…MainnetScript` and `…MainnetDeployed` concretes. Run the deployed-bytecode acceptance suites with:
Each suite is written against an abstract base that leaves *how the system and its upgrade
proposal come into being* to a small concrete contract at the bottom of the file. Every suite has
`…MainnetScript` and `…MainnetProposed` concretes, and the four Governor suites also have
`…MainnetDeployed`. Run the deployed-bytecode acceptance suites, or the suites against the live
upgrade proposal, with:

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

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

## License

Expand Down
41 changes: 33 additions & 8 deletions test/GovernorUpgradeProposal.integration.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import {IGovernor} from "@openzeppelin/contracts/governance/IGovernor.sol";
import {GitcoinGovernorWithGuardian} from "src/GitcoinGovernorWithGuardian.sol";
import {GitcoinGovernorUpgradeTestBase} from "test/helpers/GitcoinGovernorUpgradeTestBase.sol";

// Exercises the upgrade itself: the new Governor is deployed by the real deploy script, and the
// 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.
// Exercises the upgrade itself: the new Governor, deployed by the real deploy script or bound to
// the live deployment, and the upgrade proposal, submitted to the old Governor by the real
// proposal script or bound to the one live on mainnet, are walked through passing, failing, and
// post-upgrade outcomes for control of the Timelock.
abstract contract GovernorUpgradeProposalTest is GitcoinGovernorUpgradeTestBase {
function test_NewGovernorHasTheMainnetConfiguration() external view {
assertEq(governor.name(), "Gitcoin Governor Charlie");
Expand Down Expand Up @@ -40,19 +41,21 @@ abstract contract GovernorUpgradeProposalTest is GitcoinGovernorUpgradeTestBase
assertTrue(governor.proposalNeedsQueuing(type(uint256).max));
}

function test_SubmitsTheUpgradeProposalWithTheExpectedActions() external {
_submitUpgradeProposal();
function test_UpgradeProposalCarriesTheExpectedActions() external {
// Calls the provenance hook directly rather than through _proposeUpgrade, whose scaffolding
// guard would preempt the assertion this test exists to make.
(upgradeProposalId, upgradeProposalDescription) = _fetchOrSubmitUpgradeProposal();

// The id the old Governor assigned matches the id computed from the actions the proposal is
// expected to carry, proving the script proposed exactly the setPendingAdmin + __acceptAdmin
// expected to carry, proving the proposal holds exactly the setPendingAdmin + __acceptAdmin
// pair targeting this deployment.
assertEq(upgradeProposalId, _upgradeProposalDetails().id);
assertEq(OLD_GOVERNOR.state(upgradeProposalId), IGovernor.ProposalState.Pending);
assertGt(OLD_GOVERNOR.proposalSnapshot(upgradeProposalId), block.number);
}

function test_PassedUpgradeProposalTransfersTimelockControlToTheNewGovernor() external {
_submitUpgradeProposal();
_proposeUpgrade();

// The electorate passes the proposal.
_passUpgradeProposal();
Expand Down Expand Up @@ -82,7 +85,7 @@ abstract contract GovernorUpgradeProposalTest is GitcoinGovernorUpgradeTestBase
}

function test_DefeatedUpgradeProposalLeavesTheOldGovernorGoverning() external {
_submitUpgradeProposal();
_proposeUpgrade();

// The electorate votes the upgrade down.
_defeatUpgradeProposal();
Expand Down Expand Up @@ -154,6 +157,10 @@ contract GovernorUpgradeProposalMainnetScript is GovernorUpgradeProposalTest {
function _fetchOrDeploySystem() internal override returns (GitcoinGovernorWithGuardian) {
return _deployGovernorWithMainnetScript();
}

function _fetchOrSubmitUpgradeProposal() internal override returns (uint256, string memory) {
return _submitUpgradeProposalWithScript();
}
}

contract GovernorUpgradeProposalMainnetDeployed is GovernorUpgradeProposalTest {
Expand All @@ -164,4 +171,22 @@ contract GovernorUpgradeProposalMainnetDeployed is GovernorUpgradeProposalTest {
function _fetchOrDeploySystem() internal view override returns (GitcoinGovernorWithGuardian) {
return _fetchDeployedGovernor();
}

function _fetchOrSubmitUpgradeProposal() internal override returns (uint256, string memory) {
return _submitUpgradeProposalWithScript();
}
}

contract GovernorUpgradeProposalMainnetProposed is GovernorUpgradeProposalTest {
function _setUpNetwork() internal override {
_createMainnetUpgradeProposalPostSubmissionFork();
}

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

function _fetchOrSubmitUpgradeProposal() internal view override returns (uint256, string memory) {
return _fetchSubmittedUpgradeProposal();
}
}
37 changes: 34 additions & 3 deletions test/PostUpgradeFranchiserDelegation.integration.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,8 @@ abstract contract PostUpgradeFranchiserDelegationTest is PostUpgradeFranchiserTe
assertEq(_forVotes, _amount);
}

/// forge-config: default.fuzz.runs = 25
/// forge-config: ci.fuzz.runs = 25
/// forge-config: lite.fuzz.runs = 5
// This suite's fuzz-run caps sit on the provenance concretes at the bottom of the file: forge
// ignores inline config on test functions inherited from an abstract suite.
function testFuzz_PassedDelegationProposalFundsAFreshDelegateeWithAnyTreasuryAmount(
uint256 _amount,
uint256 _expiration
Expand Down Expand Up @@ -333,6 +332,9 @@ abstract contract PostUpgradeFranchiserDelegationTest is PostUpgradeFranchiserTe
}
}

/// forge-config: default.fuzz.runs = 25
/// forge-config: ci.fuzz.runs = 25
/// forge-config: lite.fuzz.runs = 5
contract PostUpgradeFranchiserDelegationMainnetScript is PostUpgradeFranchiserDelegationTest {
function _setUpNetwork() internal override {
_createMainnetFork();
Expand All @@ -342,6 +344,35 @@ contract PostUpgradeFranchiserDelegationMainnetScript is PostUpgradeFranchiserDe
return _deployGovernorWithMainnetScript();
}

function _fetchOrSubmitUpgradeProposal() internal override returns (uint256, string memory) {
return _submitUpgradeProposalWithScript();
}

function _fetchOrDeployFranchiser()
internal
override
returns (FranchiserExpiryFactory, FranchiserLens)
{
return _deployFranchiserWithMainnetScript();
}
}

/// forge-config: default.fuzz.runs = 25
/// forge-config: ci.fuzz.runs = 25
/// forge-config: lite.fuzz.runs = 5
contract PostUpgradeFranchiserDelegationMainnetProposed is PostUpgradeFranchiserDelegationTest {
function _setUpNetwork() internal override {
_createMainnetUpgradeProposalPostSubmissionFork();
}

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

function _fetchOrSubmitUpgradeProposal() internal view override returns (uint256, string memory) {
return _fetchSubmittedUpgradeProposal();
}

function _fetchOrDeployFranchiser()
internal
override
Expand Down
26 changes: 26 additions & 0 deletions test/PostUpgradeFranchiserDeploy.integration.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,32 @@ contract PostUpgradeFranchiserDeployMainnetScript is PostUpgradeFranchiserDeploy
return _deployGovernorWithMainnetScript();
}

function _fetchOrSubmitUpgradeProposal() internal override returns (uint256, string memory) {
return _submitUpgradeProposalWithScript();
}

function _fetchOrDeployFranchiser()
internal
override
returns (FranchiserExpiryFactory, FranchiserLens)
{
return _deployFranchiserWithMainnetScript();
}
}

contract PostUpgradeFranchiserDeployMainnetProposed is PostUpgradeFranchiserDeployTest {
function _setUpNetwork() internal override {
_createMainnetUpgradeProposalPostSubmissionFork();
}

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

function _fetchOrSubmitUpgradeProposal() internal view override returns (uint256, string memory) {
return _fetchSubmittedUpgradeProposal();
}

function _fetchOrDeployFranchiser()
internal
override
Expand Down
37 changes: 34 additions & 3 deletions test/PostUpgradeFranchiserExpiry.integration.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,8 @@ abstract contract PostUpgradeFranchiserExpiryTest is PostUpgradeFranchiserTestBa
assertEq(GTC_TOKEN.getCurrentVotes(_delegatee), 0);
}

/// forge-config: default.fuzz.runs = 25
/// forge-config: ci.fuzz.runs = 25
/// forge-config: lite.fuzz.runs = 5
// This suite's fuzz-run caps sit on the provenance concretes at the bottom of the file: forge
// ignores inline config on test functions inherited from an abstract suite.
function testFuzz_AnyAccountCanSweepAnExpiredPosition(address _caller) external {
address _delegatee = makeAddr("expiringDelegatee");
uint256 _amount = 500_000e18;
Expand Down Expand Up @@ -136,6 +135,9 @@ abstract contract PostUpgradeFranchiserExpiryTest is PostUpgradeFranchiserTestBa
}
}

/// forge-config: default.fuzz.runs = 25
/// forge-config: ci.fuzz.runs = 25
/// forge-config: lite.fuzz.runs = 5
contract PostUpgradeFranchiserExpiryMainnetScript is PostUpgradeFranchiserExpiryTest {
function _setUpNetwork() internal override {
_createMainnetFork();
Expand All @@ -145,6 +147,35 @@ contract PostUpgradeFranchiserExpiryMainnetScript is PostUpgradeFranchiserExpiry
return _deployGovernorWithMainnetScript();
}

function _fetchOrSubmitUpgradeProposal() internal override returns (uint256, string memory) {
return _submitUpgradeProposalWithScript();
}

function _fetchOrDeployFranchiser()
internal
override
returns (FranchiserExpiryFactory, FranchiserLens)
{
return _deployFranchiserWithMainnetScript();
}
}

/// forge-config: default.fuzz.runs = 25
/// forge-config: ci.fuzz.runs = 25
/// forge-config: lite.fuzz.runs = 5
contract PostUpgradeFranchiserExpiryMainnetProposed is PostUpgradeFranchiserExpiryTest {
function _setUpNetwork() internal override {
_createMainnetUpgradeProposalPostSubmissionFork();
}

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

function _fetchOrSubmitUpgradeProposal() internal view override returns (uint256, string memory) {
return _fetchSubmittedUpgradeProposal();
}

function _fetchOrDeployFranchiser()
internal
override
Expand Down
Loading
Loading