Gerson

Gerson

Passionate developer specializing in web development, cloud architecture, and system design.

TypeScriptReactNext.jsPythonFastAPISQLNode.jsAWS

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.

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

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.

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