My first Solidity Smart Contract
Tags: solidity, typescript, hardhat, beginners
Published: 2023-03-25 18:22:51

# My first Solidity Smart Contract

After [setting up our environment](https://dev.to/duarteroso/introduction-to-evm-smart-contract-development-1bdo) we are now ready to write our first smart contract using Solidity.

In this article we will be writing a basic smart contract, some tests as well as a deployment script to push it into the blockchain. As a bonus, we will configure Hardhat to give us cost estimations from our code. Exciting!

---

## Index

1. [Project Setup](#project-set-up)
1. [Smart Contract: Person](#smart-contract-person)
1. [Testing the contract](#testing-the-contract)
1. [Gas reporter](#gas-reporter)
1. [Deploying the Contract](#deploying-the-contract)
1. [Storage Layout](#storage-layout)
1. [Smart Contract: Better Person](#smart-contract-better-person)
1. [Better?](#better)
1. [Conclusion](#conclusion)

---

## Project Setup

If you followed the previous [article](https://dev.to/duarteroso/introduction-to-evm-smart-contract-development-1bdo) you know how to create a new project using hardhat. Go ahead and create it; you can delete the contents of `contracts`, `scripts` as well as `test`, we will be creating new files for each category.

## Smart Contract: Person

Imagine for a moment we want to represent a "person" as a smart contract. For simplicity sake a person will be limited to their birth date. Create a contract and call it `PersonV1.sol` (there will be V2 later).

We start off by the smart contract license. A full list of licenses can be found [here](https://spdx.org/licenses/). We will go with MIT because it is the second easiest to understand after [WTFPL](https://en.wikipedia.org/wiki/WTFPL)
```solidity
// SPDX-License-Identifier: MIT
```

Right after, we need to select the solidity version we want our contract to be compiled with. We will go with latest.
```solidity
pragma solidity ^0.8.17;
```

A contract is defined by the keyword `contract` followed by a meaningful name. If you are familiar with object-oriented programming languages, it is similar to defining a class.
```solidity
contract PersonV1 {
    uint256 public year;
    uint256 public month;
    uint256 public day;

    constructor(
        uint256 _year,
        uint256 _month,
        uint256 _day
    ) {
        year = _year;
        month = _month;
        day = _day;
    }
}
```

Our person is defined by the year, the month and the day she was born. Obviously, this is a rather over simplified example as nothing other than the address of this contract makes this person unique in the eyes of the law. Moving on...

The birth date of this person is set at the constructor level, meaning that once set it cannot be changed; we will be adding a method exactly for this purpose to show the cost of altering data on the blockchain. We willingly ignore bad input for the sake of keeping it simple.

```solidity
contract PersonV1 {
    // ...

    function setAge(
        uint256 _year,
        uint256 _month,
        uint256 _day
    ) public {
        year = _year;
        month = _month;
        day = _day;
    }
}
```

With this we are able to change our smart contract and measure the cost of such operations. Just like a regular program each operation has a cost, whereas on a CPU the cost is payed with time and energy, on the blockchain the cost is payed with money. We will later try to optimize our contract and understand how we can better write smart contracts to minimize the monetary cost.

You can now compile your contract to make sure the syntax is correct.
```bash
npx hardhat compile
```

## Testing the contract

Testing a smart contract is the single most important task a smart contract developer will be doing. A flawed contract is a contract that is open for attacks, potentially putting at risk the Ether you have placed into it.

Thankfully our smart contract does not receive any ETH, we don't have to worry about breaches. Regardless, it is a good practice to test every single functionality of your contract; with basic cases as well a edge cases to make sure it can handle all situations.

```ts
import { expect } from "chai";
import { BigNumberish } from "ethers";
import { ethers } from "hardhat";

describe("PersonV1", function () {

    const year: BigNumberish = 1987
    const month: BigNumberish = 4
    const day: BigNumberish = 17

    async function deployPerson() {
        const Person = await ethers.getContractFactory("PersonV1")
        const contract = await Person.deploy(year, month, day)
        return contract
    }

    describe("Deployment", function () {
        it("should set the right age", async function () {
            const contract = await deployPerson()
            //
            const y = await contract.year()
            expect(y).equal(year)
            //
            const m = await contract.month()
            expect(m).equal(month)
            //
            const d = await contract.day()
            expect(d).equal(day)
        })

        it("should update the age", async function () {
            const contract = await deployPerson()
            //
            await contract.setAge(year + 1, month + 1, day + 1)
            //
            const y = await contract.year()
            expect(y).equal(year + 1)
            //
            const m = await contract.month()
            expect(m).equal(month + 1)
            //
            const d = await contract.day()
            expect(d).equal(day + 1)
        })
    })
})
```

In our first test (defined by the first `it` function), we make sure that the values passed when constructing our smart contract are correctly saved. On our second test we call the age setter method and verify the new values have been saved into the blockchain.

As you can see, any interaction with the contract requires an `await` keyword. This is because interacting with the blockchain is not an immediate operation and therefore needs to wait for the value before proceeding.

Run the tests to make sure everything gets the green light.
```bash
npx hardhat test
```

## Gas reporter

Hardhat has a feature that, after setting it up and connecting all the pieces, allows our tests to report gas usage based on data taken from [CoinMarketCap](https://coinmarketcap.com/api/). It remains an estimate and the actual price will vary depending on a number of factors, but it is useful enough to start optimizing our smart contracts.

The gas reporter package does not come with the default project setup. Run the following command to add it:

```bash
pnpm add -D hardhat-gas-reporter
```

Now in our `hardhat.config.ts` we can import the package to expose new configuration options:

```ts
import "hardhat-gas-reporter";
```

Adding this import will add a new `gasReporter` property to `HardhatUserConfig`. This new property has multiple entries, the ones we need right now are used to set the currency and to set the API key to connect to CoinMarketCap.
To get an API key, head to [CoinMarketCap](https://coinmarketcap.com/api/), after creating an account and you will find your key in the `Overview` page. They offer multiple plans, from free to enterprise, that come with more or less limitations. For our needs the free tier is more than enough.

```ts
const config: HardhatUserConfig = {
  //...
  gasReporter: {
    currency: "USD",
    enabled: true,
    coinmarketcap: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  }
};
```

We are all set up! Running our tests will from now on include a summary of our gas usage:

```bash
pnpm hardhat test
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l953qnqjlo2b9szgx7x4.png)

As it is right now, it would cost us roughly $12 to add a new person to the blockchain... ouch! Another $1.50 to change their birth data.

## Deploying the Contract

Create a `deploy.ts` file under `scripts`. This will be our deploy script that we will use to actually deploy our smart contract into the local blockchain. It is in this stage that we can pass custom values to the constructor of our smart contract or apply any other logic you may require.

```ts
import { ethers } from "hardhat";

async function printMemoryLayout(address: string, count: number) {
  console.log("### Memory Layout")
  for (let idx = 0; idx < count; ++idx) {
    let slot = await ethers.provider.getStorageAt(address, idx)
    console.log(`Slot\t#${idx}\t${slot}`);
  }
}

async function deployPersonV1() {
  const PersonV1 = await ethers.getContractFactory("PersonV1")
  const contract = await PersonV1.deploy(1987, 4, 17)
  await contract.deployed()
  console.log("PersonV1 deployed!")
  printMemoryLayout(contract.address, 3)
}

async function main() {
  await deployPersonV1()
}

main().catch((error) => {
  console.error(error)
  process.exitCode = 1
});
```

In the `deployPersonV1` function we start off by getting the `PersonV1` contract factory and using it to deploy a new instance of the smart contract by passing it three inputs as expect by `PersonV1`'s constructor. Notice that, again, all methods that interact with the blockchain require an `await`.

We also have a `printMemoryLayout` function that we call after deploying the `PersonV1` contract. This method will be useful to check our contract storage layout.
 
Time to publish our first smart contract!
```bash
npx hardhat run scripts/deploy.ts --network localhost
```

## Storage Layout

In the EVM, the maximum size a type can have is 256 bits. It stores values in `slots` which also happen to have a size of 256 bits. This means that in a `slot` you can fit data with a combined size of at most 256 bits. For example, in a `slot` you can fit:
- 1 `int256`/`uint256`
- 2 `int128`/`uint128`
- 4 `int64`/`uint64`
- ...
- 32 `int8`/`uint`8

Or it can be combined: 1 `int128`/`uint128` and 2 `int64`/`uint64` because `1x128 (bits) + 2x64 (bits) = 256 (bits)`

When compiling your smart contract, the compiler will attempt to pack your data into the most optimized way in order to use the least slots possible.

After deploying the contract, the following layout was printed.
```bash
### Memory Layout
Slot	#0	0x00000000000000000000000000000000000000000000000000000000000007c3
Slot	#1	0x0000000000000000000000000000000000000000000000000000000000000004
Slot	#2	0x0000000000000000000000000000000000000000000000000000000000000011
```

These values might not seem relevant until you look at its hexadecimal values. Remember that our contract had only three values:
1. `uint256 public year` initialized to `1987` or `0x07C3` which can be seen at the end of Slot #0
1. `uint256 public month` initialized to `4` or `0x04` which can be seen at the end of Slot #1
1. `uint256 public day` initialized to `17` or `0x11` which can be seen at the end of Slot #2

A 256 bits long number seems a bit of a waste for values that will never be able to make full use of its capacity. Remember that the solidity compiler will try to pack the memory as best as possible but sometimes the developer needs to give it a hand.

## Smart Contract: Better Person

In our first contract, all our data was of type `uint256` which can have a maximum value of
```
115792089237316195423570985008687907853269984665640564039457584007913129639935
```

Personally, I cannot pronounce that number...

More importantly no month has that many days, no year has that many months and the universe will not have that many years (or will it?).

Therefore we can definitely save a lot of space by using appropriate types that fit our usage, namely `uint8` (whose maximum value is 255) for the month and day and an uint16 (whose maximum value is 65535) for the year.

Create a new file under `contracts` and name it `PersonV2.sol`. This will be a duplicate of `PersonV1` but instead of using 3 `uint256` we will use smaller sized types.

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

contract PersonV2 {
    uint16 public year;
    uint8 public month;
    uint8 public day;

    constructor(
        uint16 _year,
        uint8 _month,
        uint8 _day
    ) {
        year = _year;
        month = _month;
        day = _day;
    }

    function setAge(
        uint16 _year,
        uint8 _month,
        uint8 _day
    ) public {
        year = _year;
        month = _month;
        day = _day;
    }
}
```

We can use the same tests as for `PersonV1` as nothing was changed. Duplicate `test/PersonV1.ts` an name it `PersonV2.ts`. Make sure you replace any reference of `PersonV1` with `PersonV2`!

Finally let's also duplicate the deploy code for our `PersonV2` contract.
```ts
async function deployPersonV2() {
  const PersonV2 = await ethers.getContractFactory("PersonV2")
  const contract = await PersonV2.deploy(1987, 4, 17)
  await contract.deployed()
  console.log("PersonV2 deployed!")
  printMemoryLayout(contract.address, 3)
}

async function main() {
  //...
  await deployPersonV2()
}
```

## Better?

After you deploy both smart contracts, you will see a different storage layout for each:
```bash
PersonV1 deployed!
### Memory Layout
Slot	#0	0x00000000000000000000000000000000000000000000000000000000000007c3
Slot	#1	0x0000000000000000000000000000000000000000000000000000000000000004
Slot	#2	0x0000000000000000000000000000000000000000000000000000000000000011
PersonV2 deployed!
### Memory Layout
Slot	#0	0x00000000000000000000000000000000000000000000000000000000110407c3
Slot	#1	0x0000000000000000000000000000000000000000000000000000000000000000
Slot	#2	0x0000000000000000000000000000000000000000000000000000000000000000
```

For `PersonV2`, slot #1 and slot #2 are zero-ed out although both contract contain three member. Why would that be?

Unlike previously, this time the solidity compiler was able to do its magic after a bit of our help. You see, the compiler will attempt to pack storage memory into as few slots as possible. Whereas before each of our data was using 256 bits, now we are only using a fraction of that! 

You can still see that our data hasn't changed. Slot #0 of `PersonV2` has the value of `0x110407c3`, the slots aggregation of `PersonV1`. 
If we decompose this value:
- Day now fits in 8 bits, i.e. 2 bytes = `0x11` (17)
- Month also fits in 8 bits, i.e. 2 bytes = `0x04` (4)
- Year fits in 16 bits, i.e. 4 bytes = `0x07c3` (1987)

We have effectively reduced our storage footprint by a lot! Does that help cost wise? 

Each time we alter the blockchain, a cost has to be paid. This time we are deploying a contract that will use less storage space on the blockchain. Comparing the cost of deployment for each version of our contract speaks for itself:
- PersonV1 = $12.60
- PersonV2 = $9.34 (25% less!)

Same goes for the method that alters the state of the blockchain:
- PersonV1 = $1.52
- PersonV2 = $1.14 (25% less!)

![PersonV1-vs-PersonV2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z2iax3pn590i2mwd0737.png)

## Conclusion

Creating a smart contract, creating tests to verify all its functionality and creating a deploy script is the foundation of what it means to be a smart contract developer.

On top of that, thanks to some basic profiling tools and common sense, the developer is able make a smart contract more cost effective.

In this article we attempted to cover all of these points to hopefully give you a good starting point on becoming a smart contract developer. The most challending part will be to audit your contracts to avoid any exploits and potentially loosing money.

Remember: only practice makes perfect and stay curious!