Cicadas.finance (https://cicadas.finance/) is an NFT project which launched (publicly) on the 12th of August, where the artwork is based around the original “Cicada 3301” and 30% of all funds raised are used to fund elaborate cryptopuzzles in the vein of the original project. In total there are 10000 NFTs, with about 2500 sold when I managed to solve the first part. So the final smart contract for the first part had a total of 25 ETH as rewards.
The reveal of the project (17th August) brought with it the start of the treasure hunt, with no hint or trails offered to start with.
The Trail
Since there was no hint given and the puzzle was probably buried in one or several (or all) NFTs, the first thing I wanted to do was to go ahead and grab all NFT images and jsons so I can get an idea of what I’m dealing with.
For that I wrote a simple python script which slowly went through all images/jsons one by one and saved them locally. I’ll share the script here since I was asked by multiple people how to do it, but be warned that the entire collection is about 39GB in size.
import requestsuri1='https://gateway.pinata.cloud/ipfs/QmRKZCMkdZigqutxR5vN7BmMigq9YTDu4NugYwtfcLQzJs/'
uri2='.png'image_path = 'C:\\cicadas\\'# change the path where you want to savefor i in range(0,10000):
try:
with open(str(i)+uri2) as f:
continue
except:
print(i)
r = requests.get(uri1+str(i)+uri2)
while r.status_code != 200:
r = requests.get(uri1+str(i)+uri2)
with open(str(i)+uri2, 'wb') as f:
for chunk in r:
f.write(chunk)
This script can also be applied to the jsons (you just need to change the IPFS path) which are all small so it’s a much faster process. Which is why I downloaded the jsons first. Plus, the images took about 10 hours to finish downloading (IPFS is slow…)
And the jsons gave me a big rabbit hole to fall into:
It turns out that exactly 52 images have a letter on them, with each letter of the alphabet occuring exactly twice. As soon as I saw the “Letters” trait I proceeded to download all 52 images, thinking I’d already found the trail.
So after downloading the 52 images and having the entire sequence of letters I went ahead and tried a lot of methods of obtaining a message from the “ciphertext”, with no luck. I was stuck
On the next day, I had about half of the images downloaded so I figured I start looking for other potential trails (thinking that all 3 puzzles start from the images). One of the ways that I wanted to examine images was to check for steg, but of course I won’t examine 10000 images by hand. So I wrote a little script that helped me, which did the following:
— take the upper portion of the image, above where the cicada extends
- hash it
- obtain the background trait name from the jsons file
- check if all traits of the same type have the same hash
So if there were nothing “fishy” with some of the backgrounds, I was expecting that I’d obtain as many hashes as the background traits. But by the 3rd image, I had already found 2 different hashes for “Black”. Something was hiding there.
XORing the 2nd and 3rd images together (and increasing contrast) reveals an obvious dot in the top left corner as can be seen below
So at this point the black background images drew my attention. I looked at the rest of my results and found other black background images with a dot in that approximate region, but not in the exact same place. So it seemed like I had to XOR all of the black background images that featured a dot together.
The ape at the end of the tunnel
With that in mind I wrote another script to XOR all images that have the “Black” trait together. Here’s the script I ended up using:
import json
import numpy as np
import cv2json_path = 'C:\\cicadas\\json\\' # path to json files
image_path = 'C:\\cicadas\\images\\' # path to image filesimage = np.zeros((4096,4096,3), np.uint8)for i in range (0,10000):
jsf = open(json_path+str(i)+".json",'r')
js = json.loads(jsf.readline())for attr in js["attributes"]:
if (attr["trait_type"] == "Background"):
if (attr["value"] == "Black"):
print(i)
im = cv2.imread(image_path+str(i)+".png")image = cv2.bitwise_xor(image, im)
cv2.imshow('res',image)
cv2.imwrite('xord_all.png',image)
First time I ran this I had around 6500 images out of the 10000 but even so, after running, the result was very clear: I was looking at a QR code (although it was incomplete and not scannable at that point).
The only thing to do at that point was to wait until I’d have more of the images so the QR could be scanned.
Luckily, you don’t have to wait unless you want to doublecheck yourself, because here’s what you get when you XOR all Black background images together:
Yeah okay, I get it if it’s hard to see, here’s a better version (contrast increased and colors inverted):
Now we can decode the QR code, it leads to:
This website contained a simple minting interface which allowed you to interact with a smart contract that would take a certain ID, check if you owned that Cicada ID, and if you did, it would mint you a new NFT (an Ape, see https://opensea.io/collection/ape-tlvrdn3bqv ) and award you 0.25 ETH. There were 100 such claims.
So solving this QR step would grant you an Ape NFT, which held the trail for the 2nd part of the puzzle (with new rewards). Of course, since each claim had to be made using a unique ID, I ended up minting *a lot* of Cicadas NFTs!
Acknowledgements
Thanks to Cicadas.finance for this very interesting approach to cryptopuzzles. It’s the first time I’ve had to crawl through 10000 images to even find a trail! I’ve solved the next two steps of the puzzle as well (although not first) and they got more complex and quite a bit harder (especially the last part), but in a very good way.
By the way, if more Cicadas get minted, the creators of this project will create new puzzles with 30% of the funds. That would be great to see, so I encourage everyone to check this one out.