A thing about gas-golfing and test-fitting

Crypto-twitter likes puzzles. And gas-golfing. Any CTF puzzle gains huge success, although in the end it all comes down to “test-fitting”.

Let me review a recent example from RareSkills that blew up on CT a couple of days ago:

Distribution gas-puzzle by RareSkills
Distribution gas-puzzle by RareSkills

The idea is to optimize gas in the contract and run provided tests for it that have a threshold. By default, the gas is way over the target:

Provided tests results when ran on the original contract
Provided tests results when ran on the original contract

So the “rules” are: no messing with optimizer or solidity version, no messing with test files, use solidity (so no huff/yul/bytecode contracts), and don’t make functions payable (which is a weird rule, but ok).

🚨🚨🚨WARNING! SPOILERS AHEAD!🚨🚨🚨

Make sure you try to solve this puzzle yourself before reading further - it’s really fun and makes you learn stuff! But if you already solved it and want to read other ways of doing it, or just struggling and want to know what’s the trick - go ahead!

Let’s optimiiiiize!

Let’s do the obvious things first:

  1. Make vars immutable instead of storage (saves 10710 gas - because immutable vars are embedded in the contract bytecode as constants instead of reading from storage)

  2. Replace transfer with send (saves 108 gas - cause send has no check if the transfer succeeded)

  3. Replace currentTime with endTime (saves 69 gas - and do the time calculations in constructor)

That gives us 61066 gas which is already 10887 better than original, but still 4k above the target:

After simple optimizations - still 4k short
After simple optimizations - still 4k short

So what’s the trick?

There’s a particular trick this puzzle should teach you. And it’s an old way of sending ETH to addresses by SELFDESTRUCT:

Self-destructive way of solving puzzles
Self-destructive way of solving puzzles

This trick alone saves you 4066 gas and meets the target!

But what if we go… deeper?

Every sane optimization always have its insane evil-brother… Let’s see how far we can push this!

Making the contract self-destructable already feels like cheating - why would you have a contract like this that ceases to exist after the first Distribution call? But tests are green so I guess it’s… OKAY?

Let’s see what else can leave the tests green, but optimize for more gas:

  • Making contributors constant instead of immutable (saves 24 gas - cause Hardhat addresses are always the same, so why not?

  • Make amount constant 0.25 ETH instead of calculating from balance (saves 106 gas - cause again, in tests the amount is always the same, so why not?)

  • Use Assembly to do call instead of usual solidity address.send (saves 90 gas)

56780 so far
56780 so far

Even deeper?…

Putting addresses as bytes32 number directly in calls can save us 9 more gas units!

Zero-padded address in call: 56771
Zero-padded address in call: 56771

Allright, how bout we go all the way down?

So as far as we’re gaming tests, let’s game them even further.

Why don’t we just return if the test is “Gas Target”?

Knowing that Hardhat always runs tests in the same blocks, we can just put this in the beginning of our distribute() function:

if (block.number == 5) return;

And it will always return on the “Gas Target” test without consuming much gas, but will still do the rest of the function on “Business Logic” test :)

Test-fitting like there's no tomorrow!
Test-fitting like there's no tomorrow!

This gives us an amazing 21207 gas! Which is unbelievable until you understand what’s going on here…

So what? That’s cheating!

Yeah, and this space is often about cheating - MEV, exploits, code-is-law, and all other stuff: if you can do it - you do it. Remember the recent gas-CTF contests with best optimizooor getting an NFT? There nobody is solving the original problem anymore - everything’s about “cheating” the smart-contract to accept the required values and pass the required tests with the lowest bytecode and the lowest gas - and you get the #1 NFT.

And what does this mean? We all doomed?

No :) We just need better testing and better conditions. Even this gas-puzzle could still verify the business logic in the Gas Target test, and use randomized values and addresses - so there’s no incentive to write code that just makes the tests green.

If the tests are good and fuzzy - there’s no possibility of writing hacky code to just meet the minimal requirements.

Same with the CTF contests. If they tested their problem solutions many times (like 1024 times in a single transaction - all with different numbers) - first, there will be no simple block-hacking (waiting for the right randomized number to submit your solution in a particular block to pass), and also the gas-estimation would be better when using different input values.

I hope the next CTF’s and Puzzles would consider this, so the game becomes more about efficient algorithms, and not just clever ways to hack the system.

If not - I would have to write one myself :)

P.S. The solutions provided here are not optimal - there are still a lot of ways to optimize even further: I’ve seen lower numbers, different tricks (including bytecode replacement), but here I’m describing my own experience with the puzzle. And trying to raise an issue about test-fitting and how to make things better.

Cheers, and let’s discuss the tricks you discovered in CT!

Subscribe to Convergence Boy
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.