proofinprogress.com

Saving Gas on the Ethereum Mainnet

To an Ethereum developer, it shouldn't come as a surprise that Ethereum's mainnet gas fees are insane. For months or weeks now, ERC-20 transfers and decentralized exchange (short: "DEX") swaps have come in at roughly $50 in transaction fees. Suddenly finding myself affected by the situation, too, I took to the pen to publicly claim that "Ethereum isn't fun anymore." And while some say that flashbots and the rollup rollout may make matters more bearable, it's my opinion that it'll be another while.

But then how's a practical software engineer supposed to solve the solution? Indeed, my inner VC would blab something like "Apps => Infrastructure => Apps => Infrastructure," but is that genuinely actionable advice for builders?

I've tried convincing others that Ethereum's infrastructure could be better. My take: Light clients could help to save gas. However, I'm neither too influential in the space nor can I have canons full of VC fiat to further my ideas. So then what's left?

Following Pieter's advice

It's always a good idea to trust the legacy knowledge of a dead person, right? Well, let's say, at least they won't have much of a chance of turning out to be an idiot on Twitter later simply because, you know, they're dead and hence can't tweet.

But putting that tangentant/weird thought aside for a second, here's one of Pieter Hintjens' great ideas: "Don't have a technical vision. Solve problems!" [3]. OK, so gas prices are too damn high. What can we do about it?

Well, hear me out, how about... we make transactions use less gas!

And how do we make transactions use less gas? We do so by reducing the usage of expensive EVM opcodes in our blockchain scripts. Or by sending less transactions. Hence, the rest of this post summarizes my research into how to use less gas.

A Problem: Wasteful approve Transactions

On April 18, 2021, Ethereum processed 1,396,766 transactions, of which 915,104 (65,5%) were ERC-20 token transfers [1, 2]. Having an ERC-20 contract interact with another contract (e.g., a DEX) means that there will be an allowance(address owner, address spender) transaction first before transferring any value from A to B.

But why is that? See, conceptually, an ERC-20 token is a script that runs as a singleton on Ethereum. It's akin to an ordinary object (as in "Object-oriented programming," short "OOP") but technically called a "contract" (as in "smart contract"). A developer constructs it upon deployment to chain. Other contracts like automated market makers are, too, built like singletons.

Further, this means that a contract can own state and make it selectively accessible. It's similar to how classes work in, e.g., JavaScript. There's a state variable, and we must access it through getters and setters.

class Store {
  constructor(a) {
    this.a = a;
  }
  set a(value) {
    this.a = value;
  }
  get a() {
    return this.a;
  }
}

Hence, for an object store, that is an instance of the class Store, we wouldn't be able to directly access this.a. Instead, we can only set it using its setter method. Hence, we have no control over this.a from the outside.

EVM languages like Solidity and their token contracts (e.g. ERC-20) use this exact logic to transfer tokens between parties. Do you want to send one from A to B? Sure, then to move your token, call the contract's setter function transfer(address recipient, uint256 amount) to edit the underlying mapping between addresses the number of tokens they own.

Now, the problem of having to call the allowance function before making a transfer arises when interacting with a token through a third-party contract.

A typical use case is to swap two tokens on a decentralized exchange. As it has to send back the counter pair of your swap (e.g., you trade WETH for USDC), it'll have to guarantee to receive your WETH when its sending out USDC in return.

Hence, when a user calls a DEX's swap function, the DEX will contact the user's token contract to transferFrom(address user, address DEX, uint256 amount) to ingest the user's WETH and then transfer its USDC to the user.

As all of this happens in a single and atomic transaction, that will fail if any of its sub-operations fail, a DEX contract can ensure the safety of swapping of two tokens. However, since the swap contract has no authority over setting another user's token balance by, e.g., calling transferFrom(address user, address DEX, uint256 amount), an upfront approve(address DEX, uint256 amount) transaction is required.

That's why you have to do two transactions when you're starting out trading on, e.g. Uniswap.

Quantifying "wasteful"

Still, you may say that an approve transaction may only have to be sent once for a given token and a third-party contract. And that's an acceptable hypothesis. But let's look into how wasteful that is.

We can do so using the logs of Ethereum called "events." To query for them, we take an event's function signature and hash it using Keccak-256.

keccak256("Approval(address,address,uint256)")
> 8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925
keccak256("Transfer(address,address,uint256)")
> ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef

Using Dune Analytics' brilliant API, we can then query all of last week's logs in Ethereum (from block 12230342) to find how many Approval and Transfer events there are. Comparing Approval(address,address,uint256) and Transfer(address,address,uint256) events there were. It's

Of all Ethereum transfer transactions last week, it means that 9,4% signaled an Approval event. Assuming that the goal of every Approval event is a future transfer transaction, then 9,4% of transfers still require an upfront on-chain approve transaction today. Put differently: Approving transactions before a transfer is a potentially wasteful practice that we can optimize to save the Ethereum network roughly 10% of transaction fees.

Gas-Saving Improvement Proposals

Indeed there are a few canonicalized ways to avoid upfront approve transactions. Using ERC-2612, an additional permit function allows a user to sign and relay an approval message offline. In turn, that allows skipping the need for an on-chain approve transaction. A battle-tested implementation is available in Uniswap V2's ERC20 token script.

Another way of forgoing an additional Approval is by implementing either ERC-677 or ERC-777. Both of these proposals detail an extended transfer function that, upon calling, would reach out to the receiving third-party contract to update its state too solving the problem of having to send two on-chain transactions.

A Call for Further Work

Shaving of gas from transactions or shrinking transaction processes seems like a helpful method for significantly improving a dapp's user experience and for unclogging the Ethereum mainnet.

The above-quoted Ethereum Improvement Proposals look useful to generate gas savings today. They should be implemented by contract owners ASAP.

However, I feel there's a need for further work in this direction. Using compression and indexing techniques, I'm sure there are additional economically sound gas-saving opportunities.

In case you are the author of an approach or the finder of one, please make sure to forward them to me via email (tim (at) daubenschuetz (dot) de). Please consider subscribing to my newsletter too. Thanks!

References


published 2021-04-19 by timdaub