Reach out for an audit or to learn more about Macro
orMessage on Telegram
Macro Logo

Solidity Gas Optimizations Cheat Sheet

Jul 28, 2022
Author avatar
curiousapple
Lead Security Researcher
Author avatar
Tom
Lead Security Researcher

Solidity is currently the most popular language for the EVM. However, Solidity's compiler is still in its early stages, meaning there are a number of things you should keep in mind while using it to write the best and most efficient code.

One of those things is gas optimizations. On Ethereum, gas is very expensive. While we don't recommend going crazy for minimal savings, below is a list of low-hanging fruit you can apply to your contracts today.

đź’ˇ Also see Gas Numbers Every Solidity Dev Should Know

1. Packing of storage variables

Pack variables in one slot by defining them in a lower data type. Packing only helps when multiple variables of the packed slot are being accessed in the same call. If not done correctly, it increases gas costs instead, due to shifts required.

Before:

contract MyContract {
  uint32  x; // Storage slot 0
  uint256 y; // Storage slot 1
  uint32  z; // Storage slot 2
}

After:

contract MyContract {
  uint32 x;  // Storage slot 0
  uint32 z;  // Storage slot 0
  uint256 y; // Storage slot 1
}

2. Local variable assignment

Catch frequently used storage variables in memory/stack, converting multiple SLOAD into 1 SLOAD.

Before:

uint256 length = 10;

function loop() public {
    for (uint256 i = 0; i < length; i++) {
        // do something here
    }
}

After:

uint256 length = 10;

function loop() {
    uint256 l = length;

    for (uint256 i = 0; i < l; i++) {
        // do something here
    }
}

3. Use fixed size bytes array rather than string or bytes[]

If the string you are dealing with can be limited to max of 32 characters, use bytes[32] instead of dynamic bytes array or string.

Before:

string a;
function add(string str) {
    a = str;
}

After:

bytes32 a;
function add(bytes32 str) public {
    a = str;
}

4. Use immutable and constant

Use immutable if you want to assign a permanent value at construction. Use constants if you already know the permanent value. Both get directly embedded in bytecode, saving SLOAD.

contract MyContract {
    uint256 constant y = 10;
    uint256 immutable x;

    constructor() {
        x = 5;
    } 
 }

5. Using unchecked

Use unchecked for arithmetic where you are sure it won't over or underflow, saving gas costs for checks added from solidity v0.8.0.

In the example below, the variable i cannot overflow because of the condition i < length, where length is defined as uint256. The maximum value i can reach is max(uint)-1. Thus, incrementing i inside unchecked block is safe and consumes lesser gas.

function loop(uint256 length) public {
	for (uint256 i = 0; i < length; ) {
	    // do something
	    unchecked {
	        i++;
	    }
	}
}

6. Use calldata instead of memory for function parameters [Ref]

It is generally cheaper to load variables directly from calldata, rather than copying them to memory. Only use memory if the variable needs to be modified.

Before:

function loop(uint[] memory arr) external pure returns (uint sum) {
    for (uint i = 0; i < arr.length; i++) {
        sum += arr[i];
    }
}

After:

function loop(uint[] calldata arr) external pure returns (uint sum) {
    for (uint i = 0; i < arr.length; i++) {
        sum += arr[i];
    }
}

7. Use custom errors to save deployment and runtime costs in case of revert

Instead of using strings for error messages (e.g., require(msg.sender == owner, “unauthorized”)), you can use custom errors to reduce both deployment and runtime gas costs. In addition, they are very convenient as you can easily pass dynamic information to them.

Before:

function add(uint256 _amount) public {
    require(msg.sender == owner, "unauthorized");

    total += _amount;
}

After:

error Unauthorized(address caller);

function add(uint256 _amount) public {
    if (msg.sender != owner)
        revert Unauthorized(msg.sender);

    total += _amount;
}

8. Refactor a modifier to call a local function instead of directly having the code in the modifier, saving bytecode size and thereby deployment cost [Ref]

Modifiers code is copied in all instances where it's used, increasing bytecode size. By doing a refractor to the internal function, one can reduce bytecode size significantly at the cost of one JUMP. Consider doing this only if you are constrained by bytecode size.

Before:

modifier onlyOwner() {
		require(owner() == msg.sender, "Ownable: caller is not the owner");
		_;
}

After:

modifier onlyOwner() {
		_checkOwner();
		_;
}

function _checkOwner() internal view virtual {
    require(owner() == msg.sender, "Ownable: caller is not the owner");
}

9. Use indexed events as they are less costly compared to non-indexed ones [Ref]

Using the indexed keyword for value types such as uint, bool, and address saves gas costs, as seen in the example below. However, this is only the case for value types, whereas indexing bytes and strings are more expensive than their unindexed version.

Before:

event Withdraw(uint256, address);

function withdraw(uint256 amount) public {
    emit Withdraw(amount, msg.sender);
}

After:

event Withdraw(uint256 indexed, address indexed);

function withdraw(uint256 amount) public {
    emit Withdraw(amount, msg.sender);
}

10. Use struct when dealing with different input arrays to enforce array length matching

When the length of all input arrays needs to be the same, use a struct to combine multiple input arrays so you don't have to manually validate their lengths.

Before:

function vote(uint8[] calldata v, bytes[32] calldata r,  bytes[32] calldata s) public {
		require(v.length == r.length == s.length, "not matching");
}

After:

struct Signature {
        uint8 v;
        bytes32 r;
        bytes32 s;
    }

function vote(Signature[] calldata sig) public {
    // no need for length check
}

11. Provide methods for batch operations

If applicable, provide a method for batch action to users, reducing the overall gas costs.

For example: In the following case, if the user wants to call doSomething() for 10 different inputs. In the optimized version, the user would need to spend the fixed gas cost of the transaction and the gas cost for msg.sender check only once.

Before:

function doSomething(uint256 x, uint256 y, uint256 z) public {
		require msg.sender == registeredUser
		....
}

After:


function batchDoSomething(uint256[] x, uint256[] y, uint256[] z) public {
		require msg.sender == registeredUser
    loop
			_doSomething(x[i], y[i], z[i])
}
function doSomething(uint256 x, uint256 y, uint256 z) public {
		require msg.sender == registeredUser
		_doSomething(x, y, z);
}
function _doSomething(uint256 x, uint256 y, uint256 z) internal {
		....
}

12. Short-circuit with || and &&

For || and && operators, the second case is not checked if the first case gives the result of the logical expression.  Put the lower-cost expression first so the higher-cost expression may be skipped (short-circuit).

Before:

case 1: 100 gas
case 2: 50 gas

if(case1 && case2) revert 
if(case1 || case2) revert 

After:

if(case2 && case1) revert 
if(case2 || case1) revert 


General Rules


Optimize For

1. Common cases >> Rare cases

One should optimize for common cases. Optimizing for rare cases can increase gas costs for all other cases. For example, skipping balance update in case of 0 amount transfer.

As 0 amount transfer is not common, it doesn't make sense to add the cost of one condition for all other cases.

However, if the case is common, like transfer in case of infinite allowance, one should do it.

Unlimited ERC20 token allowance · Issue #717 · ethereum/EIPs

2. Runtime cost >> Deployment cost

Deployment costs are one-time, so always optimize for runtime costs.
If you are constrained by bytecode size, then optimize for deployment cost.

3. User Interaction >> Admin Interaction

As user interaction is most common, prioritize optimizing for it compared to admin actions.


Soon we will follow up with degen gas optimizations, which go to extreme lengths to save gas costs.

For example: Function Ordering, ≥ and > comparison, payable, address with leading zeros…

We hope you found this post helpful. If you want to get in touch with us to chat about audits, please click the link below:

Audits | Macro

References :

Some generic writeup about common gas optimizations, etc.

https://mudit.blog/solidity-gas-optimization-tips/