바이트코드로 토큰 타입을 식별할 수 있는 방법을 알아보았다.
로그(이벤트)를 확인하는 것보다 더 빠르고 저렴하게 토큰 타입을 식별할 수 있는 방법이기 때문이다.
이더리움 bytecode 종류와 개념을 이해했다. 다양한 decompiler를 사용해보았다. Signature의 개념과 생성 방법을 이해하고 직접 프로그래밍해봤다.
bytecode를 통해 소스코드를 복원하는 것은 불가능하며, human-readable하게 만드는 것조차도 굉장히 어려운 태스크로 알려져있다. 컴파일 과정에서 함수명, 변수명 등이 제거되고 최적화가 진행되기 때문이다.
gigahorse (elipmoc), panoramix 등 여러 bytecode decompiler를 테스트해봤지만 (매우 힘들었음..) 여전히 어떤 내용의 코드인지 이해하는 데에는 한계가 있었다. 어떤 토큰인지 이해하는 것은 당연히 불가능했다.
반대로 생각해보자.
bytecode로부터 human-readable한 소스를 복원해낼 수 없다면, 미리 human-readable한 내용을 bytecode에 맵핑시켜놓는건 어떨까?
Transfer라는 event에 해당하는 bytecode를 생성해놓고, 이를 signature라 부르기로 하자. 만약 나중에 우리가 해독하고자 하는 bytecode에서 해당 signature가 발견된다면, 그 소스코드는 아마도 Transfer라는 event를 포함하고 있을 것이다.
이는 굉장히 empirical한 방식이기 때문에 단순히 이전에 등록한 적 없는 signature가 등장한다면 감지할 수 없다는 한계가 있다. 그럼에도 토큰 타입 식별과 같이 사전에 signature list를 미리 파악, 확보할 수 있는 경우에는 매우 강력한 방법이기도 하다.
signature를 활용하여 CA만으로 해당 컨트랙트가 토큰 컨트랙트인지, 그렇다면 어떤 토큰인지 식별해주는 프로그램을 작성해보자.
Ethereum signature database (https://www.4byte.directory/)를 적극 활용했다.
여태까지 이더리움 상 등록된 각종 메서드, 이벤트들의 signature를 모아놓은 사이트이다. 컨트랙트 작성자가 의도적으로 표준을 따르지 않고 창의성을 발휘한 경우만 아니라면 웬만해서 위 사이트에서 signature를 찾아볼 수 있다.
must implement
메서드, 함수를 통해 각각의 interface를 생성한다.export const erc20signature = {
"18160ddd": "totalSupply",
dd62ed3e: "allowance",
"70a08231": "balanceOf",
a9059cbb: "transfer",
"23b872dd": "transferFrom",
"95ea7b3": "approve", // 095ea7b3
ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef:
"e_transfer",
"8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925":
"e_approval",
};
export const erc721signature = {
b88d4fde: "safeTransferFrom",
"17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c31":
"e_approvalForAll",
"42842e0e": "safeTransferFrom",
"70a08231": "balanceOf",
"6352211e": "ownerOf",
"23b872dd": "transferFrom",
"95ea7b3": "approve", // 0 하나 제거
a22cb465: "setApprovalForAll",
"81812fc": "getApproved", // 0 하나 제거
e985e9c5: "isApprovedForAll",
"1ffc9a7": "supportsInterface", // 01ffc9a7
ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef:
"e_transfer",
"8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925":
"e_approval",
};
export const erc1155signature = {
"2eb2c2d6":
"safeBatchTransferFrom(address,address,uint256[],uint256[],bytes)",
"4e1273f4": "balanceOfBatch(address[],uint256[])",
f242432a: "safeTransferFrom(address,address,uint256,uint256,bytes)",
fdd58e: "balanceOf(address,uint256)", // 0 둘 제거
"1ffc9a7": "supportsInterface(bytes4)", // 0 하나 제거
a22cb465: "setApprovalForAll(address,bool)",
e985e9c5: "isApprovedForAll(address,address)",
};
.getCode()
메서드 활용) import fss from "fast-string-search";
import Web3 from "web3";
import {
erc20signature,
erc721signature,
erc1155signature,
} from "./interfaces";
const provider =
"https://mainnet.infura.io/v3/{api key}";
const web3 = new Web3(provider);
const contractAddr: string = process.argv[2];
const exec = async (contractAddr: string) => {
if (!web3.utils.isAddress(contractAddr)) {
console.log("INVALID CONTRACT ADDRESS");
return;
}
const bytecode: string = await _getCode(contractAddr);
if (bytecode.length <= 2) {
console.log("EOA or Empty CA");
return;
}
if (_checkMatching(bytecode, 20)) return;
if (_checkMatching(bytecode, 721)) return;
if (_checkMatching(bytecode, 1155)) return;
console.log("NOT A TOKEN CONTRACT");
};
const _getCode = async (contractAddr: string): Promise<string> => {
const bytecode: string = await web3.eth.getCode(contractAddr);
return bytecode;
};
const _checkMatching = (bytecode: string, tokentype: number): boolean => {
let sig;
if (tokentype == 20) sig = erc20signature;
if (tokentype == 721) sig = erc721signature;
if (tokentype == 1155) sig = erc1155signature;
const keys: string[] = Object.keys(sig);
for (let i = 0; i < keys.length; i++) {
const key: string = keys[i];
if (fss.indexOf(bytecode, key).length == 0) {
return false;
}
}
console.log(`ERC${tokentype}`);
return true;
};
exec(contractAddr);
Invalid한 경우, ERC20, ERC721, ERC1155까지 다 잘 구분해준다.
예외케이스를 찾자면 끝도 없겠지만, EIP 표준을 따르는 토큰이라면 signature를 통해 아주 빠르고 효율적으로 종류를 구분할 수 있음을 알게 되었다.
굳!