How to Work with Ethereum Call Data
Mar 11, 2024
David Tilšer
Have you ever been browsing through Etherscan, looking at transactions, and noticed a similar string down there and thought, I wish I could understand that?
Yes, it happened to me too. I feel you. And that's why today we're going to look at it and explain how such a chain is formed.
Let's start with something simple. Open some of the Uniswap V2 pair smart contracts. For example this one. Our first task will be to call a simple function without any parameters via eth_call request. We will send the request to the Ethereum node. Let's choose for example factory() function, it meets our requirements for the first task.
So what do we need to do? First of all, we need some software. To avoid having to implement sending requests from code, we will use Postman in this article. Open it or install and open it. Then we need to have a node. Go to Infura or Alchemy, if you do not have an account, create one, create the project and get the URL to the Ethereum Goerli network. If you are using Infura, the final RPC URL should look like this:
https://goerli.infura.io/v3/<TOKEN>
This is the RPC URL to the node. We will send requests to the node, and the node will return the data from the blockchain for us. It works the same way that Metamask, for example, works. Metamask orchestrates the request depending on what you want to do and sends it to the node.
If we check the Ethereum specification, we can find out what the eth_call request looks like.
So how do we change this data to let the node know that we are interested in the "factory()" function on the Uniswap pair smart contract? How does EVM know which function we call on a smart contract? Thanks to the so called "selector". The selector is the first 4 bytes of the function name along with parameters encoded in keccak256. Easy, right? End of this part, you already know how to do it.
Ok, that was just a joke. Let's show you how to get something like this. First, let's show it in Solidity. Open your favorite IDE for Solidity. I will use Remix for this little task.
I assume you are familiar with the Solidity implementation and you know how to deploy the contract and call the function. Do it and as a parameter passes our function "factory()". The result should be 0xc45a0155. This is the selector of our function. Let's break it down into more steps and understand it better. Modify the function like this:
And call it with the same parameter. You will get: “0xc45a01550ceb4bc5c6b2e6f722b5033a03078f9bd6673457375ba94c26ac1cf0”. This is the function name encoded in HEX. Now we just take the first 4 bytes. In the HEX world, 1 byte is equal to 2 characters. So after "0x" we take 4 * 2 = 8 chars. And the result is what we got above - 0xc45a0155. The selector. Function above can be simplified and have only one line:
Ok, that was easy. Now we have a selector. How do we get a result? Let's test it in Postman. Create a new request, choose POST and in the URL put the address to your node. Choose "raw" and "JSON" as body. Then let's use json from the specification I mentioned above and modify it. We don't need the attributes from, gas, gasPrice, so remove them. As "to" we pass the address of the Uniswap V2 pair contract and as "data" we pass our selector. This is it. This is what it should look like:
Let's test it. Click Send. You should see this response:
Now go to Etherscan and check it. If you call the factory in Etherscan, you will get the result "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f". Let's remember that our result from node is 32 bytes. This is always the same. An attribute in a response is always 32 bytes, that's why our result from node has so many leading zeros. If you remove the zeros, you get the same result as Etherscan. So we did it! We understand how to call a simple function using eht_call request to node. Let's move on to more difficult examples.
Calling function with parameters
This was the simplest call we could have made. We called a function with no parameters. Let's try to call another function the same way using eth_call. We'll use the same contract and try balanceOf. When we open it on Etherscan, we can see that there is a parameter which is of the datatype address.
Let's remember that each parameter, or let's call it input, is 32 bytes long in hexadecimal format. So let's create our data.
Create a selector. Use the method we used above. As input use "balanceOf(address)". It is important not to forget the parameters.
Let's take as input what we want to put there and make a 32 byte hexadecimal string.
If you choose this input "0x186b57aFFE222D6176347D338Ed66Ea2e20D630d", this should be the final data:
Remember, a byte is two characters.
So request will be:
And you will get the same response as in Etherscan.
Static vs Dynamic Variables
Static
Let's test our knowledge and then add advanced knowledge about dynamic variables. I have prepared a test contract where we can test our knowledge. The contract is on the Goerli network and the address is 0xe67A0632A497DB5EED170A8E1D19151C63Ee8011. If you open the code, you can see that there are three functions. One is a very familiar to us createSelector function. The other two are the staticParameters and dynamicParameters functions. With the knowledge we have now, we should be able to call the function with the static parameters. Ideally, we should call the function with the parameters that the function returns true.
I need a selector. I'm going to create a selector function on contract here. As input must be this “staticParameters(uint256,address,bool)”. Result is “0xba441de2”.
If you check the code of the function, you can see that it returns "true" only if param1 is 15, as param2 must be the address of the contract and as param3 must be boolean true. So let's send these parameters.
The first parameter must be 15. The hexadecimal value for 15 is "f". We need to add zeros in front of this value to have 32 bytes. The final value should be: "000000000000000000000000000000000000000000000000000000000000000000000f".
The second parameter must be the address of our contract. So we take 0xe67A0632A497DB5EED170A8E1D19151C63Ee8011. Let's remove the "0x" at the beginning and convert it to 32 bytes. The final value should be "00000000000000000000000000e67A0632A497DB5EED170A8E1D19151C63Ee8011".
The third parameter should be true. True is "1". So the final value should be "0000000000000000000000000000000000000000000000000000000000000001".
Let's put it together. I'm going to divide the parts into lines for clarity.
Let's try it! Change the address "to" in Postman to our contract - "0xe67A0632A497DB5EED170A8E1D19151C63Ee8011" and enter our result in the data field.
It should look like this:
If everything is correct, we should get true in response. True is "1", 32 bytes long in hexadecimal format. That means “0x0000000000000000000000000000000000000000000000000000000000000001”.
Good, we made it! Now let's have a look at dynamic types!
Dynamic
In this final chapter of this article, we will call a dynamicParameters function. Calling dynamic types is a bit more complicated. First of all, you need to specify where the dynamic parameter is (it must be after inputs), then you need to specify length in the case of bytes or string, and size in the case of array. Let's go step by step. If you have dynamic types, it is better to start from the end.
The last parameter is "bytes". For example, we want to pass "David". Let's encode "David" in hexadecimal format. It is "4461766964". As always, make it 32 bytes. In case of bytes, it has to be padded to the right. So the final value should be - "4461766964000000000000000000000000000000000000000000000000000000".
Before the dynamic type "bytes" we need to give information about the length of the dynamic type. It is 5 bytes. So the final value should be - "0000000000000000000000000000000000000000000000000000000000000005".
Next is an array of uint. To make the dynamicParameters function return true, we need to send numbers that sum to 10. We will send two numbers 5 and 5. Let's add 5 twice as 32 bytes. So we will add - "0000000000000000000000000000000000000000000000000000000000000005" and "0000000000000000000000000000000000000000000000000000000000000005".
Now we need to tell how long the array is. The length of the array is two. So hexadecimal 32 bytes long value is "0000000000000000000000000000000000000000000000000000000000000002"
Now we need to provide information where the data for the last parameter is. Measurement is from the beginning of encoding (almost from the beginning, we do not count selector). I know I have two parameters, that's 2*32 bytes. And then I know that I have an array with 2 inputs. Array also needs information about how big array is, so that's 3*32 bytes. So the information about the last parameter has to start at 5*32 bytes = 160. This is in hexadecimal "a0". So the value in 32 bytes will be "0000000000000000000000000000000000000000000000000000000000000000a0".
Now we have to tell where the data for the first parameter is. It starts after two parameters, so 2*32 bytes is 64 and 64 in hexadecimal is "40". So the final is "0000000000000000000000000000000000000000000000000000000000000040".
And we need the selector as always - 0xe3a29e20
That's it! Let's put it together. I'm going to divide the parts into lines for clarity.
Now we can try it! Just change the data attribute.
The result should be true - "0x00000000000000000000000000000000000000000000000000000000000000000000000001".
Try changing the numbers in the array so that they don't add up to 10. You should get a wrong answer.
That's all for today. Thanks for reading!
Building successful
products.together.
© 2008—2023 Cleevio
Lesnicka 1802/11
613 00 Brno
Mississippi House
Karolinska 706/3
186 00 Prague
Prague office
Brno office
CIN 18008844