May 15, 2023
Dragonfly Ctf - Puzzlebox.sol Writeup

Dragonfly hosted a CTF called Puzzlebox that ran from May 6, 2023 to May 8, 2023. The competition required participants to solve a Solidity puzzle with minimal gas usage. Big thanks to dragonfly for organizing a fun CTF.


I learned about the Dragonfly CTF news on the discord. Unfortunately, the Dragonfly CTF news I saw on the Discord was not about the competition schedule, but rather a post from the announcing Chainlight team first blood. According to the rules of the CTF, the less gas used to solve a problem, the higher the score, so I set my goal to win first place through gas optimization.( is also an interesting CTF platform like Dragonfly CTF, where web3 ctf challenges are presented, and good content related to web3 is shared on Discord.)

This writeup describes my process for solving Puzzlebox and how I optimized gas usage to achieve high score. If you're curious about other people's solutions, check out the link below!

Solving Puzzlebox in Dragonfly CTF

step1. understand ctf platform

I first tried to understand how the ctf platform worked. Puzzlebox had several functions, and it seemed that it had to be solved by executing the open function.

The operate function seemed to be the easiest condition to achieve among the Puzzlebox functions. It executes when the value of code.length is 0, but since the solution had to be written inside a contract function, a another new contract had to be deployed, and the operate code had to be called inside the constructor of the another new contract.

I wrote a contract that called the operate function in its constructor as shown above, and when I clicked the Solve button, the score was displayed on the web page after a short delay. I initially thought that the judging was done remotely, so I pressed the Solve button again while monitoring network requests using the browser's developer console. However, there were no network transmission records.

I thought that the judging code was on the frontend, so I started looking for the judging code on the frontend. I searched for the string related to the "SCORE" and found a javascript code that judges it. The javascript code used the ganache and solc libraries to compile and run solidity, and was checking events. A notable feature was that the private key of the deployer was fixed, so the address of the deployed PuzzleBox contract was always the same, and gas measurement was performed after the deployment of the PuzzleBoxSolution contract.

As explained in ctf, there was a scoring code where the less gas was used, the higher the score. There were eight events that had to be triggered Operate, Lock, Drip, Torch, Zip, Creep, Spread, and Open in order to obtain the highest score. Some of these events also had a verification condition.

step2. solve the PuzzleBox (score: 15,095)

To solve the PuzzleBox, I had to meet eight conditions: Operate, Lock, Drip, Torch, Zip, Creep, Spread, and Open. I solved each condition one by one. When I first ran my solution code, the score was 15,095 points, which was 1,200 points higher than the second place team.

I suspect that the reason my first solution received a higher score than other participants was because I understood the scoring method. I analyzed the judging process by examining the javascript code exposed on the front-end. I learned that gas measurement was only performed when the PuzzleBoxSolution.solve function was executed. So I knew that gas used during the deployment of the PuzzleBoxSolution contract was not included. Therefore, I wrote most of the code in PuzzleBoxSolution and minimized the size of the contract that was created to call operate, lock, drip. The commonality among the three functions is that they can only be called within newly created contracts. The rest of the code was written in PuzzleBoxSolution, which does not incur deployment costs.

Another reason why my score was high was that I did not set the balance of the PuzzleBox contract to 7 to solve the creep function, and used a method to set the gasLimit when calling the creep function. The creepForward function was working with try, catch, so if the gas was insufficient when the function was executed, it would end normally without revert. Even if the balance of PuzzleBox is not 7, you can solve the creep function by finding the gas that creepForward works seven times.

step3. gas minify (score: 16,523)

I thought I could get a higher score because I hadn't applied gas optimization properly yet, so I worked harder to get a higher score.

  1. Add payable to the constructor. This removes the gas used to check if msg.value is 0.
  2. Instead of implementing a function in PuzzleBoxSolution, create a fallback payable function. This removes the gas used to find the function with a 4 byte sig and check if msg.value is 0.
  3. Use inline assembly to write code that optimizes gas better than the compiler.
  4. Use calldata hex instead of abi when calling functions.
  5. The deployer address is fixed, so the PuzzleBox contract address is also fixed. Take advantage of this.

As a result of applying the above, I was able to score 16,347 points.

Now I thought the only gas optimization I could do was to rewrite the contract created by create with assembly. However, it was too much work to write a contract from scratch in assembly, so I decided to try solc's optimize function to see if it would help.

The PuzzleBoxSolution contract cannot use solc's optimize function, but the contracts it creates with the create function can use optimize. To use this method, one can use remix or other similar tools to obtain the bytecode of an optimized contract, and then use the create function to deploy it directly. I submitted the following code and obtained a score of 16,523. This was 180 points higher than my previous score, and the rate at which my score increased was slowing down.

step4. gas minify with assembly (score: 16,814)

To further reduce my gas, I thought I needed to create a contract that was deployed via assembly. I executed this plan and succeeded in reducing the gas used even further.

This is manually written assembly code.(I became familiar with EVM assembly while solving and quillctf.) As a result of writing assembly code to deploy the contract, I was able to score 16,814 points, an increase of 290 points from my previous score.

step5. After the ctf ended (score: 16,869)

After the ctf ended, the first place was taken away. I was able to reclaim first place by applying the following strategy.

  • Upon reading the code again, I noticed that there were many unnecessary pop. Since the result of the call is always 1 in this code, I realized that I could reuse its value in the next call without having to pop it first.
  • If the address of the PuzzleBox contract is passed as an argument to the fallback function of the contract being created with create, it is possible to save the number of bytes equivalent to "push20 0x....". The address of the PuzzleBox contract can be obtained using calldata or msg.sender.
  • In inline assembly, using values that represent 0, such as callvalue(), instead of 0 itself can reduce gas costs. The gas cost of push1 0x00 is 3, but the gas cost of callvalue() is 2.


There was an interesting Solidity puzzle problem in this year's CTF. Unlike other CTFs, the end goal was not just to solve the puzzle, but also to optimize gas usage. There was no clear answer, and other players could take away ranking at any time. It was a fun experience to push gas optimization to its limits in this competition. I would like to thank the organizers of Dragonfly CTF once again for their work.

Although the CTF has ended, solutions can still be submitted and the scoreboard remains open. Try to challenge for first place using gas optimization 😃



Security Researcher

About the author.

Jinu is an experienced security researcher who can audit the web2 and web3.