블록 데이터 상에서 ERC20과 721 구분법을 살펴보았다.
raw 데이터를 통해 구분하는 것이 가장 비용이 적게 발생하기 때문이다.
로그(이벤트)를 통해 JSON RPC로 두 토큰을 구분할 수 있음을 이해하고 구현해보았다.
블록 정보를 JSON RPC로 받아오면 transactions 필드에서 다음과 같은 값들을 볼 수 있다.
eth_getBlockByNumber{
"blockHash": "0x570bacc6b30fcaff821fccc70126c684a1ea8d76b2ffaadb96504cdb176afc32",
"blockNumber": "0xea5398",
"from": "0x25eaff5b179f209cf186b1cdcbfa463a69df4c45",
"gas": "0xf618",
"gasPrice": "0x258325ddf",
"maxFeePerGas": "0xe8d4a51000",
"maxPriorityFeePerGas": "0x13e49ef00",
"hash": "0x360481d167ae675925baf17f8911b7768a4e7805dd566da5eddf1d1fd872c326",
"input": "0x",
"nonce": "0x275da",
"to": "0xf19a206f02a5a3d1476065423b4db1b301f4aa4f",
"transactionIndex": "0x0",
"value": "0x68bdb20d20f400",
"type": "0x2",
"accessList": [],
"chainId": "0x1",
"v": "0x0",
"r": "0x524f25e68917bdbb39f878ad172454e4d8654070f346cebdf30445ae76b127e",
"s": "0x6b9a5775c9dcbfe81b348aa6d4ac34efde871774d7d9c7efd7d2cc9319a16774"
},
{
"blockHash": "0x570bacc6b30fcaff821fccc70126c684a1ea8d76b2ffaadb96504cdb176afc32",
"blockNumber": "0xea5398",
"from": "0xb05932b109f4f19cf0069e5d38c78b3a5b482970",
"gas": "0x3d090",
"gasPrice": "0x2540be400",
"hash": "0x0564a0c209cf7d7dd6be15eff1f35d392694b1cdd2ec6c42f11af863fc105256",
"input": "0xa9059cbb0000000000000000000000003f37fe1612c282ac0a8824936fc3c785fe1267340000000000000000000000000000000000000000000000000000000029b92700",
"nonce": "0x7f1",
"to": "0xdac17f958d2ee523a2206206994597c13d831ec7",
"transactionIndex": "0x1",
"value": "0x0",
"type": "0x0",
"v": "0x26",
"r": "0xf702f56361117b157e05a34ff4376647ad3867c5a3e3548c518d868c4bfad274",
"s": "0x3421e5686207239bcf17adc53b4db1b6c28f2056455c7be90f3c1079ed1ed401"
},
...
그런데 문득 여기서 ERC20이나 721 토큰의 거래를 어떻게 찾아내지?
라는 생각이 들었다.
처음에는 트랜잭션 해시를 통해 abi를 뽑아내고 abi의 method들을 IERC 표준과 비교하는 방법을 떠올렸다. 그러나 이 방법은 매우 이더스캔 의존적이며 이더스캔 내에서도 소스코드가 verified 되어있어야 한다는 큰 단점이 있었다. 나는 JSON-RPC api
단에서의 탐지 방법을 원했다.
다음으로 든 생각은 로그를 통해 확인하는 것이었다. 다행히 구글링해보니 이 방법을 추천해주는 몇몇 레퍼런스를 찾을 수 있었다.
ERC20 표준 이벤트
ERC721 표준 이벤트
얼핏 보면 비슷해보이지만, Transfer의 경우 마지막 인자의 indexed 여부가 다르다.
Approval도 마찬가지다.
그럼, 트랜잭션 해시를 통해 트랜잭션 Receipt를 받아온 뒤 log를 확인해봄으로써 이게 그냥 송금이나 토큰과 관련 없는 트랜잭션인지, 아니면 ERC20 721 트랜잭션인지 확인할 수 있을 것이다!
Transfer과 Approval의 경우 ERC20과 721의 시그니처가 동일하다.
시그니처에는 indexed 여부는 표시되지 않고 함수명과 인자 타입만 표시되기 때문이다.
Transfer(address,address,uint256)
Approval(address,address,uint256)
다음 코드로 시그니처를 확인해보았다.
Web3.utils.keccak256("Transfer(address,address,uint256)")
// 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
그리고 위에서 가져왔던 트랜잭션 리스트에서 해시들을 전부 뽑아 receipt를 가져왔다.
eth_getTransactionByHash 중 하나의 receipt{
"jsonrpc": "2.0",
"id": 1,
"result": {
"blockHash": "0x570bacc6b30fcaff821fccc70126c684a1ea8d76b2ffaadb96504cdb176afc32",
"blockNumber": "0xea5398",
"contractAddress": null,
"cumulativeGasUsed": "0xbc782",
"effectiveGasPrice": "0x1aeeb67df",
"from": "0xb11d83c6f375e51b2a4085a8ff315063640e2aae",
"gasUsed": "0x216d7",
"logs": [
{
"address": "0x00000000006c3852cbef3e08e8df289169ede581",
"topics": [
"0x9d9af8e38d66c62e2c12f0225249fd9d721c54b83f48d9352c97c6cacdcb6f31",
"0x000000000000000000000000de72e54a672924d1ed17af7fb12958cf8ac09ebc",
"0x000000000000000000000000004c00500000ad104d7dbd00e3ae0a5c00560c00"
],
"data": "0x11c505251d46f6cb9f637fdc2f2f7c1420fa061deccb82ddb44eb3e3b8f9d599000000000000000000000000b11d83c6f375e51b2a4085a8ff315063640e2aae0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000160c404b2b49cbc3240055ceaee026df1e8497a0000000000000000000000000000000000000000000000000000000000000056600000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020dcd3742c20000000000000000000000000000de72e54a672924d1ed17af7fb12958cf8ac09ebc000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e35fa931a00000000000000000000000000008de9c5a032463c561423387a9648c5c7bcc5bc90000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001c6bf5263400000000000000000000000000002c7240f1887cd0a4380207b8a774dbee332e4bd5",
"blockNumber": "0xea5398",
"transactionHash": "0xaef578940d86aee5146fe519e85e12523df7d841fe5cdc51cea21ce2929acbfd",
"transactionIndex": "0x7",
"blockHash": "0x570bacc6b30fcaff821fccc70126c684a1ea8d76b2ffaadb96504cdb176afc32",
"logIndex": "0x9",
"removed": false
},
{
"address": "0x160c404b2b49cbc3240055ceaee026df1e8497a0",
"topics": [
"0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925",
"0x000000000000000000000000de72e54a672924d1ed17af7fb12958cf8ac09ebc",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000566"
],
"data": "0x",
"blockNumber": "0xea5398",
"transactionHash": "0xaef578940d86aee5146fe519e85e12523df7d841fe5cdc51cea21ce2929acbfd",
"transactionIndex": "0x7",
"blockHash": "0x570bacc6b30fcaff821fccc70126c684a1ea8d76b2ffaadb96504cdb176afc32",
"logIndex": "0xa",
"removed": false
},
{
"address": "0x160c404b2b49cbc3240055ceaee026df1e8497a0",
"topics": [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"0x000000000000000000000000de72e54a672924d1ed17af7fb12958cf8ac09ebc",
"0x000000000000000000000000b11d83c6f375e51b2a4085a8ff315063640e2aae",
"0x0000000000000000000000000000000000000000000000000000000000000566"
],
"data": "0x",
"blockNumber": "0xea5398",
"transactionHash": "0xaef578940d86aee5146fe519e85e12523df7d841fe5cdc51cea21ce2929acbfd",
"transactionIndex": "0x7",
"blockHash": "0x570bacc6b30fcaff821fccc70126c684a1ea8d76b2ffaadb96504cdb176afc32",
"logIndex": "0xb",
"removed": false
}
],
"logsBloom": "0x000000000000000000000000000000200000000000000000000000000000008000010000400000000000000000000000000000000000000000000000002000000000000800000000000000080080000000000000000000002000000000000000000000000202000000000004000008000000000000000000000000100000000000000000000200000000000000000000000000000000000000000000000000008a0000000080000000000000000000000000000000000002000000000000000000000002000008000000000000000000000000400000000000000800000020000410000000000000000000000000004000000080000800000000000000000000",
"status": "0x1",
"to": "0x00000000006c3852cbef3e08e8df289169ede581",
"transactionHash": "0xaef578940d86aee5146fe519e85e12523df7d841fe5cdc51cea21ce2929acbfd",
"transactionIndex": "0x7",
"type": "0x2"
}
},
```
로그가 잘 출력되는 걸 확인했다.
이 로그들 중에서 방금 구한 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
을 topic의 첫 번째 원소로 가지고 있는 경우를 찾아보았다.
다음과 같은 로그가 발견되었다.
{
"address":"0xd31a59c85ae9d8edefec411d448f90841571b89c",
"topics":[
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x000000000000000000000000afc418c0ec9434ed02a667165ac6733ebd25e252"
],
"data":"0x000000000000000000000000000000000000000000000000000000028fa6ae00",
"blockNumber":"0xea5398",
"transactionHash":"0xefbdb637964d57df2150f20d96f03bfcb15995a4eb76bc23373ae1f67af68067",
"transactionIndex":"0x5",
"blockHash":"0x570bacc6b30fcaff821fccc70126c684a1ea8d76b2ffaadb96504cdb176afc32",
"logIndex":"0x7",
"removed":false
}
topics 부분만 상세히 보자.
"topics":[
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x000000000000000000000000afc418c0ec9434ed02a667165ac6733ebd25e252"
],
topics[0]은 위에서 우리가 구한 Transfer의 signature이다.
signature를 제외한 topic은 두 개
이다.
따라서 위 topics를 통해 내릴 수 있는 결론은
Transfer(address,address,uint256)
이고 앞의 두 인자에 indexed
가 붙는다.
결과적으로 Transfer(address indexed,address indexed,uint256)
가 된다.
얘는 ERC20의 이벤트 표준과 일치한다.
이제 이더스캔에서 확인해보자. 해당 address를 따라가면,
ERC20 토큰 맞다!
시그니처가 동일한, 다른 receipt를 가져왔다.
{
"address":"0x570d4dab720d00c2ea702afee208e7b16099786d",
"topics":[
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"0x000000000000000000000000679f1c7de559cf077af7bff879e8e8fed76a0b58",
"0x000000000000000000000000dcd9eb477943518ff5911d405373cfb3b2711ff5",
"0x00000000000000000000000000000000000000000000000000000000000003c8"
],
"data":"0x",
"blockNumber":"0xea5398",
"transactionHash":"0x34e77f5ebd0cd8b82efadcf05fd59a7bc9026a234215e3faf299255a20a0fd80",
"transactionIndex":"0x4",
"blockHash":"0x570bacc6b30fcaff821fccc70126c684a1ea8d76b2ffaadb96504cdb176afc32",
"logIndex":"0x5",
"removed":false
}
얘는 위 경우와 달리 topics의 길이가 4 (signature한개 + indexed 세개)이다.
그럼 Transfer(address indexed,address indexed, uint256 indexed)
가 된다.
이는 ERC721의 이벤트 형태와 동일하다.
마찬가지로 address를 따라가보면
ERC721 맞다.
Transfer 이벤트에 대해, tx receipt의 로그의 topic을 통해 ERC20과 721을 구분할 수 있음을 보였다.
Approval 이벤트도 같은 결과를 보인다.
물론 이 방법은 완전히 이벤트 종속적이기에 이벤트를 emit하지 않는 경우에는 전혀 사용할 수 없는 방법이다. 또, (그럴 일은 적겠지만) 유저가 메서드 오버로딩을 위해 ERC20에 ERC721과 동일한 이벤트 형식을 emit하게 만들어버리면 무용지물이 된다.