WTF Ethers: 24. Identify ERC20 Contracts
I've been revisiting ethers.js
recently to refresh my understanding of the details and to write a simple tutorial called "WTF Ethers" for beginners.
Twitter: @0xAA_Science
Community: Website wtf.academy | WTF Solidity | discord | WeChat Group Application
All the code and tutorials are open-sourced on GitHub: github.com/WTFAcademy/WTF-Ethers
In this lesson, we will learn how to use ethers.js
to identify whether a contract follows the ERC20
standard.
ERC20
ERC20
is the most commonly used token standard on Ethereum. If you are unfamiliar with this standard, you can refer to WTF Solidity 31: ERC20. The ERC20
standard includes the following functions and events:
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);
}
Identifying ERC20
Contracts
In a previous tutorial, we discussed how to identify ERC721
contracts based on ERC165
. However, since the release of ERC20
predates ERC165
(20 < 165), we cannot use the same method to identify ERC20
contracts and need to find an alternative solution.
The blockchain is transparent, so we can obtain the bytecode of any contract address. Therefore, we can first retrieve the bytecode of a contract and compare it to see if it includes the functions specified in the ERC20
standard.
First, we use the getCode()
function of the provider
to retrieve the bytecode of the corresponding address:
let code = await provider.getCode(contractAddress)
Next, we need to check if the contract bytecode includes the function selectors specified in the ERC20
standard. The corresponding selectors are stored in the contract bytecode: if the contract includes the transfer(address, uint256)
function, the bytecode will include a9059cbb
; if the contract includes totalSupply()
, the bytecode will include 18160ddd
. If you're not familiar with function selectors, you can refer to the corresponding section in the WTF Solidity tutorial. If you want to delve deeper into bytecode, you can read the Dive into EVM.
In this case, we only need to check the transfer(address, uint256)
and totalSupply()
functions instead of all six functions, because:
- The
transfer(address, uint256)
function is the only one in theERC20
standard that is not included in theERC721
,ERC1155
, andERC777
standards. Therefore, if a contract includes thetransfer(address, uint256)
selector, we can say it is anERC20
token contract rather thanERC721
,ERC1155
, andERC777
. - The additional check for
totalSupply()
is to prevent selector collisions: a random bytecode sequence may accidentally match the selector oftransfer(address, uint256)
(4 bytes).
Here is the code:
async function erc20Checker(addr){
// Retrieve contract bytecode
let code = await provider.getCode(addr)
// Non-contract addresses have a bytecode of "0x"
if(code != "0x"){
// Check if bytecode includes the selectors of the transfer and totalSupply functions
if(code.includes("a9059cbb") && code.includes("18160ddd")){
// If so, it is an ERC20 contract
return true
}else{
// If not, it is not an ERC20 contract
return false
}
}else{
return null;
}
}
Test Script
Now, let's use the DAI
(ERC20) and BAYC
(ERC721) contracts to test whether the script can correctly identify an ERC20
contract.
// DAI address (mainnet)
const daiAddr = "0x6b175474e89094c44da98b954eedeac495271d0f"
// BAYC address (mainnet)
const baycAddr = "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d"
const main = async () => {
// Check if the DAI contract is an ERC20 contract
let isDaiERC20 = await erc20Checker(daiAddr)
console.log(`1. Is DAI an ERC20 contract: ${isDaiERC20}`)
// Check if the BAYC contract is an ERC20 contract
let isBaycERC20 = await erc20Checker(baycAddr)
console.log(`2. Is BAYC an ERC20 contract: ${isBaycERC20}`)
}
main()
The output is as follows:
The script successfully detects that the DAI
contract is an ERC20
contract, while the BAYC
contract is not an ERC20
contract.
Summary
In this lesson, we learned how to retrieve the bytecode of a contract using the contract address and how to use function selectors to check if a contract follows the ERC20
standard. The script successfully identified the DAI
contract as an ERC20
contract and the BAYC
contract as not an ERC20
contract. Do you have other ways to identify an ERC20 contract?