Jackpot or Honeypot? A quick lesson on solc's uint and function selectors

matta @ theredguild.org
3 min readSep 19, 2022

--

This article belongs to a greater series called A journey into smart contract security.

Oven function selector from Ebay

Introduction

A few days ago I was casually browsing Twitter, and I saw StErMi talk about the following challenge posted by 0xOptimum.

Twitter post by Optimum with the challenge.

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.

Jackpot.sol

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. Keywords uint8 to uint256 in steps of 8 (unsigned of 8 up to 256 bits) and int8 to int256. uint and int are aliases for uint256 and int256, 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.

--

--