From ea6b1b5a0ae1179edcd701dfa9c1e41b320966a8 Mon Sep 17 00:00:00 2001 From: Shiv Rai Date: Sat, 28 Sep 2024 21:20:09 +0530 Subject: [PATCH 1/4] chore: forge init --- foundry_contracts/.github/workflows/test.yml | 45 +++++++++++++ foundry_contracts/.gitignore | 14 +++++ foundry_contracts/README.md | 66 ++++++++++++++++++++ foundry_contracts/foundry.toml | 6 ++ foundry_contracts/script/Counter.s.sol | 19 ++++++ foundry_contracts/src/Counter.sol | 14 +++++ foundry_contracts/test/Counter.t.sol | 24 +++++++ 7 files changed, 188 insertions(+) create mode 100644 foundry_contracts/.github/workflows/test.yml create mode 100644 foundry_contracts/.gitignore create mode 100644 foundry_contracts/README.md create mode 100644 foundry_contracts/foundry.toml create mode 100644 foundry_contracts/script/Counter.s.sol create mode 100644 foundry_contracts/src/Counter.sol create mode 100644 foundry_contracts/test/Counter.t.sol 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..85198aa --- /dev/null +++ b/foundry_contracts/.gitignore @@ -0,0 +1,14 @@ +# Compiler files +cache/ +out/ + +# 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..9265b45 --- /dev/null +++ b/foundry_contracts/README.md @@ -0,0 +1,66 @@ +## Foundry + +**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** + +Foundry consists of: + +- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). +- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. +- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. +- **Chisel**: Fast, utilitarian, and verbose solidity REPL. + +## Documentation + +https://book.getfoundry.sh/ + +## Usage + +### Build + +```shell +$ forge build +``` + +### Test + +```shell +$ forge test +``` + +### Format + +```shell +$ forge fmt +``` + +### Gas Snapshots + +```shell +$ forge snapshot +``` + +### Anvil + +```shell +$ anvil +``` + +### Deploy + +```shell +$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key +``` + +### Cast + +```shell +$ cast +``` + +### Help + +```shell +$ forge --help +$ anvil --help +$ cast --help +``` diff --git a/foundry_contracts/foundry.toml b/foundry_contracts/foundry.toml new file mode 100644 index 0000000..25b918f --- /dev/null +++ b/foundry_contracts/foundry.toml @@ -0,0 +1,6 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/foundry_contracts/script/Counter.s.sol b/foundry_contracts/script/Counter.s.sol new file mode 100644 index 0000000..cdc1fe9 --- /dev/null +++ b/foundry_contracts/script/Counter.s.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script, console} from "forge-std/Script.sol"; +import {Counter} from "../src/Counter.sol"; + +contract CounterScript is Script { + Counter public counter; + + function setUp() public {} + + function run() public { + vm.startBroadcast(); + + counter = new Counter(); + + vm.stopBroadcast(); + } +} diff --git a/foundry_contracts/src/Counter.sol b/foundry_contracts/src/Counter.sol new file mode 100644 index 0000000..aded799 --- /dev/null +++ b/foundry_contracts/src/Counter.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +contract Counter { + uint256 public number; + + function setNumber(uint256 newNumber) public { + number = newNumber; + } + + function increment() public { + number++; + } +} diff --git a/foundry_contracts/test/Counter.t.sol b/foundry_contracts/test/Counter.t.sol new file mode 100644 index 0000000..54b724f --- /dev/null +++ b/foundry_contracts/test/Counter.t.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test, console} from "forge-std/Test.sol"; +import {Counter} from "../src/Counter.sol"; + +contract CounterTest is Test { + Counter public counter; + + function setUp() public { + counter = new Counter(); + counter.setNumber(0); + } + + function test_Increment() public { + counter.increment(); + assertEq(counter.number(), 1); + } + + function testFuzz_SetNumber(uint256 x) public { + counter.setNumber(x); + assertEq(counter.number(), x); + } +} From bb8a9ebb95e6066c0d3ad0d56c4ebc300aa62017 Mon Sep 17 00:00:00 2001 From: Shiv Rai Date: Sat, 28 Sep 2024 21:20:20 +0530 Subject: [PATCH 2/4] forge install: forge-std v1.9.3 --- .gitmodules | 3 +++ foundry_contracts/lib/forge-std | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 foundry_contracts/lib/forge-std 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/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 From aa9082b91610def6c084eadf62a72e5604358de8 Mon Sep 17 00:00:00 2001 From: Shiv Rai Date: Sun, 29 Sep 2024 00:26:45 +0530 Subject: [PATCH 3/4] Created Foundry Contract --- foundry_contracts/.gitignore | 3 + foundry_contracts/README.md | 64 +++-------- foundry_contracts/foundry.toml | 1 + foundry_contracts/script/Counter.s.sol | 19 ---- foundry_contracts/script/Deploy.s.sol | 17 +++ foundry_contracts/src/Counter.sol | 14 --- foundry_contracts/src/QuizApp.sol | 147 +++++++++++++++++++++++++ foundry_contracts/src/ScoreToken.sol | 19 ++++ foundry_contracts/test/Counter.t.sol | 24 ---- foundry_contracts/test/Unit.t.sol | 58 ++++++++++ 10 files changed, 263 insertions(+), 103 deletions(-) delete mode 100644 foundry_contracts/script/Counter.s.sol create mode 100644 foundry_contracts/script/Deploy.s.sol delete mode 100644 foundry_contracts/src/Counter.sol create mode 100644 foundry_contracts/src/QuizApp.sol create mode 100644 foundry_contracts/src/ScoreToken.sol delete mode 100644 foundry_contracts/test/Counter.t.sol create mode 100644 foundry_contracts/test/Unit.t.sol diff --git a/foundry_contracts/.gitignore b/foundry_contracts/.gitignore index 85198aa..3dd93b9 100644 --- a/foundry_contracts/.gitignore +++ b/foundry_contracts/.gitignore @@ -2,6 +2,9 @@ cache/ out/ +#lib +lib/ + # Ignores development broadcast logs !/broadcast /broadcast/*/31337/ diff --git a/foundry_contracts/README.md b/foundry_contracts/README.md index 9265b45..7d63d0a 100644 --- a/foundry_contracts/README.md +++ b/foundry_contracts/README.md @@ -1,13 +1,23 @@ -## Foundry +## QuizApp Contract -**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** +The `QuizApp` contract allows users to create quizzes, participate in them, and claim rewards based on their answers. -Foundry consists of: +### 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`. -- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). -- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. -- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. -- **Chisel**: Fast, utilitarian, and verbose solidity REPL. ## Documentation @@ -25,42 +35,4 @@ $ forge build ```shell $ forge test -``` - -### Format - -```shell -$ forge fmt -``` - -### Gas Snapshots - -```shell -$ forge snapshot -``` - -### Anvil - -```shell -$ anvil -``` - -### Deploy - -```shell -$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key -``` - -### Cast - -```shell -$ cast -``` - -### Help - -```shell -$ forge --help -$ anvil --help -$ cast --help -``` +``` \ No newline at end of file diff --git a/foundry_contracts/foundry.toml b/foundry_contracts/foundry.toml index 25b918f..fddca7f 100644 --- a/foundry_contracts/foundry.toml +++ b/foundry_contracts/foundry.toml @@ -2,5 +2,6 @@ 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/script/Counter.s.sol b/foundry_contracts/script/Counter.s.sol deleted file mode 100644 index cdc1fe9..0000000 --- a/foundry_contracts/script/Counter.s.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Script, console} from "forge-std/Script.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterScript is Script { - Counter public counter; - - function setUp() public {} - - function run() public { - vm.startBroadcast(); - - counter = new Counter(); - - vm.stopBroadcast(); - } -} 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/Counter.sol b/foundry_contracts/src/Counter.sol deleted file mode 100644 index aded799..0000000 --- a/foundry_contracts/src/Counter.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -contract Counter { - uint256 public number; - - function setNumber(uint256 newNumber) public { - number = newNumber; - } - - function increment() public { - number++; - } -} 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/Counter.t.sol b/foundry_contracts/test/Counter.t.sol deleted file mode 100644 index 54b724f..0000000 --- a/foundry_contracts/test/Counter.t.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Test, console} from "forge-std/Test.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterTest is Test { - Counter public counter; - - function setUp() public { - counter = new Counter(); - counter.setNumber(0); - } - - function test_Increment() public { - counter.increment(); - assertEq(counter.number(), 1); - } - - function testFuzz_SetNumber(uint256 x) public { - counter.setNumber(x); - assertEq(counter.number(), x); - } -} 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); + } + +} From 15b6fc3b89cd73060c8a3c3b47b3b893aed42a0c Mon Sep 17 00:00:00 2001 From: Shiv Rai Date: Sun, 29 Sep 2024 00:38:42 +0530 Subject: [PATCH 4/4] Updated readme --- foundry_contracts/README.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/foundry_contracts/README.md b/foundry_contracts/README.md index 7d63d0a..6ef1652 100644 --- a/foundry_contracts/README.md +++ b/foundry_contracts/README.md @@ -19,18 +19,36 @@ The `QuizApp` contract allows users to create quizzes, participate in them, and 5. Users can check their scores and claim rewards using `checkAndClaim`. -## Documentation +## 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