diff --git a/src/voting/FixedQuestion.sol b/src/voting/FixedQuestion.sol index e25f4bf..6008f8f 100644 --- a/src/voting/FixedQuestion.sol +++ b/src/voting/FixedQuestion.sol @@ -2,14 +2,23 @@ pragma solidity ^0.8.20; import { Question } from "./Question.sol"; +import { IFixedQuestion, IQuestion } from "./interfaces/IFixedQuestion.sol"; -contract FixedQuestion is Question { - // Mapping to store user votes +/// @title Fixed Question Contract for Voting System +/// @dev Implements a fixed-choice voting system where users can only vote once +/// @notice This contract allows users to vote on predefined options, with each user limited to one vote +contract FixedQuestion is Question, IFixedQuestion { + // Mapping to store user votes: user address => option ID mapping(address => uint256) private userVotes; - // Custom errors - error UserAlreadyVoted(); - + /// @notice Constructor to initialize the fixed question + /// @dev Sets up the initial state and options for the fixed question + /// @param initialOwner The address of the initial owner of the contract + /// @param _title The title of the question + /// @param _description The description of the question + /// @param _deadline The deadline for voting + /// @param _pointsAddress The address of the Points contract + /// @param initialOptions An array of initial options for the question constructor( address initialOwner, string memory _title, @@ -19,20 +28,27 @@ contract FixedQuestion is Question { Option[] memory initialOptions ) Question(initialOwner, _title, _description, _deadline, _pointsAddress) { questionType = QuestionType.Fixed; + // Add initial options for (uint256 i = 0; i < initialOptions.length; i++) { _addOption(initialOptions[i].title, initialOptions[i].description); } } + /// @inheritdoc Question function _processVote(uint256 optionId) internal override { - // Implement voting logic + // Check if the user has already voted if (userVotes[msg.sender] != 0) { revert UserAlreadyVoted(); } + // Record the user's vote userVotes[msg.sender] = optionId; } - function hasVoted(address voter, uint256 optionId) public view override returns (bool) { + /// @inheritdoc Question + function hasVoted( + address voter, + uint256 optionId + ) public view override(IQuestion, Question) returns (bool) { return userVotes[voter] == optionId; } } diff --git a/src/voting/OpenQuestion.sol b/src/voting/OpenQuestion.sol new file mode 100644 index 0000000..971a39e --- /dev/null +++ b/src/voting/OpenQuestion.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import { Question } from "./Question.sol"; +import { IOpenQuestion, IQuestion } from "./interfaces/IOpenQuestion.sol"; + +/// @title OpenQuestion Contract +/// @dev Implements an open-ended question where users can add options and vote +contract OpenQuestion is Question, IOpenQuestion { + // Mapping to store user votes for each option + mapping(address voter => mapping(uint256 optionId => bool hasVoted)) private userVotes; + + /// @inheritdoc IOpenQuestion + /// @notice Minimum points required to add an option + uint256 public override minPointsToAddOption; + + /// @notice Initializes the OpenQuestion contract + /// @dev Sets up the question details and minimum points required to add an option + /// @param initialOwner The address of the initial owner of the question + /// @param _title The title of the question + /// @param _description The description of the question + /// @param _deadline The deadline for voting on the question + /// @param _pointsAddress The address of the Points contract + /// @param _minPointsToAddOption The minimum points required to add a new option + constructor( + address initialOwner, + string memory _title, + string memory _description, + uint256 _deadline, + address _pointsAddress, + uint256 _minPointsToAddOption + ) Question(initialOwner, _title, _description, _deadline, _pointsAddress) { + questionType = QuestionType.Open; + minPointsToAddOption = _minPointsToAddOption; + } + + /// @inheritdoc IOpenQuestion + /// @notice Adds a new option to the question + /// @dev Checks if the user has sufficient points to add the option + /// @param _title The title of the new option + /// @param _description The description of the new option + function addOption(string memory _title, string memory _description) external override { + if (points.balanceAtTimestamp(msg.sender, deadline) < minPointsToAddOption) { + revert InsufficientPoints(); + } + _addOption(_title, _description); + } + + /// @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); + } + userVotes[msg.sender][optionId] = true; + } + + /// @inheritdoc IQuestion + /// @notice Checks if a user has voted for a specific option + /// @dev Returns true if the user has voted for the option, false otherwise + /// @param voter The address of the user + /// @param optionId The ID of the option + function hasVoted( + address voter, + uint256 optionId + ) public view override(IQuestion, Question) returns (bool) { + return userVotes[voter][optionId]; + } + + /// @inheritdoc IOpenQuestion + /// @notice Updates the minimum points required to add a new option + /// @dev Only callable by the owner + /// @param _minPointsToAddOption The new minimum points required + function updateMinPointsToAddOption(uint256 _minPointsToAddOption) external override onlyOwner { + minPointsToAddOption = _minPointsToAddOption; + emit MinPointsToAddOptionUpdated(_minPointsToAddOption); + } +} diff --git a/src/voting/Question.sol b/src/voting/Question.sol index f2e15e3..8f7a8a7 100644 --- a/src/voting/Question.sol +++ b/src/voting/Question.sol @@ -2,10 +2,12 @@ pragma solidity ^0.8.20; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IQuestion } from "./interfaces/IQuestion.sol"; import { IPoints } from "../points/interfaces/IPoints.sol"; +/// @title Abstract Question Contract for Voting System +/// @dev This contract implements the base functionality for a voting question +/// @notice This contract allows users to vote on options and manages the voting process abstract contract Question is Ownable, IQuestion { // State variables uint256 public immutable deploymentTime; @@ -21,7 +23,13 @@ abstract contract Question is Ownable, IQuestion { // Mapping to store points accrued for each option mapping(uint256 optionId => uint256 points) public optionPointsAccrued; - // Constructor + /// @notice Constructor to initialize the question + /// @dev Sets up the initial state of the question + /// @param initialOwner The address of the initial owner of the contract + /// @param _title The title of the question + /// @param _description The description of the question + /// @param _deadline The deadline for voting + /// @param _pointsAddress The address of the Points contract constructor( address initialOwner, string memory _title, @@ -39,7 +47,7 @@ abstract contract Question is Ownable, IQuestion { options.push(Option("", "", msg.sender)); } - // External functions + /// @inheritdoc IQuestion function vote(uint256 optionId) external { if (block.timestamp >= deadline) revert VotingEnded(); if (optionId == 0 || optionId > options.length) revert InvalidOption(); @@ -54,31 +62,36 @@ abstract contract Question is Ownable, IQuestion { emit Voted(msg.sender, optionId, timestamp); } + /// @inheritdoc IQuestion function updateTitle(string memory _title) external onlyOwner { title = _title; emit QuestionUpdated(_title, description, deadline); } + /// @inheritdoc IQuestion function updateDescription(string memory _description) external onlyOwner { description = _description; emit QuestionUpdated(title, _description, deadline); } + /// @inheritdoc IQuestion function updateDeadline(uint256 _deadline) external onlyOwner { deadline = _deadline; emit QuestionUpdated(title, description, _deadline); } - // External view functions + /// @inheritdoc IQuestion function getOptions() external view returns (Option[] memory) { return options; } + /// @inheritdoc IQuestion function getOption(uint256 optionId) external view returns (Option memory) { if (optionId >= options.length) revert InvalidOption(); return options[optionId]; } + /// @inheritdoc IQuestion function getQuestionView(address user) external view returns (QuestionView memory) { uint256 totalVotes = 0; // Adjust the array size to exclude the empty option at index 0 @@ -115,14 +128,20 @@ abstract contract Question is Ownable, IQuestion { // Internal functions + /// @dev Processes a vote for a specific option + /// @param optionId The ID of the option being voted for function _processVote(uint256 optionId) internal virtual; + /// @dev Adds a new option to the question + /// @param _title The title of the new option + /// @param _description The description of the new option function _addOption(string memory _title, string memory _description) internal { uint256 optionId = options.length; options.push(Option(_title, _description, msg.sender)); emit NewOption(msg.sender, optionId, _title); } + /// @inheritdoc IQuestion function getStatus() public view returns (Status) { if (block.timestamp < deadline) { return Status.Active; @@ -131,5 +150,6 @@ abstract contract Question is Ownable, IQuestion { } } + /// @inheritdoc IQuestion function hasVoted(address voter, uint256 optionId) public view virtual returns (bool); } diff --git a/src/voting/interfaces/IFixedQuestion.sol b/src/voting/interfaces/IFixedQuestion.sol new file mode 100644 index 0000000..7411281 --- /dev/null +++ b/src/voting/interfaces/IFixedQuestion.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import { IQuestion } from "./IQuestion.sol"; + +/// @title IFixedQuestion Interface +/// @notice Interface for fixed-choice voting questions +/// @dev Extends the IQuestion interface with specific functionality for fixed-choice questions +interface IFixedQuestion is IQuestion { + /// @notice Error thrown when a user attempts to vote more than once + /// @dev This error should be used in the implementation to prevent multiple votes from the same user + error UserAlreadyVoted(); +} diff --git a/src/voting/interfaces/IOpenQuestion.sol b/src/voting/interfaces/IOpenQuestion.sol new file mode 100644 index 0000000..8139ea0 --- /dev/null +++ b/src/voting/interfaces/IOpenQuestion.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import { IQuestion } from "./IQuestion.sol"; + +/// @title IOpenQuestion Interface +/// @dev Interface for an open-ended question where users can add new options +interface IOpenQuestion is IQuestion { + /// @notice Thrown when a user tries to add an option without sufficient points + error InsufficientPoints(); + + /// @notice Thrown when a user tries to vote for an option they've already voted for + /// @param voter The address of the voter + /// @param optionId The ID of the option + error UserAlreadyVotedThisOption(address voter, uint256 optionId); + + /// @notice Emitted when the minimum points required to add an option is updated + /// @param newMinPoints The new minimum points value + event MinPointsToAddOptionUpdated(uint256 newMinPoints); + + /// @notice Returns the minimum points required to add a new option + /// @return The minimum points required + function minPointsToAddOption() external view returns (uint256); + + /// @notice Adds a new option to the question + /// @dev Requires the caller to have sufficient points + /// @param _title The title of the new option + /// @param _description The description of the new option + function addOption(string memory _title, string memory _description) external; + + /// @notice Updates the minimum points required to add a new option + /// @dev Can only be called by the contract owner or authorized role + /// @param _minPointsToAddOption The new minimum points value + function updateMinPointsToAddOption(uint256 _minPointsToAddOption) external; +} diff --git a/src/voting/interfaces/IQuestion.sol b/src/voting/interfaces/IQuestion.sol index d8ddd50..d0cacc9 100644 --- a/src/voting/interfaces/IQuestion.sol +++ b/src/voting/interfaces/IQuestion.sol @@ -3,14 +3,18 @@ pragma solidity ^0.8.20; import { IPoints } from "../../points/interfaces/IPoints.sol"; +/// @title Question Interface for a Voting System +/// @dev Interface for managing questions in a voting system with various options and views interface IQuestion { // Enums + /// @dev Represents the current status of a question enum Status { Null, Active, Ended } + /// @dev Defines the type of question enum QuestionType { Null, Fixed, @@ -18,12 +22,14 @@ interface IQuestion { } // Structs + /// @dev Represents a voting option struct Option { string title; string description; address proposer; } + /// @dev Represents a view of a voting option with additional user-specific data struct OptionView { string title; string description; @@ -33,6 +39,7 @@ interface IQuestion { bool userVoted; } + /// @dev Represents a comprehensive view of a question with all its details struct QuestionView { QuestionType questionType; string title; @@ -46,43 +53,94 @@ interface IQuestion { } // Events + /// @dev Emitted when a question is updated + /// @param newTitle The new title of the question + /// @param newDescription The new description of the question + /// @param newDeadline The new deadline for voting event QuestionUpdated(string newTitle, string newDescription, uint256 newDeadline); + + /// @dev Emitted when a vote is cast + /// @param voter The address of the voter + /// @param optionId The ID of the option voted for + /// @param timestamp The timestamp of the vote event Voted(address indexed voter, uint256 indexed optionId, uint256 timestamp); + + /// @dev Emitted when a new option is added + /// @param proposer The address of the proposer + /// @param optionId The ID of the new option + /// @param title The title of the new option event NewOption(address indexed proposer, uint256 indexed optionId, string title); // Errors + /// @dev Thrown when trying to vote after the deadline error VotingEnded(); + + /// @dev Thrown when a user tries to vote more than once error AlreadyVoted(); + + /// @dev Thrown when an invalid option is selected error InvalidOption(); // Public variables + /// @notice Get the title of the question + /// @return The title of the question function title() external view returns (string memory); + /// @notice Get the description of the question + /// @return The description of the question function description() external view returns (string memory); + /// @notice Get the deadline for voting + /// @return The timestamp of the voting deadline function deadline() external view returns (uint256); + /// @notice Get the associated points contract + /// @return The IPoints interface of the associated points contract function points() external view returns (IPoints); // External functions + /// @notice Cast a vote for an option + /// @param optionId The ID of the option to vote for function vote(uint256 optionId) external; + /// @notice Update the title of the question + /// @param _title The new title function updateTitle(string memory _title) external; + /// @notice Update the description of the question + /// @param _description The new description function updateDescription(string memory _description) external; + /// @notice Update the deadline of the question + /// @param _deadline The new deadline timestamp function updateDeadline(uint256 _deadline) external; + /// @notice Get all options for the question + /// @return An array of all Option structs function getOptions() external view returns (Option[] memory); + /// @notice Get a specific option by its ID + /// @param optionId The ID of the option to retrieve + /// @return The Option struct for the specified ID function getOption(uint256 optionId) external view returns (Option memory); + /// @notice Get a comprehensive view of the question for a specific user + /// @param user The address of the user to get the view for + /// @return A QuestionView struct with all question details function getQuestionView(address user) external view returns (QuestionView memory); // Public functions + /// @notice Get the current status of the question + /// @return The current Status of the question function getStatus() external view returns (Status); + /// @notice Check if a specific user has voted for a specific option + /// @param voter The address of the voter to check + /// @param optionId The ID of the option to check + /// @return True if the user has voted for the option, false otherwise function hasVoted(address voter, uint256 optionId) external view returns (bool); + /// @notice Get the deployment time of the question contract + /// @return The timestamp when the contract was deployed function deploymentTime() external view returns (uint256); }