Advanced ABI Decoding
Last updated
Last updated
Starting with Lattice firmware v0.16.0, your device has the ability to automatically decode more complex ABI-encoded data, such as multicall
patterns, commonly used in crypto dApps like Gnosis Safe and Uniswap V3.
Let's go through a more advanced example to highlight how the information on your Lattice screen can be used to get a high degree of confidence over what you are signing. Consider the following payload, which comes from Uniswap V3 (this is the unfiltered version, pure hex):
If you send this to your Lattice, you will see the following decoded data:
Let's take a dive into this example by first looking at the contract being called. In this case, the user has tagged the address Uniswap V3
, which corresponds to this contract. If this had not been tagged, you would see the contract address 0x68b3465833fb72a70ecdf485e0e4c7bd8665fc45
.
You can look up the contract code on Etherscan here, where you should see a series of 63 contract source files - use the search bar to look for functions and definitions you want to inspect throughout this exercise.
Now that we have the source code pulled up, we're ready to verify what's going on. The Lattice screens above show that the entry point being called is multicall
, since that's the first function listed on the screen.
NOTE: On Lattice decoding screens, function calls are closed with two brackets, [[ ]]
, while parameters are closed with single brackets, [ ]
.
Indeed, this function can be found in the contract source in MulticallExtended.sol
:
Interestingly, that function is just performing some sanity checks on deadline
and passing the data
to another instance of multicall
. We can search through the code again and we find the next instance of multicall
, this time in Multicall.sol
.
This function is taking an array of bytes
and using each one to perform a delegatecall
on some function in the same contract's source code. This last part is important because it means no external contracts are being called -- everything you need to know about what's happening is on the Etherscan source page we've been already looking at.
Back to the Lattice screens. You should see that multicall
has two parameters specified on the screen: deadline
and data
. These appear to match the first multicall
function definition we found on Etherscan. So far so good.
Now take a look at data
. You will notice it contains an array of more function calls. That's because your Lattice is actually decoding the individual bytes
items that are getting looped through in the outer multicall
request. This is what we refer to as "nested ABI decoding" because there are two layers of data being decoded: the original payload, and now each one of the bytes
fields in the data
param.
The first nested function call is exactOutputSingle
. Let's go back to the source code and look this function up. You can find it in V3SwapRouter.sol
:
No comments. We should search again and see if there is a definition. Looks like there is one in IV3SwapRouter.sol
(I
usually denotes "interface", which is where function comments usually live):
So based on the comments (and basic inference), it appears this function is defining the boundaries for the swap function we would like to make. Namely, it specifies the recipient, tokens, and amounts that are within scope of the desired swap.
One important thing to point out with this function is that there is only one parameter and it is of type ExactOutputSingleParams
. This is not a native ABI type (e.g. addresss
, uint256
, etc) - it is a defined type. We can do another search on the Etherscan source page and find a definition in IV3SwapRouter.sol
:
Defined types are often used by smart contract writers when a particular set of parameters is reused across functions - doing so can prevent errors and bugs.
Displaying parameter names
Your Lattice should display named parameters, such as [data]
, if it is able to fetch the contract data from Etherscan (or similar explorer, e.g. Arbiscan). This is only possible if the author uploads the verified contract source code, i.e. "open sources" the contract. This is the norm in Ethereum, but may not be as common in other ecosystems.
If no source code is found, or if the Etherscan request fails, the requester (MetaMask or Frame) should fallback to searching 4byte for the definition. If this happens, your Lattice will end up displaying generic parameter names like [#1]
, [#2]
, etc. This is because 4byte does not hold source code - it is only a repository for raw function definitions, which do not include parameter names. In our current example, the nested function definition is:
exactOutputSingle(
(address,address,uint24,address,uint256,uint256,uint160)
)
This is still enough information to decode the parameters (based on type), but the requester can't tell your Lattice what they are named.
You probably noticed from the above screens that nested functions do not display named parameters. Instead, you see [#1]
, [#1-1]
, [#1-2]
, etc. This is because while our current example only makes calls internal to the same contract, more advanced examples might call out to external contracts, such as proxies. This becomes very tricky very fast because there is no specific way to know the address(es) of the external contract(s) being called - there are an infinite number of possible patterns and even covering common ones would introduce a lot of complexity and require continual maintenance. As such, we cannot request nested parameter names from Etherscan (which requires an address) and must instead rely on 4byte. In future updates we may add display of real param names in specific cases where nested calls are being made to functions inside the same contract, such as our current example.
This article talks more about how your Lattice utilizes the ABI spec and its limitations.
You can use the definition above to determine which parameters have which values; in this case, tokenIn
(#1-1
) is WETH
, which is another address that has been tagged by this Lattice's owner. fee
(#1-3
) is 500
. It's unclear what unit this is, but as you may have noticed by now, the entire contract is open source, so you can figure it out if you dig deep enough into the code. We will leave that as an exercise for the reader 😄.
The next nested function is refundETH
. Here's the info on that:
This function takes no arguments and refunds any ETH that is not spent in the swap. This is necessary because you call the swap with the "worst acceptable price", and usually you will get a better price than that, meaning you will likely have extra ETH that does not get swapped and should be refunded.
In summary, there are three separate functions getting called:
multicall
- accepts an array of calldata bytes
arrays plus a deadline
integer
exactOutputSingle
- nested function, makes the swap
refundETH
- nested function, refunds any unused ETH
After this exercise you should be quite confident that you are signing what you think you are. Although it is unlikely that all of your transactions will demand this level of scrutiny, remember that your Lattice should give you enough information to perform as much due diligence as you see fit.
The above exercise is only possible because of published contract code on Etherscan. If the contract you are using does not have published source code, this should make you skeptical of it. And if your Lattice does not decode the calldata at all, that should make you extra skeptical, as this means the contract was not published and the function you're calling is not defined on 4byte.