In common on-chain data parsing, there is often a large demand for determining the type of contract. This article will judge on relevant standards and engineering practices to determine whether the contract belongs to ERC20 / ERC721 / ERC1155.
For more use cases, you can refer to the developer documentation of Chainbase, or ask the original author through Discord. We are happy to discuss issues related to Web3 infra, Data SDK, Chainbase APIs, etc.
Rules to determine different contracts
With the improvement of industry standards, the Functions and Events corresponding to each contract have detailed regulations. Therefore, using the Functions that a contract supports to determine its type is a very efficient and accurate way.
The following table lists some functions that ERC20 / ERC721 / ERC1155 must support, which can provide us with a basis for determining the contract type:
EIP-20 | EIP-721 | EIP-1155 |
---|
allowance(address _owner, address _spender) | approve(address _approved, uint256 _tokenId) | balanceOf(address _owner, uint256 _id) |
approve(address _spender, uint256 _value) | balanceOf(address _owner) | balanceOfBatch(address[] calldata _owners, uint256[] calldata _ids) |
balanceOf(address _owner) | getApproved(uint256 _tokenId) | isApprovedForAll(address _owner, address _operator) |
decimals() | isApprovedForAll(address _owner, address _operator) | safeTransferFrom(address _from, address _to, uint256 _id, uint256 _value, bytes calldata _data) |
name() | ownerOf(uint256 _tokenId) | safeBatchTransferFrom(address _from, address _to, uint256[] calldata _ids, uint256[] calldata _values, bytes calldata _data) |
totalSupply() | safeTransferFrom(address _from, address _to, uint256 _tokenId) | setApprovalForAll(address _operator, bool _approved) |
transfer(address _to, uint256 _value) | safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) | supportsInterface(bytes4 interfaceID) |
transferFrom(address _from, address _to, uint256 _value) | setApprovalForAll(address _operator, bool _approved) | |
symbol() | transferFrom(address _from, address _to, uint256 _tokenId) | |
| supportsInterface(bytes4 interfaceID) | |
However, in engineering implementation and data analysis, we noticed that there are a large number of special contracts on the chain, such as:
- Early contract: Contract deployment is earlier than standard formulation;
- Simplified contract: not all specified methods are implemented;
Although they do not fully meet the standards, they also have a series of events such as Token approve/transfer. In actual operation, they can also be classified into relevant standards for analysis.
Example Showcases
Based on the above knowledge, Chainbase's solution can be used as a reference.
Our engineering implementation adopts a more streamlined and fully verified effectiveness criterion, as shown below:
Bytes Signature | Functions For ERC20 | Functions For ERC721 | Functions For ERC1155 |
---|
0x70a08231 | balanceOf(address _owner) | balanceOf(address _owner) | |
0xa22cb465 | | setApprovalForAll(address _operator, bool _approved) | setApprovalForAll(address _operator, bool _approved) |
0x00fdd58e | | | balanceOf(address _owner, uint256 _id) |
0x095ea7b3 | approve(address,uint256) | | |
0xa9059cbb | transfer(address _to, uint256 _value) | | |
0x42842e0e | | safeTransferFrom(address _from, address _to, uint256 _tokenId) | |
0xf242432a | | | safeTransferFrom(address _from, address _to, uint256 _id, uint256 _value, bytes calldata _data) |
0x2eb2c2d6 | | | safeBatchTransferFrom(address _from, address _to, uint256[] calldata _ids, uint256[] calldata _values, bytes calldata _data) |
How to implement the project?
The problem that needs to be solved in engineering is how to get a list of all the methods of the contract. To solve this problem, we have adopted two detection methods, each with its own advantages and disadvantages, and mixed use can make up for each other's shortcomings.
Method 1: Obtain the OpCode of the contract to generate the Functions Signature list
- Pros: fast
- Cons: Need to find the logic contract behind it, and there are some occasional problems in the judgment of complex proxy contracts and non-standard proxy contracts
Note: The processing method of the proxy contract
If the main contract is a proxy contract, we need to further obtain the address of the logic contract behind it, and obtain the functions supported by the contract through the logic contract.
The specific processing methods of several contracts are as follows:
EIP-1167 is a simple clone contract, the OpCode starts with 363d3d373d3d3d363d73, 10-29 bytes are the address of the main contract
For example, 0xde400a2ed5a5f649a8cff6445a24ab934ff32b2c
curl -X "POST" "<https://ethereum-mainnet.s.chainbase.online/v1/YOU_SECRET_KEY>" \
-H 'Content-Type: application/json; charset=utf-8' \
-d $'{
"jsonrpc": "2.0",
"method": "eth_getCode",
"id": 1,
"params": [
"0xde400a2ed5a5f649a8cff6445a24ab934ff32b2c",
"latest"
]
}'
{
"id": 1,
"jsonrpc": "2.0",
"result": "0x363d3d373d3d3d363d73e38f942db7a1b4213d6213f70c499b59287b01f15af43d82803e903d91602b57fd5bf3"
}
Get the main contract address:
fmt.Sprintf("0x%s", OP_CODE[20:60])
In this way, we get the main contract address: 0xe38f942db7a1b4213d6213f70c499b59287b01f1
EIP-1967 defines a series of storage slots to store the addresses of some proxy contracts, for example:
- Logic contract address storageļ¼ bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1))
- Beacon contract address storageļ¼bytes32(uint256(keccak256('eip1967.proxy.beacon')) - 1))
EIP-1967 stipulates that if āLogic contract address storageā is empty, then the āBeacon contract address storageā will be used. So we can simply judge whether the contract is an EIP-1967 proxy contract by judging whether the contract has these two slot addresses, and then get the logic contract address behind it through eth_getStorageAt.
For example, [0x49542ad0f1429932e5b0590f17e676523f0a6369(https://etherscan.io/address/0x49542ad0f1429932e5b0590f17e676523f0a6369#code)]
First determine whether there are two slot addresses in the contract:
// Logic contract address storage
strings.Contains(OP_CODE, "360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc")
// Beacon contract address storage
strings.Contains(code, "a3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50")
Then use eth_getStorageAt to get the main contract address of the relevant slot storage:
curl -X "POST" "<https://ethereum-mainnet.s.chainbase.online/v1/YOU_SECRET_KEY>" \
-H 'Content-Type: application/json; charset=utf-8' \
-d $'{
"id": 1,
"method": "eth_getStorageAt",
"jsonrpc": "2.0",
"params": [
"0x49542ad0f1429932e5b0590f17e676523f0a6369",
"0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc",
"latest"
]
}'
{
"id": 1,
"jsonrpc": "2.0",
"result": "0x0000000000000000000000007ad52eceffcb3bd41bc434a9acfecdbc8ef8450e"
}
Finally get the main contract address: 0x7ad52eceffcb3bd41bc434a9acfecdbc8ef8450e
This series of proxy contracts is similar to EIP-1967, only the storage slot address is different, so I wonāt go into details here.
Similarly, there is an upgradable contract implemented by OpenZeppelin that uses the keccak256 ("org.zeppelinos.proxy.implementation") storage slot address.
Caution
It should be noted that these contracts may not be able to easily obtain the underlying logic contracts, which will prevent us from accurately obtaining the Functions Signature list, such as:
- There are multiple logic contracts behind EIP-2535, and each logic contract may only implement a part of the logic functions
- It is difficult for other non-standard proxy contracts to obtain the logical contract address
Therefore, we need the second implementation method to make up for the shortcomings of method one:
Method 2: Use eth_call & eth_estimateGas to determine whether the method is defined
- Pros: can complete the judgment of various proxy contracts
- Cons: It needs to call the Chain RPC API multiple times to complete a judgment, and depends on the error handling of some contracts, so there is a certain rate of misjudgment
Use eth_call to instrument read-only methods, eg: balanceOf.
Use eth_estimateGas to detect the writing method. eth_estimateGas requires the contract to throw some abnormal errors, otherwise there may be misjudgment.
Here are a few examples of using eth_call & eth_estimateGas to determine whether a method exists:
1. Determine if the contract supports the balanceOf (address) method (0x70a08231)
curl -X "POST" "<https://ethereum-mainnet.s.chainbase.online/v1/YOU_SECRET_KEY>" \
-H 'Content-Type: application/json; charset=utf-8' \
-d $'{
"jsonrpc": "2.0",
"method": "eth_call",
"id": 1,
"params": [
{
"to": "0xb24cd494faE4C180A89975F1328Eab2a7D5d8f11",
"data": "0x70a082310000000000000000000000000000000000000000000000000000000000000000"
},
"latest"
]
}'
{
"id": 1,
"jsonrpc": "2.0",
"result": "0x0000000000000000000000000000000000000000000000000000000000000000"
}
2. setApprovalForAll(address operator, bool approved)
curl -X "POST" "<https://ethereum-mainnet.s.chainbase.online/v1/YOU_SECRET_KEY>" \
-H 'Content-Type: application/json; charset=utf-8' \
-d $'{
"id": 1,
"jsonrpc": "2.0",
"method": "eth_estimateGas",
"params": [
{
"to": "0x79fcdef22feed20eddacbb2587640e45491b757f",
"data": "0xa22cb4650000000000000000000000001e0049783f008a0085193e00003d00cd54003c710000000000000000000000000000000000000000000000000000000000000001"
}
]
}'
{
"id": 1,
"jsonrpc": "2.0",
"result": "0xb647"
}
3. Function: safeTransferFrom(address _from, address _to, uint256 _id, uint256 _value, bytes _data)
curl -X "POST" "<https://ethereum-mainnet.s.chainbase.online/v1/YOU_SECRET_KEY>" \
-H 'Content-Type: application/json; charset=utf-8' \
-d $'{
"id": 1,
"jsonrpc": "2.0",
"method": "eth_estimateGas",
"params": [
{
"to": "0x2079812353e2c9409a788fbf5f383fa62ad85be8",
"data": "0xf242432a00000000000000000000000090bee68eb25db284d710a0805022a8a5d720a2860000000000000000000000008f7687d014c2655519fcac41fa990d2188310d776f16452bb8d1aa8d4f0b01d855b7d4ae7e803868000000000026290000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
}
]
}'
{
"id": 1,
"jsonrpc": "2.0",
"result": null,
"error": {
"code": 3,
"message": "execution reverted: ERC1155: caller is not owner nor approved"
}
}
Note: If the above method does not exist, we usually get a -32000 exception, in this way we can check the list of methods supported by the contract.
{
"id": 1,
"jsonrpc": "2.0",
"result": null,
"error": {
"code": -32000,
"message": "execution reverted"
}
}
A simpler solution
Although the above methods have been streamlined, they are still very complicated and always require multiple tests.
If you don't want to implement such complicated data analysis, you can also try the Cloud API provided by Chainbase, which can complete the above judgment very simply and efficiently, and after many verification tests, the false positive rate is reduced to an almost non-existent level.
At present, we have completed the data parsing on multiple chains including Ethereum, Polygon, BSC, Fantom, Avalanche, Arbitrum, and Aptos, and you can easily obtain data directly through the SQL API without cleaning and structuring by yourself.
I hope this article can solve practical operational problems for friends who are troubled by the classification of contract types. It is worth mentioning that these classifications are often cumbersome, so you can use some tools to help greatly reduce repetitive work and focus more on your project.
If you have any other ideas, welcome to Chainbaseās Discord to discuss directly with me, and I can also be found on Twitter.
Thanks for reading!
About Chainbase
Chainbase is a leading Web3 blockchain interaction layer infrastructure. By providing cloud-based API services, it helps developers quickly access and utilize blockchain networks and easily build Web3 applications.
Chainbase makes blockchain interaction and data query/index on chains simple and easy to operate. Anyone can use, build and publish open APIs, which allows developers to focus on application-level innovation instead of solving the back-end hassles.
Chainbase currently supports Ethereum, Polygon, BSC, Fantom, Avalanche, Arbitrum, Aptos and other chains. This allows projects of all sizes to quickly reduce development time and costs, no matter which chains they are building on!
WebsiteĀ |Ā BlogĀ |Ā TwitterĀ |Ā DiscordĀ |Ā Link3
Want to learn more about Chainbase?
Stay connected with Chainbase onĀ Medium,Ā Twitter, andĀ Discord. If you are a back-end dev and would like to experience the product, please submit aĀ request as well as read theĀ dev documentation on our website.