Hardhat: Optimizing the size of smart contracts
In this tutorial, you’ll learn how to profile and optimize smart contract sizes with Hardhat and the Hardhat Contract Sizer plugin.Objectives
By the end of this tutorial you should be able to:- Use Hardhat Contract Sizer plugin to profile contract size
- Describe common strategies for managing the contract size limit
- Describe the impact that inheritance has on the byte code size limit
- Describe the impact that external contracts have on the byte code size limit
- Describe the impact of using libraries has on the byte code size limit
- Describe the impact of using the Solidity optimizer
Overview
In the world of blockchain and Ethereum, optimizing smart contract sizes is crucial. Smaller contracts consume less gas during deployment and execution, which is translated into gas costs savings for your users. Fortunately, you can use in Hardhat thehardhat-contract-sizer
plugin that helps you analyze and optimize the size of your smart contracts.
Setting up the Hardhat Contract Sizer plugin
Hardhat Contract Sizer is a community-developed plugin that enables the profiling of smart contract by printing the size of your smart contracts in the terminal. This is helpful during development since it allows you to immediately identify potential issues with the size of your smart contracts. Keep in mind that the maximum size of a smart contract in Ethereum is 24 KiB. To install, runnpm install -D hardhat-contract-sizer
.
Then, import hardhat-contract-sizer
in hardhat.config.ts
:
Your first size profiling
Similar to the previous tutorials, you begin by profiling the smart contractLock.sol
.
Run npx hardhat size-contracts
, which is a task added to Hardhat once you set up and configure the hardhat-contract-sizer
plugin.
You are then able to see:
hardhat-contract-sizer
plugin, since it show you the size of your contracts.
Common strategies to optimize contract sizes
In order to illustrate some of the strategies to optimize the size of your contracts, create two smart contracts,Calculator.sol
and ScientificCalculator.sol
, with the following:
npx hardhat size-contracts
again and you should be able to see:
ScientificCalculator
is bigger than Calculator
. This is because ScientificCalculator
is inheriting the contract Calculator
, which means all of its functionality and code is available in ScientificCalculator
and that will influence its size.
Code abstraction and modifiers
At this point as a smart contract developer, you can review your smart contract code and look for ways into you can optimize it. The first thing you notice in the source code is the extensive use of:ScientificCalculator
:
npx hardhat size-contracts
command, you should be able to see:
Split into multiple contracts
It is common to split your smart contracts into multiple contracts, not only because of the size limitations but to create better abstractions, to improve readability, and to avoid repetition. From a contract size perspective, having multiple independent contracts will reduce the size of each contract. For example, the original size of a smart contract was 30 KiB: by splitting into 2, you will end up with 2 smart contracts of ~15 KiB that are within the limits of Solidity. Keep in mind that this will influence gas costs during the execution of the contract because it will require it to call an external contract. In order to explain this example, create a contract calledComputer
that contains a function called executeProcess
:
executeProcess
function of Computer
requires certain functionality of Calculator
and a new contract called Printer
:
Computer
to access both functionalities is to inherit; however, as all of these contracts continue adding functionality, the size of the code will also increase. You will reach the contract size issue at some point, since you are copying the entire functionality into your contract. You can better allow that functionality to be kept with their specific contracts and if the Computer
requires to access that functionality, you could call the Calculator
and Printer
contracts.
But in this example, there is a process that must call both Calculator
and Printer
:
Computer
contract is very small but still has the capability to access all the functionality of Printer
and Calculator
.
Although this will reduce the size of each contract, the costs of this are discussed more deeply in the Gas Optimization article.
Using libraries
Libraries are another common way to encapsulate and abstract common functionality that can be shared across multiple contracts. This can significantly impact the bytecode size of the smart contracts. Remember that in Solidity, libraries can be external and internal. The way internal libraries affect the contract size is very similar to the way inherited contracts affects a contract’s size; this is because the internal functions of the library is included within the final bytecode. But when the libraries are external, the behavior is different: the way Solidity calls external libraries is by using a special function called delegate call. External libraries are commonly deployed independently and can be reused my multiple contracts. Since libraries don’t keep a state, they behave like pure functions in the Blockchain. In this example, your computer will use theCalculator
library only. Then, you would have the following:
Computer
is:
Calculator
library for uint256
and how in the executeProcess
function, you can now use the add
function from the Calculator
library in all of the uint256
.
If you run the npx hardhat size-contracts
command, you then get:
Calculator
library functions and you will then have:
Using the Solidity compiler optimizer
Another way to optimize the size of the smart contracts is to simply use the Solidity optimizer. From the Solidity official docs:Overall, the optimizer tries to simplify complicated expressions, which reduces both code size and execution cost.You can enable the solidity optimizer in hardhat by simply adding the following to the
hardhat.config.ts
file:
runs
. If you run the contract sizer command again, you will see the following:
runs
parameter value to 1000:
runs
value the more efficient during execution but more expensive during deployment. You can read more in the Solidity documentation.