Tutorial 1. Part 1.
I’m writing a set of simple-to-follow tutorials as a way to reinforce some of the skills I’ve developed myself whilst at the same time sharing knowledge that maybe helpful to others.
I’m keeping these tutorials as short as I can with detailed code examples. Please follow at your own pace and make sure to implement these features yourself & experiment as much as you can.
Do note that these tutorials require you to run a geth implementation by yourself. To be exact, I’m developing these examples for BSC’s implementation of geth.
This tutorial will cover the following:
- Get EVM to execute a Contract function with modified contract storage values.
- Implement native Tracer code to return Storage keys that were looked up during execution of a function.
- Be able to extract data from batch storage lookup, bypassing contract function calls.
- Be comfortable with the debug RPC namespace.
Let’s start with a simple problem:
Suppose you have the address of a contract that implements these Uniswap V2 Pair functions & attributes:
- token0 & token1, pertaining to the addresses of tokens in the pool
- reserve0 & reserve1, pertaining the balances of token0 & token1 for the pool
- getReserves(), which returns reserve0 & reserve1 values.
- swap(uint amount0Out, uint amount1Out, address to, bytes calldata data), which provides the ability to swap 1 token to another.
You’d like to know from that address if you are able to swap tokens successfully and calculate the Pool swap fee, without having to hold any of the tokens.
1. Modifying Contract Storage:
In order to perform a swap of Token A to token B for Pool i, you’ll need to first transfer x amount of Token A into Pool i and then call the swap() function.
The transfer of x amount of Token A into Pool i updates the balance value of Pool i for Token A. When calling the swap() function, the Pool checks if it’s Token A balance is > then the Reserve value for Token A and if so, then Pool proceeds to verify if the specified Token out amount fits the Pool Fee :
One could simply perform “eth_call()” RPC call to simulate a swap transaction if they held either Tokens utilised in the Pool. But how can you do it if you don’t ?
Go-ethereum release 1.9.2 brings about the ability to modify Smart Contract storage values prior to executing the “eth_call()” RPC call:
With this, one is able to successfully simulate a swap call between Token A to Token B via Pool i if the Token A Balance of Pool i can be modified prior the call execution.
The below function is an example function to demonstrate how to override Contract Storage. In this example, the value of the storage key for a particular ERC20 Token balance map & address is overwritten prior to executing a balanceOf() call (0x70a08231 Function signature). The returned balance should match the specified balance value. Do not try the following example with your account address as it will not produce correct results. The storage key for your account will differ to the one in the example. You would need to know what the actual storage key is for your account so that you are overwriting the right storage slot. Please refer to https://medium.com/aigang-network/how-to-read-ethereum-contract-storage-44252c8af925 for some background in reading Smart Contract Storage.
Balances are stored in a Map and “Mappings have a different indexation and should be read in other way. To read mapping value you should know the key value. Otherwise, read mapping value is impossible.” The key is calculated with the known index of the Map and if that is unknown there is no way to derive the key.
To show in a little more detail of what goes on behind the scene, see the code below (Extracted from: https://github.com/ethereum/go-ethereum/blob/master/internal/ethapi/api.go#L840) :
When a “StateOverride” map is provided, storage slots specified in the map will overwrite storage within the state acquired from:
state, header, err := b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)
But as mentioned above, this feature is not useful for the context of this tutorial, if the storage key is unknown. A method that can be used to identify the appropriate storage keys is to utilise Tracers.
2. A simple overview of Tracers:
When the go-EVM component interprets bytecode during an execution of a Contract function (see https://github.com/ethereum/go-ethereum/blob/master/internal/ethapi/api.go#L932 & https://github.com/ethereum/go-ethereum/blob/bb5633c5ee3975ce016636066ec790054ec469e4/core/state_transition.go#L275 & ), a log is maintained if a Tracer is specified. The Tracer will make meaning out of the log and can extract specific information such as:
- Specific OpCodes called during Execution.
- Any calls made to external Contracts & results of calls.
- Storage Keys involved during execution of a function.
There are a number of native golang tracers made available by the go-ethereum community (awesome work from these guys) : https://github.com/ethereum/go-ethereum/tree/bb5633c5ee3975ce016636066ec790054ec469e4/eth/tracers/native
Tracers are executed by pointing your RPC calls to the debug namespace: https://github.com/ethereum/go-ethereum/blob/bb5633c5ee3975ce016636066ec790054ec469e4/eth/tracers/api.go
To create your own Trace simply study the available ones and build on top of those. Here is an example Tracer that I wrote to return Storage Keys looked up during a contract function execution:
To implement the tracer above, insert the .go file to the /eth/tracers/native folder and rebuild geth. Below is an example function to demonstrate how to execute the tracer (I’ve named the tracer sLoadTracer) to fetch the storage key for reserve values for an LP:
Example results (for LP: “0xDCbc1D9D48016b8d5F3B0F9045Eb3B72F38E6B93” BSC):
That’s the end of Tutorial 1. Part 1.
In Part 2, we will build further on top of this and try to extract the Storage keys for the following attributes of a Uniswap V2 Lp Contract:
- Token 0 and Token 1
- Reserve0 and Reserve 1
And then we try to fetch the right storage keys for Token 0 and Token 1 of the Lp Contract. I will demonstrate implementing your own function in the debug namespace(“eth/tracers/api”) and with some concurrent techniques, execute Tracers concurrently to speed up queries.
In Part 3, once we are able to gather all storage keys, we will put everything together and perform a Swap call with modified balances and derive fees from results. We then store all the attributes in a Key / Value Pair DB (using BadgerDB).
In Part 4, I will demonstrate how to parse incoming blocks, fetch all attributes of all Lps active in the block (Token0, Token1, Reserves0, Reserve1, Fees) in the block and store it in our local DB.
I hope what I’ve written here will be useful to you ! Please don’t hesitate messaging me on Twitter @FejLeuros.
Shout me coffee or more : 0x2ADCB204d23E6983704B6C8b879a921f6D65BBB9