作者:formatting
在 Web3链上数据常见的分析中,往往会有有大量判定 合约类型的需求,本文将从相关的标准以及 工程实践上,来对合约进行是否属于 ERC20 / ERC721 / ERC1155 几个合约的判定。
更多的 使用案例,可以查阅 [Chainbase] 的[开发者文档],或者通过 Discord向 原作者提问。我们很高兴能够与大家讨论 Web3 infra、 Data SDK、Chainbase APIs 等相关的问题。
不同合约的判定规则
随着行业标准的完善,各个合约对应的 Functions 和 Events 都有详细的规定。所以,利用合约所支持的 Functions 进行类型判断,是非常高效和准确的方式。
下面表格内列出了一些ERC20 / ERC721 / ERC1155 必须支持的 Functions,可以给我们判定合约类型提供支持依据:
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) |
当然,在工程实现和数据分析中,我们注意到链上存在大量特殊合约,比如:
- 早期合约:合约部署早于标准制定;
- 精简合约:未实现所有规定的方法;
它们虽然不能完全符合标准,但也拥有 Token approve / transfer 等一系列的 events,在实际操作中,也可以归为相关标准进行分析。
参考实例
基于以上的认知,Chainbase 的解决方案可以作为参考。我们的工程实现采用了一个更加精简,并充分验证了有效性的判定标准,如下所示:
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) |
如何工程实现?
工程上需要解决的问题是如何获取合约实现的方法列表,针对这个问题,我们采用了两种检测方式,各有优缺点,混合使用可以弥补相互的不足。
方法一:获取合约的 OpCode 生成 Functions Signature 列表
- 优点:速度快
- 缺点:需要寻找到背后的逻辑合约,对于复杂代理合约和非标准的代理合约的判定,存在一些偶发问题
需要注意:代理合约的处理方法
如果主合约是一个代理合约,我们需要进一步获取背后的逻辑合约地址,通过逻辑合约来获取合约所支持的 Functions。
下面是几种合约的具体处理方式:
EIP-1167
EIP-1167 是一个简单的的克隆合约,OpCode 以 363d3d373d3d3d363d73
开头,10-29 字节为主合约的地址
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"
}
获取主合约地址:
fmt.Sprintf("0x%s", OP_CODE[20:60])
这样,我们就得到主合约地址: 0xe38f942db7a1b4213d6213f70c499b59287b01f1
了
EIP-1967
EIP-1967 对定了一系列的 storage slot 来存储一些代理合约的地址,例如:
- Logic contract address storage: bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1))
- Beacon contract address storage:bytes32(uint256(keccak256('eip1967.proxy.beacon')) - 1))
EIP-1967 规定了如果 Logic contract address storage 为空,则使用 Beacon contract address storage。所以我们可以通过判断合约是否存在这两个 slot 地址,来简单判断此合约是否是 EIP-1967 代理合约,再通过 eth_getStorageAt 获取背后的逻辑合约地址。
比如:[0x49542ad0f1429932e5b0590f17e676523f0a6369](https://www.notion.so/2022-11-28-12-5-f251c426fca34be78b435a8d3d7e4d29?pvs=21)
首先判断合约中是否存在两个 slot 地址:
// Logic contract address storage
strings.Contains(OP_CODE, "360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc")
// Beacon contract address storage
strings.Contains(code, "a3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50")
再使用 eth_getStorageAt 获取 相关 slot 存储的主合约地址:
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"
}
最终获得主合约地址:0x7ad52eceffcb3bd41bc434a9acfecdbc8ef8450e
EIP-1967 EIP-1822 EIP-3561
这一系列的代理合约与 EIP-1967 类似,只是 storage slot 地址不一样,这里就不再赘述了。
与此类似,还有 openzeppelin 实现的一个可升级合约,使用的是 keccak256("org.zeppelinos.proxy.implementation") storage slot 地址。
漏网之鱼
需要注意的是,这些合约可能无法轻易获取到背后的逻辑合约,这就会导致我们无法准确地获取 Functions Signature 列表,比如:
- EIP-2535 背后存在多个逻辑合约,每个逻辑合约可能只实现了一部分逻辑功能
- 其他非标准的代理合约难以获取逻辑合约地址
所以,我们很需要第二种实现方式,来弥补方法一的不足:
方法二:利用 eth_call & eth_estimateGas 判断方法是否被定义
- 优点:可以完成各类代理合约的判定
- 缺点:需要多次调用 Chain RPC API 才能完成一次判定,而且依赖一些合约的错误处理,存在一定的误判率
使用 eth_call 调用可对只读方法进行检测,例如:balanceOf。
使用 eth_estimateGas 可对写入的方法进行检测, eth_estimateGas 需要合约抛出一些异常错误,否则可能会存在误判。
举几个使用 eth_call & eth_estimateGas 判断方法是否存在的示例:
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"
}
}
注意:如果上述方法不存在,我们通常会得到一个 -32000 的异常,通过此方式我们可以检测合约支持的方法列表。
{
"id": 1,
"jsonrpc": "2.0",
"result": null,
"error": {
"code": -32000,
"message": "execution reverted"
}
}
一个更简单的解决方式
当然,上面这些方法虽然已经经过了精简,但仍然显得很复杂,并且总是需要多次检验。
如果不想使用这么复杂的解析数据,也可以试试 [Chainbase] 提供的 Cloud API,能够非常简单高效地完成上面的判定,并且经过了多次的验证,能够将误判率降低到几乎不存在的程度。
目前我们已经完成了 Ethereum、Polygon、BSC、Fantom、Avalanche、Optimism、Arbitrum、Aptos、Sui、zkSync、Starknet、Base、Bitcoin 等多个链上数据的解析,可以直接通过 SQL API 轻松获取数据,不需要自己进行清洗和整理。
希望这篇文章能对正在对合约类型的分类感到困扰的朋友解决实际的操作问题,值得一提的是,这些分类往往是比较繁琐的,所以你可以尽可能使用一些工具,帮助你减少重复造轮子的工作,把精力集中到你的项目上来。
如果你有任何其他的想法,欢迎来到 [Chainbase] 的 Discord,和我直接讨论,另一个能找到我的方式在 twitter。
感谢看完!
About Chainbase
Chainbase 是一个开放的 Web3 数据基础设施,用于大规模地访问、组织和分析链上数据。
Chainbase 通过一个数据平台,将丰富的数据集与开放的计算技术相结合,帮助人们更好地利用链上数据。Chainbase 的目标是让加密数据易于使用并发挥效益,使人人受益,不断帮助这个时代最有创造力的人们,实现他们的想法。
目前有超过 5,000 名开发人员在平台上进行构建,每天超过 200Mn 的后端数据请求,并将 Chainbase 集成到他们的主要工作流程中。此外,我们正在与 10+ 家头部公链进行合作,作为验证节点和提供商,非托管地管理着超过 💲500Mn 的代币。