Skip to content

Instantly share code, notes, and snippets.

@veektrie
Last active October 10, 2024 12:18
Show Gist options
  • Save veektrie/8f0fcf2567e0365a19be2f287e2ba536 to your computer and use it in GitHub Desktop.
Save veektrie/8f0fcf2567e0365a19be2f287e2ba536 to your computer and use it in GitHub Desktop.
Learn how to use Hardhat for a more advanced Solidity development workflow.
https://hardhat.org/hardhat-runner/docs/getting-started#installation
https://nodejs.org/en
Installation & Setup
A. Prerequisites:
- Node.js and npm: Ensure you have [Node.js](https://nodejs.org/) (which includes npm) installed. Verify installation by running:
```bash
node -v
npm -v
```
B. Setting Up a New Hardhat Project:
1. Create a Project Directory:
```bash
mkdir hardhat-demo
cd hardhat-demo
```
2. Initialize a New npm Project:
```bash
npm init -y
```
- Explanation: This command creates a `package.json` file with default settings, managing project dependencies.
3. Install Hardhat:
```bash
npm install --save-dev hardhat
```
- Explanation: Installs Hardhat as a development dependency.
4. Initialize Hardhat:
```bash
npx hardhat
```
- Interactive Setup:
- Choose a Task: Select "Create a basic sample project".
- Project Directory: Confirm the current directory.
- Add .gitignore: Yes, to ignore `node_modules` and other unnecessary files.
- Install Sample Project Dependencies: Agree to install them automatically.
- Result: Hardhat sets up a sample project with the following structure:
```
hardhat-demo/
├── contracts/
│ └── Greeter.sol
├── scripts/
│ └── sample-script.js
├── test/
│ └── sample-test.js
├── hardhat.config.js
├── package.json
└── .gitignore
```
C. Understanding the Folder Structure:
- `contracts/`: Contains Solidity contracts.
- `scripts/`: JavaScript scripts for deploying contracts.
- `test/`: Contains test scripts for smart contracts.
- `hardhat.config.js`: Configuration file for Hardhat settings and plugins.
- `package.json`: Manages project dependencies and scripts.
- `.gitignore`: Specifies files and directories to ignore in version control.
3. Creating a Contract: ERC20 Token
A. Overview of ERC20 Tokens:
ERC20 is a standard interface for fungible tokens on Ethereum, defining functions like `transfer`, `balanceOf`, and `approve`. Implementing an ERC20 token ensures compatibility with wallets, exchanges, and other smart contracts.
B. Creating the ERC20 Contract:
1. Navigate to the `contracts` Directory:
```bash
cd contracts
```
2. Create a New File Named `MyToken.sol`:
```bash
touch MyToken.sol
```
3. Write the ERC20 Contract:
Open `MyToken.sol` in your preferred code editor and add the following code:
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MyToken is ERC20 {
constructor(string memory name, string memory symbol, uint256 initialSupply) ERC20(name, symbol) {
_mint(msg.sender, initialSupply (10 decimals()));
}
function mint(address to, uint256 amount) public {
_mint(to, amount);
}
function burn(address from, uint256 amount) public {
_burn(from, amount);
}
}
```
Explanation:
- SPDX License Identifier: Specifies the MIT license.
- Pragma Directive: Uses Solidity version `^0.8.0`.
- Import Statement: Imports the ERC20 implementation from OpenZeppelin.
- Contract `MyToken`:
- Constructor: Initializes the token with a name, symbol, and initial supply, minting the initial supply to the deployer’s address.
- `mint` Function: Allows minting new tokens to a specified address.
- `burn` Function: Allows burning tokens from a specified address.
4. Install OpenZeppelin Contracts:
To use OpenZeppelin's ERC20 implementation, install the package:
```bash
npm install @openzeppelin/contracts
```
- Explanation: This installs the OpenZeppelin Contracts library, providing secure and community-vetted implementations of ERC20 and other standards.
4. Compilation & Local Node
A. Compiling the Contract:
1. Navigate Back to the Project Root:
```bash
cd ..
```
2. Compile the Contracts:
```bash
npx hardhat compile
```
- Output:
- Hardhat compiles the Solidity contracts, generating artifacts in the `artifacts/` and `cache/` directories.
- If there are compilation errors, they will be displayed in the console. Address any issues before proceeding.
B. Spinning Up a Local Hardhat Node:
1. Start the Hardhat Local Node:
```bash
npx hardhat node
```
- Explanation: This command starts a local Ethereum network with predefined accounts and instant mining, ideal for development and testing.
- Output:
- Provides a list of accounts with private keys and the network’s RPC URL (usually `http://127.0.0.1:8545/`).
Note: Keep this terminal window open as it runs the local node. Open a new terminal window for deploying contracts and running scripts.
5. Deploying to a Local Node
A. Writing a Deployment Script:
1. Navigate to the `scripts` Directory:
```bash
cd scripts
```
2. Create a Deployment Script Named `deploy.js`:
```bash
touch deploy.js
```
3. Write the Deployment Script:
Open `deploy.js` in your code editor and add the following code:
```javascript
const hre = require("hardhat");
async function main() {
// Fetch the contract to deploy
const MyToken = await hre.ethers.getContractFactory("MyToken");
// Define token parameters
const name = "My Custom Token";
const symbol = "MCT";
const initialSupply = 1000;
// Deploy the contract
const myToken = await MyToken.deploy(name, symbol, initialSupply);
await myToken.deployed();
console.log("MyToken deployed to:", myToken.address);
}
// Handle errors and run the main function
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
```
Explanation:
- Import Hardhat Runtime Environment (HRE): Provides access to Hardhat's functionalities.
- `main` Function:
- `getContractFactory`: Prepares the contract for deployment.
- Define Token Parameters: Sets the token’s name, symbol, and initial supply.
- Deploy the Contract: Deploys `MyToken` with the specified parameters.
- Log the Deployed Address: Outputs the contract address upon successful deployment.
- Error Handling: Catches and logs any errors during deployment.
B. Deploying the Contract:
1. Ensure the Local Node is Running:
- Confirm that the Hardhat node is active in another terminal window.
2. Deploy the Contract to the Local Node:
```bash
npx hardhat run scripts/deploy.js --network localhost
```
- Explanation: This command runs the `deploy.js` script on the `localhost` network (the Hardhat node).
- Output:
```
MyToken deployed to: 0xYourContractAddress
```
- Note: Replace `0xYourContractAddress` with the actual deployed contract address displayed.
C. Understanding Deployment Artifacts:
- Artifacts: Located in the `artifacts/` directory, containing the compiled contract's ABI and bytecode.
- Deployment Addresses: Important for interacting with the deployed contracts.
6. Console Interaction
A. Using the Hardhat Console:
1. Open a New Terminal Window:
- Keep the Hardhat node running in its terminal.
2. Access the Hardhat Console:
```bash
npx hardhat console --network localhost
```
- Explanation: Launches an interactive JavaScript console connected to the local Hardhat network.
3. Interact with the Deployed Contract:
i. Fetch the Deployed Contract Instance:
```javascript
const MyToken = await ethers.getContractFactory("MyToken");
const myToken = await MyToken.attach("0xYourContractAddress");
```
- Replace `"0xYourContractAddress"` with the actual contract address obtained during deployment.
ii. View Token Details:
```javascript
const name = await myToken.name();
console.log("Token Name:", name); // Outputs: My Custom Token
const symbol = await myToken.symbol();
console.log("Token Symbol:", symbol); // Outputs: MCT
const totalSupply = await myToken.totalSupply();
console.log("Total Supply:", totalSupply.toString()); // Outputs: 1000000000000000000000 (considering decimals)
```
iii. Check Account Balance:
```javascript
const [owner] = await ethers.getSigners();
const balance = await myToken.balanceOf(owner.address);
console.log("Owner Balance:", balance.toString()); // Outputs: 1000000000000000000000
```
iv. Mint New Tokens:
```javascript
await myToken.mint("0xRecipientAddress", 500);
const newBalance = await myToken.balanceOf("0xRecipientAddress");
console.log("Recipient Balance:", newBalance.toString()); // Outputs: 500000000000000000000
```
- Note: Replace `"0xRecipientAddress"` with a valid address from the local Hardhat node.
v. Burn Tokens:
```javascript
await myToken.burn(owner.address, 200);
const updatedBalance = await myToken.balanceOf(owner.address);
console.log("Updated Owner Balance:", updatedBalance.toString()); // Outputs: 999800000000000000000
```
B. Using the Console for Advanced Interactions:
- Transferring Ownership (if implemented):
```javascript
await myToken.transferOwnership("0xNewOwnerAddress");
const newOwner = await myToken.owner();
console.log("New Owner:", newOwner);
```
- Listing All Token Holders:
- Note: Requires maintaining a list of holders within the contract or using events.
C. Exiting the Console:
- Type `.exit` or press `Ctrl + D` to exit the Hardhat console.
7. Debugging Tools
A. Introduction to Hardhat’s Debugging Capabilities:
Hardhat provides robust debugging tools that help developers identify and resolve issues within smart contracts. These tools include detailed error messages, stack traces, and the ability to inspect the execution flow of transactions.
B. Common Debugging Scenarios:
1. Reverted Transactions:
- Occur when a `require`, `revert`, or `assert` statement fails.
- Example: Attempting to mint tokens to an invalid address.
2. Gas Limit Exceeded:
- Happens when a transaction runs out of gas.
- Example: Deploying a contract with excessive computational requirements.
3. Unexpected State Changes:
- Occurs when contract state does not change as intended.
- Example: Incorrectly updating a state variable.
C. Using Hardhat’s Built-In Debugger:
1. Trigger an Error:
- Modify the `mint` function to include a condition that can fail.
```solidity
function mint(address to, uint256 amount) public {
require(to != address(0), "Cannot mint to the zero address");
_mint(to, amount);
}
```
2. Recompile the Contract:
```bash
npx hardhat compile
```
3. Redeploy the Contract:
- Update the deployment script if necessary and redeploy using:
```bash
npx hardhat run scripts/deploy.js --network localhost
```
4. Attempt to Mint to the Zero Address:
- Use the Hardhat console or a script to call `mint` with `0x0000000000000000000000000000000000000000`.
```javascript
await myToken.mint("0x0000000000000000000000000000000000000000", 100);
```
- Result: Transaction reverts with the error message "Cannot mint to the zero address".
5. Debug the Failed Transaction:
- Using Console Logs:
- The error message is displayed, indicating where the transaction failed.
- Using Hardhat’s Trace Feature:
- Unfortunately, Hardhat doesn’t have a built-in step-through debugger like Remix. However, you can use plugins or integrate with other debugging tools.
- Using Hardhat's `debug` Task:
- Start the Hardhat node with debugging enabled.
```bash
npx hardhat node --logging
```
- Note: This provides more detailed logs that can help trace issues.
- Leveraging Stack Traces:
- When a transaction fails, Hardhat provides stack traces pointing to the exact line in the Solidity code where the error occurred.
6. Analyzing the Error:
- Identify that the `require` statement in the `mint` function failed due to an attempt to mint to the zero address.
7. Fixing the Issue:
- Ensure that the `to` address is valid before calling the `mint` function.
- Implement additional checks or handle exceptions gracefully.
D. Best Practices for Debugging:
1. Use Descriptive Error Messages:
- Always provide clear and descriptive messages in `require`, `revert`, and `assert` statements to simplify debugging.
2. Emit Events:
- Emit events for significant state changes and actions within your contract. This aids in tracking contract behavior during interactions.
```solidity
event Minted(address indexed to, uint256 amount);
event Burned(address indexed from, uint256 amount);
function mint(address to, uint256 amount) public {
require(to != address(0), "Cannot mint to the zero address");
_mint(to, amount);
emit Minted(to, amount);
}
function burn(address from, uint256 amount) public {
_burn(from, amount);
emit Burned(from, amount);
}
```
3. Write Comprehensive Tests:
- Utilize Hardhat’s testing framework to write unit and integration tests, catching issues before deployment.
4. Leverage Static Analysis Tools:
- Use tools like `solhint` or `eslint-plugin-solidity` to analyze code for potential vulnerabilities and best practice adherence.
5. Review Gas Consumption:
- Optimize functions to reduce gas usage, preventing out-of-gas errors during deployment or execution.
8. Hands-On Exercise
Objective: Apply the knowledge gained from the demo by creating, setting up, compiling, deploying, and interacting with a custom smart contract using Hardhat. Modify the deployment script to add different parameters or deploy multiple contracts.
Exercise Steps:
1. Set Up Your Hardhat Project:
A. Initialize the Project:
- If not already done, create and navigate to a new project directory.
```bash
mkdir hardhat-exercise
cd hardhat-exercise
npm init -y
npm install --save-dev hardhat
npx hardhat
```
- Choose "Create a basic sample project" and follow the prompts.
B. Install OpenZeppelin Contracts:
```bash
npm install @openzeppelin/contracts
```
2. Write a Custom Smart Contract:
A. Create a New Contract File:
```bash
cd contracts
touch SimpleStorage.sol
```
B. Implement the `SimpleStorage` Contract:
Open `SimpleStorage.sol` in your code editor and add:
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SimpleStorage {
uint256 private data;
event DataUpdated(uint256 newData);
constructor(uint256 initialData) {
data = initialData;
emit DataUpdated(initialData);
}
function setData(uint256 newData) public {
data = newData;
emit DataUpdated(newData);
}
function getData() public view returns (uint256) {
return data;
}
}
```
Explanation:
- State Variable: `data` stores an unsigned integer.
- Event: `DataUpdated` emitted whenever `data` is updated.
- Constructor: Initializes `data` with `initialData`.
- Functions:
- `setData`: Updates `data` and emits an event.
- `getData`: Retrieves the current value of `data`.
3. Compile the Contract:
```bash
cd ..
npx hardhat compile
```
- Ensure there are no compilation errors. Address any if present.
4. Deploy the Contract to a Local Node:
A. Start the Local Hardhat Node:
```bash
npx hardhat node
```
B. Open a New Terminal Window and Navigate to the Project Directory.
C. Write a Deployment Script:
1. Navigate to `scripts`:
```bash
cd scripts
```
2. Create `deploySimpleStorage.js`:
```bash
touch deploySimpleStorage.js
```
3. Implement the Deployment Script:
Open `deploySimpleStorage.js` and add:
```javascript
const hre = require("hardhat");
async function main() {
const SimpleStorage = await hre.ethers.getContractFactory("SimpleStorage");
const initialData = 42; // Example initial data
const simpleStorage = await SimpleStorage.deploy(initialData);
await simpleStorage.deployed();
console.log("SimpleStorage deployed to:", simpleStorage.address);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
```
Explanation:
- Defines `initialData` as `42` to initialize the contract.
- Deploys `SimpleStorage` with the specified `initialData`.
- Logs the deployed contract address.
D. Deploy the Contract:
```bash
npx hardhat run scripts/deploySimpleStorage.js --network localhost
```
- Output:
```
SimpleStorage deployed to: 0xYourContractAddress
```
5. Interact with the Deployed Contract:
A. Open the Hardhat Console:
```bash
npx hardhat console --network localhost
```
B. Fetch the Contract Instance:
```javascript
const SimpleStorage = await ethers.getContractFactory("SimpleStorage");
const simpleStorage = await SimpleStorage.attach("0xYourContractAddress");
```
- Replace `"0xYourContractAddress"` with the actual address from deployment.
C. Retrieve Initial Data:
```javascript
const initial = await simpleStorage.getData();
console.log("Initial Data:", initial.toString()); // Outputs: 42
```
D. Update the Data:
```javascript
const tx = await simpleStorage.setData(100);
await tx.wait();
const updated = await simpleStorage.getData();
console.log("Updated Data:", updated.toString()); // Outputs: 100
```
E. View Event Logs:
```javascript
// Fetch past events
const events = await simpleStorage.queryFilter("DataUpdated");
events.forEach((event) => {
console.log(`Data Updated to: ${event.args.newData}`);
});
```
- Output:
```
Data Updated to: 42
Data Updated to: 100
```
6. Modify the Deployment Script to Add Different Parameters or Deploy Multiple Contracts:
A. Deploying Multiple Instances:
1. Update `deploy.js`:
Open `deploy.js` and modify it to deploy both `MyToken` and `SimpleStorage` contracts.
```javascript
const hre = require("hardhat");
async function main() {
// Deploy MyToken
const MyToken = await hre.ethers.getContractFactory("MyToken");
const name = "My Custom Token";
const symbol = "MCT";
const initialSupply = 1000;
const myToken = await MyToken.deploy(name, symbol, initialSupply);
await myToken.deployed();
console.log("MyToken deployed to:", myToken.address);
// Deploy SimpleStorage
const SimpleStorage = await hre.ethers.getContractFactory("SimpleStorage");
const initialData = 2024;
const simpleStorage = await SimpleStorage.deploy(initialData);
await simpleStorage.deployed();
console.log("SimpleStorage deployed to:", simpleStorage.address);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
```
2. Run the Updated Deployment Script:
```bash
npx hardhat run scripts/deploy.js --network localhost
```
- Output:
```
MyToken deployed to: 0xTokenContractAddress
SimpleStorage deployed to: 0xSimpleStorageContractAddress
```
B. Adding Deployment Parameters:
- Example: Deploy `MyToken` with a different initial supply.
1. Modify the Deployment Script:
```javascript
// Inside the main function after deploying MyToken
const newInitialSupply = 5000;
const myToken2 = await MyToken.deploy(name, symbol, newInitialSupply);
await myToken2.deployed();
console.log("MyToken2 deployed to:", myToken2.address);
```
2. Deploy with Additional Parameters:
```bash
npx hardhat run scripts/deploy.js --network localhost
```
- Output:
```
MyToken deployed to: 0xToken1Address
SimpleStorage deployed to: 0xStorage1Address
MyToken2 deployed to: 0xToken2Address
```
C. Automating Multiple Deployments:
- Use Arrays or Loops to Deploy Multiple Contracts with Different Parameters.
```javascript
async function main() {
const contractsToDeploy = [
{
name: "MyToken1",
symbol: "MCT1",
initialSupply: 1000,
},
{
name: "MyToken2",
symbol: "MCT2",
initialSupply: 5000,
},
];
for (const contract of contractsToDeploy) {
const MyToken = await hre.ethers.getContractFactory("MyToken");
const myToken = await MyToken.deploy(contract.name, contract.symbol, contract.initialSupply);
await myToken.deployed();
console.log(`${contract.name} deployed to:`, myToken.address);
}
// Similarly, deploy multiple SimpleStorage contracts
const storageContracts = [100, 200, 300];
for (const initialData of storageContracts) {
const SimpleStorage = await hre.ethers.getContractFactory("SimpleStorage");
const simpleStorage = await SimpleStorage.deploy(initialData);
await simpleStorage.deployed();
console.log(`SimpleStorage with initialData ${initialData} deployed to:`, simpleStorage.address);
}
}
```
- Deploy All Contracts:
```bash
npx hardhat run scripts/deploy.js --network localhost
```
7. Submitting the Exercise:
A. Contract Code:
- Submit the Solidity code of the custom `SimpleStorage` (and any additional contracts if implemented).
B. Deployment Screenshot:
- Provide a screenshot of the terminal showing the deployed contract addresses.
C. Interaction Logs:
- Document interactions performed via the Hardhat console, including function calls and their outputs.
D. Debugging Evidence:
- Include screenshots or descriptions of any debugging sessions, especially if issues were encountered and resolved.
E. Reflection:
- Write a brief summary outlining what was learned during the exercise, challenges faced, and how they were overcome.
Example Reflection:
> During this exercise, I learned how to set up a Hardhat project from scratch, write and compile Solidity contracts, and deploy them to a local Hardhat node. Interacting with the deployed contracts through the Hardhat console provided hands-on experience in managing contract states and functions. Modifying the deployment script to deploy multiple contracts reinforced my understanding of scripting and automation in Hardhat. Encountering and resolving a reverted transaction taught me the importance of thorough error handling and effective debugging strategies.
Tips and Best Practices
1. Leverage TypeScript (Optional):
- Hardhat supports TypeScript for enhanced type safety. Consider setting up a TypeScript environment for larger projects.
2. Use Environment Variables:
- Manage sensitive data like private keys and API URLs using environment variables with packages like `dotenv`.
- Example:
```bash
npm install dotenv
```
- In `hardhat.config.js`:
```javascript
require("dotenv").config();
module.exports = {
networks: {
localhost: {
url: "http://127.0.0.1:8545/"
},
mainnet: {
url: process.env.MAINNET_URL,
accounts: [process.env.PRIVATE_KEY],
},
// Additional networks
},
solidity: "0.8.0",
};
```
3. Integrate Plugins:
- Enhance Hardhat’s functionality with plugins like:
- `hardhat-ethers`: Integration with Ethers.js.
- `hardhat-waffle`: Testing framework using Waffle.
- `hardhat-gas-reporter`: Gas usage reporting.
- Installation Example:
```bash
npm install --save-dev @nomiclabs/hardhat-ethers ethers
```
4. Write Comprehensive Tests:
- Utilize Hardhat’s testing framework with Mocha and Chai to write unit and integration tests, ensuring contract reliability.
Example Test (`test/SimpleStorage.test.js`):
```javascript
const { expect } = require("chai");
describe("SimpleStorage Contract", function () {
let SimpleStorage, simpleStorage;
beforeEach(async function () {
SimpleStorage = await ethers.getContractFactory("SimpleStorage");
simpleStorage = await SimpleStorage.deploy(50);
await simpleStorage.deployed();
});
it("Should return the initial data", async function () {
expect(await simpleStorage.getData()).to.equal(50);
});
it("Should update data correctly", async function () {
await simpleStorage.setData(100);
expect(await simpleStorage.getData()).to.equal(100);
});
it("Should emit DataUpdated event on setData", async function () {
await expect(simpleStorage.setData(200))
.to.emit(simpleStorage, "DataUpdated")
.withArgs(200);
});
});
```
- Run Tests:
```bash
npx hardhat test
```
5. Optimize Gas Usage:
- Analyze and optimize contract functions to reduce gas consumption, making transactions cost-effective.
6. Stay Updated with Hardhat Releases:
- Regularly update Hardhat and its plugins to benefit from the latest features and security patches.
```bash
npm update hardhat
```
7. Secure Your Contracts:
- Follow best security practices, such as using OpenZeppelin libraries, implementing access controls, and conducting thorough audits.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment