How to optimize gas cost in a Solidity smart contract? 6 tips

Simple transactions on Ethereum cost from 20-30 cents to a few dollars. But complex transactions can be much more expensive. For example, the Loom network created the ERC721x standard to avoid paying millions of dollars in gas costs when doing a batch token transfer.

Clearly, knowing how to reduce gas consumption in an Ethereum smart contract can be very valuable. In this article, I will give you 7 techniques to optimize gas costs in your transaction. This is the fruit of countless experiments, comparing gas costs in various scenario. So open your eyes, and make sure to absorb every bit of info on this page, this is worth it πŸ™‚

Tip 1: Minimize on-chain data

When you design a Dapp, you need to decide what code / data to put on-chain and off-chain. The less you put on-chain, the less your gas costs. That sounds rather obvious but few people actually do this high-level optimization first. Instead, most people go head-first to low level gas reduction techniques. Wrong choice.

How to decide what to put on-chain then? The code and data that are critical to your Dapp must generally be put on-chain: the economy of a game, the signature of school in a blockchain transcript system, etc… Generally, this will be a small part of all your code / data. What can be put off-chain is metadata, file, settings, and all non-critical pieces.

Tip 2: Turn on Solidity optimization

When you compile Solidity smart contracts, you can specify an optimization flag to tell the Solidity compiler to produce highly optimized bytecode. This bytecode will consume less gas than if you don’t use the optimization flag.

Why do we need to turn on this optimization? Can’t it be just the normal mode of operation of Solidity? Well, the reason this exist is because this optimization takes longer to run than a normal compilation. In development, this longer compilation time can be very annoying, so it is turned off by default.

If you use Truffle, you can enable optimization with this setting in your truffle-config.js file:

module.exports = {
...
  solc: {
    optimizer: {
      enabled: true,
      runs: 200
    }
  }
}

Tip 3: Optimize order of variable declaration

Solidity stores data in 256 bit memory slots. Data that does not fit in a single slot is spread over several slots. Data that fits in a single slot, well, just occupies a single slot. As for data that is smaller than a single slot, Solidity tries to optimize it and pack it together with other data in a single slot. But for this optimization to work, we need to have several small data types declared next to each other.

Let’s use integers as an example. One type of integer is unsigned integers (uint). The type is subdivided into several sub-types, based on capacity: uint128 are 128 bits integers, uint256 integers are 256 integers, etc… In the below code, the Solidity compiler is only able to put 2 uint128 in the same 256 bit memory slots when they are declared next to each other. Otherwise, uint128 variables each occupies a separate uint256 just for themselves, wasting 128 bits for each slot:

//Good :)
uint128 a;
uint128 b;
uint256 b;

// Bad :(
uint128 a;
uint256 b;
uint128 b;

Tip 4: Write literal values instead of computed one

If you know already know the value of some data at compile time, write directly this value. Don’t use Solidity functions to derive the value of the data at compile time. This might be less convenient to do this, but it avoid wasting gas.

//Good
bytes32 constant hash = 'uiHk78Uidaf....';

//Bad
bytes32 constant hash = keccack256(abi.encodePacked('MyDataToHash'));

Tip 5: Use Proxy for multiple deployment of same contract

If you have to deploy multiple times the same Solidity smart contract, it is quite wasteful to actually redeploy the same code many times. Ethereum has introduced a new opcode called DELEGATE_CALL that allows a smart contract to call the code of another smart contract, but use it in its own context. That means that we can just a smart contract for his code, but have this code modify the data on the calling smart contract.

Instead of deploying a fresh smart contract everytime, we can instead deploy a (small) proxy contract that will forward code execution to the same reference smart contract. The data will be modified on the proxy, but that’s the reference smart contract code that is executed. The bit advantage is that for each deployment we don’t need to deploy the (big) reference smart contract everytime, but only the (small) proxy contract.

If you want to know more, watch this tutorial on the ERC1167 standard:

Tip 6: Use assembly code

When you compile a Solidity smart contract, it is transformed into a series of EVM (Ethereum virtual machine) opcodes. We call this the bytecode. Solidity generally does a good job at writing optimized bytecode. But sometime, you can do better than Solidity by writing yourself the bytecode.

Actually, nobody writes directly EVM bytecode. But what you can do is to use (Solidity) assembly, a low-level smart contract language. With assembly, you write code very close to the opcode level. It’s not very easy to write code at such a low level, but the benefit is that you can manually optimize the opcode and outperform Solidity bytecode in some cases.

If you are interested in assembly, check out the last 2 videos of smart contract 30.

Leave a Reply

Your email address will not be published. Required fields are marked *