24. 识别ERC20合约
我们介绍如何用ethers.js识别一个合约是否为ERC20标准,你会在链上分析,识别貔貅,抢开盘等场景用到它。
ERC20
ERC20 是以太坊上最常用的代币标准,ERC20 标准包含以下函数和事件:
interface IERC20 {
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
}
识别 ERC20 合约
在之前的教程中,我们讲了如何基于 ERC165 识别 ERC721 合约。但是由于 ERC20 的发布早于 ERC165(20 < 165),因此我们没法用相同的办法识别 ERC20 合约,只能另找办法。
区块链是公开的,我们能获取任意合约地址上的代码(bytecode)。因此,我们可以先获取合约代码,然后对比其是否包含 ERC20 标准中的函数就可以了。
首先,我们用 provider 的 getCode() 函数来取得对应地址的 bytecode:
let code = await provider.getCode(contractAddress)
接下来我们要检查合约 bytecode 是否包含 ERC20 标准中的函数。合约 bytecode 中存储了相应的[函数选择器]:如果合约包含 transfer(address, uint256) 函数,那么 bytecode 就会包含 a9059cbb;如果合约包含 totalSupply(),那么 bytecode 就会包含 18160ddd。
这里,我们仅需检测 transfer(address, uint256) 和 totalSupply() 两个函数,而不用检查全部6个,这是因为:
ERC20标准中只有transfer(address, uint256)不包含在ERC721标准、ERC1155和ERC777标准中。因此如果一个合约包含transfer(address, uint256)的选择器,就能确定它是ERC20代币合约,而不是其他。- 额外检测
totalSupply()是为了防止选择器碰撞:一串随机的字节码可能和transfer(address, uint256)的选择器(4字节)相同。
代码如下
async function erc20Checker(addr){
// 获取合约bytecode
let code = await provider.getCode(addr)
// 非合约地址的bytecode是0x
if(code != "0x"){
// 检查bytecode中是否包含transfer函数和totalSupply函数的selector
if(code.includes("a9059cbb") && code.includes("18160ddd")){
// 如果有,则是ERC20
return true
}else{
// 如果没有,则不是ERC20
return false
}
}else{
return null;
}
}
测试脚本
下面,我们利用 DAI(ERC20)和 BAYC(ERC721)合约来测试脚本是否能正确识别 ERC20 合约。
// DAI address (mainnet)
const daiAddr = "0x6b175474e89094c44da98b954eedeac495271d0f"
// BAYC address (mainnet)
const baycAddr = "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d"
const main = async () => {
// 检查DAI合约是否为ERC20
let isDaiERC20 = await erc20Checker(daiAddr)
console.log(`1. Is DAI a ERC20 contract: ${isDaiERC20}`)
// 检查BAYC合约是否为ERC20
let isBaycERC20 = await erc20Checker(baycAddr)
console.log(`2. Is BAYC a ERC20 contract: ${isBaycERC20}`)
}
main()
输出如下:

脚本成功检测出 DAI 合约是 ERC20 合约,而 BAYC 合约不是 ERC20 合约。
总结
这一讲,我们介绍了如何通过合约地址获取合约 bytecode,并且利用函数选择器来检测合约是否为 ERC20 合约。脚本能成功检测出 DAI 合约是 ERC20 合约,而 BAYC 合约不是 ERC20 合约。你会将它用在什么场景呢?
