Structs in Solidity: Best Practices for Gas Efficiency by 0xLazard
How can we save a significant amount of gas in our smart contract, especially when using structs and functions that interact with them?
A primary concern for Solidity developers is how to minimize gas costs for both deploying the contract and for calls made by users when they interact with the contract.
In this topic, I will attempt to explain how to achieve optimal optimization for this contract, specifically focusing on populating the struct.
The Contract
The contract in question is fairly straightforward. It contains one struct named Pool Info
, which holds various parameters of different data types such as uint
, bool
, and address
. Specifically, there are two bool
parameters (IsLaunch
and IsFinish
), one address
parameter (token
), and several uint
parameters (allocation
, totalClaimed
, and fees for both deposits and withdrawals).
Newly created pools are added to the dynamic array named poolinfo
. These pools are instantiated whenever the createpool()
function is called.
To test and calculate the gas costs, I wrote a script in Solidity using Foundry. Foundry enables writing tests directly in Solidity, leading to improved execution speeds. Additionally, it facilitates the creation of fuzz tests and allows for gas snapshots. This helps pinpoint the exact gas cost of individual functions or even the overall contract.
The initial test reveals this. A significant amount of gas, precisely 128,448 gas, is consumed to call the createpool()
function. Given the price of ETH at $1670 and a gas price of 47 gwei, the cost to call this function amounts to $10.08.
Firstly, there’s room for minor optimization, especially when it comes to the data types of uints
. For instance, the parameters depositfees
and withdrawfees
will always be less than 1000. Therefore, a more efficient choice for their data type would be uint16
, which can represent values ranging from 0 to 65,535.
With, this modification 2156 gas, will be safe, in dollars this represented $9.91.
But, if we look our struct, we could improve.
Let’s take a closer look at our struct. In the realm of Solidity, one of the keys to optimizing gas costs and performance lies in how storage is managed. The sequence in which variables are declared plays a pivotal role, as the Ethereum Virtual Machine (EVM) reads and processes these variables based on that sequence. Grasping this dynamic is crucial for any developer aiming to fine-tune their contract.
For example, each variable in Solidity occupies a certain amount of space as seen in its bytecode. Here’s a breakdown of the space taken by different variables:
bool isLaunch
: 32 bytesuint allocation
: 32 bytesuint16 withdrawfees
: 32 bytesbool isFinish
: 32 bytesuint256 totalClaimed
: 32 bytesaddress token
: 32 bytesuint16 depositfees
: 32 bytes
This adds up to a total of 224 bytes.
In Solidity, the sequence of variable declarations impacts gas usage. By strategically grouping variables by type, we can optimize storage allocation:
uint256
:totalClaimed
andallocation
take up 64 bytes combined.address
:token
occupies 32 bytes.uint16
: The combined storage ofwithdrawfees
anddepositfees
is 32 bytes.bool
: Together,isFinish
andisLaunch
require 32 bytes.
Now the total will be 160 bytes.
Now that we’ve restructured the struct
and the createpool()
function, let's examine the difference in terms of gas cost.
This results in a 36% reduction, with a gas cost of 82,147. The transaction cost is now $6.44, a savings of $3.64.
While these adjustments might seem subtle at first glance, the strategic organization of structs and the sequence of variable declarations can have a profound impact on efficiency and gas costs in Solidity.
I hope these topics has been helpful for those new to Solidity.
Thank you for reading! :Dddddd
My twitter https://twitter.com/0xLazard