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);
+ }
+ });
+ });
});
});