Jackpot or Honeypot? A quick lesson on solc's uint and function selectors
This article belongs to a greater series called A journey into smart contract security.
Introduction
A few days ago I was casually browsing Twitter, and I saw StErMi talk about the following challenge posted by 0xOptimum.
Despite a typo in the constructor, the rest of the code seems pretty legit, right? I'll leave you the code below so you can take a look.
Note: If you want to skip all the explanations and explore the conclusion by yourself, I have created a repository with comments and a test.
Where's the catch?
— Did you see something funny?
— Is there some part of the code that can be prone to errors? 💭
If you are wondering why didn't JackpotProxy claimPrize()
function didn't look similar to the one below, I was wondering the same thing. Also, you have found the place where to look for.
function claimPrizeSafe(Jackpot jackpot) external payable { uint256 amount = msg.value; require(amount > 0, “zero deposit”); jackpot.claimPrize{value: amount}(amount); payable(msg.sender).transfer(address(this).balance);}
Some context
If you don't already know what a function selector is, I encourage you to read a bit about it or play a little with this example.
Basically, every function call is actually translated to the CALL
opcode with the calldata
filled with: the first four bytes from applying the keccak256 hashing algorithm to the function signature, plus parameters.
— Damn, I almost ran out of breath saying that out loud!
— Yeah, but what does this has to do with anything?
— I'll get there, just stay with me for a second.
If we go to the Integers section from Solidity's official documentation, you will find a fragment that reads as follows:
Integers
int
/uint
: Signed and unsigned integers of various sizes. Keywordsuint8
touint256
in steps of8
(unsigned of 8 up to 256 bits) andint8
toint256
.uint
andint
are aliases foruint256
andint256
, respectively.
— Now, without looking ahead, can you now realize where the bug is? 💭
The bug
Without running any code yet, the question you should now be asking yourself is:
Will the function selector of claimPrize(uint)
equal claimPrize(uint256)
? 💭
Well… if you know how hashes work then you already know the answer. NO.
Function selectors
> bytes4(keccak256(“claimPrize(uint)”));
0x5aba5e37> bytes4(keccak256(“claimPrize(uint256)”));
0xd709815> abi.encodeWithSignature(“claimPrize(uint)”, 1 ether);
0x5aba5e370000000000000000000000000000000000000000000000000de0b6b3a7640000> abi.encodeWithSignature(“claimPrize(uint256)”, 1 ether);
0xd70981540000000000000000000000000000000000000000000000000de0b6b3a7640000
— Can you imagine what happens when you execute a call on a non-existent function selector? 💭
— We end up in the fallback?
— Exactly! And what does that entails?
— Ohh, we get all our staked ether drawn into the contract!
— Exactly! It’s a honeypot 😅
Bonus: under the hood
There's a moment where the compiler (solc) upon creation of the AST uses TypeProvider which decides that the recognized token K(UInt, "uint", 0)
should be 256 bits unsigned integer.
case Token::UInt:
return integer(256, IntegerType::Modifier::Unsigned);
And there you go, that's where the alias came from in the code.
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.