Transfer your tokens 9,500% faster on Ethereum using the Bulk API

This is a multi-part series that details the business and technical architecture of HUMAN Protocol, a new approach to human-level machine intelligence which allows machines to ask us directly for the data they need to improve.

If you’ve developed on Ethereum before you have probably noticed how limiting transfers can be. Sending tokens to many Ethereum addresses is slow and expensive when using the standard methods defined in the ERC20 standard. This has prevented wide adoption of Ethereum’s public chain for micropayments and other high-volume use cases.

As part of our work on the HUMAN Protocol we needed to scale settlements to handle billions of task payments per day. Turns out this is possible to do in a backwards-compatible way even on the Ethereum mainnet. We call it the Bulk API.

Many of our friends at other projects have asked us to share these results, so in order to give back to the community we are open sourcing our reference implementation. We will also be proposing the Bulk API as an extension to the existing ERC20 standard so that everyone can benefit from faster and cheaper token transfers to multiple Ethereum addresses.

Background

EIP20, now known as ERC20 is an Ethereum API for tokens within smart contracts that defines standard methods for common needs. You can create your own token in a matter of minutes, and thousands of projects have done so due to this ease of use.

ERC20’s main functions are:

  • totalSupply: get the total amount of tokens that the ERC20 contract holds.
  • balanceOf: get the amount of tokens an Ethereum address holds.
  • transfer: transfer tokens to an Ethereum address.
  • approve: used for the withdrawal pattern to let tokens to be approved so they can be spent by another on-chain third party.

Problem

Using the transfer function lets you send tokens to only one address at a time. When sending tokens to many addresses, you need to wait for the transaction to be mined first before sending tokens to the next address in line. The reason for this is due to the way Ethereum was designed: every Ethereum address has its own nonce (not to be confused with miners’ nonces) which tracks its transaction count.

The problematic part is that you have to wait each and every time for the confirmation of the network that your nonce has been incremented successfully before you can continue with the next token transfer.

Solution

In the HUMAN Protocol we need to transfer tokens to huge numbers of website owners for the captchas their clients solve during the lifecycle of each smart bounty. Trying to use the transfer function of ERC20 would have made our system far too slow for production workloads.

The biggest issue with relying on transfer for token payments in this scenario is that it doesn’t properly support payments to multiple Ethereum addresses. The programmer has to handle the looping of token transfers on the client level for example with web3.

What if we used Solidity’s built-in loops and handled the bulk payments at the contract level instead? Enter the Bulk API.

The Bulk API adds two new functions to the existing ERC20:

  • transferBulk: send tokens to multiple Ethereum addresses with one function call
  • approveBulk: used for withdrawal pattern to let multiple Ethereum addresses “withdraw” their tokens instead of direct bulk transfer

Benchmarking

We measure two main factors in our benchmark tests: speed and gas cost. The variable we use to simulate different benchmarks is the number of wallet recipients receiving tokens. Due to possible congestion and other testnet-specific instabilities the Execution time results should be treated as indicative. Gas cost results are generally stable within each benchmark test.

Raw chart data is here.

The immediate observation is that there are no results for the transfer function on 64 and 96 wallet benchmarks. This is due to problems in the Ropsten testnet. Several attempts to get 64 wallet transfers through led to “known transaction” and “nonce too low” among other known errors. On transferBulk side we found that 96 wallets seems to be the maximum amount of wallets that succeeds with high confidence.

As can be seen from the charts, transferBulk scales much better than transfer as it doesn’t suffer from the nonce incrementation loop of the transfer function between token transfers. The fact that there needs to be a nonce incrementation between every token transfer makes transfer more error-prone, in splitting the contract’s state between paid and non-paid wallet recipients. If transaction timeouts trigger due to network congestion, there could be a real problem tracking which Ethereum addresses have been paid: the state has to be maintained off-chain. With transferBulk the token transfers are by design atomic. This means that the transaction receipt gets returned only if all the specified Ethereum addresses have been paid. The state gets reverted if any token transfers fails.

Scaling Factors

The improvements you can obtain with the Bulk API are substantial. transferBulk’s execution time scales in constant time as O(1) which is a radical improvement over the standard ERC20 transfer method which scales linearly as O(n). N is currently limited by the argument count, which is 96 wallet recipients.

Comparison of the scaling results between transfer and transferBulk methods is limited to 32 wallets as we never got transfer to successfully send tokens to 64 wallets.

It is quite clear that the increase in wallets affects transfer. We didn’t witness the same behaviour in transferBulk’s benchmark tests however. The results were more likely to indicate that the difference between transferBulk transfers had more to do with the state of the Ropsten testnet than the amount of wallet recipients. For example: 4 wallet transfers with transferBulk benchmarked to an average of 37.18 seconds whereas 64 and 96 wallets benchmarked to 12.44 and 37.02 seconds respectively. We can state from this that transferBulk closely resembles an O(1) time complexity as the amount of wallets doesn’t influence the execution time linearly.

What’s Next

We are preparing to submit a fully comprehensive EIP in the near future where we specify in more detail the implementation of transferBulk and approveBulk functions.

That said, we’ve been testing this functionality for some time. Our open source reference implementation has been audited and is ready for production use today if this is a problem that you face.

Let us know if you’ve had similar experiences in your token transfers and if you would like to hear more!

Want to see an example of this work in action? hCaptcha (the first app built on top of HUMAN Protocol) is now available for you to deploy on your website — if you already serve captchas and would like to be compensated for the effort, sign up at hCaptcha.com.

And if you’re feeling inspired by the possibilities here, perhaps we should be working together! We’re hiring ML scientists and senior developers in SF and worldwide. You’d be joining a team of seasoned folks working on research and implementation for products and services used at scale by the largest companies in the world. Details at https://www.intuitionmachines.com/jobs