Building an Auto-verified Quote DApp on Morph

Azeez Abidoye - Sep 9 - - Dev Community

Why Verify Smart Contract?

Trustlessness is the primary purpose for developing smart contracts, which means that users should not have to trust third parties (such as developers and organisations) before engaging with a contract.
Smart contracts written in a high-level language (such as Solidity) compile to the same bytecode that is executed at the contract address.
In one word, source code verification validates that the published contract code matches the code running at the contract address on the Ethereum blockchain.
Understanding smart contract verification is key for web3 developers, but enabling it programmatically is much more important.
In this tutorial, I will guide you through the steps necessary to create and deploy smart contracts and decentralized apps on the Morph Holesky Testnet, as well as the practical technique for verifying the deployed contract on Morphscan.

What is Morph?

Morph is the first optimistic zkEVM Ethereum Layer 2 solution that is 100% EVM compatible. Building on Morph is just like building on Ethereum. If youโ€™re experienced in Ethereum development, you'll find your existing code, tooling, and dependencies are fully compatible with Morph.

Prerequisites ๐Ÿ“š

  • Node JS (v16 or later)
  • NPM (v6 or later)
  • Metamask
  • Testnet ethers
  • Etherscan API Key

Dev Tools ๐Ÿ› ๏ธ

  1. Yarn
npm install -g yarn
Enter fullscreen mode Exit fullscreen mode

The source code for this tutorial is located here:

GitHub logo azeezabidoye / quote-dapp

Decentralized App for creating quotes

React + Vite

This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.

Currently, two official plugins are available:





Bonus: How to create Etherscan API Key ๐ŸŽ

Smart contract verification can be performed manually on Etherscan, but it is advisable for developers to handle this programmatically. This can be achieved using an Etherscan API key, Hardhat plugins, and some custom logic.

  • Sign up/Sign in on etherscan.io
  • Select your profile at the top right corner and choose API Key from the options.

Image etherscan dashboard

  • Select Add button to generate a new API key

Image etherscan dashboard

  • Provide a name for your project and select Create New API Key

Image etherscan api creation form

Development ๐Ÿ‘ทโ€โ™‚๏ธ

Here is a step-by-step guideย to the needed technique for developing and verifying source code of decentralized applications on Morph blockchain.

Step #1: Create a new React project

npm create vite@latest quote-dapp --template react
Enter fullscreen mode Exit fullscreen mode
  • Navigate into the new project directory.
cd quote-dapp
Enter fullscreen mode Exit fullscreen mode

Step #2: Install Hardhat dependency using yarn.

yarn add hardhat
Enter fullscreen mode Exit fullscreen mode

Step #3: Initialize Hardhat for development.

yarn hardhat init
Enter fullscreen mode Exit fullscreen mode
  • Select Create a JavaScript project
  • Approve y "Yes" to gitignore

Step #4: Setup environment variables

  • Install an NPM module that loads environment variable from .env file
yarn add --dev dotenv
Enter fullscreen mode Exit fullscreen mode
  • Create a new file in the root directory named .env.
  • Create three (3) variables needed for configuration
PRIVATE_KEY="Insert-Private-Key"
ETHERSCAN_API_KEY="Insert-Etherscan-Api-Key"
CONTRACT_ADDRESS="Insert-Contract-Address"
Enter fullscreen mode Exit fullscreen mode

โœ๏ธ An example of the file is included in the source code above. Rename the .env_example to .env and populate the variables therein accordingly

Step #5: Setup Hardhat configuration for development

  • Navigate to hardhat.config.cjs file to setup Hardhat configuration
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();

const { PRIVATE_KEY, ETHERSCAN_API_KEY} = process.env;

module.exports = {
    solidity: "0.8.24",
    networks: {
        hardhat: { chainId: 1337 },
        morphTestnet: {
           url: "https://rpc-quicknode-holesky.morphl2.io",
           accounts: [PRIVATE_KEY],
           chainId: 2810,
           gasPrice: 2000000000,
        }
    }
};
Enter fullscreen mode Exit fullscreen mode

Step #6: Write the smart contract code

  • Create a new file named QuoteDapp.sol in the contracts directory
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract QuoteDapp {
    event QuoteDapp__AddNewQuote(string username, string content);

    error QuoteNotFound();

    struct Quote {
        string username;
        string content;
    }

    // Array of quotes
    Quote[] public quotes;

    function addQuote(string memory _username, string memory _content) public {
        // Add new quote to Quotes Array
        quotes.push(Quote(_username, _content));

        // Emit new event
        emit QuoteDapp__AddNewQuote(_username, _content);
    }

    function getQuote(uint _index) public view returns (string memory, string memory) {
        // Check for availability of quote
        if (_index > quotes.length) {
            revert QuoteNotFound();
        }

        // Create a new variable to store found quote
        Quote storage quote = quotes[_index];

        // Return quote if found 
        return (quote.username, quote.content);
    }

    function getQuoteCount() public view returns (uint) {
        return quotes.length;
    }
}
Enter fullscreen mode Exit fullscreen mode

Step #7: Compile smart contract code

  • Specify the directory where the ABI should be stored

module.exports = {
    solidity: "0.8.24",
        paths: {
           artifacts: "./src/artifacts",
        },
    networks: {
        // Code here...
    }
};
Enter fullscreen mode Exit fullscreen mode
  • Run the command below in the terminal
yarn hardhat compile
Enter fullscreen mode Exit fullscreen mode

โœ๏ธ It should be noted that specifying the intended route for the Application Binary Interface (ABI) has no effect on the integration with the frontend. However, it is best practice to keep the ABI in the src folder because that is where all of our frontend code will go.

Step #8: Configure DApp for deployment

  • Create a new folder for deployment scripts in the root directory
mkdir deploy
Enter fullscreen mode Exit fullscreen mode
  • Create a file for the deployment scripts in the deploy directory like this: 00-deploy-quote-dapp.cjs

  • Install an Hardhat plugin for the contract deployment

yarn add --dev hardhat-deploy
Enter fullscreen mode Exit fullscreen mode
  • Import hardhat-deploy package into Hardhat configuration
require("hardhat-deploy")
Enter fullscreen mode Exit fullscreen mode
  • Install another Hardhat plugin to override the @nomiclabs/hardhat-ethers package
yarn add --dev @nomiclabs/hardhat-ethers@npm:hardhat-deploy-ethers
Enter fullscreen mode Exit fullscreen mode
  • Set up a deployer account in the Hardhat configuration file
networks: {
     // Code Here
},
namedAccounts: {
     deployer: {
        default: 0,
     }
}
Enter fullscreen mode Exit fullscreen mode
  • The deploy script needed to be updated with the following code
module.exports = async ({ getNamedAccounts, deployments }) => {
  const { deploy, log } = deployments;
  const { deployer } = await getNamedAccounts();
  const args = [];
  await deploy("QuoteDapp", {
    contract: "QuoteDapp",
    args: args,
    from: deployer,
    log: true,
  });
};
module.exports.tags = ["QuoteDapp"];
Enter fullscreen mode Exit fullscreen mode
  • Run this command to deploy the contract on the Morph Holesky testnet
yarn hardhat deploy --network morphTestnet 
Enter fullscreen mode Exit fullscreen mode

โœ๏ธ Copy the address of the deployed contract. To update the environment variable initially created for it

Step #9: Update Hardhat configuration for automatic source code verification

  • Update your Hardhat configuration
require("@nomicfoundation/hardhat-verify");
Enter fullscreen mode Exit fullscreen mode
  • Add the following Etherscan configs to your hardhat.config.cjs file:
module.exports = {
    namedAccounts: {
        // Code here...
    },
    etherscan: {
      apiKey: ETHERSCAN_API_KEY,
      customChains: [
        {
          networks: "morphTestnet",
          chainId: 2810,
          urls: {
            apiURL: "https://explorer-api-holesky.morphl2.io/api?",
            browserURL: "https://explorer-holesky.morphl2.io/",
          },
        },
      ],
    },
  };
Enter fullscreen mode Exit fullscreen mode
  • Create a new folder for utilities in the root directory
mkdir utils
Enter fullscreen mode Exit fullscreen mode
  • Create a new file named verify.cjs in the utils directory for the verification logic

  • Update verify.cjs with the following code:

const { run } = require("hardhat");

const verify = async (contractAddress, args) => {
  console.log(`Contract verification in progress...`);

  try {
    await run("verify:verify", {
      address: contractAddress,
      constructorArguments: args,
    });
  } catch (e) {
    if (e.message.toLowerCase().includes("verify")) {
      console.log("Contract is already verified!");
    } else {
      console.log(e);
    }
  }
};

module.exports = { verify };
Enter fullscreen mode Exit fullscreen mode
  • Restructure the deploy script with the verification logic

Here is what your updated 00-deploy-quote-dapp.cjs should look like:

const { verify } = require("../utils/verify.cjs");

module.exports = async ({ getNamedAccounts, deployments }) => {
  const { deploy, log } = deployments;
  const { deployer } = await getNamedAccounts();
  const args = [];
  const quoteDapp = await deploy("QuoteDapp", {
    contract: "QuoteDapp",
    args: args,
    from: deployer,
    log: true, // Logs statements to console
  });

  await verify(quoteDapp.target, args);
  log("Contract verified successfully...");
  log("---------------------------------");
};
module.exports.tags = ["QuoteDapp"];

Enter fullscreen mode Exit fullscreen mode
  • To verify the contract...open the terminal and run the command below:
yarn hardhat verify [CONTRACT_ADDRESS] [CONSTRUCTOR_ARGS] --network sepolia
Enter fullscreen mode Exit fullscreen mode
  • In our case, the smart contract doesn't contain a function constructor, therefore we can skip the arguments

  • Run:

yarn hardhat verify [CONTRACT_ADDRESS] --network morphTestnet
Enter fullscreen mode Exit fullscreen mode

โœ๏ธ The result of the verification should look like this... search the provided link in the browser. Yours will definitely be different from mine.

Successfully submitted source code for contract
contracts/QuoteDapp.sol:QuoteDapp at 0xF1a2D02114Ea3F9fF1C25d491BDDC2Ba0554B220
for verification on the block explorer. Waiting for verification result...

Successfully verified contract QuoteDapp on the block explorer.
https://explorer-holesky.morphl2.io/address/0xF1a2D02114Ea3F9fF1C25d491BDDC2Ba0554B220#code
Enter fullscreen mode Exit fullscreen mode

Image description

Hurray! We've achieved our goal. We deployed and verified our smart contract successfully on Morph Holesky Testnet ๐ŸŽ‰
Congratulations if you made it this far. However, a few more steps are required to complete the development of a full-stack decentralized application.

Here are a few more steps we need to complete in order to construct a full-stack DApp:
โœ… Create unit tests with Mocha and Chai.
โœ… Create and connect UI components.
โœ… Interact with our Dapp.

Step #10: Write the unit tests

  • Install the Mocha and Chai dependencies for unit testing
yarn add --dev mocha chai@4.3.7
Enter fullscreen mode Exit fullscreen mode
  • Create a new file named quote-dapp-test.cjs in the test directory.

  • Update the quote-dapp-test.cjs file with the following code.

const { expect } = require("chai");

describe("QuoteDapp", function () {
  let QuoteDapp;
  let quoteDapp;

  before(async function () {
    // Deploy contract before running tests
    QuoteDapp = await ethers.getContractFactory("QuoteDapp");
    quoteDapp = await QuoteDapp.deploy();
    await quoteDapp.waitForDeployment();
  });

  describe("addQuote", function () {
    it("Should add a new quote and emit the correct event", async function () {
      const username = "Lechero";
      const content = "Hello! Feels good to know blockchain";

      // Add a new quote
      await expect(quoteDapp.addQuote(username, content))
        .to.emit(quoteDapp, "QuoteDapp__AddNewQuote")
        .withArgs(username, content);

      // Check if the quote count is 1
      expect(await quoteDapp.getQuoteCount()).to.equal(1);

      // Retrieve the quote and check its content
      const quote = await quoteDapp.getQuote(0);
      expect(quote[0]).to.equal(username);
      expect(quote[1]).to.equal(content);
    });
  });

  describe("getQuote", function () {
    it("Should revert if the quote index is out of bounds", async function () {
      await expect(quoteDapp.getQuote(999)).to.be.revertedWithCustomError(
        quoteDapp,
        "QuoteNotFound"
      );
    });

    it("Should return the correct quote", async function () {
      const username = "Isabella";
      const content = "Do more of Web3";

      // Add another quote
      await quoteDapp.addQuote(username, content);

      // Check if the quote count is 2
      expect(await quoteDapp.getQuoteCount()).to.equal(2);

      // Retrieve the new quote
      const quote = await quoteDapp.getQuote(1);
      expect(quote[0]).to.equal(username);
      expect(quote[1]).to.equal(content);
    });
  });

  describe("getQuoteCount", function () {
    it("Should return the correct number of quotes", async function () {
      expect(await quoteDapp.getQuoteCount()).to.equal(2);
    });
  });
});
Enter fullscreen mode Exit fullscreen mode
  • Here is the command to run the tests.
yarn hardhat test
Enter fullscreen mode Exit fullscreen mode
  • The result of the test should look like this.
 QuoteDapp
    addQuote
      โœ” Should add a new quote and emit the correct event
    getQuote
      โœ” Should revert if the quote index is out of bounds
      โœ” Should return the correct quote
    getQuoteCount
      โœ” Should return the correct number of quotes


  4 passing (492ms)

โœจ  Done in 3.28s.
Enter fullscreen mode Exit fullscreen mode

Additionally, we will utilize Solidity Coverage, a testing tool designed for developers to evaluate how code performs during execution. This helps to discover vulnerabilities that are not covered by the scope of the test.

  • Install solidity-coverage dependency.
yarn add --dev solidity-coverage
Enter fullscreen mode Exit fullscreen mode
  • Require the plugin in the hardhat.config.cjs file.
require("solidity-coverage");
Enter fullscreen mode Exit fullscreen mode
  • Run
yarn hardhat coverage
Enter fullscreen mode Exit fullscreen mode
  • Having completely tested the smart contract code, the final result of the test should look like this.
Version
=======
> solidity-coverage: v0.8.13

Instrumenting for coverage...
=============================

> QuoteDapp.sol

Compilation:
============

Compiled 1 Solidity file successfully (evm target: paris).

Network Info
============
> HardhatEVM: v2.22.10
> network:    hardhat



  QuoteDapp
    addQuote
      โœ” Should add a new quote and emit the correct event
    getQuote
      โœ” Should revert if the quote index is out of bounds
      โœ” Should return the correct quote
    getQuoteCount
      โœ” Should return the correct number of quotes


  4 passing (87ms)

----------------|----------|----------|----------|----------|----------------|
File            |  % Stmts | % Branch |  % Funcs |  % Lines |Uncovered Lines |
----------------|----------|----------|----------|----------|----------------|
 contracts/     |      100 |      100 |      100 |      100 |                |
  QuoteDapp.sol |      100 |      100 |      100 |      100 |                |
----------------|----------|----------|----------|----------|----------------|
All files       |      100 |      100 |      100 |      100 |                |
----------------|----------|----------|----------|----------|----------------|

> Istanbul reports written to ./coverage/ and ./coverage.json
Enter fullscreen mode Exit fullscreen mode

โœ๏ธ This command will automatically create a directory for the coverage report. The directory and file have been included in the .gitignore file. Endeavour to accomplish this manually if necessary.

Frontend:

The next step is to create the DApp's frontend. So, if you have a smart contract on the blockchain, the only way to interact with it is through the command line. That is why you must provide a frontend that enables users to interact with the smart contract.
Decentralized apps are built by integrating a smart contract and a frontend.
The frontend can be either a mobile app or a web app, with the latter being more popular. A DApp's frontend is almost identical to that of a web application, including HTML, CSS, Javascript, and, optionally, a frontend framework like React.
Reason, we started the project by creating a React application. There are two blockchain-specific issues to address. The first is the integration method, followed by the integration with the wallet. Here's an overview of the main purpose of the React application:

  1. Allow users create new quotes.
  2. Retrieve quotes according to their index.
  3. Ascertain the total number of published quotes.

Here are the few steps we need to take to achieve these goals listed above:

  1. Createย input fields and some local state so that users may make new posts.
  2. Create an input field and some local state to obtain the total number of quotes that were published.
  3. Allow the application to connect to the user's wallet for signing transactions.
  4. Create functions for reading and writing to the blockchain.

Key Takeaways

  • โœ… Create a Solidity smart contract.
  • โœ… Generate Etherscan API key.
  • โœ… Successfully deployed smart contract on Morph Holesky Testnet.
  • โœ… Automatic verification of smart contract source code.
  • โœ… Complete unit testing using solidity-coverage plugin.

Conclusion

This tutorial demonstrates how Morph facilitates the creation of decentralized applications. This is a comprehensive guide for developers and their teams to create creative solutions on the Morph L2 blockchain. I would be delighted to see you contribute to the concept of this project, and when you do, please leave a comment with a link to your code so that we can build together.

Join our Discord channel or follow us on Twitter to learn more.

. . . .
Terabox Video Player