Token Type Identifier

iwin1203·2022년 10월 3일
0

블록체인

목록 보기
4/11

요약

  • 바이트코드로 토큰 타입을 식별할 수 있는 방법을 알아보았다.

  • 로그(이벤트)를 확인하는 것보다 더 빠르고 저렴하게 토큰 타입을 식별할 수 있는 방법이기 때문이다.

  • 이더리움 bytecode 종류와 개념을 이해했다. 다양한 decompiler를 사용해보았다. Signature의 개념과 생성 방법을 이해하고 직접 프로그래밍해봤다.



bytecode를 decode할 수 있나?


bytecode를 통해 소스코드를 복원하는 것은 불가능하며, human-readable하게 만드는 것조차도 굉장히 어려운 태스크로 알려져있다. 컴파일 과정에서 함수명, 변수명 등이 제거되고 최적화가 진행되기 때문이다.

gigahorse (elipmoc), panoramix 등 여러 bytecode decompiler를 테스트해봤지만 (매우 힘들었음..) 여전히 어떤 내용의 코드인지 이해하는 데에는 한계가 있었다. 어떤 토큰인지 이해하는 것은 당연히 불가능했다.



Signature를 활용


반대로 생각해보자.

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를 찾아볼 수 있다.

  1. eip 20, 721에 등록된 must implement 메서드, 함수를 통해 각각의 interface를 생성한다.
  • interfaces.ts
    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)",
    };


  1. 원하는 컨트랙트의 바이트코드를 받아온다. (.getCode() 메서드 활용)
  2. 해당 바이트코드에서 1의 interface가 발견되는지 확인한다.
    (보이어무어 알고리즘을 활용한 fast-string-search 모듈을 활용하여 bytecode에서 signature를 브루트포스 방식으로 찾아주었다.)
  • checkMatching_signature.ts
    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를 통해 아주 빠르고 효율적으로 종류를 구분할 수 있음을 알게 되었다.

굳!

0개의 댓글