5 min read

[EVM - 3] Function Selectors

[EVM - 3] Function Selectors
Photo by Luca Bravo / Unsplash

Before we start this section, if you want to follow along, make sure you have:

  1. Solidity REPL - Oftentimes it is really convenient to test one liner code out on cli before using them in code base. Check this for more information
  2. Testnet ETH - It is really helpful to be able to deploy smart contracts for testing. I maintain a list of faucets you can use for getting test ETH.
  3. Wallet - You have to have Metamask or some other similar wallet.

Let’s look at the following smart contract - this is a sample contract from the Remix IDE

// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.7.0 <0.9.0;

/**
 * @title Storage
 * @dev Store & retrieve value in a variable
 * @custom:dev-run-script ./scripts/deploy_with_ethers.ts
 */
contract Storage {

    uint256 number;

    /**
     * @dev Store value in variable
     * @param num value to store
     */
    function store(uint256 num) public {
        number = num;
    }

    /**
     * @dev Return value 
     * @return value of 'number'
     */
    function retrieve() public view returns (uint256){
        return number;
    }
}

Compiling this, we get the following bytecode:

608060405234801561001057600080fd5b50610150806100206000396000
f3fe608060405234801561001057600080fd5b5060043610610036576000
3560e01c80632e64cec11461003b5780636057361d14610059575b600080
fd5b610043610075565b60405161005091906100d9565b60405180910390
f35b610073600480360381019061006e919061009d565b61007e565b005b
60008054905090565b8060008190555050565b6000813590506100978161
0103565b92915050565b6000602082840312156100b3576100b26100fe56
5b5b60006100c184828501610088565b91505092915050565b6100d38161
00f4565b82525050565b60006020820190506100ee60008301846100ca56
5b92915050565b6000819050919050565b600080fd5b61010c816100f456
5b811461011757600080fd5b5056fea26469706673582212209a159a4f38
47890f10bfb87871a61eba91c5dbf5ee3cf6398207e292eee22a1664736f
6c63430008070033

Unlike the last trivial example, this contract has functions. How does bytecode handle function?

Through function selectors, let's find out what they are:

When we call a smart contract function, the data we pass in a transaction specifies the function signature we are calling and any arguments that need to be passed in. A function signature is simply a string of the function name and parameter types.

We have two functions in our smart contract, the corresponding signatures would be:

store(uint256 num)

retrieve()

However, we do not send the data as string, we sent it as bytes. Therefore, we transform the function signature to function selectors - which is first four bytes of the keccak hash of the canonical representation of the function signature.

If you have not installed a solidity REPL already, I recommend you do that now. Here's a list of REPLs you can use, use whatever one you like.

Turning the function signatures to keccak hash and taking the first 4 bytes of the has, we get:

Therefore, the two selectors are:

store(uint256 num) --> 6057361d
retrieve() --> 2e64cec1

The function names are stored as selectors in the bytecode, you can find these selectors in the bytecode:

Moreover, you can also look it up in the compilation details in remix IDE if we compile our contract:

To really understand how these selectors are used let's look at teh data field while making a smart contract call. We can actually use these function selectors to interact with smart contract directly.

First, deploy the storage smart contract in whatever testnet you have test ethers for. Here I deployed the contract to Ropsten: https://ropsten.etherscan.io/tx/0x65987e1a5c3da2d3f33b5ddec73a771f879042757494d0857d40fe22dc2a1c55

Note: Ropsten is deprecated now, you will probably use Goerli, but for our purposes, this does not matter. The steps are the same. if you have Ropsten ETH, feel free to interact with my deployed smart contract.

Once we have the smart contract on testnet, let's interact with it without using ABI. Let's say we want to call the retrieve() function. We know that the selector for this is 2e64cec1. Therefore, to call this function we can grab the contract address from etherscan and send a transaction to the contract with hexdata 2e64cec1 from our metamask.  It should look something like this:

If you did it correctly,  your resulting transaction will success and look like this:

You can see, the correct function was called.

Note: The HexData input is off by default in Metamask, you have to turn it on by going to Settings > Advanced

Next, just to understand this mechanism better, we will call the store() function that takes a parameter. The hexdata we pass in for this function is :

0x6057361d0000000000000000000000000000000000000000000000000000000000000001

Where,

6057361d is the function selector for store() and 0000000000000000000000000000000000000000000000000000000000000001 is the parameter that we pass in the function.

The resulting  transaction is: https://ropsten.etherscan.io/tx/0xfd5bf8429f0bda5d206cd70cfe45afce54f0a75ba274703e20d6a85ebab9d62c

And, you can see correct function got called and parameter got passed in:

If you have gotten here, then you know how to interact with smart contracts directly with functions selectors and what they are.

However we will not stop here. Just to drive the point in, and gain more insight let’s look at this from a different angle:

First, we modify our smart contract, compile and deploy it in Remix. The modified contract code is as follows:

// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.7.0 <0.9.0;

contract FunctionSelector {
    function computeSelector(string calldata _func) external pure returns (bytes4) {
        return bytes4(keccak256(bytes(_func)));
    }
}

/**
 * @title Storage
 * @dev Store & retrieve value in a variable
 * @custom:dev-run-script ./scripts/deploy_with_ethers.ts
 */
contract Storage {
    event Log(bytes data);

    uint256 number;

    /**
     * @dev Store value in variable
     * @param num value to store
     */
    function store(uint256 num) public {
        emit Log(msg.data);
        number = num; 
    }

    /**
     * @dev Return value 
     * @return value of 'number'
     */
    function retrieve() public returns (uint256){
        emit Log(msg.data);
        return number;
    }
}

The changes are highlighted here:

Now we have two smart contracts, that will show two different ways of getting the function selectors. After we compile and deploy the smart contracts, we can now call the functions to check the emitted logs.

if we call the receive() function, we see the following in Remix:

It shows the function selector 2e64cec1

If we call the store() function with parameter 1, we get:

The data field is same was what we passed in earlier: 0x6057361d0000000000000000000000000000000000000000000000000000000000000001

So, Remix does the hard lifting and changes the contract to correct hexdata format for us while making the transaction.

We can also verify that the msg.data returns the same function selector if we call the computeSelector() and pass in “retrieve()” and “store(uint256)” respectively.

That's all for function selectors. Next we will look at how the bytecode uses these function selectors to jump between multiple functions.