From fc4e99ab2a0011dfec671ed6c5a88a9cc951ae4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8r=E2=88=82=C2=A1?= Date: Mon, 13 Jan 2025 16:34:36 +0100 Subject: [PATCH] Refactoring interfaces and contract bases --- src/LockManager.sol | 141 ++++- ...nglePlugin.sol => LockToApprovePlugin.sol} | 83 +-- src/LockToVotePlugin.sol | 491 ++++++++++++++++++ src/interfaces/ILockManager.sol | 35 +- src/interfaces/ILockToApprove.sol | 81 +++ src/interfaces/ILockToVote.sol | 103 ++-- src/interfaces/ILockToVoteBase.sol | 43 ++ src/setup/LockToVotePluginSetup.sol | 16 +- test/LockManager.t.sol | 21 +- test/LockManager.t.yaml | 1 + ...ToVoteSingle.t.sol => LockToApprove.t.sol} | 40 +- ...VoteSingle.t.yaml => LockToApprove.t.yaml} | 0 test/util/DaoBuilder.sol | 17 +- 13 files changed, 895 insertions(+), 177 deletions(-) rename src/{LockToVoteSinglePlugin.sol => LockToApprovePlugin.sol} (82%) create mode 100644 src/LockToVotePlugin.sol create mode 100644 src/interfaces/ILockToApprove.sol create mode 100644 src/interfaces/ILockToVoteBase.sol rename test/{LockToVoteSingle.t.sol => LockToApprove.t.sol} (96%) rename test/{LockToVoteSingle.t.yaml => LockToApprove.t.yaml} (100%) diff --git a/src/LockManager.sol b/src/LockManager.sol index 107f270..dbe327b 100644 --- a/src/LockManager.sol +++ b/src/LockManager.sol @@ -1,9 +1,11 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity ^0.8.13; -import {ILockManager, LockManagerSettings, UnlockMode} from "./interfaces/ILockManager.sol"; +import {ILockManager, LockManagerSettings, UnlockMode, PluginMode} from "./interfaces/ILockManager.sol"; import {IDAO} from "@aragon/osx-commons-contracts/src/dao/IDAO.sol"; import {DaoAuthorizable} from "@aragon/osx-commons-contracts/src/permission/auth/DaoAuthorizable.sol"; +import {ILockToVoteBase, VoteOption} from "./interfaces/ILockToVote.sol"; +import {ILockToApprove} from "./interfaces/ILockToApprove.sol"; import {ILockToVote} from "./interfaces/ILockToVote.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; @@ -13,13 +15,14 @@ import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; /// @notice Helper contract acting as the vault for locked tokens used to vote on multiple plugins and proposals. contract LockManager is ILockManager, DaoAuthorizable { /// @notice The ID of the permission required to call the `updateVotingSettings` function. - bytes32 public constant UPDATE_SETTINGS_PERMISSION_ID = keccak256("UPDATE_SETTINGS_PERMISSION"); + bytes32 public constant UPDATE_SETTINGS_PERMISSION_ID = + keccak256("UPDATE_SETTINGS_PERMISSION"); /// @notice The current LockManager settings LockManagerSettings public settings; /// @notice The address of the lock to vote plugin to use - ILockToVote public plugin; + ILockToVoteBase public plugin; /// @notice The address of the token contract IERC20 public immutable token; @@ -62,17 +65,32 @@ contract LockManager is ILockManager, DaoAuthorizable { /// @notice Thrown when trying to set an invalid contract as the plugin error InvalidPlugin(); - /// @notice Thrown when trying to define the address of the plugin after it already was - error CannotUpdatePlugin(); + /// @notice Thrown when trying to set an invalid PluginMode value, or when trying to use an operation not supported by the current pluginMode + error InvalidPluginMode(); - constructor(IDAO _dao, LockManagerSettings memory _settings, IERC20 _token, IERC20 _underlyingToken) - DaoAuthorizable(_dao) - { - if (_settings.unlockMode != UnlockMode.STRICT && _settings.unlockMode != UnlockMode.EARLY) { + /// @notice Thrown when trying to define the address of the plugin after it already was + error SetPluginAddressForbidden(); + + constructor( + IDAO _dao, + LockManagerSettings memory _settings, + IERC20 _token, + IERC20 _underlyingToken + ) DaoAuthorizable(_dao) { + if ( + _settings.unlockMode != UnlockMode.STRICT && + _settings.unlockMode != UnlockMode.EARLY + ) { revert InvalidUnlockMode(); + } else if ( + _settings.pluginMode != PluginMode.APPROVAL && + _settings.pluginMode != PluginMode.VOTING + ) { + revert InvalidPluginMode(); } settings.unlockMode = _settings.unlockMode; + settings.pluginMode = _settings.pluginMode; token = _token; underlyingTokenAddress = _underlyingToken; } @@ -83,19 +101,50 @@ contract LockManager is ILockManager, DaoAuthorizable { } /// @inheritdoc ILockManager - function lockAndVote(uint256 _proposalId) public { + function lockAndApprove(uint256 _proposalId) public { + if (settings.pluginMode != PluginMode.APPROVAL) { + revert InvalidPluginMode(); + } + _lock(); - _vote(_proposalId); + _approve(_proposalId); + } + + /// @inheritdoc ILockManager + function lockAndVote(uint256 _proposalId, VoteOption _voteOption) public { + if (settings.pluginMode != PluginMode.VOTING) { + revert InvalidPluginMode(); + } + + _lock(); + + _vote(_proposalId, _voteOption); + } + + /// @inheritdoc ILockManager + function approve(uint256 _proposalId) public { + if (settings.pluginMode != PluginMode.APPROVAL) { + revert InvalidPluginMode(); + } + + _approve(_proposalId); } /// @inheritdoc ILockManager - function vote(uint256 _proposalId) public { - _vote(_proposalId); + function vote(uint256 _proposalId, VoteOption _voteOption) public { + if (settings.pluginMode != PluginMode.VOTING) { + revert InvalidPluginMode(); + } + + _vote(_proposalId, _voteOption); } /// @inheritdoc ILockManager - function canVote(uint256 _proposalId, address _voter) external view returns (bool) { + function canVote( + uint256 _proposalId, + address _voter + ) external view returns (bool) { return plugin.canVote(_proposalId, _voter); } @@ -138,7 +187,7 @@ contract LockManager is ILockManager, DaoAuthorizable { emit ProposalEnded(_proposalId); - for (uint256 _i; _i < knownProposalIds.length;) { + for (uint256 _i; _i < knownProposalIds.length; ) { if (knownProposalIds[_i] == _proposalId) { _removeKnownProposalId(_i); return; @@ -159,11 +208,31 @@ contract LockManager is ILockManager, DaoAuthorizable { } /// @inheritdoc ILockManager - function setPluginAddress(ILockToVote _newPluginAddress) public auth(UPDATE_SETTINGS_PERMISSION_ID) { - if (!IERC165(address(_newPluginAddress)).supportsInterface(type(ILockToVote).interfaceId)) { + function setPluginAddress( + ILockToVoteBase _newPluginAddress + ) public auth(UPDATE_SETTINGS_PERMISSION_ID) { + if ( + !IERC165(address(_newPluginAddress)).supportsInterface( + type(ILockToVoteBase).interfaceId + ) + ) { revert InvalidPlugin(); } else if (address(plugin) != address(0)) { - revert CannotUpdatePlugin(); + revert SetPluginAddressForbidden(); + } else if ( + settings.pluginMode == PluginMode.APPROVAL && + !IERC165(address(_newPluginAddress)).supportsInterface( + type(ILockToApprove).interfaceId + ) + ) { + revert InvalidPluginMode(); + } else if ( + settings.pluginMode == PluginMode.VOTING && + !IERC165(address(_newPluginAddress)).supportsInterface( + type(ILockToVote).interfaceId + ) + ) { + revert InvalidPluginMode(); } plugin = _newPluginAddress; @@ -182,20 +251,46 @@ contract LockManager is ILockManager, DaoAuthorizable { emit BalanceLocked(msg.sender, _allowance); } - function _vote(uint256 _proposalId) internal { + function _approve(uint256 _proposalId) internal { + uint256 _currentVotingPower = lockedBalances[msg.sender]; + if (_currentVotingPower == 0) { + revert NoBalance(); + } else if ( + _currentVotingPower == + plugin.usedVotingPower(_proposalId, msg.sender) + ) { + revert NoNewBalance(); + } + + ILockToApprove(address(plugin)).approve( + _proposalId, + msg.sender, + _currentVotingPower + ); + } + + function _vote(uint256 _proposalId, VoteOption _voteOption) internal { uint256 _currentVotingPower = lockedBalances[msg.sender]; if (_currentVotingPower == 0) { revert NoBalance(); - } else if (_currentVotingPower == plugin.usedVotingPower(_proposalId, msg.sender)) { + } else if ( + _currentVotingPower == + plugin.usedVotingPower(_proposalId, msg.sender) + ) { revert NoNewBalance(); } - plugin.vote(_proposalId, msg.sender, _currentVotingPower); + ILockToVote(address(plugin)).vote( + _proposalId, + msg.sender, + _voteOption, + _currentVotingPower + ); } function _hasActiveLocks() internal returns (bool _activeLocks) { uint256 _proposalCount = knownProposalIds.length; - for (uint256 _i; _i < _proposalCount;) { + for (uint256 _i; _i < _proposalCount; ) { if (!plugin.isProposalOpen(knownProposalIds[_i])) { _removeKnownProposalId(_i); _proposalCount = knownProposalIds.length; @@ -221,7 +316,7 @@ contract LockManager is ILockManager, DaoAuthorizable { function _withdrawActiveVotingPower() internal { uint256 _proposalCount = knownProposalIds.length; - for (uint256 _i; _i < _proposalCount;) { + for (uint256 _i; _i < _proposalCount; ) { if (!plugin.isProposalOpen(knownProposalIds[_i])) { _removeKnownProposalId(_i); _proposalCount = knownProposalIds.length; diff --git a/src/LockToVoteSinglePlugin.sol b/src/LockToApprovePlugin.sol similarity index 82% rename from src/LockToVoteSinglePlugin.sol rename to src/LockToApprovePlugin.sol index 5e9734e..f07482e 100644 --- a/src/LockToVoteSinglePlugin.sol +++ b/src/LockToApprovePlugin.sol @@ -2,7 +2,8 @@ pragma solidity ^0.8.13; import {ILockManager} from "./interfaces/ILockManager.sol"; -import {ILockToVoteSingle, LockToVoteSingleSettings, Proposal, ProposalParameters} from "./interfaces/ILockToVote.sol"; +import {ILockToVoteBase, VoteOption} from "./interfaces/ILockToVote.sol"; +import {ILockToApprove, LockToApproveSettings, ProposalApproval, ProposalApprovalParameters} from "./interfaces/ILockToApprove.sol"; import {IDAO} from "@aragon/osx-commons-contracts/src/dao/IDAO.sol"; import {ProposalUpgradeable} from "@aragon/osx-commons-contracts/src/plugin/extensions/proposal/ProposalUpgradeable.sol"; import {IMembership} from "@aragon/osx-commons-contracts/src/plugin/extensions/membership/IMembership.sol"; @@ -17,8 +18,8 @@ import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/intro import {SafeCastUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/math/SafeCastUpgradeable.sol"; import {_applyRatioCeiled} from "@aragon/osx-commons-contracts/src/utils/math/Ratio.sol"; -contract LockToVotePlugin is - ILockToVoteSingle, +contract LockToApprovePlugin is + ILockToApprove, PluginUUPSUpgradeable, ProposalUpgradeable, MetadataExtensionUpgradeable, @@ -26,12 +27,12 @@ contract LockToVotePlugin is { using SafeCastUpgradeable for uint256; - LockToVoteSingleSettings public settings; + LockToApproveSettings public settings; - /// @inheritdoc ILockToVote + /// @inheritdoc ILockToVoteBase ILockManager public lockManager; - mapping(uint256 => Proposal) proposals; + mapping(uint256 => ProposalApproval) proposals; /// @notice The ID of the permission required to call the `createProposal` functions. bytes32 public constant CREATE_PROPOSAL_PERMISSION_ID = keccak256("CREATE_PROPOSAL_PERMISSION"); @@ -57,7 +58,7 @@ contract LockToVotePlugin is function initialize( IDAO _dao, ILockManager _lockManager, - LockToVoteSingleSettings calldata _pluginSettings, + LockToApproveSettings calldata _pluginSettings, IPlugin.TargetConfig calldata _targetConfig, bytes calldata _pluginMetadata ) external onlyCallAtInitialization reinitializer(1) { @@ -81,8 +82,8 @@ contract LockToVotePlugin is override(MetadataExtensionUpgradeable, PluginUUPSUpgradeable, ProposalUpgradeable) returns (bool) { - return _interfaceId == type(IMembership).interfaceId || _interfaceId == type(ILockToVote).interfaceId - || super.supportsInterface(_interfaceId); + return _interfaceId == type(IMembership).interfaceId || _interfaceId == type(ILockToVoteBase).interfaceId + || _interfaceId == type(ILockToApprove).interfaceId || super.supportsInterface(_interfaceId); } /// @inheritdoc IProposal @@ -114,7 +115,7 @@ contract LockToVotePlugin is proposalId = _createProposalId(keccak256(abi.encode(_actions, _metadata))); // Store proposal related information - Proposal storage proposal_ = proposals[proposalId]; + ProposalApproval storage proposal_ = proposals[proposalId]; if (proposal_.parameters.startDate != 0) { revert ProposalAlreadyExists(proposalId); @@ -159,14 +160,14 @@ contract LockToVotePlugin is returns ( bool open, bool executed, - ProposalParameters memory parameters, + ProposalApprovalParameters memory parameters, uint256 approvalTally, Action[] memory actions, uint256 allowFailureMap, TargetConfig memory targetConfig ) { - Proposal storage proposal_ = proposals[_proposalId]; + ProposalApproval storage proposal_ = proposals[_proposalId]; open = _isProposalOpen(proposal_); executed = proposal_.executed; @@ -177,9 +178,9 @@ contract LockToVotePlugin is targetConfig = proposal_.targetConfig; } - /// @inheritdoc ILockToVote + /// @inheritdoc ILockToVoteBase function isProposalOpen(uint256 _proposalId) external view virtual returns (bool) { - Proposal storage proposal_ = proposals[_proposalId]; + ProposalApproval storage proposal_ = proposals[_proposalId]; return _isProposalOpen(proposal_); } @@ -190,22 +191,22 @@ contract LockToVotePlugin is return false; } - /// @inheritdoc ILockToVote - function canVote(uint256 _proposalId, address _voter) external view returns (bool) { - Proposal storage proposal_ = proposals[_proposalId]; + /// @inheritdoc ILockToApprove + function canApprove(uint256 _proposalId, address _voter) external view returns (bool) { + ProposalApproval storage proposal_ = proposals[_proposalId]; return _canVote(proposal_, _voter, lockManager.lockedBalances(_voter)); } - /// @inheritdoc ILockToVote - function vote(uint256 _proposalId, address _voter, uint256 _newVotingPower) + /// @inheritdoc ILockToApprove + function approve(uint256 _proposalId, address _voter, uint256 _newVotingPower) external auth(LOCK_MANAGER_PERMISSION_ID) { - Proposal storage proposal_ = proposals[_proposalId]; + ProposalApproval storage proposal_ = proposals[_proposalId]; if (!_canVote(proposal_, _voter, _newVotingPower)) { - revert VoteCastForbidden(_proposalId, _voter); + revert ApprovalForbidden(_proposalId, _voter); } // Add the difference between the new voting power and the current one @@ -214,14 +215,14 @@ contract LockToVotePlugin is proposal_.approvalTally += diff; proposal_.approvals[_voter] += diff; - emit VoteCast(_proposalId, _voter, _newVotingPower); + emit Approved(_proposalId, _voter, _newVotingPower); _checkEarlyExecution(_proposalId, proposal_, _voter); } - /// @inheritdoc ILockToVote - function clearVote(uint256 _proposalId, address _voter) external auth(LOCK_MANAGER_PERMISSION_ID) { - Proposal storage proposal_ = proposals[_proposalId]; + /// @inheritdoc ILockToApprove + function clearApproval(uint256 _proposalId, address _voter) external auth(LOCK_MANAGER_PERMISSION_ID) { + ProposalApproval storage proposal_ = proposals[_proposalId]; if (proposal_.approvals[_voter] == 0 || !_isProposalOpen(proposal_)) return; @@ -234,26 +235,26 @@ contract LockToVotePlugin is emit VoteCleared(_proposalId, _voter); } - /// @inheritdoc ILockToVote + /// @inheritdoc ILockToVoteBase function usedVotingPower(uint256 proposalId, address voter) public view returns (uint256) { return proposals[proposalId].approvals[voter]; } /// @inheritdoc IProposal function hasSucceeded(uint256 _proposalId) external view returns (bool) { - Proposal storage proposal_ = proposals[_proposalId]; + ProposalApproval storage proposal_ = proposals[_proposalId]; return _hasSucceeded(proposal_); } /// @inheritdoc IProposal function canExecute(uint256 _proposalId) external view returns (bool) { - Proposal storage proposal_ = proposals[_proposalId]; + ProposalApproval storage proposal_ = proposals[_proposalId]; return _canExecute(proposal_); } /// @inheritdoc IProposal function execute(uint256 _proposalId) external auth(EXECUTE_PROPOSAL_PERMISSION_ID) { - Proposal storage proposal_ = proposals[_proposalId]; + ProposalApproval storage proposal_ = proposals[_proposalId]; if (!_canExecute(proposal_)) { revert ExecutionForbidden(_proposalId); @@ -262,18 +263,18 @@ contract LockToVotePlugin is _execute(_proposalId, proposal_); } - /// @inheritdoc ILockToVote + /// @inheritdoc ILockToVoteBase function underlyingToken() external view returns (IERC20) { return lockManager.underlyingToken(); } - /// @inheritdoc ILockToVote + /// @inheritdoc ILockToVoteBase function token() external view returns (IERC20) { return lockManager.token(); } - /// @inheritdoc ILockToVote - function updatePluginSettings(LockToVoteSingleSettings calldata _newSettings) + /// @inheritdoc ILockToApprove + function updatePluginSettings(LockToApproveSettings calldata _newSettings) external auth(UPDATE_VOTING_SETTINGS_PERMISSION_ID) { @@ -282,14 +283,14 @@ contract LockToVotePlugin is // Internal helpers - function _isProposalOpen(Proposal storage proposal_) internal view returns (bool) { + function _isProposalOpen(ProposalApproval storage proposal_) internal view returns (bool) { uint64 currentTime = block.timestamp.toUint64(); return proposal_.parameters.startDate <= currentTime && currentTime < proposal_.parameters.endDate && !proposal_.executed; } - function _canVote(Proposal storage proposal_, address _voter, uint256 _newVotingBalance) + function _canVote(ProposalApproval storage proposal_, address _voter, uint256 _newVotingBalance) internal view returns (bool) @@ -306,7 +307,7 @@ contract LockToVotePlugin is return true; } - function _canExecute(Proposal storage proposal_) internal view returns (bool) { + function _canExecute(ProposalApproval storage proposal_) internal view returns (bool) { if (proposal_.executed) { return false; } else if (proposal_.parameters.endDate < block.timestamp) { @@ -318,7 +319,7 @@ contract LockToVotePlugin is return true; } - function _minApprovalTally(Proposal storage proposal_) internal view returns (uint256 _minTally) { + function _minApprovalTally(ProposalApproval storage proposal_) internal view returns (uint256 _minTally) { /// @dev Checking against the totalSupply() of the **underlying token**. /// @dev LP tokens could have important supply variations and this would impact the value of existing votes, after created. /// @dev However, the total supply of the underlying token (USDC, USDT, DAI, etc) will experiment little to no variations in comparison. @@ -329,7 +330,7 @@ contract LockToVotePlugin is _applyRatioCeiled(lockManager.underlyingToken().totalSupply(), proposal_.parameters.minApprovalRatio); } - function _hasSucceeded(Proposal storage proposal_) internal view returns (bool) { + function _hasSucceeded(ProposalApproval storage proposal_) internal view returns (bool) { return proposal_.approvalTally >= _minApprovalTally(proposal_) && proposal_.approvalTally > 0; } @@ -373,7 +374,7 @@ contract LockToVotePlugin is } } - function _checkEarlyExecution(uint256 _proposalId, Proposal storage proposal_, address _voter) internal { + function _checkEarlyExecution(uint256 _proposalId, ProposalApproval storage proposal_, address _voter) internal { if (!_canExecute(proposal_)) { return; } else if (!dao().hasPermission(address(this), _voter, EXECUTE_PROPOSAL_PERMISSION_ID, _msgData())) { @@ -383,7 +384,7 @@ contract LockToVotePlugin is _execute(_proposalId, proposal_); } - function _execute(uint256 _proposalId, Proposal storage proposal_) internal { + function _execute(uint256 _proposalId, ProposalApproval storage proposal_) internal { proposal_.executed = true; // IProposal's target execution @@ -395,7 +396,7 @@ contract LockToVotePlugin is lockManager.proposalEnded(_proposalId); } - function _updatePluginSettings(LockToVoteSingleSettings memory _newSettings) internal { + function _updatePluginSettings(LockToApproveSettings memory _newSettings) internal { settings.minApprovalRatio = _newSettings.minApprovalRatio; settings.minProposalDuration = _newSettings.minProposalDuration; } diff --git a/src/LockToVotePlugin.sol b/src/LockToVotePlugin.sol new file mode 100644 index 0000000..4764940 --- /dev/null +++ b/src/LockToVotePlugin.sol @@ -0,0 +1,491 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.13; + +import {ILockManager} from "./interfaces/ILockManager.sol"; +import {ILockToVoteBase, VoteOption} from "./interfaces/ILockToVote.sol"; +import {ILockToVote, ProposalVoting, ProposalVotingParameters, LockToVoteSettings} from "./interfaces/ILockToVote.sol"; +import {IDAO} from "@aragon/osx-commons-contracts/src/dao/IDAO.sol"; +import {ProposalUpgradeable} from "@aragon/osx-commons-contracts/src/plugin/extensions/proposal/ProposalUpgradeable.sol"; +import {IMembership} from "@aragon/osx-commons-contracts/src/plugin/extensions/membership/IMembership.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 {IPlugin} from "@aragon/osx-commons-contracts/src/plugin/IPlugin.sol"; +import {PluginUUPSUpgradeable} from "@aragon/osx-commons-contracts/src/plugin/PluginUUPSUpgradeable.sol"; +import {MetadataExtensionUpgradeable} from "@aragon/osx-commons-contracts/src/utils/metadata/MetadataExtensionUpgradeable.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; +import {SafeCastUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/math/SafeCastUpgradeable.sol"; +import {_applyRatioCeiled} from "@aragon/osx-commons-contracts/src/utils/math/Ratio.sol"; + +contract LockToVotePlugin is + ILockToVote, + PluginUUPSUpgradeable, + ProposalUpgradeable, + MetadataExtensionUpgradeable, + IMembership +{ + using SafeCastUpgradeable for uint256; + + LockToVoteSettings public settings; + + /// @inheritdoc ILockToVoteBase + ILockManager public lockManager; + + mapping(uint256 => ProposalVoting) proposals; + + /// @notice The ID of the permission required to call the `createProposal` functions. + bytes32 public constant CREATE_PROPOSAL_PERMISSION_ID = + keccak256("CREATE_PROPOSAL_PERMISSION"); + + /// @notice The ID of the permission required to call the `execute` function. + bytes32 public constant EXECUTE_PROPOSAL_PERMISSION_ID = + keccak256("EXECUTE_PROPOSAL_PERMISSION"); + + /// @notice The ID of the permission required to call the `execute` function. + bytes32 public constant LOCK_MANAGER_PERMISSION_ID = + keccak256("LOCK_MANAGER_PERMISSION"); + + /// @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 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 _pluginSettings The voting settings. + /// @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 _pluginMetadata The plugin specific information encoded in bytes. + /// This can also be an ipfs cid encoded in bytes. + function initialize( + IDAO _dao, + ILockManager _lockManager, + LockToVoteSettings calldata _pluginSettings, + IPlugin.TargetConfig calldata _targetConfig, + bytes calldata _pluginMetadata + ) external onlyCallAtInitialization reinitializer(1) { + __PluginUUPSUpgradeable_init(_dao); + _updatePluginSettings(_pluginSettings); + _setTargetConfig(_targetConfig); + _setMetadata(_pluginMetadata); + + lockManager = _lockManager; + + emit MembershipContractAnnounced({ + definingContract: address(_lockManager.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( + MetadataExtensionUpgradeable, + PluginUUPSUpgradeable, + ProposalUpgradeable + ) + returns (bool) + { + return + _interfaceId == type(IMembership).interfaceId || + _interfaceId == type(ILockToVoteBase).interfaceId || + _interfaceId == type(ILockToVote).interfaceId || + super.supportsInterface(_interfaceId); + } + + /// @inheritdoc IProposal + function customProposalParamsABI() + external + pure + override + returns (string memory) + { + return "(uint256 allowFailureMap)"; + } + + /// @inheritdoc IProposal + /// @dev Requires the `CREATE_PROPOSAL_PERMISSION_ID` permission. + function createProposal( + bytes calldata _metadata, + Action[] memory _actions, + uint64 _startDate, + uint64 _endDate, + bytes memory _data + ) + external + auth(CREATE_PROPOSAL_PERMISSION_ID) + returns (uint256 proposalId) + { + uint256 _allowFailureMap; + + if (_data.length != 0) { + (_allowFailureMap) = abi.decode(_data, (uint256)); + } + + if (lockManager.token().totalSupply() == 0) { + revert NoVotingPower(); + } + + (_startDate, _endDate) = _validateProposalDates(_startDate, _endDate); + + proposalId = _createProposalId( + keccak256(abi.encode(_actions, _metadata)) + ); + + // Store proposal related information + ProposalVoting storage proposal_ = proposals[proposalId]; + + if (proposal_.parameters.startDate != 0) { + revert ProposalAlreadyExists(proposalId); + } + + proposal_.parameters.startDate = _startDate; + proposal_.parameters.endDate = _endDate; + proposal_.parameters.minApprovalRatio = settings.minApprovalRatio; + + proposal_.targetConfig = getTargetConfig(); + + // Reduce costs + if (_allowFailureMap != 0) { + proposal_.allowFailureMap = _allowFailureMap; + } + + for (uint256 i; i < _actions.length; ) { + proposal_.actions.push(_actions[i]); + unchecked { + ++i; + } + } + + emit ProposalCreated( + proposalId, + _msgSender(), + _startDate, + _endDate, + _metadata, + _actions, + _allowFailureMap + ); + + lockManager.proposalCreated(proposalId); + } + + /// @notice Returns all information for a proposal 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. + /// @return approvalTally The current tally of the proposal. + /// @return actions The actions to be executed to the `target` contract address. + /// @return allowFailureMap The bit map representations of which actions are allowed to revert so tx still succeeds. + /// @return targetConfig Execution configuration, applied to the proposal when it was created. Added in build 3. + function getProposal( + uint256 _proposalId + ) + public + view + virtual + returns ( + bool open, + bool executed, + ProposalVotingParameters memory parameters, + uint256 approvalTally, + Action[] memory actions, + uint256 allowFailureMap, + TargetConfig memory targetConfig + ) + { + ProposalVoting storage proposal_ = proposals[_proposalId]; + + open = _isProposalOpen(proposal_); + executed = proposal_.executed; + parameters = proposal_.parameters; + approvalTally = proposal_.approvalTally; + actions = proposal_.actions; + allowFailureMap = proposal_.allowFailureMap; + targetConfig = proposal_.targetConfig; + } + + /// @inheritdoc ILockToVoteBase + function isProposalOpen( + uint256 _proposalId + ) external view virtual returns (bool) { + ProposalVoting storage proposal_ = proposals[_proposalId]; + return _isProposalOpen(proposal_); + } + + /// @inheritdoc IMembership + function isMember(address _account) external view returns (bool) { + if (lockManager.lockedBalances(_account) > 0) return true; + else if (lockManager.token().balanceOf(_account) > 0) return true; + return false; + } + + /// @inheritdoc ILockToVoteBase + function canVote( + uint256 _proposalId, + address _voter + ) external view returns (bool) { + ProposalVoting storage proposal_ = proposals[_proposalId]; + + return _canVote(proposal_, _voter, lockManager.lockedBalances(_voter)); + } + + /// @inheritdoc ILockToVote + function vote( + uint256 _proposalId, + address _voter, + VoteOption _voteOption, + uint256 _newVotingPower + ) external auth(LOCK_MANAGER_PERMISSION_ID) { + ProposalVoting storage proposal_ = proposals[_proposalId]; + + if (!_canVote(proposal_, _voter, _newVotingPower)) { + revert VoteCastForbidden(_proposalId, _voter); + } + + // Add the difference between the new voting power and the current one + + uint256 diff = _newVotingPower - proposal_.approvals[_voter]; + proposal_.approvalTally += diff; + proposal_.approvals[_voter] += diff; + + emit VoteCast(_proposalId, _voter, _voteOption, _newVotingPower); + + _checkEarlyExecution(_proposalId, proposal_, _voter); + } + + /// @inheritdoc ILockToVote + function clearVote( + uint256 _proposalId, + address _voter + ) external auth(LOCK_MANAGER_PERMISSION_ID) { + ProposalVoting storage proposal_ = proposals[_proposalId]; + + if (proposal_.approvals[_voter] == 0 || !_isProposalOpen(proposal_)) + return; + + // Subtract the old votes from the global tally + proposal_.approvalTally -= proposal_.approvals[_voter]; + + // Clear the voting power + proposal_.approvals[_voter] = 0; + + emit VoteCleared(_proposalId, _voter); + } + + /// @inheritdoc ILockToVote + function usedVotingPower( + uint256 proposalId, + address voter + ) public view returns (uint256) { + return proposals[proposalId].approvals[voter]; + } + + /// @inheritdoc IProposal + function hasSucceeded(uint256 _proposalId) external view returns (bool) { + ProposalVoting storage proposal_ = proposals[_proposalId]; + return _hasSucceeded(proposal_); + } + + /// @inheritdoc IProposal + function canExecute(uint256 _proposalId) external view returns (bool) { + ProposalVoting storage proposal_ = proposals[_proposalId]; + return _canExecute(proposal_); + } + + /// @inheritdoc IProposal + function execute( + uint256 _proposalId + ) external auth(EXECUTE_PROPOSAL_PERMISSION_ID) { + ProposalVoting storage proposal_ = proposals[_proposalId]; + + if (!_canExecute(proposal_)) { + revert ExecutionForbidden(_proposalId); + } + + _execute(_proposalId, proposal_); + } + + /// @inheritdoc ILockToVoteBase + function underlyingToken() external view returns (IERC20) { + return lockManager.underlyingToken(); + } + + /// @inheritdoc ILockToVoteBase + function token() external view returns (IERC20) { + return lockManager.token(); + } + + /// @inheritdoc ILockToVote + function updatePluginSettings( + LockToVoteSettings calldata _newSettings + ) external auth(UPDATE_VOTING_SETTINGS_PERMISSION_ID) { + _updatePluginSettings(_newSettings); + } + + // Internal helpers + + function _isProposalOpen( + ProposalVoting storage proposal_ + ) internal view returns (bool) { + uint64 currentTime = block.timestamp.toUint64(); + + return + proposal_.parameters.startDate <= currentTime && + currentTime < proposal_.parameters.endDate && + !proposal_.executed; + } + + function _canVote( + ProposalVoting storage proposal_, + address _voter, + uint256 _newVotingBalance + ) internal view returns (bool) { + // The proposal vote hasn't started or has already ended. + if (!_isProposalOpen(proposal_)) { + return false; + } + // More balance could be added + else if (_newVotingBalance <= proposal_.approvals[_voter]) { + return false; + } + + return true; + } + + function _canExecute( + ProposalVoting storage proposal_ + ) internal view returns (bool) { + if (proposal_.executed) { + return false; + } else if (proposal_.parameters.endDate < block.timestamp) { + return false; + } else if (!_hasSucceeded(proposal_)) { + return false; + } + + return true; + } + + function _minApprovalTally( + ProposalVoting storage proposal_ + ) internal view returns (uint256 _minTally) { + /// @dev Checking against the totalSupply() of the **underlying token**. + /// @dev LP tokens could have important supply variations and this would impact the value of existing votes, after created. + /// @dev However, the total supply of the underlying token (USDC, USDT, DAI, etc) will experiment little to no variations in comparison. + + // NOTE: Assuming a 1:1 correlation between token() and underlyingToken() + + _minTally = _applyRatioCeiled( + lockManager.underlyingToken().totalSupply(), + proposal_.parameters.minApprovalRatio + ); + } + + function _hasSucceeded( + ProposalVoting storage proposal_ + ) internal view returns (bool) { + return + proposal_.approvalTally >= _minApprovalTally(proposal_) && + proposal_.approvalTally > 0; + } + + /// @notice Validates and returns the proposal dates. + /// @param _start The start date of the proposal. + /// If 0, the current timestamp is used and the vote starts immediately. + /// @param _end The end date of the proposal. If 0, `_start + minDuration` is used. + /// @return startDate The validated start date of the proposal. + /// @return endDate The validated end date of the proposal. + 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 + settings.minProposalDuration; + + if (_end == 0) { + endDate = earliestEndDate; + } else { + endDate = _end; + + if (endDate < earliestEndDate) { + revert DateOutOfBounds({ + limit: earliestEndDate, + actual: endDate + }); + } + } + } + + function _checkEarlyExecution( + uint256 _proposalId, + ProposalVoting storage proposal_, + address _voter + ) internal { + if (!_canExecute(proposal_)) { + return; + } else if ( + !dao().hasPermission( + address(this), + _voter, + EXECUTE_PROPOSAL_PERMISSION_ID, + _msgData() + ) + ) { + return; + } + + _execute(_proposalId, proposal_); + } + + function _execute( + uint256 _proposalId, + ProposalVoting storage proposal_ + ) internal { + proposal_.executed = true; + + // IProposal's target execution + _execute( + bytes32(_proposalId), + proposal_.actions, + proposal_.allowFailureMap + ); + + emit Executed(_proposalId); + + // Notify the LockManager to stop tracking this proposal ID + lockManager.proposalEnded(_proposalId); + } + + function _updatePluginSettings( + LockToVoteSettings memory _newSettings + ) internal { + settings.minApprovalRatio = _newSettings.minApprovalRatio; + settings.minProposalDuration = _newSettings.minProposalDuration; + } +} diff --git a/src/interfaces/ILockManager.sol b/src/interfaces/ILockManager.sol index 7f158c6..0637219 100644 --- a/src/interfaces/ILockManager.sol +++ b/src/interfaces/ILockManager.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.17; -import {ILockToVoteBase} from "./ILockToVote.sol"; +import {ILockToVoteBase, VoteOption} from "./ILockToVote.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @notice Defines whether locked funds can be unlocked at any time or not @@ -11,10 +11,18 @@ enum UnlockMode { EARLY } -/// @notice The struct containing the LockManager helper settings +/// @notice Defines wether the voting plugin expects approvals or votes +enum PluginMode { + APPROVAL, + VOTING +} + +/// @notice The struct containing the LockManager helper settings. They are immutable after deployed. struct LockManagerSettings { - /// @param lockMode The mode defining whether funds can be unlocked at any time or not + /// @notice The mode defining whether funds can be unlocked at any time or not UnlockMode unlockMode; + /// @notice Wether the plugins expects approvals or votes + PluginMode pluginMode; } /// @title ILockManager @@ -39,13 +47,21 @@ interface ILockManager { /// @notice Locks the balance currently allowed by msg.sender on this contract function lock() external; - /// @notice Locks the balance currently allowed by msg.sender on this contract and registers a vote on the target plugin + /// @notice Locks the balance currently allowed by msg.sender on this contract and registers an approval on the target plugin + /// @param proposalId The ID of the proposal where the approval will be registered + function lockAndApprove(uint256 proposalId) external; + + /// @notice Locks the balance currently allowed by msg.sender on this contract and registers the given vote on the target plugin /// @param proposalId The ID of the proposal where the vote will be registered - function lockAndVote(uint256 proposalId) external; + function lockAndVote(uint256 proposalId, VoteOption vote) external; + + /// @notice Uses the locked balance to place an approval on the given proposal for the registered plugin + /// @param proposalId The ID of the proposal where the approval will be registered + function approve(uint256 proposalId) external; - /// @notice Uses the locked balance to place a vote on the given proposal for the given plugin + /// @notice Uses the locked balance to place the given vote on the given proposal for the registered plugin /// @param proposalId The ID of the proposal where the vote will be registered - function vote(uint256 proposalId) external; + function vote(uint256 proposalId, VoteOption vote) external; /// @notice Checks if an account can participate on a proposal. This can be because the proposal /// - has not started, @@ -56,7 +72,10 @@ interface ILockManager { /// @param voter The account address to be checked. /// @return Returns true if the account is allowed to vote. /// @dev The function assumes that the queried proposal exists. - function canVote(uint256 proposalId, address voter) external view returns (bool); + function canVote( + uint256 proposalId, + address voter + ) external view returns (bool); /// @notice If the mode allows it, releases all active locks placed on active proposals and transfers msg.sender's locked balance back. Depending on the current mode, it withdraws only if no locks are being used in active proposals. function unlock() external; diff --git a/src/interfaces/ILockToApprove.sol b/src/interfaces/ILockToApprove.sol new file mode 100644 index 0000000..23eee03 --- /dev/null +++ b/src/interfaces/ILockToApprove.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +pragma solidity ^0.8.17; + +import {IDAO} from "@aragon/osx-commons-contracts/src/dao/IDAO.sol"; +import {Action} from "@aragon/osx-commons-contracts/src/executors/IExecutor.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {PluginUUPSUpgradeable} from "@aragon/osx-commons-contracts/src/plugin/PluginUUPSUpgradeable.sol"; +import {IPlugin} from "@aragon/osx-commons-contracts/src/plugin/IPlugin.sol"; +import {ILockManager} from "./ILockManager.sol"; +import {ILockToVoteBase} from "./ILockToVoteBase.sol"; + +/// @notice A container for the voting settings that will be applied as parameters on proposal creation. +/// @param minApprovalRatio The minimum approval ratio required to approve over the total supply. +/// Its value has to be in the interval [0, 10^6] defined by `RATIO_BASE = 10**6`. +/// @param minProposalDuration The minimum duration of the proposal voting stage in seconds. +struct LockToApproveSettings { + uint32 minApprovalRatio; + uint64 minProposalDuration; +} + +/// @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 approvalTally The vote tally of the proposal. +/// @param approvals The voting power cast by each voter. +/// @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. +/// @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. +struct ProposalApproval { + bool executed; + ProposalApprovalParameters parameters; + uint256 approvalTally; + mapping(address => uint256) approvals; + Action[] actions; + uint256 allowFailureMap; + IPlugin.TargetConfig targetConfig; +} + +/// @notice A container for the proposal parameters at the time of proposal creation. +/// @param minApprovalRatio The approval threshold above which the proposal becomes executable. +/// 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. +struct ProposalApprovalParameters { + uint32 minApprovalRatio; + uint64 startDate; + uint64 endDate; +} + +/// @title ILockToApprove +/// @author Aragon X +/// @notice Governance plugin allowing token holders to use tokens locked without a snapshot requirement and engage in proposals immediately +interface ILockToApprove is ILockToVoteBase { + /// @notice Returns wether the given address can approve or increase the amount of tokens assigned to aprove a proposal + function canApprove(uint256 proposalId, address voter) external view returns (bool); + + /// @notice Registers an approval for the given proposal. + /// @param proposalId The ID of the proposal to vote on. + /// @param voter The address of the account whose vote will be registered + /// @param newVotingPower The new balance that should be allocated to the voter. It can only be bigger. + /// @dev newVotingPower updates any prior voting power, it does not add to the existing amount. + function approve(uint256 proposalId, address voter, uint256 newVotingPower) external; + + /// @notice Reverts the existing voter's approval, if any. + /// @param proposalId The ID of the proposal. + /// @param voter The voter's address. + function clearApproval(uint256 proposalId, address voter) external; + + /// @notice Updates the voting settings, which will be applied to the next proposal being created. + /// @param newSettings The new settings, including the minimum approval ratio and the minimum proposal duration. + function updatePluginSettings(LockToApproveSettings calldata newSettings) external; + + event Approved(uint256 proposalId, address voter, uint256 newVotingPower); + event VoteCleared(uint256 proposalId, address voter); + error ApprovalForbidden(uint256 proposalId, address voter); +} diff --git a/src/interfaces/ILockToVote.sol b/src/interfaces/ILockToVote.sol index bc5bd71..e801f51 100644 --- a/src/interfaces/ILockToVote.sol +++ b/src/interfaces/ILockToVote.sol @@ -2,12 +2,16 @@ pragma solidity ^0.8.17; -import {IDAO} from "@aragon/osx-commons-contracts/src/dao/IDAO.sol"; +import {ILockToVoteBase} from "./ILockToVoteBase.sol"; import {Action} from "@aragon/osx-commons-contracts/src/executors/IExecutor.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {PluginUUPSUpgradeable} from "@aragon/osx-commons-contracts/src/plugin/PluginUUPSUpgradeable.sol"; import {IPlugin} from "@aragon/osx-commons-contracts/src/plugin/IPlugin.sol"; -import {ILockManager} from "./ILockManager.sol"; + +/// @notice A container for the voting settings that will be applied as parameters on proposal creation. +/// @param minProposalDuration The minimum duration of the proposal voting stage in seconds. +struct LockToVoteSettings { + // uint32 minApprovalRatio; + uint64 minProposalDuration; +} /// @notice A container for proposal-related information. /// @param executed Whether the proposal is executed or not. @@ -21,80 +25,63 @@ import {ILockManager} from "./ILockManager.sol"; /// @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. -struct Proposal { +struct ProposalVoting { bool executed; - ProposalParameters parameters; - uint256 approvalTally; + ProposalVotingParameters parameters; + VoteTally tally; mapping(address => uint256) approvals; Action[] actions; uint256 allowFailureMap; IPlugin.TargetConfig targetConfig; } +struct VoteTally { + uint256 yes; + uint256 no; + uint256 abstain; +} + /// @notice A container for the proposal parameters at the time of proposal creation. /// @param minApprovalRatio The approval threshold above which the proposal becomes executable. /// 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. -struct ProposalParameters { +struct ProposalVotingParameters { uint32 minApprovalRatio; uint64 startDate; uint64 endDate; } -/// @notice A container for the voting settings that will be applied as parameters on proposal creation. -/// @param minApprovalRatio The support threshold value. -/// Its value has to be in the interval [0, 10^6] defined by `RATIO_BASE = 10**6`. -/// @param minProposalDuration The minimum duration of the proposal voting stage in seconds. -struct LockToVoteSingleSettings { - uint32 minApprovalRatio; - uint64 minProposalDuration; +/// @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 } /// @title ILockToVote /// @author Aragon X /// @notice Governance plugin allowing token holders to use tokens locked without a snapshot requirement and engage in proposals immediately -interface ILockToVoteBase { - /// @notice Returns the address of the manager contract, which holds the locked balances and the allocated vote balances. - function lockManager() external view returns (ILockManager); - - /// @notice Returns the address of the token contract used to determine the voting power. - /// @return The address of the token used for voting. - function token() external view returns (IERC20); - - /// @notice If applicable, returns the address of the token that can be stacked to obtain `token()`. Else, it returns the voting token's address. - /// @return The address of the underlying token. - function underlyingToken() external view returns (IERC20); - - /// @notice Internal function to check if a proposal is still open. - /// @param _proposalId The ID of the proposal. - /// @return True if the proposal is open, false otherwise. - function isProposalOpen(uint256 _proposalId) external view returns (bool); - +interface ILockToVote is ILockToVoteBase { /// @notice Returns wether the given address can vote or increase the amount of tokens assigned to a proposal - function canVote( - uint256 proposalId, - address voter - ) external view returns (bool); - - error NoVotingPower(); - error ProposalAlreadyExists(uint256 proposalId); - error DateOutOfBounds(uint256 limit, uint256 actual); - error VoteCastForbidden(uint256 proposalId, address voter); - error ExecutionForbidden(uint256 proposalId); + function canVote(uint256 proposalId, address voter) external view returns (bool); - event Executed(uint256 proposalId); -} - -interface ILockToVoteSingle is ILockToVoteBase { - /// @notice Registers an approval vote for the given proposal. + /// @notice Registers a vote for the given proposal. /// @param proposalId The ID of the proposal to vote on. /// @param voter The address of the account whose vote will be registered + /// @param voteOption The value of the new vote to register. If an existing vote existed, it will be replaced. /// @param newVotingPower The new balance that should be allocated to the voter. It can only be bigger. /// @dev newVotingPower updates any prior voting power, it does not add to the existing amount. function vote( uint256 proposalId, address voter, + VoteOption voteOption, uint256 newVotingPower ) external; @@ -103,24 +90,18 @@ interface ILockToVoteSingle is ILockToVoteBase { /// @param voter The voter's address. function clearVote(uint256 proposalId, address voter) external; - /// @notice Returns whether the account has voted for the proposal. - /// @param proposalId The ID of the proposal. - /// @param voter The account address to be checked. - /// @return The amount of balance that has been allocated to the proposal by the given account. - function usedVotingPower( - uint256 proposalId, - address voter - ) external view returns (uint256); - /// @notice Updates the voting settings, which will be applied to the next proposal being created. /// @param newSettings The new settings, including the minimum approval ratio and the minimum proposal duration. function updatePluginSettings( - LockToVoteSingleSettings calldata newSettings + LockToVoteSettings calldata newSettings ) external; - event VoteCast(uint256 proposalId, address voter, uint256 newVotingPower); + event VoteCast( + uint256 proposalId, + address voter, + VoteOption voteOption, + uint256 newVotingPower + ); event VoteCleared(uint256 proposalId, address voter); + error VoteCastForbidden(uint256 proposalId, address voter); } - -interface ILockToVoteMajority is ILockToVoteBase { -} \ No newline at end of file diff --git a/src/interfaces/ILockToVoteBase.sol b/src/interfaces/ILockToVoteBase.sol new file mode 100644 index 0000000..56e8a56 --- /dev/null +++ b/src/interfaces/ILockToVoteBase.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +pragma solidity ^0.8.17; + +import {IDAO} from "@aragon/osx-commons-contracts/src/dao/IDAO.sol"; +import {Action} from "@aragon/osx-commons-contracts/src/executors/IExecutor.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {PluginUUPSUpgradeable} from "@aragon/osx-commons-contracts/src/plugin/PluginUUPSUpgradeable.sol"; +import {IPlugin} from "@aragon/osx-commons-contracts/src/plugin/IPlugin.sol"; +import {ILockManager} from "./ILockManager.sol"; + +/// @title ILockToVoteBase +/// @author Aragon X +interface ILockToVoteBase { + /// @notice Returns the address of the manager contract, which holds the locked balances and the allocated vote balances. + function lockManager() external view returns (ILockManager); + + /// @notice Returns the address of the token contract used to determine the voting power. + /// @return The address of the token used for voting. + function token() external view returns (IERC20); + + /// @notice If applicable, returns the address of the token that can be stacked to obtain `token()`. Else, it returns the voting token's address. + /// @return The address of the underlying token. + function underlyingToken() external view returns (IERC20); + + /// @notice Internal function to check if a proposal is still open. + /// @param _proposalId The ID of the proposal. + /// @return True if the proposal is open, false otherwise. + function isProposalOpen(uint256 _proposalId) external view returns (bool); + + /// @notice Returns whether the account has voted for the proposal. + /// @param proposalId The ID of the proposal. + /// @param voter The account address to be checked. + /// @return The amount of balance that has been allocated to the proposal by the given account. + function usedVotingPower(uint256 proposalId, address voter) external view returns (uint256); + + error NoVotingPower(); + error ProposalAlreadyExists(uint256 proposalId); + error DateOutOfBounds(uint256 limit, uint256 actual); + error ExecutionForbidden(uint256 proposalId); + + event Executed(uint256 proposalId); +} diff --git a/src/setup/LockToVotePluginSetup.sol b/src/setup/LockToVotePluginSetup.sol index 960ee79..6ec8d54 100644 --- a/src/setup/LockToVotePluginSetup.sol +++ b/src/setup/LockToVotePluginSetup.sol @@ -12,20 +12,20 @@ import {IDAO} from "@aragon/osx-commons-contracts/src/dao/IDAO.sol"; import {DAO} from "@aragon/osx/src/core/dao/DAO.sol"; import {PermissionLib} from "@aragon/osx-commons-contracts/src/permission/PermissionLib.sol"; import {PluginSetup, IPluginSetup} from "@aragon/osx-commons-contracts/src/plugin/setup/PluginSetup.sol"; -import {LockToVotePlugin} from "../LockToVotePlugin.sol"; +import {LockToApprovePlugin} from "../LockToApprovePlugin.sol"; import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; /// @title LockToVotePluginSetup /// @author Aragon Association - 2022-2024 -/// @notice The setup contract of the `LockToVotePlugin` contract. +/// @notice The setup contract of the `LockToApprovePlugin` contract. /// @custom:security-contact sirt@aragon.org contract LockToVotePluginSetup { // is PluginSetup { // using Address for address; // using Clones for address; // using ERC165Checker for address; -// /// @notice The address of the `LockToVotePlugin` base contract. -// LockToVotePlugin private immutable lockToVotePluginBase; +// /// @notice The address of the `LockToApprovePlugin` base contract. +// LockToApprovePlugin private immutable lockToVotePluginBase; // /// @notice The address of the `GovernanceERC20` base contract. // address public immutable governanceERC20Base; // /// @notice The address of the `GovernanceWrappedERC20` base contract. @@ -40,7 +40,7 @@ contract LockToVotePluginSetup { // string symbol; // } // struct InstallationParameters { -// LockToVotePlugin.PluginSettings settings; +// LockToApprovePlugin.PluginSettings settings; // TokenSettings tokenSettings; // // only used for GovernanceERC20 (when token is not passed) // GovernanceERC20.MintSettings mintSettings; @@ -64,7 +64,7 @@ contract LockToVotePluginSetup { // GovernanceERC20 _governanceERC20Base, // GovernanceWrappedERC20 _governanceWrappedERC20Base // ) { -// lockToVotePluginBase = new LockToVotePlugin(); +// lockToVotePluginBase = new LockToApprovePlugin(); // governanceERC20Base = address(_governanceERC20Base); // governanceWrappedERC20Base = address(_governanceWrappedERC20Base); // } @@ -76,7 +76,7 @@ contract LockToVotePluginSetup { // external // returns (address plugin, PreparedSetupData memory preparedSetupData) // { -// // Decode `_installParameters` to extract the params needed for deploying and initializing `LockToVotePlugin` contract, +// // Decode `_installParameters` to extract the params needed for deploying and initializing `LockToApprovePlugin` contract, // // and the required helpers // InstallationParameters // memory installationParams = decodeInstallationParams( @@ -121,7 +121,7 @@ contract LockToVotePluginSetup { // plugin = createERC1967Proxy( // address(lockToVotePluginBase), // abi.encodeCall( -// LockToVotePlugin.initialize, +// LockToApprovePlugin.initialize, // ( // IDAO(_dao), // installationParams.settings, diff --git a/test/LockManager.t.sol b/test/LockManager.t.sol index 391711f..ccf4f6f 100644 --- a/test/LockManager.t.sol +++ b/test/LockManager.t.sol @@ -8,7 +8,7 @@ import {Action} from "@aragon/osx-commons-contracts/src/executors/IExecutor.sol" import {createProxyAndCall} from "../src/util/proxy.sol"; import {IProposal} from "@aragon/osx-commons-contracts/src/plugin/extensions/proposal/IProposal.sol"; import {IPlugin} from "@aragon/osx-commons-contracts/src/plugin/IPlugin.sol"; -import {LockToVotePlugin} from "../src/LockToVotePlugin.sol"; +import {LockToApprovePlugin} from "../src/LockToApprovePlugin.sol"; import {ILockToVote} from "../src/interfaces/ILockToVote.sol"; import {LockManagerSettings, UnlockMode} from "../src/interfaces/ILockManager.sol"; import {LockManager} from "../src/LockManager.sol"; @@ -17,13 +17,13 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract LockManagerTest is AragonTest { DaoBuilder builder; DAO dao; - LockToVotePlugin plugin; + LockToApprovePlugin plugin; LockManager lockManager; IERC20 lockableToken; IERC20 underlyingToken; uint256 proposalId; - address immutable LOCK_TO_VOTE_BASE = address(new LockToVotePlugin()); + address immutable LOCK_TO_VOTE_BASE = address(new LockToApprovePlugin()); address immutable LOCK_MANAGER_BASE = address( new LockManager( IDAO(address(0)), LockManagerSettings(UnlockMode.STRICT), IERC20(address(0)), IERC20(address(0)) @@ -104,14 +104,14 @@ contract LockManagerTest is AragonTest { lockManager = new LockManager(dao, LockManagerSettings(UnlockMode.STRICT), lockableToken, underlyingToken); dao.grant(address(lockManager), alice, lockManager.UPDATE_SETTINGS_PERMISSION_ID()); vm.expectRevert(); - lockManager.setPluginAddress(LockToVotePlugin(address(0x5555))); + lockManager.setPluginAddress(LockToApprovePlugin(address(0x5555))); } function test_RevertWhen_SetPluginAddressWithoutThePermission() external whenCallingSetPluginAddress { // It should revert - (, LockToVotePlugin plugin2,,,) = builder.build(); - (, LockToVotePlugin plugin3,,,) = builder.build(); + (, LockToApprovePlugin plugin2,,,) = builder.build(); + (, LockToApprovePlugin plugin3,,,) = builder.build(); lockManager = new LockManager(dao, LockManagerSettings(UnlockMode.STRICT), lockableToken, underlyingToken); @@ -137,9 +137,10 @@ contract LockManagerTest is AragonTest { function test_WhenSetPluginAddressWithThePermission() external whenCallingSetPluginAddress { // It should update the address + // It should revert if trying to update it later - (, LockToVotePlugin plugin2,,,) = builder.build(); - (, LockToVotePlugin plugin3,,,) = builder.build(); + (, LockToApprovePlugin plugin2,,,) = builder.build(); + (, LockToApprovePlugin plugin3,,,) = builder.build(); lockManager = new LockManager(dao, LockManagerSettings(UnlockMode.STRICT), lockableToken, underlyingToken); dao.grant(address(lockManager), alice, lockManager.UPDATE_SETTINGS_PERMISSION_ID()); @@ -152,6 +153,10 @@ contract LockManagerTest is AragonTest { dao.grant(address(lockManager), alice, lockManager.UPDATE_SETTINGS_PERMISSION_ID()); lockManager.setPluginAddress(plugin3); assertEq(address(lockManager.plugin()), address(plugin3)); + + // Attempt to set when already defined + vm.expectRevert(abi.encodeWithSelector(SetPluginAddressForbidden.selector)); + lockManager.setPluginAddress(plugin2); } modifier givenNoLockedTokens() { diff --git a/test/LockManager.t.yaml b/test/LockManager.t.yaml index b1a55df..5d267a6 100644 --- a/test/LockManager.t.yaml +++ b/test/LockManager.t.yaml @@ -21,6 +21,7 @@ LockManagerTest: - when: setPluginAddress with the permission then: - it: should update the address + - it: should revert if trying to update it later # Locking and voting calls diff --git a/test/LockToVoteSingle.t.sol b/test/LockToApprove.t.sol similarity index 96% rename from test/LockToVoteSingle.t.sol rename to test/LockToApprove.t.sol index 1211ba3..3016570 100644 --- a/test/LockToVoteSingle.t.sol +++ b/test/LockToApprove.t.sol @@ -2,14 +2,14 @@ pragma solidity 0.8.17; import {AragonTest} from "./util/AragonTest.sol"; -import {LockToVoteSinglePlugin} from "../src/LockToVoteSinglePlugin.sol"; +import {LockToApprovePlugin} from "../src/LockToApprovePlugin.sol"; import {LockManager} from "../src/LockManager.sol"; import {LockManagerSettings, UnlockMode} from "../src/interfaces/ILockManager.sol"; import {ILockToVote} from "../src/interfaces/ILockToVote.sol"; import {DaoBuilder} from "./util/DaoBuilder.sol"; import {DAO, IDAO} from "@aragon/osx/src/core/dao/DAO.sol"; import {DaoUnauthorized} from "@aragon/osx-commons-contracts/src/permission/auth/auth.sol"; -import {LockToVoteSingleSettings, Proposal, ProposalParameters} from "../src/interfaces/ILockToVote.sol"; +import {LockToApproveSettings, ProposalApproval, ProposalApprovalParameters} from "../src/interfaces/ILockToApprove.sol"; import {TestToken} from "./mocks/TestToken.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {Action} from "@aragon/osx-commons-contracts/src/executors/IExecutor.sol"; @@ -25,13 +25,13 @@ contract LockToVoteTest is AragonTest { DaoBuilder builder; DAO dao; - LockToVoteSinglePlugin plugin; + LockToApprovePlugin plugin; LockManager lockManager; IERC20 lockableToken; IERC20 underlyingToken; uint256 proposalId; - address immutable LOCK_TO_VOTE_BASE = address(new LockToVoteSinglePlugin()); + address immutable LOCK_TO_VOTE_BASE = address(new LockToApprovePlugin()); address immutable LOCK_MANAGER_BASE = address( new LockManager( IDAO(address(0)), LockManagerSettings(UnlockMode.STRICT), IERC20(address(0)), IERC20(address(0)) @@ -81,7 +81,7 @@ contract LockToVoteTest is AragonTest { plugin.initialize( dao, lockManager, - LockToVoteSingleSettings({ + LockToApproveSettings({ minApprovalRatio: 100_000, // 10% minProposalDuration: 10 days }), @@ -98,7 +98,7 @@ contract LockToVoteTest is AragonTest { // It should set the DAO address // It should initialize normally - LockToVoteSingleSettings memory pluginSettings = LockToVoteSingleSettings({ + LockToApproveSettings memory pluginSettings = LockToApproveSettings({ minApprovalRatio: 100_000, // 10% minProposalDuration: 10 days }); @@ -106,11 +106,11 @@ contract LockToVoteTest is AragonTest { IPlugin.TargetConfig({target: address(dao), operation: IPlugin.Operation.Call}); bytes memory pluginMetadata = ""; - plugin = LockToVoteSinglePlugin( + plugin = LockToApprovePlugin( createProxyAndCall( address(LOCK_TO_VOTE_BASE), abi.encodeCall( - LockToVoteSinglePlugin.initialize, (dao, lockManager, pluginSettings, targetConfig, pluginMetadata) + LockToApprovePlugin.initialize, (dao, lockManager, pluginSettings, targetConfig, pluginMetadata) ) ) ); @@ -127,7 +127,7 @@ contract LockToVoteTest is AragonTest { // It should revert vm.startPrank(address(bob)); vm.expectRevert(); - LockToVoteSingleSettings memory newSettings = LockToVoteSingleSettings({ + LockToApproveSettings memory newSettings = LockToApproveSettings({ minApprovalRatio: 600000, // 60% minProposalDuration: 5 days }); @@ -135,7 +135,7 @@ contract LockToVoteTest is AragonTest { vm.startPrank(address(0x1337)); vm.expectRevert(); - newSettings = LockToVoteSingleSettings({ + newSettings = LockToApproveSettings({ minApprovalRatio: 600000, // 60% minProposalDuration: 5 days }); @@ -151,7 +151,7 @@ contract LockToVoteTest is AragonTest { // vm.startPrank(alice); dao.grant(address(plugin), alice, plugin.UPDATE_VOTING_SETTINGS_PERMISSION_ID()); - LockToVoteSingleSettings memory newSettings = LockToVoteSingleSettings({ + LockToApproveSettings memory newSettings = LockToApproveSettings({ minApprovalRatio: 700000, // 70% minProposalDuration: 3 days }); @@ -243,7 +243,7 @@ contract LockToVoteTest is AragonTest { ( bool open, bool executed, - ProposalParameters memory parameters, + ProposalApprovalParameters memory parameters, uint256 approvalTally, Action[] memory actions, uint256 allowFailureMap, @@ -298,7 +298,7 @@ contract LockToVoteTest is AragonTest { ( bool open, bool executed, - ProposalParameters memory parameters, + ProposalApprovalParameters memory parameters, uint256 approvalTally, Action[] memory pActions, uint256 allowFailureMap, @@ -363,7 +363,7 @@ contract LockToVoteTest is AragonTest { ( bool open, bool executed, - ProposalParameters memory parameters, + ProposalApprovalParameters memory parameters, uint256 approvalTally, Action[] memory actions, uint256 allowFailureMap, @@ -424,7 +424,7 @@ contract LockToVoteTest is AragonTest { ( bool open, bool executed, - ProposalParameters memory parameters, + ProposalApprovalParameters memory parameters, uint256 approvalTally, Action[] memory actions, uint256 allowFailureMap, @@ -722,7 +722,7 @@ contract LockToVoteTest is AragonTest { ( bool open, bool executed, - ProposalParameters memory parameters, + ProposalApprovalParameters memory parameters, uint256 approvalTally, Action[] memory actions, uint256 allowFailureMap, @@ -812,7 +812,7 @@ contract LockToVoteTest is AragonTest { ( bool open, bool executed, - ProposalParameters memory parameters, + ProposalApprovalParameters memory parameters, uint256 approvalTally, Action[] memory actions, uint256 allowFailureMap, @@ -959,7 +959,7 @@ contract LockToVoteTest is AragonTest { ( bool open, bool executed, - ProposalParameters memory parameters, + ProposalApprovalParameters memory parameters, uint256 approvalTally, Action[] memory actions, uint256 allowFailureMap, @@ -1051,7 +1051,7 @@ contract LockToVoteTest is AragonTest { // It Should set the new values // It Settings() should return the right values - LockToVoteSingleSettings memory newSettings = LockToVoteSingleSettings({ + LockToApproveSettings memory newSettings = LockToApproveSettings({ minApprovalRatio: 612345, // 61% minProposalDuration: 13.4 days }); @@ -1071,7 +1071,7 @@ contract LockToVoteTest is AragonTest { function test_RevertWhen_CallingUpdatePluginSettingsNotGranted() public givenNoUpdateVotingSettingsPermission { // It Should revert - LockToVoteSingleSettings memory newSettings = LockToVoteSingleSettings({ + LockToApproveSettings memory newSettings = LockToApproveSettings({ minApprovalRatio: 612345, // 61% minProposalDuration: 13.4 days }); diff --git a/test/LockToVoteSingle.t.yaml b/test/LockToApprove.t.yaml similarity index 100% rename from test/LockToVoteSingle.t.yaml rename to test/LockToApprove.t.yaml diff --git a/test/util/DaoBuilder.sol b/test/util/DaoBuilder.sol index 6920408..2afe066 100644 --- a/test/util/DaoBuilder.sol +++ b/test/util/DaoBuilder.sol @@ -5,8 +5,8 @@ import {Test} from "forge-std/Test.sol"; import {DAO} from "@aragon/osx/src/core/dao/DAO.sol"; import {createProxyAndCall, createSaltedProxyAndCall, predictProxyAddress} from "../../src/util/proxy.sol"; import {ALICE_ADDRESS} from "../constants.sol"; -import {LockToVotePlugin} from "../../src/LockToVotePlugin.sol"; -import {LockToVoteSingleSettings} from "../../src/interfaces/ILockToVote.sol"; +import {LockToApprovePlugin} from "../../src/LockToApprovePlugin.sol"; +import {LockToApproveSettings} from "../../src/interfaces/ILockToApprove.sol"; import {LockManager} from "../../src/LockManager.sol"; import {LockManagerSettings, UnlockMode} from "../../src/interfaces/ILockManager.sol"; import {RATIO_BASE} from "@aragon/osx-commons-contracts/src/utils/math/Ratio.sol"; @@ -16,7 +16,7 @@ import {TestToken} from "../mocks/TestToken.sol"; contract DaoBuilder is Test { address immutable DAO_BASE = address(new DAO()); - address immutable LOCK_TO_VOTE_BASE = address(new LockToVotePlugin()); + address immutable LOCK_TO_VOTE_BASE = address(new LockToApprovePlugin()); struct MintEntry { address tokenHolder; @@ -68,7 +68,7 @@ contract DaoBuilder is Test { /// @dev The setup is done on block/timestamp 0 and tests should be made on block/timestamp 1 or later. function build() public - returns (DAO dao, LockToVotePlugin plugin, LockManager helper, IERC20 lockableToken, IERC20 underlyingToken) + returns (DAO dao, LockToApprovePlugin plugin, LockManager helper, IERC20 lockableToken, IERC20 underlyingToken) { // Deploy the DAO with `this` as root dao = DAO( @@ -96,18 +96,19 @@ contract DaoBuilder is Test { helper = new LockManager(dao, LockManagerSettings(unlockMode), lockableToken, underlyingToken); - LockToVoteSingleSettings memory targetContractSettings = - LockToVoteSingleSettings({minApprovalRatio: minApprovalRatio, minProposalDuration: minProposalDuration}); + LockToApproveSettings memory targetContractSettings = + LockToApproveSettings({minApprovalRatio: minApprovalRatio, minProposalDuration: minProposalDuration}); IPlugin.TargetConfig memory targetConfig = IPlugin.TargetConfig({target: address(dao), operation: IPlugin.Operation.Call}); bytes memory pluginMetadata = ""; - plugin = LockToVotePlugin( + plugin = LockToApprovePlugin( createProxyAndCall( address(LOCK_TO_VOTE_BASE), abi.encodeCall( - LockToVotePlugin.initialize, (dao, helper, targetContractSettings, targetConfig, pluginMetadata) + LockToApprovePlugin.initialize, + (dao, helper, targetContractSettings, targetConfig, pluginMetadata) ) ) );