Hardhat has become the gold standard for Ethereum development. It provides a complete environment for writing, testing, and deploying smart contracts with first-class TypeScript support.
In this guide, we'll build and deploy a simple smart contract from scratch, covering everything you need to know to get started with Web3 development.
Why Hardhat?
Hardhat is a Solidity development environment built on Node.js. Unlike alternatives, Hardhat offers:
- Local Ethereum Network - Built-in Hardhat Network for testing
- TypeScript Support - First-class TS integration
- Flexible Testing - Write tests in Solidity or TypeScript
- Plugin Ecosystem - Extensive plugins for verification, gas reporting, etc.
- Debugging - Solidity stack traces and console.log
Project Setup
Create a new Hardhat project:
Terminal
# Create project directory
mkdir my-smart-contract
cd my-smart-contract
# Initialize npm and install Hardhat
npm init -y
npm install --save-dev hardhat
# Initialize Hardhat project
npx hardhat init
Select "Create a TypeScript project" when prompted. This sets up your project with TypeScript, ethers.js, and testing frameworks.
Writing Your First Smart Contract
Let's create a simple token contract. Create contracts/MyToken.sol:
contracts/MyToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyToken is ERC20, Ownable {
uint256 public constant MAX_SUPPLY = 1_000_000 * 10**18;
constructor() ERC20("MyToken", "MTK") Ownable(msg.sender) {
// Mint initial supply to deployer
_mint(msg.sender, 100_000 * 10**18);
}
function mint(address to, uint256 amount) public onlyOwner {
require(totalSupply() + amount <= MAX_SUPPLY, "Exceeds max supply");
_mint(to, amount);
}
function burn(uint256 amount) public {
_burn(msg.sender, amount);
}
}
Install OpenZeppelin contracts:
Terminal
npm install @openzeppelin/contracts
Best Practice: Always use audited libraries like OpenZeppelin for standard functionality. Don't reinvent the wheel for tokens, access control, or security patterns.
Compiling the Contract
Compile your contract to check for errors:
Terminal
npx hardhat compile
This generates artifacts in the artifacts/ directory including ABI files you'll need for frontend integration.
Writing Tests
Testing is critical for smart contracts - bugs can be costly and irreversible. At least 70% of your development time should focus on testing.
test/MyToken.test.ts
import { expect } from "chai";
import { ethers } from "hardhat";
import { MyToken } from "../typechain-types";
import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers";
describe("MyToken", function () {
let token: MyToken;
let owner: SignerWithAddress;
let addr1: SignerWithAddress;
beforeEach(async function () {
[owner, addr1] = await ethers.getSigners();
const MyToken = await ethers.getContractFactory("MyToken");
token = await MyToken.deploy();
});
describe("Deployment", function () {
it("Should set the right owner", async function () {
expect(await token.owner()).to.equal(owner.address);
});
it("Should mint initial supply to owner", async function () {
const ownerBalance = await token.balanceOf(owner.address);
expect(ownerBalance).to.equal(ethers.parseEther("100000"));
});
});
describe("Minting", function () {
it("Should allow owner to mint", async function () {
await token.mint(addr1.address, ethers.parseEther("1000"));
expect(await token.balanceOf(addr1.address))
.to.equal(ethers.parseEther("1000"));
});
it("Should prevent non-owner from minting", async function () {
await expect(
token.connect(addr1).mint(addr1.address, ethers.parseEther("1000"))
).to.be.revertedWithCustomError(token, "OwnableUnauthorizedAccount");
});
it("Should prevent minting beyond max supply", async function () {
await expect(
token.mint(owner.address, ethers.parseEther("1000000"))
).to.be.revertedWith("Exceeds max supply");
});
});
describe("Burning", function () {
it("Should allow users to burn their tokens", async function () {
const initialBalance = await token.balanceOf(owner.address);
await token.burn(ethers.parseEther("1000"));
expect(await token.balanceOf(owner.address))
.to.equal(initialBalance - ethers.parseEther("1000"));
});
});
});
Run your tests:
Terminal
npx hardhat test
Critical: Never deploy a contract without comprehensive tests. Smart contract bugs can result in permanent loss of funds.
Deploying to Testnet
Configure Hardhat for the Sepolia testnet:
hardhat.config.ts
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
import * as dotenv from "dotenv";
dotenv.config();
const config: HardhatUserConfig = {
solidity: "0.8.20",
networks: {
sepolia: {
url: process.env.SEPOLIA_RPC_URL || "",
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
},
},
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY,
},
};
export default config;
Create a deployment script:
scripts/deploy.ts
import { ethers } from "hardhat";
async function main() {
console.log("Deploying MyToken...");
const MyToken = await ethers.getContractFactory("MyToken");
const token = await MyToken.deploy();
await token.waitForDeployment();
const address = await token.getAddress();
console.log(`MyToken deployed to: ${address}`);
// Verify on Etherscan
console.log("Waiting for block confirmations...");
await token.deploymentTransaction()?.wait(5);
console.log("Verifying contract on Etherscan...");
await hre.run("verify:verify", {
address: address,
constructorArguments: [],
});
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Terminal
npx hardhat run scripts/deploy.ts --network sepolia
Next Steps
Now that you have a working smart contract:
- Add More Features - Staking, governance, or NFT functionality
- Frontend Integration - Connect with ethers.js or wagmi
- Security Audit - Get professional review before mainnet
- Gas Optimization - Use Hardhat's gas reporter plugin
Conclusion
Hardhat makes smart contract development accessible and professional. With TypeScript support, excellent testing tools, and a robust plugin ecosystem, it's the ideal choice for both beginners and experienced blockchain developers.
Remember: security is paramount in Web3. Test thoroughly, use audited libraries, and consider professional audits before handling real value.
