Compare commits

..

No commits in common. "59a13fec9d0b1047959c91e5ec4a8e61d0b45d89" and "635853f170088bf4f84deda75512581ec1e081ff" have entirely different histories.

12 changed files with 780 additions and 68 deletions

View File

@ -3,7 +3,7 @@ set -e
AVAILABLE_NETWORKS=(anvil-localnet sepolia-testnet hoodi-testnet mordor-testnet)
NETWORK="${AVAILABLE_NETWORKS[0]}"
START=1
END=12
END=13
VERIFY_NEEDED="--verify"
LEGACY_FLAGS=""
@ -25,9 +25,10 @@ usage() {
echo "7) BondDepository"
echo "8) StakingDistributor"
echo "9) BondingCalculator"
echo "10) AfterPartyFirst"
echo "11) AfterPartySecond"
echo "12) AfterPartyThird"
echo "10) Governor"
echo "11) AfterPartyFirst"
echo "12) AfterPartySecond"
echo "13) AfterPartyThird"
echo "69) Gatekeeper"
exit 1
}
@ -110,10 +111,12 @@ for i in $(seq $START $END); do
elif [ "9" == "$i" ]; then
CONTRACT="BondingCalculator"
elif [ "10" == "$i" ]; then
CONTRACT="AfterPartyFirst"
CONTRACT="Governor"
elif [ "11" == "$i" ]; then
CONTRACT="AfterPartySecond"
CONTRACT="AfterPartyFirst"
elif [ "12" == "$i" ]; then
CONTRACT="AfterPartySecond"
elif [ "13" == "$i" ]; then
CONTRACT="AfterPartyThird"
elif [ "69" == "$i" ]; then
CONTRACT="Gatekeeper"

View File

@ -109,12 +109,14 @@ contract Gatekeeper is IGatekeeper, Weaver, ReentrancyGuard {
return (publicKey, state.parity, state.session);
}
function ghost(bytes32 receiver, uint256 amount) external override {
function ghost(bytes32 receiver, uint256 amount) external override returns (uint256) {
if (msg.sender != staking) revert NotStaking();
if (amount < EXISTENTIAL_DEPOSIT) revert NonExistentAmount();
IStorageHistory(storageHistory).increaseBridgeIn(amount);
_insertTreeNode(receiver, amount);
emit Ghosted(receiver, amount);
return _insertTreeNode(receiver, amount);
}
function verify(

View File

@ -2,17 +2,25 @@
pragma solidity ^0.8.20;
import {ERC20} from "@openzeppelin-contracts/token/ERC20/ERC20.sol";
import {ERC20Permit} from "@openzeppelin-contracts/token/ERC20/extensions/ERC20Permit.sol";
import {ERC20Votes} from "@openzeppelin-contracts/token/ERC20/extensions/ERC20Votes.sol";
import {Nonces} from "@openzeppelin-contracts/utils/Nonces.sol";
import {ISTNK} from "./interfaces/ISTNK.sol";
import {IGHST} from "./interfaces/IGHST.sol";
import {FullMath} from "./libraries/FullMath.sol";
contract Ghost is IGHST, ERC20 {
contract Ghost is IGHST, ERC20, ERC20Permit, ERC20Votes {
address public override staking;
address public override stnk;
address private _initializer;
constructor(address _stnk, string memory name, string memory symbol) ERC20(name, symbol) {
error GhostDelegationIsBuiltIn();
constructor(address _stnk, string memory name, string memory symbol)
ERC20(name, symbol)
ERC20Permit(name)
{
stnk = _stnk;
_initializer = msg.sender;
}
@ -33,6 +41,21 @@ contract Ghost is IGHST, ERC20 {
_burn(_from, _amount);
}
function delegate(address) public override pure {
revert GhostDelegationIsBuiltIn();
}
function delegateBySig(
address,
uint256,
uint256,
uint8,
bytes32,
bytes32
) public override pure {
revert GhostDelegationIsBuiltIn();
}
function index() public view override returns (uint256) {
return ISTNK(stnk).index();
}
@ -44,4 +67,16 @@ contract Ghost is IGHST, ERC20 {
function balanceTo(uint256 _amount) public view override returns (uint256) {
return FullMath.mulDiv(_amount, 1e18, index());
}
function nonces(address owner) public view override(ERC20Permit, Nonces) returns (uint256) {
return super.nonces(owner);
}
function delegates(address account) public pure override returns (address) {
return account;
}
function _update(address from, address to, uint256 amount) internal override(ERC20, ERC20Votes) {
super._update(from, to, amount);
}
}

View File

@ -14,6 +14,7 @@ interface IGatekeeper {
error InsufficientValue();
error ExecutionReverted();
event Ghosted(bytes32 indexed receiver, uint256 indexed amount);
event Recalled(address indexed receiver, uint256 indexed amount);
event Rotated(bytes32 indexed aggregatedPublicKey, uint8 indexed parity);
event VoluntaryVerification(address indexed sender, uint256 indexed gasSpent);
@ -28,6 +29,6 @@ interface IGatekeeper {
function govern(uint256 session, uint256 payload, bytes calldata call) external returns (bytes calldata);
function recall(uint256 session, uint256 amount, uint256 payload) external;
function rotate(uint256 session, bytes32 publicKey, uint8 parity) external;
function ghost(bytes32 receiver, uint256 amount) external;
function ghost(bytes32 receiver, uint256 amount) external returns (uint256);
function initialize(address previousAddress) external;
}

View File

@ -2,8 +2,6 @@
pragma solidity ^0.8.20;
interface IWeaver {
event ThreadInserted(uint256 index, uint256 session);
function currentWeavingSession() external view returns (uint256);
function startWeavingSession() external view returns (uint256);
function getSlotValues(uint256 globalIndex, uint256 session, uint256 atBlock) external view returns (bytes32[] memory);

View File

@ -14,8 +14,8 @@ contract WeaverMock is Weaver {
return _previousAddress;
}
function insertTreeNode(bytes32 receiver, uint256 amount) external {
_insertTreeNode(receiver, amount);
function insertTreeNode(bytes32 receiver, uint256 amount) external returns (uint256) {
return _insertTreeNode(receiver, amount);
}
function computeArgumentsHash(

View File

@ -134,10 +134,9 @@ abstract contract Weaver is IWeaver {
return proof;
}
function _insertTreeNode(bytes32 who, uint256 amount) internal {
function _insertTreeNode(bytes32 who, uint256 amount) internal returns (uint256 globalIndex) {
uint256 session = currentWeavingSession;
uint256 globalIndex = _filledEntries[session];
globalIndex = _filledEntries[session];
if (globalIndex >= ENTRIES) {
unchecked { ++session; }
currentWeavingSession = session;
@ -159,8 +158,6 @@ abstract contract Weaver is IWeaver {
_slotValues[session][slotIndex].push(preimage);
unchecked { ++_filledEntries[session]; }
emit ThreadInserted(globalIndex, session);
}
function _computeArgumentsHash(uint256 i, uint256 a, bytes32 r) internal pure returns (bytes32) {

View File

@ -203,7 +203,7 @@ contract GatekeeperTest is Test {
Gatekeeper gatekeeper;
MockStaking staking;
event ThreadInserted(uint256 index, uint256 session);
event Ghosted(bytes32 indexed receiver, uint256 indexed amount);
function setUp() public {
vm.prank(ALICE, ALICE);
@ -247,7 +247,7 @@ contract GatekeeperTest is Test {
bytes32 receiver = bytes32(abi.encodePacked(ALICE));
vm.expectEmit(true, true, true, false, address(gatekeeper));
emit ThreadInserted(0, 0);
emit Ghosted(receiver, ghostAmount);
vm.prank(ALICE);
staking.runGhost(receiver, ghostAmount);
}

View File

@ -1,6 +1,6 @@
pragma solidity 0.8.20;
import {Test, Vm} from "forge-std/Test.sol";
import {Test} from "forge-std/Test.sol";
import {Gatekeeper} from "../../src/Gatekeeper.sol";
import {Hashes} from "../../src/libraries/Hashes.sol";
@ -21,9 +21,9 @@ contract MockStaking {
governor = msg.sender;
}
function ghost(bytes32 receiver, uint256 amount) external {
function ghost(bytes32 receiver, uint256 amount) external returns (uint256) {
require(msg.sender == governor);
gatekeeper.ghost(receiver, amount);
return gatekeeper.ghost(receiver, amount);
}
function createNewGatekeeper() external {
@ -127,25 +127,25 @@ contract GatekeeperWeaverTest is Test {
}
function test_insertationWorksAsExpected() public {
uint256 currentWeavingSession = gatekeeper.currentWeavingSession();
uint256 maxCount = gatekeeper.ENTRIES();
uint256 currentWeavingSession;
uint256 newWeavingSession;
uint256 globalIndex;
(bytes32[] memory whos, uint256[] memory amounts) = _prepareArrays(maxCount);
vm.recordLogs();
for (uint256 i = 0; i < maxCount; i++) {
if (i % 5 == 0) { vm.roll(block.number + 1); }
(globalIndex, currentWeavingSession) = _insertWithAssert(amounts[i], whos[i]);
globalIndex = _insertWithAssert(currentWeavingSession, amounts[i], whos[i]);
assertTrue(_verifyProof(globalIndex, currentWeavingSession, block.number, amounts[i], whos[i]));
}
(globalIndex, newWeavingSession) = _insertWithAssert(amounts[69], whos[69]);
assertEq(currentWeavingSession + 1, newWeavingSession);
globalIndex = _insertWithAssert(currentWeavingSession, amounts[69], whos[69]);
uint256 newSession = gatekeeper.currentWeavingSession();
assertEq(currentWeavingSession + 1, newSession);
vm.roll(block.number + 1337);
assertTrue(_verifyProof(globalIndex, newWeavingSession, block.number, amounts[69], whos[69]));
assertTrue(_verifyProof(globalIndex, newSession, block.number, amounts[69], whos[69]));
assertTrue(_verifyProof(0, currentWeavingSession, block.number, amounts[0], whos[0]));
assertTrue(_verifyProof(69, currentWeavingSession, block.number, amounts[69], whos[69]));
assertTrue(_verifyProof(420, currentWeavingSession, block.number, amounts[420], whos[420]));
@ -156,7 +156,7 @@ contract GatekeeperWeaverTest is Test {
assertTrue(_verifyProof(69, currentWeavingSession, 100, amounts[69], whos[69]));
assertTrue(_verifyProof(420, currentWeavingSession, 100, amounts[420], whos[420]));
assertFalse(_verifyProof(globalIndex, newWeavingSession, 100, amounts[69], whos[69]));
assertFalse(_verifyProof(globalIndex, newSession, 100, amounts[69], whos[69]));
assertFalse(_verifyProof(1337, currentWeavingSession, 100, amounts[1337], whos[1337]));
assertFalse(_verifyProof(2047, currentWeavingSession, 100, amounts[2047], whos[2047]));
@ -174,7 +174,7 @@ contract GatekeeperWeaverTest is Test {
staking.ghost(whos[i], amounts[i]);
}
assertTrue(_verifyProof(globalIndex, newWeavingSession, block.number, amounts[69], whos[69]));
assertTrue(_verifyProof(globalIndex, newSession, block.number, amounts[69], whos[69]));
assertTrue(_verifyProof(0, currentWeavingSession, block.number, amounts[0], whos[0]));
assertTrue(_verifyProof(69, currentWeavingSession, block.number, amounts[69], whos[69]));
assertTrue(_verifyProof(420, currentWeavingSession, block.number, amounts[420], whos[420]));
@ -185,7 +185,7 @@ contract GatekeeperWeaverTest is Test {
assertTrue(_verifyProof(69, currentWeavingSession, 100, amounts[69], whos[69]));
assertTrue(_verifyProof(420, currentWeavingSession, 100, amounts[420], whos[420]));
assertFalse(_verifyProof(globalIndex, newWeavingSession, 100, amounts[69], whos[69]));
assertFalse(_verifyProof(globalIndex, newSession, 100, amounts[69], whos[69]));
assertFalse(_verifyProof(1337, currentWeavingSession, 100, amounts[1337], whos[1337]));
assertFalse(_verifyProof(2047, currentWeavingSession, 100, amounts[2047], whos[2047]));
@ -212,43 +212,41 @@ contract GatekeeperWeaverTest is Test {
}
function _insertWithAssert(
uint256 session,
uint256 amount,
bytes32 who
) private returns (uint256 globalIndex, uint256 emittedSession) {
uint256 preSession = gatekeeper.currentWeavingSession();
uint256 targetSlot = gatekeeper.filledEntries(preSession);
bytes32 previousHash = gatekeeper.treeNodesLatest(preSession, targetSlot);
uint256 prevEntries = gatekeeper.filledEntries(preSession);
uint160 prevLength = gatekeeper.slotLengths(preSession, targetSlot);
) private returns (uint256 globalIndex) {
uint256 targetSlot = gatekeeper.filledEntries(session);
bytes32 previousHash = gatekeeper.treeNodesLatest(session, targetSlot);
uint256 prevEntries = gatekeeper.filledEntries(session);
uint160 prevLength = gatekeeper.slotLengths(session, targetSlot);
vm.prank(ALICE);
staking.ghost(who, amount);
(globalIndex, emittedSession) = _getInsertedDataFromLogs();
if (preSession != emittedSession) {
assertEq(preSession + 1, emittedSession);
assertEq(emittedSession, gatekeeper.currentWeavingSession());
globalIndex = staking.ghost(who, amount);
if (session + 1 == gatekeeper.currentWeavingSession()) {
assertEq(prevLength, gatekeeper.DEPTH());
assertEq(gatekeeper.slotLengths(emittedSession, 0), 1);
assertEq(gatekeeper.slotLengths(session + 1, 0), 1);
assertEq(prevEntries, gatekeeper.ENTRIES());
assertEq(gatekeeper.filledEntries(emittedSession), 1);
assertEq(gatekeeper.filledEntries(session + 1), 1);
assertEq(session + 1, gatekeeper.currentWeavingSession());
session += 1;
previousHash = bytes32(0);
prevLength = gatekeeper.slotLengths(session, 0);
} else {
assertEq(prevLength + 1, gatekeeper.slotLengths(emittedSession, targetSlot));
assertEq(prevEntries + 1, gatekeeper.filledEntries(emittedSession));
assertEq(prevLength + 1, gatekeeper.slotLengths(session, targetSlot));
assertEq(prevEntries + 1, gatekeeper.filledEntries(session));
}
{
bytes32 preimage1 = gatekeeper.slotValues(emittedSession, globalIndex);
bytes32 preimage1 = gatekeeper.slotValues(session, globalIndex);
bytes32 preimage2 = gatekeeper.computePreimage(globalIndex, amount, who);
assertEq(preimage1, preimage2);
}
{
bytes32 nodeHash1 = gatekeeper.treeNodesLatest(emittedSession, globalIndex);
bytes32 nodeHash1 = gatekeeper.treeNodesLatest(session, globalIndex);
bytes32 preimageHash = Hashes.efficientKeccak256(
gatekeeper.computePreimage(globalIndex, amount, who)
);
@ -270,17 +268,4 @@ contract GatekeeperWeaverTest is Test {
return (whos, amounts);
}
function _getInsertedDataFromLogs() private returns (uint256 index, uint256 session) {
Vm.Log[] memory entries = vm.getRecordedLogs();
bytes32 targetTopic = keccak256("ThreadInserted(uint256,uint256)");
for (uint256 i = entries.length; i > 0; i--) {
if (entries[i - 1].topics.length > 0 && entries[i - 1].topics[0] == targetTopic) {
(index, session) = abi.decode(entries[i - 1].data, (uint256, uint256));
return (index, session);
}
}
revert("Target event not found in logs");
}
}

View File

@ -0,0 +1,647 @@
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));
}
}

View File

@ -86,6 +86,7 @@ contract StakingTest is Test {
event DistributorSet(address distributor);
event WarmupSet(uint256 warmup);
event Ghosted(bytes32 indexed receiver, uint256 indexed amount);
function setUp() public {
vm.startPrank(INITIALIZER);
@ -639,6 +640,23 @@ contract StakingTest is Test {
assertEq(ghst.totalSupply(), 0);
}
function test_ghostTokensEmitsEvent() public {
_prepareAndRoll(ALICE, BIG_AMOUNT, true, true);
uint256 aliceBalance = stnk.balanceOf(ALICE);
vm.startPrank(ALICE);
stnk.approve(address(staking), aliceBalance);
uint256 ghstBalance = staking.wrap(ALICE, aliceBalance);
vm.stopPrank();
bytes32 receiver = bytes32(abi.encodePacked(ALICE));
vm.expectEmit(true, true, true, false, staking.gatekeeper());
emit Ghosted(receiver, ghstBalance);
vm.prank(ALICE);
staking.ghost(receiver, ghstBalance);
}
function test_breakoutLogicWorks() public {
uint256 initialIndex = staking.index();
bytes32 receiver = bytes32(abi.encodePacked(ALICE));
@ -660,6 +678,9 @@ contract StakingTest is Test {
uint256 range = (payout * 3) / 100;
requestedPayout = (pseudoRandom % range) + 1;
vm.expectEmit(true, true, true, false, staking.gatekeeper());
emit Ghosted(receiver, requestedPayout);
vm.prank(ALICE);
staking.breakout(receiver, requestedPayout);
expectedPayout += requestedPayout;

View File

@ -7,10 +7,18 @@ import {Stinky} from "../../src/StinkyERC20.sol";
import {GhostAuthority} from "../../src/GhostAuthority.sol";
import {GhostStaking} from "../../src/Staking.sol";
import {ERC20PermitTest} from "./Permit.t.sol";
import {ERC20AllowanceTest} from "./Allowance.t.sol";
import {ERC20TransferTest} from "./Transfer.t.sol";
import {ERC20VotesTest} from "./Votes.t.sol";
contract GhostTest is Test, ERC20AllowanceTest, ERC20TransferTest {
contract GhostTest is
Test,
ERC20PermitTest,
ERC20AllowanceTest,
ERC20TransferTest,
ERC20VotesTest
{
uint256 constant public INITIAL_INDEX = 10819917194513808e56;
address constant INITIALIZER = 0x0000000000000000000000000000000000000001;
@ -51,8 +59,10 @@ contract GhostTest is Test, ERC20AllowanceTest, ERC20TransferTest {
ghst.initialize(address(staking));
vm.stopPrank();
initializePermit(address(ghst), AMOUNT, MAX_AMOUNT);
initializeAllowance(ALICE, BOB, address(ghst), AMOUNT, MAX_AMOUNT, AMOUNT);
initializeTransfer(ALICE, BOB, address(ghst), AMOUNT, MAX_AMOUNT);
initializeVotes(ALICE, BOB, address(ghst), AMOUNT);
}
function test_isConstructedCorrectly() public view {
@ -147,6 +157,19 @@ contract GhostTest is Test, ERC20AllowanceTest, ERC20TransferTest {
_mintTokens(who, value);
}
function _mintPermitTokens(address who, uint256 value) internal override {
_mintTokens(who, value);
}
function _mintVotesTokens(address who, uint256 value) internal override {
_mintTokens(who, value);
}
function _burnVotesTokens(address who, uint256 value) internal override {
vm.prank(address(staking));
ghst.burn(who, value);
}
function _getCurrentTotalSupply() internal override view returns (uint256) {
return ghst.totalSupply();
}