From 492611c1805a626994b6bb7b26364aaf8baf8584 Mon Sep 17 00:00:00 2001 From: Michael Hammond <33749130+mickeymond@users.noreply.github.com> Date: Mon, 30 Oct 2023 10:46:38 +0000 Subject: [PATCH] Develop Market Place Contract (#1) * wip: getting started with MENT based on openzeppelin contracts * wip: adding github workflow to test contracts * wip: first run for the emt marketplace contract --- .github/workflows/blockchain-ci.yaml | 27 ++++++++ blockchain/.env.example | 3 + blockchain/.gitignore | 14 +++++ blockchain/README.md | 13 ++++ blockchain/contracts/EMTMarketplace.sol | 75 +++++++++++++++++++++++ blockchain/contracts/MentorToken.sol | 27 ++++++++ blockchain/delete-me.md | 1 - blockchain/hardhat.config.ts | 24 ++++++++ blockchain/package.json | 18 ++++++ blockchain/scripts/deploy-mentor-token.ts | 24 ++++++++ blockchain/test/EMTMarketplace.ts | 34 ++++++++++ blockchain/test/MentorToken.ts | 31 ++++++++++ blockchain/tsconfig.json | 11 ++++ 13 files changed, 301 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/blockchain-ci.yaml create mode 100644 blockchain/.env.example create mode 100644 blockchain/.gitignore create mode 100644 blockchain/README.md create mode 100644 blockchain/contracts/EMTMarketplace.sol create mode 100644 blockchain/contracts/MentorToken.sol delete mode 100644 blockchain/delete-me.md create mode 100644 blockchain/hardhat.config.ts create mode 100644 blockchain/package.json create mode 100644 blockchain/scripts/deploy-mentor-token.ts create mode 100644 blockchain/test/EMTMarketplace.ts create mode 100644 blockchain/test/MentorToken.ts create mode 100644 blockchain/tsconfig.json diff --git a/.github/workflows/blockchain-ci.yaml b/.github/workflows/blockchain-ci.yaml new file mode 100644 index 0000000..f17a056 --- /dev/null +++ b/.github/workflows/blockchain-ci.yaml @@ -0,0 +1,27 @@ +# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs + +name: Blockchain CI + +env: + PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [14.x, 16.x, 18.x] + # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + + steps: + - uses: actions/checkout@v3 + - name: Setup and run tests using Node.js ${{ matrix.node-version }} + working-directory: ./blockchain + run: | + npm install + npm run test \ No newline at end of file diff --git a/blockchain/.env.example b/blockchain/.env.example new file mode 100644 index 0000000..c19ae67 --- /dev/null +++ b/blockchain/.env.example @@ -0,0 +1,3 @@ +PRIVATE_KEY= +MENTOR_TOKEN_DEFAULT_ADMIN= +MENTOR_TOKEN_MINTER= \ No newline at end of file diff --git a/blockchain/.gitignore b/blockchain/.gitignore new file mode 100644 index 0000000..9893637 --- /dev/null +++ b/blockchain/.gitignore @@ -0,0 +1,14 @@ +node_modules +.env +coverage +coverage.json +typechain +typechain-types + +# Hardhat files +cache +artifacts + +# Miscellaneous +package-lock.json +yarn.lock diff --git a/blockchain/README.md b/blockchain/README.md new file mode 100644 index 0000000..7be82e5 --- /dev/null +++ b/blockchain/README.md @@ -0,0 +1,13 @@ +# Sample Hardhat Project + +This project demonstrates a basic Hardhat use case. It comes with a sample contract, a test for that contract, and a script that deploys that contract. + +Try running some of the following tasks: + +```shell +npx hardhat help +npx hardhat test +REPORT_GAS=true npx hardhat test +npx hardhat node +npx hardhat run scripts/deploy.ts +``` diff --git a/blockchain/contracts/EMTMarketplace.sol b/blockchain/contracts/EMTMarketplace.sol new file mode 100644 index 0000000..fc4c837 --- /dev/null +++ b/blockchain/contracts/EMTMarketplace.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: APACHE +pragma solidity ^0.8.20; + +import "./MentorToken.sol"; + +contract EMTMarketplace { + // Event Definitions + + event ContentUpVoted(string indexed, uint256); + event ContentDownVoted(string indexed, uint256); + + // Data Definitions + + address _mentToken; + mapping(string => uint256) _contentVotes; + mapping(address => mapping(string => bool)) _memberUpVotes; + mapping(address => mapping(string => bool)) _memberDownVotes; + + function setMentToken(address _MENT_TOKEN_ADDRESS) public { + _mentToken = _MENT_TOKEN_ADDRESS; + } + + // Function Definitions + function contentVotes(string memory _id) public view returns (uint256) { + return _contentVotes[_id]; + } + + function memberUpVotes(string memory _id) public view returns (bool) { + return _memberUpVotes[msg.sender][_id]; + } + + function memberDownVotes(string memory _id) public view returns (bool) { + return _memberDownVotes[msg.sender][_id]; + } + + function upVoteContent(string memory _id, address _mentor) public { + // Check if MENT Token is not address zero + require(_mentToken != address(0), "Ment Token is Address Zero!"); + // Check if msg.sender has not upvoted + require( + !_memberUpVotes[msg.sender][_id], + "Member has already up voted!" + ); + // Increment Content Vote + _contentVotes[_id]++; + // Update Member Votes Status + _memberUpVotes[msg.sender][_id] = true; + _memberDownVotes[msg.sender][_id] = false; + // Mint MENT Token for Content Creator (should the mentor address to passed in as an argument or stored on the blockchain?) + MentorToken(_mentToken).mint(_mentor, 1); + // Emit Event + emit ContentUpVoted(_id, _contentVotes[_id]); + } + + function downVoteContent(string memory _id, address _mentor) public { + // Check if MENT Token is not address zero + require(_mentToken != address(0), "Ment Token is Address Zero!"); + // Check if msg.sender has already up voted the content + require(_memberUpVotes[msg.sender][_id], "Member has not up voted!"); + // Check if msg.sender has not downvoted the content + require( + !_memberDownVotes[msg.sender][_id], + "Member has already down voted!" + ); + // Decrement Content Vote + _contentVotes[_id]--; + // Update Member Votes Status + _memberDownVotes[msg.sender][_id] = true; + _memberUpVotes[msg.sender][_id] = false; + // Burn MENT Token for Content Creator (should the mentor address to passed in as an argument or stored on the blockchain?) + MentorToken(_mentToken).burnFrom(_mentor, 1); + // Emit Event + emit ContentDownVoted(_id, _contentVotes[_id]); + } +} diff --git a/blockchain/contracts/MentorToken.sol b/blockchain/contracts/MentorToken.sol new file mode 100644 index 0000000..9e43a8e --- /dev/null +++ b/blockchain/contracts/MentorToken.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: APACHE +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; +import "@openzeppelin/contracts/access/AccessControl.sol"; + +/// @custom:security-contact odafe@mowblox.com +contract MentorToken is ERC20, ERC20Burnable, AccessControl { + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + + constructor( + address defaultAdmin, + address minter + ) ERC20("Mentor Token", "MENT") { + _grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin); + _grantRole(MINTER_ROLE, minter); + } + + function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) { + _mint(to, amount); + } + + function decimals() public view virtual override returns (uint8) { + return 0; + } +} diff --git a/blockchain/delete-me.md b/blockchain/delete-me.md deleted file mode 100644 index 8b13789..0000000 --- a/blockchain/delete-me.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/blockchain/hardhat.config.ts b/blockchain/hardhat.config.ts new file mode 100644 index 0000000..3a4824e --- /dev/null +++ b/blockchain/hardhat.config.ts @@ -0,0 +1,24 @@ +import { HardhatUserConfig } from "hardhat/config"; +import "@nomicfoundation/hardhat-toolbox"; +import dotenv from "dotenv"; + +dotenv.config(); + +const config: HardhatUserConfig = { + solidity: "0.8.20", + networks: { + topos: { + url: 'https://rpc.topos-subnet.testnet-1.topos.technology', + accounts: [process.env.PRIVATE_KEY as string] + }, + incal: { + url: 'https://rpc.incal.testnet-1.topos.technology', + accounts: [process.env.PRIVATE_KEY as string] + } + }, + gasReporter: { + enabled: true + } +}; + +export default config; diff --git a/blockchain/package.json b/blockchain/package.json new file mode 100644 index 0000000..e3f373c --- /dev/null +++ b/blockchain/package.json @@ -0,0 +1,18 @@ +{ + "name": "blockchain", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "npx hardhat test" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@nomicfoundation/hardhat-toolbox": "^3.0.0", + "@openzeppelin/contracts": "^5.0.0", + "dotenv": "^16.3.1", + "hardhat": "^2.18.3" + } +} \ No newline at end of file diff --git a/blockchain/scripts/deploy-mentor-token.ts b/blockchain/scripts/deploy-mentor-token.ts new file mode 100644 index 0000000..b62b532 --- /dev/null +++ b/blockchain/scripts/deploy-mentor-token.ts @@ -0,0 +1,24 @@ +import { ethers } from "hardhat"; +import dotenv from "dotenv"; + +dotenv.config(); + +async function main() { + const [_owner, defaultAdmin, minter] = await ethers.getSigners(); + + const mentorToken = await ethers.deployContract("MentorToken", [ + process.env.MENTOR_TOKEN_DEFAULT_ADMIN || defaultAdmin.address, + process.env.MENTOR_TOKEN_MINTER || minter.address, + ]); + + await mentorToken.waitForDeployment(); + + console.log("Mentor Token deployed at: ", mentorToken.target); +} + +// We recommend this pattern to be able to use async/await everywhere +// and properly handle errors. +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/blockchain/test/EMTMarketplace.ts b/blockchain/test/EMTMarketplace.ts new file mode 100644 index 0000000..16443c8 --- /dev/null +++ b/blockchain/test/EMTMarketplace.ts @@ -0,0 +1,34 @@ +import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers"; +import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs"; +import { expect } from "chai"; +import { ethers } from "hardhat"; + +describe("EMTMarketplace", function () { + // We define a fixture to reuse the same setup in every test. + // We use loadFixture to run this setup once, snapshot that state, + // and reset Hardhat Network to that snapshot in every test. + async function deployEMTMarketplaceFixture() { + // Contracts are deployed using the first signer/account by default + const [owner, mentor, member] = await ethers.getSigners(); + + const EMTMarketplace = await ethers.getContractFactory("EMTMarketplace"); + const emtMarketplace = await EMTMarketplace.deploy(); + + const MentorToken = await ethers.getContractFactory("MentorToken"); + const mentorToken = await MentorToken.deploy(owner.address, emtMarketplace.target); + + // Mentor Approve MarketPlace Token + await mentorToken.connect(mentor).approve(emtMarketplace.target, 1); + + return { emtMarketplace, mentorToken, owner, mentor, member }; + } + + // Test Goes Below + describe("Deployment", function () { + it("deploys mentor token with all expected defaults", async function () { + const { emtMarketplace, mentorToken, mentor } = await loadFixture(deployEMTMarketplaceFixture); + + console.log("Market Place Allowance: ", await mentorToken.allowance(mentor.address, emtMarketplace.target)); + }) + }); +}); diff --git a/blockchain/test/MentorToken.ts b/blockchain/test/MentorToken.ts new file mode 100644 index 0000000..3ccc932 --- /dev/null +++ b/blockchain/test/MentorToken.ts @@ -0,0 +1,31 @@ +import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers"; +import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs"; +import { expect } from "chai"; +import { ethers } from "hardhat"; + +describe("MentorToken", function () { + // We define a fixture to reuse the same setup in every test. + // We use loadFixture to run this setup once, snapshot that state, + // and reset Hardhat Network to that snapshot in every test. + async function deployMentorTokenFixture() { + // Contracts are deployed using the first signer/account by default + const [owner, minter] = await ethers.getSigners(); + + const MentorToken = await ethers.getContractFactory("MentorToken"); + const mentorToken = await MentorToken.deploy(owner.address, minter.address); + + return { mentorToken, owner, minter }; + } + + // Test Goes Below + describe("Deployment", function () { + it("deploys mentor token with all expected defaults", async function () { + const { mentorToken } = await loadFixture(deployMentorTokenFixture); + + console.log("Total Supply: ", await mentorToken.totalSupply()); + console.log("Name: ", await mentorToken.name()); + console.log("Symbol: ", await mentorToken.symbol()); + console.log("Decimals: ", await mentorToken.decimals()); + }) + }); +}); diff --git a/blockchain/tsconfig.json b/blockchain/tsconfig.json new file mode 100644 index 0000000..574e785 --- /dev/null +++ b/blockchain/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true + } +}