// 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 {IProposal} from "@aragon/osx-commons-contracts/src/plugin/extensions/proposal/IProposal.sol"; import {Action} from "@aragon/osx-commons-contracts/src/executors/IExecutor.sol"; import {MajorityVotingBase} from "./base/MajorityVotingBase.sol"; /// @title TokenVoting /// @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). For each upgrade, if the reinitialization step is required, /// increment the version numbers in the modifier for both the initialize and initializeFrom functions. /// @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.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. /// @param _targetConfig Configuration for the execution target, specifying the target address and operation type /// (either `Call` or `DelegateCall`). Defined by `TargetConfig` in the `IPlugin` interface, /// part of the `osx-commons-contracts` package, added in build 3. /// @param _minApprovals The minimal amount of approvals the proposal needs to succeed. /// @param _pluginMetadata The plugin specific information encoded in bytes. /// This can also be an ipfs cid encoded in bytes. function initialize( IDAO _dao, VotingSettings calldata _votingSettings, IVotesUpgradeable _token, TargetConfig calldata _targetConfig, uint256 _minApprovals, bytes calldata _pluginMetadata ) external onlyCallAtInitialization reinitializer(2) { __MajorityVotingBase_init(_dao, _votingSettings, _targetConfig, _minApprovals, _pluginMetadata); votingToken = _token; emit MembershipContractAnnounced({definingContract: address(_token)}); } /// @notice Reinitializes the TokenVoting after an upgrade from a previous build version. For each /// reinitialization step, use the `_fromBuild` version to decide which internal functions to /// call for reinitialization. /// @dev WARNING: The contract should only be upgradeable through PSP to ensure that _fromBuild is not /// incorrectly passed, and that the appropriate permissions for the upgrade are properly configured. /// @param _fromBuild Build version number of previous implementation contract this upgrade is transitioning from. /// @param _initData The initialization data to be passed to via `upgradeToAndCall` /// (see [ERC-1967](https://docs.openzeppelin.com/contracts/4.x/api/proxy#ERC1967Upgrade)). function initializeFrom(uint16 _fromBuild, bytes calldata _initData) external reinitializer(2) { if (_fromBuild < 3) { (uint256 minApprovals, TargetConfig memory targetConfig, bytes memory pluginMetadata) = abi.decode(_initData, (uint256, TargetConfig, bytes)); _updateMinApprovals(minApprovals); _setTargetConfig(targetConfig); _setMetadata(pluginMetadata); } } /// @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 /// @dev Requires the `CREATE_PROPOSAL_PERMISSION_ID` permission. function createProposal( bytes calldata _metadata, Action[] calldata _actions, uint256 _allowFailureMap, uint64 _startDate, uint64 _endDate, VoteOption _voteOption, bool _tryEarlyExecution ) public override auth(CREATE_PROPOSAL_PERMISSION_ID) returns (uint256 proposalId) { 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 = _createProposalId(keccak256(abi.encode(_actions, _metadata))); // Store proposal related information Proposal storage proposal_ = proposals[proposalId]; if (proposal_.parameters.snapshotBlock != 0) { revert ProposalAlreadyExists(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()); proposal_.minApprovalPower = _applyRatioCeiled(totalVotingPower_, minApproval()); proposal_.targetConfig = getTargetConfig(); // 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); } _emitProposalCreatedEvent(_metadata, _actions, _allowFailureMap, proposalId, _startDate, _endDate); } /// @inheritdoc IProposal function createProposal( bytes calldata _metadata, Action[] calldata _actions, uint64 _startDate, uint64 _endDate, bytes memory _data ) external override returns (uint256 proposalId) { // Note that this calls public function for permission check. uint256 allowFailureMap; VoteOption _voteOption = VoteOption.None; bool tryEarlyExecution; if (_data.length != 0) { (allowFailureMap, _voteOption, tryEarlyExecution) = abi.decode(_data, (uint256, VoteOption, bool)); } proposalId = createProposal(_metadata, _actions, allowFailureMap, _startDate, _endDate, _voteOption, tryEarlyExecution); } /// @inheritdoc IProposal function customProposalParamsABI() external pure override returns (string memory) { return "(uint256 allowFailureMap, uint8 voteOption, bool 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) { return; } if ( _canExecute(_proposalId) && dao().hasPermission(address(this), _voter, EXECUTE_PROPOSAL_PERMISSION_ID, _msgData()) ) { _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 Helper function to avoid stack too deep in non via-ir compilation mode. function _emitProposalCreatedEvent( bytes calldata _metadata, Action[] calldata _actions, uint256 _allowFailureMap, uint256 proposalId, uint64 _startDate, uint64 _endDate ) private { emit ProposalCreated(proposalId, _msgSender(), _startDate, _endDate, _metadata, _actions, _allowFailureMap); } /// @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; }