Smart Contract Optimization Part-2 : Understanding the Factors

Atharva Paliwal
Coinmonks
8 min readJan 20, 2023

--

This is the part-2 for the series that’s going on optimizing your smart contracts. In the Part-1 of the series we learnt about some of the fundamentals that were necessary for this article to proceed upon.

In this article we will solely focus on the factors that are essential for optimizing smart contract. We discussed about the factors in Part-1 in brief but now we will focus on the factors in depth.

The factors that we are going to discuss are —

  1. Opcode
  2. State Change
  3. Transactions
  4. Memory

So, let’s start learning about them in depth!!!

1. Opcode

Going by definition, Opcode is the portion of the machine language instruction that specifies the operation to be performed.

Well, let me guess!! You did not understood the definition :) Don’t worry we will understand this as we move ahead.

I expect that most of you might have return a code in some language, where you might have given some instructions in your code like addition of variables, subtraction etc. Now this instructions and code is converted into a machine understandable language i.e. in the binary form. This instructions are then internally divided into 3 parts —

  1. Mode
  2. Opcode
  3. Operand
Instructions

Operand contains either the data or the memory location where your data is stored and opcode specifies which instruction we have to apply on the operand i.e. it specifies that we have to apply addition, subtraction etc.

Mode tells us about the data location whether it’s directly given in the operand or it’s in memory location. Some of the addressing modes -

  1. Immediate addressing mode
  2. Register mode
  3. Direct addressing
  4. Indirect addressing

In ethereum, we also have opcodes which costs us certain amount of gas fees. You can check it out here.

Let’s now see opcodes via a smart contract example. Let’s visit remix ide and write some code!

// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.7.0 <0.9.0;

contract Opcodes{
uint private num;

function increment() public view returns(uint256){
return num+1;
}
}

I just wrote a small piece of code as we just need to understand about opcodes. You can compile & deploy the code and increment the value.

The highlighted transaction represents the increment of the value, let’s now debug the transaction.

This is what you all must be seeing on your screen, you can see in the bottom left the machine code and for every line you can check the gas fees associated with that instruction.

If you will scroll the machine code instructions you might see ADD which tells about the operation that we need to apply and that is our opcode.

Why debugging is necessary?

Well the simple answer to this is that as a smart contract developer, you can check for every instruction how much gas it is costing you and you can then try to reduce the gas fees for that instruction.

For an example — If we want to use a multiplication & division in our code, we would normally use * & / operators normally. But, instead of this we can use << (left shift) & >> (right shift) for multiplication & division respectively. (Note : Read more about left & right shift here!!)

Now, why I am saying this? For this, let’s look at the opcode gas consumption table for both scenarios.

If you see, the gas consumption is less while using the left & right shift opcodes.

This is how your opcodes can help you reduce gas consumption.

2. Types Of Transactions

Let’s now look at the second factor which tells how the type of transaction impacts gas consumption.

Let’s do a normal transaction from metamask, just follow the steps below from the image.

Step -1 : I tried sending 0 Matic to recipient, to see how much gas fees it costs me. So, it was 0.0000315 Matic.

Step-2 : Enable Show Hex Data from the advanced settings in the metamask.

Step-3 : Now, again I tried sending 0 Matic to the recipient with a Hex Data 01 i.e. 1 in decimal.

Step-4 : Notice the gas fees, it increased from 0.0000315 to 0.00004729.

This was because by sending in the Hex Data we are also sending some extra information on the block for which we need to pay, so it costs us some amount of gas fees.

Let’s understand which one out of the payable and non-payable functions in solidity consumes more gas!

Non-Payable
Payable

So, notice the difference. We can see that payable function is using less gas than non-payable function. I guess many of you would have been in a shock!! Where is the difference??

Well the answer lies if you check their opcodes. Non payable function adds a internal check to itself of a revert case if there is transfer of ether that is taking place inside it whereas payable function doesn’t need to do so and due to this check in non payable function gas consumption increases.

So, I think many of you would be surprised but it is a fact that payable functions consume less gas fees than non-payable functions.

3. Memory

Some of the basic things that we need to keep in mind while optimizing our contracts is —

  1. Avoid using state variables unless and until it is quite important to use because state variables use a huge amount of gas.
  2. In ethereum everything that is stored is in 32 bytes slots.

Let’s understand the second point by an example. Suppose in a contract you are using uint8 instead of uint256 What do you think will cost us more gas?

Well, if you guessed uint256 then you are wrong!! This is because if you give a variable uint256 datatype, all the bits get occupied but in case of uint8 datatype, 256–8 i.e. 248 bits remain empty as by default ethereum has everything in a 32 bytes slots i.e. 32*8=256 bits.

Now, ethereum in the remaining 248 bits adds the zero padding which means it adds zeroes in that data slots which means additional data gets added thus increasing the cost for uint8.

That’s why it is always advisable to use uint256, also even if you keep uint datatype that corresponds to uint256.

Now, I know what you must be thinking…if this datatypes are consuming more gas than why are they brought in or why are they used. For this, let’s understand what variable packing means!

Variable Packing

If you are able to understand and use this concept properly, you can save a lot of gas consumption in your contracts!

Look at the two images pasted below and try to guess what is happening in the code and also notice the gas consumed. The first image resembles the non packing of variables and second image resembles variable packing.

Non variable packing
Variable Packing

So, if you compare in between two images you can see there is just a difference of one line i.e. line 7 & line 8 interchanged. But wait, why did it created a so much difference in gas used?

The reason is simple, as I gave you the reason for uint8 & uint256 similarly in this case when numA with a datatype of uint128 is put together with numB with a datatype of uint128. This two are packed in the same 256 bit which would remain empty if we used them separately.

Empty bits left after numA allocation : 256–128 i.e. 128, in this empty space when we used variable packing numB is placed, thus reducing the total bits required and hence it reduces the gas consumption in variable packing.

In non variable packing, the space of 128 bits is left empty for both numA and numB as they are not placed together and hence here comes the zero padding which adds up the gas consumption.

Interesting right!! This was also my reaction when i studied this for the first time!

So, try to use variable packing wherever possible and incase you can’t use then use uint256.

4. State Change

State change here refers to constant updation of the state variables in the code due to which gas consumption increases instead we should try to use local variables which do not cost us much.

Let’s learn with the help of an example-

// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.7.0 <0.9.0;

contract StateChange{
uint[] public array;
uint public sum;

constructor(){
array = [1,2,3,4,5];
}

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

In this code, we can see the state variable sum is changing its state quite often. Let’s check for the gas consumption.

Now, let’s change our code a bit and use local variable.

// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.7.0 <0.9.0;

contract StateChange{
uint[] public array;
uint public sum;

constructor(){
array = [1,2,3,4,5];
}

function loop() external{
uint _sum;
for(uint i=0;i<array.length;i++){
_sum = _sum + array[i];
}

sum = _sum;
}
}

Here we have used local variable _sum, let’s check in the gas consumption for this code.

So, the gas consumption is quite low in the case where we have used local variable.

So, that’s it for this article!!! Quite a long article and I appreciate your patient reading but you learnt about some of the best techniques in the solidity gas optimization. I will try to come up with Part-3 for this series with some tricks & techniques for gas optimization so stay tuned!!

If you found this article useful and interesting, do leave claps for it and also let me know in the comments. You can also connect with me on Twitter & Linkedin and you can follow me on Medium as I keep writing about some of the new and less known things about Blockchain.

--

--

Atharva Paliwal
Coinmonks

Blockchain Developer (R&D) at Persistent Systems | Writes about new and less known things that you may want to add to your skills portfolio ✍️💫