560 lines
19 KiB
Solidity
560 lines
19 KiB
Solidity
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));
|
|
}
|
|
}
|