Writing Your First Smart Contract On Solidity

Writing Your First Smart Contract On Solidity

This article is an introduction to setting up Solidity and writing your first smart contract based on it

In previous articles of our series, we went through important concepts around blockchain, web3, and the eco-system around it. In this article, let's take a serious look at what a blockchain developer does. There are two different types of blockchain developers:

Core Blockchain Developer: The Core Blockchain Developer creates the foundation upon which others will then build, i.e., they design the security and the architecture of the proposed Blockchain system.

Blockchain Software Developers: These developers use the core web architecture built by the developer to create apps, specifically the decentralized (dapps) and its web varieties.

In order to get started with the blockchain development, developers have various framework options such as Ethereum, Hyperledger, R3, Ripple, and EOS allowing developers to develop and host applications on the blockchain. Out of all the available options, Ethereum is the most popular, due to it being open source and having a huge ecosystem of platforms and tools around. In this article, we will be using Ethereum to write our first smart contract and parallelly use solidity for developing smart contracts that run on Ethereum.

Prerequisites

The article assumes that you have a fundamental understanding of blockchain, smart contracts, and a very basic understanding of solidity. However, if you have even never heard of Solidity and smart contracts before, the article aims to inspire you to dig deeper into the world of smart contracts and write your own contracts. Before we start the development, let's get the following tools installed.

1. Visual Studio Code.

Visual Studio Marketplace has a solidity plugin which provides compiler management, syntax highlighting, snippet support, quick compilation, and code completion.

2. NodeJS

To check if Node.js and NPM are already installed, input the following commands in the terminal:

node --version
npm --version

Go to Node.js if you need to install it.

Setting up the project.

Now that we have the above tools installed, let's get started with setting up our smart contract project and installing the necessary dependencies.

Initialise an npm project.

Solidity has extensive support for JavaScript and hence we will run it on Node using an npm project. It will be basically like any other web project.

  • We will start by creating a folder for our first project and name it as counter-smart-contract. We will be writing a smart contract, with a single function that will increment the stored value to create a persistent counter using the command given below:
mdkir counter-smart-contract
  • Now that our folder is ready, let's initialize a new npm project using the following snippet:
cd counter-smart-contract
npm init -y
  • The above command creates a basic package.json for us that looks as shown below:
  "name": "counter-smart-contract",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

Development environment

We will use Hardhat for a local Solidity development environment. It comes with an npm package and various tools that enable us to develop smoothly with Solidity. It allows us to deploy our contracts, run tests and debug Solidity code without dealing with live environments.

  • Let's use the initialize command using which we can execute with npx. as shown below:
npx hardhat
  • As you run the command, you will be presented with a prompt which looks like this:
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.1 

 What do you want to do? · Create a basic sample project
 Hardhat project root: · /Users/pushkarkumar/Desktop/projects/counter-smart-contract
 Do you want to add a .gitignore? (Y/n) · y
 Help us improve Hardhat with anonymous crash reports & basic usage data? (Y/n) · true
 Do you want to install this sample project's dependencies with npm (hardhat @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers)? (Y/n) · y
  • As shown, the Hardhat init command creates a config file, updates package.json and creates a base folder for our solidity project. In case the init command does not update the package.json, execute the same manually using the following snippet:
npm install --save-dev hardhat

Folder structure

The three essential folders of our projects are as follows:

contracts: This is where our Solidity code will go to.

scripts: These include task runners with a development environment.

test: This command comes up with a test framework that allows us to test contracts locally by creating some kind of more natural language for test cases.

Development dependencies

ethereum-waffle: Waffle is a Solidity testing library. It allows us to write tests for your contracts with JavaScript.

chai: Chai is a javascript assertion library and provides functions like expect.

ethers: This is a popular Ethereum client library. It allows us to interface with blockchains that implement the Ethereum API.

hardhat-waffle: It's a Hardhat plugin that enables waffle support.

hardhat-ethers: It's a Hardhat plugin that enables ethers support.

Hardhat config

The hardhat.config.js file configures the plugins to be used. The plugins to be used needs to be implemented within the config file as follows :

require("@nomiclabs/hardhat-waffle");

// This is a sample Hardhat task. To learn how to create your own go to
// https://hardhat.org/guides/create-task.html
task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
  const accounts = await hre.ethers.getSigners();

  for (const account of accounts) {
    console.log(account.address);
  }
});

// You need to export an object to set up your config
// Go to https://hardhat.org/config/ to learn more

/**
 * @type import('hardhat/config').HardhatUserConfig
 */
module.exports = {
  solidity: "0.8.4",
};

Updating the scripts

In our project we will be writing a contract, testing it, and then deploying the same to a local testnet. Let's update the commands in the script to build and test our contract respectively in package.json as shown:

    "build": "hardhat compile",
    "test:waffle": "hardhat test",
    "test": "hardhat coverage"

build: It tells hardhat to take our Solidity files from the contracts folder and run them through the Solidity compiler.

test:waffle: This command invokes Waffle to test our contracts.

test: It not only invokes Waffle but it also additionally generates a coverage report for us.

The modified package.json looks something like this:

  "name": "counter-smart-contract",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "hardhat compile",
    "test:waffle": "hardhat test",
    "test": "hardhat coverage"  
  },
  "keywords": [],
  "author": "Pushkar kumar",
  "license": "ISC",
  "devDependencies": {
    "@nomiclabs/hardhat-ethers": "^2.0.5",
    "@nomiclabs/hardhat-waffle": "^2.0.3",
    "chai": "^4.3.6",
    "ethereum-waffle": "^3.4.0",
    "ethers": "^5.6.1",
    "hardhat": "^2.9.1"
  }
}

Implementing our first smart contract

To get started we will keep our contract very simple. It is going to be a counter contract with a method to increment the persistent storage count values. We will cover complex contracts in the future with various case studies.

Solidity code

Our code goes into contracts/Counter.sol. Let's go over the below code step by step:

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "hardhat/console.sol";

contract Counter {
    uint256 count; // persistent contract storage

    constructor(uint256 _count) {
        count = _count;
    }

    function increment() public {
        count += 1;
        console.log("The count value after increment", count);
    }

    function getCount() public view returns (uint256) {
        console.log("The count value on getCount", count);
        return count;
    }
}

Use the following code to implement the License header:

//SPDX-License-Identifier: MIT

License builds trust with the community and hence it is recommended to add a license to our contracts.

Use the following code to implement the Compiler Directive:

pragma solidity ^0.8.0;

pragma is a keyword that states which specific source file needs Solidity compiler version of 0.80 or more and doesn't work with compiler versions below that. This is only local to the source file, hence if we import a file that requires a compiler version less than 0.80, it does not affect the file. However one would get compiler errors at some point as shown:

contract Counter {
}

Similar to javascript classes, solidity has a contract. A contract holds states and has methods as shown below:

uint256 count; // persistent contract storage

This is a public unit count of the contract. A typical format for declaring variable in solidity is [type] [visibility modifier] [identifier]. Use the following code snippet to proceed:

constructor(uint256 _count) {
        count = _count;
    }

The constructor is the function that is invoked when the smart contract is created. It is basically like an object constructor and can be implemented using the following code:

    function increment() public {
        count += 1;
        console.log("The count value after increment", count);
    }

The increment function requires a transaction because it modifies the property name of the contract. The public keyword marks that this function is accessible from the outside. Transactions are function that writes to the blockchain costs money to be executed! Use the following snippet to proceed:

function getCount() public view returns (uint256) {
        console.log("The count value on getCount", count);
        return count;
    }

getCount on the other hand is a getter method. It does not need a transaction because it only reads data. The public keyword makes this function accessible from the outside. Read-only methods do not cost anything.

Compiling the contract

Now that our contract is ready, let's compile the contract. In order to compile the contract, run the following command:

npm run build

The output should look like this:

➜  counter-smart-contract npm run build

> counter-smart-contract@1.0.0 build /Users/pushkarkumar/Desktop/projects/counter-smart-contract
> hardhat compile

Downloading compiler 0.8.4
Compiled 2 Solidity files successfully

As a result of running this code, a new folder artifacts gets created. Within these artifacts, you will find two more folders: contracts and hardhat. On compiling the contracts an abi file gets created at artifacts/contracts/Counter.sol/Counter.json. The abifile defines the standard way of interacting with smart contracts in the Ethereum ecosystem.

Testing smart contracts

We will be using Waffle and chai to write tests that verify our contract. Everything on the blockchain is immutable, even our code, hence test cases become even more important.

Waffle enables us to test our smart contracts not with Solidity itself but with JavaScript, all in an environment you are probably already comfortable with. Let's write our test case under test/Counter.test.js as shown:

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

describe("Counter", function () {
  it("Should return 1 once it's changed", async function () {
    const Counter = await ethers.getContractFactory("Counter");
    const counter = await Counter.deploy(0);
    await counter.deployed();

    expect(await counter.getCount()).to.equal(0);

    const incrementTx = await counter.increment();

    // wait until the transaction is mined
    await incrementTx.wait();

    expect(await counter.getCount()).to.equal(1);
  });
});

Let's go through the test case and understand the flow of what's happening here:

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

The statements given above are import statements and it loads from everywhere except chai and ethers from hardhat. Run the following code:

describe("Counter", () => {
});

These are logical blocks that create a structure for our tests that helps us to identify issues when reported on your terminal. Use the following code to proceed:

it("Should return 1 once it's changed", async function () {
  });

You will notice that you will recieve a logical block as well and together with the description, it creates some kind of more natural language and marks an inner block as shown below:

const Counter = await ethers.getContractFactory("Counter");

The ethers are injected globally in our tests through @nomiclabs/hardhat-ethers which was configured earlier. It basically instructs ethers to look up your smart contract and create a factory so you can later instantiate it as shown below:

const counter = await Counter.deploy(0);

The above line calls the constructor of our smart contract and the code block within the constructor is now executed as shown below:

const incrementTx = await counter.increment();

The incrementTx calls the increment method of our contract which updates the state of the count variable as shown below:

expect(await counter.getCount()).to.equal(1);

This line checks if one is calling the increment method worked perfectly or not. In our case, the value of count should be increment from 0 to 1. It's time to run the test and check if it worked fine. Run the following command:

npm run test

The output should look like this:

➜  counter-smart-contract npm run test

> counter-smart-contract@1.0.0 test /Users/pushkarkumar/Desktop/projects/counter-smart-contract
> hardhat coverage


Version
=======
> solidity-coverage: v0.7.20

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

> Counter.sol

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

Nothing to compile

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



  Counter
The count value on getCount 0
The count value after increment 1
The count value on getCount 1
    ✔ Should return 1 once it's changed (170ms)


  1 passing (179ms)

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

> Istanbul reports written to ./coverage/ and ./coverage.json

Congratulations!!! We achieved 100% of the test coverage.

Deployment

Solidity runs within Ethereum virtual machines that are part of the Ethereum nodes. Our smart contracts need to be deployed to the blockchain to be usable. We will be using Hardhat's task runner to deploy our smart contract. These are JavaScript files that are executed on demand.

Let's create our scripts at scripts/counter.js as shown below:

// We require the Hardhat Runtime Environment explicitly here. This is optional
// but useful for running the script in a standalone fashion through `node <script>`.
//
// When running the script with `npx hardhat run <script>` you'll find the Hardhat
// Runtime Environment's members available in the global scope.
const hre = require("hardhat");

async function main() {
  // Hardhat always runs the compile task when running scripts with its command
  // line interface.
  //
  // If this script is run directly using `node` you may want to call compile
  // manually to make sure everything is compiled
  // await hre.run('compile');

  // We get the contract to deploy
  const Counter = await hre.ethers.getContractFactory("Counter");
  const counter = await Counter.deploy(0);

  await counter.deployed();

  console.log("Counter deployed to:", counter.address);
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

Hardhat comes with a local testnet where one can test their contracts and play around with them locally. By adding the following script to your package.json it executes your script and defines the target network as localhost. Run the following code:

"deploy:local": "hardhat run --network localhost scripts/counter.js"

Another script to configure the local testnet so that you have somewhere to deploy your contract is given below:

"local-testnet": "hardhat node",

You updated package.json should look like this:

{
  "name": "counter-smart-contract",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "hardhat compile",
    "test:waffle": "hardhat test",
    "test": "hardhat coverage",
    "local-testnet": "hardhat node",
    "deploy:local": "hardhat run --network localhost scripts/counter.js"
  },
  "keywords": [],
  "author": "Pushkar Kumar",
  "license": "ISC",
  "devDependencies": {
    "@nomiclabs/hardhat-ethers": "^2.0.5",
    "@nomiclabs/hardhat-waffle": "^2.0.3",
    "chai": "^4.3.6",
    "ethereum-waffle": "^3.4.0",
    "ethers": "^5.6.1",
    "hardhat": "^2.9.1",
    "solidity-coverage": "^0.7.20"
  }
}

Open a new terminal and execute this command:

npm run local-testnet

The output which you receive should look like this:

  counter-smart-contract npm run local-testnet


> counter-smart-contract@1.0.0 local-testnet /Users/pushkarkumar/Desktop/projects/counter-smart-contract
> 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: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 (10000 ETH)
Private Key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

Account #1: 0x70997970c51812dc3a010c7d01b50e0d17dc79c8 (10000 ETH)
Private Key: 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d

Account #2: 0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc (10000 ETH)
Private Key: 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a

Account #3: 0x90f79bf6eb2c4f870365e785982e1f101e93b906 (10000 ETH)
Private Key: 0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6

Account #4: 0x15d34aaf54267db7d7c367839aaf71a00a2c6a65 (10000 ETH)
Private Key: 0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a

Account #5: 0x9965507d1a55bcc2695c58ba16fb37d819b0a4dc (10000 ETH)
Private Key: 0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba

Account #6: 0x976ea74026e726554db657fa54763abd0c3a0aa9 (10000 ETH)
Private Key: 0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e

Account #7: 0x14dc79964da2c08b23698b3d3cc7ca32193d9955 (10000 ETH)
Private Key: 0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356

Account #8: 0x23618e81e3f5cdf7f54c3d65f7fbc0abf5b21e8f (10000 ETH)
Private Key: 0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97

Account #9: 0xa0ee7a142d267c1f36714e4a8f75612f20a79720 (10000 ETH)
Private Key: 0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6

Account #10: 0xbcd4042de499d14e55001ccbb24a551f3b954096 (10000 ETH)
Private Key: 0xf214f2b2cd398c806f84e317254e0f0b801d0643303237d97a22a48e01628897

Account #11: 0x71be63f3384f5fb98995898a86b02fb2426c5788 (10000 ETH)
Private Key: 0x701b615bbdfb9de65240bc28bd21bbc0d996645a3dd57e7b12bc2bdf6f192c82

Account #12: 0xfabb0ac9d68b0b445fb7357272ff202c5651694a (10000 ETH)
Private Key: 0xa267530f49f8280200edf313ee7af6b827f2a8bce2897751d06a843f644967b1

Account #13: 0x1cbd3b2770909d4e10f157cabc84c7264073c9ec (10000 ETH)
Private Key: 0x47c99abed3324a2707c28affff1267e45918ec8c3f20b8aa892e8b065d2942dd

Account #14: 0xdf3e18d64bc6a983f673ab319ccae4f1a57c7097 (10000 ETH)
Private Key: 0xc526ee95bf44d8fc405a158bb884d9d1238d99f0612e9f33d006bb0789009aaa

Account #15: 0xcd3b766ccdd6ae721141f452c550ca635964ce71 (10000 ETH)
Private Key: 0x8166f546bab6da521a8369cab06c5d2b9e46670292d85c875ee9ec20e84ffb61

Account #16: 0x2546bcd3c84621e976d8185a91a922ae77ecec30 (10000 ETH)
Private Key: 0xea6c44ac03bff858b476bba40716402b03e41b8e97e276d1baec7c37d42484a0

Account #17: 0xbda5747bfd65f08deb54cb465eb87d40e51b197e (10000 ETH)
Private Key: 0x689af8efa8c651a91ad287602527f3af2fe9f6501a7ac4b061667b5a93e037fd

Account #18: 0xdd2fd4581271e230360230f9337d5c0430bf44c0 (10000 ETH)
Private Key: 0xde9be858da4a475276426320d5e9262ecfc3ba460bfac56360bfa6c4c28b4ee0

Account #19: 0x8626f6940e2eb28930efb4cef49b2d1f2c9c1199 (10000 ETH)
Private Key: 0xdf57089febbacf7ba0bc227dafbffa9fc08a93fdc68e1e42411a14efcf23656e

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.

hardhat_addCompilationResult
web3_clientVersion (2)
eth_chainId
eth_accounts
eth_blockNumber
eth_chainId (2)
eth_estimateGas
eth_getBlockByNumber
eth_feeHistory
eth_sendTransaction
  Contract deployment: Counter
  Contract address:    0x5fbdb2315678afecb367f032d93f642f64180aa3
  Transaction:         0x64096e95b87bda37f8899b218bdac427263af339411ca83d9997cf7603ff36b7
  From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
  Value:               0 ETH
  Gas used:            252191 of 252191
  Block #1:            0x7dcd289632dfc972c6a7b81e88cc14a227b04e55b765a1feb4bc167d1a6cb7d5

eth_chainId
eth_getTransactionByHash
eth_chainId
eth_getTransactionReceipt

Hardhat started a local Ethereum network for us and printed out all existing accounts, including their private keys. These are test eths. Now switch back to the main terminal and execute the following command:

npm run deploy:local

The output should look like this:

counter-smart-contract npm run deploy:local

> counter-smart-contract@1.0.0 deploy:local /Users/pushkarkumar/Desktop/projects/counter-smart-contract
> hardhat run --network localhost scripts/counter.js

Compiled 2 Solidity files successfully
Counter deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3

Congratulations!!! We have successfully deployed our first smart contract to a local test net. 0x5FbDB2315678afecb367f032d93F642f64180aa3 is the address we need in order to access our smart contract methods in our local testnet.

Conclusion

Hardhat task runner and npm's flexibility come with well-maintained libraries and tools, making the setup pretty solid. Go ahead and add more features to this contract and play around with it. Meanwhile you can find the full source code on GitHub . Feel free to fork or clone it and use it in your next project!

What next?

Now that we have written, tested, and deployed our first smart contract. In the next article, we will understand how to use these contracts through a Node.js server.

This article is part of Research & Development work being done by Pushkar Kumar, Suresh Konakanchi, and Ruchika Gupta. We will be covering a series of articles along with open source projects around blockchain, smart contracts, and web3 in general. Here is the list of all the articles that you can follow to start with Blockchain and write your first contract: