How to Eat Gas in Ethereum

Consuming 99.9% of the Gas Limit in an Ethereum Transaction

Tim Cotten
Cotten.IO

--

A method is presented to consume the available gas in an Ethereum transaction while still performing a defined amount of work: exchanging the spent gas for an equivalent amount of token value. Solidity code and a Ropsten test network demonstration are provided.

Recently I was tasked with bringing an Ethereum thought experiment to life and it went something like this:

“ERC20 contracts trade ether for tokens. Could we trade gas for tokens?”

Sure. In fact, GasTokens do this by taking advantage of the gas refund for zeroing out storage or destroying contracts to buy-low and sell-high.

“OK, but could we make it one-way (no refunds like GasToken), and base the received token value on how much gas was specified in the gas limit for the transaction?”

I didn’t see why not.

Quick Review

Ether vs Gas

Ether is a currency, used for storing transactional value on the Ethereum network. Gas is Ether being spent to perform transactions. It’s more of a commodity, but, at the end of the day, is still Ether.

A smart contract doesn’t “store” gas. It burns it to perform operations. If a smart contract requires 1,000 operations to perform some calculation and each operation costs 2 gas, then that’s 2,000 gas spent.

One key difference is that the user can set a “gas price” that tells the miners what Ether value to peg to each unit of gas being sent. This allows a smart contract user to prioritize their transaction in the queue by spending more per gas unit than another person.

Spending Gas

When a transaction is performed, the user has to guess how much gas to provide for the transaction: too little, and the transaction fails and the Ether spent (converted to gas) is lost.

Providing too much, on the other hand, is “safe”: the extra, unused amount is simply refunded.

Even if the contract determines something is wrong, no problem, most will simply revert() and the unused gas is sent back to the message sender.

Gas Limits

The amount of gas you, the user, make available to a smart contract to execute a function is called the gas limit.

Most Ethereum clients can estimate the required gas for a transaction based on the network rules and known gas-values for the operations inside a given contract.

Equally, you can override those suggested limits in the smart Ethereum clients like Metamask or Geth and send whatever gas limit you want, such as three million where only fifty thousand was actually needed.

Gas Minimums

Any transaction in Ethereum, even just transferring Ether from one address to another, requires a minimum of 21,000 gas.

Thus, all smart contracts should expect to burn, at a minimum, at least 21,000 gas. The additional overhead of calling functions, copying variables, or storing information add much more gas on top of that.

Designing a Gas Eating Function

Simple Algorithm

Eating all the gas available (specified by the gas limit) is simple enough on the surface; any such function would:

  • See how much gas is left
  • Do some operation that costs gas
  • Repeat until the gas is gone

Our stated goal of assigning a token value based on the consumed gas won’t work with just the steps above, however.

After all, if all of the gas is gone, how do we spend gas assigning a token value? Does not compute.

Workable Composition

In order to perform any actual work the gas burning function needs to stop before all the gas is consumed and leave some for the final operations to be performed.

So we need to calculate where to stop, and also set aside enough logic to assign the token value.

Thus, the gas burning is a sub-function of the wrapper function to mint() the token value.

The mint() function should:

  • Calculate what the gas limit was (how much gas was made available)
  • Calculate the required gas to finish assigning token value
  • Burn the rest of the gas
  • Assign a token value

Reconstructing the Gas Limit

Solidity exposes a function called gasleft() which is equivalent to the now deprecated msg.gas property.

However, by the time you call gasleft() you’ve already spent hundreds of gas units entering the smart contract and jumping to the function you’re calling, not to mention the minimum 21,000 gas used by the transaction itself.

For instance, calling mint() on this smart contract with a gas limit of 3,000,000 might result in the first gasleft() function call reporting a uint256 value of 2,978,604.

Does Solidity expose the gas limit of the transaction? No.

But since we know that the minimum transaction is 21,000 gas, we could add that back into gasleft().

If we make the gasleft() call the first thing the function does, then we should also be able to audit the compiled operations stack and predictably spend the same amount of gas every time mint() is called up to that point.

In this case, it’s always going to be 396 gas.

Even if more gas is spent after this, we can reliably reconstruct the original gas limit if we compile the contract and deploy it in the same manner (turning off compiler optimizations would help) and call and use gasleft() immediately after the mint() function enters execution.

uint256 constant MIN_COMMIT = 21000 + 396; // TX cost + entrancy gas

Remember why we need this: we want to assign a token value based on the gas limit specified, because that’s how much gas we’re trying to burn in exchange for the token value.

We don’t need to create a variable that tracks the gas being spent in a loop: we’ll just burn gas until we don’t have enough to use anymore (except for the final assignment of the token value).

function mint() public returns (uint256) {
uint256 token_amt = gasleft() + MIN_COMMIT;

Calculating the Assignment Gas Cost

Assuming our use case is for an ERC20-like token (but using gas instead of ether payments), we should expect to have a data-structure like:

mapping (address => uint256) private _balances;

We need to set aside enough leftover gas to ensure that we successfully set a new value in the _balances mapping for the msg.sender address.

In other words, whoever calls the mint() function and feeds it gas will receive some token amount equivalent to the gas limit they specified, while burning as much of that gas as possible.

We can certainly use the same methodology as we did before: compile the contract without burning gas intentionally and see what the delta is between one where we don’t assign token value and one where we do.

Here’s the code we’d use to assign the token value we derived at the start of the mint() function.

_balances[msg.sender] += token_amt;

That seems simple enough, but there’s a big gotcha.

If this is the first time this msg.sender is calling the function, they’re going to spend a minimum of 20,000 gas on this particular operation.

If it’s not the first time, then they’ll spend a minimum of 5,000.

What?

Nick Johnson reminds us in EIP 1087 how the SSTOR operation works: setting a zero value to non-zero in Ethereum costs 20,000 gas. Updating a non-zero value to another non-zero value costs 5,000 gas.

So not only do we need to guestimate (through compiling and testing) the overhead of calling the math and assignments, but the actual storage operation has a variable cost!

uint256 constant MIN_FINAL  = 1110; // Required gas for finalization
uint256 constant MIN_WITH_NON_ZERO = 5000 + MIN_FINAL;
uint256 constant MIN_WITH_ZERO = 20000 + MIN_FINAL;

The MIN_FINAL amount of 1110 was derived from how much gas above and beyond the 5,000 or 20,000 was spent in my test transactions.

To tell the burn() function which amount to stop at (so we have enough left over for assigning the token value) we might do something like:

burn(token_amt, (_balances[msg.sender] == 0 ? MIN_WITH_ZERO : MIN_WITH_NON_ZERO));

Burning All the Gas We Can

Speaking of the burn() function, here’s a possible implementation:

function burn(uint256 start, uint256 end) internal {
while (gasleft() <= start && gasleft() > end) {}
}

All we’re doing is a while loop that performs calculations (comparing gasleft() with the start and ending amounts).

We guarantee that gas will be consumed by this operation and will terminate before exhausting all the available gas, while still leaving enough left to finalize the transaction’s token value assignment.

If you look at the comparisons, you might wonder “I understand the gasleft() > end bit, you’re just burning until you hit the floor value you specified, but why the gasleft() <= start comparison? Isn’t that always true?”

My answer would be “Yeah, probably. But I didn’t use any SafeMath routines in this example, so this will guarantee that an underflow doesn’t occur. It’s probably not necessary but I’m just having fun with it and haven’t done any formal verification.”

Example Implementation

Here’s an example, bare-bones implementation that you could wrap with an ERC20 set of interfaces:

GasEater Example Contract

After deploying on the Ropsten test network you can find it at address:

https://ropsten.etherscan.io/address/0xdbfa9008c970c7d908b5fb142a533fb6f5cab7dc

GasEater Contract on Ropsten Test Network

If you’re using MetaMask, make sure you use the Advanced feature to set the gas limit yourself instead of its estimate, like so:

Override Estimated Gas Limit in MetaMask

And here are two test transactions, each for 2,000,000 gas:

Look How Much We Burnt!

As you can see from the transaction tests above we were able to burn the vast majority of gas. Success!

Issues

The most obvious issue with the implementation above is: what happens if you send less gas than minimally required? There’s no early detection to bail and revert the remaining funds.

The second is that it’s just a prototype: nothing is fleshed out enough to use as an actual ERC20 token, so please don’t copy/paste this and expect balance transfers to work.

Third, if the goal is to make this a gas only token swap, then the contract needs additional protections to reject attempts to transfer Ether, and possibly handle token transfer fallbacks.

Finally, the utility and safety of burning gas on the network purely to generate tokens is certain to inspire controversy. You could fill entire blocks with these sorts of token minting calls instead of using some other, off-chain allocation method.

Use Case: Reward Tokens

The primary use case for this sort of gas eating contract is to create gas “swap” token factories.

An example might be a “reward token” that you want to be able to mint more of at any time, but still require some sort of cost. However, because the contract is an autonomous factory where no Ether balance is being stored, it’s not an investment vehicle (do I smell a Howey Test?).

Why make a “reward token” that isn’t just a standard ICO?

With recent crypto-regulation and guidance from the SEC its clear that many tokens are considered securities. If a token deployer truly wants to divorce themselves from creating securities unintentionally then using a gas swap function should be a viable answer.

Could users still trade the tokens themselves on markets? Sure, but there’s no involvement of the original token creator. No management, no expectation of profit.

There are other alternatives as well: you could make time-release reward tokens that only mint a certain amount per block, for instance.

Conclusion

It’s entirely possible to reliably predict the gas limit sent to a smart contract and burn enough gas to come within using 99.9% of the available gas. This could be utilized for a new type of token that swaps gas for unit value instead of transferring Ether to the contract or contract owner.

--

--

Founder of Scrypted Inc: Building interactive digital assets for the Metaverse. <tim@cotten.io> @cottenio