diff --git a/packages/contracts/src/IVocdoniProposal.sol b/packages/contracts/src/IVocdoniProposal.sol index b89bcd6c..b981525d 100644 --- a/packages/contracts/src/IVocdoniProposal.sol +++ b/packages/contracts/src/IVocdoniProposal.sol @@ -5,16 +5,16 @@ pragma solidity ^0.8.17; import {IDAO} from "@aragon/osx/core/dao/IDAO.sol"; /// @title IVocdoniProposal -/// @notice An interface to be implemented by DAO plugins that create and execute off-chain proposals. -/// @dev Slighly modified from the original Aragon OSx IProposal interface. +/// @notice An interface to be implemented by DAO plugins that create and execute gasless proposals. +/// @dev Slightly modified from the original Aragon OSx IProposal interface. interface IVocdoniProposal { /// @notice Emitted when a proposal is created. /// @param proposalId The ID of the proposal. /// @param vochainProposalId The ID of the proposal in the Vochain. /// @param creator The creator of the proposal. /// @param startDate The start date of the proposal in seconds. - /// @param endDate The end date of the proposal in seconds. - /// @param expirationDate The expiration date of the proposal in seconds. + /// @param voteEndDate The vote end date of the proposal in seconds. + /// @param tallyEndDate The tally end date of the proposal in seconds. /// @param actions The actions that will be executed if the proposal passes. /// @param allowFailureMap A bitmap allowing the proposal to succeed, even if individual actions might revert. If the bit at index `i` is 1, the proposal succeeds even if the `i`th action reverts. A failure map value of 0 requires every action to not revert. event ProposalCreated( @@ -22,8 +22,8 @@ interface IVocdoniProposal { bytes32 indexed vochainProposalId, address indexed creator, uint64 startDate, - uint64 endDate, - uint64 expirationDate, + uint64 voteEndDate, + uint64 tallyEndDate, IDAO.Action[] actions, uint256 allowFailureMap ); @@ -35,4 +35,4 @@ interface IVocdoniProposal { /// @notice Returns the proposal count determining the next proposal ID. /// @return The proposal count. function proposalCount() external view returns (uint256); -} \ No newline at end of file +} diff --git a/packages/contracts/src/IVocdoniVoting.sol b/packages/contracts/src/IVocdoniVoting.sol index a8e9eb28..2c0dedc4 100644 --- a/packages/contracts/src/IVocdoniVoting.sol +++ b/packages/contracts/src/IVocdoniVoting.sol @@ -1,20 +1,18 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity ^0.8.17; -import {IDAO} from "@aragon/osx/core/dao/IDAO.sol"; - /// @title IVocdoniVoting /// @author Vocdoni -/// @notice The Vocdoni off-chain voting contract interface for the OSX plugin. -/// @notice The voting Proposal is managed off-chain on the Vocdoni blockchain. +/// @notice The Vocdoni gasless voting contract interface for the OSX plugin. +/// @notice The voting Proposal is managed gasless on the Vocdoni blockchain. interface IVocdoniVoting { - /// @notice Emitted when one or more committee members are added. - /// @param newMembers The addresses of the new committee members. - event CommitteeMembersAdded(address[] indexed newMembers); + /// @notice Emitted when one or more execution multisig members are added. + /// @param newMembers The addresses of the new execution multisig members. + event ExecutionMultisigMembersAdded(address[] newMembers); - /// @notice Emitted when one or more committee member are removed. - /// @param removedMembers The addresses of the removed committee members. - event CommitteeMembersRemoved(address[] indexed removedMembers); + /// @notice Emitted when one or more execution multisig member are removed. + /// @param removedMembers The addresses of the removed execution multisig members. + event ExecutionMultisigMembersRemoved(address[] removedMembers); /// @notice Emitted when the tally of a proposal is set. /// @param proposalId The ID of the proposal. @@ -25,7 +23,7 @@ interface IVocdoniVoting { /// @param proposalId The ID of the proposal. event TallyApproval(uint256 indexed proposalId, address indexed approver); - /// @notice Thrown if the address list length is out of bounds. + /// @notice Thrown if the address list length is out of bounds. /// @param limit The limit value. /// @param actual The actual value. error AddresslistLengthOutOfBounds(uint16 limit, uint256 actual); @@ -35,15 +33,15 @@ interface IVocdoniVoting { /// @param actual The actual value. error MinApprovalsOutOfBounds(uint16 limit, uint16 actual); - /// @notice Thrown if the minimal duration value is out of bounds (less than one hour or greater than 1 year). + /// @notice Thrown if the vote phase duration is out of bounds (more than 1 year or less than 1 hour). /// @param limit The limit value. /// @param actual The actual value. - error MinDurationOutOfBounds(uint64 limit, uint64 actual); + error VoteDurationOutOfBounds(uint64 limit, uint64 actual); - /// @notice Trown if the maximum proposal expiration time is out of bounds (more than 1 year). + /// @notice Trown if the tally phase duration is out of bounds (more than 1 year or less than 1 hour). /// @param limit The limit value. /// @param actual The actual value. - error ExpirationTimeOutOfBounds(uint64 limit, uint64 actual); + error TallyDurationOutOfBounds(uint64 limit, uint64 actual); /// @notice Thrown if the start date is invalid. /// @param limit The limit value. @@ -53,20 +51,20 @@ interface IVocdoniVoting { /// @notice Thrown if the end date is invalid. /// @param limit The limit value. /// @param actual The actual value. - error InvalidEndDate(uint64 limit, uint64 actual); + error InvalidVoteEndDate(uint64 limit, uint64 actual); - /// @notice Thrown if the expiration date is invalid. - /// @param limit The expiration date. + /// @notice Thrown if the tally end date is invalid. + /// @param limit The tally end date. /// @param actual The actual value. - error InvalidExpirationDate(uint64 limit, uint64 actual); + error InvalidTallyEndDate(uint64 limit, uint64 actual); /// @notice Thrown if the plugin settings are updated too recently. /// @param lastUpdate The block number of the last update. error PluginSettingsUpdatedTooRecently(uint64 lastUpdate); - /// @notice Thrown if the committee is updated too recently. + /// @notice Thrown if the execution multisig is updated too recently. /// @param lastUpdate The block number of the last update. - error CommitteeUpdatedTooRecently(uint64 lastUpdate); + error ExecutionMultisigUpdatedTooRecently(uint64 lastUpdate); /// @notice Thrown if the proposal is already executed. /// @param proposalId The ID of the proposal. @@ -85,7 +83,7 @@ interface IVocdoniVoting { /// @param sender The sender. error TallyAlreadyApprovedBySender(address sender); - /// @notice Thrown if the proposal tally is not approved by enough committee members. + /// @notice Thrown if the proposal tally is not approved by enough execution multisig members. /// @param minApprovals The minimum number of approvals required. /// @param actualApprovals The actual number of approvals. error NotEnoughApprovals(uint16 minApprovals, uint16 actualApprovals); @@ -94,19 +92,23 @@ interface IVocdoniVoting { /// @param addr The address error InvalidAddress(address addr); - /// @notice Thrown if the prosal is not in the tally phase - /// @param endDate The end date of the proposal - /// @param expirationDate The expiration date of the proposal + /// @notice Thrown if the proposal is not in the tally phase + /// @param voteEndDate The end date of the proposal + /// @param tallyEndDate The tally end date of the proposal /// @param currentTimestamp The current timestamp - error ProposalNotInTallyPhase(uint64 endDate, uint64 expirationDate, uint256 currentTimestamp); + error ProposalNotInTallyPhase( + uint64 voteEndDate, + uint64 tallyEndDate, + uint256 currentTimestamp + ); /// @notice Thrown if the msg.sender does not have enough voting power /// @param required The required voting power error NotEnoughVotingPower(uint256 required); - /// @notice Thrown if the msg.sender is not a committee member + /// @notice Thrown if the msg.sender is not a execution multisig member /// @param sender The sender - error OnlyCommittee(address sender); + error OnlyExecutionMultisig(address sender); /// @notice Thrown if the support threshold is not reached /// @param currentSupport The current support @@ -114,22 +116,30 @@ interface IVocdoniVoting { error SupportThresholdNotReached(uint256 currentSupport, uint32 supportThreshold); /// @notice Thrown if the minimum participation is not reached - /// @param currentParticipation The current participation - /// @param minParticipation The minimum participation - error MinParticipationNotReached(uint256 currentParticipation,uint32 minParticipation); + /// @param currentVotingPower The current voting power + /// @param minVotingPower The minimum voting power to reach + error MinParticipationNotReached(uint256 currentVotingPower, uint256 minVotingPower); + + /// @notice Thrown if the total voting power is invalid + /// @param totalVotingPower The total voting power + error InvalidTotalVotingPower(uint256 totalVotingPower); + + /// @notice Thrown if invalid list length + /// @param length The actual length + error InvalidListLength(uint256 length); - /// @notice Adds new committee members. - /// @param _members The addresses of the new committee members. - function addCommitteeMembers(address[] calldata _members) external; + /// @notice Adds new execution multisig members. + /// @param _members The addresses of the new execution multisig members. + function addExecutionMultisigMembers(address[] calldata _members) external; - /// @notice Removes committee members. - /// @param _members The addresses of the committee members to remove. - function removeCommitteeMembers(address[] calldata _members) external; + /// @notice Removes execution multisig members. + /// @param _members The addresses of the execution multisig members to remove. + function removeExecutionMultisigMembers(address[] calldata _members) external; - /// @notice Returns whether an address is a committee member. + /// @notice Returns whether an address is a execution ultisig member. /// @param _member The address to check. - /// @return Whether the address is a committee member. - function isCommitteeMember(address _member) external view returns (bool); + /// @return Whether the address is a execution multisig member. + function isExecutionMultisigMember(address _member) external view returns (bool); /// @notice Sets the tally of a given proposal. /// @param _proposalId The ID of the proposal to set the tally of. @@ -138,9 +148,10 @@ interface IVocdoniVoting { /// @notice Approves a proposal tally. /// @param _proposalId The ID of the proposal to approve. + /// @param _tryExecution Whether to try to execute the proposal if the tally is approved. function approveTally(uint256 _proposalId, bool _tryExecution) external; /// @notice Executes a proposal. /// @param _proposalId The ID of the proposal to execute. function executeProposal(uint256 _proposalId) external; -} \ No newline at end of file +} diff --git a/packages/contracts/src/VocdoniProposalUpgradeable.sol b/packages/contracts/src/VocdoniProposalUpgradeable.sol index e7039c6c..40391b5d 100644 --- a/packages/contracts/src/VocdoniProposalUpgradeable.sol +++ b/packages/contracts/src/VocdoniProposalUpgradeable.sol @@ -8,7 +8,7 @@ import {IVocdoniProposal} from "./IVocdoniProposal.sol"; import {IDAO} from "@aragon/osx/core/dao/IDAO.sol"; /// @title VocdoniProposalUpgradeable -/// @notice An abstract contract containing the traits and internal functionality to create and execute off-chain proposals that can be inherited by upgradeable DAO plugins. +/// @notice An abstract contract containing the traits and internal functionality to create and execute gasless proposals that can be inherited by upgradeable DAO plugins. /// @dev Slighly modified from the original Aragon OSx ProposalUpgradeable contract. abstract contract VocdoniProposalUpgradeable is IVocdoniProposal, ERC165Upgradeable { using CountersUpgradeable for CountersUpgradeable.Counter; @@ -25,7 +25,9 @@ abstract contract VocdoniProposalUpgradeable is IVocdoniProposal, ERC165Upgradea /// @param _interfaceId The ID of the interface. /// @return Returns `true` if the interface is supported. function supportsInterface(bytes4 _interfaceId) public view virtual override returns (bool) { - return _interfaceId == type(IVocdoniProposal).interfaceId || super.supportsInterface(_interfaceId); + return + _interfaceId == type(IVocdoniProposal).interfaceId || + super.supportsInterface(_interfaceId); } /// @notice Creates a proposal ID. @@ -53,4 +55,4 @@ abstract contract VocdoniProposalUpgradeable is IVocdoniProposal, ERC165Upgradea /// @notice This empty reserved space is put in place to allow future versions to add new variables without shifting down storage in the inheritance chain (see [OpenZeppelin's guide about storage gaps](https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps)). uint256[49] private __gap; -} \ No newline at end of file +} diff --git a/packages/contracts/src/VocdoniVoting.sol b/packages/contracts/src/VocdoniVoting.sol index c78be64d..57568447 100644 --- a/packages/contracts/src/VocdoniVoting.sol +++ b/packages/contracts/src/VocdoniVoting.sol @@ -7,7 +7,7 @@ import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20 import {IDAO} from "@aragon/osx/core/dao/IDAO.sol"; import {PluginUUPSUpgradeable} from "@aragon/osx/core/plugin/PluginUUPSUpgradeable.sol"; -import {RATIO_BASE, RatioOutOfBounds} from "@aragon/osx/plugins/utils/Ratio.sol"; +import {RATIO_BASE, _applyRatioCeiled, RatioOutOfBounds} from "@aragon/osx/plugins/utils/Ratio.sol"; import {Addresslist} from "@aragon/osx/plugins/utils/Addresslist.sol"; import {VocdoniProposalUpgradeable} from "./VocdoniProposalUpgradeable.sol"; @@ -15,93 +15,95 @@ import {IVocdoniVoting} from "./IVocdoniVoting.sol"; /// @title VocdoniVoting /// @author Vocdoni -/// @notice The Vocdoni off-chain voting data contract for the OSX plugin. -/// @notice The voting Proposal is managed off-chain on the Vocdoni blockchain. -contract VocdoniVoting is IVocdoniVoting, PluginUUPSUpgradeable, VocdoniProposalUpgradeable, Addresslist { - +/// @notice The Vocdoni gasless voting data contract for the OSX plugin. +/// @notice The voting Proposal is managed gasless on the Vocdoni blockchain. +contract VocdoniVoting is + IVocdoniVoting, + PluginUUPSUpgradeable, + VocdoniProposalUpgradeable, + Addresslist +{ using SafeCastUpgradeable for uint256; /// @notice The [ERC-165](https://eips.ethereum.org/EIPS/eip-165) interface ID of the contract. bytes4 internal constant VOCDONI_INTERFACE_ID = - this.initialize.selector ^ - this.addCommitteeMembers.selector ^ - this.removeCommitteeMembers.selector ^ - this.isCommitteeMember.selector ^ - this.setTally.selector ^ - this.approveTally.selector ^ - this.executeProposal.selector; + this.initialize.selector ^ this.createProposal.selector; /// @notice The ID of the permission required to update the plugin settings. bytes32 public constant UPDATE_PLUGIN_SETTINGS_PERMISSION_ID = keccak256("UPDATE_PLUGIN_SETTINGS_PERMISSION"); - /// @notice The ID of the permission required to add/remove committee members. - bytes32 public constant UPDATE_PLUGIN_COMMITTEE_PERMISSION_ID = - keccak256("UPDATE_PLUGIN_COMMITTEE_PERMISSION"); + /// @notice The ID of the permission required to add/remove executionMultisig members. + bytes32 public constant UPDATE_PLUGIN_EXECUTION_MULTISIG_PERMISSION_ID = + keccak256("UPDATE_PLUGIN_EXECUTION_MULTISIG_PERMISSION"); /// @notice Emitted when the plugin settings are updated. - /// @param onlyCommitteeProposalCreation If true, only committee members can create proposals. + /// @param onlyExecutionMultisigProposalCreation If true, only executionMultisig members can create proposals. /// @param minTallyApprovals The minimum number of approvals required for a tally to be considered accepted. - /// @param minDuration The minimum duration of a propsal. - /// @param expirationTime The maximum expiration time of a proposal. Proposal cannot be executed after this time. /// @param minParticipation The minimum participation value. Its value has to be in the interval [0, 10^6] defined by `RATIO_BASE = 10**6`. /// @param supportThreshold The support threshold value. Its value has to be in the interval [0, 10^6] defined by `RATIO_BASE = 10**6`. + /// @param minVoteDuration The minimum duration of the vote phase of a proposal. + /// @param minTallyDuration The minimum duration of the tally phase of a proposal. /// @param daoTokenAddress The address of the DAO token. - /// @param censusStrategy The predicate of the census strategy to be used in the proposals. See: https://github.com/vocdoni/census3 + /// @param censusStrategyURI The URI containing the predicate of the census strategy to be used in the proposals. See: https://github.com/vocdoni/census3 /// @param minProposerVotingPower The minimum voting power required to create a proposal. Voting power is extracted from the DAO token event PluginSettingsUpdated( - bool onlyCommitteeProposalCreation, - uint16 minTallyApprovals, - uint64 minDuration, - uint64 expirationTime, + bool onlyExecutionMultisigProposalCreation, + uint16 indexed minTallyApprovals, uint32 minParticipation, uint32 supportThreshold, - address daoTokenAddress, - string censusStrategy, + uint64 minVoteDuration, + uint64 minTallyDuration, + address indexed daoTokenAddress, + string indexed censusStrategyURI, uint256 minProposerVotingPower ); /// @notice A container for the Vocdoni voting plugin settings - /// @param onlyCommitteeProposalCreation If true, only committee members can create proposals. + /// @param onlyExecutionMultisigProposalCreation If true, only executionMultisig members can create proposals. /// @param minTallyApprovals The minimum number of approvals required for the tally to be considered valid. /// @param minParticipation The minimum participation value. Its value has to be in the interval [0, 10^6] defined by `RATIO_BASE = 10**6`. /// @param supportThreshold The support threshold value. Its value has to be in the interval [0, 10^6] defined by `RATIO_BASE = 10**6`. - /// @param minDuration The minimum duration of a proposal. - /// @param expirationTime The maximum expiration time of a proposal. Proposal cannot be executed after. + /// @param minVoteDuration The minimum duration of the vote phase of a proposal. + /// @param minTallyDuration The minimum duration of the tally phase of a proposal. /// @param daoTokenAddress The address of the DAO token. /// @param minProposerVotingPower The minimum voting power required to create a proposal. Voting power is extracted from the DAO token - /// @param censusStrategy The predicate of the census strategy to be used in the proposals. See: https://github.com/vocdoni/census3 + /// @param censusStrategyURI The URI containing he census strategy to be used in the proposals. See: https://github.com/vocdoni/census3 struct PluginSettings { - bool onlyCommitteeProposalCreation; + bool onlyExecutionMultisigProposalCreation; uint16 minTallyApprovals; uint32 minParticipation; uint32 supportThreshold; - uint64 minDuration; - uint64 expirationTime; + uint64 minVoteDuration; + uint64 minTallyDuration; address daoTokenAddress; uint256 minProposerVotingPower; - string censusStrategy; + string censusStrategyURI; } /// @notice A container for the proposal parameters. - /// @param censusBlock The block numbers used to generate the census of the proposal /// @param securityBlock Block number used for limiting contract usage when plugin settings are updated /// @param startDate The timestamp when the proposal starts. - /// @param endDate The timestamp when the proposal ends. At this point the tally can be set. - /// @param expirationDate The timestamp when the proposal expires. Proposal can't be executed after. + /// @param voteEndDate The timestamp when the proposal ends. At this point the tally can be set. + /// @param tallyEndDate The timestamp when the proposal expires. Proposal can't be executed after. + /// @param totalVotingPower The total voting power of the proposal. + /// @param censusURI The URI of the census. + /// @param censusRoot The root of the census. struct ProposalParameters { - string[] censusBlock; uint64 securityBlock; uint64 startDate; - uint64 endDate; - uint64 expirationDate; + uint64 voteEndDate; + uint64 tallyEndDate; + uint256 totalVotingPower; + string censusURI; + bytes32 censusRoot; } /// @notice A container for proposal-related information. /// @param executed Whether the proposal is executed or not. /// @param vochainProposalId The ID of the proposal in the Vochain. /// @param allowFailureMap A bitmap allowing the proposal to succeed, even if individual actions might revert. If the bit at index `i` is 1, - // the proposal succeeds even if the `i`th action reverts. A failure map value of 0 requires every action to not revert. + // the proposal succeeds even if the nth action reverts. A failure map value of 0 requires every action to not revert. /// @param parameters The parameters of the proposal. /// @param tally The tally of the proposal. /// @dev tally only supports [[Yes, No, Abstain]] schema in this order. i.e [[10, 5, 2]] means 10 Yes, 5 No, 2 Abstain. @@ -117,34 +119,29 @@ contract VocdoniVoting is IVocdoniVoting, PluginUUPSUpgradeable, VocdoniProposal IDAO.Action[] actions; } + /// @notice Keeps track at which block number the plugin settings have been changed the last time. + uint64 private lastPluginSettingsChange; + + /// @notice Keeps track at which block number the executionMultisig has been changed the last time. + uint64 private lastExecutionMultisigChange; + /// @notice A mapping between proposal IDs and proposal information. mapping(uint256 => Proposal) private proposals; /// @notice The current plugin settings. PluginSettings private pluginSettings; - /// @notice Keeps track at which block number the plugin settings have been changed the last time. - uint64 private lastPluginSettingsChange; - - /// @notice Keeps track at which block number the committee has been changed the last time. - uint64 private lastCommitteeChange; - - /// @notice Initializes the plugin. /// @param _dao The DAO address. - /// @param _committeeAddresses The addresses of the committee. + /// @param _executionMultisigAddresses The addresses of the executionMultisig. /// @param _pluginSettings The initial plugin settings. - function initialize(IDAO _dao, address[] calldata _committeeAddresses, PluginSettings memory _pluginSettings) external initializer { + function initialize( + IDAO _dao, + address[] calldata _executionMultisigAddresses, + PluginSettings memory _pluginSettings + ) external initializer { __PluginUUPSUpgradeable_init(_dao); - - if (_committeeAddresses.length > type(uint16).max) { - revert AddresslistLengthOutOfBounds({limit: type(uint16).max, actual: _committeeAddresses.length}); - } - - _addAddresses(_committeeAddresses); - lastCommitteeChange = uint64(block.number); - emit CommitteeMembersAdded({newMembers: _committeeAddresses}); - + _addExecutionMultisigMembers(_executionMultisigAddresses); _updatePluginSettings(_pluginSettings); } @@ -168,11 +165,18 @@ contract VocdoniVoting is IVocdoniVoting, PluginUUPSUpgradeable, VocdoniProposal } /// @inheritdoc IVocdoniVoting - function addCommitteeMembers( + function addExecutionMultisigMembers( address[] calldata _members - ) external override auth(UPDATE_PLUGIN_COMMITTEE_PERMISSION_ID) { + ) external override auth(UPDATE_PLUGIN_EXECUTION_MULTISIG_PERMISSION_ID) { + _addExecutionMultisigMembers(_members); + } + + /// @notice Private function for adding execution multisig members. + /// @param _members The addresses to add. + function _addExecutionMultisigMembers(address[] calldata _members) private { + _guardExecutionMultisig(); if (_members.length == 0) { - revert("No members provided"); + revert InvalidListLength({length: _members.length}); } uint256 newAddresslistLength = addresslistLength() + _members.length; @@ -185,24 +189,25 @@ contract VocdoniVoting is IVocdoniVoting, PluginUUPSUpgradeable, VocdoniProposal }); } - if (lastCommitteeChange == uint64(block.number)) { - revert CommitteeUpdatedTooRecently({ - lastUpdate: lastPluginSettingsChange - }); - } - _addAddresses(_members); - lastCommitteeChange = uint64(block.number); + lastExecutionMultisigChange = uint64(block.number); - emit CommitteeMembersAdded({newMembers: _members}); + emit ExecutionMultisigMembersAdded({newMembers: _members}); } /// @inheritdoc IVocdoniVoting - function removeCommitteeMembers( + function removeExecutionMultisigMembers( address[] calldata _members - ) external override auth(UPDATE_PLUGIN_COMMITTEE_PERMISSION_ID) { + ) external override auth(UPDATE_PLUGIN_EXECUTION_MULTISIG_PERMISSION_ID) { + _removeExecutionMultisigMember(_members); + } + + /// @notice Private function for removing execution multisig members. + /// @param _members The addresses to remove. + function _removeExecutionMultisigMember(address[] calldata _members) private { + _guardExecutionMultisig(); if (_members.length == 0) { - revert("No members provided"); + revert InvalidListLength({length: _members.length}); } uint16 newAddresslistLength = uint16(addresslistLength() - _members.length); @@ -215,63 +220,75 @@ contract VocdoniVoting is IVocdoniVoting, PluginUUPSUpgradeable, VocdoniProposal }); } - if (lastCommitteeChange == uint64(block.number)) { - revert CommitteeUpdatedTooRecently({ - lastUpdate: lastPluginSettingsChange - }); - } - _removeAddresses(_members); - lastCommitteeChange = uint64(block.number); + lastExecutionMultisigChange = uint64(block.number); - emit CommitteeMembersRemoved({removedMembers: _members}); + emit ExecutionMultisigMembersRemoved({removedMembers: _members}); } /// @inheritdoc IVocdoniVoting - function isCommitteeMember(address _member) public view override returns (bool) { - return _isCommitteeMember(_member); + function isExecutionMultisigMember(address _member) public view override returns (bool) { + return _isExecutionMultisigMember(_member); } - /// @notice Internal function for checking whether an address is a committee member. + /// @notice Internal function for checking whether an address is a executionMultisig member. /// @param _member The address to check. - /// @return Whether the address is a committee member. - function _isCommitteeMember(address _member) internal view returns (bool) { + /// @return Whether the address is a executionMultisig member. + function _isExecutionMultisigMember(address _member) internal view returns (bool) { return isListed(_member); } /// @notice Updates the plugin settings. /// @param _pluginSettings The new plugin settings. /// @dev The called must have the UPDATE_PLUGIN_SETTINGS_PERMISSION_ID permission. - function updatePluginSettings(PluginSettings memory _pluginSettings) public auth(UPDATE_PLUGIN_SETTINGS_PERMISSION_ID) { + function updatePluginSettings( + PluginSettings memory _pluginSettings + ) external auth(UPDATE_PLUGIN_SETTINGS_PERMISSION_ID) { _updatePluginSettings(_pluginSettings); } /// @notice Internal function for updating the plugin settings. /// @param _pluginSettings The new plugin settings. function _updatePluginSettings(PluginSettings memory _pluginSettings) private { + _guardPluginSettings(); + if (_pluginSettings.supportThreshold > RATIO_BASE - 1) { revert RatioOutOfBounds({ limit: RATIO_BASE - 1, actual: _pluginSettings.supportThreshold }); } - + // Require the minimum participation value to be in the interval [0, 10^6], because `>=` comparision is used in the participation criterion. if (_pluginSettings.minParticipation > RATIO_BASE) { revert RatioOutOfBounds({limit: RATIO_BASE, actual: _pluginSettings.minParticipation}); } - if (_pluginSettings.minDuration > 365 days) { - revert MinDurationOutOfBounds({limit: 365 days, actual: _pluginSettings.minDuration}); + if (_pluginSettings.minVoteDuration > 365 days) { + revert VoteDurationOutOfBounds({ + limit: 365 days, + actual: _pluginSettings.minVoteDuration + }); } - if (_pluginSettings.expirationTime > 365 days) { - revert ExpirationTimeOutOfBounds({limit: 365 days, actual: _pluginSettings.expirationTime}); + if (_pluginSettings.minVoteDuration < 60 minutes) { + revert VoteDurationOutOfBounds({ + limit: 60 minutes, + actual: _pluginSettings.minVoteDuration + }); } - if (lastPluginSettingsChange == uint64(block.number)) { - revert PluginSettingsUpdatedTooRecently({ - lastUpdate: lastPluginSettingsChange + if (_pluginSettings.minTallyDuration > 365 days) { + revert TallyDurationOutOfBounds({ + limit: 365 days, + actual: _pluginSettings.minTallyDuration + }); + } + + if (_pluginSettings.minTallyDuration < 60 minutes) { + revert TallyDurationOutOfBounds({ + limit: 60 minutes, + actual: _pluginSettings.minTallyDuration }); } @@ -280,14 +297,15 @@ contract VocdoniVoting is IVocdoniVoting, PluginUUPSUpgradeable, VocdoniProposal lastPluginSettingsChange = uint64(block.number); emit PluginSettingsUpdated({ - onlyCommitteeProposalCreation: _pluginSettings.onlyCommitteeProposalCreation, + onlyExecutionMultisigProposalCreation: _pluginSettings + .onlyExecutionMultisigProposalCreation, minTallyApprovals: _pluginSettings.minTallyApprovals, - minDuration: _pluginSettings.minDuration, - expirationTime: _pluginSettings.expirationTime, + minVoteDuration: _pluginSettings.minVoteDuration, + minTallyDuration: _pluginSettings.minTallyDuration, minParticipation: _pluginSettings.minParticipation, supportThreshold: _pluginSettings.supportThreshold, daoTokenAddress: _pluginSettings.daoTokenAddress, - censusStrategy: _pluginSettings.censusStrategy, + censusStrategyURI: _pluginSettings.censusStrategyURI, minProposerVotingPower: _pluginSettings.minProposerVotingPower }); } @@ -301,17 +319,22 @@ contract VocdoniVoting is IVocdoniVoting, PluginUUPSUpgradeable, VocdoniProposal /// @return allowFailureMap The allow failure map of the proposal. /// @return tally The tally of the proposal. /// @return actions The actions of the proposal. - function getProposal(uint256 _proposalId) public view returns ( - bool executed, - address[] memory approvers, - bytes32 vochainProposalId, - ProposalParameters memory parameters, - uint256 allowFailureMap, - uint256[][] memory tally, - IDAO.Action[] memory actions - - ) { - Proposal storage proposal = proposals[_proposalId]; + function getProposal( + uint256 _proposalId + ) + public + view + returns ( + bool executed, + address[] memory approvers, + bytes32 vochainProposalId, + ProposalParameters memory parameters, + uint256 allowFailureMap, + uint256[][] memory tally, + IDAO.Action[] memory actions + ) + { + Proposal memory proposal = proposals[_proposalId]; executed = proposal.executed; approvers = proposal.approvers; vochainProposalId = proposal.vochainProposalId; @@ -333,50 +356,63 @@ contract VocdoniVoting is IVocdoniVoting, PluginUUPSUpgradeable, VocdoniProposal ProposalParameters memory _parameters, IDAO.Action[] memory _actions ) external returns (uint256) { - _guardCommittee(); + _guardExecutionMultisig(); _guardPluginSettings(); PluginSettings memory _pluginSettings = pluginSettings; address sender = _msgSender(); - - if (_pluginSettings.onlyCommitteeProposalCreation && !_isCommitteeMember(sender)) { - revert OnlyCommittee({ - sender: sender - }); + + if ( + _pluginSettings.onlyExecutionMultisigProposalCreation && + !_isExecutionMultisigMember(sender) + ) { + revert OnlyExecutionMultisig({sender: sender}); } if (_pluginSettings.minProposerVotingPower != 0) { // Because of the checks in `VocdoniVotingSetup`, we can assume that `votingToken` is an [ERC-20](https://eips.ethereum.org/EIPS/eip-20) token. uint256 votes = IVotesUpgradeable(_pluginSettings.daoTokenAddress).getVotes(sender); uint256 balance = IERC20Upgradeable(_pluginSettings.daoTokenAddress).balanceOf(sender); - - if (votes < _pluginSettings.minProposerVotingPower && balance < _pluginSettings.minProposerVotingPower) { - revert NotEnoughVotingPower({ - required: _pluginSettings.minProposerVotingPower - }); + + if ( + votes < _pluginSettings.minProposerVotingPower && + balance < _pluginSettings.minProposerVotingPower + ) { + revert NotEnoughVotingPower({required: _pluginSettings.minProposerVotingPower}); } } - - (_parameters.startDate, - _parameters.endDate, - _parameters.expirationDate) = _validateProposalDates( + + ( + _parameters.startDate, + _parameters.voteEndDate, + _parameters.tallyEndDate + ) = _validateProposalDates( _parameters.startDate, - _parameters.endDate, - _parameters.expirationDate + _parameters.voteEndDate, + _parameters.tallyEndDate ); + if (_parameters.totalVotingPower == 0) { + revert InvalidTotalVotingPower({totalVotingPower: _parameters.totalVotingPower}); + } + uint256 _proposalId = _createProposalId(); Proposal storage proposal = proposals[_proposalId]; - + proposal.vochainProposalId = _vochainProposalId; proposal.parameters.startDate = _parameters.startDate; - proposal.parameters.endDate = _parameters.endDate; - proposal.parameters.expirationDate = _parameters.expirationDate; - proposal.parameters.censusBlock = _parameters.censusBlock; - proposal.allowFailureMap = _allowFailureMap; + proposal.parameters.voteEndDate = _parameters.voteEndDate; + proposal.parameters.tallyEndDate = _parameters.tallyEndDate; + proposal.parameters.totalVotingPower = _parameters.totalVotingPower; + proposal.parameters.censusURI = _parameters.censusURI; + proposal.parameters.censusRoot = _parameters.censusRoot; proposal.parameters.securityBlock = block.number.toUint64(); - for (uint16 i = 0; i < _actions.length; i++) { + proposal.allowFailureMap = _allowFailureMap; + for (uint256 i = 0; i < _actions.length; ) { proposal.actions.push(_actions[i]); + unchecked { + i++; + } } emit ProposalCreated( @@ -384,8 +420,8 @@ contract VocdoniVoting is IVocdoniVoting, PluginUUPSUpgradeable, VocdoniProposal _vochainProposalId, sender, _parameters.startDate, - _parameters.endDate, - _parameters.expirationDate, + _parameters.voteEndDate, + _parameters.tallyEndDate, _actions, _allowFailureMap ); @@ -396,33 +432,35 @@ contract VocdoniVoting is IVocdoniVoting, PluginUUPSUpgradeable, VocdoniProposal /// @inheritdoc IVocdoniVoting function setTally(uint256 _proposalId, uint256[][] memory _tally) public override { _setTally(_proposalId, _tally); - } + } /// @notice Internal function for setting the tally of a given proposal. /// @param _proposalId The ID of the proposal to set the tally of. /// @param _tally The tally to set. - /// @dev The caller must be a committee member if the ONLY_COMMITTEE_SET_TALLY flag is set. + /// @dev The caller must be a executionMultisig member. function _setTally(uint256 _proposalId, uint256[][] memory _tally) internal { - _guardCommittee(); + _guardExecutionMultisig(); _guardPluginSettings(); address sender = _msgSender(); - if (!_isCommitteeMember(sender)) { - revert OnlyCommittee({ - sender: sender - }); + if (!_isExecutionMultisigMember(sender)) { + revert OnlyExecutionMultisig({sender: sender}); } Proposal storage proposal = proposals[_proposalId]; + if (proposal.executed) { + revert ProposalAlreadyExecuted({proposalId: _proposalId}); + } + if (!_isProposalOnTallyPhase(proposal)) { revert ProposalNotInTallyPhase({ - endDate: proposal.parameters.endDate, - expirationDate: proposal.parameters.expirationDate, + voteEndDate: proposal.parameters.voteEndDate, + tallyEndDate: proposal.parameters.tallyEndDate, currentTimestamp: block.timestamp }); } - + // only supported tally is [[Yes, No, Abstain]] if (_tally.length != 1 || _tally[0].length != 3) { revert InvalidTally({tally: _tally}); @@ -438,16 +476,17 @@ contract VocdoniVoting is IVocdoniVoting, PluginUUPSUpgradeable, VocdoniProposal }); } // check if the new tally is different - if (_tally[0][0] == proposal.tally[0][0] && + if ( + _tally[0][0] == proposal.tally[0][0] && _tally[0][1] == proposal.tally[0][1] && _tally[0][2] == proposal.tally[0][2] ) { revert InvalidTally({tally: _tally}); } // reset approvers - delete proposal.approvers; + delete proposal.approvers; } - + proposal.tally = _tally; proposal.approvers.push(sender); @@ -456,35 +495,32 @@ contract VocdoniVoting is IVocdoniVoting, PluginUUPSUpgradeable, VocdoniProposal } /// @inheritdoc IVocdoniVoting - function approveTally(uint256 _proposalId, bool _tryExecution) public override { + function approveTally(uint256 _proposalId, bool _tryExecution) external override { return _approveTally(_proposalId, _tryExecution); } /// @notice Internal function for approving a proposal tally. /// @param _proposalId The ID of the proposal to approve. + /// @param _tryExecution Whether to try to execute the proposal after approving the tally. function _approveTally(uint256 _proposalId, bool _tryExecution) internal { - _guardCommittee(); + _guardExecutionMultisig(); _guardPluginSettings(); address sender = _msgSender(); - - if (!_isCommitteeMember(sender)) { - revert OnlyCommittee({ - sender: sender - }); + + if (!_isExecutionMultisigMember(sender)) { + revert OnlyExecutionMultisig({sender: sender}); } Proposal storage proposal = proposals[_proposalId]; - + // also checks that proposal is in tally phase, as the tally cannot be set otherwise if (proposal.tally.length == 0) { revert InvalidTally(proposal.tally); } if (_hasApprovedTally(proposal, sender)) { - revert TallyAlreadyApprovedBySender({ - sender: sender - }); + revert TallyAlreadyApprovedBySender({sender: sender}); } if (proposal.approvers.length >= pluginSettings.minTallyApprovals) { @@ -494,26 +530,34 @@ contract VocdoniVoting is IVocdoniVoting, PluginUUPSUpgradeable, VocdoniProposal }); } - // if committee changed since proposal creation, the proposal approvals of the previous committee members are not valid - if (proposal.parameters.securityBlock <= lastCommitteeChange) { + // if executionMultisig changed since proposal creation, the proposal approvals of the previous executionMultisig members are not valid + if (proposal.parameters.securityBlock < lastExecutionMultisigChange) { address[] memory newApprovers = new address[](0); - // newApprovers are the oldApprovers list without the non committee members at the current block - uint16 newApproversCount = 0; - for (uint16 i = 0; i < proposal.approvers.length; i++) { + // newApprovers are the oldApprovers list without the non executionMultisig members at the current block + uint8 newApproversCount = 0; + for (uint256 i = 0; i < proposal.approvers.length; ) { address oldApprover = proposal.approvers[i]; - if (_isCommitteeMember(oldApprover) && _hasApprovedTally(proposal, oldApprover)) { + if ( + _isExecutionMultisigMember(oldApprover) && + _hasApprovedTally(proposal, oldApprover) + ) { newApprovers[newApproversCount] = oldApprover; - newApproversCount++; + unchecked { + newApproversCount++; + } + } + unchecked { + i++; } } proposal.approvers = newApprovers; - proposal.parameters.securityBlock = lastCommitteeChange; + proposal.parameters.securityBlock = lastExecutionMultisigChange; } - + proposal.approvers.push(sender); emit TallyApproval({proposalId: _proposalId, approver: sender}); - + if (_tryExecution) { _checkTallyAndExecute(_proposalId); } @@ -521,7 +565,7 @@ contract VocdoniVoting is IVocdoniVoting, PluginUUPSUpgradeable, VocdoniProposal /// @inheritdoc IVocdoniVoting function executeProposal(uint256 _proposalId) public override { - _guardCommittee(); + _guardExecutionMultisig(); _guardPluginSettings(); _checkTallyAndExecute(_proposalId); @@ -529,14 +573,15 @@ contract VocdoniVoting is IVocdoniVoting, PluginUUPSUpgradeable, VocdoniProposal /// @notice Internal function to check if a proposal is on the tally phase. /// @param _proposal The proposal to check - function _isProposalOnTallyPhase(Proposal storage _proposal) internal view returns (bool) { + function _isProposalOnTallyPhase(Proposal memory _proposal) internal view returns (bool) { uint64 currentBlockTimestamp = uint64(block.timestamp); - /// [... startDate ............ endDate ............ expirationDate ...] - /// [............. Voting phase ....... Tally phase ...................] - if (_proposal.parameters.startDate < currentBlockTimestamp && - _proposal.parameters.endDate <= currentBlockTimestamp && - _proposal.parameters.expirationDate > currentBlockTimestamp && - !_proposal.executed) { + /// [... startDate ............ voteEndDate ............ tallyEndDate ...] + /// [............. Voting phase ............ Tally phase ................] + if ( + _proposal.parameters.startDate < currentBlockTimestamp && + _proposal.parameters.voteEndDate < currentBlockTimestamp - 1 && + _proposal.parameters.tallyEndDate > currentBlockTimestamp + ) { return true; } return false; @@ -545,57 +590,84 @@ contract VocdoniVoting is IVocdoniVoting, PluginUUPSUpgradeable, VocdoniProposal /// @notice Internal function to check the tally and execute a proposal if the tally /// number of YES votes is greater than the tally number of NO votes. function _checkTallyAndExecute(uint256 _proposalId) internal { - Proposal storage proposal = proposals[_proposalId]; + Proposal memory proposal = proposals[_proposalId]; + + if (proposal.executed) { + revert ProposalAlreadyExecuted({proposalId: _proposalId}); + } // also checks that proposal is in tally phase, as the tally cannot be set otherwise - if(proposal.tally.length == 0) { - revert InvalidTally({ - tally: proposal.tally + if (proposal.tally.length == 0) { + revert InvalidTally({tally: proposal.tally}); + } + + if (proposal.parameters.tallyEndDate < block.timestamp) { + revert ProposalNotInTallyPhase({ + voteEndDate: proposal.parameters.voteEndDate, + tallyEndDate: proposal.parameters.tallyEndDate, + currentTimestamp: block.timestamp }); } - + if (proposal.approvers.length < pluginSettings.minTallyApprovals) { revert NotEnoughApprovals({ minApprovals: pluginSettings.minTallyApprovals, actualApprovals: proposal.approvers.length.toUint16() }); } - - uint256 _currentParticipation = proposal.tally[0][0] + proposal.tally[0][1] + proposal.tally[0][2]; - if (_currentParticipation < pluginSettings.minParticipation) { + + uint256 currentVotingPower = proposal.tally[0][0] + + proposal.tally[0][1] + + proposal.tally[0][2]; + + uint256 minVotingPower = _applyRatioCeiled( + proposal.parameters.totalVotingPower, + pluginSettings.minParticipation + ); + + if (minVotingPower > currentVotingPower) { revert MinParticipationNotReached({ - currentParticipation: _currentParticipation, - minParticipation: pluginSettings.minParticipation + currentVotingPower: currentVotingPower, + minVotingPower: minVotingPower }); } - - uint256 _currentSupport = (RATIO_BASE - pluginSettings.supportThreshold) * proposal.tally[0][0]; - if (_currentSupport <= pluginSettings.supportThreshold * proposal.tally[0][1]) { + + uint256 yesRatioPart = (RATIO_BASE - pluginSettings.supportThreshold) * + proposal.tally[0][0]; + uint256 noRatioPart = pluginSettings.supportThreshold * proposal.tally[0][1]; + if (yesRatioPart <= noRatioPart) { revert SupportThresholdNotReached({ - currentSupport: _currentSupport, + currentSupport: _getCurrentSupport(proposal.tally), supportThreshold: pluginSettings.supportThreshold }); } - - proposal.executed = true; - _executeProposal( - dao(), - _proposalId, - proposal.actions, - proposal.allowFailureMap - ); + + proposals[_proposalId].executed = true; + _executeProposal(dao(), _proposalId, proposal.actions, proposal.allowFailureMap); } + /// @notice Internal function calculating the current support of a proposal + /// @param _tally The tally of the proposal. + /// @return The current support of the proposal. + function _getCurrentSupport(uint256[][] memory _tally) internal pure returns (uint256) { + return (_tally[0][0] * RATIO_BASE) / (_tally[0][0] + _tally[0][1]); + } - function _validateProposalDates(uint64 _startDate, uint64 _endDate, uint64 _expirationDate) - internal - view - virtual - returns( - uint64 startDate, - uint64 endDate, - uint64 expirationDate - ) { + /// @notice Internal function for validating the proposal dates. + /// If the start date is 0, it is set to the current block timestamp. + /// If the end vote date is 0, it is set to the start date + min vote duration. + /// If the tally end date is 0, it is set to the end date + min tally duration. + /// @param _startDate The start date of the proposal. + /// @param _voteEndDate The vote end date of the proposal. + /// @param _tallyEndDate The tally end date of the proposal. + /// @return startDate The validated start date. + /// @return voteEndDate The validated vote end date. + /// @return tallyEndDate The validated tally end date. + function _validateProposalDates( + uint64 _startDate, + uint64 _voteEndDate, + uint64 _tallyEndDate + ) internal view virtual returns (uint64 startDate, uint64 voteEndDate, uint64 tallyEndDate) { uint64 currentBlockTimestamp = block.timestamp.toUint64(); // check proposal start date and set it to the current block timestamp if it is 0 if (_startDate == 0) { @@ -603,39 +675,30 @@ contract VocdoniVoting is IVocdoniVoting, PluginUUPSUpgradeable, VocdoniProposal } else { startDate = _startDate; if (startDate < currentBlockTimestamp) { - revert InvalidStartDate({ - limit: currentBlockTimestamp, - actual: startDate - }); + revert InvalidStartDate({limit: currentBlockTimestamp, actual: startDate}); } } // check proposal end date and set it to the start date + min duration if it is 0 - uint64 earliestEndDate = startDate + pluginSettings.minDuration; - // Since `minDuration` is limited to 1 year, `startDate + minDuration` - // can only overflow if the `startDate` is after `type(uint64).max - minDuration`. + uint64 earliestVoteEndDate = startDate + pluginSettings.minVoteDuration; + // Since `minVoteDuration` is limited to 1 year, `startDate + minVoteDuration` + // can only overflow if the `startDate` is after `type(uint64).max - minVoteDuration`. // In this case, the proposal creation will revert and another date can be picked. - if (_endDate == 0) { - endDate = earliestEndDate; + if (_voteEndDate == 0) { + voteEndDate = earliestVoteEndDate; } else { - endDate = _endDate; - if (endDate < earliestEndDate) { - revert InvalidEndDate({ - limit: earliestEndDate, - actual: endDate - }); + voteEndDate = _voteEndDate; + if (voteEndDate < earliestVoteEndDate) { + revert InvalidVoteEndDate({limit: earliestVoteEndDate, actual: voteEndDate}); } } - uint64 maxExpirationDate = endDate + pluginSettings.expirationTime; - if (_expirationDate == 0 || _expirationDate <= endDate) { - expirationDate = maxExpirationDate; + uint64 earliestTallyEndDate = voteEndDate + pluginSettings.minTallyDuration; + if (_tallyEndDate == 0) { + tallyEndDate = earliestTallyEndDate; } else { - expirationDate = _expirationDate; - if (expirationDate > maxExpirationDate) { - revert InvalidExpirationDate({ - limit: maxExpirationDate, - actual: expirationDate - }); + tallyEndDate = _tallyEndDate; + if (tallyEndDate < earliestTallyEndDate) { + revert InvalidTallyEndDate({limit: earliestTallyEndDate, actual: tallyEndDate}); } } } @@ -646,45 +709,55 @@ contract VocdoniVoting is IVocdoniVoting, PluginUUPSUpgradeable, VocdoniProposal return pluginSettings; } - /// @notice Returns true if msg.sender has approved the given proposal tally + /// @notice Returns true if the provided _member has approved the given proposal tally /// @param _proposalId The ID of the proposal. /// @return Whether the msg.sender has approved the proposal tally. - function hasApprovedTally(uint256 _proposalId, address _member) public view returns (bool) { - Proposal storage proposal = proposals[_proposalId]; - return _hasApprovedTally(proposal, _member); + function hasApprovedTally(uint256 _proposalId, address _member) external view returns (bool) { + return _hasApprovedTally(proposals[_proposalId], _member); } - function _hasApprovedTally(Proposal memory _proposal, address _member) internal pure returns (bool) { - for (uint16 i = 0; i < _proposal.approvers.length; i++) { + /// @notice Internal function for checking if a member has approved a proposal tally. + /// @param _proposal The proposal to check. + /// @param _member The member to check. + /// @return Whether the member has approved the proposal tally. + function _hasApprovedTally( + Proposal memory _proposal, + address _member + ) internal pure returns (bool) { + for (uint256 i = 0; i < _proposal.approvers.length; ) { if (_proposal.approvers[i] == _member) { return true; } + unchecked { + i++; + } } return false; } /// @notice Guard checks that processes key updates are not executed in the same block - /// where the committee changed. - function _guardCommittee() internal view { - if (lastCommitteeChange == uint64(block.number)) { - revert CommitteeUpdatedTooRecently({ - lastUpdate: lastCommitteeChange - }); + /// where the executionMultisig changed. + function _guardExecutionMultisig() internal view { + if (lastExecutionMultisigChange == uint64(block.number)) { + revert ExecutionMultisigUpdatedTooRecently({lastUpdate: lastExecutionMultisigChange}); } } - /// @notice Guard checks that processes key updates are not executed in the same block + /// @notice Guard checks that processes key updates are not executed in the same block /// where the plugin settings changed. function _guardPluginSettings() internal view { if (lastPluginSettingsChange == uint64(block.number)) { - revert PluginSettingsUpdatedTooRecently({ - lastUpdate: lastPluginSettingsChange - }); + revert PluginSettingsUpdatedTooRecently({lastUpdate: lastPluginSettingsChange}); } } - /// @notice This empty reserved space is put in place to allow future versions to add new variables - /// without shifting down storage in the inheritance chain (see [OpenZeppelin's guide about storage gaps] - /// (https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps)). - uint256[49] private __gap; + // get last executionMultisig change block number + function getLastExecutionMultisigChange() external view returns (uint64) { + return lastExecutionMultisigChange; + } + + // get last plugin settings change block number + function getLastPluginSettingsChange() external view returns (uint64) { + return lastPluginSettingsChange; + } } diff --git a/packages/contracts/src/VocdoniVotingSetup.sol b/packages/contracts/src/VocdoniVotingSetup.sol index 03b01426..1c28994c 100644 --- a/packages/contracts/src/VocdoniVotingSetup.sol +++ b/packages/contracts/src/VocdoniVotingSetup.sol @@ -28,7 +28,7 @@ contract VocdoniVotingSetup is PluginSetup { /// @notice The address of `VocdoniVoting` plugin logic contract to be used in creating proxy contracts. VocdoniVoting private immutable vocdoniVoting; - /// @notice The address of the `GovernanceERC20` base contract. + /// @notice The address of the `GovernanceERC20` base contract. address public immutable governanceERC20Base; /// @notice The address of the `GovernanceWrappedERC20` base contract. @@ -60,26 +60,25 @@ contract VocdoniVotingSetup is PluginSetup { constructor( GovernanceERC20 _governanceERC20Base, GovernanceWrappedERC20 _governanceWrappedERC20Base - ) { + ) { governanceERC20Base = address(_governanceERC20Base); governanceWrappedERC20Base = address(_governanceWrappedERC20Base); vocdoniVoting = new VocdoniVoting(); } /// @inheritdoc IPluginSetup - function prepareInstallation(address _dao, bytes calldata _data) - external - returns (address plugin, PreparedSetupData memory preparedSetupData) - { + function prepareInstallation( + address _dao, + bytes calldata _data + ) external returns (address plugin, PreparedSetupData memory preparedSetupData) { // Decode `_data` to extract the params needed for deploying and initializing `VocdoniVoting` plugin. ( - address[] memory committee, + address[] memory executionMultisig, VocdoniVoting.PluginSettings memory pluginSettings, TokenSettings memory tokenSettings, // only used for GovernanceERC20(token is not passed) GovernanceERC20.MintSettings memory mintSettings - ) = - abi.decode( + ) = abi.decode( _data, ( address[], @@ -87,7 +86,7 @@ contract VocdoniVotingSetup is PluginSetup { TokenSettings, GovernanceERC20.MintSettings ) - ); + ); address token = tokenSettings.addr; @@ -142,11 +141,14 @@ contract VocdoniVotingSetup is PluginSetup { // Prepare and Deploy the plugin proxy. plugin = createERC1967Proxy( address(vocdoniVoting), - abi.encodeWithSelector(VocdoniVoting.initialize.selector, _dao, committee, pluginSettings) + abi.encodeCall( + VocdoniVoting.initialize, + (IDAO(_dao), executionMultisig, pluginSettings) + ) ); // Prepare permissions - PermissionLib.MultiTargetPermission[] + PermissionLib.MultiTargetPermission[] memory permissions = new PermissionLib.MultiTargetPermission[]( tokenSettings.addr != address(0) ? 4 : 5 ); @@ -166,7 +168,7 @@ contract VocdoniVotingSetup is PluginSetup { plugin, _dao, PermissionLib.NO_CONDITION, - vocdoniVoting.UPDATE_PLUGIN_COMMITTEE_PERMISSION_ID() + vocdoniVoting.UPDATE_PLUGIN_EXECUTION_MULTISIG_PERMISSION_ID() ); permissions[2] = PermissionLib.MultiTargetPermission( @@ -203,12 +205,11 @@ contract VocdoniVotingSetup is PluginSetup { } /// @inheritdoc IPluginSetup - function prepareUninstallation(address _dao, SetupPayload calldata _payload) - external - view - returns (PermissionLib.MultiTargetPermission[] memory permissions) - { - // Prepare permissions. + function prepareUninstallation( + address _dao, + SetupPayload calldata _payload + ) external view returns (PermissionLib.MultiTargetPermission[] memory permissions) { + // Prepare permissions. uint256 helperLength = _payload.currentHelpers.length; if (helperLength != 1) { revert WrongHelpersArrayLength({length: helperLength}); @@ -233,12 +234,12 @@ contract VocdoniVotingSetup is PluginSetup { vocdoniVoting.UPDATE_PLUGIN_SETTINGS_PERMISSION_ID() ); - permissions[1] = PermissionLib.MultiTargetPermission( + permissions[1] = PermissionLib.MultiTargetPermission( PermissionLib.Operation.Revoke, _payload.plugin, _dao, PermissionLib.NO_CONDITION, - vocdoniVoting.UPDATE_PLUGIN_COMMITTEE_PERMISSION_ID() + vocdoniVoting.UPDATE_PLUGIN_EXECUTION_MULTISIG_PERMISSION_ID() ); permissions[2] = PermissionLib.MultiTargetPermission( @@ -276,7 +277,7 @@ contract VocdoniVotingSetup is PluginSetup { return address(vocdoniVoting); } - /// @notice Retrieves the interface identifiers supported by the token contract. + /// @notice Retrieves the interface identifiers supported by the token contract. /// @dev It is crucial to verify if the provided token address represents a valid contract before using the below. /// @param token The token address function _getTokenInterfaceIds(address token) private view returns (bool[] memory) { @@ -292,7 +293,7 @@ contract VocdoniVotingSetup is PluginSetup { /// @param token The token address function _isERC20(address token) private view returns (bool) { (bool success, bytes memory data) = token.staticcall( - abi.encodeWithSelector(IERC20Upgradeable.balanceOf.selector, address(this)) + abi.encodeCall(IERC20Upgradeable.balanceOf, (address(this))) ); return success && data.length == 0x20; } diff --git a/packages/contracts/src/architecture.md b/packages/contracts/src/architecture.md deleted file mode 100644 index 249fbbf3..00000000 --- a/packages/contracts/src/architecture.md +++ /dev/null @@ -1,3 +0,0 @@ -# Contracts Architecture - -## Overview diff --git a/packages/contracts/src/build-metadata.json b/packages/contracts/src/build-metadata.json index 5e9ee4d8..3cdf660c 100644 --- a/packages/contracts/src/build-metadata.json +++ b/packages/contracts/src/build-metadata.json @@ -1,139 +1,129 @@ { - "ui": {}, - "pluginSetup": { - "prepareInstallation": { - "description": "The information required for the installation.", - "inputs": [ - { - "internalType": "address[]", - "name": "committee", - "type": "address[]", - "description": "The addresses of the initial committee members to be added." - }, - { - "components": [ - { - "internalType": "bool", - "name": "onlyCommitteeProposalCreation", - "type": "bool", - "description": "Whether only committee members can create proposals" - }, - { - "internalType": "uint16", - "name": "minTallyApprovals", - "type": "uint16", - "description": "The minimal number of approvals required for a tally to be considered accepted" - }, - { - "internalType": "uint32", - "name": "minParticipation", - "type": "uint32", - "description": "The minimal participation required for a proposal to pass" - }, - { - "internalType": "uint32", - "name": "supportThreshold", - "type": "uint32", - "description": "The minimal support required for a proposal to pass" - }, - { - "internalType": "uint64", - "name": "minDuration", - "type": "uint64", - "description": "The minimal duration of the tally phase of a voting process" - }, - { - "internalType": "uint64", - "name": "expirationTime", - "type": "uint64", - "description": "The expiration time of the voting process. Cannot be executed afterwards" - }, - { - "internalType": "address", - "name": "daoTokenAddress", - "type": "address", - "description": "The address of the DAO token" - }, - { - "internalType": "uint256", - "name": "minProposerVotingPower", - "type": "uint256", - "description": "The minimum voting power required for a voter to be able to create a voting process" - }, - { - "internalType": "string", - "name": "censusStrategy", - "type": "string", - "description": "The census strategy to be used for the voting process" - } - ], - "internalType": "struct VocdoniVoting.PluginSettings", - "name": "pluginSettingsSettings", - "type": "tuple", - "description": "The inital vocdoniVoting settings." - }, - { - "components": [ - { - "internalType": "address", - "name": "token", - "type": "address", - "description": "The token address. If this is `address(0)`, a new `GovernanceERC20` token is deployed. If not, the existing token is wrapped as an `GovernanceWrappedERC20`." - }, - { - "internalType": "string", - "name": "name", - "type": "string", - "description": "The token name. This parameter is only relevant if the token address is `address(0)`." - }, - { - "internalType": "string", - "name": "symbol", - "type": "string", - "description": "The token symbol. This parameter is only relevant if the token address is `address(0)`." - } - ], - "internalType": "struct TokenVotingSetup.TokenSettings", - "name": "tokenSettings", - "type": "tuple", - "description": "The token settings that either specify an existing ERC-20 token (`token = address(0)`) or the name and symbol of a new `GovernanceERC20` token to be created." - }, - { - "components": [ - { - "internalType": "address[]", - "name": "receivers", - "type": "address[]", - "description": "The receivers of the tokens." - }, - { - "internalType": "uint256[]", - "name": "amounts", - "type": "uint256[]", - "description": "The amounts of tokens to be minted for each receiver." - } - ], - "internalType": "struct GovernanceERC20.MintSettings", - "name": "mintSettings", - "type": "tuple", - "description": "The token mint settings struct containing the `receivers` and `amounts`." - } - ], - "prepareUpdate": { - "1": { - "description": "No input is required for the update.", - "inputs": [] - }, - "2": { - "description": "No input is required for the update.", - "inputs": [] - } + "ui": {}, + "pluginSetup": { + "prepareInstallation": { + "description": "The information required for the installation.", + "inputs": [ + { + "internalType": "address[]", + "name": "executionMultisig", + "type": "address[]", + "description": "The addresses of the initial executionMultisig members to be added." }, - "prepareUninstallation": { - "description": "No input is required for the uninstallation.", - "inputs": [] + { + "components": [ + { + "internalType": "bool", + "name": "onlyExecutionMultisigProposalCreation", + "type": "bool", + "description": "Whether only execution multisig members can create proposals" + }, + { + "internalType": "uint16", + "name": "minTallyApprovals", + "type": "uint16", + "description": "The minimal number of approvals required for a tally to be considered accepted" + }, + { + "internalType": "uint32", + "name": "minParticipation", + "type": "uint32", + "description": "The minimal participation required for a proposal to pass" + }, + { + "internalType": "uint32", + "name": "supportThreshold", + "type": "uint32", + "description": "The minimal support required for a proposal to pass" + }, + { + "internalType": "uint64", + "name": "minVoteDuration", + "type": "uint64", + "description": "The minimal duration of the tally phase of a voting process" + }, + { + "internalType": "uint64", + "name": "minTallyDuration", + "type": "uint64", + "description": "The expiration time of the voting process. Cannot be executed afterwards" + }, + { + "internalType": "address", + "name": "daoTokenAddress", + "type": "address", + "description": "The address of the DAO token" + }, + { + "internalType": "uint256", + "name": "minProposerVotingPower", + "type": "uint256", + "description": "The minimum voting power required for a voter to be able to create a voting process" + }, + { + "internalType": "string", + "name": "censusStrategyURI", + "type": "string", + "description": "The census strategy to be used for the voting process" + } + ], + "internalType": "struct VocdoniVoting.PluginSettings", + "name": "pluginSettingsSettings", + "type": "tuple", + "description": "The inital vocdoniVoting settings." + }, + { + "components": [ + { + "internalType": "address", + "name": "addr", + "type": "address", + "description": "The token address. If this is `address(0)`, a new `GovernanceERC20` token is deployed. If not, the existing token is wrapped as an `GovernanceWrappedERC20`." + }, + { + "internalType": "string", + "name": "name", + "type": "string", + "description": "The token name. This parameter is only relevant if the token address is `address(0)`." + }, + { + "internalType": "string", + "name": "symbol", + "type": "string", + "description": "The token symbol. This parameter is only relevant if the token address is `address(0)`." + } + ], + "internalType": "struct TokenVotingSetup.TokenSettings", + "name": "tokenSettings", + "type": "tuple", + "description": "The token settings that either specify an existing ERC-20 token (`token = address(0)`) or the name and symbol of a new `GovernanceERC20` token to be created." + }, + { + "components": [ + { + "internalType": "address[]", + "name": "receivers", + "type": "address[]", + "description": "The receivers of the tokens." + }, + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]", + "description": "The amounts of tokens to be minted for each receiver." + } + ], + "internalType": "struct GovernanceERC20.MintSettings", + "name": "mintSettings", + "type": "tuple", + "description": "The token mint settings struct containing the `receivers` and `amounts`." } + ], + "prepareUpdate": {}, + "prepareUninstallation": { + "description": "No input is required for the uninstallation.", + "inputs": [] } } } - \ No newline at end of file +} diff --git a/packages/contracts/src/dependencies/dependencies.sol b/packages/contracts/src/dependencies/dependencies.sol index 6ee46f1f..1d5770b4 100644 --- a/packages/contracts/src/dependencies/dependencies.sol +++ b/packages/contracts/src/dependencies/dependencies.sol @@ -1,90 +1,26 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity ^0.8.17; -import { IDAO } from "@aragon/osx/core/dao/IDAO.sol"; -import { DAO } from "@aragon/osx/core/dao/DAO.sol"; -import { DAOFactory } from "@aragon/osx/framework/dao/DAOFactory.sol"; -import { DAORegistry } from "@aragon/osx/framework/dao/DAORegistry.sol"; +import {IDAO} from "@aragon/osx/core/dao/IDAO.sol"; +import {DAO} from "@aragon/osx/core/dao/DAO.sol"; +import {DAOFactory} from "@aragon/osx/framework/dao/DAOFactory.sol"; +import {DAORegistry} from "@aragon/osx/framework/dao/DAORegistry.sol"; -import { PluginRepoFactory } from "@aragon/osx/framework/plugin/repo/PluginRepoFactory.sol"; -import { PluginRepoRegistry } from "@aragon/osx/framework/plugin/repo/PluginRepoRegistry.sol"; -import { PluginSetupProcessor } from "@aragon/osx/framework/plugin/setup/PluginSetupProcessor.sol"; -import { InterfaceBasedRegistry } from "@aragon/osx/framework/utils/InterfaceBasedRegistry.sol"; -import { PluginUUPSUpgradeable } from "@aragon/osx/core/plugin/PluginUUPSUpgradeable.sol"; +import {PluginRepoFactory} from "@aragon/osx/framework/plugin/repo/PluginRepoFactory.sol"; +import {PluginRepoRegistry} from "@aragon/osx/framework/plugin/repo/PluginRepoRegistry.sol"; +import {PluginSetupProcessor} from "@aragon/osx/framework/plugin/setup/PluginSetupProcessor.sol"; +import {InterfaceBasedRegistry} from "@aragon/osx/framework/utils/InterfaceBasedRegistry.sol"; +import {PluginUUPSUpgradeable} from "@aragon/osx/core/plugin/PluginUUPSUpgradeable.sol"; import {GovernanceERC20} from "@aragon/osx/token/ERC20/governance/GovernanceERC20.sol"; import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; import {IVotesUpgradeable} from "@openzeppelin/contracts-upgradeable/governance/utils/IVotesUpgradeable.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import { ENSSubdomainRegistrar } from "@aragon/osx/framework/utils/ens/ENSSubdomainRegistrar.sol"; -import { ENSRegistry } from "@ensdomains/ens-contracts/contracts/registry/ENSRegistry.sol"; -import { FIFSRegistrar } from "@ensdomains/ens-contracts/contracts/registry/FIFSRegistrar.sol"; -import { PublicResolver } from "@ensdomains/ens-contracts/contracts/resolvers/PublicResolver.sol"; - - -contract InterfaceBasedRegistryMock is InterfaceBasedRegistry { - bytes32 public constant REGISTER_PERMISSION_ID = keccak256("REGISTER_PERMISSION"); - - event Registered(address); - - function initialize(IDAO _dao, bytes4 targetInterface) external initializer { - __InterfaceBasedRegistry_init(_dao, targetInterface); - } - - function register(address registrant) external auth(REGISTER_PERMISSION_ID) { - _register(registrant); - - emit Registered(registrant); - } -} - -contract PluginUUPSUpgradeableV1Mock is PluginUUPSUpgradeable { - uint256 public state1; - - function initialize(IDAO _dao) external initializer { - __PluginUUPSUpgradeable_init(_dao); - state1 = 1; - } -} - -contract PluginUUPSUpgradeableV2Mock is PluginUUPSUpgradeable { - uint256 public state1; - uint256 public state2; - - function initialize(IDAO _dao) external reinitializer(2) { - __PluginUUPSUpgradeable_init(_dao); - state1 = 1; - state2 = 2; - } - - function initializeV1toV2() external reinitializer(2) { - state2 = 2; - } -} - -contract PluginUUPSUpgradeableV3Mock is PluginUUPSUpgradeable { - uint256 public state1; - uint256 public state2; - uint256 public state3; - - function initialize(IDAO _dao) external reinitializer(3) { - __PluginUUPSUpgradeable_init(_dao); - state1 = 1; - state2 = 2; - state3 = 3; - } - - function initializeV1toV3() external reinitializer(3) { - state2 = 2; - state3 = 3; - } - - function initializeV2toV3() external reinitializer(3) { - state3 = 3; - } -} - +import {ENSSubdomainRegistrar} from "@aragon/osx/framework/utils/ens/ENSSubdomainRegistrar.sol"; +import {ENSRegistry} from "@ensdomains/ens-contracts/contracts/registry/ENSRegistry.sol"; +import {FIFSRegistrar} from "@ensdomains/ens-contracts/contracts/registry/FIFSRegistrar.sol"; +import {PublicResolver} from "@ensdomains/ens-contracts/contracts/resolvers/PublicResolver.sol"; contract GovernanceERC20Mock is GovernanceERC20 { constructor( @@ -111,4 +47,4 @@ contract GovernanceERC20Mock is GovernanceERC20 { _burn(to, old - amount); } } -} \ No newline at end of file +} diff --git a/packages/contracts/src/release-metadata.json b/packages/contracts/src/release-metadata.json index a47dc61a..1b1d9648 100644 --- a/packages/contracts/src/release-metadata.json +++ b/packages/contracts/src/release-metadata.json @@ -1,5 +1,5 @@ { - "name": "VocdoniVoting", - "description": "", - "images": {} + "name": "VocdoniVoting", + "description": "", + "images": {} } diff --git a/packages/contracts/test/utils/abi.ts b/packages/contracts/test/utils/abi.ts index bd8cad73..c5486f32 100644 --- a/packages/contracts/test/utils/abi.ts +++ b/packages/contracts/test/utils/abi.ts @@ -1,32 +1,31 @@ -import { HardhatRuntimeEnvironment } from 'hardhat/types'; +import {HardhatRuntimeEnvironment} from 'hardhat/types'; export async function getMergedABI( - hre: HardhatRuntimeEnvironment, - primaryABI: string, - secondaryABIs: string[] - ): Promise<{abi: any; bytecode: any}> { - const primaryArtifact = await hre.artifacts.readArtifact(primaryABI); - - const secondariesArtifacts = secondaryABIs.map( - async name => await hre.artifacts.readArtifact(name) - ); - - const _merged = [...primaryArtifact.abi]; - - for (let i = 0; i < secondariesArtifacts.length; i++) { - const artifact = await secondariesArtifacts[i]; - _merged.push(...artifact.abi.filter((f: any) => f.type === 'event')); - } - - // remove duplicated events - const merged = _merged.filter( - (value, index, self) => - index === self.findIndex(event => event.name === value.name) - ); - - return { - abi: merged, - bytecode: primaryArtifact.bytecode, - }; + hre: HardhatRuntimeEnvironment, + primaryABI: string, + secondaryABIs: string[] +): Promise<{abi: any; bytecode: any}> { + const primaryArtifact = await hre.artifacts.readArtifact(primaryABI); + + const secondariesArtifacts = secondaryABIs.map( + async name => await hre.artifacts.readArtifact(name) + ); + + const _merged = [...primaryArtifact.abi]; + + for (let i = 0; i < secondariesArtifacts.length; i++) { + const artifact = await secondariesArtifacts[i]; + _merged.push(...artifact.abi.filter((f: any) => f.type === 'event')); } - \ No newline at end of file + + // remove duplicated events + const merged = _merged.filter( + (value, index, self) => + index === self.findIndex(event => event.name === value.name) + ); + + return { + abi: merged, + bytecode: primaryArtifact.bytecode, + }; +} diff --git a/packages/contracts/test/utils/dao.ts b/packages/contracts/test/utils/dao.ts index 5f525fce..0d51cb8d 100644 --- a/packages/contracts/test/utils/dao.ts +++ b/packages/contracts/test/utils/dao.ts @@ -1,13 +1,9 @@ import {ethers} from 'hardhat'; -import { - DAO, - DAO__factory, -} from '../../typechain'; +import {DAO, DAO__factory} from '../../typechain'; import {SignerWithAddress} from '@nomiclabs/hardhat-ethers/signers'; import {ContractFactory} from 'ethers'; import {upgrades} from 'hardhat'; - export const ZERO_BYTES32 = '0x0000000000000000000000000000000000000000000000000000000000000000'; export const daoExampleURI = 'https://example.com'; diff --git a/packages/contracts/test/utils/ens.ts b/packages/contracts/test/utils/ens.ts index e208e830..bac6ab47 100644 --- a/packages/contracts/test/utils/ens.ts +++ b/packages/contracts/test/utils/ens.ts @@ -3,7 +3,7 @@ import {ENSRegistry__factory} from '../../typechain'; import ensRegistryArtifact from '../../artifacts/@ensdomains/ens-contracts/contracts/registry/ENSRegistry.sol/ENSRegistry.json'; import publicResolverArtifact from '../../artifacts/@ensdomains/ens-contracts/contracts/resolvers/PublicResolver.sol/PublicResolver.json'; -import { HardhatRuntimeEnvironment } from 'hardhat/types'; +import {HardhatRuntimeEnvironment} from 'hardhat/types'; export function ensLabelHash(label: string): string { return ethers.utils.id(label); @@ -13,7 +13,10 @@ export function ensDomainHash(name: string): string { return ethers.utils.namehash(name); } -export async function setupENS(domains: string[], hre: HardhatRuntimeEnvironment): Promise { +export async function setupENS( + domains: string[], + hre: HardhatRuntimeEnvironment +): Promise { const {deployments, ethers} = hre; const {deploy} = deployments; const [deployer] = await ethers.getSigners(); diff --git a/packages/contracts/test/utils/event.ts b/packages/contracts/test/utils/event.ts index 05627b41..4390073a 100644 --- a/packages/contracts/test/utils/event.ts +++ b/packages/contracts/test/utils/event.ts @@ -65,10 +65,10 @@ export const MEMBERSHIP_EVENTS = { }; export const VOCDONI_EVENTS = { - COMMITTEE_MEMBERS_ADDED: 'CommitteeMembersAdded', - COMMITTEE_MEMBERS_REMOVED: 'CommitteeMembersRemoved', + EXECUTION_MULTISIG_MEMBERS_ADDED: 'ExecutionMultisigMembersAdded', + EXECUTION_MULTISIG_MEMBERS_REMOVED: 'ExecutionMultisigMembersRemoved', TALLY_SET: 'TallySet', - TALLY_APPROVED: 'TallyApproved', + TALLY_APPROVAL: 'TallyApproval', PLUGIN_SETTINGS_UPDATED: 'PluginSettingsUpdated', PROPOSAL_CREATED: 'ProposalCreated', PROPOSAL_EXECUTED: 'ProposalExecuted', diff --git a/packages/contracts/test/utils/helpers.ts b/packages/contracts/test/utils/helpers.ts index 09f39c77..2e8e2cd3 100644 --- a/packages/contracts/test/utils/helpers.ts +++ b/packages/contracts/test/utils/helpers.ts @@ -1,164 +1,163 @@ -import { HardhatRuntimeEnvironment } from "hardhat/types"; -import { promises as fs } from "fs"; -import { Permission } from "./types"; -import { Operation } from "./types"; -import { Contract } from "ethers"; -import { ethers } from "hardhat"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; -import { ENSRegistry__factory } from "../../typechain"; -import { getMergedABI } from "./abi"; -import { findEvent } from "./event"; -import { PluginRepoRegisteredEvent } from "../../typechain/PluginRepoRegistry"; -import { VersionTag } from "./types"; -import { PluginRepo__factory } from "../../typechain"; -import { VersionCreatedEvent } from "../../typechain/PluginRepo"; +import {HardhatRuntimeEnvironment} from 'hardhat/types'; +import {promises as fs} from 'fs'; +import {Permission} from '../../utils/types'; +import {Operation} from '../../utils/types'; +import {Contract} from 'ethers'; +import {ethers} from 'hardhat'; +import {SignerWithAddress} from '@nomiclabs/hardhat-ethers/signers'; +import {ENSRegistry__factory} from '../../typechain'; +import {getMergedABI} from './abi'; +import {findEvent} from './event'; +import {PluginRepoRegisteredEvent} from '../../typechain/PluginRepoRegistry'; +import {VersionTag} from '../../utils/types'; +import {PluginRepo__factory} from '../../typechain'; +import {VersionCreatedEvent} from '../../typechain/PluginRepo'; import IPFS from 'ipfs-http-client'; import {defaultAbiCoder, keccak256} from 'ethers/lib/utils'; import {ethers as ethersDirect} from 'ethers'; - export async function getContractAddress( - contractName: string, - hre: HardhatRuntimeEnvironment - ): Promise { - const {deployments, network} = hre; - - let networkName = network.name; - - try { - const contract = await deployments.get(contractName); - if (contract) { - return contract.address; - } - } catch (e) {} - - return getActiveContractAddressInNetwork(contractName, networkName); + contractName: string, + hre: HardhatRuntimeEnvironment +): Promise { + const {deployments, network} = hre; + + let networkName = network.name; + + try { + const contract = await deployments.get(contractName); + if (contract) { + return contract.address; + } + } catch (e) {} + + return getActiveContractAddressInNetwork(contractName, networkName); +} + +export async function getActiveContractAddressInNetwork( + contractName: string, + networkName: string +): Promise { + const activeContracts = await getActiveContractsJSON(); + try { + return activeContracts[networkName][contractName]; + } catch (e) { + console.error(e); + return ''; } - - export async function getActiveContractAddressInNetwork( - contractName: string, - networkName: string - ): Promise { - const activeContracts = await getActiveContractsJSON(); - try { - return activeContracts[networkName][contractName]; - } catch (e) { - console.error(e); - return ''; +} + +export async function getActiveContractsJSON(): Promise<{ + [index: string]: {[index: string]: string}; +}> { + const repoPath = process.env.GITHUB_WORKSPACE || '../../'; + const activeContractsFile = await fs.readFile( + `${repoPath}/active_contracts.json` + ); + const activeContracts = JSON.parse(activeContractsFile.toString()); + return activeContracts; +} + +export async function managePermissions( + permissionManagerContract: Contract, + permissions: Permission[] +): Promise { + // filtering permission to only apply those that are needed + const items: Permission[] = []; + for (const permission of permissions) { + if (await isPermissionSetCorrectly(permissionManagerContract, permission)) { + continue; } + items.push(permission); } - export async function getActiveContractsJSON(): Promise<{ - [index: string]: {[index: string]: string}; - }> { - const repoPath = process.env.GITHUB_WORKSPACE || '../../'; - const activeContractsFile = await fs.readFile( - `${repoPath}/active_contracts.json` - ); - const activeContracts = JSON.parse(activeContractsFile.toString()); - return activeContracts; + if (items.length === 0) { + console.log(`Contract call skipped. No permissions to set...`); + return; } - export async function managePermissions( - permissionManagerContract: Contract, - permissions: Permission[] - ): Promise { - // filtering permission to only apply those that are needed - const items: Permission[] = []; - for (const permission of permissions) { - if (await isPermissionSetCorrectly(permissionManagerContract, permission)) { - continue; - } - items.push(permission); - } - - if (items.length === 0) { - console.log(`Contract call skipped. No permissions to set...`); - return; - } - + console.log( + `Setting ${items.length} permissions. Skipped ${ + permissions.length - items.length + }` + ); + const tx = await permissionManagerContract.applyMultiTargetPermissions( + items.map(item => [ + item.operation, + item.where.address, + item.who.address, + item.condition || ethers.constants.AddressZero, + ethers.utils.id(item.permission), + ]) + ); + console.log(`Set permissions with ${tx.hash}. Waiting for confirmation...`); + await tx.wait(); + + items.forEach(permission => { console.log( - `Setting ${items.length} permissions. Skipped ${ - permissions.length - items.length - }` + `${ + permission.operation === Operation.Grant ? 'Granted' : 'Revoked' + } the ${permission.permission} of (${permission.where.name}: ${ + permission.where.address + }) for (${permission.who.name}: ${permission.who.address}), see (tx: ${ + tx.hash + })` ); - const tx = await permissionManagerContract.applyMultiTargetPermissions( - items.map(item => [ - item.operation, - item.where.address, - item.who.address, - item.condition || ethers.constants.AddressZero, - ethers.utils.id(item.permission), - ]) - ); - console.log(`Set permissions with ${tx.hash}. Waiting for confirmation...`); - await tx.wait(); - - items.forEach(permission => { - console.log( - `${ - permission.operation === Operation.Grant ? 'Granted' : 'Revoked' - } the ${permission.permission} of (${permission.where.name}: ${ - permission.where.address - }) for (${permission.who.name}: ${permission.who.address}), see (tx: ${ - tx.hash - })` - ); - }); + }); +} + +export async function isPermissionSetCorrectly( + permissionManagerContract: Contract, + {operation, where, who, permission, data = '0x'}: Permission +): Promise { + const permissionId = ethers.utils.id(permission); + const isGranted = await permissionManagerContract.isGranted( + where.address, + who.address, + permissionId, + data + ); + if (!isGranted && operation === Operation.Grant) { + return false; } - export async function isPermissionSetCorrectly( - permissionManagerContract: Contract, - {operation, where, who, permission, data = '0x'}: Permission - ): Promise { - const permissionId = ethers.utils.id(permission); - const isGranted = await permissionManagerContract.isGranted( - where.address, - who.address, - permissionId, - data - ); - if (!isGranted && operation === Operation.Grant) { - return false; - } - - if (isGranted && operation === Operation.Revoke) { - return false; - } - return true; + if (isGranted && operation === Operation.Revoke) { + return false; } + return true; +} - export const DAO_PERMISSIONS = [ - 'ROOT_PERMISSION', - 'UPGRADE_DAO_PERMISSION', - 'SET_SIGNATURE_VALIDATOR_PERMISSION', - 'SET_TRUSTED_FORWARDER_PERMISSION', - 'SET_METADATA_PERMISSION', - 'REGISTER_STANDARD_CALLBACK_PERMISSION', - ]; - - export async function checkPermission( - permissionManagerContract: Contract, - permission: Permission - ) { - const checkStatus = await isPermissionSetCorrectly( - permissionManagerContract, - permission - ); - if (!checkStatus) { - const {who, where, operation} = permission; - if (operation === Operation.Grant) { - throw new Error( - `(${who.name}: ${who.address}) doesn't have ${permission.permission} on (${where.name}: ${where.address}) in ${permissionManagerContract.address}` - ); - } +export const DAO_PERMISSIONS = [ + 'ROOT_PERMISSION', + 'UPGRADE_DAO_PERMISSION', + 'SET_SIGNATURE_VALIDATOR_PERMISSION', + 'SET_TRUSTED_FORWARDER_PERMISSION', + 'SET_METADATA_PERMISSION', + 'REGISTER_STANDARD_CALLBACK_PERMISSION', +]; + +export async function checkPermission( + permissionManagerContract: Contract, + permission: Permission +) { + const checkStatus = await isPermissionSetCorrectly( + permissionManagerContract, + permission + ); + if (!checkStatus) { + const {who, where, operation} = permission; + if (operation === Operation.Grant) { throw new Error( - `(${who.name}: ${who.address}) has ${permission.permission} on (${where.name}: ${where.address}) in ${permissionManagerContract.address}` + `(${who.name}: ${who.address}) doesn't have ${permission.permission} on (${where.name}: ${where.address}) in ${permissionManagerContract.address}` ); } + throw new Error( + `(${who.name}: ${who.address}) has ${permission.permission} on (${where.name}: ${where.address}) in ${permissionManagerContract.address}` + ); } +} - // TODO: Add support for L2 such as Arbitrum. (https://discuss.ens.domains/t/register-using-layer-2/688) +// TODO: Add support for L2 such as Arbitrum. (https://discuss.ens.domains/t/register-using-layer-2/688) // Make sure you own the ENS set in the {{NETWORK}}_ENS_DOMAIN variable in .env export const ENS_ADDRESSES: {[key: string]: string} = { mainnet: '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e', @@ -289,7 +288,6 @@ export async function isENSDomainRegistered( return ensRegistryContract.recordExists(ethers.utils.namehash(domain)); } - export async function createPluginRepo( hre: HardhatRuntimeEnvironment, pluginName: string diff --git a/packages/contracts/test/utils/new-osx-environment/0-managing-dao/0-0-managing-dao.ts b/packages/contracts/test/utils/new-osx-environment/0-managing-dao/0-0-managing-dao.ts index f6db93cc..3a57a4eb 100644 --- a/packages/contracts/test/utils/new-osx-environment/0-managing-dao/0-0-managing-dao.ts +++ b/packages/contracts/test/utils/new-osx-environment/0-managing-dao/0-0-managing-dao.ts @@ -50,4 +50,4 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { }); }; export default func; -func.tags = ['New', 'ManagingDao']; \ No newline at end of file +func.tags = ['New', 'ManagingDao']; diff --git a/packages/contracts/test/utils/new-osx-environment/0-managing-dao/0-1-managing-dao-permissions.ts b/packages/contracts/test/utils/new-osx-environment/0-managing-dao/0-1-managing-dao-permissions.ts index 099940e8..cc421830 100644 --- a/packages/contracts/test/utils/new-osx-environment/0-managing-dao/0-1-managing-dao-permissions.ts +++ b/packages/contracts/test/utils/new-osx-environment/0-managing-dao/0-1-managing-dao-permissions.ts @@ -1,7 +1,7 @@ import {DeployFunction} from 'hardhat-deploy/types'; import {HardhatRuntimeEnvironment} from 'hardhat/types'; -import {Operation} from '../../types'; +import {Operation} from '../../../../utils/types'; import {getContractAddress, managePermissions} from '../../helpers'; import {DAO__factory} from '../../../../typechain'; diff --git a/packages/contracts/test/utils/new-osx-environment/0-managing-dao/0-3-set-dao-permission.ts b/packages/contracts/test/utils/new-osx-environment/0-managing-dao/0-3-set-dao-permission.ts index 35128a06..1c3eef38 100644 --- a/packages/contracts/test/utils/new-osx-environment/0-managing-dao/0-3-set-dao-permission.ts +++ b/packages/contracts/test/utils/new-osx-environment/0-managing-dao/0-3-set-dao-permission.ts @@ -1,7 +1,7 @@ import {HardhatRuntimeEnvironment} from 'hardhat/types'; import {DeployFunction} from 'hardhat-deploy/types'; -import {Operation} from '../../types'; +import {Operation} from '../../../../utils/types'; import { DAO_PERMISSIONS, getContractAddress, diff --git a/packages/contracts/test/utils/new-osx-environment/0-managing-dao/0-4-verify-steps.ts b/packages/contracts/test/utils/new-osx-environment/0-managing-dao/0-4-verify-steps.ts index 0b3a1c87..484125bf 100644 --- a/packages/contracts/test/utils/new-osx-environment/0-managing-dao/0-4-verify-steps.ts +++ b/packages/contracts/test/utils/new-osx-environment/0-managing-dao/0-4-verify-steps.ts @@ -1,7 +1,7 @@ import {DeployFunction} from 'hardhat-deploy/types'; import {HardhatRuntimeEnvironment} from 'hardhat/types'; -import {Operation} from '../../types'; +import {Operation} from '../../../../utils/types'; import { checkPermission, DAO_PERMISSIONS, diff --git a/packages/contracts/test/utils/new-osx-environment/2-permissions/2-0-ens-permissions.ts b/packages/contracts/test/utils/new-osx-environment/2-permissions/2-0-ens-permissions.ts index d59e662a..91186bd2 100644 --- a/packages/contracts/test/utils/new-osx-environment/2-permissions/2-0-ens-permissions.ts +++ b/packages/contracts/test/utils/new-osx-environment/2-permissions/2-0-ens-permissions.ts @@ -1,7 +1,7 @@ import {HardhatRuntimeEnvironment} from 'hardhat/types'; import {DeployFunction} from 'hardhat-deploy/types'; -import {Operation} from '../../types'; +import {Operation} from '../../../../utils/types'; import {getContractAddress, managePermissions} from '../../helpers'; import {DAO__factory} from '../../../../typechain'; diff --git a/packages/contracts/test/utils/new-osx-environment/2-permissions/2-1-dao-registry-permissions.ts b/packages/contracts/test/utils/new-osx-environment/2-permissions/2-1-dao-registry-permissions.ts index d789d157..5705b217 100644 --- a/packages/contracts/test/utils/new-osx-environment/2-permissions/2-1-dao-registry-permissions.ts +++ b/packages/contracts/test/utils/new-osx-environment/2-permissions/2-1-dao-registry-permissions.ts @@ -1,7 +1,7 @@ import {HardhatRuntimeEnvironment} from 'hardhat/types'; import {DeployFunction} from 'hardhat-deploy/types'; -import {Operation} from '../../types'; +import {Operation} from '../../../../utils/types'; import {getContractAddress, managePermissions} from '../../helpers'; import {DAO__factory} from '../../../../typechain'; diff --git a/packages/contracts/test/utils/new-osx-environment/2-permissions/2-2-plugin-registry-permissions.ts b/packages/contracts/test/utils/new-osx-environment/2-permissions/2-2-plugin-registry-permissions.ts index ed83db78..fa86ef83 100644 --- a/packages/contracts/test/utils/new-osx-environment/2-permissions/2-2-plugin-registry-permissions.ts +++ b/packages/contracts/test/utils/new-osx-environment/2-permissions/2-2-plugin-registry-permissions.ts @@ -1,7 +1,7 @@ import {HardhatRuntimeEnvironment} from 'hardhat/types'; import {DeployFunction} from 'hardhat-deploy/types'; -import {Operation} from '../../types'; +import {Operation} from '../../../../utils/types'; import {getContractAddress, managePermissions} from '../../helpers'; import {DAO__factory} from '../../../../typechain'; diff --git a/packages/contracts/test/utils/new-osx-environment/2-permissions/2-3-verify-steps.ts b/packages/contracts/test/utils/new-osx-environment/2-permissions/2-3-verify-steps.ts index c4f4ae2c..6496551f 100644 --- a/packages/contracts/test/utils/new-osx-environment/2-permissions/2-3-verify-steps.ts +++ b/packages/contracts/test/utils/new-osx-environment/2-permissions/2-3-verify-steps.ts @@ -1,7 +1,7 @@ import {DeployFunction} from 'hardhat-deploy/types'; import {HardhatRuntimeEnvironment} from 'hardhat/types'; -import {Operation} from '../../types'; +import {Operation} from '../../../../utils/types'; import {checkPermission, getContractAddress} from '../../helpers'; import {DAO__factory} from '../../../../typechain'; diff --git a/packages/contracts/test/utils/new-osx-environment/3-plugin/3-1-vocdoni-voting-setup-conclude.ts b/packages/contracts/test/utils/new-osx-environment/3-plugin/3-1-vocdoni-voting-setup-conclude.ts index 3cfeb297..6f75ca4b 100644 --- a/packages/contracts/test/utils/new-osx-environment/3-plugin/3-1-vocdoni-voting-setup-conclude.ts +++ b/packages/contracts/test/utils/new-osx-environment/3-plugin/3-1-vocdoni-voting-setup-conclude.ts @@ -9,7 +9,9 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const {deployments, network} = hre; - const VocdoniVotingSetupDeployment = await deployments.get('VocdoniVotingSetup'); + const VocdoniVotingSetupDeployment = await deployments.get( + 'VocdoniVotingSetup' + ); const vocdoniVotingSetup = VocdoniVotingSetup__factory.connect( VocdoniVotingSetupDeployment.address, deployer diff --git a/packages/contracts/test/utils/new-osx-environment/3-plugin/3-2-create-vocdoni-voting-repo.ts b/packages/contracts/test/utils/new-osx-environment/3-plugin/3-2-create-vocdoni-voting-repo.ts index d6b18ee2..5b065e26 100644 --- a/packages/contracts/test/utils/new-osx-environment/3-plugin/3-2-create-vocdoni-voting-repo.ts +++ b/packages/contracts/test/utils/new-osx-environment/3-plugin/3-2-create-vocdoni-voting-repo.ts @@ -30,7 +30,10 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { network.name ); - const vocdoniVotingSetupContract = await getContractAddress('MultisigSetup', hre); + const vocdoniVotingSetupContract = await getContractAddress( + 'MultisigSetup', + hre + ); await createPluginRepo(hre, 'vocdoniVoting'); await populatePluginRepo(hre, 'vocdoniVoting', [ diff --git a/packages/contracts/test/utils/new-osx-environment/4-finalize-managing-dao/4-0-grant-permissions.ts b/packages/contracts/test/utils/new-osx-environment/4-finalize-managing-dao/4-0-grant-permissions.ts index 56863f75..5e058d06 100644 --- a/packages/contracts/test/utils/new-osx-environment/4-finalize-managing-dao/4-0-grant-permissions.ts +++ b/packages/contracts/test/utils/new-osx-environment/4-finalize-managing-dao/4-0-grant-permissions.ts @@ -1,7 +1,7 @@ import {HardhatRuntimeEnvironment} from 'hardhat/types'; import {DeployFunction} from 'hardhat-deploy/types'; -import {Operation, Permission} from '../../types'; +import {Operation, Permission} from '../../../../utils/types'; import {getContractAddress, managePermissions} from '../../helpers'; import {DAO__factory, PluginRepo__factory} from '../../../../typechain'; diff --git a/packages/contracts/test/utils/new-osx-environment/4-finalize-managing-dao/4-2-install-vocdoni-voting-on-managing-dao.ts b/packages/contracts/test/utils/new-osx-environment/4-finalize-managing-dao/4-2-install-vocdoni-voting-on-managing-dao.ts index 0c017cb7..0827a824 100644 --- a/packages/contracts/test/utils/new-osx-environment/4-finalize-managing-dao/4-2-install-vocdoni-voting-on-managing-dao.ts +++ b/packages/contracts/test/utils/new-osx-environment/4-finalize-managing-dao/4-2-install-vocdoni-voting-on-managing-dao.ts @@ -4,7 +4,7 @@ import buildMetadataJson from '../../../../contracts/build-metadata.json'; import {findEvent} from '../../event'; import {checkPermission, getContractAddress, hashHelpers} from '../../helpers'; -import {Operation} from '../../types'; +import {Operation} from '../../../../utils/types'; import { DAO__factory, VocdoniVotingSetup__factory, @@ -98,7 +98,10 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { ); // Adding plugin to verify array - const vocdoniVotingSetupAddress = await getContractAddress('VocdoniVotingSetup', hre); + const vocdoniVotingSetupAddress = await getContractAddress( + 'VocdoniVotingSetup', + hre + ); const vocdoniVotingSetup = VocdoniVotingSetup__factory.connect( vocdoniVotingSetupAddress, deployer @@ -135,7 +138,10 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { await checkPermission(managingDaoContract, { operation: Operation.Grant, where: {name: 'ManagingDAO', address: managingDAOAddress}, - who: {name: 'VocdoniVoting plugin', address: installationPreparedEvent.plugin}, + who: { + name: 'VocdoniVoting plugin', + address: installationPreparedEvent.plugin, + }, permission: 'EXECUTE_PERMISSION', }); diff --git a/packages/contracts/test/utils/new-osx-environment/4-finalize-managing-dao/4-3-revoke-permissions.ts b/packages/contracts/test/utils/new-osx-environment/4-finalize-managing-dao/4-3-revoke-permissions.ts index b70ff83e..d27d554a 100644 --- a/packages/contracts/test/utils/new-osx-environment/4-finalize-managing-dao/4-3-revoke-permissions.ts +++ b/packages/contracts/test/utils/new-osx-environment/4-finalize-managing-dao/4-3-revoke-permissions.ts @@ -1,7 +1,7 @@ import {DeployFunction} from 'hardhat-deploy/types'; import {getContractAddress, managePermissions} from '../../helpers'; -import {Operation, Permission} from '../../types'; +import {Operation, Permission} from '../../../../utils/types'; import {DAO__factory, PluginRepo__factory} from '../../../../typechain'; import {HardhatRuntimeEnvironment} from 'hardhat/types'; diff --git a/packages/contracts/test/utils/new-osx-environment/4-finalize-managing-dao/4-4-verify-steps.ts b/packages/contracts/test/utils/new-osx-environment/4-finalize-managing-dao/4-4-verify-steps.ts index e0a316be..fbc80b72 100644 --- a/packages/contracts/test/utils/new-osx-environment/4-finalize-managing-dao/4-4-verify-steps.ts +++ b/packages/contracts/test/utils/new-osx-environment/4-finalize-managing-dao/4-4-verify-steps.ts @@ -1,7 +1,7 @@ import {DeployFunction} from 'hardhat-deploy/types'; import {HardhatRuntimeEnvironment} from 'hardhat/types'; -import {Operation} from '../../types'; +import {Operation} from '../../../../utils/types'; import {checkPermission, getContractAddress} from '../../helpers'; import {DAO__factory} from '../../../../typechain'; diff --git a/packages/contracts/test/utils/types.ts b/packages/contracts/test/utils/types.ts deleted file mode 100644 index fc812a3a..00000000 --- a/packages/contracts/test/utils/types.ts +++ /dev/null @@ -1,58 +0,0 @@ -export enum Operation { - Grant, - Revoke, - GrantWithCondition -} - -/** - * Represents a testing fork configuration. - * - * @network The name of the forked network. - * @osxVersion The version of OSx at the moment of the fork. - */ -export type TestingFork = { - network: string; - osxVersion: string; -}; - -export type Permission = { - operation: Operation; - where: {name: string; address: string}; - who: {name: string; address: string}; - permission: string; - condition?: string; - data?: string; -}; - -export type AragonVerifyEntry = { - address: string; - args?: any[]; -}; - -export type AragonPluginRepos = { - 'address-list-voting': string; - 'token-voting': string; - // prettier-ignore - 'admin': string; - // prettier-ignore - 'multisig': string; - // prettier-ignore - 'vocdoni': string; - [index: string]: string; - }; - - // release, build -export type VersionTag = [number, number]; - -export type UpdateInfo = { - tags: string | string[]; - forkBlockNumber: number; - }; - - export const UPDATE_INFOS: {[index: string]: UpdateInfo} = { - v1_3_0: { - tags: 'update/to_v1.3.0', - forkBlockNumber: 16722881, - }, - }; - \ No newline at end of file diff --git a/packages/contracts/test/utils/update-plugin/0-vocdoni-voting.ts b/packages/contracts/test/utils/update-plugin/0-vocdoni-voting.ts index 143bd9ef..f33f67ab 100644 --- a/packages/contracts/test/utils/update-plugin/0-vocdoni-voting.ts +++ b/packages/contracts/test/utils/update-plugin/0-vocdoni-voting.ts @@ -7,7 +7,7 @@ import vocdoniVotingSetupArtifact from '../../../artifacts/contracts/VocdoniVoti import vocdoniVotingReleaseMetadata from '../../../contracts/release-metadata.json'; import vocdoniVotingBuildMetadata from '../../../contracts/build-metadata.json'; -import {UPDATE_INFOS} from '../types'; +import {UPDATE_INFOS} from '../../../utils/types'; const TARGET_RELEASE = 1; @@ -33,7 +33,10 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { network.name ); - const vocdoniVotingRepoAddress = await getContractAddress('vocdoniVoting-repo', hre); + const vocdoniVotingRepoAddress = await getContractAddress( + 'vocdoniVoting-repo', + hre + ); const vocdoniVotingRepo = PluginRepo__factory.connect( vocdoniVotingRepoAddress, ethers.provider diff --git a/packages/contracts/test/utils/update-plugin/1-vocdoni-voting-conclude.ts b/packages/contracts/test/utils/update-plugin/1-vocdoni-voting-conclude.ts index 405236fd..92d57a08 100644 --- a/packages/contracts/test/utils/update-plugin/1-vocdoni-voting-conclude.ts +++ b/packages/contracts/test/utils/update-plugin/1-vocdoni-voting-conclude.ts @@ -1,11 +1,15 @@ import {DeployFunction} from 'hardhat-deploy/types'; import {HardhatRuntimeEnvironment} from 'hardhat/types'; -import {UPDATE_INFOS} from '../types'; +import {UPDATE_INFOS} from '../../../utils/types'; const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { console.log('\nConcluding VocdoniVoting Plugin Update'); - hre.aragonToVerifyContracts.push(await hre.deployments.get('VocdoniVotingSetup')); + hre.aragonToVerifyContracts.push( + await hre.deployments.get('VocdoniVotingSetup') + ); }; export default func; -func.tags = ['VocdoniVotingPlugin', 'Verify'].concat(UPDATE_INFOS['v1_3_0'].tags); +func.tags = ['VocdoniVotingPlugin', 'Verify'].concat( + UPDATE_INFOS['v1_3_0'].tags +); diff --git a/packages/contracts/test/utils/update-plugin/2-update-managing-dao.ts b/packages/contracts/test/utils/update-plugin/2-update-managing-dao.ts index 1c99ced5..4e18b3d7 100644 --- a/packages/contracts/test/utils/update-plugin/2-update-managing-dao.ts +++ b/packages/contracts/test/utils/update-plugin/2-update-managing-dao.ts @@ -33,4 +33,4 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { }); }; export default func; -func.tags = ['ManagingDAO', 'v1.3.0']; // This is the only change \ No newline at end of file +func.tags = ['ManagingDAO', 'v1.3.0']; // This is the only change diff --git a/packages/contracts/test/utils/uups-upgradeable.ts b/packages/contracts/test/utils/uups-upgradeable.ts index be5808fb..ebef6412 100644 --- a/packages/contracts/test/utils/uups-upgradeable.ts +++ b/packages/contracts/test/utils/uups-upgradeable.ts @@ -129,7 +129,7 @@ export async function getProtocolVersion( try { contract.interface.getFunction('protocolVersion'); protocolVersion = await contract.protocolVersion(); - } catch (error) { + } catch (error: any) { if (error.code === errors.INVALID_ARGUMENT) { protocolVersion = [1, 0, 0]; } else { @@ -137,4 +137,4 @@ export async function getProtocolVersion( } } return protocolVersion; -} \ No newline at end of file +} diff --git a/packages/contracts/test/utils/verification/1-managing-dao-proposal.ts b/packages/contracts/test/utils/verification/1-managing-dao-proposal.ts index 4bed08b7..c85c8b13 100644 --- a/packages/contracts/test/utils/verification/1-managing-dao-proposal.ts +++ b/packages/contracts/test/utils/verification/1-managing-dao-proposal.ts @@ -1,69 +1,69 @@ -import {writeFile} from 'fs/promises'; -import {DeployFunction} from 'hardhat-deploy/types'; -import {HardhatRuntimeEnvironment} from 'hardhat/types'; -import {VocdoniVoting__factory} from '../../../typechain'; -import {getManagingDAOVocdoniVotingAddress, uploadToIPFS} from '../helpers'; +// import {writeFile} from 'fs/promises'; +// import {DeployFunction} from 'hardhat-deploy/types'; +// import {HardhatRuntimeEnvironment} from 'hardhat/types'; +// import {VocdoniVoting__factory} from '../../../typechain'; +// import {getManagingDAOVocdoniVotingAddress, uploadToIPFS} from '../helpers'; -const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { - console.log('\nCreating managing DAO Proposal'); - if (hre.managingDAOActions.length === 0) { - console.log('No actions defined'); - return; - } +// const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { +// console.log('\nCreating managing DAO Proposal'); +// if (hre.managingDAOActions.length === 0) { +// console.log('No actions defined'); +// return; +// } - const {ethers, network} = hre; - const [deployer] = await ethers.getSigners(); +// const {ethers, network} = hre; +// const [deployer] = await ethers.getSigners(); - const managingDAOVocdoniVotingAddress = await getManagingDAOVocdoniVotingAddress(hre); - const managingDAOVocdoniVoting = VocdoniVoting__factory.connect( - managingDAOVocdoniVotingAddress, - ethers.provider - ); - const managingDAOVocdoniVotingSettings = - await managingDAOVocdoniVoting.callStatic.multisigSettings(); +// const managingDAOVocdoniVotingAddress = await getManagingDAOVocdoniVotingAddress(hre); +// const managingDAOVocdoniVoting = VocdoniVoting__factory.connect( +// managingDAOVocdoniVotingAddress, +// ethers.provider +// ); +// const managingDAOVocdoniVotingSettings = +// await managingDAOVocdoniVoting.callStatic.multisigSettings(); - const proposalDescription = hre.managingDAOActions - .map(action => action.description) - .join('\n'); - const cid = await uploadToIPFS(proposalDescription, network.name); +// const proposalDescription = hre.managingDAOActions +// .map(action => action.description) +// .join('\n'); +// const cid = await uploadToIPFS(proposalDescription, network.name); - if (managingDAOVocdoniVotingSettings.onlyListed) { - if (!(await managingDAOVocdoniVoting.callStatic.isMember(deployer.address))) { - console.log( - `ManagingDAOVocdoniVoting (${managingDAOVocdoniVotingAddress}) doesn't allow deployer ${deployer.address} to create proposal.` - ); - const tx = await managingDAOVocdoniVoting.populateTransaction.createProposal( - ethers.utils.toUtf8Bytes(`ipfs://${cid}`), // CHANGE TO VOCHAIN PROCESS ID - ethers.utils.toUtf8Bytes(`ipfs://${cid}`), - 0, - Math.round(Date.now() / 1000) + 30 * 24 * 60 * 60, // Lets the proposal end in 30 days, - Math.round(Date.now() / 1000) + 35 * 24 * 60 * 60, // Lets the proposal expiry in 35 days, - hre.managingDAOActions, - 0, - ); - await writeFile('./managingDAOTX.json', JSON.stringify(tx)); - console.log('Saved transaction to managingDAOTX.json'); - } - return; - } +// if (managingDAOVocdoniVotingSettings.onlyListed) { +// if (!(await managingDAOVocdoniVoting.callStatic.isMember(deployer.address))) { +// console.log( +// `ManagingDAOVocdoniVoting (${managingDAOVocdoniVotingAddress}) doesn't allow deployer ${deployer.address} to create proposal.` +// ); +// const tx = await managingDAOVocdoniVoting.populateTransaction.createProposal( +// ethers.utils.toUtf8Bytes(`ipfs://${cid}`), // CHANGE TO VOCHAIN PROCESS ID +// ethers.utils.toUtf8Bytes(`ipfs://${cid}`), +// 0, +// Math.round(Date.now() / 1000) + 30 * 24 * 60 * 60, // Lets the proposal end in 30 days, +// Math.round(Date.now() / 1000) + 35 * 24 * 60 * 60, // Lets the proposal expiry in 35 days, +// hre.managingDAOActions, +// 0, +// ); +// await writeFile('./managingDAOTX.json', JSON.stringify(tx)); +// console.log('Saved transaction to managingDAOTX.json'); +// } +// return; +// } - console.log( - `ManagingDAOVocdoniVoting (${managingDAOVocdoniVotingAddress}) does allow deployer ${deployer.address} to create proposal.` - ); - const tx = await managingDAOVocdoniVoting.createProposal( - ethers.utils.toUtf8Bytes(`ipfs://${cid}`), // CHANGE TO VOCHAIN PROCESS ID - ethers.utils.toUtf8Bytes(`ipfs://${cid}`), - 0, - Math.round(Date.now() / 1000) + 30 * 24 * 60 * 60, // Lets the proposal end in 30 days, - Math.round(Date.now() / 1000) + 35 * 24 * 60 * 60, // Lets the proposal expiry in 35 days, - hre.managingDAOActions, - 0, - ); - console.log(`Creating proposal with tx ${tx.hash}`); - await tx.wait(); - console.log( - `Proposal created in managingDAO VocdoniVoting ${managingDAOVocdoniVotingAddress}` - ); -}; -export default func; -func.tags = ['New', 'ManagingDAOProposal']; +// console.log( +// `ManagingDAOVocdoniVoting (${managingDAOVocdoniVotingAddress}) does allow deployer ${deployer.address} to create proposal.` +// ); +// const tx = await managingDAOVocdoniVoting.createProposal( +// ethers.utils.toUtf8Bytes(`ipfs://${cid}`), // CHANGE TO VOCHAIN PROCESS ID +// ethers.utils.toUtf8Bytes(`ipfs://${cid}`), +// 0, +// Math.round(Date.now() / 1000) + 30 * 24 * 60 * 60, // Lets the proposal end in 30 days, +// Math.round(Date.now() / 1000) + 35 * 24 * 60 * 60, // Lets the proposal expiry in 35 days, +// hre.managingDAOActions, +// 0, +// ); +// console.log(`Creating proposal with tx ${tx.hash}`); +// await tx.wait(); +// console.log( +// `Proposal created in managingDAO VocdoniVoting ${managingDAOVocdoniVotingAddress}` +// ); +// }; +// export default func; +// func.tags = ['New', 'ManagingDAOProposal']; diff --git a/packages/contracts/test/utils/verification/2-verify-contracts.ts b/packages/contracts/test/utils/verification/2-verify-contracts.ts index c10e9c3e..39932e31 100644 --- a/packages/contracts/test/utils/verification/2-verify-contracts.ts +++ b/packages/contracts/test/utils/verification/2-verify-contracts.ts @@ -38,78 +38,73 @@ func.skip = (hre: HardhatRuntimeEnvironment) => hre.network.name === 'coverage' ); +const verifyContract = async (address: string, constructorArguments: any[]) => { + const currentNetwork = HRE.network.name; - const verifyContract = async ( - address: string, - constructorArguments: any[] - ) => { - const currentNetwork = HRE.network.name; - - const networks = await fs.promises.readFile( - path.join(__dirname, '../networks.json'), - 'utf8' + const networks = await fs.promises.readFile( + path.join(__dirname, '../networks.json'), + 'utf8' + ); + const networksJSON = JSON.parse(networks.toString()); + if (!Object.keys(networksJSON).includes(currentNetwork)) { + throw Error( + `Current network ${currentNetwork} not supported. Please change to one of the next networks: ${Object.keys( + networksJSON + ).join(',')}` ); - const networksJSON = JSON.parse(networks.toString()); - if (!Object.keys(networksJSON).includes(currentNetwork)) { - throw Error( - `Current network ${currentNetwork} not supported. Please change to one of the next networks: ${Object.keys( - networksJSON - ).join(',')}` - ); - } - - try { - const msDelay = 500; // minimum dely between tasks - const times = 2; // number of retries - - // Write a temporal file to host complex parameters for hardhat-etherscan https://github.com/nomiclabs/hardhat/tree/master/packages/hardhat-etherscan#complex-arguments - const {fd, path, cleanup} = await file({ - prefix: 'verify-params-', - postfix: '.js', - }); - fs.writeSync( - fd, - `module.exports = ${JSON.stringify([...constructorArguments])};` + } + + try { + const msDelay = 500; // minimum dely between tasks + const times = 2; // number of retries + + // Write a temporal file to host complex parameters for hardhat-etherscan https://github.com/nomiclabs/hardhat/tree/master/packages/hardhat-etherscan#complex-arguments + const {fd, path, cleanup} = await file({ + prefix: 'verify-params-', + postfix: '.js', + }); + fs.writeSync( + fd, + `module.exports = ${JSON.stringify([...constructorArguments])};` + ); + + const params = { + address: address, + constructorArgs: path, + }; + await runTaskWithRetry('verify', params, times, msDelay, cleanup); + } catch (error) { + console.warn(`Verify task error: ${error}`); + } +}; + +const runTaskWithRetry = async ( + task: string, + params: any, + times: number, + msDelay: number, + cleanup: () => void +) => { + let counter = times; + await delay(msDelay); + + try { + if (times) { + await HRE.run(task, params); + cleanup(); + } else { + cleanup(); + console.error( + 'Errors after all the retries, check the logs for more information.' ); - - const params = { - address: address, - constructorArgs: path, - }; - await runTaskWithRetry('verify', params, times, msDelay, cleanup); - } catch (error) { - console.warn(`Verify task error: ${error}`); } - }; - - const runTaskWithRetry = async ( - task: string, - params: any, - times: number, - msDelay: number, - cleanup: () => void - ) => { - let counter = times; - await delay(msDelay); - - try { - if (times) { - await HRE.run(task, params); - cleanup(); - } else { - cleanup(); - console.error( - 'Errors after all the retries, check the logs for more information.' - ); - } - } catch (error: any) { - counter--; - // This is not the ideal check, but it's all that's possible for now https://github.com/nomiclabs/hardhat/issues/1301 - if (!/already verified/i.test(error.message)) { - console.log(`Retrying attemps: ${counter}.`); - console.error(error.message); - await runTaskWithRetry(task, params, counter, msDelay, cleanup); - } + } catch (error: any) { + counter--; + // This is not the ideal check, but it's all that's possible for now https://github.com/nomiclabs/hardhat/issues/1301 + if (!/already verified/i.test(error.message)) { + console.log(`Retrying attemps: ${counter}.`); + console.error(error.message); + await runTaskWithRetry(task, params, counter, msDelay, cleanup); } - }; - \ No newline at end of file + } +}; diff --git a/packages/contracts/test/utils/voting.ts b/packages/contracts/test/utils/voting.ts index 393fa639..e24e54c7 100644 --- a/packages/contracts/test/utils/voting.ts +++ b/packages/contracts/test/utils/voting.ts @@ -102,12 +102,23 @@ export function toBytes32(num: number): string { } export type VocdoniVotingSettings = { - onlyCommitteeProposalCreation: boolean; + onlyExecutionMultisigProposalCreation: boolean; minTallyApprovals: number; - minDuration: number; - minParticipation: BigNumber; - supportThreshold: BigNumber; + minParticipation: number; + supportThreshold: number; + minVoteDuration: number; + minTallyDuration: number; daoTokenAddress: string; - minProposerVotingPower: number; - censusStrategy: string; -}; \ No newline at end of file + minProposerVotingPower: BigNumber; + censusStrategyURI: string; +}; + +export type VocdoniProposalParams = { + securityBlock: number; + startDate: number; + voteEndDate: number; + tallyEndDate: number; + totalVotingPower: BigNumber; + censusURI: string; + censusRoot: string; +}; diff --git a/packages/contracts/test/vocdoni-voting-setup.ts b/packages/contracts/test/vocdoni-voting-setup.ts index 8bd52c85..dcda6d6d 100644 --- a/packages/contracts/test/vocdoni-voting-setup.ts +++ b/packages/contracts/test/vocdoni-voting-setup.ts @@ -15,17 +15,13 @@ import { } from '../typechain'; import {deployNewDAO} from './utils/dao'; import {getInterfaceID} from './utils/helpers'; -import {Operation} from './utils/types'; +import {Operation} from '../utils/types'; import metadata from '../contracts/build-metadata.json'; -import { - VocdoniVotingSettings, - VotingMode, - pctToRatio, - ONE_HOUR, -} from './utils/voting'; +import {VocdoniVotingSettings, pctToRatio, ONE_HOUR} from './utils/voting'; import {vocdoniVotingInterface} from './vocdoni-voting'; import {getNamedTypesFromMetadata} from './utils/metadata'; +import {BigNumber} from 'ethers'; let defaultData: any; let defaultVocdoniVotingSettings: VocdoniVotingSettings; @@ -50,9 +46,9 @@ const UPDATE_PLUGIN_SETTINGS_PERMISSION_ID = ethers.utils.id( 'UPDATE_PLUGIN_SETTINGS_PERMISSION' ); -const UPDATE_PLUGIN_COMMITTEE_PERMISSION_ID = ethers.utils.id( - 'UPDATE_PLUGIN_COMMITTEE_PERMISSION' - ); +const UPDATE_PLUGIN_EXECUTION_MULTISIG_PERMISSION_ID = ethers.utils.id( + 'UPDATE_PLUGIN_EXECUTION_MULTISIG_PERMISSION' +); const UPGRADE_PERMISSION_ID = ethers.utils.id('UPGRADE_PLUGIN_PERMISSION'); const EXECUTE_PERMISSION_ID = ethers.utils.id('EXECUTE_PERMISSION'); const MINT_PERMISSION_ID = ethers.utils.id('MINT_PERMISSION'); @@ -71,14 +67,15 @@ describe('VocdoniVotingSetup', function () { targetDao = await deployNewDAO(signers[0]); defaultVocdoniVotingSettings = { - onlyCommitteeProposalCreation: true, + onlyExecutionMultisigProposalCreation: true, minTallyApprovals: 1, - minDuration: ONE_HOUR, - minParticipation: pctToRatio(20), - supportThreshold: pctToRatio(50), + minParticipation: 20, + supportThreshold: 50, + minVoteDuration: ONE_HOUR, + minTallyDuration: ONE_HOUR, daoTokenAddress: AddressZero, - minProposerVotingPower: 0, - censusStrategy: "", + minProposerVotingPower: BigNumber.from(10), + censusStrategyURI: '', }; const emptyName = ''; @@ -130,7 +127,8 @@ describe('VocdoniVotingSetup', function () { }); it('does not support the empty interface', async () => { - expect(await vocdoniVotingSetup.supportsInterface('0xffffffff')).to.be.false; + expect(await vocdoniVotingSetup.supportsInterface('0xffffffff')).to.be + .false; }); it('stores the bases provided through the constructor', async () => { @@ -147,7 +145,9 @@ describe('VocdoniVotingSetup', function () { const vocdoniVoting = factory.attach(implementationAddress); expect( - await vocdoniVoting.supportsInterface(getInterfaceID(vocdoniVotingInterface)) + await vocdoniVoting.supportsInterface( + getInterfaceID(vocdoniVotingInterface) + ) ).to.be.eq(true); }); @@ -168,7 +168,7 @@ describe('VocdoniVotingSetup', function () { vocdoniVotingSetup.prepareInstallation(targetDao.address, defaultData) ).not.to.be.reverted; }); - + it('fails if `MintSettings` arrays do not have the same length', async () => { const receivers: string[] = [AddressZero]; const amounts: number[] = []; @@ -201,7 +201,6 @@ describe('VocdoniVotingSetup', function () { .withArgs(1, 0); }); - it('fails if passed token address is not a contract', async () => { const tokenAddress = signers[0].address; const data = abiCoder.encode(prepareInstallationDataTypes, [ @@ -279,7 +278,7 @@ describe('VocdoniVotingSetup', function () { plugin, targetDao.address, AddressZero, - UPDATE_PLUGIN_COMMITTEE_PERMISSION_ID, + UPDATE_PLUGIN_EXECUTION_MULTISIG_PERMISSION_ID, ], [ Operation.Grant, @@ -384,7 +383,7 @@ describe('VocdoniVotingSetup', function () { plugin, targetDao.address, AddressZero, - UPDATE_PLUGIN_COMMITTEE_PERMISSION_ID, + UPDATE_PLUGIN_EXECUTION_MULTISIG_PERMISSION_ID, ], [ Operation.Grant, @@ -442,7 +441,7 @@ describe('VocdoniVotingSetup', function () { plugin, targetDao.address, AddressZero, - UPDATE_PLUGIN_COMMITTEE_PERMISSION_ID, + UPDATE_PLUGIN_EXECUTION_MULTISIG_PERMISSION_ID, ], [ Operation.Grant, @@ -580,11 +579,11 @@ describe('VocdoniVotingSetup', function () { UPDATE_PLUGIN_SETTINGS_PERMISSION_ID, ], [ - Operation.Revoke, - plugin, - targetDao.address, - AddressZero, - UPDATE_PLUGIN_COMMITTEE_PERMISSION_ID, + Operation.Revoke, + plugin, + targetDao.address, + AddressZero, + UPDATE_PLUGIN_EXECUTION_MULTISIG_PERMISSION_ID, ], [ Operation.Revoke, @@ -628,4 +627,4 @@ describe('VocdoniVotingSetup', function () { ]); }); }); -}); \ No newline at end of file +}); diff --git a/packages/contracts/test/vocdoni-voting.ts b/packages/contracts/test/vocdoni-voting.ts index bd57a7e4..57d5fdad 100644 --- a/packages/contracts/test/vocdoni-voting.ts +++ b/packages/contracts/test/vocdoni-voting.ts @@ -1,3 +1,8 @@ +import {expect} from 'chai'; +import {ethers} from 'hardhat'; +import {Contract, BigNumber} from 'ethers'; +import {SignerWithAddress} from '@nomiclabs/hardhat-ethers/signers'; + import { Addresslist__factory, DAO, @@ -9,44 +14,27 @@ import { GovernanceERC20Mock, GovernanceERC20Mock__factory, } from '../typechain'; + +import {VOCDONI_EVENTS} from './utils/event'; import {deployNewDAO} from './utils/dao'; +import { + timestampIn, + setTimeForNextBlock, + VocdoniProposalParams, + VocdoniVotingSettings, +} from './utils/voting'; import {deployWithProxy} from './utils/dao'; -import {VOCDONI_EVENTS} from './utils/event'; import {getInterfaceID, OZ_ERRORS} from './utils/helpers'; -import {timestampIn} from './utils/voting'; -import {SignerWithAddress} from '@nomiclabs/hardhat-ethers/signers'; -import {expect} from 'chai'; -import {Contract, BigNumber} from 'ethers'; -import {ethers} from 'hardhat'; export const vocdoniVotingInterface = new ethers.utils.Interface([ - 'function addCommitteeMembers(address[] calldata _members)', - 'function removeCommitteeMembers(address[] calldata _members)', - 'function isCommitteeMember(address _member)', + 'function addExecutionMultisigMembers(address[] calldata _members)', + 'function removeExecutionMultisigMembers(address[] calldata _members)', + 'function isExecutionMultisigMember(address _member)', 'function setTally(uint256 _proposalId, uint256[][] memory _tally)', 'function approveTally(uint256 _proposalId, bool _tryExecution)', 'function executeProposal(uint256 _proposalId)', ]); -export type VocdoniVotingSettings = { - onlyCommitteeProposalCreation: boolean; - minTallyApprovals: number; - minDuration: number; - minParticipation: number; - supportThreshold: number; - daoTokenAddress: string; - censusStrategy: string; - minProposerVotingPower: number; -}; - -export type vocdoniProposalParams = { - censusBlock: number; - securityBlock: number; - startDate: number; - endDate: number; - expirationDate: number; -}; - export async function approveWithSigners( vocdoniVoting: Contract, proposalId: number, @@ -69,7 +57,7 @@ describe('Vocdoni Plugin', function () { let dummyMetadata: string; let dummyActions: any; let vocdoniVotingSettings: VocdoniVotingSettings; - let vocdoniProposalParams: vocdoniProposalParams; + let vocdoniProposalParams: VocdoniProposalParams; let governanceErc20Mock: GovernanceERC20Mock; let GovernanceERC20Mock: GovernanceERC20Mock__factory; @@ -126,22 +114,26 @@ describe('Vocdoni Plugin', function () { ); vocdoniVotingSettings = { - onlyCommitteeProposalCreation: true, + onlyExecutionMultisigProposalCreation: true, minTallyApprovals: 2, - minDuration: 1, minParticipation: 0, supportThreshold: 0, + minVoteDuration: 3600, + minTallyDuration: 3600, daoTokenAddress: governanceErc20Mock.address, - censusStrategy: '', - minProposerVotingPower: 0, + minProposerVotingPower: BigNumber.from(0), + censusStrategyURI: 'ipfs://Qm...', }; vocdoniProposalParams = { - censusBlock: await ethers.provider.getBlockNumber(), securityBlock: await ethers.provider.getBlockNumber(), startDate: 0, - endDate: 0, - expirationDate: 0, + voteEndDate: 0, + tallyEndDate: 0, + totalVotingPower: BigNumber.from(1), + censusURI: 'ipfs://Qm...', + censusRoot: + '0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', }; const VocdoniVotingFactory = new VocdoniVoting__factory(signers[0]); @@ -152,7 +144,7 @@ describe('Vocdoni Plugin', function () { vocdoniVoting.address, ethers.utils.id('EXECUTE_PERMISSION') ); - // grant committee permissions to signers[0] + // grant executionMultisig permissions to signers[0] dao.grant( vocdoniVoting.address, signers[0].address, @@ -161,9 +153,9 @@ describe('Vocdoni Plugin', function () { dao.grant( vocdoniVoting.address, signers[0].address, - ethers.utils.id('UPDATE_PLUGIN_COMMITTEE_PERMISSION') + ethers.utils.id('UPDATE_PLUGIN_EXECUTION_MULTISIG_PERMISSION') ); - // grant committee permissions to signers[1] + // grant executionMultisig permissions to signers[1] dao.grant( vocdoniVoting.address, signers[1].address, @@ -172,7 +164,7 @@ describe('Vocdoni Plugin', function () { dao.grant( vocdoniVoting.address, signers[1].address, - ethers.utils.id('UPDATE_PLUGIN_COMMITTEE_PERMISSION') + ethers.utils.id('UPDATE_PLUGIN_EXECUTION_MULTISIG_PERMISSION') ); }); @@ -208,6 +200,51 @@ describe('Vocdoni Plugin', function () { expect(await vocdoniVoting.isListed(signers[1].address)).to.equal(true); }); + it('should revert if the members list is empty', async () => { + await expect( + vocdoniVoting.initialize(dao.address, [], vocdoniVotingSettings) + ).to.be.revertedWithCustomError(vocdoniVoting, 'InvalidListLength'); + }); + + it('should revert if members list is longer than uint16 max', async () => { + const megaMember = signers[1]; + const members: string[] = new Array(65537).fill(megaMember.address); + await expect( + vocdoniVoting.initialize(dao.address, members, vocdoniVotingSettings) + ) + .to.revertedWithCustomError( + vocdoniVoting, + 'AddresslistLengthOutOfBounds' + ) + .withArgs(65535, members.length); + }); + + // lastExecutionMultisigChange is updated to the current block number + it('should set the `lastExecutionMultisigChange` to the current block number', async () => { + await vocdoniVoting.initialize( + dao.address, + signers.slice(0, 5).map(s => s.address), + vocdoniVotingSettings + ); + const blockNumber = await ethers.provider.getBlockNumber(); + expect( + (await vocdoniVoting.getLastExecutionMultisigChange()).toNumber() + ).to.be.eq(blockNumber); + }); + + // lastPluginSettingsChange is updated to the current block number + it('should set the `lastPluginSettingsChange` to the current block number', async () => { + await vocdoniVoting.initialize( + dao.address, + signers.slice(0, 5).map(s => s.address), + vocdoniVotingSettings + ); + const blockNumber = await ethers.provider.getBlockNumber(); + expect( + (await vocdoniVoting.getLastPluginSettingsChange()).toNumber() + ).to.be.eq(blockNumber); + }); + it('should set the `minTallyApprovals`', async () => { await vocdoniVoting.initialize( dao.address, @@ -219,15 +256,26 @@ describe('Vocdoni Plugin', function () { ).to.be.eq(vocdoniVotingSettings.minTallyApprovals); }); - it('should set `minDuration`', async () => { + it('should set `minVoteDuration`', async () => { await vocdoniVoting.initialize( dao.address, signers.slice(0, 5).map(s => s.address), vocdoniVotingSettings ); - expect((await vocdoniVoting.getPluginSettings()).minDuration).to.be.eq( - vocdoniVotingSettings.minDuration + expect( + (await vocdoniVoting.getPluginSettings()).minVoteDuration + ).to.be.eq(vocdoniVotingSettings.minVoteDuration); + }); + + it('should set `minTallyDuration`', async () => { + await vocdoniVoting.initialize( + dao.address, + signers.slice(0, 5).map(s => s.address), + vocdoniVotingSettings ); + expect( + (await vocdoniVoting.getPluginSettings()).minTallyDuration + ).to.be.eq(vocdoniVotingSettings.minTallyDuration); }); it('should set `daoTokenAddress`', async () => { @@ -241,15 +289,15 @@ describe('Vocdoni Plugin', function () { ).to.be.eq(vocdoniVotingSettings.daoTokenAddress); }); - it('should set `censusStrategy`', async () => { + it('should set `censusStrategyURI`', async () => { await vocdoniVoting.initialize( dao.address, signers.slice(0, 5).map(s => s.address), vocdoniVotingSettings ); - expect((await vocdoniVoting.getPluginSettings()).censusStrategy).to.be.eq( - vocdoniVotingSettings.censusStrategy - ); + expect( + (await vocdoniVoting.getPluginSettings()).censusStrategyURI + ).to.be.eq(vocdoniVotingSettings.censusStrategyURI); }); it('should set `minProposerVotingPower`', async () => { @@ -295,28 +343,25 @@ describe('Vocdoni Plugin', function () { ) .to.emit(vocdoniVoting, VOCDONI_EVENTS.PLUGIN_SETTINGS_UPDATED) .withArgs( - vocdoniVotingSettings.onlyCommitteeProposalCreation, + vocdoniVotingSettings.onlyExecutionMultisigProposalCreation, vocdoniVotingSettings.minTallyApprovals, - vocdoniVotingSettings.minDuration, vocdoniVotingSettings.minParticipation, vocdoniVotingSettings.supportThreshold, + vocdoniVotingSettings.minVoteDuration, + vocdoniVotingSettings.minTallyDuration, vocdoniVotingSettings.daoTokenAddress, - vocdoniVotingSettings.censusStrategy, + vocdoniVotingSettings.censusStrategyURI, vocdoniVotingSettings.minProposerVotingPower ); }); - it('should revert if members list is longer than uint16 max', async () => { - const megaMember = signers[1]; - const members: string[] = new Array(65537).fill(megaMember.address); + it('should emit `ExecutionMultisigMembersAdded` during initialization', async () => { + const members = signers.slice(0, 5).map(s => s.address); + await expect( vocdoniVoting.initialize(dao.address, members, vocdoniVotingSettings) - ) - .to.revertedWithCustomError( - vocdoniVoting, - 'AddresslistLengthOutOfBounds' - ) - .withArgs(65535, members.length); + ).to.emit(vocdoniVoting, VOCDONI_EVENTS.EXECUTION_MULTISIG_MEMBERS_ADDED); + // returns a hash of the members array }); }); @@ -362,16 +407,138 @@ describe('Vocdoni Plugin', function () { await expect(vocdoniVoting.updatePluginSettings(vocdoniVotingSettings)) .to.emit(vocdoniVoting, VOCDONI_EVENTS.PLUGIN_SETTINGS_UPDATED) .withArgs( - vocdoniVotingSettings.onlyCommitteeProposalCreation, + vocdoniVotingSettings.onlyExecutionMultisigProposalCreation, vocdoniVotingSettings.minTallyApprovals, - vocdoniVotingSettings.minDuration, vocdoniVotingSettings.minParticipation, vocdoniVotingSettings.supportThreshold, + vocdoniVotingSettings.minVoteDuration, + vocdoniVotingSettings.minTallyDuration, vocdoniVotingSettings.daoTokenAddress, - vocdoniVotingSettings.censusStrategy, + vocdoniVotingSettings.censusStrategyURI, vocdoniVotingSettings.minProposerVotingPower ); }); + it('should update the `minTallyApprovals`', async () => { + vocdoniVotingSettings.minTallyApprovals = 3; + await vocdoniVoting.updatePluginSettings(vocdoniVotingSettings); + expect( + (await vocdoniVoting.getPluginSettings()).minTallyApprovals + ).to.be.eq(vocdoniVotingSettings.minTallyApprovals); + }); + it('should update `minVoteDuration`', async () => { + vocdoniVotingSettings.minVoteDuration = 20000; + await vocdoniVoting.updatePluginSettings(vocdoniVotingSettings); + expect( + (await vocdoniVoting.getPluginSettings()).minVoteDuration + ).to.be.eq(vocdoniVotingSettings.minVoteDuration); + }); + it('should update `minTallyDuration`', async () => { + vocdoniVotingSettings.minTallyDuration = 20000; + await vocdoniVoting.updatePluginSettings(vocdoniVotingSettings); + expect( + (await vocdoniVoting.getPluginSettings()).minTallyDuration + ).to.be.eq(vocdoniVotingSettings.minTallyDuration); + }); + it('should update `minParticipation`', async () => { + vocdoniVotingSettings.minParticipation = 1; + await vocdoniVoting.updatePluginSettings(vocdoniVotingSettings); + expect( + (await vocdoniVoting.getPluginSettings()).minParticipation + ).to.be.eq(vocdoniVotingSettings.minParticipation); + }); + it('should update `supportThreshold`', async () => { + vocdoniVotingSettings.supportThreshold = 1; + await vocdoniVoting.updatePluginSettings(vocdoniVotingSettings); + expect( + (await vocdoniVoting.getPluginSettings()).supportThreshold + ).to.be.eq(vocdoniVotingSettings.supportThreshold); + }); + it('should update `daoTokenAddress`', async () => { + vocdoniVotingSettings.daoTokenAddress = signers[1].address; + await vocdoniVoting.updatePluginSettings(vocdoniVotingSettings); + expect( + (await vocdoniVoting.getPluginSettings()).daoTokenAddress + ).to.be.eq(vocdoniVotingSettings.daoTokenAddress); + }); + it('should update `censusStrategyURI`', async () => { + vocdoniVotingSettings.censusStrategyURI = '0x123456789'; + await vocdoniVoting.updatePluginSettings(vocdoniVotingSettings); + expect( + (await vocdoniVoting.getPluginSettings()).censusStrategyURI + ).to.be.eq(vocdoniVotingSettings.censusStrategyURI); + }); + it('should update `minProposerVotingPower`', async () => { + vocdoniVotingSettings.minProposerVotingPower = BigNumber.from(1); + await vocdoniVoting.updatePluginSettings(vocdoniVotingSettings); + expect( + (await vocdoniVoting.getPluginSettings()).minProposerVotingPower + ).to.be.eq(vocdoniVotingSettings.minProposerVotingPower); + }); + it('should revert with RatioOutOfBounds if supportThreshold is greater than 10^6', async () => { + vocdoniVotingSettings.supportThreshold = 1000001; + await expect( + vocdoniVoting.updatePluginSettings(vocdoniVotingSettings) + ).to.be.revertedWithCustomError(vocdoniVoting, 'RatioOutOfBounds'); + }); + it('should revert with RatioOutOfBounds if minParticipation is greater than 10^6', async () => { + vocdoniVotingSettings.minParticipation = 1000001; + await expect( + vocdoniVoting.updatePluginSettings(vocdoniVotingSettings) + ).to.be.revertedWithCustomError(vocdoniVoting, 'RatioOutOfBounds'); + }); + it('should revert with VoteDurationOutOfBounds if minVoteDuration is greater than 365 days', async () => { + vocdoniVotingSettings.minVoteDuration = 31536001; + await expect( + vocdoniVoting.updatePluginSettings(vocdoniVotingSettings) + ).to.be.revertedWithCustomError(vocdoniVoting, 'VoteDurationOutOfBounds'); + }); + it('should revert with TallyDurationOutOfBounds if MinTallyDuration is greater than 365 days', async () => { + vocdoniVotingSettings.minTallyDuration = 31536001; + await expect( + vocdoniVoting.updatePluginSettings(vocdoniVotingSettings) + ).to.be.revertedWithCustomError( + vocdoniVoting, + 'TallyDurationOutOfBounds' + ); + }); + it('should revert with VoteDurationOutOfBounds if minVoteDuration is less than 1 hour', async () => { + vocdoniVotingSettings.minVoteDuration = 0; + await expect( + vocdoniVoting.updatePluginSettings(vocdoniVotingSettings) + ).to.be.revertedWithCustomError(vocdoniVoting, 'VoteDurationOutOfBounds'); + }); + it('should revert with TallyDurationOutOfBounds if MinTallyDuration is less than 1 hour', async () => { + vocdoniVotingSettings.minTallyDuration = 3599; + await expect( + vocdoniVoting.updatePluginSettings(vocdoniVotingSettings) + ).to.be.revertedWithCustomError( + vocdoniVoting, + 'TallyDurationOutOfBounds' + ); + }); + it('should revert with PluginSettingsUpdatedTooRecently if settings changed in the same block', async () => { + await ethers.provider.send('evm_setAutomine', [false]); + await vocdoniVoting + .connect(signers[0]) + .updatePluginSettings(vocdoniVotingSettings); + await expect( + vocdoniVoting + .connect(signers[0]) + .updatePluginSettings(vocdoniVotingSettings) + ).to.be.revertedWithCustomError( + vocdoniVoting, + 'PluginSettingsUpdatedTooRecently' + ); + await ethers.provider.send('evm_setAutomine', [true]); + }); + // should set lastPluginSettingsChange to the current block number + it('should set `lastPluginSettingsChange` to the current block number', async () => { + await vocdoniVoting.updatePluginSettings(vocdoniVotingSettings); + let blockNumber = await ethers.provider.getBlockNumber(); + expect( + (await vocdoniVoting.getLastPluginSettingsChange()).toNumber() + ).to.be.eq(blockNumber); + }); }); describe('isListed:', async () => { @@ -387,27 +554,25 @@ describe('Vocdoni Plugin', function () { }); }); - describe('isCommitteeMember', async () => { + describe('isExecutionMultisigMember', async () => { it('should return false, if user is not listed', async () => { - expect(await vocdoniVoting.isCommitteeMember(signers[0].address)).to.be - .false; + expect(await vocdoniVoting.isExecutionMultisigMember(signers[0].address)) + .to.be.false; }); it('should return true if user is in the latest list', async () => { - vocdoniVotingSettings.minTallyApprovals = 1; await vocdoniVoting.initialize( dao.address, [signers[0].address], vocdoniVotingSettings ); - expect(await vocdoniVoting.isCommitteeMember(signers[0].address)).to.be - .true; + expect(await vocdoniVoting.isExecutionMultisigMember(signers[0].address)) + .to.be.true; }); }); - describe('addCommitteeMembers:', async () => { - it('should add new members to the committee address list and emit the `CommitteeMembersAdded` event', async () => { - vocdoniVotingSettings.minTallyApprovals = 1; + describe('addExecutionMultisigMembers:', async () => { + it('should add new members to the executionMultisig address list and emit the `ExecutionMultisigMembersAdded` event', async () => { await vocdoniVoting.initialize( dao.address, [signers[0].address], @@ -419,8 +584,8 @@ describe('Vocdoni Plugin', function () { // add a new member await expect( - vocdoniVoting.addCommitteeMembers([signers[1].address]) - ).to.emit(vocdoniVoting, VOCDONI_EVENTS.COMMITTEE_MEMBERS_ADDED); + vocdoniVoting.addExecutionMultisigMembers([signers[1].address]) + ).to.emit(vocdoniVoting, VOCDONI_EVENTS.EXECUTION_MULTISIG_MEMBERS_ADDED); //.withArgs({newMembers: [signers[1].address]}); expect(await vocdoniVoting.isListed(signers[0].address)).to.equal(true); @@ -428,8 +593,8 @@ describe('Vocdoni Plugin', function () { }); }); - describe('removeCommitteeMembers:', async () => { - it('should remove users from the committee address list and emit the `CommitteeMembersRemoved` event', async () => { + describe('removeExecutionMultisigMembers:', async () => { + it('should remove users from the executionMultisig address list and emit the `ExecutionMultisigMembersRemoved` event', async () => { vocdoniVotingSettings.minTallyApprovals = 1; await vocdoniVoting.initialize( dao.address, @@ -442,8 +607,11 @@ describe('Vocdoni Plugin', function () { // remove an existing member await expect( - vocdoniVoting.removeCommitteeMembers([signers[1].address]) - ).to.emit(vocdoniVoting, VOCDONI_EVENTS.COMMITTEE_MEMBERS_REMOVED); + vocdoniVoting.removeExecutionMultisigMembers([signers[1].address]) + ).to.emit( + vocdoniVoting, + VOCDONI_EVENTS.EXECUTION_MULTISIG_MEMBERS_REMOVED + ); //.withArgs([signers[1].address]); expect(await vocdoniVoting.isListed(signers[0].address)).to.equal(true); @@ -458,7 +626,9 @@ describe('Vocdoni Plugin', function () { vocdoniVotingSettings ); - await expect(vocdoniVoting.removeCommitteeMembers([signers[0].address])) + await expect( + vocdoniVoting.removeExecutionMultisigMembers([signers[0].address]) + ) .to.be.revertedWithCustomError(vocdoniVoting, 'MinApprovalsOutOfBounds') .withArgs( (await vocdoniVoting.addresslistLength()).sub(1), @@ -474,10 +644,13 @@ describe('Vocdoni Plugin', function () { vocdoniVotingSettings ); - await expect(vocdoniVoting.removeCommitteeMembers([signers[1].address])) - .not.to.be.reverted; + await expect( + vocdoniVoting.removeExecutionMultisigMembers([signers[1].address]) + ).not.to.be.reverted; - await expect(vocdoniVoting.removeCommitteeMembers([signers[2].address])) + await expect( + vocdoniVoting.removeExecutionMultisigMembers([signers[2].address]) + ) .to.be.revertedWithCustomError(vocdoniVoting, 'MinApprovalsOutOfBounds') .withArgs( (await vocdoniVoting.addresslistLength()).sub(1), @@ -570,9 +743,8 @@ describe('Vocdoni Plugin', function () { }); it('reverts if the vocdoniVoting settings have been changed in the same block', async () => { - vocdoniVotingSettings.minDuration = 1; - vocdoniProposalParams.expirationDate = - (await ethers.provider.getBlock('latest')).timestamp + 1000; + vocdoniVotingSettings.minVoteDuration = 3601; + vocdoniVotingSettings.minTallyApprovals = 2; await vocdoniVoting.initialize( dao.address, [signers[0].address, signers[1].address], // signers[0] is listed @@ -596,50 +768,54 @@ describe('Vocdoni Plugin', function () { [ { minTallyApprovals: 2, - minDuration: 1, + minVoteDuration: 3601, minParticipation: 0, + minTallyDuration: 10000, supportThreshold: 0, daoTokenAddress: ethers.constants.AddressZero, - censusStrategy: 0, + censusStrategyURI: 0, minProposerVotingPower: 0, }, ] ), }, ]); - await vocdoniVoting.connect(signers[0]).setTally(0, [[10, 0, 0]]); // tally already approved by signers[0] - await vocdoniVoting.connect(signers[1]).approveTally(0, true); + setTimeForNextBlock( + (await ethers.provider.getBlock('latest')).timestamp + 4000 + ); + + await vocdoniVoting.connect(signers[0]).setTally(0, [[10, 0, 0]]); // tally already approved by signers[0] await ethers.provider.send('evm_setAutomine', [false]); - await vocdoniVoting - .connect(signers[0]) - .createProposal( - ethers.utils.randomBytes(32), - 0, - vocdoniProposalParams, - dummyActions - ); + await vocdoniVoting.connect(signers[1]).approveTally(0, true); await expect( - vocdoniVoting.connect(signers[0]).setTally(1, [[10, 0, 0]]) - ).to.revertedWithCustomError( + vocdoniVoting + .connect(signers[0]) + .createProposal( + ethers.utils.randomBytes(32), + 0, + vocdoniProposalParams, + dummyActions + ) + ).to.be.revertedWithCustomError( vocdoniVoting, 'PluginSettingsUpdatedTooRecently' ); - await ethers.provider.send('evm_setAutomine', [true]); }); context('onlyCommitteProposalCreation', async () => { it('creates a proposal when unlisted accounts are allowed', async () => { - vocdoniVotingSettings.onlyCommitteeProposalCreation = false; + vocdoniVotingSettings.onlyExecutionMultisigProposalCreation = false; await vocdoniVoting.initialize( dao.address, [signers[0].address], // signers[0] is listed vocdoniVotingSettings ); + await expect( vocdoniVoting .connect(signers[2]) // not listed @@ -653,8 +829,8 @@ describe('Vocdoni Plugin', function () { }); it('creates a proposal when unlisted accounts are allowed and have tokens', async () => { - vocdoniVotingSettings.onlyCommitteeProposalCreation = false; - vocdoniVotingSettings.minProposerVotingPower = 1; + vocdoniVotingSettings.onlyExecutionMultisigProposalCreation = false; + vocdoniVotingSettings.minProposerVotingPower = BigNumber.from(1); await setBalances([{receiver: signers[2].address, amount: 1}]); await vocdoniVoting.initialize( @@ -700,7 +876,7 @@ describe('Vocdoni Plugin', function () { vocdoniProposalParams, dummyActions ) - ).to.revertedWithCustomError(vocdoniVoting, 'OnlyCommittee'); + ).to.revertedWithCustomError(vocdoniVoting, 'OnlyExecutionMultisig'); }); }); @@ -746,7 +922,7 @@ describe('Vocdoni Plugin', function () { it('reverts if invalid end date', async () => { let currentBlock = await ethers.provider.getBlock('latest'); - vocdoniProposalParams.endDate = currentBlock.timestamp; + vocdoniProposalParams.voteEndDate = currentBlock.timestamp; await expect( vocdoniVoting .connect(signers[0]) @@ -756,9 +932,9 @@ describe('Vocdoni Plugin', function () { vocdoniProposalParams, dummyActions ) - ).to.be.revertedWithCustomError(vocdoniVoting, 'InvalidEndDate'); + ).to.be.revertedWithCustomError(vocdoniVoting, 'InvalidVoteEndDate'); - vocdoniProposalParams.endDate = currentBlock.timestamp - 1; + vocdoniProposalParams.voteEndDate = currentBlock.timestamp - 1; await expect( vocdoniVoting .connect(signers[0]) @@ -768,9 +944,9 @@ describe('Vocdoni Plugin', function () { vocdoniProposalParams, dummyActions ) - ).to.be.revertedWithCustomError(vocdoniVoting, 'InvalidEndDate'); + ).to.be.revertedWithCustomError(vocdoniVoting, 'InvalidVoteEndDate'); - vocdoniProposalParams.endDate = 0; + vocdoniProposalParams.voteEndDate = 0; await expect( vocdoniVoting .connect(signers[0]) @@ -785,19 +961,7 @@ describe('Vocdoni Plugin', function () { it('reverts if invalid expiration date', async () => { let currentBlock = await ethers.provider.getBlock('latest'); - vocdoniProposalParams.expirationDate = currentBlock.timestamp; - await expect( - vocdoniVoting - .connect(signers[0]) - .createProposal( - ethers.utils.randomBytes(32), - 0, - vocdoniProposalParams, - dummyActions - ) - ).to.be.revertedWithCustomError(vocdoniVoting, 'InvalidExpirationDate'); - - vocdoniProposalParams.expirationDate = currentBlock.timestamp - 2; + vocdoniProposalParams.tallyEndDate = 10; await expect( vocdoniVoting .connect(signers[0]) @@ -807,19 +971,7 @@ describe('Vocdoni Plugin', function () { vocdoniProposalParams, dummyActions ) - ).to.be.revertedWithCustomError(vocdoniVoting, 'InvalidExpirationDate'); - - vocdoniProposalParams.expirationDate = currentBlock.timestamp + 1000; - await expect( - vocdoniVoting - .connect(signers[0]) - .createProposal( - ethers.utils.randomBytes(32), - 0, - vocdoniProposalParams, - dummyActions - ) - ).to.not.be.reverted; + ).to.be.revertedWithCustomError(vocdoniVoting, 'InvalidTallyEndDate'); }); }); }); @@ -829,7 +981,7 @@ describe('Vocdoni Plugin', function () { vocdoniVotingSettings.minTallyApprovals = 1; }); - it('reverts if not a committee member', async () => { + it('reverts if not a executionMultisig member', async () => { await vocdoniVoting.initialize( dao.address, [signers[0].address], // signers[0] is listed @@ -846,11 +998,12 @@ describe('Vocdoni Plugin', function () { ).not.to.be.reverted; await expect(vocdoniVoting.connect(signers[1]).setTally(0, [[10, 0, 0]])) - .to.be.revertedWithCustomError(vocdoniVoting, 'OnlyCommittee') + .to.be.revertedWithCustomError(vocdoniVoting, 'OnlyExecutionMultisig') .withArgs(signers[1].address); }); - it('reverts if plugin settings changed after the process stored security block', async () => { + it('reverts if plugin settings changed in the same block', async () => { + vocdoniVotingSettings.minTallyApprovals = 2; await vocdoniVoting.initialize( dao.address, [signers[0].address, signers[1].address], // signers[0] is listed @@ -870,18 +1023,12 @@ describe('Vocdoni Plugin', function () { vocdoniProposalParams, dummyActions ); + // change plugin settings const vochainProposalId = ethers.utils.randomBytes(32); - let proposalParams = { - censusBlock: await ethers.provider.getBlockNumber(), - securityBlock: await ethers.provider.getBlockNumber(), - startDate: 0, - endDate: 0, - expirationDate: await timestampIn(1000), - }; await vocdoniVoting .connect(signers[0]) - .createProposal(vochainProposalId, 0, proposalParams, [ + .createProposal(vochainProposalId, 0, vocdoniProposalParams, [ { to: vocdoniVoting.address, value: 0, @@ -889,19 +1036,28 @@ describe('Vocdoni Plugin', function () { 'updatePluginSettings', [ { - minTallyApprovals: 1, - minDuration: 1, + minTallyApprovals: 2, + minVoteDuration: 3601, + minTallyDuration: 10000, minParticipation: 0, supportThreshold: 0, daoTokenAddress: ethers.constants.AddressZero, - censusStrategy: 'TKN', - minProposerVotingPower: 0, + censusStrategyURI: 'TKN', + minProposerVotingPower: 1, }, ] ), }, ]); + + setTimeForNextBlock( + (await ethers.provider.getBlock('latest')).timestamp + 4000 + ); await vocdoniVoting.connect(signers[0]).setTally(1, [[10, 0, 0]]); // tally already approved by signers[0] + + // set automine to false + await ethers.provider.send('evm_setAutomine', [false]); + await vocdoniVoting.connect(signers[1]).approveTally(1, true); // should revert if trying to set tally on process 0 @@ -911,6 +1067,9 @@ describe('Vocdoni Plugin', function () { vocdoniVoting, 'PluginSettingsUpdatedTooRecently' ); + + // set automine to true + await ethers.provider.send('evm_setAutomine', [true]); }); it('reverts if process not in tally phase', async () => { @@ -924,19 +1083,12 @@ describe('Vocdoni Plugin', function () { dao.address, await vocdoniVoting.UPDATE_PLUGIN_SETTINGS_PERMISSION_ID() ); - let proposalParams = { - censusBlock: await ethers.provider.getBlockNumber(), - securityBlock: await ethers.provider.getBlockNumber(), - startDate: 0, - endDate: await timestampIn(3000), - expirationDate: await timestampIn(4000), - }; await vocdoniVoting .connect(signers[0]) .createProposal( ethers.utils.randomBytes(32), 0, - proposalParams, + vocdoniProposalParams, dummyActions ); await expect( @@ -945,8 +1097,6 @@ describe('Vocdoni Plugin', function () { }); it('reverts if invalid tally', async () => { - vocdoniProposalParams.expirationDate = - (await ethers.provider.getBlock('latest')).timestamp + 1000; await vocdoniVoting.initialize( dao.address, [signers[0].address, signers[1].address], // signers[0] is listed @@ -965,6 +1115,9 @@ describe('Vocdoni Plugin', function () { vocdoniProposalParams, dummyActions ); + setTimeForNextBlock( + (await ethers.provider.getBlock('latest')).timestamp + 4000 + ); await expect( vocdoniVoting.connect(signers[0]).setTally(0, [ [10, 0, 0], @@ -978,8 +1131,6 @@ describe('Vocdoni Plugin', function () { }); it('reverts if trying to set same tally twice', async () => { - vocdoniProposalParams.expirationDate = - (await ethers.provider.getBlock('latest')).timestamp + 1000; vocdoniVotingSettings.minTallyApprovals = 2; await vocdoniVoting.initialize( dao.address, @@ -999,6 +1150,9 @@ describe('Vocdoni Plugin', function () { vocdoniProposalParams, dummyActions ); + setTimeForNextBlock( + (await ethers.provider.getBlock('latest')).timestamp + 4000 + ); await expect(vocdoniVoting.connect(signers[0]).setTally(0, [[10, 0, 0]])) .to.not.reverted; @@ -1008,8 +1162,6 @@ describe('Vocdoni Plugin', function () { }); it('reverts if trying to set the tally and already approved', async () => { - vocdoniProposalParams.expirationDate = - (await ethers.provider.getBlock('latest')).timestamp + 1000; vocdoniVotingSettings.minTallyApprovals = 1; await vocdoniVoting.initialize( dao.address, @@ -1029,6 +1181,10 @@ describe('Vocdoni Plugin', function () { vocdoniProposalParams, dummyActions ); + + setTimeForNextBlock( + (await ethers.provider.getBlock('latest')).timestamp + 4000 + ); await expect(vocdoniVoting.connect(signers[0]).setTally(0, [[10, 0, 0]])) .to.not.reverted; @@ -1037,9 +1193,7 @@ describe('Vocdoni Plugin', function () { ).to.be.revertedWithCustomError(vocdoniVoting, 'TallyAlreadyApproved'); }); - it('resets the approval counter if tally is already set but changed by another committee member', async () => { - vocdoniProposalParams.expirationDate = - (await ethers.provider.getBlock('latest')).timestamp + 1000; + it('resets the approval counter if tally is already set but changed by another executionMultisig member', async () => { vocdoniVotingSettings.minTallyApprovals = 2; await vocdoniVoting.initialize( dao.address, @@ -1059,6 +1213,10 @@ describe('Vocdoni Plugin', function () { vocdoniProposalParams, dummyActions ); + + setTimeForNextBlock( + (await ethers.provider.getBlock('latest')).timestamp + 4000 + ); await expect(vocdoniVoting.connect(signers[0]).setTally(0, [[10, 0, 0]])) .to.not.reverted; @@ -1074,8 +1232,6 @@ describe('Vocdoni Plugin', function () { }); it('sets the tally correctly and modifies the approvers count', async () => { - vocdoniProposalParams.expirationDate = - (await ethers.provider.getBlock('latest')).timestamp + 1000; vocdoniVotingSettings.minTallyApprovals = 2; await vocdoniVoting.initialize( dao.address, @@ -1095,6 +1251,10 @@ describe('Vocdoni Plugin', function () { vocdoniProposalParams, dummyActions ); + + setTimeForNextBlock( + (await ethers.provider.getBlock('latest')).timestamp + 4000 + ); await expect(vocdoniVoting.connect(signers[0]).setTally(0, [[10, 0, 0]])) .to.not.reverted; @@ -1128,11 +1288,15 @@ describe('Vocdoni Plugin', function () { vocdoniProposalParams, dummyActions ); + + setTimeForNextBlock( + (await ethers.provider.getBlock('latest')).timestamp + 4000 + ); let setTallyTx = await vocdoniVoting .connect(signers[0]) .setTally(0, [[10, 0, 0]]); expect(setTallyTx).to.emit(vocdoniVoting, VOCDONI_EVENTS.TALLY_SET); - expect(setTallyTx).to.emit(vocdoniVoting, VOCDONI_EVENTS.TALLY_APPROVED); + expect(setTallyTx).to.emit(vocdoniVoting, VOCDONI_EVENTS.TALLY_APPROVAL); }); }); @@ -1141,9 +1305,7 @@ describe('Vocdoni Plugin', function () { vocdoniVotingSettings.minTallyApprovals = 1; }); - it('reverts if not a committee member', async () => { - vocdoniProposalParams.expirationDate = - (await ethers.provider.getBlock('latest')).timestamp + 1000; + it('reverts if not a executionMultisig member', async () => { await vocdoniVoting.initialize( dao.address, [signers[0].address], // signers[0] is listed @@ -1162,16 +1324,17 @@ describe('Vocdoni Plugin', function () { vocdoniProposalParams, dummyActions ); + setTimeForNextBlock( + (await ethers.provider.getBlock('latest')).timestamp + 4000 + ); await expect(vocdoniVoting.connect(signers[0]).setTally(0, [[10, 0, 0]])) .to.not.reverted; await expect(vocdoniVoting.connect(signers[1]).approveTally(0, false)) - .to.be.revertedWithCustomError(vocdoniVoting, 'OnlyCommittee') + .to.be.revertedWithCustomError(vocdoniVoting, 'OnlyExecutionMultisig') .withArgs(signers[1].address); }); it('reverts if tally is not set', async () => { - vocdoniProposalParams.expirationDate = - (await ethers.provider.getBlock('latest')).timestamp + 1000; await vocdoniVoting.initialize( dao.address, [signers[0].address], // signers[0] is listed @@ -1190,14 +1353,16 @@ describe('Vocdoni Plugin', function () { vocdoniProposalParams, dummyActions ); + setTimeForNextBlock( + (await ethers.provider.getBlock('latest')).timestamp + 4000 + ); await expect( vocdoniVoting.connect(signers[0]).approveTally(0, false) ).to.be.revertedWithCustomError(vocdoniVoting, 'InvalidTally'); }); - it('committee member can approve the tally only once if not changed', async () => { - vocdoniProposalParams.expirationDate = - (await ethers.provider.getBlock('latest')).timestamp + 1000; + it('executionMultisig member can approve the tally only once if not changed', async () => { + vocdoniVotingSettings.minTallyApprovals = 2; await vocdoniVoting.initialize( dao.address, [signers[0].address, signers[1].address], // signers[0] is listed @@ -1208,7 +1373,6 @@ describe('Vocdoni Plugin', function () { dao.address, await vocdoniVoting.UPDATE_PLUGIN_SETTINGS_PERMISSION_ID() ); - vocdoniProposalParams.expirationDate = await timestampIn(10000); await vocdoniVoting .connect(signers[0]) .createProposal( @@ -1217,11 +1381,17 @@ describe('Vocdoni Plugin', function () { vocdoniProposalParams, dummyActions ); + setTimeForNextBlock( + (await ethers.provider.getBlock('latest')).timestamp + 4000 + ); await expect(vocdoniVoting.connect(signers[0]).setTally(0, [[10, 0, 0]])) .to.not.reverted; await expect( vocdoniVoting.connect(signers[0]).approveTally(0, false) - ).to.be.revertedWithCustomError(vocdoniVoting, 'TallyAlreadyApproved'); + ).to.be.revertedWithCustomError( + vocdoniVoting, + 'TallyAlreadyApprovedBySender' + ); // approve with signer[1] await expect(vocdoniVoting.connect(signers[1]).approveTally(0, false)).to .not.reverted; @@ -1230,9 +1400,7 @@ describe('Vocdoni Plugin', function () { expect(proposal.approvers.length).to.be.equal(2); }); - it('committee member can approve the tally only once if changed', async () => { - vocdoniProposalParams.expirationDate = - (await ethers.provider.getBlock('latest')).timestamp + 1000; + it('executionMultisig member can approve the tally only once if changed', async () => { vocdoniVotingSettings.minTallyApprovals = 2; await vocdoniVoting.initialize( dao.address, @@ -1244,7 +1412,7 @@ describe('Vocdoni Plugin', function () { dao.address, await vocdoniVoting.UPDATE_PLUGIN_SETTINGS_PERMISSION_ID() ); - vocdoniProposalParams.expirationDate = await timestampIn(10000); + vocdoniProposalParams.tallyEndDate = await timestampIn(10000); await vocdoniVoting .connect(signers[0]) .createProposal( @@ -1253,6 +1421,9 @@ describe('Vocdoni Plugin', function () { vocdoniProposalParams, dummyActions ); + setTimeForNextBlock( + (await ethers.provider.getBlock('latest')).timestamp + 4000 + ); await expect(vocdoniVoting.connect(signers[0]).setTally(0, [[10, 0, 0]])) .to.not.reverted; @@ -1269,12 +1440,13 @@ describe('Vocdoni Plugin', function () { await expect( vocdoniVoting.connect(signers[1]).approveTally(0, false) - ).to.be.revertedWithCustomError(vocdoniVoting, 'TallyAlreadyApproved'); + ).to.be.revertedWithCustomError( + vocdoniVoting, + 'TallyAlreadyApprovedBySender' + ); }); it('emits an event when the tally is approved', async () => { - vocdoniProposalParams.expirationDate = - (await ethers.provider.getBlock('latest')).timestamp + 1000; vocdoniVotingSettings.minTallyApprovals = 2; await vocdoniVoting.initialize( dao.address, @@ -1286,8 +1458,6 @@ describe('Vocdoni Plugin', function () { dao.address, await vocdoniVoting.UPDATE_PLUGIN_SETTINGS_PERMISSION_ID() ); - - vocdoniProposalParams.expirationDate = await timestampIn(10000); await vocdoniVoting .connect(signers[0]) .createProposal( @@ -1296,13 +1466,15 @@ describe('Vocdoni Plugin', function () { vocdoniProposalParams, dummyActions ); - + setTimeForNextBlock( + (await ethers.provider.getBlock('latest')).timestamp + 4000 + ); await expect(vocdoniVoting.connect(signers[0]).setTally(0, [[10, 0, 0]])) .to.not.reverted; await expect( vocdoniVoting.connect(signers[1]).approveTally(0, false) - ).to.emit(vocdoniVoting, VOCDONI_EVENTS.TALLY_APPROVED); + ).to.emit(vocdoniVoting, VOCDONI_EVENTS.TALLY_APPROVAL); }); }); @@ -1322,7 +1494,6 @@ describe('Vocdoni Plugin', function () { dao.address, await vocdoniVoting.UPDATE_PLUGIN_SETTINGS_PERMISSION_ID() ); - vocdoniProposalParams.endDate = await timestampIn(10000); await vocdoniVoting .connect(signers[0]) .createProposal( @@ -1331,9 +1502,12 @@ describe('Vocdoni Plugin', function () { vocdoniProposalParams, dummyActions ); + setTimeForNextBlock( + (await ethers.provider.getBlock('latest')).timestamp + 4000 + ); await expect( vocdoniVoting.connect(signers[0]).executeProposal(0) - ).to.be.revertedWithCustomError(vocdoniVoting, 'ProposalNotInTallyPhase'); + ).to.be.revertedWithCustomError(vocdoniVoting, 'InvalidTally'); }); it('reverts if tally is not set', async () => { @@ -1348,7 +1522,9 @@ describe('Vocdoni Plugin', function () { dao.address, await vocdoniVoting.UPDATE_PLUGIN_SETTINGS_PERMISSION_ID() ); - vocdoniProposalParams.expirationDate = await timestampIn(10000); + setTimeForNextBlock( + (await ethers.provider.getBlock('latest')).timestamp + 4000 + ); await vocdoniVoting .connect(signers[0]) .createProposal( @@ -1374,7 +1550,6 @@ describe('Vocdoni Plugin', function () { dao.address, await vocdoniVoting.UPDATE_PLUGIN_SETTINGS_PERMISSION_ID() ); - vocdoniProposalParams.expirationDate = await timestampIn(10000); await vocdoniVoting .connect(signers[0]) .createProposal( @@ -1383,6 +1558,9 @@ describe('Vocdoni Plugin', function () { vocdoniProposalParams, dummyActions ); + setTimeForNextBlock( + (await ethers.provider.getBlock('latest')).timestamp + 4000 + ); // set tally await expect(vocdoniVoting.connect(signers[0]).setTally(0, [[10, 0, 0]])) .to.not.reverted; @@ -1395,7 +1573,8 @@ describe('Vocdoni Plugin', function () { it('reverts if min participation not reached', async () => { vocdoniVotingSettings.minTallyApprovals = 1; - vocdoniVotingSettings.minParticipation = 50; + vocdoniVotingSettings.minParticipation = 200000; + vocdoniProposalParams.totalVotingPower = BigNumber.from(100); await vocdoniVoting.initialize( dao.address, [signers[0].address, signers[1].address], // signers[0] is listed @@ -1406,7 +1585,6 @@ describe('Vocdoni Plugin', function () { dao.address, await vocdoniVoting.UPDATE_PLUGIN_SETTINGS_PERMISSION_ID() ); - vocdoniProposalParams.expirationDate = await timestampIn(10000); await vocdoniVoting .connect(signers[0]) .createProposal( @@ -1415,8 +1593,11 @@ describe('Vocdoni Plugin', function () { vocdoniProposalParams, dummyActions ); + setTimeForNextBlock( + (await ethers.provider.getBlock('latest')).timestamp + 4000 + ); // set tally - await expect(vocdoniVoting.connect(signers[0]).setTally(0, [[1, 2, 0]])) + await expect(vocdoniVoting.connect(signers[0]).setTally(0, [[1, 18, 0]])) .to.not.reverted; // try to execute @@ -1441,7 +1622,6 @@ describe('Vocdoni Plugin', function () { dao.address, await vocdoniVoting.UPDATE_PLUGIN_SETTINGS_PERMISSION_ID() ); - vocdoniProposalParams.expirationDate = await timestampIn(10000); await vocdoniVoting .connect(signers[0]) .createProposal( @@ -1450,6 +1630,9 @@ describe('Vocdoni Plugin', function () { vocdoniProposalParams, dummyActions ); + setTimeForNextBlock( + (await ethers.provider.getBlock('latest')).timestamp + 4000 + ); // set tally await expect( vocdoniVoting.connect(signers[0]).setTally(0, [[1, 200000, 0]]) @@ -1464,9 +1647,40 @@ describe('Vocdoni Plugin', function () { ); }); + // reverts if already executed + it('reverts if already executed', async () => { + vocdoniVotingSettings.minTallyApprovals = 1; + vocdoniVotingSettings.supportThreshold = 0; + await vocdoniVoting.initialize( + dao.address, + [signers[0].address], // signers[0] is listed + vocdoniVotingSettings + ); + await expect( + vocdoniVoting + .connect(signers[0]) + .createProposal( + ethers.utils.randomBytes(32), + 0, + vocdoniProposalParams, + dummyActions + ) + ).to.not.be.reverted; + setTimeForNextBlock( + (await ethers.provider.getBlock('latest')).timestamp + 4000 + ); + await expect(vocdoniVoting.connect(signers[0]).setTally(0, [[10, 0, 0]])) + .to.not.be.reverted; + + await expect(vocdoniVoting.connect(signers[0]).executeProposal(0)).to.not + .be.reverted; + + await expect( + vocdoniVoting.connect(signers[0]).executeProposal(0) + ).to.be.revertedWithCustomError(vocdoniVoting, 'ProposalAlreadyExecuted'); + }); + it('emit an event if proposal executed', async () => { - vocdoniProposalParams.expirationDate = - (await ethers.provider.getBlock('latest')).timestamp + 1000; vocdoniVotingSettings.minTallyApprovals = 1; await vocdoniVoting.initialize( dao.address, @@ -1486,6 +1700,9 @@ describe('Vocdoni Plugin', function () { vocdoniProposalParams, dummyActions ); + setTimeForNextBlock( + (await ethers.provider.getBlock('latest')).timestamp + 4000 + ); // set tally await expect(vocdoniVoting.connect(signers[0]).setTally(0, [[10, 0, 0]])) .to.not.reverted;