Ethereum — Reducing Gas Consumption with Batch Operations

Tzahi Sofer | Stox
5 min readOct 3, 2018

--

“breads on wooden tray” by Jonathan Farber on Unsplash

In this post we will discuss a simple implementation, that performs bulks of similar transactions in the Ethereum blockchain (for example, transferring tokens to multiple accounts), with a reduced Gas consumption.

It is assumed that the reader has basic knowledge of Solidity, the Ethereum transaction architecture and Gas.

Transaction Costs

Before moving on, let’s set a baseline of definitions and terminology. When sending a transaction to the blockchain, the overall gas usage (“Gas Used By Txn”, as shown in an Etherscan transaction details page) consists of two parameters: The Transaction Cost and Execution cost.

Etherscan — Transaction Details

We will not get into the details of how each of these parameters is calculated, so just in general :

— The Transaction Cost represents how much Gas is consumed for just sending a transaction (any transaction, regardless of what it actually does). This includes a fixed 21000 Gas amount and a fixed Gas amount per byte of data (this can vary between transactions of course, since data can be different). When deploying a contract, there’s an additional fixed Gas amount added.

— The Execution Cost represents how much Gas is consumed for executing the transaction by the EVM (the Ethereum Virtual Machine — the actual software running on each node and executing the commands sent in a transaction — methods, operations, memory allocations etc).

If you are less familiar with the definition of Gas and how to optimize Gas Price per individual transaction — it is also recommended you visit this nice post.

Transaction Batch Operation — Definition

So now , we can better define the problem we are trying to solve, and that is — How can we reduce the Gas consumed by a transaction, when sending bulks of similar ones?

Let’s assume we have 1,000 transactions we would like to send to the Ethereum blockchain . If the transactions are all different, meaning they consist of different execution details(a variety of methods, operations, etc.), or if a single transaction is not “cheap” (i.e. a few 100,000 Gas and above), then the method described below will probably not be helpful. But, what if the transactions consist of the same execution details?

As an example, let’s say that we need to send tokens to 1,000 different accounts. To execute the transaction on the blockchain, we need to call the transfer method of the token, with the recipient account and the amount, as parameters.

Here is the function signature, from the IERC20Token interface contract:

function transfer(address _to, uint256 _value) public returns (bool success);

One way we can do this, is to send the transactions one-by-one. However, by doing this, each and every transaction will bare the fixed Transaction Cost, as we defined above, meaning that our overall Gas usage for transferring tokens for 1,000 accounts, will consist of 1,000 times the Transaction Cost (in addition to the Execution Cost of 1,000 token transfers).

Not so cost-effective, ha?

What would make more sense here is to send one transaction, that executes 1000 methods of token transfers, thus having an overhead of only just 1*Transaction Cost for the whole shebang!

Now, in reality, there is a limit of how much Gas we can consume in one transaction (This limit is actually derived from the Block Gas Limit — a number representing the maximum Gas allowed for all transactions in one block). Therefore, we need some kind of mechanism, that will create the minimum amount of transactions for executing the whole 1,000 token transfers. This is exactly the implementation we will present below.

Transaction Batch Operation — Implementation

Our implementation consist of two elements:

Batch Distribution Contract — A simple smart contract, that has a method that gets a token, an array of addresses and an array of amounts, as parameters, which executes a token transfer to each address.

Here is the main function we use in the contract for transferring tokens in a batch:

function batchTokensTransfer(IERC20Token _token, 
address[] _usersWithdrawalAccounts,
uint256[] _amounts)
public
ownerOnly()
{
require(_usersWithdrawalAccounts.length == _amounts.length);
for (uint i = 0; i < _usersWithdrawalAccounts.length; i++) {
if (_usersWithdrawalAccounts[i] != 0x0) {
_token.transfer(_usersWithdrawalAccounts[i], _amounts[i]);
}
}
}

Few notes about the code snippet above:

  • We make two validation checks: one for making sure the addresses and amounts lists have the same length (in this case, we do not want to transfer any token, as it could potentially mess up the original transfer intentions) and second, for making sure that addresses are not empty.
  • You can find our contract deployed here (It has two more helper functions transferToken() and transferAllTokensToOwner(), for internal use).

Batch Distribution Script — A script that calculates, composes and sends the minimum amount of transactions, covering all the transfers needing to be executed. Following our example above, this can potentially be 6 transactions of 150 transfer addresses and an additional transaction of 100 transfers, adding up to the desired 1000 transfers).

Here is the basic script logic:

  • Read and verify the lists of addresses and amounts (we use csv file in our implementation)
  • Estimate the batch size by calculating Gas usage (using the built in estimateGas() function of a smart contract’s function). Initially, we estimated this value by calculating the Gas usage for executing the batchTokensTransfer() with just one address, then we did the same for two addresses and, using the difference between the two, we finally estimated how big of a batch we can target (this actually means that we calculate the Transaction Cost and Execution Cost, as described above).

Here is a simple example: Let’s say we target a maximum Gas limit of 7,000,000 Gas per transaction. Now, let’s say that estimating Gas usage for a batch transfer with just one address consumes 30,000 Gas and, that a batch of two — 40,000 Gas. We can then assume that each new transfer will consume 10,000 Gas (Execution Cost for one transfer) and the total batch size would be:

(7,000,000 (Gas Limit) — 30,000(function execution Gas)) / 10,000 (single transfer Gas) = 697

(698, to be more accurate, adding the first batch’s address)

In practice, the method described above was not accurate enough. In some cases (some tokens), we have seen that the overhead of Gas for every additional address is not fixed in size, causing the transaction to fail due to Gas Limit overflow. One straight forward way to overcome this, is to make Gas estimation on an initial batch size (for example, 150) and add/subtract according to the estimateGas() result, until we find a batch size that, in high probability, will not exceed the Gas Limit.

  • Go over the addresses and amounts lists and make a transaction, using the full list of calculated-batch-size addresses and amounts. So, continuing the example form above , if we have 3,000 addresses to transfer tokens to, we will have 4 transactions of 698 addresses and one last transaction of 208 addresses (adds up to 3000).

Conclusion

In the above implementation, we saw a method of reducing overall Gas consumption for large amounts of transactions, where each one has the same execution on the blockchain with a relatively low amount of Gas consumption.

This implementation is part of Stox’s blockchain prediction markets platform. We hope you find this post informative and useful.

--

--