Gas Optimisation

Economic patterns for gas optimisation

String Equality Comparison

  • String Equality Comparison: Check for the equality of two provided strings in a way that minimizes average gas consumption for a large number of different inputs.

  • Solidity does not yet have a native method for comparing strings. The pairwise comparison method may result in high gas consumption for strings that are long and actually equal.

  • We can use hash functions for comparison, combined with a check for matching length of the provided strings.

function hashCompareWithLengthCheck(string a, string b) internal returns (bool) {
    if(bytes(a).length != bytes(b).length) {
        return false;
    } else {
        return keccak256(a) == keccak256(b);
    }
}

Tight Variable Packing

  • Tight Variable Packing: Optimize gas consumption when storing or loading statically-sized variables.

  • Gas can be saved when reading multiple storage variables if they are all stored in a single slot.

Memory Array Building

  • Memory Array Building: Aggregate and retrieve data from contract storage in a gas efficient way.

  • To reduce gas costs associated with reading storage variables, we store data in an array for efficient data retrieval. A view function can then be implemented to read and return this data.

Example of how a collection of items can be aggregated over its owners:

contract MemoryArrayBuilding {

    struct Item {
        string name;
        string category;
        address owner;
        uint32 zipcode;
        uint32 price;
    }

    Item[] public items;

    mapping(address => uint) public ownerItemCount;

    function getItemIDsByOwner(address _owner) public view returns (uint[]) {
        uint[] memory result = new uint[](ownerItemCount[_owner]);
        uint counter = 0;
        
        for (uint i = 0; i < items.length; i++) {
            if (items[i].owner == _owner) {
                result[counter] = i;
                counter++;
            }
        }
        return result;
    }
}

By labelling a function as view, we get a ‘free’ query because an external call to a view function would require zero gas, while a non-view function would require a transaction on the blockchain.

Mappings over Arrays

  • Except where iteration is required or data types can be packed, it is advised to use mappings to manage lists of data in order to conserve gas. This is beneficial for both memory and storage.

  • An integer index can be used as a key in a mapping to control an ordered list, so you can access any value without having to iterate through an array as would otherwise be necessary.

Batching Transactions

  • Every transaction sent by an EOA has a minimum of 21,000 gas.

  • Batching allows more operations to be executed with less gas.

function batchSend(Call[] memory _calls) public payable {
       for(uint256 i = 0; i < _calls.length; i++) {
           (bool _success, bytes memory _data) = _calls[i].recipient.call{gas: _calls[i].gas, value: _calls[i].value}(_calls[i].data);
           if (!_success) {
              
               assembly { revert(add(0x20, _data), mload(_data)) }
           }
       }
   }

Indexed Events for reducing storage gas

  • Event data is stored in the transaction receipts trie, which can be queried and stored off-chain if the info is required.

  • Search for logged events using indexed parameters.

Use calldata instead of memory for function params

  • Instead of copying variables to memory, it is typically more cost-effective to load them immediately from calldata.

  • If all you need to do is read data, you can conserve gas by saving the data in calldata.

Free up unused storage

  • Deleting your unused variables helps free up space and earns a gas refund. Deleting unused variables has the same effect as reassigning the value type with its default value, such as the integer's default value of 0, or the address zero for addresses.

  • Mappings, however, are unaffected by deletion, as the keys of mappings may be arbitrary and are generally unknown. Therefore, if you delete a struct, all of its members that are not mappings will reset and also recurse into its members. However, individual keys and the values they relate to can be removed.

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.

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
    }
}

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;
}

Use 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++;
	    }
	}
}

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.

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

// 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");
}

Use indexed events as they are less costly compared to non-indexed ones

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

  • This is because you're reading them off the stack instead of putting them in memory.

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
}

Short-circuit with || and &&

Last updated