From f980891f29e52933ded848fe31aae6de078331ad Mon Sep 17 00:00:00 2001 From: Jordaniza Date: Wed, 5 Jun 2024 15:24:40 +0400 Subject: [PATCH] feat: removed majority voting construct --- packages/contracts/src/ITokenVoting.sol | 253 ++++++++++ packages/contracts/src/TokenVoting.sol | 434 +++++++++++++++++- packages/contracts/src/TokenVotingSetup.sol | 6 +- .../src/mocks/MajorityVotingMock.sol | 2 +- .../src/{ => old}/IMajorityVoting.sol | 0 .../src/{ => old}/MajorityVotingBase.sol | 0 packages/contracts/src/old/_TokenVoting.sol | 248 ++++++++++ .../contracts/src/old/_TokenVotingSetup.sol | 284 ++++++++++++ 8 files changed, 1204 insertions(+), 23 deletions(-) create mode 100644 packages/contracts/src/ITokenVoting.sol rename packages/contracts/src/{ => old}/IMajorityVoting.sol (100%) rename packages/contracts/src/{ => old}/MajorityVotingBase.sol (100%) create mode 100644 packages/contracts/src/old/_TokenVoting.sol create mode 100644 packages/contracts/src/old/_TokenVotingSetup.sol diff --git a/packages/contracts/src/ITokenVoting.sol b/packages/contracts/src/ITokenVoting.sol new file mode 100644 index 00000000..5e5fc9cf --- /dev/null +++ b/packages/contracts/src/ITokenVoting.sol @@ -0,0 +1,253 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.8; + +import {IDAO} from "@aragon/osx-commons-contracts/src/dao/IDAO.sol"; +import {IVotesUpgradeable} from "@openzeppelin/contracts-upgradeable/governance/utils/IVotesUpgradeable.sol"; +import {IMembership} from "@aragon/osx-commons-contracts/src/plugin/extensions/membership/IMembership.sol"; + +interface ITokenVoting { + /// @notice Vote options that a voter can chose from. + /// @param None The default option state of a voter indicating the absence from the vote. + /// This option neither influences support nor participation. + /// @param Abstain This option does not influence the support but counts towards participation. + /// @param Yes This option increases the support and counts towards participation. + /// @param No This option decreases the support and counts towards participation. + enum VoteOption { + None, + Abstain, + Yes, + No + } + + /// @notice The different voting modes available. + /// @param Standard In standard mode, early execution and vote replacement are disabled. + /// @param EarlyExecution In early execution mode, a proposal can be executed + /// early before the end date if the vote outcome cannot mathematically change by more voters voting. + /// @param VoteReplacement In vote replacement mode, voters can change their vote + /// multiple times and only the latest vote option is tallied. + enum VotingMode { + Standard, + EarlyExecution, + VoteReplacement + } + + /// @notice A container for the majority voting settings that will be applied as parameters on proposal creation. + /// @param votingMode A parameter to select the vote mode. + /// In standard mode (0), early execution and vote replacement are disabled. + /// In early execution mode (1), a proposal can be executed early before the end date + /// if the vote outcome cannot mathematically change by more voters voting. + /// In vote replacement mode (2), voters can change their vote multiple times + /// and only the latest vote option is tallied. + /// @param supportThreshold The support threshold value. + /// Its value has to be in the interval [0, 10^6] defined by `RATIO_BASE = 10**6`. + /// @param minParticipation The minimum participation value. + /// Its value has to be in the interval [0, 10^6] defined by `RATIO_BASE = 10**6`. + /// @param minDuration The minimum duration of the proposal vote in seconds. + /// @param minProposerVotingPower The minimum voting power required to create a proposal. + struct VotingSettings { + VotingMode votingMode; + uint32 supportThreshold; + uint32 minParticipation; + uint64 minDuration; + uint256 minProposerVotingPower; + } + + /// @notice A container for proposal-related information. + /// @param executed Whether the proposal is executed or not. + /// @param parameters The proposal parameters at the time of the proposal creation. + /// @param tally The vote tally of the proposal. + /// @param voters The votes casted by the voters. + /// @param actions The actions to be executed when 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. + struct Proposal { + bool executed; + ProposalParameters parameters; + Tally tally; + mapping(address => VoteOption) voters; + IDAO.Action[] actions; + uint256 allowFailureMap; + } + + /// @notice A container for the proposal parameters at the time of proposal creation. + /// @param votingMode A parameter to select the vote mode. + /// @param supportThreshold The support threshold value. + /// The value has to be in the interval [0, 10^6] defined by `RATIO_BASE = 10**6`. + /// @param startDate The start date of the proposal vote. + /// @param endDate The end date of the proposal vote. + /// @param snapshotBlock The number of the block prior to the proposal creation. + /// @param minVotingPower The minimum voting power needed. + struct ProposalParameters { + VotingMode votingMode; + uint32 supportThreshold; + uint64 startDate; + uint64 endDate; + uint64 snapshotBlock; + uint256 minVotingPower; + } + + /// @notice A container for the proposal vote tally. + /// @param abstain The number of abstain votes casted. + /// @param yes The number of yes votes casted. + /// @param no The number of no votes casted. + struct Tally { + uint256 abstain; + uint256 yes; + uint256 no; + } + + /// ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + /// --------- SETTERS --------- + /// ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + /// @notice Creates a new majority voting proposal. + /// @param _metadata The metadata of the proposal. + /// @param _actions The actions that will be executed after the proposal passes. + /// @param _allowFailureMap Allows proposal to succeed even if an action reverts. + /// Uses bitmap representation. + /// If the bit at index `x` is 1, the tx succeeds even if the action at `x` failed. + /// Passing 0 will be treated as atomic execution. + /// @param _startDate The start date of the proposal vote. + /// If 0, the current timestamp is used and the vote starts immediately. + /// @param _endDate The end date of the proposal vote. + /// If 0, `_startDate + minDuration` is used. + /// @param _voteOption The chosen vote option to be casted on proposal creation. + /// @param _tryEarlyExecution If `true`, early execution is tried after the vote cast. + /// The call does not revert if early execution is not possible. + /// @return proposalId The ID of the proposal. + function createProposal( + bytes calldata _metadata, + IDAO.Action[] calldata _actions, + uint256 _allowFailureMap, + uint64 _startDate, + uint64 _endDate, + VoteOption _voteOption, + bool _tryEarlyExecution + ) external returns (uint256 proposalId); + + /// @notice Votes for a vote option and, optionally, executes the proposal. + /// @dev `_voteOption`, 1 -> abstain, 2 -> yes, 3 -> no + /// @param _proposalId The ID of the proposal. + /// @param _voteOption The chosen vote option. + /// @param _tryEarlyExecution If `true`, early execution is tried after the vote cast. + /// The call does not revert if early execution is not possible. + function vote(uint256 _proposalId, VoteOption _voteOption, bool _tryEarlyExecution) external; + + /// @notice Executes a proposal. + /// @param _proposalId The ID of the proposal to be executed. + function execute(uint256 _proposalId) external; + + /// @notice Updates the voting settings. + /// @param _votingSettings The new voting settings. + function updateVotingSettings(VotingSettings calldata _votingSettings) external; + + /// ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + /// --------- GETTERS --------- + /// ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + /// @notice getter function for the voting token. + /// @dev external function also useful for registering interfaceId + /// and for distinguishing from majority voting interface. + /// @return The token used for voting. + function getVotingToken() external view returns (IVotesUpgradeable); + + /// @notice Returns whether the account has voted for the proposal. + /// Note, that this does not check if the account has voting power. + /// @param _proposalId The ID of the proposal. + /// @param _account The account address to be checked. + /// @return The vote option cast by a voter for a certain proposal. + function getVoteOption( + uint256 _proposalId, + address _account + ) external view returns (VoteOption); + + /// @notice Returns all information for a proposal vote by its ID. + /// @param _proposalId The ID of the proposal. + /// @return open Whether the proposal is open or not. + /// @return executed Whether the proposal is executed or not. + /// @return parameters The parameters of the proposal vote. + /// @return tally The current tally of the proposal vote. + /// @return actions The actions to be executed in the associated DAO after the proposal has passed. + /// @return allowFailureMap The bit map representations of which actions are allowed to revert so tx still succeeds. + function getProposal( + uint256 _proposalId + ) + external + view + returns ( + bool open, + bool executed, + ProposalParameters memory parameters, + Tally memory tally, + IDAO.Action[] memory actions, + uint256 allowFailureMap + ); + + /// @notice Returns the total voting power checkpointed for a specific block number. + /// @param _blockNumber The block number. + /// @return The total voting power. + function totalVotingPower(uint256 _blockNumber) external view returns (uint256); + + /// @notice Returns the support threshold parameter stored in the voting settings. + /// @return The support threshold parameter. + function supportThreshold() external view returns (uint32); + + /// @notice Returns the minimum participation parameter stored in the voting settings. + /// @return The minimum participation parameter. + function minParticipation() external view returns (uint32); + + /// @notice Returns the minimum duration parameter stored in the voting settings. + /// @return The minimum duration parameter. + function minDuration() external view returns (uint64); + + /// @notice Returns the minimum voting power required to create a proposal stored in the voting settings. + /// @return The minimum voting power required to create a proposal. + function minProposerVotingPower() external view returns (uint256); + + /// @notice Returns the vote mode stored in the voting settings. + /// @return The vote mode parameter. + function votingMode() external view returns (VotingMode); + + /// @notice Checks if the support value defined as: + /// $$\texttt{support} = \frac{N_\text{yes}}{N_\text{yes}+N_\text{no}}$$ + /// for a proposal vote is greater than the support threshold. + /// @param _proposalId The ID of the proposal. + /// @return Returns `true` if the support is greater than the support threshold and `false` otherwise. + function isSupportThresholdReached(uint256 _proposalId) external view returns (bool); + + /// @notice Checks if the worst-case support value defined as: + /// $$\texttt{worstCaseSupport} = \frac{N_\text{yes}}{ N_\text{total}-N_\text{abstain}}$$ + /// for a proposal vote is greater than the support threshold. + /// @param _proposalId The ID of the proposal. + /// @return Returns `true` if the worst-case support is greater than the support threshold and `false` otherwise. + function isSupportThresholdReachedEarly(uint256 _proposalId) external view returns (bool); + + /// @notice Checks if the participation value defined as: + /// $$\texttt{participation} = \frac{N_\text{yes}+N_\text{no}+N_\text{abstain}}{N_\text{total}}$$ + /// for a proposal vote is greater or equal than the minimum participation value. + /// @param _proposalId The ID of the proposal. + /// @return Returns `true` if the participation is greater than the minimum participation and `false` otherwise. + function isMinParticipationReached(uint256 _proposalId) external view returns (bool); + + /// @notice Checks if an account can participate on a proposal vote. This can be because the vote + /// - has not started, + /// - has ended, + /// - was executed, or + /// - the voter doesn't have voting powers. + /// @param _proposalId The proposal Id. + /// @param _account The account address to be checked. + /// @param _voteOption Whether the voter abstains, supports or opposes the proposal. + /// @return Returns true if the account is allowed to vote. + /// @dev The function assumes the queried proposal exists. + function canVote( + uint256 _proposalId, + address _account, + VoteOption _voteOption + ) external view returns (bool); + + /// @notice Checks if a proposal can be executed. + /// @param _proposalId The ID of the proposal to be checked. + /// @return True if the proposal can be executed, false otherwise. + function canExecute(uint256 _proposalId) external view returns (bool); +} diff --git a/packages/contracts/src/TokenVoting.sol b/packages/contracts/src/TokenVoting.sol index 95a061a0..e95c9420 100644 --- a/packages/contracts/src/TokenVoting.sol +++ b/packages/contracts/src/TokenVoting.sol @@ -10,29 +10,121 @@ import {IMembership} from "@aragon/osx-commons-contracts/src/plugin/extensions/m import {_applyRatioCeiled} from "@aragon/osx-commons-contracts/src/utils/math/Ratio.sol"; import {IDAO} from "@aragon/osx-commons-contracts/src/dao/IDAO.sol"; -import {MajorityVotingBase} from "./MajorityVotingBase.sol"; + +import {ITokenVoting} from "./ITokenVoting.sol"; + +import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {SafeCastUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/math/SafeCastUpgradeable.sol"; + +import {ProposalUpgradeable} from "@aragon/osx-commons-contracts/src/plugin/extensions/proposal/ProposalUpgradeable.sol"; +import {RATIO_BASE, RatioOutOfBounds} from "@aragon/osx-commons-contracts/src/utils/math/Ratio.sol"; +import {PluginUUPSUpgradeable} from "@aragon/osx-commons-contracts/src/plugin/PluginUUPSUpgradeable.sol"; +import {IDAO} from "@aragon/osx-commons-contracts/src/dao/IDAO.sol"; /// @title TokenVoting -/// @author Aragon X - 2021-2023 +/// @author Aragon X - 2021-2024 /// @notice The majority voting implementation using an /// [OpenZeppelin `Votes`](https://docs.openzeppelin.com/contracts/4.x/api/governance#Votes) /// compatible governance token. -/// @dev v1.3 (Release 1, Build 3) +/// @dev v2.0 (Release 2, Build 0) /// @custom:security-contact sirt@aragon.org -contract TokenVoting is IMembership, MajorityVotingBase { +contract TokenVoting is + IMembership, + ITokenVoting, + Initializable, + ERC165Upgradeable, + PluginUUPSUpgradeable, + ProposalUpgradeable +{ using SafeCastUpgradeable for uint256; /// @notice The [ERC-165](https://eips.ethereum.org/EIPS/eip-165) interface ID of the contract. bytes4 internal constant TOKEN_VOTING_INTERFACE_ID = this.initialize.selector ^ this.getVotingToken.selector; + /// @notice The [ERC-165](https://eips.ethereum.org/EIPS/eip-165) interface ID of the contract. + bytes4 internal constant MAJORITY_VOTING_BASE_INTERFACE_ID = + this.minDuration.selector ^ + this.minProposerVotingPower.selector ^ + this.votingMode.selector ^ + this.totalVotingPower.selector ^ + this.getProposal.selector ^ + this.updateVotingSettings.selector ^ + this.createProposal.selector; + + /// @notice The ID of the permission required to call the `updateVotingSettings` function. + bytes32 public constant UPDATE_VOTING_SETTINGS_PERMISSION_ID = + keccak256("UPDATE_VOTING_SETTINGS_PERMISSION"); + + /// @notice A mapping between proposal IDs and proposal information. + // solhint-disable-next-line named-parameters-mapping + mapping(uint256 => Proposal) internal proposals; + + /// @notice The struct storing the voting settings. + VotingSettings private votingSettings; + /// @notice An [OpenZeppelin `Votes`](https://docs.openzeppelin.com/contracts/4.x/api/governance#Votes) /// compatible contract referencing the token being used for voting. IVotesUpgradeable private votingToken; + /// @notice Thrown if a date is out of bounds. + /// @param limit The limit value. + /// @param actual The actual value. + error DateOutOfBounds(uint64 limit, uint64 actual); + + /// @notice Thrown if the minimal duration value is out of bounds (less than one hour or greater than 1 year). + /// @param limit The limit value. + /// @param actual The actual value. + error MinDurationOutOfBounds(uint64 limit, uint64 actual); + + /// @notice Thrown when a sender is not allowed to create a proposal. + /// @param sender The sender address. + error ProposalCreationForbidden(address sender); + + /// @notice Thrown if an account is not allowed to cast a vote. This can be because the vote + /// - has not started, + /// - has ended, + /// - was executed, or + /// - the account doesn't have voting powers. + /// @param proposalId The ID of the proposal. + /// @param account The address of the _account. + /// @param voteOption The chosen vote option. + error VoteCastForbidden(uint256 proposalId, address account, VoteOption voteOption); + + /// @notice Thrown if the proposal execution is forbidden. + /// @param proposalId The ID of the proposal. + error ProposalExecutionForbidden(uint256 proposalId); + /// @notice Thrown if the voting power is zero error NoVotingPower(); + /// @notice Emitted when the voting settings are updated. + /// @param votingMode A parameter to select the vote mode. + /// @param supportThreshold The support threshold value. + /// @param minParticipation The minimum participation value. + /// @param minDuration The minimum duration of the proposal vote in seconds. + /// @param minProposerVotingPower The minimum voting power required to create a proposal. + event VotingSettingsUpdated( + VotingMode votingMode, + uint32 supportThreshold, + uint32 minParticipation, + uint64 minDuration, + uint256 minProposerVotingPower + ); + + /// @notice Emitted when a vote is cast by a voter. + /// @param proposalId The ID of the proposal. + /// @param voter The voter casting the vote. + /// @param voteOption The casted vote option. + /// @param votingPower The voting power behind this vote. + event VoteCast( + uint256 indexed proposalId, + address indexed voter, + VoteOption voteOption, + uint256 votingPower + ); + /// @notice Initializes the component. /// @dev This method is required to support [ERC-1822](https://eips.ethereum.org/EIPS/eip-1822). /// @param _dao The IDAO interface of the associated DAO. @@ -43,7 +135,8 @@ contract TokenVoting is IMembership, MajorityVotingBase { VotingSettings calldata _votingSettings, IVotesUpgradeable _token ) external initializer { - __MajorityVotingBase_init(_dao, _votingSettings); + __PluginUUPSUpgradeable_init(_dao); + _updateVotingSettings(_votingSettings); votingToken = _token; @@ -53,27 +146,32 @@ contract TokenVoting is IMembership, MajorityVotingBase { /// @notice Checks if this or the parent contract supports an interface by its ID. /// @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) { + function supportsInterface( + bytes4 _interfaceId + ) + public + view + virtual + override(ERC165Upgradeable, PluginUUPSUpgradeable, ProposalUpgradeable) + returns (bool) + { return _interfaceId == TOKEN_VOTING_INTERFACE_ID || _interfaceId == type(IMembership).interfaceId || super.supportsInterface(_interfaceId); } - /// @notice getter function for the voting token. - /// @dev public function also useful for registering interfaceId - /// and for distinguishing from majority voting interface. - /// @return The token used for voting. + /// @inheritdoc ITokenVoting function getVotingToken() public view returns (IVotesUpgradeable) { return votingToken; } - /// @inheritdoc MajorityVotingBase - function totalVotingPower(uint256 _blockNumber) public view override returns (uint256) { + /// @inheritdoc ITokenVoting + function totalVotingPower(uint256 _blockNumber) public view returns (uint256) { return votingToken.getPastTotalSupply(_blockNumber); } - /// @inheritdoc MajorityVotingBase + /// @inheritdoc ITokenVoting function createProposal( bytes calldata _metadata, IDAO.Action[] calldata _actions, @@ -82,7 +180,7 @@ contract TokenVoting is IMembership, MajorityVotingBase { uint64 _endDate, VoteOption _voteOption, bool _tryEarlyExecution - ) external override returns (uint256 proposalId) { + ) external returns (uint256 proposalId) { // Check that either `_msgSender` owns enough tokens or has enough voting power from being a delegatee. { uint256 minProposerVotingPower_ = minProposerVotingPower(); @@ -154,6 +252,296 @@ contract TokenVoting is IMembership, MajorityVotingBase { } } + /// @inheritdoc ITokenVoting + function vote( + uint256 _proposalId, + VoteOption _voteOption, + bool _tryEarlyExecution + ) public virtual { + address account = _msgSender(); + + if (!_canVote(_proposalId, account, _voteOption)) { + revert VoteCastForbidden({ + proposalId: _proposalId, + account: account, + voteOption: _voteOption + }); + } + _vote(_proposalId, _voteOption, account, _tryEarlyExecution); + } + + /// @inheritdoc ITokenVoting + function execute(uint256 _proposalId) public virtual { + if (!_canExecute(_proposalId)) { + revert ProposalExecutionForbidden(_proposalId); + } + _execute(_proposalId); + } + + /// @inheritdoc ITokenVoting + function getVoteOption( + uint256 _proposalId, + address _voter + ) public view virtual returns (VoteOption) { + return proposals[_proposalId].voters[_voter]; + } + + /// @inheritdoc ITokenVoting + function canVote( + uint256 _proposalId, + address _voter, + VoteOption _voteOption + ) public view virtual returns (bool) { + return _canVote(_proposalId, _voter, _voteOption); + } + + /// @inheritdoc ITokenVoting + function canExecute(uint256 _proposalId) public view virtual returns (bool) { + return _canExecute(_proposalId); + } + + /// @inheritdoc ITokenVoting + function isSupportThresholdReached(uint256 _proposalId) public view virtual returns (bool) { + Proposal storage proposal_ = proposals[_proposalId]; + + // The code below implements the formula of the support criterion explained in the top of this file. + // `(1 - supportThreshold) * N_yes > supportThreshold * N_no` + return + (RATIO_BASE - proposal_.parameters.supportThreshold) * proposal_.tally.yes > + proposal_.parameters.supportThreshold * proposal_.tally.no; + } + + /// @inheritdoc ITokenVoting + function isSupportThresholdReachedEarly( + uint256 _proposalId + ) public view virtual returns (bool) { + Proposal storage proposal_ = proposals[_proposalId]; + + uint256 noVotesWorstCase = totalVotingPower(proposal_.parameters.snapshotBlock) - + proposal_.tally.yes - + proposal_.tally.abstain; + + // The code below implements the formula of the + // early execution support criterion explained in the top of this file. + // `(1 - supportThreshold) * N_yes > supportThreshold * N_no,worst-case` + return + (RATIO_BASE - proposal_.parameters.supportThreshold) * proposal_.tally.yes > + proposal_.parameters.supportThreshold * noVotesWorstCase; + } + + /// @inheritdoc ITokenVoting + function isMinParticipationReached(uint256 _proposalId) public view virtual returns (bool) { + Proposal storage proposal_ = proposals[_proposalId]; + + // The code below implements the formula of the + // participation criterion explained in the top of this file. + // `N_yes + N_no + N_abstain >= minVotingPower = minParticipation * N_total` + return + proposal_.tally.yes + proposal_.tally.no + proposal_.tally.abstain >= + proposal_.parameters.minVotingPower; + } + + /// @inheritdoc ITokenVoting + function supportThreshold() public view virtual returns (uint32) { + return votingSettings.supportThreshold; + } + + /// @inheritdoc ITokenVoting + function minParticipation() public view virtual returns (uint32) { + return votingSettings.minParticipation; + } + + /// @notice Returns the minimum duration parameter stored in the voting settings. + /// @return The minimum duration parameter. + function minDuration() public view virtual returns (uint64) { + return votingSettings.minDuration; + } + + /// @notice Returns the minimum voting power required to create a proposal stored in the voting settings. + /// @return The minimum voting power required to create a proposal. + function minProposerVotingPower() public view virtual returns (uint256) { + return votingSettings.minProposerVotingPower; + } + + /// @notice Returns the vote mode stored in the voting settings. + /// @return The vote mode parameter. + function votingMode() public view virtual returns (VotingMode) { + return votingSettings.votingMode; + } + + /// @notice Returns all information for a proposal vote by its ID. + /// @param _proposalId The ID of the proposal. + /// @return open Whether the proposal is open or not. + /// @return executed Whether the proposal is executed or not. + /// @return parameters The parameters of the proposal vote. + /// @return tally The current tally of the proposal vote. + /// @return actions The actions to be executed in the associated DAO after the proposal has passed. + /// @return allowFailureMap The bit map representations of which actions are allowed to revert so tx still succeeds. + function getProposal( + uint256 _proposalId + ) + public + view + virtual + returns ( + bool open, + bool executed, + ProposalParameters memory parameters, + Tally memory tally, + IDAO.Action[] memory actions, + uint256 allowFailureMap + ) + { + Proposal storage proposal_ = proposals[_proposalId]; + + open = _isProposalOpen(proposal_); + executed = proposal_.executed; + parameters = proposal_.parameters; + tally = proposal_.tally; + actions = proposal_.actions; + allowFailureMap = proposal_.allowFailureMap; + } + + /// @notice Updates the voting settings. + /// @param _votingSettings The new voting settings. + function updateVotingSettings( + VotingSettings calldata _votingSettings + ) external virtual auth(UPDATE_VOTING_SETTINGS_PERMISSION_ID) { + _updateVotingSettings(_votingSettings); + } + + /// @notice Internal function to execute a vote. It assumes the queried proposal exists. + /// @param _proposalId The ID of the proposal. + function _execute(uint256 _proposalId) internal virtual { + proposals[_proposalId].executed = true; + + _executeProposal( + dao(), + _proposalId, + proposals[_proposalId].actions, + proposals[_proposalId].allowFailureMap + ); + } + + /// @notice Internal function to check if a proposal can be executed. It assumes the queried proposal exists. + /// @param _proposalId The ID of the proposal. + /// @return True if the proposal can be executed, false otherwise. + /// @dev Threshold and minimal values are compared with `>` and `>=` comparators, respectively. + function _canExecute(uint256 _proposalId) internal view virtual returns (bool) { + Proposal storage proposal_ = proposals[_proposalId]; + + // Verify that the vote has not been executed already. + if (proposal_.executed) { + return false; + } + + if (_isProposalOpen(proposal_)) { + // Early execution + if (proposal_.parameters.votingMode != VotingMode.EarlyExecution) { + return false; + } + if (!isSupportThresholdReachedEarly(_proposalId)) { + return false; + } + } else { + // Normal execution + if (!isSupportThresholdReached(_proposalId)) { + return false; + } + } + if (!isMinParticipationReached(_proposalId)) { + return false; + } + + return true; + } + + /// @notice Internal function to check if a proposal vote is still open. + /// @param proposal_ The proposal struct. + /// @return True if the proposal vote is open, false otherwise. + function _isProposalOpen(Proposal storage proposal_) internal view virtual returns (bool) { + uint64 currentTime = block.timestamp.toUint64(); + + return + proposal_.parameters.startDate <= currentTime && + currentTime < proposal_.parameters.endDate && + !proposal_.executed; + } + + /// @notice Internal function to update the plugin-wide proposal vote settings. + /// @param _votingSettings The voting settings to be validated and updated. + function _updateVotingSettings(VotingSettings calldata _votingSettings) internal virtual { + // Require the support threshold value to be in the interval [0, 10^6-1], + // because `>` comparision is used in the support criterion and >100% could never be reached. + if (_votingSettings.supportThreshold > RATIO_BASE - 1) { + revert RatioOutOfBounds({ + limit: RATIO_BASE - 1, + actual: _votingSettings.supportThreshold + }); + } + + // Require the minimum participation value to be in the interval [0, 10^6], + // because `>=` comparision is used in the participation criterion. + if (_votingSettings.minParticipation > RATIO_BASE) { + revert RatioOutOfBounds({limit: RATIO_BASE, actual: _votingSettings.minParticipation}); + } + + if (_votingSettings.minDuration < 60 minutes) { + revert MinDurationOutOfBounds({limit: 60 minutes, actual: _votingSettings.minDuration}); + } + + if (_votingSettings.minDuration > 365 days) { + revert MinDurationOutOfBounds({limit: 365 days, actual: _votingSettings.minDuration}); + } + + votingSettings = _votingSettings; + + emit VotingSettingsUpdated({ + votingMode: _votingSettings.votingMode, + supportThreshold: _votingSettings.supportThreshold, + minParticipation: _votingSettings.minParticipation, + minDuration: _votingSettings.minDuration, + minProposerVotingPower: _votingSettings.minProposerVotingPower + }); + } + + /// @notice Validates and returns the proposal vote dates. + /// @param _start The start date of the proposal vote. + /// If 0, the current timestamp is used and the vote starts immediately. + /// @param _end The end date of the proposal vote. If 0, `_start + minDuration` is used. + /// @return startDate The validated start date of the proposal vote. + /// @return endDate The validated end date of the proposal vote. + function _validateProposalDates( + uint64 _start, + uint64 _end + ) internal view virtual returns (uint64 startDate, uint64 endDate) { + uint64 currentTimestamp = block.timestamp.toUint64(); + + if (_start == 0) { + startDate = currentTimestamp; + } else { + startDate = _start; + + if (startDate < currentTimestamp) { + revert DateOutOfBounds({limit: currentTimestamp, actual: startDate}); + } + } + // Since `minDuration` is limited to 1 year, + // `startDate + minDuration` can only overflow if the `startDate` is after `type(uint64).max - minDuration`. + // In this case, the proposal creation will revert and another date can be picked. + uint64 earliestEndDate = startDate + votingSettings.minDuration; + + if (_end == 0) { + endDate = earliestEndDate; + } else { + endDate = _end; + + if (endDate < earliestEndDate) { + revert DateOutOfBounds({limit: earliestEndDate, actual: endDate}); + } + } + } + /// @inheritdoc IMembership function isMember(address _account) external view returns (bool) { // A member must own at least one token or have at least one token delegated to her/him. @@ -162,13 +550,17 @@ contract TokenVoting is IMembership, MajorityVotingBase { IERC20Upgradeable(address(votingToken)).balanceOf(_account) > 0; } - /// @inheritdoc MajorityVotingBase + /// @notice Internal function to cast a vote. It assumes the queried vote exists. + /// @param _proposalId The ID of the proposal. + /// @param _voteOption The chosen vote option to be casted on the proposal vote. + /// @param _tryEarlyExecution If `true`, early execution is tried after the vote cast. + /// The call does not revert if early execution is not possible. function _vote( uint256 _proposalId, VoteOption _voteOption, address _voter, bool _tryEarlyExecution - ) internal override { + ) internal { Proposal storage proposal_ = proposals[_proposalId]; // This could re-enter, though we can assume the governance token is not malicious @@ -207,12 +599,16 @@ contract TokenVoting is IMembership, MajorityVotingBase { } } - /// @inheritdoc MajorityVotingBase + /// @notice Internal function to check if a voter can vote. It assumes the queried proposal exists. + /// @param _proposalId The ID of the proposal. + /// @param _account The address of the voter to check. + /// @param _voteOption Whether the voter abstains, supports or opposes the proposal. + /// @return Returns `true` if the given voter can vote on a certain proposal and `false` otherwise. function _canVote( uint256 _proposalId, address _account, VoteOption _voteOption - ) internal view override returns (bool) { + ) internal view returns (bool) { Proposal storage proposal_ = proposals[_proposalId]; // The proposal vote hasn't started or has already ended. @@ -244,5 +640,5 @@ contract TokenVoting is IMembership, MajorityVotingBase { /// @dev This empty reserved space is put in place to allow future versions to add new /// variables without shifting down storage in the inheritance chain. /// https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps - uint256[49] private __gap; + uint256[46] private __gap; } diff --git a/packages/contracts/src/TokenVotingSetup.sol b/packages/contracts/src/TokenVotingSetup.sol index 0459fb90..1eb60c61 100644 --- a/packages/contracts/src/TokenVotingSetup.sol +++ b/packages/contracts/src/TokenVotingSetup.sol @@ -17,8 +17,8 @@ import {PermissionLib} from "@aragon/osx-commons-contracts/src/permission/Permis import {IPluginSetup} from "@aragon/osx-commons-contracts/src/plugin/setup/IPluginSetup.sol"; import {PluginUpgradeableSetup} from "@aragon/osx-commons-contracts/src/plugin/setup/PluginUpgradeableSetup.sol"; -import {MajorityVotingBase} from "./MajorityVotingBase.sol"; import {TokenVoting} from "./TokenVoting.sol"; +import {ITokenVoting} from "./ITokenVoting.sol"; import {ProxyLib} from "@aragon/osx-commons-contracts/src/utils/deployment/ProxyLib.sol"; @@ -93,13 +93,13 @@ contract TokenVotingSetup is PluginUpgradeableSetup { // Decode `_data` to extract the params needed for deploying and initializing `TokenVoting` plugin, // and the required helpers ( - MajorityVotingBase.VotingSettings memory votingSettings, + ITokenVoting.VotingSettings memory votingSettings, TokenSettings memory tokenSettings, // only used for GovernanceERC20(token is not passed) GovernanceERC20.MintSettings memory mintSettings ) = abi.decode( _data, - (MajorityVotingBase.VotingSettings, TokenSettings, GovernanceERC20.MintSettings) + (ITokenVoting.VotingSettings, TokenSettings, GovernanceERC20.MintSettings) ); address token = tokenSettings.addr; diff --git a/packages/contracts/src/mocks/MajorityVotingMock.sol b/packages/contracts/src/mocks/MajorityVotingMock.sol index 55bc8c58..c0ec4836 100644 --- a/packages/contracts/src/mocks/MajorityVotingMock.sol +++ b/packages/contracts/src/mocks/MajorityVotingMock.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.8; -import {MajorityVotingBase, IDAO} from "../MajorityVotingBase.sol"; +import {MajorityVotingBase, IDAO} from "../old/MajorityVotingBase.sol"; contract MajorityVotingMock is MajorityVotingBase { function initializeMock(IDAO _dao, VotingSettings calldata _votingSettings) public initializer { diff --git a/packages/contracts/src/IMajorityVoting.sol b/packages/contracts/src/old/IMajorityVoting.sol similarity index 100% rename from packages/contracts/src/IMajorityVoting.sol rename to packages/contracts/src/old/IMajorityVoting.sol diff --git a/packages/contracts/src/MajorityVotingBase.sol b/packages/contracts/src/old/MajorityVotingBase.sol similarity index 100% rename from packages/contracts/src/MajorityVotingBase.sol rename to packages/contracts/src/old/MajorityVotingBase.sol diff --git a/packages/contracts/src/old/_TokenVoting.sol b/packages/contracts/src/old/_TokenVoting.sol new file mode 100644 index 00000000..95a061a0 --- /dev/null +++ b/packages/contracts/src/old/_TokenVoting.sol @@ -0,0 +1,248 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +pragma solidity ^0.8.8; + +import {IVotesUpgradeable} from "@openzeppelin/contracts-upgradeable/governance/utils/IVotesUpgradeable.sol"; +import {SafeCastUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/math/SafeCastUpgradeable.sol"; +import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; + +import {IMembership} from "@aragon/osx-commons-contracts/src/plugin/extensions/membership/IMembership.sol"; +import {_applyRatioCeiled} from "@aragon/osx-commons-contracts/src/utils/math/Ratio.sol"; + +import {IDAO} from "@aragon/osx-commons-contracts/src/dao/IDAO.sol"; +import {MajorityVotingBase} from "./MajorityVotingBase.sol"; + +/// @title TokenVoting +/// @author Aragon X - 2021-2023 +/// @notice The majority voting implementation using an +/// [OpenZeppelin `Votes`](https://docs.openzeppelin.com/contracts/4.x/api/governance#Votes) +/// compatible governance token. +/// @dev v1.3 (Release 1, Build 3) +/// @custom:security-contact sirt@aragon.org +contract TokenVoting is IMembership, MajorityVotingBase { + using SafeCastUpgradeable for uint256; + + /// @notice The [ERC-165](https://eips.ethereum.org/EIPS/eip-165) interface ID of the contract. + bytes4 internal constant TOKEN_VOTING_INTERFACE_ID = + this.initialize.selector ^ this.getVotingToken.selector; + + /// @notice An [OpenZeppelin `Votes`](https://docs.openzeppelin.com/contracts/4.x/api/governance#Votes) + /// compatible contract referencing the token being used for voting. + IVotesUpgradeable private votingToken; + + /// @notice Thrown if the voting power is zero + error NoVotingPower(); + + /// @notice Initializes the component. + /// @dev This method is required to support [ERC-1822](https://eips.ethereum.org/EIPS/eip-1822). + /// @param _dao The IDAO interface of the associated DAO. + /// @param _votingSettings The voting settings. + /// @param _token The [ERC-20](https://eips.ethereum.org/EIPS/eip-20) token used for voting. + function initialize( + IDAO _dao, + VotingSettings calldata _votingSettings, + IVotesUpgradeable _token + ) external initializer { + __MajorityVotingBase_init(_dao, _votingSettings); + + votingToken = _token; + + emit MembershipContractAnnounced({definingContract: address(_token)}); + } + + /// @notice Checks if this or the parent contract supports an interface by its ID. + /// @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 == TOKEN_VOTING_INTERFACE_ID || + _interfaceId == type(IMembership).interfaceId || + super.supportsInterface(_interfaceId); + } + + /// @notice getter function for the voting token. + /// @dev public function also useful for registering interfaceId + /// and for distinguishing from majority voting interface. + /// @return The token used for voting. + function getVotingToken() public view returns (IVotesUpgradeable) { + return votingToken; + } + + /// @inheritdoc MajorityVotingBase + function totalVotingPower(uint256 _blockNumber) public view override returns (uint256) { + return votingToken.getPastTotalSupply(_blockNumber); + } + + /// @inheritdoc MajorityVotingBase + function createProposal( + bytes calldata _metadata, + IDAO.Action[] calldata _actions, + uint256 _allowFailureMap, + uint64 _startDate, + uint64 _endDate, + VoteOption _voteOption, + bool _tryEarlyExecution + ) external override returns (uint256 proposalId) { + // Check that either `_msgSender` owns enough tokens or has enough voting power from being a delegatee. + { + uint256 minProposerVotingPower_ = minProposerVotingPower(); + + if (minProposerVotingPower_ != 0) { + // Because of the checks in `TokenVotingSetup`, we can assume that `votingToken` + // is an [ERC-20](https://eips.ethereum.org/EIPS/eip-20) token. + if ( + votingToken.getVotes(_msgSender()) < minProposerVotingPower_ && + IERC20Upgradeable(address(votingToken)).balanceOf(_msgSender()) < + minProposerVotingPower_ + ) { + revert ProposalCreationForbidden(_msgSender()); + } + } + } + + uint256 snapshotBlock; + unchecked { + // The snapshot block must be mined already to + // protect the transaction against backrunning transactions causing census changes. + snapshotBlock = block.number - 1; + } + + uint256 totalVotingPower_ = totalVotingPower(snapshotBlock); + + if (totalVotingPower_ == 0) { + revert NoVotingPower(); + } + + (_startDate, _endDate) = _validateProposalDates(_startDate, _endDate); + + proposalId = _createProposal({ + _creator: _msgSender(), + _metadata: _metadata, + _startDate: _startDate, + _endDate: _endDate, + _actions: _actions, + _allowFailureMap: _allowFailureMap + }); + + // Store proposal related information + Proposal storage proposal_ = proposals[proposalId]; + + proposal_.parameters.startDate = _startDate; + proposal_.parameters.endDate = _endDate; + proposal_.parameters.snapshotBlock = snapshotBlock.toUint64(); + proposal_.parameters.votingMode = votingMode(); + proposal_.parameters.supportThreshold = supportThreshold(); + proposal_.parameters.minVotingPower = _applyRatioCeiled( + totalVotingPower_, + minParticipation() + ); + + // Reduce costs + if (_allowFailureMap != 0) { + proposal_.allowFailureMap = _allowFailureMap; + } + + for (uint256 i; i < _actions.length; ) { + proposal_.actions.push(_actions[i]); + unchecked { + ++i; + } + } + + if (_voteOption != VoteOption.None) { + vote(proposalId, _voteOption, _tryEarlyExecution); + } + } + + /// @inheritdoc IMembership + function isMember(address _account) external view returns (bool) { + // A member must own at least one token or have at least one token delegated to her/him. + return + votingToken.getVotes(_account) > 0 || + IERC20Upgradeable(address(votingToken)).balanceOf(_account) > 0; + } + + /// @inheritdoc MajorityVotingBase + function _vote( + uint256 _proposalId, + VoteOption _voteOption, + address _voter, + bool _tryEarlyExecution + ) internal override { + Proposal storage proposal_ = proposals[_proposalId]; + + // This could re-enter, though we can assume the governance token is not malicious + uint256 votingPower = votingToken.getPastVotes(_voter, proposal_.parameters.snapshotBlock); + VoteOption state = proposal_.voters[_voter]; + + // If voter had previously voted, decrease count + if (state == VoteOption.Yes) { + proposal_.tally.yes = proposal_.tally.yes - votingPower; + } else if (state == VoteOption.No) { + proposal_.tally.no = proposal_.tally.no - votingPower; + } else if (state == VoteOption.Abstain) { + proposal_.tally.abstain = proposal_.tally.abstain - votingPower; + } + + // write the updated/new vote for the voter. + if (_voteOption == VoteOption.Yes) { + proposal_.tally.yes = proposal_.tally.yes + votingPower; + } else if (_voteOption == VoteOption.No) { + proposal_.tally.no = proposal_.tally.no + votingPower; + } else if (_voteOption == VoteOption.Abstain) { + proposal_.tally.abstain = proposal_.tally.abstain + votingPower; + } + + proposal_.voters[_voter] = _voteOption; + + emit VoteCast({ + proposalId: _proposalId, + voter: _voter, + voteOption: _voteOption, + votingPower: votingPower + }); + + if (_tryEarlyExecution && _canExecute(_proposalId)) { + _execute(_proposalId); + } + } + + /// @inheritdoc MajorityVotingBase + function _canVote( + uint256 _proposalId, + address _account, + VoteOption _voteOption + ) internal view override returns (bool) { + Proposal storage proposal_ = proposals[_proposalId]; + + // The proposal vote hasn't started or has already ended. + if (!_isProposalOpen(proposal_)) { + return false; + } + + // The voter votes `None` which is not allowed. + if (_voteOption == VoteOption.None) { + return false; + } + + // The voter has no voting power. + if (votingToken.getPastVotes(_account, proposal_.parameters.snapshotBlock) == 0) { + return false; + } + + // The voter has already voted but vote replacment is not allowed. + if ( + proposal_.voters[_account] != VoteOption.None && + proposal_.parameters.votingMode != VotingMode.VoteReplacement + ) { + return false; + } + + return true; + } + + /// @dev This empty reserved space is put in place to allow future versions to add new + /// variables without shifting down storage in the inheritance chain. + /// https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + uint256[49] private __gap; +} diff --git a/packages/contracts/src/old/_TokenVotingSetup.sol b/packages/contracts/src/old/_TokenVotingSetup.sol new file mode 100644 index 00000000..3b2e9833 --- /dev/null +++ b/packages/contracts/src/old/_TokenVotingSetup.sol @@ -0,0 +1,284 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +pragma solidity ^0.8.8; + +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {ERC165Checker} from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; +import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import {IVotesUpgradeable} from "@openzeppelin/contracts-upgradeable/governance/utils/IVotesUpgradeable.sol"; + +import {GovernanceERC20} from "../ERC20/governance/GovernanceERC20.sol"; +import {IGovernanceWrappedERC20} from "../ERC20/governance/IGovernanceWrappedERC20.sol"; +import {GovernanceWrappedERC20} from "../ERC20/governance/GovernanceWrappedERC20.sol"; + +import {IDAO} from "@aragon/osx-commons-contracts/src/dao/IDAO.sol"; +import {PermissionLib} from "@aragon/osx-commons-contracts/src/permission/PermissionLib.sol"; +import {IPluginSetup} from "@aragon/osx-commons-contracts/src/plugin/setup/IPluginSetup.sol"; +import {PluginUpgradeableSetup} from "@aragon/osx-commons-contracts/src/plugin/setup/PluginUpgradeableSetup.sol"; + +import {MajorityVotingBase} from "./MajorityVotingBase.sol"; +import {TokenVoting} from "./_TokenVoting.sol"; + +import {ProxyLib} from "@aragon/osx-commons-contracts/src/utils/deployment/ProxyLib.sol"; + +/// @title TokenVotingSetup +/// @author Aragon X - 2022-2023 +/// @notice The setup contract of the `TokenVoting` plugin. +/// @dev v1.3 (Release 1, Build 3) +/// @custom:security-contact sirt@aragon.org +contract TokenVotingSetup is PluginUpgradeableSetup { + using Address for address; + using Clones for address; + using ERC165Checker for address; + using ProxyLib for address; + + /// @notice The identifier of the `EXECUTE_PERMISSION` permission. + /// @dev TODO: Migrate this constant to a common library that can be shared across plugins. + bytes32 public constant EXECUTE_PERMISSION_ID = keccak256("EXECUTE_PERMISSION"); + + /// @notice The address of the `TokenVoting` base contract. + // solhint-disable-next-line immutable-vars-naming + TokenVoting private immutable tokenVotingBase; + + /// @notice The address of the `GovernanceERC20` base contract. + // solhint-disable-next-line immutable-vars-naming + address public immutable governanceERC20Base; + + /// @notice The address of the `GovernanceWrappedERC20` base contract. + // solhint-disable-next-line immutable-vars-naming + address public immutable governanceWrappedERC20Base; + + /// @notice The token settings struct. + /// @param addr The token address. If this is `address(0)`, a new `GovernanceERC20` token is deployed. + /// If not, the existing token is wrapped as an `GovernanceWrappedERC20`. + /// @param name The token name. This parameter is only relevant if the token address is `address(0)`. + /// @param symbol The token symbol. This parameter is only relevant if the token address is `address(0)`. + struct TokenSettings { + address addr; + string name; + string symbol; + } + + /// @notice Thrown if token address is passed which is not a token. + /// @param token The token address + error TokenNotContract(address token); + + /// @notice Thrown if token address is not ERC20. + /// @param token The token address + error TokenNotERC20(address token); + + /// @notice Thrown if passed helpers array is of wrong length. + /// @param length The array length of passed helpers. + error WrongHelpersArrayLength(uint256 length); + + /// @notice The contract constructor deploying the plugin implementation contract + /// and receiving the governance token base contracts to clone from. + /// @param _governanceERC20Base The base `GovernanceERC20` contract to create clones from. + /// @param _governanceWrappedERC20Base The base `GovernanceWrappedERC20` contract to create clones from. + constructor( + GovernanceERC20 _governanceERC20Base, + GovernanceWrappedERC20 _governanceWrappedERC20Base + ) PluginUpgradeableSetup(address(new TokenVoting())) { + tokenVotingBase = TokenVoting(IMPLEMENTATION); + governanceERC20Base = address(_governanceERC20Base); + governanceWrappedERC20Base = address(_governanceWrappedERC20Base); + } + + /// @inheritdoc IPluginSetup + 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 `TokenVoting` plugin, + // and the required helpers + ( + MajorityVotingBase.VotingSettings memory votingSettings, + TokenSettings memory tokenSettings, + // only used for GovernanceERC20(token is not passed) + GovernanceERC20.MintSettings memory mintSettings + ) = abi.decode( + _data, + (MajorityVotingBase.VotingSettings, TokenSettings, GovernanceERC20.MintSettings) + ); + + address token = tokenSettings.addr; + bool tokenAddressNotZero = token != address(0); + + // Prepare helpers. + address[] memory helpers = new address[](1); + + if (tokenAddressNotZero) { + if (!token.isContract()) { + revert TokenNotContract(token); + } + + if (!_isERC20(token)) { + revert TokenNotERC20(token); + } + + // [0] = IERC20Upgradeable, [1] = IVotesUpgradeable, [2] = IGovernanceWrappedERC20 + bool[] memory supportedIds = _getTokenInterfaceIds(token); + + if ( + // If token supports none of them + // it's simply ERC20 which gets checked by _isERC20 + // Currently, not a satisfiable check. + (!supportedIds[0] && !supportedIds[1] && !supportedIds[2]) || + // If token supports IERC20, but neither + // IVotes nor IGovernanceWrappedERC20, it needs wrapping. + (supportedIds[0] && !supportedIds[1] && !supportedIds[2]) + ) { + token = governanceWrappedERC20Base.clone(); + // User already has a token. We need to wrap it in + // GovernanceWrappedERC20 in order to make the token + // include governance functionality. + GovernanceWrappedERC20(token).initialize( + IERC20Upgradeable(tokenSettings.addr), + tokenSettings.name, + tokenSettings.symbol + ); + } + } else { + // Clone a `GovernanceERC20`. + token = governanceERC20Base.clone(); + GovernanceERC20(token).initialize( + IDAO(_dao), + tokenSettings.name, + tokenSettings.symbol, + mintSettings + ); + } + + helpers[0] = token; + + // Prepare and deploy plugin proxy. + plugin = address(tokenVotingBase).deployUUPSProxy( + abi.encodeCall( + TokenVoting.initialize, + (IDAO(_dao), votingSettings, IVotesUpgradeable(token)) + ) + ); + + // Prepare permissions + PermissionLib.MultiTargetPermission[] + memory permissions = new PermissionLib.MultiTargetPermission[]( + tokenAddressNotZero ? 2 : 3 + ); + + // Set plugin permissions to be granted. + // Grant the list of permissions of the plugin to the DAO. + permissions[0] = PermissionLib.MultiTargetPermission({ + operation: PermissionLib.Operation.Grant, + where: plugin, + who: _dao, + condition: PermissionLib.NO_CONDITION, + permissionId: tokenVotingBase.UPDATE_VOTING_SETTINGS_PERMISSION_ID() + }); + + // Grant `EXECUTE_PERMISSION` of the DAO to the plugin. + permissions[1] = PermissionLib.MultiTargetPermission({ + operation: PermissionLib.Operation.Grant, + where: _dao, + who: plugin, + condition: PermissionLib.NO_CONDITION, + permissionId: EXECUTE_PERMISSION_ID + }); + + if (!tokenAddressNotZero) { + bytes32 tokenMintPermission = GovernanceERC20(token).MINT_PERMISSION_ID(); + + permissions[2] = PermissionLib.MultiTargetPermission({ + operation: PermissionLib.Operation.Grant, + where: token, + who: _dao, + condition: PermissionLib.NO_CONDITION, + permissionId: tokenMintPermission + }); + } + + preparedSetupData.helpers = helpers; + preparedSetupData.permissions = permissions; + } + + /// @inheritdoc IPluginSetup + /// @dev Revoke the upgrade plugin permission to the DAO for all builds prior the current one (3). + function prepareUpdate( + address _dao, + uint16 _fromBuild, + SetupPayload calldata _payload + ) + external + view + override + returns (bytes memory initData, PreparedSetupData memory preparedSetupData) + { + (initData); + if (_fromBuild < 3) { + PermissionLib.MultiTargetPermission[] + memory permissions = new PermissionLib.MultiTargetPermission[](1); + + permissions[0] = PermissionLib.MultiTargetPermission({ + operation: PermissionLib.Operation.Revoke, + where: _payload.plugin, + who: _dao, + condition: PermissionLib.NO_CONDITION, + permissionId: tokenVotingBase.UPGRADE_PLUGIN_PERMISSION_ID() + }); + + preparedSetupData.permissions = permissions; + } + } + + /// @inheritdoc IPluginSetup + 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}); + } + + permissions = new PermissionLib.MultiTargetPermission[](2); + + // Set permissions to be Revoked. + permissions[0] = PermissionLib.MultiTargetPermission({ + operation: PermissionLib.Operation.Revoke, + where: _payload.plugin, + who: _dao, + condition: PermissionLib.NO_CONDITION, + permissionId: tokenVotingBase.UPDATE_VOTING_SETTINGS_PERMISSION_ID() + }); + + permissions[1] = PermissionLib.MultiTargetPermission({ + operation: PermissionLib.Operation.Revoke, + where: _dao, + who: _payload.plugin, + condition: PermissionLib.NO_CONDITION, + permissionId: EXECUTE_PERMISSION_ID + }); + } + + /// @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) { + bytes4[] memory interfaceIds = new bytes4[](3); + interfaceIds[0] = type(IERC20Upgradeable).interfaceId; + interfaceIds[1] = type(IVotesUpgradeable).interfaceId; + interfaceIds[2] = type(IGovernanceWrappedERC20).interfaceId; + return token.getSupportedInterfaces(interfaceIds); + } + + /// @notice Unsatisfiably determines if the contract is an ERC20 token. + /// @dev It's important to first check whether token is a contract prior to this call. + /// @param token The token address + function _isERC20(address token) private view returns (bool) { + (bool success, bytes memory data) = token.staticcall( + abi.encodeCall(IERC20Upgradeable.balanceOf, (address(this))) + ); + return success && data.length == 0x20; + } +}