Skip to content

Instantly share code, notes, and snippets.

@grGred
Last active March 31, 2024 07:18
Show Gist options
  • Save grGred/9bab8b9bad0cd42fc23d4e31e7347144 to your computer and use it in GitHub Desktop.
Save grGred/9bab8b9bad0cd42fc23d4e31e7347144 to your computer and use it in GitHub Desktop.
Solidity gas optimizations and tricks

Upgrade pragma to latest Solidity compiler version


Using newer compiler versions and the optimizer gives gas optimizations and additional safety checks for free!

The advantages of versions 0.8.* over <0.8.0 are:

  • Safemath by default from 0.8.0 (can be more gas efficient than some library based safemath).
  • Low level inliner from 0.8.2, leads to cheaper runtime gas. Especially relevant when the contract has small functions. For example, OpenZeppelin libraries typically have a lot of small helper functions and if they are not inlined, they cost an additional 20 to 40 gas because of 2 extra jump instructions and additional stack operations needed for function calls.
  • Optimizer improvements in packed structs: Before 0.8.3, storing packed structs, in some cases used an additional storage read operation. After EIP-2929, if the slot was already cold, this means unnecessary stack operations and extra deploy time costs. However, if the slot was already warm, this means additional cost of 100 gas alongside the same unnecessary stack operations and extra deploy time costs.
  • Custom errors from 0.8.4, leads to cheaper deploy time cost and run time cost. Note: the run time cost is only relevant when the revert condition is met. In short, replace revert strings by custom errors.
  • Solidity 0.8.10 has a useful change which reduced gas costs of external calls which expect a return value. Code Generator skips existence check for external contract if return data is expected. In this case, the ABI decoder will revert if the contract does not exist. 0.8.10 also enables the new EVM code generator for pure Yul mode.
  • Improved Inlining Heuristics in Yul Optimizer. The compiler used to be very conservative before Solidity version 0.8.15 in deciding whether to inline a function or not. This was necessary due to the fact that inlining may easily increase stack pressure and lead to the dreaded Stack too deep error. In 0.8.15 the conditions necessary for inlining are relaxed. Benchmarks show that the change significantly decreases the bytecode size (which impacts the deployment cost) while the effect on the runtime gas usage is smaller.
  • Overflow checks on multiplication more efficient in Solidity v0.8.17. Yul Optimizer: Prevent the incorrect removal of storage writes before calls to Yul functions that conditionally terminate the external EVM call; Simplify the starting offset of zero-length operations to zero. Code Generator: More efficient overflow checks for multiplication.

For loops improvement:

  • Caching the length in for loops

Reading array length at each iteration of the loop takes 6 gas (3 for mload and 3 to place memory_offset ) in the stack. Caching the array length in the stack saves around 3 gas per iteration. I suggest storing the array’s length in a variable before the for-loop.

Example of an array arr and the following loop:

for (uint i = 0; i < length; i++) {
    // do something that doesn't change the value of i
}

In the above case, the solidity compiler will always read the length of the array during each iteration.

  1. If it is a storage array, this is an extra sload operation (100 additional extra gas (EIP-2929) for each iteration except for the first),
  2. If it is a memory array, this is an extra mload operation (3 additional gas for each iteration except for the first),
  3. If it is a calldata array, this is an extra calldataload operation (3 additional gas for each iteration except for the first) This extra costs can be avoided by caching the array length (in stack):
uint length = arr.length;
for (uint i = 0; i < length; i++) {
    // do something that doesn't change arr.length
}

In the above example, the sload or mload or calldataload operation is only called once and subsequently replaced by a cheap dupN instruction. Even though mload, calldataload and dupN have the same gas cost, mload and calldataload needs an additional dupN to put the offset in the stack, i.e., an extra 3 gas.

This optimization is especially important if it is a storage array or if it is a lengthy for loop.

  • The increment in for loop post condition can be made unchecked

In Solidity 0.8+, there’s a default overflow check on unsigned integers. It’s possible to uncheck this in for-loops and save some gas at each iteration, but at the cost of some code readability, as this uncheck cannot be made inline.

Example for loop:

for (uint i = 0; i < length; i++) {
    // do something that doesn't change the value of i
}

In this example, the for loop post condition, i.e., i++ involves checked arithmetic, which is not required. This is because the value of i is always strictly less than length <= 2**256 - 1. Therefore, the theoretical maximum value of i to enter the for-loop body is 2**256 - 2. This means that the i++ in the for loop can never overflow. Regardless, the overflow checks are performed by the compiler.

Unfortunately, the Solidity optimizer is not smart enough to detect this and remove the checks. You should manually do this by:

for (uint i = 0; i < length; i = unchecked_inc(i)) {
    // do something that doesn't change the value of i
}

function unchecked_inc(uint i) returns (uint) {
    unchecked {
        return i + 1;
    }
}

Or just:

for (uint i = 0; i < length;) {
    // do something that doesn't change the value of i
    unchecked { i++; }
}

Note that it’s important that the call to unchecked_inc is inlined. This is only possible for solidity versions starting from 0.8.2.

Gas savings: roughly speaking this can save 30-40 gas per loop iteration. For lengthy loops, this can be significant! (This is only relevant if you are using the default solidity checked arithmetic.)

  • ++i costs less gas compared to i++ or i += 1

++i costs less gas compared to i++ or i += 1 for unsigned integer, as pre-increment is cheaper (about 5 gas per iteration). This statement is true even with the optimizer enabled.

Example: i++ increments i and returns the initial value of i. Which means:

uint i = 1; 
i++; // == 1 but i == 2 

But ++i returns the actual incremented value:

uint i = 1; 
++i; // == 2 and i == 2 too, so no need for a temporary variable 

In the first case, the compiler has to create a temporary variable (when used) for returning 1 instead of 2

  • No need to explicitly initialize variables with default values

If a variable is not set/initialized, it is assumed to have the default value (0 for uint, false for bool, address(0) for address…). Explicitly initializing it with its default value is an anti-pattern and wastes gas. As an example: for (uint256 i = 0; i < numIterations; ++i) { should be replaced with: for (uint256 i; i < numIterations; ++i) {

  • Don't remove initialization of i varible in for loops

I see a lot of projects where developers mistakenly believe that the removal of i vatiable outside of the for loop will save gas. In following snippets you can see that this is wrong:

    function loopCheck1(uint256[] memory arr) external returns (uint256[] memory) {
        gas = gasleft(); // 29863 gas
        uint length = arr.length;
        for (uint i; i < length;) {
            unchecked { ++i; }
        }
        return arr;
        gas -= gasleft();
    }
    
    function loopCheck2(uint256[] memory arr) external  returns (uint256[] memory) {
        gas = gasleft();
        uint i;
        uint length = arr.length;
        for (; i < length;) { // 29912 gas
            unchecked { ++i; }
        }
        return arr;
        gas -= gasleft();
    }
  • To sum up, the best gas optimized loop will be:
uint length = arr.length;
for (uint i; i < length;) {
    unchecked { ++i; }
}

Use calldata instead of memory for function parameters

In some cases, having function arguments in calldata instead of memory is more optimal. When arguments are read-only on external functions, the data location should be calldata.

Example:

contract C {
    function add(uint[] memory arr) external returns (uint sum) {
        uint length = arr.length;
        for (uint i = 0; i < arr.length;) {
            sum += arr[i];
            unchecked { ++i; }
        }
    }
}

In the above example, the dynamic array arr has the storage location memory. When the function gets called externally, the array values are kept in calldata and copied to memory during ABI decoding (using the opcode calldataload and mstore). And during the for loop, arr[i] accesses the value in memory using a mload. However, for the above example this is inefficient. Consider the following snippet instead:

contract C {
    function add(uint[] calldata arr) external returns (uint sum) {
        uint length = arr.length;
        for (uint i = 0; i < arr.length;) {
            sum += arr[i];
            unchecked { ++i; }
        }
    }
}

In the above snippet, instead of going via memory, the value is directly read from calldata using calldataload. That is, there are no intermediate memory operations that carries this value.

Gas savings: In the former example, the ABI decoding begins with copying value from calldata to memory in a for loop. Each iteration would cost at least 60 gas. In the latter example, this can be completely avoided. This will also reduce the number of instructions and therefore reduces the deploy time cost of the contract.

In short, use calldata instead of memory if the function argument is only read.

Note that in older Solidity versions, changing some function arguments from memory to calldata may cause “unimplemented feature error”. This can be avoided by using a newer (0.8.*) Solidity compiler.


Change state variables to immutable where possible

Solidity 0.6.5 introduced immutable as a major feature. It allows setting contract-level variables at construction time which gets stored in code rather than storage.

Example:

contract C {
    /// The owner is set during contruction time, and never changed afterwards.
    address public owner = msg.sender;
}

In the above example, each call to the function owner() reads from storage, using a sload. After EIP-2929, this costs 2100 gas cold or 100 gas warm. However, the following snippet is more gas efficient:

contract C {
    /// The owner is set during contruction time, and never changed afterwards.
    address public immutable owner = msg.sender;
}

In the above example, each storage read of the owner state variable is replaced by the instruction push32 value, where value is set during contract construction time. Unlike the last example, this costs only 3 gas.


Change constant to immutable for keccak variables

Use of constant keccak variables results in extra hashing (and so gas). This results in the keccak operation being performed whenever the variable is used, increasing gas costs relative to just storing the output hash. Changing to immutable will only perform hashing on contract deployment which will save gas. You should use immutables until the referenced issues are implemented, then you only pay the gas costs for the computation at deploy time.

Example:

contract Immutables is AccessControl {
    uint256 public gas;

    bytes32 public immutable MANAGER_ROLE_IMMUT;
    bytes32 public constant MANAGER_ROLE_CONST = keccak256('MANAGER_ROLE');

    constructor(){
        MANAGER_ROLE_IMMUT = keccak256('MANAGER_ROLE');
        _setupRole(MANAGER_ROLE_CONST, msg.sender);
        _setupRole(MANAGER_ROLE_IMMUT, msg.sender);
    }

    function immutableCheck() external {
        gas = gasleft();
        require(hasRole(MANAGER_ROLE_IMMUT, msg.sender), 'Caller is not in manager role'); // 24408 gas
        gas -= gasleft();
    }

    function constantCheck() external {
        gas = gasleft();
        require(hasRole(MANAGER_ROLE_CONST, msg.sender), 'Caller is not in manager role'); // 24419 gas
        gas -= gasleft();
    }
}

As you can see on the compiler version 0.8.15 and with optimizator on 200 runs immutables are cheaper, and saves you about 20 gas. For other variables, constants are equal to immutables.

See: (ethereum/solidity#9232 (comment), Inefficient Hash Constants)


Consider having short revert strings

Consider the following require statement:

// condition is boolean
// str is a string
require(condition, str)

The string str is split into 32-byte sized chunks and then stored in memory using mstore, then the memory offsets are provided to revert(offset, length). For chunks shorter than 32 bytes, and for low --optimize-runs value (usually even the default value of 200), instead of push32 val, where val is the 32 byte hexadecimal representation of the string with 0 padding on the least significant bits, the solidity compiler replaces it by shl(value, short-value)). Where short-value does not have any 0 padding. This saves the total bytes in the deploy code and therefore saves deploy time cost, at the expense of extra 6 gas during runtime. This means that shorter revert strings saves deploy time costs of the contract. Note that this kind of saving is not relevant for high values of --optimize-runs as push32 value will not be replaced by a shl(..., ...) equivalent by the Solidity compiler.

Going back, each 32 byte chunk of the string requires an extra mstore. That is, additional cost for mstore, memory expansion costs, as well as stack operations. Note that, this runtime cost is only relevant when the revert condition is met.

Overall, shorter revert strings can save deploy time as well as runtime costs.

Note that if your contracts already allow using at least Solidity 0.8.4, then consider using Custom errors. This is more gas efficient, while allowing the developer to describe the errors in detail using NatSpec. A disadvantage to this approach is that, some tooling may not have proper support for this.


Use clones for cheap contract deployments

Example:

     function _executeTransfer(address _owner, uint256 _idx) internal {
         (bytes32 salt, ) = precompute(_owner, _idx);
         new FlashEscrow{salt: salt}( //gas: deployment can cost less through clones
             nftAddress,
             _encodeFlashEscrowPayload(_idx)
         );
     }

There’s a way to save a significant amount of gas on deployment using Clones: OpenZeppelin video This is a solution that was adopted, as an example, by Porter Finance. They realized that deploying using clones was 10x cheaper. I suggest applying a similar pattern in factory contracts.

See: porter-finance/v1-core#15 (comment) porter-finance/v1-core#34


Use modifiers instead of functions to save gas

Example of two contracts with modifiers and internal view function:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.9;

contract Inlined {
    function isNotExpired(bool _true) internal view {
        require(_true == true, "Exchange: EXPIRED");
    }

    function foo(bool _test) public returns(uint){
            isNotExpired(_test);
            return 1;
    }
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.9;

contract Modifier {
modifier isNotExpired(bool _true) {
        require(_true == true, "Exchange: EXPIRED");
        _;
    }

function foo(bool _test) public isNotExpired(_test)returns(uint){
        return 1;
    }
}

Differences:

Deploy Modifier.sol
108727
Deploy Inlined.sol
110473
Modifier.foo
21532
Inlined.foo
21556

This with 0.8.9 compiler and optimization enabled. As you can see it's cheaper to deploy with modifier, and it will save you about 30 gas. But sometimes modifiers increase code size of the contract.


Use internal view functions in modifiers to save bytecode

It is recommended to move the modifiers require statements into an internal virtual function. This reduces the size of compiled contracts that use the modifiers. Putting the require in an internal function decreases contract size when modifier is used multiple times. There is no difference in deployment gas cost with private and internal functions.

With Solidity 0.8.14 and optimisations on (200):

contract Ownable is Context {
    address public owner = _msgSender();

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

contract Ownable2 is Context {
    address public owner = _msgSender();

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

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

// This is deployment gas cost for each function
// 0: 107172
// 1: 145772
// 2: 181610
// 3: 198170
// 4: 214532
// 5: 241059
contract T1 is Ownable {
    event Call(bytes4 selector);
    function f0() external onlyOwner() { emit Call(this.f0.selector); }
    function f1() external onlyOwner() { emit Call(this.f1.selector); }
    function f2() external onlyOwner() { emit Call(this.f2.selector); }
    function f3() external onlyOwner() { emit Call(this.f3.selector); }
    function f4() external onlyOwner() { emit Call(this.f4.selector); }
}

// 0: 107172
// 1: 147908
// 2: 165818
// 3: 183506
// 4: 192500
// 5: 211682
contract T2 is Ownable2 {
    event Call(bytes4 selector);
    function f0() external onlyOwner() { emit Call(this.f0.selector); }
    function f1() external onlyOwner() { emit Call(this.f1.selector); }
    function f2() external onlyOwner() { emit Call(this.f2.selector); }
    function f3() external onlyOwner() { emit Call(this.f3.selector); }
    function f4() external onlyOwner() { emit Call(this.f4.selector); }
}

See: Optimize Ownable and Pausable modifiers' size impact #3347 Reduce contract size and deployment gas for onlyOwner modifier


>= is cheaper than >

Non-strict inequalities (>=) are cheaper than strict ones (>). This is due to some supplementary checks (ISZERO, 3 gas)).

    uint256 public gas;

    function checkStrict() external {
        gas = gasleft();
        require(999999999999999999 > 1); // gas 5017
        gas -= gasleft();
    }

    function checkNonStrict() external {
        gas = gasleft();
        require(999999999999999999 >= 1); // gas 5006
        gas -= gasleft(); 
    }

Gas savings: non-strict inequalities will save you 15-20 gas.


> 0 is cheaper than != 0 sometimes

!= 0 costs less gas compared to > 0 for unsigned integers in require statements with the optimizer enabled. But > 0 is cheaper than !=, with the optimizer enabled and outside a require statement. https://twitter.com/gzeon/status/1485428085885640706

Example with optimizer disabled:

    uint256 public gas;

    function check1() external {
        gas = gasleft();
        require(99999999999999 != 0); // gas 22136 --disabled optimizer
        gas -= gasleft();
    }

    function check2() external {
        gas = gasleft();
        require(99999999999999 > 0); // gas 22136 --disabled optimizer
        gas -= gasleft();
    }

    function check3() external {
        gas = gasleft();
        if (99999999999999 != 0){ // 22149 gas --disabled optimizer
            uint256 i = 123;
        }
        gas -= gasleft();
    }

    function check4() external {
        gas = gasleft();
        if (99999999999999 > 0){ // 22152 gas --disabled optimizer
            uint256 i = 123;
        }
        gas -= gasleft();
    }

Example with optimizer enabled:

    uint256 public gas;

    function check1() external {
        gas = gasleft();
        require(99999999999999 != 0); // gas 22106 --enabled optimizer
        gas -= gasleft();
    }

    function check2() external {
        gas = gasleft();
        require(99999999999999 > 0); // gas 22117 --enabled optimizer
        gas -= gasleft();
    }

    function check3() external {
        gas = gasleft();
        if (99999999999999 != 0){ // 22106 gas --enabled optimizer
            uint256 i = 123;
        }
        gas -= gasleft();
    }

    function check4() external {
        gas = gasleft();
        if (99999999999999 > 0){ // 22105 gas --enabled optimizer
            uint256 i = 123;
        }
        gas -= gasleft();
    }

Gas savings: it will save you about 10 gas.

To sum up on 0.8.15:
    Without optimizer:
        In require:
            `> 0` equals to `!= 0`
        Outside require:
            `> 0` more expensive than `!= 0` 
    With optimizer:
        In require:
            `> 0` more expensive than `!= 0`
        Outside require:
            `> 0` cheaper than `!= 0`

See: https://twitter.com/gzeon/status/1485428085885640706


Use Shift Right/Left instead of Division/Multiplication if possible

A division/multiplication by any number x being a power of 2 can be calculated by shifting log2(x) to the right/left. While the DIV opcode uses 5 gas, the SHR opcode only uses 3 gas. Furthermore, Solidity's division operation also includes a division-by-0 prevention which is bypassed using shifting.


Use double require instead of operator &&

contract Requires {
    uint256 public gas;

    function check1(uint x) public {
        gas = gasleft();    
        require(x == 0 && x < 1 ); // gas cost 22156
        gas -= gasleft();
    }

    function check2(uint x) public {
        gas = gasleft(); 
        require(x == 0); // gas cost 22148
        require(x < 1);
        gas -= gasleft();
    }
}

Gas savings: Usage of double require will save you around 10 gas with the optimizer enabled.


Use custom errors instead of revert strings to save gas

I already mentioned it earlier, and I strongly recommend use this. Custom errors from Solidity 0.8.4 are cheaper than revert strings (cheaper deployment cost and runtime cost when the revert condition is met) Source: https://blog.soliditylang.org/2021/04/21/custom-errors/: Starting from Solidity v0.8.4, there is a convenient and gas-efficient way to explain to users why an operation failed through the use of custom errors. Until now, you could already use strings to give more information about failures (e.g., revert("Insufficient funds.");), but they are rather expensive, especially when it comes to deploy cost, and it is difficult to use dynamic information in them. Custom errors are defined using the error statement, which can be used inside and outside of contracts (including interfaces and libraries).


There is no difference in gas cost between public/external/internal/private for vairables and functions

It's still better to define function visibility strictly because of security reasons, but it won't affect gas usage. But it would affect deployment gas cost for such functions. There is no difference in internal and private functions in deployment gas cost.

contract Requires {
    uint256 public gas;

    function check1(uint x) public {
        gas = gasleft();    
        require(x == 0 && x < 1 ); // gas cost 5131
        x += 1;
        gas -= gasleft();
    }

    function check2(uint x) public {
        gas = gasleft(); 
        require(x == 0 && x < 1 ); // gas cost 5131
        x += 1;
        gas -= gasleft();
    }
    
    function check3(uint x) public {
        gas = gasleft(); 
        _check3(x); // gas cost 5157
        gas -= gasleft();
    }

    function _check3(uint x) internal pure {  
        require(x == 0 && x < 1 );
        x += 1;
    }

    function check4(uint x) public {
        gas = gasleft(); 
        _check4(x); // gas cost 5157
        gas -= gasleft();
    }
    
    function _check4(uint x) private pure {
        require(x == 0 && x < 1 );
        x += 1;
    }

    function check5(uint x) public {
        gas = gasleft(); 
        _check5(x); // gas cost 5157
        gas -= gasleft();
    }

    function _check5(uint x) internal {  
        require(x == 0 && x < 1 );
        x += 1;
    }

    function check6(uint x) public {
        gas = gasleft(); 
        _check6(x); // gas cost 5157
        gas -= gasleft();
    }
    
    function _check6(uint x) private {
        require(x == 0 && x < 1 );
        x += 1;
    }
}

Public variables cost the same amount as the internal and private ones:

    uint256 public gas;

    uint256 public x1 = 9999999999999;
    uint256 private x2 = 9999999999999;
    uint256 internal x3 = 9999999999999;


    function check1() public {
        gas = gasleft();   
        x1 += 1; // public - 10108 gas
        gas -= gasleft();   
    }

    function check2() public {
        gas = gasleft();   
        x2 += 1; // private - 10108 gas
        gas -= gasleft();   
    }

    function check3() public {
        gas = gasleft();   
        x3 += 1; // internal - 10108 gas
        gas -= gasleft();   

By the way, variable visability affect on deploy gas. public variables cost more than private or internal, but there is no difference in deployment gas cost between private and internal.


Caching storage variables in memory to save gas

Anytime you are reading from storage more than once, it is cheaper in gas cost to cache the variable in memory: a SLOAD cost 100gas, while MLOAD and MSTORE cost 3 gas.

Gas savings: at least 97 gas.


Be careful with copying whole struct to memory

Sometimes it's better to use storage instead of copying struct in memory. Example:

// struct LockPosition use 3 slots
// struct LockPosition {
// address owner;
// uint256 unlockAt;
// uint256 lockAmount;
//}

     function unlock(uint256 _nftIndex) external nonReentrant {
         LockPosition memory position = positions[_nftIndex]; // gas: costing 3 SLOADs while only lockAmount is needed twice. 
         //Replace "memory" with "storage" and cache only position.lockAmount

         require(position.owner == msg.sender, "unauthorized");
         require(position.unlockAt <= block.timestamp, "locked");

         delete positions[_nftIndex];

         jpeg.safeTransfer(msg.sender, position.lockAmount);

         emit Unlock(msg.sender, _nftIndex, position.lockAmount);
     }

Here, a copy in memory is costing 3 SLOADs and 3 MSTORES. The, 2 variables are only read once through MLOAD (position.owner and position.unlockAt) and one is read twice (position.lockAmount). It's better to replace the memory keyword with storage and only copying position.lockAmount in memory.


Help the optimizer by saving a storage variable's reference

To help the optimizer, declare a storage type variable and use it instead of repeatedly fetching the reference in a map or an array. The effect can be quite significant.

function borrow(
   Position storage position = positions[_nftIndex];

Leave 1 wei of token to reduce gas cost for transfer

When your smart contract does an ERC-20 transfer, always leave 1 unit of the smallest denomination of the token in your smart contract balance. It’ll save gas the next time you interact with the token.


Using Addresses with lots of leading zeroes

Well if you have 2 addresses - 0x000000a4323… and 0x0000000000f38210 because of the leading zeroes you can pack them both into the same storage slot, then just prepend the necessary amount of zeroes when using them. This saves you storage when doing things such as checking the owner of a contract.


Writing to an existing Storage Slot is cheaper than using a new one

EIP - 2200 changed a lot with gas, and now if you hold 1 Wei of a token it’s cheaper to use the token than if you hold 0. There is a lot to unpack here so just google EIP 2200 and learn if you want, but in general, if you need to use a storage slot, don’t empty it if you plan to re-fill it later.


Pack Structs in Solidity

Struct packing article here

A basic optimization but important to know, structs should be organized so that they sequentially add up to multiples of 256 bits in size. So use:

Struct {
    uint112 
    uint112 
    uint256 
}

Instead of:

Struct {
    uint112 
    uint256 
    uint112
}

The same thing with the order of variables in contract, follow the the optimized order to save gas for deployments. So use:

contract Test {
    uint256 // 2 storage slots
    address // address is 20 bytes and bool is 1 byte are packed in one slot
    bool
}

Instead of:

contract Test {
    address // 3 storage slots
    uint256 
    bool
}

Use gasleft() to measure used gas instead of checking transatcion gas cost, in order to find gas optimization

While auditing code for gas optimization improvements it's very convinient to have hardhat gas reporter. It will show gas usage per unit test. But if you don't have time to initialize new project and write tests for your function, or you want to check a myth about gas optimization, you shouldn't look on a gas cost of the transaction. Don't forget that in Solidity function names and their order in the contract take different amount of gas.

Example:

contract Test {
    function a() public { // 125 gas
    }
    
    function b() public { // 147 gas
    }
}

That's why you should wrap the body of the transaction as:

contract Test {
uint256 public gas;
    
    function a() public {
        gas = gasleft();
        doStuff();
        gas -= gasleft();
    }
    
    function b() public {
        gas = gasleft();
        doStuff();
        gas -= gasleft();
    }
}

And don't forget to test both functions with the same conditions. Remember the tip "Writing to an existing Storage Slot is cheaper than using a new one".

@OnahProsperity
Copy link

Nice...

@ko1ebayev
Copy link

very interesting&helpfull stuff, thanks Vlad 👍

@playmonkey
Copy link

This is gold, thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment