Skip to main content

Documentation Index

Fetch the complete documentation index at: https://seilabs-docs-evm-fixes-hardhat-v3.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

This tutorial will guide you through setting up Hardhat for Sei EVM development and using OpenZeppelin contracts to build secure, standardized smart contracts. We’ll cover environment setup, contract creation, deployment, and show how to leverage OpenZeppelin’s pre-built components.
Watch the video walkthrough for this topic in the Video Tutorials section.
Deploy to Testnet FirstIt is highly recommended that you deploy to testnet (atlantic-2) first and verify everything works as expected before committing to mainnet. Doing so helps you catch bugs early, avoid unnecessary gas costs, and keep your users safe.

Table of Contents

Prerequisites

Before we begin, ensure you have the following installed:
  • Node.js (v18.0.0 or later)
  • npm (v7.0.0 or later) or yarn
  • A code editor (VS Code recommended)

Setting Up Your Development Environment

Let’s create a new project and set up Hardhat:
# Create a new directory for your project
mkdir sei-hardhat-project
cd sei-hardhat-project

# Scaffold a Hardhat 3 project (ESM + TypeScript)
npx hardhat --init
When prompted, choose Hardhat 3 and a TypeScript project using Mocha and Ethers.js, then follow the prompts. This generates an ESM project ("type": "module" in package.json) with @nomicfoundation/hardhat-toolbox-mocha-ethers, ethers, Hardhat Ignition, TypeScript, and a ready-to-use tsconfig.json. Then add the OpenZeppelin contract library:
npm install @openzeppelin/contracts
Prefer a non-interactive setup (CI or scripted environments)? Do the same thing by hand — no prompts:
npm init -y
npm pkg set type=module
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox-mocha-ethers @openzeppelin/contracts typescript @types/node
The @nomicfoundation/hardhat-toolbox-mocha-ethers bundle pulls in ethers, Ignition, Mocha/Chai, and the keystore, network-helpers, verify, and typechain plugins — so this single install covers everything in this guide. If you hit peer-dependency errors, re-run with --legacy-peer-deps.

Configuring Hardhat for Sei EVM

Next, we’ll need to configure Hardhat to work with the Sei EVM. Update your hardhat.config.ts file:
hardhat.config.ts
import type { HardhatUserConfig } from 'hardhat/config';
import { configVariable } from 'hardhat/config';
import hardhatToolboxMochaEthers from '@nomicfoundation/hardhat-toolbox-mocha-ethers';

const config: HardhatUserConfig = {
  // Hardhat 3 loads plugins from an explicit array — no side-effect `import` statements
  plugins: [hardhatToolboxMochaEthers],
  solidity: {
    version: '0.8.28',
    settings: {
      optimizer: {
        enabled: true,
        runs: 200
      }
    }
  },
  networks: {
    // Sei testnet (atlantic-2)
    seitestnet: {
      type: 'http',
      chainType: 'l1',
      url: 'https://evm-rpc-testnet.sei-apis.com',
      accounts: [configVariable('SEI_PRIVATE_KEY')],
      chainId: 1328
    },
    // Sei mainnet (pacific-1)
    seimainnet: {
      type: 'http',
      chainType: 'l1',
      url: 'https://evm-rpc.sei-apis.com',
      accounts: [configVariable('SEI_PRIVATE_KEY')],
      chainId: 1329
    }
  }
};

export default config;
Hardhat 3 is ESM-first and requires "type": "module" in package.json (the scaffold sets this for you). Each network needs an explicit type: 'http', and plugins load via the plugins array rather than the Hardhat 2 side-effect import '@nomicfoundation/hardhat-toolbox'. A local simulated network is provided automatically — no hardhat/localhost entry required.
Hardhat 3 reads secrets through configVariable(...), backed by an encrypted keystore — so your key is never stored in a plaintext file. Set it once:
npx hardhat keystore set SEI_PRIVATE_KEY
You’ll be prompted to paste the private key of the account you deploy from; Hardhat stores it encrypted on your machine. For CI, export a SEI_PRIVATE_KEY environment variable instead — configVariable falls back to it.
Use a dedicated, throwaway deploy key funded with only what you need — never a personal wallet holding real funds.

Using OpenZeppelin Contracts

OpenZeppelin provides a library of secure, tested smart contract components that you can use to build your applications. First, let’s install the OpenZeppelin Contracts package:
npm install @openzeppelin/contracts

Creating and Deploying an ERC20 Token, ERC721 NFT or an Upgradeable UUPS Token

Let’s create a simple ERC20 token using OpenZeppelin contracts. Create a new file in the contracts directory called SeiToken.sol:
contracts/SeiToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract SeiToken is ERC20, Ownable {
    constructor(address initialOwner)
        ERC20("Sei Token", "SEI")
        Ownable(initialOwner)
    {
        // Mint 1 million tokens to the contract deployer (with 18 decimals)
        _mint(msg.sender, 1000000 * 10 ** decimals());
    }

    // Function to mint new tokens (only owner)
    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }

    // Function to burn tokens
    function burn(uint256 amount) public {
        _burn(msg.sender, amount);
    }
}
Now, create a deployment script in the ignition/modules directory called deploy-sei-token.ts:
ignition/modules/deploy-sei-token.ts
import { buildModule } from '@nomicfoundation/hardhat-ignition/modules';

export default buildModule('SeiTokenModule', (m) => {
  const deployer = m.getAccount(0);

  const seiToken = m.contract('SeiToken', [deployer]);

  return { seiToken };
});
To deploy the token to the Sei testnet:
npx hardhat ignition deploy ignition/modules/deploy-sei-token.ts --network seitestnet

Testing Your Smart Contracts

Hardhat makes it easy to test your contracts before deploying them. Create a test file test/sei-token-test.ts:
test/sei-token-test.ts
import { expect } from 'chai';
import { network } from 'hardhat';

describe('SeiToken', function () {
  let ethers;
  let seiToken;
  let owner;
  let addr1;
  let addr2;

  beforeEach(async function () {
    // Hardhat 3 exposes ethers through a network connection
    ({ ethers } = await network.getOrCreate());

    // Get signers
    [owner, addr1, addr2] = await ethers.getSigners();

    // Deploy the token
    seiToken = await ethers.deployContract('SeiToken', [owner.address]);
  });

  describe('Deployment', function () {
    it('Should set the right owner', async function () {
      expect(await seiToken.owner()).to.equal(owner.address);
    });

    it('Should assign the total supply of tokens to the owner', async function () {
      const ownerBalance = await seiToken.balanceOf(owner.address);
      const totalSupply = await seiToken.totalSupply();
      expect(totalSupply).to.equal(ownerBalance);
    });

    it('Should have correct name and symbol', async function () {
      expect(await seiToken.name()).to.equal('Sei Token');
      expect(await seiToken.symbol()).to.equal('SEI');
    });
  });

  describe('Transactions', function () {
    it('Should transfer tokens between accounts', async function () {
      // Transfer 50 tokens from owner to addr1
      await seiToken.transfer(addr1.address, 50);
      expect(await seiToken.balanceOf(addr1.address)).to.equal(50);

      // Transfer 50 tokens from addr1 to addr2
      await seiToken.connect(addr1).transfer(addr2.address, 50);
      expect(await seiToken.balanceOf(addr2.address)).to.equal(50);
    });

    it("Should fail if sender doesn't have enough tokens", async function () {
      const initialOwnerBalance = await seiToken.balanceOf(owner.address);

      // Try to send 1 token from addr1 (0 tokens) to owner
      await expect(seiToken.connect(addr1).transfer(owner.address, 1)).to.be.reverted;

      // Owner balance shouldn't change
      expect(await seiToken.balanceOf(owner.address)).to.equal(initialOwnerBalance);
    });
  });

  describe('Minting', function () {
    it('Should allow owner to mint new tokens', async function () {
      await seiToken.mint(addr1.address, 1000);
      expect(await seiToken.balanceOf(addr1.address)).to.equal(1000);
    });

    it('Should not allow non-owners to mint', async function () {
      await expect(seiToken.connect(addr1).mint(addr1.address, 1000)).to.be.reverted;
    });
  });
});
Run your tests with:
npx hardhat test

Deploying to Sei Testnet and Mainnet

Once you’ve tested your contracts, you can deploy them to the Sei testnet or mainnet. To deploy, you’ll need:
  1. SEI tokens in your wallet for gas fees
  2. Your private key in the .env file
Deploy to the testnet:
npx hardhat ignition deploy ignition/modules/deploy-sei-token.ts --network seitestnet
Deploy to the mainnet (only when you’re ready for production):
npx hardhat ignition deploy ignition/modules/deploy-sei-token.ts --network seimainnet