diff --git a/src/questions/OpenQuestion.sol b/src/questions/OpenQuestion.sol index 6163583..5183305 100644 --- a/src/questions/OpenQuestion.sol +++ b/src/questions/OpenQuestion.sol @@ -11,6 +11,9 @@ contract OpenQuestion is Question, IOpenQuestion { // Mapping to store user votes for each option mapping(address user => mapping(uint256 optionId => bool hasVoted)) private userVotes; + /// @notice Mapping to track if an option is vetoed + mapping(uint256 => bool) public vetoed; + /// @notice Initializes the OpenQuestion contract /// @dev Sets up the question details and minimum points required to add an option /// @param _space The address of the Space contract @@ -26,13 +29,11 @@ contract OpenQuestion is Question, IOpenQuestion { string[] memory _tags, uint256 _deadline, address _plasa - ) Question(_space, _points, _title, _description, _deadline, _tags, _plasa) { + ) + Question(_space, _points, _title, _description, _deadline, _tags, _plasa) + onlyAllowed(ISpaceAccessControl.PermissionName.CreateOpenQuestion) + { questionType = QuestionType.Open; - - // Check if creator has permission to create open questions - if (!space.hasPermission(ISpaceAccessControl.PermissionName.CreateOpenQuestion, msg.sender)) { - revert NotAllowed(msg.sender, ISpaceAccessControl.PermissionName.CreateOpenQuestion); - } } /// @inheritdoc IOpenQuestion @@ -40,19 +41,50 @@ contract OpenQuestion is Question, IOpenQuestion { string memory _title, string memory _description ) external whileActive returns (uint256 optionId) { - if (!space.canAddOpenQuestionOption(msg.sender)) { - revert InsufficientPoints(); - } + if (!space.canAddOpenQuestionOption(msg.sender)) revert InsufficientPoints(); + optionId = _addOption(_title, _description); } + /// @inheritdoc IOpenQuestion + function vetoOption( + uint256 optionId + ) + external + whileActive + validOption(optionId) + onlyAllowed(ISpaceAccessControl.PermissionName.VetoOpenQuestionOption) + { + vetoed[optionId] = true; + + emit OptionVetoed(msg.sender, optionId); + } + + /// @inheritdoc IOpenQuestion + function liftOptionVeto( + uint256 optionId + ) + external + whileActive + validOption(optionId) + onlyAllowed(ISpaceAccessControl.PermissionName.LiftVetoOpenQuestionOption) + { + vetoed[optionId] = false; + + emit OptionVetoLifted(msg.sender, optionId); + } + + /// @inheritdoc Question + function _isVetoed(uint256 optionId) internal view override returns (bool) { + return vetoed[optionId]; + } + /// @notice Processes a vote for a specific option /// @dev Overrides the base _processVote function to check for duplicate votes /// @param optionId The ID of the option being voted for function _processVote(uint256 optionId) internal override { - if (userVotes[msg.sender][optionId]) { - revert UserAlreadyVotedThisOption(msg.sender, optionId); - } + if (userVotes[msg.sender][optionId]) revert UserAlreadyVotedOption(msg.sender, optionId); + userVotes[msg.sender][optionId] = true; } diff --git a/src/questions/interfaces/IOpenQuestion.sol b/src/questions/interfaces/IOpenQuestion.sol index 349cffa..612b4da 100644 --- a/src/questions/interfaces/IOpenQuestion.sol +++ b/src/questions/interfaces/IOpenQuestion.sol @@ -11,11 +11,21 @@ interface IOpenQuestion is IQuestion { /// @dev This error should be used when enforcing point requirements for adding options error InsufficientPoints(); + /// @dev Emitted when a question is vetoed + /// @param moderator The address of the moderator who vetoed the option + /// @param optionId The ID of the option vetoed + event OptionVetoed(address indexed moderator, uint256 optionId); + + /// @dev Emitted when a veto is lifted + /// @param moderator The address of the moderator who lifted the veto + /// @param optionId The ID of the option whose veto was lifted + event OptionVetoLifted(address indexed moderator, uint256 optionId); + /// @notice Thrown when a user tries to vote for an option they've already voted for /// @dev This error helps prevent double voting /// @param voter The address of the voter attempting to vote again /// @param optionId The ID of the option the user is attempting to vote for again - error UserAlreadyVotedThisOption(address voter, uint256 optionId); + error UserAlreadyVotedOption(address voter, uint256 optionId); /// @notice Adds a new option to the question /// @dev Requires the caller to have sufficient points. Implementers should emit an event after adding the option. @@ -23,4 +33,14 @@ interface IOpenQuestion is IQuestion { /// @param _description The description of the new option /// @return optionId The ID of the newly added option function addOption(string memory _title, string memory _description) external returns (uint256 optionId); + + /// @notice Vets an option + /// @dev Only allowed users can veto options + /// @param optionId The ID of the option to veto + function vetoOption(uint256 optionId) external; + + /// @notice Lifts a veto on an option + /// @dev Only allowed users can lift vetoes + /// @param optionId The ID of the option to lift the veto on + function liftOptionVeto(uint256 optionId) external; } diff --git a/src/questions/interfaces/IQuestionView.sol b/src/questions/interfaces/IQuestionView.sol index 4d8f1cd..ed83069 100644 --- a/src/questions/interfaces/IQuestionView.sol +++ b/src/questions/interfaces/IQuestionView.sol @@ -55,6 +55,7 @@ interface IQuestionView { string proposerName; uint256 voteCount; uint256 pointsAtDeadline; + bool isVetoed; } /// @notice Struct containing user-specific option data