Abstract
The journey of the tool
The journey of creating the tracer tool began with a requirement: we needed to identify contract addresses that contained certain opcodes for better analysis and security auditing. Initially, we built a JavaScript version of the tool. This early iteration allowed us to interact with the blockchain and search for specific opcodes in blocks, providing a foundation for the solution.
We iterated on a few approaches, including using JS scripts for parsing and developing a native tracer, eventually coming up with a solution that provides excellent coverage of analysis in terms of the code executions being considered. This solution also delivers good performance, making it feasible to efficiently search a long stretch of historical data.
This solution includes a tracer to identify contract addresses within blocks and locate contracts containing specific opcodes. The specific implementation can be found here, and below is an example request:
Request Example:
RPC="http://172.16.3.16:8547"
NUMBER="0x399e"
curl -s -X POST -H "Content-Type: application/json" ${RPC} -d '{
"jsonrpc": "2.0",
"id": 1002,
"method": "debug_traceBlockByNumber",
"params": [
"'"${NUMBER}"'", {
"tracer":"contractTracer"
}
]
}' | jq
Response Example:
{
"jsonrpc": "2.0",
"id": 1002,
"result": [
{
"result": [
"0xf63f8ba05d790b93db0005de3e65d90cd0b68bf6"
]
}
]
}
In this request, contractTracer
is used to trace contract code in a specified block. By matching a specific opcode, it filters contract addresses. The result is an array of contract addresses that contain the specified opcode.
JavaScript Version
The first version of the tool was built in JavaScript, leveraging Node.js to interact with blockchain nodes. It worked by iterating over blocks and using the RPC endpoint to analyze contract code. The JavaScript version was functional but had limitations:
Retrieving Transactions from Blocks: By querying blocks (e.g., using
eth_getBlockByNumber
), all transactions within that block were obtained.Checking Each Transaction's Receipt: Using
eth_getTransactionReceipt
, the receipt of each transaction was queried. In the receipt, thecontractAddress
field would display the address of any newly created contract.Confirming If the Address Is a Contract: For each
contractAddress
,eth_getCode
was used to query the code at that address. If the returned code was non-zero, it indicated that the address was a smart contract. If a contract was created,eth_getCode
was used to query the opcodes of that contract, matching them against the specified opcode. If a match was found, the contract address was written to a file.
This approach has a limitation: it cannot detect contracts created by the CREATE
or CREATE2
opcodes. The reason is that it only checks the To
field of a transaction, which shows the transaction's target address. However, contracts created with CREATE
or CREATE2
are usually generated by internal calls within a contract, not by transactions directly sent from an externally owned account (EOA). As a result, these contracts don’t appear in the tx.To()
field because they are created during the transaction execution, not as part of the transaction's target address.
Although callTracer
can be used to check for opcodes used in a transaction, this method requires decoding a large amount of data, which can lead to frequent timeouts and significantly affect performance. This is especially true when scanning a large range of blocks. Therefore, we decided to explore a solution that requires fewer RPC calls and is much faster.
Golang version
Golang is usually faster than JS. By implementing a native tracer directly in the Golang client, we could reduce encoding/decoding and networking overhead. This led us to focus on developing a native tracer.
CaptureEnter Iteration
To address the limitations of the JavaScript version, we introduced the CaptureEnter
iteration. In this version, we created a tracer within the node to track opcodes during contract execution. We added the possibility for users to customize all the tracers within XDC Golang client. This feature is utilized in our tool to specify the opcode of interest. This tracer accepted an opcode and returned the contract addresses containing the specified opcode.
This version focused on enhancing the tool's performance and scalability. It achieved a significant reduction in processing time (approximately 45-fold). The first approach took 1 day to process 22k blocks, while the improved version processed 1 million blocks in the same amount of time. This makes the tool far more practical for handling larger block ranges
CaptureState Version
The latest iteration, known as CaptureState, implemented its main logic in CaptureState, allowing for precise tracking of contract deployment and execution. Instead of analyzing static code, this version focused on actual execution threads of contracts, enabling us to track the code that was actively used rather than dormant.
Key Logic Explanation
When implementing the tracer, it follows these main steps to locate and filter contracts:
Initialization and Setup: In the Go code, the tracer's structure and initialization function are defined. Parameters in the structure control which opcode to search for.
-
Opcode Matching: While going through blocks and executing code, the tracer compares each instruction’s opcode with the user-specified opcode.
Note: If a contract contains an opcode but it is not called, the tracer will not detect this opcode.
Matching contract addresses are added to the result array.
Limitations
Currently, the tracer directly compares opcodes without differentiating between PREVRANDAO
and DIFFICULTY
. Both share the same opcode value (0x44).The PREVRANDAO
opcode was enabled after block 76321000. Therefore, when using this tool, if you search for this opcode in blocks before 76321000, the matching results will correspond to the DIFFICULTY
opcode. Understanding this limitation is crucial for accurately interpreting the results.
How to Use (Script)
For easier batch processing and data storage, a Node.js script opCodeFinder.js
is provided and the implementation can be found here.This script searches for contracts with specific opcodes in a given block range and stores the results in a .txt file.
Script Example:
node src\opCodeFinder.js 72290000 latest PREVRANDAO https://rpc.ankr.com/xdc
Parameters
[START_BLOCK]: The starting block number for the search, e.g.,
72290000
.[END_BLOCK]: The ending block number or
latest
for the most recent block.[OPCODE]: The opcode to search for, e.g.,
PREVRANDAO
(case-sensitive).[RPC_ENDPOINT]: The URL of the RPC node, e.g.,
https://rpc.ankr.com/xdc
.
After running this script, all matching contract addresses are stored in a specified txt file for further analysis and use.
Results and Analysis
After developing the CaptureState
version, we ran extensive analysis to identify contracts that might be vulnerable due to the presence of specific opcodes. For instance, contracts containing the PREVRANDAO
opcode were of particular interest because of their potential impact on randomness generation and security.
Through our analysis, we identified several contracts that could be vulnerable based on their use of certain opcodes. This information is crucial for developers and auditors to assess the security of smart contracts and take appropriate measures to mitigate risks.
By using this method, developers can efficiently locate contracts with specific opcodes on the blockchain, providing valuable support for security audits and code analysis.
Discussion (0)