28 Ways to Optimize Gas Usage in Solidity Code
A selection of 28+ small tips and tricks you can use in your Solidity code to reduce gas. These are easy to implement and often forgotten about mini tricks that reduce Gas when writing Solidity code for the EVM.
Table of Contents for 28 Ways to Optimize Gas Usage in Solidity Code
The programming language Solidity is used to compile smart contracts to EVM bytecode. There is a cost to each bytecode, which is measured in ‘gas’. The higher the gas, the more expensive it is. People pay the gas fee when they are interacting with your smart contract. You also pay gas when deploying. So there are big advantages to minimizing gas usage.
Gas is calculated as a mix of:
- the data sent with the transaction,
- the opcodes that were run the amount of state changes,
- and the amount of memory used.
Here is a list of gas optimizations, and examples of how you can use them in your Solidity code.
Some of these examples are going to be quite simple, in order to make the examples easier to read.
Please don’t blindly replace your current code with the optimizations here - be sure to understand when and why to use them.
Example gas usages are the execution costs shown when running in Remix in the Remix VM.. Some of the examples have smaller differences in gas than in the real world, due to the easy to read use cases. Also note that this gas includes the cost of the whole transaction (so its always at least 21,000 gas for just the transaction, plus whatever the function’s gas cost is)
If you know of more gas optimizations, please get in touch and let me know, I’d love to add more here.
Increment/Decrement numbers carefully to reduce gas costs
There are a few ways to increment (or decrement) numbers, such as i++
, ++i
, i = i + 1
or i += 1
. They all have slightly different implementations, and so have different gas costs.
If possible, see if you can use a cheaper way to increment. Of course, they all have slightly different use cases so cannot always be interchanged.
The examples below show how they can be used, along with the gas cost for calling each of these functions.
pragma solidity =0.8.7;
contract GasExamples {
uint256 public someNumber;
function increment() external {
// four ways to increment...
// cheapest in this example is with ++someNumber, at 49,982 gas
// ++someNumber;
// 49,989 gas:
// someNumber++;
// 50,044 gas:
// someNumber = someNumber + 1;
// 50,059 gas
// someNumber += 1;
}
}
View this gas optimization tip on YouTube
Chose your function names carefully
Your functions are converted into function selectors, which are the first four bytes of a keccak256 hash of the function name.
When your smart contract is called, Solidity compiles code that checks in order if you are calling each function selector until it finds it.
If you have a very commonly used function, make sure its selector has a low value, so it is likely to be the first comparison made.
The savings are quite minimal (44 gas when 2 functions) and for most smart contracts it isn’t a consideration, but it’s a really neat trick to get every last bit of gas saving.
pragma solidity >=0.8.4;
contract FunctionSelectorExample {
/*
This is the mapping of selectors (in order) to the function names
{
"706f4c9e": "funcA()", // first - so will be cheapest as itll be the first comparison
"7092150a": "funcC()"
"84c5a5ef": "funcB()", // last - so a bit more expensive as it will be the third comparison
} */
// costs 24,364 gas
// (cheapest as selector is 706f4c9e, which is first in order)
function funcA() public {
}
// costs 24,415 gas
// (most expensive, as selector is 84c5a5ef, which is third in order)
function funcB() public {
}
// costs 24,390 gas
function funcC() public {
}
}
*note: in many examples elsewhere on the page I’ve used two functions in a contract - optimized()
and unoptimized()
. Due to this, there is always going to be a 44 gas difference, but as this is much smaller than almost all other gas optimizations I’ll keep the contracts with those two functions to make them easier to read.
Accessing mapped values
Instead of accessing the same mapped value multiple times, use a local variable for reduced gas
Every time you access a mapped value, there is a fresh lookup of where that value is stored. This involves generating keccak256 hashes and accessing the storage.
It can save gas if you create a variable for the object you’re accessing, for example:
pragma solidity >=0.8.4;
contract GasOptimizer {
struct Person {
uint age;
uint highScore;
uint numVisits;
}
mapping (address => Person) people;
// 60449 gas
function badExample() public {
people[msg.sender].age = 20;
people[msg.sender].highScore = 30;
people[msg.sender].numVisits = 40;
}
// 60306 gas - slightly less, and it is doing the same thing
function goodExample() public {
Person storage person = people[msg.sender];
person.age = 20;
person.highScore = 30;
person.numVisits = 40;
}
}
Payable functions are cheaper
When Solidity is compiled, non payable
functions get more opcodes to check ETH was not sent. So therefore, payable functions have cheaper gas.
pragma solidity >=0.8.4;
contract Example {
// 24,390 gas
function NotPayableFn() public {}
// 24,337 gas
function PayableFn() public payable {}
}
Use constants and immutable if possible
If you can get away with it, using constants/immutable can reduce costs.
pragma solidity >=0.8.4;
// deploy cost 145,354
contract UnoptimizedExample {
uint public decimals;
constructor(uint _decimals) {
decimals = _decimals;
}
// not really needed, as decimals is public anyway
// gas 23,501
function getDecimalsFunction() public view returns(uint) {
return decimals;
}
}
// deploy cost 117,007
contract OptimizedExample {
uint immutable decimals;
constructor(uint _decimals) {
decimals = _decimals;
}
// gas 21,379
function getDecimalsFunction() public view returns(uint) {
return decimals;
}
}
Use indexed event params to reduce gas usage
Using indexed event params can reduce gas. Use them on value types, such as uint
, address
, bool
. The saving is quite minimal, but it all adds up.
pragma solidity >=0.8.4;
contract Example {
event EventUnoptimized(uint someNumber, string someName);
event EventOptimized(uint indexed someNumber, string someName);
// using events without indexed params is more expensive
// 26,899 gas
function Unoptimized() public {
emit EventUnoptimized(123, "hi");
}
// function using indexed event params can save 28 gas
// 26,871 gas
function Optimized() public {
emit EventOptimized(123, "hi");
}
}
“Less than or equal to” (<=) is more expensive than just “less than” (<)
If checking if something is less than or equal to, or greater than or equal to, then the EVM needs to do more opcodes. So it is cheaper to check just <
or >
, instead of <=
or >=
.
Example below shows it with greater than or equal to, but it works both ways!
pragma solidity >=0.8.4;
contract Example {
// 21,416 gas
function GTE() public pure returns (bool) {
// or could be <=
return 1 >= 2;
}
// 21,391 gas
function GT() public pure returns (bool) {
// or could be <
return 1 > 2;
}
}
Revert with an error instead of using revert with strings
Reverting with an error can be cheaper than using require()
, and it is easier to add additional arguments (as shown).
pragma solidity >=0.8.4;
contract Example {
error InvalidGuess(address guesser);
// 21,923 gas when guessing with 5
function Unoptimized(uint guessANumber) public pure returns (uint) {
require(guessANumber > 5, "InvalidGuess");
return 1;
}
// 21,810 gas when guessing with 5
function Optimized(uint guessANumber) external view returns (uint ) {
if(guessANumber > 5) {
revert InvalidGuess(msg.sender);
}
return 1;
}
}
Use calldata instead of memory, you are not mutating that data
Loading from calldata is much cheaper than memory. Deployment is also cheaper, especially with large error message strings in require()
If memory
is used, then the calldata (which is always there) has to be copied over to memory data so it has a higher cost.
The advantage of memory
data though is that you can mutate it (which you cannot do with calldata).
pragma solidity >=0.8.4;
contract Example {
// 23,677 gas, then called with [1,2,3] (returns 6)
function Unoptimized(uint[3] memory inputs) external pure returns (uint) {
return inputs[0] + inputs[1] + inputs[2];
}
// 22,536 gas, when called with [1,2,3] (returns 6)
function Optimized(uint[3] calldata inputs) external pure returns (uint ) {
return inputs[0] + inputs[1] + inputs[2];
}
}
Pack storage variables in an optimized order
Pack storage variables in order so they can fully use up each 256byte slot.
If you have a 32 byte, 256 byte and another 32 byte variable and you define them in that order, they will take up three slots.
- The First slot will be with the 32 byte variable.
- The 256 byte variable will need a new slot to itself.
- Then the final 32 byte variable will be in a new slot.
But if the 256byte variable was defined first, then both of the 32 byte variables could be together in slot 2.
As well as organizing them into an order so they can fit into small number of slots, you should also pack them so that variables that are often accessed together are in the same slot (as you pay for cold slot reads - but if they’re both in the same slot this means you have fewer cold slot reads)
For more details about how storage slots work, see my guide here
pragma solidity >=0.8.4;
// 150,624 gas to deploy
contract NotOptimized {
uint32 a; // slot 0 (takes up first 32 bytes in slot 0)
uint256 b; // slot 1 (has to have a new storage slot, as 256 + 32 is more than max slot size (256 bytes)
uint32 c; // slot 2 (can't merge with last slot)
}
// 124,641 gas to deploy
contract Optimized {
uint32 a; // slot 0
uint32 b; // slot 0 (both of these can be in first slot (32 + 32 < 256)
uint256 c; // slot 1 in its own slot
}
Use bytes instead of string (in very specific situations)
Using bytes of a fixed size (e.g. bytes32
), can be cheaper than using an unbound string
.
This only makes sense if you are sure it would fit into the size of the bytes variable - don’t just replace all uses of string with bytes…
pragma solidity >=0.8.4;
contract Example {
string name;
bytes32 name2;
// passing in "abc" = 234,291 gas
function unoptimized(string calldata yourName) public {
name = yourName;
}
// passing in "abc" = 51,177 gas
function optimized(bytes32 yourName2) public {
name2 = yourName2;
}
}
Move frequently accessed storage variables into memory (stack)
If you are regularly accessing storage variables in a function, it can save gas by just creating a new memory variable (on the stack), as calling the SLOAD opcode multiple times is very expensive.
pragma solidity >=0.8.4;
contract Example {
uint currentCounter; // << storage data variable
// note: these examples have nothing to do with the use of require()
// but simply due to how many times it loads from storage as opposed to memory.
// 2786 gas
function unoptimized() view public returns (uint) {
require(currentCounter < 5); // load from storage
require(currentCounter != 10); // again loads from storage
return currentCounter; // again loads from storage
}
// 1177 gas
function optimized() view public returns (uint) {
uint _counter = currentCounter; // << load from storage, put it on stack once
require(_counter < 5); // loads from the _counter on stack (cheap!)
require(_counter != 10); // loads from the _counter on stack (cheap!)
return _counter; // again loads from _counter
}
}
Another example:
pragma solidity >=0.8.4;
contract Example {
uint favNumber;
// first run (setting to 5): 27310 gas
// second run (setting again to 5): 27425 gas
function unoptimized(uint newNum) public {
require(newNum < 10);
favNumber = newNum;
}
// first run (setting to 5): 50310 gas
// second run (setting again to 5): 23847 gas
function optimized(uint newNum) public {
uint _currentFavNum = favNumber;
require(_currentFavNum < 10);
if(_currentFavNum != newNum) {
favNumber = newNum;
}
}
}
Don’t access a dynamic array’s length property in a for loop
This is similar to the previously mentioned tip of copying variables from storage data to memory (on the stack) if you are accessing them often. If you are doing a for loop, don’t access an array’s length directly every time.
This tip only works if you have an array’s length before the for loop, and the array’s length does not change during the loop. It also doesn’t work on a fixed-length array, as that does not require storage data reads.
pragma solidity >=0.8.4;
contract CountUp {
uint[] someArray = [1,20,30,400,50,6,70];
// 42,937 gas
function unoptimized() public view returns(uint) {
uint total = 0;
for(uint i = 0; i < someArray.length; i++) {
total += someArray[i];
}
return total;
}
// 42,172 gas
function optimized() public view returns(uint) {
uint total = 0;
uint len = someArray.length; // cached array length
for(uint i = 0; i < len; i++) {
total += someArray[i];
}
return total;
}
}
Use unchecked, if you are certain there is no chance of overflow or underflow
If you are doing math operations, and you are sure there is no chance of an over or underflow, you can use unchecked to reduce gas costs.
One place this can often be added is when doing for loops. If you have for(uint256 i = 0; i < someValue; i++)
you can safely assume the i++
is never going to overflow (you would run out of gas before you overflow a uin256). But to wrap that in unchecked its a bit messy, but worth it to save some gas.
Note: this should only be used if you fully understand how to use unchecked and what security issues you can run into when this is used.
pragma solidity >=0.8.4;
contract Example {
uint counter;
// 57,000 gas when called with 10
function withDefaultChecked(uint256 length) public {
require(length < 100);
for (uint256 i = 0; i < length; i++) {
counter += i;
}
}
// 35,148 gas when called with 10
function withUnchecked(uint256 length) public {
require(length < 100);
for (uint256 i = 0; i < length; i++) {
unchecked {
// solidity will not add any extra checks to ensure it does not overflow, so this saves gas
counter += i;
}
}
}
}
Other good places to use unchecked is where you have already checked the amounts. For example if on line 1 of a function you had require(maxNum < 100)
, then something like var someAnswer = 123 + maxNum
could be safely wrapped in unchecked { ... }
Don’t make multiple updates to storage variables if possible
Making many changes to storage data is expensive. Making many changes to memory data on the stack is quite cheap - so you can use that as a temporary variable, and then write to storage at the end.
pragma solidity >=0.8.4;
contract Example {
uint counter1;
uint counter2;
// 55,785 gas
function Unoptimized() public {
for(uint i = 0; i < 10; i++) {
counter1++; // writes directly on storage 10 times
}
}
// 53,564 gas
function Optimized() public {
uint tmpCounter = counter2;
for(uint i = 0; i < 10; i++) {
tmpCounter++; // updates stack data (cheap!) 10 times
}
counter2 = tmpCounter; // writes directly on storage once
}
}
Use ‘send’ instead of ‘transfer’
If you use send
instead of transfer
, you can save gas as send
does not check if it was successful.
(Of course this means you’re missing one of the key features of transfer
, which is to know if it was successful. But if your only focus is gas saving, this can help a little)
pragma solidity >=0.8.4;
contract Example {
address toPay;
constructor() payable {
toPay = msg.sender;
}
// 27,048 gas
function paySomeoneWithTransfer() public payable {
payable(toPay).transfer(msg.value);
}
// 26,997 gas
function paySomeoneWithSend() public payable {
payable(toPay).send(msg.value);
}
}
Note: you might get compiler warnings, telling you that “Failure condition of ‘send’ ignored. Consider using ‘transfer’ instead”. Be sure to understand the differences (other than gas) before you use it like this!
Use selfdestruct to send eth
If the final thing your smart contract will ever do is send a remaining balance to an address, then instead of using send()
or transfer()
, it is cheaper in gas to use selfdestruct
. Read about selfdestruct in Solidity here.
And after calling selfdestruct()
, your smart contract will no longer be able to be used, use it with caution. This is probably a useful trick for ‘codegolf’ style challenges but not useful in many practical situations.
pragma solidity >=0.8.4;
contract Example {
address who;
constructor() payable {
who = msg.sender;
}
// 34,785 gas
function payEverythingWithTransfer() public {
payable(who).transfer(address(this).balance);
}
// 32,567 gas
function payEverythingWithSelfDestruct() public {
selfdestruct(payable(who));
}
}
Note: there used to be a refund when using selfdestruct, but this was removed in EIP-3529 (went live in London Ethereum update)
Use chained conditionals in the order of cheapest gas
If you have an if statement, like if(funtionA() && functionB()) { ... }
, then make sure that you order the function calls by cheapest gas.
If functionA
costs 1000 gas, but functionB
costs only 100 gas then swap them around if(functionB() && functionA())
. If the first chained condition returns false, then the more expensive function will never have to execute.
pragma solidity >=0.8.4;
contract Example {
// 21,745 gas
function Unoptimized() public pure returns(bool) {
return expensiveFunctionA() && cheapFunctionB();
}
// 21,446 gas - the difference here could be much larger depending
// on how much gas the cheapFunctionB/expensiveFunctionA cost.
function Optimized() public pure returns(bool) {
return cheapFunctionB() && expensiveFunctionA();
}
function expensiveFunctionA() private pure returns(bool) {
// implementation details of these do not matter!
uint256 tmp = 1;
tmp = tmp * 2;
return tmp < 5;
}
function cheapFunctionB() private pure returns(bool) {
// implementation details of these do not matter!
return 999 < 5;
}
}
Don’t feel the need to always set initial values
You can save gas by not defining the initial value. In this example, we’re either setting someNumber
as just a defined variable (not explicitly setting it to 0), or we are defining it and setting it to 0.
These both have the same output, but slightly different gas cost.
pragma solidity >=0.8.4;
contract Example {
// 21,600 gas
function UnoptimizedIncrementer() public pure returns (uint) {
uint someNumber; // no initial value
return someNumber + 5; // same as 0 + 5 = 5
}
// 21,586 gas
function OptimizedIncrementer() public pure returns (uint) {
uint someNumber = 0; // setting initial value
return someNumber + 5; 0 + 5 = 5
}
}
Push to arrays, instead of completely overwriting them
This is quite an obvious one, but if you have an array with element(s) already in it, and you want to add new item(s) to it, use .push()
instead of overwriting the whole array.
pragma solidity >=0.8.4;
contract Example {
uint[] favNumbers;
// helper function, to reset favNumbers to [50]
function reset() public {
favNumbers = [50];
}
// running this (after setting favNumbers to [50]
// cost 84,213 gas
function unoptimized() public {
// makes no assumption about what favNumbers was set to, as it overwrites it
favNumbers = [50, 51, 52];
}
// running this (after setting favNumbers to [50]
// cost 81,457 gas
function optimized() public {
// assumes that when this is run, favNumbers was set to [50]
favNumbers.push(51);
favNumbers.push(52);
}
}
Remember that counting from 0 upwards can mean more gas in your function calls.
This is a bit of a convoluted example, but it can be useful sometimes so I thought I’d mention it.
If you have a counter, going from a zero to a non-zero value is more expensive.
For example, compare these two contracts - the first one is much cheaper to call amILuckyVisitor()
three times. Of course, the deployment cost will be more expensive in the CountDown contract, as counter
has to be set to 3
.
pragma solidity >=0.8.4;
contract CountDown {
uint counter = 3;
constructor() payable {}
// gas on first run: 30416
// gas on second run: 30416
// gas on third run: 36163
function amILuckyVisitor() payable public {
counter--;
if(counter == 0) {
selfdestruct(payable(msg.sender));
}
}
}
pragma solidity >=0.8.4;
contract CountUp {
uint counter = 0;
constructor() payable {}
// gas on first run: 50081
// gas on second run: 30416
// gas on third run: 36163
function amILuckyVisitor() payable public {
counter++;
if(counter == 3) {
selfdestruct(payable(msg.sender));
}
}
}
It can also be cheaper sometimes to keep balances at a value of at least 1 (and not resetting them to 0) if it is likely that in the future they’ll be moving again to a non-zero balance. But you still have to take into account the possible refund (for setting to a zero value)
Use separate checks in require() to save gas
// 229,940 gas to deploy
pragma solidity >=0.8.4;
contract UnoptimizedVersion {
mapping(address => uint256) balances;
bool isActive = true;
constructor() {
balances[msg.sender] = 999;
}
// 29,622 gas to run
function Unoptimized() external payable returns (uint) {
require(isActive && balances[msg.sender] > 0 && msg.value > 0, "errmsg");
return 1;
}
}
And the optimized one (has higher deploy costs):
pragma solidity >=0.8.4;
// 309,456 gas to deploy
contract OptimizedVersion {
mapping(address => uint256) balances;
bool isActive = true;
constructor() {
balances[msg.sender] = 999;
}
// 29,604 gas to run
function Optimized() external payable returns (uint) {
require(isActive, "errMsg");
require(balances[msg.sender] > 0 , "errMsg2");
require(msg.value > 0, "errmsg");
return 1;
}
}
This will cost more to deploy, but can save gas when the function is run. This can be useful if you know a function in your smart contract will be called many times that it can justify the larger deploy cost.
(You could also save more gas by using custom errors - covered elsewhere on this page)
Don’t compare to boolean literals (true/false)
Doing a full comparison with boolean literals (if (something == true)
) is more expensive than just doing if(something)
.
pragma solidity >=0.8.4;
contract Unoptimied {
// 21,839 gas when isTrue is true
// 21,817 gas when isTrue is false
function Compare(bool isTrue) pure external returns (uint) {
if (isTrue == true) {
return 1;
}
return 1;
}
}
contract Optimized {
// 21,821 gas when isTrue is true
// 21,799 gas when isTrue is false
function Compare(bool isTrue) pure external returns (uint) {
if (isTrue) {
return 1;
}
return 1;
}
}
Use selfbalance instead of address(this).balance for more efficient gas usage (older versions of Solidity)
The typical way to get the balance of the smart contract is to call address(this).balance
.
In the most recent versions of Solidity this is optimized, however there are lots of smart contracts still using older versions of Solidity. In v 0.7.6, it is cheaper in gas to use assembly and call selfbalance
.
For internal calls it can save 15 gas, and external calls saves 6 gas
function unoptimised() external returns (uint bal) {
bal = address(this).balance;
}
function optimised() external returns (uint bal) {
assembly {
bal := selfbalance()
}
}
7 Further smaller tricks
There are some additional tips/tricks, that are more generic and harder to show with Solidity code examples but they should also be on your mind when trying to reduce gas usage in your Solidity code.
- use the Solidity Optimizer. You can do this when compiling your Solidity code. You probably want to optimize for a high number of runs.
- revert as soon as possible. If possible, revert before doing as many opcodes, to save the caller gas
- aim to optimize for gas cost of calling your functions, and not optimizing for deployment gas cost
- aim to optimize for gas cost of normal functions your users will call, and not for gas cost of admin-like functions
- get a gas refund by deleting storage data
- using fixed size arrays (e.g.
uint[3] topThree
) is cheaper than dynamic sized arrays (e.g.uint[] topThree
), so should be used if you know the max length of an array. - don’t always avoid uint256 even if you know the number can fit in fewer bits! Converting a number from a uint256 to something like uint8 might look like it will save gas, but then you have to actually do the conversion which costs more gas.
- instead of using a storage varible as bool (which is either
0
or1
), useuint256(1)
anduint256(2)
for true/false, to avoid 20,000 gas when setting from false to true. If its accessed via a mapping likemapping(address => bool)
, it doesn’t use extra storage space anyway as it will still be a 256byte boolean - Don’t use
public
constants (e.g.string public constant VERSION = "0.1";
oruint256 public constant FEE = 500
;) - set them to private. If someone needs to get access to them, they look at the source. This will save on deployment costs due to no overhead of the public getter functions.
Spotted a typo or have a suggestion to make this crypto dev article better? Please let me know!
Next post
Previous post
📙 Solidity Auditing online quiz
Learn how to audit smart contracts by looking at some example code and trying to find the bugs
⛽ Solidity Gas Optimizations Guide
How to optimize and reduce gas usage in your smart contracts in Solidity
🧪 Guide to testing with Foundry
Guide to adding testing for your Solidity contracts, using the Foundry and Forge tools
📌 Guide to UTXO
UTXO and the UTXO set (used by blockchains such as Bitcoin) explained
📐 Solidity Assembly Guide
Introduction guide to using assembly in your Solidity smart contracts
📦 Ethereum EOF format explained
Information explaining what the upcoming Ethereum EOF format is all about