The millennium problem
On the road to foreseeing complex Ethereum transactions outcome
There are two different types of transactions in Ethereum: simple value transfers and contract executions. I will refer to the latter as complex transactions in this article. A value transfer just moves Ether from one account to another. If, however, the recipient of a transaction is a contract account with associated EVM (Ethereum Virtual Machine) bytecode — besides transferring any Ether — the code will also be executed as part of the transaction.
Contracts code is most of the time written in solidity — and then compiled to EVM bytecode which is a binary representation of EVM opcodes. Opcodes are instructions that execute on the Turing-complete EVM, which makes Ethereum the first world computer.
If complex transactions are the foundation for a decentralized future, they raise significant security concerns. Contracts can call themselves incredibly often, generating internal transactions, which makes them quite unpredictable. What if one of that calls reaches a scam contract? What if the overall transaction fails for blurry reasons, burning all the gas for nothing?
Overall losses caused by DeFi exploits on Ethereum counts in billion of dollars. Enough to legitimate those “what if” statements and start bringing more security to the ecosystem.
How to maximize people’s confidence when signing complex transactions? How this guardian angel tool would work in an ideal world?
Contract Code Review
Smart contract owners usually have their contracts verified.
The tool could already raise a warning when interacting with an unverified contract.
If smart contracts are wild beasts that need to be tamed, we can’t blame them for hiding: their code is usually public on Etherscan, and the compiled version always is.
Reviewing the solidity code associated with a smart contract, parsing the functions being called, and summarizing any risky function calls to the user could be an interesting first step for our intelligent security tool. You surely want to avoid unintentional setApprovalForAll call. Unfortunately, this approach is not practical since function names can be disguised or a contract can call another contract to obscure what is happening.
Security tool used Reading on a (too)-smart contract
Smart Contract metadata checks 💁♂
A more “social engineering” approach is a better way to secure complex transactions. If the contract has been freshly deployed on-chain or if it’s the first time the user calls it, you might want to raise severe warnings. But it’s not enough. Again, the destination contract could be an angel interface that calls evil scam contracts under the hood.
Complex transaction simulation
The only way to fight against internal contract calls is to dry-run the complex transaction on the latest blockchain state trie to analyze the exact EVM opcodes executed in a specific context like the latest block.
Because the block where the transaction will be mined is unpredictable and the outcome of a transaction depends on it — amongst the contract code and the transaction parameter — the zero risk can’t exist. Still, mixing the above-mentioned web3 checks with a smart transaction simulation would significantly reduce it.
This way, we can trace all the internal contract calls, check their input/output, deduce the final account balance and spot any contract call that grants approval to other contracts.
Hexagate leveraged today, the 10th of August 2022, the power of these techniques to detect the curve DNS exploit pre-transaction!
Hexagate shield
In addition to being the strategy that offers the most security opportunities, transaction simulation is also the most interesting from a technical point of view. Before I get into the nitty-gritty, let me tell you how I got hooked on the topic.
It all started at the latest Ledger Hackaton, full of promising projects. It was also the first one open to the public. Unfortunately, I couldn’t attend but I wouldn’t have missed the pitch session for the world. One of them particularly caught my attention. It was about predicting the outcome of a complex transaction on Ethereum. The team delivered a solid presentation, providing Ledger Live users with the ability to preview already valuable information such as transaction failure/success, account balance and exact gas spending.
Although my colleagues are the most incredible guys in the world [join us], I wondered how they could add so much value in less than two days. A friend who participated in the project gently granted me access to their repository, where I could discover BlockNative did the simulation. These bad kids understood everything about the “fake it until you make it” hackaton culture where feature completely overcomes implementation. They almost won the competition, cheers to them !
The Hackaton winner API call
But what is a Hackaton project for Ledger sometimes is a production solution for other companies. Brave wallet is currently integrating support for EVM transactions simulation and they are investigating third-party simulation platform such as BlockNative or Tenderly.
Now bear with me if you’re brave enough to dive into the EVM simulation world and understand the internals of those simulation platforms.
Let’s get our hands dirty with a bit of practice!
A basic contract execution simulation
I don’t want to spread too much love to my colleagues, but one dude in the Ledger explorer team shared with me a token to communicate with our in-house archive Ethereum node.
You could achieve the same by either joining us ( ͡° ͜ʖ ͡°) or deploying a QuickNode instance in a few minutes.
You only need an archive node if you want to simulate arbitrary transactions at any point in the history of the chain. Simulating a single transaction requires re-executing all preceding transactions in the same block. A basic full synced node is enough to simulate transactions in the latest block context.
Once you have your node running, you can start interacting with it through JSON-RPC procedures, either directly using curl or a library in your favorite language … mine is Python, so I’ll stick with web3.py.
Transaction simulation the hard way
transaction[“to”] is the destination contract address, here it’s the Dai contract
transaction[“data”] is the contract input data one can decode here
Decoded contract input data
To sum up: we are on our way to simulate a balanceOf method call on the Dai contract for the account address 6E0d01A76C3Cf4288372a29124A26D4353EE51BE (c.f inputs in the decoded data) within the latest block.
Note: considering the security focus of this post, it would have been way smarter to demonstrate the simulation with a write contract call (i.e a call that modifies the state of the blockchain like any transfer) rather than a harmless balanceOf. But I’ve conducted my research without paying too much attention to the input data I was testing … And I am too lazy to update the post now. Still, from a technical point of view, it’s enough to get a grip on EVM transactions simulation.
The magic sauce lies in the Ethereum primitive function to call (line 15): debug_traceCall. You feed it with a transaction object, a block information — be it a block hash or “latest” for the latest mined block — optionally a tracer and it will run the transaction EVM opcodes for the provided block context.
The code executes two simulations, the one without tracer (line 28) renders the following output:
EVM opcodes
If this information doesn’t make sense to you, that’s a good sign — it means you’re not a computer.
The second call to simulate (line 29) takes the default Ethereum tracer callTracer as a parameter and returns the following output:
Traced output of a transaction simulation
If this information still doesn’t make sense to you, it’s even a better sign — it means you’re not a Software Engineer — but it encodes valuable insights:
type: the trace Action. It can be CALL or CREATE when using callTracer
from: hex-encoded address. Default to 0 if not present in the tx parameter
to: the address of the contract to call
value: hex-encoded amount of value transfer
gas: hex-encoded gas provided for call
gasUsed: hex-encoded gas used during call
input: call data (hex encoded representation of function and parameter)
output: return data
Here, the output field results from the balanceOf call in wei.
Let’s convert the result in Dai unit, but how? I was happy to google “wei to dai” — for the amusing pun — but only got information about this fellow:
Wei Dei
Pardon my ignorance, Wei Dei is a superstar computer scientist.
Anyway, with a bit of trickery, you end up with the Dai value:
from web3 import Web3
output = int(0x0000000000000000000000000000000858898f93629000)
Web3.fromWei(output, “ether”)
Decimal(‘0.6013818’)
And you can validate the entire experience on Etherscan by searching with the account address from the input data.
The simulated balanceOf output matches the real Dai account balance!
Etherscan information for account 6E0d01A76C3Cf4288372a29124A26D4353EE51BE
As we have just seen, passing a tracer to the simulate function produced a way more concise response than the raw opcode version, which can easily extend to 20 MB.
From the official documentation: “The callTracer tracks all the call frames executed during a transaction, including depth 0. The result will be a nested list of call frames, resembling how EVM works.”
Since we simulated a transaction involving solely one smart contract call, we have a depth of one for all opcodes in the first output and only one top-level dictionary call frame in the second.
It’s worth being aware of Go Ethereum (Geth) allowing anyone to develop their own JS tracer to extract even more information from the raw opcodes. Events (they map to LOG opcodes) are a legitimate candidate for further tracing improvements to reveal potential malicious data stored in the Logs.
Conclusion
EVM transaction simulation is one of the hottest topics at the moment.
It is usually done with a third-party simulation platform like BlockNative or Tenderly. Internally, these platforms rely on the native debug capabilities of Geth (the Official Go implementation of the Ethereum protocol) to simulate any transaction on the latest block and extract information from the opcodes. This process is called “Tracing”.
Since the transaction is run against a specific chain state, and the state may not be the same as when the transaction is confirmed, simulation is also referred to as ‘speculative execution.’
If you’re not happy with the tracing strategy of these providers, or if you don’t like them for any reason (be it the privacy concerns, the limited features they offer, or even their pricing plans), you’re free to implement your simulation.
And if you’re not into simulation — that is very admirable — still, I hope you had a fun ride and learned a thing or two!
See you later, alligator