pragma solidity 0.8.20; import {Test} from "forge-std/Test.sol"; import {Strings} from "@openzeppelin-contracts/utils/Strings.sol"; import {Ghost} from "../../src/GhstERC20.sol"; import {Stinky} from "../../src/StinkyERC20.sol"; import {GhostGovernor} from "../../src/governance/GhostGovernor.sol"; import {IGovernor} from "@openzeppelin-contracts/governance/IGovernor.sol"; import {IVotes} from "@openzeppelin-contracts/governance/utils/IVotes.sol"; import {SafeERC20} from "@openzeppelin-contracts/token/ERC20/utils/SafeERC20.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; using SafeERC20 for Ghost; 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.Succeeded)); vm.expectRevert(); vm.prank(CAROL); governor.castVote(proposalId, 0); 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); (uint256 againstVotes, uint256 forVotes, uint256 abstainVotes) = governor.proposalVotes(proposalId); assertEq(againstVotes, 0); assertEq(forVotes, amount); assertEq(abstainVotes, 0); _castVoteWrapper(proposalId, BOB, 1, true, true); (againstVotes, forVotes, abstainVotes) = governor.proposalVotes(proposalId); assertEq(againstVotes, 0); assertEq(forVotes, 3 * 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, true, 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.Succeeded)); 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), 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, true); vm.roll(governor.proposalDeadline(proposalId)); _castVoteWrapper(proposalId, CAROL, 0, true, 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.safeTransfer(BOB, 420); _assertVotesEqualToBalance(); uint256 aliceBalance = ghst.balanceOf(ALICE); vm.prank(ALICE); ghst.safeTransfer(BOB, aliceBalance); _assertVotesEqualToBalance(); vm.prank(BOB); ghst.safeTransfer(CAROL, 1337); _assertVotesEqualToBalance(); uint256 bobBalance = ghst.balanceOf(BOB); vm.prank(BOB); ghst.safeTransfer(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 test_proposeAutoForVote() public { vm.startPrank(INIT); ghst.mint(ALICE, PROPOSAL_THRESHOLD); vm.stopPrank(); vm.roll(block.number + 1); (uint256 proposalId,,,,) = _proposeDummy(ALICE, 69); (uint256 againstVotes, uint256 forVotes, uint256 abstainVotes) = governor.proposalVotes(proposalId); assertEq(forVotes, PROPOSAL_THRESHOLD); assertEq(againstVotes, 0); assertEq(abstainVotes, 0); vm.roll(block.number + 1); vm.expectRevert(); vm.prank(ALICE); governor.castVote(proposalId, 1); } function test_releaseAfterDefeated() public { vm.startPrank(INIT); ghst.mint(ALICE, PROPOSAL_THRESHOLD); ghst.mint(BOB, PROPOSAL_THRESHOLD); ghst.mint(CAROL, PROPOSAL_THRESHOLD); vm.stopPrank(); vm.roll(block.number + 1); (uint256 proposalId,,,,) = _proposeDummy(ALICE, 69); assertEq(governor.releaseLocked(proposalId), 0); assertEq(ghst.balanceOf(ALICE), 0); assertEq(ghst.balanceOf(address(governor)), PROPOSAL_THRESHOLD); _waitForActive(proposalId); _castVoteWrapper(proposalId, BOB, 0, true, false); _castVoteWrapper(proposalId, CAROL, 0, true, false); assertEq(uint8(governor.state(proposalId)), uint8(IGovernor.ProposalState.Defeated)); assertEq(governor.releaseLocked(proposalId), PROPOSAL_THRESHOLD); assertEq(ghst.balanceOf(ALICE), PROPOSAL_THRESHOLD); assertEq(ghst.balanceOf(address(governor)), 0); } function test_releaseAfterSucceeded() public { vm.startPrank(INIT); ghst.mint(ALICE, PROPOSAL_THRESHOLD); ghst.mint(BOB, PROPOSAL_THRESHOLD); ghst.mint(CAROL, PROPOSAL_THRESHOLD); vm.stopPrank(); vm.roll(block.number + 1); (uint256 proposalId,,,,) = _proposeDummy(ALICE, 69); assertEq(governor.releaseLocked(proposalId), 0); assertEq(ghst.balanceOf(ALICE), 0); assertEq(ghst.balanceOf(address(governor)), PROPOSAL_THRESHOLD); _waitForActive(proposalId); _castVoteWrapper(proposalId, BOB, 1, true, true); assertEq(uint8(governor.state(proposalId)), uint8(IGovernor.ProposalState.Succeeded)); assertEq(governor.releaseLocked(proposalId), PROPOSAL_THRESHOLD); assertEq(ghst.balanceOf(ALICE), PROPOSAL_THRESHOLD); assertEq(ghst.balanceOf(address(governor)), 0); } function test_releaseAfterDeadline() public { vm.startPrank(INIT); ghst.mint(ALICE, PROPOSAL_THRESHOLD); ghst.mint(BOB, 420 * PROPOSAL_THRESHOLD); vm.stopPrank(); vm.roll(block.number + 1); (uint256 proposalId,,,,) = _proposeDummy(ALICE, 69); assertEq(governor.releaseLocked(proposalId), 0); assertEq(ghst.balanceOf(ALICE), 0); assertEq(ghst.balanceOf(address(governor)), PROPOSAL_THRESHOLD); _waitForActive(proposalId); vm.roll(block.number + governor.proposalDeadline(proposalId)); assertEq(uint8(governor.state(proposalId)), uint8(IGovernor.ProposalState.Defeated)); assertEq(governor.releaseLocked(proposalId), PROPOSAL_THRESHOLD); assertEq(ghst.balanceOf(ALICE), PROPOSAL_THRESHOLD); assertEq(ghst.balanceOf(address(governor)), 0); } 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)); // forge-lint: disable-line(unsafe-typecast) 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)); } }