pragma solidity 0.8.20; import {Test} from "forge-std/Test.sol"; import {Strings} from "@openzeppelin-contracts/utils/Strings.sol"; import "../../src/governance/GhostGovernor.sol"; import "../../src/GhstERC20.sol"; import "../../src/StinkyERC20.sol"; contract GhostGovernorExposed is GhostGovernor { constructor( IVotes _ghst, uint48 _initialVoteExtension, uint48 _initialVotingDelay, uint32 _initialVotingPeriod, uint256 _initialProposalThreshold, uint256 _quorumFraction ) GhostGovernor( _ghst, _initialVoteExtension, _initialVotingDelay, _initialVotingPeriod, _initialProposalThreshold, _quorumFraction ) {} function isValidDescriptionForProposer( address proposer, string memory description ) public view returns (bool) { return super._isValidDescriptionForProposer(proposer, description); } function quorumReached(uint256 proposalId) public view returns (bool) { return super._quorumReached(proposalId); } function voteSucceeded(uint256 proposalId) public view returns (bool) { return super._voteSucceeded(proposalId); } } contract GhostGovernorTest is Test { using Strings for address; uint48 public constant VOTE_EXTENSION = 69; uint48 public constant VOTING_DELAY = 420; uint32 public constant VOTING_PERIOD = 1337; uint256 public constant PROPOSAL_THRESHOLD = 69 * 1e18; uint256 public constant QUORUM_FRACTION = 20; // percent address constant init = 0x0000000000000000000000000000000000000001; address constant alice = 0x0000000000000000000000000000000000000002; address constant bob = 0x0000000000000000000000000000000000000003; address constant carol = 0x0000000000000000000000000000000000000004; address constant dave = 0x0000000000000000000000000000000000000005; address constant eve = 0x0000000000000000000000000000000000000006; GhostGovernorExposed public governor; Stinky public stnk; Ghost public ghst; function setUp() public { vm.startPrank(init); stnk = new Stinky(10819917194513808e56, "Stinky Test Name", "STNKTST"); ghst = new Ghost(address(stnk), "Ghost Test Name", "GHSTTST"); ghst.initialize(init); governor = new GhostGovernorExposed( ghst, VOTE_EXTENSION, VOTING_DELAY, VOTING_PERIOD, PROPOSAL_THRESHOLD, QUORUM_FRACTION ); vm.stopPrank(); vm.prank(alice); ghst.approve(address(governor), type(uint256).max); vm.prank(bob); ghst.approve(address(governor), type(uint256).max); vm.prank(carol); ghst.approve(address(governor), type(uint256).max); } function test_correctGovernanceName() public view { assertEq(governor.name(), "GhostGovernorV1"); } function test_correctGovernanceVersion() public view { assertEq(governor.version(), "1"); } function test_correctGovernanceConfig() public view { assertEq(governor.COUNTING_MODE(), "support=bravo&quorum=for"); } function test_correctGovernanceProposalThreshold() public view { assertEq(governor.proposalThreshold(), PROPOSAL_THRESHOLD); } function test_correctGovernanceVotingDelay() public view { assertEq(governor.votingDelay(), VOTING_DELAY); } function test_correctGovernanceVotingPeriod() public view { assertEq(governor.votingPeriod(), VOTING_PERIOD); } function test_correctGovernanceVoteExtension() public view { assertEq(governor.lateQuorumVoteExtension(), VOTE_EXTENSION); } function test_correctGovernanceQuorumFraction() public view { assertEq(governor.quorumNumerator(), QUORUM_FRACTION); } function test_validDescriptionForProposer( string memory description, address proposer, bool includeProposer ) public view { string memory finalDescription = description; if (includeProposer) { finalDescription = string.concat( description, "#proposer=", Strings.toHexString(proposer) ); } bool isValid = governor.isValidDescriptionForProposer(proposer, finalDescription); assertTrue(isValid); } function test_invalidDescriptionForProposer( string memory description, address commitProposer, address actualProposer ) public view { vm.assume(commitProposer != actualProposer); string memory wrongDescription = string.concat( description, "#proposer=", Strings.toHexString(commitProposer) ); bool isValid = governor.isValidDescriptionForProposer(actualProposer, wrongDescription); assertFalse(isValid); } function test_succeededOnAbsoluteMajority() public { vm.startPrank(init); ghst.mint(alice, PROPOSAL_THRESHOLD); ghst.mint(bob, 555 * 1e17); ghst.mint(carol, 345 * 1e17 + 1); ghst.mint(dave, 21 * 1e18); vm.stopPrank(); vm.roll(block.number + 1); (uint256 proposalId,,,,) = _proposeDummy(alice, 69); _waitForActive(proposalId); assertEq(uint8(governor.state(proposalId)), uint8(IGovernor.ProposalState.Active)); _castVoteWrapper(proposalId, bob, 1, true, true); assertEq(uint8(governor.state(proposalId)), uint8(IGovernor.ProposalState.Active)); _castVoteWrapper(proposalId, carol, 1, true, true); assertEq(uint8(governor.state(proposalId)), uint8(IGovernor.ProposalState.Succeeded)); vm.expectRevert(); vm.prank(dave); governor.castVote(proposalId, 0); } function test_defeatedOnAbsoluteMajority() public { vm.startPrank(init); ghst.mint(alice, PROPOSAL_THRESHOLD); ghst.mint(bob, 555 * 1e17); ghst.mint(carol, 345 * 1e17 + 1); ghst.mint(dave, 21 * 1e18); vm.stopPrank(); vm.roll(block.number + 1); (uint256 proposalId,,,,) = _proposeDummy(alice, 69); _waitForActive(proposalId); assertEq(uint8(governor.state(proposalId)), uint8(IGovernor.ProposalState.Active)); _castVoteWrapper(proposalId, bob, 0, true, false); assertEq(uint8(governor.state(proposalId)), uint8(IGovernor.ProposalState.Active)); _castVoteWrapper(proposalId, carol, 0, true, false); assertEq(uint8(governor.state(proposalId)), uint8(IGovernor.ProposalState.Defeated)); vm.expectRevert(); vm.prank(dave); governor.castVote(proposalId, 1); } function test_onlyOneActiveProposal() public { uint256 amount = PROPOSAL_THRESHOLD; vm.startPrank(init); ghst.mint(alice, amount); ghst.mint(bob, 2 * amount); ghst.mint(carol, 3 * amount); vm.stopPrank(); vm.roll(block.number + 1); (uint256 proposalId,,,,) = _proposeDummy(alice, 69); _waitForActive(proposalId); vm.prank(alice); assertEq(governor.releaseLocked(proposalId), 0); assertEq(governor.lockedAmounts(proposalId), amount); assertEq(ghst.balanceOf(alice), 0); _castVoteWrapper(proposalId, alice, 1, false, false); (uint256 againstVotes, uint256 forVotes, uint256 abstainVotes) = governor.proposalVotes(proposalId); assertEq(againstVotes, 0); assertEq(forVotes, 0); assertEq(abstainVotes, 0); _castVoteWrapper(proposalId, bob, 1, true, true); (againstVotes, forVotes, abstainVotes) = governor.proposalVotes(proposalId); assertEq(againstVotes, 0); assertEq(forVotes, 2 * amount); assertEq(abstainVotes, 0); assertEq(ghst.balanceOf(alice), 0); assertEq(ghst.balanceOf(bob), 2 * amount); assertEq(ghst.balanceOf(carol), 3 * amount); _waitForSucceed(proposalId); vm.prank(alice); assertEq(governor.releaseLocked(proposalId), amount); assertEq(governor.lockedAmounts(proposalId), 0); assertEq(ghst.balanceOf(alice), amount); vm.prank(alice); assertEq(governor.releaseLocked(proposalId), 0); assertEq(governor.lockedAmounts(proposalId), 0); assertEq(ghst.balanceOf(alice), amount); assertEq(ghst.balanceOf(alice), amount); assertEq(ghst.balanceOf(bob), 2 * amount); assertEq(ghst.balanceOf(carol), 3 * amount); (proposalId,,,,) = _proposeDummy(bob, 420); _waitForActive(proposalId); _castVoteWrapper(proposalId, alice, 1, false, true); _castVoteWrapper(proposalId, carol, 1, true, true); vm.roll(block.number + 3); (uint256 newProposalId,,,,) = _proposeDummy(carol, 1337); assertEq(uint8(governor.state(proposalId)), uint8(IGovernor.ProposalState.Canceled)); assertEq(uint8(governor.state(newProposalId)), uint8(IGovernor.ProposalState.Pending)); assertEq(ghst.balanceOf(alice), amount); assertEq(ghst.balanceOf(bob), 0); assertEq(ghst.balanceOf(carol), 0); vm.prank(bob); assertEq(governor.releaseLocked(proposalId), 0); assertEq(governor.lockedAmounts(proposalId), 2 * amount); assertEq(ghst.balanceOf(bob), 0); vm.roll(governor.proposalDeadline(proposalId) + 1); vm.prank(bob); assertEq(governor.releaseLocked(proposalId), 2 * amount); assertEq(governor.lockedAmounts(proposalId), 0); assertEq(ghst.balanceOf(bob), 2 * amount); vm.prank(carol); assertEq(governor.releaseLocked(newProposalId), 0); assertEq(governor.lockedAmounts(newProposalId), 3 * amount); assertEq(ghst.balanceOf(carol), 0); } function test_proposalCountWorks() public { assertEq(governor.proposalCount(), 0); uint256 i = 1; for (; i < 69; ) { { uint256 amount = governor.activeProposedLock() + PROPOSAL_THRESHOLD; if (amount + ghst.totalSupply() > type(uint208).max) { break; } vm.prank(init); ghst.mint(alice, amount); vm.roll(block.number + 1); } ( uint256 proposalId, address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description ) = _proposeDummy(alice, i); { ( address[] memory thisTargets, uint256[] memory thisValues, bytes[] memory thisCalldatas, bytes32 thisDescriptionHash ) = governor.proposalDetails(proposalId); assertEq(thisTargets.length, targets.length); assertEq(thisValues.length, values.length); assertEq(thisCalldatas.length, calldatas.length); assertEq(thisTargets[0], targets[0]); assertEq(thisValues[0], values[0]); assertEq(thisCalldatas[0], calldatas[0]); assertEq(thisDescriptionHash, keccak256(bytes(description))); } { ( uint256 otherProposalId, address[] memory otherTargets, uint256[] memory otherValues, bytes[] memory otherCalldatas, bytes32 otherDescriptionHash ) = governor.proposalDetailsAt(i - 1); assertEq(otherTargets.length, targets.length); assertEq(otherValues.length, values.length); assertEq(otherCalldatas.length, calldatas.length); assertEq(otherProposalId, governor.hashProposal(targets, values, calldatas, keccak256(bytes(description)))); assertEq(otherTargets[0], targets[0]); assertEq(otherValues[0], values[0]); assertEq(otherCalldatas[0], calldatas[0]); assertEq(otherDescriptionHash, keccak256(bytes(description))); } assertEq(governor.proposalCount(), i); unchecked { ++i; } } } function test_preventLateQuorumWorks() public { vm.startPrank(init); ghst.mint(alice, PROPOSAL_THRESHOLD); ghst.mint(bob, 50 * 1e18); ghst.mint(carol, 100 * 1e18); ghst.mint(dave, 100 * 1e18); ghst.mint(eve, 500 * 1e18); vm.stopPrank(); vm.roll(block.number + 1); (uint256 proposalId,,,,) = _proposeDummy(alice, 69); assertEq(uint8(governor.state(proposalId)), uint8(IGovernor.ProposalState.Pending)); vm.roll(block.number + VOTING_DELAY + 1); assertEq(uint8(governor.state(proposalId)), uint8(IGovernor.ProposalState.Active)); assertEq(governor.voteOf(proposalId, bob), 0); assertEq(governor.voteOf(proposalId, carol), 0); assertEq(governor.voteOf(proposalId, dave), 0); assertEq(governor.voteOf(proposalId, eve), 0); vm.roll(governor.proposalDeadline(proposalId)); _castVoteWrapper(proposalId, bob, 1, false, false); vm.roll(governor.proposalDeadline(proposalId)); _castVoteWrapper(proposalId, carol, 0, false, false); vm.roll(governor.proposalDeadline(proposalId)); _castVoteWrapper(proposalId, dave, 1, true, false); vm.roll(governor.proposalDeadline(proposalId)); _castVoteWrapper(proposalId, eve, 1, true, true); vm.prank(alice); governor.execute(proposalId); assertEq(uint8(governor.state(proposalId)), uint8(IGovernor.ProposalState.Executed)); assertEq(governor.voteOf(proposalId, bob), 2); assertEq(governor.voteOf(proposalId, carol), 1); assertEq(governor.voteOf(proposalId, dave), 2); assertEq(governor.voteOf(proposalId, eve), 2); } function test_quorumNumeratorWorks() public { uint256 numerator = governor.quorumNumerator(); uint256 denominator = governor.quorumDenominator(); uint256 currentBlock = block.number; vm.startPrank(init); ghst.mint(alice, 1 * 1e18); ghst.mint(bob, 69 * 1e18); ghst.mint(carol, 420 * 1e18); ghst.mint(dave, 1337 * 1e18); ghst.mint(eve, 69420 * 1e18); vm.stopPrank(); vm.roll(currentBlock + 2); assertEq(governor.quorum(currentBlock), 71247 * 1e18 * numerator / denominator); } function test_votingPowerTransfer() public { vm.startPrank(init); ghst.mint(alice, 35 * 1e18); ghst.mint(bob, 34 * 1e18); vm.stopPrank(); vm.roll(block.number + 1); vm.expectRevert(); vm.prank(alice); ghst.delegate(alice); vm.expectRevert(); vm.prank(bob); ghst.delegate(bob); _assertVotesEqualToBalance(); vm.prank(alice); ghst.transfer(bob, 420); _assertVotesEqualToBalance(); uint256 aliceBalance = ghst.balanceOf(alice); vm.prank(alice); ghst.transfer(bob, aliceBalance); _assertVotesEqualToBalance(); vm.prank(bob); ghst.transfer(carol, 1337); _assertVotesEqualToBalance(); uint256 bobBalance = ghst.balanceOf(bob); vm.prank(bob); ghst.transfer(carol, bobBalance); _assertVotesEqualToBalance(); vm.expectRevert(); vm.prank(carol); ghst.delegate(carol); assertEq(ghst.getVotes(alice), 0); assertEq(ghst.getVotes(bob), 0); assertEq(ghst.getVotes(carol), 69 * 1e18); assertEq(ghst.balanceOf(alice), 0); assertEq(ghst.balanceOf(bob), 0); assertEq(ghst.balanceOf(carol), 69 * 1e18); vm.prank(init); ghst.burn(carol, 69 * 1e18); assertEq(ghst.getVotes(carol), 0); assertEq(ghst.balanceOf(carol), 0); assertEq(ghst.totalSupply(), 0); } function test_changesOfTotalSupplyHasNoAffectOnPastTotalSupply() public { vm.prank(init); ghst.mint(alice, 100); vm.roll(block.number + 1); assertEq(ghst.getPastTotalSupply(block.number - 1), 100); assertEq(ghst.totalSupply(), 100); vm.prank(init); ghst.burn(alice, 50); vm.roll(block.number + 1); assertEq(ghst.getPastTotalSupply(block.number - 2), 100); assertEq(ghst.getPastTotalSupply(block.number - 1), 50); assertEq(ghst.totalSupply(), 50); vm.prank(init); ghst.mint(bob, 150); vm.roll(block.number + 1); assertEq(ghst.getPastTotalSupply(block.number - 3), 100); assertEq(ghst.getPastTotalSupply(block.number - 2), 50); assertEq(ghst.getPastTotalSupply(block.number - 1), 200); assertEq(ghst.totalSupply(), 200); } function _proposeDummy(address who, uint256 index) private returns ( uint256 proposalId, address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description ) { targets = new address[](1); targets[0] = address(uint160(index + 1)); values = new uint256[](1); values[0] = 0; calldatas = new bytes[](1); calldatas[0] = abi.encodeWithSignature("setVotingDelay(uint48)", 0); description = string.concat("Proposal #", Strings.toString(index)); vm.prank(who); proposalId = governor.propose( targets, values, calldatas, description ); vm.roll(block.number + 1); } function _castVoteWrapper( uint256 proposalId, address who, uint8 vote, bool isQuorumReached, bool isVoteSucceeded ) private { assertEq(governor.hasVoted(proposalId, who), false); vm.prank(who); governor.castVote(proposalId, vote); assertEq(governor.hasVoted(proposalId, who), true); assertEq(governor.quorumReached(proposalId), isQuorumReached); assertEq(governor.voteSucceeded(proposalId), isVoteSucceeded); } function _assertVotesEqualToBalance() private view { assertEq(ghst.getVotes(alice), ghst.balanceOf(alice)); assertEq(ghst.getVotes(bob), ghst.balanceOf(bob)); assertEq(ghst.getVotes(carol), ghst.balanceOf(carol)); } function _waitForActive(uint256 proposalId) private { assertEq(uint8(governor.state(proposalId)), uint8(IGovernor.ProposalState.Pending)); vm.roll(block.number + VOTING_DELAY + 1); assertEq(uint8(governor.state(proposalId)), uint8(IGovernor.ProposalState.Active)); } function _waitForSucceed(uint256 proposalId) private { assertEq(uint8(governor.state(proposalId)), uint8(IGovernor.ProposalState.Active)); vm.roll(governor.proposalDeadline(proposalId) + 1); assertEq(uint8(governor.state(proposalId)), uint8(IGovernor.ProposalState.Succeeded)); } }