# Deploy an ERC-20 Token on Humanity Testnet using Hardhat

This guide walks you through deploying an ERC-20 token smart contract on the **Humanity** network using [Hardhat](https://hardhat.org/) and [MetaMask](https://metamask.io/).

## **Prerequisites**

Before you begin, you need to install the following dependencies:

* Node.js v22.14.0 or later

> ⚠️ Note
>
> If you are on Windows, we strongly recommend using [WSL 2](https://learn.microsoft.com/en-us/windows/wsl/about) when following this guide.

You can find the code for this tutorial in the following repo. Remember to rename your `.env.example` to `.env` adding your private key and [Alchemy API Key](https://www.alchemy.com/)

{% embed url="<https://github.com/humanity-org/hp-human-only-airdrop-off-chain-contracts>" %}

It covers the complete development workflow, from initial setup and configuration to deployment and verification. You'll learn how to set up Hardhat for the Humanity testnet, configure your environment variables, write and test an ERC-20 smart contract with optional initial token transfer functionality, deploy it to the network, and verify your deployed contract on the Humanity block explorer.

Before deploying your first smart contract to the Humanity testnet, you'll need to configure your wallet and get some testnet tokens.

If you haven't already, install the [MetaMask](https://metamask.io/) browser extension and create a wallet.

> ⚠️
>
> Always use a dedicated **test wallet** for development — never use a wallet that holds real assets. Testnets are for experimentation, and it's safer to separate your development activities from your personal or mainnet funds.

To connect MetaMask to the Humanity testnet and get some testnet tokens, follow the steps here:

📚 [Working with Humanity Testnet](https://docs.humanity.org/testnet-overview/working-with-testnet)

## **Project Setup**

Now we have completed our wallet setup, let's first create the folder and basic structure of our project:

```bash
mkdir humanity-erc20-contract && cd humanity-erc20-contract
echo "node_modules \\\\n .env \\\\n cache \\\\n artifacts" >> .gitignore
echo "# humanity-erc20-contract" >> README.md
git init
git branch -M main
touch package.json
```

Add the following content to your `package.json`:

```json
{
  "name": "humanity-erc20-contract",
  "version": "1.0.0",
  "description": "Simple project for deploying an ERC-20 token (DemoToken) to Humanity testnet. The token can be used for backend-controlled airdrops with off-chain human verification via Humanity SDK/API.",
  "scripts": {
    "compile": "npx hardhat compile",
    "test": "npx hardhat test",
    "deploy": "npx hardhat run scripts/deploy.js --network humanity-testnet"
  },
  "devDependencies": {
    "@nomicfoundation/hardhat-toolbox": "^5.0.0",
    "@openzeppelin/contracts": "^5.0.0",
    "dotenv": "^16.5.0",
    "hardhat": "^2.22.17"
  },
  "dependencies": {
    "ethers": "^6.14.4"
  }
}
```

Install all the packages with:

```bash
npm install
```

Let's now create the folder structure for our project by creating `contracts`, `scripts` and `test` folders inside the `/humanity-erc20-contract` root folder. Also let's create an `.env` file and a `hardhat.config.js` file at the root level. Your folder structure should look like this:

```
humanity-erc20-contract/
├── contracts/
├── scripts/
├── test/
├── .env
├── .gitignore
├── hardhat.config.js
├── README.md
└── package.json
```

### **Environment Configuration**

Now inside our `.env` file add the following variables:

```bash
# Wallet Configuration
# This wallet will be the contract owner and receive initial tokens
PRIVATE_KEY=<YOUR_PRIVATE_KEY>

# Humanity testnet Configuration
# Get a free API key at <https://dashboard.alchemy.com/> (recommended)
# Or use the public endpoint: <https://humanity-testnet.g.alchemy.com/public>
HUMANITY_TESTNET=https://humanity-testnet.g.alchemy.com/v2/<YOUR_ALCHEMY_API_KEY>
HUMANITY_API_URL=https://humanity-testnet.explorer.alchemy.com/api
HUMANITY_BROWSER_URL=https://humanity-testnet.explorer.alchemy.com

# Optional: Initial Token Transfer on Deployment
# Send tokens to another address during contract deployment (saves gas)
# Example use cases:
# - Airdrop backend wallet: Send tokens directly to your distribution wallet
# - Treasury: Send to a multisig or treasury address
# - Team allocation: Send to team vesting contract
# Leave blank or use 0x0000000000000000000000000000000000000000 to skip
INITIAL_RECIPIENT=
# Amount in DEMO tokens (not wei). Example: 500000 = 500,000 DEMO tokens
INITIAL_AMOUNT=
```

For this tutorial we are using MetaMask and the private key associated with our test account holding our testnet tokens. In order to get the private key from your account follow these instructions:

{% embed url="<https://support.metamask.io/configure/accounts/how-to-export-an-accounts-private-key/>" %}

Replace `<YOUR_PRIVATE_KEY>` with the one you just copied from MetaMask.

In order to use the Alchemy API Key to connect with Humanity testnet network please follow this tutorial:

{% embed url="<https://www.alchemy.com/support/how-to-create-a-new-alchemy-api-key>" %}

Once we have our Alchemy API Key, let's add it to our `.env` file replacing `<YOUR_ALCHEMY_API_KEY>` with your own Key.

> ⚠️ Alchemy API Key
>
> If you do not have an [Alchemy](https://dashboard.alchemy.com/) account you can still use `https://humanity-testnet.g.alchemy.com/public` as your `HUMANITY_TESTNET` variable but you may encounter errors during deployment later on due to polling to public endpoint for block confirmations too often.

### **Hardhat Configuration**

Let's now configure Hardhat. Add the following to the `hardhat.config.js`:

```jsx
require('@nomicfoundation/hardhat-toolbox')
require('dotenv').config()

const PRIVATE_KEY = process.env.PRIVATE_KEY || '0xkey'

const HUMANITY_TESTNET = process.env.HUMANITY_TESTNET || ''
const HUMANITY_API_URL = process.env.HUMANITY_API_URL || ''
const HUMANITY_BROWSER_URL = process.env.HUMANITY_BROWSER_URL || ''

/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
  solidity: {
    compilers: [
      {
        version: '0.8.20',
      },
    ],
  },
  defaultNetwork: 'hardhat',
  networks: {
    hardhat: {},
    'humanity-testnet': {
      url: HUMANITY_TESTNET,
      accounts: PRIVATE_KEY !== '0xkey' ? [PRIVATE_KEY] : [],
    },
    localhost: {
      url: '<http://127.0.0.1:8545/>',
      chainId: 31337,
    },
  },
  etherscan: {
    apiKey: {
      'humanity-testnet': 'empty',
    },
    customChains: [
      {
        network: 'humanity-testnet',
        chainId: 7080969,
        urls: {
          apiURL: HUMANITY_API_URL,
          browserURL: HUMANITY_BROWSER_URL,
        },
      },
    ],
  },
}
```

### **Smart Contract**

Inside our `contracts` folder let's create our smart contract. Let's call it `DemoToken.sol`:

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

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

/**
 * @title DemoToken
 * @dev Simple ERC-20 token for demonstration purposes.
 *
 * Features:
 * - Initial supply of 1,000,000 DEMO tokens
 * - Owner can mint additional tokens if needed
 * - Optional initial transfer during deployment
 * - 18 decimals (standard)
 */
contract DemoToken is ERC20, Ownable {
    uint256 public constant INITIAL_SUPPLY = 1_000_000 * 10**18; // 1 million tokens

    /**
     * @dev Constructor mints initial supply to deployer
     * @param initialRecipient Optional address to send tokens to (use address(0) to send all to deployer)
     * @param initialAmount Optional amount to send to initialRecipient (only used if initialRecipient != address(0))
     */
    constructor(
        address initialRecipient,
        uint256 initialAmount
    ) ERC20("Demo Token", "DEMO") Ownable(msg.sender) {
        // Mint all tokens to deployer first
        _mint(msg.sender, INITIAL_SUPPLY);

        // If initialRecipient is specified and valid, transfer tokens
        if (initialRecipient != address(0) && initialAmount > 0) {
            require(initialAmount <= INITIAL_SUPPLY, "Initial amount exceeds supply");
            _transfer(msg.sender, initialRecipient, initialAmount);
        }
    }

    /**
     * @dev Allows owner to mint additional tokens
     * @param to Address to mint tokens to
     * @param amount Amount of tokens to mint (in wei, i.e., with 18 decimals)
     */
    function mint(address to, uint256 amount) external onlyOwner {
        _mint(to, amount);
    }

    /**
     * @dev Returns the number of decimals used for token amounts
     */
    function decimals() public pure override returns (uint8) {
        return 18;
    }
}
```

This contract includes:

* **ERC-20 standard implementation** using OpenZeppelin
* **Initial supply of 1,000,000 DEMO tokens** minted to deployer
* **Optional initial transfer** - Can send tokens to another address during deployment (gas efficient!)
* **Minting capability** - Owner can mint additional tokens as needed
* **Ownership control** - Owner can transfer ownership or renounce it

#### **Use Cases**

This token setup is ideal for:

* **Backend-controlled airdrops** - Send tokens directly from distribution wallet
* **Treasury management** - Send initial supply to multisig
* **Team allocations** - Transfer to vesting contracts
* **Staking rewards** - Fund reward pools

> The optional initial transfer feature saves approximately **\~21,000 gas** compared to separate deployment and transfer transactions!

### **Testing**

Inside our `test` folder let's create a test file to test our contract before deployment, so we are sure it works before sending it to the blockchain and spending any tokens. Let's call it `DemoToken.test.js`:

```jsx
const { expect } = require('chai')
const { ethers } = require('hardhat')

describe('DemoToken', function () {
  let demoToken, owner, addr1, addr2

  beforeEach(async function () {
    ;[owner, addr1, addr2] = await ethers.getSigners()

    const DemoToken = await ethers.getContractFactory('DemoToken')
    // Deploy without initial transfer (all tokens to deployer)
    demoToken = await DemoToken.deploy(ethers.ZeroAddress, 0)
    await demoToken.waitForDeployment()
  })

  describe('Deployment', function () {
    it('Should set the correct token name', async function () {
      expect(await demoToken.name()).to.equal('Demo Token')
    })

    it('Should set the correct token symbol', async function () {
      expect(await demoToken.symbol()).to.equal('DEMO')
    })

    it('Should set the correct decimals', async function () {
      expect(await demoToken.decimals()).to.equal(18)
    })

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

    it('Should mint initial supply to deployer', async function () {
      const expectedSupply = ethers.parseEther('1000000')
      expect(await demoToken.balanceOf(owner.address)).to.equal(expectedSupply)
    })

    it('Should have correct total supply', async function () {
      const expectedSupply = ethers.parseEther('1000000')
      expect(await demoToken.totalSupply()).to.equal(expectedSupply)
    })
  })

  describe('Initial Transfer on Deployment', function () {
    it('Should transfer tokens to initial recipient on deployment', async function () {
      const DemoToken = await ethers.getContractFactory('DemoToken')
      const transferAmount = ethers.parseEther('500000') // 500k tokens

      const newToken = await DemoToken.deploy(addr1.address, transferAmount)
      await newToken.waitForDeployment()

      // Check recipient received tokens
      expect(await newToken.balanceOf(addr1.address)).to.equal(transferAmount)

      // Check deployer has remaining tokens
      const expectedDeployerBalance = ethers.parseEther('500000')
      expect(await newToken.balanceOf(owner.address)).to.equal(expectedDeployerBalance)
    })

    it('Should send all tokens to deployer if no recipient specified', async function () {
      const DemoToken = await ethers.getContractFactory('DemoToken')
      const newToken = await DemoToken.deploy(ethers.ZeroAddress, 0)
      await newToken.waitForDeployment()

      const expectedSupply = ethers.parseEther('1000000')
      expect(await newToken.balanceOf(owner.address)).to.equal(expectedSupply)
      expect(await newToken.balanceOf(addr1.address)).to.equal(0)
    })

    it('Should ignore amount if recipient is zero address', async function () {
      const DemoToken = await ethers.getContractFactory('DemoToken')
      const newToken = await DemoToken.deploy(ethers.ZeroAddress, ethers.parseEther('100000'))
      await newToken.waitForDeployment()

      const expectedSupply = ethers.parseEther('1000000')
      expect(await newToken.balanceOf(owner.address)).to.equal(expectedSupply)
    })

    it('Should fail if initial amount exceeds supply', async function () {
      const DemoToken = await ethers.getContractFactory('DemoToken')
      const tooMuch = ethers.parseEther('2000000') // More than INITIAL_SUPPLY

      await expect(
        DemoToken.deploy(addr1.address, tooMuch)
      ).to.be.revertedWith('Initial amount exceeds supply')
    })
  })
})
```

### **Deployment Script**

Finally let's create a deployment script inside our `scripts` folder called `deploy.js`:

```jsx
const hre = require('hardhat')

async function main() {
  console.log('🚀 Deploying DemoToken to Humanity Testnet...\\n')
  console.log(`Network: ${hre.network.name}`)

  const [deployer] = await hre.ethers.getSigners()
  console.log('📝 Deploying with account:', deployer.address)

  const balance = await hre.ethers.provider.getBalance(deployer.address)
  console.log('💰 Account balance:', hre.ethers.formatEther(balance), 'tHP\\n')

  // ========================================
  // Deploy DemoToken
  // ========================================
  console.log('1️⃣  Deploying DemoToken...')

  // Optional: Set initial recipient and amount
  // To send tokens to backend wallet on deployment, set these values:
  const INITIAL_RECIPIENT = process.env.INITIAL_RECIPIENT || hre.ethers.ZeroAddress
  const INITIAL_AMOUNT = process.env.INITIAL_AMOUNT
    ? hre.ethers.parseEther(process.env.INITIAL_AMOUNT)
    : 0

  if (INITIAL_RECIPIENT !== hre.ethers.ZeroAddress && INITIAL_AMOUNT > 0) {
    console.log(`   Initial transfer: ${hre.ethers.formatEther(INITIAL_AMOUNT)} DEMO to ${INITIAL_RECIPIENT}`)
  }

  const DemoToken = await hre.ethers.getContractFactory('DemoToken')
  const demoToken = await DemoToken.deploy(INITIAL_RECIPIENT, INITIAL_AMOUNT)
  await demoToken.waitForDeployment()
  const demoTokenAddress = await demoToken.getAddress()
  console.log('✅ DemoToken deployed to:', demoTokenAddress)

  // ========================================
  // Verify token details
  // ========================================
  console.log('\\n2️⃣  Verifying token details...')
  const name = await demoToken.name()
  const symbol = await demoToken.symbol()
  const decimals = await demoToken.decimals()
  const totalSupply = await demoToken.totalSupply()
  const deployerBalance = await demoToken.balanceOf(deployer.address)

  console.log('   Token Name:', name)
  console.log('   Token Symbol:', symbol)
  console.log('   Decimals:', decimals)
  console.log('   Total Supply:', hre.ethers.formatEther(totalSupply), symbol)
  console.log('   Deployer Balance:', hre.ethers.formatEther(deployerBalance), symbol)

  // ========================================
  // Wait for confirmations before verification
  // ========================================
  if (hre.network.name !== 'hardhat' && hre.network.name !== 'localhost') {
    console.log('\\n3️⃣  Waiting for block confirmations...')
    try {
      await demoToken.deploymentTransaction().wait(5)
      console.log('✅ Deployment confirmed!')

      // Wait additional time for explorer indexing
      console.log('⏳ Waiting 30 seconds for explorer to index contract...')
      await new Promise((resolve) => setTimeout(resolve, 30000))
    } catch (error) {
      console.log(
        '⚠️  Warning: Could not wait for confirmations (rate limit), but deployment was successful!'
      )
    }

    // ========================================
    // Verify contract on block explorer
    // ========================================
    console.log('\\n4️⃣  Verifying contract on block explorer...')

    try {
      await hre.run('verify:verify', {
        address: demoTokenAddress,
        constructorArguments: [INITIAL_RECIPIENT, INITIAL_AMOUNT.toString()],
      })
      console.log('✅ DemoToken verified!')
    } catch (error) {
      if (error.message.includes('Already Verified')) {
        console.log('ℹ️  DemoToken already verified')
      } else {
        console.log('⚠️  DemoToken verification failed:', error.message)
        console.log('\\n   You can verify manually with:')
        console.log(
          `   npx hardhat verify --network humanity-testnet ${demoTokenAddress} "${INITIAL_RECIPIENT}" "${INITIAL_AMOUNT.toString()}"`
        )
      }
    }
  }

  // ========================================
  // Summary
  // ========================================
  console.log('\\n✨ Deployment complete!\\n')
  console.log('📋 Contract Information:\\n')
  console.log(`TOKEN_CONTRACT_ADDRESS=${demoTokenAddress}`)
  console.log(`TOKEN_NAME=${name}`)
  console.log(`TOKEN_SYMBOL=${symbol}`)
  console.log(`DEPLOYER_ADDRESS=${deployer.address}`)
  console.log(`INITIAL_SUPPLY=${hre.ethers.formatEther(totalSupply)}`)
  console.log('\\n📋 Network Information:\\n')
  console.log(`CHAIN_ID=7080969`)
  console.log(
    `RPC_URL=${process.env.HUMANITY_TESTNET || '<https://humanity-testnet.g.alchemy.com/public>'}`
  )
  console.log(
    `BLOCK_EXPLORER_URL=${process.env.HUMANITY_BROWSER_URL || '<https://humanity-testnet.explorer.alchemy.com>'}`
  )
  console.log(
    `\\n🔗 View on Explorer: ${process.env.HUMANITY_BROWSER_URL || '<https://humanity-testnet.explorer.alchemy.com>'}/address/${demoTokenAddress}\\n`
  )
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error)
    process.exit(1)
  })
```

### **Compile the Contract**

We are ready to compile our contract by running:

```bash
npm run compile
```

You should see output indicating successful compilation.

### **Run Tests**

Let's first test our contract by running:

```bash
npm run test
```

We should see something like this on our terminal:

```bash
DemoToken
  Deployment
    ✔ Should set the correct token name
    ✔ Should set the correct token symbol
    ✔ Should set the correct decimals
    ✔ Should set the right owner
    ✔ Should mint initial supply to deployer
    ✔ Should have correct total supply
  Initial Transfer on Deployment
    ✔ Should transfer tokens to initial recipient on deployment
    ✔ Should send all tokens to deployer if no recipient specified
    ✔ Should ignore amount if recipient is zero address
    ✔ Should fail if initial amount exceeds supply

10 passing (4s)
```

## **Deploy to Humanity Testnet**

Now we are finally ready to deploy our contract! You have two options:

### **Option A: Deploy with all tokens to deployer (default)**

```bash
npm run deploy
```

### **Option B: Deploy with initial token transfer (Recommended)**

This saves gas by combining deployment and token transfer in a single transaction.

First, add to your `.env` file:

```bash
INITIAL_RECIPIENT=0xYourBackendWalletAddress
INITIAL_AMOUNT=500000  # 500,000 DEMO tokens
```

Then deploy:

```bash
npm run deploy
```

You should see something like this in your terminal:

```bash
🚀 Deploying DemoToken to Humanity Testnet...

Network: humanity-testnet
📝 Deploying with account: 0xYourAddress...
💰 Account balance: 0.5 tHP

1️⃣  Deploying DemoToken...
   Initial transfer: 500000.0 DEMO to 0xRecipientAddress
✅ DemoToken deployed to: 0xABC123...

2️⃣  Verifying token details...
   Token Name: Demo Token
   Token Symbol: DEMO
   Decimals: 18
   Total Supply: 1000000.0 DEMO
   Deployer Balance: 500000.0 DEMO

3️⃣  Waiting for block confirmations...
✅ Deployment confirmed!
⏳ Waiting 30 seconds for explorer to index contract...

4️⃣  Verifying contract on block explorer...
✅ DemoToken verified!

✨ Deployment complete!

📋 Contract Information:

TOKEN_CONTRACT_ADDRESS=0xABC123...
TOKEN_NAME=Demo Token
TOKEN_SYMBOL=DEMO
DEPLOYER_ADDRESS=0xYourAddress...
INITIAL_SUPPLY=1000000.0

📋 Network Information:

CHAIN_ID=7080969
RPC_URL=https://humanity-testnet.g.alchemy.com/v2/...
BLOCK_EXPLORER_URL=https://humanity-testnet.explorer.alchemy.com

🔗 View on Explorer: <https://humanity-testnet.explorer.alchemy.com/address/0xABC123>...

```

> ⚠️
>
> If you encounter some errors, it's probably due to using the Alchemy public RPC endpoint. To avoid this, please get your own API key [here](https://dashboard.alchemy.com/). **In any case, even with errors, your contract should be deployed!**

## **View Your Contract on Explorer**

In order to see your deployed contract, visit [Humanity Testnet Explorer](https://humanity-testnet.explorer.alchemy.com/) and search for your contract address using the `TOKEN_CONTRACT_ADDRESS` from the previous step and click on the item that pops up.

It will take you to your contract page. If you click on the **Transactions** tab you will see the details from your deployment transaction.

<figure><img src="/files/cwiaCcouPNIUujxrKCEG" alt=""><figcaption></figcaption></figure>

### **Manual Verification (if needed)**

If automatic verification failed, you can verify manually using Hardhat:

**Without initial transfer:**

```bash
npx hardhat verify --network humanity-testnet <TOKEN_ADDRESS> "0x0000000000000000000000000000000000000000" "0"
```

**With initial transfer:**

```bash
npx hardhat verify --network humanity-testnet <TOKEN_ADDRESS> "<INITIAL_RECIPIENT>" "<INITIAL_AMOUNT_IN_WEI>"
```

Example:

```bash
npx hardhat verify --network humanity-testnet 0xABC123... "0xRecipientAddress" "500000000000000000000000"
```

You should see something similar in your terminal:

```bash
[WARNING] Network and explorer-specific api keys are deprecated...
Successfully submitted source code for contract
contracts/DemoToken.sol:DemoToken at <DEPLOYED_CONTRACT_ADDRESS>
for verification on the block explorer. Waiting for verification result...

Successfully verified contract DemoToken on the block explorer.
<https://humanity-testnet.explorer.alchemy.com/address/><DEPLOYED_CONTRACT_ADDRESS>#code
```

You should see our contract has been verified and able to see our Solidity code, compiler and ABI details rather than just our Bytecode.

<figure><img src="/files/82yeRghqtFQ0LdFYFEFE" alt=""><figcaption></figcaption></figure>

## **Interact with Your Contract**

Besides viewing the contract, verifying gives us the ability to test our functions directly from the explorer. If we click on the **Contract** tab and then **Write Contract** sub-tab, we will see our `DemoToken.sol` functions and we can start calling them directly from the explorer.

> ⚠️
>
> In order to interact with our contract we must connect to the Humanity website explorer by clicking on the **Connect** button located at the top right portion of the screen.

The explorer will give us also the option to simulate our transactions and will print the estimated gas for the real transaction.

<figure><img src="/files/6ZwCfMpDpEWxc0lavBDd" alt=""><figcaption></figcaption></figure>

Congratulations, you have successfully set up your development environment with Hardhat, connected your MetaMask wallet to the Humanity Testnet, obtained test tokens from the faucet, configured your project settings, and tested as well as deployed an ERC-20 token contract on the network.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.humanity.org/developer-guides-and-tutorials/on-chain-guides/deploy-an-erc-20-token-on-humanity-testnet-using-hardhat.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
