Were you whitelisted?
After downloading merkleDrop.zip, we cloud see that there are three solidity files here, MerkleDistributor, Setup and MerkleProof and there are some merkle tree info in tree.json. Obviously, It’s a challenge on MerkleTree.
solved condition
If we want to pass the challenge, there are two conditions
The balance of token in merkleDistributor must equal to zero.
There are 64 address in whitelist, not all of them is claimed.
The conditions 2 seems wired, why not just all of them? So I guess there are some address are able to be claimed and some are not.
MerkleDistributor.sol
And then, when I’m deep into MerkleDistributor, I find something unusual that using ‘uint96’ as type of amount. In most cases, we’d love to use uint256 instead of uint96. Besides, they make up exactly 256 by address and uint96, looks like some gas optimization tricks?
MerkleProof.sol
But, if you take a look at verify function in MerkleProof.sol, you’ll somethings interesting.
difference between computedHash and node
All of computedHash, proofElement, index are 32 bytes, and account + amount = 32 bytes, too. I was wondering if I cloud use some middle nodes as valid user.
Basic process of verification
There is a basic case of verification process with my normal leaf, in which proofElement is the right Leaf and computedHash is My Leaf.Looks very similar? That’s right! We could just separate a known leaf into address and amount, and use leaf that could make a pair with the chosen one as index. Let’s take a look at the tree diagram below.
middle node attack
If we got a known leaf, we’re able to separate it into first 20 bytes as address( used as “middle address”) and lat 12 bytes as amount (“middle amount”). At the same time, using another leaf that could be paired as index. So we get the following derivation process.
You find that we got the hash of paired leaves. Of course, It’s could pass the MerkleProof verification.
It’s clear that we find the bug. So we just need find a node satisfy conditions that the last 12 bytes must be smaller that total token amount , 0x0fe1c215e8f838e00000.
After searching in the tree.json(it’s a full binary tree), we could find only one leaf “0xd48451c19959e2d9bd4e620fbe88aa5f6f7ea72a00000f40f0c122ae08d2207b” is fine. But we could claim known addresses which match the amount desired.
known proof
Finally! You made it. But there are still some tips and analysis we shoud know.
Through abi.encodePacked()
, Solidity supports a non-standard packed mode where:
types shorter than 32 bytes are concatenated directly, without padding or sign extension
dynamic types are encoded in-place and without the length.
array elements are padded, but still encoded in-place
The encoding of an array is the concatenation of the encoding of its elements with padding.
Dynamically-sized types like string
, bytes
or uint[]
are encoded without their length field.
The encoding of string
or bytes
does not apply padding at the end, unless it is part of an array or struct (then it is padded to a multiple of 32 bytes).
BTW, "abi.encode” will pad each parameter to 32 bytes.
You could get full code and repo in https://github.com/SilasZhr/paradigm-ctf-2022-solution, I’ll keep update.
twitter: https://twitter.com/SilasYayoi
github: https://github.com/SilasZhr