Solving more-evm-puzzles differently? | Part I

matta @ theredguild.org
6 min readAug 30, 2022

--

This post is the first part of a small series dedicated to motivating myself while learning how the EVM works. Go to the second part.

Ethereum Virtuelle Machine — cryptoast.fr

With my return to the study of blockchain technologies, I included along my road map some challenges to make my learning a little more entertaining.

Who wouldn't? 😃

In this case, I will be talking about two challenges only ✌️. They are from more-evm-puzzles by Dalton Sweeney, an inspiration from Franco Victorio's evm-puzzles.

And by the way, if you haven't tried solving these puzzles already, please go ahead and come back later! 💨

Each puzzle consists in sending a successful transaction to a contract. The bytecode of the contract is provided, and you need to fill the transaction data that won’t revert the execution.

Dalton's focuses mostly on CREATE and CALL opcodes.

Why just two?

Despite there being a single objective to fulfill within each challenge, there could be a great number of ways to approach it 😌.

Out of curiosity I checked online for others' solutions and found differences in two of them. The challenges that will be discussed in these series are puzzles #4 and #9.

To solve them, we will be using pen, paper, and both EVM Codes interactive reference and its playgrounds.

Puzzle #4

Link to the challenge playground.

Mnemonic for this puzzle's bytecode

Understanding the logic

We are going to separate some parts of the code into chunks. They will be displayed stating the instruction current position in the program, followed by the mnemonic of the instruction in that position, and after a double slash our guess of how the stack will end if that instruction is executed.

#. INSTRUCTION // [STACKITEM_0, STACKITEM_1]

Let's begin!

These first two instructions provide a similar behavior as running address(this).balance.

00. ADDRESS // [AccountAddress]
01. BALANCE // [AccountBalance]
It pushes the current account's address into the stack, and then retrieves the balance of that account, pushing it to the stack as well.

Why don't I say equal? Because there's a specific instruction to get your own balance which is cheaper, and that is SELFBALANCE. This instruction costs 5 gas only, and the use of solely BALANCE costs 100 gas 😧.

But now, before we continue, I feel obliged to make a mandatory pause to introduce a caveat for this kind of challenges.

The playground accumulates the msg.value that is being sent — from thin air — on every Run into your account, therefore making it almost impossible to hardcode a solution based on this kind of input, unless you restarted the playground by reloading the site or using SELDESTRUCT on yourself 😅.

However, this behavior won’t be happening the moment you provide the solution through hardhat play, but taking this into account will make you a little bit more aware of how it works.

If you want to test it out for yourselves, just execute the first two instructions (ADDRESS, BALANCE) with 1 Wei as an example, and you will see the account balance add up when running them consecutive times.

Enough talking!
— Ok, sorrry!

Now that I have provided this piece of information, let’s get back to the puzzle.

This copies everything you sent on your calldata to memory!

02. CALLDATASIZE // [CallDataSize, AccountBalance]
04. PUSH1 0x00 // [0, CallDataSize, AccountBalance]
06. PUSH1 0x00 // [0, 0, CallDataSize, AccountBalance]
07. CALLDATACOPY // [AccountBalance]
CALLDATACOPY gathers everything starting at the 0 position from the calldata, with an offset of 0, up to its true size, since CALLDATASIZE has been used.

This chunk creates a new contract with the calldata that got stored in memory and sends all our current balance to it.

08. CALLDATASIZE // [CallDataSize, AccountBalance]
0A. PUSH1 0x00 // [0, CallDataSize, AccountBalance]
0B. ADDRESS // [AccountAddress, 0, CallDataSize, AccountBalance]
0C. BALANCE // [AccBalance, AccAddr, 0, CallDataSize, AccBalance]
0D. CREATE // [ContractAddress, AccountBalance]
CREATE consumes Value, Offset, Size, and returns the Address of the new contract. It basically establishes the balance value in Wei to initialize the new contract with, and the size of the its code with the offset to start with, from memory.

Afterward, it checks if our original balance is double as big as this new contract value.

0E. BALANCE // [BalanceOfContract, AccountBalance]
0F. SWAP1 // [AccountBalance, BalanceOfContract]
10. DIV // [AccountBalance / BalanceOfContract]
12. PUSH1 0x02 // [2, AccountBalance / BalanceOfContract]
13. EQ // [ (AccountBalance / BalanceOfContract) == 2? 1 : 0]
Gets the balance of the new contract left on the stack by CREATE, and orders the stack to be able to do a division between our initial balance versus the one in the current contract. Checks whether the result of their division is 2. If it is, then pushes 1 into the stuck, otherwise a 0.

If it is, then you nailed it! 💅
Otherwise, the program execution will halt with a revert. ❌

15. PUSH1 0x18 // [18, (AccBalance / BalanceOfContract) == 2? 1 : 0]
16. JUMPI // [ ] — Jumps to 18 if stack[1] == 1, otherwise reverts.
17. REVERT // Fail
18. JUMPDEST // Solved!
19. STOP
Set the jump destination to finish the execution properly if the division gave 2 as a result, otherwise follow with the next instruction and revert. Either way, the stack ends up empty.

Solving the puzzle

It should be pretty straightforward, right? We just have to make sure that the new contract reduces its balance in half before we reach the division.

The only way to do so, as you may notice, is within the creation of the contract itself.

Some of the solutions I've seen send a fixed value. But what if you can't possibly know your current balance in-beforehand?

Solution

Link to the solution below at evm code's playground.

Our approach here will be, using CALL of course — , to burn half the amount of the current given value. Basically, we're just gonna send ether to the 0x0 address account.

The call instruction requires 7 parameters:
call(gas, address, value, argsOffset, argsSize, retOffset, retSize).

Since we do not need to call with arguments, nor expect a value in return, we can ignore the last 4 parameters. The construction will be as follows:
call(currentGas, 0x0, msg.value/2, 0, 0, 0, 0).

First things first, let's get our msg.value in half.

00. PUSH1 0x02 // [2]
02. CALLVALUE // [CallValue, 2]
03. DIV // [(CallValueInHalf)]
We push the divisor first, then the dividend, and then we call on divide, which pushes the result into the stack.

Now we create the remaining parameters necessary to execute the call.

05. PUSH1 0x00 // [0, CallValueInHalf]
06. DUP1 // [0, 0, CallValueInHalf]
07. DUP1 // [0, 0, 0, CallValueInHalf]
08. DUP1 // [0, 0, 0, 0, CallValueInHalf]
09. SWAP4 // [CallValueInHalf, 0, 0, 0, 0]
0A. DUP2 // [0, CallValueInHalf, 0, 0, 0, 0]
0B. GAS // [CurrentGas, 0, CallValueInHalf, 0, 0, 0, 0]
0C. CALL // [Success]
We start by getting all the parameters we mentioned above in a reversed order, and swap positions with CallValueInHalf when we get to value. Then we call, and get if it succeeded or not.

Finally, we return to the program execution hoping everything worked!

0E. PUSH1 0x00 // [0, Success]
0F. DUP1 // [0, 0, Success]
10. RETURN // [Success]
RETURN expects an offset and a size, as return data, but since there's no need to do that, we can just return 'empty'.

We just use that code bytecode as the calldata for our solution and that would be it, right?

But wwaaaitt a minute!

— What would happen if we somehow have an odd number as an input parameter for our value? What would happen? Would it still work?!

— I'm glad you asked! Happily for us, we can control message value, so we just submit even numbers and that will be all.

— But wasn't not knowing what the balance would be the whole purpose for this post?

— Well, yeah, but otherwise I would never have got a proper excuse to do a write-up for this solution, so shut up.

Go to the second part of these series ➡️

Thanks for reading! My name is Matt, and I’m learning how to make Ethereum more secure. I will be sharing some things from time to time.
Follow me on twitter
@mattaereal.

--

--