diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..77ac413 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "foundry_contracts/lib/forge-std"] + path = foundry_contracts/lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/foundry_contracts/.github/workflows/test.yml b/foundry_contracts/.github/workflows/test.yml new file mode 100644 index 0000000..762a296 --- /dev/null +++ b/foundry_contracts/.github/workflows/test.yml @@ -0,0 +1,45 @@ +name: CI + +on: + push: + pull_request: + workflow_dispatch: + +env: + FOUNDRY_PROFILE: ci + +jobs: + check: + strategy: + fail-fast: true + + name: Foundry project + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Show Forge version + run: | + forge --version + + - name: Run Forge fmt + run: | + forge fmt --check + id: fmt + + - name: Run Forge build + run: | + forge build --sizes + id: build + + - name: Run Forge tests + run: | + forge test -vvv + id: test diff --git a/foundry_contracts/.gitignore b/foundry_contracts/.gitignore new file mode 100644 index 0000000..3dd93b9 --- /dev/null +++ b/foundry_contracts/.gitignore @@ -0,0 +1,17 @@ +# Compiler files +cache/ +out/ + +#lib +lib/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ + +# Docs +docs/ + +# Dotenv file +.env diff --git a/foundry_contracts/README.md b/foundry_contracts/README.md new file mode 100644 index 0000000..6ef1652 --- /dev/null +++ b/foundry_contracts/README.md @@ -0,0 +1,56 @@ +## QuizApp Contract + +The `QuizApp` contract allows users to create quizzes, participate in them, and claim rewards based on their answers. + +### Key Features + +- **Create Quizzes**: Creators can create quizzes by providing hash id of quiz questions and options store on ipfs/arweave through frontend interaction. +- **Participate in Quizzes**: Users can participate in quizzes and submit their answers in form of an array. +- **Manage Quizzes**: Quiz creators can end quizzes and set correct answers. +- **Claim Rewards**: Users can claim rewards based on the correctness of their answers. + + +### Usage Example + +1. Deploy the `QuizApp` contract. +2. Create a quiz using `createQuiz`. +3. Users can participate using `participateInQuiz`. +4. Quiz owners can end the quiz using `endQuiz` and set correct answers with `tellCorrectAnswers`. +5. Users can check their scores and claim rewards using `checkAndClaim`. + + +## Documentation of Foundry + +https://book.getfoundry.sh/ + +## Usage + +## Install Openzeppelin Contracts +```shell +$ forge install OpenZeppelin/openzeppelin-contracts --no-commit +``` + + +### Build + +```shell +$ forge build +``` + +### Compile + +```shell +$ forge compile +``` + +### Deploy + +```shell +$ forge script script/Deploy.s.sol +``` + +### Test + +```shell +$ forge test +``` \ No newline at end of file diff --git a/foundry_contracts/foundry.toml b/foundry_contracts/foundry.toml new file mode 100644 index 0000000..fddca7f --- /dev/null +++ b/foundry_contracts/foundry.toml @@ -0,0 +1,7 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] +remappings=["@openzeppelin/contracts=lib/openzeppelin-contracts/contracts"] + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/foundry_contracts/lib/forge-std b/foundry_contracts/lib/forge-std new file mode 160000 index 0000000..8f24d6b --- /dev/null +++ b/foundry_contracts/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 8f24d6b04c92975e0795b5868aa0d783251cdeaa diff --git a/foundry_contracts/script/Deploy.s.sol b/foundry_contracts/script/Deploy.s.sol new file mode 100644 index 0000000..5c7dc4b --- /dev/null +++ b/foundry_contracts/script/Deploy.s.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.27; + +import {Script} from "forge-std/Script.sol"; +import {QuizApp} from "../src/QuizApp.sol"; + +contract Deploy is Script { + QuizApp quizapp; + + function run() public returns(QuizApp) { + vm.startBroadcast(); + quizapp = new QuizApp(); + vm.stopBroadcast(); + return quizapp; + + } +} diff --git a/foundry_contracts/src/QuizApp.sol b/foundry_contracts/src/QuizApp.sol new file mode 100644 index 0000000..d2bba95 --- /dev/null +++ b/foundry_contracts/src/QuizApp.sol @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import {ScoreToken} from "./ScoreToken.sol"; + +contract QuizApp { + + ScoreToken scoreToken; + uint256 public quizCount; + + struct Quiz { + uint256 id; // id of quiz + address by; // address of user who started quiz + string storageId; // storage id of Quiz on ipfs/arweave + uint256 totalQues; // total ques in a quiz + bool isActive; // whether quiz is active or not + uint256[] correctOptions; + } + + // id to Quiz + mapping(uint256 => Quiz) public quizzes; + // address of user to quiz participation + mapping(address => mapping(uint256 => bool)) public participation; + // option chosen by a user in a quiz + mapping(address => mapping(uint256 => uint256[])) public userChosenOptions; + // address of user to quiz rewards claimed + mapping(address => mapping(uint256 => bool)) public claimed; + + // Events emitted + event QuizCreated(uint quizId, string storageId, uint256 totalQues); + event QuizParticipated(address participant, uint256 quizId, uint256[] selectedOptions); + event QuizHasEnded(uint quizId); + + // Custom errors + error QuizNotActive(); + error QuizActive(); + error AlreadyParticipated(); + error CorrectOptionsNotSet(); + error NotParticipated(); + error AlreadyClaimed(); + error AlreadySetCorrectOptions(); + error QuizEnded(); + error QuizNotEnded(); + error NotQuizOwner(); + error NotAllAnswersProvided(); + + // Modifiers for checking quiz participation and status + modifier quizActive(Quiz memory quiz) { + require(quiz.isActive, QuizNotActive()); + _; + } + + modifier quizNotActive(Quiz memory quiz) { + require(!quiz.isActive, QuizActive()); + _; + } + + modifier hasNotParticipated(uint256 quizId) { + require(!participation[msg.sender][quizId], AlreadyParticipated()); + _; + } + + modifier correctOptionsSet(Quiz memory quiz) { + require(quiz.correctOptions.length != 0, CorrectOptionsNotSet()); + _; + } + + modifier hasParticipated(uint256 quizId) { + require(participation[msg.sender][quizId], NotParticipated()); + _; + } + + modifier hasNotClaimed(uint256 quizId) { + require(!claimed[msg.sender][quizId], AlreadyClaimed()); + _; + } + + modifier quizEnded(uint quizId) { + require(!quizzes[quizId].isActive, QuizNotEnded()); + _; + } + modifier quizNotEnded(uint quizId) { + require(quizzes[quizId].isActive, QuizEnded()); + _; + } + + modifier onlyQuizOwner(uint quizId) { + require(quizzes[quizId].by == msg.sender, NotQuizOwner()); + _; + } + + constructor(){ + scoreToken = new ScoreToken(); + } + + // create a quiz using hash id as the storage id provided by ipfs/arweave and total ques + function createQuiz( + string memory storageId, + uint256 totalQues + ) external { + quizzes[quizCount] = Quiz(quizCount, msg.sender, storageId, totalQues, true, new uint256[](0)); + quizCount++; + emit QuizCreated(quizCount, storageId, totalQues); + } + + // users can participate and answer quiz ques + function participateInQuiz(uint quizId, uint256[] memory selectedOptions) external quizActive(quizzes[quizId]) hasNotParticipated(quizId) { + require(selectedOptions.length == quizzes[quizId].totalQues, NotAllAnswersProvided()); + participation[msg.sender][quizId] = true; + userChosenOptions[msg.sender][quizId] = selectedOptions; + emit QuizParticipated(msg.sender, quizId, selectedOptions); + } + + // quiz creator can end participation in the quiz + function endQuiz(uint quizId) external quizNotEnded(quizId) onlyQuizOwner(quizId) { + quizzes[quizId].isActive = false; + emit QuizHasEnded(quizId); + } + + // quiz creator enters the correct answer sequence + function tellCorrectAnswers(uint256 quizId, uint256[] memory correctOptions) public quizEnded(quizId) onlyQuizOwner(quizId) { + Quiz memory quiz = quizzes[quizId]; + require(quiz.correctOptions.length == 0, AlreadySetCorrectOptions()); + quizzes[quizId].correctOptions = correctOptions; + } + + // users can match their answers and claim their reward based on correct answers + function checkAndClaim(uint256 quizId) external quizNotActive(quizzes[quizId])correctOptionsSet(quizzes[quizId]) hasParticipated(quizId) hasNotClaimed(quizId) { + Quiz memory quiz = quizzes[quizId]; + uint256[] memory chosenOptions = userChosenOptions[msg.sender][quizId]; + uint256[] memory correctOptions = quiz.correctOptions; + uint256 score = 0; + for (uint256 i = 0; i < quiz.totalQues; i++) { + if (chosenOptions[i] == correctOptions[i]) { + score++; + } + } + scoreToken.mint(msg.sender, score); + claimed[msg.sender][quizId] = true; + } + + // users can viwe the score tokens they have till now + function viewTokens() public view returns (uint256){ + return scoreToken.bal(msg.sender); + } + +} diff --git a/foundry_contracts/src/ScoreToken.sol b/foundry_contracts/src/ScoreToken.sol new file mode 100644 index 0000000..e23499b --- /dev/null +++ b/foundry_contracts/src/ScoreToken.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +// Compatible with OpenZeppelin Contracts ^5.0.0 +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract ScoreToken is ERC20, Ownable { + constructor() + ERC20("ScoreToken", "ST") + Ownable(msg.sender) + {} + function mint(address to, uint256 amount) onlyOwner public { + _mint(to, amount); + } + function bal(address to) onlyOwner public view returns (uint256) { + return balanceOf(to); + } +} \ No newline at end of file diff --git a/foundry_contracts/test/Unit.t.sol b/foundry_contracts/test/Unit.t.sol new file mode 100644 index 0000000..7d0dff7 --- /dev/null +++ b/foundry_contracts/test/Unit.t.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.27; + +import {Test} from "forge-std/Test.sol"; +import {QuizApp} from "../src/QuizApp.sol"; +import {Deploy} from "../script/Deploy.s.sol"; + +contract Unit is Test { + QuizApp quizApp; + Deploy deploy; + + function setUp() public { + deploy = new Deploy(); + quizApp = deploy.run(); + } + + function testCreateQuiz() public { + quizApp.createQuiz("quiz1", 5); + (uint256 id, address by, string memory storageId, uint256 totalQues, bool isActive ) = quizApp.quizzes(0); + assertEq(id, 0); + assertEq(by, address(this)); + assertEq(storageId, "quiz1"); + assertEq(totalQues, 5); + assertTrue(isActive); + } + + function testParticipateInQuiz() public { + quizApp.createQuiz("quiz1", 3); + uint256[] memory selectedOptions = new uint256[](3); + selectedOptions[0] = 1; + selectedOptions[1] = 2; + selectedOptions[2] = 3; + quizApp.participateInQuiz(0, selectedOptions); + assertTrue(quizApp.participation(address(this), 0)); + } + + function testClaimReward() public { + quizApp.createQuiz("quiz1", 3); + + uint256[] memory selectedOptions = new uint256[](3); + selectedOptions[0] = 1; + selectedOptions[1] = 2; + selectedOptions[2] = 3; + quizApp.participateInQuiz(0, selectedOptions); + + uint256[] memory correctOptions = new uint256[](3); + correctOptions[0] = 1; + correctOptions[1] = 2; + correctOptions[2] = 3; + quizApp.endQuiz(0); + + quizApp.tellCorrectAnswers(0, correctOptions); + + quizApp.checkAndClaim(0); + assertEq(quizApp.viewTokens(),3); + } + +}