From 6ef63503675848368045ee099f92eb24fba9ace4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8r=E2=88=82=C2=A1?= <4456749@users.noreply.github.com> Date: Wed, 15 Nov 2023 14:57:20 +0000 Subject: [PATCH 1/6] Scaffold --- src/IOptimisticTokenVoting.sol | 2 +- src/OptimisticTokenVotingPlugin.sol | 5 +- src/OptimisticTokenVotingPluginSetup.sol | 2 +- test/OptimisticTokenVotingPlugin.t.sol | 369 +++++++++++++++++++- test/OptimisticTokenVotingPluginSetup.t.sol | 2 +- test/common.sol | 2 +- test/mocks/ERC20Mock.sol | 56 +++ 7 files changed, 424 insertions(+), 14 deletions(-) create mode 100644 test/mocks/ERC20Mock.sol diff --git a/src/IOptimisticTokenVoting.sol b/src/IOptimisticTokenVoting.sol index 0b4348d..01fdd4e 100644 --- a/src/IOptimisticTokenVoting.sol +++ b/src/IOptimisticTokenVoting.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity ^0.8.8; +pragma solidity ^0.8.17; import {IVotesUpgradeable} from "@openzeppelin/contracts-upgradeable/governance/utils/IVotesUpgradeable.sol"; import {IDAO} from "@aragon/osx/core/dao/IDAO.sol"; diff --git a/src/OptimisticTokenVotingPlugin.sol b/src/OptimisticTokenVotingPlugin.sol index 68fe8ab..ad41736 100644 --- a/src/OptimisticTokenVotingPlugin.sol +++ b/src/OptimisticTokenVotingPlugin.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity ^0.8.8; +pragma solidity ^0.8.17; import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; @@ -12,9 +12,8 @@ import {IOptimisticTokenVoting} from "./IOptimisticTokenVoting.sol"; import {ProposalUpgradeable} from "@aragon/osx/core/plugin/proposal/ProposalUpgradeable.sol"; import {PluginUUPSUpgradeable} from "@aragon/osx/core/plugin/PluginUUPSUpgradeable.sol"; -import {RATIO_BASE, _applyRatioCeiled} from "@aragon/osx/plugins/utils/Ratio.sol"; +import {RATIO_BASE, _applyRatioCeiled, RatioOutOfBounds} from "@aragon/osx/plugins/utils/Ratio.sol"; import {IDAO} from "@aragon/osx/core/dao/IDAO.sol"; -import {RATIO_BASE, RatioOutOfBounds} from "@aragon/osx/plugins/utils/Ratio.sol"; /// @title OptimisticTokenVotingPlugin /// @author Aragon Association - 2023 diff --git a/src/OptimisticTokenVotingPluginSetup.sol b/src/OptimisticTokenVotingPluginSetup.sol index ff0f61c..3250633 100644 --- a/src/OptimisticTokenVotingPluginSetup.sol +++ b/src/OptimisticTokenVotingPluginSetup.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity ^0.8.8; +pragma solidity ^0.8.17; import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; diff --git a/test/OptimisticTokenVotingPlugin.t.sol b/test/OptimisticTokenVotingPlugin.t.sol index 1fc045f..00d08a0 100644 --- a/test/OptimisticTokenVotingPlugin.t.sol +++ b/test/OptimisticTokenVotingPlugin.t.sol @@ -1,24 +1,379 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; +pragma solidity ^0.8.17; import {Test, console2} from "forge-std/Test.sol"; import {OptimisticTokenVotingPlugin} from "../src/OptimisticTokenVotingPlugin.sol"; +import {DAO} from "@aragon/osx/core/dao/DAO.sol"; +import {IDAO} from "@aragon/osx/core/dao/IDAO.sol"; +import {ERC20Mock} from "./mocks/ERC20Mock.sol"; +import {RATIO_BASE} from "@aragon/osx/plugins/utils/Ratio.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; contract OptimisticTokenVotingPluginTest is Test { + address immutable daoBase = address(new DAO()); + address immutable pluginBase = address(new OptimisticTokenVotingPlugin()); + address immutable votingTokenBase = address(new ERC20Mock()); + + DAO public dao; OptimisticTokenVotingPlugin public plugin; + ERC20Mock votingToken; + + address alice = address(0xa11ce); + address bob = address(0xB0B); + address randomUser = vm.addr(1234567890); + error Unimplemented(); function setUp() public { - plugin = new OptimisticTokenVotingPlugin(); - + // Deploy a DAO with Alice as root + dao = DAO( + payable( + createProxyAndCall( + address(daoBase), + abi.encodeWithSelector( + DAO.initialize.selector, + "", + alice, + address(0x0), + "" + ) + ) + ) + ); + + // Deploy ERC20 token + votingToken = ERC20Mock( + createProxyAndCall( + address(votingTokenBase), + abi.encodeWithSelector(ERC20Mock.initialize.selector) + ) + ); + + vm.roll(block.number + 1); + + // Deploy a new plugin instance + OptimisticTokenVotingPlugin.OptimisticGovernanceSettings + memory settings = OptimisticTokenVotingPlugin + .OptimisticGovernanceSettings({ + minVetoRatio: uint32(RATIO_BASE / 10), + minDuration: 10 days, + minProposerVotingPower: 0 ether + }); + + plugin = OptimisticTokenVotingPlugin( + createProxyAndCall( + address(pluginBase), + abi.encodeWithSelector( + OptimisticTokenVotingPlugin.initialize.selector, + dao, + settings, + votingToken + ) + ) + ); + + // The plugin can execute on the DAO + dao.grant(address(dao), address(plugin), dao.EXECUTE_PERMISSION_ID()); + + // Alice can create proposals on the plugin + dao.grant(address(plugin), alice, plugin.PROPOSER_PERMISSION_ID()); + } + + function test_InitializeRevertsIfInitializing() public { + revert Unimplemented(); + } + + function test_InitializeSetsTheProperValues() public { revert Unimplemented(); } - function test_OnlyProposers() public {} - function test_NonProposersCannotCreate() public {} - function test_CorrectDates() public {} + function test_InitializeCreatesANewERC20Token() public { + revert Unimplemented(); + } - function testFuzz_SetNumber(uint256 x) public { + function test_InitializeWrapsAnExistingToken() public { revert Unimplemented(); } + + function test_InitializeUsesAnExistingERC20Token() public { + revert Unimplemented(); + } + + function test_InitializeEmitsEvent() public { + revert Unimplemented(); + } + + function test_SupportsInterfaceReturnsTrueForOptimisticGovernance() public { + revert Unimplemented(); + } + + function test_SupportsInterfaceReturnsTrueForIOptimisticTokenVoting() + public + { + revert Unimplemented(); + } + + function test_SupportsInterfaceReturnsTrueForIMembership() public { + revert Unimplemented(); + } + + function test_SupportsInterfaceReturnsTrueForIProposal() public { + revert Unimplemented(); + } + + function test_SupportsInterfaceReturnsTrueForIERC165Upgradeable() public { + revert Unimplemented(); + } + + function test_SupportsInterfaceReturnsFalseOtherwise() public { + revert Unimplemented(); + } + + function test_GetVotingTokenReturnsTheRightAddress() public { + revert Unimplemented(); + } + + function test_TotalVotingPowerReturnsTheRightSupply() public { + revert Unimplemented(); + } + + function test_MinVetoRatioReturnsTheRightValue() public { + revert Unimplemented(); + } + + function test_MinDurationReturnsTheRightValue() public { + revert Unimplemented(); + } + + function test_MinProposerVotingPowerReturnsTheRightValue() public { + revert Unimplemented(); + } + + function test_TokenHoldersAreMembers() public { + revert Unimplemented(); + } + + function test_HasVetoedReturnsTheRightValue() public { + revert Unimplemented(); + } + + function test_CanVetoReturnsFalseWhenAProposalDoesntExist() public { + revert Unimplemented(); + } + + function test_CanVetoReturnsFalseWhenAProposalHasNotStarted() public { + revert Unimplemented(); + } + + function test_CanVetoReturnsFalseWhenAVoterAlreadyVetoed() public { + revert Unimplemented(); + } + + function test_CanVetoReturnsFalseWhenAnAddressHasNoVotingPower() public { + revert Unimplemented(); + } + + function test_CanVetoReturnsTrueOtherwise() public { + revert Unimplemented(); + } + + function test_CanExecuteReturnsFalseWhenAlreadyExecuted() public { + revert Unimplemented(); + } + + function test_CanExecuteReturnsFalseWhenStillOngoing() public { + revert Unimplemented(); + } + + function test_CanExecuteReturnsFalseWhenEnoughVetoesAreRegistered() public { + revert Unimplemented(); + } + + function test_CanExecuteReturnsTrueOtherwise() public { + revert Unimplemented(); + } + + function test_IsMinVetoRatioReachedReturnsFalseWhenNotEnoughPeopleHaveVetoed() + public + { + revert Unimplemented(); + } + + function test_IsMinVetoRatioReachedReturnsTrueWhenEnoughPeopleHaveVetoed() + public + { + revert Unimplemented(); + } + + function test_GetProposalReturnsTheRightValues() public { + revert Unimplemented(); + } + + function test_CreateProposalRevertsWhenCalledByANonProposer() public { + revert Unimplemented(); + } + + function test_CreateProposalSucceedsWhenMinimumVotingPowerIsZero() public { + revert Unimplemented(); + } + + function test_CreateProposalRevertsWhenTheCallerOwnsLessThanTheMinimumVotingPower() + public + { + revert Unimplemented(); + } + + function test_CreateProposalRevertsIfThereIsNoVotingPower() public { + revert Unimplemented(); + } + + function test_CreateProposalRevertsIfTheStartDateIsAfterTheEndDate() + public + { + revert Unimplemented(); + } + + function test_CreateProposalRevertsIfTheStartDateIsInThePast() public { + revert Unimplemented(); + } + + function test_CreateProposalStartsNowWhenStartDateIsZero() public { + revert Unimplemented(); + } + + function test_CreateProposalEndsAfterTheMinDurationWhenEndDateIsZero() + public + { + revert Unimplemented(); + } + + function test_CreateProposalUsesTheCurrentMinVetoRatio() public { + revert Unimplemented(); + } + + function test_CreateProposalReturnsTheProposalId() public { + revert Unimplemented(); + } + + function test_CreateProposalEmitsAnEvent() public { + revert Unimplemented(); + } + + function test_VetoRevertsWhenTheProposalDoesntExist() public { + revert Unimplemented(); + } + + function test_VetoRevertsWhenTheProposalHasNotStarted() public { + revert Unimplemented(); + } + + function test_VetoRevertsWhenNotATokenHolder() public { + revert Unimplemented(); + } + + function test_VetoRevertsWhenAlreadyVetoed() public { + revert Unimplemented(); + } + + function test_VetoRegistersAVetoForTheTokenHolderAndIncreasesTheTally() + public + { + revert Unimplemented(); + } + + function test_VetoEmitsAnEvent() public { + revert Unimplemented(); + } + + function test_ExecuteRevertsWhenAlreadyExecuted() public { + revert Unimplemented(); + } + + function test_ExecuteRevertsWhenStillOngoing() public { + revert Unimplemented(); + } + + function test_ExecuteRevertsWhenEnoughVetoesAreRegistered() public { + revert Unimplemented(); + } + + function testFuzz_ExecuteExecutesTheProposalActionsWhenNonDefeated( + uint256 _tokenSupply + ) public { + revert Unimplemented(); + } + + function test_ExecuteMarksTheProposalAsExecuted() public { + revert Unimplemented(); + } + + function test_ExecuteEmitsAnEvent() public { + revert Unimplemented(); + } + + function test_UpdateOptimisticGovernanceSettingsRevertsWhenNoPermission() + public + { + revert Unimplemented(); + } + + function test_UpdateOptimisticGovernanceSettingsRevertsWhenTheMinVetoRatioIsZero() + public + { + revert Unimplemented(); + } + + function test_UpdateOptimisticGovernanceSettingsRevertsWhenTheMinVetoRatioIsAboveTheMaximum() + public + { + revert Unimplemented(); + } + + function test_UpdateOptimisticGovernanceSettingsRevertsWhenTheMinDurationIsLessThanFourDays() + public + { + revert Unimplemented(); + } + + function test_UpdateOptimisticGovernanceSettingsRevertsWhenTheMinDurationIsMoreThanOneYear() + public + { + revert Unimplemented(); + } + + function test_UpdateOptimisticGovernanceSettingsRevertsWhenMinProposerVotingPowerIsMoreThanTheTokenSupply() + public + { + revert Unimplemented(); + } + + function test_UpdateOptimisticGovernanceSettingsEmitsAnEventWhenSuccessful() + public + { + revert Unimplemented(); + } + + function test_UpgradeToRevertsWhenCalledFromNonUpgrader() public { + revert Unimplemented(); + } + + function test_UpgradeToAndCallRevertsWhenCalledFromNonUpgrader() public { + revert Unimplemented(); + } + + function test_UpgradeToSucceedsWhenCalledFromUpgrader() public { + revert Unimplemented(); + } + + function test_UpgradeToAndCallSucceedsWhenCalledFromUpgrader() public { + revert Unimplemented(); + } + + // HELPERS + function createProxyAndCall( + address _logic, + bytes memory _data + ) private returns (address) { + return address(new ERC1967Proxy(_logic, _data)); + } } diff --git a/test/OptimisticTokenVotingPluginSetup.t.sol b/test/OptimisticTokenVotingPluginSetup.t.sol index d8d23db..aa75a92 100644 --- a/test/OptimisticTokenVotingPluginSetup.t.sol +++ b/test/OptimisticTokenVotingPluginSetup.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; +pragma solidity ^0.8.17; import {Test, console2} from "forge-std/Test.sol"; import {OptimisticTokenVotingPluginSetup} from "../src/OptimisticTokenVotingPluginSetup.sol"; diff --git a/test/common.sol b/test/common.sol index 5a45097..5318fae 100644 --- a/test/common.sol +++ b/test/common.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; +pragma solidity ^0.8.17; bytes32 constant PROPOSER_PERMISSION_ID = keccak256("PROPOSER_PERMISSION"); bytes32 constant EXECUTE_PERMISSION_ID = keccak256("EXECUTE_PERMISSION"); diff --git a/test/mocks/ERC20Mock.sol b/test/mocks/ERC20Mock.sol new file mode 100644 index 0000000..94dcf7d --- /dev/null +++ b/test/mocks/ERC20Mock.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.17 <0.9.0; + +import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import {ERC20VotesUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20VotesUpgradeable.sol"; +import {ERC20PermitUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PermitUpgradeable.sol"; + +import {IVotesUpgradeable} from "@openzeppelin/contracts-upgradeable/governance/utils/IVotesUpgradeable.sol"; + +contract ERC20Mock is + ERC20Upgradeable, + ERC20PermitUpgradeable, + ERC20VotesUpgradeable +{ + constructor() { + _disableInitializers(); + } + + function initialize() public initializer { + __ERC20_init("Mock Token", "MOCK"); + __ERC20Permit_init("Mock Token"); + __ERC20Votes_init(); + } + + // The functions below are overrides required by Solidity. + + function _afterTokenTransfer( + address from, + address to, + uint256 amount + ) internal override(ERC20Upgradeable, ERC20VotesUpgradeable) { + super._afterTokenTransfer(from, to, amount); + } + + function _mint( + address to, + uint256 amount + ) internal override(ERC20Upgradeable, ERC20VotesUpgradeable) { + super._mint(to, amount); + } + + function _burn( + address account, + uint256 amount + ) internal override(ERC20Upgradeable, ERC20VotesUpgradeable) { + super._burn(account, amount); + } + + function mint() external { + _mint(msg.sender, 10 ether); + } + + function mint(address receiver, uint256 amount) external { + _mint(receiver, amount); + } +} From d4d71602e524228a1faba04a40a72da4dc776f24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8r=E2=88=82=C2=A1?= <4456749@users.noreply.github.com> Date: Wed, 15 Nov 2023 17:36:15 +0000 Subject: [PATCH 2/6] Round 1 --- src/OptimisticTokenVotingPlugin.sol | 6 +- test/OptimisticTokenVotingPlugin.t.sol | 540 +++++++++++++++++--- test/OptimisticTokenVotingPluginSetup.t.sol | 3 + 3 files changed, 483 insertions(+), 66 deletions(-) diff --git a/src/OptimisticTokenVotingPlugin.sol b/src/OptimisticTokenVotingPlugin.sol index ad41736..f5080e4 100644 --- a/src/OptimisticTokenVotingPlugin.sol +++ b/src/OptimisticTokenVotingPlugin.sol @@ -78,7 +78,7 @@ contract OptimisticTokenVotingPlugin is keccak256("UPDATE_OPTIMISTIC_GOVERNANCE_SETTINGS_PERMISSION"); /// @notice The [ERC-165](https://eips.ethereum.org/EIPS/eip-165) interface ID of the contract. - bytes4 internal constant OPTIMISTIC_GOVERNANCE_INTERFACE_ID = + bytes4 public constant OPTIMISTIC_GOVERNANCE_INTERFACE_ID = this.initialize.selector ^ this.getProposal.selector ^ this.updateOptimisticGovernanceSettings.selector; @@ -480,11 +480,11 @@ contract OptimisticTokenVotingPlugin is } if ( - totalVotingPower(block.number) < + totalVotingPower(block.number - 1) < _governanceSettings.minProposerVotingPower ) { revert MinProposerVotingPowerOutOfBounds({ - limit: totalVotingPower(block.number), + limit: totalVotingPower(block.number - 1), actual: _governanceSettings.minProposerVotingPower }); } diff --git a/test/OptimisticTokenVotingPlugin.t.sol b/test/OptimisticTokenVotingPlugin.t.sol index 00d08a0..88f232f 100644 --- a/test/OptimisticTokenVotingPlugin.t.sol +++ b/test/OptimisticTokenVotingPlugin.t.sol @@ -3,11 +3,15 @@ pragma solidity ^0.8.17; import {Test, console2} from "forge-std/Test.sol"; import {OptimisticTokenVotingPlugin} from "../src/OptimisticTokenVotingPlugin.sol"; +import {IOptimisticTokenVoting} from "../src/IOptimisticTokenVoting.sol"; import {DAO} from "@aragon/osx/core/dao/DAO.sol"; import {IDAO} from "@aragon/osx/core/dao/IDAO.sol"; -import {ERC20Mock} from "./mocks/ERC20Mock.sol"; +import {IProposal} from "@aragon/osx/core/plugin/proposal/IProposal.sol"; +import {IMembership} from "@aragon/osx/core/plugin/membership/IMembership.sol"; import {RATIO_BASE} from "@aragon/osx/plugins/utils/Ratio.sol"; +import {ERC20Mock} from "./mocks/ERC20Mock.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {IERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/IERC165Upgradeable.sol"; contract OptimisticTokenVotingPluginTest is Test { address immutable daoBase = address(new DAO()); @@ -20,11 +24,15 @@ contract OptimisticTokenVotingPluginTest is Test { address alice = address(0xa11ce); address bob = address(0xB0B); - address randomUser = vm.addr(1234567890); + address randomWallet = vm.addr(1234567890); + // Events from external contracts + event Initialized(uint8 version); error Unimplemented(); function setUp() public { + vm.startPrank(alice); + // Deploy a DAO with Alice as root dao = DAO( payable( @@ -49,15 +57,13 @@ contract OptimisticTokenVotingPluginTest is Test { ) ); - vm.roll(block.number + 1); - // Deploy a new plugin instance OptimisticTokenVotingPlugin.OptimisticGovernanceSettings memory settings = OptimisticTokenVotingPlugin .OptimisticGovernanceSettings({ minVetoRatio: uint32(RATIO_BASE / 10), minDuration: 10 days, - minProposerVotingPower: 0 ether + minProposerVotingPower: 0 }); plugin = OptimisticTokenVotingPlugin( @@ -79,12 +85,137 @@ contract OptimisticTokenVotingPluginTest is Test { dao.grant(address(plugin), alice, plugin.PROPOSER_PERMISSION_ID()); } - function test_InitializeRevertsIfInitializing() public { - revert Unimplemented(); + function test_InitializeRevertsIfInitialized() public { + OptimisticTokenVotingPlugin.OptimisticGovernanceSettings + memory settings = OptimisticTokenVotingPlugin + .OptimisticGovernanceSettings({ + minVetoRatio: uint32(RATIO_BASE / 10), + minDuration: 10 days, + minProposerVotingPower: 0 + }); + + vm.expectRevert( + bytes("Initializable: contract is already initialized") + ); + plugin.initialize(dao, settings, votingToken); } function test_InitializeSetsTheProperValues() public { - revert Unimplemented(); + // Initial settings + OptimisticTokenVotingPlugin.OptimisticGovernanceSettings + memory settings = OptimisticTokenVotingPlugin + .OptimisticGovernanceSettings({ + minVetoRatio: uint32(RATIO_BASE / 10), + minDuration: 10 days, + minProposerVotingPower: 0 + }); + plugin = OptimisticTokenVotingPlugin( + createProxyAndCall( + address(pluginBase), + abi.encodeWithSelector( + OptimisticTokenVotingPlugin.initialize.selector, + dao, + settings, + votingToken + ) + ) + ); + assertEq( + plugin.totalVotingPower(block.number - 1), + 0, + "Incorrect token supply" + ); + assertEq( + plugin.minVetoRatio(), + uint32(RATIO_BASE / 10), + "Incorrect minVetoRatio" + ); + assertEq(plugin.minDuration(), 10 days, "Incorrect minDuration"); + assertEq( + plugin.minProposerVotingPower(), + 0, + "Incorrect minProposerVotingPower" + ); + + // Different minVetoRatio + settings.minVetoRatio = uint32(RATIO_BASE / 5); + plugin = OptimisticTokenVotingPlugin( + createProxyAndCall( + address(pluginBase), + abi.encodeWithSelector( + OptimisticTokenVotingPlugin.initialize.selector, + dao, + settings, + votingToken + ) + ) + ); + assertEq( + plugin.minVetoRatio(), + uint32(RATIO_BASE / 5), + "Incorrect minVetoRatio" + ); + + // Different minDuration + settings.minDuration = 25 days; + plugin = OptimisticTokenVotingPlugin( + createProxyAndCall( + address(pluginBase), + abi.encodeWithSelector( + OptimisticTokenVotingPlugin.initialize.selector, + dao, + settings, + votingToken + ) + ) + ); + assertEq(plugin.minDuration(), 25 days, "Incorrect minDuration"); + + // A token with 10 eth supply + votingToken = ERC20Mock( + createProxyAndCall( + address(votingTokenBase), + abi.encodeWithSelector(ERC20Mock.initialize.selector) + ) + ); + votingToken.mint(alice, 10 ether); + vm.roll(block.number + 5); + + plugin = OptimisticTokenVotingPlugin( + createProxyAndCall( + address(pluginBase), + abi.encodeWithSelector( + OptimisticTokenVotingPlugin.initialize.selector, + dao, + settings, + votingToken + ) + ) + ); + assertEq( + plugin.totalVotingPower(block.number - 1), + 10 ether, + "Incorrect token supply" + ); + + // Different minProposerVotingPower + settings.minProposerVotingPower = 1 ether; + plugin = OptimisticTokenVotingPlugin( + createProxyAndCall( + address(pluginBase), + abi.encodeWithSelector( + OptimisticTokenVotingPlugin.initialize.selector, + dao, + settings, + votingToken + ) + ) + ); + assertEq( + plugin.minProposerVotingPower(), + 1 ether, + "Incorrect minProposerVotingPower" + ); } function test_InitializeCreatesANewERC20Token() public { @@ -100,162 +231,445 @@ contract OptimisticTokenVotingPluginTest is Test { } function test_InitializeEmitsEvent() public { - revert Unimplemented(); + OptimisticTokenVotingPlugin.OptimisticGovernanceSettings + memory settings = OptimisticTokenVotingPlugin + .OptimisticGovernanceSettings({ + minVetoRatio: uint32(RATIO_BASE / 10), + minDuration: 10 days, + minProposerVotingPower: 0 + }); + + vm.expectEmit(); + emit Initialized(uint8(1)); + + plugin = OptimisticTokenVotingPlugin( + createProxyAndCall( + address(pluginBase), + abi.encodeWithSelector( + OptimisticTokenVotingPlugin.initialize.selector, + dao, + settings, + votingToken + ) + ) + ); } - function test_SupportsInterfaceReturnsTrueForOptimisticGovernance() public { - revert Unimplemented(); + function test_SupportsOptimisticGovernanceInterface() public { + bool supported = plugin.supportsInterface( + plugin.OPTIMISTIC_GOVERNANCE_INTERFACE_ID() + ); + assertEq( + supported, + true, + "Should support OPTIMISTIC_GOVERNANCE_INTERFACE_ID" + ); } - function test_SupportsInterfaceReturnsTrueForIOptimisticTokenVoting() - public - { - revert Unimplemented(); + function test_SupportsIOptimisticTokenVotingInterface() public { + bool supported = plugin.supportsInterface( + type(IOptimisticTokenVoting).interfaceId + ); + assertEq(supported, true, "Should support IOptimisticTokenVoting"); } - function test_SupportsInterfaceReturnsTrueForIMembership() public { - revert Unimplemented(); + function test_SupportsIMembershipInterface() public { + bool supported = plugin.supportsInterface( + type(IMembership).interfaceId + ); + assertEq(supported, true, "Should support IMembership"); } - function test_SupportsInterfaceReturnsTrueForIProposal() public { - revert Unimplemented(); + function test_SupportsIProposalInterface() public { + bool supported = plugin.supportsInterface(type(IProposal).interfaceId); + assertEq(supported, true, "Should support IProposal"); } - function test_SupportsInterfaceReturnsTrueForIERC165Upgradeable() public { - revert Unimplemented(); + function test_SupportsIERC165UpgradeableInterface() public { + bool supported = plugin.supportsInterface( + type(IERC165Upgradeable).interfaceId + ); + assertEq(supported, true, "Should support IERC165Upgradeable"); } - function test_SupportsInterfaceReturnsFalseOtherwise() public { - revert Unimplemented(); + function testFuzz_SupportsInterfaceReturnsFalseOtherwise( + bytes4 _randomInterfaceId + ) public { + bool supported = plugin.supportsInterface(bytes4(0x000000)); + assertEq(supported, false, "Should not support any other interface"); + + supported = plugin.supportsInterface(bytes4(0xffffffff)); + assertEq(supported, false, "Should not support any other interface"); + + supported = plugin.supportsInterface(_randomInterfaceId); + assertEq(supported, false, "Should not support any other interface"); } function test_GetVotingTokenReturnsTheRightAddress() public { - revert Unimplemented(); + assertEq( + address(plugin.getVotingToken()), + address(votingToken), + "Incorrect voting token" + ); + + address oldToken = address(plugin.getVotingToken()); + + // New token + votingToken = ERC20Mock( + createProxyAndCall( + address(votingTokenBase), + abi.encodeWithSelector(ERC20Mock.initialize.selector) + ) + ); + + // Deploy a new plugin instance + OptimisticTokenVotingPlugin.OptimisticGovernanceSettings + memory settings = OptimisticTokenVotingPlugin + .OptimisticGovernanceSettings({ + minVetoRatio: uint32(RATIO_BASE / 10), + minDuration: 10 days, + minProposerVotingPower: 0 + }); + + plugin = OptimisticTokenVotingPlugin( + createProxyAndCall( + address(pluginBase), + abi.encodeWithSelector( + OptimisticTokenVotingPlugin.initialize.selector, + dao, + settings, + votingToken + ) + ) + ); + + assertEq( + address(plugin.getVotingToken()), + address(votingToken), + "Incorrect voting token" + ); + assertEq( + address(votingToken) != oldToken, + true, + "The token address sould have changed" + ); } function test_TotalVotingPowerReturnsTheRightSupply() public { - revert Unimplemented(); + assertEq( + plugin.totalVotingPower(block.number - 1), + votingToken.getPastTotalSupply(block.number - 1), + "Incorrect total voting power" + ); + assertEq( + plugin.totalVotingPower(block.number - 1), + 0 ether, + "Incorrect total voting power" + ); + + // New token + votingToken = ERC20Mock( + createProxyAndCall( + address(votingTokenBase), + abi.encodeWithSelector(ERC20Mock.initialize.selector) + ) + ); + votingToken.mint(alice, 10 ether); + vm.roll(block.number + 1); + + // Deploy a new plugin instance + OptimisticTokenVotingPlugin.OptimisticGovernanceSettings + memory settings = OptimisticTokenVotingPlugin + .OptimisticGovernanceSettings({ + minVetoRatio: uint32(RATIO_BASE / 10), + minDuration: 10 days, + minProposerVotingPower: 0 + }); + + plugin = OptimisticTokenVotingPlugin( + createProxyAndCall( + address(pluginBase), + abi.encodeWithSelector( + OptimisticTokenVotingPlugin.initialize.selector, + dao, + settings, + votingToken + ) + ) + ); + + assertEq( + plugin.totalVotingPower(block.number - 1), + votingToken.getPastTotalSupply(block.number - 1), + "Incorrect total voting power" + ); + assertEq( + plugin.totalVotingPower(block.number - 1), + 10 ether, + "Incorrect total voting power" + ); } function test_MinVetoRatioReturnsTheRightValue() public { - revert Unimplemented(); + assertEq( + plugin.minVetoRatio(), + uint32(RATIO_BASE / 10), + "Incorrect minVetoRatio" + ); + + // New plugin instance + OptimisticTokenVotingPlugin.OptimisticGovernanceSettings + memory settings = OptimisticTokenVotingPlugin + .OptimisticGovernanceSettings({ + minVetoRatio: uint32(RATIO_BASE / 5), + minDuration: 10 days, + minProposerVotingPower: 0 + }); + + plugin = OptimisticTokenVotingPlugin( + createProxyAndCall( + address(pluginBase), + abi.encodeWithSelector( + OptimisticTokenVotingPlugin.initialize.selector, + dao, + settings, + votingToken + ) + ) + ); + + assertEq( + plugin.minVetoRatio(), + uint32(RATIO_BASE / 5), + "Incorrect minVetoRatio" + ); } function test_MinDurationReturnsTheRightValue() public { - revert Unimplemented(); + assertEq(plugin.minDuration(), 10 days, "Incorrect minDuration"); + + // New plugin instance + OptimisticTokenVotingPlugin.OptimisticGovernanceSettings + memory settings = OptimisticTokenVotingPlugin + .OptimisticGovernanceSettings({ + minVetoRatio: uint32(RATIO_BASE / 5), + minDuration: 25 days, + minProposerVotingPower: 0 + }); + + plugin = OptimisticTokenVotingPlugin( + createProxyAndCall( + address(pluginBase), + abi.encodeWithSelector( + OptimisticTokenVotingPlugin.initialize.selector, + dao, + settings, + votingToken + ) + ) + ); + + assertEq(plugin.minDuration(), 25 days, "Incorrect minDuration"); } function test_MinProposerVotingPowerReturnsTheRightValue() public { - revert Unimplemented(); + assertEq( + plugin.minProposerVotingPower(), + 0, + "Incorrect minProposerVotingPower" + ); + + // New token + votingToken = ERC20Mock( + createProxyAndCall( + address(votingTokenBase), + abi.encodeWithSelector(ERC20Mock.initialize.selector) + ) + ); + votingToken.mint(alice, 10 ether); + vm.roll(block.number + 1); + + // Deploy a new plugin instance + OptimisticTokenVotingPlugin.OptimisticGovernanceSettings + memory settings = OptimisticTokenVotingPlugin + .OptimisticGovernanceSettings({ + minVetoRatio: uint32(RATIO_BASE / 10), + minDuration: 10 days, + minProposerVotingPower: 1 ether + }); + + plugin = OptimisticTokenVotingPlugin( + createProxyAndCall( + address(pluginBase), + abi.encodeWithSelector( + OptimisticTokenVotingPlugin.initialize.selector, + dao, + settings, + votingToken + ) + ) + ); + + assertEq( + plugin.minProposerVotingPower(), + 1 ether, + "Incorrect minProposerVotingPower" + ); } function test_TokenHoldersAreMembers() public { - revert Unimplemented(); + assertEq(plugin.isMember(alice), false, "Alice should not be a member"); + assertEq(plugin.isMember(bob), false, "Bob should not be a member"); + assertEq( + plugin.isMember(randomWallet), + false, + "Random wallet should not be a member" + ); + + // New token + votingToken = ERC20Mock( + createProxyAndCall( + address(votingTokenBase), + abi.encodeWithSelector(ERC20Mock.initialize.selector) + ) + ); + votingToken.mint(alice, 10 ether); + votingToken.mint(bob, 5 ether); + vm.roll(block.number + 1); + + // Deploy a new plugin instance + OptimisticTokenVotingPlugin.OptimisticGovernanceSettings + memory settings = OptimisticTokenVotingPlugin + .OptimisticGovernanceSettings({ + minVetoRatio: uint32(RATIO_BASE / 10), + minDuration: 10 days, + minProposerVotingPower: 1 ether + }); + + plugin = OptimisticTokenVotingPlugin( + createProxyAndCall( + address(pluginBase), + abi.encodeWithSelector( + OptimisticTokenVotingPlugin.initialize.selector, + dao, + settings, + votingToken + ) + ) + ); + + assertEq(plugin.isMember(alice), true, "Alice should be a member"); + assertEq(plugin.isMember(bob), true, "Bob should be a member"); + assertEq( + plugin.isMember(randomWallet), + false, + "Random wallet should not be a member" + ); } - function test_HasVetoedReturnsTheRightValue() public { + function test_CreateProposalRevertsWhenCalledByANonProposer() public { revert Unimplemented(); } - function test_CanVetoReturnsFalseWhenAProposalDoesntExist() public { + function test_CreateProposalSucceedsWhenMinimumVotingPowerIsZero() public { revert Unimplemented(); } - function test_CanVetoReturnsFalseWhenAProposalHasNotStarted() public { + function test_CreateProposalRevertsWhenTheCallerOwnsLessThanTheMinimumVotingPower() + public + { revert Unimplemented(); } - function test_CanVetoReturnsFalseWhenAVoterAlreadyVetoed() public { + function test_CreateProposalRevertsIfThereIsNoVotingPower() public { revert Unimplemented(); } - function test_CanVetoReturnsFalseWhenAnAddressHasNoVotingPower() public { + function test_CreateProposalRevertsIfTheStartDateIsAfterTheEndDate() + public + { revert Unimplemented(); } - function test_CanVetoReturnsTrueOtherwise() public { + function test_CreateProposalRevertsIfTheStartDateIsInThePast() public { revert Unimplemented(); } - function test_CanExecuteReturnsFalseWhenAlreadyExecuted() public { + function test_CreateProposalStartsNowWhenStartDateIsZero() public { revert Unimplemented(); } - function test_CanExecuteReturnsFalseWhenStillOngoing() public { + function test_CreateProposalEndsAfterTheMinDurationWhenEndDateIsZero() + public + { revert Unimplemented(); } - function test_CanExecuteReturnsFalseWhenEnoughVetoesAreRegistered() public { + function test_CreateProposalUsesTheCurrentMinVetoRatio() public { revert Unimplemented(); } - function test_CanExecuteReturnsTrueOtherwise() public { + function test_CreateProposalReturnsTheProposalId() public { revert Unimplemented(); } - function test_IsMinVetoRatioReachedReturnsFalseWhenNotEnoughPeopleHaveVetoed() - public - { + function test_CreateProposalEmitsAnEvent() public { revert Unimplemented(); } - function test_IsMinVetoRatioReachedReturnsTrueWhenEnoughPeopleHaveVetoed() - public - { + function test_HasVetoedReturnsTheRightValue() public { revert Unimplemented(); } - function test_GetProposalReturnsTheRightValues() public { + function test_CanVetoReturnsFalseWhenAProposalDoesntExist() public { revert Unimplemented(); } - function test_CreateProposalRevertsWhenCalledByANonProposer() public { + function test_CanVetoReturnsFalseWhenAProposalHasNotStarted() public { revert Unimplemented(); } - function test_CreateProposalSucceedsWhenMinimumVotingPowerIsZero() public { + function test_CanVetoReturnsFalseWhenAVoterAlreadyVetoed() public { revert Unimplemented(); } - function test_CreateProposalRevertsWhenTheCallerOwnsLessThanTheMinimumVotingPower() - public - { + function test_CanVetoReturnsFalseWhenAnAddressHasNoVotingPower() public { revert Unimplemented(); } - function test_CreateProposalRevertsIfThereIsNoVotingPower() public { + function test_CanVetoReturnsTrueOtherwise() public { revert Unimplemented(); } - function test_CreateProposalRevertsIfTheStartDateIsAfterTheEndDate() - public - { + function test_CanExecuteReturnsFalseWhenAlreadyExecuted() public { revert Unimplemented(); } - function test_CreateProposalRevertsIfTheStartDateIsInThePast() public { + function test_CanExecuteReturnsFalseWhenStillOngoing() public { revert Unimplemented(); } - function test_CreateProposalStartsNowWhenStartDateIsZero() public { + function test_CanExecuteReturnsFalseWhenEnoughVetoesAreRegistered() public { revert Unimplemented(); } - function test_CreateProposalEndsAfterTheMinDurationWhenEndDateIsZero() - public - { + function test_CanExecuteReturnsTrueOtherwise() public { revert Unimplemented(); } - function test_CreateProposalUsesTheCurrentMinVetoRatio() public { + function test_IsMinVetoRatioReachedReturnsFalseWhenNotEnoughPeopleHaveVetoed() + public + { revert Unimplemented(); } - function test_CreateProposalReturnsTheProposalId() public { + function test_IsMinVetoRatioReachedReturnsTrueWhenEnoughPeopleHaveVetoed() + public + { revert Unimplemented(); } - function test_CreateProposalEmitsAnEvent() public { + function test_GetProposalReturnsTheRightValues() public { revert Unimplemented(); } diff --git a/test/OptimisticTokenVotingPluginSetup.t.sol b/test/OptimisticTokenVotingPluginSetup.t.sol index aa75a92..b217127 100644 --- a/test/OptimisticTokenVotingPluginSetup.t.sol +++ b/test/OptimisticTokenVotingPluginSetup.t.sol @@ -6,6 +6,7 @@ import {OptimisticTokenVotingPluginSetup} from "../src/OptimisticTokenVotingPlug contract OptimisticTokenVotingPluginSetupTest is Test { OptimisticTokenVotingPluginSetup public plugin; + error Unimplemented(); function setUp() public { // plugin = new OptimisticTokenVotingPluginSetup(); @@ -15,10 +16,12 @@ contract OptimisticTokenVotingPluginSetupTest is Test { function test_Increment() public { // plugin.increment(); // assertEq(plugin.number(), 1); + revert Unimplemented(); } function testFuzz_SetNumber(uint256 x) public { // plugin.setNumber(x); // assertEq(plugin.number(), x); + revert Unimplemented(); } } From 0169850643a3a9d329fe103df718fcdb2015dd4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8r=E2=88=82=C2=A1?= <4456749@users.noreply.github.com> Date: Wed, 15 Nov 2023 19:49:01 +0000 Subject: [PATCH 3/6] Round 2 --- test/OptimisticTokenVotingPlugin.t.sol | 383 ++++++++++++++++++++++--- 1 file changed, 346 insertions(+), 37 deletions(-) diff --git a/test/OptimisticTokenVotingPlugin.t.sol b/test/OptimisticTokenVotingPlugin.t.sol index 88f232f..7e7da5e 100644 --- a/test/OptimisticTokenVotingPlugin.t.sol +++ b/test/OptimisticTokenVotingPlugin.t.sol @@ -9,6 +9,7 @@ import {IDAO} from "@aragon/osx/core/dao/IDAO.sol"; import {IProposal} from "@aragon/osx/core/plugin/proposal/IProposal.sol"; import {IMembership} from "@aragon/osx/core/plugin/membership/IMembership.sol"; import {RATIO_BASE} from "@aragon/osx/plugins/utils/Ratio.sol"; +import {DaoUnauthorized} from "@aragon/osx/core/utils/auth.sol"; import {ERC20Mock} from "./mocks/ERC20Mock.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {IERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/IERC165Upgradeable.sol"; @@ -28,6 +29,16 @@ contract OptimisticTokenVotingPluginTest is Test { // Events from external contracts event Initialized(uint8 version); + event ProposalCreated( + uint256 indexed proposalId, + address indexed creator, + uint64 startDate, + uint64 endDate, + bytes metadata, + IDAO.Action[] actions, + uint256 allowFailureMap + ); + error Unimplemented(); function setUp() public { @@ -56,6 +67,8 @@ contract OptimisticTokenVotingPluginTest is Test { abi.encodeWithSelector(ERC20Mock.initialize.selector) ) ); + votingToken.mint(alice, 10 ether); + vm.roll(block.number + 1); // Deploy a new plugin instance OptimisticTokenVotingPlugin.OptimisticGovernanceSettings @@ -122,7 +135,7 @@ contract OptimisticTokenVotingPluginTest is Test { ); assertEq( plugin.totalVotingPower(block.number - 1), - 0, + 10 ether, "Incorrect token supply" ); assertEq( @@ -363,7 +376,7 @@ contract OptimisticTokenVotingPluginTest is Test { ); assertEq( plugin.totalVotingPower(block.number - 1), - 0 ether, + 10 ether, "Incorrect total voting power" ); @@ -374,7 +387,7 @@ contract OptimisticTokenVotingPluginTest is Test { abi.encodeWithSelector(ERC20Mock.initialize.selector) ) ); - votingToken.mint(alice, 10 ether); + votingToken.mint(alice, 15 ether); vm.roll(block.number + 1); // Deploy a new plugin instance @@ -405,7 +418,7 @@ contract OptimisticTokenVotingPluginTest is Test { ); assertEq( plugin.totalVotingPower(block.number - 1), - 10 ether, + 15 ether, "Incorrect total voting power" ); } @@ -518,7 +531,7 @@ contract OptimisticTokenVotingPluginTest is Test { } function test_TokenHoldersAreMembers() public { - assertEq(plugin.isMember(alice), false, "Alice should not be a member"); + assertEq(plugin.isMember(alice), true, "Alice should not be a member"); assertEq(plugin.isMember(bob), false, "Bob should not be a member"); assertEq( plugin.isMember(randomWallet), @@ -568,57 +581,353 @@ contract OptimisticTokenVotingPluginTest is Test { } function test_CreateProposalRevertsWhenCalledByANonProposer() public { - revert Unimplemented(); + vm.stopPrank(); + vm.startPrank(bob); + IDAO.Action[] memory actions = new IDAO.Action[](0); + vm.expectRevert( + abi.encodeWithSelector( + DaoUnauthorized.selector, + address(dao), + address(plugin), + bob, + plugin.PROPOSER_PERMISSION_ID() + ) + ); + plugin.createProposal("", actions, 0, 0, 0); + + vm.stopPrank(); + vm.startPrank(alice); + + plugin.createProposal("", actions, 0, 0, 0); } function test_CreateProposalSucceedsWhenMinimumVotingPowerIsZero() public { - revert Unimplemented(); + // Bob can create proposals on the plugin now + dao.grant(address(plugin), bob, plugin.PROPOSER_PERMISSION_ID()); + + vm.stopPrank(); + vm.startPrank(bob); + + IDAO.Action[] memory actions = new IDAO.Action[](0); + uint256 proposalId = plugin.createProposal("", actions, 0, 0, 0); + assertEq(proposalId, 0); + proposalId = plugin.createProposal("", actions, 0, 0, 0); + assertEq(proposalId, 1); } function test_CreateProposalRevertsWhenTheCallerOwnsLessThanTheMinimumVotingPower() public { - revert Unimplemented(); + vm.stopPrank(); + vm.startPrank(alice); + + IDAO.Action[] memory actions = new IDAO.Action[](0); + OptimisticTokenVotingPlugin.OptimisticGovernanceSettings + memory newSettings = OptimisticTokenVotingPlugin + .OptimisticGovernanceSettings({ + minVetoRatio: uint32(RATIO_BASE / 10), + minDuration: 10 days, + minProposerVotingPower: 5 ether + }); + dao.grant(address(plugin), bob, plugin.PROPOSER_PERMISSION_ID()); + dao.grant( + address(plugin), + alice, + plugin.UPDATE_OPTIMISTIC_GOVERNANCE_SETTINGS_PERMISSION_ID() + ); + plugin.updateOptimisticGovernanceSettings(newSettings); + + vm.stopPrank(); + vm.startPrank(bob); + + // Bob holds no tokens + vm.expectRevert( + abi.encodeWithSelector( + OptimisticTokenVotingPlugin.ProposalCreationForbidden.selector, + bob + ) + ); + plugin.createProposal("", actions, 0, 0, 0); } function test_CreateProposalRevertsIfThereIsNoVotingPower() public { - revert Unimplemented(); + vm.stopPrank(); + vm.startPrank(alice); + + // Deploy ERC20 token (0 supply) + votingToken = ERC20Mock( + createProxyAndCall( + address(votingTokenBase), + abi.encodeWithSelector(ERC20Mock.initialize.selector) + ) + ); + + // Deploy a new plugin instance + IDAO.Action[] memory actions = new IDAO.Action[](0); + OptimisticTokenVotingPlugin.OptimisticGovernanceSettings + memory settings = OptimisticTokenVotingPlugin + .OptimisticGovernanceSettings({ + minVetoRatio: uint32(RATIO_BASE / 10), + minDuration: 10 days, + minProposerVotingPower: 0 + }); + plugin = OptimisticTokenVotingPlugin( + createProxyAndCall( + address(pluginBase), + abi.encodeWithSelector( + OptimisticTokenVotingPlugin.initialize.selector, + dao, + settings, + votingToken + ) + ) + ); + dao.grant(address(plugin), alice, plugin.PROPOSER_PERMISSION_ID()); + + // Try to create + vm.expectRevert( + abi.encodeWithSelector( + OptimisticTokenVotingPlugin.NoVotingPower.selector + ) + ); + plugin.createProposal("", actions, 0, 0, 0); } function test_CreateProposalRevertsIfTheStartDateIsAfterTheEndDate() public { - revert Unimplemented(); + IDAO.Action[] memory actions = new IDAO.Action[](0); + uint32 startDate = 200000; + uint32 endDate = 10; + vm.expectRevert( + abi.encodeWithSelector( + OptimisticTokenVotingPlugin.DateOutOfBounds.selector, + startDate + 10 days, + endDate + ) + ); + plugin.createProposal("", actions, 0, startDate, endDate); } - function test_CreateProposalRevertsIfTheStartDateIsInThePast() public { - revert Unimplemented(); - } + function test_CreateProposalRevertsIfStartDateIsInThePast() public { + vm.warp(10); // timestamp = 10 - function test_CreateProposalStartsNowWhenStartDateIsZero() public { - revert Unimplemented(); + IDAO.Action[] memory actions = new IDAO.Action[](0); + vm.expectRevert( + abi.encodeWithSelector( + OptimisticTokenVotingPlugin.DateOutOfBounds.selector, + block.timestamp, + 1 + ) + ); + uint32 startDate = 1; + plugin.createProposal("", actions, 0, startDate, startDate + 10 days); } - function test_CreateProposalEndsAfterTheMinDurationWhenEndDateIsZero() + function test_CreateProposalRevertsIfEndDateIsEarlierThanMinDuration() public { - revert Unimplemented(); + vm.warp(500); // timestamp = 500 + + IDAO.Action[] memory actions = new IDAO.Action[](0); + uint32 startDate = 1000; + vm.expectRevert( + abi.encodeWithSelector( + OptimisticTokenVotingPlugin.DateOutOfBounds.selector, + startDate + 10 days, + startDate + 10 minutes + ) + ); + plugin.createProposal( + "", + actions, + 0, + startDate, + startDate + 10 minutes + ); + } + + function test_CreateProposalStartsNowWhenStartDateIsZero() public { + vm.warp(500); + + IDAO.Action[] memory actions = new IDAO.Action[](0); + uint32 startDate = 0; + uint256 proposalId = plugin.createProposal( + "", + actions, + 0, + startDate, + 0 + ); + + ( + , + , + OptimisticTokenVotingPlugin.ProposalParameters memory parameters, + , + , + + ) = plugin.getProposal(proposalId); + assertEq(500, parameters.startDate, "Incorrect startDate"); + } + + function test_CreateProposalEndsAfterMinDurationWhenEndDateIsZero() public { + vm.warp(500); + + IDAO.Action[] memory actions = new IDAO.Action[](0); + uint32 startDate = 0; + uint32 endDate = 0; + uint256 proposalId = plugin.createProposal( + "", + actions, + 0, + startDate, + endDate + ); + + ( + , + , + OptimisticTokenVotingPlugin.ProposalParameters memory parameters, + , + , + + ) = plugin.getProposal(proposalId); + assertEq(500 + 10 days, parameters.endDate, "Incorrect endDate"); } function test_CreateProposalUsesTheCurrentMinVetoRatio() public { - revert Unimplemented(); + IDAO.Action[] memory actions = new IDAO.Action[](0); + uint256 proposalId = plugin.createProposal("", actions, 0, 0, 0); + + ( + , + , + OptimisticTokenVotingPlugin.ProposalParameters memory parameters, + , + , + + ) = plugin.getProposal(proposalId); + assertEq( + parameters.minVetoVotingPower, + 1 ether, + "Incorrect minVetoVotingPower" + ); + + // Now with a different value + OptimisticTokenVotingPlugin.OptimisticGovernanceSettings + memory settings = OptimisticTokenVotingPlugin + .OptimisticGovernanceSettings({ + minVetoRatio: uint32(RATIO_BASE / 5), + minDuration: 10 days, + minProposerVotingPower: 0 + }); + plugin = OptimisticTokenVotingPlugin( + createProxyAndCall( + address(pluginBase), + abi.encodeWithSelector( + OptimisticTokenVotingPlugin.initialize.selector, + dao, + settings, + votingToken + ) + ) + ); + + dao.grant(address(plugin), alice, plugin.PROPOSER_PERMISSION_ID()); + proposalId = plugin.createProposal("", actions, 0, 0, 0); + (, , parameters, , , ) = plugin.getProposal(proposalId); + assertEq( + parameters.minVetoVotingPower, + 2 ether, + "Incorrect minVetoVotingPower" + ); } function test_CreateProposalReturnsTheProposalId() public { - revert Unimplemented(); + IDAO.Action[] memory actions = new IDAO.Action[](0); + uint256 proposalId = plugin.createProposal("", actions, 0, 0, 0); + assertEq(proposalId == 0, true, "Should have created proposal 0"); + + proposalId = plugin.createProposal("", actions, 0, 0, 0); + assertEq(proposalId == 1, true, "Should have created proposal 1"); } function test_CreateProposalEmitsAnEvent() public { - revert Unimplemented(); + IDAO.Action[] memory actions = new IDAO.Action[](0); + vm.expectEmit(); + emit ProposalCreated( + 0, + alice, + uint64(block.timestamp), + uint64(block.timestamp + 10 days), + "", + actions, + 0 + ); + plugin.createProposal("", actions, 0, 0, 0); } - function test_HasVetoedReturnsTheRightValue() public { - revert Unimplemented(); + function test_GetProposalReturnsTheRightValues() public { + vm.warp(500); + uint32 startDate = 600; + uint32 endDate = startDate + 15 days; + + IDAO.Action[] memory actions = new IDAO.Action[](1); + actions[0].to = address(plugin); + actions[0].value = 1 wei; + actions[0].data = abi.encodeWithSelector( + OptimisticTokenVotingPlugin.totalVotingPower.selector, + 0 + ); + uint256 failSafeBitmap = 1; + + uint256 proposalId = plugin.createProposal( + "ipfs://", + actions, + failSafeBitmap, + startDate, + endDate + ); + + (bool open0, , , , , ) = plugin.getProposal(proposalId); + assertEq(open0, false, "The proposal should not be open"); + + // Move on + vm.warp(startDate); + + ( + bool open, + bool executed, + OptimisticTokenVotingPlugin.ProposalParameters memory parameters, + uint256 vetoTally, + IDAO.Action[] memory actualActions, + uint256 actualFailSafeBitmap + ) = plugin.getProposal(proposalId); + + assertEq(open, true, "The proposal should be open"); + assertEq(executed, false, "The proposal should not be executed"); + assertEq(parameters.startDate, startDate, "Incorrect startDate"); + assertEq(parameters.endDate, endDate, "Incorrect endDate"); + assertEq(parameters.snapshotBlock, 1, "Incorrect snapshotBlock"); + assertEq( + parameters.minVetoVotingPower, + plugin.totalVotingPower(block.number - 1) / 10, + "Incorrect minVetoVotingPower" + ); + assertEq(vetoTally, 0, "The tally should be zero"); + assertEq(actualActions.length, 1, "Actions should have one item"); + assertEq( + actualFailSafeBitmap, + failSafeBitmap, + "Incorrect failsafe bitmap" + ); + + // Move on + vm.warp(endDate); + + (bool open1, , , , , ) = plugin.getProposal(proposalId); + assertEq(open1, false, "The proposal should not be open anymore"); } function test_CanVetoReturnsFalseWhenAProposalDoesntExist() public { @@ -641,61 +950,61 @@ contract OptimisticTokenVotingPluginTest is Test { revert Unimplemented(); } - function test_CanExecuteReturnsFalseWhenAlreadyExecuted() public { + function test_VetoRevertsWhenTheProposalDoesntExist() public { revert Unimplemented(); } - function test_CanExecuteReturnsFalseWhenStillOngoing() public { + function test_VetoRevertsWhenTheProposalHasNotStarted() public { revert Unimplemented(); } - function test_CanExecuteReturnsFalseWhenEnoughVetoesAreRegistered() public { + function test_VetoRevertsWhenNotATokenHolder() public { revert Unimplemented(); } - function test_CanExecuteReturnsTrueOtherwise() public { + function test_VetoRevertsWhenAlreadyVetoed() public { revert Unimplemented(); } - function test_IsMinVetoRatioReachedReturnsFalseWhenNotEnoughPeopleHaveVetoed() + function test_VetoRegistersAVetoForTheTokenHolderAndIncreasesTheTally() public { revert Unimplemented(); } - function test_IsMinVetoRatioReachedReturnsTrueWhenEnoughPeopleHaveVetoed() - public - { + function test_VetoEmitsAnEvent() public { revert Unimplemented(); } - function test_GetProposalReturnsTheRightValues() public { + function test_HasVetoedReturnsTheRightValue() public { revert Unimplemented(); } - function test_VetoRevertsWhenTheProposalDoesntExist() public { + function test_CanExecuteReturnsFalseWhenAlreadyExecuted() public { revert Unimplemented(); } - function test_VetoRevertsWhenTheProposalHasNotStarted() public { + function test_CanExecuteReturnsFalseWhenStillOngoing() public { revert Unimplemented(); } - function test_VetoRevertsWhenNotATokenHolder() public { + function test_CanExecuteReturnsFalseWhenEnoughVetoesAreRegistered() public { revert Unimplemented(); } - function test_VetoRevertsWhenAlreadyVetoed() public { + function test_CanExecuteReturnsTrueOtherwise() public { revert Unimplemented(); } - function test_VetoRegistersAVetoForTheTokenHolderAndIncreasesTheTally() + function test_IsMinVetoRatioReachedReturnsFalseWhenNotEnoughPeopleHaveVetoed() public { revert Unimplemented(); } - function test_VetoEmitsAnEvent() public { + function test_IsMinVetoRatioReachedReturnsTrueWhenEnoughPeopleHaveVetoed() + public + { revert Unimplemented(); } From e056a14c269a13fa5cb4723bac87e72e7542e379 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8r=E2=88=82=C2=A1?= <4456749@users.noreply.github.com> Date: Thu, 16 Nov 2023 12:22:59 +0000 Subject: [PATCH 4/6] Round 3 --- test/OptimisticTokenVotingPlugin.t.sol | 855 +++++++++++++++++++++++-- 1 file changed, 795 insertions(+), 60 deletions(-) diff --git a/test/OptimisticTokenVotingPlugin.t.sol b/test/OptimisticTokenVotingPlugin.t.sol index 7e7da5e..2b51330 100644 --- a/test/OptimisticTokenVotingPlugin.t.sol +++ b/test/OptimisticTokenVotingPlugin.t.sol @@ -38,6 +38,12 @@ contract OptimisticTokenVotingPluginTest is Test { IDAO.Action[] actions, uint256 allowFailureMap ); + event VetoCast( + uint256 indexed proposalId, + address indexed voter, + uint256 votingPower + ); + event ProposalExecuted(uint256 indexed proposalId); error Unimplemented(); @@ -68,6 +74,7 @@ contract OptimisticTokenVotingPluginTest is Test { ) ); votingToken.mint(alice, 10 ether); + votingToken.delegate(alice); vm.roll(block.number + 1); // Deploy a new plugin instance @@ -98,6 +105,7 @@ contract OptimisticTokenVotingPluginTest is Test { dao.grant(address(plugin), alice, plugin.PROPOSER_PERMISSION_ID()); } + // Initialize function test_InitializeRevertsIfInitialized() public { OptimisticTokenVotingPlugin.OptimisticGovernanceSettings memory settings = OptimisticTokenVotingPlugin @@ -268,6 +276,7 @@ contract OptimisticTokenVotingPluginTest is Test { ); } + // Getters function test_SupportsOptimisticGovernanceInterface() public { bool supported = plugin.supportsInterface( plugin.OPTIMISTIC_GOVERNANCE_INTERFACE_ID() @@ -580,6 +589,7 @@ contract OptimisticTokenVotingPluginTest is Test { ); } + // Create proposal function test_CreateProposalRevertsWhenCalledByANonProposer() public { vm.stopPrank(); vm.startPrank(bob); @@ -930,108 +940,833 @@ contract OptimisticTokenVotingPluginTest is Test { assertEq(open1, false, "The proposal should not be open anymore"); } + // Can Veto function test_CanVetoReturnsFalseWhenAProposalDoesntExist() public { - revert Unimplemented(); + vm.roll(10); + + IDAO.Action[] memory actions = new IDAO.Action[](0); + uint256 proposalId = plugin.createProposal("ipfs://", actions, 0, 0, 0); + vm.roll(20); + + assertEq( + plugin.canVeto(proposalId, alice), + true, + "Alice should be able to veto" + ); + + // non existing + assertEq( + plugin.canVeto(proposalId + 200, alice), + false, + "Alice should not be able to veto on non existing proposals" + ); } function test_CanVetoReturnsFalseWhenAProposalHasNotStarted() public { - revert Unimplemented(); + uint64 startDate = 50; + vm.warp(startDate - 1); + + IDAO.Action[] memory actions = new IDAO.Action[](0); + uint256 proposalId = plugin.createProposal( + "ipfs://", + actions, + 0, + startDate, + 0 + ); + + // Unstarted + assertEq( + plugin.canVeto(proposalId, alice), + false, + "Alice should not be able to veto" + ); + + // Started + vm.warp(startDate + 1); + assertEq( + plugin.canVeto(proposalId, alice), + true, + "Alice should be able to veto" + ); } function test_CanVetoReturnsFalseWhenAVoterAlreadyVetoed() public { - revert Unimplemented(); - } + uint64 startDate = 50; + vm.warp(startDate - 1); - function test_CanVetoReturnsFalseWhenAnAddressHasNoVotingPower() public { - revert Unimplemented(); - } + IDAO.Action[] memory actions = new IDAO.Action[](0); + uint256 proposalId = plugin.createProposal( + "ipfs://", + actions, + 0, + startDate, + 0 + ); + vm.warp(startDate + 1); - function test_CanVetoReturnsTrueOtherwise() public { - revert Unimplemented(); - } + plugin.veto(proposalId); - function test_VetoRevertsWhenTheProposalDoesntExist() public { - revert Unimplemented(); + assertEq( + plugin.canVeto(proposalId, alice), + false, + "Alice should not be able to veto" + ); } - function test_VetoRevertsWhenTheProposalHasNotStarted() public { - revert Unimplemented(); - } + function test_CanVetoReturnsFalseWhenAVoterAlreadyEnded() public { + uint64 startDate = 50; + uint64 endDate = startDate + 10 days; + vm.warp(startDate - 1); - function test_VetoRevertsWhenNotATokenHolder() public { - revert Unimplemented(); - } + IDAO.Action[] memory actions = new IDAO.Action[](0); + uint256 proposalId = plugin.createProposal( + "ipfs://", + actions, + 0, + startDate, + endDate + ); + vm.warp(endDate + 1); - function test_VetoRevertsWhenAlreadyVetoed() public { - revert Unimplemented(); + assertEq( + plugin.canVeto(proposalId, alice), + false, + "Alice should not be able to veto" + ); } - function test_VetoRegistersAVetoForTheTokenHolderAndIncreasesTheTally() - public - { - revert Unimplemented(); - } + function test_CanVetoReturnsFalseWhenNoVotingPower() public { + uint64 startDate = 50; + vm.warp(startDate - 1); - function test_VetoEmitsAnEvent() public { - revert Unimplemented(); + IDAO.Action[] memory actions = new IDAO.Action[](0); + uint256 proposalId = plugin.createProposal( + "ipfs://", + actions, + 0, + startDate, + 0 + ); + + vm.warp(startDate + 1); + + // Alice owns tokens + assertEq( + plugin.canVeto(proposalId, alice), + true, + "Alice should be able to veto" + ); + + // Bob owns no tokens + assertEq( + plugin.canVeto(proposalId, bob), + false, + "Bob should not be able to veto" + ); } - function test_HasVetoedReturnsTheRightValue() public { - revert Unimplemented(); + function test_CanVetoReturnsTrueOtherwise() public { + uint64 startDate = 50; + vm.warp(startDate - 1); + + IDAO.Action[] memory actions = new IDAO.Action[](0); + uint256 proposalId = plugin.createProposal( + "ipfs://", + actions, + 0, + startDate, + 0 + ); + vm.warp(startDate + 1); + + assertEq( + plugin.canVeto(proposalId, alice), + true, + "Alice should be able to veto" + ); } - function test_CanExecuteReturnsFalseWhenAlreadyExecuted() public { - revert Unimplemented(); + // Veto + function test_VetoRevertsWhenAProposalDoesntExist() public { + vm.roll(10); + uint64 startDate = 50; + vm.warp(startDate - 1); + + IDAO.Action[] memory actions = new IDAO.Action[](0); + uint256 proposalId = plugin.createProposal( + "ipfs://", + actions, + 0, + startDate, + 0 + ); + vm.roll(20); + + // non existing + vm.expectRevert( + abi.encodeWithSelector( + OptimisticTokenVotingPlugin.ProposalVetoingForbidden.selector, + proposalId + 200, + alice + ) + ); + plugin.veto(proposalId + 200); } - function test_CanExecuteReturnsFalseWhenStillOngoing() public { - revert Unimplemented(); + function test_VetoRevertsWhenAProposalHasNotStarted() public { + uint64 startDate = 50; + vm.warp(startDate - 1); + + IDAO.Action[] memory actions = new IDAO.Action[](0); + uint256 proposalId = plugin.createProposal( + "ipfs://", + actions, + 0, + startDate, + 0 + ); + + // Unstarted + vm.expectRevert( + abi.encodeWithSelector( + OptimisticTokenVotingPlugin.ProposalVetoingForbidden.selector, + proposalId, + alice + ) + ); + plugin.veto(proposalId); + assertEq( + plugin.hasVetoed(proposalId, alice), + false, + "Alice should not have vetoed" + ); + + // Started + vm.warp(startDate + 1); + plugin.veto(proposalId); + assertEq( + plugin.hasVetoed(proposalId, alice), + true, + "Alice should have vetoed" + ); } - function test_CanExecuteReturnsFalseWhenEnoughVetoesAreRegistered() public { - revert Unimplemented(); + function test_VetoRevertsWhenAVoterAlreadyVetoed() public { + uint64 startDate = 50; + vm.warp(startDate - 1); + + IDAO.Action[] memory actions = new IDAO.Action[](0); + uint256 proposalId = plugin.createProposal( + "ipfs://", + actions, + 0, + startDate, + 0 + ); + vm.warp(startDate + 1); + + assertEq( + plugin.hasVetoed(proposalId, alice), + false, + "Alice should not have vetoed" + ); + plugin.veto(proposalId); + assertEq( + plugin.hasVetoed(proposalId, alice), + true, + "Alice should have vetoed" + ); + + vm.expectRevert( + abi.encodeWithSelector( + OptimisticTokenVotingPlugin.ProposalVetoingForbidden.selector, + proposalId, + alice + ) + ); + plugin.veto(proposalId); + assertEq( + plugin.hasVetoed(proposalId, alice), + true, + "Alice should have vetoed" + ); } - function test_CanExecuteReturnsTrueOtherwise() public { - revert Unimplemented(); + function test_VetoRevertsWhenAVoterAlreadyEnded() public { + uint64 startDate = 50; + uint64 endDate = startDate + 10 days; + vm.warp(startDate - 1); + + IDAO.Action[] memory actions = new IDAO.Action[](0); + uint256 proposalId = plugin.createProposal( + "ipfs://", + actions, + 0, + startDate, + endDate + ); + vm.warp(endDate + 1); + + vm.expectRevert( + abi.encodeWithSelector( + OptimisticTokenVotingPlugin.ProposalVetoingForbidden.selector, + proposalId, + alice + ) + ); + plugin.veto(proposalId); + assertEq( + plugin.hasVetoed(proposalId, alice), + false, + "Alice should not have vetoed" + ); } - function test_IsMinVetoRatioReachedReturnsFalseWhenNotEnoughPeopleHaveVetoed() - public - { - revert Unimplemented(); + function test_VetoRevertsWhenNoVotingPower() public { + uint64 startDate = 50; + vm.warp(startDate - 1); + + IDAO.Action[] memory actions = new IDAO.Action[](0); + uint256 proposalId = plugin.createProposal( + "ipfs://", + actions, + 0, + startDate, + 0 + ); + + vm.warp(startDate + 1); + + vm.stopPrank(); + vm.startPrank(bob); + + // Bob owns no tokens + vm.expectRevert( + abi.encodeWithSelector( + OptimisticTokenVotingPlugin.ProposalVetoingForbidden.selector, + proposalId, + bob + ) + ); + plugin.veto(proposalId); + assertEq( + plugin.hasVetoed(proposalId, bob), + false, + "Bob should not have vetoed" + ); + + vm.stopPrank(); + vm.startPrank(alice); + + // Alice owns tokens + plugin.veto(proposalId); + assertEq( + plugin.hasVetoed(proposalId, alice), + true, + "Alice should have vetoed" + ); } - function test_IsMinVetoRatioReachedReturnsTrueWhenEnoughPeopleHaveVetoed() + function test_VetoRegistersAVetoForTheTokenHolderAndIncreasesTheTally() public { - revert Unimplemented(); - } + uint64 startDate = 50; + vm.warp(startDate - 1); - function test_ExecuteRevertsWhenAlreadyExecuted() public { - revert Unimplemented(); - } + IDAO.Action[] memory actions = new IDAO.Action[](0); + uint256 proposalId = plugin.createProposal( + "ipfs://", + actions, + 0, + startDate, + 0 + ); + vm.warp(startDate + 1); - function test_ExecuteRevertsWhenStillOngoing() public { - revert Unimplemented(); - } + (, , , uint256 tally1, , ) = plugin.getProposal(proposalId); + assertEq(tally1, 0, "Tally should be zero"); - function test_ExecuteRevertsWhenEnoughVetoesAreRegistered() public { - revert Unimplemented(); - } + plugin.veto(proposalId); + assertEq( + plugin.hasVetoed(proposalId, alice), + true, + "Alice should have vetoed" + ); - function testFuzz_ExecuteExecutesTheProposalActionsWhenNonDefeated( - uint256 _tokenSupply - ) public { - revert Unimplemented(); + (, , , uint256 tally2, , ) = plugin.getProposal(proposalId); + assertEq(tally2, 10 ether, "Tally should be 10 eth"); } - function test_ExecuteMarksTheProposalAsExecuted() public { - revert Unimplemented(); + function test_VetoEmitsAnEvent() public { + uint64 startDate = 50; + vm.warp(startDate - 1); + + IDAO.Action[] memory actions = new IDAO.Action[](0); + uint256 proposalId = plugin.createProposal( + "ipfs://", + actions, + 0, + startDate, + 0 + ); + vm.warp(startDate + 1); + + vm.expectEmit(); + emit VetoCast(proposalId, alice, 10 ether); + plugin.veto(proposalId); + } + + // Has vetoed + function test_HasVetoedReturnsTheRightValues() public { + uint64 startDate = 50; + vm.warp(startDate - 1); + + IDAO.Action[] memory actions = new IDAO.Action[](0); + uint256 proposalId = plugin.createProposal( + "ipfs://", + actions, + 0, + startDate, + 0 + ); + vm.warp(startDate + 1); + + assertEq( + plugin.hasVetoed(proposalId, alice), + false, + "Alice should not have vetoed" + ); + plugin.veto(proposalId); + assertEq( + plugin.hasVetoed(proposalId, alice), + true, + "Alice should have vetoed" + ); + } + + // Can execute + function test_CanExecuteReturnsFalseWhenNotEnded() public { + uint64 startDate = 50; + uint64 endDate = startDate + 10 days; + vm.warp(startDate - 1); + + IDAO.Action[] memory actions = new IDAO.Action[](0); + uint256 proposalId = plugin.createProposal( + "ipfs://", + actions, + 0, + startDate, + endDate + ); + + assertEq( + plugin.canExecute(proposalId), + false, + "The proposal shouldn't be executable" + ); + + vm.warp(startDate + 1); + + assertEq( + plugin.canExecute(proposalId), + false, + "The proposal shouldn't be executable" + ); + plugin.veto(proposalId); + + assertEq( + plugin.canExecute(proposalId), + false, + "The proposal shouldn't be executable yet" + ); + } + + function test_CanExecuteReturnsFalseWhenDefeated() public { + uint64 startDate = 50; + uint64 endDate = startDate + 10 days; + vm.warp(startDate - 1); + + IDAO.Action[] memory actions = new IDAO.Action[](0); + uint256 proposalId = plugin.createProposal( + "ipfs://", + actions, + 0, + startDate, + endDate + ); + + assertEq( + plugin.canExecute(proposalId), + false, + "The proposal shouldn't be executable" + ); + + vm.warp(startDate + 1); + + plugin.veto(proposalId); + assertEq( + plugin.canExecute(proposalId), + false, + "The proposal shouldn't be executable yet" + ); + + vm.warp(endDate + 1); + + assertEq( + plugin.canExecute(proposalId), + false, + "The proposal shouldn't be executable" + ); + } + + function test_CanExecuteReturnsFalseWhenAlreadyExecuted() public { + uint64 startDate = 50; + uint64 endDate = startDate + 10 days; + vm.warp(startDate - 1); + + IDAO.Action[] memory actions = new IDAO.Action[](0); + uint256 proposalId = plugin.createProposal( + "ipfs://", + actions, + 0, + startDate, + endDate + ); + + vm.warp(endDate + 1); + assertEq( + plugin.canExecute(proposalId), + true, + "The proposal should be executable" + ); + + plugin.execute(proposalId); + + assertEq( + plugin.canExecute(proposalId), + false, + "The proposal shouldn't be executable" + ); + } + + function test_CanExecuteReturnsTrueOtherwise() public { + uint64 startDate = 50; + uint64 endDate = startDate + 10 days; + vm.warp(startDate - 1); + + IDAO.Action[] memory actions = new IDAO.Action[](0); + uint256 proposalId = plugin.createProposal( + "ipfs://", + actions, + 0, + startDate, + endDate + ); + + assertEq( + plugin.canExecute(proposalId), + false, + "The proposal shouldn't be executable" + ); + + vm.warp(startDate + 1); + + assertEq( + plugin.canExecute(proposalId), + false, + "The proposal shouldn't be executable yet" + ); + + vm.warp(endDate + 1); + + assertEq( + plugin.canExecute(proposalId), + true, + "The proposal should be executable" + ); + } + + // Veto threshold reached + function test_IsMinVetoRatioReachedReturnsTheAppropriateValues() public { + // Deploy ERC20 token + votingToken = ERC20Mock( + createProxyAndCall( + address(votingTokenBase), + abi.encodeWithSelector(ERC20Mock.initialize.selector) + ) + ); + votingToken.mint(alice, 24 ether); + votingToken.mint(bob, 1 ether); + votingToken.mint(randomWallet, 75 ether); + + votingToken.delegate(alice); + + vm.stopPrank(); + vm.startPrank(bob); + votingToken.delegate(bob); + + vm.stopPrank(); + vm.startPrank(randomWallet); + votingToken.delegate(randomWallet); + + vm.roll(block.number + 1); + + // Deploy a new plugin instance + OptimisticTokenVotingPlugin.OptimisticGovernanceSettings + memory settings = OptimisticTokenVotingPlugin + .OptimisticGovernanceSettings({ + minVetoRatio: uint32((RATIO_BASE * 25) / 100), + minDuration: 10 days, + minProposerVotingPower: 0 + }); + + plugin = OptimisticTokenVotingPlugin( + createProxyAndCall( + address(pluginBase), + abi.encodeWithSelector( + OptimisticTokenVotingPlugin.initialize.selector, + dao, + settings, + votingToken + ) + ) + ); + + vm.stopPrank(); + vm.startPrank(alice); + + // Permissions + dao.grant(address(dao), address(plugin), dao.EXECUTE_PERMISSION_ID()); + dao.grant(address(plugin), alice, plugin.PROPOSER_PERMISSION_ID()); + + uint64 startDate = 50; + uint64 endDate = startDate + 10 days; + vm.warp(startDate - 1); + + IDAO.Action[] memory actions = new IDAO.Action[](0); + uint256 proposalId = plugin.createProposal( + "ipfs://", + actions, + 0, + startDate, + endDate + ); + + vm.warp(startDate + 1); + + assertEq( + plugin.isMinVetoRatioReached(proposalId), + false, + "The veto threshold shouldn't be met" + ); + // Alice vetoes 24% + plugin.veto(proposalId); + + assertEq( + plugin.isMinVetoRatioReached(proposalId), + false, + "The veto threshold shouldn't be met" + ); + + vm.stopPrank(); + vm.startPrank(bob); + + // Bob vetoes +1% => met + plugin.veto(proposalId); + + assertEq( + plugin.isMinVetoRatioReached(proposalId), + true, + "The veto threshold should be met" + ); + + vm.stopPrank(); + vm.startPrank(randomWallet); + + // Random wallet vetoes +75% => still met + plugin.veto(proposalId); + + assertEq( + plugin.isMinVetoRatioReached(proposalId), + true, + "The veto threshold should still be met" + ); + } + + // Execute + function test_ExecuteRevertsWhenNotEnded() public { + uint64 startDate = 50; + uint64 endDate = startDate + 10 days; + vm.warp(startDate - 1); + + IDAO.Action[] memory actions = new IDAO.Action[](0); + uint256 proposalId = plugin.createProposal( + "ipfs://", + actions, + 0, + startDate, + endDate + ); + + vm.expectRevert( + abi.encodeWithSelector( + OptimisticTokenVotingPlugin.ProposalExecutionForbidden.selector, + proposalId + ) + ); + plugin.execute(proposalId); + + vm.warp(startDate + 1); + + vm.expectRevert( + abi.encodeWithSelector( + OptimisticTokenVotingPlugin.ProposalExecutionForbidden.selector, + proposalId + ) + ); + plugin.execute(proposalId); + + vm.warp(endDate); + plugin.execute(proposalId); + + (, bool executed, , , , ) = plugin.getProposal(proposalId); + assertEq(executed, true, "The proposal should be executed"); + } + + function test_ExecuteRevertsWhenDefeated() public { + uint64 startDate = 50; + uint64 endDate = startDate + 10 days; + vm.warp(startDate - 1); + + IDAO.Action[] memory actions = new IDAO.Action[](0); + uint256 proposalId = plugin.createProposal( + "ipfs://", + actions, + 0, + startDate, + endDate + ); + + vm.warp(startDate + 1); + + plugin.veto(proposalId); + + vm.expectRevert( + abi.encodeWithSelector( + OptimisticTokenVotingPlugin.ProposalExecutionForbidden.selector, + proposalId + ) + ); + plugin.execute(proposalId); + + vm.warp(endDate + 1); + + vm.expectRevert( + abi.encodeWithSelector( + OptimisticTokenVotingPlugin.ProposalExecutionForbidden.selector, + proposalId + ) + ); + plugin.execute(proposalId); + + (, bool executed, , , , ) = plugin.getProposal(proposalId); + assertEq(executed, false, "The proposal should not be executed"); + } + + function test_ExecuteRevertsWhenAlreadyExecuted() public { + uint64 startDate = 50; + uint64 endDate = startDate + 10 days; + vm.warp(startDate - 1); + + IDAO.Action[] memory actions = new IDAO.Action[](0); + uint256 proposalId = plugin.createProposal( + "ipfs://", + actions, + 0, + startDate, + endDate + ); + + vm.warp(endDate + 1); + + plugin.execute(proposalId); + + (, bool executed1, , , , ) = plugin.getProposal(proposalId); + assertEq(executed1, true, "The proposal should be executed"); + + vm.expectRevert( + abi.encodeWithSelector( + OptimisticTokenVotingPlugin.ProposalExecutionForbidden.selector, + proposalId + ) + ); + plugin.execute(proposalId); + + (, bool executed2, , , , ) = plugin.getProposal(proposalId); + assertEq(executed2, true, "The proposal should be executed"); + } + + function test_ExecuteSucceedsOtherwise() public { + uint64 startDate = 50; + uint64 endDate = startDate + 10 days; + vm.warp(startDate - 1); + + IDAO.Action[] memory actions = new IDAO.Action[](0); + uint256 proposalId = plugin.createProposal( + "ipfs://", + actions, + 0, + startDate, + endDate + ); + + vm.warp(endDate + 1); + + plugin.execute(proposalId); + } + + function test_ExecuteMarksTheProposalAsExecuted() public { + uint64 startDate = 50; + uint64 endDate = startDate + 10 days; + vm.warp(startDate - 1); + + IDAO.Action[] memory actions = new IDAO.Action[](0); + uint256 proposalId = plugin.createProposal( + "ipfs://", + actions, + 0, + startDate, + endDate + ); + + vm.warp(endDate + 1); + + plugin.execute(proposalId); + + (, bool executed2, , , , ) = plugin.getProposal(proposalId); + assertEq(executed2, true, "The proposal should be executed"); } function test_ExecuteEmitsAnEvent() public { - revert Unimplemented(); + uint64 startDate = 50; + uint64 endDate = startDate + 10 days; + vm.warp(startDate - 1); + + IDAO.Action[] memory actions = new IDAO.Action[](0); + uint256 proposalId = plugin.createProposal( + "ipfs://", + actions, + 0, + startDate, + endDate + ); + + vm.warp(endDate + 1); + + vm.expectEmit(); + emit ProposalExecuted(proposalId); + plugin.execute(proposalId); } function test_UpdateOptimisticGovernanceSettingsRevertsWhenNoPermission() From 1de3710f9aa68e5c44e1558da7d8e6dc2229680a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8r=E2=88=82=C2=A1?= <4456749@users.noreply.github.com> Date: Thu, 16 Nov 2023 13:04:01 +0000 Subject: [PATCH 5/6] Unit testing tentatively ready --- test/OptimisticTokenVotingPlugin.t.sol | 369 ++++++++++++++++++-- test/OptimisticTokenVotingPluginSetup.t.sol | 12 - 2 files changed, 345 insertions(+), 36 deletions(-) diff --git a/test/OptimisticTokenVotingPlugin.t.sol b/test/OptimisticTokenVotingPlugin.t.sol index 2b51330..ab6c164 100644 --- a/test/OptimisticTokenVotingPlugin.t.sol +++ b/test/OptimisticTokenVotingPlugin.t.sol @@ -8,7 +8,7 @@ import {DAO} from "@aragon/osx/core/dao/DAO.sol"; import {IDAO} from "@aragon/osx/core/dao/IDAO.sol"; import {IProposal} from "@aragon/osx/core/plugin/proposal/IProposal.sol"; import {IMembership} from "@aragon/osx/core/plugin/membership/IMembership.sol"; -import {RATIO_BASE} from "@aragon/osx/plugins/utils/Ratio.sol"; +import {RATIO_BASE, RatioOutOfBounds} from "@aragon/osx/plugins/utils/Ratio.sol"; import {DaoUnauthorized} from "@aragon/osx/core/utils/auth.sol"; import {ERC20Mock} from "./mocks/ERC20Mock.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; @@ -44,6 +44,12 @@ contract OptimisticTokenVotingPluginTest is Test { uint256 votingPower ); event ProposalExecuted(uint256 indexed proposalId); + event OptimisticGovernanceSettingsUpdated( + uint32 minVetoRatio, + uint64 minDuration, + uint256 minProposerVotingPower + ); + event Upgraded(address indexed implementation); error Unimplemented(); @@ -239,18 +245,6 @@ contract OptimisticTokenVotingPluginTest is Test { ); } - function test_InitializeCreatesANewERC20Token() public { - revert Unimplemented(); - } - - function test_InitializeWrapsAnExistingToken() public { - revert Unimplemented(); - } - - function test_InitializeUsesAnExistingERC20Token() public { - revert Unimplemented(); - } - function test_InitializeEmitsEvent() public { OptimisticTokenVotingPlugin.OptimisticGovernanceSettings memory settings = OptimisticTokenVotingPlugin @@ -1769,62 +1763,389 @@ contract OptimisticTokenVotingPluginTest is Test { plugin.execute(proposalId); } + // Update settings function test_UpdateOptimisticGovernanceSettingsRevertsWhenNoPermission() public { - revert Unimplemented(); + OptimisticTokenVotingPlugin.OptimisticGovernanceSettings + memory newSettings = OptimisticTokenVotingPlugin + .OptimisticGovernanceSettings({ + minVetoRatio: uint32(RATIO_BASE / 5), + minDuration: 15 days, + minProposerVotingPower: 1 ether + }); + vm.expectRevert( + abi.encodeWithSelector( + DaoUnauthorized.selector, + address(dao), + address(plugin), + alice, + plugin.UPDATE_OPTIMISTIC_GOVERNANCE_SETTINGS_PERMISSION_ID() + ) + ); + plugin.updateOptimisticGovernanceSettings(newSettings); + + dao.grant( + address(plugin), + alice, + plugin.UPDATE_OPTIMISTIC_GOVERNANCE_SETTINGS_PERMISSION_ID() + ); + + plugin.updateOptimisticGovernanceSettings(newSettings); } function test_UpdateOptimisticGovernanceSettingsRevertsWhenTheMinVetoRatioIsZero() public { - revert Unimplemented(); + dao.grant( + address(plugin), + alice, + plugin.UPDATE_OPTIMISTIC_GOVERNANCE_SETTINGS_PERMISSION_ID() + ); + + OptimisticTokenVotingPlugin.OptimisticGovernanceSettings + memory newSettings = OptimisticTokenVotingPlugin + .OptimisticGovernanceSettings({ + minVetoRatio: 0, + minDuration: 10 days, + minProposerVotingPower: 0 ether + }); + vm.expectRevert( + abi.encodeWithSelector(RatioOutOfBounds.selector, 1, 0) + ); + plugin.updateOptimisticGovernanceSettings(newSettings); } function test_UpdateOptimisticGovernanceSettingsRevertsWhenTheMinVetoRatioIsAboveTheMaximum() public { - revert Unimplemented(); + dao.grant( + address(plugin), + alice, + plugin.UPDATE_OPTIMISTIC_GOVERNANCE_SETTINGS_PERMISSION_ID() + ); + + OptimisticTokenVotingPlugin.OptimisticGovernanceSettings + memory newSettings = OptimisticTokenVotingPlugin + .OptimisticGovernanceSettings({ + minVetoRatio: uint32(RATIO_BASE + 1), + minDuration: 10 days, + minProposerVotingPower: 0 ether + }); + vm.expectRevert( + abi.encodeWithSelector( + RatioOutOfBounds.selector, + RATIO_BASE, + uint32(RATIO_BASE + 1) + ) + ); + plugin.updateOptimisticGovernanceSettings(newSettings); } function test_UpdateOptimisticGovernanceSettingsRevertsWhenTheMinDurationIsLessThanFourDays() public { - revert Unimplemented(); + dao.grant( + address(plugin), + alice, + plugin.UPDATE_OPTIMISTIC_GOVERNANCE_SETTINGS_PERMISSION_ID() + ); + + OptimisticTokenVotingPlugin.OptimisticGovernanceSettings + memory newSettings = OptimisticTokenVotingPlugin + .OptimisticGovernanceSettings({ + minVetoRatio: uint32(RATIO_BASE / 10), + minDuration: 4 days - 1, + minProposerVotingPower: 0 ether + }); + vm.expectRevert( + abi.encodeWithSelector( + OptimisticTokenVotingPlugin.MinDurationOutOfBounds.selector, + 4 days, + 4 days - 1 + ) + ); + plugin.updateOptimisticGovernanceSettings(newSettings); + + // 2 + newSettings = OptimisticTokenVotingPlugin.OptimisticGovernanceSettings({ + minVetoRatio: uint32(RATIO_BASE / 10), + minDuration: 10 hours, + minProposerVotingPower: 0 ether + }); + vm.expectRevert( + abi.encodeWithSelector( + OptimisticTokenVotingPlugin.MinDurationOutOfBounds.selector, + 4 days, + 10 hours + ) + ); + plugin.updateOptimisticGovernanceSettings(newSettings); + + // 3 + newSettings = OptimisticTokenVotingPlugin.OptimisticGovernanceSettings({ + minVetoRatio: uint32(RATIO_BASE / 10), + minDuration: 0, + minProposerVotingPower: 0 ether + }); + vm.expectRevert( + abi.encodeWithSelector( + OptimisticTokenVotingPlugin.MinDurationOutOfBounds.selector, + 4 days, + 0 + ) + ); + plugin.updateOptimisticGovernanceSettings(newSettings); } function test_UpdateOptimisticGovernanceSettingsRevertsWhenTheMinDurationIsMoreThanOneYear() public { - revert Unimplemented(); + dao.grant( + address(plugin), + alice, + plugin.UPDATE_OPTIMISTIC_GOVERNANCE_SETTINGS_PERMISSION_ID() + ); + + OptimisticTokenVotingPlugin.OptimisticGovernanceSettings + memory newSettings = OptimisticTokenVotingPlugin + .OptimisticGovernanceSettings({ + minVetoRatio: uint32(RATIO_BASE / 10), + minDuration: 365 days + 1, + minProposerVotingPower: 0 ether + }); + vm.expectRevert( + abi.encodeWithSelector( + OptimisticTokenVotingPlugin.MinDurationOutOfBounds.selector, + 365 days, + 365 days + 1 + ) + ); + plugin.updateOptimisticGovernanceSettings(newSettings); + + // 2 + newSettings = OptimisticTokenVotingPlugin.OptimisticGovernanceSettings({ + minVetoRatio: uint32(RATIO_BASE / 10), + minDuration: 500 days, + minProposerVotingPower: 0 ether + }); + vm.expectRevert( + abi.encodeWithSelector( + OptimisticTokenVotingPlugin.MinDurationOutOfBounds.selector, + 365 days, + 500 days + ) + ); + plugin.updateOptimisticGovernanceSettings(newSettings); + + // 3 + newSettings = OptimisticTokenVotingPlugin.OptimisticGovernanceSettings({ + minVetoRatio: uint32(RATIO_BASE / 10), + minDuration: 1000 days, + minProposerVotingPower: 0 ether + }); + vm.expectRevert( + abi.encodeWithSelector( + OptimisticTokenVotingPlugin.MinDurationOutOfBounds.selector, + 365 days, + 1000 days + ) + ); + plugin.updateOptimisticGovernanceSettings(newSettings); } function test_UpdateOptimisticGovernanceSettingsRevertsWhenMinProposerVotingPowerIsMoreThanTheTokenSupply() public { - revert Unimplemented(); + dao.grant( + address(plugin), + alice, + plugin.UPDATE_OPTIMISTIC_GOVERNANCE_SETTINGS_PERMISSION_ID() + ); + + OptimisticTokenVotingPlugin.OptimisticGovernanceSettings + memory newSettings = OptimisticTokenVotingPlugin + .OptimisticGovernanceSettings({ + minVetoRatio: uint32(RATIO_BASE / 10), + minDuration: 10 days, + minProposerVotingPower: 10 ether + 1 + }); + vm.expectRevert( + abi.encodeWithSelector( + OptimisticTokenVotingPlugin + .MinProposerVotingPowerOutOfBounds + .selector, + 10 ether, + 10 ether + 1 + ) + ); + plugin.updateOptimisticGovernanceSettings(newSettings); + + // 2 + newSettings = OptimisticTokenVotingPlugin.OptimisticGovernanceSettings({ + minVetoRatio: uint32(RATIO_BASE / 10), + minDuration: 10 days, + minProposerVotingPower: 50 ether + }); + vm.expectRevert( + abi.encodeWithSelector( + OptimisticTokenVotingPlugin + .MinProposerVotingPowerOutOfBounds + .selector, + 10 ether, + 50 ether + ) + ); + plugin.updateOptimisticGovernanceSettings(newSettings); + + // 3 + newSettings = OptimisticTokenVotingPlugin.OptimisticGovernanceSettings({ + minVetoRatio: uint32(RATIO_BASE / 10), + minDuration: 10 days, + minProposerVotingPower: 200 ether + }); + vm.expectRevert( + abi.encodeWithSelector( + OptimisticTokenVotingPlugin + .MinProposerVotingPowerOutOfBounds + .selector, + 10 ether, + 200 ether + ) + ); + plugin.updateOptimisticGovernanceSettings(newSettings); } function test_UpdateOptimisticGovernanceSettingsEmitsAnEventWhenSuccessful() public { - revert Unimplemented(); + dao.grant( + address(plugin), + alice, + plugin.UPDATE_OPTIMISTIC_GOVERNANCE_SETTINGS_PERMISSION_ID() + ); + + OptimisticTokenVotingPlugin.OptimisticGovernanceSettings + memory newSettings = OptimisticTokenVotingPlugin + .OptimisticGovernanceSettings({ + minVetoRatio: uint32(RATIO_BASE / 5), + minDuration: 15 days, + minProposerVotingPower: 1 ether + }); + + vm.expectEmit(); + emit OptimisticGovernanceSettingsUpdated({ + minVetoRatio: uint32(RATIO_BASE / 5), + minDuration: 15 days, + minProposerVotingPower: 1 ether + }); + + plugin.updateOptimisticGovernanceSettings(newSettings); } + // Upgrade plugin function test_UpgradeToRevertsWhenCalledFromNonUpgrader() public { - revert Unimplemented(); + address _pluginBase = address(new OptimisticTokenVotingPlugin()); + + vm.expectRevert( + abi.encodeWithSelector( + DaoUnauthorized.selector, + address(dao), + address(plugin), + alice, + plugin.UPGRADE_PLUGIN_PERMISSION_ID() + ) + ); + + plugin.upgradeTo(_pluginBase); } function test_UpgradeToAndCallRevertsWhenCalledFromNonUpgrader() public { - revert Unimplemented(); + dao.grant( + address(plugin), + alice, + plugin.UPDATE_OPTIMISTIC_GOVERNANCE_SETTINGS_PERMISSION_ID() + ); + + address _pluginBase = address(new OptimisticTokenVotingPlugin()); + + OptimisticTokenVotingPlugin.OptimisticGovernanceSettings + memory settings = OptimisticTokenVotingPlugin + .OptimisticGovernanceSettings({ + minVetoRatio: uint32(RATIO_BASE / 5), + minDuration: 15 days, + minProposerVotingPower: 1 ether + }); + + vm.expectRevert( + abi.encodeWithSelector( + DaoUnauthorized.selector, + address(dao), + address(plugin), + alice, + plugin.UPGRADE_PLUGIN_PERMISSION_ID() + ) + ); + + plugin.upgradeToAndCall( + _pluginBase, + abi.encodeWithSelector( + OptimisticTokenVotingPlugin + .updateOptimisticGovernanceSettings + .selector, + settings + ) + ); } function test_UpgradeToSucceedsWhenCalledFromUpgrader() public { - revert Unimplemented(); + dao.grant( + address(plugin), + alice, + plugin.UPGRADE_PLUGIN_PERMISSION_ID() + ); + + address _pluginBase = address(new OptimisticTokenVotingPlugin()); + + vm.expectEmit(); + emit Upgraded(_pluginBase); + + plugin.upgradeTo(_pluginBase); } function test_UpgradeToAndCallSucceedsWhenCalledFromUpgrader() public { - revert Unimplemented(); + dao.grant( + address(plugin), + alice, + plugin.UPGRADE_PLUGIN_PERMISSION_ID() + ); + dao.grant( + address(plugin), + alice, + plugin.UPDATE_OPTIMISTIC_GOVERNANCE_SETTINGS_PERMISSION_ID() + ); + + address _pluginBase = address(new OptimisticTokenVotingPlugin()); + + vm.expectEmit(); + emit Upgraded(_pluginBase); + + OptimisticTokenVotingPlugin.OptimisticGovernanceSettings + memory settings = OptimisticTokenVotingPlugin + .OptimisticGovernanceSettings({ + minVetoRatio: uint32(RATIO_BASE / 5), + minDuration: 15 days, + minProposerVotingPower: 1 ether + }); + plugin.upgradeToAndCall( + _pluginBase, + abi.encodeWithSelector( + OptimisticTokenVotingPlugin + .updateOptimisticGovernanceSettings + .selector, + settings + ) + ); } // HELPERS diff --git a/test/OptimisticTokenVotingPluginSetup.t.sol b/test/OptimisticTokenVotingPluginSetup.t.sol index b217127..42d9301 100644 --- a/test/OptimisticTokenVotingPluginSetup.t.sol +++ b/test/OptimisticTokenVotingPluginSetup.t.sol @@ -12,16 +12,4 @@ contract OptimisticTokenVotingPluginSetupTest is Test { // plugin = new OptimisticTokenVotingPluginSetup(); // plugin.setNumber(0); } - - function test_Increment() public { - // plugin.increment(); - // assertEq(plugin.number(), 1); - revert Unimplemented(); - } - - function testFuzz_SetNumber(uint256 x) public { - // plugin.setNumber(x); - // assertEq(plugin.number(), x); - revert Unimplemented(); - } } From 5019cfe35592fbb601c66981d0b216ef66967a45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8r=E2=88=82=C2=A1?= <4456749@users.noreply.github.com> Date: Thu, 16 Nov 2023 13:08:37 +0000 Subject: [PATCH 6/6] Workflow file updated --- .github/workflows/test.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9282e82..3f38b02 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,14 @@ name: test -on: workflow_dispatch +on: + workflow_dispatch: + push: + paths: + - 'lib/**' + - 'script/**' + - 'src/**' + - 'tests/**' + - '.github/workflows/*.yml' env: FOUNDRY_PROFILE: ci