chore: forge init

This commit is contained in:
hoelee 2024-08-13 04:13:15 +08:00
parent 43f858a68a
commit 3e5079217a
13 changed files with 270 additions and 235 deletions

45
.github/workflows/test.yml vendored Normal file
View File

@ -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

4
.gitignore vendored
View File

@ -99,4 +99,6 @@ lint/outputs/
lint/tmp/
# lint/reports/
gas-report.txt
gas-report.txt
lib

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "lib/chainlink-brownie-contracts"]
path = lib/chainlink-brownie-contracts
url = https://github.com/smartcontractkit/chainlink-brownie-contracts

236
README.md
View File

@ -1,210 +1,66 @@
# Practical Sample With Hardhat + Solidity
## Foundry
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
**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.**
This project mainly to keep as a reference for future Web 3.0 Developments.
Foundry consists of:
---
### Success Deploy & Verified Of This Smart Contract To Sepolia Testnet
- **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.
0xc2022b56eBC140B5FebCf9FBaB14c17db4C315C4
https://sepolia.etherscan.io/address/0xc2022b56eBC140B5FebCf9FBaB14c17db4C315C4#code
Via deploy.js
https://sepolia.etherscan.io/address/0x3a827C119e1D746bb3C7bcbbf95c55246C8CcBdd#code
Via yarn hardhat deploy --network sepolia
## Documentation
### Public Reported Hacked Code References:
https://book.getfoundry.sh/
This website is records of all kind previous hacked smart contract:
## Usage
https://rekt.news/leaderboard/
### Build
## 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
```shell
$ forge build
```
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
### Test
```shell
$ forge test
```
Development need exlude file can create root file with name .gitignore
```
node_modules
package.json
img
artifacts
cache
coverage
.env
.*
README.md
coverage.json
### Format
```shell
$ forge fmt
```
## 2. Setup Visual Studio Code Development Environment
### Gas Snapshots
Windows need to download install WSL
```shell
$ forge snapshot
```
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
### Anvil
```shell
$ anvil
```
Visual Studio need to update code setting
### Deploy
```shell
$ forge script script/Counter.s.sol:CounterScript --rpc-url <your_rpc_url> --private-key <your_private_key>
```
"[solidity]": {
"editor.defaultFormatter":"NomicFoundation.hardhat-solidity"
}
### Cast
```shell
$ cast <subcommand>
```
Preparing of solidity development environment:
### Help
```shell
$ forge --help
$ anvil --help
$ cast --help
```
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 <a href="https://wa.me/60175885290">+60175885290</a>
.
Or You can email <a href="mailto:me@hoelee.com">me@hoelee.com</a> now. Thanks.
## 7. Like this project?
If you are feeling generous, buy me a coffee! - <a href="https://buymeacoffee.com/hoelee">buymeacoffee.com/hoelee</a>

View File

@ -1,13 +1,27 @@
// SPDX-License-Identifier: Unlicense
/*
Raffle Contract
Enter the lottery (paying some amount)
Pick a random winner (verifiably random)
Winner to be selected every X minutes -> completely automated
Chainlink Oracle -> Randomness, Automated Execution (Chainlink Keeper)
Layout of Contract:
version
imports
errors
interfaces, libraries, contracts
Type declarations
State variables
Events
Modifiers
Functions
Layout of Functions:
constructor
receive function (if exists)
fallback function (if exists)
external
public
internal
private
view & pure functions
*/
pragma solidity ^0.8.7;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
///// UPDATE IMPORTS TO V2.5 /////
import {VRFConsumerBaseV2Plus} from "@chainlink/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol";
@ -22,6 +36,16 @@ error Raffle__RaffleNotOpen();
error Raffle__IntervalNotPassed();
error Raffle__UpkeepNotNeeded(uint256 currentBalance, uint256 numPlayers, uint256 raffleState);
/**
* @title Automatic Raffle Contract
* @author Hoelee
* @notice This contract is a lottery contract that allows users to enter the lottery by paying a fee.
* @dev Raffle Contract
Enter the lottery (paying some amount)
Pick a random winner (verifiably random)
Winner to be selected every X minutes -> completely automated
Chainlink Oracle -> Randomness, Automated Execution (Chainlink Keeper)
*/
contract Raffle is VRFConsumerBaseV2Plus, AutomationCompatibleInterface {
/* Type declarations */
enum RaffleState {
@ -131,18 +155,17 @@ contract Raffle is VRFConsumerBaseV2Plus, AutomationCompatibleInterface {
// Encode ExtraArgs to bytes
bytes memory extraArgs = VRFV2PlusClient._argsToBytes(extraArgsV1);
// Prepare the RandomWordsRequest structure
VRFV2PlusClient.RandomWordsRequest memory req = VRFV2PlusClient.RandomWordsRequest({
keyHash: i_gasLane,
subId: i_subscriptionId,
requestConfirmations: REQUEST_CONFIRMATIONS,
callbackGasLimit: i_callbackGasLimit,
numWords: NUM_WORDS,
extraArgs: extraArgs
});
// Request random words using the prepared structure
requestId = i_vrfCoordinator.requestRandomWords(req);
requestId = i_vrfCoordinator.requestRandomWords(
VRFV2PlusClient.RandomWordsRequest({
keyHash: i_gasLane,
subId: i_subscriptionId,
requestConfirmations: REQUEST_CONFIRMATIONS,
callbackGasLimit: i_callbackGasLimit,
numWords: NUM_WORDS,
extraArgs: extraArgs
})
);
emit RequestedRaffleWinner(requestId);
}

View File

@ -18,11 +18,6 @@ module.exports = async ({ getNamedAccounts, deployments }) => {
// Mock Auto Create VRF V2.5 Subscription
/* // Deprecated
//vrfCoordinatorV2_5Mock = await ethers.getContract("VRFCoordinatorV2Mock");
vrfCoordinatorV2_5Address = vrfCoordinatorV2_5Mock.address;
const transactionResponse = await vrfCoordinatorV2_5Mock.createSubscription();
*/
vrfCoordinatorV2_5Mock = await get("VRFCoordinatorV2_5Mock");
vrfCoordinatorV2_5Address = vrfCoordinatorV2_5Mock.address;
@ -30,15 +25,23 @@ module.exports = async ({ getNamedAccounts, deployments }) => {
const vrfCoordinatorV2_5MockInstance = await ethers.getContractAt(
"VRFCoordinatorV2_5Mock",
vrfCoordinatorV2_5Address,
);
);
*/
// Create a subscription
const createSubTx = await vrfCoordinatorV2_5MockInstance.createSubscription();
const createSubReceipt = await createSubTx.wait(1);
vrfCoordinatorV2_5Mock = await ethers.getContract("VRFCoordinatorV2_5Mock");
vrfCoordinatorV2_5Address = vrfCoordinatorV2_5Mock.address;
const transactionResponse = await vrfCoordinatorV2_5Mock.createSubscription();
const createSubReceipt = await transactionResponse.wait(1);
subscriptionId = createSubReceipt.events[0].args.subId; // Keep as BigNumber
// Fund the subscription
await vrfCoordinatorV2_5MockInstance.fundSubscription(subscriptionId, FUND_AMOUNT);
await vrfCoordinatorV2_5Mock.fundSubscription(subscriptionId, FUND_AMOUNT);
const subscription = await vrfCoordinatorV2_5Mock.getSubscription(subscriptionId);
const balance = subscription.balance;
console.log(`Subscription balance: ${ethers.utils.formatEther(balance)} Ether`);
} else {
vrfCoordinatorV2_5Address = networkConfig[chainId]["vrfCoordinatorV2"];
subscriptionId = networkConfig[chainId]["subscriptionId"];
@ -68,11 +71,14 @@ module.exports = async ({ getNamedAccounts, deployments }) => {
// Ensure the Raffle contract is a valid consumer of the VRFCoordinatorV2Mock contract.
if (developmentChains.includes(network.name)) {
/* // Deprecated
//const vrfCoordinatorV2_5Mock = await ethers.getContract("VRFCoordinatorV2Mock");
//await vrfCoordinatorV2_5Mock.addConsumer(subscriptionId, raffle.address);
*/
const vrfCoordinatorV2_5Mock = await ethers.getContract("VRFCoordinatorV2_5Mock");
await vrfCoordinatorV2_5Mock.addConsumer(subscriptionId, raffle.address);
// Verify if the consumer is added correctly
const subscription = await vrfCoordinatorV2_5Mock.getSubscription(subscriptionId);
console.log(`Consumers: ${subscription.consumers}`); // Should now include raffle.address
/* // Deprecated
vrfCoordinatorV2_5Mock = await deployments.get("VRFCoordinatorV2_5Mock");
vrfCoordinatorV2_5Address = vrfCoordinatorV2_5Mock.address;
@ -83,6 +89,7 @@ module.exports = async ({ getNamedAccounts, deployments }) => {
);
await vrfCoordinatorV2_5MockInstance.addConsumer(subscriptionId, raffle.address);
*/
}
// Verify the deployment

8
foundry.toml Normal file
View File

@ -0,0 +1,8 @@
# Foundry Configuration
[profile.default]
src = "contracts"
out = "out"
libs = ["lib"]
remappings = ['@chainlink.=.contracts=/lib/chainlink-brownie-contracts/contracts/']

View File

@ -101,7 +101,7 @@ module.exports = {
solidity: {
compilers: [
{
version: "0.8.24",
version: "0.8.19",
},
],
},

@ -0,0 +1 @@
Subproject commit 12393bd475bd60c222ff12e75c0f68effe1bbaaf

19
script/Counter.s.sol Normal file
View File

@ -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();
}
}

14
src/Counter.sol Normal file
View File

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

24
test/Counter.t.sol Normal file
View File

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

View File

@ -6,8 +6,6 @@ const {
VERIFICATION_BLOCK_CONFIRMATIONS,
} = require("../../helper-hardhat-config");
const isDevelopment = developmentChains.includes(network.name);
// Run on development chain
!developmentChains.includes(network.name)
? describe.skip
@ -29,8 +27,15 @@ const isDevelopment = developmentChains.includes(network.name);
"VRFCoordinatorV2_5Mock",
addressMock,
);
raffle = await ethers.getContractAt("Raffle", addressRaffle);
raffle = await ethers.getContractAt("Raffle", addressRaffle); // Default
//raffle = await ethers.getContractAt("Raffle", addressPlayer);
//raffle = await raffle.connect(addressPlayer); // Connect the player to the Raffle contract
//raffleContract = await ethers.getContract("Raffle"); // Returns a new connection to the Raffle contract
// raffle = raffleContract.connect(addressPlayer); // Returns a new instance of the Raffle contract connected to player
/*
// Add fund to Mock for fulfillRandomWords
const tx = await vrfCoordinatorV2_5Mock.createSubscription();
const txReceipt = await tx.wait(1);
@ -55,6 +60,7 @@ const isDevelopment = developmentChains.includes(network.name);
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
@ -187,10 +193,34 @@ const isDevelopment = developmentChains.includes(network.name);
});
describe("fulfillRandomWords", function () {
let subscriptionId;
beforeEach(async () => {
//raffle = await raffle.connect(addressPlayer); // Test
await raffle.enterRaffle({ value: raffleEntranceFee });
await network.provider.send("evm_increaseTime", [interval.toNumber() + 1]);
await network.provider.send("evm_mine", []);
const subIdArr = await vrfCoordinatorV2_5Mock.getActiveSubscriptionIds(0, 100);
subscriptionId = subIdArr[0];
//subscriptionId =
// "91765082012550290938840732836101412737423938684399204465191383082124961026867";
/*
vrfCoordinatorV2_5Address = vrfCoordinatorV2_5Mock.address;
const transactionResponse = await vrfCoordinatorV2_5Mock.createSubscription();
const createSubReceipt = await transactionResponse.wait(1);
subscriptionId = createSubReceipt.events[0].args.subId; // Keep as BigNumber
await vrfCoordinatorV2_5Mock.fundSubscription(subscriptionId, FUND_AMOUNT);
const FUND_AMOUNT = ethers.utils.parseEther("1"); // 1 Ether, or 1e18 (10^18) Wei
const subscription = await vrfCoordinatorV2_5Mock.getSubscription(subscriptionId);
const balance = subscription.balance;
console.log(`Subscription balance: ${ethers.utils.formatEther(balance)} Ether`);
console.log(`Consumers: ${subscription.consumers}`); // Should now include raffle.address
*/
//console.log(subscriptionId);
});
it("can only be called after performUpKeep", async () => {
@ -198,6 +228,10 @@ const isDevelopment = developmentChains.includes(network.name);
await expect(
vrfCoordinatorV2_5Mock.fulfillRandomWords(0, raffle.address),
).to.be.revertedWith("InvalidRequest");
await expect(
vrfCoordinatorV2_5Mock.fulfillRandomWords(1, raffle.address), // reverts if not fulfilled
).to.be.revertedWith("InvalidRequest");
});
// Complete Test
@ -218,8 +252,8 @@ const isDevelopment = developmentChains.includes(network.name);
const startingTimeStamp = await raffle.getLastTimeStamp();
// This will be more important for our staging tests...
await new Promise(async (resolve, reject) => {
// just a promise to handle the event listener
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
@ -250,14 +284,12 @@ const isDevelopment = developmentChains.includes(network.name);
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;
@ -277,17 +309,18 @@ const isDevelopment = developmentChains.includes(network.name);
"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`,
);
// Here cannot run, always InsufficientBalance()
await vrfCoordinatorV2_5Mock.fulfillRandomWords(
txReceipt.events[1].args.requestId,
raffle.address,
);
} catch (e) {
reject(e);
}