ghost-dao-contracts/test/governance/GhostGovernor.t.sol
Uncle Fatso 8ac8b67767
get rid of annoying foundry warnings
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-03-09 00:37:41 +03:00

648 lines
22 KiB
Solidity

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));
}
}