diff --git a/README.md b/README.md new file mode 100644 index 0000000..46dae12 --- /dev/null +++ b/README.md @@ -0,0 +1,210 @@ +# Practical Sample With Hardhat + Solidity + +A fully functional Smart Contract wrriten with ```solidity```, ```node.js``` and using ```Hardhat``` to show usage of all kinds of standard development features, including: +* running blockchain node in localhost - ```hardhat``` & ```ganache``` +* solidity prettier code beautifier setup +* standard of developing Smart Contract with ```solidity``` +* standard of developing ```node.js``` script to use ```hardhat``` library efficiently +* check gas fee & gas price in real-time, with usage of ```coverage``` +* auto verify contract on Etherscan.io +* creating automate test cases - unit test & staging test +* creating tasks etc. +* refactor codes to reduce gas fee +* write code in best practice, with usage of ```solhint``` +* prettier ```solidity``` & ```node.js``` code +* gitea version control + +This project mainly to keep as a reference for future Web 3.0 Developments. + +--- +### Success Deploy & Verified Of This Smart Contract To Sepolia Testnet + +0xc2022b56eBC140B5FebCf9FBaB14c17db4C315C4 +https://sepolia.etherscan.io/address/0xc2022b56eBC140B5FebCf9FBaB14c17db4C315C4#code + Via deploy.js +https://sepolia.etherscan.io/address/0x3a827C119e1D746bb3C7bcbbf95c55246C8CcBdd#code + Via yarn hardhat deploy --network sepolia + +### Public Reported Hacked Code References: + +This website is records of all kind previous hacked smart contract: + +https://rekt.news/leaderboard/ + + +## 1. Git Version Control +First time initialize: +``` +git config --global user.name "hoelee" +git config --global user.email "me@hoelee.com" +git init . +git add . +git checkout -b main +git commit -m "Initial Commit" +git remote set-url origin https://username:accessToken@git.hoelee.com/hoelee/ethers-simple-storage.git +git credential-cache exit // Fix Credential Error +``` + +Standard Update: +``` +git add . +git commit -m "Describe what changes" +git push -u origin main + // After set this, later easier usage via below line +git push +git pull +``` + +Development need exlude file can create root file with name .gitignore +``` +node_modules +package.json +img +artifacts +cache +coverage +.env +.* +README.md +coverage.json +``` + +## 2. Setup Visual Studio Code Development Environment + +Windows need to download install WSL +``` +wsl --set-default Ubuntu-22.04 +mkdir theProjectFolderName +cd theProjectFolderName +code . +// Install nvm +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash +nvm install 16.14.2 +nvm install node.js +nvm install 18 // Update node JS to v18 +``` +Visual Studio need to update code setting +``` +"[solidity]": { + "editor.defaultFormatter":"NomicFoundation.hardhat-solidity" + } +``` +Preparing of solidity development environment: +``` +corepack enable // Enable yarn +yarn install solc +yarn add solc@0.8.7fixed +yarn solcjs --bin --abi --include-path node_modules/ --base-path . -o . SimpleStorage.sol +yarn add ethers // Compiler Error, Downgraded to v5.7.2 +yarn add fs-extra +yarn add dotenv +yarn add prettier +yarn add prettier-plugin-solidity +``` +Preparing of Hardhat Development Environment +``` +yarn init +// Manual delete main: index.js in package.json +yarn add --dev hardhat // Production no need --dev +nvm install 18 +nvm use 18 +nvm alias default 18 +corepack enable // Enable yarn +yarn hardhat +yarn add --dev prettier prettier-plugin-solidity +yarn add --dev dotenv +yarn add --dev @nomiclabs/hardhat-etherscan // Auto verify Etherscan Samrt Contract +yarn add --dev @nomiclabs/hardhat-waffle +yarn add --dev solhint +yarn add --dev @nomiclabs/hardhat-ethers@npm:hardhat-deploy-ethers ethers +yarn add --dev hardhat-gas-reporter +yarn add --dev solidity-coverage +yarn add --dev solhint +``` +Other Terminal Useful Command 1: +``` +// For Debuging Hardhat +npx hardhat --versose + +// For Recompile +yarn hardhat clean // Or manual delete artifacts & cache folder +npm install + +// For Listing Hardhat Local Blockchain node +yarn hardhat accounts +yarn hardhat node // Run in Dedicated Terminal, Getting Blockchain server +yarn hardhat console --network localhost // Short Life To Test Solidity Code In Terminal +yarn hardhat compile +yarn hardhat run scripts/deploy.js --network localhost +yarn hardhat custom-task-name + // Need create file in /tasks/custom-task-name.js + // Add import in hardhat.config.js -> requir("/tasks/custom-task-name"); +yarn hardhat test +yarn hardhat test --grep customSearchKeyword + // Only will run the test with describe test that contain "customSearchKeyword" +``` +Other Terminal Useful Command 2: +``` +// For Getting Gas Used & Gas Price +yarn hardhat test + // Will create a file in ./gas-report.txt + // With .env of etherscan API key +// For Getting Coverage +yarn hardhat coverage + // Checking code usage & tested percentage +// For Checking Code Best Practice +yarn solhint contracts/*.sol +// For Get Fake Price Feed On Localhost & Ganache +yarn hardhat deploy --tags mocks --network localhost +``` +Debug ```Node.js``` need to open **Javascript Debug Terminal** first, via ```ctrl + shift + p``` + +#### Reduce Gas Used: +* Prioritize use ```private``` instead of ```public``` +* Use ```constant``` which declare once in constructor +* Use ```immutable ``` which declare once only + +This is because blockchain will have higher ```read``` and ```store``` gas fee on storage block, lesser in bytes code block. + + +## 3. Known Issue +* Dependencies combination is old, need update... +* ... + +### Find a bug? +If you found an issue or would like to submit an improvement to this demo project, please submit an issue using the issues tab above. + +## 4. Well-known Vulnerabilities +* Reentrancy Attack + * Locked with modifier while running withdraw function + * Update variable immediately before call external function +* Integer Overflow / Underflow + * Use compiler version >0.8.0 have check in place +* Front-Running + * Use average gas fee / off peak times + * Use commit-reveal schema + * Use submarine send + +## 5. Other Explored Features +* Generate Random Words + * Create subscription at https://vrf.chain.link/ (MetaMask #1) + * Add Fund To The Created Subsciption Contract (MetaMask #2) + * Add Consumer + * Get Subscription ID + * Open Remix To Prepare Deploy Consumer Contract https://docs.chain.link/vrf/v2-5/migration-from-v2 + * Change To Correct gwei limit hash address at https://docs.chain.link/vrf/v2/subscription/supported-networks + * Adjust Setting - random words count, confirmation blocks etc. + * Insert Subscription ID - v2 is uint64 BUT **v2.5 is uint256** + * Deploy And Get hash address (MetaMask #3) + * Insert in chainlink consumer (Metamask #4) + * Ready to use, record down the consumer contract address + + +## 6. Looking Web 3.0 Developer For Your Project? +**Mr Hoelee** is Welcome Web 3.0 Remote Job, Contact Me Immediately Via WhatsApp +60175885290 +. + +Or You can email me@hoelee.com now. Thanks. + +## 7. Like this project? +If you are feeling generous, buy me a coffee! - buymeacoffee.com/hoelee diff --git a/contracts/Raffle.sol b/contracts/Raffle.sol index 590fbbb..d3fcb01 100644 --- a/contracts/Raffle.sol +++ b/contracts/Raffle.sol @@ -250,4 +250,8 @@ contract Raffle is VRFConsumerBaseV2Plus, AutomationCompatibleInterface { function getInterval() public view returns (uint256) { return i_interval; } + + function getLastTimeStamp() public view returns (uint256) { + return s_lastTimeStamp; + } } diff --git a/hardhat.config.js b/hardhat.config.js index 00eafba..0326d0c 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -105,9 +105,7 @@ module.exports = { }, ], }, - /* mocha: { timeout: 500000, // 500 seconds max for running tests }, - */ }; diff --git a/test/unit/Raffle.test.js b/test/unit/Raffle.test.js index 0af2b55..e1acd37 100644 --- a/test/unit/Raffle.test.js +++ b/test/unit/Raffle.test.js @@ -13,11 +13,11 @@ const isDevelopment = developmentChains.includes(network.name); ? describe.skip : describe("Raffle Unit Test", function () { let addressMock, addressRaffle, addressDeployer, addressPlayer; - let raffle, vrfCoordinatorV2_5Mock, vrfCoordinatorV2_5Address, player; + let raffle, vrfCoordinatorV2_5Mock, namedAccounts, subscriptionId; let chainId, raffleEntranceFee, interval; beforeEach(async () => { - const namedAccounts = await getNamedAccounts(); + namedAccounts = await getNamedAccounts(); addressDeployer = namedAccounts.deployer; addressPlayer = namedAccounts.player; await deployments.fixture(["all"]); @@ -31,6 +31,31 @@ const isDevelopment = developmentChains.includes(network.name); ); raffle = await ethers.getContractAt("Raffle", addressRaffle); + // Add fund to Mock for fulfillRandomWords + const tx = await vrfCoordinatorV2_5Mock.createSubscription(); + const txReceipt = await tx.wait(1); + subscriptionId = txReceipt.events[0].args.subId; + //0x8d03209e7b30987dddca60349de1dc942195aadc9d6c6b5ab324388762f3b57e + await vrfCoordinatorV2_5Mock.addConsumer(subscriptionId, raffle.address); + + const FUND_AMOUNT = ethers.utils.parseEther("1"); // 1 Ether + await vrfCoordinatorV2_5Mock.fundSubscription(subscriptionId, FUND_AMOUNT); + // Retrieve subscription details + const subscription = await vrfCoordinatorV2_5Mock.getSubscription(subscriptionId); + // Verify if the consumer is added correctly + console.log(`Consumers: ${subscription.consumers}`); // Should now include raffle.address + + // Verify the balance + const balance = subscription.balance; + console.log(`Subscription balance: ${ethers.utils.formatEther(balance)} Ether`); + + // Assert the balance to ensure it matches the FUND_AMOUNT + assert.equal( + balance.toString(), + FUND_AMOUNT.toString(), + "Subscription balance should match the fund amount", + ); + // raffle = await deployments.get("Raffle"); // Wrong, not the contract instance // raffle = await ethers.getContract("Raffle"); // With hardaht-ethers dependency override chainId = network.config.chainId; @@ -53,12 +78,6 @@ const isDevelopment = developmentChains.includes(network.name); describe("enterRaffle", function () { it("should revert if not enough payment", async () => { - /* - const entranceFee = networkConfig[chainId]["raffleEntranceFee"]; - const entranceFeeString = ethers.utils.formatEther(entranceFee); - const entranceFeeWei = ethers.utils.parseEther(entranceFeeString); - const insufficientPayment = entranceFeeWei.sub(ethers.utils.parseEther("0.01")); // Set payment less than entrance fee - */ const entranceFee = await raffle.getEntranceFee(); const entranceFeeString = ethers.utils.formatEther(entranceFee); console.log(entranceFeeString); // 0.01 @@ -126,7 +145,8 @@ const isDevelopment = developmentChains.includes(network.name); await network.provider.send("evm_increaseTime", [interval.toNumber() - 5]); // use a higher number here if this test fails await network.provider.request({ method: "evm_mine", params: [] }); // Alternative to write const { upkeepNeeded } = await raffle.callStatic.checkUpkeep("0x"); // upkeepNeeded = (timePassed && isOpen && hasBalance && hasPlayers) - assert(!upkeepNeeded); + + assert.isFalse(upkeepNeeded); }); it("returns true if enough time has passed, has players, eth, and is open", async () => { await raffle.enterRaffle({ value: raffleEntranceFee }); @@ -179,5 +199,99 @@ const isDevelopment = developmentChains.includes(network.name); vrfCoordinatorV2_5Mock.fulfillRandomWords(0, raffle.address), ).to.be.revertedWith("InvalidRequest"); }); + + // Complete Test + it("picks a winner, reset the raffle, and send money to the winner", async () => { + const additionalEntrances = 3; + const startingAccountIndex = 2; // deployer = 0, player = 1 + let accounts = await ethers.getSigners(); // Many accounts + // getNamedAccounts() is not working here, because only 2 accounts are named + + for ( + let i = startingAccountIndex; + i < startingAccountIndex + additionalEntrances; + i++ + ) { + const accountConnectedRaffle = await raffle.connect(accounts[i]); + await accountConnectedRaffle.enterRaffle({ value: raffleEntranceFee }); + } + const startingTimeStamp = await raffle.getLastTimeStamp(); + // This will be more important for our staging tests... + await new Promise(async (resolve, reject) => { + raffle.once("WinnerPicked", async () => { + // event listener for WinnerPicked + console.log("WinnerPicked event fired!"); + // assert throws an error if it fails, so we need to wrap + // it in a try/catch so that the promise returns event + // if it fails. + try { + // Now lets get the ending values... + const recentWinner = await raffle.getRecentWinner(); + const raffleState = await raffle.getRaffleState(); + const winnerBalance = await accounts[2].getBalance(); + const endingTimeStamp = await raffle.getLastTimeStamp(); + await expect(raffle.getPlayer(0)).to.be.reverted; + // Comparisons to check if our ending values are correct: + assert.equal(recentWinner.toString(), accounts[2].address); + assert.equal(raffleState, 0); + assert.equal( + winnerBalance.toString(), + startingBalance // startingBalance + ( (raffleEntranceFee * additionalEntrances) + raffleEntranceFee ) + .add( + raffleEntranceFee + .mul(additionalEntrances) + .add(raffleEntranceFee), + ) + .toString(), + ); + assert(endingTimeStamp > startingTimeStamp); + resolve(); // if try passes, resolves the promise + } catch (e) { + reject(e); // if try fails, rejects the promise + } + }); + + // kicking off the event by mocking the chainlink keepers and vrf coordinator + try { + const tx = await raffle.performUpkeep("0x"); + const txReceipt = await tx.wait(1); + startingBalance = await accounts[2].getBalance(); + + // Log subscription details + const subscription = + await vrfCoordinatorV2_5Mock.getSubscription(subscriptionId); + const balance = subscription.balance; + const owner = subscription.owner; + const consumers = subscription.consumers; + + console.log(`Subscription ID: ${subscriptionId}`); + console.log(`Subscription Owner: ${owner}`); + console.log( + `Subscription Balance: ${ethers.utils.formatEther(balance)} ETH`, + ); + console.log(`Consumers: ${consumers}`); // This should include your Raffle contract address + + // Verify the Raffle contract address matches the consumer + assert( + consumers.includes(raffle.address), + "Raffle contract not authorized as a consumer!", + ); + + await vrfCoordinatorV2_5Mock.fulfillRandomWords( + txReceipt.events[1].args.requestId, + raffle.address, + ); + + // Log post-fulfillment details + const updatedSubscription = + await vrfCoordinatorV2_5Mock.getSubscription(subscriptionId); + console.log( + `Updated Subscription Balance: ${ethers.utils.formatEther(updatedSubscription.balance)} ETH`, + ); + } catch (e) { + reject(e); + } + }); + }); }); });