DEV Community

Cover image for The PetShop project, day 1: Creating a simple token with Hardhat
Mr. Z
Mr. Z

Posted on • Updated on

The PetShop project, day 1: Creating a simple token with Hardhat

I'm planning to build a simple web3 pet shop application from scratch. Every day I'll try to enhance the application and write down some notes. By doing so, hopefully I could teach myself about Ethereum, Solidity, Hardhat, and web3 development.

The project is hosted on GitHub: https://github.com/zhengzhong/petshop

The first day objectives

  • Set up a Hardhat project
  • Create and compile a simple token in Solidity
  • Create and run some tests for this contract
  • Deploy the contract to the local Hardhat network
  • Deploy the contract to the Goerli testnet

Set up a Hardhat project

I was basically following the Hardhat's tutorial for beginners. I was using node v16.9.1 and npm 8.7.0. Everything went on smoothly. So I'm summarizing the steps below without detailed explanation.

Firstly, create an npm project, install some dependencies, and create an empty hardhat.config.js file:

$ npm init
...

$ npm install --save-dev \
    hardhat \
    @nomicfoundation/hardhat-toolbox \
    @nomiclabs/hardhat-ethers \
    ethers \
    dotenv
...

$ npx hardhat
888    888                      888 888               888
888    888                      888 888               888
888    888                      888 888               888
8888888888  8888b.  888d888 .d88888 88888b.   8888b.  888888
888    888     "88b 888P"  d88" 888 888 "88b     "88b 888
888    888 .d888888 888    888  888 888  888 .d888888 888
888    888 888  888 888    Y88b 888 888  888 888  888 Y88b.
888    888 "Y888888 888     "Y88888 888  888 "Y888888  "Y888
👷 Welcome to Hardhat v2.9.9 👷‍
? What do you want to do? …
  Create a JavaScript project
  Create a TypeScript project
❯ Create an empty hardhat.config.js
  Quit
Enter fullscreen mode Exit fullscreen mode

Create a simple token

Following the Hardhat convention, I'll use the following directories:

  • contracts/ to hold all smart contracts (in Solidity)
  • test/ to hold all unit tests
  • scripts/ to hold automation scripts
  • tasks/ to hold extra Hardhat tasks

Create contracts/SimpleToken.sol that implements a simple (non-ERC20) token. The contract code is copied and slightly modified from Hardhat's tutorial.

// SPDX-License-Identifier: UNLICENSED

// Solidity files have to start with this pragma.
// It will be used by the Solidity compiler to validate its version.
pragma solidity ^0.8.16;

import "hardhat/console.sol";

// This is the main building block for smart contracts.
contract SimpleToken {
    // Some string type variables to identify the token.
    string public name = "My Simple Token";
    string public symbol = "MST";

    // The fixed amount of tokens, stored in an unsigned integer type variable.
    uint256 public totalSupply = 1_000_000;

    // An address type variable is used to store ethereum accounts.
    address public owner;

    // A mapping is a key/value map. Here we store each account's balance.
    mapping(address => uint256) balances;

    // The Transfer event is emitted when someone transfers some token(s) to someone else.
    // The event helps off-chain applications understand what happens within the contract.
    event Transfer(address indexed _from, address indexed _to, uint256 _value);

    /**
     * Contract initialization.
     */
    constructor() {
        // The totalSupply is assigned to the transaction sender, which is the
        // account that is deploying the contract.
        balances[msg.sender] = totalSupply;
        owner = msg.sender;
    }

    /**
     * A function to transfer tokens.
     *
     * The `external` modifier makes a function *only* callable from *outside*
     * the contract.
     */
    function transfer(address to, uint256 amount) external {
        // Check if the transaction sender has enough tokens.
        require(balances[msg.sender] >= amount, "Not enough tokens");
        if (msg.sender != to) {
            console.log("Transferring %s tokens: %s => %s", amount, msg.sender, to);
            balances[msg.sender] -= amount;
            balances[to] += amount;
            // Notify off-chain applications of the transfer.
            emit Transfer(msg.sender, to, amount);
        }
    }

    /**
     * Read only function to retrieve the token balance of a given account.
     *
     * The `view` modifier indicates that it doesn't modify the contract's
     * state, which allows us to call it without executing a transaction.
     */
    function balanceOf(address account) external view returns (uint256) {
        console.log("Querying balance of %s: %s", account, balances[account]);
        return balances[account];
    }
}
Enter fullscreen mode Exit fullscreen mode

Compile the contract:

$ npx hardhat compile
...
Compiled 2 Solidity files successfully
Enter fullscreen mode Exit fullscreen mode

On a successful compilation, Hardhat will generate the artifact, including the contract ABI and its bytecode, in the artifacts/ directory.

Test the simple token

We use ethers.js to interact with the contract we built and deployed on Ethereum. We use Mocha as our test runner.

Before writing the test, update hardhat.config.js to import hardhat-toolbox plugin. This plugin bundles all the commonly used packages and Hardhat plugins, including ethers.js that we need in our test.

require("@nomicfoundation/hardhat-toolbox");
Enter fullscreen mode Exit fullscreen mode

Then, create test/SimpleToken.js (this file, again, is copied and slightly modified from Hardhat's tutorial):

// This is an example test file. Hardhat will run every *.js file in `test/`,
// so feel free to add new ones.

// Hardhat tests are normally written with Mocha and Chai.

// Optional: `ethers` is injected in global scope automatically.
// TODO: Is this because of `require("@nomicfoundation/hardhat-toolbox")` in `hardhat.config.js`?
const { ethers } = require("hardhat");

// We import Chai to use its asserting functions here.
// See also: https://hardhat.org/hardhat-runner/plugins/nomicfoundation-hardhat-chai-matchers
const { expect } = require("chai");

// We use `loadFixture` to share common setups (or fixtures) between tests.
// Using this simplifies your tests and makes them run faster, by taking
// advantage of Hardhat Network's snapshot functionality.
const { loadFixture } = require("@nomicfoundation/hardhat-network-helpers");

// `describe` is a Mocha function that allows you to organize your tests.
// Having your tests organized makes debugging them easier. All Mocha
// functions are available in the global scope.
//
// `describe` receives the name of a section of your test suite, and a
// callback. The callback must define the tests of that section. This callback
// can't be an async function.
describe("SimpleToken contract", function () {

  // A fixture is a setup function that is run only the first time it's invoked.
  // On subsequent invocations, instead of re-running it, Hardhat will reset the state
  // of the network to what it was at the point after the fixture was initially executed.
  async function deploySimpleTokenFixture() {
    const SimpleToken = await ethers.getContractFactory("SimpleToken");
    const [owner, addr1, addr2] = await ethers.getSigners();

    // To deploy our contract, we just have to call Token.deploy() and await
    // its deployed() method, which happens onces its transaction has been mined.
    const simpleToken = await SimpleToken.deploy();
    await simpleToken.deployed();

    // Fixtures can return anything you consider useful for your tests.
    return { SimpleToken, simpleToken, owner, addr1, addr2 };
  }


  // You can nest describe calls to create subsections.
  describe("Deployment", function() {
    // `it` is another Mocha function. This is the one you use to define each
    // of your tests. It receives the test name, and a callback function.
    // If the callback function is async, Mocha will `await` it.
    it("should set the right owner", async function() {
      const { simpleToken, owner } = await loadFixture(deploySimpleTokenFixture);
      expect(await simpleToken.owner()).to.equal(owner.address);
    });

    it("should assign the total supply of tokens to the owner", async function () {
      const { simpleToken, owner } = await loadFixture(deploySimpleTokenFixture);
      const ownerBalance = await simpleToken.balanceOf(owner.address);
      expect(await simpleToken.totalSupply()).to.equal(ownerBalance);
    });

    it("should initialize the contract correctly", async function () {
      const { simpleToken } = await loadFixture(deploySimpleTokenFixture);
      expect(await simpleToken.name()).to.equal("My Simple Token");
      expect(await simpleToken.symbol()).to.equal("MST");
      expect(await simpleToken.totalSupply()).to.equal(1000000);
    });
  });


  describe("Transactions", function() {

    it("should transfer tokens between accounts", async function() {
      const { simpleToken, owner, addr1, addr2 } = await loadFixture(deploySimpleTokenFixture);

      expect(await simpleToken.balanceOf(addr1.address)).to.equal(0);
      expect(await simpleToken.balanceOf(addr2.address)).to.equal(0);

      // Transfer 50 tokens from owner to addr1.
      await expect(simpleToken.transfer(addr1.address, 50))
        .to.changeTokenBalances(simpleToken, [owner, addr1], [-50, 50]);

      // Transfer 50 tokens from addr1 to addr2.
      await expect(simpleToken.connect(addr1).transfer(addr2.address, 50))
        .to.changeTokenBalances(simpleToken, [addr1, addr2], [-50, +50]);
    });

    it("should emit Transfer events", async function () {
      const { simpleToken, owner, addr1, addr2 } = await loadFixture(deploySimpleTokenFixture);

      // Transfer 50 tokens from owner to addr1.
      await expect(simpleToken.transfer(addr1.address, 50))
        .to.emit(simpleToken, "Transfer").withArgs(owner.address, addr1.address, 50);

      // Transfer 50 tokens from addr1 to addr2.
      await expect(simpleToken.connect(addr1).transfer(addr2.address, 50))
        .to.emit(simpleToken, "Transfer").withArgs(addr1.address, addr2.address, 50);
    });

    it("should fail if sender doesn't have enough tokens", async function () {
      const { simpleToken, owner, addr1 } = await loadFixture(deploySimpleTokenFixture);
      const initialOwnerBalance = await simpleToken.balanceOf(owner.address);

      // Try to send 1 token from addr1 (0 tokens) to owner (1000 tokens).
      // `require` will evaluate false and revert the transaction.
      await expect(simpleToken.connect(addr1).transfer(owner.address, 1))
        .to.be.revertedWith("Not enough tokens");

      // Owner balance shouldn't have changed.
      expect(await simpleToken.balanceOf(owner.address)).to.equal(initialOwnerBalance);
    });

    it("should do nothing if transfer to self", async function () {
      const { simpleToken, owner } = await loadFixture(deploySimpleTokenFixture);
      await expect(simpleToken.transfer(owner.address, 1))
        .to.changeTokenBalances(simpleToken, [owner], [0]);
      await expect(simpleToken.transfer(owner.address, 1))
        .to.not.emit(simpleToken, "Transfer");
    });

  });

});
Enter fullscreen mode Exit fullscreen mode

Run the test:

$ npx hardhat test
  SimpleToken contract
    Deployment
      âś” should set the right owner (1213ms)
      âś” should assign the total supply of tokens to the owner (44ms)
      âś” should initialize the contract correctly (42ms)
    Transactions
      âś” should transfer tokens between accounts (196ms)
      âś” should emit Transfer events (45ms)
      âś” should fail if sender doesn't have enough tokens (60ms)
      âś” should do nothing if transfer to self (58ms)
7 passing (2s)
Enter fullscreen mode Exit fullscreen mode

Deploy to the local Hardhat network

We are now going to deploy the simple token contract to the local Hardhat network.

Create a deploy script in scripts/deploySimpleToken.js:

async function main() {
  // According to `hardhat.config.js` and the network we use,
  // this will give us an array of accounts.
  const [deployer] = await ethers.getSigners();
  console.log(`Deployer: ${deployer.address}`);
  console.log(`Deployer has a balance of: ${await deployer.getBalance()}`);

  const SimpleToken = await ethers.getContractFactory("SimpleToken");
  const simpleToken = await SimpleToken.deploy();
  await simpleToken.deployed();
  console.log(`Deployed SimpleToken at: ${simpleToken.address}`);
  console.log(`Deployer now has a balance of: ${await deployer.getBalance()}`);

  console.log(`Current block number: ${await ethers.provider.getBlockNumber()}`);
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });
Enter fullscreen mode Exit fullscreen mode

Firstly, try to deploy the contract to the local Hardhat network. To do so, we invoke the script without providing a --network argument:

$ npx hardhat run scripts/deploySimpleToken.js
...
Deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Deployer has a balance of: 10000000000000000000000
Deployed SimpleToken at: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Deployer now has a balance of: 9999998536390000000000
Current block number: 1
Enter fullscreen mode Exit fullscreen mode

Now our first contract is deployed and we get the contract address. Try to write another script to interact with the contract.

// Load the compiled contract artifact, following Hardhat's project layout.
const CONTRACT_ARTIFACT = require("../artifacts/contracts/SimpleToken.sol/SimpleToken.json");

// NOTE: We hard-code the address at which the contract is deployed.
// Depending on the network, make sure it has the correct value.
const CONTRACT_ADDRESS = "0x5FbDB2315678afecb367f032d93F642f64180aa3";

async function showBalances(accounts, tokenContract) {
  const formatRow = (cells, padChar) => {
    const paddings = [10, 42, 25, 10];
    return cells
      .map((cell, i) => `${cell}`.padEnd(paddings[i], padChar || ' '))
      .join(' | ');
  };

  const headings = ['Name', 'Address', 'ETH', 'Token (MST)'];
  console.log(formatRow(headings));
  console.log(formatRow(['', '', '', ''], '-'));
  for (const name of Object.keys(accounts)) {
    const account = accounts[name];
    const wei = await ethers.provider.getBalance(account.address);
    const eth = ethers.utils.formatEther(wei);
    const mst = await tokenContract.balanceOf(account.address);
    const cells = [name, account.address, eth, mst];
    console.log(formatRow(cells));
  }
  console.log(formatRow(['', '', '', ''], '-'));
}

async function measureTime(tag, asyncFn) {
  const startTime = new Date();
  const result = await asyncFn();
  const endTime = new Date();
  const seconds = (endTime - startTime) / 1000.0;
  console.log(`    * ${tag} - Used ${seconds.toFixed(2)} seconds`);
  return result;
}


async function transferTokens(from, to, amount, contract) {
  // Create and send a transaction to transfer tokens. Return a promise of `TransactionResponse`
  // which is published on the network but not necessarily mined.
  console.log(`Transferring ${amount} MST from ${from.address} to ${to.address}...`);
  const tx = await measureTime(
    'Creating tx',
    async () => contract.connect(from).transfer(to.address, amount)
  );

  // Wait for the transaction to be confirmed and included in the chain.
  console.log(`Waiting for tx ${tx.hash} to be mined...`);
  const receipt = await measureTime(
    'Waiting tx to be mined',
    () => tx.wait()
  );

  console.log(`Tx ${receipt.transactionHash} mined successfully.`);
  console.log(`    From / To     : ${receipt.from} => ${receipt.to}`);
  console.log(`    EIP-2718 Type : ${receipt.type}`);
  console.log(`    Status        : ${receipt.status}`);
  console.log(`    Block Number  : ${receipt.blockNumber}`);
  console.log(`    Block Hash    : ${receipt.blockHash}`);
  console.log(`    Gas Used      : ${receipt.gasUsed} (${receipt.effectiveGasPrice} wei / gas)`);

  // A successful transfer should emit a `Transfer` event.
  const event = receipt.events.find(event => event.event === 'Transfer');
  const [eventFrom, eventTo, eventValue] = event.args;
  console.log(`Found Transfer event: ${eventFrom} => ${eventTo}: ${eventValue}`);

  return receipt;
}

async function main() {
  console.log(`Current block number: ${await ethers.provider.getBlockNumber()}`);

  // Assume that we have at least 2 accounts.
  const [jason, orphee] = await ethers.getSigners();
  const accounts = {
    'Jason': jason,
    'Orphee': orphee,
  }

  // Construct the contract from its address and ABI.
  // NOTE: It's too hard to query the contract ABI from its address, so ABI must be provided.
  // See: https://github.com/ethers-io/ethers.js/issues/129
  const contract = new ethers.Contract(
    CONTRACT_ADDRESS,
    CONTRACT_ARTIFACT.abi,
    ethers.provider
  );
  const tokenName = await contract.name();
  const tokenSymbol = await contract.symbol();
  const owner = await contract.owner();
  console.log(`Constructed token contract: ${tokenName} (${tokenSymbol}), owner is ${owner}`);

  // Query the account balances.
  await showBalances(accounts, contract);

  // Transfer tokens from Jason to Orphée.
  console.log('Transferring tokens from Jason to Orphée...');
  await transferTokens(jason, orphee, 16, contract);
  await showBalances(accounts, contract);

  // Transfer tokens back from Jason to Orphée.
  console.log('Transferring tokens from Orphée to Jason...');
  await transferTokens(orphee, jason, 16, contract);
  await showBalances(accounts, contract);

  console.log(`Current block number: ${await ethers.provider.getBlockNumber()}`);
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });
Enter fullscreen mode Exit fullscreen mode

Note: It's troublesome to pass command-line arguments to scripts that run by hardhat run command, so we hard-code the contract address inside the script. Make sure the CONTRACT_ADDRESS constant is defined properly.

Let's try to run the script on the Hardhat local network:

$ npx hardhat run scripts/testSimpleToken.js
Current block number: 0
Error: call revert exception [ See: https://links.ethers.org/v5-errors-CALL_EXCEPTION ] (method="name()", data="0x", errorArgs=null, errorName=null, errorSignature=null, reason=null, code=CALL_EXCEPTION, version=abi/5.7.0)
    at ... {
  reason: null,
  code: 'CALL_EXCEPTION',
  method: 'name()',
  data: '0x',
  errorArgs: null,
  errorName: null,
  errorSignature: null,
  address: '0x5FbDB2315678afecb367f032d93F642f64180aa3',
  args: [],
  transaction: {
    data: '0x06fdde03',
    to: '0x5FbDB2315678afecb367f032d93F642f64180aa3'
  }
}
Enter fullscreen mode Exit fullscreen mode

Oops! It failed at calling the contract method name(). Notice that the current block number is 0! This implies that, while our script is running, the network is empty and the contract we want to interact is NOT deployed.

This is because the local Hardhat network is running as an in-process node, which is started when we invoke the hardhat run command, and is stopped when the command ends. Everything on it, including our contract, could not survive through two hardhat run commands.

To fix this, we will start the Hardhat network as a standalone daemon. Open a new terminal and run:

$ npx hardhat node
Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:8545/
Accounts
========
WARNING: These accounts, and their private keys, are publicly known.
Any funds sent to them on Mainnet or any other live network WILL BE LOST.
Account #0: 0xf39F**** (10000 ETH)
Private Key: 0xac09****
Account #1: 0x7099**** (10000 ETH)
Private Key: 0x59c6****
...
Enter fullscreen mode Exit fullscreen mode

Keep it running. Then we switch to another terminal and deploy our contract again on this network (named localhost):

$ npx hardhat run scripts/deploySimpleToken.js --network localhost
Deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Deployer has a balance of: 10000000000000000000000
Deployed SimpleToken at: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Deployer now has a balance of: 9999999316982000000000
Current block number: 1
Enter fullscreen mode Exit fullscreen mode

Make sure the CONTRACT_ADDRESS constant in our test file matches the contract address. Then run the test again:

$ npx hardhat run scripts/testSimpleToken.js --network localhost
Current block number: 1
Constructed token contract: My Simple Token (MST), owner is 0xf39F****
Name    | Address    | ETH                     | Token (MST)
------- | ---------- | ----------------------- | ----------
Jason   | 0xf39F**** | 9999.999316982          | 1000000
Orphee  | 0x7099**** | 10000.0                 | 0
------- | ---------- | ----------------------- | ----------
Transferring tokens from Jason to Orphée...
Transferring 16 MST from 0xf39F****... to 0x7099****...
    * Creating tx - Used 0.09 seconds
Waiting for tx 0xcbb4**** to be mined...
    * Waiting tx to be mined - Used 0.01 seconds
Tx 0xcbb4**** mined successfully.
    From / To     : 0xf39F**** => 0x5FbD****
    EIP-2718 Type : 2
    Status        : 1
    Block Number  : 2
    Block Hash    : 0x7ab8****
    Gas Used      : 56004 (771316817 wei / gas)
Found Transfer event: 0xf39F**** => 0x7099****: 16
Name    | Address    | ETH                     | Token (MST)
------- | ---------- | ----------------------- | ----------
Jason   | 0xf39F**** | 9999.999273785172980732 | 999984
Orphee  | 0x7099**** | 10000.0                 | 16
------- | ---------- | ----------------------- | ----------
Transferring tokens from Orphée to Jason...
Transferring 16 MST from 0x7099**** to 0xf39F****...
    * Creating tx - Used 0.22 seconds
Waiting for tx 0x1c87**** to be mined...
    * Waiting tx to be mined - Used 0.01 seconds
Tx 0x1c87**** mined successfully.
    From / To     : 0x7099**** => 0x5FbD****
    EIP-2718 Type : 2
    Status        : 1
    Block Number  : 3
    Block Hash    : 0x7cc0****
    Gas Used      : 34104 (675262189 wei / gas)
Found Transfer event: 0x7099**** => 0xf39F****: 16
Name    | Address    | ETH                     | Token (MST)
------- | ---------- | ----------------------- | ----------
Jason   | 0xf39F**** | 9999.999273785172980732 | 1000000
Orphee  | 0x7099**** | 9999.999976970858306344 | 0
------- | ---------- | ----------------------- | ----------
Current block number: 3
Enter fullscreen mode Exit fullscreen mode

This time, everything looks good.

Configure Goerli testnet in Hardhat

We want to deploy this contract to the Goerli testnet. As of this writing, Goerli is a proof-of-authority testnet recommended for application developers. Before we can do that, we need to do some preparations:

  • Export private keys of some test accounts from MetaMask
  • Create an API key for Alchemy
  • Create an API key for Etherscan
  • Configure Goerli testnet in hardhat.config.js
  • Request some ether from the Goerli faucet

I installed MetaMask as a Chrome plugin. Click the MetaMask plugin icon. Switch the network to "Goerli Test Network". Make sure we have at least 2 accounts in MetaMask. If not, create (or import) some accounts.

Image description

Next, export the private keys of the 2 accounts from MetaMask.

Note: Do NOT disclose your private keys to anyone!

Go to Alchemy, sign up, create a new app in its dashboard, and grab the API key. Alchemy provides blockchain APIs and Ethereum node infrastructure. We will connect to the Goerli testnet via a node (and a JSON-RPC server running on the node) provided by Alchemy.

Image description

Go to Etherscan, sign up, and create an API key token. Etherscan is a blockchain explorer allowing us to view contracts, transactions, and everything on the Ethereum networks. It also provide a service of source code verification for smart contracts. We will use Etherscan to verify our contract.

Image description

Now we have 2 private keys for our accounts (I named these 2 accounts Jason and Orphée), an Alchemy API key, and an Etherscan API key. We will put these secrets into a .env file, keep this file out of source control, and use dotenv to load them into process.env. Our .env file looks like this:

# My MetaMask test accounts:
#
# | Alias  | Address                                    |
# +--------+--------------------------------------------+
# | Jason  | 0xCc4c8184CC4A5A03babC13D832cEE3E41bE92d08 |
# | Orphée | 0xBf1381Fc04fe3e500e0fFD8adb985b9b8Fe95437 |
#
GOERLI_PRIVATE_KEY_JASON  = "****"
GOERLI_PRIVATE_KEY_ORPHEE = "****"
# Grabbed from: https://dashboard.alchemyapi.io/
ALCHEMY_API_KEY = "****"
# Grabbed from: https://etherscan.io/myapikey
ETHERSCAN_API_KEY = "****"
Enter fullscreen mode Exit fullscreen mode

Update hardhat.config.js like the following:

require("@nomicfoundation/hardhat-toolbox");
require("@nomiclabs/hardhat-etherscan");

// Load secrets from `.env` file into `process.env`.
require('dotenv').config();

const {
  GOERLI_PRIVATE_KEY_JASON,
  GOERLI_PRIVATE_KEY_ORPHEE,
  ALCHEMY_API_KEY,
  ETHERSCAN_API_KEY,
} = process.env;

/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
  solidity: "0.8.16",
  networks: {
    goerli: {
      url: `https://eth-goerli.alchemyapi.io/v2/${ALCHEMY_API_KEY}`,
      accounts: [GOERLI_PRIVATE_KEY_JASON, GOERLI_PRIVATE_KEY_ORPHEE],
    },
  },
  etherscan: {
    apiKey: ETHERSCAN_API_KEY,
  },
};
Enter fullscreen mode Exit fullscreen mode

Finally, request some test ether from the Goerli faucet. To send transactions to the Goerli testnet, we will need some ether to pay for the gas fee.

Deploy to Goerli testnet

Now we can re-run the deployment script on the Goerli testnet:

$ npx hardhat run scripts/deploySimpleToken.js --network goerli
Deployer: 0xCc4c8184CC4A5A03babC13D832cEE3E41bE92d08
Deployer has a balance of: 736171761944897241
Deployed SimpleToken at: 0xf7Df331661C4CFbbBb85Fca9c7b7C3657C50D783
Deployer now has a balance of: 736045099082540281
Current block number: 7464064
Enter fullscreen mode Exit fullscreen mode

Nice! Now our contract is live on Goerli testnet, and we get its address. We can view our contract via Etherscan.

Let's try to run the other script to transfer some tokens. Update CONTRACT_ADDRESS in scripts/testSimpleToken.js and run:

$ npx hardhat run scripts/testSimpleToken.js --network goerli
Current block number: 7464081
Constructed token contract: My Simple Token (MST), owner is 0xCc4c****
Name       | Address    | ETH                   | Token (MST)
---------- | ---------- | --------------------- | ----------
Jason      | 0xCc4c**** | 0.736045099082540281  | 1000000
Orphee     | 0xBf13**** | 0.0                   | 0
---------- | ---------- | --------------------- | ----------
Transferring tokens from Jason to Orphée...
Transferring 16 MST from 0xCc4c**** to 0xBf13****...
    * Creating tx - Used 1.09 seconds
Waiting for tx 0x4273**** to be mined...
    * Waiting tx to be mined - Used 16.63 seconds
Tx 0x4273**** mined successfully.
    From / To     : 0xCc4c**** => 0xf7Df****
    EIP-2718 Type : 2
    Status        : 1
    Block Number  : 7464083
    Block Hash    : 0x7d4c****
    Gas Used      : 56004 (1003264582 wei / gas)
Found Transfer event: 0xCc4c**** => 0xBf13****: 16
Name       | Address    | ETH                   | Token (MST)
---------- | ---------- | --------------------- | ----------
Jason      | 0xCc4c**** | 0.735988912252889953  | 999984
Orphee     | 0xBf13**** | 0.0                   | 16
---------- | ---------- | --------------------- | ----------
Transferring tokens from Orphée to Jason...
Transferring 16 MST from 0xBf13**** to 0xCc4c****...
Error: insufficient funds for intrinsic transaction cost ...
Enter fullscreen mode Exit fullscreen mode

Everything worked fine except for the last transfer: It failed with an INSUFFICIENT_FUNDS error. Actually this makes sense because my second test account (Orphée) has no test ether to pay for the gas fee.

Verify the contract

We'll use the hardhat-etherscan plugin to verify our contract. By verifying the contract, we upload its source code to match the contract bytecode deployed at a given address. Once verified, a smart contract's source code becomes publicly available and verifiable. This creates transparency and trust. It also allows Etherscan to expose external methods of our contract via its web UI.

$ npx hardhat verify 0xf7Df331661C4CFbbBb85Fca9c7b7C3657C50D783 --network goerli
Nothing to compile
Successfully submitted source code for contract
contracts/SimpleToken.sol:SimpleToken at 0xf7Df331661C4CFbbBb85Fca9c7b7C3657C50D783
for verification on the block explorer. Waiting for verification result...
Successfully verified contract SimpleToken on Etherscan.
https://goerli.etherscan.io/address/0xf7Df331661C4CFbbBb85Fca9c7b7C3657C50D783#code
Enter fullscreen mode Exit fullscreen mode

Now go back to Etherscan and view our contract. This time, we can see all the external methods of our contract and invoke them via the "Read Contract" and "Write Contract" tabs:

Conclusion

That's enough for the first day. Source code from the first day can be found at: https://github.com/zhengzhong/petshop/releases/tag/day01

Top comments (1)

Collapse
 
shelleyolivia profile image
0xShelley

Hey Mr. Z - solid writing, what an awesome 5 parter tutorial! Had some thoughts on this tutorial, do you have a Twitter DM or email that I could reach you out to share? Way to crush this!