Solidity – How to Write an Optimized Gas-Cost Smart Contract

contract-deploymentcontract-designcontract-developmentgassolidity

As we all know, there are many factors that determine a good smart contract, such as:

  • Security: it has minimal/zero vulnerability so they cannot be exploited by an adversary. Immune to Attacks.

  • Cost: how much in total a

      (a) smart contract deployment costs, 
    
      (b) running/invoking each function of it costs. 
    
  • Correctness: it executes as it has been planned.


In this question, I'd like to focus on the cost of a smart contract.

Question 1: How to make a cost-effective smart contract?

Question 2: How to avoid using some expensive functions and what are the alternatives? In other words, are there any functions well-known to be more expensive than other operations and can we replace them with better ones?

Question 3: In general, what are the good practice of writing a smart contract with minimal cost?

In short: How can we save gas : ether : money?

Best Answer

In Ethereum, transactions cost gas and hence ether. The gas consumption of a transaction depends on the opcodes that the EVM has to execute. The gas cost for each Opcode can be found as explained in this question. Few common opcodes and gas are,

Operation         Gas           Description

ADD/SUB           3             Arithmetic operation
MUL/DIV           5             Arithmetic operation
ADDMOD/MULMOD     8             Arithmetic operation
AND/OR/XOR        3             Bitwise logic operation
LT/GT/SLT/SGT/EQ  3             Comparison operation
POP               2             Stack operation 
PUSH/DUP/SWAP     3             Stack operation
MLOAD/MSTORE      3             Memory operation
JUMP              8             Unconditional jump
JUMPI             10            Conditional jump
SLOAD             200           Storage operation
SSTORE            5,000/20,000  Storage operation
BALANCE           400           Get balance of an account
CREATE            32,000        Create a new account using CREATE
CALL              25,000        Create a new account using CALL

This is a concern when it comes to smart contracts as transactions are also involved and it's important to consider the gas cost when designing a contract.

Reducing the gas consumed by a contract is important in two situations,

  1. Cost of deploying a contract
  2. Cost to call the contract functions

Cost of deploying a contract

For this, most of the optimizations are done at compilation time as described in the documentation-faqs,

Are comments included with deployed contracts and do they increase deployment gas?

No, everything that is not needed for execution is removed during compilation. This includes, among others, comments, variable names and type names.

And the details of the optimizer can be found here.

Another way of reducing the size is by removing useless code. For example:

1 function p1 ( uint x ){ 
2    if ( x > 5)
3     if ( x*x < 20)
4        XXX }

In above code, line 3 and 4 will never be executed and these type of useless code can be avoided by carefully going through the contract logic and that will reduce the size of the smart contract.

Cost to call the contract functions

When contracts' functions are called, for the execution of function it costs gas. Hence optimizing functions to use less gas is important. There can be many different ways of doing it when individual contract is considered. Here are few that might save gas during execution,

  1. Reduce Expensive operations

Expensive operations are the opcodes that has more gas values such as SSTORE. Below are some methods of reducing expensive operations.

A) Use of Short Circuiting rules

The operators || and && apply the common short-circuiting rules. This means that in the expression f(x) || g(y), if f(x) evaluates to true, g(y) will not be evaluated even if it may have side-effects.

So if a logical operation includes an expensive operation and a low cost operation, arranging in a way that the expensive operation can be short circuited will reduce gas at some executions.

If f(x) is low cost and g(y) is expensive, arranging logical operations

  • OR : f(x) || g(y)
  • AND : f(x) && g(y)

will save more gas if short circuited.

If f(x) has a considerably higher probability of returning false compared to g(y), arranging AND operations as f(x) && g(y) might cause to save more gas in execution by short circuiting.

If f(x) has a considerably higher probability of returning true compared to g(y), arranging OR operations as f(x) || g(y) might cause to save more gas in execution by short circuiting.

B) expensive operations in a loop

eg:

 uint sum = 0;
 function p3 ( uint x ){
     for ( uint i = 0 ; i < x ; i++)
         sum += i; }

In the above code, since sum storage variable is read and written every time inside the loop, storage operations that are expensive take place at every iteration. This can be avoided by introducing a local variable as follows to save gas.

 uint sum = 0;
 function p3 ( uint x ){
     uint temp = 0;
     for ( uint i = 0 ; i < x ; i++)
         temp += i; }
     sum += temp;
  1. Other loop related patterns

loop combining

function p5 ( uint x ){
    uint m = 0;
    uint v = 0;
    for ( uint i = 0 ; i < x ; i++) //loop-1
        m += i;
    for ( uint j = 0 ; j < x ; j++) //loop-2
        v -= j; }

loop-1 and loop-2 can be combined and gas can be saved,

 function p5 ( uint x ){
    uint m = 0;
    uint v = 0;
    for ( uint i = 0 ; i < x ; i++) //loop-1
       m += i;
       v -= j; }

and a few more loop patterns can be found here.

  1. Using of Fixed-size bytes arrays

From Docs,

It is possible to use an array of bytes as byte[], but it is wasting a lot of space, 31 bytes every element, to be exact, when passing in calls. It is better to use bytes.

and

As a rule of thumb, use bytes for arbitrary-length raw byte data and string for arbitrary-length string (UTF-8) data. If you can limit the length to a certain number of bytes, always use one of bytes1 to bytes32 because they are much cheaper.

having a fixed length always saves gas. Refer to this question as well.

  1. Removing useless code as explained earlier under contract deployment will save gas even when functions are executed, if that can be done inside functions.

  2. Not using libraries when implementing the functionality is cheaper for simple usages.

Calling library for simple usages may be costly. If the functionality is simple and feasible to implement inside the contract as it avoids the step of calling the library. execution cost for the functionality only will still be the same for both.

  1. Using visibility external for the functions only accessed externally forces to use calldata as the parameter location and this saves some gas when the function executes.

  2. Using memory variables within functions locally when possible saves gas of accessing the storage.

These are some ways of saving gas and there may be many other methods depending on the requirements.

Related Topic