diff --git a/.github/workflows/protocol_tests.yml b/.github/workflows/protocol_tests.yml index 5e8570684c9..f9859097908 100644 --- a/.github/workflows/protocol_tests.yml +++ b/.github/workflows/protocol_tests.yml @@ -40,32 +40,32 @@ jobs: - name: Run tests common # can't use gas limit because some setUp function use more than the limit - run: forge test -vvv --match-path "test-sol/common/*" # --block-gas-limit 20000000 + run: forge test -vvv --match-path "test-sol/common/*" # --block-gas-limit 50000000 - name: Run tests compatibility if: success() || failure() - run: forge test -vvv --block-gas-limit 20000000 --match-path "test-sol/compatibility/*" + run: forge test -vvv --block-gas-limit 50000000 --match-path "test-sol/compatibility/*" - name: Run tests governance/network if: success() || failure() - run: forge test -vvv --block-gas-limit 20000000 --match-path "test-sol/governance/network/*" + run: forge test -vvv --block-gas-limit 50000000 --match-path "test-sol/governance/network/*" - name: Run tests governance/validators if: success() || failure() - run: forge test -vvv --block-gas-limit 20000000 --match-path "test-sol/governance/validators/*" + run: forge test -vvv --block-gas-limit 50000000 --match-path "test-sol/governance/validators/*" - name: Run tests governance/voting # can't use gas limit because some setUp function use more than the limit if: success() || failure() - run: forge test -vvv --match-path "test-sol/governance/voting/*" # --block-gas-limit 20000000 + run: forge test -vvv --match-path "test-sol/governance/voting/*" --block-gas-limit 50000000 - name: Run tests stability if: success() || failure() - run: forge test -vvv --block-gas-limit 20000000 --match-path "test-sol/stability/*" + run: forge test -vvv --block-gas-limit 50000000 --match-path "test-sol/stability/*" - name: Run tests identity if: success() || failure() - run: forge test -vvv --match-path "test-sol/identity/*" + run: forge test -vvv --block-gas-limit 50000000 --match-path "test-sol/identity/*" - name: Fail if there are tests without folder if: success() || failure() diff --git a/.gitmodules b/.gitmodules index b26686ad5b9..760f3753adf 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "packages/protocol/lib/celo-foundry"] - path = packages/protocol/lib/celo-foundry - url = https://github.com/celo-org/celo-foundry [submodule "packages/protocol/lib/solidity-bytes-utils"] path = packages/protocol/lib/solidity-bytes-utils url = https://github.com/GNSPS/solidity-bytes-utils @@ -13,6 +10,9 @@ [submodule "packages/protocol/lib/memview.sol"] path = packages/protocol/lib/memview.sol url = https://github.com/summa-tx/memview.sol +[submodule "packages/protocol/lib/celo-foundry"] + path = packages/protocol/lib/celo-foundry + url = https://github.com/celo-org/celo-foundry [submodule "packages/protocol/lib/openzeppelin-contracts8"] path = packages/protocol/lib/openzeppelin-contracts8 url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/SETUP.md b/SETUP.md index 03df2cbf70e..12a511129bf 100644 --- a/SETUP.md +++ b/SETUP.md @@ -42,7 +42,7 @@ Once you have go installed run the following to install gobind #### Install Node -Currently Node.js v18.14.2 is required in order to work with this repo. +Currently Node.js v18 is required in order to work with this repo. Install `nvm` (allows you to manage multiple versions of Node) by following the [instructions here](https://github.com/nvm-sh/nvm). @@ -50,8 +50,8 @@ Once `nvm` is successfully installed, restart the terminal and run the following ```bash # restart the terminal after installing nvm -nvm install 18.14.2 -nvm alias default 18.14.2 +nvm install 18 +nvm alias default 18 ``` ### MacOS diff --git a/packages/protocol/contracts/governance/Governance.sol b/packages/protocol/contracts/governance/Governance.sol index f38e2016fae..633b7d2d23c 100644 --- a/packages/protocol/contracts/governance/Governance.sol +++ b/packages/protocol/contracts/governance/Governance.sol @@ -225,7 +225,7 @@ contract Governance is * @return Patch version of the contract. */ function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { - return (1, 4, 0, 0); + return (1, 4, 1, 0); } /** @@ -762,7 +762,6 @@ contract Governance is noVotes, abstainVotes ); - } else { proposal.updateVote( previousVoteRecord.yesVotes, @@ -1592,6 +1591,24 @@ contract Governance is .fromFixed(); } + /** + * @param values The values of CELO to be sent in the proposed transactions. + * @param destinations The destination addresses of the proposed transactions. + * @param data The concatenated data to be included in the proposed transactions. + * @param dataLengths The lengths of each transaction's data. + * @param salt Arbitrary salt associated with hotfix which guarantees uniqueness of hash. + * @return The hash of the hotfix. + */ + function getHotfixHash( + uint256[] calldata values, + address[] calldata destinations, + bytes calldata data, + uint256[] calldata dataLengths, + bytes32 salt + ) external pure returns (bytes32) { + return keccak256(abi.encode(values, destinations, data, dataLengths, salt)); + } + /** * @notice Returns the stage of a dequeued proposal. * @param proposal The proposal struct. diff --git a/packages/protocol/contracts/governance/test/GovernanceTest.sol b/packages/protocol/contracts/governance/test/GovernanceTest.sol deleted file mode 100644 index 04070aa90e5..00000000000 --- a/packages/protocol/contracts/governance/test/GovernanceTest.sol +++ /dev/null @@ -1,40 +0,0 @@ -pragma solidity ^0.5.13; - -import "../Governance.sol"; - -contract GovernanceTest is Governance(true) { - address[] validatorSet; - - // Minimally override core functions from UsingPrecompiles - function numberValidatorsInCurrentSet() public view returns (uint256) { - return validatorSet.length; - } - - function numberValidatorsInSet(uint256) public view returns (uint256) { - return validatorSet.length; - } - - function validatorSignerAddressFromCurrentSet(uint256 index) public view returns (address) { - return validatorSet[index]; - } - - // Expose test utilities - function addValidator(address validator) external { - validatorSet.push(validator); - } - - // exposes removeVotesWhenRevokingDelegatedVotes for tests - function removeVotesWhenRevokingDelegatedVotesTest(address account, uint256 maxAmountAllowed) - public - { - _removeVotesWhenRevokingDelegatedVotes(account, maxAmountAllowed); - } - - function setDeprecatedWeight(address voterAddress, uint256 proposalIndex, uint256 weight) - external - { - Voter storage voter = voters[voterAddress]; - VoteRecord storage voteRecord = voter.referendumVotes[proposalIndex]; - voteRecord.deprecated_weight = weight; - } -} diff --git a/packages/protocol/migrationsConfig.js b/packages/protocol/migrationsConfig.js index 9212953b5a3..86f5cacb47d 100644 --- a/packages/protocol/migrationsConfig.js +++ b/packages/protocol/migrationsConfig.js @@ -579,10 +579,10 @@ NetworkConfigs.alfajoresstaging = NetworkConfigs.alfajores NetworkConfigs.mainnet = NetworkConfigs.rc1 const linkedLibraries = { - Proposals: ['Governance', 'GovernanceTest'], + Proposals: ['Governance'], AddressLinkedList: ['Validators', 'ValidatorsTest'], AddressSortedLinkedList: ['Election', 'ElectionTest'], - IntegerSortedLinkedList: ['Governance', 'GovernanceTest', 'IntegerSortedLinkedListTest'], + IntegerSortedLinkedList: ['Governance', 'IntegerSortedLinkedListTest'], AddressSortedLinkedListWithMedian: ['SortedOracles', 'AddressSortedLinkedListWithMedianTest'], Signatures: [ 'Accounts', diff --git a/packages/protocol/test-sol/governance/network/Governance.t.sol b/packages/protocol/test-sol/governance/network/Governance.t.sol new file mode 100644 index 00000000000..5cfd1db5286 --- /dev/null +++ b/packages/protocol/test-sol/governance/network/Governance.t.sol @@ -0,0 +1,3688 @@ +pragma solidity ^0.5.13; + +import "celo-foundry/Test.sol"; +import "solidity-bytes-utils/contracts/BytesLib.sol"; +import "openzeppelin-solidity/contracts/cryptography/ECDSA.sol"; + +// Contract to test +import "@celo-contracts/governance/Governance.sol"; +import "@celo-contracts/governance/Proposals.sol"; +import "@celo-contracts/governance/test/MockLockedGold.sol"; +import "@celo-contracts/governance/test/MockValidators.sol"; +import "@celo-contracts/governance/test/TestTransactions.sol"; +import "@celo-contracts/common/Accounts.sol"; +import "@celo-contracts/common/Signatures.sol"; +import "@celo-contracts/common/Registry.sol"; +import "@celo-contracts/common/FixidityLib.sol"; + +contract GovernanceForTest is Governance(true) { + address[] validatorSet; + + // Minimally override core functions from UsingPrecompiles + function numberValidatorsInCurrentSet() public view returns (uint256) { + return validatorSet.length; + } + + function numberValidatorsInSet(uint256) public view returns (uint256) { + return validatorSet.length; + } + + function validatorSignerAddressFromCurrentSet(uint256 index) public view returns (address) { + return validatorSet[index]; + } + + // Expose test utilities + function addValidator(address validator) external { + validatorSet.push(validator); + } + + // exposes removeVotesWhenRevokingDelegatedVotes for tests + function removeVotesWhenRevokingDelegatedVotesTest(address account, uint256 maxAmountAllowed) + public + { + _removeVotesWhenRevokingDelegatedVotes(account, maxAmountAllowed); + } + + function setDeprecatedWeight(address voterAddress, uint256 proposalIndex, uint256 weight) + external + { + Voter storage voter = voters[voterAddress]; + VoteRecord storage voteRecord = voter.referendumVotes[proposalIndex]; + voteRecord.deprecated_weight = weight; + } +} + +contract GovernanceBaseTest is Test { + using FixidityLib for FixidityLib.Fraction; + using BytesLib for bytes; + + struct Proposal { + uint256[] values; + address[] destinations; + bytes data; + uint256[] dataLengths; + string description; + } + + address accVoter; + address accOwner; + address accApprover; + uint256 constant DEPOSIT = 5; + uint256 constant VOTER_GOLD = 100; + uint256 constant REFERENDUM_STAGE_DURATION = 5 * 60; + uint256 constant CONCURRENT_PROPOSALS = 1; + uint256 constant DEQUEUE_FREQUENCY = 10 * 60; + uint256 constant QUERY_EXPIRY = 60 * 60; + uint256 constant EXECUTION_STAGE_DURATION = 1 * 60; + + GovernanceForTest governance; + Accounts accounts; + MockLockedGold mockLockedGold; + MockValidators mockValidators; + TestTransactions testTransactions; + + Proposal okProp; + Proposal twoTxProp; + Proposal failingProp; + Proposal emptyProp; + + uint256 expectedParticipationBaseline; + FixidityLib.Fraction baselineUpdateFactor; + FixidityLib.Fraction participationBaseline; + FixidityLib.Fraction participationFloor; + FixidityLib.Fraction baselineQuorumFactor; + + function setUp() public { + // Define Accounts + accVoter = actor("voter"); + accOwner = actor("owner"); + accApprover = actor("approver"); + + baselineUpdateFactor = FixidityLib.newFixedFraction(1, 5); + participationBaseline = FixidityLib.newFixedFraction(5, 10); + participationFloor = FixidityLib.newFixedFraction(5, 100); + baselineQuorumFactor = FixidityLib.fixed1(); + expectedParticipationBaseline = FixidityLib + .multiply(baselineUpdateFactor, FixidityLib.fixed1()) + .add( + FixidityLib.multiply( + FixidityLib.fixed1().subtract(baselineUpdateFactor), + participationBaseline + ) + ) + .unwrap(); + + // change block.tiemstamp so we're not on timestamp = 0 + vm.warp(100 * 60); + + setUpContracts(); + setUpVoterAccount(); + setUpProposalStubs(); + } + + function setUpVoterAccount() private { + vm.prank(accVoter); + accounts.createAccount(); + + mockLockedGold.setAccountTotalLockedGold(accVoter, VOTER_GOLD); + mockLockedGold.setAccountTotalGovernancePower(accVoter, VOTER_GOLD); + } + + function setUpContracts() private { + vm.startPrank(accOwner); + + Registry registry = new Registry(true); + + mockValidators = new MockValidators(); + registry.setAddressFor("Validators", address(mockValidators)); + + mockLockedGold = new MockLockedGold(); + mockLockedGold.setTotalLockedGold(VOTER_GOLD); + registry.setAddressFor("LockedGold", address(mockLockedGold)); + + accounts = new Accounts(true); + accounts.initialize(address(registry)); + registry.setAddressFor("Accounts", address(accounts)); + + governance = new GovernanceForTest(); + governance.initialize( + address(registry), + accApprover, + CONCURRENT_PROPOSALS, + DEPOSIT, + QUERY_EXPIRY, + DEQUEUE_FREQUENCY, + REFERENDUM_STAGE_DURATION, + EXECUTION_STAGE_DURATION, + participationBaseline.unwrap(), + participationFloor.unwrap(), + baselineUpdateFactor.unwrap(), + baselineQuorumFactor.unwrap() + ); + vm.stopPrank(); + } + + function setUpProposalStubs() private { + testTransactions = new TestTransactions(); + + string memory setValueSignature = "setValue(uint256,uint256,bool)"; + + // Define OK Proposal + okProp.data = abi.encodeWithSignature(setValueSignature, 1, 1, true); + okProp.dataLengths.push(okProp.data.length); + okProp.values.push(0); + okProp.destinations.push(address(testTransactions)); + okProp.description = "1 tx proposal"; + + // Define two TX proposal + bytes memory txDataFirst = abi.encodeWithSignature(setValueSignature, 1, 1, true); + bytes memory txDataSecond = abi.encodeWithSignature(setValueSignature, 2, 1, true); + + twoTxProp.values.push(0); + twoTxProp.values.push(0); + twoTxProp.destinations.push(address(testTransactions)); + twoTxProp.destinations.push(address(testTransactions)); + twoTxProp.data = txDataFirst.concat(txDataSecond); + twoTxProp.dataLengths.push(txDataFirst.length); + twoTxProp.dataLengths.push(txDataSecond.length); + twoTxProp.description = "2 txs proposal"; + + // Define failing proposal + failingProp.data = abi.encodeWithSignature(setValueSignature, 3, 1, false); + failingProp.dataLengths.push(failingProp.data.length); + failingProp.values.push(0); + failingProp.destinations.push(address(testTransactions)); + failingProp.description = "failing proposal"; + } + + function assertNotEq(uint256 a, uint256 b) internal { + if (a == b) { + emit log("Error: a != b not satisfied [uint]"); + emit log_named_uint(" Left", a); + emit log_named_uint(" Right", b); + fail(); + } + } + + function makeValidProposal() internal returns (uint256 proposalId) { + return + governance.propose.value(DEPOSIT)( + okProp.values, + okProp.destinations, + okProp.data, + okProp.dataLengths, + okProp.description + ); + } + + function makeEmptyProposal() internal returns (uint256 proposalId) { + Proposal memory emptyProposal; + return + governance.propose.value(DEPOSIT)( + emptyProposal.values, + emptyProposal.destinations, + emptyProposal.data, + emptyProposal.dataLengths, + "empty proposal" + ); + } + + function makeAndApproveProposal(uint256 index) internal returns (uint256 id) { + id = makeValidProposal(); + vm.warp(block.timestamp + governance.dequeueFrequency()); + + vm.prank(accApprover); + governance.approve(id, index); + } + + function authorizeValidatorSigner(uint256 signerPk, address account) internal { + bytes32 messageHash = keccak256(abi.encodePacked(account)); + bytes32 prefixedHash = ECDSA.toEthSignedMessageHash(messageHash); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, prefixedHash); + vm.prank(account); + accounts.authorizeValidatorSigner(vm.addr(signerPk), v, r, s); + } + + function authorizeVoteSigner(uint256 signerPk, address account) internal { + bytes32 messageHash = keccak256(abi.encodePacked(account)); + bytes32 prefixedHash = ECDSA.toEthSignedMessageHash(messageHash); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, prefixedHash); + vm.prank(account); + accounts.authorizeVoteSigner(vm.addr(signerPk), v, r, s); + } +} + +contract GovernanceInitialize is GovernanceBaseTest { + function test_SetsTheOwner() public { + assertEq(governance.owner(), accOwner); + } + + function test_SetsConcurrentProposals() public { + assertEq(governance.concurrentProposals(), 1); + } + + function test_SetsMinDeposit() public { + assertEq(governance.minDeposit(), 5); + } + + function test_SetsQueueExpiry() public { + assertEq(governance.queueExpiry(), QUERY_EXPIRY); + } + + function test_SetsDequeueFrequency() public { + assertEq(governance.dequeueFrequency(), DEQUEUE_FREQUENCY); + } + + function test_SetsStageDurations() public { + assertEq(governance.getReferendumStageDuration(), REFERENDUM_STAGE_DURATION); + assertEq(governance.getExecutionStageDuration(), EXECUTION_STAGE_DURATION); + } + + function test_SetsParticipationParameters() public { + (uint256 actualParticipationBaseline, uint256 actualParticipationFloor, uint256 actualBaselineUpdateFactor, uint256 actualBaselineQuorumFactor) = governance + .getParticipationParameters(); + assertEq(actualParticipationBaseline, FixidityLib.newFixedFraction(5, 10).unwrap()); + assertEq(actualParticipationFloor, FixidityLib.newFixedFraction(5, 100).unwrap()); + assertEq(actualBaselineUpdateFactor, FixidityLib.newFixedFraction(1, 5).unwrap()); + assertEq(actualBaselineQuorumFactor, FixidityLib.newFixed(1).unwrap()); + } + + // TODO: Consider testing reversion when 0 values provided + function test_RevertIf_CalledAgain() public { + vm.expectRevert("contract already initialized"); + governance.initialize( + address(1), + accApprover, + 1, + 1, + 1, + 1, + 1, + 1, + FixidityLib.newFixed(1).unwrap(), + FixidityLib.newFixed(1).unwrap(), + FixidityLib.newFixed(1).unwrap(), + FixidityLib.newFixed(1).unwrap() + ); + } +} + +contract GovernanceSetApprover is GovernanceBaseTest { + event ApproverSet(address indexed approver); + + address NEW_APPROVER = address(7777); + + function test_SetsValue() public { + vm.prank(accOwner); + governance.setApprover(NEW_APPROVER); + assertEq(governance.approver(), NEW_APPROVER); + } + + function test_emitTheApproverSetEvent() public { + vm.expectEmit(true, true, true, true); + emit ApproverSet(NEW_APPROVER); + vm.prank(accOwner); + governance.setApprover(NEW_APPROVER); + } + + function test_RevertIf_NullAddress() public { + vm.expectRevert("Approver cannot be 0"); + vm.prank(accOwner); + governance.setApprover(address(0)); + } + + function test_RevertIf_Unchanged() public { + vm.expectRevert("Approver unchanged"); + vm.prank(accOwner); + governance.setApprover(accApprover); + } + + function test_RevertWhen_CalledByNotOwner() public { + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(address(9999)); + governance.setApprover(NEW_APPROVER); + } +} + +contract GovernanceSetMinDeposit is GovernanceBaseTest { + event MinDepositSet(uint256 minDeposit); + + uint256 NEW_MINDEPOSIT = 45; + + function test_SetsValue() public { + vm.prank(accOwner); + governance.setMinDeposit(NEW_MINDEPOSIT); + assertEq(governance.minDeposit(), NEW_MINDEPOSIT); + } + + function test_emitTheMinDepositSetEvent() public { + vm.expectEmit(true, true, true, true); + emit MinDepositSet(NEW_MINDEPOSIT); + vm.prank(accOwner); + governance.setMinDeposit(NEW_MINDEPOSIT); + } + + function test_RevertIf_Unchanged() public { + vm.expectRevert("Minimum deposit unchanged"); + vm.prank(accOwner); + governance.setMinDeposit(5); + } + + function test_RevertWhen_CalledByNotOwner() public { + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(address(9999)); + governance.setMinDeposit(NEW_MINDEPOSIT); + } +} + +contract GovernanceSetConcurrentProposals is GovernanceBaseTest { + event ConcurrentProposalsSet(uint256 concurrentProposals); + + uint256 NEW_CONCURRENT_PROPOSALS = 45; + + function test_SetsValue() public { + vm.prank(accOwner); + governance.setConcurrentProposals(NEW_CONCURRENT_PROPOSALS); + assertEq(governance.concurrentProposals(), NEW_CONCURRENT_PROPOSALS); + } + + function test_emitTheConcurrentProposalsSetEvent() public { + vm.expectEmit(true, true, true, true); + emit ConcurrentProposalsSet(NEW_CONCURRENT_PROPOSALS); + vm.prank(accOwner); + governance.setConcurrentProposals(NEW_CONCURRENT_PROPOSALS); + } + + function test_RevertIf_SetZero() public { + vm.expectRevert("Number of proposals must be larger than zero"); + vm.prank(accOwner); + governance.setConcurrentProposals(0); + } + + function test_RevertIf_Unchanged() public { + vm.expectRevert("Number of proposals unchanged"); + vm.prank(accOwner); + governance.setConcurrentProposals(1); + } + + function test_RevertWhen_CalledByNotOwner() public { + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(address(9999)); + governance.setConcurrentProposals(NEW_CONCURRENT_PROPOSALS); + } +} + +contract GovernanceSetQueueExpiry is GovernanceBaseTest { + event QueueExpirySet(uint256 queueExpiry); + + uint256 NEW_VALUE = 45; + + function test_SetsValue() public { + vm.prank(accOwner); + governance.setQueueExpiry(NEW_VALUE); + assertEq(governance.queueExpiry(), NEW_VALUE); + } + + function test_emitTheQueueExpirySetEvent() public { + vm.expectEmit(true, true, true, true); + emit QueueExpirySet(NEW_VALUE); + vm.prank(accOwner); + governance.setQueueExpiry(NEW_VALUE); + } + + function test_RevertIf_SetZero() public { + vm.expectRevert("QueueExpiry must be larger than 0"); + vm.prank(accOwner); + governance.setQueueExpiry(0); + } + + function test_RevertIf_Unchanged() public { + vm.expectRevert("QueueExpiry unchanged"); + vm.prank(accOwner); + governance.setQueueExpiry(QUERY_EXPIRY); + } + + function test_RevertWhen_CalledByNotOwner() public { + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(address(9999)); + governance.setQueueExpiry(NEW_VALUE); + } +} + +contract GovernanceSetDequeueFrequency is GovernanceBaseTest { + event DequeueFrequencySet(uint256 dequeueFrequency); + + uint256 NEW_VALUE = 45; + + function test_SetsValue() public { + vm.prank(accOwner); + governance.setDequeueFrequency(NEW_VALUE); + assertEq(governance.dequeueFrequency(), NEW_VALUE); + } + + function test_emitTheDequeueFrequencySetEvent() public { + vm.expectEmit(true, true, true, true); + emit DequeueFrequencySet(NEW_VALUE); + vm.prank(accOwner); + governance.setDequeueFrequency(NEW_VALUE); + } + + function test_RevertIf_SetZero() public { + vm.expectRevert("dequeueFrequency must be larger than 0"); + vm.prank(accOwner); + governance.setDequeueFrequency(0); + } + + function test_RevertIf_Unchanged() public { + vm.expectRevert("dequeueFrequency unchanged"); + vm.prank(accOwner); + governance.setDequeueFrequency(DEQUEUE_FREQUENCY); + } + + function test_RevertWhen_CalledByNotOwner() public { + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(address(9999)); + governance.setDequeueFrequency(NEW_VALUE); + } +} + +contract GovernanceSetReferendumStageDuration is GovernanceBaseTest { + event ReferendumStageDurationSet(uint256 value); + + uint256 NEW_VALUE = 45; + + function test_SetsValue() public { + vm.prank(accOwner); + governance.setReferendumStageDuration(NEW_VALUE); + assertEq(governance.getReferendumStageDuration(), NEW_VALUE); + } + + function test_emitTheReferendumStageDurationSetEvent() public { + vm.expectEmit(true, true, true, true); + emit ReferendumStageDurationSet(NEW_VALUE); + vm.prank(accOwner); + governance.setReferendumStageDuration(NEW_VALUE); + } + + function test_RevertIf_SetZero() public { + vm.expectRevert("Duration must be larger than 0"); + vm.prank(accOwner); + governance.setReferendumStageDuration(0); + } + + function test_RevertIf_Unchanged() public { + vm.expectRevert("Duration unchanged"); + vm.prank(accOwner); + governance.setReferendumStageDuration(5 * 60); + } + + function test_RevertWhen_CalledByNotOwner() public { + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(address(9999)); + governance.setReferendumStageDuration(NEW_VALUE); + } +} + +contract GovernanceSetExecutionStageDuration is GovernanceBaseTest { + event ExecutionStageDurationSet(uint256 dequeueFrequency); + + uint256 NEW_VALUE = 45; + + function test_SetsValue() public { + vm.prank(accOwner); + governance.setExecutionStageDuration(NEW_VALUE); + assertEq(governance.getExecutionStageDuration(), NEW_VALUE); + } + + function test_emitTheExecutionStageDurationSetEvent() public { + vm.expectEmit(true, true, true, true); + emit ExecutionStageDurationSet(NEW_VALUE); + vm.prank(accOwner); + governance.setExecutionStageDuration(NEW_VALUE); + } + + function test_RevertIf_SetToZero() public { + vm.expectRevert("Duration must be larger than 0"); + vm.prank(accOwner); + governance.setExecutionStageDuration(0); + } + + function test_RevertIf_Unchanged() public { + vm.expectRevert("Duration unchanged"); + vm.prank(accOwner); + governance.setExecutionStageDuration(EXECUTION_STAGE_DURATION); + } + + function test_RevertWhen_CalledByNotOwner() public { + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(address(9999)); + governance.setExecutionStageDuration(NEW_VALUE); + } +} + +contract GovernanceSetParticipationFloor is GovernanceBaseTest { + event ParticipationFloorSet(uint256 value); + + uint256 NEW_VALUE = 45; + + function test_SetsValue() public { + vm.prank(accOwner); + governance.setParticipationFloor(NEW_VALUE); + (uint256 baseline, uint256 baselineFloor, uint256 _baselineUpdateFactor, uint256 _baselineQuorumFactor) = governance + .getParticipationParameters(); + assertEq(baselineFloor, NEW_VALUE); + } + + function test_emitTheParticipationFloorSetEvent() public { + vm.expectEmit(true, true, true, true); + emit ParticipationFloorSet(NEW_VALUE); + vm.prank(accOwner); + governance.setParticipationFloor(NEW_VALUE); + } + + function test_RevertIf_SetAboveOne() public { + vm.expectRevert("Participation floor greater than one"); + vm.prank(accOwner); + governance.setParticipationFloor(FixidityLib.newFixedFraction(11, 10).unwrap()); + } + + function test_RevertWhen_CalledByNotOwner() public { + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(address(9999)); + governance.setParticipationFloor(NEW_VALUE); + } +} + +contract GovernanceSetBaselineUpdateFactor is GovernanceBaseTest { + event ParticipationBaselineUpdateFactorSet(uint256 value); + + uint256 NEW_VALUE = 45; + + function test_SetsValue() public { + vm.prank(accOwner); + governance.setBaselineUpdateFactor(NEW_VALUE); + (uint256 baseline, uint256 baselineFloor, uint256 _baselineUpdateFactor, uint256 _baselineQuorumFactor) = governance + .getParticipationParameters(); + assertEq(_baselineUpdateFactor, NEW_VALUE); + } + + function test_emitTheParticipationBaselineUpdateFactorSetEvent() public { + vm.expectEmit(true, true, true, true); + emit ParticipationBaselineUpdateFactorSet(NEW_VALUE); + vm.prank(accOwner); + governance.setBaselineUpdateFactor(NEW_VALUE); + } + + function test_RevertIf_SetAboveOne() public { + vm.expectRevert("Baseline update factor greater than one"); + vm.prank(accOwner); + governance.setBaselineUpdateFactor(FixidityLib.newFixedFraction(11, 10).unwrap()); + } + + function test_RevertWhen_CalledByNotOwner() public { + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(address(9999)); + governance.setBaselineUpdateFactor(NEW_VALUE); + } +} + +contract GovernanceSetBaselineQuorumFactor is GovernanceBaseTest { + event ParticipationBaselineQuorumFactorSet(uint256 value); + + uint256 NEW_VALUE = 45; + + function test_SetsValue() public { + vm.prank(accOwner); + governance.setBaselineQuorumFactor(NEW_VALUE); + (uint256 baseline, uint256 baselineFloor, uint256 _baselineUpdateFactor, uint256 _baselineQuorumFactor) = governance + .getParticipationParameters(); + assertEq(_baselineQuorumFactor, NEW_VALUE); + } + + function test_emitTheBaselineQuorumFactorSetEvent() public { + vm.expectEmit(true, true, true, true); + emit ParticipationBaselineQuorumFactorSet(NEW_VALUE); + vm.prank(accOwner); + governance.setBaselineQuorumFactor(NEW_VALUE); + } + + function test_RevertIf_SetAboveOne() public { + vm.expectRevert("Baseline quorum factor greater than one"); + vm.prank(accOwner); + governance.setBaselineQuorumFactor(FixidityLib.newFixedFraction(11, 10).unwrap()); + } + + function test_RevertWhen_CalledByNotOwner() public { + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(address(9999)); + governance.setBaselineQuorumFactor(NEW_VALUE); + } +} + +contract GovernanceSetConstitution is GovernanceBaseTest { + event ConstitutionSet(address indexed destination, bytes4 indexed functionId, uint256 threshold); + + function test_RevertIf_DestinationIsZeroAddress() public { + vm.expectRevert("Destination cannot be zero"); + uint256 threshold = FixidityLib.newFixedFraction(2, 3).unwrap(); + vm.prank(accOwner); + governance.setConstitution(address(0), 0x00000000, threshold); + } + + function test_RevertIf_ThresholdIsZero() public { + vm.expectRevert("Threshold has to be greater than majority and not greater than unanimity"); + uint256 threshold = FixidityLib.newFixed(0).unwrap(); + vm.prank(accOwner); + governance.setConstitution(address(governance), 0x00000000, threshold); + } + + function test_RevertIf_ThresholdIsNotGreaterThanMajority() public { + uint256 threshold = FixidityLib.newFixedFraction(1, 2).unwrap(); + vm.expectRevert("Threshold has to be greater than majority and not greater than unanimity"); + vm.prank(accOwner); + governance.setConstitution(address(governance), 0x00000000, threshold); + } + + function test_RevertWhen_CalledByNotOwner() public { + vm.expectRevert("Ownable: caller is not the owner"); + uint256 threshold = FixidityLib.newFixedFraction(101, 100).unwrap(); + vm.prank(address(9999)); + governance.setConstitution(address(governance), 0x00000000, threshold); + } + + function test_RevertIf_ThresholdIsGreaterThan100Percent() public { + vm.expectRevert("Threshold has to be greater than majority and not greater than unanimity"); + uint256 threshold = FixidityLib.newFixedFraction(101, 100).unwrap(); + vm.prank(accOwner); + governance.setConstitution(address(governance), 0x00000000, threshold); + } + + function test_SetDefaultThreshold_WhenFunctionIdIsZero() public { + uint256 threshold = FixidityLib.newFixedFraction(2, 3).unwrap(); + vm.prank(accOwner); + governance.setConstitution(address(governance), 0x00000000, threshold); + assertEq(governance.getConstitution(address(governance), 0x12340000), threshold); + } + + function test_EmitConstitutionSet_WhenFunctionIdIsZero() public { + uint256 threshold = FixidityLib.newFixedFraction(2, 3).unwrap(); + vm.expectEmit(true, true, true, true); + emit ConstitutionSet(address(governance), 0x00000000, threshold); + vm.prank(accOwner); + governance.setConstitution(address(governance), 0x00000000, threshold); + } + + function test_SetThreshold_WhenFunctionIdIsNotZero() public { + uint256 threshold = FixidityLib.newFixedFraction(2, 3).unwrap(); + vm.prank(accOwner); + governance.setConstitution(address(governance), 0x11111111, threshold); + assertEq(governance.getConstitution(address(governance), 0x11111111), threshold); + } + + function test_NotSetDefaultThreshold_WhenFunctionIdIsNotZero() public { + uint256 threshold = FixidityLib.newFixedFraction(2, 3).unwrap(); + vm.prank(accOwner); + governance.setConstitution(address(governance), 0x11111111, threshold); + assertNotEq(governance.getConstitution(address(governance), 0x12340000), threshold); + } + + function test_EmitConstitutionSet_WhenFunctionIdIsNotZero() public { + uint256 threshold = FixidityLib.newFixedFraction(2, 3).unwrap(); + vm.expectEmit(true, true, true, true); + emit ConstitutionSet(address(governance), 0x11111111, threshold); + vm.prank(accOwner); + governance.setConstitution(address(governance), 0x11111111, threshold); + } +} + +contract GovernancePropose is GovernanceBaseTest { + event ProposalQueued( + uint256 indexed proposalId, + address indexed proposer, + uint256 transactionCount, + uint256 deposit, + uint256 timestamp + ); + + function test_returnsProposalId() public { + uint256 id = makeValidProposal(); + + assertEq(id, 1); + } + + function test_incrementsProposalCount() public { + makeValidProposal(); + + assertEq(governance.proposalCount(), 1); + } + + function test_addProposalToTheQueue() public { + uint256 id = makeValidProposal(); + + assertTrue(governance.isQueued(id)); + (uint256[] memory proposalIds, uint256[] memory upVotes) = governance.getQueue(); + + assertEq(proposalIds[0], 1); + assertEq(upVotes[0], 0); + } + + function check_registerProposal(Proposal memory proposal) private { + uint256 id = governance.propose.value(DEPOSIT)( + proposal.values, + proposal.destinations, + proposal.data, + proposal.dataLengths, + proposal.description + ); + + (address proposer, uint256 deposit, uint256 timestamp, uint256 txCount, string memory description, uint256 networkWeight, bool approved) = governance + .getProposal(id); + + assertEq(proposer, address(this)); + assertEq(deposit, DEPOSIT); + assertEq(timestamp, block.timestamp); + assertEq(txCount, proposal.values.length); + assertEq(description, proposal.description); + assertEq(networkWeight, 0); + assertEq(approved, false); + } + + function check_registerProposalTransactions(Proposal memory proposal) private { + uint256 id = governance.propose.value(DEPOSIT)( + proposal.values, + proposal.destinations, + proposal.data, + proposal.dataLengths, + proposal.description + ); + + uint256 dataPosition = 0; + for (uint256 i = 0; i < proposal.values.length; i++) { + (uint256 value, address destination, bytes memory data) = governance.getProposalTransaction( + id, + i + ); + assertEq(proposal.values[i], value); + assertEq(proposal.destinations[i], destination); + bytes memory expectedData = proposal.data.slice(dataPosition, proposal.dataLengths[i]); + assertEq(data, expectedData); + dataPosition = dataPosition + proposal.dataLengths[i]; + } + } + + function check_emitsProposalQueuedEvents(Proposal memory proposal) private { + vm.expectEmit(true, true, true, true); + emit ProposalQueued(1, address(this), proposal.values.length, DEPOSIT, block.timestamp); + governance.propose.value(DEPOSIT)( + proposal.values, + proposal.destinations, + proposal.data, + proposal.dataLengths, + proposal.description + ); + } + + function test_registerTheProposal_whenProposalHasZeroTransactions() public { + Proposal memory zeroProp; + zeroProp.description = "zero tx proposal"; + check_registerProposal(zeroProp); + } + + function test_emitProposalQueued_whenProposalHasZeroTransactions() public { + Proposal memory zeroProp; + zeroProp.description = "zero tx proposal"; + check_emitsProposalQueuedEvents(zeroProp); + } + + function test_registerTheProposal_whenProposalWithOneTransaction() public { + check_registerProposal(okProp); + } + + function test_registerTheProposalTransactions_whenProposalWithOneTransaction() public { + check_registerProposalTransactions(okProp); + } + + function test_emitProposalQueued_whenProposalWithOneTransaction() public { + check_emitsProposalQueuedEvents(okProp); + } + + function test_RevertIf_descriptionIsEmtpy_whenProposalWithOneTransaction() public { + vm.expectRevert("Description url must have non-zero length"); + governance.propose.value(DEPOSIT)( + okProp.values, + okProp.destinations, + okProp.data, + okProp.dataLengths, + "" + ); + } + + function test_registerTheProposal_whenProposalWithTwoTransaction() public { + check_registerProposal(twoTxProp); + } + + function test_registerTheProposalTransactions_whenProposalWithTwoTransaction() public { + check_registerProposalTransactions(twoTxProp); + } + + function test_emitProposalQueued_whenProposalWithTwoTransaction() public { + check_emitsProposalQueuedEvents(twoTxProp); + } + + function test_dequeuesOldProposal_whenItHasBeenMoreThanDequeueFrequencySinceLastDequeue() public { + uint256 originalLastDequeue = governance.lastDequeue(); + uint256 firstId = makeValidProposal(); + + // wait "dequeueFrequency" + vm.warp(block.timestamp + DEQUEUE_FREQUENCY); + + governance.propose.value(DEPOSIT)( + okProp.values, + okProp.destinations, + okProp.data, + okProp.dataLengths, + okProp.description + ); + + assertFalse(governance.isQueued(firstId)); + assertEq(governance.getQueueLength(), 1); + assertEq(governance.dequeued(0), firstId); + assertGt(governance.lastDequeue(), originalLastDequeue); + } + + function test_NotUpdateLastDequeueWhenNoQueuedProposal() public { + uint256 originalLastDequeue = governance.lastDequeue(); + + vm.warp(block.timestamp + DEQUEUE_FREQUENCY); + + makeValidProposal(); + + assertEq(governance.getQueueLength(), 1); + assertEq(governance.lastDequeue(), originalLastDequeue); + } +} + +contract GovernanceUpvote is GovernanceBaseTest { + event ProposalUpvoted(uint256 indexed proposalId, address indexed account, uint256 upvotes); + event ProposalExpired(uint256 indexed proposalId); + + uint256 proposalId; + + function setUp() public { + super.setUp(); + proposalId = makeValidProposal(); + } + + function test_increaseNumberOfUpvotes() public { + vm.prank(accVoter); + governance.upvote(proposalId, 0, 0); + assertEq(governance.getUpvotes(proposalId), VOTER_GOLD); + } + + function test_markAccountAsHavingUpvotedProposal() public { + vm.prank(accVoter); + governance.upvote(proposalId, 0, 0); + (uint256 recordId, uint256 recordWeight) = governance.getUpvoteRecord(accVoter); + assertEq(recordId, proposalId); + assertEq(recordWeight, VOTER_GOLD); + } + + function test_returnsTrue() public { + vm.prank(accVoter); + assertTrue(governance.upvote(proposalId, 0, 0)); + } + + function test_emitsProposalUpvotedEvent() public { + vm.expectEmit(true, true, true, true); + emit ProposalUpvoted(proposalId, accVoter, VOTER_GOLD); + + vm.prank(accVoter); + governance.upvote(proposalId, 0, 0); + } + + function test_RevertIf_UpvotingNotQueuedProposal() public { + vm.expectRevert("cannot upvote a proposal not in the queue"); + vm.prank(accVoter); + governance.upvote(proposalId + 1, 0, 0); + } + + function test_SortsUpvotedProposalToFrontOfQueue_WhenItWasAtTheEnd() public { + // make another proposal, thus leave first "at the end of the queue" + uint256 newProposalId = makeValidProposal(); + + // upvotes first + vm.prank(accVoter); + governance.upvote(newProposalId, proposalId, 0); + + (uint256[] memory proposalIds, uint256[] memory upvotes) = governance.getQueue(); + + assertEq(proposalIds[0], newProposalId); + assertEq(upvotes[0], VOTER_GOLD); + } + + function setUp_whenUpvotedProposalIsExpired() private { + uint256 queueExpiry = governance.queueExpiry(); + + // Prevent dequeues for the sake of this test. + vm.prank(accOwner); + governance.setDequeueFrequency(queueExpiry * 2); + + // make another proposal (id=2) + uint256 newProposalId = makeValidProposal(); + + address accOtherVoter = actor("otherVoter"); + vm.startPrank(accOtherVoter); + accounts.createAccount(); + mockLockedGold.setAccountTotalLockedGold(accOtherVoter, VOTER_GOLD); + mockLockedGold.setAccountTotalGovernancePower(accOtherVoter, VOTER_GOLD); + governance.upvote(newProposalId, proposalId, 0); + vm.stopPrank(); + + vm.warp(block.timestamp + queueExpiry); + } + + function test_returnsFalse_whenUpvotedProposalIsExpired() public { + setUp_whenUpvotedProposalIsExpired(); + vm.prank(accVoter); + assertFalse(governance.upvote(proposalId, 2, 0)); + } + + function test_removeFromQueue_whenUpvotedProposalIsExpired() public { + setUp_whenUpvotedProposalIsExpired(); + + vm.prank(accVoter); + governance.upvote(proposalId, 2, 0); + + (uint256[] memory proposalIds, ) = governance.getQueue(); + // proposalId(1) has been dequeued + assertEq(proposalIds.length, 1); + assertNotEq(proposalIds[0], proposalId); + } + + function test_emitProposalExpired_whenUpvotedProposalIsExpired() public { + setUp_whenUpvotedProposalIsExpired(); + + vm.expectEmit(true, true, true, true); + emit ProposalExpired(proposalId); + + vm.prank(accVoter); + governance.upvote(proposalId, 2, 0); + } + + function test_DequeueQueuedProposals_whenItHasBeenMoreThanDequeueFrequencySinceLastDequeue() + public + { + uint256 originalLastDequeue = governance.lastDequeue(); + uint256 newProposalId = makeValidProposal(); + vm.warp(block.timestamp + governance.dequeueFrequency()); + + uint256 queueLength = governance.getQueueLength(); + vm.startPrank(accVoter); + governance.upvote(newProposalId, 0, 0); + assertFalse(governance.isQueued(proposalId)); + assertEq(governance.getQueueLength(), queueLength - CONCURRENT_PROPOSALS); + + assertEq(governance.dequeued(0), proposalId); + assertLt(originalLastDequeue, governance.lastDequeue()); + } + + function test_RevertIf_UpvotingAProposalThatWillBeDequeued_whenItHasBeenMoreThanDequeueFrequencySinceLastDequeue() + public + { + makeValidProposal(); + vm.warp(block.timestamp + governance.dequeueFrequency()); + + vm.expectRevert("cannot upvote a proposal not in the queue"); + vm.prank(accVoter); + governance.upvote(proposalId, 0, 0); + } + + function setUp_whenPreviousUpvotedProposalIsInQueueAndExpired() + private + returns (uint256 newProposalId) + { + uint256 queueExpiry = 60; + vm.prank(accOwner); + governance.setQueueExpiry(queueExpiry); + + vm.prank(accVoter); + governance.upvote(proposalId, 0, 0); + + vm.warp(block.timestamp + queueExpiry); + return makeValidProposal(); + } + + function test_increaseNumberOfUpvotesForTheProposal_whenPreviousUpvotedProposalIsInQueueAndExpired() + public + { + uint256 newProposalId = setUp_whenPreviousUpvotedProposalIsInQueueAndExpired(); + vm.prank(accVoter); + governance.upvote(newProposalId, 0, 0); + assertEq(governance.getUpvotes(newProposalId), VOTER_GOLD); + } + + function test_markTheAccountAsHavingUpvotedTheProposal_whenPreviousUpvotedProposalIsInQueueAndExpired() + public + { + uint256 newProposalId = setUp_whenPreviousUpvotedProposalIsInQueueAndExpired(); + vm.prank(accVoter); + governance.upvote(newProposalId, 0, 0); + (uint256 recordId, uint256 recordWeight) = governance.getUpvoteRecord(accVoter); + assertEq(recordId, newProposalId); + assertEq(recordWeight, VOTER_GOLD); + } + + function test_returnTrue_whenPreviousUpvotedProposalIsInQueueAndExpired() public { + uint256 newProposalId = setUp_whenPreviousUpvotedProposalIsInQueueAndExpired(); + vm.prank(accVoter); + assertTrue(governance.upvote(newProposalId, 0, 0)); + } + + function test_emitTheProposalExpiredEvent_whenPreviousUpvotedProposalIsInQueueAndExpired() + public + { + uint256 newProposalId = setUp_whenPreviousUpvotedProposalIsInQueueAndExpired(); + + vm.expectEmit(true, true, true, true); + emit ProposalExpired(proposalId); + + vm.prank(accVoter); + governance.upvote(newProposalId, 0, 0); + } + + function test_emitTheProposalUpvotedEvent_whenPreviousUpvotedProposalIsInQueueAndExpired() + public + { + uint256 newProposalId = setUp_whenPreviousUpvotedProposalIsInQueueAndExpired(); + + vm.expectEmit(true, true, true, true); + emit ProposalUpvoted(newProposalId, accVoter, VOTER_GOLD); + + vm.prank(accVoter); + governance.upvote(newProposalId, 0, 0); + } +} + +contract GovernanceRevokeUpvote is GovernanceBaseTest { + event ProposalExpired(uint256 indexed proposalId); + event ProposalUpvoteRevoked( + uint256 indexed proposalId, + address indexed account, + uint256 revokedUpvotes + ); + + uint256 proposalId; + + function setUp() public { + super.setUp(); + proposalId = makeValidProposal(); + vm.startPrank(accVoter); + governance.upvote(proposalId, 0, 0); + } + + function test_returnTrue() public { + assertTrue(governance.revokeUpvote(0, 0)); + } + + function test_decreaseUpvotesNumber() public { + governance.revokeUpvote(0, 0); + assertEq(governance.getUpvotes(proposalId), 0); + } + + function test_markAccountAsNotHavingUpvoted() public { + governance.revokeUpvote(0, 0); + (uint256 recordId, uint256 recordWeight) = governance.getUpvoteRecord(accVoter); + assertEq(recordId, 0); + assertEq(recordWeight, 0); + } + + function test_emitProposalUpvoteRevokedEvent() public { + vm.expectEmit(true, true, true, true); + emit ProposalUpvoteRevoked(proposalId, accVoter, VOTER_GOLD); + + governance.revokeUpvote(0, 0); + } + + function test_RevertIf_accountHasntUpvoted() public { + governance.revokeUpvote(0, 0); + vm.expectRevert("Account has no historical upvote"); + governance.revokeUpvote(0, 0); + } + + function test_removeProposalFromQueue_whenProposalExpired() public { + vm.warp(block.timestamp + governance.queueExpiry()); + governance.revokeUpvote(0, 0); + assertFalse(governance.isQueued(proposalId)); + (uint256[] memory proposalIds, uint256[] memory upvotes) = governance.getQueue(); + assertEq(proposalIds.length, 0); + assertEq(upvotes.length, 0); + } + + function test_markAccountAsNotHavingUpvoted_whenProposalExpired() public { + vm.warp(block.timestamp + governance.queueExpiry()); + governance.revokeUpvote(0, 0); + (uint256 recordId, uint256 recordWeight) = governance.getUpvoteRecord(accVoter); + assertEq(recordId, 0); + assertEq(recordWeight, 0); + } + + function test_emitProposalExpiredEvent_whenProposalExpired() public { + vm.warp(block.timestamp + governance.queueExpiry()); + vm.expectEmit(true, true, true, true); + emit ProposalExpired(proposalId); + governance.revokeUpvote(0, 0); + } + + function test_dequeueProposal_whenMoreThanDequeueFrequencySinceLastDequeue() public { + uint256 originalLastDequeue = governance.lastDequeue(); + vm.warp(block.timestamp + governance.dequeueFrequency()); + governance.revokeUpvote(0, 0); + assertFalse(governance.isQueued(proposalId)); + assertEq(governance.getQueueLength(), 0); + assertEq(governance.dequeued(0), proposalId); + assertLt(originalLastDequeue, governance.lastDequeue()); + } + + function test_markAccountAsNotHavingUpvoted_whenMoreThanDequeueFrequencySinceLastDequeue() + public + { + uint256 originalLastDequeue = governance.lastDequeue(); + vm.warp(block.timestamp + governance.dequeueFrequency()); + governance.revokeUpvote(0, 0); + (uint256 recordId, uint256 recordWeight) = governance.getUpvoteRecord(accVoter); + assertEq(recordId, 0); + assertEq(recordWeight, 0); + } +} + +contract GovernanceWithdraw is GovernanceBaseTest { + uint256 proposalId; + + address accProposer; + + function setUp() public { + super.setUp(); + accProposer = actor("proposer"); + vm.deal(accProposer, DEPOSIT * 2); + + vm.prank(accProposer); + proposalId = makeValidProposal(); + + vm.warp(block.timestamp + governance.dequeueFrequency()); + + vm.prank(accApprover); + governance.approve(proposalId, 0); + } + + function test_returnTrue_whenCallerIsProposer() public { + vm.prank(accProposer); + assertTrue(governance.withdraw()); + } + + function test_withdrawRefundedDepositWhenProposalWasDequeued_whenCallerIsProposer() public { + uint256 startBalance = accProposer.balance; + + vm.prank(accProposer); + governance.withdraw(); + assertEq(accProposer.balance, startBalance + DEPOSIT); + } + + function test_RevertIf_CallerNotOriginalProposer() public { + vm.expectRevert("Nothing to withdraw"); + vm.prank(actor("somebody")); + governance.withdraw(); + } +} + +contract GovernanceApprove is GovernanceBaseTest { + event ProposalDequeued(uint256 indexed proposalId, uint256 timestamp); + event ProposalApproved(uint256 indexed proposalId); + + uint256 INDEX = 0; // first proposal index + uint256 proposalId; + + function setUp() public { + super.setUp(); + proposalId = makeValidProposal(); + vm.warp(block.timestamp + governance.dequeueFrequency()); + } + + function test_returnTrue() public { + vm.prank(accApprover); + assertTrue(governance.approve(proposalId, INDEX)); + } + + function test_UpdateProposalDetails() public { + vm.prank(accApprover); + governance.approve(proposalId, INDEX); + + (address proposer, uint256 deposit, , uint256 txCount, string memory description, uint256 networkWeight, bool approved) = governance + .getProposal(proposalId); + + assertEq(proposer, address(this)); + assertEq(deposit, DEPOSIT); + assertEq(txCount, 1); + assertEq(description, "1 tx proposal"); + assertEq(networkWeight, VOTER_GOLD); + assertEq(approved, true); + } + + function test_MarkProposalAsApproved() public { + vm.prank(accApprover); + governance.approve(proposalId, INDEX); + assertTrue(governance.isApproved(proposalId)); + } + + function test_emitProposalDequeuedEvent() public { + vm.expectEmit(true, true, true, true); + emit ProposalDequeued(proposalId, block.timestamp); + vm.prank(accApprover); + governance.approve(proposalId, INDEX); + } + + function test_emitProposalApprovedEvent() public { + vm.expectEmit(true, true, true, true); + emit ProposalApproved(proposalId); + vm.prank(accApprover); + governance.approve(proposalId, INDEX); + } + + function test_RevertIf_IndexOutOfBounds() public { + vm.expectRevert("Provided index greater than dequeue length."); + vm.prank(accApprover); + governance.approve(proposalId, INDEX + 1); + } + + function test_RevertIf_ProposalIdDontMatchIndex() public { + uint256 newProposalId = makeValidProposal(); + vm.warp(block.timestamp + governance.dequeueFrequency()); + vm.expectRevert("Proposal not dequeued"); + vm.prank(accApprover); + governance.approve(newProposalId, INDEX); + } + + function test_RevertIf_NotCalledByApprover() public { + vm.expectRevert("msg.sender not approver"); + vm.prank(actor("somebody")); + governance.approve(proposalId, INDEX); + } + + function test_RevertIf_ProposalIsQueued() public { + uint256 newProposalId = makeValidProposal(); + vm.expectRevert("Proposal not dequeued"); + vm.prank(accApprover); + governance.approve(newProposalId, INDEX); + } + + function test_RevertIf_ProposalAlreadyApproved() public { + vm.startPrank(accApprover); + governance.approve(proposalId, INDEX); + vm.expectRevert("Proposal already approved"); + governance.approve(proposalId, INDEX); + } + + function test_returnsTrue_whenInReferendumStage() public { + // Dequeue the other proposal. + makeValidProposal(); + vm.prank(accApprover); + assertTrue(governance.approve(proposalId, INDEX)); + } + + function test_ShouldNotDeleteProposal_whenInReferendumStage() public { + // Dequeue the other proposal. + makeValidProposal(); + vm.prank(accApprover); + governance.approve(proposalId, INDEX); + assertTrue(governance.proposalExists(proposalId)); + } + + function test_NotRemoveProposalIDFromDequeued_whenInReferendumStage() public { + // Dequeue the other proposal. + makeValidProposal(); + vm.prank(accApprover); + governance.approve(proposalId, INDEX); + uint256[] memory dequeued = governance.getDequeue(); + + assertEq(dequeued.length, 1, "only one is dequeued"); + assertEq(dequeued[0], proposalId); + } + + function test_emitParticipationBaselineUpdatedEvent_whenInReferendumStage() public { + // Dequeue the other proposal. + makeValidProposal(); + + vm.expectEmit(true, true, true, true); + emit ProposalApproved(proposalId); + vm.prank(accApprover); + governance.approve(proposalId, INDEX); + } + + function test_returnFalse_whenPastReferendumStage() public { + makeValidProposal(); + vm.warp(block.timestamp + REFERENDUM_STAGE_DURATION + 1); + vm.prank(accApprover); + assertFalse(governance.approve(proposalId, 0)); + } + + function test_deleteProposal_whenPastReferendumStage() public { + makeValidProposal(); + vm.warp(block.timestamp + REFERENDUM_STAGE_DURATION + 1); + vm.prank(accApprover); + governance.approve(proposalId, INDEX); + assertFalse(governance.proposalExists(proposalId)); + } + + function test_removeProposalIDFromDequeued_whenPastReferendumStage() public { + makeValidProposal(); + vm.warp(block.timestamp + REFERENDUM_STAGE_DURATION + 1); + vm.prank(accApprover); + governance.approve(proposalId, INDEX); + + uint256[] memory dequeued = governance.getDequeue(); + assertEq(dequeued.length, 1); + assertNotEq(dequeued[0], proposalId); + } + + function test_addIndexToEmptyIndices_whenPastReferendumStage() public { + makeValidProposal(); + vm.warp(block.timestamp + REFERENDUM_STAGE_DURATION + 1); + vm.prank(accApprover); + governance.approve(proposalId, INDEX); + assertEq(governance.emptyIndices(0), INDEX); + } + + // TODO Fix when migrate to 0.8 + function SKIPtest_NoEmitParticipationBaselineUpdatedEvent_whenPastReferendumStage() public { + makeValidProposal(); + vm.warp(block.timestamp + REFERENDUM_STAGE_DURATION + 1); + vm.recordLogs(); + vm.prank(accApprover); + // Vm.Log[] memory entries = vm.getRecordedLogs(); + // assertEq(entries.length, 0); + } +} + +contract GovernanceRevokeVotes is GovernanceBaseTest { + event ProposalVoteRevokedV2( + uint256 indexed proposalId, + address indexed account, + uint256 yesVotes, + uint256 noVotes, + uint256 abstainVotes + ); + + uint256 numVoted; + + function setUp() public { + super.setUp(); + vm.prank(accOwner); + governance.setConcurrentProposals(3); + + makeValidProposal(); + makeValidProposal(); + makeValidProposal(); + + vm.warp(block.timestamp + governance.dequeueFrequency()); + + vm.startPrank(accApprover); + governance.approve(1, 0); + governance.approve(2, 1); + governance.approve(3, 2); + vm.stopPrank(); + + vm.startPrank(accVoter); + } + + modifier voteForEachNumVoted() { + for (uint256 _numVoted = 0; _numVoted < 3; _numVoted++) { + uint256 snapshot = vm.snapshot(); + numVoted = _numVoted; + + for (uint256 i = 0; i < numVoted; i++) { + governance.vote(i + 1, i, Proposals.VoteValue.Yes); + } + + _; + vm.revertTo(snapshot); + } + } + + modifier votePartiallyForEachNumVoted() { + for (uint256 _numVoted = 0; _numVoted < 3; _numVoted++) { + uint256 snapshot = vm.snapshot(); + numVoted = _numVoted; + + for (uint256 i = 0; i < numVoted; i++) { + governance.votePartially(i + 1, i, 10, 30, 0); + } + + _; + vm.revertTo(snapshot); + } + } + + function test_unsetMostRecentReferendumProposalVotedOn_whenAccountHasVotedOnXProposal() + public + voteForEachNumVoted + { + governance.revokeVotes(); + assertEq(governance.getMostRecentReferendumProposal(accVoter), 0); + } + + function test_isVotingReturnsFalse_whenAccountHasVotedOnXProposal() public voteForEachNumVoted { + governance.revokeVotes(); + assertFalse(governance.isVoting(accVoter)); + } + + function test_shouldEmitProposalVoteRevokedV2EventXtimes_whenAccountHasVotedOnXProposal() + public + voteForEachNumVoted + { + for (uint256 i = 0; i < numVoted; i++) { + vm.expectEmit(true, true, true, true); + emit ProposalVoteRevokedV2(i + 1, accVoter, VOTER_GOLD, 0, 0); + } + governance.revokeVotes(); + } + + function test_notRevertWhenProposalsAreNotInTheReferendumStage_whenAccountHasVotedOnXProposal() + public + voteForEachNumVoted + { + vm.warp(governance.getReferendumStageDuration()); + assertTrue(governance.revokeVotes()); + } + + function test_unsetMostRecentReferendumProposalVotedOn_whenAccountHasVotedPartiallyOnXProposal() + public + votePartiallyForEachNumVoted + { + governance.revokeVotes(); + assertEq(governance.getMostRecentReferendumProposal(accVoter), 0); + } + + function test_isVotingReturnsFalse_whenAccountHasVotedPartiallyOnXProposal() + public + votePartiallyForEachNumVoted + { + governance.revokeVotes(); + assertFalse(governance.isVoting(accVoter)); + assertEq(governance.getAmountOfGoldUsedForVoting(accVoter), 0); + } + + function test_shouldEmitProposalVoteRevokedV2EventXtimes_whenAccountHasVotedPartiallyOnXProposal() + public + votePartiallyForEachNumVoted + { + for (uint256 i = 0; i < numVoted; i++) { + vm.expectEmit(true, true, true, true); + emit ProposalVoteRevokedV2(i + 1, accVoter, 10, 30, 0); + } + governance.revokeVotes(); + } + + function test_notRevertWhenProposalsAreNotInTheReferendumStage_whenAccountHasVotedPartiallyOnXProposal() + public + votePartiallyForEachNumVoted + { + vm.warp(governance.getReferendumStageDuration()); + assertTrue(governance.revokeVotes()); + } +} + +contract GovernanceVoteWhenProposalIsApproved is GovernanceBaseTest { + event ProposalVotedV2( + uint256 indexed proposalId, + address indexed account, + uint256 yesVotes, + uint256 noVotes, + uint256 abstainVotes + ); + + event ParticipationBaselineUpdated(uint256 participationBaseline); + + uint256 proposalId; + + function setUp() public { + super.setUp(); + proposalId = makeValidProposal(); + vm.warp(block.timestamp + governance.dequeueFrequency()); + + vm.prank(accApprover); + governance.approve(proposalId, 0); + } + + function test_returnTrue() public { + vm.prank(accVoter); + assertTrue(governance.vote(proposalId, 0, Proposals.VoteValue.Yes)); + } + + function test_incrementVoteTotals() public { + vm.prank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + (uint256 yes, , ) = governance.getVoteTotals(proposalId); + assertEq(yes, VOTER_GOLD); + } + + function test_SetVotersVoteRecord() public { + vm.prank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + (uint256 recordProposalId, , , uint256 yesVotesRecord, uint256 noVotesRecord, uint256 abstainVotesRecord) = governance + .getVoteRecord(accVoter, 0); + assertEq(recordProposalId, proposalId); + assertEq(yesVotesRecord, VOTER_GOLD); + assertEq(noVotesRecord, 0); + assertEq(abstainVotesRecord, 0); + } + + function test_SetMostRecentReferendumProposalVotedOn() public { + vm.prank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + assertEq(governance.getMostRecentReferendumProposal(accVoter), proposalId); + } + + function test_emitProposalVotedV2Event() public { + vm.expectEmit(true, true, true, true); + emit ProposalVotedV2(proposalId, accVoter, VOTER_GOLD, 0, 0); + vm.prank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + } + + function test_RevertIf_AccountWeightIs0() public { + mockLockedGold.setAccountTotalGovernancePower(accVoter, 0); + vm.expectRevert("Voter weight zero"); + vm.prank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + } + + function test_RevertIf_IndexIsOutOfBounds() public { + vm.expectRevert("Provided index greater than dequeue length."); + vm.prank(accVoter); + governance.vote(proposalId, 1, Proposals.VoteValue.Yes); + } + + function test_RevertIf_ProposalIdDoesNotMatchTheIndex() public { + uint256 otherProposalId = makeValidProposal(); + vm.warp(block.timestamp + governance.dequeueFrequency()); + vm.expectRevert("Proposal not dequeued"); + vm.prank(accVoter); + governance.vote(otherProposalId, 0, Proposals.VoteValue.Yes); + } + + function test_setMostRecentReferendumProposalToTheYoungestProposalVotedOn_WhenVotingOnTwoProposals() + public + { + uint256 sndProposal = makeAndApproveProposal(1); + vm.startPrank(accVoter); + governance.vote(sndProposal, 1, Proposals.VoteValue.Yes); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + assertEq(governance.getMostRecentReferendumProposal(accVoter), sndProposal); + } + + function test_IsVotingReturnsTrue_WhenVotingOnTwoProposals() public { + uint256 sndProposal = makeAndApproveProposal(1); + vm.startPrank(accVoter); + governance.vote(sndProposal, 1, Proposals.VoteValue.Yes); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + assertTrue(governance.isVoting(accVoter)); + } + + function test_IsVotingReturnsTrue_WhenVotingOnTwoProposalsAfterFirstProposalExpires() public { + uint256 sndProposal = makeAndApproveProposal(1); + vm.startPrank(accVoter); + governance.vote(sndProposal, 1, Proposals.VoteValue.Yes); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + vm.warp(block.timestamp + governance.getReferendumStageDuration() - 10); + assertTrue(governance.isVoting(accVoter)); + } + + function test_IsVotingReturnsFalse_WhenVotingOnTwoProposalsAfterBothProposalExpires() public { + uint256 sndProposal = makeAndApproveProposal(1); + vm.startPrank(accVoter); + governance.vote(sndProposal, 1, Proposals.VoteValue.Yes); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + vm.warp(block.timestamp + governance.getReferendumStageDuration() + 1); + assertFalse(governance.isVoting(accVoter)); + } + + function test_ModifyVoteTotals_WhenChangingVoteFromYesToNo() public { + vm.startPrank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + governance.vote(proposalId, 0, Proposals.VoteValue.No); + (uint256 yes, uint256 no, uint256 abstain) = governance.getVoteTotals(proposalId); + assertEq(yes, 0); + assertEq(no, VOTER_GOLD); + } + + function test_UpdateVotersVoteRecord_WhenChangingVoteFromYesToNo() public { + vm.startPrank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + governance.vote(proposalId, 0, Proposals.VoteValue.No); + (uint256 id, uint256 _1, uint256 _2, uint256 yes, uint256 no, uint256 abstain) = governance + .getVoteRecord(accVoter, 0); + assertEq(id, proposalId); + assertEq(yes, 0); + assertEq(no, VOTER_GOLD); + } + + function test_ModifyVoteTotals_WhenChangingVoteFromNoToAbstain() public { + vm.startPrank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.No); + governance.vote(proposalId, 0, Proposals.VoteValue.Abstain); + (uint256 yes, uint256 no, uint256 abstain) = governance.getVoteTotals(proposalId); + assertEq(no, 0); + assertEq(abstain, VOTER_GOLD); + } + + function test_UpdateVotersVoteRecord_WhenChangingVoteFromNoToAbstain() public { + vm.startPrank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.No); + governance.vote(proposalId, 0, Proposals.VoteValue.Abstain); + (uint256 id, uint256 _1, uint256 _2, uint256 yes, uint256 no, uint256 abstain) = governance + .getVoteRecord(accVoter, 0); + assertEq(id, proposalId); + assertEq(no, 0); + assertEq(abstain, VOTER_GOLD); + } + + function test_ModifyVoteTotals_WhenChangingVoteFromAbstainToYes() public { + vm.startPrank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.Abstain); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + (uint256 yes, uint256 no, uint256 abstain) = governance.getVoteTotals(proposalId); + assertEq(abstain, 0); + assertEq(yes, VOTER_GOLD); + } + + function test_UpdateVotersVoteRecord_WhenChangingVoteFromAbstainToYes() public { + vm.startPrank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.Abstain); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + (uint256 id, uint256 _1, uint256 _2, uint256 yes, uint256 no, uint256 abstain) = governance + .getVoteRecord(accVoter, 0); + assertEq(id, proposalId); + assertEq(abstain, 0); + assertEq(yes, VOTER_GOLD); + } + + function test_RevertIf_IsPastReferendumStageAndPassing() public { + vm.startPrank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + vm.warp(block.timestamp + governance.getReferendumStageDuration()); + vm.expectRevert("Incorrect proposal state"); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + } + + function test_returnFalse_WhenIsPastReferendumStageAndFailing() public { + vm.startPrank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.No); + vm.warp(block.timestamp + governance.getReferendumStageDuration()); + assertFalse(governance.vote(proposalId, 0, Proposals.VoteValue.Yes)); + } + + function test_deleteProposal_WhenIsPastReferendumStageAndFailing() public { + vm.startPrank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.No); + vm.warp(block.timestamp + governance.getReferendumStageDuration()); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + assertFalse(governance.proposalExists(proposalId)); + } + + function test_removeProposalFromDequeued_WhenIsPastReferendumStageAndFailing() public { + vm.startPrank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.No); + vm.warp(block.timestamp + governance.getReferendumStageDuration()); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + + uint256[] memory dequeued = governance.getDequeue(); + assertEq(dequeued.length, 1); + assertNotEq(dequeued[0], proposalId); + } + + function test_AddsIndexToEmptyIndices_WhenIsPastReferendumStageAndFailing() public { + vm.startPrank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.No); + vm.warp(block.timestamp + governance.getReferendumStageDuration()); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + assertEq(governance.emptyIndices(0), 0); + } + + function test_UpdateTheParticipationBaseline_WhenIsPastReferendumStageAndFailing() public { + vm.startPrank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.No); + vm.warp(block.timestamp + governance.getReferendumStageDuration()); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + + (uint256 baseline, , , ) = governance.getParticipationParameters(); + assertEq(baseline, expectedParticipationBaseline); + } + + function test_emitParticipationBaselineUpdatedEvent_WhenIsPastReferendumStageAndFailing() public { + vm.startPrank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.No); + vm.warp(block.timestamp + governance.getReferendumStageDuration()); + + vm.expectEmit(true, true, true, true); + emit ParticipationBaselineUpdated(expectedParticipationBaseline); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + } +} + +contract GovernanceVoteWhenProposalIsApprovedAndHaveSigner is GovernanceBaseTest { + event ProposalVotedV2( + uint256 indexed proposalId, + address indexed account, + uint256 yesVotes, + uint256 noVotes, + uint256 abstainVotes + ); + + address accSigner; + uint256 proposalId; + + function setUp() public { + super.setUp(); + bytes32 voteSignerRole = keccak256(abi.encodePacked("celo.org/core/vote")); + + (address signer, uint256 signerPk) = actorWithPK("voterSigner"); + authorizeVoteSigner(signerPk, accVoter); + vm.prank(signer); + accounts.completeSignerAuthorization(accVoter, voteSignerRole); + accSigner = signer; + + proposalId = makeValidProposal(); + vm.warp(block.timestamp + governance.dequeueFrequency()); + + vm.prank(accApprover); + governance.approve(proposalId, 0); + } + + function test_returnTrue() public { + vm.prank(accSigner); + assertTrue(governance.vote(proposalId, 0, Proposals.VoteValue.Yes)); + } + + function test_incrementVoteTotals() public { + vm.prank(accSigner); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + (uint256 yes, , ) = governance.getVoteTotals(proposalId); + assertEq(yes, VOTER_GOLD); + } + + function test_SetVotersVoteRecord() public { + vm.prank(accSigner); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + (uint256 recordProposalId, , , uint256 yesVotesRecord, uint256 noVotesRecord, uint256 abstainVotesRecord) = governance + .getVoteRecord(accVoter, 0); + assertEq(recordProposalId, proposalId); + assertEq(yesVotesRecord, VOTER_GOLD); + assertEq(noVotesRecord, 0); + assertEq(abstainVotesRecord, 0); + } + + function test_SetMostRecentReferendumProposalVotedOn() public { + vm.prank(accSigner); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + assertEq(governance.getMostRecentReferendumProposal(accVoter), proposalId); + } + + function test_emitProposalVotedV2Event() public { + governance.dequeueProposalsIfReady(); + vm.expectEmit(true, true, true, true); + emit ProposalVotedV2(proposalId, accVoter, VOTER_GOLD, 0, 0); + vm.prank(accSigner); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + } + + function test_RevertIf_AccountWeightIs0() public { + mockLockedGold.setAccountTotalGovernancePower(accVoter, 0); + vm.expectRevert("Voter weight zero"); + vm.prank(accSigner); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + } +} + +contract GovernanceVoteWhenProposalIsNotApproved is GovernanceBaseTest { + event ProposalVotedV2( + uint256 indexed proposalId, + address indexed account, + uint256 yesVotes, + uint256 noVotes, + uint256 abstainVotes + ); + uint256 proposalId; + + function setUp() public { + super.setUp(); + proposalId = makeValidProposal(); + vm.warp(block.timestamp + governance.dequeueFrequency()); + } + + function test_returnTrue() public { + vm.prank(accVoter); + assertTrue(governance.vote(proposalId, 0, Proposals.VoteValue.Yes)); + } + + function test_incrementVoteTotals() public { + vm.prank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + (uint256 yes, , ) = governance.getVoteTotals(proposalId); + assertEq(yes, VOTER_GOLD); + } + + function test_SetVotersValueRecord() public { + vm.prank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + (uint256 recordProposalId, , , uint256 yesVotesRecord, uint256 noVotesRecord, uint256 abstainVotesRecord) = governance + .getVoteRecord(accVoter, 0); + assertEq(recordProposalId, proposalId); + assertEq(yesVotesRecord, VOTER_GOLD); + assertEq(noVotesRecord, 0); + assertEq(abstainVotesRecord, 0); + } + + function test_SetTheMostRecentReferendumProposalVotedOn() public { + vm.prank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + assertEq(governance.getMostRecentReferendumProposal(accVoter), proposalId); + } + + function test_emitProposalVotedV2Event() public { + governance.dequeueProposalsIfReady(); + vm.expectEmit(true, true, true, true); + emit ProposalVotedV2(proposalId, accVoter, VOTER_GOLD, 0, 0); + vm.prank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + } + + function test_RevertIf_accountWeightIs0() public { + mockLockedGold.setAccountTotalGovernancePower(accVoter, 0); + vm.expectRevert("Voter weight zero"); + vm.prank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + } + + function test_RevertIf_IndexIsOutOfBounds() public { + vm.expectRevert("Provided index greater than dequeue length."); + vm.prank(accVoter); + governance.vote(proposalId, 1, Proposals.VoteValue.Yes); + } + + function test_RevertIf_proposalIdDoesNotMatchTheIndex() public { + uint256 otherProposalId = makeValidProposal(); + vm.warp(block.timestamp + governance.dequeueFrequency()); + vm.expectRevert("Proposal not dequeued"); + vm.prank(accVoter); + governance.vote(otherProposalId, 0, Proposals.VoteValue.Yes); + } +} + +contract GovernanceVoteWhenVotingOnDifferentProposalWithSameIndex is GovernanceBaseTest { + function test_IgnoreVotesFromPreviousProposal() public { + uint256 proposalId1 = makeValidProposal(); + + vm.warp(block.timestamp + governance.dequeueFrequency()); + vm.prank(accApprover); + governance.approve(proposalId1, 0); + + vm.prank(accVoter); + governance.vote(proposalId1, 0, Proposals.VoteValue.Yes); + + vm.warp( + block.timestamp + + governance.getReferendumStageDuration() + + governance.getExecutionStageDuration() + ); + assertEq(governance.dequeued(0), proposalId1); + + uint256 proposalId2 = makeValidProposal(); + + governance.execute(proposalId1, 0); + assertFalse(governance.proposalExists(proposalId1)); + + vm.warp(block.timestamp + governance.dequeueFrequency() + 1); + governance.dequeueProposalsIfReady(); + vm.prank(accApprover); + governance.approve(proposalId2, 0); + assertTrue(governance.proposalExists(proposalId2)); + + assertEq(governance.dequeued(0), proposalId2); + + address sndVoter = actor("sndVoter"); + uint256 sndVoterWeight = 100; + vm.prank(sndVoter); + accounts.createAccount(); + mockLockedGold.setAccountTotalGovernancePower(sndVoter, sndVoterWeight); + + vm.prank(sndVoter); + governance.vote(proposalId2, 0, Proposals.VoteValue.Yes); + vm.prank(accVoter); + governance.vote(proposalId2, 0, Proposals.VoteValue.No); + + (uint256 yes, uint256 no, uint256 abstain) = governance.getVoteTotals(proposalId2); + + assertEq(yes, sndVoterWeight); + assertEq(no, VOTER_GOLD); + assertEq(abstain, 0); + } +} + +contract GovernanceVotePartiallyWhenProposalIsApproved is GovernanceBaseTest { + event ProposalVotedV2( + uint256 indexed proposalId, + address indexed account, + uint256 yesVotes, + uint256 noVotes, + uint256 abstainVotes + ); + + event ParticipationBaselineUpdated(uint256 participationBaseline); + + uint256 proposalId; + + function setUp() public { + super.setUp(); + proposalId = makeValidProposal(); + vm.warp(block.timestamp + governance.dequeueFrequency()); + + vm.prank(accApprover); + governance.approve(proposalId, 0); + } + + function test_returnTrue() public { + vm.prank(accVoter); + assertTrue(governance.votePartially(proposalId, 0, VOTER_GOLD, 0, 0)); + } + + function test_incrementVoteTotals() public { + vm.prank(accVoter); + governance.votePartially(proposalId, 0, VOTER_GOLD, 0, 0); + (uint256 yes, , ) = governance.getVoteTotals(proposalId); + assertEq(yes, VOTER_GOLD); + } + + function test_incrementVoteTotalsWhenVotingPartially() public { + vm.prank(accVoter); + governance.votePartially(proposalId, 0, 10, 50, 30); + (uint256 yes, uint256 no, uint256 abstain) = governance.getVoteTotals(proposalId); + assertEq(yes, 10); + assertEq(no, 50); + assertEq(abstain, 30); + } + + function test_SetTheVotersVoteRecord() public { + vm.prank(accVoter); + governance.votePartially(proposalId, 0, VOTER_GOLD, 0, 0); + (uint256 recordProposalId, , , uint256 yesVotesRecord, uint256 noVotesRecord, uint256 abstainVotesRecord) = governance + .getVoteRecord(accVoter, 0); + assertEq(recordProposalId, proposalId); + assertEq(yesVotesRecord, VOTER_GOLD); + assertEq(noVotesRecord, 0); + assertEq(abstainVotesRecord, 0); + } + + function test_SetMostRecentReferendumProposalVotedOn() public { + vm.prank(accVoter); + governance.votePartially(proposalId, 0, VOTER_GOLD, 0, 0); + assertEq(governance.getMostRecentReferendumProposal(accVoter), proposalId); + } + + function test_emitProposalVotedV2Event() public { + vm.expectEmit(true, true, true, true); + emit ProposalVotedV2(proposalId, accVoter, VOTER_GOLD, 0, 0); + vm.prank(accVoter); + governance.votePartially(proposalId, 0, VOTER_GOLD, 0, 0); + } + + function test_RevertIf_AccountWeightIsZero() public { + mockLockedGold.setAccountTotalGovernancePower(accVoter, 0); + vm.expectRevert("Voter doesn't have enough locked Celo (formerly known as Celo Gold)"); + vm.prank(accVoter); + governance.votePartially(proposalId, 0, VOTER_GOLD, 0, 0); + } + + function test_RevertIf_AccountDoesNotHaveEnoughGold() public { + vm.expectRevert("Voter doesn't have enough locked Celo (formerly known as Celo Gold)"); + vm.prank(accVoter); + governance.votePartially(proposalId, 0, VOTER_GOLD + 1, 0, 0); + } + + function test_RevertIf_AccountDoesNotHaveEnoughGoldWhenVotingPartially() public { + vm.expectRevert("Voter doesn't have enough locked Celo (formerly known as Celo Gold)"); + vm.prank(accVoter); + governance.votePartially(proposalId, 0, VOTER_GOLD, VOTER_GOLD, 0); + } + + function test_RevertIf_IndexIsOutOfBounds() public { + vm.expectRevert("Provided index greater than dequeue length."); + vm.prank(accVoter); + governance.votePartially(proposalId, 1, VOTER_GOLD, 0, 0); + } + + function test_RevertIf_ProposalIdDoesNotMatchTheIndex() public { + uint256 otherProposalId = makeValidProposal(); + vm.warp(block.timestamp + governance.dequeueFrequency()); + + vm.expectRevert("Proposal not dequeued"); + vm.prank(accVoter); + governance.votePartially(otherProposalId, 0, VOTER_GOLD, 0, 0); + } + + function test_setMostRecentReferendumProposalToTheYoungestProposalVotedOn_WhenVotingOnTwoProposals() + public + { + uint256 sndProposal = makeAndApproveProposal(1); + vm.startPrank(accVoter); + governance.votePartially(sndProposal, 1, VOTER_GOLD, 0, 0); + governance.votePartially(proposalId, 0, VOTER_GOLD, 0, 0); + assertEq(governance.getMostRecentReferendumProposal(accVoter), sndProposal); + } + + function test_IsVotingReturnsTrue_WhenVotingOnTwoProposals() public { + uint256 sndProposal = makeAndApproveProposal(1); + vm.startPrank(accVoter); + governance.votePartially(sndProposal, 1, VOTER_GOLD, 0, 0); + governance.votePartially(proposalId, 0, VOTER_GOLD, 0, 0); + assertTrue(governance.isVoting(accVoter)); + } + + function test_IsVotingReturnsTrue_WhenVotingOnTwoProposalsAfterFirstProposalExpires() public { + uint256 sndProposal = makeAndApproveProposal(1); + vm.startPrank(accVoter); + governance.votePartially(sndProposal, 1, VOTER_GOLD, 0, 0); + governance.votePartially(proposalId, 0, VOTER_GOLD, 0, 0); + vm.warp(block.timestamp + governance.getReferendumStageDuration() - 10); + assertTrue(governance.isVoting(accVoter)); + } + + function test_IsVotingReturnsFalse_WhenVotingOnTwoProposalsAfterBothProposalExpires() public { + uint256 sndProposal = makeAndApproveProposal(1); + vm.startPrank(accVoter); + governance.votePartially(sndProposal, 1, VOTER_GOLD, 0, 0); + governance.votePartially(proposalId, 0, VOTER_GOLD, 0, 0); + vm.warp(block.timestamp + governance.getReferendumStageDuration() + 1); + assertFalse(governance.isVoting(accVoter)); + } + + function test_ModifyVoteTotals_WhenChangingPartialVotes() public { + vm.startPrank(accVoter); + governance.votePartially(proposalId, 0, 10, 50, 30); + governance.votePartially(proposalId, 0, 30, 20, 40); + (uint256 yes, uint256 no, uint256 abstain) = governance.getVoteTotals(proposalId); + assertEq(yes, 30); + assertEq(no, 20); + assertEq(abstain, 40); + } + + function test_UpdateVotersVoteRecord_WhenChangingPartialVotes() public { + vm.startPrank(accVoter); + governance.votePartially(proposalId, 0, 10, 50, 30); + governance.votePartially(proposalId, 0, 30, 20, 40); + (uint256 id, uint256 _1, uint256 _2, uint256 yes, uint256 no, uint256 abstain) = governance + .getVoteRecord(accVoter, 0); + assertEq(id, proposalId); + assertEq(yes, 30); + assertEq(no, 20); + assertEq(abstain, 40); + } + + function test_RevertIf_IsPastReferendumStageAndPassing() public { + vm.startPrank(accVoter); + governance.votePartially(proposalId, 0, VOTER_GOLD, 0, 0); + vm.warp(block.timestamp + governance.getReferendumStageDuration()); + vm.expectRevert("Incorrect proposal state"); + governance.votePartially(proposalId, 0, 10, 50, 30); + } + + function test_returnFalse_WhenIsPastReferendumStageAndFailing() public { + vm.startPrank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.No); + vm.warp(block.timestamp + governance.getReferendumStageDuration()); + assertFalse(governance.votePartially(proposalId, 0, VOTER_GOLD, 0, 0)); + } + + function test_deleteProposal_WhenIsPastReferendumStageAndFailing() public { + vm.startPrank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.No); + vm.warp(block.timestamp + governance.getReferendumStageDuration()); + governance.votePartially(proposalId, 0, VOTER_GOLD, 0, 0); + assertFalse(governance.proposalExists(proposalId)); + } + + function test_removeProposalFromDequeued_WhenIsPastReferendumStageAndFailing() public { + vm.startPrank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.No); + vm.warp(block.timestamp + governance.getReferendumStageDuration()); + governance.votePartially(proposalId, 0, VOTER_GOLD, 0, 0); + + uint256[] memory dequeued = governance.getDequeue(); + assertEq(dequeued.length, 1); + assertNotEq(dequeued[0], proposalId); + } + + function test_AddsIndexToEmptyIndices_WhenIsPastReferendumStageAndFailing() public { + vm.startPrank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.No); + vm.warp(block.timestamp + governance.getReferendumStageDuration()); + governance.votePartially(proposalId, 0, VOTER_GOLD, 0, 0); + assertEq(governance.emptyIndices(0), 0); + } + + function test_UpdateTheParticipationBaseline_WhenIsPastReferendumStageAndFailing() public { + vm.startPrank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.No); + vm.warp(block.timestamp + governance.getReferendumStageDuration()); + governance.votePartially(proposalId, 0, VOTER_GOLD, 0, 0); + + (uint256 baseline, , , ) = governance.getParticipationParameters(); + assertEq(baseline, expectedParticipationBaseline); + } + + function test_emitParticipationBaselineUpdatedEvent_WhenIsPastReferendumStageAndFailing() public { + vm.startPrank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.No); + vm.warp(block.timestamp + governance.getReferendumStageDuration()); + + vm.expectEmit(true, true, true, true); + emit ParticipationBaselineUpdated(expectedParticipationBaseline); + governance.votePartially(proposalId, 0, VOTER_GOLD, 0, 0); + } +} + +contract GovernanceVotePartiallyWhenProposalIsApprovedAndHaveSigner is GovernanceBaseTest { + event ProposalVotedV2( + uint256 indexed proposalId, + address indexed account, + uint256 yesVotes, + uint256 noVotes, + uint256 abstainVotes + ); + + address accSigner; + uint256 proposalId; + + function setUp() public { + super.setUp(); + bytes32 voteSignerRole = keccak256(abi.encodePacked("celo.org/core/vote")); + + (address signer, uint256 signerPk) = actorWithPK("voterSigner"); + authorizeVoteSigner(signerPk, accVoter); + vm.prank(signer); + accounts.completeSignerAuthorization(accVoter, voteSignerRole); + accSigner = signer; + + proposalId = makeValidProposal(); + vm.warp(block.timestamp + governance.dequeueFrequency()); + + vm.prank(accApprover); + governance.approve(proposalId, 0); + } + + function test_returnTrue() public { + vm.prank(accSigner); + assertTrue(governance.votePartially(proposalId, 0, VOTER_GOLD, 0, 0)); + } + + function test_incrementVoteTotals() public { + vm.prank(accSigner); + governance.votePartially(proposalId, 0, VOTER_GOLD, 0, 0); + (uint256 yes, , ) = governance.getVoteTotals(proposalId); + assertEq(yes, VOTER_GOLD); + } + + function test_incrementVoteTotalsWhenVotingPartially() public { + vm.prank(accSigner); + governance.votePartially(proposalId, 0, 10, 50, 30); + (uint256 yes, uint256 no, uint256 abstain) = governance.getVoteTotals(proposalId); + assertEq(yes, 10); + assertEq(no, 50); + assertEq(abstain, 30); + } + + function test_SetTheVotersVoteRecord() public { + vm.prank(accSigner); + governance.votePartially(proposalId, 0, VOTER_GOLD, 0, 0); + (uint256 recordProposalId, , , uint256 yesVotesRecord, uint256 noVotesRecord, uint256 abstainVotesRecord) = governance + .getVoteRecord(accVoter, 0); + assertEq(recordProposalId, proposalId); + assertEq(yesVotesRecord, VOTER_GOLD); + assertEq(noVotesRecord, 0); + assertEq(abstainVotesRecord, 0); + } + + function test_SetMostRecentReferendumProposalVotedOn() public { + vm.prank(accSigner); + governance.votePartially(proposalId, 0, VOTER_GOLD, 0, 0); + assertEq(governance.getMostRecentReferendumProposal(accVoter), proposalId); + } + + function test_emitProposalVotedV2Event() public { + vm.expectEmit(true, true, true, true); + emit ProposalVotedV2(proposalId, accVoter, VOTER_GOLD, 0, 0); + vm.prank(accSigner); + governance.votePartially(proposalId, 0, VOTER_GOLD, 0, 0); + } + + function test_RevertIf_AccountWeightIs0() public { + mockLockedGold.setAccountTotalGovernancePower(accVoter, 0); + vm.expectRevert("Voter doesn't have enough locked Celo (formerly known as Celo Gold)"); + vm.prank(accSigner); + governance.votePartially(proposalId, 0, VOTER_GOLD, 0, 0); + } + + function test_RevertIf_AccountDoesNotHaveEnoughGold() public { + vm.expectRevert("Voter doesn't have enough locked Celo (formerly known as Celo Gold)"); + vm.prank(accSigner); + governance.votePartially(proposalId, 0, VOTER_GOLD + 1, 0, 0); + } + + function test_RevertIf_AccountDoesNotHaveEnoughGoldWhenVotingPartially() public { + vm.expectRevert("Voter doesn't have enough locked Celo (formerly known as Celo Gold)"); + vm.prank(accSigner); + governance.votePartially(proposalId, 0, VOTER_GOLD, VOTER_GOLD, 0); + } + + function test_RevertIf_IndexIsOutOfBounds() public { + vm.expectRevert("Provided index greater than dequeue length."); + vm.prank(accSigner); + governance.votePartially(proposalId, 1, VOTER_GOLD, 0, 0); + } + + function test_RevertIf_ProposalIdDoesNotMatchTheIndex() public { + uint256 otherProposalId = makeValidProposal(); + vm.warp(block.timestamp + governance.dequeueFrequency()); + + vm.expectRevert("Proposal not dequeued"); + vm.prank(accSigner); + governance.votePartially(otherProposalId, 0, VOTER_GOLD, 0, 0); + } +} + +contract GovernanceVotePartiallyWhenProposalIsNotApproved is GovernanceBaseTest { + event ProposalVotedV2( + uint256 indexed proposalId, + address indexed account, + uint256 yesVotes, + uint256 noVotes, + uint256 abstainVotes + ); + uint256 proposalId; + + function setUp() public { + super.setUp(); + proposalId = makeValidProposal(); + vm.warp(block.timestamp + governance.dequeueFrequency()); + } + + function test_returnTrue() public { + vm.prank(accVoter); + assertTrue(governance.votePartially(proposalId, 0, VOTER_GOLD, 0, 0)); + } + + function test_incrementVoteTotals() public { + vm.prank(accVoter); + governance.votePartially(proposalId, 0, VOTER_GOLD, 0, 0); + (uint256 yes, , ) = governance.getVoteTotals(proposalId); + assertEq(yes, VOTER_GOLD); + } + + function test_SetVotersValueRecord() public { + vm.prank(accVoter); + governance.votePartially(proposalId, 0, VOTER_GOLD, 0, 0); + (uint256 recordProposalId, , , uint256 yesVotesRecord, uint256 noVotesRecord, uint256 abstainVotesRecord) = governance + .getVoteRecord(accVoter, 0); + assertEq(recordProposalId, proposalId); + assertEq(yesVotesRecord, VOTER_GOLD); + assertEq(noVotesRecord, 0); + assertEq(abstainVotesRecord, 0); + } + + function test_SetTheMostRecentReferendumProposalVotedOn() public { + vm.prank(accVoter); + governance.votePartially(proposalId, 0, VOTER_GOLD, 0, 0); + assertEq(governance.getMostRecentReferendumProposal(accVoter), proposalId); + } + + function test_emitProposalVotedV2Event() public { + governance.dequeueProposalsIfReady(); + vm.expectEmit(true, true, true, true); + emit ProposalVotedV2(proposalId, accVoter, VOTER_GOLD, 0, 0); + vm.prank(accVoter); + governance.votePartially(proposalId, 0, VOTER_GOLD, 0, 0); + } + + function test_RevertIf_AccountWeightIsZero() public { + mockLockedGold.setAccountTotalGovernancePower(accVoter, 0); + vm.expectRevert("Voter doesn't have enough locked Celo (formerly known as Celo Gold)"); + vm.prank(accVoter); + governance.votePartially(proposalId, 0, VOTER_GOLD, 0, 0); + } + + function test_RevertIf_IndexIsOutOfBounds() public { + vm.expectRevert("Provided index greater than dequeue length."); + vm.prank(accVoter); + governance.votePartially(proposalId, 1, VOTER_GOLD, 0, 0); + } + + function test_RevertIf_proposalIdDoesNotMatchTheIndex() public { + uint256 otherProposalId = makeValidProposal(); + vm.warp(block.timestamp + governance.dequeueFrequency()); + vm.expectRevert("Proposal not dequeued"); + vm.prank(accVoter); + governance.votePartially(otherProposalId, 0, VOTER_GOLD, 0, 0); + } +} + +contract GovernanceVotePartiallyWhenVotingOnDifferentProposalWithSameIndex is GovernanceBaseTest { + function test_IgnoreVotesFromPreviousProposal() public { + uint256 proposalId1 = makeValidProposal(); + + vm.warp(block.timestamp + governance.dequeueFrequency()); + vm.prank(accApprover); + governance.approve(proposalId1, 0); + + vm.prank(accVoter); + governance.votePartially(proposalId1, 0, VOTER_GOLD, 0, 0); + + vm.warp( + block.timestamp + + governance.getReferendumStageDuration() + + governance.getExecutionStageDuration() + ); + assertEq(governance.dequeued(0), proposalId1); + + uint256 proposalId2 = makeValidProposal(); + + governance.execute(proposalId1, 0); + assertFalse(governance.proposalExists(proposalId1)); + + vm.warp(block.timestamp + governance.dequeueFrequency() + 1); + governance.dequeueProposalsIfReady(); + vm.prank(accApprover); + governance.approve(proposalId2, 0); + assertTrue(governance.proposalExists(proposalId2)); + + assertEq(governance.dequeued(0), proposalId2); + + address sndVoter = actor("sndVoter"); + uint256 sndVoterWeight = 100; + vm.prank(sndVoter); + accounts.createAccount(); + mockLockedGold.setAccountTotalGovernancePower(sndVoter, sndVoterWeight); + + vm.prank(sndVoter); + governance.votePartially(proposalId2, 0, sndVoterWeight, 0, 0); + vm.prank(accVoter); + governance.votePartially(proposalId2, 0, 0, VOTER_GOLD, 0); + + (uint256 yes, uint256 no, uint256 abstain) = governance.getVoteTotals(proposalId2); + + assertEq(yes, sndVoterWeight); + assertEq(no, VOTER_GOLD); + assertEq(abstain, 0); + } +} + +contract GovernanceExecute is GovernanceBaseTest { + event ParticipationBaselineUpdated(uint256 participationBaseline); + event ProposalExecuted(uint256 indexed proposalId); + + uint256 proposalId; + + function setupProposalCanExecute() private { + proposalId = makeAndApproveProposal(0); + vm.prank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + vm.warp(block.timestamp + REFERENDUM_STAGE_DURATION); + } + + function test_returnTrue_WhenProposalCanExecute() public { + setupProposalCanExecute(); + assertTrue(governance.execute(proposalId, 0)); + } + + function test_executeProposal_WhenProposalCanExecute() public { + setupProposalCanExecute(); + governance.execute(proposalId, 0); + assertEq(testTransactions.getValue(1), 1); + } + + function test_deleteProposal_WhenProposalCanExecute() public { + setupProposalCanExecute(); + governance.execute(proposalId, 0); + assertFalse(governance.proposalExists(proposalId)); + } + + function test_updateParticipationBaseline_WhenProposalCanExecute() public { + setupProposalCanExecute(); + governance.execute(proposalId, 0); + (uint256 baseline, , , ) = governance.getParticipationParameters(); + assertEq(baseline, expectedParticipationBaseline); + } + + function test_emitProposalExecutedEvent_WhenProposalCanExecute() public { + setupProposalCanExecute(); + vm.expectEmit(true, true, true, true); + emit ProposalExecuted(proposalId); + governance.execute(proposalId, 0); + } + + function test_emitParticipationBaselineUpdatedEvent_WhenProposalCanExecute() public { + setupProposalCanExecute(); + vm.expectEmit(true, true, true, true); + emit ParticipationBaselineUpdated(expectedParticipationBaseline); + governance.execute(proposalId, 0); + } + + function test_RevertIf_IndexIsOutOfBounds_WhenProposalCanExecute() public { + setupProposalCanExecute(); + vm.expectRevert("Provided index greater than dequeue length."); + governance.execute(proposalId, 1); + } + + function setupWhenProposalApprovedInExecutionStage() private { + proposalId = makeValidProposal(); + vm.warp(block.timestamp + governance.dequeueFrequency()); + vm.prank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + vm.warp(block.timestamp + REFERENDUM_STAGE_DURATION); + vm.prank(accApprover); + governance.approve(proposalId, 0); + } + + function test_returnTrue_WhenProposalApprovedInExecutionStage() public { + setupWhenProposalApprovedInExecutionStage(); + assertTrue(governance.execute(proposalId, 0)); + } + + function test_executeProposal_WhenProposalApprovedInExecutionStage() public { + setupWhenProposalApprovedInExecutionStage(); + governance.execute(proposalId, 0); + assertEq(testTransactions.getValue(1), 1); + } + + function test_deleteProposal_WhenProposalApprovedInExecutionStage() public { + setupWhenProposalApprovedInExecutionStage(); + governance.execute(proposalId, 0); + assertFalse(governance.proposalExists(proposalId)); + } + + function test_updateParticipationBaseline_WhenProposalApprovedInExecutionStage() public { + setupWhenProposalApprovedInExecutionStage(); + governance.execute(proposalId, 0); + (uint256 baseline, , , ) = governance.getParticipationParameters(); + assertEq(baseline, expectedParticipationBaseline); + } + + function test_emitProposalExecutedEvent_WhenProposalApprovedInExecutionStage() public { + setupWhenProposalApprovedInExecutionStage(); + vm.expectEmit(true, true, true, true); + emit ProposalExecuted(proposalId); + governance.execute(proposalId, 0); + } + + function test_emitParticipationBaselineUpdatedEvent_WhenProposalApprovedInExecutionStage() + public + { + setupWhenProposalApprovedInExecutionStage(); + vm.expectEmit(true, true, true, true); + emit ParticipationBaselineUpdated(expectedParticipationBaseline); + governance.execute(proposalId, 0); + } + + function test_RevertIf_IndexIsOutOfBounds_WhenProposalApprovedInExecutionStage() public { + setupWhenProposalApprovedInExecutionStage(); + vm.expectRevert("Provided index greater than dequeue length."); + governance.execute(proposalId, 1); + } + + function test_RevertIf_ProposalIsNotApproved() public { + proposalId = makeValidProposal(); + + vm.warp(block.timestamp + governance.dequeueFrequency()); + vm.prank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + vm.warp(block.timestamp + REFERENDUM_STAGE_DURATION); + + vm.expectRevert("Proposal not approved"); + governance.execute(proposalId, 0); + } + + function test_RevertIf_ProposalCannotExecuteSuccessfully() public { + proposalId = governance.propose.value(DEPOSIT)( + failingProp.values, + failingProp.destinations, + failingProp.data, + failingProp.dataLengths, + failingProp.description + ); + + vm.warp(block.timestamp + governance.dequeueFrequency()); + vm.prank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + vm.warp(block.timestamp + REFERENDUM_STAGE_DURATION); + vm.prank(accApprover); + governance.approve(proposalId, 0); + + vm.expectRevert("Proposal execution failed"); + governance.execute(proposalId, 0); + } + + function test_RevertIf_ProposalCannotExecuteBecauseInvalidContractAddress() public { + okProp.destinations[0] = actor("someAddress"); + proposalId = governance.propose.value(DEPOSIT)( + okProp.values, + okProp.destinations, + okProp.data, + okProp.dataLengths, + okProp.description + ); + + vm.warp(block.timestamp + governance.dequeueFrequency()); + vm.prank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + vm.warp(block.timestamp + REFERENDUM_STAGE_DURATION); + vm.prank(accApprover); + governance.approve(proposalId, 0); + + vm.expectRevert("Invalid contract address"); + governance.execute(proposalId, 0); + } + + function setup2TxProposal() private { + proposalId = governance.propose.value(DEPOSIT)( + twoTxProp.values, + twoTxProp.destinations, + twoTxProp.data, + twoTxProp.dataLengths, + twoTxProp.description + ); + vm.warp(block.timestamp + governance.dequeueFrequency()); + + vm.prank(accApprover); + governance.approve(proposalId, 0); + vm.prank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + vm.warp(block.timestamp + REFERENDUM_STAGE_DURATION); + } + + function test_returnTrue_When2TxProposal() public { + setup2TxProposal(); + assertTrue(governance.execute(proposalId, 0)); + } + + function test_executeProposal_When2TxProposal() public { + setup2TxProposal(); + governance.execute(proposalId, 0); + assertEq(testTransactions.getValue(1), 1); + assertEq(testTransactions.getValue(2), 1); + } + + function test_deleteProposal_When2TxProposal() public { + setup2TxProposal(); + governance.execute(proposalId, 0); + assertFalse(governance.proposalExists(proposalId)); + } + + function test_updateParticipationBaseline_When2TxProposal() public { + setup2TxProposal(); + governance.execute(proposalId, 0); + (uint256 baseline, , , ) = governance.getParticipationParameters(); + assertEq(baseline, expectedParticipationBaseline); + } + + function test_emitProposalExecutedEvent_When2TxProposal() public { + setup2TxProposal(); + vm.expectEmit(true, true, true, true); + emit ProposalExecuted(proposalId); + governance.execute(proposalId, 0); + } + + function test_emitParticipationBaselineUpdatedEvent_When2TxProposal() public { + setup2TxProposal(); + vm.expectEmit(true, true, true, true); + emit ParticipationBaselineUpdated(expectedParticipationBaseline); + governance.execute(proposalId, 0); + } + + function test_RevertIf_IndexIsOutOfBounds_When2TxProposal() public { + setup2TxProposal(); + vm.expectRevert("Provided index greater than dequeue length."); + governance.execute(proposalId, 1); + } + + function test_RevertIf_2TxProposalButFirstFails() public { + string memory setValueSignature = "setValue(uint256,uint256,bool)"; + bytes memory txDataFirst = abi.encodeWithSignature(setValueSignature, 1, 1, false); // fails + bytes memory txDataSecond = abi.encodeWithSignature(setValueSignature, 2, 1, true); + twoTxProp.data = txDataFirst.concat(txDataSecond); + + proposalId = governance.propose.value(DEPOSIT)( + twoTxProp.values, + twoTxProp.destinations, + twoTxProp.data, + twoTxProp.dataLengths, + twoTxProp.description + ); + vm.warp(block.timestamp + governance.dequeueFrequency()); + + vm.prank(accApprover); + governance.approve(proposalId, 0); + vm.prank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + vm.warp(block.timestamp + REFERENDUM_STAGE_DURATION); + + vm.expectRevert("Proposal execution failed"); + governance.execute(proposalId, 0); + } + + function test_RevertIf_2TxProposalButSecondFails() public { + string memory setValueSignature = "setValue(uint256,uint256,bool)"; + bytes memory txDataFirst = abi.encodeWithSignature(setValueSignature, 1, 1, true); + bytes memory txDataSecond = abi.encodeWithSignature(setValueSignature, 2, 1, false); // fails + twoTxProp.data = txDataFirst.concat(txDataSecond); + + proposalId = governance.propose.value(DEPOSIT)( + twoTxProp.values, + twoTxProp.destinations, + twoTxProp.data, + twoTxProp.dataLengths, + twoTxProp.description + ); + vm.warp(block.timestamp + governance.dequeueFrequency()); + + vm.prank(accApprover); + governance.approve(proposalId, 0); + vm.prank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + vm.warp(block.timestamp + REFERENDUM_STAGE_DURATION); + + vm.expectRevert("Proposal execution failed"); + governance.execute(proposalId, 0); + } + + function setupProposalPastExecutionStage() private { + proposalId = makeAndApproveProposal(0); + vm.prank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + vm.warp(block.timestamp + REFERENDUM_STAGE_DURATION); + vm.warp(block.timestamp + governance.getExecutionStageDuration()); + } + + function test_returnFalse_WhenProposalIsPastExecutionStage() public { + setupProposalPastExecutionStage(); + assertFalse(governance.execute(proposalId, 0)); + } + + function test_deleteProposal_WhenProposalIsPastExecutionStage() public { + setupProposalPastExecutionStage(); + governance.execute(proposalId, 0); + assertFalse(governance.proposalExists(proposalId)); + } + + function test_removeProposalFromDequeued_WhenProposalIsPastExecutionStage() public { + setupProposalPastExecutionStage(); + governance.execute(proposalId, 0); + + uint256[] memory dequeued = governance.getDequeue(); + assertEq(dequeued.length, 1); + assertNotEq(dequeued[0], proposalId); + } + + function test_AddsIndexToEmptyIndices_WhenProposalIsPastExecutionStage() public { + setupProposalPastExecutionStage(); + governance.execute(proposalId, 0); + assertEq(governance.emptyIndices(0), 0); + } + + function test_updateParticipationBaseline_WhenProposalIsPastExecutionStage() public { + setupProposalPastExecutionStage(); + governance.execute(proposalId, 0); + (uint256 baseline, , , ) = governance.getParticipationParameters(); + assertEq(baseline, expectedParticipationBaseline); + } + + function test_emitParticipationBaselineUpdatedEvent_WhenProposalIsPastExecutionStage() public { + setupProposalPastExecutionStage(); + vm.expectEmit(true, true, true, true); + emit ParticipationBaselineUpdated(expectedParticipationBaseline); + governance.execute(proposalId, 0); + } + + // TODO fix when migrate to 0.8 + function SKIPtest_NoEmitProposalExecutedWhenEmptyProposalNotApproved() public { + proposalId = governance.propose.value(DEPOSIT)( + emptyProp.values, + emptyProp.destinations, + emptyProp.data, + emptyProp.dataLengths, + "empty proposal" + ); + vm.warp(block.timestamp + governance.dequeueFrequency()); + + vm.prank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + + vm.warp(block.timestamp + REFERENDUM_STAGE_DURATION); + vm.warp(block.timestamp + governance.getExecutionStageDuration()); + + vm.recordLogs(); + governance.execute(proposalId, 0); + // Vm.Log[] memory entries = vm.getRecordedLogs(); + // assertEq(entries.length, 0); + } + + // TODO fix when migrate to 0.8 + function SKIPtest_NoEmitProposalExecutedWhenEmptyProposalNotPassing() public { + proposalId = governance.propose.value(DEPOSIT)( + emptyProp.values, + emptyProp.destinations, + emptyProp.data, + emptyProp.dataLengths, + "empty proposal" + ); + vm.warp(block.timestamp + governance.dequeueFrequency()); + + vm.prank(accApprover); + governance.approve(proposalId, 0); + + vm.warp(block.timestamp + REFERENDUM_STAGE_DURATION); + vm.warp(block.timestamp + governance.getExecutionStageDuration()); + + vm.recordLogs(); + governance.execute(proposalId, 0); + // Vm.Log[] memory entries = vm.getRecordedLogs(); + // assertEq(entries.length, 0); + } + + function setUpEmptyProposalReadyForExecution() public { + proposalId = governance.propose.value(DEPOSIT)( + emptyProp.values, + emptyProp.destinations, + emptyProp.data, + emptyProp.dataLengths, + "empty proposal" + ); + vm.warp(block.timestamp + governance.dequeueFrequency()); + + vm.prank(accApprover); + governance.approve(proposalId, 0); + + vm.prank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + + vm.warp(block.timestamp + REFERENDUM_STAGE_DURATION); + vm.warp(block.timestamp + governance.getExecutionStageDuration()); + } + + function test_returnTrue_WhenEmptyProposalReadyForExecution() public { + setUpEmptyProposalReadyForExecution(); + assertTrue(governance.execute(proposalId, 0)); + } + + function test_deleteProposal_WhenEmptyProposalReadyForExecution() public { + setUpEmptyProposalReadyForExecution(); + governance.execute(proposalId, 0); + assertFalse(governance.proposalExists(proposalId)); + } + + function test_removeProposalFromDequeued_WhenEmptyProposalReadyForExecution() public { + setUpEmptyProposalReadyForExecution(); + governance.execute(proposalId, 0); + + uint256[] memory dequeued = governance.getDequeue(); + assertEq(dequeued.length, 1); + assertNotEq(dequeued[0], proposalId); + } + + function test_AddsIndexToEmptyIndices_WhenEmptyProposalReadyForExecution() public { + setUpEmptyProposalReadyForExecution(); + governance.execute(proposalId, 0); + assertEq(governance.emptyIndices(0), 0); + } + + function test_updateParticipationBaseline_WhenEmptyProposalReadyForExecution() public { + setUpEmptyProposalReadyForExecution(); + governance.execute(proposalId, 0); + (uint256 baseline, , , ) = governance.getParticipationParameters(); + assertEq(baseline, expectedParticipationBaseline); + } + + function test_emitParticipationBaselineUpdatedEvent_WhenEmptyProposalReadyForExecution() public { + setUpEmptyProposalReadyForExecution(); + vm.expectEmit(true, true, true, true); + emit ParticipationBaselineUpdated(expectedParticipationBaseline); + governance.execute(proposalId, 0); + } +} + +contract GovernanceApproveHotfix is GovernanceBaseTest { + event HotfixApproved(bytes32 indexed hash); + + bytes32 constant HOTFIX_HASH = bytes32(uint256(0x123456789)); + + function test_markHotfixRecordApprovedWhenCalledByApprover() public { + vm.prank(accApprover); + governance.approveHotfix(HOTFIX_HASH); + (bool approved, , ) = governance.getHotfixRecord(HOTFIX_HASH); + assertTrue(approved); + } + + function test_emitHotfixApprovedEvent() public { + vm.expectEmit(true, true, true, true); + emit HotfixApproved(HOTFIX_HASH); + vm.prank(accApprover); + governance.approveHotfix(HOTFIX_HASH); + } + + function test_RevertIf_CalledByNonApprover() public { + vm.expectRevert("msg.sender not approver"); + governance.approveHotfix(HOTFIX_HASH); + } +} + +contract GovernanceWhitelistHotfix is GovernanceBaseTest { + event HotfixWhitelisted(bytes32 indexed hash, address whitelister); + + bytes32 constant HOTFIX_HASH = bytes32(uint256(0x123456789)); + + function test_EmitHotfixWhitelistEvent() public { + address validator = actor("validator1"); + governance.addValidator(validator); + governance.addValidator(actor("validator2")); + + vm.expectEmit(true, true, true, true); + emit HotfixWhitelisted(HOTFIX_HASH, validator); + vm.prank(validator); + governance.whitelistHotfix(HOTFIX_HASH); + } +} + +contract GovernanceHotfixWhitelistValidatorTally is GovernanceBaseTest { + bytes32 constant HOTFIX_HASH = bytes32(uint256(0x123456789)); + + address[] validators; + address[] signers; + + function setUp() public { + super.setUp(); + for (uint256 i = 1; i < 4; i++) { + address validator = vm.addr(i); + uint256 signerPk = i * 10; + address signer = vm.addr(signerPk); + + vm.prank(validator); + accounts.createAccount(); + authorizeValidatorSigner(signerPk, validator); + + governance.addValidator(signer); + + validators.push(validator); + signers.push(signer); + } + } + + function test_countValidatorAccountsThatHaveWhitelisted() public { + for (uint256 i = 0; i < 3; i++) { + vm.prank(validators[i]); + governance.whitelistHotfix(HOTFIX_HASH); + } + + assertEq(governance.hotfixWhitelistValidatorTally(HOTFIX_HASH), 3); + } + + function test_count_authorizedValidatorSignersThatHaveWhitelisted() public { + for (uint256 i = 0; i < 3; i++) { + vm.prank(signers[i]); + governance.whitelistHotfix(HOTFIX_HASH); + } + + assertEq(governance.hotfixWhitelistValidatorTally(HOTFIX_HASH), 3); + } + + function test_notDoubleCountValidatorAccountAndAuthorizedSignerAccounts() public { + for (uint256 i = 0; i < 3; i++) { + vm.prank(validators[i]); + governance.whitelistHotfix(HOTFIX_HASH); + vm.prank(signers[i]); + governance.whitelistHotfix(HOTFIX_HASH); + } + + assertEq(governance.hotfixWhitelistValidatorTally(HOTFIX_HASH), 3); + } + + function test_returnTheCorrectTallyAfterKeyRotation() public { + for (uint256 i = 0; i < 3; i++) { + vm.prank(signers[i]); + governance.whitelistHotfix(HOTFIX_HASH); + } + + // rotate signer + uint256 signerPk = 44; + authorizeValidatorSigner(signerPk, validators[0]); + + assertEq(governance.hotfixWhitelistValidatorTally(HOTFIX_HASH), 3); + } +} + +contract GovernanceIsHotfixPassing is GovernanceBaseTest { + bytes32 constant HOTFIX_HASH = bytes32(uint256(0x123456789)); + + function setUp() public { + super.setUp(); + address val1 = actor("validator1"); + governance.addValidator(val1); + vm.prank(val1); + accounts.createAccount(); + + address val2 = actor("validator2"); + governance.addValidator(val2); + vm.prank(val2); + accounts.createAccount(); + } + + function test_returnFalseWhenHotfixHasNotBeenWhitelisted() public { + assertFalse(governance.isHotfixPassing(HOTFIX_HASH)); + } + + function test_returnFalseWhenHotfixHasBeenWhitelistedButNotByQuorum() public { + vm.prank(actor("validator1")); + governance.whitelistHotfix(HOTFIX_HASH); + assertFalse(governance.isHotfixPassing(HOTFIX_HASH)); + } + + function test_returnTrueWhenHotfixIsWhitelistedByQuorum() public { + vm.prank(actor("validator1")); + governance.whitelistHotfix(HOTFIX_HASH); + vm.prank(actor("validator2")); + governance.whitelistHotfix(HOTFIX_HASH); + assertTrue(governance.isHotfixPassing(HOTFIX_HASH)); + } +} + +contract GovernancePrepareHotfix is GovernanceBaseTest { + event HotfixPrepared(bytes32 indexed hash, uint256 indexed epoch); + + bytes32 constant HOTFIX_HASH = bytes32(uint256(0x123456789)); + + function setUp() public { + super.setUp(); + address val1 = actor("validator1"); + governance.addValidator(val1); + vm.prank(val1); + accounts.createAccount(); + } + + function test_RevertIf_HotfixIsNotPassing() public { + vm.expectRevert("hotfix not whitelisted by 2f+1 validators"); + governance.prepareHotfix(HOTFIX_HASH); + } + + function test_markHotfixRecordPreparedEpoch_whenHotfixIsPassing() public { + vm.roll(block.number + governance.getEpochSize()); + vm.prank(actor("validator1")); + governance.whitelistHotfix(HOTFIX_HASH); + governance.prepareHotfix(HOTFIX_HASH); + (, , uint256 preparedEpoch) = governance.getHotfixRecord(HOTFIX_HASH); + + assertEq(preparedEpoch, governance.getEpochNumber()); + } + + function test_emitHotfixPreparedEvent_whenHotfixIsPassing() public { + vm.roll(block.number + governance.getEpochSize()); + vm.prank(actor("validator1")); + governance.whitelistHotfix(HOTFIX_HASH); + + uint256 epoch = governance.getEpochNumber(); + vm.expectEmit(true, true, true, true); + emit HotfixPrepared(HOTFIX_HASH, epoch); + governance.prepareHotfix(HOTFIX_HASH); + } + + function test_RevertIf_EpochEqualsPreparedEpoch_whenHotfixIsPassing() public { + vm.roll(block.number + governance.getEpochSize()); + vm.prank(actor("validator1")); + governance.whitelistHotfix(HOTFIX_HASH); + governance.prepareHotfix(HOTFIX_HASH); + vm.expectRevert("hotfix already prepared for this epoch"); + governance.prepareHotfix(HOTFIX_HASH); + } + + function test_succeedForEpochDifferentPreparedEpoch_whenHotfixIsPassing() public { + vm.roll(block.number + governance.getEpochSize()); + vm.prank(actor("validator1")); + governance.whitelistHotfix(HOTFIX_HASH); + governance.prepareHotfix(HOTFIX_HASH); + vm.roll(block.number + governance.getEpochSize()); + governance.prepareHotfix(HOTFIX_HASH); + } +} + +contract GovernanceExecuteHotfix is GovernanceBaseTest { + event HotfixExecuted(bytes32 indexed hash); + + bytes32 SALT = 0x657ed9d64e84fa3d1af43b3a307db22aba2d90a158015df1c588c02e24ca08f0; + bytes32 hotfixHash; + + address validator; + + function setUp() public { + super.setUp(); + validator = actor("validator"); + vm.prank(validator); + accounts.createAccount(); + governance.addValidator(validator); + + // call governance test method to generate proper hotfix (needs calldata arguments) + hotfixHash = governance.getHotfixHash( + okProp.values, + okProp.destinations, + okProp.data, + okProp.dataLengths, + SALT + ); + } + + function executeHotfixTx() private { + governance.executeHotfix( + okProp.values, + okProp.destinations, + okProp.data, + okProp.dataLengths, + SALT + ); + } + + function approveAndPrepareHotfix() private { + vm.prank(accApprover); + governance.approveHotfix(hotfixHash); + vm.roll(block.number + governance.getEpochSize()); + vm.prank(validator); + governance.whitelistHotfix(hotfixHash); + governance.prepareHotfix(hotfixHash); + } + + function test_RevertIf_hotfixNotApproved() public { + vm.expectRevert("hotfix not approved"); + executeHotfixTx(); + } + + function test_RevertIf_hotfixNotPreparedForCurrentEpoch() public { + vm.roll(block.number + governance.getEpochSize()); + vm.prank(accApprover); + governance.approveHotfix(hotfixHash); + + vm.expectRevert("hotfix must be prepared for this epoch"); + executeHotfixTx(); + } + + function test_RevertIf_hotfixPreparedButNotForCurrentEpoch() public { + vm.prank(accApprover); + governance.approveHotfix(hotfixHash); + vm.prank(validator); + governance.whitelistHotfix(hotfixHash); + governance.prepareHotfix(hotfixHash); + vm.roll(block.number + governance.getEpochSize()); + vm.expectRevert("hotfix must be prepared for this epoch"); + executeHotfixTx(); + } + + function test_executeHotfix_WhenApprovedAndPreparedForCurrentEpoch() public { + approveAndPrepareHotfix(); + executeHotfixTx(); + assertEq(testTransactions.getValue(1), 1); + } + + function test_markHotfixAsExecuted_WhenApprovedAndPreparedForCurrentEpoch() public { + approveAndPrepareHotfix(); + executeHotfixTx(); + (, bool executed, ) = governance.getHotfixRecord(hotfixHash); + assertTrue(executed); + } + + function test_emitHotfixExecutedEvent_WhenApprovedAndPreparedForCurrentEpoch() public { + approveAndPrepareHotfix(); + vm.expectEmit(true, true, true, true); + emit HotfixExecuted(hotfixHash); + executeHotfixTx(); + } + + function test_notBeExecutableAgain_WhenApprovedAndPreparedForCurrentEpoch() public { + approveAndPrepareHotfix(); + executeHotfixTx(); + vm.expectRevert("hotfix already executed"); + executeHotfixTx(); + } +} + +contract GovernanceIsVoting is GovernanceBaseTest { + uint256 proposalId; + + function setUp() public { + super.setUp(); + proposalId = makeValidProposal(); + } + + function test_returnsFalse_whenAccountNeverActedOnProposal() public { + emit log_uint(governance.getMostRecentReferendumProposal(accVoter)); + assertFalse(governance.isVoting(accVoter)); + } + + function test_ReturnsTrue_whenUpvoted() public { + vm.prank(accVoter); + governance.upvote(proposalId, 0, 0); + assertTrue(governance.isVoting(accVoter)); + } + + function test_returnsFalse_whenUpvoteIsRevoked() public { + vm.prank(accVoter); + governance.upvote(proposalId, 0, 0); + vm.prank(accVoter); + governance.revokeUpvote(0, 0); + assertFalse(governance.isVoting(accVoter)); + } + + function test_returnsFalse_WhenAfterUpvoteProposalExpired() public { + vm.prank(accVoter); + governance.upvote(proposalId, 0, 0); + vm.warp(block.timestamp + governance.queueExpiry()); + assertFalse(governance.isVoting(accVoter)); + } + + function test_ReturnsTrue_WhenVoted() public { + vm.warp(block.timestamp + governance.dequeueFrequency()); + vm.prank(accApprover); + governance.approve(proposalId, 0); + vm.prank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.Abstain); + + assertTrue(governance.isVoting(accVoter)); + } + + function test_returnsFalse_WhenVotedButAfterReferendumStage() public { + vm.warp(block.timestamp + governance.dequeueFrequency()); + vm.prank(accApprover); + governance.approve(proposalId, 0); + vm.prank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.Abstain); + vm.warp(block.timestamp + REFERENDUM_STAGE_DURATION); + + assertFalse(governance.isVoting(accVoter)); + } +} + +contract GovernanceIsProposalPassing is GovernanceBaseTest { + uint256 proposalId; + address accSndVoter; + + function setUp() public { + super.setUp(); + accSndVoter = actor("sndVoter"); + vm.prank(accSndVoter); + accounts.createAccount(); + + mockLockedGold.setTotalLockedGold(100); + proposalId = makeValidProposal(); + + vm.warp(block.timestamp + governance.dequeueFrequency()); + vm.prank(accApprover); + governance.approve(proposalId, 0); + } + + function test_ReturnsTrue_whenAdjustedSupportIsGreaterThanThreshold() public { + mockLockedGold.setAccountTotalGovernancePower(accVoter, 51); + mockLockedGold.setAccountTotalGovernancePower(accSndVoter, 49); + + vm.prank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + vm.prank(accSndVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.No); + + assertTrue(governance.isProposalPassing(proposalId)); + } + + function test_returnsFalse_whenAdjustedSupportIsLessThanOrEqualToThreshold() public { + mockLockedGold.setAccountTotalGovernancePower(accVoter, 50); + mockLockedGold.setAccountTotalGovernancePower(accSndVoter, 50); + + vm.prank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + vm.prank(accSndVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.No); + + assertFalse(governance.isProposalPassing(proposalId)); + } +} + +contract GovernanceDequeueProposalsIfReady is GovernanceBaseTest { + function test_notUpdateLastDequeueWhenThereAreNoQueuedProposals() public { + uint256 originalLastDequeue = governance.lastDequeue(); + vm.warp(block.timestamp + governance.dequeueFrequency()); + governance.dequeueProposalsIfReady(); + + assertEq(governance.getQueueLength(), 0); + assertEq(governance.lastDequeue(), originalLastDequeue); + } + + function test_updateLastDequeue_whenProposalExists() public { + makeValidProposal(); + uint256 originalLastDequeue = governance.lastDequeue(); + + vm.warp(block.timestamp + governance.dequeueFrequency()); + governance.dequeueProposalsIfReady(); + + assertEq(governance.getQueueLength(), 0); + assertTrue(governance.lastDequeue() > originalLastDequeue); + } + + function test_notUpdateLastDequeueWhenOnlyExpiredProposalQueued() public { + makeValidProposal(); + uint256 originalLastDequeue = governance.lastDequeue(); + + vm.warp(block.timestamp + governance.queueExpiry()); + governance.dequeueProposalsIfReady(); + + assertEq(governance.getQueueLength(), 0); + assertEq(governance.lastDequeue(), originalLastDequeue); + } +} + +contract GovernanceGetProposalStage is GovernanceBaseTest { + function test_returnNoneStageWhenProposalDoesNotExists() public { + assertEq(uint256(governance.getProposalStage(0)), uint256(Proposals.Stage.None)); + assertEq(uint256(governance.getProposalStage(1)), uint256(Proposals.Stage.None)); + } + + function test_returnQueuedWhenNotExpired() public { + uint256 proposalId = makeValidProposal(); + assertEq(uint256(governance.getProposalStage(proposalId)), uint256(Proposals.Stage.Queued)); + } + + function test_returnExpirationWhenExpired() public { + uint256 proposalId = makeValidProposal(); + vm.warp(block.timestamp + governance.queueExpiry()); + assertEq(uint256(governance.getProposalStage(proposalId)), uint256(Proposals.Stage.Expiration)); + } + + function test_returnReferendumWhenNotVotedAndNotExpired() public { + uint256 proposalId = makeValidProposal(); + vm.warp(block.timestamp + governance.dequeueFrequency()); + governance.dequeueProposalsIfReady(); + assertEq(uint256(governance.getProposalStage(proposalId)), uint256(Proposals.Stage.Referendum)); + } + + function test_returnExpirationWhenExpiredButDequeued() public { + uint256 proposalId = makeValidProposal(); + vm.warp(block.timestamp + governance.dequeueFrequency()); + governance.dequeueProposalsIfReady(); + vm.warp(block.timestamp + REFERENDUM_STAGE_DURATION); + assertEq(uint256(governance.getProposalStage(proposalId)), uint256(Proposals.Stage.Expiration)); + } + + function test_returnReferendumWhenNotExpiredButApproved() public { + uint256 proposalId = makeValidProposal(); + vm.warp(block.timestamp + governance.dequeueFrequency()); + governance.dequeueProposalsIfReady(); + vm.prank(accApprover); + governance.approve(proposalId, 0); + + assertEq(uint256(governance.getProposalStage(proposalId)), uint256(Proposals.Stage.Referendum)); + } + + function test_returnExpirationWhenExpiredButApproved() public { + uint256 proposalId = makeValidProposal(); + vm.warp(block.timestamp + governance.dequeueFrequency()); + governance.dequeueProposalsIfReady(); + vm.prank(accApprover); + governance.approve(proposalId, 0); + vm.warp(block.timestamp + REFERENDUM_STAGE_DURATION); + assertEq(uint256(governance.getProposalStage(proposalId)), uint256(Proposals.Stage.Expiration)); + } + + function test_returnExecutionWhenInExecutionStageAndNotExpired() public { + uint256 proposalId = makeValidProposal(); + vm.warp(block.timestamp + governance.dequeueFrequency()); + governance.dequeueProposalsIfReady(); + vm.prank(accApprover); + governance.approve(proposalId, 0); + vm.prank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + vm.warp(block.timestamp + REFERENDUM_STAGE_DURATION); + assertEq(uint256(governance.getProposalStage(proposalId)), uint256(Proposals.Stage.Execution)); + } + + function test_returnExpirationWhenExpiredAfterExecutionState() public { + uint256 proposalId = makeValidProposal(); + vm.warp(block.timestamp + governance.dequeueFrequency()); + governance.dequeueProposalsIfReady(); + vm.prank(accApprover); + governance.approve(proposalId, 0); + vm.prank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + vm.warp(block.timestamp + REFERENDUM_STAGE_DURATION); + vm.warp(block.timestamp + governance.getExecutionStageDuration()); + + assertEq(uint256(governance.getProposalStage(proposalId)), uint256(Proposals.Stage.Expiration)); + assertTrue(governance.isDequeuedProposalExpired(proposalId)); + } + + function test_returnExpirationPastTheExecutionStageWhenNotApproved_WithEmptyProposal() public { + uint256 proposalId = makeEmptyProposal(); + vm.warp(block.timestamp + governance.dequeueFrequency()); + governance.dequeueProposalsIfReady(); + vm.prank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + vm.warp( + block.timestamp + REFERENDUM_STAGE_DURATION + governance.getExecutionStageDuration() + 1 + ); + assertEq(uint256(governance.getProposalStage(proposalId)), uint256(Proposals.Stage.Expiration)); + assertTrue(governance.isDequeuedProposalExpired(proposalId)); + } + + function test_returnExpirationPastTheExecutionStageWhenNotPassing_WithEmptyProposal() public { + uint256 proposalId = makeEmptyProposal(); + vm.warp(block.timestamp + governance.dequeueFrequency()); + governance.dequeueProposalsIfReady(); + vm.prank(accApprover); + governance.approve(proposalId, 0); + vm.warp( + block.timestamp + REFERENDUM_STAGE_DURATION + governance.getExecutionStageDuration() + 1 + ); + assertEq(uint256(governance.getProposalStage(proposalId)), uint256(Proposals.Stage.Expiration)); + assertTrue(governance.isDequeuedProposalExpired(proposalId)); + } + + function test_returnExecutionWhenInExecutionStageAndNotExpired_WithEmptyProposal() public { + uint256 proposalId = makeEmptyProposal(); + vm.warp(block.timestamp + governance.dequeueFrequency()); + governance.dequeueProposalsIfReady(); + vm.prank(accApprover); + governance.approve(proposalId, 0); + vm.prank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + vm.warp(block.timestamp + REFERENDUM_STAGE_DURATION); + assertEq(uint256(governance.getProposalStage(proposalId)), uint256(Proposals.Stage.Execution)); + } + + function test_returnExecutionPastTheExecutionStageIfPassedAndApproved_WithEmptyProposal() public { + uint256 proposalId = makeEmptyProposal(); + vm.warp(block.timestamp + governance.dequeueFrequency()); + governance.dequeueProposalsIfReady(); + vm.prank(accApprover); + governance.approve(proposalId, 0); + vm.prank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + vm.warp(block.timestamp + REFERENDUM_STAGE_DURATION); + vm.warp(block.timestamp + governance.getExecutionStageDuration() + 1); + assertEq(uint256(governance.getProposalStage(proposalId)), uint256(Proposals.Stage.Execution)); + assertFalse(governance.isDequeuedProposalExpired(proposalId)); + } +} + +contract GovernanceGetAmountOfGoldUsedForVoting is GovernanceBaseTest { + function makeAndApprove3ConcurrentProposals() private { + vm.prank(accOwner); + governance.setConcurrentProposals(3); + makeValidProposal(); + makeValidProposal(); + makeValidProposal(); + vm.warp(block.timestamp + governance.dequeueFrequency()); + vm.startPrank(accApprover); + governance.approve(1, 0); + governance.approve(2, 1); + governance.approve(3, 2); + vm.stopPrank(); + } + + function test_showCorrectNumberOfVotes_whenVotingOn1ConcurrentProposal() public { + makeAndApprove3ConcurrentProposals(); + + vm.startPrank(accVoter); + governance.votePartially(1, 0, 10, 30, 0); + vm.stopPrank(); + + uint256 totalVotes = governance.getAmountOfGoldUsedForVoting(accVoter); + assertEq(totalVotes, 40); + } + + function test_showCorrectNumberOfVotes_whenVotingOn2ConcurrentProposal() public { + // TODO mcortesi: check if this makes sense + makeAndApprove3ConcurrentProposals(); + + vm.startPrank(accVoter); + governance.votePartially(1, 0, 10, 30, 0); + governance.votePartially(1, 0, 10, 30, 0); + vm.stopPrank(); + + uint256 totalVotes = governance.getAmountOfGoldUsedForVoting(accVoter); + assertEq(totalVotes, 40); + } + + function test_showCorrectNumberOfVotes_whenVotingOn3ConcurrentProposal() public { + // TODO mcortesi: check if this makes sense + makeAndApprove3ConcurrentProposals(); + + vm.startPrank(accVoter); + governance.votePartially(1, 0, 10, 30, 0); + governance.votePartially(1, 0, 10, 30, 0); + governance.votePartially(1, 0, 10, 30, 0); + vm.stopPrank(); + + uint256 totalVotes = governance.getAmountOfGoldUsedForVoting(accVoter); + assertEq(totalVotes, 40); + } + + function test_returnNumberOfVotes_WhenDequeuedAndVotingHappenInV8() public { + uint256 proposalId = makeValidProposal(); + vm.warp(block.timestamp + governance.dequeueFrequency()); + vm.prank(accApprover); + governance.approve(proposalId, 0); + governance.setDeprecatedWeight(accVoter, 0, 100); + assertEq(governance.getAmountOfGoldUsedForVoting(accVoter), 100); + } + + function test_returnNumberOfVotes_whenDequeuedAndVotedPartially() public { + uint256 proposalId = makeValidProposal(); + vm.warp(block.timestamp + governance.dequeueFrequency()); + vm.prank(accApprover); + governance.approve(proposalId, 0); + + vm.prank(accVoter); + governance.votePartially(proposalId, 0, 10, 30, 0); + uint256 totalVotes = governance.getAmountOfGoldUsedForVoting(accVoter); + assertEq(totalVotes, 40); + } + + function test_return0Votes_whenDequeuedAndVotedPartiallyButExpired() public { + uint256 proposalId = makeValidProposal(); + vm.warp(block.timestamp + governance.dequeueFrequency()); + vm.prank(accApprover); + governance.approve(proposalId, 0); + vm.prank(accVoter); + governance.votePartially(proposalId, 0, 10, 30, 0); + vm.warp( + block.timestamp + REFERENDUM_STAGE_DURATION + governance.getExecutionStageDuration() + 1 + ); + assertEq(governance.getAmountOfGoldUsedForVoting(accVoter), 0); + } + + function test_returnFullWeightWhenUpvoting_WhenProposalInQueue() public { + vm.prank(accOwner); + governance.setConcurrentProposals(3); + uint256 proposalId = makeValidProposal(); + vm.prank(accVoter); + governance.upvote(proposalId, 0, 0); + assertEq(governance.getAmountOfGoldUsedForVoting(accVoter), VOTER_GOLD); + } + + function test_return0IfProposalExpired_WhenProposalInQueue() public { + vm.prank(accOwner); + governance.setConcurrentProposals(3); + uint256 proposalId = makeValidProposal(); + vm.prank(accVoter); + governance.upvote(proposalId, 0, 0); + vm.warp(block.timestamp + governance.queueExpiry()); + assertEq(governance.getAmountOfGoldUsedForVoting(accVoter), 0); + } +} + +contract GovernanceRemoveVotesWhenRevokingDelegatedVotes is GovernanceBaseTest { + uint256[] proposalIds; + + function test_RevertWhen_NotCalledByStakedCeloContract() public { + vm.expectRevert("msg.sender not lockedGold"); + governance.removeVotesWhenRevokingDelegatedVotes(address(0), 0); + } + + function test_shouldPassWhenNoProposalIsDequeued() public { + governance.removeVotesWhenRevokingDelegatedVotesTest(address(0), 0); + } + + function assertVotesTotal( + uint256 proposalId, + uint256 expectedYes, + uint256 expectedNo, + uint256 expectedAbstain + ) private { + (uint256 yes, uint256 no, uint256 abstain) = governance.getVoteTotals(proposalId); + + assertEq(yes, expectedYes); + assertEq(no, expectedNo); + assertEq(abstain, expectedAbstain); + } + + function assertVoteRecord( + uint256 index, + uint256 expectedProposalId, + uint256 expectedYes, + uint256 expectedNo, + uint256 expectedAbstain + ) private { + (uint256 proposalId, , , uint256 yes, uint256 no, uint256 abstain) = governance.getVoteRecord( + accVoter, + index + ); + assertEq(proposalId, expectedProposalId); + assertEq(yes, expectedYes); + assertEq(no, expectedNo); + assertEq(abstain, expectedAbstain); + } + + function makeAndApprove3Proposals() private { + vm.prank(accOwner); + governance.setConcurrentProposals(3); + vm.prank(accOwner); + governance.setDequeueFrequency(60); + + proposalIds.push(makeValidProposal()); + proposalIds.push(makeValidProposal()); + proposalIds.push(makeValidProposal()); + + vm.warp(block.timestamp + governance.dequeueFrequency()); + vm.prank(accApprover); + governance.approve(proposalIds[0], 0); + + vm.warp(block.timestamp + governance.dequeueFrequency()); + vm.prank(accApprover); + governance.approve(proposalIds[1], 1); + + vm.warp(block.timestamp + governance.dequeueFrequency()); + vm.prank(accApprover); + governance.approve(proposalIds[2], 2); + } + + function setUpVotingOnlyforYes() private { + vm.prank(accVoter); + governance.votePartially(proposalIds[0], 0, 100, 0, 0); + vm.prank(accVoter); + governance.votePartially(proposalIds[1], 1, 0, 100, 0); + + assertVoteRecord(0, proposalIds[0], 100, 0, 0); + assertVoteRecord(1, proposalIds[1], 0, 100, 0); + + assertVotesTotal(proposalIds[0], 100, 0, 0); + assertVotesTotal(proposalIds[1], 0, 100, 0); + } + + function test_adjustVotesCorrectlyTo0_WhenVotingOnlyforYes() public { + makeAndApprove3Proposals(); + setUpVotingOnlyforYes(); + governance.removeVotesWhenRevokingDelegatedVotesTest(accVoter, 0); + + assertVoteRecord(0, proposalIds[0], 0, 0, 0); + assertVoteRecord(1, proposalIds[1], 0, 0, 0); + + assertVotesTotal(proposalIds[0], 0, 0, 0); + assertVotesTotal(proposalIds[1], 0, 0, 0); + } + + function test_adjust_votes_correctly_to_30_WhenVotingOnlyForYes() public { + makeAndApprove3Proposals(); + setUpVotingOnlyforYes(); + governance.removeVotesWhenRevokingDelegatedVotesTest(accVoter, 30); + + assertVoteRecord(0, proposalIds[0], 30, 0, 0); + assertVoteRecord(1, proposalIds[1], 0, 30, 0); + + assertVotesTotal(proposalIds[0], 30, 0, 0); + assertVotesTotal(proposalIds[1], 0, 30, 0); + } + + function setupVotingForAllChoices() private { + vm.prank(accVoter); + governance.votePartially(proposalIds[0], 0, 50, 20, 30); + vm.prank(accVoter); + governance.votePartially(proposalIds[1], 1, 0, 40, 60); + vm.prank(accVoter); + governance.votePartially(proposalIds[2], 2, 0, 0, 51); + + assertVoteRecord(0, proposalIds[0], 50, 20, 30); + assertVoteRecord(1, proposalIds[1], 0, 40, 60); + assertVoteRecord(2, proposalIds[2], 0, 0, 51); + } + + function test_adjustVotesCorrectlyTo0_WhenVotingForAllChoices() public { + makeAndApprove3Proposals(); + setupVotingForAllChoices(); + + governance.removeVotesWhenRevokingDelegatedVotesTest(accVoter, 0); + + assertVoteRecord(0, proposalIds[0], 0, 0, 0); + assertVoteRecord(1, proposalIds[1], 0, 0, 0); + } + + function test_adjustVotesCorrectlyTo50_WhenVotingForAllChoices() public { + makeAndApprove3Proposals(); + setupVotingForAllChoices(); + + uint256 maxAmount = 50; // means that votes will be halved + governance.removeVotesWhenRevokingDelegatedVotesTest(accVoter, maxAmount); + + (uint256 yes0, uint256 no0, uint256 abstain0) = governance.getVoteTotals(proposalIds[0]); + (uint256 yes1, uint256 no1, uint256 abstain1) = governance.getVoteTotals(proposalIds[1]); + (uint256 yes2, uint256 no2, uint256 abstain2) = governance.getVoteTotals(proposalIds[2]); + + assertEq(yes0 + no0 + abstain0, maxAmount); + assertEq(yes1 + no1 + abstain1, maxAmount); + assertEq(yes2 + no2 + abstain2, maxAmount); + + assertEq(yes0, 50 / 2); + assertEq(no0, 20 / 2); + assertEq(abstain0, 30 / 2); + + assertEq(yes1, 0); + assertEq(no1, 40 / 2); + assertEq(abstain1, 60 / 2); + + assertVoteRecord(0, proposalIds[0], 50 / 2, 20 / 2, 30 / 2); + assertVoteRecord(1, proposalIds[1], 0, 40 / 2, 60 / 2); + } + + function test_notAdjustVotes_WhenVotingForAllChoicesAndProposalsExpired() public { + makeAndApprove3Proposals(); + setupVotingForAllChoices(); + vm.warp(block.timestamp + governance.queueExpiry()); + assertVoteRecord(0, proposalIds[0], 50, 20, 30); + assertVoteRecord(1, proposalIds[1], 0, 40, 60); + } +} diff --git a/packages/protocol/test/governance/network/governance.ts b/packages/protocol/test/governance/network/governance.ts deleted file mode 100644 index 4617193a91e..00000000000 --- a/packages/protocol/test/governance/network/governance.ts +++ /dev/null @@ -1,4511 +0,0 @@ -import { NULL_ADDRESS, trimLeading0x } from '@celo/base/lib/address' -import { CeloContractName } from '@celo/protocol/lib/registry-utils' -import { getParsedSignatureOfAddress } from '@celo/protocol/lib/signing-utils' -import { - assertBalance, - assertEqualBN, - assertLogMatches2, - assertRevert, - assertTransactionRevertWithReason, - createAndAssertDelegatorDelegateeSigners, - matchAny, - mineToNextEpoch, - stripHexEncoding, - timeTravel, -} from '@celo/protocol/lib/test-utils' -import { concurrentMap } from '@celo/utils/lib/async' -import { zip } from '@celo/utils/lib/collections' -import { fixed1, multiply, toFixed } from '@celo/utils/lib/fixidity' -import { bufferToHex, toBuffer } from '@ethereumjs/util' -import BigNumber from 'bignumber.js' -import { keccak256 } from 'ethereum-cryptography/keccak' -import { hexToBytes, utf8ToBytes } from 'ethereum-cryptography/utils' -import { - AccountsContract, - AccountsInstance, - GovernanceTestContract, - GovernanceTestInstance, - MockLockedGoldContract, - MockLockedGoldInstance, - MockValidatorsContract, - MockValidatorsInstance, - RegistryContract, - RegistryInstance, - TestTransactionsContract, - TestTransactionsInstance, -} from 'types' - -const Governance: GovernanceTestContract = artifacts.require('GovernanceTest') -const Accounts: AccountsContract = artifacts.require('Accounts') -const MockLockedGold: MockLockedGoldContract = artifacts.require('MockLockedGold') -const MockValidators: MockValidatorsContract = artifacts.require('MockValidators') -const Registry: RegistryContract = artifacts.require('Registry') -const TestTransactions: TestTransactionsContract = artifacts.require('TestTransactions') - -// @ts-ignore -// TODO(mcortesi): Use BN -Governance.numberFormat = 'BigNumber' - -const parseProposalParams = (proposalParams: any) => { - return { - proposer: proposalParams[0], - deposit: proposalParams[1], - timestamp: proposalParams[2].toNumber(), - transactionCount: proposalParams[3].toNumber(), - descriptionUrl: proposalParams[4], - networkWeight: proposalParams[5], - approved: proposalParams[6], - } -} - -const parseTransactionParams = (transactionParams: any) => { - return { - value: transactionParams[0].toNumber(), - destination: transactionParams[1], - data: transactionParams[2], - } -} - -enum VoteValue { - None = 0, - Abstain, - No, - Yes, -} - -enum Stage { - None = 0, - Queued, - Approval, - Referendum, - Execution, - Expiration, -} - -interface Transaction { - value: number - destination: string - data: Buffer -} - -// TODO(asa): Test dequeueProposalsIfReady -// TODO(asa): Dequeue explicitly to make the gas cost of operations more clear -contract('Governance', (accounts: string[]) => { - let governance: GovernanceTestInstance - let accountsInstance: AccountsInstance - let mockLockedGold: MockLockedGoldInstance - let mockValidators: MockValidatorsInstance - let testTransactions: TestTransactionsInstance - let registry: RegistryInstance - const nullFunctionId = '0x00000000' - const account = accounts[0] - const approver = accounts[0] - const otherAccount = accounts[1] - const nonOwner = accounts[1] - const nonApprover = accounts[1] - const concurrentProposals = 1 - const minDeposit = 5 - const queueExpiry = 60 * 60 // 1 hour - const dequeueFrequency = 10 * 60 // 10 minutes - const referendumStageDuration = 5 * 60 // 5 minutes - const executionStageDuration = 1 * 60 // 1 minute - const participationBaseline = toFixed(5 / 10) - const participationFloor = toFixed(5 / 100) - const baselineUpdateFactor = toFixed(1 / 5) - const baselineQuorumFactor = toFixed(1) - const yesVotes = 100 - const participation = toFixed(1) - const expectedParticipationBaseline = multiply(baselineUpdateFactor, participation).plus( - multiply(fixed1.minus(baselineUpdateFactor), participationBaseline) - ) - const descriptionUrl = 'https://descriptionUrl.sample.com' - let transactionSuccess1: Transaction - let transactionSuccess2: Transaction - let transactionFail: Transaction - let salt: string - let hotfixHash: Buffer - let hotfixHashStr: string - beforeEach(async () => { - accountsInstance = await Accounts.new(true) - governance = await Governance.new() - mockLockedGold = await MockLockedGold.new() - mockValidators = await MockValidators.new() - registry = await Registry.new(true) - testTransactions = await TestTransactions.new() - await governance.initialize( - registry.address, - approver, - concurrentProposals, - minDeposit, - queueExpiry, - dequeueFrequency, - referendumStageDuration, - executionStageDuration, - participationBaseline, - participationFloor, - baselineUpdateFactor, - baselineQuorumFactor - ) - await registry.setAddressFor(CeloContractName.Accounts, accountsInstance.address) - await registry.setAddressFor(CeloContractName.LockedGold, mockLockedGold.address) - await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) - await accountsInstance.initialize(registry.address) - await accountsInstance.createAccount() - await mockLockedGold.setAccountTotalGovernancePower(account, yesVotes) - await mockLockedGold.setTotalLockedGold(yesVotes) - transactionSuccess1 = { - value: 0, - destination: testTransactions.address, - data: toBuffer( - // @ts-ignore - testTransactions.contract.methods.setValue(1, 1, true).encodeABI() - ), - } - transactionSuccess2 = { - value: 0, - destination: testTransactions.address, - data: toBuffer( - // @ts-ignore - testTransactions.contract.methods.setValue(2, 1, true).encodeABI() - ), - } - transactionFail = { - value: 0, - destination: testTransactions.address, - data: toBuffer( - // @ts-ignore - testTransactions.contract.methods.setValue(3, 1, false).encodeABI() - ), - } - salt = '0x657ed9d64e84fa3d1af43b3a307db22aba2d90a158015df1c588c02e24ca08f0' - const encodedParam = web3.eth.abi.encodeParameters( - ['uint256[]', 'address[]', 'bytes', 'uint256[]', 'bytes32'], - [ - [String(transactionSuccess1.value)], - [transactionSuccess1.destination.toString()], - transactionSuccess1.data, - [String(transactionSuccess1.data.length)], - salt, - ] - ) - - hotfixHash = toBuffer(keccak256(hexToBytes(trimLeading0x(encodedParam)))) - hotfixHashStr = bufferToHex(hotfixHash) - }) - - describe('#initialize()', () => { - it('should have set the owner', async () => { - const owner: string = await governance.owner() - assert.equal(owner, accounts[0]) - }) - - it('should have set concurrentProposals', async () => { - const actualConcurrentProposals = await governance.concurrentProposals() - assertEqualBN(actualConcurrentProposals, concurrentProposals) - }) - - it('should have set minDeposit', async () => { - const actualMinDeposit = await governance.minDeposit() - assertEqualBN(actualMinDeposit, minDeposit) - }) - - it('should have set queueExpiry', async () => { - const actualQueueExpiry = await governance.queueExpiry() - assertEqualBN(actualQueueExpiry, queueExpiry) - }) - - it('should have set dequeueFrequency', async () => { - const actualDequeueFrequency = await governance.dequeueFrequency() - assertEqualBN(actualDequeueFrequency, dequeueFrequency) - }) - - it('should have set stageDurations', async () => { - const actualReferendumStageDuration = await governance.getReferendumStageDuration() - const actualExecutionStageDuration = await governance.getExecutionStageDuration() - assertEqualBN(actualReferendumStageDuration, referendumStageDuration) - assertEqualBN(actualExecutionStageDuration, executionStageDuration) - }) - - it('should have set participationParameters', async () => { - const [ - actualParticipationBaseline, - actualParticipationFloor, - actualBaselineUpdateFactor, - actualBaselineQuorumFactor, - ] = await governance.getParticipationParameters() - assertEqualBN(actualParticipationBaseline, participationBaseline) - assertEqualBN(actualParticipationFloor, participationFloor) - assertEqualBN(actualBaselineUpdateFactor, baselineUpdateFactor) - assertEqualBN(actualBaselineQuorumFactor, baselineQuorumFactor) - }) - - // TODO(asa): Consider testing reversion when 0 values provided - it('should not be callable again', async () => { - await assertTransactionRevertWithReason( - governance.initialize( - registry.address, - approver, - concurrentProposals, - minDeposit, - queueExpiry, - dequeueFrequency, - referendumStageDuration, - executionStageDuration, - participationBaseline, - participationFloor, - baselineUpdateFactor, - baselineQuorumFactor - ), - 'contract already initialized' - ) - }) - }) - - describe('#setApprover', () => { - const newApprover = accounts[2] - it('should set the approver', async () => { - await governance.setApprover(newApprover) - assert.equal(await governance.approver(), newApprover) - }) - - it('should emit the ApproverSet event', async () => { - const resp = await governance.setApprover(newApprover) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches2(log, { - event: 'ApproverSet', - args: { - approver: newApprover, - }, - }) - }) - - it('should revert when approver is the null address', async () => { - await assertTransactionRevertWithReason( - governance.setApprover(NULL_ADDRESS), - 'Approver cannot be 0' - ) - }) - - it('should revert when the approver is unchanged', async () => { - await assertTransactionRevertWithReason( - governance.setApprover(approver), - 'Approver unchanged' - ) - }) - - it('should revert when called by anyone other than the owner', async () => { - await assertTransactionRevertWithReason( - governance.setApprover(newApprover, { from: nonOwner }), - 'Ownable: caller is not the owner' - ) - }) - }) - - describe('#setMinDeposit', () => { - const newMinDeposit = 1 - it('should set the minimum deposit', async () => { - await governance.setMinDeposit(newMinDeposit) - assert.equal((await governance.minDeposit()).toNumber(), newMinDeposit) - }) - - it('should emit the MinDepositSet event', async () => { - const resp = await governance.setMinDeposit(newMinDeposit) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches2(log, { - event: 'MinDepositSet', - args: { - minDeposit: new BigNumber(newMinDeposit), - }, - }) - }) - - it('should revert when the minDeposit is unchanged', async () => { - await assertTransactionRevertWithReason( - governance.setMinDeposit(minDeposit), - 'Minimum deposit unchanged' - ) - }) - - it('should revert when called by anyone other than the owner', async () => { - await assertTransactionRevertWithReason( - governance.setMinDeposit(newMinDeposit, { from: nonOwner }), - 'Ownable: caller is not the owner' - ) - }) - }) - - describe('#setConcurrentProposals', () => { - const newConcurrentProposals = 2 - it('should set the concurrent proposals', async () => { - await governance.setConcurrentProposals(newConcurrentProposals) - assert.equal((await governance.concurrentProposals()).toNumber(), newConcurrentProposals) - }) - - it('should emit the ConcurrentProposalsSet event', async () => { - const resp = await governance.setConcurrentProposals(newConcurrentProposals) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches2(log, { - event: 'ConcurrentProposalsSet', - args: { - concurrentProposals: new BigNumber(newConcurrentProposals), - }, - }) - }) - - it('should revert when concurrent proposals is 0', async () => { - await assertTransactionRevertWithReason( - governance.setConcurrentProposals(0), - 'Number of proposals must be larger than zero' - ) - }) - - it('should revert when concurrent proposals is unchanged', async () => { - await assertTransactionRevertWithReason( - governance.setConcurrentProposals(concurrentProposals), - 'Number of proposals unchanged' - ) - }) - - it('should revert when called by anyone other than the owner', async () => { - await assertTransactionRevertWithReason( - governance.setConcurrentProposals(newConcurrentProposals, { from: nonOwner }), - 'Ownable: caller is not the owner' - ) - }) - }) - - describe('#setQueueExpiry', () => { - const newQueueExpiry = 2 - it('should set the queue expiry', async () => { - await governance.setQueueExpiry(newQueueExpiry) - assert.equal((await governance.queueExpiry()).toNumber(), newQueueExpiry) - }) - - it('should emit the QueueExpirySet event', async () => { - const resp = await governance.setQueueExpiry(newQueueExpiry) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches2(log, { - event: 'QueueExpirySet', - args: { - queueExpiry: new BigNumber(newQueueExpiry), - }, - }) - }) - - it('should revert when queue expiry is 0', async () => { - await assertTransactionRevertWithReason( - governance.setQueueExpiry(0), - 'QueueExpiry must be larger than 0' - ) - }) - - it('should revert when queue expiry is unchanged', async () => { - await assertTransactionRevertWithReason( - governance.setQueueExpiry(queueExpiry), - 'QueueExpiry unchanged' - ) - }) - - it('should revert when called by anyone other than the owner', async () => { - await assertTransactionRevertWithReason( - governance.setQueueExpiry(newQueueExpiry, { from: nonOwner }), - 'Ownable: caller is not the owner' - ) - }) - }) - - describe('#setDequeueFrequency', () => { - const newDequeueFrequency = 2 - it('should set the dequeue frequency', async () => { - await governance.setDequeueFrequency(newDequeueFrequency) - assert.equal((await governance.dequeueFrequency()).toNumber(), newDequeueFrequency) - }) - - it('should emit the DequeueFrequencySet event', async () => { - const resp = await governance.setDequeueFrequency(newDequeueFrequency) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches2(log, { - event: 'DequeueFrequencySet', - args: { - dequeueFrequency: new BigNumber(newDequeueFrequency), - }, - }) - }) - - it('should revert when dequeue frequency is 0', async () => { - await assertTransactionRevertWithReason( - governance.setDequeueFrequency(0), - 'dequeueFrequency must be larger than 0' - ) - }) - - it('should revert when dequeue frequency is unchanged', async () => { - await assertTransactionRevertWithReason( - governance.setDequeueFrequency(dequeueFrequency), - 'dequeueFrequency unchanged' - ) - }) - - it('should revert when called by anyone other than the owner', async () => { - await assertTransactionRevertWithReason( - governance.setDequeueFrequency(newDequeueFrequency, { from: nonOwner }), - 'Ownable: caller is not the owner' - ) - }) - }) - - describe('#setReferendumStageDuration', () => { - const newReferendumStageDuration = 2 - it('should set the referendum stage duration', async () => { - await governance.setReferendumStageDuration(newReferendumStageDuration) - const actualReferendumStageDuration = await governance.getReferendumStageDuration() - assertEqualBN(actualReferendumStageDuration, newReferendumStageDuration) - }) - - it('should emit the ReferendumStageDurationSet event', async () => { - const resp = await governance.setReferendumStageDuration(newReferendumStageDuration) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches2(log, { - event: 'ReferendumStageDurationSet', - args: { - referendumStageDuration: new BigNumber(newReferendumStageDuration), - }, - }) - }) - - it('should revert when referendum stage duration is 0', async () => { - await assertTransactionRevertWithReason( - governance.setReferendumStageDuration(0), - 'Duration must be larger than 0' - ) - }) - - it('should revert when referendum stage duration is unchanged', async () => { - await assertTransactionRevertWithReason( - governance.setReferendumStageDuration(referendumStageDuration), - 'Duration unchanged' - ) - }) - - it('should revert when called by anyone other than the owner', async () => { - await assertTransactionRevertWithReason( - governance.setReferendumStageDuration(newReferendumStageDuration, { from: nonOwner }), - 'Ownable: caller is not the owner' - ) - }) - }) - - describe('#setExecutionStageDuration', () => { - const newExecutionStageDuration = 2 - it('should set the execution stage duration', async () => { - await governance.setExecutionStageDuration(newExecutionStageDuration) - const actualExecutionStageDuration = await governance.getExecutionStageDuration() - assertEqualBN(actualExecutionStageDuration, newExecutionStageDuration) - }) - - it('should emit the ExecutionStageDurationSet event', async () => { - const resp = await governance.setExecutionStageDuration(newExecutionStageDuration) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches2(log, { - event: 'ExecutionStageDurationSet', - args: { - executionStageDuration: new BigNumber(newExecutionStageDuration), - }, - }) - }) - - it('should revert when execution stage duration is 0', async () => { - await assertTransactionRevertWithReason( - governance.setExecutionStageDuration(0), - 'Duration must be larger than 0' - ) - }) - - it('should revert when execution stage duration is unchanged', async () => { - await assertTransactionRevertWithReason( - governance.setExecutionStageDuration(executionStageDuration), - 'Duration unchanged' - ) - }) - - it('should revert when called by anyone other than the owner', async () => { - await assertTransactionRevertWithReason( - governance.setExecutionStageDuration(newExecutionStageDuration, { from: nonOwner }), - 'Ownable: caller is not the owner' - ) - }) - }) - - describe('#setParticipationFloor', () => { - const differentParticipationFloor = toFixed(2 / 100) - - it('should set the participation floor', async () => { - await governance.setParticipationFloor(differentParticipationFloor) - const [, actualParticipationFloor, ,] = await governance.getParticipationParameters() - assertEqualBN(actualParticipationFloor, differentParticipationFloor) - }) - - it('should emit the ParticipationFloorSet event', async () => { - const resp = await governance.setParticipationFloor(differentParticipationFloor) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches2(log, { - event: 'ParticipationFloorSet', - args: { - participationFloor: differentParticipationFloor, - }, - }) - }) - - it('should revert if new participation floor is above 1', async () => { - await assertTransactionRevertWithReason( - governance.setParticipationFloor(toFixed(101 / 100)), - 'Participation floor greater than one' - ) - }) - - it('should revert when called by anyone other than the owner', async () => { - await assertTransactionRevertWithReason( - governance.setParticipationFloor(differentParticipationFloor, { from: nonOwner }), - 'Ownable: caller is not the owner' - ) - }) - }) - - describe('#setBaselineUpdateFactor', () => { - const differentBaselineUpdateFactor = toFixed(2 / 5) - - it('should set the participation update coefficient', async () => { - await governance.setBaselineUpdateFactor(differentBaselineUpdateFactor) - const [, , actualBaselineUpdateFactor] = await governance.getParticipationParameters() - assertEqualBN(actualBaselineUpdateFactor, differentBaselineUpdateFactor) - }) - - it('should emit the ParticipationBaselineUpdateFactorSet event', async () => { - const resp = await governance.setBaselineUpdateFactor(differentBaselineUpdateFactor) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches2(log, { - event: 'ParticipationBaselineUpdateFactorSet', - args: { - baselineUpdateFactor: differentBaselineUpdateFactor, - }, - }) - }) - - it('should revert if new update coefficient is above 1', async () => { - await assertTransactionRevertWithReason( - governance.setBaselineUpdateFactor(toFixed(101 / 100)), - 'Baseline update factor greater than one' - ) - }) - - it('should revert when called by anyone other than the owner', async () => { - await assertTransactionRevertWithReason( - governance.setBaselineUpdateFactor(differentBaselineUpdateFactor, { from: nonOwner }), - 'Ownable: caller is not the owner' - ) - }) - }) - - describe('#setBaselineQuorumFactor', () => { - const differentBaselineQuorumFactor = toFixed(8 / 10) - - it('should set the critical baseline level', async () => { - await governance.setBaselineQuorumFactor(differentBaselineQuorumFactor) - const [, , , actualBaselineQuorumFactor] = await governance.getParticipationParameters() - assertEqualBN(actualBaselineQuorumFactor, differentBaselineQuorumFactor) - }) - - it('should emit the ParticipationBaselineQuorumFactorSet event', async () => { - const resp = await governance.setBaselineQuorumFactor(differentBaselineQuorumFactor) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches2(log, { - event: 'ParticipationBaselineQuorumFactorSet', - args: { - baselineQuorumFactor: differentBaselineQuorumFactor, - }, - }) - }) - - it('should revert if new critical baseline level is above 1', async () => { - await assertTransactionRevertWithReason( - governance.setBaselineQuorumFactor(toFixed(101 / 100)), - 'Baseline quorum factor greater than one' - ) - }) - - it('should revert when called by anyone other than the owner', async () => { - await assertTransactionRevertWithReason( - governance.setBaselineQuorumFactor(differentBaselineQuorumFactor, { from: nonOwner }), - 'Ownable: caller is not the owner' - ) - }) - }) - - // TODO(asa): Verify that when we set the constitution for a function ID then the proper constitution is applied to a proposal. - describe('#setConstitution', () => { - const threshold = toFixed(2 / 3) - let functionId - let differentFunctionId - let destination - - beforeEach(() => { - destination = governance.address - }) - - describe('when the function id is 0', () => { - beforeEach(() => { - functionId = nullFunctionId - differentFunctionId = '0x12345678' - }) - - it('should set the default threshold', async () => { - await governance.setConstitution(destination, functionId, threshold) - const differentThreshold = await governance.getConstitution( - destination, - differentFunctionId - ) - assert.isTrue(differentThreshold.eq(threshold)) - }) - - it('should emit the ConstitutionSet event', async () => { - const resp = await governance.setConstitution(destination, functionId, threshold) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches2(log, { - event: 'ConstitutionSet', - args: { - destination, - functionId: web3.utils.padRight(functionId, 64), - threshold, - }, - }) - }) - }) - - describe('when the function id is not 0', () => { - beforeEach(() => { - functionId = '0x87654321' - differentFunctionId = '0x12345678' - }) - - it('should set the function threshold', async () => { - await governance.setConstitution(destination, functionId, threshold) - const actualThreshold = await governance.getConstitution(destination, functionId) - assert.isTrue(actualThreshold.eq(threshold)) - }) - - it('should not set the default threshold', async () => { - await governance.setConstitution(destination, functionId, threshold) - const actualThreshold = await governance.getConstitution(destination, differentFunctionId) - assert.isTrue(actualThreshold.eq(toFixed(1 / 2))) - }) - - it('should emit the ConstitutionSet event', async () => { - const resp = await governance.setConstitution(destination, functionId, threshold) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches2(log, { - event: 'ConstitutionSet', - args: { - destination, - functionId: web3.utils.padRight(functionId, 64), - threshold, - }, - }) - }) - }) - - it('should revert when the destination is the null address', async () => { - await assertTransactionRevertWithReason( - governance.setConstitution(NULL_ADDRESS, nullFunctionId, threshold), - 'Destination cannot be zero' - ) - }) - - it('should revert when the threshold is zero', async () => { - await assertTransactionRevertWithReason( - governance.setConstitution(destination, nullFunctionId, 0), - 'Threshold has to be greater than majority and not greater than unanimity' - ) - }) - - it('should revert when the threshold is not greater than a majority', async () => { - await assertTransactionRevertWithReason( - governance.setConstitution(destination, nullFunctionId, toFixed(1 / 2)), - 'Threshold has to be greater than majority and not greater than unanimity' - ) - }) - - it('should revert when the threshold is greater than 100%', async () => { - await assertTransactionRevertWithReason( - governance.setConstitution(destination, nullFunctionId, toFixed(101 / 100)), - 'Threshold has to be greater than majority and not greater than unanimity' - ) - }) - - it('should revert when called by anyone other than the owner', async () => { - await assertTransactionRevertWithReason( - governance.setConstitution(destination, nullFunctionId, threshold, { - from: nonOwner, - }), - 'Ownable: caller is not the owner' - ) - }) - }) - - describe('#propose()', () => { - const proposalId = 1 - - it('should return the proposal id', async () => { - const id = await governance.propose.call( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - assertEqualBN(id, proposalId) - }) - - it('should increment the proposal count', async () => { - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - assertEqualBN(await governance.proposalCount(), proposalId) - }) - - it('should add the proposal to the queue', async () => { - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - assert.isTrue(await governance.isQueued(proposalId)) - const [proposalIds, upvotes] = await governance.getQueue() - assertEqualBN(proposalIds[0], proposalId) - assertEqualBN(upvotes[0], 0) - }) - - describe('when making a proposal with zero transactions', () => { - it('should register the proposal', async () => { - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - await governance.propose([], [], [], [], descriptionUrl, { value: minDeposit }) - const timestamp = (await web3.eth.getBlock('latest')).timestamp - const proposal = parseProposalParams(await governance.getProposal(proposalId)) - assert.equal(proposal.proposer, accounts[0]) - assert.equal(proposal.deposit, minDeposit) - assert.equal(proposal.timestamp, timestamp) - assert.equal(proposal.transactionCount, 0) - assert.equal(proposal.descriptionUrl, descriptionUrl) - assertEqualBN(proposal.networkWeight, 0) - assert.equal(proposal.approved, false) - }) - - it('should emit the ProposalQueued event', async () => { - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - const resp = await governance.propose([], [], [], [], descriptionUrl, { value: minDeposit }) - const timestamp = (await web3.eth.getBlock('latest')).timestamp - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches2(log, { - event: 'ProposalQueued', - args: { - proposalId: new BigNumber(1), - proposer: accounts[0], - deposit: new BigNumber(minDeposit), - timestamp, - transactionCount: 0, - }, - }) - }) - }) - - describe('when making a proposal with one transaction', () => { - it('should register the proposal', async () => { - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - const timestamp = (await web3.eth.getBlock('latest')).timestamp - const proposal = parseProposalParams(await governance.getProposal(proposalId)) - assert.equal(proposal.proposer, accounts[0]) - assert.equal(proposal.deposit, minDeposit) - assert.equal(proposal.timestamp, timestamp) - assert.equal(proposal.transactionCount, 1) - assert.equal(proposal.descriptionUrl, descriptionUrl) - assertEqualBN(proposal.networkWeight, 0) - assert.equal(proposal.approved, false) - }) - - it('should register the proposal transactions', async () => { - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - const transaction = parseTransactionParams( - await governance.getProposalTransaction(proposalId, 0) - ) - assert.equal(transaction.value, transactionSuccess1.value) - assert.equal(transaction.destination, transactionSuccess1.destination) - assert.isTrue( - Buffer.from(stripHexEncoding(transaction.data), 'hex').equals(transactionSuccess1.data) - ) - }) - - it('should emit the ProposalQueued event', async () => { - const resp = await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - const timestamp = (await web3.eth.getBlock('latest')).timestamp - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches2(log, { - event: 'ProposalQueued', - args: { - proposalId: new BigNumber(1), - proposer: accounts[0], - deposit: new BigNumber(minDeposit), - timestamp, - transactionCount: 1, - }, - }) - }) - - it('should revert if one tries to make a proposal without description', async () => { - await assertRevert( - governance.propose.call( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - '', - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - ) - }) - }) - - describe('when making a proposal with two transactions', () => { - it('should register the proposal', async () => { - await governance.propose( - [transactionSuccess1.value, transactionSuccess2.value], - [transactionSuccess1.destination, transactionSuccess2.destination], - // @ts-ignore - Buffer.concat([transactionSuccess1.data, transactionSuccess2.data]), - [transactionSuccess1.data.length, transactionSuccess2.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - const timestamp = (await web3.eth.getBlock('latest')).timestamp - const proposal = parseProposalParams(await governance.getProposal(proposalId)) - assert.equal(proposal.proposer, accounts[0]) - assert.equal(proposal.deposit, minDeposit) - assert.equal(proposal.timestamp, timestamp) - assert.equal(proposal.transactionCount, 2) - assert.equal(proposal.descriptionUrl, descriptionUrl) - assertEqualBN(proposal.networkWeight, 0) - assert.equal(proposal.approved, false) - }) - - it('should register the proposal transactions', async () => { - await governance.propose( - [transactionSuccess1.value, transactionSuccess2.value], - [transactionSuccess1.destination, transactionSuccess2.destination], - // @ts-ignore - Buffer.concat([transactionSuccess1.data, transactionSuccess2.data]), - [transactionSuccess1.data.length, transactionSuccess2.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - const transaction1 = parseTransactionParams( - await governance.getProposalTransaction(proposalId, 0) - ) - assert.equal(transaction1.value, transactionSuccess1.value) - assert.equal(transaction1.destination, transactionSuccess1.destination) - assert.isTrue( - Buffer.from(stripHexEncoding(transaction1.data), 'hex').equals(transactionSuccess1.data) - ) - const transaction2 = parseTransactionParams( - await governance.getProposalTransaction(proposalId, 1) - ) - assert.equal(transaction2.value, transactionSuccess2.value) - assert.equal(transaction2.destination, transactionSuccess2.destination) - assert.isTrue( - Buffer.from(stripHexEncoding(transaction2.data), 'hex').equals(transactionSuccess2.data) - ) - }) - - it('should emit the ProposalQueued event', async () => { - const resp = await governance.propose( - [transactionSuccess1.value, transactionSuccess2.value], - [transactionSuccess1.destination, transactionSuccess2.destination], - // @ts-ignore - Buffer.concat([transactionSuccess1.data, transactionSuccess2.data]), - [transactionSuccess1.data.length, transactionSuccess2.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - const timestamp = (await web3.eth.getBlock('latest')).timestamp - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches2(log, { - event: 'ProposalQueued', - args: { - proposalId: new BigNumber(1), - proposer: accounts[0], - deposit: new BigNumber(minDeposit), - timestamp, - transactionCount: 2, - }, - }) - }) - }) - - describe('when it has been more than dequeueFrequency since the last dequeue', () => { - let originalLastDequeue: BigNumber - beforeEach(async () => { - originalLastDequeue = await governance.lastDequeue() - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await timeTravel(dequeueFrequency, web3) - }) - - it('should dequeue queued proposal(s)', async () => { - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - assert.isFalse(await governance.isQueued(proposalId)) - assert.equal((await governance.getQueueLength()).toNumber(), 1) - assert.equal((await governance.dequeued(0)).toNumber(), proposalId) - assert.isBelow(originalLastDequeue.toNumber(), (await governance.lastDequeue()).toNumber()) - }) - }) - - it('should not update lastDequeue when no queued proposal(s)', async () => { - const originalLastDequeue = await governance.lastDequeue() - await timeTravel(dequeueFrequency, web3) - - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - - assert.equal((await governance.getQueueLength()).toNumber(), 1) - assert.equal((await governance.lastDequeue()).toNumber(), originalLastDequeue.toNumber()) - }) - }) - - describe('#upvote()', () => { - const proposalId = new BigNumber(1) - beforeEach(async () => { - await mockLockedGold.setAccountTotalLockedGold(account, yesVotes) - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - }) - - it('should increase the number of upvotes for the proposal', async () => { - await governance.upvote(proposalId, 0, 0) - assertEqualBN(await governance.getUpvotes(proposalId), yesVotes) - }) - - it('should mark the account as having upvoted the proposal', async () => { - await governance.upvote(proposalId, 0, 0) - const [recordId, recordWeight] = await governance.getUpvoteRecord(account) - assertEqualBN(recordId, proposalId) - assertEqualBN(recordWeight, yesVotes) - }) - - it('should return true', async () => { - const success = await governance.upvote.call(proposalId, 0, 0) - assert.isTrue(success) - }) - - it('should emit the ProposalUpvoted event', async () => { - const resp = await governance.upvote(proposalId, 0, 0) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches2(log, { - event: 'ProposalUpvoted', - args: { - proposalId: new BigNumber(proposalId), - account, - upvotes: new BigNumber(yesVotes), - }, - }) - }) - - it('should revert when upvoting a proposal that is not queued', async () => { - await assertTransactionRevertWithReason( - governance.upvote(proposalId.plus(1), 0, 0), - 'cannot upvote a proposal not in the queue' - ) - }) - - describe('when the upvoted proposal is at the end of the queue', () => { - const upvotedProposalId = 2 - beforeEach(async () => { - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - }) - - it('should sort the upvoted proposal to the front of the queue', async () => { - await governance.upvote(upvotedProposalId, proposalId, 0) - const [proposalIds, upvotes] = await governance.getQueue() - assert.equal(proposalIds[0].toNumber(), upvotedProposalId) - assertEqualBN(upvotes[0], yesVotes) - }) - }) - - describe('when the upvoted proposal is expired', () => { - const otherProposalId = 2 - beforeEach(async () => { - // Prevent dequeues for the sake of this test. - await governance.setDequeueFrequency(queueExpiry * 2) - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - const otherAccount1 = accounts[1] - await accountsInstance.createAccount({ from: otherAccount1 }) - await mockLockedGold.setAccountTotalLockedGold(otherAccount1, yesVotes) - await governance.upvote(otherProposalId, proposalId, 0, { from: otherAccount1 }) - await timeTravel(queueExpiry, web3) - }) - - it('should return false', async () => { - const success = await governance.upvote.call(proposalId, otherProposalId, 0) - assert.isFalse(success) - }) - - it('should remove the proposal from the queue', async () => { - await governance.upvote(proposalId, 0, 0) - assert.isFalse(await governance.isQueued(proposalId)) - const [proposalIds] = await governance.getQueue() - assert.notInclude( - proposalIds.map((x) => x.toNumber()), - proposalId.toNumber() - ) - }) - - it('should emit the ProposalExpired event', async () => { - const resp = await governance.upvote(proposalId, 0, 0) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches2(log, { - event: 'ProposalExpired', - args: { - proposalId: new BigNumber(proposalId), - }, - }) - }) - }) - - describe('when it has been more than dequeueFrequency since the last dequeue', () => { - const upvotedProposalId = 2 - let originalLastDequeue: BigNumber - beforeEach(async () => { - originalLastDequeue = await governance.lastDequeue() - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await timeTravel(dequeueFrequency, web3) - }) - - it('should dequeue queued proposal(s)', async () => { - const queueLength = await governance.getQueueLength() - await governance.upvote(upvotedProposalId, 0, 0) - assert.isFalse(await governance.isQueued(proposalId)) - assert.equal( - (await governance.getQueueLength()).toNumber(), - queueLength.minus(concurrentProposals).toNumber() - ) - assertEqualBN(await governance.dequeued(0), proposalId) - assert.isBelow(originalLastDequeue.toNumber(), (await governance.lastDequeue()).toNumber()) - }) - - it('should revert when upvoting a proposal that will be dequeued', async () => { - await assertTransactionRevertWithReason( - governance.upvote(proposalId, 0, 0), - 'cannot upvote a proposal not in the queue' - ) - }) - }) - - describe('when the previously upvoted proposal is in the queue and expired', () => { - const upvotedProposalId = 2 - // Expire the upvoted proposal without dequeueing it. - const queueExpiry1 = 60 - beforeEach(async () => { - await governance.setQueueExpiry(60) - await governance.upvote(proposalId, 0, 0) - await timeTravel(queueExpiry1, web3) - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - }) - - it('should increase the number of upvotes for the proposal', async () => { - await governance.upvote(upvotedProposalId, 0, 0) - assertEqualBN(await governance.getUpvotes(upvotedProposalId), yesVotes) - }) - - it('should mark the account as having upvoted the proposal', async () => { - await governance.upvote(upvotedProposalId, 0, 0) - const [recordId, recordWeight] = await governance.getUpvoteRecord(account) - assertEqualBN(recordId, upvotedProposalId) - assertEqualBN(recordWeight, yesVotes) - }) - - it('should return true', async () => { - const success = await governance.upvote.call(upvotedProposalId, 0, 0) - assert.isTrue(success) - }) - - it('should emit the ProposalExpired event', async () => { - const resp = await governance.upvote(upvotedProposalId, 0, 0) - assert.equal(resp.logs.length, 2) - const log = resp.logs[0] - assertLogMatches2(log, { - event: 'ProposalExpired', - args: { - proposalId: new BigNumber(proposalId), - }, - }) - }) - it('should emit the ProposalUpvoted event', async () => { - const resp = await governance.upvote(upvotedProposalId, 0, 0) - assert.equal(resp.logs.length, 2) - const log = resp.logs[1] - assertLogMatches2(log, { - event: 'ProposalUpvoted', - args: { - proposalId: new BigNumber(upvotedProposalId), - account, - upvotes: new BigNumber(yesVotes), - }, - }) - }) - }) - }) - - describe('#revokeUpvote()', () => { - const proposalId = new BigNumber(1) - beforeEach(async () => { - await mockLockedGold.setAccountTotalLockedGold(account, yesVotes) - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await governance.upvote(proposalId, 0, 0) - }) - - it('should return true', async () => { - const success = await governance.revokeUpvote.call(0, 0) - assert.isTrue(success) - }) - - it('should decrease the number of upvotes for the proposal', async () => { - await governance.revokeUpvote(0, 0) - assertEqualBN(await governance.getUpvotes(proposalId), 0) - }) - - it('should mark the account as not having upvoted a proposal', async () => { - await governance.revokeUpvote(0, 0) - const [recordId, recordWeight] = await governance.getUpvoteRecord(account) - assertEqualBN(recordId, 0) - assertEqualBN(recordWeight, 0) - }) - - it('should emit the ProposalUpvoteRevoked event', async () => { - const resp = await governance.revokeUpvote(0, 0) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches2(log, { - event: 'ProposalUpvoteRevoked', - args: { - proposalId: new BigNumber(proposalId), - account, - revokedUpvotes: new BigNumber(yesVotes), - }, - }) - }) - - it('should revert when the account does not have an upvoted proposal', async () => { - await governance.revokeUpvote(0, 0) - await assertTransactionRevertWithReason( - governance.revokeUpvote(0, 0), - 'Account has no historical upvote' - ) - }) - - describe('when the upvoted proposal has expired', () => { - beforeEach(async () => { - await timeTravel(queueExpiry, web3) - }) - - it('should remove the proposal from the queue', async () => { - await governance.revokeUpvote(0, 0) - assert.isFalse(await governance.isQueued(proposalId)) - const [proposalIds, upvotes] = await governance.getQueue() - assert.equal(proposalIds.length, 0) - assert.equal(upvotes.length, 0) - }) - - it('should mark the account as not having upvoted a proposal', async () => { - await governance.revokeUpvote(0, 0) - const [recordId, recordWeight] = await governance.getUpvoteRecord(account) - assertEqualBN(recordId, 0) - assertEqualBN(recordWeight, 0) - }) - - it('should emit the ProposalExpired event', async () => { - const resp = await governance.revokeUpvote(0, 0) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches2(log, { - event: 'ProposalExpired', - args: { - proposalId: new BigNumber(proposalId), - }, - }) - }) - }) - - describe('when it has been more than dequeueFrequency since the last dequeue', () => { - let originalLastDequeue: BigNumber - beforeEach(async () => { - originalLastDequeue = await governance.lastDequeue() - await timeTravel(dequeueFrequency, web3) - }) - - it('should dequeue the proposal', async () => { - await governance.revokeUpvote(0, 0) - assert.isFalse(await governance.isQueued(proposalId)) - assert.equal((await governance.getQueueLength()).toNumber(), 0) - assertEqualBN(await governance.dequeued(0), proposalId) - assert.isBelow(originalLastDequeue.toNumber(), (await governance.lastDequeue()).toNumber()) - }) - - it('should mark the account as not having upvoted a proposal', async () => { - await governance.revokeUpvote(0, 0) - const [recordId, recordWeight] = await governance.getUpvoteRecord(account) - assertEqualBN(recordId, 0) - assertEqualBN(recordWeight, 0) - }) - }) - }) - - describe('#withdraw()', () => { - const proposalId = 1 - const index = 0 - beforeEach(async () => { - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await timeTravel(dequeueFrequency, web3) - await governance.approve(proposalId, index) - }) - - describe('when the caller was the proposer of a dequeued proposal', () => { - it('should return true', async () => { - // @ts-ignore - const success = await governance.withdraw.call() - assert.isTrue(success) - }) - - it('should withdraw the refunded deposit when the proposal was dequeued', async () => { - const startBalance = new BigNumber(await web3.eth.getBalance(account)) - await governance.withdraw() - await assertBalance(account, startBalance.plus(minDeposit)) - }) - }) - - it('should revert when the caller was not the proposer of a dequeued proposal', async () => { - await assertTransactionRevertWithReason( - governance.withdraw({ from: accounts[1] }), - 'Nothing to withdraw' - ) - }) - }) - - describe('#approve()', () => { - const proposalId = 1 - const index = 0 - beforeEach(async () => { - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await timeTravel(dequeueFrequency, web3) - }) - - it('should return true', async () => { - const success = await governance.approve.call(proposalId, index) - assert.isTrue(success) - }) - - it('should return updated proposal details correctly', async () => { - await governance.approve(proposalId, index) - const proposal = parseProposalParams(await governance.getProposal(proposalId)) - assert.equal(proposal.proposer, accounts[0]) - assert.equal(proposal.deposit, minDeposit) - assert.equal(proposal.transactionCount, 1) - assert.equal(proposal.descriptionUrl, descriptionUrl) - assert.equal(proposal.approved, true) - assertEqualBN(proposal.networkWeight, yesVotes) - }) - - it('should set the proposal to approved', async () => { - await governance.approve(proposalId, index) - assert.isTrue(await governance.isApproved(proposalId)) - }) - - it('should emit the ProposalDequeued event', async () => { - const resp = await governance.approve(proposalId, index) - assert.equal(resp.logs.length, 2) - const log = resp.logs[0] - assertLogMatches2(log, { - event: 'ProposalDequeued', - args: { - proposalId: new BigNumber(proposalId), - timestamp: matchAny, - }, - }) - }) - - it('should emit the ProposalApproved event', async () => { - const resp = await governance.approve(proposalId, index) - assert.equal(resp.logs.length, 2) - const log = resp.logs[1] - assertLogMatches2(log, { - event: 'ProposalApproved', - args: { - proposalId: new BigNumber(proposalId), - }, - }) - }) - - it('should revert when the index is out of bounds', async () => { - await assertTransactionRevertWithReason( - governance.approve(proposalId, index + 1), - 'Provided index greater than dequeue length.' - ) - }) - - it('should revert if the proposal id does not match the index', async () => { - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await timeTravel(dequeueFrequency, web3) - const otherProposalId = 2 - await assertTransactionRevertWithReason( - governance.approve(otherProposalId, index), - 'Proposal not dequeued' - ) - }) - - it('should revert when not called by the approver', async () => { - await assertTransactionRevertWithReason( - governance.approve(proposalId, index, { from: nonApprover }), - 'msg.sender not approver' - ) - }) - - it('should revert when the proposal is queued', async () => { - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await assertTransactionRevertWithReason( - governance.approve(proposalId + 1, index), - 'Proposal not dequeued' - ) - }) - - it('should revert if the proposal has already been approved', async () => { - await governance.approve(proposalId, index) - await assertTransactionRevertWithReason( - governance.approve(proposalId, index), - 'Proposal already approved' - ) - }) - - describe('when the proposal is within referendum stage', () => { - beforeEach(async () => { - // Dequeue the other proposal. - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - }) - - it('should return true', async () => { - const success = await governance.approve.call(proposalId, index) - assert.isTrue(success) - }) - - it('should not delete the proposal', async () => { - await governance.approve(proposalId, index) - assert.isTrue(await governance.proposalExists(proposalId)) - }) - - it('should not remove the proposal ID from dequeued', async () => { - await governance.approve(proposalId, index) - const dequeued = await governance.getDequeue() - assert.include( - dequeued.map((x) => x.toNumber()), - proposalId - ) - }) - - it('should emit the ParticipationBaselineUpdated event', async () => { - const resp = await governance.approve(proposalId, index) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches2(log, { - event: 'ProposalApproved', - args: { - proposalId: new BigNumber(proposalId), - }, - }) - }) - }) - - describe('when the proposal is past the referendum stage', () => { - beforeEach(async () => { - // Dequeue the other proposal. - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await timeTravel(referendumStageDuration + 1, web3) - }) - - it('should return false', async () => { - const success = await governance.approve.call(proposalId, index) - assert.isFalse(success) - }) - - it('should delete the proposal', async () => { - await governance.approve(proposalId, index) - assert.isFalse(await governance.proposalExists(proposalId)) - }) - - it('should remove the proposal ID from dequeued', async () => { - await governance.approve(proposalId, index) - const dequeued = await governance.getDequeue() - assert.notInclude( - dequeued.map((x) => x.toNumber()), - proposalId - ) - }) - - it('should add the index to empty indices', async () => { - await governance.approve(proposalId, index) - const emptyIndex = await governance.emptyIndices(0) - assert.equal(emptyIndex.toNumber(), index) - }) - - it('should not emit the ParticipationBaselineUpdated event', async () => { - const resp = await governance.approve(proposalId, index) - assert.equal(resp.logs.length, 0) - }) - }) - }) - - describe('#revokeVotes()', () => { - beforeEach(async () => { - await governance.setConcurrentProposals(3) - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await governance.propose( - [transactionSuccess2.value], - [transactionSuccess2.destination], - // @ts-ignore bytes type - transactionSuccess2.data, - [transactionSuccess2.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await timeTravel(dequeueFrequency, web3) - await governance.approve(1, 0) - await governance.approve(2, 1) - await governance.approve(3, 2) - await mockLockedGold.setAccountTotalGovernancePower(account, yesVotes) - }) - - for (let numVoted = 0; numVoted < 3; numVoted++) { - describe(`when account has voted on ${numVoted} proposals`, () => { - const value = VoteValue.Yes - beforeEach(async () => { - for (let i = 0; i < numVoted; i++) { - await governance.vote(i + 1, i, value) - } - }) - - it('should unset the most recent referendum proposal voted on', async () => { - await governance.revokeVotes() - const mostRecentReferendum = await governance.getMostRecentReferendumProposal(account) - assert.equal(mostRecentReferendum.toNumber(), 0) - }) - - it('should return false on `isVoting`', async () => { - await governance.revokeVotes() - const voting = await governance.isVoting(accounts[0]) - assert.isFalse(voting) - }) - - it(`should emit the ProposalVoteRevokedV2 event ${numVoted} times`, async () => { - const resp = await governance.revokeVotes() - assert.equal(resp.logs.length, numVoted) - resp.logs.map((log, i) => - assertLogMatches2(log, { - event: 'ProposalVoteRevokedV2', - args: { - proposalId: i + 1, - account, - yesVotes, - noVotes: 0, - abstainVotes: 0, - }, - }) - ) - }) - - it('should not revert when proposals are not in the Referendum stage', async () => { - await timeTravel(referendumStageDuration, web3) - const success = await governance.revokeVotes.call() - assert.isTrue(success) - }) - }) - } - - for (let numVoted = 0; numVoted < 3; numVoted++) { - describe(`when account has partially voted on ${numVoted} proposals`, () => { - const yes = 10 - const no = 30 - const abstain = 0 - beforeEach(async () => { - for (let i = 0; i < numVoted; i++) { - await governance.votePartially(i + 1, i, yes, no, abstain) - } - }) - - it('should unset the most recent referendum proposal voted on', async () => { - await governance.revokeVotes() - const mostRecentReferendum = await governance.getMostRecentReferendumProposal(account) - assert.equal(mostRecentReferendum.toNumber(), 0) - }) - - it('should return false on `isVoting`', async () => { - await governance.revokeVotes() - const voting = await governance.isVoting(accounts[0]) - const totalVotesByAccount = await governance.getAmountOfGoldUsedForVoting(accounts[0]) - assert.isFalse(voting) - assert.isTrue(totalVotesByAccount.eq(0)) - }) - - it(`should emit the ProposalVoteRevokedV2 event ${numVoted} times`, async () => { - const resp = await governance.revokeVotes() - assert.equal(resp.logs.length, numVoted) - resp.logs.map((log, i) => - assertLogMatches2(log, { - event: 'ProposalVoteRevokedV2', - args: { - proposalId: i + 1, - account, - yesVotes: yes, - noVotes: no, - abstainVotes: abstain, - }, - }) - ) - }) - - it('should not revert when proposals are not in the Referendum stage', async () => { - await timeTravel(referendumStageDuration, web3) - const success = await governance.revokeVotes.call() - assert.isTrue(success) - }) - }) - } - }) - - describe('#vote()', () => { - const proposalId = 1 - const index = 0 - const value = VoteValue.Yes - - describe('when proposal is approved', () => { - beforeEach(async () => { - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await timeTravel(dequeueFrequency, web3) - await governance.approve(proposalId, index) - await mockLockedGold.setAccountTotalGovernancePower(account, yesVotes) - }) - - it('should return true', async () => { - const success = await governance.vote.call(proposalId, index, value, { - gas: 7000000, - from: account, - }) - assert.isTrue(success) - }) - - it('should increment the vote totals', async () => { - await governance.vote(proposalId, index, value) - const [yes, ,] = await governance.getVoteTotals(proposalId) - assert.equal(yes.toNumber(), yesVotes) - }) - - it("should set the voter's vote record", async () => { - await governance.vote(proposalId, index, value) - const [recordProposalId, , , yesVotesRecord, noVotesRecord, abstainVotesRecord] = - await governance.getVoteRecord(account, index) - assertEqualBN(recordProposalId, proposalId) - assertEqualBN(yesVotesRecord, yesVotes) - assertEqualBN(noVotesRecord, 0) - assertEqualBN(abstainVotesRecord, 0) - }) - - it('should set the most recent referendum proposal voted on', async () => { - await governance.vote(proposalId, index, value) - assert.equal( - (await governance.getMostRecentReferendumProposal(account)).toNumber(), - proposalId - ) - }) - - it('should emit the ProposalVotedV2 event', async () => { - const resp = await governance.vote(proposalId, index, value) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches2(log, { - event: 'ProposalVotedV2', - args: { - proposalId: new BigNumber(proposalId), - account, - yesVotes, - noVotes: 0, - abstainVotes: 0, - }, - }) - }) - - it('should revert when the account weight is 0', async () => { - await mockLockedGold.setAccountTotalGovernancePower(account, 0) - await assertTransactionRevertWithReason( - governance.vote(proposalId, index, value), - 'Voter weight zero' - ) - }) - - it('should revert when the index is out of bounds', async () => { - await assertTransactionRevertWithReason( - governance.vote(proposalId, index + 1, value), - 'Provided index greater than dequeue length.' - ) - }) - - it('should revert if the proposal id does not match the index', async () => { - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await timeTravel(dequeueFrequency, web3) - const otherProposalId = 2 - await assertTransactionRevertWithReason( - governance.vote(otherProposalId, index, value), - 'Proposal not dequeued' - ) - }) - - describe('when voting on two proposals', () => { - const proposalId1 = 2 - const proposalId2 = 3 - const index1 = 1 - const index2 = 2 - beforeEach(async () => { - const newDequeueFrequency = 60 - await governance.setDequeueFrequency(newDequeueFrequency) - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await timeTravel(newDequeueFrequency, web3) - await governance.approve(proposalId1, index1) - await governance.propose( - [transactionSuccess2.value], - [transactionSuccess2.destination], - // @ts-ignore bytes type - transactionSuccess2.data, - [transactionSuccess2.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await timeTravel(newDequeueFrequency, web3) - await governance.approve(proposalId2, index2) - await mockLockedGold.setAccountTotalGovernancePower(account, yesVotes) - }) - - it('should set mostRecentReferendumProposal to the youngest proposal voted on', async () => { - await governance.vote(proposalId2, index2, value) - await governance.vote(proposalId1, index1, value) - const mostRecent = await governance.getMostRecentReferendumProposal(accounts[0]) - assert.equal(mostRecent.toNumber(), proposalId2) - }) - - it('should return true on `isVoting`', async () => { - await governance.vote(proposalId2, index2, value) - await governance.vote(proposalId1, index1, value) - const voting = await governance.isVoting(accounts[0]) - assert.isTrue(voting) - }) - - describe('after the first proposal expires', () => { - beforeEach(async () => { - await governance.vote(proposalId2, index2, value) - await governance.vote(proposalId1, index1, value) - await timeTravel(referendumStageDuration - 10, web3) - }) - - it('should still return true on `isVoting`', async () => { - const voting = await governance.isVoting(accounts[0]) - assert.isTrue(voting) - }) - - it('should no longer return true on `isVoting` after both expire', async () => { - await timeTravel(11, web3) - const voting = await governance.isVoting(accounts[0]) - assert.isFalse(voting) - }) - }) - }) - - describe('when the account has already voted on this proposal', () => { - const revoteTests = (oldValue, newValue) => { - it('should decrement the vote total from the previous vote', async () => { - await governance.vote(proposalId, index, newValue) - const voteTotals = await governance.getVoteTotals(proposalId) - assert.equal(voteTotals[3 - oldValue].toNumber(), 0) - }) - - it('should increment the vote total for the new vote', async () => { - await governance.vote(proposalId, index, newValue) - const voteTotals = await governance.getVoteTotals(proposalId) - assert.equal(voteTotals[3 - newValue].toNumber(), yesVotes) - }) - - it("should set the voter's vote record", async () => { - await governance.vote(proposalId, index, newValue) - const [recordProposalId, , , yesVotesRecord, noVotesRecord, abstainVotesRecord] = - await governance.getVoteRecord(account, index) - assert.equal(recordProposalId.toNumber(), proposalId) - - const votesNormalized = [0, abstainVotesRecord, noVotesRecord, yesVotesRecord] - - assertEqualBN(votesNormalized[newValue], yesVotes) - }) - } - - describe('when the account has already voted yes on this proposal', () => { - beforeEach(async () => { - await governance.vote(proposalId, index, VoteValue.Yes) - }) - - revoteTests(VoteValue.Yes, VoteValue.No) - }) - - describe('when the account has already voted no on this proposal', () => { - beforeEach(async () => { - await governance.vote(proposalId, index, VoteValue.No) - }) - - revoteTests(VoteValue.No, VoteValue.Abstain) - }) - - describe('when the account has already voted abstain on this proposal', () => { - beforeEach(async () => { - await governance.vote(proposalId, index, VoteValue.Abstain) - }) - - revoteTests(VoteValue.Abstain, VoteValue.Yes) - }) - }) - - describe('when the proposal is past the referendum stage and passing', () => { - beforeEach(async () => { - await governance.vote(proposalId, index, VoteValue.Yes) - await timeTravel(referendumStageDuration, web3) - }) - - it('should revert', async () => { - await assertRevert(governance.vote.call(proposalId, index, value)) - }) - }) - - describe('when the proposal is past the referendum stage and failing', () => { - beforeEach(async () => { - await governance.vote(proposalId, index, VoteValue.No) - await timeTravel(referendumStageDuration, web3) - }) - - it('should return false', async () => { - const success = await governance.vote.call(proposalId, index, value) - assert.isFalse(success) - }) - - it('should delete the proposal', async () => { - await governance.vote(proposalId, index, value) - assert.isFalse(await governance.proposalExists(proposalId)) - }) - - it('should remove the proposal ID from dequeued', async () => { - await governance.vote(proposalId, index, value) - const dequeued = await governance.getDequeue() - assert.notInclude( - dequeued.map((x) => x.toNumber()), - proposalId - ) - }) - - it('should add the index to empty indices', async () => { - await governance.vote(proposalId, index, value) - const emptyIndex = await governance.emptyIndices(0) - assert.equal(emptyIndex.toNumber(), index) - }) - - it('should update the participation baseline', async () => { - await governance.vote(proposalId, index, value) - const [actualParticipationBaseline, , ,] = await governance.getParticipationParameters() - assertEqualBN(actualParticipationBaseline, expectedParticipationBaseline) - }) - - it('should emit the ParticipationBaselineUpdated event', async () => { - const resp = await governance.vote(proposalId, index, value) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches2(log, { - event: 'ParticipationBaselineUpdated', - args: { - participationBaseline: expectedParticipationBaseline, - }, - }) - }) - }) - }) - - describe('When proposal is approved and have signer', () => { - let accountSigner - beforeEach(async () => { - ;[accountSigner] = await createAndAssertDelegatorDelegateeSigners( - accountsInstance, - accounts, - account - ) - - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await timeTravel(dequeueFrequency, web3) - await governance.approve(proposalId, index) - await mockLockedGold.setAccountTotalGovernancePower(account, yesVotes) - }) - - it('should return true', async () => { - const success = await governance.vote.call(proposalId, index, value, { - from: accountSigner, - }) - assert.isTrue(success) - }) - - it('should increment the vote totals', async () => { - await governance.vote(proposalId, index, value, { from: accountSigner }) - const [yes, ,] = await governance.getVoteTotals(proposalId) - assert.equal(yes.toNumber(), yesVotes) - }) - - it("should set the voter's vote record", async () => { - await governance.vote(proposalId, index, value, { from: accountSigner }) - const [recordProposalId, , , yesVotesRecord, noVotesRecord, abstainVotesRecord] = - await governance.getVoteRecord(account, index) - assertEqualBN(recordProposalId, proposalId) - assertEqualBN(yesVotesRecord, yesVotes) - assertEqualBN(noVotesRecord, 0) - assertEqualBN(abstainVotesRecord, 0) - }) - - it('should set the most recent referendum proposal voted on', async () => { - await governance.vote(proposalId, index, value, { from: accountSigner }) - assert.equal( - (await governance.getMostRecentReferendumProposal(account)).toNumber(), - proposalId - ) - }) - - it('should emit the ProposalVotedV2 event', async () => { - await governance.dequeueProposalsIfReady() - const resp = await governance.vote(proposalId, index, value, { from: accountSigner }) - assert.equal(resp.logs.length, resp.logs.length) - const log = resp.logs[0] - assertLogMatches2(log, { - event: 'ProposalVotedV2', - args: { - proposalId: new BigNumber(proposalId), - account, - yesVotes, - noVotes: 0, - abstainVotes: 0, - }, - }) - }) - - it('should revert when the account weight is 0', async () => { - await mockLockedGold.setAccountTotalGovernancePower(account, 0) - await assertTransactionRevertWithReason( - governance.vote(proposalId, index, value, { from: accountSigner }), - 'Voter weight zero' - ) - }) - }) - - describe('when proposal is not approved', () => { - beforeEach(async () => { - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await timeTravel(dequeueFrequency, web3) - await mockLockedGold.setAccountTotalGovernancePower(account, yesVotes) - }) - - it('should return true', async () => { - const success = await governance.vote.call(proposalId, index, value) - assert.isTrue(success) - }) - - it('should increment the vote totals', async () => { - await governance.vote(proposalId, index, value) - const [yes, ,] = await governance.getVoteTotals(proposalId) - assert.equal(yes.toNumber(), yesVotes) - }) - - it("should set the voter's vote record", async () => { - await governance.vote(proposalId, index, value) - const [recordProposalId, , , yesVotesRecord, noVotesRecord, abstainVotesRecord] = - await governance.getVoteRecord(account, index) - assertEqualBN(recordProposalId, proposalId) - assertEqualBN(yesVotesRecord, yesVotes) - assertEqualBN(noVotesRecord, 0) - assertEqualBN(abstainVotesRecord, 0) - }) - - it('should set the most recent referendum proposal voted on', async () => { - await governance.vote(proposalId, index, value) - assert.equal( - (await governance.getMostRecentReferendumProposal(account)).toNumber(), - proposalId - ) - }) - - it('should emit the ProposalVotedV2 event', async () => { - await governance.dequeueProposalsIfReady() - const resp = await governance.vote(proposalId, index, value) - assert.equal(resp.logs.length, resp.logs.length) - const log = resp.logs[0] - assertLogMatches2(log, { - event: 'ProposalVotedV2', - args: { - proposalId: new BigNumber(proposalId), - account, - yesVotes, - noVotes: 0, - abstainVotes: 0, - }, - }) - }) - - it('should revert when the account weight is 0', async () => { - await mockLockedGold.setAccountTotalGovernancePower(account, 0) - await assertTransactionRevertWithReason( - governance.vote(proposalId, index, value), - 'Voter weight zero' - ) - }) - - it('should revert when the index is out of bounds', async () => { - await assertTransactionRevertWithReason( - governance.vote(proposalId, index + 1, value), - 'Provided index greater than dequeue length.' - ) - }) - - it('should revert if the proposal id does not match the index', async () => { - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await timeTravel(dequeueFrequency, web3) - const otherProposalId = 2 - await assertTransactionRevertWithReason( - governance.vote(otherProposalId, index, value), - 'Proposal not dequeued' - ) - }) - }) - - describe('When voting on different proposal with same index', () => { - const proposalId2 = 2 - const otherAccountWeight = 100 - beforeEach(async () => { - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await timeTravel(dequeueFrequency, web3) - await governance.approve(proposalId, index) - await mockLockedGold.setAccountTotalGovernancePower(account, yesVotes) - await governance.vote(proposalId, index, value) - await timeTravel(referendumStageDuration, web3) - await timeTravel(executionStageDuration, web3) - }) - - it('should ignore votes from previous proposal', async () => { - const dequeuedProposal1Dequeued = await governance.dequeued(index) - assertEqualBN(dequeuedProposal1Dequeued, proposalId) - - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await governance.execute(proposalId, index) - assert.isFalse(await governance.proposalExists(proposalId)) - - await timeTravel(dequeueFrequency + 1, web3) - await governance.dequeueProposalsIfReady() - await governance.approve.call(proposalId2, index) - assert.isTrue(await governance.proposalExists(proposalId2)) - - const dequeuedProposal2 = await governance.dequeued(index) - assertEqualBN(dequeuedProposal2, proposalId2) - await governance.getVoteTotals(proposalId2) - - const otherAccount1 = accounts[1] - await accountsInstance.createAccount({ from: otherAccount1 }) - await mockLockedGold.setAccountTotalGovernancePower(otherAccount1, otherAccountWeight) - await governance.vote(proposalId2, index, value, { from: otherAccount1 }) - - await governance.vote(proposalId2, index, VoteValue.No) - - const [yesVotesTotal, noVotesTotal, abstainVotesTotal] = await governance.getVoteTotals( - proposalId2 - ) - - assertEqualBN(yesVotesTotal, otherAccountWeight) - assertEqualBN(noVotesTotal, yesVotes) - assertEqualBN(abstainVotesTotal, 0) - }) - }) - }) - - describe('#votePartially()', () => { - const proposalId = 1 - const index = 0 - - describe('when proposal is approved', () => { - beforeEach(async () => { - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await timeTravel(dequeueFrequency, web3) - await governance.approve(proposalId, index) - await mockLockedGold.setAccountTotalGovernancePower(account, yesVotes) - }) - - it('should return true', async () => { - const success = await governance.votePartially.call(proposalId, index, yesVotes, 0, 0, { - gas: 7000000, - from: account, - }) - assert.isTrue(success) - }) - - it('should increment the vote totals', async () => { - await governance.votePartially(proposalId, index, yesVotes, 0, 0) - const [yes, ,] = await governance.getVoteTotals(proposalId) - assert.equal(yes.toNumber(), yesVotes) - }) - - it('should increment the vote totals when voting partially', async () => { - const yes = 10 - const no = 50 - const abstain = 30 - await governance.votePartially(proposalId, index, yes, no, abstain) - const [yesTotal, noTotal, abstainTotal] = await governance.getVoteTotals(proposalId) - assert.equal(yesTotal.toNumber(), yes) - assert.equal(noTotal.toNumber(), no) - assert.equal(abstainTotal.toNumber(), abstain) - }) - - it("should set the voter's vote record", async () => { - await governance.votePartially(proposalId, index, yesVotes, 0, 0) - const [recordProposalId, , , yesVotesRecord] = await governance.getVoteRecord( - account, - index - ) - assertEqualBN(recordProposalId, proposalId) - assertEqualBN(yesVotesRecord, yesVotes) - }) - - it('should set the most recent referendum proposal voted on', async () => { - await governance.votePartially(proposalId, index, yesVotes, 0, 0) - assert.equal( - (await governance.getMostRecentReferendumProposal(account)).toNumber(), - proposalId - ) - }) - - it('should emit the ProposalVotedV2 event', async () => { - const resp = await governance.votePartially(proposalId, index, yesVotes, 0, 0) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches2(log, { - event: 'ProposalVotedV2', - args: { - proposalId: new BigNumber(proposalId), - account, - yesVotes, - noVotes: 0, - abstainVotes: 0, - }, - }) - }) - - it('should revert when the account weight is 0', async () => { - await mockLockedGold.setAccountTotalGovernancePower(account, 0) - await assertTransactionRevertWithReason( - governance.votePartially(proposalId, index, yesVotes, 0, 0), - "Voter doesn't have enough locked Celo [(]formerly known as Celo Gold[)]" - ) - }) - - it('should revert when the account does not have enough gold', async () => { - await assertTransactionRevertWithReason( - governance.votePartially(proposalId, index, yesVotes + 1, 0, 0), - "Voter doesn't have enough locked Celo [(]formerly known as Celo Gold[)]" - ) - }) - - it('should revert when the account does not have enough gold when voting partially', async () => { - const noVotes = yesVotes - await assertTransactionRevertWithReason( - governance.votePartially(proposalId, index, yesVotes, noVotes, 0), - "Voter doesn't have enough locked Celo [(]formerly known as Celo Gold[)]" - ) - }) - - it('should revert when the index is out of bounds', async () => { - await assertTransactionRevertWithReason( - governance.votePartially(proposalId, index + 1, yesVotes, 0, 0), - 'Provided index greater than dequeue length.' - ) - }) - - it('should revert if the proposal id does not match the index', async () => { - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await timeTravel(dequeueFrequency, web3) - const otherProposalId = 2 - await assertTransactionRevertWithReason( - governance.votePartially(otherProposalId, index, yesVotes, 0, 0), - 'Proposal not dequeued' - ) - }) - - describe('when voting on two proposals', () => { - const proposalId1 = 2 - const proposalId2 = 3 - const index1 = 1 - const index2 = 2 - beforeEach(async () => { - const newDequeueFrequency = 60 - await governance.setDequeueFrequency(newDequeueFrequency) - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await timeTravel(newDequeueFrequency, web3) - await governance.approve(proposalId1, index1) - await governance.propose( - [transactionSuccess2.value], - [transactionSuccess2.destination], - // @ts-ignore bytes type - transactionSuccess2.data, - [transactionSuccess2.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await timeTravel(newDequeueFrequency, web3) - await governance.approve(proposalId2, index2) - await mockLockedGold.setAccountTotalGovernancePower(account, yesVotes) - }) - - it('should set mostRecentReferendumProposal to the youngest proposal voted on', async () => { - await governance.votePartially(proposalId1, index1, yesVotes, 0, 0) - await governance.votePartially(proposalId2, index2, yesVotes, 0, 0) - const mostRecent = await governance.getMostRecentReferendumProposal(accounts[0]) - assert.equal(mostRecent.toNumber(), proposalId2) - }) - - it('should return true on `isVoting`', async () => { - await governance.votePartially(proposalId2, index2, yesVotes, 0, 0) - await governance.votePartially(proposalId1, index1, yesVotes, 0, 0) - const voting = await governance.isVoting(accounts[0]) - assert.isTrue(voting) - }) - - describe('after the first proposal expires', () => { - beforeEach(async () => { - await governance.votePartially(proposalId2, index2, yesVotes, 0, 0) - await governance.votePartially(proposalId1, index1, yesVotes, 0, 0) - await timeTravel(referendumStageDuration - 10, web3) - }) - - it('should still return true on `isVoting`', async () => { - const voting = await governance.isVoting(accounts[0]) - assert.isTrue(voting) - }) - - it('should no longer return true on `isVoting` after both expire', async () => { - await timeTravel(11, web3) - const voting = await governance.isVoting(accounts[0]) - assert.isFalse(voting) - }) - }) - }) - - describe('when the account has already voted partially on this proposal', () => { - const revoteTests = (newYes: number, newNo: number, newAbstain) => { - it('should set vote total correctly', async () => { - await governance.votePartially(proposalId, index, newYes, newNo, newAbstain) - const voteTotals = await governance.getVoteTotals(proposalId) - - assertEqualBN(voteTotals[0], newYes) - assertEqualBN(voteTotals[1], newNo) - assertEqualBN(voteTotals[2], newAbstain) - }) - - it("should set the voter's vote record", async () => { - await governance.votePartially(proposalId, index, newYes, newNo, newAbstain) - const [recordProposalId, , , yesVotesRecord, noVotesRecord, abstainVotesRecord] = - await governance.getVoteRecord(account, index) - assert.equal(recordProposalId.toNumber(), proposalId) - - assertEqualBN(yesVotesRecord, newYes) - assertEqualBN(noVotesRecord, newNo) - assertEqualBN(abstainVotesRecord, newAbstain) - }) - } - - describe('when the account has already voted yes and no on this proposal', () => { - const oldYes = 70 - const oldNo = 30 - const oldAbstain = 0 - - beforeEach(async () => { - await governance.votePartially(proposalId, index, oldYes, oldNo, oldAbstain) - }) - - revoteTests(30, 70, 0) - }) - - describe('when the account has already voted abstain and yes on this proposal', () => { - const oldYes = 0 - const oldNo = 70 - const oldAbstain = 30 - - beforeEach(async () => { - await governance.votePartially(proposalId, index, oldYes, oldNo, oldAbstain) - }) - - revoteTests(30, 0, 20) - }) - }) - - describe('when the account has already voted on this proposal', () => { - const voteWeight = yesVotes - const revoteTests = (newYes, newNo, newAbstain) => { - it('should decrement the vote total from the previous vote', async () => { - await governance.votePartially(proposalId, index, newYes, newNo, newAbstain) - const voteTotals = await governance.getVoteTotals(proposalId) - assertEqualBN(voteTotals[0], newYes) - assertEqualBN(voteTotals[1], newNo) - assertEqualBN(voteTotals[2], newAbstain) - }) - - it('should increment the vote total for the new vote', async () => { - await governance.votePartially(proposalId, index, newYes, newNo, newAbstain) - const voteTotals = await governance.getVoteTotals(proposalId) - assertEqualBN(voteTotals[0], newYes) - assertEqualBN(voteTotals[1], newNo) - assertEqualBN(voteTotals[2], newAbstain) - }) - - it("should set the voter's vote record", async () => { - await governance.votePartially(proposalId, index, newYes, newNo, newAbstain) - const [recordProposalId, , , yesVotesRecord, noVotesRecord, abstainVotesRecord] = - await governance.getVoteRecord(account, index) - assert.equal(recordProposalId.toNumber(), proposalId) - assertEqualBN(yesVotesRecord, newYes) - assertEqualBN(noVotesRecord, newNo) - assertEqualBN(abstainVotesRecord, newAbstain) - }) - } - - describe('when the account has already voted yes on this proposal', () => { - beforeEach(async () => { - await governance.votePartially(proposalId, index, yesVotes, 0, 0) - }) - - revoteTests(0, voteWeight, 0) - }) - - describe('when the account has already voted no on this proposal', () => { - beforeEach(async () => { - await governance.votePartially(proposalId, index, voteWeight, 0, 0) - }) - - revoteTests(0, 0, voteWeight) - }) - - describe('when the account has already voted abstain on this proposal', () => { - beforeEach(async () => { - await governance.votePartially(proposalId, index, 0, 0, voteWeight) - }) - - revoteTests(voteWeight, 0, 0) - }) - }) - - describe('when the proposal is past the referendum stage and passing', () => { - beforeEach(async () => { - await governance.votePartially(proposalId, index, yesVotes, 0, 0) - await timeTravel(referendumStageDuration, web3) - }) - - it('should revert', async () => { - await assertRevert(governance.votePartially.call(proposalId, index, yesVotes, 0, 0)) - }) - }) - - describe('when the proposal is past the referendum stage and failing', () => { - beforeEach(async () => { - await governance.votePartially(proposalId, index, 0, yesVotes, 0) - await timeTravel(referendumStageDuration, web3) - }) - - it('should return false', async () => { - const success = await governance.votePartially.call(proposalId, index, yesVotes, 0, 0) - assert.isFalse(success) - }) - - it('should delete the proposal', async () => { - await governance.votePartially(proposalId, index, yesVotes, 0, 0) - assert.isFalse(await governance.proposalExists(proposalId)) - }) - - it('should remove the proposal ID from dequeued', async () => { - await governance.votePartially(proposalId, index, yesVotes, 0, 0) - const dequeued = await governance.getDequeue() - assert.notInclude( - dequeued.map((x) => x.toNumber()), - proposalId - ) - }) - - it('should add the index to empty indices', async () => { - await governance.votePartially(proposalId, index, yesVotes, 0, 0) - const emptyIndex = await governance.emptyIndices(0) - assert.equal(emptyIndex.toNumber(), index) - }) - - it('should update the participation baseline', async () => { - await governance.votePartially(proposalId, index, yesVotes, 0, 0) - const [actualParticipationBaseline, , ,] = await governance.getParticipationParameters() - assertEqualBN(actualParticipationBaseline, expectedParticipationBaseline) - }) - - it('should emit the ParticipationBaselineUpdated event', async () => { - const resp = await governance.votePartially(proposalId, index, yesVotes, 0, 0) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches2(log, { - event: 'ParticipationBaselineUpdated', - args: { - participationBaseline: expectedParticipationBaseline, - }, - }) - }) - }) - }) - - describe('when proposal is approved with signer', () => { - let accountSigner - - beforeEach(async () => { - ;[accountSigner] = await createAndAssertDelegatorDelegateeSigners( - accountsInstance, - accounts, - account - ) - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await timeTravel(dequeueFrequency, web3) - await governance.approve(proposalId, index) - await mockLockedGold.setAccountTotalGovernancePower(account, yesVotes) - }) - - it('should return true', async () => { - const success = await governance.votePartially.call(proposalId, index, yesVotes, 0, 0, { - gas: 7000000, - from: accountSigner, - }) - assert.isTrue(success) - }) - - it('should increment the vote totals', async () => { - await governance.votePartially(proposalId, index, yesVotes, 0, 0, { from: accountSigner }) - const [yes, ,] = await governance.getVoteTotals(proposalId) - assert.equal(yes.toNumber(), yesVotes) - }) - - it('should increment the vote totals when voting partially', async () => { - const yes = 10 - const no = 50 - const abstain = 30 - await governance.votePartially(proposalId, index, yes, no, abstain, { from: accountSigner }) - const [yesTotal, noTotal, abstainTotal] = await governance.getVoteTotals(proposalId) - assert.equal(yesTotal.toNumber(), yes) - assert.equal(noTotal.toNumber(), no) - assert.equal(abstainTotal.toNumber(), abstain) - }) - - it("should set the voter's vote record", async () => { - await governance.votePartially(proposalId, index, yesVotes, 0, 0, { from: accountSigner }) - const [recordProposalId, , , yesVotesRecord] = await governance.getVoteRecord( - account, - index - ) - assertEqualBN(recordProposalId, proposalId) - assertEqualBN(yesVotesRecord, yesVotes) - }) - - it('should set the most recent referendum proposal voted on', async () => { - await governance.votePartially(proposalId, index, yesVotes, 0, 0, { from: accountSigner }) - assert.equal( - (await governance.getMostRecentReferendumProposal(account)).toNumber(), - proposalId - ) - }) - - it('should emit the ProposalVotedV2 event', async () => { - const resp = await governance.votePartially(proposalId, index, yesVotes, 0, 0, { - from: accountSigner, - }) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches2(log, { - event: 'ProposalVotedV2', - args: { - proposalId: new BigNumber(proposalId), - account, - yesVotes, - noVotes: 0, - abstainVotes: 0, - }, - }) - }) - - it('should revert when the account weight is 0', async () => { - await mockLockedGold.setAccountTotalGovernancePower(account, 0) - await assertTransactionRevertWithReason( - governance.votePartially(proposalId, index, yesVotes, 0, 0, { from: accountSigner }), - "Voter doesn't have enough locked Celo [(]formerly known as Celo Gold[)]" - ) - }) - - it('should revert when the account does not have enough gold', async () => { - await assertTransactionRevertWithReason( - governance.votePartially(proposalId, index, yesVotes + 1, 0, 0, { from: accountSigner }), - "Voter doesn't have enough locked Celo [(]formerly known as Celo Gold[)]" - ) - }) - - it('should revert when the account does not have enough gold when voting partially', async () => { - const noVotes = yesVotes - await assertTransactionRevertWithReason( - governance.votePartially(proposalId, index, yesVotes, noVotes, 0, { - from: accountSigner, - }), - "Voter doesn't have enough locked Celo [(]formerly known as Celo Gold[)]" - ) - }) - - it('should revert when the index is out of bounds', async () => { - await assertTransactionRevertWithReason( - governance.votePartially(proposalId, index + 1, yesVotes, 0, 0, { from: accountSigner }), - 'Provided index greater than dequeue length.' - ) - }) - - it('should revert if the proposal id does not match the index', async () => { - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await timeTravel(dequeueFrequency, web3) - const otherProposalId = 2 - await assertTransactionRevertWithReason( - governance.votePartially(otherProposalId, index, yesVotes, 0, 0, { from: accountSigner }), - 'Reason given: Proposal not dequeued.' - ) - }) - }) - - describe('when proposal is not approved', () => { - beforeEach(async () => { - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await timeTravel(dequeueFrequency, web3) - await mockLockedGold.setAccountTotalGovernancePower(account, yesVotes) - }) - - it('should return true', async () => { - const success = await governance.votePartially.call(proposalId, index, yesVotes, 0, 0) - assert.isTrue(success) - }) - - it('should increment the vote totals', async () => { - await governance.votePartially(proposalId, index, yesVotes, 0, 0) - const [yes, ,] = await governance.getVoteTotals(proposalId) - assert.equal(yes.toNumber(), yesVotes) - }) - - it("should set the voter's vote record", async () => { - await governance.votePartially(proposalId, index, yesVotes, 0, 0) - const [recordProposalId, , , yesVotesRecord, noVotesRecord, abstainVotesRecord] = - await governance.getVoteRecord(account, index) - assertEqualBN(recordProposalId, proposalId) - assertEqualBN(yesVotesRecord, yesVotes) - assertEqualBN(noVotesRecord, 0) - assertEqualBN(abstainVotesRecord, 0) - }) - - it('should set the most recent referendum proposal voted on', async () => { - await governance.votePartially(proposalId, index, yesVotes, 0, 0) - assert.equal( - (await governance.getMostRecentReferendumProposal(account)).toNumber(), - proposalId - ) - }) - - it('should emit the ProposalVotedV2 event', async () => { - await governance.dequeueProposalsIfReady() - const resp = await governance.votePartially(proposalId, index, yesVotes, 0, 0) - assert.equal(resp.logs.length, resp.logs.length) - const log = resp.logs[0] - assertLogMatches2(log, { - event: 'ProposalVotedV2', - args: { - proposalId: new BigNumber(proposalId), - account, - yesVotes: new BigNumber(yesVotes), - noVotes: new BigNumber(0), - abstainVotes: new BigNumber(0), - }, - }) - }) - - it('should revert when the account weight is 0', async () => { - await mockLockedGold.setAccountTotalGovernancePower(account, 0) - await assertTransactionRevertWithReason( - governance.votePartially(proposalId, index, yesVotes, 0, 0) - ) - }) - - it('should revert when the index is out of bounds', async () => { - await assertTransactionRevertWithReason( - governance.votePartially(proposalId, index + 1, yesVotes, 0, 0), - 'Provided index greater than dequeue length.' - ) - }) - - it('should revert if the proposal id does not match the index', async () => { - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await timeTravel(dequeueFrequency, web3) - const otherProposalId = 2 - await assertTransactionRevertWithReason( - governance.votePartially(otherProposalId, index, yesVotes, 0, 0), - 'Proposal not dequeued' - ) - }) - }) - - describe('When voting on different proposal with same index', () => { - const proposalId2 = 2 - const otherAccountWeight = 100 - beforeEach(async () => { - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await timeTravel(dequeueFrequency, web3) - await governance.approve(proposalId, index) - await mockLockedGold.setAccountTotalGovernancePower(account, yesVotes) - await governance.votePartially(proposalId, index, yesVotes, 0, 0) - await timeTravel(referendumStageDuration, web3) - await timeTravel(executionStageDuration, web3) - }) - - it('should ignore votes from previous proposal', async () => { - const dequeuedProposal1Dequeued = await governance.dequeued(index) - assertEqualBN(dequeuedProposal1Dequeued, proposalId) - - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await governance.execute(proposalId, index) - assert.isFalse(await governance.proposalExists(proposalId)) - - await timeTravel(dequeueFrequency + 1, web3) - await governance.dequeueProposalsIfReady() - await governance.approve.call(proposalId2, index) - assert.isTrue(await governance.proposalExists(proposalId2)) - - const dequeuedProposal2 = await governance.dequeued(index) - assertEqualBN(dequeuedProposal2, proposalId2) - await governance.getVoteTotals(proposalId2) - - const otherAccount1 = accounts[1] - await accountsInstance.createAccount({ from: otherAccount1 }) - await mockLockedGold.setAccountTotalGovernancePower(otherAccount1, otherAccountWeight) - await governance.votePartially(proposalId2, index, otherAccountWeight, 0, 0, { - from: otherAccount1, - }) - - await governance.votePartially(proposalId2, index, 0, yesVotes, 0) - - const [yesVotesRecord, noVotesRecord, abstainVotesRecord] = await governance.getVoteTotals( - proposalId2 - ) - - assertEqualBN(yesVotesRecord, otherAccountWeight) - assertEqualBN(noVotesRecord, yesVotes) - assertEqualBN(abstainVotesRecord, 0) - }) - }) - }) - - describe('#execute()', () => { - const proposalId = 1 - const index = 0 - const value = VoteValue.Yes - - describe('when executing a proposal with one transaction', () => { - describe('when the proposal can execute successfully', () => { - beforeEach(async () => { - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await timeTravel(dequeueFrequency, web3) - await governance.approve(proposalId, index) - await mockLockedGold.setAccountTotalGovernancePower(account, yesVotes) - await governance.vote(proposalId, index, value) - await timeTravel(referendumStageDuration, web3) - }) - - it('should return true', async () => { - const success = await governance.execute.call(proposalId, index) - assert.isTrue(success) - }) - - it('should execute the proposal', async () => { - await governance.execute(proposalId, index) - assert.equal(await testTransactions.getValue(1).valueOf(), 1) - }) - - it('should delete the proposal', async () => { - await governance.execute(proposalId, index) - assert.isFalse(await governance.proposalExists(proposalId)) - }) - - it('should update the participation baseline', async () => { - await governance.execute(proposalId, index) - const [actualParticipationBaseline, , ,] = await governance.getParticipationParameters() - assertEqualBN(actualParticipationBaseline, expectedParticipationBaseline) - }) - - it('should emit the ProposalExecuted event', async () => { - const resp = await governance.execute(proposalId, index) - assert.equal(resp.logs.length, 2) - const log = resp.logs[0] - assertLogMatches2(log, { - event: 'ProposalExecuted', - args: { - proposalId: new BigNumber(proposalId), - }, - }) - }) - - it('should emit the ParticipationBaselineUpdated event', async () => { - const resp = await governance.execute(proposalId, index) - assert.equal(resp.logs.length, 2) - const log = resp.logs[1] - assertLogMatches2(log, { - event: 'ParticipationBaselineUpdated', - args: { - participationBaseline: expectedParticipationBaseline, - }, - }) - }) - - it('should revert when the index is out of bounds', async () => { - await assertTransactionRevertWithReason( - governance.execute(proposalId, index + 1), - 'Provided index greater than dequeue length.' - ) - }) - }) - - describe('when the proposal can execute successfully - approved in execution stage', () => { - beforeEach(async () => { - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await timeTravel(dequeueFrequency, web3) - await mockLockedGold.setAccountTotalGovernancePower(account, yesVotes) - await governance.vote(proposalId, index, value) - await timeTravel(referendumStageDuration + 1, web3) - await governance.approve(proposalId, index) - }) - - it('should return true', async () => { - const success = await governance.execute.call(proposalId, index) - assert.isTrue(success) - }) - - it('should execute the proposal', async () => { - await governance.execute(proposalId, index) - assert.equal(await testTransactions.getValue(1).valueOf(), 1) - }) - - it('should delete the proposal', async () => { - await governance.execute(proposalId, index) - assert.isFalse(await governance.proposalExists(proposalId)) - }) - - it('should update the participation baseline', async () => { - await governance.execute(proposalId, index) - const [actualParticipationBaseline, , ,] = await governance.getParticipationParameters() - assertEqualBN(actualParticipationBaseline, expectedParticipationBaseline) - }) - - it('should emit the ProposalExecuted event', async () => { - const resp = await governance.execute(proposalId, index) - assert.equal(resp.logs.length, 2) - const log = resp.logs[0] - assertLogMatches2(log, { - event: 'ProposalExecuted', - args: { - proposalId: new BigNumber(proposalId), - }, - }) - }) - - it('should emit the ParticipationBaselineUpdated event', async () => { - const resp = await governance.execute(proposalId, index) - assert.equal(resp.logs.length, 2) - const log = resp.logs[1] - assertLogMatches2(log, { - event: 'ParticipationBaselineUpdated', - args: { - participationBaseline: expectedParticipationBaseline, - }, - }) - }) - - it('should revert when the index is out of bounds', async () => { - await assertTransactionRevertWithReason( - governance.execute(proposalId, index + 1), - 'Provided index greater than dequeue length.' - ) - }) - }) - - describe('when the proposal cannot execute successfully because it is not approved', () => { - beforeEach(async () => { - await governance.propose( - [transactionFail.value], - [transactionFail.destination], - // @ts-ignore bytes type - transactionFail.data, - [transactionFail.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await timeTravel(dequeueFrequency, web3) - await mockLockedGold.setAccountTotalGovernancePower(account, yesVotes) - await governance.vote(proposalId, index, value) - await timeTravel(referendumStageDuration, web3) - }) - - it('should revert', async () => { - await assertTransactionRevertWithReason( - governance.execute(proposalId, index), - 'Proposal not approved' - ) - }) - }) - - describe('when the proposal cannot execute successfully', () => { - beforeEach(async () => { - await governance.propose( - [transactionFail.value], - [transactionFail.destination], - // @ts-ignore bytes type - transactionFail.data, - [transactionFail.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await timeTravel(dequeueFrequency, web3) - await governance.approve(proposalId, index) - await mockLockedGold.setAccountTotalGovernancePower(account, yesVotes) - await governance.vote(proposalId, index, value) - await timeTravel(referendumStageDuration, web3) - }) - - it('should revert', async () => { - await assertTransactionRevertWithReason( - governance.execute(proposalId, index), - 'Proposal execution failed' - ) - }) - }) - - describe('when the proposal cannot execute because it is not a contract address', () => { - beforeEach(async () => { - await governance.propose( - [transactionSuccess1.value], - [accounts[1]], - // @ts-ignore - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await timeTravel(dequeueFrequency, web3) - await governance.approve(proposalId, index) - await mockLockedGold.setAccountTotalGovernancePower(account, yesVotes) - await governance.vote(proposalId, index, value) - await timeTravel(referendumStageDuration, web3) - }) - - it('should revert', async () => { - await assertTransactionRevertWithReason( - governance.execute(proposalId, index), - 'Invalid contract address' - ) - }) - }) - }) - - describe('when executing a proposal with two transactions', () => { - describe('when the proposal can execute successfully', () => { - beforeEach(async () => { - await governance.propose( - [transactionSuccess1.value, transactionSuccess2.value], - [transactionSuccess1.destination, transactionSuccess2.destination], - // @ts-ignore - Buffer.concat([transactionSuccess1.data, transactionSuccess2.data]), - [transactionSuccess1.data.length, transactionSuccess2.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await timeTravel(dequeueFrequency, web3) - await governance.approve(proposalId, index) - await mockLockedGold.setAccountTotalGovernancePower(account, yesVotes) - await governance.vote(proposalId, index, value) - await timeTravel(referendumStageDuration, web3) - }) - - it('should return true', async () => { - const success = await governance.execute.call(proposalId, index) - assert.isTrue(success) - }) - - it('should execute the proposal', async () => { - await governance.execute(proposalId, index) - assert.equal(await testTransactions.getValue(1).valueOf(), 1) - assert.equal(await testTransactions.getValue(2).valueOf(), 1) - }) - - it('should delete the proposal', async () => { - await governance.execute(proposalId, index) - assert.isFalse(await governance.proposalExists(proposalId)) - }) - - it('should update the participation baseline', async () => { - await governance.execute(proposalId, index) - const [actualParticipationBaseline, , ,] = await governance.getParticipationParameters() - assertEqualBN(actualParticipationBaseline, expectedParticipationBaseline) - }) - - it('should emit the ProposalExecuted event', async () => { - const resp = await governance.execute(proposalId, index) - assert.equal(resp.logs.length, 2) - const log = resp.logs[0] - assertLogMatches2(log, { - event: 'ProposalExecuted', - args: { - proposalId: new BigNumber(proposalId), - }, - }) - }) - - it('should emit the ParticipationBaselineUpdated event', async () => { - const resp = await governance.execute(proposalId, index) - assert.equal(resp.logs.length, 2) - const log = resp.logs[1] - assertLogMatches2(log, { - event: 'ParticipationBaselineUpdated', - args: { - participationBaseline: expectedParticipationBaseline, - }, - }) - }) - }) - - describe('when the proposal cannot execute successfully', () => { - describe('when the first transaction cannot execute', () => { - beforeEach(async () => { - await governance.propose( - [transactionSuccess1.value, transactionFail.value], - [transactionSuccess1.destination, transactionFail.destination], - // @ts-ignore - Buffer.concat([transactionSuccess1.data, transactionFail.data]), - [transactionSuccess1.data.length, transactionFail.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await timeTravel(dequeueFrequency, web3) - await governance.approve(proposalId, index) - await mockLockedGold.setAccountTotalGovernancePower(account, yesVotes) - await governance.vote(proposalId, index, value) - await timeTravel(referendumStageDuration, web3) - }) - - it('should revert', async () => { - await assertTransactionRevertWithReason( - governance.execute(proposalId, index), - 'Proposal execution failed' - ) - }) - }) - - describe('when the second transaction cannot execute', () => { - beforeEach(async () => { - await governance.propose( - [transactionFail.value, transactionSuccess1.value], - [transactionFail.destination, transactionSuccess1.destination], - // @ts-ignore - Buffer.concat([transactionFail.data, transactionSuccess1.data]), - [transactionFail.data.length, transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await timeTravel(dequeueFrequency, web3) - await governance.approve(proposalId, index) - await mockLockedGold.setAccountTotalGovernancePower(account, yesVotes) - await governance.vote(proposalId, index, value) - await timeTravel(referendumStageDuration, web3) - }) - - it('should revert', async () => { - await assertTransactionRevertWithReason( - governance.execute(proposalId, index), - 'Proposal execution failed' - ) - }) - }) - }) - }) - - describe('when the proposal is past the execution stage', () => { - beforeEach(async () => { - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await timeTravel(dequeueFrequency, web3) - await governance.approve(proposalId, index) - await mockLockedGold.setAccountTotalGovernancePower(account, yesVotes) - await governance.vote(proposalId, index, value) - await timeTravel(referendumStageDuration, web3) - await timeTravel(executionStageDuration, web3) - }) - - it('should return false', async () => { - const success = await governance.execute.call(proposalId, index) - assert.isFalse(success) - }) - - it('should delete the proposal', async () => { - await governance.execute(proposalId, index) - assert.isFalse(await governance.proposalExists(proposalId)) - }) - - it('should remove the proposal ID from dequeued', async () => { - await governance.execute(proposalId, index) - const dequeued = await governance.getDequeue() - assert.notInclude( - dequeued.map((x) => x.toNumber()), - proposalId - ) - }) - - it('should add the index to empty indices', async () => { - await governance.execute(proposalId, index) - const emptyIndex = await governance.emptyIndices(0) - assert.equal(emptyIndex.toNumber(), index) - }) - - it('should update the participation baseline', async () => { - await governance.execute(proposalId, index) - const [actualParticipationBaseline, , ,] = await governance.getParticipationParameters() - assertEqualBN(actualParticipationBaseline, expectedParticipationBaseline) - }) - - it('should emit the ParticipationBaselineUpdated event', async () => { - const resp = await governance.execute(proposalId, index) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches2(log, { - event: 'ParticipationBaselineUpdated', - args: { - participationBaseline: expectedParticipationBaseline, - }, - }) - }) - }) - - describe('when a proposal with 0 transactions is past the execution stage', () => { - beforeEach(async () => { - await governance.propose( - [], - [], - // @ts-ignore - [], - [], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await timeTravel(dequeueFrequency, web3) - await mockLockedGold.setAccountTotalGovernancePower(account, yesVotes) - }) - - it('should not emit ProposalExecuted when not approved', async () => { - await governance.vote(proposalId, index, value) - await timeTravel(referendumStageDuration, web3) - await timeTravel(executionStageDuration, web3) - const resp = await governance.execute(proposalId, index) - assert.isTrue( - resp.logs.every((log) => log.event !== 'ProposalExecuted'), - 'ProposalExecuted should not be emitted' - ) - }) - - it('should not emit ProposalExecuted when not passing', async () => { - await governance.approve(proposalId, index) - await timeTravel(referendumStageDuration, web3) - await timeTravel(executionStageDuration, web3) - const resp = await governance.execute(proposalId, index) - assert.isTrue( - resp.logs.every((log) => log.event !== 'ProposalExecuted'), - 'ProposalExecuted should not be emitted' - ) - }) - - describe('Proposal approved and passing', () => { - beforeEach(async () => { - await governance.approve(proposalId, index) - await governance.vote(proposalId, index, value) - await timeTravel(referendumStageDuration, web3) - await timeTravel(executionStageDuration, web3) - }) - - it('should return true', async () => { - const success = await governance.execute.call(proposalId, index) - assert.isTrue(success) - }) - - it('should delete the proposal', async () => { - await governance.execute(proposalId, index) - assert.isFalse(await governance.proposalExists(proposalId)) - }) - - it('should remove the proposal ID from dequeued', async () => { - await governance.execute(proposalId, index) - const dequeued = await governance.getDequeue() - assert.notInclude( - dequeued.map((x) => x.toNumber()), - proposalId - ) - }) - - it('should add the index to empty indices', async () => { - await governance.execute(proposalId, index) - const emptyIndex = await governance.emptyIndices(0) - assert.equal(emptyIndex.toNumber(), index) - }) - - it('should update the participation baseline', async () => { - await governance.execute(proposalId, index) - const [actualParticipationBaseline, , ,] = await governance.getParticipationParameters() - assertEqualBN(actualParticipationBaseline, expectedParticipationBaseline) - }) - - it('should emit ProposalExecuted and ParticipationBaselineUpdated event', async () => { - const resp = await governance.execute(proposalId, index) - assert.equal(resp.logs.length, 2) - const log = resp.logs[0] - assertLogMatches2(log, { - event: 'ProposalExecuted', - args: { - proposalId: new BigNumber(proposalId), - }, - }) - const log2 = resp.logs[1] - assertLogMatches2(log2, { - event: 'ParticipationBaselineUpdated', - args: { - participationBaseline: expectedParticipationBaseline, - }, - }) - }) - }) - }) - }) - - describe('#approveHotfix()', () => { - it('should mark the hotfix record approved when called by approver', async () => { - await governance.approveHotfix(hotfixHashStr, { from: approver }) - const [approved, ,] = await governance.getHotfixRecord.call(hotfixHashStr) - assert.isTrue(approved) - }) - - it('should emit the HotfixApproved event', async () => { - const resp = await governance.approveHotfix(hotfixHashStr, { from: approver }) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches2(log, { - event: 'HotfixApproved', - args: { - hash: matchAny, - }, - }) - assert.isTrue(Buffer.from(stripHexEncoding(log.args.hash), 'hex').equals(hotfixHash)) - }) - - it('should revert when called by non-approver', async () => { - await assertTransactionRevertWithReason( - governance.approveHotfix(hotfixHashStr, { from: accounts[2] }), - 'msg.sender not approver' - ) - }) - }) - - describe('#whitelistHotfix()', () => { - beforeEach(async () => { - // from GovernanceTest - await governance.addValidator(accounts[2]) - await governance.addValidator(accounts[3]) - }) - - it('should emit the HotfixWhitelist event', async () => { - const resp = await governance.whitelistHotfix(hotfixHashStr, { from: accounts[3] }) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches2(log, { - event: 'HotfixWhitelisted', - args: { - hash: matchAny, - whitelister: accounts[3], - }, - }) - assert.isTrue(Buffer.from(stripHexEncoding(log.args.hash), 'hex').equals(hotfixHash)) - }) - }) - - describe('#hotfixWhitelistValidatorTally', () => { - const newHotfixHash = bufferToHex(toBuffer(keccak256(utf8ToBytes('celo bug fix')))) - - const validators = zip( - (_account, signer) => ({ account: _account, signer }), - accounts.slice(2, 5), - accounts.slice(5, 8) - ) - - beforeEach(async () => { - for (const validator of validators) { - await accountsInstance.createAccount({ from: validator.account }) - const sig = await getParsedSignatureOfAddress(web3, validator.account, validator.signer) - await accountsInstance.authorizeValidatorSigner(validator.signer, sig.v, sig.r, sig.s, { - from: validator.account, - }) - // add signers for mock precompile - await governance.addValidator(validator.signer) - } - }) - - const whitelistFrom = (t: keyof (typeof validators)[0]) => - concurrentMap(5, validators, (v) => governance.whitelistHotfix(newHotfixHash, { from: v[t] })) - - const checkTally = async () => { - const tally = await governance.hotfixWhitelistValidatorTally(newHotfixHash) - assert.equal(tally.toNumber(), validators.length) - } - - it('should count validator accounts that have whitelisted', async () => { - await whitelistFrom('account') - await checkTally() - }) - - it('should count authorized validator signers that have whitelisted', async () => { - await whitelistFrom('signer') - await checkTally() - }) - - it('should not double count validator account and authorized signer accounts', async () => { - await whitelistFrom('signer') - await whitelistFrom('account') - await checkTally() - }) - - it('should return the correct tally after key rotation', async () => { - await whitelistFrom('signer') - const newSigner = accounts[9] - const sig = await getParsedSignatureOfAddress(web3, validators[0].account, newSigner) - await accountsInstance.authorizeValidatorSigner(newSigner, sig.v, sig.r, sig.s, { - from: validators[0].account, - }) - await checkTally() - }) - }) - - describe('#isHotfixPassing', () => { - beforeEach(async () => { - await governance.addValidator(accounts[2]) - await governance.addValidator(accounts[3]) - await accountsInstance.createAccount({ from: accounts[2] }) - await accountsInstance.createAccount({ from: accounts[3] }) - }) - - it('should return false when hotfix has not been whitelisted', async () => { - const passing = await governance.isHotfixPassing.call(hotfixHashStr) - assert.isFalse(passing) - }) - - it('should return false when hotfix has been whitelisted but not by quorum', async () => { - await governance.whitelistHotfix(hotfixHashStr, { from: accounts[2] }) - const passing = await governance.isHotfixPassing.call(hotfixHashStr) - assert.isFalse(passing) - }) - - it('should return true when hotfix is whitelisted by quorum', async () => { - await governance.whitelistHotfix(hotfixHashStr, { from: accounts[2] }) - await governance.whitelistHotfix(hotfixHashStr, { from: accounts[3] }) - const passing = await governance.isHotfixPassing.call(hotfixHashStr) - assert.isTrue(passing) - }) - }) - - describe('#prepareHotfix()', () => { - beforeEach(async () => { - await governance.addValidator(accounts[2]) - await accountsInstance.createAccount({ from: accounts[2] }) - }) - - it('should revert when hotfix is not passing', async () => { - await assertTransactionRevertWithReason( - governance.prepareHotfix(hotfixHashStr), - 'hotfix not whitelisted by 2f[+]1 validators' - ) - }) - - describe('when hotfix is passing', () => { - beforeEach(async () => { - await mineToNextEpoch(web3) - await governance.whitelistHotfix(hotfixHashStr, { from: accounts[2] }) - }) - - it('should mark the hotfix record prepared epoch', async () => { - await governance.prepareHotfix(hotfixHashStr) - const [, , preparedEpoch] = await governance.getHotfixRecord.call(hotfixHashStr) - const currEpoch = new BigNumber(await governance.getEpochNumber()) - assertEqualBN(preparedEpoch, currEpoch) - }) - - it('should emit the HotfixPrepared event', async () => { - const resp = await governance.prepareHotfix(hotfixHashStr) - const currEpoch = new BigNumber(await governance.getEpochNumber()) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches2(log, { - event: 'HotfixPrepared', - args: { - hash: matchAny, - epoch: currEpoch, - }, - }) - assert.isTrue(Buffer.from(stripHexEncoding(log.args.hash), 'hex').equals(hotfixHash)) - }) - - it('should revert when epoch == preparedEpoch', async () => { - await governance.prepareHotfix(hotfixHashStr) - await assertTransactionRevertWithReason( - governance.prepareHotfix(hotfixHashStr), - 'hotfix already prepared for this epoch' - ) - }) - - it('should succeed for epoch != preparedEpoch', async () => { - await governance.prepareHotfix(hotfixHashStr) - await mineToNextEpoch(web3) - await governance.prepareHotfix(hotfixHashStr) - }) - }) - }) - - describe('#executeHotfix()', () => { - const executeHotfixTx = () => { - return governance.executeHotfix( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - salt - ) - } - - it('should revert when hotfix not approved', async () => { - await assertTransactionRevertWithReason(executeHotfixTx(), 'hotfix not approved') - }) - - it('should revert when hotfix not prepared for current epoch', async () => { - await mineToNextEpoch(web3) - await governance.approveHotfix(hotfixHashStr, { from: approver }) - await assertTransactionRevertWithReason( - executeHotfixTx(), - 'hotfix must be prepared for this epoch' - ) - }) - - it('should revert when hotfix prepared but not for current epoch', async () => { - await governance.approveHotfix(hotfixHashStr, { from: approver }) - await governance.addValidator(accounts[2]) - await accountsInstance.createAccount({ from: accounts[2] }) - await governance.whitelistHotfix(hotfixHashStr, { from: accounts[2] }) - await governance.prepareHotfix(hotfixHashStr, { from: accounts[2] }) - await mineToNextEpoch(web3) - await assertTransactionRevertWithReason( - executeHotfixTx(), - 'hotfix must be prepared for this epoch' - ) - }) - - describe('when hotfix is approved and prepared for current epoch', () => { - beforeEach(async () => { - await governance.approveHotfix(hotfixHashStr, { from: approver }) - await mineToNextEpoch(web3) - await governance.addValidator(accounts[2]) - await accountsInstance.createAccount({ from: accounts[2] }) - await governance.whitelistHotfix(hotfixHashStr, { from: accounts[2] }) - await governance.prepareHotfix(hotfixHashStr) - }) - - it('should execute the hotfix tx', async () => { - await executeHotfixTx() - assert.equal(await testTransactions.getValue(1).valueOf(), 1) - }) - - it('should mark the hotfix record as executed', async () => { - await executeHotfixTx() - const [, executed] = await governance.getHotfixRecord.call(hotfixHashStr) - assert.isTrue(executed) - }) - - it('should emit the HotfixExecuted event', async () => { - const resp = await executeHotfixTx() - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches2(log, { - event: 'HotfixExecuted', - args: { - hash: matchAny, - }, - }) - assert.isTrue(Buffer.from(stripHexEncoding(log.args.hash), 'hex').equals(hotfixHash)) - }) - - it('should not be executable again', async () => { - await executeHotfixTx() - await assertTransactionRevertWithReason(executeHotfixTx(), 'hotfix already executed') - }) - }) - }) - - describe('#isVoting()', () => { - describe('when the account has never acted on a proposal', () => { - it('should return false', async () => { - assert.isFalse(await governance.isVoting(account)) - }) - }) - - describe('when the account has upvoted a proposal', () => { - const proposalId = 1 - beforeEach(async () => { - await mockLockedGold.setAccountTotalLockedGold(account, yesVotes) - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await governance.upvote(proposalId, 0, 0) - }) - - it('should return true', async () => { - assert.isTrue(await governance.isVoting(account)) - }) - - describe('when that upvote has been revoked', () => { - beforeEach(async () => { - await governance.revokeUpvote(0, 0) - }) - - it('should return false', async () => { - assert.isFalse(await governance.isVoting(account)) - }) - }) - - describe('when that proposal has expired from the queue', () => { - beforeEach(async () => { - await timeTravel(queueExpiry, web3) - }) - - it('should return false', async () => { - assert.isFalse(await governance.isVoting(account)) - }) - }) - }) - - describe('when the account has voted on a proposal', () => { - const proposalId = 1 - const index = 0 - const value = VoteValue.Abstain - beforeEach(async () => { - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await timeTravel(dequeueFrequency, web3) - await governance.approve(proposalId, index) - await mockLockedGold.setAccountTotalGovernancePower(account, yesVotes) - await governance.vote(proposalId, index, value) - }) - - it('should return true', async () => { - assert.isTrue(await governance.isVoting(account)) - }) - - describe('when that proposal is no longer in the referendum stage', () => { - beforeEach(async () => { - await timeTravel(referendumStageDuration, web3) - }) - - it('should return false', async () => { - assert.isFalse(await governance.isVoting(account)) - }) - }) - }) - }) - - describe('#isProposalPassing()', () => { - const proposalId = 1 - const index = 0 - beforeEach(async () => { - await accountsInstance.createAccount({ from: otherAccount }) - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await timeTravel(dequeueFrequency, web3) - await governance.approve(proposalId, index) - }) - - describe('when the adjusted support is greater than threshold', () => { - beforeEach(async () => { - await mockLockedGold.setAccountTotalGovernancePower(account, (yesVotes * 51) / 100) - await mockLockedGold.setAccountTotalGovernancePower(otherAccount, (yesVotes * 49) / 100) - await governance.vote(proposalId, index, VoteValue.Yes) - await governance.vote(proposalId, index, VoteValue.No, { from: otherAccount }) - }) - - it('should return true', async () => { - const passing = await governance.isProposalPassing(proposalId) - assert.isTrue(passing) - }) - }) - - describe('when the adjusted support is less than or equal to threshold', () => { - beforeEach(async () => { - await mockLockedGold.setAccountTotalGovernancePower(account, (yesVotes * 50) / 100) - await mockLockedGold.setAccountTotalGovernancePower(otherAccount, (yesVotes * 50) / 100) - await governance.vote(proposalId, index, VoteValue.Yes) - await governance.vote(proposalId, index, VoteValue.No, { from: otherAccount }) - }) - - it('should return false', async () => { - const passing = await governance.isProposalPassing(proposalId) - assert.isFalse(passing) - }) - }) - }) - - describe('#dequeueProposalsIfReady()', () => { - it('should not update lastDequeue when there are no queued proposals', async () => { - const originalLastDequeue = await governance.lastDequeue() - await timeTravel(dequeueFrequency, web3) - await governance.dequeueProposalsIfReady() - - assert.equal((await governance.getQueueLength()).toNumber(), 0) - assert.equal((await governance.lastDequeue()).toNumber(), originalLastDequeue.toNumber()) - }) - - describe('when a proposal exists', () => { - beforeEach(async () => { - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - { value: minDeposit } - ) - }) - - it('should update lastDequeue', async () => { - const originalLastDequeue = await governance.lastDequeue() - - await timeTravel(dequeueFrequency, web3) - await governance.dequeueProposalsIfReady() - - assert.equal((await governance.getQueueLength()).toNumber(), 0) - assert.isTrue((await governance.lastDequeue()).toNumber() > originalLastDequeue.toNumber()) - }) - - it('should not update lastDequeue when only expired proposal queued', async () => { - const originalLastDequeue = await governance.lastDequeue() - - await timeTravel(queueExpiry, web3) - await governance.dequeueProposalsIfReady() - - assert.equal((await governance.getQueueLength()).toNumber(), 0) - assert.equal((await governance.lastDequeue()).toNumber(), originalLastDequeue.toNumber()) - }) - }) - }) - - describe('#getProposalStage()', () => { - const expectStage = async (expected: Stage, _proposalId: number) => { - const stage = await governance.getProposalStage(_proposalId) - assertEqualBN(stage, expected) - } - - it('should return None stage when proposal does not exist', async () => { - await expectStage(Stage.None, 0) - await expectStage(Stage.None, 1) - }) - - describe('when proposal exists', () => { - let proposalId: number - beforeEach(async () => { - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - { value: minDeposit } - ) - proposalId = 1 - const exists = await governance.proposalExists(proposalId) - assert.isTrue(exists, 'proposal does not exist') - }) - - describe('when proposal is queued', () => { - beforeEach(async () => { - const queued = await governance.isQueued(proposalId) - assert.isTrue(queued, 'proposal not queued') - }) - - it('should return Queued when not expired', () => expectStage(Stage.Queued, proposalId)) - - it('should return Expiration when expired', async () => { - await timeTravel(queueExpiry, web3) - await expectStage(Stage.Expiration, proposalId) - }) - }) - - describe('when proposal is dequeued', () => { - const index = 0 - beforeEach(async () => { - await timeTravel(dequeueFrequency, web3) - await governance.dequeueProposalsIfReady() - const dequeued = await governance.isDequeuedProposal(proposalId, index) - assert.isTrue(dequeued, 'proposal not dequeued') - }) - - describe('when in referendum stage', () => { - describe('when not approved', () => { - it('should return Referendum when not voted and not expired', () => - expectStage(Stage.Referendum, proposalId)) - - it('should return Expiration when expired', async () => { - await timeTravel(referendumStageDuration, web3) - await expectStage(Stage.Expiration, proposalId) - }) - }) - - describe('when approved', () => { - beforeEach(async () => { - await governance.approve(proposalId, index) - }) - - it('should return Referendum when not expired', () => - expectStage(Stage.Referendum, proposalId)) - - it('should return Expiration when expired', async () => { - await timeTravel(referendumStageDuration, web3) - await expectStage(Stage.Expiration, proposalId) - }) - }) - }) - - describe('when in execution stage', () => { - beforeEach(async () => { - await governance.approve(proposalId, index) - await governance.vote(proposalId, index, VoteValue.Yes) - const passing = await governance.isProposalPassing(proposalId) - assert.isTrue(passing, 'proposal not passing') - await timeTravel(referendumStageDuration, web3) - }) - - it('should return Execution when not expired', () => - expectStage(Stage.Execution, proposalId)) - - it('should return Expiration when expired', async () => { - await timeTravel(executionStageDuration, web3) - await expectStage(Stage.Expiration, proposalId) - const isDequeuedProposalExpired = await governance.isDequeuedProposalExpired(proposalId) - assert.isTrue(isDequeuedProposalExpired) - }) - }) - }) - }) - - describe('when a proposal with 0 transactions exists', () => { - let proposalId: number - beforeEach(async () => { - await governance.propose( - [], - [], - // @ts-ignore - [], - [], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - proposalId = 1 - const exists = await governance.proposalExists(proposalId) - assert.isTrue(exists, 'proposal does not exist') - }) - - describe('when proposal with 0 transactions is dequeued', () => { - const index = 0 - beforeEach(async () => { - await timeTravel(dequeueFrequency, web3) - await governance.dequeueProposalsIfReady() - const dequeued = await governance.isDequeuedProposal(proposalId, index) - assert.isTrue(dequeued, 'proposal not dequeued') - }) - - it('should return Expiration past the execution stage when not approved', async () => { - await governance.vote(proposalId, index, VoteValue.Yes) - await timeTravel(referendumStageDuration + executionStageDuration + 1, web3) - await expectStage(Stage.Expiration, proposalId) - const isDequeuedProposalExpired = await governance.isDequeuedProposalExpired(proposalId) - assert.isTrue(isDequeuedProposalExpired) - }) - - it('should return Expiration past the execution stage when not passing', async () => { - await governance.approve(proposalId, index) - await timeTravel(referendumStageDuration + executionStageDuration + 1, web3) - await expectStage(Stage.Expiration, proposalId) - const isDequeuedProposalExpired = await governance.isDequeuedProposalExpired(proposalId) - assert.isTrue(isDequeuedProposalExpired) - }) - - describe('when in execution stage', () => { - beforeEach(async () => { - await governance.approve(proposalId, index) - await governance.vote(proposalId, index, VoteValue.Yes) - const passing = await governance.isProposalPassing(proposalId) - assert.isTrue(passing, 'proposal not passing') - await timeTravel(referendumStageDuration, web3) - }) - - it('should return Execution when not expired', () => - expectStage(Stage.Execution, proposalId)) - - it('should return Execution past the execution stage if passed and approved', async () => { - await timeTravel(executionStageDuration + 1, web3) - await expectStage(Stage.Execution, proposalId) - const isDequeuedProposalExpired = await governance.isDequeuedProposalExpired(proposalId) - assert.isFalse(isDequeuedProposalExpired) - }) - }) - }) - }) - }) - - describe('#getAmountOfGoldUsedForVoting()', () => { - describe('3 concurrent proposals dequeued', () => { - beforeEach(async () => { - await governance.setConcurrentProposals(3) - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await governance.propose( - [transactionSuccess2.value], - [transactionSuccess2.destination], - // @ts-ignore bytes type - transactionSuccess2.data, - [transactionSuccess2.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await timeTravel(dequeueFrequency, web3) - await governance.approve(1, 0) - await governance.approve(2, 1) - await governance.approve(3, 2) - await mockLockedGold.setAccountTotalGovernancePower(account, yesVotes) - }) - for (let numVoted = 0; numVoted < 3; numVoted++) { - describe(`when account has partially voted on ${numVoted} proposals`, () => { - const yes = 10 - const no = 30 - const abstain = 0 - - beforeEach(async () => { - for (let i = 0; i < numVoted; i++) { - await governance.votePartially(1, 0, yes, no, abstain) - } - }) - - it('Should return correct number of votes', async () => { - const totalVotesByAccount = await governance.getAmountOfGoldUsedForVoting(accounts[0]) - const expectedArraySum = yes + no + abstain - assertEqualBN(totalVotesByAccount, numVoted === 0 ? 0 : expectedArraySum) - }) - }) - } - }) - - describe('proposal dequeued', () => { - beforeEach(async () => { - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await timeTravel(dequeueFrequency, web3) - await governance.approve(1, 0) - await mockLockedGold.setAccountTotalGovernancePower(account, yesVotes) - }) - - describe('When account voted on proposal in V8', () => { - beforeEach(async () => { - await governance.setDeprecatedWeight(accounts[0], 0, 100) - }) - - it('Should return correct number of votes', async () => { - const totalVotesByAccount = await governance.getAmountOfGoldUsedForVoting(accounts[0]) - assertEqualBN(totalVotesByAccount, 100) - }) - }) - - describe(`when account has partially voted on proposal`, () => { - const yes = 10 - const no = 30 - const abstain = 0 - - beforeEach(async () => { - await governance.votePartially(1, 0, yes, no, abstain) - }) - - it('Should return correct number of votes', async () => { - const totalVotesByAccount = await governance.getAmountOfGoldUsedForVoting(accounts[0]) - const expectedArraySum = yes + no + abstain - - assertEqualBN(totalVotesByAccount, expectedArraySum) - }) - - it('Should return 0 votes since expired', async () => { - await timeTravel(executionStageDuration + referendumStageDuration + 1, web3) - const totalVotesByAccount = await governance.getAmountOfGoldUsedForVoting(accounts[0]) - - assertEqualBN(totalVotesByAccount, 0) - }) - }) - }) - - describe('proposal in queue', () => { - beforeEach(async () => { - const proposalId1 = 1 - - await governance.setConcurrentProposals(3) - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - await mockLockedGold.setAccountTotalLockedGold(account, yesVotes) - await governance.upvote(proposalId1, 0, 0) - }) - - it('should return full weight when upvoting', async () => { - const totalVotesByAccount = await governance.getAmountOfGoldUsedForVoting(accounts[0]) - assertEqualBN(totalVotesByAccount, yesVotes) - }) - - it('should return 0 since proposal is already expired', async () => { - await timeTravel(queueExpiry, web3) - const totalVotesByAccount = await governance.getAmountOfGoldUsedForVoting(accounts[0]) - assertEqualBN(totalVotesByAccount, 0) - }) - }) - }) - - describe('#removeVotesWhenRevokingDelegatedVotes()', () => { - it('should revert when not called by staked celo contract', async () => { - await assertTransactionRevertWithReason( - governance.removeVotesWhenRevokingDelegatedVotes(NULL_ADDRESS, 0), - 'msg.sender not lockedGold' - ) - }) - - it('should should pass when no proposal is dequeued', async () => { - await governance.removeVotesWhenRevokingDelegatedVotesTest(NULL_ADDRESS, 0) - }) - - describe('When having three proposals voted', () => { - const proposalId = 1 - const index = 0 - - const proposal2Id = 2 - const index2 = 1 - - const proposal3Id = 3 - const index3 = 2 - beforeEach(async () => { - const newDequeueFrequency = 60 - await governance.setDequeueFrequency(newDequeueFrequency) - - await governance.propose( - [transactionSuccess1.value], - [transactionSuccess1.destination], - // @ts-ignore bytes type - transactionSuccess1.data, - [transactionSuccess1.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - - await governance.propose( - [transactionSuccess2.value], - [transactionSuccess2.destination], - // @ts-ignore bytes type - transactionSuccess2.data, - [transactionSuccess2.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - - await governance.propose( - [transactionSuccess2.value], - [transactionSuccess2.destination], - // @ts-ignore bytes type - transactionSuccess2.data, - [transactionSuccess2.data.length], - descriptionUrl, - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - { value: minDeposit } - ) - - await governance.setConcurrentProposals(3) - await timeTravel(newDequeueFrequency, web3) - await governance.approve(proposalId, index) - await timeTravel(newDequeueFrequency, web3) - await governance.approve(proposal2Id, index2) - await timeTravel(newDequeueFrequency, web3) - await governance.approve(proposal3Id, index3) - await mockLockedGold.setAccountTotalGovernancePower(account, yesVotes) - }) - - describe('When voting only for yes', () => { - const yes = 100 - const no = 0 - const abstain = 0 - - const yes2 = 0 - const no2 = 100 - const abstain2 = 0 - beforeEach(async () => { - await governance.votePartially(proposalId, index, yes, no, abstain) - await governance.votePartially(proposal2Id, index2, yes2, no2, abstain2) - - await assertVoteRecord(governance, account, index, proposalId, yes, no, abstain) - await assertVoteRecord(governance, account, index2, proposal2Id, yes2, no2, abstain2) - - await assertVotesTotal(governance, proposalId, yes, no, abstain) - await assertVotesTotal(governance, proposal2Id, yes2, no2, abstain2) - }) - - it('should adjust votes correctly to 0', async () => { - const maxAmount = 0 - - await governance.removeVotesWhenRevokingDelegatedVotesTest(account, maxAmount) - - await assertVoteRecord(governance, account, index, proposalId, 0, 0, 0) - await assertVoteRecord(governance, account, index2, proposal2Id, 0, 0, 0) - - await assertVotesTotal(governance, proposalId, 0, 0, 0) - await assertVotesTotal(governance, proposal2Id, 0, 0, 0) - }) - - it('should adjust votes correctly to 30', async () => { - const maxAmount = 30 - - await governance.removeVotesWhenRevokingDelegatedVotesTest(account, maxAmount) - - await assertVoteRecord(governance, account, index, proposalId, maxAmount, 0, 0) - await assertVoteRecord(governance, account, index2, proposal2Id, 0, maxAmount, 0) - - await assertVotesTotal(governance, proposalId, maxAmount, 0, 0) - await assertVotesTotal(governance, proposal2Id, 0, maxAmount, 0) - }) - }) - - describe('When voting for all choices', () => { - const yes = 34 - const no = 33 - const abstain = 33 - - const yes2 = 0 - const no2 = 35 - const abstain2 = 65 - - const yes3 = 0 - const no3 = 0 - const abstain3 = 51 - - beforeEach(async () => { - await governance.votePartially(proposalId, index, yes, no, abstain) - await governance.votePartially(proposal2Id, index2, yes2, no2, abstain2) - await governance.votePartially(proposal3Id, index3, yes3, no3, abstain3) - - await assertVoteRecord(governance, account, index, proposalId, yes, no, abstain) - await assertVoteRecord(governance, account, index2, proposal2Id, yes2, no2, abstain2) - await assertVoteRecord(governance, account, index3, proposal3Id, yes3, no3, abstain3) - }) - - it('should adjust votes correctly to 0', async () => { - const maxAmount = 0 - - await governance.removeVotesWhenRevokingDelegatedVotesTest(account, maxAmount) - - await assertVoteRecord(governance, account, index, proposalId, 0, 0, 0) - await assertVoteRecord(governance, account, index2, proposal2Id, 0, 0, 0) - }) - - it('should adjust votes correctly to 50', async () => { - const maxAmount = 50 - const sumOfVotes = yes + no + abstain - const toRemove = sumOfVotes - maxAmount - const yesPortion = (toRemove * yes) / sumOfVotes - const noPortion = (toRemove * no) / sumOfVotes - const abstainPortion = (toRemove * abstain) / sumOfVotes - - const no2Portion = (toRemove * no2) / sumOfVotes - const abstain2Portion = (toRemove * abstain2) / sumOfVotes - - await governance.removeVotesWhenRevokingDelegatedVotesTest(account, maxAmount) - - const [yes1Total, no1Total, abstain1Total] = await governance.getVoteTotals(proposalId) - const [yes2Total, no2Total, abstain2Total] = await governance.getVoteTotals(proposal2Id) - const [yes3Total, no3Total, abstain3Total] = await governance.getVoteTotals(proposal3Id) - - assertEqualBN(yes1Total.plus(no1Total).plus(abstain1Total), maxAmount) - assertEqualBN(yes2Total.plus(no2Total).plus(abstain2Total), maxAmount) - assertEqualBN(yes3Total.plus(no3Total).plus(abstain3Total), maxAmount) - - assertEqualBN(yes1Total, Math.ceil(yesPortion) - 1) // -1 because of rounding - assertEqualBN(no1Total, Math.ceil(noPortion)) - assertEqualBN(abstain1Total, Math.ceil(abstainPortion)) - - assertEqualBN(yes2Total, 0) - assertEqualBN(no2Total, Math.ceil(no2Portion) - 1) // -1 because of rounding - assertEqualBN(abstain2Total, Math.ceil(abstain2Portion)) - - await assertVoteRecord( - governance, - account, - index, - proposalId, - Math.ceil(yesPortion - 1), // -1 because of rounding - Math.ceil(noPortion), - Math.ceil(abstainPortion) - ) - await assertVoteRecord( - governance, - account, - index2, - proposal2Id, - Math.ceil(0), - Math.ceil(no2Portion - 1), // -1 because of rounding - Math.ceil(abstain2Portion) - ) - }) - - describe('When proposals are expired', () => { - beforeEach(async () => { - await timeTravel(queueExpiry, web3) - }) - - it('should not adjust votes', async () => { - await assertVoteRecord(governance, account, index, proposalId, yes, no, abstain) - await assertVoteRecord(governance, account, index2, proposal2Id, yes2, no2, abstain2) - }) - }) - }) - }) - }) -}) - -async function assertVoteRecord( - governance: GovernanceTestInstance, - account: string, - index: number, - assertId: number, - assertYes: number, - assertNo: number, - asssertAbstain: number -) { - const [recordProposalId2, , , yesVotesRecord2, noVotesRecord2, abstainVotesRecord2] = - await governance.getVoteRecord(account, index) - - assertEqualBN(recordProposalId2, assertId) - assertEqualBN(yesVotesRecord2, assertYes) - assertEqualBN(noVotesRecord2, assertNo) - assertEqualBN(abstainVotesRecord2, asssertAbstain) -} - -async function assertVotesTotal( - governance: GovernanceTestInstance, - proposalId: number, - assertYes: number, - assertNo: number, - assertAbstain: number -) { - const [yesVotes, noVotes, abstainVotes] = await governance.getVoteTotals(proposalId) - assertEqualBN(yesVotes, assertYes) - assertEqualBN(noVotes, assertNo) - assertEqualBN(abstainVotes, assertAbstain) -}