How to Determine the Type of an EVM Contract

How to Determine the Type of an EVM Contract

Bruce

Bruce

Tech

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-20EIP-721EIP-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:

  1. Early contract: Contract deployment is earlier than standard formulation;
  2. 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 SignatureFunctions For ERC20Functions For ERC721Functions For ERC1155
0x70a08231balanceOf(address _owner)balanceOf(address _owner)
0xa22cb465setApprovalForAll(address _operator, bool _approved)setApprovalForAll(address _operator, bool _approved)
0x00fdd58ebalanceOf(address _owner, uint256 _id)
0x095ea7b3approve(address,uint256)
0xa9059cbbtransfer(address _to, uint256 _value)
0x42842e0esafeTransferFrom(address _from, address _to, uint256 _tokenId)
0xf242432asafeTransferFrom(address _from, address _to, uint256 _id, uint256 _value, bytes calldata _data)
0x2eb2c2d6safeBatchTransferFrom(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

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

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

EIP-1967 EIP-1822 EIP-3561

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.

image.png

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.