|
| 1 | +// SPDX-License-Identifier: UNLICENSED |
| 2 | +pragma solidity ^0.8.0; |
| 3 | + |
| 4 | +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; |
| 5 | +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; |
| 6 | + |
| 7 | +contract Questify is ReentrancyGuard { |
| 8 | + address public owner; |
| 9 | + IERC20 public rewardToken; |
| 10 | + |
| 11 | + struct Question { |
| 12 | + uint id; |
| 13 | + address author; |
| 14 | + string title; |
| 15 | + string content; |
| 16 | + string category; |
| 17 | + uint upvotes; |
| 18 | + uint downvotes; |
| 19 | + uint timestamp; |
| 20 | + } |
| 21 | + |
| 22 | + struct Answer { |
| 23 | + uint id; |
| 24 | + uint questionId; |
| 25 | + string content; |
| 26 | + address author; |
| 27 | + uint upvotes; |
| 28 | + uint downvotes; |
| 29 | + uint nextMilestone; |
| 30 | + uint timestamp; |
| 31 | + } |
| 32 | + |
| 33 | + struct UserStats { |
| 34 | + uint totalEarned; // Total rewards earned |
| 35 | + uint totalWithdrawn; // Total rewards withdrawn |
| 36 | + uint currentBalance; // Current rewards available for withdrawal |
| 37 | + } |
| 38 | + |
| 39 | + uint public questionCounter; |
| 40 | + uint public answerCounter; |
| 41 | + uint public rewardPerMilestone = 10 * 1e18; |
| 42 | + |
| 43 | + mapping(uint => Question) public questions; |
| 44 | + mapping(uint => Answer) public answers; |
| 45 | + mapping(uint => uint[]) public questionToAnswers; |
| 46 | + mapping(address => mapping(uint => bool)) public hasVotedQuestion; |
| 47 | + mapping(address => mapping(uint => bool)) public hasVotedAnswer; |
| 48 | + mapping(address => UserStats) public userStats; |
| 49 | + |
| 50 | + event QuestionPosted(uint id, address author, string title); |
| 51 | + event AnswerPosted(uint id, uint questionId, address author); |
| 52 | + event AnswerVoted(uint id, address voter, bool isUpvote); |
| 53 | + event QuestionVoted(uint id, address voter, bool isUpvote); |
| 54 | + event RewardAllocated(address user, uint amount, uint milestone); |
| 55 | + event TokensWithdrawn(address user, uint amount); |
| 56 | + event RewardPoolFunded(uint amount); |
| 57 | + event OwnershipTransferred( |
| 58 | + address indexed previousOwner, |
| 59 | + address indexed newOwner |
| 60 | + ); |
| 61 | + |
| 62 | + modifier onlyOwner() { |
| 63 | + require(msg.sender == owner, "Only owner can perform this action"); |
| 64 | + _; |
| 65 | + } |
| 66 | + |
| 67 | + constructor(address _rewardToken) { |
| 68 | + owner = msg.sender; |
| 69 | + rewardToken = IERC20(_rewardToken); |
| 70 | + } |
| 71 | + |
| 72 | + // Post a question |
| 73 | + function postQuestion( |
| 74 | + string memory _title, |
| 75 | + string memory _category, |
| 76 | + string memory _content |
| 77 | + ) public { |
| 78 | + questionCounter++; |
| 79 | + questions[questionCounter] = Question({ |
| 80 | + id: questionCounter, |
| 81 | + author: msg.sender, |
| 82 | + title: _title, |
| 83 | + content: _content, |
| 84 | + category: _category, |
| 85 | + upvotes: 0, |
| 86 | + downvotes: 0, |
| 87 | + timestamp: block.timestamp |
| 88 | + }); |
| 89 | + emit QuestionPosted(questionCounter, msg.sender, _title); |
| 90 | + } |
| 91 | + |
| 92 | + // Post an answer to a specific question |
| 93 | + function postAnswer(uint _questionId, string memory _content) public { |
| 94 | + require(questions[_questionId].id != 0, "The question does not exist"); |
| 95 | + |
| 96 | + answerCounter++; |
| 97 | + answers[answerCounter] = Answer({ |
| 98 | + id: answerCounter, |
| 99 | + questionId: _questionId, |
| 100 | + content: _content, |
| 101 | + author: msg.sender, |
| 102 | + upvotes: 0, |
| 103 | + downvotes: 0, |
| 104 | + nextMilestone: 10, |
| 105 | + timestamp: block.timestamp |
| 106 | + }); |
| 107 | + questionToAnswers[_questionId].push(answerCounter); |
| 108 | + emit AnswerPosted(answerCounter, _questionId, msg.sender); |
| 109 | + } |
| 110 | + |
| 111 | + function voteQuestion(uint _questionId, bool isUpvote) public { |
| 112 | + require(questions[_questionId].id != 0, "Question does not exist"); |
| 113 | + require( |
| 114 | + !hasVotedQuestion[msg.sender][_questionId], |
| 115 | + "Already voted on this question" |
| 116 | + ); |
| 117 | + |
| 118 | + hasVotedQuestion[msg.sender][_questionId] = true; |
| 119 | + if (isUpvote) { |
| 120 | + questions[_questionId].upvotes++; |
| 121 | + } else { |
| 122 | + questions[_questionId].downvotes++; |
| 123 | + } |
| 124 | + emit QuestionVoted(_questionId, msg.sender, isUpvote); |
| 125 | + } |
| 126 | + |
| 127 | + function voteAnswer(uint _answerId, bool isUpvote) public { |
| 128 | + require(answers[_answerId].id != 0, "Answer does not exist"); |
| 129 | + require( |
| 130 | + !hasVotedAnswer[msg.sender][_answerId], |
| 131 | + "Already voted on this answer" |
| 132 | + ); |
| 133 | + |
| 134 | + hasVotedAnswer[msg.sender][_answerId] = true; |
| 135 | + if (isUpvote) { |
| 136 | + checkAndAllocateReward(_answerId); |
| 137 | + answers[_answerId].upvotes++; |
| 138 | + } else { |
| 139 | + answers[_answerId].downvotes++; |
| 140 | + } |
| 141 | + emit AnswerVoted(_answerId, msg.sender, isUpvote); |
| 142 | + } |
| 143 | + |
| 144 | + // Withdraw available tokens |
| 145 | + function withdrawTokens() public nonReentrant { |
| 146 | + uint rewards = userStats[msg.sender].currentBalance; |
| 147 | + require(rewards > 0, "No rewards available for withdrawal"); |
| 148 | + require( |
| 149 | + rewardToken.balanceOf(address(this)) >= rewards, |
| 150 | + "Not enough tokens in the reward pool" |
| 151 | + ); |
| 152 | + |
| 153 | + userStats[msg.sender].currentBalance = 0; |
| 154 | + userStats[msg.sender].totalWithdrawn += rewards; |
| 155 | + rewardToken.transfer(msg.sender, rewards); |
| 156 | + |
| 157 | + emit TokensWithdrawn(msg.sender, rewards); |
| 158 | + } |
| 159 | + |
| 160 | + // Fund the reward pool |
| 161 | + function fundRewardPool(uint _amount) public onlyOwner { |
| 162 | + require( |
| 163 | + rewardToken.transferFrom(msg.sender, address(this), _amount), |
| 164 | + "Funding the reward pool failed" |
| 165 | + ); |
| 166 | + emit RewardPoolFunded(_amount); |
| 167 | + } |
| 168 | + |
| 169 | + // Fetch all questions (Gasless Read Function) |
| 170 | + function getAllQuestions() public view returns (Question[] memory) { |
| 171 | + Question[] memory allQuestions = new Question[](questionCounter); |
| 172 | + for (uint i = 1; i <= questionCounter; i++) { |
| 173 | + allQuestions[i - 1] = questions[i]; |
| 174 | + } |
| 175 | + return allQuestions; |
| 176 | + } |
| 177 | + |
| 178 | + function getQuestionsPaginated( |
| 179 | + uint start, |
| 180 | + uint limit |
| 181 | + ) public view returns (Question[] memory) { |
| 182 | + require(start > 0 && start <= questionCounter, "Invalid start index"); |
| 183 | + |
| 184 | + uint end = start + limit - 1; |
| 185 | + if (end > questionCounter) end = questionCounter; |
| 186 | + |
| 187 | + Question[] memory paginatedQuestions = new Question[](end - start + 1); |
| 188 | + for (uint i = start; i <= end; i++) { |
| 189 | + paginatedQuestions[i - start] = questions[i]; |
| 190 | + } |
| 191 | + return paginatedQuestions; |
| 192 | + } |
| 193 | + |
| 194 | + // Fetch all answers for a specific question (Gasless Read Function) |
| 195 | + function getAnswersForQuestion( |
| 196 | + uint _questionId |
| 197 | + ) public view returns (Answer[] memory) { |
| 198 | + uint[] memory answerIds = questionToAnswers[_questionId]; |
| 199 | + Answer[] memory questionAnswers = new Answer[](answerIds.length); |
| 200 | + |
| 201 | + for (uint i = 0; i < answerIds.length; i++) { |
| 202 | + questionAnswers[i] = answers[answerIds[i]]; |
| 203 | + } |
| 204 | + return questionAnswers; |
| 205 | + } |
| 206 | + |
| 207 | + function getQuestionDetails( |
| 208 | + uint _questionId |
| 209 | + ) public view returns (Question memory, Answer[] memory) { |
| 210 | + require(questions[_questionId].id != 0, "Question does not exist"); |
| 211 | + uint[] memory answerIds = questionToAnswers[_questionId]; |
| 212 | + Answer[] memory questionAnswers = new Answer[](answerIds.length); |
| 213 | + for (uint i = 0; i < answerIds.length; i++) { |
| 214 | + questionAnswers[i] = answers[answerIds[i]]; |
| 215 | + } |
| 216 | + return (questions[_questionId], questionAnswers); |
| 217 | + } |
| 218 | + |
| 219 | + function getQuestionsByCategory( |
| 220 | + string memory _category |
| 221 | + ) public view returns (Question[] memory) { |
| 222 | + uint count; |
| 223 | + for (uint i = 1; i <= questionCounter; i++) { |
| 224 | + if ( |
| 225 | + keccak256(abi.encodePacked(questions[i].category)) == |
| 226 | + keccak256(abi.encodePacked(_category)) |
| 227 | + ) { |
| 228 | + count++; |
| 229 | + } |
| 230 | + } |
| 231 | + Question[] memory filteredQuestions = new Question[](count); |
| 232 | + uint index = 0; |
| 233 | + for (uint i = 1; i <= questionCounter; i++) { |
| 234 | + if ( |
| 235 | + keccak256(abi.encodePacked(questions[i].category)) == |
| 236 | + keccak256(abi.encodePacked(_category)) |
| 237 | + ) { |
| 238 | + filteredQuestions[index++] = questions[i]; |
| 239 | + } |
| 240 | + } |
| 241 | + return filteredQuestions; |
| 242 | + } |
| 243 | + |
| 244 | + // Fetch user stats (Gasless Read Function) |
| 245 | + function getUserStats( |
| 246 | + address _user |
| 247 | + ) |
| 248 | + public |
| 249 | + view |
| 250 | + returns (uint totalEarned, uint totalWithdrawn, uint currentBalance) |
| 251 | + { |
| 252 | + UserStats memory stats = userStats[_user]; |
| 253 | + return (stats.totalEarned, stats.totalWithdrawn, stats.currentBalance); |
| 254 | + } |
| 255 | + |
| 256 | + function getRewardPoolBalance() public view returns (uint256) { |
| 257 | + return rewardToken.balanceOf(address(this)); |
| 258 | + } |
| 259 | + |
| 260 | + function getQuestionsByRange( |
| 261 | + uint start, |
| 262 | + uint end |
| 263 | + ) public view returns (Question[] memory) { |
| 264 | + require( |
| 265 | + start > 0 && end >= start && end <= questionCounter, |
| 266 | + "Invalid range" |
| 267 | + ); |
| 268 | + Question[] memory questionsRange = new Question[](end - start + 1); |
| 269 | + for (uint i = start; i <= end; i++) { |
| 270 | + questionsRange[i - start] = questions[i]; |
| 271 | + } |
| 272 | + return questionsRange; |
| 273 | + } |
| 274 | + |
| 275 | + function getTotalAllocatedRewards() public view returns (uint256) { |
| 276 | + uint256 totalRewards = 0; |
| 277 | + for (uint i = 1; i <= answerCounter; i++) { |
| 278 | + totalRewards += (answers[i].upvotes / 10) * 10; // Summing up rewards by milestones |
| 279 | + } |
| 280 | + return totalRewards; |
| 281 | + } |
| 282 | + |
| 283 | + function transferOwnership(address newOwner) external onlyOwner { |
| 284 | + require(newOwner != address(0), "New owner cannot be zero address"); |
| 285 | + emit OwnershipTransferred(owner, newOwner); |
| 286 | + owner = newOwner; |
| 287 | + } |
| 288 | + |
| 289 | + function getNetVotesForQuestion( |
| 290 | + uint _questionId |
| 291 | + ) public view returns (int) { |
| 292 | + require(questions[_questionId].id != 0, "Question does not exist"); |
| 293 | + return |
| 294 | + int(questions[_questionId].upvotes) - |
| 295 | + int(questions[_questionId].downvotes); |
| 296 | + } |
| 297 | + |
| 298 | + function getNetVotesForAnswer(uint _answerId) public view returns (int) { |
| 299 | + require(answers[_answerId].id != 0, "Answer does not exist"); |
| 300 | + return |
| 301 | + int(answers[_answerId].upvotes) - int(answers[_answerId].downvotes); |
| 302 | + } |
| 303 | + |
| 304 | + function checkAndAllocateReward(uint _answerId) internal { |
| 305 | + Answer storage ans = answers[_answerId]; |
| 306 | + if (ans.upvotes >= ans.nextMilestone) { |
| 307 | + uint rewardAmount = rewardPerMilestone; |
| 308 | + userStats[ans.author].currentBalance += rewardAmount; |
| 309 | + userStats[ans.author].totalEarned += rewardAmount; |
| 310 | + ans.nextMilestone += 10; // Move to the next milestone |
| 311 | + |
| 312 | + emit RewardAllocated(ans.author, rewardAmount, ans.nextMilestone); |
| 313 | + } |
| 314 | + } |
| 315 | + |
| 316 | + function rewardTopQuestion(uint _questionId) public onlyOwner { |
| 317 | + require(questions[_questionId].id != 0, "Question does not exist"); |
| 318 | + require( |
| 319 | + questions[_questionId].upvotes >= 50, |
| 320 | + "Minimum 50 upvotes required" |
| 321 | + ); |
| 322 | + |
| 323 | + uint rewardAmount = 50 * 1e18; // Example: 50 tokens |
| 324 | + userStats[questions[_questionId].author].currentBalance += rewardAmount; |
| 325 | + userStats[questions[_questionId].author].totalEarned += rewardAmount; |
| 326 | + |
| 327 | + emit RewardAllocated(questions[_questionId].author, rewardAmount, 0); |
| 328 | + } |
| 329 | + |
| 330 | + function setRewardPerMilestone(uint _newReward) external onlyOwner { |
| 331 | + rewardPerMilestone = _newReward; |
| 332 | + } |
| 333 | + |
| 334 | + function hasUserVotedOnQuestion( |
| 335 | + address _user, |
| 336 | + uint _questionId |
| 337 | + ) public view returns (bool) { |
| 338 | + return hasVotedQuestion[_user][_questionId]; |
| 339 | + } |
| 340 | + |
| 341 | + function hasUserVotedOnAnswer( |
| 342 | + address _user, |
| 343 | + uint _answerId |
| 344 | + ) public view returns (bool) { |
| 345 | + return hasVotedAnswer[_user][_answerId]; |
| 346 | + } |
| 347 | +} |
0 commit comments