From 3d85654dd5ab6ee8c40a0c744d02b59348db0147 Mon Sep 17 00:00:00 2001 From: Mike Date: Thu, 25 May 2023 17:59:04 -0400 Subject: [PATCH 01/20] expand unit test code coverage --- src/grants/GrantFund.sol | 8 ++++---- test/unit/StandardFunding.t.sol | 5 ++++- test/utils/GrantFundTestHelper.sol | 6 ++++++ 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/grants/GrantFund.sol b/src/grants/GrantFund.sol index a40bcea2..f6a4aed1 100644 --- a/src/grants/GrantFund.sol +++ b/src/grants/GrantFund.sol @@ -572,15 +572,15 @@ contract GrantFund is IGrantFund, Storage, ReentrancyGuard { for (uint256 i = 0; i < numProposalsInSlate_; ) { Proposal memory proposal = _proposals[proposalIds_[i]]; - // account for fundingVotesReceived possibly being negative - // block proposals that recieve no positive funding votes from entering a finalized slate - if (proposal.fundingVotesReceived <= 0) revert InvalidProposalSlate(); - // check if Proposal is in the topTenProposals list if ( _findProposalIndex(proposalIds_[i], _topTenProposals[distributionId_]) == -1 ) revert InvalidProposalSlate(); + // account for fundingVotesReceived possibly being negative + // block proposals that recieve no positive funding votes from entering a finalized slate + if (proposal.fundingVotesReceived <= 0) revert InvalidProposalSlate(); + // update counters // since we are converting from int128 to uint128, we can safely assume that the value will not overflow sum_ += uint128(proposal.fundingVotesReceived); diff --git a/test/unit/StandardFunding.t.sol b/test/unit/StandardFunding.t.sol index e2f3dbb9..3d512468 100644 --- a/test/unit/StandardFunding.t.sol +++ b/test/unit/StandardFunding.t.sol @@ -1049,6 +1049,7 @@ contract StandardFundingGrantFundTest is GrantFundTestHelper { // skip to the end of the DistributionPeriod vm.roll(_startBlock + 650_000); + assertEq(_grantFund.getStage(), keccak256(bytes("Challenge"))); // ensure updateSlate won't accept a slate containing a proposal that is not in topTenProposal (funding Stage) vm.expectRevert(IGrantFundErrors.InvalidProposalSlate.selector); @@ -1433,7 +1434,6 @@ contract StandardFundingGrantFundTest is GrantFundTestHelper { assertEq(treasuryAtId3, (treasuryAtId2 + surplus) * 97 / 100); (, , , uint128 gbc_distribution3, , ) = _grantFund.getDistributionPeriodInfo(distributionId3); assertEq(gbc_distribution3, 14_206_350 * 1e18); - } /** @@ -1486,6 +1486,9 @@ contract StandardFundingGrantFundTest is GrantFundTestHelper { // funding period votes _fundingVote(_grantFund, _tokenHolder1, screenedProposals_distribution1[0].proposalId, voteYes, 25_000_000 * 1e18); + // check can't claim delegate rewards before the funding stage ends + assertClaimDelegateRewardStillActiveRevert(_grantFund, _tokenHolder1, distributionId); + // skip to the Challenge period vm.roll(_startBlock + 650_000); assertEq(_grantFund.getStage(), keccak256(bytes("Challenge"))); diff --git a/test/utils/GrantFundTestHelper.sol b/test/utils/GrantFundTestHelper.sol index c4475573..07c9f745 100644 --- a/test/utils/GrantFundTestHelper.sol +++ b/test/utils/GrantFundTestHelper.sol @@ -685,4 +685,10 @@ abstract contract GrantFundTestHelper is Test { grantFund_.startNewDistributionPeriod(); } + function assertClaimDelegateRewardStillActiveRevert(GrantFund grantFund_, address voter_, uint24 distributionId_) internal { + changePrank(voter_); + vm.expectRevert(IGrantFundErrors.DistributionPeriodStillActive.selector); + grantFund_.claimDelegateReward(distributionId_); + } + } From ed47e523dbbb48e263b03de186d9fc8201147cba Mon Sep 17 00:00:00 2001 From: Prateek Gupta Date: Sat, 10 Jun 2023 01:34:37 +0530 Subject: [PATCH 02/20] Add invariants DP7, SS10, SS11, ES4, ES5 and DR4 (#105) * Add invariants DP7, SS10, SS11, ES4, ES5 and DR4 * Fix stack too deep issue * PR feedback --- test/invariants/INVARIANTS.md | 4 +- .../StandardFinalizeInvariant.t.sol | 193 +++++++++++------- ...tandardMultipleDistributionInvariant.t.sol | 9 +- .../StandardScreeningInvariant.t.sol | 20 +- 4 files changed, 136 insertions(+), 90 deletions(-) diff --git a/test/invariants/INVARIANTS.md b/test/invariants/INVARIANTS.md index 037a5e42..5129204f 100644 --- a/test/invariants/INVARIANTS.md +++ b/test/invariants/INVARIANTS.md @@ -27,7 +27,7 @@ - **SS8**: A proposal can only receive screening votes if it was created via `propose()`. - **SS9**: A proposal can only be created during a distribution period's screening stage. - **SS10**: A proposal's proposalId must be unique. - - **SS11**: A proposal's tokens requested must be <= GBC>. + - **SS11**: A proposal's tokens requested must be <= 90% of GBC. - #### Funding Stage: - **FS1**: Only 10 proposals can be voted on in the funding stage @@ -52,7 +52,7 @@ - **ES2**: A proposal can only be executed after the challenge stage is complete. - **ES3**: A proposal can only be executed once. - **ES4**: A proposal can only be executed if it was in the top ten screened proposals at the end of the screening stage. - - **ES5**: An executed proposal should only ever transfer tokens <= GBC. + - **ES5**: An executed proposal should only ever transfer tokens <= 90% of GBC. - #### Delegation Rewards: - **DR1**: Cumulative delegation rewards should be <= 10% of a distribution periods GBC. diff --git a/test/invariants/StandardFinalizeInvariant.t.sol b/test/invariants/StandardFinalizeInvariant.t.sol index b0a5b815..8623e1a1 100644 --- a/test/invariants/StandardFinalizeInvariant.t.sol +++ b/test/invariants/StandardFinalizeInvariant.t.sol @@ -15,6 +15,21 @@ import { Handler } from "./handlers/Handler.sol"; contract StandardFinalizeInvariant is StandardTestBase { + struct LocalVotersInfo { + uint128 fundingVotingPower; + uint128 fundingRemainingVotingPower; + uint256 votesCast; + } + + struct DistributionInfo { + uint24 id; + uint48 startBlock; + uint48 endBlock; + uint128 fundsAvailable; + uint256 fundingVotePowerCast; + bytes32 fundedSlateHash; + } + // override setup to start tests in the challenge stage with proposals that have already been screened and funded function setUp() public override { super.setUp(); @@ -113,111 +128,133 @@ contract StandardFinalizeInvariant is StandardTestBase { } } - function invariant_ES1_ES2_ES3() external { + function invariant_ES1_ES2_ES3_ES4_ES5() external { uint24 distributionId = _grantFund.getDistributionId(); - (, , , , , bytes32 topSlateHash) = _grantFund.getDistributionPeriodInfo(distributionId); + while (distributionId > 0) { + (, , , uint256 gbc, , bytes32 topSlateHash) = _grantFund.getDistributionPeriodInfo(distributionId); + + uint256[] memory topSlateProposalIds = _grantFund.getFundedProposalSlate(topSlateHash); + + uint256[] memory standardFundingProposals = _standardHandler.getStandardFundingProposals(distributionId); + uint256[] memory topTenScreenedProposalIds = _grantFund.getTopTenProposals(distributionId); + + // check the state of every proposal submitted in this distribution period + for (uint256 i = 0; i < standardFundingProposals.length; ++i) { + uint256 proposalId = standardFundingProposals[i]; + (, uint24 proposalDistributionId, , uint256 tokenRequested, , bool executed) = _grantFund.getProposalInfo(proposalId); + int256 proposalIndex = _findProposalIndex(proposalId, topSlateProposalIds); + // invariant ES1: A proposal can only be executed if it's listed in the final funded proposal slate at the end of the challenge round. + if (proposalIndex == -1) { + assertFalse(executed); + } - uint256[] memory topSlateProposalIds = _grantFund.getFundedProposalSlate(topSlateHash); + // invariant ES2: A proposal can only be executed after the challenge stage is complete. + assertEq(distributionId, proposalDistributionId); + if (executed) { + (, , uint48 endBlock, , , ) = _grantFund.getDistributionPeriodInfo(proposalDistributionId); + // TODO: store and check proposal execution time + require( + currentBlock > endBlock, + "invariant ES2: A proposal can only be executed after the challenge stage is complete." + ); - // calculate the total tokens requested by the proposals in the top slate - uint256 totalTokensRequested = 0; - for (uint256 i = 0; i < topSlateProposalIds.length; ++i) { - uint256 proposalId = topSlateProposalIds[i]; - (, , , uint128 tokensRequested, , ) = _grantFund.getProposalInfo(proposalId); - totalTokensRequested += tokensRequested; - } + // check if proposalId exist in topTenScreenedProposals if it is executed + require( + _findProposalIndex(proposalId, topTenScreenedProposalIds) != -1, + "invariant ES4: A proposal can only be executed if it was in the top ten screened proposals at the end of the screening stage." + ); - uint256[] memory standardFundingProposals = _standardHandler.getStandardFundingProposals(distributionId); + require( + tokenRequested <= gbc * 9 / 10, + "invariant ES5: An executed proposal should only ever transfer tokens <= 90% of GBC" + ); - // check the state of every proposal submitted in this distribution period - for (uint256 i = 0; i < standardFundingProposals.length; ++i) { - uint256 proposalId = standardFundingProposals[i]; - (, uint24 proposalDistributionId, , , , bool executed) = _grantFund.getProposalInfo(proposalId); - int256 proposalIndex = _findProposalIndex(proposalId, topSlateProposalIds); - // invariant ES1: A proposal can only be executed if it's listed in the final funded proposal slate at the end of the challenge round. - if (proposalIndex == -1) { - assertFalse(executed); + } } - // invariant ES2: A proposal can only be executed if it's listed in the final funded proposal slate at the end of the challenge round. - assertEq(distributionId, proposalDistributionId); - if (executed) { - (, , uint48 endBlock, , , ) = _grantFund.getDistributionPeriodInfo(proposalDistributionId); - // TODO: store and check proposal execution time - require( - currentBlock > endBlock, - "invariant ES2: A proposal can only be executed after the challenge stage is complete." - ); - } - } + require( + !_standardHandler.hasDuplicates(_standardHandler.getProposalsExecuted()), + "invariant ES3: A proposal can only be executed once." + ); - require( - !_standardHandler.hasDuplicates(_standardHandler.getProposalsExecuted()), - "invariant ES3: A proposal can only be executed once." - ); + --distributionId; + } } - function invariant_DR1_DR2_DR3_DR5() external { + function invariant_DR1_DR2_DR3_DR4_DR5() external { uint24 distributionId = _grantFund.getDistributionId(); - (, , , uint128 fundsAvailable, uint256 fundingVotePowerCast, ) = _grantFund.getDistributionPeriodInfo(distributionId); + DistributionInfo memory distributionInfo; + while (distributionId > 0) { + (, , distributionInfo.endBlock, distributionInfo.fundsAvailable, distributionInfo.fundingVotePowerCast, ) = _grantFund.getDistributionPeriodInfo(distributionId); - uint256 totalRewardsClaimed; + uint256 totalRewardsClaimed; - for (uint256 i = 0; i < _standardHandler.getActorsCount(); ++i) { - address actor = _standardHandler.actors(i); + for (uint256 i = 0; i < _standardHandler.getActorsCount(); ++i) { + address actor = _standardHandler.actors(i); - // get the initial funding stage voting power of the actor - (uint128 votingPower, uint128 remainingVotingPower, ) = _grantFund.getVoterInfo(distributionId, actor); + // get the initial funding stage voting power of the actor + LocalVotersInfo memory votersInfo; + (votersInfo.fundingVotingPower, votersInfo.fundingRemainingVotingPower, ) = _grantFund.getVoterInfo(distributionId, actor); - // get actor info from standard handler - ( - IGrantFund.FundingVoteParams[] memory fundingVoteParams, - IGrantFund.ScreeningVoteParams[] memory screeningVoteParams, - uint256 delegationRewardsClaimed - ) = _standardHandler.getVotingActorsInfo(actor, distributionId); + // get actor info from standard handler + ( + IGrantFund.FundingVoteParams[] memory fundingVoteParams, + IGrantFund.ScreeningVoteParams[] memory screeningVoteParams, + uint256 delegationRewardsClaimed + ) = _standardHandler.getVotingActorsInfo(actor, distributionId); - totalRewardsClaimed += delegationRewardsClaimed; + totalRewardsClaimed += delegationRewardsClaimed; - if (delegationRewardsClaimed != 0) { - // check that delegation rewards are greater tahn 0 if they did vote in both stages - assertTrue(delegationRewardsClaimed >= 0); + if (delegationRewardsClaimed != 0) { + // check that delegation rewards are greater tahn 0 if they did vote in both stages + assertTrue(delegationRewardsClaimed >= 0); - uint256 votingPowerAllocatedByDelegatee = votingPower - remainingVotingPower; - uint256 rootVotingPowerAllocatedByDelegatee = Math.sqrt(votingPowerAllocatedByDelegatee * 1e18); + uint256 votingPowerAllocatedByDelegatee = votersInfo.fundingVotingPower - votersInfo.fundingRemainingVotingPower; + uint256 rootVotingPowerAllocatedByDelegatee = Math.sqrt(votingPowerAllocatedByDelegatee * 1e18); - require( - fundingVoteParams.length > 0 && screeningVoteParams.length > 0, - "invariant DR2: Delegation rewards are 0 if voter didn't vote in both stages." - ); + require( + fundingVoteParams.length > 0 && screeningVoteParams.length > 0, + "invariant DR2: Delegation rewards are 0 if voter didn't vote in both stages." + ); - uint256 rewards; - if (votingPowerAllocatedByDelegatee > 0) { - rewards = Math.mulDiv( - fundsAvailable, - rootVotingPowerAllocatedByDelegatee, - 10 * fundingVotePowerCast + uint256 rewards; + if (votingPowerAllocatedByDelegatee > 0) { + rewards = Math.mulDiv( + distributionInfo.fundsAvailable, + rootVotingPowerAllocatedByDelegatee, + 10 * distributionInfo.fundingVotePowerCast + ); + } + + require( + delegationRewardsClaimed == rewards, + "invariant DR3: Delegation rewards are proportional to voters funding power allocated in the funding stage." ); + + if (distributionInfo.endBlock >= block.timestamp) { + require( + _grantFund.getHasClaimedRewards(distributionId, actor) == false, + "invariant DR4: Delegation rewards can only be claimed for a distribution period after it ended" + ); + } } + } + require( + totalRewardsClaimed <= distributionInfo.fundsAvailable * 1 / 10, + "invariant DR1: Cumulative delegation rewards should be <= 10% of a distribution periods GBC" + ); + + // check state after all possible delegation rewards have been claimed + if (_standardHandler.numberOfCalls('SFH.claimDelegateReward.success') == _standardHandler.getActorsCount()) { require( - delegationRewardsClaimed == rewards, - "invariant DR3: Delegation rewards are proportional to voters funding power allocated in the funding stage." + totalRewardsClaimed >= Maths.wmul(distributionInfo.fundsAvailable * 1 / 10, 0.9999 * 1e18), + "invariant DR5: Cumulative rewards claimed should be within 99.99% -or- 0.01 AJNA tokens of all available delegation rewards" ); + assertEq(totalRewardsClaimed, distributionInfo.fundsAvailable * 1 / 10); } - } - require( - totalRewardsClaimed <= fundsAvailable * 1 / 10, - "invariant DR1: Cumulative delegation rewards should be <= 10% of a distribution periods GBC" - ); - - // check state after all possible delegation rewards have been claimed - if (_standardHandler.numberOfCalls('SFH.claimDelegateReward.success') == _standardHandler.getActorsCount()) { - require( - totalRewardsClaimed >= Maths.wmul(fundsAvailable * 1 / 10, 0.9999 * 1e18), - "invariant DR5: Cumulative rewards claimed should be within 99.99% -or- 0.01 AJNA tokens of all available delegation rewards" - ); - assertEq(totalRewardsClaimed, fundsAvailable * 1 / 10); + --distributionId; } } diff --git a/test/invariants/StandardMultipleDistributionInvariant.t.sol b/test/invariants/StandardMultipleDistributionInvariant.t.sol index 81c79bbb..c5dd0084 100644 --- a/test/invariants/StandardMultipleDistributionInvariant.t.sol +++ b/test/invariants/StandardMultipleDistributionInvariant.t.sol @@ -122,15 +122,10 @@ contract StandardMultipleDistributionInvariant is StandardTestBase { } } - function invariant_DP6() external { + function invariant_DP6_DP7() external { uint24 distributionId = _grantFund.getDistributionId(); - for (uint24 i = 0; i <= distributionId; ) { - if (i == 0) { - ++i; - continue; - } - + for (uint24 i = 1; i <= distributionId; ) { ( , , diff --git a/test/invariants/StandardScreeningInvariant.t.sol b/test/invariants/StandardScreeningInvariant.t.sol index 50f8c10e..729374ff 100644 --- a/test/invariants/StandardScreeningInvariant.t.sol +++ b/test/invariants/StandardScreeningInvariant.t.sol @@ -30,11 +30,11 @@ contract StandardScreeningInvariant is StandardTestBase { } - function invariant_SS1_SS3_SS4_SS5_SS6_SS7_SS9() external { + function invariant_SS1_SS3_SS4_SS5_SS6_SS7_SS9_SS10_SS11() external { uint24 distributionId = _grantFund.getDistributionId(); uint256 standardFundingProposalsSubmitted = _standardHandler.getStandardFundingProposals(distributionId).length; uint256[] memory topTenProposals = _grantFund.getTopTenProposals(distributionId); - (, uint256 startBlock, , , , ) = _grantFund.getDistributionPeriodInfo(distributionId); + (, uint256 startBlock, , uint256 gbc, , ) = _grantFund.getDistributionPeriodInfo(distributionId); require( topTenProposals.length <= 10 && standardFundingProposalsSubmitted >= topTenProposals.length, @@ -72,9 +72,11 @@ contract StandardScreeningInvariant is StandardTestBase { assertGe(votesReceivedLast, 0); } + uint256[] memory proposalIds = new uint256[](standardFundingProposalsSubmitted); + // check invariants against all submitted proposals for (uint256 j = 0; j < standardFundingProposalsSubmitted; ++j) { - (uint256 proposalId, , uint256 votesReceived, , , ) = _grantFund.getProposalInfo(_standardHandler.standardFundingProposals(distributionId, j)); + (uint256 proposalId, , uint256 votesReceived, uint256 tokensRequested, , ) = _grantFund.getProposalInfo(_standardHandler.standardFundingProposals(distributionId, j)); require( votesReceived >= 0, "invariant SS4: Screening votes recieved for a proposal can only be positive" @@ -101,6 +103,18 @@ contract StandardScreeningInvariant is StandardTestBase { testProposal.blockAtCreation <= _grantFund.getScreeningStageEndBlock(startBlock), "invariant SS9: A proposal can only be created during a distribution period's screening stage" ); + + // check proposalId is unique + require( + !hasDuplicates(proposalIds), "invariant SS10: A proposal's proposalId must be unique" + ); + + // Add current proposal Id to proposalIds set + proposalIds[j] = proposalId; + + require( + tokensRequested <= gbc * 9 / 10, "invariant SS11: A proposal's tokens requested must be <= 90% of GBC" + ); } // invariant SS6: proposals should be incorporated into the top ten list if, and only if, they have as many or more votes as the last member of the top ten list. From e7e9d324538b8d05cb1dd9b0888dcc0ce7d69af8 Mon Sep 17 00:00:00 2001 From: Mike Hathaway Date: Tue, 13 Jun 2023 14:21:58 -0400 Subject: [PATCH 03/20] Update invariants (#106) * rename invariant tests * refactor invariant test structure * additional refactoring * refactor multiple distribution invariants * fix stack too deep error * fix screening invariants * fix DR4 bug * remove redundant DP6 check * pr feedback --------- Co-authored-by: Mike --- .../invariants/StandardFundingInvariant.t.sol | 152 ------------ ...tandardMultipleDistributionInvariant.t.sol | 216 ------------------ .../base/DistributionPeriodInvariants.sol | 155 +++++++++++++ .../FinalizeInvariants.sol} | 152 +++--------- test/invariants/base/FundingInvariants.sol | 109 +++++++++ .../ScreeningInvariants.sol} | 95 +++----- test/invariants/base/StandardTestBase.sol | 12 +- .../scenarios/FinalizeInvariant.t.sol | 98 ++++++++ .../scenarios/FundingInvariant.t.sol | 73 ++++++ .../MultipleDistributionInvariant.t.sol | 98 ++++++++ .../scenarios/ScreeningInvariant.t.sol | 46 ++++ 11 files changed, 660 insertions(+), 546 deletions(-) delete mode 100644 test/invariants/StandardFundingInvariant.t.sol delete mode 100644 test/invariants/StandardMultipleDistributionInvariant.t.sol create mode 100644 test/invariants/base/DistributionPeriodInvariants.sol rename test/invariants/{StandardFinalizeInvariant.t.sol => base/FinalizeInvariants.sol} (52%) create mode 100644 test/invariants/base/FundingInvariants.sol rename test/invariants/{StandardScreeningInvariant.t.sol => base/ScreeningInvariants.sol} (61%) create mode 100644 test/invariants/scenarios/FinalizeInvariant.t.sol create mode 100644 test/invariants/scenarios/FundingInvariant.t.sol create mode 100644 test/invariants/scenarios/MultipleDistributionInvariant.t.sol create mode 100644 test/invariants/scenarios/ScreeningInvariant.t.sol diff --git a/test/invariants/StandardFundingInvariant.t.sol b/test/invariants/StandardFundingInvariant.t.sol deleted file mode 100644 index bb6cb7f9..00000000 --- a/test/invariants/StandardFundingInvariant.t.sol +++ /dev/null @@ -1,152 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.18; - -import { console } from "@std/console.sol"; -import { SafeCast } from "@oz/utils/math/SafeCast.sol"; - -import { IGrantFund } from "../../src/grants/interfaces/IGrantFund.sol"; - -import { StandardTestBase } from "./base/StandardTestBase.sol"; -import { StandardHandler } from "./handlers/StandardHandler.sol"; - -contract StandardFundingInvariant is StandardTestBase { - - // hash the top ten proposals at the start of the funding stage to check composition - bytes32 initialTopTenHash; - - // override setup to start tests in the funding stage with already screened proposals - function setUp() public override { - super.setUp(); - - startDistributionPeriod(); - - // create 15 proposals - _standardHandler.createProposals(15); - - // cast screening votes on proposals - _standardHandler.screeningVoteProposals(); - - // skip time into the funding stage - uint24 distributionId = _grantFund.getDistributionId(); - (, uint256 startBlock, , , , ) = _grantFund.getDistributionPeriodInfo(distributionId); - uint256 fundingStageStartBlock = _grantFund.getScreeningStageEndBlock(startBlock) + 1; - vm.roll(fundingStageStartBlock + 100); - currentBlock = fundingStageStartBlock + 100; - - // set the list of function selectors to run - bytes4[] memory selectors = new bytes4[](2); - selectors[0] = _standardHandler.fundingVote.selector; - selectors[1] = _standardHandler.updateSlate.selector; - - // ensure utility functions are excluded from the invariant runs - targetSelector(FuzzSelector({ - addr: address(_standardHandler), - selectors: selectors - })); - - uint256[] memory initialTopTenProposals = _grantFund.getTopTenProposals(_grantFund.getDistributionId()); - initialTopTenHash = keccak256(abi.encode(initialTopTenProposals)); - } - - function invariant_FS1_FS2_FS3() external { - uint256[] memory topTenProposals = _grantFund.getTopTenProposals(_grantFund.getDistributionId()); - - // invariant FS1: 10 or less proposals should make it through the screening stage - assertTrue(topTenProposals.length <= 10); - assertTrue(topTenProposals.length > 0); // check if something went wrong in setup - - uint24 distributionId = _grantFund.getDistributionId(); - uint256[] memory standardFundingProposals = _standardHandler.getStandardFundingProposals(distributionId); - - // check invariants against every proposal - for (uint256 j = 0; j < standardFundingProposals.length; ++j) { - uint256 proposalId = _standardHandler.standardFundingProposals(distributionId, j); - (, uint24 proposalDistributionId, , , int128 fundingVotesReceived, ) = _grantFund.getProposalInfo(proposalId); - - // invariant FS2: proposals not in the top ten should not be able to recieve funding votes - if (_findProposalIndex(proposalId, topTenProposals) == -1) { - assertEq(fundingVotesReceived, 0); - } - - require( - distributionId == proposalDistributionId, - "invariant FS3: distribution id for a proposal should be the same as the current distribution id" - ); - } - } - - function invariant_FS4_FS5_FS6_FS7_FS8() external { - uint24 distributionId = _grantFund.getDistributionId(); - - // check invariants against every actor - for (uint256 i = 0; i < _standardHandler.getActorsCount(); ++i) { - address actor = _standardHandler.actors(i); - - // get the initial funding stage voting power of the actor - (uint128 votingPower, uint128 remainingVotingPower, uint256 numberOfProposalsVotedOn) = _grantFund.getVoterInfo(distributionId, actor); - - // get the voting info of the actor - (IGrantFund.FundingVoteParams[] memory fundingVoteParams, , ) = _standardHandler.getVotingActorsInfo(actor, distributionId); - - uint128 sumOfSquares = SafeCast.toUint128(_standardHandler.sumSquareOfVotesCast(fundingVoteParams)); - - // check voter votes cast are less than or equal to the sqrt of the voting power of the actor - IGrantFund.FundingVoteParams[] memory fundingVotesCast = _grantFund.getFundingVotesCast(distributionId, actor); - - require( - sumOfSquares <= votingPower, - "invariant FS4: sum of square of votes cast <= voting power of actor" - ); - // invariant FS5: Sum of voter's votesCast should be equal to the square root of the voting power expended (FS4 restated, but added to test intermediate state as well as final). - assertEq(sumOfSquares, votingPower - remainingVotingPower); - - // check that the test functioned as expected - if (votingPower != 0 && remainingVotingPower == 0) { - assertTrue(numberOfProposalsVotedOn == fundingVotesCast.length); - assertTrue(numberOfProposalsVotedOn > 0); - } - - require( - uint256(_standardHandler.sumFundingVotes(fundingVoteParams)) <= _ajna.totalSupply(), - "invariant FS8: a voter should never be able to cast more votes than the Ajna token supply" - ); - - // check that there weren't any duplicate proposal entries, as votes for same proposal should be combined - uint256[] memory proposalIdsVotedOn = new uint256[](fundingVotesCast.length); - for (uint j = 0; j < fundingVotesCast.length; ) { - proposalIdsVotedOn[j] = fundingVotesCast[j].proposalId; - ++j; - } - require( - _standardHandler.hasDuplicates(proposalIdsVotedOn) == false, - "invariant FS6: All voter funding votes on a proposal should be cast in the same direction. Multiple votes on the same proposal should see the voting power increase according to the combined cost of votes." - ); - } - - require( - keccak256(abi.encode(_grantFund.getTopTenProposals(distributionId))) == initialTopTenHash, - "invariant FS7: List of top ten proposals should never change once the funding stage has started" - ); - } - - function invariant_call_summary() external view { - uint24 distributionId = _grantFund.getDistributionId(); - - _standardHandler.logCallSummary(); - // _standardHandler.logProposalSummary(); - // _standardHandler.logActorSummary(distributionId, true, false); - _logFundingSummary(distributionId); - } - - function _logFundingSummary(uint24 distributionId_) internal view { - console.log("\nFunding Summary\n"); - console.log("------------------"); - console.log("number of funding stage starts: ", _standardHandler.numberOfCalls("SFH.FundingStage")); - console.log("number of funding stage success votes: ", _standardHandler.numberOfCalls("SFH.fundingVote.success")); - console.log("distributionId: ", distributionId_); - console.log("SFH.updateSlate.success: ", _standardHandler.numberOfCalls("SFH.updateSlate.success")); - console.log("------------------"); - } - -} diff --git a/test/invariants/StandardMultipleDistributionInvariant.t.sol b/test/invariants/StandardMultipleDistributionInvariant.t.sol deleted file mode 100644 index c5dd0084..00000000 --- a/test/invariants/StandardMultipleDistributionInvariant.t.sol +++ /dev/null @@ -1,216 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.18; - -import { console } from "@std/console.sol"; -import { SafeCast } from "@oz/utils/math/SafeCast.sol"; - -import { Maths } from "../../src/grants/libraries/Maths.sol"; - -import { StandardTestBase } from "./base/StandardTestBase.sol"; -import { StandardHandler } from "./handlers/StandardHandler.sol"; -import { Handler } from "./handlers/Handler.sol"; - -contract StandardMultipleDistributionInvariant is StandardTestBase { - - // run tests against all functions, having just started a distribution period - function setUp() public override { - super.setUp(); - - // set the list of function selectors to run - bytes4[] memory selectors = new bytes4[](8); - selectors[0] = _standardHandler.startNewDistributionPeriod.selector; - selectors[1] = _standardHandler.propose.selector; - selectors[2] = _standardHandler.screeningVote.selector; - selectors[3] = _standardHandler.fundingVote.selector; - selectors[4] = _standardHandler.updateSlate.selector; - selectors[5] = _standardHandler.execute.selector; - selectors[6] = _standardHandler.claimDelegateReward.selector; - selectors[7] = _standardHandler.roll.selector; - - // ensure utility functions are excluded from the invariant runs - targetSelector(FuzzSelector({ - addr: address(_standardHandler), - selectors: selectors - })); - - // update scenarioType to fast to have larger rolls - _standardHandler.setCurrentScenarioType(Handler.ScenarioType.Fast); - - vm.roll(block.number + 100); - currentBlock = block.number; - } - - function invariant_DP1_DP2_DP3_DP4_DP5() external { - uint24 distributionId = _grantFund.getDistributionId(); - console.log("distributionId??", distributionId); - ( - , - uint256 startBlockCurrent, - uint256 endBlockCurrent, - , - , - ) = _grantFund.getDistributionPeriodInfo(distributionId); - - uint256 totalFundsAvailable = 0; - - uint24 i = distributionId; - while (i > 0) { - ( - , - uint256 startBlockPrev, - uint256 endBlockPrev, - uint128 fundsAvailablePrev, - , - ) = _grantFund.getDistributionPeriodInfo(i); - StandardHandler.DistributionState memory state = _standardHandler.getDistributionState(i); - uint256 currentTreasury = state.treasuryBeforeStart; - - totalFundsAvailable += fundsAvailablePrev; - require( - totalFundsAvailable < currentTreasury, - "invariant DP5: The treasury balance should be greater than the sum of the funds available in all distribution periods" - ); - - require( - fundsAvailablePrev == Maths.wmul(.03 * 1e18, state.treasuryAtStartBlock + fundsAvailablePrev), - "invariant DP3: A distribution's fundsAvailablePrev should be equal to 3% of the treasurie's balance at the block `startNewDistributionPeriod()` is called" - ); - - require( - endBlockPrev > startBlockPrev, - "invariant DP4: A distribution's endBlock should be greater than its startBlock" - ); - - uint256 totalTokensRequestedByProposals = 0; - - // check the top funded proposal slate - uint256[] memory proposalSlate = _grantFund.getFundedProposalSlate(state.currentTopSlate); - for (uint j = 0; j < proposalSlate.length; ++j) { - ( - , - uint24 proposalDistributionId, - , - uint128 tokensRequested, - , - bool executed - ) = _grantFund.getProposalInfo(proposalSlate[j]); - assertEq(proposalDistributionId, i); - - if (executed) { - // invariant DP2: Each winning proposal successfully claims no more that what was finalized in the challenge stage - assertLt(tokensRequested, fundsAvailablePrev); - } - totalTokensRequestedByProposals += tokensRequested; - } - assertTrue(totalTokensRequestedByProposals <= fundsAvailablePrev); - - // check invariants against each previous distribution periods - if (i != distributionId) { - // check each distribution period's end block and ensure that only 1 has an endblock not in the past. - require( - endBlockPrev < startBlockCurrent && endBlockPrev < currentBlock, - "invariant DP1: Only one distribution period should be active at a time" - ); - - // decrement blocks to ensure that the next distribution period's end block is less than the current block - startBlockCurrent = startBlockPrev; - endBlockCurrent = endBlockPrev; - } - - --i; - } - } - - function invariant_DP6_DP7() external { - uint24 distributionId = _grantFund.getDistributionId(); - - for (uint24 i = 1; i <= distributionId; ) { - ( - , - , - , - uint128 fundsAvailable, - , - ) = _grantFund.getDistributionPeriodInfo(i); - StandardHandler.DistributionState memory state = _standardHandler.getDistributionState(i); - - // check prior distributions for surplus to return to treasury - uint24 prevDistributionId = i - 1; - ( - , - , - , - uint128 fundsAvailablePrev, - , - bytes32 topSlateHashPrev - ) = _grantFund.getDistributionPeriodInfo(prevDistributionId); - - // calculate the expected treasury amount at the start of the current distribution period - uint256 expectedTreasury = state.treasuryBeforeStart; - uint256 surplus = _standardHandler.updateTreasury(prevDistributionId, fundsAvailablePrev, topSlateHashPrev); - expectedTreasury += surplus; - - if (i == 1) { - require( - fundsAvailable == Maths.wmul(.03 * 1e18, state.treasuryBeforeStart), - "invariant DP6: Surplus funds from distribution periods whose token's requested in the final funded slate was less than the total funds available are readded to the treasury" - ); - } - else { - require( - fundsAvailable == Maths.wmul(.03 * 1e18, expectedTreasury), - "invariant DP6: Surplus funds from distribution periods whose token's requested in the final funded slate was less than the total funds available are readded to the treasury" - ); - } - - ++i; - } - } - - function invariant_T1_T2() external view { - require( - _grantFund.treasury() <= _ajna.balanceOf(address(_grantFund)), - "invariant T1: The Grant Fund's treasury should always be less than or equal to the contract's token blance" - ); - - require( - _grantFund.treasury() <= _ajna.totalSupply(), - "invariant T2: The Grant Fund's treasury should always be less than or equal to the Ajna token total supply" - ); - } - - function invariant_call_summary() external view { - // uint24 distributionId = _grantFund.getDistributionId(); - - _standardHandler.logCallSummary(); - _standardHandler.logTimeSummary(); - - console.log("scenario type", uint8(_standardHandler.getCurrentScenarioType())); - - console.log("Delegation Rewards: ", _standardHandler.numberOfCalls('delegationRewardSet')); - console.log("Delegation Rewards Claimed: ", _standardHandler.numberOfCalls('SFH.claimDelegateReward.success')); - console.log("Proposal Execute attempt: ", _standardHandler.numberOfCalls('SFH.execute.attempt')); - console.log("Proposal Execute Count: ", _standardHandler.numberOfCalls('SFH.execute.success')); - console.log("Slate Update Prep: ", _standardHandler.numberOfCalls('SFH.updateSlate.prep')); - console.log("Slate Update length: ", _standardHandler.numberOfCalls('updateSlate.length')); - console.log("Slate Update Called: ", _standardHandler.numberOfCalls('SFH.updateSlate.called')); - console.log("Slate Update Success: ", _standardHandler.numberOfCalls('SFH.updateSlate.success')); - console.log("Slate Proposals: ", _standardHandler.numberOfCalls('proposalsInSlates')); - console.log("unused proposal: ", _standardHandler.numberOfCalls('unused.proposal')); - console.log("unexecuted proposal: ", _standardHandler.numberOfCalls('unexecuted.proposal')); - console.log("funding stage starts: ", _standardHandler.numberOfCalls("SFH.FundingStage")); - console.log("funding stage success votes ", _standardHandler.numberOfCalls("SFH.fundingVote.success")); - - - (, , , , uint256 fundingPowerCast, ) = _grantFund.getDistributionPeriodInfo(2); - console.log("Total Funding Power Cast ", fundingPowerCast); - - - if (_standardHandler.numberOfCalls('unexecuted.proposal') != 0) { - console.log("state of unexecuted: ", uint8(_grantFund.state(_standardHandler.numberOfCalls('unexecuted.proposal')))); - } - // _standardHandler.logProposalSummary(); - // _standardHandler.logActorSummary(distributionId, true, true); - } -} diff --git a/test/invariants/base/DistributionPeriodInvariants.sol b/test/invariants/base/DistributionPeriodInvariants.sol new file mode 100644 index 00000000..42bf6d03 --- /dev/null +++ b/test/invariants/base/DistributionPeriodInvariants.sol @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.18; + +import { console } from "@std/console.sol"; +import { Math } from "@oz/utils/math/Math.sol"; +import { SafeCast } from "@oz/utils/math/SafeCast.sol"; + +import { GrantFund } from "../../../src/grants/GrantFund.sol"; +import { IGrantFund } from "../../../src/grants/interfaces/IGrantFund.sol"; +import { Maths } from "../../../src/grants/libraries/Maths.sol"; + +import { TestBase } from "./TestBase.sol"; +import { StandardHandler } from "../handlers/StandardHandler.sol"; + +abstract contract DistributionPeriodInvariants is TestBase { + + function _invariant_DP1_DP2_DP3_DP4_DP5(GrantFund grantFund_, StandardHandler standardHandler_) internal { + uint24 distributionId = grantFund_.getDistributionId(); + ( + , + uint256 startBlockCurrent, + uint256 endBlockCurrent, + , + , + ) = grantFund_.getDistributionPeriodInfo(distributionId); + + uint256 totalFundsAvailable = 0; + + uint24 i = distributionId; + while (i > 0) { + ( + , + uint256 startBlockPrev, + uint256 endBlockPrev, + uint128 fundsAvailablePrev, + , + ) = grantFund_.getDistributionPeriodInfo(i); + StandardHandler.DistributionState memory state = standardHandler_.getDistributionState(i); + + totalFundsAvailable += fundsAvailablePrev; + require( + totalFundsAvailable < state.treasuryBeforeStart, + "invariant DP5: The treasury balance should be greater than the sum of the funds available in all distribution periods" + ); + + require( + fundsAvailablePrev == Maths.wmul(.03 * 1e18, state.treasuryAtStartBlock + fundsAvailablePrev), + "invariant DP3: A distribution's fundsAvailablePrev should be equal to 3% of the treasury's balance at the block `startNewDistributionPeriod()` is called" + ); + + require( + endBlockPrev > startBlockPrev, + "invariant DP4: A distribution's endBlock should be greater than its startBlock" + ); + + // check invariant DP5 + // seperate function avoids stack too deep error + _invariant_DP5(_grantFund, state, i, fundsAvailablePrev); + + // check invariants against each previous distribution periods + if (i != distributionId) { + // check each distribution period's end block and ensure that only 1 has an endblock not in the past. + require( + endBlockPrev < startBlockCurrent && endBlockPrev < currentBlock, + "invariant DP1: Only one distribution period should be active at a time" + ); + + // decrement blocks to ensure that the next distribution period's end block is less than the current block + startBlockCurrent = startBlockPrev; + endBlockCurrent = endBlockPrev; + } + + --i; + } + } + + function _invariant_DP5(GrantFund grantFund_, StandardHandler.DistributionState memory state, uint256 distributionId_, uint256 fundsAvailablePrev_) internal { + uint256 totalTokensRequestedByProposals = 0; + + // check the top funded proposal slate + uint256[] memory proposalSlate = grantFund_.getFundedProposalSlate(state.currentTopSlate); + for (uint j = 0; j < proposalSlate.length; ++j) { + ( + , + uint24 proposalDistributionId, + , + uint128 tokensRequested, + , + bool executed + ) = grantFund_.getProposalInfo(proposalSlate[j]); + assertEq(proposalDistributionId, distributionId_); + + if (executed) { + require( + tokensRequested < fundsAvailablePrev_, + "invariant DP2: Each winning proposal successfully claims no more that what was finalized in the challenge stage" + ); + } + totalTokensRequestedByProposals += tokensRequested; + } + assertTrue(totalTokensRequestedByProposals <= fundsAvailablePrev_); + } + + function _invariant_DP6(GrantFund grantFund_, StandardHandler standardHandler_) internal { + uint24 distributionId = grantFund_.getDistributionId(); + + for (uint24 i = 1; i <= distributionId; ) { + ( + , + , + , + uint128 fundsAvailable, + , + ) = grantFund_.getDistributionPeriodInfo(i); + StandardHandler.DistributionState memory state = standardHandler_.getDistributionState(i); + + // check prior distributions for surplus to return to treasury + uint24 prevDistributionId = i - 1; + ( + , + , + , + uint128 fundsAvailablePrev, + , + bytes32 topSlateHashPrev + ) = grantFund_.getDistributionPeriodInfo(prevDistributionId); + + // calculate the expected treasury amount at the start of the current distribution period + uint256 expectedTreasury = state.treasuryBeforeStart; + uint256 surplus = standardHandler_.updateTreasury(prevDistributionId, fundsAvailablePrev, topSlateHashPrev); + expectedTreasury += surplus; + + require( + fundsAvailable == Maths.wmul(.03 * 1e18, expectedTreasury), + "invariant DP6: Surplus funds from distribution periods whose token's requested in the final funded slate was less than the total funds available are readded to the treasury" + ); + + ++i; + } + } + + function _invariant_T1_T2(GrantFund grantFund_, StandardHandler standardHandler_) internal view { + require( + grantFund_.treasury() <= _ajna.balanceOf(address(grantFund_)), + "invariant T1: The Grant Fund's treasury should always be less than or equal to the contract's token blance" + ); + + require( + grantFund_.treasury() <= _ajna.totalSupply(), + "invariant T2: The Grant Fund's treasury should always be less than or equal to the Ajna token total supply" + ); + } + +} diff --git a/test/invariants/StandardFinalizeInvariant.t.sol b/test/invariants/base/FinalizeInvariants.sol similarity index 52% rename from test/invariants/StandardFinalizeInvariant.t.sol rename to test/invariants/base/FinalizeInvariants.sol index 8623e1a1..ed8398f3 100644 --- a/test/invariants/StandardFinalizeInvariant.t.sol +++ b/test/invariants/base/FinalizeInvariants.sol @@ -3,17 +3,17 @@ pragma solidity 0.8.18; import { console } from "@std/console.sol"; -import { Math } from "@oz/utils/math/Math.sol"; +import { Math } from "@oz/utils/math/Math.sol"; import { SafeCast } from "@oz/utils/math/SafeCast.sol"; -import { IGrantFund } from "../../src/grants/interfaces/IGrantFund.sol"; -import { Maths } from "../../src/grants/libraries/Maths.sol"; +import { GrantFund } from "../../../src/grants/GrantFund.sol"; +import { IGrantFund } from "../../../src/grants/interfaces/IGrantFund.sol"; +import { Maths } from "../../../src/grants/libraries/Maths.sol"; -import { StandardTestBase } from "./base/StandardTestBase.sol"; -import { StandardHandler } from "./handlers/StandardHandler.sol"; -import { Handler } from "./handlers/Handler.sol"; +import { TestBase } from "./TestBase.sol"; +import { StandardHandler } from "../handlers/StandardHandler.sol"; -contract StandardFinalizeInvariant is StandardTestBase { +abstract contract FinalizeInvariants is TestBase { struct LocalVotersInfo { uint128 fundingVotingPower; @@ -30,58 +30,13 @@ contract StandardFinalizeInvariant is StandardTestBase { bytes32 fundedSlateHash; } - // override setup to start tests in the challenge stage with proposals that have already been screened and funded - function setUp() public override { - super.setUp(); + function _invariant_CS1_CS2_CS3_CS4_CS5_CS6(GrantFund grantFund_, StandardHandler standardHandler_) view internal { + uint24 distributionId = grantFund_.getDistributionId(); - startDistributionPeriod(); + (, , uint256 endBlock, uint128 fundsAvailable, , bytes32 topSlateHash) = grantFund_.getDistributionPeriodInfo(distributionId); - // create 15 proposals - _standardHandler.createProposals(15); - - // cast screening votes on proposals - _standardHandler.screeningVoteProposals(); - - // skip time into the funding stage - uint24 distributionId = _grantFund.getDistributionId(); - (, uint256 startBlock, uint256 endBlock, , , ) = _grantFund.getDistributionPeriodInfo(distributionId); - uint256 fundingStageStartBlock = _grantFund.getScreeningStageEndBlock(startBlock) + 1; - vm.roll(fundingStageStartBlock + 100); - currentBlock = fundingStageStartBlock + 100; - - // cast funding votes on proposals - _standardHandler.fundingVoteProposals(); - - _standardHandler.setCurrentScenarioType(Handler.ScenarioType.Medium); - - // skip time into the challenge stage - uint256 challengeStageStartBlock = _grantFund.getChallengeStageStartBlock(endBlock); - vm.roll(challengeStageStartBlock + 100); - currentBlock = challengeStageStartBlock + 100; - - // set the list of function selectors to run - bytes4[] memory selectors = new bytes4[](5); - selectors[0] = _standardHandler.fundingVote.selector; - selectors[1] = _standardHandler.updateSlate.selector; - selectors[2] = _standardHandler.execute.selector; - selectors[3] = _standardHandler.claimDelegateReward.selector; - selectors[4] = _standardHandler.roll.selector; - - // ensure utility functions are excluded from the invariant runs - targetSelector(FuzzSelector({ - addr: address(_standardHandler), - selectors: selectors - })); - } - - function invariant_CS1_CS2_CS3_CS4_CS5_CS6() view external { - uint24 distributionId = _grantFund.getDistributionId(); - - (, , uint256 endBlock, uint128 fundsAvailable, , bytes32 topSlateHash) = _grantFund.getDistributionPeriodInfo(distributionId); - - uint256[] memory topSlateProposalIds = _grantFund.getFundedProposalSlate(topSlateHash); - - uint256[] memory topTenScreenedProposalIds = _grantFund.getTopTenProposals(distributionId); + uint256[] memory topSlateProposalIds = grantFund_.getFundedProposalSlate(topSlateHash); + uint256[] memory topTenScreenedProposalIds = grantFund_.getTopTenProposals(distributionId); require( topSlateProposalIds.length <= 10, @@ -92,7 +47,7 @@ contract StandardFinalizeInvariant is StandardTestBase { uint256 totalTokensRequested = 0; for (uint256 i = 0; i < topSlateProposalIds.length; ++i) { uint256 proposalId = topSlateProposalIds[i]; - (, , , uint128 tokensRequested, int128 fundingVotesReceived, ) = _grantFund.getProposalInfo(proposalId); + (, , , uint128 tokensRequested, int128 fundingVotesReceived, ) = grantFund_.getProposalInfo(proposalId); totalTokensRequested += tokensRequested; require( @@ -112,36 +67,34 @@ contract StandardFinalizeInvariant is StandardTestBase { ); require( - !_standardHandler.hasDuplicates(topSlateProposalIds), + !standardHandler_.hasDuplicates(topSlateProposalIds), "invariant CS5: proposal slate should never contain duplicate proposals" ); // check DistributionState for top slate updates - StandardHandler.DistributionState memory state = _standardHandler.getDistributionState(distributionId); + StandardHandler.DistributionState memory state = standardHandler_.getDistributionState(distributionId); for (uint i = 0; i < state.topSlates.length; ++i) { StandardHandler.Slate memory slate = state.topSlates[i]; require( - slate.updateBlock <= endBlock && slate.updateBlock >= _grantFund.getChallengeStageStartBlock(endBlock), + slate.updateBlock <= endBlock && slate.updateBlock >= grantFund_.getChallengeStageStartBlock(endBlock), "invariant CS6: Funded proposal slate's can only be updated during a distribution period's challenge stage" ); } } - function invariant_ES1_ES2_ES3_ES4_ES5() external { - uint24 distributionId = _grantFund.getDistributionId(); + function _invariant_ES1_ES2_ES3_ES4_ES5(GrantFund grantFund_, StandardHandler standardHandler_) internal { + uint24 distributionId = grantFund_.getDistributionId(); while (distributionId > 0) { - (, , , uint256 gbc, , bytes32 topSlateHash) = _grantFund.getDistributionPeriodInfo(distributionId); - - uint256[] memory topSlateProposalIds = _grantFund.getFundedProposalSlate(topSlateHash); - - uint256[] memory standardFundingProposals = _standardHandler.getStandardFundingProposals(distributionId); - uint256[] memory topTenScreenedProposalIds = _grantFund.getTopTenProposals(distributionId); + (, , , uint256 gbc, , bytes32 topSlateHash) = grantFund_.getDistributionPeriodInfo(distributionId); + uint256[] memory topSlateProposalIds = grantFund_.getFundedProposalSlate(topSlateHash); + uint256[] memory standardFundingProposals = standardHandler_.getStandardFundingProposals(distributionId); + uint256[] memory topTenScreenedProposalIds = grantFund_.getTopTenProposals(distributionId); // check the state of every proposal submitted in this distribution period for (uint256 i = 0; i < standardFundingProposals.length; ++i) { uint256 proposalId = standardFundingProposals[i]; - (, uint24 proposalDistributionId, , uint256 tokenRequested, , bool executed) = _grantFund.getProposalInfo(proposalId); + (, uint24 proposalDistributionId, , uint256 tokenRequested, , bool executed) = grantFund_.getProposalInfo(proposalId); int256 proposalIndex = _findProposalIndex(proposalId, topSlateProposalIds); // invariant ES1: A proposal can only be executed if it's listed in the final funded proposal slate at the end of the challenge round. if (proposalIndex == -1) { @@ -151,7 +104,7 @@ contract StandardFinalizeInvariant is StandardTestBase { // invariant ES2: A proposal can only be executed after the challenge stage is complete. assertEq(distributionId, proposalDistributionId); if (executed) { - (, , uint48 endBlock, , , ) = _grantFund.getDistributionPeriodInfo(proposalDistributionId); + (, , uint48 endBlock, , , ) = grantFund_.getDistributionPeriodInfo(proposalDistributionId); // TODO: store and check proposal execution time require( currentBlock > endBlock, @@ -173,7 +126,7 @@ contract StandardFinalizeInvariant is StandardTestBase { } require( - !_standardHandler.hasDuplicates(_standardHandler.getProposalsExecuted()), + !standardHandler_.hasDuplicates(standardHandler_.getProposalsExecuted()), "invariant ES3: A proposal can only be executed once." ); @@ -181,27 +134,27 @@ contract StandardFinalizeInvariant is StandardTestBase { } } - function invariant_DR1_DR2_DR3_DR4_DR5() external { - uint24 distributionId = _grantFund.getDistributionId(); + function _invariant_DR1_DR2_DR3_DR4_DR5(GrantFund grantFund_, StandardHandler standardHandler_) internal { + uint24 distributionId = grantFund_.getDistributionId(); DistributionInfo memory distributionInfo; while (distributionId > 0) { - (, , distributionInfo.endBlock, distributionInfo.fundsAvailable, distributionInfo.fundingVotePowerCast, ) = _grantFund.getDistributionPeriodInfo(distributionId); + (, , distributionInfo.endBlock, distributionInfo.fundsAvailable, distributionInfo.fundingVotePowerCast, ) = grantFund_.getDistributionPeriodInfo(distributionId); uint256 totalRewardsClaimed; - for (uint256 i = 0; i < _standardHandler.getActorsCount(); ++i) { - address actor = _standardHandler.actors(i); + for (uint256 i = 0; i < standardHandler_.getActorsCount(); ++i) { + address actor = standardHandler_.actors(i); // get the initial funding stage voting power of the actor LocalVotersInfo memory votersInfo; - (votersInfo.fundingVotingPower, votersInfo.fundingRemainingVotingPower, ) = _grantFund.getVoterInfo(distributionId, actor); + (votersInfo.fundingVotingPower, votersInfo.fundingRemainingVotingPower, ) = grantFund_.getVoterInfo(distributionId, actor); // get actor info from standard handler ( IGrantFund.FundingVoteParams[] memory fundingVoteParams, IGrantFund.ScreeningVoteParams[] memory screeningVoteParams, uint256 delegationRewardsClaimed - ) = _standardHandler.getVotingActorsInfo(actor, distributionId); + ) = standardHandler_.getVotingActorsInfo(actor, distributionId); totalRewardsClaimed += delegationRewardsClaimed; @@ -231,9 +184,9 @@ contract StandardFinalizeInvariant is StandardTestBase { "invariant DR3: Delegation rewards are proportional to voters funding power allocated in the funding stage." ); - if (distributionInfo.endBlock >= block.timestamp) { + if (distributionInfo.endBlock >= currentBlock) { require( - _grantFund.getHasClaimedRewards(distributionId, actor) == false, + grantFund_.getHasClaimedRewards(distributionId, actor) == false, "invariant DR4: Delegation rewards can only be claimed for a distribution period after it ended" ); } @@ -246,7 +199,7 @@ contract StandardFinalizeInvariant is StandardTestBase { ); // check state after all possible delegation rewards have been claimed - if (_standardHandler.numberOfCalls('SFH.claimDelegateReward.success') == _standardHandler.getActorsCount()) { + if (standardHandler_.numberOfCalls('SFH.claimDelegateReward.success') == standardHandler_.getActorsCount()) { require( totalRewardsClaimed >= Maths.wmul(distributionInfo.fundsAvailable * 1 / 10, 0.9999 * 1e18), "invariant DR5: Cumulative rewards claimed should be within 99.99% -or- 0.01 AJNA tokens of all available delegation rewards" @@ -258,39 +211,4 @@ contract StandardFinalizeInvariant is StandardTestBase { } } - function invariant_call_summary() external view { - uint24 distributionId = _grantFund.getDistributionId(); - - _standardHandler.logCallSummary(); - _standardHandler.logActorSummary(distributionId, false, false); - _standardHandler.logProposalSummary(); - _standardHandler.logTimeSummary(); - _logFinalizeSummary(distributionId); - } - - function _logFinalizeSummary(uint24 distributionId_) internal view { - (, , , uint128 fundsAvailable, , bytes32 topSlateHash) = _grantFund.getDistributionPeriodInfo(distributionId_); - uint256[] memory topSlateProposalIds = _grantFund.getFundedProposalSlate(topSlateHash); - - uint256[] memory topTenScreenedProposalIds = _grantFund.getTopTenProposals(distributionId_); - - console.log("\nFinalize Summary\n"); - console.log("------------------"); - console.log("Distribution Id: ", distributionId_); - console.log("Delegation Rewards Claimed: ", _standardHandler.numberOfCalls('SFH.claimDelegateReward.success')); - console.log("Proposal Execute attempt: ", _standardHandler.numberOfCalls('SFH.execute.attempt')); - console.log("Proposal Execute Count: ", _standardHandler.numberOfCalls('SFH.execute.success')); - console.log("Slate Created: ", _standardHandler.numberOfCalls('SFH.updateSlate.prep')); - console.log("Slate Update Called: ", _standardHandler.numberOfCalls('SFH.updateSlate.called')); - console.log("Slate Update Count: ", _standardHandler.numberOfCalls('SFH.updateSlate.success')); - console.log("Next Slate length: ", _standardHandler.numberOfCalls('updateSlate.length')); - console.log("Top Slate Proposal Count: ", topSlateProposalIds.length); - console.log("Top Ten Proposal Count: ", topTenScreenedProposalIds.length); - console.log("Funds Available: ", fundsAvailable); - console.log("Top slate funds requested: ", _standardHandler.getTokensRequestedInFundedSlateInvariant(topSlateHash)); - (, , , , uint256 fundingPowerCast, ) = _grantFund.getDistributionPeriodInfo(distributionId_); - console.log("Total Funding Power Cast ", fundingPowerCast); - console.log("------------------"); - } - } diff --git a/test/invariants/base/FundingInvariants.sol b/test/invariants/base/FundingInvariants.sol new file mode 100644 index 00000000..d371f9bb --- /dev/null +++ b/test/invariants/base/FundingInvariants.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.18; + +import { console } from "@std/console.sol"; +import { SafeCast } from "@oz/utils/math/SafeCast.sol"; + +import { GrantFund } from "../../../src/grants/GrantFund.sol"; +import { IGrantFund } from "../../../src/grants/interfaces/IGrantFund.sol"; + +import { TestBase } from "./TestBase.sol"; +import { StandardHandler } from "../handlers/StandardHandler.sol"; + +abstract contract FundingInvariants is TestBase { + + // hash the top ten proposals at the start of the funding stage to check composition + bytes32 initialTopTenHash; + + function _invariant_FS1_FS2_FS3(GrantFund grantFund_, StandardHandler standardHandler_) internal { + uint256[] memory topTenProposals = grantFund_.getTopTenProposals(grantFund_.getDistributionId()); + + // check if something went wrong in test setup + assertTrue(topTenProposals.length > 0); + + require( + topTenProposals.length <= 10, + "invariant FS1: 10 or less proposals should make it through the screening stage" + ); + + uint24 distributionId = grantFund_.getDistributionId(); + uint256[] memory standardFundingProposals = standardHandler_.getStandardFundingProposals(distributionId); + + // check invariants against every proposal + for (uint256 j = 0; j < standardFundingProposals.length; ++j) { + uint256 proposalId = standardHandler_.standardFundingProposals(distributionId, j); + (, uint24 proposalDistributionId, , , int128 fundingVotesReceived, ) = grantFund_.getProposalInfo(proposalId); + + if (_findProposalIndex(proposalId, topTenProposals) == -1) { + require( + fundingVotesReceived == 0, + "invariant FS2: proposals not in the top ten should not be able to recieve funding votes" + ); + } + + require( + distributionId == proposalDistributionId, + "invariant FS3: distribution id for a proposal should be the same as the current distribution id" + ); + } + } + + function _invariant_FS4_FS5_FS6_FS7_FS8(GrantFund grantFund_, StandardHandler standardHandler_) internal { + uint24 distributionId = grantFund_.getDistributionId(); + + // check invariants against every actor + for (uint256 i = 0; i < standardHandler_.getActorsCount(); ++i) { + address actor = standardHandler_.actors(i); + + // get the initial funding stage voting power of the actor + (uint128 votingPower, uint128 remainingVotingPower, uint256 numberOfProposalsVotedOn) = grantFund_.getVoterInfo(distributionId, actor); + + // get the voting info of the actor + (IGrantFund.FundingVoteParams[] memory fundingVoteParams, , ) = standardHandler_.getVotingActorsInfo(actor, distributionId); + + uint128 sumOfSquares = SafeCast.toUint128(standardHandler_.sumSquareOfVotesCast(fundingVoteParams)); + + // check voter votes cast are less than or equal to the sqrt of the voting power of the actor + IGrantFund.FundingVoteParams[] memory fundingVotesCast = grantFund_.getFundingVotesCast(distributionId, actor); + + require( + sumOfSquares <= votingPower, + "invariant FS4: sum of square of votes cast <= voting power of actor" + ); + require( + sumOfSquares == votingPower - remainingVotingPower, + "invariant FS5: Sum of voter's votesCast should be equal to the square root of the voting power expended (FS4 restated, but added to test intermediate state as well as final)." + ); + + // check that the test functioned as expected + if (votingPower != 0 && remainingVotingPower == 0) { + assertTrue(numberOfProposalsVotedOn == fundingVotesCast.length); + assertTrue(numberOfProposalsVotedOn > 0); + } + + require( + uint256(standardHandler_.sumFundingVotes(fundingVoteParams)) <= _ajna.totalSupply(), + "invariant FS8: a voter should never be able to cast more votes than the Ajna token supply" + ); + + // check that there weren't any duplicate proposal entries, as votes for same proposal should be combined + uint256[] memory proposalIdsVotedOn = new uint256[](fundingVotesCast.length); + for (uint j = 0; j < fundingVotesCast.length; ) { + proposalIdsVotedOn[j] = fundingVotesCast[j].proposalId; + ++j; + } + require( + standardHandler_.hasDuplicates(proposalIdsVotedOn) == false, + "invariant FS6: All voter funding votes on a proposal should be cast in the same direction. Multiple votes on the same proposal should see the voting power increase according to the combined cost of votes." + ); + } + + require( + keccak256(abi.encode(grantFund_.getTopTenProposals(distributionId))) == initialTopTenHash, + "invariant FS7: List of top ten proposals should never change once the funding stage has started" + ); + } + +} + diff --git a/test/invariants/StandardScreeningInvariant.t.sol b/test/invariants/base/ScreeningInvariants.sol similarity index 61% rename from test/invariants/StandardScreeningInvariant.t.sol rename to test/invariants/base/ScreeningInvariants.sol index 729374ff..e5281619 100644 --- a/test/invariants/StandardScreeningInvariant.t.sol +++ b/test/invariants/base/ScreeningInvariants.sol @@ -4,37 +4,24 @@ pragma solidity 0.8.18; import { console } from "@std/console.sol"; -import { IGrantFund } from "../../src/grants/interfaces/IGrantFund.sol"; +import { GrantFund } from "../../../src/grants/GrantFund.sol"; +import { IGrantFund } from "../../../src/grants/interfaces/IGrantFund.sol"; -import { StandardTestBase } from "./base/StandardTestBase.sol"; -import { StandardHandler } from "./handlers/StandardHandler.sol"; +import { TestBase } from "./TestBase.sol"; +import { StandardHandler } from "../handlers/StandardHandler.sol"; -contract StandardScreeningInvariant is StandardTestBase { +abstract contract ScreeningInvariants is TestBase { - function setUp() public override { - super.setUp(); + /********************/ + /**** Invariants ****/ + /********************/ - startDistributionPeriod(); - - // set the list of function selectors to run - bytes4[] memory selectors = new bytes4[](3); - selectors[0] = _standardHandler.startNewDistributionPeriod.selector; - selectors[1] = _standardHandler.propose.selector; - selectors[2] = _standardHandler.screeningVote.selector; - - // ensure utility functions are excluded from the invariant runs - targetSelector(FuzzSelector({ - addr: address(_standardHandler), - selectors: selectors - })); - - } - - function invariant_SS1_SS3_SS4_SS5_SS6_SS7_SS9_SS10_SS11() external { - uint24 distributionId = _grantFund.getDistributionId(); - uint256 standardFundingProposalsSubmitted = _standardHandler.getStandardFundingProposals(distributionId).length; - uint256[] memory topTenProposals = _grantFund.getTopTenProposals(distributionId); - (, uint256 startBlock, , uint256 gbc, , ) = _grantFund.getDistributionPeriodInfo(distributionId); + function _invariant_SS1_SS3_SS4_SS5_SS6_SS7_SS9_SS10_SS11(GrantFund grantFund_, StandardHandler standardHandler_) internal { + uint24 distributionId = grantFund_.getDistributionId(); + uint256[] memory allProposals = standardHandler_.getStandardFundingProposals(distributionId); + uint256 standardFundingProposalsSubmitted = allProposals.length; + uint256[] memory topTenProposals = grantFund_.getTopTenProposals(distributionId); + (, uint256 startBlock, , uint256 gbc, , ) = grantFund_.getDistributionPeriodInfo(distributionId); require( topTenProposals.length <= 10 && standardFundingProposalsSubmitted >= topTenProposals.length, @@ -45,8 +32,8 @@ contract StandardScreeningInvariant is StandardTestBase { if (topTenProposals.length > 1) { for (uint256 i = 0; i < topTenProposals.length - 1; ++i) { // check the current proposals votes received against the next proposal in the top ten list - (, uint24 distributionIdCurr, uint256 votesReceivedCurr, , , ) = _grantFund.getProposalInfo(topTenProposals[i]); - (, uint24 distributionIdNext, uint256 votesReceivedNext, , , ) = _grantFund.getProposalInfo(topTenProposals[i + 1]); + (, uint24 distributionIdCurr, uint256 votesReceivedCurr, , , ) = grantFund_.getProposalInfo(topTenProposals[i]); + (, uint24 distributionIdNext, uint256 votesReceivedNext, , , ) = grantFund_.getProposalInfo(topTenProposals[i + 1]); require( votesReceivedCurr >= votesReceivedNext, "invariant SS3: proposals should be sorted in descending order" @@ -68,15 +55,13 @@ contract StandardScreeningInvariant is StandardTestBase { // find the number of screening votes received by the last proposal in the top ten list uint256 votesReceivedLast; if (topTenProposals.length != 0) { - (, , votesReceivedLast, , , ) = _grantFund.getProposalInfo(topTenProposals[topTenProposals.length - 1]); + (, , votesReceivedLast, , , ) = grantFund_.getProposalInfo(topTenProposals[topTenProposals.length - 1]); assertGe(votesReceivedLast, 0); } - uint256[] memory proposalIds = new uint256[](standardFundingProposalsSubmitted); - // check invariants against all submitted proposals for (uint256 j = 0; j < standardFundingProposalsSubmitted; ++j) { - (uint256 proposalId, , uint256 votesReceived, uint256 tokensRequested, , ) = _grantFund.getProposalInfo(_standardHandler.standardFundingProposals(distributionId, j)); + (uint256 proposalId, , uint256 votesReceived, uint256 tokensRequested, , ) = grantFund_.getProposalInfo(standardHandler_.standardFundingProposals(distributionId, j)); require( votesReceived >= 0, "invariant SS4: Screening votes recieved for a proposal can only be positive" @@ -98,47 +83,45 @@ contract StandardScreeningInvariant is StandardTestBase { } // TODO: account for multiple distribution periods? - TestProposal memory testProposal = _standardHandler.getTestProposal(proposalId); + TestProposal memory testProposal = standardHandler_.getTestProposal(proposalId); require( - testProposal.blockAtCreation <= _grantFund.getScreeningStageEndBlock(startBlock), + testProposal.blockAtCreation <= grantFund_.getScreeningStageEndBlock(startBlock), "invariant SS9: A proposal can only be created during a distribution period's screening stage" ); - // check proposalId is unique - require( - !hasDuplicates(proposalIds), "invariant SS10: A proposal's proposalId must be unique" - ); - - // Add current proposal Id to proposalIds set - proposalIds[j] = proposalId; - require( tokensRequested <= gbc * 9 / 10, "invariant SS11: A proposal's tokens requested must be <= 90% of GBC" ); } + // check proposalIds for duplicates + require( + !hasDuplicates(allProposals), "invariant SS10: A proposal's proposalId must be unique" + ); + + // TODO: expand this assertion // invariant SS6: proposals should be incorporated into the top ten list if, and only if, they have as many or more votes as the last member of the top ten list. - if (_standardHandler.screeningVotesCast() > 0) { + if (standardHandler_.screeningVotesCast() > 0) { assertTrue(topTenProposals.length > 0); } } - function invariant_SS2_SS4_SS8() external view { - uint256 actorCount = _standardHandler.getActorsCount(); - uint24 distributionId = _grantFund.getDistributionId(); + function _invariant_SS2_SS4_SS8(GrantFund grantFund_, StandardHandler standardHandler_) internal view { + uint256 actorCount = standardHandler_.getActorsCount(); + uint24 distributionId = grantFund_.getDistributionId(); // check invariants for all actors for (uint256 i = 0; i < actorCount; ++i) { - address actor = _standardHandler.actors(i); - uint256 votingPower = _grantFund.getVotesScreening(distributionId, actor); + address actor = standardHandler_.actors(i); + uint256 votingPower = grantFund_.getVotesScreening(distributionId, actor); require( - _standardHandler.sumVoterScreeningVotes(actor, distributionId) <= votingPower, + standardHandler_.sumVoterScreeningVotes(actor, distributionId) <= votingPower, "invariant SS2: can only vote up to the amount of voting power at the snapshot blocks" ); // check the screening votes cast by the actor - ( , IGrantFund.ScreeningVoteParams[] memory screeningVoteParams, ) = _standardHandler.getVotingActorsInfo(actor, distributionId); + ( , IGrantFund.ScreeningVoteParams[] memory screeningVoteParams, ) = standardHandler_.getVotingActorsInfo(actor, distributionId); for (uint256 j = 0; j < screeningVoteParams.length; ++j) { require( screeningVoteParams[j].votes >= 0, @@ -146,19 +129,11 @@ contract StandardScreeningInvariant is StandardTestBase { ); require( - _findProposalIndex(screeningVoteParams[j].proposalId, _standardHandler.getStandardFundingProposals(distributionId)) != -1, + _findProposalIndex(screeningVoteParams[j].proposalId, standardHandler_.getStandardFundingProposals(distributionId)) != -1, "invariant SS8: a proposal can only receive screening votes if it was created via propose()" ); } } } - function invariant_call_summary() external view { - uint24 distributionId = _grantFund.getDistributionId(); - - _standardHandler.logCallSummary(); - // _standardHandler.logProposalSummary(); - _standardHandler.logActorSummary(distributionId, false, true); - } - } diff --git a/test/invariants/base/StandardTestBase.sol b/test/invariants/base/StandardTestBase.sol index 76f7fd13..b82d6604 100644 --- a/test/invariants/base/StandardTestBase.sol +++ b/test/invariants/base/StandardTestBase.sol @@ -7,7 +7,12 @@ import { console } from "@std/console.sol"; import { TestBase } from "./TestBase.sol"; import { StandardHandler } from "../handlers/StandardHandler.sol"; -contract StandardTestBase is TestBase { +import { DistributionPeriodInvariants } from "./DistributionPeriodInvariants.sol"; +import { FinalizeInvariants } from "./FinalizeInvariants.sol"; +import { FundingInvariants } from "./FundingInvariants.sol"; +import { ScreeningInvariants } from "./ScreeningInvariants.sol"; + +contract StandardTestBase is DistributionPeriodInvariants, FinalizeInvariants, FundingInvariants, ScreeningInvariants { uint256 internal constant NUM_ACTORS = 20; uint256 public constant TOKENS_TO_DISTRIBUTE = 500_000_000 * 1e18; @@ -30,10 +35,15 @@ contract StandardTestBase is TestBase { targetContract(address(_standardHandler)); } + /***************************/ + /**** Utility Functions ****/ + /***************************/ + function startDistributionPeriod() internal { // skip time for snapshots and start distribution period vm.roll(currentBlock + 100); currentBlock = block.number; _grantFund.startNewDistributionPeriod(); } + } diff --git a/test/invariants/scenarios/FinalizeInvariant.t.sol b/test/invariants/scenarios/FinalizeInvariant.t.sol new file mode 100644 index 00000000..2f576de3 --- /dev/null +++ b/test/invariants/scenarios/FinalizeInvariant.t.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.18; + +import { console } from "@std/console.sol"; + +import { StandardTestBase } from "../base/StandardTestBase.sol"; +import { StandardHandler } from "../handlers/StandardHandler.sol"; +import { Handler } from "../handlers/Handler.sol"; + +contract FinalizeInvariant is StandardTestBase { + + // override setup to start tests in the challenge stage with proposals that have already been screened and funded + function setUp() public override { + super.setUp(); + + startDistributionPeriod(); + + // create 15 proposals + _standardHandler.createProposals(15); + + // cast screening votes on proposals + _standardHandler.screeningVoteProposals(); + + // skip time into the funding stage + uint24 distributionId = _grantFund.getDistributionId(); + (, uint256 startBlock, uint256 endBlock, , , ) = _grantFund.getDistributionPeriodInfo(distributionId); + uint256 fundingStageStartBlock = _grantFund.getScreeningStageEndBlock(startBlock) + 1; + vm.roll(fundingStageStartBlock + 100); + currentBlock = fundingStageStartBlock + 100; + + // cast funding votes on proposals + _standardHandler.fundingVoteProposals(); + + _standardHandler.setCurrentScenarioType(Handler.ScenarioType.Medium); + + // skip time into the challenge stage + uint256 challengeStageStartBlock = _grantFund.getChallengeStageStartBlock(endBlock); + vm.roll(challengeStageStartBlock + 100); + currentBlock = challengeStageStartBlock + 100; + + // set the list of function selectors to run + bytes4[] memory selectors = new bytes4[](5); + selectors[0] = _standardHandler.fundingVote.selector; + selectors[1] = _standardHandler.updateSlate.selector; + selectors[2] = _standardHandler.execute.selector; + selectors[3] = _standardHandler.claimDelegateReward.selector; + selectors[4] = _standardHandler.roll.selector; + + // ensure utility functions are excluded from the invariant runs + targetSelector(FuzzSelector({ + addr: address(_standardHandler), + selectors: selectors + })); + } + + function invariant_finalize() external { + _invariant_CS1_CS2_CS3_CS4_CS5_CS6(_grantFund, _standardHandler); + _invariant_ES1_ES2_ES3_ES4_ES5(_grantFund, _standardHandler); + _invariant_DR1_DR2_DR3_DR4_DR5(_grantFund, _standardHandler); + } + + function invariant_call_summary() external view { + uint24 distributionId = _grantFund.getDistributionId(); + + _standardHandler.logCallSummary(); + _standardHandler.logActorSummary(distributionId, false, false); + _standardHandler.logProposalSummary(); + _standardHandler.logTimeSummary(); + _logFinalizeSummary(distributionId); + } + + function _logFinalizeSummary(uint24 distributionId_) internal view { + (, , , uint128 fundsAvailable, , bytes32 topSlateHash) = _grantFund.getDistributionPeriodInfo(distributionId_); + uint256[] memory topSlateProposalIds = _grantFund.getFundedProposalSlate(topSlateHash); + + uint256[] memory topTenScreenedProposalIds = _grantFund.getTopTenProposals(distributionId_); + + console.log("\nFinalize Summary\n"); + console.log("------------------"); + console.log("Distribution Id: ", distributionId_); + console.log("Delegation Rewards Claimed: ", _standardHandler.numberOfCalls('SFH.claimDelegateReward.success')); + console.log("Proposal Execute attempt: ", _standardHandler.numberOfCalls('SFH.execute.attempt')); + console.log("Proposal Execute Count: ", _standardHandler.numberOfCalls('SFH.execute.success')); + console.log("Slate Created: ", _standardHandler.numberOfCalls('SFH.updateSlate.prep')); + console.log("Slate Update Called: ", _standardHandler.numberOfCalls('SFH.updateSlate.called')); + console.log("Slate Update Count: ", _standardHandler.numberOfCalls('SFH.updateSlate.success')); + console.log("Next Slate length: ", _standardHandler.numberOfCalls('updateSlate.length')); + console.log("Top Slate Proposal Count: ", topSlateProposalIds.length); + console.log("Top Ten Proposal Count: ", topTenScreenedProposalIds.length); + console.log("Funds Available: ", fundsAvailable); + console.log("Top slate funds requested: ", _standardHandler.getTokensRequestedInFundedSlateInvariant(topSlateHash)); + (, , , , uint256 fundingPowerCast, ) = _grantFund.getDistributionPeriodInfo(distributionId_); + console.log("Total Funding Power Cast ", fundingPowerCast); + console.log("------------------"); + } + +} diff --git a/test/invariants/scenarios/FundingInvariant.t.sol b/test/invariants/scenarios/FundingInvariant.t.sol new file mode 100644 index 00000000..030e3ea5 --- /dev/null +++ b/test/invariants/scenarios/FundingInvariant.t.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.18; + +import { console } from "@std/console.sol"; +import { SafeCast } from "@oz/utils/math/SafeCast.sol"; + +import { IGrantFund } from "../../../src/grants/interfaces/IGrantFund.sol"; + +import { StandardTestBase } from "../base/StandardTestBase.sol"; +import { StandardHandler } from "../handlers/StandardHandler.sol"; + +contract FundingInvariant is StandardTestBase { + + // override setup to start tests in the funding stage with already screened proposals + function setUp() public override { + super.setUp(); + + startDistributionPeriod(); + + // create 15 proposals + _standardHandler.createProposals(15); + + // cast screening votes on proposals + _standardHandler.screeningVoteProposals(); + + // skip time into the funding stage + uint24 distributionId = _grantFund.getDistributionId(); + (, uint256 startBlock, , , , ) = _grantFund.getDistributionPeriodInfo(distributionId); + uint256 fundingStageStartBlock = _grantFund.getScreeningStageEndBlock(startBlock) + 1; + vm.roll(fundingStageStartBlock + 100); + currentBlock = fundingStageStartBlock + 100; + + // set the list of function selectors to run + bytes4[] memory selectors = new bytes4[](2); + selectors[0] = _standardHandler.fundingVote.selector; + selectors[1] = _standardHandler.updateSlate.selector; + + // ensure utility functions are excluded from the invariant runs + targetSelector(FuzzSelector({ + addr: address(_standardHandler), + selectors: selectors + })); + + uint256[] memory initialTopTenProposals = _grantFund.getTopTenProposals(_grantFund.getDistributionId()); + initialTopTenHash = keccak256(abi.encode(initialTopTenProposals)); + } + + function invariant_funding_stage() external { + _invariant_FS1_FS2_FS3(_grantFund, _standardHandler); + _invariant_FS4_FS5_FS6_FS7_FS8(_grantFund, _standardHandler); + } + + function invariant_call_summary() external view { + uint24 distributionId = _grantFund.getDistributionId(); + + _standardHandler.logCallSummary(); + // _standardHandler.logProposalSummary(); + // _standardHandler.logActorSummary(distributionId, true, false); + _logFundingSummary(distributionId); + } + + function _logFundingSummary(uint24 distributionId_) internal view { + console.log("\nFunding Summary\n"); + console.log("------------------"); + console.log("number of funding stage starts: ", _standardHandler.numberOfCalls("SFH.FundingStage")); + console.log("number of funding stage success votes: ", _standardHandler.numberOfCalls("SFH.fundingVote.success")); + console.log("distributionId: ", distributionId_); + console.log("SFH.updateSlate.success: ", _standardHandler.numberOfCalls("SFH.updateSlate.success")); + console.log("------------------"); + } + +} diff --git a/test/invariants/scenarios/MultipleDistributionInvariant.t.sol b/test/invariants/scenarios/MultipleDistributionInvariant.t.sol new file mode 100644 index 00000000..af7006fc --- /dev/null +++ b/test/invariants/scenarios/MultipleDistributionInvariant.t.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.18; + +import { console } from "@std/console.sol"; +import { SafeCast } from "@oz/utils/math/SafeCast.sol"; + +import { Maths } from "../../../src/grants/libraries/Maths.sol"; + +import { StandardTestBase } from "../base/StandardTestBase.sol"; +import { StandardHandler } from "../handlers/StandardHandler.sol"; +import { Handler } from "../handlers/Handler.sol"; + +contract MultipleDistributionInvariant is StandardTestBase { + + // run tests against all functions, having just started a distribution period + function setUp() public override { + super.setUp(); + + // set the list of function selectors to run + bytes4[] memory selectors = new bytes4[](8); + selectors[0] = _standardHandler.startNewDistributionPeriod.selector; + selectors[1] = _standardHandler.propose.selector; + selectors[2] = _standardHandler.screeningVote.selector; + selectors[3] = _standardHandler.fundingVote.selector; + selectors[4] = _standardHandler.updateSlate.selector; + selectors[5] = _standardHandler.execute.selector; + selectors[6] = _standardHandler.claimDelegateReward.selector; + selectors[7] = _standardHandler.roll.selector; + + // ensure utility functions are excluded from the invariant runs + targetSelector(FuzzSelector({ + addr: address(_standardHandler), + selectors: selectors + })); + + // update scenarioType to fast to have larger rolls + _standardHandler.setCurrentScenarioType(Handler.ScenarioType.Fast); + + vm.roll(block.number + 100); + currentBlock = block.number; + } + + function invariant_distribution_period() external { + _invariant_DP1_DP2_DP3_DP4_DP5(_grantFund, _standardHandler); + _invariant_DP6(_grantFund, _standardHandler); + _invariant_T1_T2(_grantFund, _standardHandler); + } + + function invariant_all() external { + // // screening invariants + // _invariant_SS1_SS3_SS4_SS5_SS6_SS7_SS9_SS10_SS11(_grantFund, _standardHandler); + // _invariant_SS2_SS4_SS8(_grantFund, _standardHandler); + + // // funding invariants + // _invariant_FS1_FS2_FS3(_grantFund, _standardHandler); + // _invariant_FS4_FS5_FS6_FS7_FS8(_grantFund, _standardHandler); + + // finalize invariants + _invariant_CS1_CS2_CS3_CS4_CS5_CS6(_grantFund, _standardHandler); + _invariant_ES1_ES2_ES3_ES4_ES5(_grantFund, _standardHandler); + _invariant_DR1_DR2_DR3_DR4_DR5(_grantFund, _standardHandler); + } + + function invariant_call_summary() external view { + // uint24 distributionId = _grantFund.getDistributionId(); + + _standardHandler.logCallSummary(); + _standardHandler.logTimeSummary(); + + console.log("scenario type", uint8(_standardHandler.getCurrentScenarioType())); + + console.log("Delegation Rewards: ", _standardHandler.numberOfCalls('delegationRewardSet')); + console.log("Delegation Rewards Claimed: ", _standardHandler.numberOfCalls('SFH.claimDelegateReward.success')); + console.log("Proposal Execute attempt: ", _standardHandler.numberOfCalls('SFH.execute.attempt')); + console.log("Proposal Execute Count: ", _standardHandler.numberOfCalls('SFH.execute.success')); + console.log("Slate Update Prep: ", _standardHandler.numberOfCalls('SFH.updateSlate.prep')); + console.log("Slate Update length: ", _standardHandler.numberOfCalls('updateSlate.length')); + console.log("Slate Update Called: ", _standardHandler.numberOfCalls('SFH.updateSlate.called')); + console.log("Slate Update Success: ", _standardHandler.numberOfCalls('SFH.updateSlate.success')); + console.log("Slate Proposals: ", _standardHandler.numberOfCalls('proposalsInSlates')); + console.log("unused proposal: ", _standardHandler.numberOfCalls('unused.proposal')); + console.log("unexecuted proposal: ", _standardHandler.numberOfCalls('unexecuted.proposal')); + console.log("funding stage starts: ", _standardHandler.numberOfCalls("SFH.FundingStage")); + console.log("funding stage success votes ", _standardHandler.numberOfCalls("SFH.fundingVote.success")); + + + (, , , , uint256 fundingPowerCast, ) = _grantFund.getDistributionPeriodInfo(2); + console.log("Total Funding Power Cast ", fundingPowerCast); + + + if (_standardHandler.numberOfCalls('unexecuted.proposal') != 0) { + console.log("state of unexecuted: ", uint8(_grantFund.state(_standardHandler.numberOfCalls('unexecuted.proposal')))); + } + // _standardHandler.logProposalSummary(); + // _standardHandler.logActorSummary(distributionId, true, true); + } +} diff --git a/test/invariants/scenarios/ScreeningInvariant.t.sol b/test/invariants/scenarios/ScreeningInvariant.t.sol new file mode 100644 index 00000000..6171a17b --- /dev/null +++ b/test/invariants/scenarios/ScreeningInvariant.t.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.18; + +import { console } from "@std/console.sol"; + +import { IGrantFund } from "../../../src/grants/interfaces/IGrantFund.sol"; + +import { StandardTestBase } from "../base/StandardTestBase.sol"; +import { StandardHandler } from "../handlers/StandardHandler.sol"; + +contract ScreeningInvariant is StandardTestBase { + + function setUp() public override { + super.setUp(); + + startDistributionPeriod(); + + // set the list of function selectors to run + bytes4[] memory selectors = new bytes4[](3); + selectors[0] = _standardHandler.startNewDistributionPeriod.selector; + selectors[1] = _standardHandler.propose.selector; + selectors[2] = _standardHandler.screeningVote.selector; + + // ensure utility functions are excluded from the invariant runs + targetSelector(FuzzSelector({ + addr: address(_standardHandler), + selectors: selectors + })); + + } + + function invariant_screening_stage() external { + _invariant_SS1_SS3_SS4_SS5_SS6_SS7_SS9_SS10_SS11(_grantFund, _standardHandler); + _invariant_SS2_SS4_SS8(_grantFund, _standardHandler); + } + + function invariant_call_summary() external view { + uint24 distributionId = _grantFund.getDistributionId(); + + _standardHandler.logCallSummary(); + // _standardHandler.logProposalSummary(); + _standardHandler.logActorSummary(distributionId, false, true); + } + +} From 48f027fd79ff4a08dbbc3282e4125bfecdb3d16f Mon Sep 17 00:00:00 2001 From: Mike Hathaway Date: Fri, 16 Jun 2023 16:34:03 -0400 Subject: [PATCH 04/20] Check all invariants across multiple distribution periods (#107) * add support for multiple distribution periods to funding invariants * fix FS7 for multiple dist * begin updating screening invariants * remaining fixes to funding stage invariants * get screening stage invariants working in multiple distribution scenario * pr feedback --------- Co-authored-by: Mike --- test/invariants/INVARIANTS.md | 13 +- test/invariants/base/FundingInvariants.sol | 139 +++++++------ test/invariants/base/ScreeningInvariants.sol | 194 ++++++++++-------- test/invariants/handlers/StandardHandler.sol | 12 +- .../scenarios/FinalizeInvariant.t.sol | 4 + .../scenarios/FundingInvariant.t.sol | 1 + .../MultipleDistributionInvariant.t.sol | 12 +- .../scenarios/ScreeningInvariant.t.sol | 4 +- 8 files changed, 211 insertions(+), 168 deletions(-) diff --git a/test/invariants/INVARIANTS.md b/test/invariants/INVARIANTS.md index 5129204f..ed2b4cf9 100644 --- a/test/invariants/INVARIANTS.md +++ b/test/invariants/INVARIANTS.md @@ -22,12 +22,13 @@ - **SS4**: Screening vote's cast can only be positive. - **SS5**: Screening votes can only be cast on a proposal in it's distribution period's screening stage. - **SS6**: For every proposal, it is included in the top 10 list if, and only if, it has as many or more votes as the last member of the top ten list (typically the 10th of course, but it may be shorter than ten proposals). + - **SS7**: Screening vote's on a proposal should cause addition to the topTenProposals if no proposal has been added yet. - - **SS7**: A proposal should never receive more vote than the Ajna token supply. - - **SS8**: A proposal can only receive screening votes if it was created via `propose()`. - - **SS9**: A proposal can only be created during a distribution period's screening stage. - - **SS10**: A proposal's proposalId must be unique. - - **SS11**: A proposal's tokens requested must be <= 90% of GBC. + - **SS8**: A proposal should never receive more vote than the Ajna token supply. + - **SS9**: A proposal can only receive screening votes if it was created via `propose()`. + - **SS10**: A proposal can only be created during a distribution period's screening stage. + - **SS11**: A proposal's proposalId must be unique. + - **SS12**: A proposal's tokens requested must be <= 90% of GBC. - #### Funding Stage: - **FS1**: Only 10 proposals can be voted on in the funding stage @@ -36,7 +37,7 @@ - **FS4**: Sum of square of votes cast by a given actor are less than or equal to the actor's Ajna delegated balance, squared. - **FS5**: Sum of voter's votesCast should be equal to the square root of the voting power expended (FS4 restated, but added to test intermediate state as well as final). - **FS6**: All voter funding votes on a proposal should be cast in the same direction. Multiple votes on the same proposal should see the voting power increase according to the combined cost of votes. - - **FS7** List of top ten proposals should never change once the funding stage has started. + - **FS7**: List of top ten proposals should never change once the funding stage has started. - **FS8**: a voter should never be able to cast more votes than the Ajna token supply. - #### Challenge Stage: diff --git a/test/invariants/base/FundingInvariants.sol b/test/invariants/base/FundingInvariants.sol index d371f9bb..76972863 100644 --- a/test/invariants/base/FundingInvariants.sol +++ b/test/invariants/base/FundingInvariants.sol @@ -16,93 +16,108 @@ abstract contract FundingInvariants is TestBase { // hash the top ten proposals at the start of the funding stage to check composition bytes32 initialTopTenHash; - function _invariant_FS1_FS2_FS3(GrantFund grantFund_, StandardHandler standardHandler_) internal { - uint256[] memory topTenProposals = grantFund_.getTopTenProposals(grantFund_.getDistributionId()); + function _invariant_FS1_FS2_FS3(GrantFund grantFund_, StandardHandler standardHandler_) internal view { + uint24 distributionId = grantFund_.getDistributionId(); + while (distributionId > 0) { - // check if something went wrong in test setup - assertTrue(topTenProposals.length > 0); + uint256[] memory topTenProposals = grantFund_.getTopTenProposals(distributionId); - require( - topTenProposals.length <= 10, - "invariant FS1: 10 or less proposals should make it through the screening stage" - ); + require( + topTenProposals.length <= 10, + "invariant FS1: 10 or less proposals should make it through the screening stage" + ); - uint24 distributionId = grantFund_.getDistributionId(); - uint256[] memory standardFundingProposals = standardHandler_.getStandardFundingProposals(distributionId); + uint256[] memory standardFundingProposals = standardHandler_.getStandardFundingProposals(distributionId); - // check invariants against every proposal - for (uint256 j = 0; j < standardFundingProposals.length; ++j) { - uint256 proposalId = standardHandler_.standardFundingProposals(distributionId, j); - (, uint24 proposalDistributionId, , , int128 fundingVotesReceived, ) = grantFund_.getProposalInfo(proposalId); + // check invariants against every proposal + for (uint256 j = 0; j < standardFundingProposals.length; ++j) { + uint256 proposalId = standardHandler_.standardFundingProposals(distributionId, j); + (, uint24 proposalDistributionId, , , int128 fundingVotesReceived, ) = grantFund_.getProposalInfo(proposalId); + + if (_findProposalIndex(proposalId, topTenProposals) == -1) { + require( + fundingVotesReceived == 0, + "invariant FS2: proposals not in the top ten should not be able to recieve funding votes" + ); + } - if (_findProposalIndex(proposalId, topTenProposals) == -1) { require( - fundingVotesReceived == 0, - "invariant FS2: proposals not in the top ten should not be able to recieve funding votes" + distributionId == proposalDistributionId, + "invariant FS3: distribution id for a proposal should be the same as the current distribution id" ); } - - require( - distributionId == proposalDistributionId, - "invariant FS3: distribution id for a proposal should be the same as the current distribution id" - ); + --distributionId; } } function _invariant_FS4_FS5_FS6_FS7_FS8(GrantFund grantFund_, StandardHandler standardHandler_) internal { uint24 distributionId = grantFund_.getDistributionId(); + while (distributionId > 0) { - // check invariants against every actor - for (uint256 i = 0; i < standardHandler_.getActorsCount(); ++i) { - address actor = standardHandler_.actors(i); + // check invariants against every actor + for (uint256 i = 0; i < standardHandler_.getActorsCount(); ++i) { + address actor = standardHandler_.actors(i); - // get the initial funding stage voting power of the actor - (uint128 votingPower, uint128 remainingVotingPower, uint256 numberOfProposalsVotedOn) = grantFund_.getVoterInfo(distributionId, actor); + // get the initial funding stage voting power of the actor + (uint128 votingPower, uint128 remainingVotingPower, uint256 numberOfProposalsVotedOn) = grantFund_.getVoterInfo(distributionId, actor); - // get the voting info of the actor - (IGrantFund.FundingVoteParams[] memory fundingVoteParams, , ) = standardHandler_.getVotingActorsInfo(actor, distributionId); + // get the voting info of the actor + (IGrantFund.FundingVoteParams[] memory fundingVoteParams, , ) = standardHandler_.getVotingActorsInfo(actor, distributionId); - uint128 sumOfSquares = SafeCast.toUint128(standardHandler_.sumSquareOfVotesCast(fundingVoteParams)); + uint128 sumOfSquares = SafeCast.toUint128(standardHandler_.sumSquareOfVotesCast(fundingVoteParams)); - // check voter votes cast are less than or equal to the sqrt of the voting power of the actor - IGrantFund.FundingVoteParams[] memory fundingVotesCast = grantFund_.getFundingVotesCast(distributionId, actor); + // check voter votes cast are less than or equal to the sqrt of the voting power of the actor + IGrantFund.FundingVoteParams[] memory fundingVotesCast = grantFund_.getFundingVotesCast(distributionId, actor); - require( - sumOfSquares <= votingPower, - "invariant FS4: sum of square of votes cast <= voting power of actor" - ); - require( - sumOfSquares == votingPower - remainingVotingPower, - "invariant FS5: Sum of voter's votesCast should be equal to the square root of the voting power expended (FS4 restated, but added to test intermediate state as well as final)." - ); + require( + sumOfSquares <= votingPower, + "invariant FS4: sum of square of votes cast <= voting power of actor" + ); + require( + sumOfSquares == votingPower - remainingVotingPower, + "invariant FS5: Sum of voter's votesCast should be equal to the square root of the voting power expended (FS4 restated, but added to test intermediate state as well as final)." + ); + + // check that the test functioned as expected + if (votingPower != 0 && remainingVotingPower == 0) { + assertTrue(numberOfProposalsVotedOn == fundingVotesCast.length); + assertTrue(numberOfProposalsVotedOn > 0); + } + + require( + uint256(standardHandler_.sumFundingVotes(fundingVoteParams)) <= _ajna.totalSupply(), + "invariant FS8: a voter should never be able to cast more votes than the Ajna token supply" + ); - // check that the test functioned as expected - if (votingPower != 0 && remainingVotingPower == 0) { - assertTrue(numberOfProposalsVotedOn == fundingVotesCast.length); - assertTrue(numberOfProposalsVotedOn > 0); + // check that there weren't any duplicate proposal entries, as votes for same proposal should be combined + uint256[] memory proposalIdsVotedOn = new uint256[](fundingVotesCast.length); + for (uint j = 0; j < fundingVotesCast.length; ) { + proposalIdsVotedOn[j] = fundingVotesCast[j].proposalId; + ++j; + } + require( + standardHandler_.hasDuplicates(proposalIdsVotedOn) == false, + "invariant FS6: All voter funding votes on a proposal should be cast in the same direction. Multiple votes on the same proposal should see the voting power increase according to the combined cost of votes." + ); } - require( - uint256(standardHandler_.sumFundingVotes(fundingVoteParams)) <= _ajna.totalSupply(), - "invariant FS8: a voter should never be able to cast more votes than the Ajna token supply" - ); + (, uint256 startBlock, , , , ) = grantFund_.getDistributionPeriodInfo(distributionId); + StandardHandler.DistributionState memory state = standardHandler_.getDistributionState(distributionId); + // hash the top ten proposals at the start of the funding stage to check composition in FS7 + // if calling from the funding scenario, this is prepoulated and not accessed. Can't rely on hashing empty array as it will be non-zero + initialTopTenHash = state.topTenHashAtLastScreeningVote != 0 ? state.topTenHashAtLastScreeningVote : keccak256(abi.encode(grantFund_.getTopTenProposals(distributionId))); - // check that there weren't any duplicate proposal entries, as votes for same proposal should be combined - uint256[] memory proposalIdsVotedOn = new uint256[](fundingVotesCast.length); - for (uint j = 0; j < fundingVotesCast.length; ) { - proposalIdsVotedOn[j] = fundingVotesCast[j].proposalId; - ++j; + // check global invariants + if (currentBlock > grantFund_.getScreeningStageEndBlock(startBlock)) { + require( + keccak256(abi.encode(grantFund_.getTopTenProposals(distributionId))) == initialTopTenHash, + "invariant FS7: List of top ten proposals should never change once the funding stage has started" + ); } - require( - standardHandler_.hasDuplicates(proposalIdsVotedOn) == false, - "invariant FS6: All voter funding votes on a proposal should be cast in the same direction. Multiple votes on the same proposal should see the voting power increase according to the combined cost of votes." - ); - } - require( - keccak256(abi.encode(grantFund_.getTopTenProposals(distributionId))) == initialTopTenHash, - "invariant FS7: List of top ten proposals should never change once the funding stage has started" - ); + // check the previous distribution period if available + --distributionId; + } } } diff --git a/test/invariants/base/ScreeningInvariants.sol b/test/invariants/base/ScreeningInvariants.sol index e5281619..cc2c6b99 100644 --- a/test/invariants/base/ScreeningInvariants.sol +++ b/test/invariants/base/ScreeningInvariants.sol @@ -16,123 +16,139 @@ abstract contract ScreeningInvariants is TestBase { /**** Invariants ****/ /********************/ - function _invariant_SS1_SS3_SS4_SS5_SS6_SS7_SS9_SS10_SS11(GrantFund grantFund_, StandardHandler standardHandler_) internal { - uint24 distributionId = grantFund_.getDistributionId(); - uint256[] memory allProposals = standardHandler_.getStandardFundingProposals(distributionId); - uint256 standardFundingProposalsSubmitted = allProposals.length; - uint256[] memory topTenProposals = grantFund_.getTopTenProposals(distributionId); - (, uint256 startBlock, , uint256 gbc, , ) = grantFund_.getDistributionPeriodInfo(distributionId); - - require( - topTenProposals.length <= 10 && standardFundingProposalsSubmitted >= topTenProposals.length, - "invariant SS1: 10 or less proposals should make it through the screening stage" - ); - - // check the state of the top ten proposals - if (topTenProposals.length > 1) { - for (uint256 i = 0; i < topTenProposals.length - 1; ++i) { - // check the current proposals votes received against the next proposal in the top ten list - (, uint24 distributionIdCurr, uint256 votesReceivedCurr, , , ) = grantFund_.getProposalInfo(topTenProposals[i]); - (, uint24 distributionIdNext, uint256 votesReceivedNext, , , ) = grantFund_.getProposalInfo(topTenProposals[i + 1]); - require( - votesReceivedCurr >= votesReceivedNext, - "invariant SS3: proposals should be sorted in descending order" - ); - - require( - votesReceivedCurr >= 0 && votesReceivedNext >= 0, - "invariant SS4: Screening votes recieved for a proposal can only be positive" - ); + function _invariant_SS1_SS3_SS4_SS5_SS6_SS7_SS8_SS10_SS11_SS12(GrantFund grantFund_, StandardHandler standardHandler_) internal { + // set block number to current block + // TODO: find more elegant solution to block.number not being updated in time for the snapshot -> probably a modifier + vm.roll(currentBlock); - // TODO: improve this check - require( - distributionIdCurr == distributionIdNext && distributionIdCurr == distributionId, - "invariant SS5: distribution id for a proposal should be the same as the current distribution id" - ); - } - } + uint24 distributionId = grantFund_.getDistributionId(); + while (distributionId > 0) { - // find the number of screening votes received by the last proposal in the top ten list - uint256 votesReceivedLast; - if (topTenProposals.length != 0) { - (, , votesReceivedLast, , , ) = grantFund_.getProposalInfo(topTenProposals[topTenProposals.length - 1]); - assertGe(votesReceivedLast, 0); - } + uint256[] memory allProposals = standardHandler_.getStandardFundingProposals(distributionId); + uint256 standardFundingProposalsSubmitted = allProposals.length; + uint256[] memory topTenProposals = grantFund_.getTopTenProposals(distributionId); + (, uint256 startBlock, , uint256 gbc, , ) = grantFund_.getDistributionPeriodInfo(distributionId); - // check invariants against all submitted proposals - for (uint256 j = 0; j < standardFundingProposalsSubmitted; ++j) { - (uint256 proposalId, , uint256 votesReceived, uint256 tokensRequested, , ) = grantFund_.getProposalInfo(standardHandler_.standardFundingProposals(distributionId, j)); require( - votesReceived >= 0, - "invariant SS4: Screening votes recieved for a proposal can only be positive" + topTenProposals.length <= 10 && standardFundingProposalsSubmitted >= topTenProposals.length, + "invariant SS1: 10 or less proposals should make it through the screening stage" ); - require( - votesReceived <= _ajna.totalSupply(), - "invariant SS7: a proposal should never receive more screening votes than the token supply" - ); + // check the state of the top ten proposals + if (topTenProposals.length > 1) { + for (uint256 i = 0; i < topTenProposals.length - 1; ++i) { + // check the current proposals votes received against the next proposal in the top ten list + (, uint24 distributionIdCurr, uint256 votesReceivedCurr, , , ) = grantFund_.getProposalInfo(topTenProposals[i]); + (, uint24 distributionIdNext, uint256 votesReceivedNext, , , ) = grantFund_.getProposalInfo(topTenProposals[i + 1]); + require( + votesReceivedCurr >= votesReceivedNext, + "invariant SS3: proposals should be sorted in descending order" + ); - // check each submitted proposals votes against the last proposal in the top ten list - if (_findProposalIndex(proposalId, topTenProposals) == -1) { - if (votesReceivedLast != 0) { require( - votesReceived <= votesReceivedLast, - "invariant SS6: proposals should be incorporated into the top ten list if, and only if, they have as many or more votes as the last member of the top ten list." + votesReceivedCurr >= 0 && votesReceivedNext >= 0, + "invariant SS4: Screening votes recieved for a proposal can only be positive" + ); + + require( + distributionIdCurr == distributionIdNext && distributionIdCurr == distributionId, + "invariant SS5: distribution id for a proposal should be the same as the current distribution id" ); } } - // TODO: account for multiple distribution periods? - TestProposal memory testProposal = standardHandler_.getTestProposal(proposalId); - require( - testProposal.blockAtCreation <= grantFund_.getScreeningStageEndBlock(startBlock), - "invariant SS9: A proposal can only be created during a distribution period's screening stage" - ); + // find the number of screening votes received by the last proposal in the top ten list + uint256 votesReceivedLast; + if (topTenProposals.length != 0) { + (, , votesReceivedLast, , , ) = grantFund_.getProposalInfo(topTenProposals[topTenProposals.length - 1]); + assertGe(votesReceivedLast, 0); + } + + // check invariants against all submitted proposals + for (uint256 j = 0; j < standardFundingProposalsSubmitted; ++j) { + (uint256 proposalId, , uint256 votesReceived, uint256 tokensRequested, , ) = grantFund_.getProposalInfo(standardHandler_.standardFundingProposals(distributionId, j)); + require( + votesReceived >= 0, + "invariant SS4: Screening votes recieved for a proposal can only be positive" + ); + + require( + votesReceived <= _ajna.totalSupply(), + "invariant SS8: a proposal should never receive more screening votes than the token supply" + ); + + // check each submitted proposals votes against the last proposal in the top ten list + if (_findProposalIndex(proposalId, topTenProposals) == -1) { + if (votesReceivedLast != 0) { + require( + votesReceived <= votesReceivedLast, + "invariant SS6: proposals should be incorporated into the top ten list if, and only if, they have as many or more votes as the last member of the top ten list." + ); + } + } + + TestProposal memory testProposal = standardHandler_.getTestProposal(proposalId); + require( + testProposal.blockAtCreation <= grantFund_.getScreeningStageEndBlock(startBlock), + "invariant SS10: A proposal can only be created during a distribution period's screening stage" + ); + require( + tokensRequested <= gbc * 9 / 10, "invariant SS12: A proposal's tokens requested must be <= 90% of GBC" + ); + } + + // check proposalIds for duplicates require( - tokensRequested <= gbc * 9 / 10, "invariant SS11: A proposal's tokens requested must be <= 90% of GBC" + !hasDuplicates(allProposals), "invariant SS11: A proposal's proposalId must be unique" ); - } - // check proposalIds for duplicates - require( - !hasDuplicates(allProposals), "invariant SS10: A proposal's proposalId must be unique" - ); + if (standardHandler_.screeningVotesCast(distributionId) > 0) { + require( + topTenProposals.length > 0, + "invariant SS7: Screening vote's on a proposal should cause addition to the topTenProposals if the array is unpopulated." + ); + } - // TODO: expand this assertion - // invariant SS6: proposals should be incorporated into the top ten list if, and only if, they have as many or more votes as the last member of the top ten list. - if (standardHandler_.screeningVotesCast() > 0) { - assertTrue(topTenProposals.length > 0); + --distributionId; } } - function _invariant_SS2_SS4_SS8(GrantFund grantFund_, StandardHandler standardHandler_) internal view { + function _invariant_SS2_SS4_SS9(GrantFund grantFund_, StandardHandler standardHandler_) internal { + // set block number to current block + // TODO: find more elegant solution to block.number not being updated in time for the snapshot -> probably a modifier + vm.roll(currentBlock); + uint256 actorCount = standardHandler_.getActorsCount(); uint24 distributionId = grantFund_.getDistributionId(); + while (distributionId > 0) { - // check invariants for all actors - for (uint256 i = 0; i < actorCount; ++i) { - address actor = standardHandler_.actors(i); - uint256 votingPower = grantFund_.getVotesScreening(distributionId, actor); + // check invariants for all actors + for (uint256 i = 0; i < actorCount; ++i) { + address actor = standardHandler_.actors(i); + uint256 votingPower = grantFund_.getVotesScreening(distributionId, actor); - require( - standardHandler_.sumVoterScreeningVotes(actor, distributionId) <= votingPower, - "invariant SS2: can only vote up to the amount of voting power at the snapshot blocks" - ); - - // check the screening votes cast by the actor - ( , IGrantFund.ScreeningVoteParams[] memory screeningVoteParams, ) = standardHandler_.getVotingActorsInfo(actor, distributionId); - for (uint256 j = 0; j < screeningVoteParams.length; ++j) { require( - screeningVoteParams[j].votes >= 0, - "invariant SS4: can only cast positive votes" + standardHandler_.sumVoterScreeningVotes(actor, distributionId) <= votingPower, + "invariant SS2: can only vote up to the amount of voting power at the snapshot blocks" ); - require( - _findProposalIndex(screeningVoteParams[j].proposalId, standardHandler_.getStandardFundingProposals(distributionId)) != -1, - "invariant SS8: a proposal can only receive screening votes if it was created via propose()" - ); + // check the screening votes cast by the actor + ( , IGrantFund.ScreeningVoteParams[] memory screeningVoteParams, ) = standardHandler_.getVotingActorsInfo(actor, distributionId); + for (uint256 j = 0; j < screeningVoteParams.length; ++j) { + require( + screeningVoteParams[j].votes >= 0, + "invariant SS4: can only cast positive votes" + ); + + require( + _findProposalIndex(screeningVoteParams[j].proposalId, standardHandler_.getStandardFundingProposals(distributionId)) != -1, + "invariant SS9: a proposal can only receive screening votes if it was created via propose()" + ); + } } + + --distributionId; } } diff --git a/test/invariants/handlers/StandardHandler.sol b/test/invariants/handlers/StandardHandler.sol index 21d52468..bb64acfc 100644 --- a/test/invariants/handlers/StandardHandler.sol +++ b/test/invariants/handlers/StandardHandler.sol @@ -27,7 +27,7 @@ contract StandardHandler is Handler { uint256[] public proposalsExecuted; // number of proposals that recieved a vote in the given stage - uint256 public screeningVotesCast; + // uint256 public screeningVotesCast; uint256 public fundingVotesCast; struct VotingActor { @@ -43,6 +43,7 @@ contract StandardHandler is Handler { Slate[] topSlates; // assume that the last element in the list is the top slate bool treasuryUpdated; // whether the distribution period's surplus tokens have been readded to the treasury uint256 totalRewardsClaimed; // total delegation rewards claimed in a distribution period + bytes32 topTenHashAtLastScreeningVote; // slate hash of top ten proposals at the last time a sreening vote is cast } struct Slate { @@ -58,6 +59,7 @@ contract StandardHandler is Handler { mapping(address => mapping(uint24 => VotingActor)) internal votingActors; // actor => distributionId => VotingActor mapping(uint256 => TestProposal) public testProposals; // proposalId => TestProposal mapping(uint24 => bool) public distributionIdSurplusAdded; + mapping(uint24 => uint256) public screeningVotesCast; // total screening votes cast in a distribution period /*******************/ /*** Constructor ***/ @@ -116,6 +118,9 @@ contract StandardHandler is Handler { string memory description ) = generateProposalParams(address(_ajna), testProposalParams); + // limit the number of proposals created in a distribution period to 200 + if (standardFundingProposals[distributionId].length >= 200) return; + try _grantFund.propose(targets, values, calldatas, description) returns (uint256 proposalId) { standardFundingProposals[distributionId].push(proposalId); @@ -166,10 +171,11 @@ contract StandardHandler is Handler { for (uint256 i = 0; i < proposalsToVoteOn_; ) { actor.screeningVotes.push(screeningVoteParams[i]); - screeningVotesCast++; + screeningVotesCast[distributionId]++; ++i; } + distributionStates[distributionId].topTenHashAtLastScreeningVote = keccak256(abi.encode(_grantFund.getTopTenProposals(distributionId))); } catch (bytes memory _err){ bytes32 err = keccak256(_err); @@ -663,7 +669,7 @@ contract StandardHandler is Handler { VotingActor storage actor = votingActors[actor_][distributionId]; for (uint256 i = 0; i < numProposalsToVoteOn; ) { actor.screeningVotes.push(screeningVoteParams[i]); - screeningVotesCast++; + screeningVotesCast[distributionId]++; ++i; } diff --git a/test/invariants/scenarios/FinalizeInvariant.t.sol b/test/invariants/scenarios/FinalizeInvariant.t.sol index 2f576de3..f29595b6 100644 --- a/test/invariants/scenarios/FinalizeInvariant.t.sol +++ b/test/invariants/scenarios/FinalizeInvariant.t.sol @@ -52,6 +52,10 @@ contract FinalizeInvariant is StandardTestBase { addr: address(_standardHandler), selectors: selectors })); + + // check test setup + uint256[] memory topTenProposals = _grantFund.getTopTenProposals(distributionId); + assertTrue(topTenProposals.length > 0); } function invariant_finalize() external { diff --git a/test/invariants/scenarios/FundingInvariant.t.sol b/test/invariants/scenarios/FundingInvariant.t.sol index 030e3ea5..975ab292 100644 --- a/test/invariants/scenarios/FundingInvariant.t.sol +++ b/test/invariants/scenarios/FundingInvariant.t.sol @@ -44,6 +44,7 @@ contract FundingInvariant is StandardTestBase { uint256[] memory initialTopTenProposals = _grantFund.getTopTenProposals(_grantFund.getDistributionId()); initialTopTenHash = keccak256(abi.encode(initialTopTenProposals)); + assertTrue(initialTopTenProposals.length > 0); } function invariant_funding_stage() external { diff --git a/test/invariants/scenarios/MultipleDistributionInvariant.t.sol b/test/invariants/scenarios/MultipleDistributionInvariant.t.sol index af7006fc..5f4bc120 100644 --- a/test/invariants/scenarios/MultipleDistributionInvariant.t.sol +++ b/test/invariants/scenarios/MultipleDistributionInvariant.t.sol @@ -48,13 +48,13 @@ contract MultipleDistributionInvariant is StandardTestBase { } function invariant_all() external { - // // screening invariants - // _invariant_SS1_SS3_SS4_SS5_SS6_SS7_SS9_SS10_SS11(_grantFund, _standardHandler); - // _invariant_SS2_SS4_SS8(_grantFund, _standardHandler); + // screening invariants + _invariant_SS1_SS3_SS4_SS5_SS6_SS7_SS8_SS10_SS11_SS12(_grantFund, _standardHandler); + _invariant_SS2_SS4_SS9(_grantFund, _standardHandler); - // // funding invariants - // _invariant_FS1_FS2_FS3(_grantFund, _standardHandler); - // _invariant_FS4_FS5_FS6_FS7_FS8(_grantFund, _standardHandler); + // funding invariants + _invariant_FS1_FS2_FS3(_grantFund, _standardHandler); + _invariant_FS4_FS5_FS6_FS7_FS8(_grantFund, _standardHandler); // finalize invariants _invariant_CS1_CS2_CS3_CS4_CS5_CS6(_grantFund, _standardHandler); diff --git a/test/invariants/scenarios/ScreeningInvariant.t.sol b/test/invariants/scenarios/ScreeningInvariant.t.sol index 6171a17b..3a81c300 100644 --- a/test/invariants/scenarios/ScreeningInvariant.t.sol +++ b/test/invariants/scenarios/ScreeningInvariant.t.sol @@ -31,8 +31,8 @@ contract ScreeningInvariant is StandardTestBase { } function invariant_screening_stage() external { - _invariant_SS1_SS3_SS4_SS5_SS6_SS7_SS9_SS10_SS11(_grantFund, _standardHandler); - _invariant_SS2_SS4_SS8(_grantFund, _standardHandler); + _invariant_SS1_SS3_SS4_SS5_SS6_SS7_SS8_SS10_SS11_SS12(_grantFund, _standardHandler); + _invariant_SS2_SS4_SS9(_grantFund, _standardHandler); } function invariant_call_summary() external view { From dc667a793a01ac7bd5e7b5d0e6e0fa2a810e0c25 Mon Sep 17 00:00:00 2001 From: Prateek Gupta Date: Mon, 19 Jun 2023 23:10:30 +0530 Subject: [PATCH 05/20] Add grant fund interaction with gnosis safe unit test (#109) --- .../GrantFundWithGnosisSafe.t.sol | 191 ++++++++++++++++++ test/interactions/interfaces.sol | 53 +++++ 2 files changed, 244 insertions(+) create mode 100644 test/interactions/GrantFundWithGnosisSafe.t.sol create mode 100644 test/interactions/interfaces.sol diff --git a/test/interactions/GrantFundWithGnosisSafe.t.sol b/test/interactions/GrantFundWithGnosisSafe.t.sol new file mode 100644 index 00000000..ebcf271a --- /dev/null +++ b/test/interactions/GrantFundWithGnosisSafe.t.sol @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.18; + +import { Strings } from "@oz/utils/Strings.sol"; + +import { GrantFund } from "../../src/grants/GrantFund.sol"; +import { IGrantFundState } from "../../src/grants/interfaces/IGrantFundState.sol"; + +import "./interfaces.sol"; +import { GrantFundTestHelper } from "../utils/GrantFundTestHelper.sol"; +import { IAjnaToken } from "../utils/IAjnaToken.sol"; + +contract GrantFundWithGnosisSafe is GrantFundTestHelper { + + IGnosisSafeFactory internal _gnosisSafeFactory; + IGnosisSafe internal _gnosisSafe; + + IAjnaToken internal _token; + GrantFund internal _grantFund; + + // Ajna token Holder at the Ajna contract creation on mainnet + address internal _tokenDeployer = 0x666cf594fB18622e1ddB91468309a7E194ccb799; + + struct MultiSigOwner { + address walletAddress; + uint256 privateKey; + } + + struct Proposals { + address[] targets; + uint256[] values; + bytes[] calldatas; + string description; + bytes32 descriptionHash; + uint256 proposalId; + } + + address[] internal _votersArr; + + uint256 _treasury = 500_000_000 * 1e18; + + uint256 _nonces = 0; + + function setUp() external { + vm.createSelectFork(vm.envString("ETH_RPC_URL")); + address gnosisSafeFactoryAddress = 0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2; // mainnet gnosisSafeFactory contract address + _gnosisSafeFactory = IGnosisSafeFactory(gnosisSafeFactoryAddress); + + address singletonAddress = 0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552; // mainnet singleton contract address + + // deploy gnosis safe + address gnosisSafeAddress = _gnosisSafeFactory.createProxy(singletonAddress, ""); + + _gnosisSafe = IGnosisSafe(gnosisSafeAddress); + + (_grantFund, _token) = _deployAndFundGrantFund(_tokenDeployer, _treasury, _votersArr, 0); + + // transfer tokens to gnosis safe + changePrank(_tokenDeployer); + _token.transfer(gnosisSafeAddress, 25_000_000 * 1e18); + } + + function testGrantFundWithMultiSigWallet() external { + MultiSigOwner[] memory multiSigOwners = new MultiSigOwner[](3); + + (multiSigOwners[0].walletAddress, multiSigOwners[0].privateKey) = makeAddrAndKey("_multiSigOwner1"); + (multiSigOwners[1].walletAddress, multiSigOwners[1].privateKey) = makeAddrAndKey("_multiSigOwner2"); + (multiSigOwners[2].walletAddress, multiSigOwners[2].privateKey) = makeAddrAndKey("_multiSigOwner3"); + + address[] memory owners = new address[](3); + owners[0] = multiSigOwners[0].walletAddress; + owners[1] = multiSigOwners[1].walletAddress; + owners[2] = multiSigOwners[2].walletAddress; + + // Setup gnosis safe with 3 owners and 2 threshold to execute transaction + _gnosisSafe.setup(owners, 2, address(0), "", address(0), address(0), 0, payable(address(0))); + + // self delegate votes + bytes memory callData = abi.encodeWithSignature("delegate(address)", address(_gnosisSafe)); + _executeTransaction(address(_token), callData, multiSigOwners); + + vm.roll(block.number + 100); + + // Start distribution period + _startDistributionPeriod(_grantFund); + + uint24 distributionId = _grantFund.getDistributionId(); + + // generate proposals for distribution + Proposals[] memory proposals = _generateProposals(2); + + // propose first proposal + callData = abi.encodeWithSignature("propose(address[],uint256[],bytes[],string)", proposals[0].targets, proposals[0].values, proposals[0].calldatas, proposals[0].description); + _executeTransaction(address(_grantFund), callData, multiSigOwners); + + // propose second proposal + callData = abi.encodeWithSignature("propose(address[],uint256[],bytes[],string)", proposals[1].targets, proposals[1].values, proposals[1].calldatas, proposals[1].description); + _executeTransaction(address(_grantFund), callData, multiSigOwners); + + // skip to screening stage + vm.roll(block.number + 100); + + // construct vote params + IGrantFundState.ScreeningVoteParams[] memory screeningVoteParams = new IGrantFundState.ScreeningVoteParams[](1); + screeningVoteParams[0].proposalId = proposals[0].proposalId; + screeningVoteParams[0].votes = 20_000_000 * 1e18; + + // cast screening vote + callData = abi.encodeWithSignature("screeningVote((uint256,uint256)[])", screeningVoteParams); + _executeTransaction(address(_grantFund), callData, multiSigOwners); + + // skip to funding stage + vm.roll(block.number + 550_000); + + // construct vote params + IGrantFundState.FundingVoteParams[] memory fundingVoteParams = new IGrantFundState.FundingVoteParams[](1); + fundingVoteParams[0].proposalId = proposals[0].proposalId; + fundingVoteParams[0].votesUsed = 20_000_000 * 1e18; + + // cast funding vote + callData = abi.encodeWithSignature("fundingVote((uint256,int256)[])", fundingVoteParams); + _executeTransaction(address(_grantFund), callData, multiSigOwners); + + // skip to the Challenge period + vm.roll(block.number + 50_000); + + // construct potential proposal slate + uint256[] memory potentialProposalSlate = new uint256[](1); + potentialProposalSlate[0] = proposals[0].proposalId; + + // update slate + callData = abi.encodeWithSignature("updateSlate(uint256[],uint24)", potentialProposalSlate, distributionId); + _executeTransaction(address(_grantFund), callData, multiSigOwners); + + // skip to the end of distribution period + vm.roll(block.number + 100_000); + + // execute proposal + callData = abi.encodeWithSignature("execute(address[],uint256[],bytes[],bytes32)", proposals[0].targets, proposals[0].values, proposals[0].calldatas, proposals[0].descriptionHash); + _executeTransaction(address(_grantFund), callData, multiSigOwners); + + // claim delegate reward + callData = abi.encodeWithSignature("claimDelegateReward(uint24)", distributionId); + _executeTransaction(address(_grantFund), callData, multiSigOwners); + } + + function _executeTransaction(address contractAddress, bytes memory callData, MultiSigOwner[] memory multiSigOwners) internal { + bytes32 transactionHash = _gnosisSafe.getTransactionHash(contractAddress, 0, callData, IGnosisSafe.Operation.Call, 0, 0, 0, address(0), address(0), _nonces++); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(multiSigOwners[0].privateKey, transactionHash); + bytes memory signature1 = abi.encodePacked(r, s, v); + + (v, r, s) = vm.sign(multiSigOwners[1].privateKey, transactionHash); + bytes memory signature2 = abi.encodePacked(r, s, v); + + bytes memory signatures = abi.encodePacked(signature1, signature2); + _gnosisSafe.execTransaction(contractAddress, 0, callData, IGnosisSafe.Operation.Call, 0, 0, 0, address(0), payable(address(0)), signatures); + + } + + function _generateProposals(uint256 noOfProposals_) internal view returns(Proposals[] memory) { + Proposals[] memory proposals_ = new Proposals[](noOfProposals_); + + // generate proposal targets + address[] memory ajnaTokenTargets = new address[](1); + ajnaTokenTargets[0] = address(_token); + + // generate proposal values + uint256[] memory values = new uint256[](1); + values[0] = 0; + + // generate proposal calldata + bytes[] memory proposalCalldata = new bytes[](1); + proposalCalldata[0] = abi.encodeWithSignature( + "transfer(address,uint256)", + address(_gnosisSafe), + 1_000_000 * 1e18 + ); + + for(uint i = 0; i < noOfProposals_; i++) { + // generate proposal message + string memory description = string(abi.encodePacked("Proposal", Strings.toString(i))); + bytes32 descriptionHash = _grantFund.getDescriptionHash(description); + uint256 proposalId = _grantFund.hashProposal(ajnaTokenTargets, values, proposalCalldata, descriptionHash); + proposals_[i] = Proposals(ajnaTokenTargets, values, proposalCalldata, description, descriptionHash, proposalId); + } + return proposals_; + } + +} \ No newline at end of file diff --git a/test/interactions/interfaces.sol b/test/interactions/interfaces.sol new file mode 100644 index 00000000..57715669 --- /dev/null +++ b/test/interactions/interfaces.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.18; + +interface IGnosisSafeFactory { + function createProxy( + address _singleton, + bytes memory data + ) external returns(address gnosisSafe_); +} + +interface IGnosisSafe { + enum Operation { + Call, + DelegateCall + } + function setup( + address[]calldata _owners, + uint256 _threshold, + address to, + bytes calldata data, + address fallbackHandler, + address paymentToken, + uint256 payment, + address payable paymentReceiver + ) external; + + function execTransaction( + address to, + uint256 value, + bytes calldata data, + Operation operation, + uint256 safeTxGas, + uint256 baseGas, + uint256 gasPrice, + address gasToken, + address payable refundReceiver, + bytes memory signatures + ) external payable returns (bool success); + + function getTransactionHash ( + address to, + uint256 value, + bytes calldata data, + Operation operation, + uint256 safeTxGas, + uint256 baseGas, + uint256 gasPrice, + address gasToken, + address refundReceiver, + uint256 _nonce + ) external view returns (bytes32); +} \ No newline at end of file From 85ac65e874fd579a7945fb8787b3f9b6e0995ac7 Mon Sep 17 00:00:00 2001 From: Mike Hathaway Date: Wed, 21 Jun 2023 13:40:16 -0400 Subject: [PATCH 06/20] Invariant improvements (#108) * add support for multiple distribution periods to funding invariants * fix FS7 for multiple dist * begin updating screening invariants * remaining fixes to funding stage invariants * get screening stage invariants working in multiple distribution scenario * begin fixing todos * fix negative funding votes in happy path; update proposal token requested; update DR5 invariant check * improve ES2 invariant check * add Logger contract; cleanup logging; add useCurrentBlock modifier to TestBase * add support for env variables in invariant tests * expand usage of useCurrentBlock * cleanup invariants list doc * add P1 invariant * cleanups * cleanup multiple distribution scenario logging; add logActorDelegationRewards * fix findUnclaimedRewards; expand logging * improve DR5 check across multiple periods * cleanups * initial pr feedback * Invariant Improvements: Fix Actor log and improve `screeningVote` handler (#110) * Fix actors logs for Multiple Distribution Invariant when no distribution started * Improve screeningVote by using _screeningVoteParams to generate parameter * Add configuration for logging in invariants (#111) * update README * update docs * paramterize percentageTokensReq * fix compilation warnings * Add invariant CS7: The highest submitted funded proposal slate should have won or tied depending on when it was submitted. (#113) --------- Co-authored-by: Mike Co-authored-by: Prateek Gupta --- .env.example | 6 +- Makefile | 10 +- README.md | 22 +- foundry.toml | 3 +- test/README.md | 51 ++++ test/invariants/INVARIANTS.md | 16 +- .../base/DistributionPeriodInvariants.sol | 2 +- test/invariants/base/FinalizeInvariants.sol | 34 ++- test/invariants/base/Logger.sol | 246 ++++++++++++++++++ test/invariants/base/ScreeningInvariants.sol | 30 ++- test/invariants/base/StandardTestBase.sol | 15 +- test/invariants/base/TestBase.sol | 26 +- test/invariants/handlers/Handler.sol | 6 +- test/invariants/handlers/StandardHandler.sol | 227 +++++----------- .../scenarios/FinalizeInvariant.t.sol | 42 +-- .../scenarios/FundingInvariant.t.sol | 22 +- .../MultipleDistributionInvariant.t.sol | 54 ++-- .../scenarios/ScreeningInvariant.t.sol | 12 +- test/invariants/test-invariant.sh | 16 ++ test/utils/GrantFundTestHelper.sol | 3 +- 20 files changed, 528 insertions(+), 315 deletions(-) create mode 100644 test/README.md create mode 100644 test/invariants/base/Logger.sol create mode 100755 test/invariants/test-invariant.sh diff --git a/.env.example b/.env.example index 68cfd3c7..c7e21dd9 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,7 @@ ## Ethereum node endpoint ## ETH_RPC_URL= -FOUNDRY_EVM_VERSION=paris \ No newline at end of file +FOUNDRY_EVM_VERSION=paris +SCENARIO=MultipleDistribution # Type of Invariant Scenario to run: Screening | FundingInvariant | Finalize | MultipleDistribution +NUM_ACTORS=10 # Number of actors to simulate in invariants +NUM_PROPOSALS=10 # Maximum number of proposals to simulate in invariants +PER_ADDRESS_TOKEN_REQ_CAP=.1 # Percentage of funds available to request per proposal recipient in invariants diff --git a/Makefile b/Makefile index 85dddc1b..04483d15 100644 --- a/Makefile +++ b/Makefile @@ -15,10 +15,12 @@ install :; git submodule update --init --recursive build :; forge clean && forge build --optimize --optimizer-runs 1000000 # Tests -tests :; forge clean && forge test --mt test --optimize --optimizer-runs 1000000 -v # --ffi # enable if you need the `ffi` cheat code on HEVM -test-with-gas-report :; forge clean && forge build && forge test --mt test --optimize --optimizer-runs 1000000 -v --gas-report # --ffi # enable if you need the `ffi` cheat code on HEVM -test-invariant :; forge clean && forge t --mt invariant -coverage :; forge coverage +tests :; forge clean && forge test --mt test --optimize --optimizer-runs 1000000 -v # --ffi # enable if you need the `ffi` cheat code on HEVM +test-with-gas-report :; forge clean && forge build && forge test --mt test --optimize --optimizer-runs 1000000 -v --gas-report # --ffi # enable if you need the `ffi` cheat code on HEVM +test-invariant :; ./test/invariants/test-invariant.sh ${SCENARIO} ${NUM_ACTORS} ${NUM_PROPOSALS} ${PER_ADDRESS_TOKEN_REQ_CAP} +test-invariant-all :; forge clean && forge t --mt invariant +test-invariant-multiple-distribution :; forge clean && ./test/invariants/test-invariant.sh MultipleDistribution 25 200 +coverage :; forge coverage # Generate Gas Snapshots snapshot :; forge clean && forge snapshot --optimize --optimize-runs 1000000 diff --git a/README.md b/README.md index 2322d12c..debc61fe 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,11 @@
+## **[Tests](./test/README.md)** + +For information on running tests and checking code coverage see the **[Tests README](./test/README.md)**. + + ## **Development** Install Foundry [instructions](https://github.com/gakonst/foundry/blob/master/README.md#installation) then, install the [foundry](https://github.com/gakonst/foundry) toolchain installer (`foundryup`) with: @@ -31,23 +36,6 @@ foundryup make all ``` -#### Run Tests - -```bash -make tests -``` - -#### Code Coverage -- generate basic code coverage report: -```bash -make coverage -``` -- exclude tests from code coverage report: -``` -apt-get install lcov -bash ./check-code-coverage.sh -``` - ### Contract Deployment Ensure the following env variables are in your `.env` file or exported into your environment. | Environment Variable | Purpose | diff --git a/foundry.toml b/foundry.toml index 4cfb918f..3de0b88b 100644 --- a/foundry.toml +++ b/foundry.toml @@ -10,11 +10,12 @@ runs = 150 [invariant] runs = 3 # The number of calls to make in the invariant tests -depth = 10000 # The number of times to run the invariant tests +depth = 10000 # The number of times to run the invariant tests call_override = false # Override calls fail_on_revert = true # Fail the test if the contract reverts dictionary_weight = 80 include_storage = true include_push_bytes = true +shrink_sequence = false # See more config options https://github.com/foundry-rs/foundry/tree/master/config diff --git a/test/README.md b/test/README.md new file mode 100644 index 00000000..ea914889 --- /dev/null +++ b/test/README.md @@ -0,0 +1,51 @@ +# Ecosystem Coordination Tests +## Forge tests +### Unit tests +- validation tests: +```bash +make tests +``` +- validation tests with gas report: +```bash +make test-with-gas-report +``` + +### Invariant tests +#### Configuration +Invariant test scenarios can be externally configured by customizing following environment variables: +| Variable | Default | Description | +| ------------- | ------------- | ------------- | +| SCENARIO | MultipleDistribution | Type of Invariant Scenario to run: Screening, FundingInvariant, Finalize, MultipleDistribution | +| PER_ADDRESS_TOKEN_REQ_CAP | .1 | Percentage of funds available to request per proposal recipient in invariants | +| NUM_ACTORS | 20 | Max number of actors to participate in invariant testing | +| NUM_PROPOSALS | 200 | Max number of proposals that can be proposed in invariant testing | +| LOGS_VERBOSITY | 0 |

Details to log

0 = No Logs

1 = Calls details, Proposal details, Time details

2 = Calls details, Proposal details, Time details, Funding proposal details, Finalize proposals details

3 = Calls details, Proposal details, Time details, Funding proposal details, Finalize proposals details, Actor details + +#### Custom Scenarios + +Custom scenario configurations are defined in [scenarios](forge/invariants/scenarios/) directory. +For running a custom scenario +```bash +make test-invariant SCENARIO= +``` +For example, to test all invariants for multiple distribution (Roll between each handler call can be max 5000 blocks): +```bash +make test-invariant SCENARIO=MultipleDistribution +``` + +#### Commands +- run all invariant tests: +```bash +make test-invariant-all +``` + +### Code Coverage +- generate basic code coverage report: +```bash +make coverage +``` +- exclude tests from code coverage report: +``` +apt-get install lcov +bash ../check-code-coverage.sh +``` diff --git a/test/invariants/INVARIANTS.md b/test/invariants/INVARIANTS.md index ed2b4cf9..c28f2519 100644 --- a/test/invariants/INVARIANTS.md +++ b/test/invariants/INVARIANTS.md @@ -22,13 +22,11 @@ - **SS4**: Screening vote's cast can only be positive. - **SS5**: Screening votes can only be cast on a proposal in it's distribution period's screening stage. - **SS6**: For every proposal, it is included in the top 10 list if, and only if, it has as many or more votes as the last member of the top ten list (typically the 10th of course, but it may be shorter than ten proposals). - - **SS7**: Screening vote's on a proposal should cause addition to the topTenProposals if no proposal has been added yet. - - - **SS8**: A proposal should never receive more vote than the Ajna token supply. + - **SS7**: Screening votes on a proposal should cause addition to the topTenProposals if no proposal has been added yet + - **SS8**: A proposal should never receive more screening votes than the Ajna token supply. - **SS9**: A proposal can only receive screening votes if it was created via `propose()`. - **SS10**: A proposal can only be created during a distribution period's screening stage. - - **SS11**: A proposal's proposalId must be unique. - - **SS12**: A proposal's tokens requested must be <= 90% of GBC. + - **SS11**: A proposal's tokens requested must be <= 90% of GBC. - #### Funding Stage: - **FS1**: Only 10 proposals can be voted on in the funding stage @@ -47,6 +45,7 @@ - **CS4**: Funded proposals are all a subset of the ones voted on in funding stage. - **CS5**: Funded proposal slate's should never contain duplicate proposals. - **CS6**: Funded proposal slate's can only be updated during a distribution period's challenge stage. + - **CS7**: The highest submitted funded proposal slate should have won or tied depending on when it was submitted. - #### Execute: - **ES1**: A proposal can only be executed if it's listed in the final funded proposal slate at the end of the challenge round. @@ -62,7 +61,6 @@ - **DR4**: Delegation rewards can only be claimed for a distribution period after it ended. - **DR5**: Cumulative rewards claimed should be within 99.99% of all available delegation rewards. - - -## Global Invariants: - - **G1**: A proposal should never enter an unused state (canceled, queued, expired). +- #### Proposal: + - **P1**: A proposal should never enter an unused state (pending, canceled, queued, expired). + - **P2**: A proposal's proposalId must be unique. diff --git a/test/invariants/base/DistributionPeriodInvariants.sol b/test/invariants/base/DistributionPeriodInvariants.sol index 42bf6d03..a481ba5e 100644 --- a/test/invariants/base/DistributionPeriodInvariants.sol +++ b/test/invariants/base/DistributionPeriodInvariants.sol @@ -140,7 +140,7 @@ abstract contract DistributionPeriodInvariants is TestBase { } } - function _invariant_T1_T2(GrantFund grantFund_, StandardHandler standardHandler_) internal view { + function _invariant_T1_T2(GrantFund grantFund_) internal view { require( grantFund_.treasury() <= _ajna.balanceOf(address(grantFund_)), "invariant T1: The Grant Fund's treasury should always be less than or equal to the contract's token blance" diff --git a/test/invariants/base/FinalizeInvariants.sol b/test/invariants/base/FinalizeInvariants.sol index ed8398f3..ef16e536 100644 --- a/test/invariants/base/FinalizeInvariants.sol +++ b/test/invariants/base/FinalizeInvariants.sol @@ -30,7 +30,7 @@ abstract contract FinalizeInvariants is TestBase { bytes32 fundedSlateHash; } - function _invariant_CS1_CS2_CS3_CS4_CS5_CS6(GrantFund grantFund_, StandardHandler standardHandler_) view internal { + function _invariant_CS1_CS2_CS3_CS4_CS5_CS6_CS7(GrantFund grantFund_, StandardHandler standardHandler_) view internal { uint24 distributionId = grantFund_.getDistributionId(); (, , uint256 endBlock, uint128 fundsAvailable, , bytes32 topSlateHash) = grantFund_.getDistributionPeriodInfo(distributionId); @@ -81,6 +81,11 @@ abstract contract FinalizeInvariants is TestBase { "invariant CS6: Funded proposal slate's can only be updated during a distribution period's challenge stage" ); } + + require( + state.currentTopSlate == topSlateHash, + "invariant CS7: The highest submitted funded proposal slate should have won or tied depending on when it was submitted." + ); } function _invariant_ES1_ES2_ES3_ES4_ES5(GrantFund grantFund_, StandardHandler standardHandler_) internal { @@ -96,18 +101,20 @@ abstract contract FinalizeInvariants is TestBase { uint256 proposalId = standardFundingProposals[i]; (, uint24 proposalDistributionId, , uint256 tokenRequested, , bool executed) = grantFund_.getProposalInfo(proposalId); int256 proposalIndex = _findProposalIndex(proposalId, topSlateProposalIds); - // invariant ES1: A proposal can only be executed if it's listed in the final funded proposal slate at the end of the challenge round. if (proposalIndex == -1) { - assertFalse(executed); + require( + !executed, + "invariant ES1: A proposal can only be executed if it's listed in the final funded proposal slate at the end of the challenge round." + ); } // invariant ES2: A proposal can only be executed after the challenge stage is complete. assertEq(distributionId, proposalDistributionId); if (executed) { (, , uint48 endBlock, , , ) = grantFund_.getDistributionPeriodInfo(proposalDistributionId); - // TODO: store and check proposal execution time + TestProposal memory testProposal = standardHandler_.getTestProposal(proposalId); require( - currentBlock > endBlock, + testProposal.blockAtExecution > endBlock, "invariant ES2: A proposal can only be executed after the challenge stage is complete." ); @@ -141,6 +148,7 @@ abstract contract FinalizeInvariants is TestBase { (, , distributionInfo.endBlock, distributionInfo.fundsAvailable, distributionInfo.fundingVotePowerCast, ) = grantFund_.getDistributionPeriodInfo(distributionId); uint256 totalRewardsClaimed; + uint256 actorsWithRewards; for (uint256 i = 0; i < standardHandler_.getActorsCount(); ++i) { address actor = standardHandler_.actors(i); @@ -162,6 +170,8 @@ abstract contract FinalizeInvariants is TestBase { // check that delegation rewards are greater tahn 0 if they did vote in both stages assertTrue(delegationRewardsClaimed >= 0); + actorsWithRewards += 1; + uint256 votingPowerAllocatedByDelegatee = votersInfo.fundingVotingPower - votersInfo.fundingRemainingVotingPower; uint256 rootVotingPowerAllocatedByDelegatee = Math.sqrt(votingPowerAllocatedByDelegatee * 1e18); @@ -199,13 +209,19 @@ abstract contract FinalizeInvariants is TestBase { ); // check state after all possible delegation rewards have been claimed - if (standardHandler_.numberOfCalls('SFH.claimDelegateReward.success') == standardHandler_.getActorsCount()) { - require( - totalRewardsClaimed >= Maths.wmul(distributionInfo.fundsAvailable * 1 / 10, 0.9999 * 1e18), + StandardHandler.DistributionState memory state = standardHandler_.getDistributionState(distributionId); + if (state.numVoterRewardsClaimed == standardHandler_.getNumVotersWithRewards(distributionId) && distributionInfo.endBlock < currentBlock) { + requireWithinDiff( + totalRewardsClaimed, + distributionInfo.fundsAvailable * 1 / 10, + 1e12, "invariant DR5: Cumulative rewards claimed should be within 99.99% -or- 0.01 AJNA tokens of all available delegation rewards" ); - assertEq(totalRewardsClaimed, distributionInfo.fundsAvailable * 1 / 10); } + require( + totalRewardsClaimed <= distributionInfo.fundsAvailable * 1 / 10, + "invariant DR5: Cumulative rewards claimed should be within 99.99% -or- 0.01 AJNA tokens of all available delegation rewards" + ); --distributionId; } diff --git a/test/invariants/base/Logger.sol b/test/invariants/base/Logger.sol new file mode 100644 index 00000000..587e4116 --- /dev/null +++ b/test/invariants/base/Logger.sol @@ -0,0 +1,246 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.18; + +import { console } from "@std/console.sol"; +import { Math } from "@oz/utils/math/Math.sol"; +import { Test } from "@std/Test.sol"; + +import { GrantFund } from "../../../src/grants/GrantFund.sol"; +import { IGrantFundState } from "../../../src/grants/interfaces/IGrantFundState.sol"; +import { Maths } from "../../../src/grants/libraries/Maths.sol"; + +import { StandardHandler } from "../handlers/StandardHandler.sol"; + +import { IAjnaToken } from "../../utils/IAjnaToken.sol"; +import { ITestBase } from "../base/ITestBase.sol"; + +contract Logger is Test { + + IAjnaToken internal _ajna; + GrantFund internal _grantFund; + ITestBase internal testContract; + StandardHandler internal _standardHandler; + uint256 internal logFileVerbosity; + + constructor(address grantFund_, address standardHandler_, address testContract_) { + _ajna = IAjnaToken(GrantFund(grantFund_).ajnaTokenAddress()); + _grantFund = GrantFund(grantFund_); + _standardHandler = StandardHandler(standardHandler_); + testContract = ITestBase(testContract_); + // Verbosity of Log file + logFileVerbosity = uint256(vm.envOr("LOGS_VERBOSITY", uint256(2))); + } + + /**************************/ + /*** Logging Functions ****/ + /**************************/ + + function logCallSummary() external view { + if (logFileVerbosity < 1) return; + console.log("\nCall Summary\n"); + console.log("--SFM----------"); + console.log("SFH.startNewDistributionPeriod ", _standardHandler.numberOfCalls("SFH.startNewDistributionPeriod")); + console.log("SFH.propose ", _standardHandler.numberOfCalls("SFH.propose")); + console.log("SFH.screeningVote ", _standardHandler.numberOfCalls("SFH.screeningVote")); + console.log("SFH.fundingVote ", _standardHandler.numberOfCalls("SFH.fundingVote")); + console.log("SFH.updateSlate ", _standardHandler.numberOfCalls("SFH.updateSlate")); + console.log("SFH.execute ", _standardHandler.numberOfCalls("SFH.execute")); + console.log("SFH.claimDelegateReward ", _standardHandler.numberOfCalls("SFH.claimDelegateReward")); + console.log("roll ", _standardHandler.numberOfCalls("roll")); + console.log("------------------"); + console.log( + "Total Calls:", + _standardHandler.numberOfCalls("SFH.startNewDistributionPeriod") + + _standardHandler.numberOfCalls("SFH.propose") + + _standardHandler.numberOfCalls("SFH.screeningVote") + + _standardHandler.numberOfCalls("SFH.fundingVote") + + _standardHandler.numberOfCalls("SFH.updateSlate") + + _standardHandler.numberOfCalls("SFH.execute") + + _standardHandler.numberOfCalls("SFH.claimDelegateReward") + + _standardHandler.numberOfCalls("roll") + ); + } + + function logProposalSummary() external view { + if (logFileVerbosity < 1) return; + uint24 distributionId = _grantFund.getDistributionId(); + uint256[] memory proposals = _standardHandler.getStandardFundingProposals(distributionId); + + console.log("\nProposal Summary\n"); + console.log("Number of Proposals", proposals.length); + for (uint256 i = 0; i < proposals.length; ++i) { + console.log("------------------"); + (uint256 proposalId, , uint128 votesReceived, uint128 tokensRequested, int128 fundingVotesReceived, bool executed) = _grantFund.getProposalInfo(proposals[i]); + console.log("proposalId: ", proposalId); + console.log("distributionId: ", distributionId); + console.log("executed: ", executed); + console.log("screening votesReceived: ", votesReceived); + console.log("tokensRequested: ", tokensRequested); + if (fundingVotesReceived < 0) { + console.log("Negative fundingVotesReceived: ", uint256(Maths.abs(fundingVotesReceived))); + } + else { + console.log("Positive fundingVotesReceived: ", uint256(int256(fundingVotesReceived))); + } + + console.log("------------------"); + } + console.log("\n"); + } + + function logTimeSummary() external view { + if (logFileVerbosity < 1) return; + uint24 distributionId = _grantFund.getDistributionId(); + (, uint256 startBlock, uint256 endBlock, , , ) = _grantFund.getDistributionPeriodInfo(distributionId); + console.log("\nTime Summary\n"); + console.log("------------------"); + console.log("Distribution Id: %s", distributionId); + console.log("start block: %s", startBlock); + console.log("end block: %s", endBlock); + console.log("block number: %s", block.number); + console.log("current block: %s", testContract.currentBlock()); + console.log("------------------"); + } + + function logFinalizeSummary(uint24 distributionId_) external view { + if (logFileVerbosity < 2) return; + (, , , uint128 fundsAvailable, , bytes32 topSlateHash) = _grantFund.getDistributionPeriodInfo(distributionId_); + uint256[] memory topSlateProposalIds = _grantFund.getFundedProposalSlate(topSlateHash); + + uint256[] memory topTenScreenedProposalIds = _grantFund.getTopTenProposals(distributionId_); + + console.log("\nFinalize Summary\n"); + console.log("------------------"); + console.log("Distribution Id: ", distributionId_); + console.log("Funds Available: ", fundsAvailable); + // DELEGATION REWARDS LOGS + console.log("Delegation Rewards Set: ", _standardHandler.numberOfCalls('delegationRewardSet')); + console.log("Delegation Rewards Claimed: ", _standardHandler.numberOfCalls('SFH.claimDelegateReward.success')); + // EXECUTE LOGS + console.log("Proposal Execute attempt: ", _standardHandler.numberOfCalls('SFH.execute.attempt')); + console.log("Proposal Execute Count: ", _standardHandler.numberOfCalls('SFH.execute.success')); + console.log("unexecuted proposal: ", _standardHandler.numberOfCalls('unexecuted.proposal')); + // UPDATE SLATE LOGS + console.log("Slate Update Prep: ", _standardHandler.numberOfCalls('SFH.updateSlate.prep')); + console.log("Slate Update length: ", _standardHandler.numberOfCalls('updateSlate.length')); + console.log("Slate Update Called: ", _standardHandler.numberOfCalls('SFH.updateSlate.called')); + console.log("Slate Update Success: ", _standardHandler.numberOfCalls('SFH.updateSlate.success')); + console.log("Slate Proposals: ", _standardHandler.numberOfCalls('proposalsInSlates')); + console.log("Next Slate length: ", _standardHandler.numberOfCalls('updateSlate.length')); + console.log("unused proposal: ", _standardHandler.numberOfCalls('unused.proposal')); + console.log("Top Slate Proposal Count: ", topSlateProposalIds.length); + console.log("Top Ten Proposal Count: ", topTenScreenedProposalIds.length); + console.log("Top slate funds requested: ", _standardHandler.getTokensRequestedInFundedSlateInvariant(topSlateHash)); + console.log("------------------"); + } + + function logFundingSummary(uint24 distributionId_) external view { + if (logFileVerbosity < 2) return; + console.log("\nFunding Summary\n"); + console.log("------------------"); + console.log("number of funding stage starts: ", _standardHandler.numberOfCalls("SFH.FundingStage")); + console.log("number of funding stage success votes: ", _standardHandler.numberOfCalls("SFH.fundingVote.success")); + console.log("number of proposals receiving funding: ", _standardHandler.numberOfCalls("SFH.fundingVote.proposal")); + console.log("number of funding stage negative votes: ", _standardHandler.numberOfCalls("SFH.negativeFundingVote")); + console.log("distributionId: ", distributionId_); + console.log("SFH.updateSlate.success: ", _standardHandler.numberOfCalls("SFH.updateSlate.success")); + (, , , , uint256 fundingPowerCast, ) = _grantFund.getDistributionPeriodInfo(distributionId_); + console.log("Total Funding Power Cast: ", fundingPowerCast); + console.log("------------------"); + } + + function logActorSummary(uint24 distributionId_, bool funding_, bool screening_) external view { + if (logFileVerbosity < 3) return; + console.log("\nActor Summary\n"); + + console.log("------------------"); + console.log("Number of Actors", _standardHandler.getActorsCount()); + + // sum proposal votes of each actor + for (uint256 i = 0; i < _standardHandler.getActorsCount(); ++i) { + address actor = _standardHandler.actors(i); + + // get actor info + ( + IGrantFundState.FundingVoteParams[] memory fundingVoteParams, + IGrantFundState.ScreeningVoteParams[] memory screeningVoteParams, + uint256 delegationRewardsClaimed + ) = _standardHandler.getVotingActorsInfo(actor, distributionId_); + + console.log("Actor: ", actor); + console.log("Delegate: ", _ajna.delegates(actor)); + console.log("delegationRewardsClaimed: ", delegationRewardsClaimed); + console.log("\n"); + + // log funding info + if (funding_) { + console.log("--Funding----------"); + console.log("Funding proposals voted for: ", fundingVoteParams.length); + console.log("Sum of squares of fvc: ", _standardHandler.sumSquareOfVotesCast(fundingVoteParams)); + console.log("Funding Votes Cast: ", uint256(_standardHandler.sumFundingVotes(fundingVoteParams))); + console.log("Negative Funding Votes Cast: ", _standardHandler.countNegativeFundingVotes(fundingVoteParams)); + console.log("------------------"); + console.log("\n"); + } + + if (screening_) { + console.log("--Screening----------"); + console.log("Screening Voting Power: ", _grantFund.getVotesScreening(distributionId_, actor)); + console.log("Screening Votes Cast: ", _standardHandler.sumVoterScreeningVotes(actor, distributionId_)); + console.log("Screening proposals voted for: ", screeningVoteParams.length); + console.log("------------------"); + console.log("\n"); + } + } + } + + function logActorDelegationRewards(uint24 distributionId_) external view { + if (logFileVerbosity < 3) return; + console.log("\nActor Delegation Rewards\n"); + + console.log("------------------"); + console.log("Number of Actors", _standardHandler.getActorsCount()); + console.log("------------------"); + console.log("\n"); + + uint256 totalDelegationRewardsClaimed = 0; + + // get voter info + for (uint256 i = 0; i < _standardHandler.getActorsCount(); ++i) { + address actor = _standardHandler.actors(i); + + // get actor info + ( + , + , + uint256 delegationRewardsClaimed + ) = _standardHandler.getVotingActorsInfo(actor, distributionId_); + + totalDelegationRewardsClaimed += delegationRewardsClaimed; + + console.log("Actor: ", actor); + console.log("Delegate: ", _ajna.delegates(actor)); + console.log("delegationRewardsClaimed: ", delegationRewardsClaimed); + + (uint256 votingPower, uint256 remainingVotingPower, ) = _grantFund.getVoterInfo(distributionId_, actor); + + uint256 votingPowerAllocatedByDelegatee = votingPower - remainingVotingPower; + uint256 rootVotingPowerAllocatedByDelegatee = Math.sqrt(votingPowerAllocatedByDelegatee * 1e18); + console.log("votingPower: ", votingPower); + console.log("remainingVotingPower: ", remainingVotingPower); + console.log("votingPowerAllocatedByDelegatee: ", votingPowerAllocatedByDelegatee); + console.log("rootVotingPowerAllocatedByDelegatee: ", rootVotingPowerAllocatedByDelegatee); + + if (votingPowerAllocatedByDelegatee > 0 && rootVotingPowerAllocatedByDelegatee == 0) { + console.log("ACTOR ROUNDED TO 0 REWARDS: ", actor); + } + console.log("------------------"); + console.log("\n"); + + } + console.log("totalDelegationRewardsClaimed: ", totalDelegationRewardsClaimed); + } +} + + diff --git a/test/invariants/base/ScreeningInvariants.sol b/test/invariants/base/ScreeningInvariants.sol index cc2c6b99..349f06c1 100644 --- a/test/invariants/base/ScreeningInvariants.sol +++ b/test/invariants/base/ScreeningInvariants.sol @@ -4,8 +4,9 @@ pragma solidity 0.8.18; import { console } from "@std/console.sol"; -import { GrantFund } from "../../../src/grants/GrantFund.sol"; -import { IGrantFund } from "../../../src/grants/interfaces/IGrantFund.sol"; +import { GrantFund } from "../../../src/grants/GrantFund.sol"; +import { IGrantFund } from "../../../src/grants/interfaces/IGrantFund.sol"; +import { IGrantFundState } from "../../../src/grants/interfaces/IGrantFundState.sol"; import { TestBase } from "./TestBase.sol"; import { StandardHandler } from "../handlers/StandardHandler.sol"; @@ -16,11 +17,7 @@ abstract contract ScreeningInvariants is TestBase { /**** Invariants ****/ /********************/ - function _invariant_SS1_SS3_SS4_SS5_SS6_SS7_SS8_SS10_SS11_SS12(GrantFund grantFund_, StandardHandler standardHandler_) internal { - // set block number to current block - // TODO: find more elegant solution to block.number not being updated in time for the snapshot -> probably a modifier - vm.roll(currentBlock); - + function _invariant_SS1_SS3_SS4_SS5_SS6_SS7_SS8_SS10_SS11_P1_P2(GrantFund grantFund_, StandardHandler standardHandler_) internal { uint24 distributionId = grantFund_.getDistributionId(); while (distributionId > 0) { @@ -94,13 +91,22 @@ abstract contract ScreeningInvariants is TestBase { ); require( - tokensRequested <= gbc * 9 / 10, "invariant SS12: A proposal's tokens requested must be <= 90% of GBC" + tokensRequested <= gbc * 9 / 10, "invariant SS11: A proposal's tokens requested must be <= 90% of GBC" + ); + + IGrantFundState.ProposalState state = grantFund_.state(proposalId); + require( + state != IGrantFundState.ProposalState.Pending && + state != IGrantFundState.ProposalState.Canceled && + state != IGrantFundState.ProposalState.Expired && + state != IGrantFundState.ProposalState.Queued, + "Invariant P1: A proposal should never enter an unused state (pending, canceled, queued, expired)." ); } // check proposalIds for duplicates require( - !hasDuplicates(allProposals), "invariant SS11: A proposal's proposalId must be unique" + !hasDuplicates(allProposals), "invariant P2: A proposal's proposalId must be unique" ); if (standardHandler_.screeningVotesCast(distributionId) > 0) { @@ -114,11 +120,7 @@ abstract contract ScreeningInvariants is TestBase { } } - function _invariant_SS2_SS4_SS9(GrantFund grantFund_, StandardHandler standardHandler_) internal { - // set block number to current block - // TODO: find more elegant solution to block.number not being updated in time for the snapshot -> probably a modifier - vm.roll(currentBlock); - + function _invariant_SS2_SS4_SS9(GrantFund grantFund_, StandardHandler standardHandler_) internal view { uint256 actorCount = standardHandler_.getActorsCount(); uint24 distributionId = grantFund_.getDistributionId(); while (distributionId > 0) { diff --git a/test/invariants/base/StandardTestBase.sol b/test/invariants/base/StandardTestBase.sol index b82d6604..b21b348e 100644 --- a/test/invariants/base/StandardTestBase.sol +++ b/test/invariants/base/StandardTestBase.sol @@ -4,8 +4,9 @@ pragma solidity 0.8.18; import { console } from "@std/console.sol"; -import { TestBase } from "./TestBase.sol"; +import { Logger } from "./Logger.sol"; import { StandardHandler } from "../handlers/StandardHandler.sol"; +import { TestBase } from "./TestBase.sol"; import { DistributionPeriodInvariants } from "./DistributionPeriodInvariants.sol"; import { FinalizeInvariants } from "./FinalizeInvariants.sol"; @@ -14,10 +15,13 @@ import { ScreeningInvariants } from "./ScreeningInvariants.sol"; contract StandardTestBase is DistributionPeriodInvariants, FinalizeInvariants, FundingInvariants, ScreeningInvariants { - uint256 internal constant NUM_ACTORS = 20; + uint256 internal constant NUM_ACTORS = 20; // default number of actors + uint256 internal constant NUM_PROPOSALS = 200; // default maximum number of proposals that can be created in a distribution period + uint256 internal constant PER_ADDRESS_TOKEN_REQ_CAP = 10; // Percentage of funds available to request per proposal recipient in invariants uint256 public constant TOKENS_TO_DISTRIBUTE = 500_000_000 * 1e18; StandardHandler internal _standardHandler; + Logger internal _logger; function setUp() public virtual override { super.setUp(); @@ -26,11 +30,16 @@ contract StandardTestBase is DistributionPeriodInvariants, FinalizeInvariants, F payable(address(_grantFund)), address(_ajna), _tokenDeployer, - NUM_ACTORS, + vm.envOr("NUM_ACTORS", NUM_ACTORS), + vm.envOr("NUM_PROPOSALS", NUM_PROPOSALS), + vm.envOr("PER_ADDRESS_TOKEN_REQ_CAP", PER_ADDRESS_TOKEN_REQ_CAP), TOKENS_TO_DISTRIBUTE, address(this) ); + // instantiate logger + _logger = new Logger(address(_grantFund), address(_standardHandler), address(this)); + // explicitly target handler targetContract(address(_standardHandler)); } diff --git a/test/invariants/base/TestBase.sol b/test/invariants/base/TestBase.sol index cf3eb53b..f2600c60 100644 --- a/test/invariants/base/TestBase.sol +++ b/test/invariants/base/TestBase.sol @@ -36,8 +36,32 @@ contract TestBase is Test, GrantFundTestHelper { currentBlock = block.number; } - function setCurrentBlock(uint256 currentBlock_) external { + /*****************/ + /*** Modifiers ***/ + /*****************/ + + modifier useCurrentBlock() { + vm.roll(currentBlock); + + _; + + setCurrentBlock(block.number); + } + + /***************************/ + /**** Utility Functions ****/ + /***************************/ + + function setCurrentBlock(uint256 currentBlock_) public { currentBlock = currentBlock_; } + function getDiff(uint256 x, uint256 y) internal pure returns (uint256 diff) { + diff = x > y ? x - y : y - x; + } + + function requireWithinDiff(uint256 x, uint256 y, uint256 expectedDiff, string memory err) internal pure { + require(getDiff(x, y) <= expectedDiff, err); + } + } diff --git a/test/invariants/handlers/Handler.sol b/test/invariants/handlers/Handler.sol index 1921a355..19efd4e3 100644 --- a/test/invariants/handlers/Handler.sol +++ b/test/invariants/handlers/Handler.sol @@ -198,7 +198,7 @@ contract Handler is Test, GrantFundTestHelper { } function randomAmount(uint256 maxAmount_) internal returns (uint256) { - return constrictToRange(randomSeed(), 1, maxAmount_); + return constrictToRange(randomSeed(), 0, maxAmount_); } function randomActor() internal returns (address) { @@ -218,6 +218,10 @@ contract Handler is Test, GrantFundTestHelper { /*** View Functions ****/ /***********************/ + function getActors() public view returns(address[] memory) { + return actors; + } + function getActorsCount() public view returns(uint256) { return actors.length; } diff --git a/test/invariants/handlers/StandardHandler.sol b/test/invariants/handlers/StandardHandler.sol index bb64acfc..1fb1caf5 100644 --- a/test/invariants/handlers/StandardHandler.sol +++ b/test/invariants/handlers/StandardHandler.sol @@ -4,7 +4,6 @@ pragma solidity 0.8.18; import { console } from "@std/console.sol"; import { Test } from "forge-std/Test.sol"; -import { Math } from "@oz/utils/math/Math.sol"; import { SafeCast } from "@oz/utils/math/SafeCast.sol"; import { Strings } from "@oz/utils/Strings.sol"; import { Math } from "@oz/utils/math/Math.sol"; @@ -25,9 +24,10 @@ contract StandardHandler is Handler { // proposalId of proposals executed uint256[] public proposalsExecuted; + uint256 public maxProposals; // maximum number of proposals for a distribution period + uint256 public percentageTokensReq; // Percentage of funds available to request per proposal recipient in invariants // number of proposals that recieved a vote in the given stage - // uint256 public screeningVotesCast; uint256 public fundingVotesCast; struct VotingActor { @@ -43,6 +43,7 @@ contract StandardHandler is Handler { Slate[] topSlates; // assume that the last element in the list is the top slate bool treasuryUpdated; // whether the distribution period's surplus tokens have been readded to the treasury uint256 totalRewardsClaimed; // total delegation rewards claimed in a distribution period + uint256 numVoterRewardsClaimed; // number of unique voters who claimed rewards in a distribution period bytes32 topTenHashAtLastScreeningVote; // slate hash of top ten proposals at the last time a sreening vote is cast } @@ -70,9 +71,14 @@ contract StandardHandler is Handler { address token_, address tokenDeployer_, uint256 numOfActors_, + uint256 maxProposals_, + uint256 percentageTokensReq_, uint256 treasury_, address testContract_ - ) Handler(grantFund_, token_, tokenDeployer_, numOfActors_, treasury_, testContract_) {} + ) Handler(grantFund_, token_, tokenDeployer_, numOfActors_, treasury_, testContract_) { + maxProposals = maxProposals_; + percentageTokensReq = percentageTokensReq_; + } /*************************/ /*** Wrapped Functions ***/ @@ -118,8 +124,8 @@ contract StandardHandler is Handler { string memory description ) = generateProposalParams(address(_ajna), testProposalParams); - // limit the number of proposals created in a distribution period to 200 - if (standardFundingProposals[distributionId].length >= 200) return; + // liimit the number of proposals created in a distribution period + if (standardFundingProposals[distributionId].length >= maxProposals) return; try _grantFund.propose(targets, values, calldatas, description) returns (uint256 proposalId) { standardFundingProposals[distributionId].push(proposalId); @@ -149,21 +155,8 @@ contract StandardHandler is Handler { vm.roll(block.number + 100); - // get actor voting power - uint256 votingPower = _grantFund.getVotesScreening(_grantFund.getDistributionId(), _actor); - // construct vote params - IGrantFundState.ScreeningVoteParams[] memory screeningVoteParams = new IGrantFundState.ScreeningVoteParams[](proposalsToVoteOn_); - for (uint256 i = 0; i < proposalsToVoteOn_; i++) { - // get a random proposal - uint256 proposalId = randomProposal(); - - // generate screening vote params - screeningVoteParams[i] = IGrantFundState.ScreeningVoteParams({ - proposalId: proposalId, - votes: constrictToRange(randomSeed(), 0, votingPower) // TODO: account for previously used voting power in a happy path scenario - }); - } + IGrantFundState.ScreeningVoteParams[] memory screeningVoteParams = _screeningVoteParams(_actor, distributionId, proposalsToVoteOn_, true); try _grantFund.screeningVote(screeningVoteParams) { // update actor screeningVotes if vote was successful @@ -204,7 +197,6 @@ contract StandardHandler is Handler { // bind proposalsToVoteOn_ to the number of proposals proposalsToVoteOn_ = constrictToRange(proposalsToVoteOn_, 1, standardFundingProposals[distributionId].length); - // TODO: switch this to true or false? potentially also move flip coin up // get the fundingVoteParams for the votes the actor is about to cast // take the chaotic path, and cast votes that will likely exceed the actor's voting power IGrantFundState.FundingVoteParams[] memory fundingVoteParams = _fundingVoteParams(_actor, distributionId, proposalsToVoteOn_, true); @@ -330,7 +322,6 @@ contract StandardHandler is Handler { uint24 distributionId = _grantFund.getDistributionId(); if (distributionId == 0) return; - // TODO: implement unhappy path uint256 proposalId = _findUnexecutedProposalId(distributionId); TestProposal memory proposal = testProposals[proposalId]; numberOfCalls['unexecuted.proposal'] = proposalId; @@ -346,8 +337,10 @@ contract StandardHandler is Handler { try _grantFund.execute(targets, values, calldatas, keccak256(bytes(proposal.description))) returns (uint256 proposalId_) { assertEq(proposalId_, proposal.proposalId); + numberOfCalls['SFH.execute.success']++; proposalsExecuted.push(proposalId_); + testProposals[proposalId].blockAtExecution = block.number; } catch (bytes memory _err){ bytes32 err = keccak256(_err); @@ -365,20 +358,23 @@ contract StandardHandler is Handler { uint24 distributionId = _grantFund.getDistributionId(); if (distributionId == 0) return; - uint24 distributionIdToClaim = _findUnclaimedReward(_actor, distributionId); + (address actor, uint24 distributionIdToClaim) = _findUnclaimedReward(distributionId); + + changePrank(actor); try _grantFund.claimDelegateReward(distributionIdToClaim) returns (uint256 rewardClaimed_) { numberOfCalls['SFH.claimDelegateReward.success']++; // should only be able to claim delegation rewards once - assertEq(votingActors[_actor][distributionIdToClaim].delegationRewardsClaimed, 0); + assertEq(votingActors[actor][distributionIdToClaim].delegationRewardsClaimed, 0); // rewards should be non zero assertTrue(rewardClaimed_ > 0); // record the newly claimed rewards - votingActors[_actor][distributionIdToClaim].delegationRewardsClaimed = rewardClaimed_; + votingActors[actor][distributionIdToClaim].delegationRewardsClaimed = rewardClaimed_; distributionStates[distributionIdToClaim].totalRewardsClaimed += rewardClaimed_; + distributionStates[distributionIdToClaim].numVoterRewardsClaimed++; } catch (bytes memory _err){ bytes32 err = keccak256(_err); @@ -386,6 +382,7 @@ contract StandardHandler is Handler { err == keccak256(abi.encodeWithSignature("DelegateRewardInvalid()")) || err == keccak256(abi.encodeWithSignature("DistributionPeriodStillActive()")) || err == keccak256(abi.encodeWithSignature("RewardAlreadyClaimed()")), + // err == keccak256("Division or modulo by 0"), // when called with 0 funding voting power or Math.sqrt() rounds down to 0 power UNEXPECTED_REVERT ); } @@ -447,8 +444,9 @@ contract StandardHandler is Handler { uint24 distributionId = _grantFund.getDistributionId(); (, , , uint128 fundsAvailable, , ) = _grantFund.getDistributionPeriodInfo(distributionId); - // account for amount that was previously requested - uint256 additionalTokensRequested = randomAmount(uint256(fundsAvailable * 9 /10) - totalTokensRequested); + // set a proposals tokens requested for an address's max amount to a configurable percentage of the funds available in a period + // account for amount that was previously requested with totalTokensRequested accumulator + uint256 additionalTokensRequested = randomAmount(uint256(fundsAvailable * percentageTokensReq / 100) - totalTokensRequested); totalTokensRequested += additionalTokensRequested; testProposalParams_[i] = TestProposalParams({ @@ -549,10 +547,26 @@ contract StandardHandler is Handler { } } - // Need to account for a proposal prior vote direction in test setup + // flip a coin to see if we should generate a positive or negative vote + if (randomSeed() % 2 == 0) { + numberOfCalls['SFH.negativeFundingVote']++; + // generate negative vote + fundingVoteParams_[i] = IGrantFundState.FundingVoteParams({ + proposalId: proposalId, + votesUsed: -1 * votesToCast + }); + } + numberOfCalls['SFH.fundingVote.proposal']++; + + // Ensure new vote won't revert from a change of direction if (priorVoteIndex != -1) { - // check if prior vote was negative - if (priorVotes[uint256(priorVoteIndex)].votesUsed < 0) { + int256 priorVotesUsed = priorVotes[uint256(priorVoteIndex)].votesUsed; + // check if prior vote was negative and this vote is positive + if (priorVotesUsed < 0 && votesToCast > 0) { + votesToCast = votesToCast * -1; + } + // check if prior vote was positive and this vote is negative + if (priorVotesUsed > 0 && votesToCast < 0) { votesToCast = votesToCast * -1; } } @@ -576,20 +590,6 @@ contract StandardHandler is Handler { // start voting on the next proposal ++i; } - else { - // TODO: move flip coin into happy path - // flip a coin to see if should instead use a negative vote - if (randomSeed() % 2 == 0) { - numberOfCalls['SFH.negativeFundingVote']++; - // generate negative vote - fundingVoteParams_[i] = IGrantFundState.FundingVoteParams({ - proposalId: proposalId, - votesUsed: -1 * votesToCast - }); - ++i; - continue; - } - } } } @@ -599,8 +599,8 @@ contract StandardHandler is Handler { uint256 numProposalsToVoteOn_, bool happyPath_ ) internal returns (IGrantFundState.ScreeningVoteParams[] memory screeningVoteParams_) { - uint256 votingPower = _grantFund.getVotesScreening(distributionId_, actor_); - uint256 totalVotesUsed = 0; + uint256 votingPower = _grantFund.getVotesScreening(distributionId_, actor_); + uint256 totalVotesUsed = _grantFund.getScreeningVotesCast(distributionId_, actor_); // determine which proposals should be voted upon screeningVoteParams_ = new IGrantFundState.ScreeningVoteParams[](numProposalsToVoteOn_); @@ -723,135 +723,36 @@ contract StandardHandler is Handler { } } - function _findUnclaimedReward(address actor_, uint24 endingDistributionId_) internal returns (uint24 distributionIdToClaim_) { - for (uint24 i = 1; i <= endingDistributionId_; ) { - uint256 delegationReward = _grantFund.getDelegateReward(i, actor_); - numberOfCalls["delegationRewardSet"]++; - if (delegationReward > 0) { - numberOfCalls["delegationRewardSet"]++; - distributionIdToClaim_ = i; - break; - } - ++i; - } - } - - /**************************/ - /*** Logging Functions ****/ - /**************************/ - - function logActorSummary(uint24 distributionId_, bool funding_, bool screening_) external view { - console.log("\nActor Summary\n"); - - console.log("------------------"); - console.log("Number of Actors", getActorsCount()); - - // sum proposal votes of each actor - for (uint256 i = 0; i < getActorsCount(); ++i) { + function _findUnclaimedReward(uint24 endingDistributionId_) internal returns (address, uint24) { + for (uint256 i = 0; i < actors.length; ++i) { + // get an actor who hasn't already claimed rewards for a period address actor = actors[i]; - // get actor info - ( - IGrantFundState.FundingVoteParams[] memory fundingVoteParams, - IGrantFundState.ScreeningVoteParams[] memory screeningVoteParams, - uint256 delegationRewardsClaimed - ) = getVotingActorsInfo(actor, distributionId_); - - console.log("Actor: ", actor); - console.log("Delegate: ", _ajna.delegates(actor)); - console.log("delegationRewardsClaimed: ", delegationRewardsClaimed); - console.log("\n"); - - // log funding info - if (funding_) { - console.log("--Funding----------"); - console.log("Funding proposals voted for: ", fundingVoteParams.length); - console.log("Sum of squares of fvc: ", sumSquareOfVotesCast(fundingVoteParams)); - console.log("Funding Votes Cast: ", uint256(sumFundingVotes(fundingVoteParams))); - console.log("Negative Funding Votes Cast: ", countNegativeFundingVotes(fundingVoteParams)); - console.log("------------------"); - console.log("\n"); - } - - if (screening_) { - console.log("--Screening----------"); - console.log("Screening Voting Power: ", _grantFund.getVotesScreening(distributionId_, actor)); - console.log("Screening Votes Cast: ", sumVoterScreeningVotes(actor, distributionId_)); - console.log("Screening proposals voted for: ", screeningVoteParams.length); - console.log("------------------"); - console.log("\n"); + for (uint24 j = 1; j <= endingDistributionId_; ) { + uint256 delegationReward = _grantFund.getDelegateReward(j, actor); + numberOfCalls["delegationRewardSet"]++; + if (delegationReward > 0 && _grantFund.getHasClaimedRewards(j, actor) == false) { + numberOfCalls["delegationRewardSet"]++; + return (actor, j); + } + ++j; } } + return (address(0), 0); } - function logCallSummary() external view { - console.log("\nCall Summary\n"); - console.log("--SFM----------"); - console.log("SFH.startNewDistributionPeriod ", numberOfCalls["SFH.startNewDistributionPeriod"]); - console.log("SFH.propose ", numberOfCalls["SFH.propose"]); - console.log("SFH.screeningVote ", numberOfCalls["SFH.screeningVote"]); - console.log("SFH.fundingVote ", numberOfCalls["SFH.fundingVote"]); - console.log("SFH.updateSlate ", numberOfCalls["SFH.updateSlate"]); - console.log("SFH.execute ", numberOfCalls["SFH.execute"]); - console.log("SFH.claimDelegateReward ", numberOfCalls["SFH.claimDelegateReward"]); - console.log("roll ", numberOfCalls["roll"]); - console.log("------------------"); - console.log( - "Total Calls:", - numberOfCalls["SFH.startNewDistributionPeriod"] + - numberOfCalls["SFH.propose"] + - numberOfCalls["SFH.screeningVote"] + - numberOfCalls["SFH.fundingVote"] + - numberOfCalls["SFH.updateSlate"] + - numberOfCalls["SFH.execute"] + - numberOfCalls["SFH.claimDelegateReward"] + - numberOfCalls["roll"] - ); - } + /***********************/ + /*** View Functions ****/ + /***********************/ - function logProposalSummary() external view { - uint24 distributionId = _grantFund.getDistributionId(); - uint256[] memory proposals = standardFundingProposals[distributionId]; - - console.log("\nProposal Summary\n"); - console.log("Number of Proposals", proposals.length); - for (uint256 i = 0; i < proposals.length; ++i) { - console.log("------------------"); - (uint256 proposalId, , uint128 votesReceived, uint128 tokensRequested, int128 fundingVotesReceived, bool executed) = _grantFund.getProposalInfo(proposals[i]); - console.log("proposalId: ", proposalId); - console.log("distributionId: ", distributionId); - console.log("executed: ", executed); - console.log("screening votesReceived: ", votesReceived); - console.log("tokensRequested: ", tokensRequested); - if (fundingVotesReceived < 0) { - console.log("Negative fundingVotesReceived: ", uint256(Maths.abs(fundingVotesReceived))); - } - else { - console.log("Positive fundingVotesReceived: ", uint256(int256(fundingVotesReceived))); + function getNumVotersWithRewards(uint24 distributionId) external view returns (uint256 numVoters_) { + for (uint256 i = 0; i < actors.length; ++i) { + if (_grantFund.getDelegateReward(distributionId, actors[i]) > 0) { + numVoters_++; } - - console.log("------------------"); } - console.log("\n"); } - function logTimeSummary() external view { - uint24 distributionId = _grantFund.getDistributionId(); - (, uint256 startBlock, uint256 endBlock, , , ) = _grantFund.getDistributionPeriodInfo(distributionId); - console.log("\nTime Summary\n"); - console.log("------------------"); - console.log("Distribution Id: %s", distributionId); - console.log("start block: %s", startBlock); - console.log("end block: %s", endBlock); - console.log("block number: %s", block.number); - console.log("current block: %s", testContract.currentBlock()); - console.log("------------------"); - } - - /***********************/ - /*** View Functions ****/ - /***********************/ - function getDistributionFundsUpdated(uint24 distributionId_) external view returns (bool) { return distributionStates[distributionId_].treasuryUpdated; } diff --git a/test/invariants/scenarios/FinalizeInvariant.t.sol b/test/invariants/scenarios/FinalizeInvariant.t.sol index f29595b6..8a13b33a 100644 --- a/test/invariants/scenarios/FinalizeInvariant.t.sol +++ b/test/invariants/scenarios/FinalizeInvariant.t.sol @@ -58,45 +58,21 @@ contract FinalizeInvariant is StandardTestBase { assertTrue(topTenProposals.length > 0); } - function invariant_finalize() external { - _invariant_CS1_CS2_CS3_CS4_CS5_CS6(_grantFund, _standardHandler); + function invariant_finalize() external useCurrentBlock { + _invariant_CS1_CS2_CS3_CS4_CS5_CS6_CS7(_grantFund, _standardHandler); _invariant_ES1_ES2_ES3_ES4_ES5(_grantFund, _standardHandler); _invariant_DR1_DR2_DR3_DR4_DR5(_grantFund, _standardHandler); } - function invariant_call_summary() external view { + function invariant_call_summary() external useCurrentBlock { uint24 distributionId = _grantFund.getDistributionId(); - _standardHandler.logCallSummary(); - _standardHandler.logActorSummary(distributionId, false, false); - _standardHandler.logProposalSummary(); - _standardHandler.logTimeSummary(); - _logFinalizeSummary(distributionId); - } - - function _logFinalizeSummary(uint24 distributionId_) internal view { - (, , , uint128 fundsAvailable, , bytes32 topSlateHash) = _grantFund.getDistributionPeriodInfo(distributionId_); - uint256[] memory topSlateProposalIds = _grantFund.getFundedProposalSlate(topSlateHash); - - uint256[] memory topTenScreenedProposalIds = _grantFund.getTopTenProposals(distributionId_); - - console.log("\nFinalize Summary\n"); - console.log("------------------"); - console.log("Distribution Id: ", distributionId_); - console.log("Delegation Rewards Claimed: ", _standardHandler.numberOfCalls('SFH.claimDelegateReward.success')); - console.log("Proposal Execute attempt: ", _standardHandler.numberOfCalls('SFH.execute.attempt')); - console.log("Proposal Execute Count: ", _standardHandler.numberOfCalls('SFH.execute.success')); - console.log("Slate Created: ", _standardHandler.numberOfCalls('SFH.updateSlate.prep')); - console.log("Slate Update Called: ", _standardHandler.numberOfCalls('SFH.updateSlate.called')); - console.log("Slate Update Count: ", _standardHandler.numberOfCalls('SFH.updateSlate.success')); - console.log("Next Slate length: ", _standardHandler.numberOfCalls('updateSlate.length')); - console.log("Top Slate Proposal Count: ", topSlateProposalIds.length); - console.log("Top Ten Proposal Count: ", topTenScreenedProposalIds.length); - console.log("Funds Available: ", fundsAvailable); - console.log("Top slate funds requested: ", _standardHandler.getTokensRequestedInFundedSlateInvariant(topSlateHash)); - (, , , , uint256 fundingPowerCast, ) = _grantFund.getDistributionPeriodInfo(distributionId_); - console.log("Total Funding Power Cast ", fundingPowerCast); - console.log("------------------"); + _logger.logCallSummary(); + _logger.logTimeSummary(); + _logger.logFinalizeSummary(distributionId); + _logger.logActorSummary(distributionId, false, false); + _logger.logProposalSummary(); + _logger.logActorDelegationRewards(distributionId); } } diff --git a/test/invariants/scenarios/FundingInvariant.t.sol b/test/invariants/scenarios/FundingInvariant.t.sol index 975ab292..3ddd9183 100644 --- a/test/invariants/scenarios/FundingInvariant.t.sol +++ b/test/invariants/scenarios/FundingInvariant.t.sol @@ -47,28 +47,18 @@ contract FundingInvariant is StandardTestBase { assertTrue(initialTopTenProposals.length > 0); } - function invariant_funding_stage() external { + function invariant_funding_stage() external useCurrentBlock { _invariant_FS1_FS2_FS3(_grantFund, _standardHandler); _invariant_FS4_FS5_FS6_FS7_FS8(_grantFund, _standardHandler); } - function invariant_call_summary() external view { + function invariant_call_summary() external useCurrentBlock { uint24 distributionId = _grantFund.getDistributionId(); - _standardHandler.logCallSummary(); - // _standardHandler.logProposalSummary(); - // _standardHandler.logActorSummary(distributionId, true, false); - _logFundingSummary(distributionId); - } - - function _logFundingSummary(uint24 distributionId_) internal view { - console.log("\nFunding Summary\n"); - console.log("------------------"); - console.log("number of funding stage starts: ", _standardHandler.numberOfCalls("SFH.FundingStage")); - console.log("number of funding stage success votes: ", _standardHandler.numberOfCalls("SFH.fundingVote.success")); - console.log("distributionId: ", distributionId_); - console.log("SFH.updateSlate.success: ", _standardHandler.numberOfCalls("SFH.updateSlate.success")); - console.log("------------------"); + _logger.logCallSummary(); + _logger.logProposalSummary(); + _logger.logActorSummary(distributionId, true, false); + _logger.logFundingSummary(distributionId); } } diff --git a/test/invariants/scenarios/MultipleDistributionInvariant.t.sol b/test/invariants/scenarios/MultipleDistributionInvariant.t.sol index 5f4bc120..8fcdb7cc 100644 --- a/test/invariants/scenarios/MultipleDistributionInvariant.t.sol +++ b/test/invariants/scenarios/MultipleDistributionInvariant.t.sol @@ -41,15 +41,9 @@ contract MultipleDistributionInvariant is StandardTestBase { currentBlock = block.number; } - function invariant_distribution_period() external { - _invariant_DP1_DP2_DP3_DP4_DP5(_grantFund, _standardHandler); - _invariant_DP6(_grantFund, _standardHandler); - _invariant_T1_T2(_grantFund, _standardHandler); - } - - function invariant_all() external { + function invariant_all() external useCurrentBlock { // screening invariants - _invariant_SS1_SS3_SS4_SS5_SS6_SS7_SS8_SS10_SS11_SS12(_grantFund, _standardHandler); + _invariant_SS1_SS3_SS4_SS5_SS6_SS7_SS8_SS10_SS11_P1_P2(_grantFund, _standardHandler); _invariant_SS2_SS4_SS9(_grantFund, _standardHandler); // funding invariants @@ -57,42 +51,32 @@ contract MultipleDistributionInvariant is StandardTestBase { _invariant_FS4_FS5_FS6_FS7_FS8(_grantFund, _standardHandler); // finalize invariants - _invariant_CS1_CS2_CS3_CS4_CS5_CS6(_grantFund, _standardHandler); + _invariant_CS1_CS2_CS3_CS4_CS5_CS6_CS7(_grantFund, _standardHandler); _invariant_ES1_ES2_ES3_ES4_ES5(_grantFund, _standardHandler); _invariant_DR1_DR2_DR3_DR4_DR5(_grantFund, _standardHandler); - } - function invariant_call_summary() external view { - // uint24 distributionId = _grantFund.getDistributionId(); + // distribution period invariants + _invariant_DP1_DP2_DP3_DP4_DP5(_grantFund, _standardHandler); + _invariant_DP6(_grantFund, _standardHandler); + _invariant_T1_T2(_grantFund); + } - _standardHandler.logCallSummary(); - _standardHandler.logTimeSummary(); + function invariant_call_summary() external useCurrentBlock { + uint24 distributionId = _grantFund.getDistributionId(); + _logger.logCallSummary(); + _logger.logTimeSummary(); + _logger.logProposalSummary(); console.log("scenario type", uint8(_standardHandler.getCurrentScenarioType())); - console.log("Delegation Rewards: ", _standardHandler.numberOfCalls('delegationRewardSet')); - console.log("Delegation Rewards Claimed: ", _standardHandler.numberOfCalls('SFH.claimDelegateReward.success')); - console.log("Proposal Execute attempt: ", _standardHandler.numberOfCalls('SFH.execute.attempt')); - console.log("Proposal Execute Count: ", _standardHandler.numberOfCalls('SFH.execute.success')); - console.log("Slate Update Prep: ", _standardHandler.numberOfCalls('SFH.updateSlate.prep')); - console.log("Slate Update length: ", _standardHandler.numberOfCalls('updateSlate.length')); - console.log("Slate Update Called: ", _standardHandler.numberOfCalls('SFH.updateSlate.called')); - console.log("Slate Update Success: ", _standardHandler.numberOfCalls('SFH.updateSlate.success')); - console.log("Slate Proposals: ", _standardHandler.numberOfCalls('proposalsInSlates')); - console.log("unused proposal: ", _standardHandler.numberOfCalls('unused.proposal')); - console.log("unexecuted proposal: ", _standardHandler.numberOfCalls('unexecuted.proposal')); - console.log("funding stage starts: ", _standardHandler.numberOfCalls("SFH.FundingStage")); - console.log("funding stage success votes ", _standardHandler.numberOfCalls("SFH.fundingVote.success")); - - - (, , , , uint256 fundingPowerCast, ) = _grantFund.getDistributionPeriodInfo(2); - console.log("Total Funding Power Cast ", fundingPowerCast); + while (distributionId > 0) { + _logger.logFundingSummary(distributionId); + _logger.logFinalizeSummary(distributionId); + _logger.logActorSummary(distributionId, true, true); + _logger.logActorDelegationRewards(distributionId); - if (_standardHandler.numberOfCalls('unexecuted.proposal') != 0) { - console.log("state of unexecuted: ", uint8(_grantFund.state(_standardHandler.numberOfCalls('unexecuted.proposal')))); + --distributionId; } - // _standardHandler.logProposalSummary(); - // _standardHandler.logActorSummary(distributionId, true, true); } } diff --git a/test/invariants/scenarios/ScreeningInvariant.t.sol b/test/invariants/scenarios/ScreeningInvariant.t.sol index 3a81c300..c7a100cf 100644 --- a/test/invariants/scenarios/ScreeningInvariant.t.sol +++ b/test/invariants/scenarios/ScreeningInvariant.t.sol @@ -30,17 +30,17 @@ contract ScreeningInvariant is StandardTestBase { } - function invariant_screening_stage() external { - _invariant_SS1_SS3_SS4_SS5_SS6_SS7_SS8_SS10_SS11_SS12(_grantFund, _standardHandler); + function invariant_screening_stage() external useCurrentBlock { + _invariant_SS1_SS3_SS4_SS5_SS6_SS7_SS8_SS10_SS11_P1_P2(_grantFund, _standardHandler); _invariant_SS2_SS4_SS9(_grantFund, _standardHandler); } - function invariant_call_summary() external view { + function invariant_call_summary() external useCurrentBlock { uint24 distributionId = _grantFund.getDistributionId(); - _standardHandler.logCallSummary(); - // _standardHandler.logProposalSummary(); - _standardHandler.logActorSummary(distributionId, false, true); + _logger.logCallSummary(); + _logger.logProposalSummary(); + _logger.logActorSummary(distributionId, false, true); } } diff --git a/test/invariants/test-invariant.sh b/test/invariants/test-invariant.sh new file mode 100755 index 00000000..d2d26935 --- /dev/null +++ b/test/invariants/test-invariant.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -ex + +echo "Exporting environment variables" + +# export environment variables retrieved from user input +export SCENARIO=${1:-${SCENARIO}} +export NUM_ACTORS=${2:-${NUM_ACTORS}} +export NUM_PROPOSALS=${3:-${NUM_PROPOSALS}} +export PER_ADDRESS_TOKEN_REQ_CAP=${4:-${PER_ADDRESS_TOKEN_REQ_CAP}} +export LOGS_VERBOSITY=${5:-${LOGS_VERBOSITY}} + +echo "Running invariant test" + +# run invariant test +forge t --mc $SCENARIO diff --git a/test/utils/GrantFundTestHelper.sol b/test/utils/GrantFundTestHelper.sol index bc4eb405..d3682cd2 100644 --- a/test/utils/GrantFundTestHelper.sol +++ b/test/utils/GrantFundTestHelper.sol @@ -67,6 +67,7 @@ abstract contract GrantFundTestHelper is Test { string description; uint256 totalTokensRequested; uint256 blockAtCreation; // block number of test proposal creation + uint256 blockAtExecution; // block number of proposal execution GeneratedTestProposalParams[] params; } @@ -235,7 +236,7 @@ abstract contract GrantFundTestHelper is Test { // return a TestProposal struct containing the state of a created proposal function _createTestProposal(uint24 distributionId_, uint256 proposalId_, address[] memory targets_, uint256[] memory values_, bytes[] memory calldatas_, string memory description) internal view returns (TestProposal memory proposal_) { (GeneratedTestProposalParams[] memory params, uint256 totalTokensRequested) = _getGeneratedTestProposalParamsFromParams(targets_, values_, calldatas_); - proposal_ = TestProposal(proposalId_, distributionId_, description, totalTokensRequested, block.number, params); + proposal_ = TestProposal(proposalId_, distributionId_, description, totalTokensRequested, block.number, 0, params); } /** From ce90939b20501ff55cdd6eb3041077f5435affcf Mon Sep 17 00:00:00 2001 From: Mike Hathaway Date: Thu, 22 Jun 2023 11:24:16 -0400 Subject: [PATCH 07/20] update makefile (#115) --- Makefile | 2 +- test/invariants/test-invariant.sh | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 04483d15..4cd653ae 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ tests :; forge clean && forge test --mt test --optimize --optimizer test-with-gas-report :; forge clean && forge build && forge test --mt test --optimize --optimizer-runs 1000000 -v --gas-report # --ffi # enable if you need the `ffi` cheat code on HEVM test-invariant :; ./test/invariants/test-invariant.sh ${SCENARIO} ${NUM_ACTORS} ${NUM_PROPOSALS} ${PER_ADDRESS_TOKEN_REQ_CAP} test-invariant-all :; forge clean && forge t --mt invariant -test-invariant-multiple-distribution :; forge clean && ./test/invariants/test-invariant.sh MultipleDistribution 25 200 +test-invariant-multiple-distribution :; forge clean && ./test/invariants/test-invariant.sh MultipleDistribution 2 25 200 coverage :; forge coverage # Generate Gas Snapshots diff --git a/test/invariants/test-invariant.sh b/test/invariants/test-invariant.sh index d2d26935..b2515ef8 100755 --- a/test/invariants/test-invariant.sh +++ b/test/invariants/test-invariant.sh @@ -5,10 +5,10 @@ echo "Exporting environment variables" # export environment variables retrieved from user input export SCENARIO=${1:-${SCENARIO}} -export NUM_ACTORS=${2:-${NUM_ACTORS}} -export NUM_PROPOSALS=${3:-${NUM_PROPOSALS}} -export PER_ADDRESS_TOKEN_REQ_CAP=${4:-${PER_ADDRESS_TOKEN_REQ_CAP}} -export LOGS_VERBOSITY=${5:-${LOGS_VERBOSITY}} +export LOGS_VERBOSITY=${2:-${LOGS_VERBOSITY}} +export NUM_ACTORS=${3:-${NUM_ACTORS}} +export NUM_PROPOSALS=${4:-${NUM_PROPOSALS}} +export PER_ADDRESS_TOKEN_REQ_CAP=${5:-${PER_ADDRESS_TOKEN_REQ_CAP}} echo "Running invariant test" From 23cdb60b230cd54099535307ee027ce988ba3216 Mon Sep 17 00:00:00 2001 From: Mike Hathaway Date: Tue, 27 Jun 2023 00:55:54 -0400 Subject: [PATCH 08/20] various invariant doc cleanups (#117) --- .env.example | 2 +- test/README.md | 2 +- test/invariants/INVARIANTS.md | 10 ++++------ 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/.env.example b/.env.example index c7e21dd9..82a3be48 100644 --- a/.env.example +++ b/.env.example @@ -4,4 +4,4 @@ FOUNDRY_EVM_VERSION=paris SCENARIO=MultipleDistribution # Type of Invariant Scenario to run: Screening | FundingInvariant | Finalize | MultipleDistribution NUM_ACTORS=10 # Number of actors to simulate in invariants NUM_PROPOSALS=10 # Maximum number of proposals to simulate in invariants -PER_ADDRESS_TOKEN_REQ_CAP=.1 # Percentage of funds available to request per proposal recipient in invariants +PER_ADDRESS_TOKEN_REQ_CAP=10 # Percentage of funds available to request per proposal recipient in invariants diff --git a/test/README.md b/test/README.md index ea914889..64cd25a2 100644 --- a/test/README.md +++ b/test/README.md @@ -16,7 +16,7 @@ Invariant test scenarios can be externally configured by customizing following e | Variable | Default | Description | | ------------- | ------------- | ------------- | | SCENARIO | MultipleDistribution | Type of Invariant Scenario to run: Screening, FundingInvariant, Finalize, MultipleDistribution | -| PER_ADDRESS_TOKEN_REQ_CAP | .1 | Percentage of funds available to request per proposal recipient in invariants | +| PER_ADDRESS_TOKEN_REQ_CAP | 10 | Percentage of funds available to request per proposal recipient in invariants | | NUM_ACTORS | 20 | Max number of actors to participate in invariant testing | | NUM_PROPOSALS | 200 | Max number of proposals that can be proposed in invariant testing | | LOGS_VERBOSITY | 0 |

Details to log

0 = No Logs

1 = Calls details, Proposal details, Time details

2 = Calls details, Proposal details, Time details, Funding proposal details, Finalize proposals details

3 = Calls details, Proposal details, Time details, Funding proposal details, Finalize proposals details, Actor details diff --git a/test/invariants/INVARIANTS.md b/test/invariants/INVARIANTS.md index c28f2519..85eb2ad3 100644 --- a/test/invariants/INVARIANTS.md +++ b/test/invariants/INVARIANTS.md @@ -1,11 +1,5 @@ # GrantFund Invariants -## Treasury Invariants: - - **T1**: The Grant Fund's `treasury` should always be less than or equal to the contract's token balance. - - **T2**: The Grant Fund's `treasury` should always be less than or equal to the Ajna token total supply. - -## Standard Funding Mechanism Invariants - - #### Distribution Period: - **DP1**: Only one distribution period should be active at a time. Each successive distribution period's start block should be greater than the previous periods end block. - **DP2**: Each winning proposal successfully claims no more that what was finalized in the challenge stage @@ -64,3 +58,7 @@ - #### Proposal: - **P1**: A proposal should never enter an unused state (pending, canceled, queued, expired). - **P2**: A proposal's proposalId must be unique. + +- #### Treasury: + - **T1**: The Grant Fund's `treasury` should always be less than or equal to the contract's token balance. + - **T2**: The Grant Fund's `treasury` should always be less than or equal to the Ajna token total supply. From e6180395edab8f18eb7e84d47c1072b86500b3d7 Mon Sep 17 00:00:00 2001 From: Prateek Gupta Date: Wed, 28 Jun 2023 00:46:13 +0530 Subject: [PATCH 09/20] Add fuzz test for screening and funding stage (#116) --- test/unit/StandardFunding.t.sol | 199 ++++++++++++++++++++++++++++++++ 1 file changed, 199 insertions(+) diff --git a/test/unit/StandardFunding.t.sol b/test/unit/StandardFunding.t.sol index dc358c22..c4d6b0b1 100644 --- a/test/unit/StandardFunding.t.sol +++ b/test/unit/StandardFunding.t.sol @@ -2134,6 +2134,205 @@ contract StandardFundingGrantFundTest is GrantFundTestHelper { assertGe(gbc / 10, totalDelegationReward); } + function testFuzzScreeningStage(uint256 noOfVoters_, uint256 noOfProposals_, uint256 noOfScreeningVoteCast_) external { + + /******************************/ + /*** Screening stage fuzz ***/ + /******************************/ + + noOfVoters_ = bound(noOfVoters_, 1, 500); + noOfProposals_ = bound(noOfProposals_, 1, 50); + noOfScreeningVoteCast_ = bound(noOfScreeningVoteCast_, 200, 500); + + vm.roll(_startBlock + 20); + + // Initialize N voter addresses + address[] memory voters = _getVoters(noOfVoters_); + assertEq(voters.length, noOfVoters_); + + // Transfer random ajna tokens to all voters and self delegate + uint256[] memory votes = _setVotingPower(noOfVoters_, voters, _token, _tokenDeployer); + assertEq(votes.length, noOfVoters_); + + vm.roll(block.number + 100); + + _startDistributionPeriod(_grantFund); + + vm.roll(block.number + 100); + + // ensure user gets the vote for screening stage + for(uint i = 0; i < noOfVoters_; i++) { + assertEq(votes[i], _getScreeningVotes(_grantFund, voters[i])); + } + + uint24 distributionId = _grantFund.getDistributionId(); + + // submit N proposals + TestProposal[] memory proposals = _getProposals(noOfProposals_, _grantFund, _tokenHolder1, _token); + + // Random voter votes on a random proposal from all Proposals for random number of times + for(uint i = 0; i < noOfScreeningVoteCast_; i++) { + + // get random voter + uint256 randomVoterIndex = bound(i, 0, noOfVoters_ - 1); + address randomVoter = voters[randomVoterIndex]; + + // get random proposal to vote on + uint256 randomProposalIndex = _getRandomProposal(noOfProposals_); + uint256 randomProposalId = proposals[randomProposalIndex].proposalId; + + uint256 previousVoteCast = _grantFund.getScreeningVotesCast(distributionId, randomVoter); + + uint256 totalVoteAvailable = _getScreeningVotes(_grantFund, randomVoter) - previousVoteCast; + + // get random vote to cast based on available voting power + uint256 voteToCast = bound(totalVoteAvailable, 0, totalVoteAvailable); + + noOfVotesOnProposal[randomProposalId] += voteToCast; + + (,, uint256 beforeVoteReceived,,,) = _grantFund.getProposalInfo(randomProposalId); + + // vote on proposal if voteToCast is more than 0 + if (voteToCast > 0) _screeningVote(_grantFund, randomVoter, randomProposalId, voteToCast); + + // ensure that votes are added into screeningVotesCast accumulator + assertEq(_grantFund.getScreeningVotesCast(distributionId, randomVoter), previousVoteCast + voteToCast); + + (,, uint256 afterVoteReceived,,,) = _grantFund.getProposalInfo(randomProposalId); + + // ensure that votes are added into votesReceived accumulator + assertEq(afterVoteReceived, beforeVoteReceived + voteToCast); + } + + // calculate top 10 proposals based on total vote casted on each proposal + for(uint i = 0; i < noOfProposals_; i++) { + uint256 currentProposalId = proposals[i].proposalId; + uint256 votesOnCurrentProposal = noOfVotesOnProposal[currentProposalId]; + uint256 lengthOfArray = topTenProposalIds.length; + + // only add proposals having atleast a vote + if (votesOnCurrentProposal > 0) { + + // if there are less than 10 proposals in topTenProposalIds , add current proposals and sort topTenProposalIds based on Votes + if (lengthOfArray < 10) { + topTenProposalIds.push(currentProposalId); + + // ensure if there are more than 1 proposalId in topTenProposalIds to sort + if(topTenProposalIds.length > 1) { + _insertionSortProposalsByVotes(topTenProposalIds); + } + } + + // if there are 10 proposals in topTenProposalIds, check new proposal has more votes than the last proposal in topTenProposalIds + else if(noOfVotesOnProposal[topTenProposalIds[lengthOfArray - 1]] < votesOnCurrentProposal) { + + // remove last proposal with least no of vote in topTenProposalIds + topTenProposalIds.pop(); + + // add new proposal with more votes than last + topTenProposalIds.push(currentProposalId); + + // sort topTenProposalIds + _insertionSortProposalsByVotes(topTenProposalIds); + } + } + } + + // get top ten proposals from contract + uint256[] memory topTenProposalIdsFromContract = _grantFund.getTopTenProposals(distributionId); + + // ensure the no of proposals are correct + assertEq(topTenProposalIds.length, topTenProposalIdsFromContract.length); + + for (uint i = 0; i < topTenProposalIds.length; i++) { + // ensure that each proposal in topTenProposalIdsFromContract is correct + assertEq(topTenProposalIds[i], topTenProposalIdsFromContract[i]); + } + + } + + function testFuzzFundingStage(uint256 noOfVoters_, uint256 noOfProposals_, uint256 noOfFundingVoteCast_) external { + + noOfVoters_ = bound(noOfVoters_, 1, 500); + noOfProposals_ = bound(noOfProposals_, 1, 50); + noOfFundingVoteCast_ = bound(noOfFundingVoteCast_, 200, 500); + + vm.roll(_startBlock + 20); + + // Initialize N voter addresses + address[] memory voters = _getVoters(noOfVoters_); + + // Transfer random ajna tokens to all voters and self delegate + _setVotingPower(noOfVoters_, voters, _token, _tokenDeployer); + + vm.roll(block.number + 100); + + _startDistributionPeriod(_grantFund); + + vm.roll(block.number + 100); + + uint24 distributionId = _grantFund.getDistributionId(); + + // submit N proposals + TestProposal[] memory proposals = _getProposals(noOfProposals_, _grantFund, _tokenHolder1, _token); + + // Each voter votes on a random proposal from all Proposals + for(uint i = 0; i < noOfVoters_; i++) { + uint256 randomProposalIndex = _getRandomProposal(noOfProposals_); + + uint256 randomProposalId = proposals[randomProposalIndex].proposalId; + + _screeningVote(_grantFund, voters[i], randomProposalId, _getScreeningVotes(_grantFund, voters[i])); + } + + /******************************/ + /*** Funding stage fuzz ***/ + /******************************/ + + // skip to funding stage + vm.roll(block.number + 550_000); + + // get top ten proposals from contract + topTenProposalIds = _grantFund.getTopTenProposals(distributionId); + + // random voter votes random number of votes on random proposal + for(uint i = 0; i < noOfFundingVoteCast_; i++) { + + // get random voter + uint256 randomVoterIndex = bound(i, 0, noOfVoters_ - 1); + address randomVoter = voters[randomVoterIndex]; + + // get random proposal to vote on + uint256 randomProposalIndex = _getRandomProposal(topTenProposalIds.length); + uint256 randomProposalId = topTenProposalIds[randomProposalIndex]; + + (, uint256 beforeRemainingVotingPower,) = _grantFund.getVoterInfo(distributionId, randomVoter); + + // get random vote to cast based on available voting power + uint256 voteToCast = bound(beforeRemainingVotingPower, 0, beforeRemainingVotingPower); + + // get random support + uint8 support = (voteToCast % 2 == 0) ? voteYes: voteNo; + + (,,,, int256 beforeVoteReceived,) = _grantFund.getProposalInfo(randomProposalId); + + // vote on proposal if voteToCast is more than 0 + if (voteToCast != 0) _fundingVote(_grantFund, randomVoter, randomProposalId, support, int256(voteToCast)); + + (, uint256 afterRemainingVotingPower,) = _grantFund.getVoterInfo(distributionId, randomVoter); + + // ensure that voter voting power decreased + assertEq(afterRemainingVotingPower, beforeRemainingVotingPower - voteToCast); + + (,,,, int256 afterVoteReceived,) = _grantFund.getProposalInfo(randomProposalId); + + int256 expectedVoteReceived = (support == 1 ? beforeVoteReceived + int256(voteToCast) : beforeVoteReceived - int256(voteToCast)); + + // ensure quadratic vote received on proposal is updated + assertEq(afterVoteReceived, expectedVoteReceived); + } + } + // helper method that sort proposals based on votes on them function _insertionSortProposalsByVotes(uint256[] storage arr) internal { for (uint i = 1; i < arr.length; i++) { From 76ce84ef605421a1bee7cc2c026455264f7018bb Mon Sep 17 00:00:00 2001 From: Mike Hathaway Date: Tue, 27 Jun 2023 16:24:26 -0400 Subject: [PATCH 10/20] expand CS7 invariant check (#118) * expand CS7 invariant check * fix nit --------- Co-authored-by: Mike --- test/invariants/base/FinalizeInvariants.sol | 12 ++++++++++++ test/invariants/handlers/StandardHandler.sol | 8 ++++++++ 2 files changed, 20 insertions(+) diff --git a/test/invariants/base/FinalizeInvariants.sol b/test/invariants/base/FinalizeInvariants.sol index ef16e536..05da2894 100644 --- a/test/invariants/base/FinalizeInvariants.sol +++ b/test/invariants/base/FinalizeInvariants.sol @@ -45,10 +45,12 @@ abstract contract FinalizeInvariants is TestBase { // check proposal state of the constituents of the top slate uint256 totalTokensRequested = 0; + int256 topSlateTotalVotesReceived = 0; for (uint256 i = 0; i < topSlateProposalIds.length; ++i) { uint256 proposalId = topSlateProposalIds[i]; (, , , uint128 tokensRequested, int128 fundingVotesReceived, ) = grantFund_.getProposalInfo(proposalId); totalTokensRequested += tokensRequested; + topSlateTotalVotesReceived += fundingVotesReceived; require( fundingVotesReceived >= 0, @@ -80,6 +82,16 @@ abstract contract FinalizeInvariants is TestBase { slate.updateBlock <= endBlock && slate.updateBlock >= grantFund_.getChallengeStageStartBlock(endBlock), "invariant CS6: Funded proposal slate's can only be updated during a distribution period's challenge stage" ); + + if (slate.slateHash != topSlateHash) { + // ensure slates that aren't the top slates have total votes less than the top slate's votes + int256 sumSlateFundingVotes = standardHandler_.sumSlateFundingVotes(slate.slateHash); + + require( + sumSlateFundingVotes <= topSlateTotalVotesReceived, + "invariant CS7: The highest submitted funded proposal slate should have won or tied depending on when it was submitted." + ); + } } require( diff --git a/test/invariants/handlers/StandardHandler.sol b/test/invariants/handlers/StandardHandler.sol index 1fb1caf5..c95547a4 100644 --- a/test/invariants/handlers/StandardHandler.sol +++ b/test/invariants/handlers/StandardHandler.sol @@ -824,6 +824,14 @@ contract StandardHandler is Handler { } } + function sumSlateFundingVotes(bytes32 slateHash_) public view returns (int256 sum_) { + uint256[] memory fundedProposals = _grantFund.getFundedProposalSlate(slateHash_); + for (uint256 i = 0; i < fundedProposals.length; ++i) { + (, , , , int256 fundingVotesReceived, ) = _grantFund.getProposalInfo(fundedProposals[i]); + sum_ += fundingVotesReceived; + } + } + function countNegativeFundingVotes(IGrantFundState.FundingVoteParams[] memory fundingVotes_) public pure returns (uint256 count_) { for (uint256 i = 0; i < fundingVotes_.length; ++i) { if (fundingVotes_[i].votesUsed < 0) { From 1446c5bd985b4b5259aef9d3ab9795ab7e5af455 Mon Sep 17 00:00:00 2001 From: Ed Noepel Date: Tue, 4 Jul 2023 12:08:57 -0400 Subject: [PATCH 11/20] adjusted README --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index debc61fe..b8b1d0b4 100644 --- a/README.md +++ b/README.md @@ -71,8 +71,6 @@ Record the address of the token upon deployment. See [AJNA_TOKEN.md](src/token/ #### Grant Fund Deployment of the Grant Coordination Fund requires an argument to specify the address of the AJNA token. The deployment script also uses the token address to determine funding level. -Before deploying, edit `src/grants/base/Storage.sol` to set the correct AJNA token address for the target chain. - To deploy, run: ``` make deploy-grantfund ajna= From 545ab189fd74acdd704b60db23be87e0ec587f16 Mon Sep 17 00:00:00 2001 From: prateek105 Date: Wed, 5 Jul 2023 10:13:45 +0530 Subject: [PATCH 12/20] Fix make file spacing --- Makefile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 4cd653ae..dd21549d 100644 --- a/Makefile +++ b/Makefile @@ -15,12 +15,12 @@ install :; git submodule update --init --recursive build :; forge clean && forge build --optimize --optimizer-runs 1000000 # Tests -tests :; forge clean && forge test --mt test --optimize --optimizer-runs 1000000 -v # --ffi # enable if you need the `ffi` cheat code on HEVM -test-with-gas-report :; forge clean && forge build && forge test --mt test --optimize --optimizer-runs 1000000 -v --gas-report # --ffi # enable if you need the `ffi` cheat code on HEVM +tests :; forge clean && forge test --mt test --optimize --optimizer-runs 1000000 -v # --ffi # enable if you need the `ffi` cheat code on HEVM +test-with-gas-report :; forge clean && forge build && forge test --mt test --optimize --optimizer-runs 1000000 -v --gas-report # --ffi # enable if you need the `ffi` cheat code on HEVM test-invariant :; ./test/invariants/test-invariant.sh ${SCENARIO} ${NUM_ACTORS} ${NUM_PROPOSALS} ${PER_ADDRESS_TOKEN_REQ_CAP} -test-invariant-all :; forge clean && forge t --mt invariant +test-invariant-all :; forge clean && forge t --mt invariant test-invariant-multiple-distribution :; forge clean && ./test/invariants/test-invariant.sh MultipleDistribution 2 25 200 -coverage :; forge coverage +coverage :; forge coverage # Generate Gas Snapshots snapshot :; forge clean && forge snapshot --optimize --optimize-runs 1000000 From 7e546fff90a6820b3337be4b28cac8389ebb1a4f Mon Sep 17 00:00:00 2001 From: Ed Noepel Date: Tue, 1 Aug 2023 14:25:43 -0400 Subject: [PATCH 13/20] deploy BurnWrapper --- Makefile | 4 ++++ README.md | 6 ++++++ script/BurnWrapper.s.sol | 20 ++++++++++++++++++++ 3 files changed, 30 insertions(+) create mode 100644 script/BurnWrapper.s.sol diff --git a/Makefile b/Makefile index dd21549d..7465d9ec 100644 --- a/Makefile +++ b/Makefile @@ -34,3 +34,7 @@ deploy-grantfund: eval AJNA_TOKEN=${ajna} forge script script/GrantFund.s.sol:DeployGrantFund \ --rpc-url ${ETH_RPC_URL} --sender ${DEPLOY_ADDRESS} --keystore ${DEPLOY_KEY} --broadcast -vvv --verify +deploy-burnwrapper: + eval AJNA_TOKEN=${ajna} + forge script script/BurnWrapper.s.sol:DeployBurnWrapper \ + --rpc-url ${ETH_RPC_URL} --sender ${DEPLOY_ADDRESS} --keystore ${DEPLOY_KEY} --broadcast -vvv --verify \ No newline at end of file diff --git a/README.md b/README.md index b8b1d0b4..d5232f37 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,12 @@ make deploy-ajnatoken mintto= ``` Record the address of the token upon deployment. See [AJNA_TOKEN.md](src/token/AJNA_TOKEN.md#deployment) for validation. +#### Burn Wrapper +To deploy, ensure the correct AJNA token address is specified in code. Then, run: +``` +make deploy-burnwrapper ajna= +``` + #### Grant Fund Deployment of the Grant Coordination Fund requires an argument to specify the address of the AJNA token. The deployment script also uses the token address to determine funding level. diff --git a/script/BurnWrapper.s.sol b/script/BurnWrapper.s.sol new file mode 100644 index 00000000..d0441de0 --- /dev/null +++ b/script/BurnWrapper.s.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.7; + +import { Script } from "forge-std/Script.sol"; +import { console } from "forge-std/console.sol"; + +import { BurnWrappedAjna } from "../src/token/BurnWrapper.sol"; +import { IERC20 } from "@oz/token/ERC20/IERC20.sol"; + +contract DeployBurnWrapper is Script { + function run() public { + IERC20 ajna = IERC20(vm.envAddress("AJNA_TOKEN")); + + vm.startBroadcast(); + address wrapperAddress = address(new BurnWrappedAjna(ajna)); + vm.stopBroadcast(); + + console.log("Created BurnWrapper at %s for AJNA token at %s", wrapperAddress, address(ajna)); + } +} From 82bb87c5c91349fb40c80b720c590c8ae8e9012b Mon Sep 17 00:00:00 2001 From: prateek105 Date: Fri, 4 Aug 2023 14:00:17 +0530 Subject: [PATCH 14/20] Add deployer contract to deploy grantfund, fund treasury and startNewDistribution --- src/grants/Deployer.sol | 26 ++++++++++++++++++++++++++ test/unit/Deployer.t.sol | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 src/grants/Deployer.sol create mode 100644 test/unit/Deployer.t.sol diff --git a/src/grants/Deployer.sol b/src/grants/Deployer.sol new file mode 100644 index 00000000..891c06f1 --- /dev/null +++ b/src/grants/Deployer.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.18; + +import { IERC20 } from "@oz/token/ERC20/IERC20.sol"; + +import { GrantFund } from "./GrantFund.sol"; + +contract Deployer { + + GrantFund public grantFund; + + function deployGrantFund(address ajnaToken_, uint256 treasury_) public returns (GrantFund) { + + IERC20(ajnaToken_).transferFrom(msg.sender, address(this), treasury_); + + grantFund = new GrantFund(ajnaToken_); + + IERC20(ajnaToken_).approve(address(grantFund), treasury_); + + grantFund.fundTreasury(treasury_); + + grantFund.startNewDistributionPeriod(); + return grantFund; + } +} \ No newline at end of file diff --git a/test/unit/Deployer.t.sol b/test/unit/Deployer.t.sol new file mode 100644 index 00000000..346e70c8 --- /dev/null +++ b/test/unit/Deployer.t.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import { Test } from "@std/Test.sol"; + +import { Deployer } from "../../src/grants/Deployer.sol"; +import { GrantFund } from "../../src/grants/GrantFund.sol"; +import { TestAjnaToken } from "../utils/harness/TestAjnaToken.sol"; + +contract DeployerTest is Test { + + function testGrantFundDeployment() external { + address owner = makeAddr("owner"); + vm.startPrank(owner); + + uint256 treasury = 50_000_000 * 1e18; + + TestAjnaToken ajnaToken = new TestAjnaToken(); + ajnaToken.mint(owner, treasury); + + Deployer deployer = new Deployer(); + ajnaToken.approve(address(deployer), treasury); + + GrantFund grantFund = deployer.deployGrantFund(address(ajnaToken), treasury); + + assertEq(grantFund.getDistributionId(), 1); + + (,,,uint256 fundAvailable,,) = grantFund.getDistributionPeriodInfo(1); + + assertEq(grantFund.treasury(), treasury - fundAvailable); + } +} \ No newline at end of file From 9732ea2db5d52b8cddc6e3e5448ad2a04ead4d51 Mon Sep 17 00:00:00 2001 From: prateek105 Date: Mon, 7 Aug 2023 18:59:21 +0530 Subject: [PATCH 15/20] Add readme for deployer contract --- Makefile | 2 ++ README.md | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/Makefile b/Makefile index dd21549d..f43697c9 100644 --- a/Makefile +++ b/Makefile @@ -34,3 +34,5 @@ deploy-grantfund: eval AJNA_TOKEN=${ajna} forge script script/GrantFund.s.sol:DeployGrantFund \ --rpc-url ${ETH_RPC_URL} --sender ${DEPLOY_ADDRESS} --keystore ${DEPLOY_KEY} --broadcast -vvv --verify +deploy-grantfund-deployer: + forge create --rpc-url ${ETH_RPC_URL} --sender ${DEPLOY_ADDRESS} --keystore ${DEPLOY_KEY} --verify src/grants/Deployer.sol:Deployer diff --git a/README.md b/README.md index b8b1d0b4..94c84c98 100644 --- a/README.md +++ b/README.md @@ -77,3 +77,17 @@ make deploy-grantfund ajna= ``` See [GRANT_FUND.md](src/grants/GRANT_FUND.md#deployment) for next steps. + +#### Grant Fund deployer +Deployer contract can be used to deploy grant fund, fund treasury and start distribution in a single call to avoid someone starting a distribution without treasury. + +Steps to use Deployer contract to deploy grant Fund: +1. Deploy `Deployer` contract. +2. Approve `` `AJNA ` to `Deployer` contract from `treasury` address. +3. Call `deployGrantFund(address ajnaToken_, uint256 treasury_)` from `treasury` address to deploy grant fund and start distribution with treasury amount. +4. GrantFund can be verified using remix contract verification plugin, foundry or hardhat. + +To deploy Deployer contract, run: +``` +make deploy-grantfund-deployer +``` From d50335b997d90ad87f8b49378463e319d724628d Mon Sep 17 00:00:00 2001 From: prateek105 Date: Wed, 9 Aug 2023 19:34:15 +0530 Subject: [PATCH 16/20] PR feedback --- src/grants/Deployer.sol | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/grants/Deployer.sol b/src/grants/Deployer.sol index 891c06f1..796c0116 100644 --- a/src/grants/Deployer.sol +++ b/src/grants/Deployer.sol @@ -8,19 +8,33 @@ import { GrantFund } from "./GrantFund.sol"; contract Deployer { + error IncorrectTreasuryBalance(); + + error DistributionNotStarted(); + GrantFund public grantFund; - function deployGrantFund(address ajnaToken_, uint256 treasury_) public returns (GrantFund) { + function deployGrantFund(address ajnaToken_, uint256 treasury_) public returns (GrantFund grantFund_) { + + // deploy grant Fund + grantFund_ = new GrantFund(ajnaToken_); + + // Approve ajna token to fund treasury + IERC20(ajnaToken_).approve(address(grantFund_), treasury_); + // Transfer treasury ajna tokens to Deployer contract IERC20(ajnaToken_).transferFrom(msg.sender, address(this), treasury_); - grantFund = new GrantFund(ajnaToken_); + // Fund treasury and start new distribution + grantFund_.fundTreasury(treasury_); + grantFund_.startNewDistributionPeriod(); - IERC20(ajnaToken_).approve(address(grantFund), treasury_); + // check treasury balance is correct + if(IERC20(ajnaToken_).balanceOf(address(grantFund_)) != treasury_) revert IncorrectTreasuryBalance(); - grantFund.fundTreasury(treasury_); + // check new distribution started + if(grantFund_.getDistributionId() != 1) revert DistributionNotStarted(); - grantFund.startNewDistributionPeriod(); - return grantFund; + grantFund = grantFund_; } } \ No newline at end of file From 7d49734f42369dbb8e2c144ad0893396bd95542e Mon Sep 17 00:00:00 2001 From: Prateek Gupta Date: Wed, 16 Aug 2023 23:27:16 +0530 Subject: [PATCH 17/20] Update unit test to improve test coverage (#126) --- test/unit/StandardFunding.t.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/unit/StandardFunding.t.sol b/test/unit/StandardFunding.t.sol index afbc1e10..bf3478ca 100644 --- a/test/unit/StandardFunding.t.sol +++ b/test/unit/StandardFunding.t.sol @@ -905,6 +905,10 @@ contract StandardFundingGrantFundTest is GrantFundTestHelper { _screeningVote(_grantFund, _tokenHolder3, testProposals[12].proposalId, 5_000 * 1e18); _screeningVote(_grantFund, _tokenHolder5, testProposals[12].proposalId, 50_000 * 1e18); assertEq(_findProposalIndex(testProposals[12].proposalId, _grantFund.getTopTenProposals(distributionId)), -1); + + // cast screening votes on a proposal not in the top 10 to make it top 10 proposal + _screeningVote(_grantFund, _tokenHolder7, testProposals[12].proposalId, 2_500_000 * 1e18); + assertEq(_findProposalIndex(testProposals[12].proposalId, _grantFund.getTopTenProposals(distributionId)), 2); } function testStartNewDistributionPeriod() external { From 42bb2cacf317e7c8737f605ca73a89c56ea94afa Mon Sep 17 00:00:00 2001 From: Prateek Gupta Date: Wed, 16 Aug 2023 23:28:01 +0530 Subject: [PATCH 18/20] Add unit test for cycling voting delegation (#128) --- test/unit/AjnaToken.t.sol | 48 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/test/unit/AjnaToken.t.sol b/test/unit/AjnaToken.t.sol index 096aee31..68b500e1 100644 --- a/test/unit/AjnaToken.t.sol +++ b/test/unit/AjnaToken.t.sol @@ -313,4 +313,52 @@ contract AjnaTokenTest is Test { assertEq(_token.getVotes(address(3333)), 0); assertEq(_token.getVotes(address(4444)), 2_000_000_000 * 1e18); } + + function testCyclingVotingDelegation() external { + // define actors and set their balances + address actor1 = makeAddr("actor1"); + deal(address(_token), actor1, 1_000 * 1e18); + + address actor2 = makeAddr("actor2"); + deal(address(_token), actor2, 2_000 * 1e18); + + address actor3 = makeAddr("actor3"); + deal(address(_token), actor3, 5_000 * 1e18); + + // actor1 delegates votes to actor2 + changePrank(actor1); + _token.delegate(actor2); + + // ensure actor2 has votes equals to actor1 balance + assertEq(_token.getVotes(actor1), 0); + assertEq(_token.getVotes(actor2), 1_000 * 1e18); + assertEq(_token.getVotes(actor3), 0); + + // actor2 delegates votes to actor3 + changePrank(actor2); + _token.delegate(actor3); + + // ensure actor3 has votes equals to actor2 balance + assertEq(_token.getVotes(actor1), 0); + assertEq(_token.getVotes(actor2), 1_000 * 1e18); + assertEq(_token.getVotes(actor3), 2_000 * 1e18); + + // actor3 delegates votes to actor1 + changePrank(actor3); + _token.delegate(actor1); + + // ensure actor1 has votes equals to actor3 balance + assertEq(_token.getVotes(actor1), 5_000 * 1e18); + assertEq(_token.getVotes(actor2), 1_000 * 1e18); + assertEq(_token.getVotes(actor3), 2_000 * 1e18); + + // actor3 delegates votes to actor2 + changePrank(actor3); + _token.delegate(actor2); + + // ensure actor2 has votes equals to sum of actor3 and actor1 balance + assertEq(_token.getVotes(actor1), 0); + assertEq(_token.getVotes(actor2), 6_000 * 1e18); + assertEq(_token.getVotes(actor3), 2_000 * 1e18); + } } From 86bd09c855e1c039bfc460b5c929d71d211cb5b5 Mon Sep 17 00:00:00 2001 From: Prateek Gupta Date: Wed, 16 Aug 2023 23:35:39 +0530 Subject: [PATCH 19/20] Add invariant scenario for multiple fund treasury (#129) --- test/invariants/handlers/Handler.sol | 9 +- test/invariants/handlers/StandardHandler.sol | 49 +++++++++++ .../MultipleTreasuryFundingInvariant.t.sol | 85 +++++++++++++++++++ 3 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 test/invariants/scenarios/MultipleTreasuryFundingInvariant.t.sol diff --git a/test/invariants/handlers/Handler.sol b/test/invariants/handlers/Handler.sol index 19efd4e3..37e9aeed 100644 --- a/test/invariants/handlers/Handler.sol +++ b/test/invariants/handlers/Handler.sol @@ -70,7 +70,10 @@ contract Handler is Test, GrantFundTestHelper { _tokenDeployer = tokenDeployer_; // instantiate actors - actors = _buildActors(numOfActors_, tokensToDistribute_); + address[] memory newActors = _buildActors(numOfActors_, tokensToDistribute_); + for (uint256 i = 0; i < newActors.length; ++i) { + if (newActors[i] != address(0)) actors.push(newActors[i]); + } // set Test invariant contract testContract = ITestBase(testContract_); @@ -141,9 +144,11 @@ contract Handler is Test, GrantFundTestHelper { actors_ = new address[](numOfActors_); uint256 tokensDistributed = 0; + uint256 existingActors = actors.length; + for (uint256 i = 0; i < numOfActors_; ++i) { // create actor - address actor = makeAddr(string(abi.encodePacked("Actor", Strings.toString(i)))); + address actor = makeAddr(string(abi.encodePacked("Actor", Strings.toString(existingActors + i)))); actors_[i] = actor; // transfer ajna tokens to the actor diff --git a/test/invariants/handlers/StandardHandler.sol b/test/invariants/handlers/StandardHandler.sol index c95547a4..d569fb6f 100644 --- a/test/invariants/handlers/StandardHandler.sol +++ b/test/invariants/handlers/StandardHandler.sol @@ -388,6 +388,55 @@ contract StandardHandler is Handler { } } + function fundTreasury(uint256 actorIndex_, uint256 treasuryAmount_) external useCurrentBlock useRandomActor(actorIndex_) { + numberOfCalls['SFH.fundTreasury']++; + + // bound treasury amount + treasuryAmount_ = bound(treasuryAmount_, 0, _ajna.balanceOf(_actor)); + + if (treasuryAmount_ == 0) return; + + uint256 previousTreasury = _grantFund.treasury(); + + // fund treasury + changePrank(_actor); + _ajna.approve(address(_grantFund), type(uint256).max); + _grantFund.fundTreasury(treasuryAmount_); + + // ensure amount is added into treasury + assertEq(_grantFund.treasury(), previousTreasury + treasuryAmount_); + } + + function transferAjna(uint256 fromActorIndex_, uint256 toActorIndex_, uint256 amountToTransfer_) external useCurrentBlock useRandomActor(fromActorIndex_) { + numberOfCalls['SFH.transferAjna']++; + + // bound actor + toActorIndex_ = bound(toActorIndex_, 0, actors.length - 1); + address toActor = actors[toActorIndex_]; + + amountToTransfer_ = bound(amountToTransfer_, 0, _ajna.balanceOf(_actor)); + + if (amountToTransfer_ == 0 || _actor == toActor) return; + + _ajna.transfer(toActor, amountToTransfer_); + } + + function addActors(uint256 noOfActorsToAdd_, uint256 tokensToDistribute_) external useCurrentBlock { + numberOfCalls['SFH.addActors']++; + + // bound tokens to distribute and no of actors to add + noOfActorsToAdd_ = bound(noOfActorsToAdd_, 1, 10); + tokensToDistribute_ = bound(tokensToDistribute_, 0, _ajna.balanceOf(_tokenDeployer)); + + if (tokensToDistribute_ == 0) return; + + address[] memory newActors = _buildActors(noOfActorsToAdd_, tokensToDistribute_); + + // add new actors to actors array + for (uint256 i = 0; i < newActors.length; ++i) { + if (newActors[i] != address(0)) actors.push(newActors[i]); + } + } /**********************************/ /*** External Utility Functions ***/ /**********************************/ diff --git a/test/invariants/scenarios/MultipleTreasuryFundingInvariant.t.sol b/test/invariants/scenarios/MultipleTreasuryFundingInvariant.t.sol new file mode 100644 index 00000000..af114b4a --- /dev/null +++ b/test/invariants/scenarios/MultipleTreasuryFundingInvariant.t.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.18; + +import { console } from "@std/console.sol"; +import { SafeCast } from "@oz/utils/math/SafeCast.sol"; + +import { Maths } from "../../../src/grants/libraries/Maths.sol"; + +import { StandardTestBase } from "../base/StandardTestBase.sol"; +import { StandardHandler } from "../handlers/StandardHandler.sol"; +import { Handler } from "../handlers/Handler.sol"; + +contract MultipleTreasuryFundingInvariant is StandardTestBase { + + // run tests against all functions, having just started a distribution period + function setUp() public virtual override { + super.setUp(); + + // set the list of function selectors to run + bytes4[] memory selectors = new bytes4[](11); + selectors[0] = _standardHandler.startNewDistributionPeriod.selector; + selectors[1] = _standardHandler.propose.selector; + selectors[2] = _standardHandler.screeningVote.selector; + selectors[3] = _standardHandler.fundingVote.selector; + selectors[4] = _standardHandler.updateSlate.selector; + selectors[5] = _standardHandler.execute.selector; + selectors[6] = _standardHandler.claimDelegateReward.selector; + selectors[7] = _standardHandler.roll.selector; + selectors[8] = _standardHandler.fundTreasury.selector; + selectors[9] = _standardHandler.transferAjna.selector; + selectors[10] = _standardHandler.addActors.selector; + + // ensure utility functions are excluded from the invariant runs + targetSelector(FuzzSelector({ + addr: address(_standardHandler), + selectors: selectors + })); + + // update scenarioType to fast to have larger rolls + _standardHandler.setCurrentScenarioType(Handler.ScenarioType.Fast); + + vm.roll(block.number + 100); + currentBlock = block.number; + } + + function invariant_all() external useCurrentBlock { + // screening invariants + _invariant_SS1_SS3_SS4_SS5_SS6_SS7_SS8_SS10_SS11_P1_P2(_grantFund, _standardHandler); + _invariant_SS2_SS4_SS9(_grantFund, _standardHandler); + + // funding invariants + _invariant_FS1_FS2_FS3(_grantFund, _standardHandler); + _invariant_FS4_FS5_FS6_FS7_FS8(_grantFund, _standardHandler); + + // finalize invariants + _invariant_CS1_CS2_CS3_CS4_CS5_CS6_CS7(_grantFund, _standardHandler); + _invariant_ES1_ES2_ES3_ES4_ES5(_grantFund, _standardHandler); + _invariant_DR1_DR2_DR3_DR4_DR5(_grantFund, _standardHandler); + + // distribution period invariants + _invariant_DP1_DP2_DP3_DP4_DP5(_grantFund, _standardHandler); + _invariant_DP6(_grantFund, _standardHandler); + _invariant_T1_T2(_grantFund); + } + + function invariant_call_summary() external useCurrentBlock { + uint24 distributionId = _grantFund.getDistributionId(); + + _logger.logCallSummary(); + _logger.logTimeSummary(); + _logger.logProposalSummary(); + console.log("scenario type", uint8(_standardHandler.getCurrentScenarioType())); + + while (distributionId > 0) { + + _logger.logFundingSummary(distributionId); + _logger.logFinalizeSummary(distributionId); + _logger.logActorSummary(distributionId, true, true); + _logger.logActorDelegationRewards(distributionId); + + --distributionId; + } + } +} From 92880a9bc73e48402a279719fda0152e324565c7 Mon Sep 17 00:00:00 2001 From: prateek105 Date: Fri, 18 Aug 2023 14:41:37 +0530 Subject: [PATCH 20/20] PR feedback --- test/unit/Deployer.t.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/unit/Deployer.t.sol b/test/unit/Deployer.t.sol index 346e70c8..6d0cd6f3 100644 --- a/test/unit/Deployer.t.sol +++ b/test/unit/Deployer.t.sol @@ -28,5 +28,7 @@ contract DeployerTest is Test { (,,,uint256 fundAvailable,,) = grantFund.getDistributionPeriodInfo(1); assertEq(grantFund.treasury(), treasury - fundAvailable); + + assertEq(fundAvailable, treasury * 3 / 100); } } \ No newline at end of file