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:
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:
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).
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 do the obvious things first:
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)
Replace transfer
with send
(saves 108 gas - cause send has no check if the transfer succeeded)
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:
There’s a particular trick this puzzle should teach you. And it’s an old way of sending ETH to addresses by SELFDESTRUCT
:
This trick alone saves you 4066 gas and meets the target!
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)
Putting addresses as bytes32 number directly in calls can save us 9 more gas units!
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 :)
This gives us an amazing 21207 gas! Which is unbelievable until you understand what’s going on here…
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.
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!