diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c5a0bb4..21b19ab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,10 +40,29 @@ jobs: with: version: nightly + - name: Cache fork requests + uses: actions/cache@v3 + with: + path: ~/.foundry/cache + key: ${{ runner.os }}-foundry-network-fork-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-foundry-network-fork- + + # https://twitter.com/PaulRBerg/status/1611116650664796166 + - name: Generate fuzz seed with 1 week TTL + run: > + echo "FOUNDRY_FUZZ_SEED=$( + echo $(($EPOCHSECONDS - $EPOCHSECONDS % 604800)) + )" >> $GITHUB_ENV + - name: Run tests run: forge test coverage: + env: + DEPLOYER_PRIVATE_KEY: ${{ secrets.DEPLOYER_PRIVATE_KEY }} + MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }} + PROPOSER_PRIVATE_KEY: ${{ secrets.PROPOSER_PRIVATE_KEY }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -53,6 +72,21 @@ jobs: with: version: nightly + - name: Cache fork requests + uses: actions/cache@v3 + with: + path: ~/.foundry/cache + key: ${{ runner.os }}-foundry-network-fork-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-foundry-network-fork- + + # https://twitter.com/PaulRBerg/status/1611116650664796166 + - name: Recycle the fuzz seed from the test run + run: > + echo "FOUNDRY_FUZZ_SEED=$( + echo $(($EPOCHSECONDS - $EPOCHSECONDS % 604800)) + )" >> $GITHUB_ENV + - name: Run coverage run: forge coverage --report summary --report lcov diff --git a/foundry.toml b/foundry.toml index 0cb3b6b..27eec7d 100644 --- a/foundry.toml +++ b/foundry.toml @@ -5,11 +5,11 @@ verbosity = 3 [profile.ci] - fuzz = { runs = 5000 } - invariant = { runs = 1000 } + fuzz = { runs = 500 } + invariant = { runs = 500 } [profile.lite] - fuzz = { runs = 50 } + fuzz = { runs = 50, seed = 1673481600 } invariant = { runs = 10 } # Speed up compilation and tests during development. optimizer = false diff --git a/src/GitcoinGovernor.sol b/src/GitcoinGovernor.sol index b9fc14f..395208f 100644 --- a/src/GitcoinGovernor.sol +++ b/src/GitcoinGovernor.sol @@ -54,7 +54,7 @@ contract GitcoinGovernor is public view virtual - override (Governor, GovernorTimelockCompound) + override(Governor, GovernorTimelockCompound) returns (bool) { return GovernorTimelockCompound.supportsInterface(interfaceId); @@ -65,7 +65,7 @@ contract GitcoinGovernor is public view virtual - override (Governor, GovernorSettings) + override(Governor, GovernorSettings) returns (uint256) { return GovernorSettings.proposalThreshold(); @@ -76,7 +76,7 @@ contract GitcoinGovernor is public view virtual - override (Governor, GovernorTimelockCompound) + override(Governor, GovernorTimelockCompound) returns (ProposalState) { return GovernorTimelockCompound.state(proposalId); @@ -96,7 +96,7 @@ contract GitcoinGovernor is uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash - ) internal virtual override (Governor, GovernorTimelockCompound) { + ) internal virtual override(Governor, GovernorTimelockCompound) { return GovernorTimelockCompound._execute(proposalId, targets, values, calldatas, descriptionHash); } @@ -107,7 +107,7 @@ contract GitcoinGovernor is uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash - ) internal virtual override (Governor, GovernorTimelockCompound) returns (uint256) { + ) internal virtual override(Governor, GovernorTimelockCompound) returns (uint256) { return GovernorTimelockCompound._cancel(targets, values, calldatas, descriptionHash); } @@ -116,7 +116,7 @@ contract GitcoinGovernor is internal view virtual - override (Governor, GovernorTimelockCompound) + override(Governor, GovernorTimelockCompound) returns (address) { return GovernorTimelockCompound._executor(); diff --git a/test/GitcoinGovernor.t.sol b/test/GitcoinGovernor.t.sol index 9aeed85..0eff158 100644 --- a/test/GitcoinGovernor.t.sol +++ b/test/GitcoinGovernor.t.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.17; import "forge-std/Test.sol"; import {IERC20} from "openzeppelin-contracts/interfaces/IERC20.sol"; +import {IGovernor} from "openzeppelin-contracts/governance/IGovernor.sol"; import {GitcoinGovernor, ICompoundTimelock} from "src/GitcoinGovernor.sol"; import {DeployInput, DeployScript} from "script/Deploy.s.sol"; import {IGovernorAlpha} from "src/interfaces/IGovernorAlpha.sol"; @@ -15,7 +16,7 @@ contract GitcoinGovernorTestHelper is Test, DeployInput { address constant TIMELOCK = 0x57a8865cfB1eCEf7253c27da6B4BC3dAEE5Be518; address constant PROPOSER = 0xc2E2B715d9e302947Ec7e312fd2384b5a1296099; // kbw.eth - GitcoinGovernor governor; + GitcoinGovernor governorBravo; function setUp() public virtual { uint256 _forkBlock = 15_980_096; // The latest block when this test was written @@ -23,20 +24,20 @@ contract GitcoinGovernorTestHelper is Test, DeployInput { DeployScript _deployScript = new DeployScript(); _deployScript.setUp(); - governor = _deployScript.run(); + governorBravo = _deployScript.run(); } } contract GitcoinGovernorDeployTest is GitcoinGovernorTestHelper { function testFuzz_deployment(uint256 _blockNumber) public { - assertEq(governor.name(), "GTC Governor Bravo"); - assertEq(address(governor.token()), GTC_TOKEN); - assertEq(governor.votingDelay(), INITIAL_VOTING_DELAY); - assertEq(governor.votingPeriod(), INITIAL_VOTING_PERIOD); - assertEq(governor.proposalThreshold(), INITIAL_PROPOSAL_THRESHOLD); - assertEq(governor.quorum(_blockNumber), QUORUM); - assertEq(governor.timelock(), TIMELOCK); - assertEq(governor.COUNTING_MODE(), "support=bravo&quorum=for,abstain"); + assertEq(governorBravo.name(), "GTC Governor Bravo"); + assertEq(address(governorBravo.token()), GTC_TOKEN); + assertEq(governorBravo.votingDelay(), INITIAL_VOTING_DELAY); + assertEq(governorBravo.votingPeriod(), INITIAL_VOTING_PERIOD); + assertEq(governorBravo.proposalThreshold(), INITIAL_PROPOSAL_THRESHOLD); + assertEq(governorBravo.quorum(_blockNumber), QUORUM); + assertEq(governorBravo.timelock(), TIMELOCK); + assertEq(governorBravo.COUNTING_MODE(), "support=bravo&quorum=for,abstain"); } } @@ -45,9 +46,13 @@ contract GitcoinGovernorProposalTestHelper is GitcoinGovernorTestHelper { IGovernorAlpha governorAlpha = IGovernorAlpha(0xDbD27635A534A3d3169Ef0498beB56Fb9c937489); IGTC gtcToken = IGTC(GTC_TOKEN); + address constant USDC_ADDRESS = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + address constant RAD_ADDRESS = 0x31c8EAcBFFdD875c74b94b077895Bd78CF1E64A3; + IERC20 usdcToken = IERC20(USDC_ADDRESS); + IERC20 radToken = IERC20(RAD_ADDRESS); ICompoundTimelock timelock = ICompoundTimelock(payable(TIMELOCK)); uint256 initialProposalCount; - uint256 proposalId; + uint256 upgradeProposalId; address[] delegates = [ PROPOSER, // kbw.eth (~1.8M) 0x2df9a188fBE231B0DC36D14AcEb65dEFbB049479, // janineleger.eth (~1.53M) @@ -71,61 +76,73 @@ contract GitcoinGovernorProposalTestHelper is GitcoinGovernorTestHelper { initialProposalCount = governorAlpha.proposalCount(); ProposeScript _proposeScript = new ProposeScript(); - proposalId = _proposeScript.run(governor); + upgradeProposalId = _proposeScript.run(governorBravo); } //--------------- HELPERS ---------------// - function proposalStartBlock() public view returns (uint256) { - (,,, uint256 _startBlock,,,,,) = governorAlpha.proposals(proposalId); + function _randomERC20Token(uint256 _seed) internal view returns (IERC20 _token) { + if (_seed % 3 == 0) _token = IERC20(address(gtcToken)); + if (_seed % 3 == 1) _token = usdcToken; + if (_seed % 3 == 2) _token = radToken; + } + + function _upgradeProposalStartBlock() internal view returns (uint256) { + (,,, uint256 _startBlock,,,,,) = governorAlpha.proposals(upgradeProposalId); return _startBlock; } - function proposalEndBlock() public view returns (uint256) { - (,,,, uint256 _endBlock,,,,) = governorAlpha.proposals(proposalId); + function _upgradeProposalEndBlock() internal view returns (uint256) { + (,,,, uint256 _endBlock,,,,) = governorAlpha.proposals(upgradeProposalId); return _endBlock; } - function proposalEta() public view returns (uint256) { - (,, uint256 _eta,,,,,,) = governorAlpha.proposals(proposalId); + function _upgradeProposalEta() internal view returns (uint256) { + (,, uint256 _eta,,,,,,) = governorAlpha.proposals(upgradeProposalId); return _eta; } - function jumpToActiveProposal() public { - vm.roll(proposalStartBlock() + 1); + function _jumpToActiveUpgradeProposal() internal { + vm.roll(_upgradeProposalStartBlock() + 1); } - function jumpToVoteComplete() public { - vm.roll(proposalEndBlock() + 1); + function _jumpToUpgradeVoteComplete() internal { + vm.roll(_upgradeProposalEndBlock() + 1); } - function jumpPastProposalEta() public { + function _jumpPastProposalEta() internal { vm.roll(block.number + 1); // move up one block so we're not in the same block as when queued - vm.warp(proposalEta() + 1); // jump past the eta timestamp + vm.warp(_upgradeProposalEta() + 1); // jump past the eta timestamp } - function delegatesVoteOnProposal(bool _support) public { + function _delegatesVoteOnUpgradeProposal(bool _support) internal { for (uint256 _index = 0; _index < delegates.length; _index++) { vm.prank(delegates[_index]); - governorAlpha.castVote(proposalId, _support); + governorAlpha.castVote(upgradeProposalId, _support); } } - function passProposal() public { - jumpToActiveProposal(); - delegatesVoteOnProposal(true); - jumpToVoteComplete(); + function _passUpgradeProposal() internal { + _jumpToActiveUpgradeProposal(); + _delegatesVoteOnUpgradeProposal(true); + _jumpToUpgradeVoteComplete(); + } + + function _defeatUpgradeProposal() internal { + _jumpToActiveUpgradeProposal(); + _delegatesVoteOnUpgradeProposal(false); + _jumpToUpgradeVoteComplete(); } - function defeatProposal() public { - jumpToActiveProposal(); - delegatesVoteOnProposal(false); - jumpToVoteComplete(); + function _passAndQueueUpgradeProposal() internal { + _passUpgradeProposal(); + governorAlpha.queue(upgradeProposalId); } - function passAndQueueProposal() public { - passProposal(); - governorAlpha.queue(proposalId); + function _upgradeToBravoGovernor() internal { + _passAndQueueUpgradeProposal(); + _jumpPastProposalEta(); + governorAlpha.execute(upgradeProposalId); } } @@ -135,7 +152,7 @@ contract GitcoinGovernorProposalTest is GitcoinGovernorProposalTestHelper { assertEq(governorAlpha.proposalCount(), initialProposalCount + 1); // Proposal is in the expected state - uint8 _state = governorAlpha.state(proposalId); + uint8 _state = governorAlpha.state(upgradeProposalId); assertEq(_state, PENDING); // Proposal actions correspond to Governor upgrade @@ -144,10 +161,10 @@ contract GitcoinGovernorProposalTest is GitcoinGovernorProposalTestHelper { uint256[] memory _values, string[] memory _signatures, bytes[] memory _calldatas - ) = governorAlpha.getActions(proposalId); + ) = governorAlpha.getActions(upgradeProposalId); assertEq(_targets.length, 2); assertEq(_targets[0], TIMELOCK); - assertEq(_targets[1], address(governor)); + assertEq(_targets[1], address(governorBravo)); assertEq(_values.length, 2); assertEq(_values[0], 0); assertEq(_values[1], 0); @@ -155,53 +172,53 @@ contract GitcoinGovernorProposalTest is GitcoinGovernorProposalTestHelper { assertEq(_signatures[0], "setPendingAdmin(address)"); assertEq(_signatures[1], "__acceptAdmin()"); assertEq(_calldatas.length, 2); - assertEq(_calldatas[0], abi.encode(address(governor))); + assertEq(_calldatas[0], abi.encode(address(governorBravo))); assertEq(_calldatas[1], ""); } function test_proposalActiveAfterDelay() public { - jumpToActiveProposal(); + _jumpToActiveUpgradeProposal(); // Ensure proposal has become active the block after the voting delay - uint8 _state = governorAlpha.state(proposalId); + uint8 _state = governorAlpha.state(upgradeProposalId); assertEq(_state, ACTIVE); } function testFuzz_ProposerCanCastVote(bool _willSupport) public { - jumpToActiveProposal(); - uint256 _proposerVotes = gtcToken.getPriorVotes(PROPOSER, proposalStartBlock()); + _jumpToActiveUpgradeProposal(); + uint256 _proposerVotes = gtcToken.getPriorVotes(PROPOSER, _upgradeProposalStartBlock()); vm.prank(PROPOSER); - governorAlpha.castVote(proposalId, _willSupport); + governorAlpha.castVote(upgradeProposalId, _willSupport); - IGovernorAlpha.Receipt memory _receipt = governorAlpha.getReceipt(proposalId, PROPOSER); + IGovernorAlpha.Receipt memory _receipt = governorAlpha.getReceipt(upgradeProposalId, PROPOSER); assertEq(_receipt.hasVoted, true); assertEq(_receipt.support, _willSupport); assertEq(_receipt.votes, _proposerVotes); } function test_ProposalSucceedsWhenAllDelegatesVoteFor() public { - passProposal(); + _passUpgradeProposal(); // Ensure proposal state is now succeeded - uint8 _state = governorAlpha.state(proposalId); + uint8 _state = governorAlpha.state(upgradeProposalId); assertEq(_state, SUCCEEDED); } function test_ProposalDefeatedWhenAllDelegatesVoteAgainst() public { - defeatProposal(); + _defeatUpgradeProposal(); // Ensure proposal state is now defeated - uint8 _state = governorAlpha.state(proposalId); + uint8 _state = governorAlpha.state(upgradeProposalId); assertEq(_state, DEFEATED); } function test_ProposalCanBeQueuedAfterSucceeding() public { - passProposal(); - governorAlpha.queue(proposalId); + _passUpgradeProposal(); + governorAlpha.queue(upgradeProposalId); // Ensure proposal can be queued after success - uint8 _state = governorAlpha.state(proposalId); + uint8 _state = governorAlpha.state(upgradeProposalId); assertEq(_state, QUEUED); ( @@ -209,7 +226,7 @@ contract GitcoinGovernorProposalTest is GitcoinGovernorProposalTestHelper { uint256[] memory _values, string[] memory _signatures, bytes[] memory _calldatas - ) = governorAlpha.getActions(proposalId); + ) = governorAlpha.getActions(upgradeProposalId); uint256 _eta = block.timestamp + timelock.delay(); @@ -226,32 +243,24 @@ contract GitcoinGovernorProposalTest is GitcoinGovernorProposalTestHelper { } function test_ProposalCanBeExecutedAfterDelay() public { - passAndQueueProposal(); - jumpPastProposalEta(); + _passAndQueueUpgradeProposal(); + _jumpPastProposalEta(); // Execute the proposal - governorAlpha.execute(proposalId); + governorAlpha.execute(upgradeProposalId); // Ensure the proposal is now executed - uint8 _state = governorAlpha.state(proposalId); + uint8 _state = governorAlpha.state(upgradeProposalId); assertEq(_state, EXECUTED); - // Ensure the governor is now the admin of the timelock - assertEq(timelock.admin(), address(governor)); + // Ensure the governorBravo is now the admin of the timelock + assertEq(timelock.admin(), address(governorBravo)); } } contract GitcoinGovernorAlphaPostProposalTest is GitcoinGovernorProposalTestHelper { - address constant USDC_ADDRESS = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; - address constant RAD_ADDRESS = 0x31c8EAcBFFdD875c74b94b077895Bd78CF1E64A3; - - IERC20 usdcToken = IERC20(USDC_ADDRESS); - IERC20 radToken = IERC20(RAD_ADDRESS); - function setUp() public override { GitcoinGovernorProposalTestHelper.setUp(); - usdcToken = IERC20(USDC_ADDRESS); - radToken = IERC20(RAD_ADDRESS); } function testFuzz_OldGovernorSendsGTCAfterProposalIsDefeated( @@ -265,7 +274,7 @@ contract GitcoinGovernorAlphaPostProposalTest is GitcoinGovernorProposalTestHelp uint256 _initialGtcBalance = gtcToken.balanceOf(_gtcReceiver); // Defeat the proposal to upgrade the Governor - defeatProposal(); + _defeatUpgradeProposal(); // Craft a new proposal to send GTC address[] memory _targets = new address[](1); @@ -330,7 +339,7 @@ contract GitcoinGovernorAlphaPostProposalTest is GitcoinGovernorProposalTestHelp uint256 _initialRadBalance = radToken.balanceOf(_radReceiver); // Defeat the proposal to upgrade the Governor - defeatProposal(); + _defeatUpgradeProposal(); // Craft a new proposal to send amounts of all three tokens address[] memory _targets = new address[](2); @@ -392,9 +401,7 @@ contract GitcoinGovernorAlphaPostProposalTest is GitcoinGovernorProposalTestHelp _gtcAmount = bound(_gtcAmount, 0, _timelockGtcBalance); // Pass and execute the proposal to upgrade the Governor - passAndQueueProposal(); - jumpPastProposalEta(); - governorAlpha.execute(proposalId); + _upgradeToBravoGovernor(); // Craft a new proposal to send GTC address[] memory _targets = new address[](1); @@ -427,3 +434,476 @@ contract GitcoinGovernorAlphaPostProposalTest is GitcoinGovernorProposalTestHelp governorAlpha.queue(_newProposalId); } } + +contract NewGitcoinGovernorProposalTest is GitcoinGovernorProposalTestHelper { + // From GovernorCountingSimple + uint8 constant AGAINST = 0; + uint8 constant FOR = 1; + uint8 constant ABSTAIN = 2; + + function assumeReceiver(address _receiver) public { + // We don't want the receiver to be the Timelock, as that would make our + // assertions less meaningful -- most of our tests want to confirm that + // proposals can cause tokens to be sent *from* the timelock to somewhere + // else. We also can't have the receiver be the zero address because GTC + // blocks transfers to the zero address -- see line 546: + // https://etherscan.io/address/0xDe30da39c46104798bB5aA3fe8B9e0e1F348163F#code + vm.assume(_receiver != TIMELOCK && _receiver != address(0x0)); + } + + function buildProposalData(string memory _signature, bytes memory _calldata) + public + pure + returns (bytes memory) + { + return abi.encodePacked(bytes4(keccak256(bytes(_signature))), _calldata); + } + + function jumpToActiveProposal(uint256 _proposalId) public { + uint256 _snapshot = governorBravo.proposalSnapshot(_proposalId); + vm.roll(_snapshot + 1); + } + + function jumpToVotingComplete(uint256 _proposalId) public { + // Jump one block past the proposal voting deadline + uint256 _deadline = governorBravo.proposalDeadline(_proposalId); + vm.roll(_deadline + 1); + } + + function _jumpPastProposalEta(uint256 _proposalId) public { + uint256 _eta = governorBravo.proposalEta(_proposalId); + vm.roll(block.number + 1); + vm.warp(_eta + 1); + } + + function delegatesVoteOnBravoGovernor(uint256 _proposalId, uint8 _support) public { + require(_support < 3, "Invalid value for support"); + + for (uint256 _index = 0; _index < delegates.length; _index++) { + vm.prank(delegates[_index]); + governorBravo.castVote(_proposalId, _support); + } + } + + function submitTokenSendProposal(address _token, uint256 _amount, address _receiver) + public + returns (uint256, address[] memory, uint256[] memory, bytes[] memory, string memory) + { + // Craft a new proposal to send GTC + address[] memory _targets = new address[](1); + uint256[] memory _values = new uint256[](1); + bytes[] memory _calldatas = new bytes[](1); + + _targets[0] = _token; + _values[0] = 0; + _calldatas[0] = buildProposalData("transfer(address,uint256)", abi.encode(_receiver, _amount)); + string memory _description = "Transfer some tokens from the new Governor"; + + // Submit the new proposal + vm.prank(PROPOSER); + uint256 _newProposalId = governorBravo.propose(_targets, _values, _calldatas, _description); + + return (_newProposalId, _targets, _values, _calldatas, _description); + } + + function assertEq(IGovernor.ProposalState _actual, IGovernor.ProposalState _expected) public { + assertEq(uint8(_actual), uint8(_expected)); + } + + function testFuzz_NewGovernorCanReceiveNewProposal(uint256 _gtcAmount, address _gtcReceiver) + public + { + assumeReceiver(_gtcReceiver); + _upgradeToBravoGovernor(); + (uint256 _newProposalId,,,,) = + submitTokenSendProposal(address(gtcToken), _gtcAmount, _gtcReceiver); + + // Ensure proposal is in the expected state + IGovernor.ProposalState _state = governorBravo.state(_newProposalId); + assertEq(_state, IGovernor.ProposalState.Pending); + } + + function testFuzz_NewGovernorCanDefeatProposal(uint256 _amount, address _receiver, uint256 _seed) + public + { + IERC20 _token = _randomERC20Token(_seed); + assumeReceiver(_receiver); + + _upgradeToBravoGovernor(); + ( + uint256 _newProposalId, + address[] memory _targets, + uint256[] memory _values, + bytes[] memory _calldatas, + string memory _description + ) = submitTokenSendProposal(address(_token), _amount, _receiver); + + // Ensure proposal is in the expected state + IGovernor.ProposalState _state = governorBravo.state(_newProposalId); + assertEq(_state, IGovernor.ProposalState.Pending); + + jumpToActiveProposal(_newProposalId); + + // Ensure the proposal is now Active + _state = governorBravo.state(_newProposalId); + assertEq(_state, IGovernor.ProposalState.Active); + + delegatesVoteOnBravoGovernor(_newProposalId, AGAINST); + jumpToVotingComplete(_newProposalId); + + // Ensure the proposal has failed + _state = governorBravo.state(_newProposalId); + assertEq(_state, IGovernor.ProposalState.Defeated); + + // It should not be possible to queue the proposal + vm.expectRevert("Governor: proposal not successful"); + governorBravo.queue(_targets, _values, _calldatas, keccak256(bytes(_description))); + } + + function testFuzz_NewGovernorCanPassProposalToSendToken( + uint256 _amount, + address _receiver, + uint256 _seed + ) public { + IERC20 _token = _randomERC20Token(_seed); + assumeReceiver(_receiver); + uint256 _timelockTokenBalance = _token.balanceOf(TIMELOCK); + + // bound by the number of tokens the timelock currently controls + _amount = bound(_amount, 0, _timelockTokenBalance); + uint256 _initialTokenBalance = _token.balanceOf(_receiver); + + _upgradeToBravoGovernor(); + ( + uint256 _newProposalId, + address[] memory _targets, + uint256[] memory _values, + bytes[] memory _calldatas, + string memory _description + ) = submitTokenSendProposal(address(_token), _amount, _receiver); + + // Ensure proposal is in the expected state + IGovernor.ProposalState _state = governorBravo.state(_newProposalId); + assertEq(_state, IGovernor.ProposalState.Pending); + + jumpToActiveProposal(_newProposalId); + + // Ensure the proposal is now Active + _state = governorBravo.state(_newProposalId); + assertEq(_state, IGovernor.ProposalState.Active); + + delegatesVoteOnBravoGovernor(_newProposalId, FOR); + jumpToVotingComplete(_newProposalId); + + // Ensure the proposal has succeeded + _state = governorBravo.state(_newProposalId); + assertEq(_state, IGovernor.ProposalState.Succeeded); + + // Queue the proposal + governorBravo.queue(_targets, _values, _calldatas, keccak256(bytes(_description))); + + // Ensure the proposal is queued + _state = governorBravo.state(_newProposalId); + assertEq(_state, IGovernor.ProposalState.Queued); + + _jumpPastProposalEta(_newProposalId); + + // Execute the proposal + governorBravo.execute(_targets, _values, _calldatas, keccak256(bytes(_description))); + + // Ensure the proposal is executed + _state = governorBravo.state(_newProposalId); + assertEq(_state, IGovernor.ProposalState.Executed); + + // Ensure the tokens have been transferred + assertEq(_token.balanceOf(_receiver), _initialTokenBalance + _amount); + assertEq(_token.balanceOf(TIMELOCK), _timelockTokenBalance - _amount); + } + + function testFuzz_NewGovernorCanUpdateSettingsViaSuccessfulProposal( + uint256 _newDelay, + uint256 _newVotingPeriod, + uint256 _newProposalThreshold + ) public { + // The upper bounds are arbitrary here. + _newDelay = bound(_newDelay, 0, 50_000); // about a week at 1 block per 12s + _newVotingPeriod = bound(_newVotingPeriod, 1, 200_000); // about a month + _newProposalThreshold = bound(_newProposalThreshold, 0, 42 ether); + + _upgradeToBravoGovernor(); + + address[] memory _targets = new address[](3); + uint256[] memory _values = new uint256[](3); + bytes[] memory _calldatas = new bytes[](3); + string memory _description = "Update governance settings"; + + _targets[0] = address(governorBravo); + _calldatas[0] = buildProposalData("setVotingDelay(uint256)", abi.encode(_newDelay)); + + _targets[1] = address(governorBravo); + _calldatas[1] = buildProposalData("setVotingPeriod(uint256)", abi.encode(_newVotingPeriod)); + + _targets[2] = address(governorBravo); + _calldatas[2] = + buildProposalData("setProposalThreshold(uint256)", abi.encode(_newProposalThreshold)); + + // Submit the new proposal + vm.prank(PROPOSER); + uint256 _newProposalId = governorBravo.propose(_targets, _values, _calldatas, _description); + + // Ensure proposal is in the expected state + IGovernor.ProposalState _state = governorBravo.state(_newProposalId); + assertEq(_state, IGovernor.ProposalState.Pending); + + jumpToActiveProposal(_newProposalId); + + // Ensure the proposal is now Active + _state = governorBravo.state(_newProposalId); + assertEq(_state, IGovernor.ProposalState.Active); + + delegatesVoteOnBravoGovernor(_newProposalId, FOR); + jumpToVotingComplete(_newProposalId); + + // Ensure the proposal has succeeded + _state = governorBravo.state(_newProposalId); + assertEq(_state, IGovernor.ProposalState.Succeeded); + + // Queue the proposal + governorBravo.queue(_targets, _values, _calldatas, keccak256(bytes(_description))); + + // Ensure the proposal is queued + _state = governorBravo.state(_newProposalId); + assertEq(_state, IGovernor.ProposalState.Queued); + + _jumpPastProposalEta(_newProposalId); + + // Execute the proposal + governorBravo.execute(_targets, _values, _calldatas, keccak256(bytes(_description))); + + // Ensure the proposal is executed + _state = governorBravo.state(_newProposalId); + assertEq(_state, IGovernor.ProposalState.Executed); + + // Confirm that governance settings have updated. + assertEq(governorBravo.votingDelay(), _newDelay); + assertEq(governorBravo.votingPeriod(), _newVotingPeriod); + assertEq(governorBravo.proposalThreshold(), _newProposalThreshold); + } + + function testFuzz_NewGovernorCanPassMixedProposal( + uint256 _amount, + address _receiver, + uint256 _seed + ) public { + IERC20 _token = _randomERC20Token(_seed); + assumeReceiver(_receiver); + uint256 _timelockTokenBalance = _token.balanceOf(TIMELOCK); + + // bound by the number of tokens the timelock currently controls + _amount = bound(_amount, 0, _timelockTokenBalance); + uint256 _initialTokenBalance = _token.balanceOf(_receiver); + + _upgradeToBravoGovernor(); + ( + uint256 _newProposalId, + address[] memory _targets, + uint256[] memory _values, + bytes[] memory _calldatas, + string memory _description + ) = submitTokenSendProposal(address(_token), _amount, _receiver); + + // Ensure proposal is in the expected state + IGovernor.ProposalState _state = governorBravo.state(_newProposalId); + assertEq(_state, IGovernor.ProposalState.Pending); + + jumpToActiveProposal(_newProposalId); + + // Ensure the proposal is now Active + _state = governorBravo.state(_newProposalId); + assertEq(_state, IGovernor.ProposalState.Active); + + // Delegates vote with a mix of For/Against/Abstain with For winning. + vm.prank(PROPOSER); // kbw.eth (~1.8M votes) + governorBravo.castVote(_newProposalId, FOR); + vm.prank(0x2df9a188fBE231B0DC36D14AcEb65dEFbB049479); // janineleger.eth (~1.53M) + governorBravo.castVote(_newProposalId, FOR); + vm.prank(0x4Be88f63f919324210ea3A2cCAD4ff0734425F91); // kevinolsen.eth (~1.35M) + governorBravo.castVote(_newProposalId, FOR); + vm.prank(0x34aA3F359A9D614239015126635CE7732c18fDF3); // Austin Griffith (~1.05M) + governorBravo.castVote(_newProposalId, AGAINST); + vm.prank(0x7E052Ef7B4bB7E5A45F331128AFadB1E589deaF1); // Kris Is (~1.05M) + governorBravo.castVote(_newProposalId, ABSTAIN); + vm.prank(0x5e349eca2dc61aBCd9dD99Ce94d04136151a09Ee); // Linda Xie (~1.02M) + governorBravo.castVote(_newProposalId, AGAINST); + + jumpToVotingComplete(_newProposalId); + + // Ensure the proposal has succeeded + _state = governorBravo.state(_newProposalId); + assertEq(_state, IGovernor.ProposalState.Succeeded); + + // Queue the proposal + governorBravo.queue(_targets, _values, _calldatas, keccak256(bytes(_description))); + + _jumpPastProposalEta(_newProposalId); + + // Execute the proposal + governorBravo.execute(_targets, _values, _calldatas, keccak256(bytes(_description))); + + // Ensure the proposal is executed + _state = governorBravo.state(_newProposalId); + assertEq(_state, IGovernor.ProposalState.Executed); + + // Ensure the tokens have been transferred + assertEq(_token.balanceOf(_receiver), _initialTokenBalance + _amount); + assertEq(_token.balanceOf(TIMELOCK), _timelockTokenBalance - _amount); + } + + function testFuzz_NewGovernorCanDefeatMixedProposal( + uint256 _amount, + address _receiver, + uint256 _seed + ) public { + IERC20 _token = _randomERC20Token(_seed); + assumeReceiver(_receiver); + uint256 _timelockTokenBalance = _token.balanceOf(TIMELOCK); + + // bound by the number of tokens the timelock currently controls + _amount = bound(_amount, 0, _timelockTokenBalance); + + _upgradeToBravoGovernor(); + ( + uint256 _newProposalId, + address[] memory _targets, + uint256[] memory _values, + bytes[] memory _calldatas, + string memory _description + ) = submitTokenSendProposal(address(_token), _amount, _receiver); + + // Ensure proposal is in the expected state + IGovernor.ProposalState _state = governorBravo.state(_newProposalId); + assertEq(_state, IGovernor.ProposalState.Pending); + + jumpToActiveProposal(_newProposalId); + + // Ensure the proposal is now Active + _state = governorBravo.state(_newProposalId); + assertEq(_state, IGovernor.ProposalState.Active); + + // Delegates vote with a mix of For/Against/Abstain with Against/Abstain winning. + vm.prank(PROPOSER); // kbw.eth (~1.8M votes) + governorBravo.castVote(_newProposalId, ABSTAIN); + vm.prank(0x2df9a188fBE231B0DC36D14AcEb65dEFbB049479); // janineleger.eth (~1.53M) + governorBravo.castVote(_newProposalId, FOR); + vm.prank(0x4Be88f63f919324210ea3A2cCAD4ff0734425F91); // kevinolsen.eth (~1.35M) + governorBravo.castVote(_newProposalId, FOR); + vm.prank(0x34aA3F359A9D614239015126635CE7732c18fDF3); // Austin Griffith (~1.05M) + governorBravo.castVote(_newProposalId, AGAINST); + vm.prank(0x7E052Ef7B4bB7E5A45F331128AFadB1E589deaF1); // Kris Is (~1.05M) + governorBravo.castVote(_newProposalId, AGAINST); + vm.prank(0x5e349eca2dc61aBCd9dD99Ce94d04136151a09Ee); // Linda Xie (~1.02M) + governorBravo.castVote(_newProposalId, AGAINST); + + jumpToVotingComplete(_newProposalId); + + // Ensure the proposal has failed + _state = governorBravo.state(_newProposalId); + assertEq(_state, IGovernor.ProposalState.Defeated); + + // It should not be possible to queue the proposal + vm.expectRevert("Governor: proposal not successful"); + governorBravo.queue(_targets, _values, _calldatas, keccak256(bytes(_description))); + } + + struct NewGovernorUnaffectedByVotesOnOldGovernorVars { + uint256 alphaProposalId; + address[] alphaTargets; + uint256[] alphaValues; + string[] alphaSignatures; + bytes[] alphaCalldatas; + string alphaDescription; + uint256 bravoProposalId; + address[] bravoTargets; + uint256[] bravoValues; + bytes[] bravoCalldatas; + string bravoDescription; + } + + function testFuzz_NewGovernorUnaffectedByVotesOnOldGovernor( + uint256 _amount, + address _receiver, + uint256 _seed + ) public { + NewGovernorUnaffectedByVotesOnOldGovernorVars memory _vars; + IERC20 _token = _randomERC20Token(_seed); + assumeReceiver(_receiver); + + _upgradeToBravoGovernor(); + + // Create a new proposal to send the token. + _vars.alphaTargets = new address[](1); + _vars.alphaValues = new uint256[](1); + _vars.alphaSignatures = new string [](1); + _vars.alphaCalldatas = new bytes[](1); + _vars.alphaDescription = "Transfer some tokens from the new Governor"; + + _vars.alphaTargets[0] = address(_token); + _vars.alphaSignatures[0] = "transfer(address,uint256)"; + _vars.alphaCalldatas[0] = abi.encode(_receiver, _amount); + + // Submit the new proposal to Governor Alpha, which is now deprecated. + vm.prank(PROPOSER); + _vars.alphaProposalId = governorAlpha.propose( + _vars.alphaTargets, + _vars.alphaValues, + _vars.alphaSignatures, + _vars.alphaCalldatas, + _vars.alphaDescription + ); + + // Now construct and submit an identical proposal on Governor Bravo, which is active. + ( + _vars.bravoProposalId, + _vars.bravoTargets, + _vars.bravoValues, + _vars.bravoCalldatas, + _vars.bravoDescription + ) = submitTokenSendProposal(address(_token), _amount, _receiver); + + assertEq(governorBravo.state(_vars.bravoProposalId), IGovernor.ProposalState.Pending); + assertEq( + uint8(governorAlpha.state(_vars.alphaProposalId)), + uint8(governorBravo.state(_vars.bravoProposalId)) + ); + + jumpToActiveProposal(_vars.bravoProposalId); + + // Defeat the proposal on Bravo. + assertEq(governorBravo.state(_vars.bravoProposalId), IGovernor.ProposalState.Active); + delegatesVoteOnBravoGovernor(_vars.bravoProposalId, AGAINST); + + // Pass the proposal on Alpha. + for (uint256 _index = 0; _index < delegates.length; _index++) { + vm.prank(delegates[_index]); + governorAlpha.castVote(_vars.alphaProposalId, true); + } + + jumpToVotingComplete(_vars.bravoProposalId); + + // Ensure the Bravo proposal has failed and Alpha has succeeded. + assertEq(governorBravo.state(_vars.bravoProposalId), IGovernor.ProposalState.Defeated); + assertEq(governorAlpha.state(_vars.alphaProposalId), uint8(IGovernor.ProposalState.Succeeded)); + + // It should not be possible to queue either proposal, confirming that votes + // on alpha do not affect votes on bravo. + vm.expectRevert("Governor: proposal not successful"); + governorBravo.queue( + _vars.bravoTargets, + _vars.bravoValues, + _vars.bravoCalldatas, + keccak256(bytes(_vars.bravoDescription)) + ); + vm.expectRevert("Timelock::queueTransaction: Call must come from admin."); + governorAlpha.queue(_vars.alphaProposalId); + } +}