[Write-Up: Writing a Smart-Contract Deployment Script]

Introduction

Today we're going to talk about creating and employing a smart contract based on Ethereum using the Solidity language to write the contract and TypeScript for automating the deployment process. For this demonstration, we'll use the contract "Example," which contains four variables and a function that returns the product of these variables. We'll examine the deployment process in simple steps and highlight places where it's possible to make mistakes that can lead to a waste of time.

Step 1: Write the smart contract using Solidity

First, create the smart contract with Solidity. In this example, the contract is named Example and contains the following elements:

  • Four public variables: a, b, c, d
  • A constructor to initiate variable a
  • Functions for installing the values of variables b, c, d
  • The product function that returns the product of all the variables

It's important to note that the product() function won’t work correctly if any of the parameters aren’t initiated (if the parameters are given, then the product will be different from 0 or will always be 0 in the opposite case).

An example of the smart contract's code:

pragma solidity ^0.8.18;

contract Example {
   uint public a;
   uint public b;
   uint public c;
   uint public d;

    constructor(uint _a) {
        a = _a;
    }

    function setB(uint _b) external {
        b = _b;
    }

    function setC(uint _c) external {
        c = _c;
    }

    function setD(uint _d) external {
        d = _d;
    }

    function product() external view returns(uint) {
        return a * b * c * d;
    }
}

Step 2: Configure HardHat

Next, you need to decide on the blockchain you'll use for deployment and for the necessary prerequisites. In our example, we’ll need the following:

  • An account that has the required amount of native currency
  • A link to a resource with an estimate of the current transaction costs (in this case, API PolygonGasStation)
  • A link to the access point to the JSON-RPC provider (you can find them, for example, here https://www.alchemy.com/ or here https://www.infura.io/)

Then, we create a .env file where we'll put everything from the list:

MAINTAINER_PRIVATE_KEY= <Private key of deployer account>
GAS_STATION_URL=https://gasstation.polygon.technology/v2
POLYGON_URL= <Link to polygon jsonRpc access point>

We also need to add the required networks in the HardHat configuration (hardhat.config.ts):

import "@nomicfoundation/hardhat-toolbox";
import * as dotenv from "dotenv";
import { HardhatUserConfig } from "hardhat/config";

dotenv.config();

const config: HardhatUserConfig = {
   networks: {
       polygon: {
           url: process.env.POLYGON_URL || "",
           chainId: 137
       }
   },
   solidity: {
       compilers: [
           {
               version: "0.8.18",
               settings: {
                   optimizer: {
                       enabled: true,
                       runs: 100
                   }
               }
           }
       ]
   }
   // Everything else you need…
};

export default config;

Step 3: Create the scripts for deploying on TypeScript

Manual deployment isn't the best option, so we'll automate the process with a script using the ethers.js library and TypeScript. This script performs the following tasks:

1. Receives data on current commissions through API PolygonGasStation

Different networks may have different rules for receiving commissions. The Polygon network used in this example requires the PoA mechanism and EIP-1559 transactions. To do this, we'll adapt the HardHat provider a bit.

2. Connects maintainer wallet

3. Displays information on the network where the deployment from the account occurs

4. Deploys the "Example" smart contract to the network

5. Assigns values to variables a, b, c, d

It's important to call the wait() function. Even though it may seem enough to call the set* method, it doesn’t indicate that the transaction has been fixed in the block. Wait for the transactions to appear in the blockchain before performing the next steps in the process.

6. Verifies the assigned values are correct

A best practice is to verify the values before executing any operations. If you've configured something incorrectly, then subsequent executions could end successfully but with inaccurate results. In smart contracts with more complex topologies, a small mistake at this stage could mean several hours of lag or a neglected vulnerability.

7. Outputs information on the deployed contract and the current network

After deploying, it's a good idea to output information about the deployed contracts. You can save an artifact with data in a place where the team knows how to find it.

An example of the deployment script code:

import { FeeData, parseUnits, Wallet } from 'ethers';
import { ethers } from "hardhat";
import axios from 'axios';
import { Example__factory } from '../typechain-types';

type GetFeedDataFunction = () => Promise<FeeData>;

interface PolygonGasStationAnswer {
   fast: {
       maxPriorityFee: number;
       maxFee: number;
   };
}

function assert(
   condition: boolean,
   message: string,
): asserts condition {
   if (!condition) throw new Error(message);
}

const getFeeData: (overpay: number) => GetFeedDataFunction = (overpay = 0) => async () => {
   const data = (await axios
       .get(process.env.GAS_STATION_URL!)
       .then((v) => v.data)) as PolygonGasStationAnswer;

   console.log('Data: ', data)
   return {
       maxPriorityFeePerGas: parseUnits(
           (Math.ceil(data.fast.maxPriorityFee) + overpay).toString(),
           "gwei"
       ),
       maxFeePerGas: parseUnits(
           (Math.ceil(data.fast.maxFee) + overpay).toString(),
           "gwei"
       ),
       lastBaseFeePerGas: null,
       gasPrice: null,
       toJSON: () => ({})
   };
};

async function main() {
   const maintainer: Wallet = new ethers.Wallet(
       process.env.MAINTAINER_PRIVATE_KEY as string
   ).connect(ethers.provider);

   // Patch provider to correctly compute gas prices for Polygon
   ethers.provider.getFeeData = getFeeData(1);

   const maintainerAddress = await maintainer.getAddress();

   const network = await ethers.provider.getNetwork();

   console.log("Current network:", network.toJSON());
   console.log("Current deployer:", maintainerAddress);

   console.log("----".repeat(20));

   const exampleFactory = new Example__factory(maintainer);

   // Deploying contract
   const example = await exampleFactory.deploy(1);

   await example.waitForDeployment();

   // Configure after deploy
   await example.setB(2).then(tx => tx.wait());
   await example.setC(3).then(tx => tx.wait());
   await example.setD(4).then(tx => tx.wait());

   console.log('Example: ', await example.getAddress());
   console.log('A: ', await example.a());
   console.log('B: ', await example.b());
   console.log('C: ', await example.c());
   console.log('D: ', await example.d());

   // Optionally check for value correctness before proceeding
   assert(await example.b() === 2n, 'B is not correct');

   // Check for target functions
   console.log('Product: ', await example.product());

   const deployData = {
       Example: await example.getAddress(),
       Network: network.toJSON(),
       Deployer: maintainerAddress
   }

   console.log('Deploy artifact: ', JSON.stringify(deployData, undefined, 4));
}

main();

Conclusion

Using an automated script to deploy smart contracts significantly simplifies the process and allows you to avoid mistakes that occur when manually inputting data. The Example contract above and the script for deployment demonstrates the basic steps and approaches that can be adapted to any contract and network.