Why Optimize Smart Contracts?
Smart contracts optimization is needed to cut gas fees. It can happen by making the code more efficient. Every action in a smart contract blockchain uses gas. That's why, streamlined code can make lower fees, faster transactions, and better performance overall. Optimized smart contracts help projects manage more users and grow smoothly.
Reducing gas costs isn’t just about saving money. It enhances the user experience and supports the long-term stability of blockchain networks. In this article, we’ll cover five strategies to help you write efficient, high-performance Solidity code.
Step 1: Packing State Variables
Time stamp 01:35
In Solidity, each variable in a smart contract occupies space in 32-byte storage slots. Efficient packing of variables is essential because unused space in these slots leads to higher gas costs. Understanding how to organize bytes within these slots can significantly reduce gas fees.
How Bytes Occupy Storage Slots
Each data type uses a specific amount of space:
bool: 1 byte
uint8: 1 byte
uint32: 4 bytes
address: 20 bytes
uint256: 32 bytes
Solidity allocates variables sequentially. If a variable doesn’t fully occupy a slot, any remaining space goes unused unless another small variable can fit in the same slot. However, Solidity doesn’t backtrack to fill empty bytes in previous slots, so arranging variables effectively is crucial.
Inefficient vs. Efficient Packing
Let’s look at two ways of organizing variables: NonPackedVariables which we usually call inefficient and PackedVariables which is efficient. We’ll go through the reasoning behind each allocation and why one approach saves more space than the other.
Inefficient Packing (NonPackedVariables)
Slot 1: The first variable,
alpha
, occupies 1 byte of space in Slot 1. Since each slot has 32 bytes, there are still 31 bytes available in this slot. However, Solidity’s compiler moves to the next variable without attempting to backtrack or “fit” future variables into the remaining space, which means the remaining 31 bytes are unused unless another variable can fill them immediately.Slot 2: The next variable,
beta
, is auint256
type that requires the full 32 bytes of storage. This fits perfectly into Slot 2, but since it occupies the entire slot, there’s no unused space here.Slot 3: Finally,
gamma
, another 1-bytebool
, is placed in Slot 3. Similar to Slot 1, this leaves 31 bytes unused in Slot 3, as Solidity won’t move backward to place it in the remaining space from Slot 1.
In this layout, Solidity doesn’t combine alpha
and gamma
into the same slot even though they’re both small variables. Solidity compiles variables in the order they’re declared, meaning any space left behind in a previous slot remains unused. As a result, we end up with 3 slots used and 62 bytes of unused space across Slot 1 and Slot 3.
Efficient Packing (PackedVariables)
Slot 1: With
alpha
andgamma
declared consecutively, both variables can share Slot 1.alpha
takes up 1 byte, andgamma
takes another 1 byte, totaling 2 bytes and leaving 30 bytes unused in Slot 1. This is an efficient use of the slot since Solidity places them together, filling the slot with both small variables before moving on.Slot 2: The final variable,
beta
, is auint256
and requires a full 32-byte slot. Since Slot 1 is nearly full,beta
is stored in a new slot, Slot 2, occupying all 32 bytes.
By rearranging the order of alpha
, gamma
, and beta
, we’ve reduced the total slots needed from 3 to 2. This is because Solidity can efficiently use the remaining space in Slot 1 for both alpha
and gamma
, instead of leaving an empty gap. This layout leaves only 30 bytes unused (in Slot 1) compared to 62 bytes in the inefficient layout, saving gas in the process.
Gas Usage Comparison
Testing both layouts reveals a significant difference in gas consumption:
NonPackedVariables: 189,135 gas
PackedVariables: 167,733 gas
This shows that efficient packing in smart contract optimization can save around 22,000 gas. While this may seem minor for a simple contract, it becomes significant for large or frequently executed contracts. Efficient packing not only saves on gas costs but also contributes to the scalability and sustainability of blockchain applications.
Step 2: How to Keep an Eye on Unused Variables
Time stamp 19:50
Unused variables in a smart contract blockchain can increase gas costs unnecessarily. Even if a variable isn’t actively used in the logic of the contract, it still occupies storage space, consuming gas whenever the contract is deployed or executed. Removing these variables is a straightforward way to reduce gas fees and improve contract efficiency.
The
unusedVariable
is declared and stored, but it’s not used anywhere in the contract logic.Additionally,
unusedVariable
is marked as public, meaning it’s accessible externally and consumes gas whenever the contract is deployed.This visibility modifier makes the variable readable from outside the contract, but in this case, it’s unnecessary since the variable isn’t contributing to the contract’s functionality.
By removing unusedVariable, the contract becomes more efficient. A testing showed a noticeable difference:
InefficientContract: 39,242 gas
EfficientContract: 34,192 gas
This results in around 5,000 gas saved. While this may seem small, unused variables can add up, especially in complex contracts where multiple unused variables might be present. In larger contracts or high-traffic applications, this small smart contract optimization can lead to significant cumulative savings.
Step 3: Optimize On Chain Data Storage
Time stamp 26:00
Efficient data storage is crucial in smart contracts. Storing data directly on the blockchain is one of the most expensive operations in terms of gas fees. By carefully choosing how data is stored, you can achieve significant gas savings.
In Solidity, saving data in a contract’s storage is much more expensive than logging information with events. Here are the difference between storage and events:
Storage: When data is saved in storage, it’s kept permanently in the contract’s state on the blockchain. This storage is accessible across all transactions, which makes it expensive because every blockchain node has to store and keep track of this data forever.
Events: Events record information in the blockchain’s transaction logs, not within the contract’s storage. This makes events cheaper because they don’t take up permanent space in the contract itself. Events are great for logging actions or results that can be accessed off-chain later, but aren’t needed directly within the contract’s functions.
The image above shows a test comparing the gas usage between storing votes on-chain and logging them with events. Here’s a summary of the results:
Storage-based Voting: The gas cost for voting with storage was recorded as 2,453,905 gas.
Event-based Voting: The gas cost for voting with events was significantly lower, at 324,926 gas.
This test proves that using events can lead to over 2,000,000 gas savings in this specific scenario. The event-based approach that we use for this smart contract optimization is more efficient because it avoids adding data to the contract’s storage slots, making it an ideal choice for logging large amounts of data without impacting performance.
Step 4: Gas Efficient Alternatives for Min Max
Time stamp 31:38
Instead of using typical if-else logic, assembly-level operations are a good approach to achieve the same result more efficiently. This approach can reduce gas costs, especially in contracts that perform frequent calculations.
Using XOR for the Max Function
The XOR (exclusive OR) operation is a bitwise method that compares two binary numbers. XOR returns 1 when the bits are different and 0
when they’re the same. This property can be used to calculate the maximum of two numbers without traditional if-else
logic, making it more gas efficient.
Here’s how it works, using x = 19
and y = 25
as an example:
1. Calculate XOR of x
and y
:
2. Check if y
is greater than x
:
3. Multiply XOR result by the comparison result:
4. Calculate Final Result Using XOR:
By using this bitwise approach with XOR and comparison, you achieve the same result as an if-else
max function but with fewer steps, saving gas.
Why This Approach is Gas Efficient
This XOR-based method avoids conditional statements, making it faster and cheaper to execute. Bitwise operations like XOR are low-cost because they work directly at the binary level, which is more efficient for the Ethereum Virtual Machine (EVM).
Step 5: Declaring Variables as Constants or Immutable
Time stamp 46:47
In Solidity, variables that don’t need to change after they’re assigned can be marked as constant or immutable. Using these keywords as a smart contract optimization makes the contract more gas-efficient by reducing storage costs. This is because the compiler handles these variables differently from regular state variables.
Why Constants and Immutables Save Gas
In Solidity, constant
and immutable
variables are stored directly in the contract’s bytecode rather than in storage slots, which saves gas. Here’s why:
Constants: These values are hardcoded into the contract during compilation. This means they don’t require a storage slot, reducing storage costs and making retrieval faster and cheaper.
Immutables: Although they are set during contract deployment, immutables are also stored in the bytecode instead of storage slots, saving gas in the same way as constants. This makes them ideal for values that may depend on deployment conditions but won’t change afterward.