Collatz Puzzle

Idea

The parameter addr should be a contract that we deploy, so this is our solution contract. The function callMe() takes addr as input and does some math. In the following line:

q = ICollatz(addr).collatzIteration{gas: 100}(q);

We learn that it calls the collatzIteration() function in our solution contract, so our task is to implement this function with respect to the following standard:

  function collatzIteration(uint256 n) public pure override returns (uint256) {
    if (n % 2 == 0) {
      return n / 2;
    } else {
      return 3 * n + 1;
    }
  }

There are two difficulties we encounter:

  1. Our solution contract must have size ≤ 32 bytes (size := extcodesize(addr)).

  2. The gas limit is 100 (ICollatz(addr).collatzIteration{gas: 100}(q))

Therefore, there is no way we can do this with Solidity, even with assembly. In such scenario, Huff is a great choice since it is low-level enough for optimization.

In Huff, usually we want to implement things like function dispatcher. For this challenge, we will skip that part since the code size is heavily constrained. Skipping function dispatching works because we can implement the logic of collatzIteration() inside MAIN macro and simply throw away the first 4 bytes in calldata. Since the MAIN macro is the entry point, the logic inside it will be executed anyway. That is, calling collatzIteration() is the same as calling the MAIN macro.

Just keep in mind that skipping function dispatching is bad practice in production.

First we write a Foundry test for callMe():

This piece of code grabs the Huff code size and display it in the console:

In order to read the content of console.log(), we should use -vv verbosity when running forge test.

Now let’s implement the logic of collatzIteration(). Here is my first try:

Test result returns “bad code size!” and shows that the current code size is 40:

Oversize

Let’s see how to optimize the Huff code.

Take a look at this part of the code:

The ret jump here is redundant since the control flow will move on to the ret label anyway. Deleting ret jump makes code size 36. We are 4 bytes away from our goal.

The next optimization is a bit tricky: we can actually get rid of the ret jump at the end of the odd case as well.

Here is the logic of the odd case:

Here we can compute 2 * (3 * n + 1) in the odd case and let the control flow goes into the even case and get 2 * (3 * n + 1) / 2, and the computation is still correct. Modified version:

This gives code size 35. The math expression in the odd case can be simplified to 6 * n + 2, so we come up with a more optimized version:

This version gives code size exactly 32 and the test is passed:

Test passed

PoC

Collatz Puzzle

Last updated