## /articles/blockchain

Smart Contract Development with Solidity and Hardhat

Get started with Ethereum smart contract development using Solidity and Hardhat. Learn to write, test, and deploy your first smart contract.

4 min read#blockchain_web3

Ethereum blockchain visualization

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:

TerminalBash
# 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.

Blockchain network visualization
Hardhat provides a local blockchain for development and testing

Writing Your First Smart Contract

Let's create a simple token contract. Create contracts/MyToken.sol:

contracts/MyToken.solsolidity
// 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:

TerminalBash
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:

TerminalBash
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.tsTypeScript
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:

TerminalBash
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.tsTypeScript
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.tsTypeScript
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;
});
TerminalBash
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.