How to Navigate Contract Code (Solidity) as an Analyst
Navigating contracts across protocols and chains to find that one bit of data you need can be hard - let me make it easier for you.
Other Beginner Resources:
It’s easier and more fun to build with a community, come join the Bytexplorers to learn and earn during your data journey.
Oftentimes you start looking into a protocol, and quickly find an example transaction that calls 5+ contracts and evoke 20+ logs. You go to their docs (if they have any) and maybe see something like this:
How can you navigate these solidity contracts to find the answers and data you need for your analysis? I’ll cover a full example with my tips and tricks in this guide. We’ll be studying the new Zora x Uniswap contracts, deployed in early August 2024.
You should try searching on Dune for existing queries/dashboards covering a protocol first, since it is usually easier to read/fork existing SQL than to go straight into reading Solidity.
Starting From Transactions
Whenever you start analyzing a protocol, there are two example transactions you want to find:
The main contract deployment transaction (deploying a new nft or uniswap pool)
The main action transaction (swap, mint, etc)
I say “main” because oftentimes there are many contracts and actions, but they usually all stem from one starter contract/action. Most of the time your actual start comes from a tweet somewhere, and you have to work your way to these two transactions.
Let’s take the example of Zora’s new uniswap extension on their protocol - essentially allows you to create an ERC1155 that can be traded on uniswap as secondary as an ERC20 instead of opensea/blur. They tweeted out this short overview guide and also their first mint on this new protocol. In true crypto fashion, the real documentation doesn’t come till much later (or sometimes never). You should still try searching for documentation first, but this example will rely on purely onchain data for sleuthing.
I normally find the easiest way to get the main contracts and transactions is by just interacting with the product myself. So I minted some limitless zorbs when it first went live.
This transaction gives me the main action I can refer back to later for volume calculations, but more importantly it gives me two of the main contracts to refer to:
ZoraTimedSaleStrategyImpl: I know this must be central to this new protocol upgrade since its the main “to” contract, and likely is where configurations for mints/secondary are set
Limitless Zorb: I know this is the ERC1155 token that somehow gets converted to an ERC20 for uniswap trading.
Contract pages on explorers always have a “creation tx hash” link (usually you are first taken the token page, then you have to click on the contract again to get to the contract page).
So if we quickly aggregate what we have, we get:
ZoraTimedSaleStrategy (ZTSS)
created txn - a quick look tells me this is NOT a factory contract, because it calls a “Deterministic Caller” contract which is used to deploy with CREATE2 instead of CREATE. This deploys it at an exact address, usually so the same contract can have the same address across multiple chains.
The ERC1155/ERC20 Collection (Limitless Zorb)
creation txn - I see a “factory” is called to deploy this, so likely this is a factory created contract where each collection gets its own deploy.
a mint txn - the one I sent from the product interface
we don’t have a secondary/uniswap transaction yet
With this base in mind, we can define a question and dive into the code.
Diving Into the Contracts
Alright, once you have a general lay of the land with the main contracts and transactions, you will be ready to start sleuthing around. You should lay out your questions and try and navigate the code to answer each one, for this guide I want to answer the question:
OUESTIONS: “How is the Uniswap pool initiated after the mint period ends, who controls that first LP position, and who gets the swap fees from it?”
If you think you’re good at this, try and answer this question using only the links I’ve given thus far.
We can guess that permissions/functions on sales are tied to the ZoraTimedSaleStrategy (ZTSS) contract, and not to the collection contract itself. Funny enough, I don’t see any events from this contract emitted in the collection creation transaction - that’s a signal for me that you can create an ERC1155 and likely attach on the ERC20/timed sale component afterwards.
I go the ZTSS contract code page on the explorer, and first thing I see is that it’s a proxy. This means that the “proxy” handles storage of variables but there is an “implementation” contract with the actual contract logic. You usually have to go to the “read proxy” or “write proxy” tabs to then get the implementation address:
⚠ WARNING: Proxies can be upgraded to point at different implementations to change the logic. The proxy in this example was upgraded on 8/23/2024 to this implementation instead, but the rest of the guide references the old implementation. This is to introduce a new launchMarket() condition.
Alright, now we’re getting somewhere…. kind of. Looking at the code explorer, I can see there are actually dozens of inherited/referenced contracts here.
A lot of this you can straight up ignore - however the most important ones to keep an eye on are the “interfaces”. Interfaces are used to let one contract call another contract function. I know that Uniswap is actually a different set of contracts, so I want to look for where the uniswap interface for creating/adding liquidity to a pool is.
This is as simple as ctrl+f “IUniswapV3Pool” which I see in the imports at the top of the file. I get 5 hits, 2 of which are the import line and 3 of which sit on lines 285-290.
Scrolling up a bit, I can see that it’s this “launchMarket()” function being called. The notes here say that anyone can call it after a primary sale ends to create the uniswap pool.
Now if I scroll back down, I will notice that I don’t see the uniswap pool being created or liquidity added actually - just a price being set. Instead, the end of the function has an “IERC20Z” interface being called with the “activate” function.
Alright - so now we know we have to find an ERC20Z contract somehow and check the “activate” function definition. Your mind should hopefully go to finding the ERC20Z related to the limitless zorb collection. There are a few ways to go about this:
Expect a read function on the collection to get the erc20 address
Look for some event from the collection creation txn for the erc20 address
Look for some event/function on the ZTSS that ties the erc20 contract to the collection contract
The first two are actually dead ends in this case, because the erc20 is an optional component of the collection. If I go to the ZTSS proxy page and the “logs” column, I can see a bunch of SaleSet() events that seem to contain a “erc20zAddress” and “poolAddress” fields. This is not always the most foolproof method, I recommend you plug in the contract into my EVM quickstart dashboard and check examples of the most emitted events/functions.
I’ll click into one of the recent SetSale() transactions to see what is going on. By following the logs, I can see that the ERC20z and uniswap pool are actually already deployed (CREATE2) when the sale extension is activated! It just doesn’t have any liquidity yet, so no trades can be made.
The ERC20z is also deployed from the ZTSS contract, so I can be 100% sure there is some read function to search up a collection and gets its ERC20z (and uniswap pool) details. Sure enough, we have a “sale()” read function:
I plug in the Limitless Zorb collection address and token id (from the URL) and get our erc20zAddress and poolAddress! You could have also found this by ctrl + f “IERC20Z” in the ZTSS implementation code if you had noticed it earlier 😉.
Alright now we the erc20z address - so now let’s get back to looking for the “activate()” function. It’s a proxy, so I again go “read contract” → “implementation” to get to the actual code. Then I just ctrl + f for “function activate(“ since that’s how functions are defined in solidity.
Within that function, I see this key line that calls the Uniswap position manager through an interface to create the first liquidity position:
It actually goes to a “royalties” address! Alright, one more ctrl + f to find where royalties is defined.
It’s a public variable, which means it automatically gets a read function. I go read contract “royalties()” to get the royalty contract address:
We’re so close now - let’s go to the royalties contract code next. This one is simple in terms of there are just two relevant public functions: claim() and claimFor() which passes the fees from the liquidity pool position to the creator of the token. The complexity here lies in reading “internal” functions, which are basically functions that can’t be called outside the contract and are called within the external functions.
I usually paste the code into chatgpt and ask it “Here is a solidity contract. Can you draw a node graph that shows all function call invocation flows? Show public/external functions in orange, internal functions in light blue, and interface functions in grey. Display this as a tree diagram from left to right, where the start of a function call is on the left. Increase figure size to fit all nodes.”
So the the function invocation flow we’re interested in looks like this:
claim() → _claim() → _collect() / _transfer()
You will only see “claim()” called in the transaction traces since in-contract calls don’t have traces (besides memory traces but we won’t get into that). _collect() gets the fees from the pool by calling the position manager uniswap interface, and _transfer() passes those fees to the creator.
Notably, the _transfer() function has a fee that goes to Zora team.
We can find a public variable feeBps in getFee() function, so we read it from the contract “read” page:
Its in “bps” which means basis points, where 100 basis points goes into 1 percentage point. 2500/100 is 25, so they take a 25% fee off of the initial LP rewards! If you go to the uniswap pool you’ll see it takes a 1% fee on all swaps. So effectively, the creator takes a 0.75% fee and Zora takes a 0.25% fee on all swaps.
Notably, there is no withdraw LP position function anywhere in the contract - so that LP position can NEVER be withdrawn from the pool. The creator (and zora) can never rug the pool, albeit those ERC20/ETH LP tokens are unclaimable now too (unless you buy out the whole curve).
All in all, we have our answers now:
How is the Uniswap pool initiated after the mint period ends? The launchMarket() function, and anyone can call it. The pool is already deployed with the erc20z on SetSale() event.
Who controls that first LP position? No one, it goes to the royalty contract and is burned. The royalty contract can still collect fees from the position though.
Who gets the swap fees from it? It’s split between Zora and the Creator, effectively 0.25% and 0.75% respectively.
Contract Mastery
What a journey! If you’ve follow everything here, then you truly have a grasp of how to navigate across contracts, interfaces, proxies, and transactions. Knowing how to do this by only relying on block explorers and tools like Dune will make you a super analyst.
Again, all of that was to answer “How is the Uniswap pool initiated after the mint period ends, who controls that first LP position, and who gets the swap fees from it?”
Our list of found resources over this journey:
ZoraTimedSaleStrategy (ZTSS), (impl)
created txn - a quick look tells me this is NOT a factory contract. So it must be shared across all collections.
The ERC1155/ERC20 Collection (Limitless Zorb)
creation txn - I see a “factory” is called to deploy this, so likely this is a factory created contract where each collection gets its own deploy.
a mint txn - the one I sent from the product interface
Royalties Contract (not a proxy)
You’ll pick up patterns on how functions are structured and stuff like system parameters and deploys are managed the more you dig into the data. Keeping a running list of notes like I have above when doing your research will make your life a lot easier too.
Happy learning, and good luck with your queries!