# 2. ERC20 토큰 개발 및 배포하기 ``` 개발 환경 - 네트워크: 프라이빗 네트워크 - openzeppelin - solc: v0.8.12 - web3.py - solcx ``` [준비] Openzeppelin 사용하기 --- - Openzeppelin은 Smart Contract 개발을 위한 수많은 라이브러리들과 표준을 제공하고 있다. 그래서 내가 표준을 찾아서 구현하기 보다는 간단하게 openzeppelin에서 제공하는 것들을 사용하는 것이 안전하고 더 편리하다. 대표적으로 ERC20 토큰 개발 표준 라이브러리, ERC721 NFT 개발 표준 라이브러리 등을 제공하고 있기 때문에, 이들을 import 하는 것만으로도 사용자 정의 토큰 및 NFT 개발하는 것이 용이하다. - 온라인 환경에서는 `npm install @openzeppelin/contracts` 과 같은 명령어로 간단히 설치할 수 있다. 하지만, 오프라인 환경에서 사용하기 위해서는 github에서 코드를 다운로드 받고, contracts 해당하는 부분을 복사해야한다. 그리고 그 환경을 인식할 수 있도록 base-path를 설정해줘야 한다. [@openzeppelin/contracts github 코드](https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/contracts) [Openzeppelin에 대해서 자세히](https://docs.openzeppelin.com/contracts/4.x/erc20-supply) [준비] solcx 사용하여 compile 하기 --- - solcx는 Python으로 개발된 Solidity 컴파일러 wrapper와 같은 것인데, 2018년까지는 solc 라는 것을 사용했는데, 2018년 이후 solc 는 개발이 중단되었고 현재는 solcx라는 것을 사용한다. - solcx 는 Python 기반으로 solidity compile 하는 기능을 갖고 있는데, **compile 한 후 반환값은 abi 와 bytecode** 이다. abi는 json 형태로 된, Smart Contract의 메타 정보를 담고 있는 데이터이고, bytecode 는 EVM에서 동작하기 위해 고수준의 언어(Solidity)가 저수준의 언어로 변환된 결과라고 볼 수 있다. abi 와 bytecode 의 예제는 다음과 같다. **abi 예시** ```json "abi": [ { "inputs": [ { "internalType": "uint256", "name": "initialSupply", "type": "uint256" } ], "stateMutability": "nonpayable", "type": "constructor" }, { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", "name": "owner", "type": "address" }, { "indexed": true, "internalType": "address", "name": "spender", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "value", "type": "uint256" } ], ... }, ``` **bytecode 예시** ``` 60806040523480156200001157600080fd5b50604051620017a83803... ``` - 결과로 나온 abi는 나중에 Smart Contract를 호출할 때 contract 주소와 같이 입력값으로 사용된다. 그리고 bytecode는 Smart Contract 배포할 때 블록체인에 저장되기 때문에 따로 보관하지 않아도 된다. - solcx를 사용할 때 가장 어려웠던 점은 여러 개의 파일을 import 했을 때, 이들의 abi 와 bytecode를 어떻게 하나로 합칠 수 있을까였다. 즉, 내가 A.sol 이라는 파일을 컴파일하고 싶은데, A.sol 은 내부적으로 B.sol 을 상속받아서 사용하고 있을 수 있다. 처음에 `truffle` 이나 `brownie` 의 동작을 보니 build 라는 디렉토리에 각각 파일별로 json 파일들을 떨궈줬다. 그래서 `truffle` 이나 `brownie` 가 배포할 때, 뭔가 이들을 통합해서 하나의 파일로 만든 다음 배포를 하는 줄 알았다. - 오랜 관찰 끝에, **abi 와 bytecode는 compile할 때 하나로 다 합쳐서 생긴다는 것을 발견했다**. 즉, 내가 A.sol 안에서 B.sol 의 b_do 라는 함수를 사용한다고 할 때, compile 하면 b_do 에 대한 메타 정보가 compile 결과에 포함되었다. 따라서, 나는 다른 파일들의 compile 결과를 볼 필요없이 그냥 A.sol 의 결과만 보면 되었다. - 결과적으로, solcx 를 사용하여 compile한 것은 아래와 같다. ```python res = solcx.compile_files([A.sol], solc_binary="solc binary 경로", output_values=["abi", "bin"], base_path = "import 파일 루트 경로") # key는 combined.json 같은 파일에 저장된 것을 잘 보면 파일명이 있다. # 그냥 파일명은 아니고 어쩌구 저쩌구 붙어 있다 abi = res["key"]["abi"] bytecode = res["key"] ``` [준비] web3.py 사용하기 --- - web3.py 의 몇 가지 초기 설정과 내가 사용한 함수들에 대해서 정리해보았다. (추후 작성 예정) [개발] ERC20 토큰 개발하기 --- ### 1. Smart Contract 개발하기 - 위의 내용을 토대로, ERC20 토큰을 개발하는 것은 매우 간단했다. 코드는 다음과 같다 ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.12; // compile 할 때 base path 를 잡으면, 이런 상대 주소도 잡을 수 있다. import "openzeppelin/contracts/token/ERC20/ERC20.sol"; contract MyToken is ERC20 { uint public constant INITAL_SUPPLY = 10000; // 초기 공급량을 주는 경우 constructor(uint256 initialSupply) ERC20("My Token", "MTK") { _mint(msg.sender, initialSupply); } // 초기 공급량을 주지 않는 경우 constructor() ERC20("My Token", "MTK") { _mint(msg.sender, INITIAL_SUPPLY); } } ``` ### 2. Smart Contract 배포하기 - abi 와 bytecode 를 알고 있기 때문에 web3.py를 통해 Smart Contract 를 배포할 수 있었다. ```python # Client 와 기본 계정을 설정 URL = "ws://{Web Provider IP 주소}:{Web Provider Port}" client = Web3(Web3.WebsocketProvider(URL)) client.eth.default_account = client.eth.coinbase # Smart Contract 배포를 위한 계정 unlock client.geth.personal.unlock_account(client.eth.default_account, input ("Enter phrase word (one word): "), duration=1000) mContract = client.eth.contract(bytecode=bytecode, abi=abi) # 초기 수량을 기본 값을 쓰냐 내가 직접 지정하냐의 차이임. # 가스비는 반드시 입력해야지 에러가 안 뜨고 # 너무 낮은 양의 가스비를 입력하는 것 또한 에러를 발생시킨다. #txHash = mContract.constructor().transact({"gas": 2100000}) # gas is required!! # too low gas doesn't work... txHash = mContract.constructor(9999).transact({"gas": 2100000}) # Smart Contract를 배포하고, 블록에 포함될 때까지 일정 시간이 필요하다. 동기화된 방식으로 하기 위해서 기다린다. txReceipt = client.eth.wait_for_transaction_receipt(txHash, timeout=10000) ``` ### 3. 배포한 Smart Contract 확인하기 - Contract가 정상적으로 배포 되었는지 확인하기 위한 방법으로는 txReceipt 의 status 를 보면 된다. txReceipt 은 dict 와 유사한 타입인데, **`txReceipt['status']` 값이 1이면 성공적으로 배포가 된거고, 0이면 배포에 실패한 것이다**. 실제로 저 값이 0이라고 해서 블록에 담겨지지 않는 것이 아니다. Smart Contract 는 정상적으로 블록에 담기지만, 실행할 때 opcode 가 잘못되었다는 등 에러를 뱉어내면서 정상적으로 수행되지 않는다 - 잘 수행되는지 확인해보려면, public으로 공개되어 있는 함수를 수행해보면 된다. ERC20 의 경우 다양한 public 함수를 지원하기 때문에, 그걸 상속받은 자체 토큰 또한 같은 함수를 갖고 있다. - 예를 들어, 전체 공급량을 확인하기 위한 totalSupply 함수가 정상적으로 동작하는지 아래와 같은 코드를 수행해봤다. ```python # Geth 버전에 따라 다를 수 있지만, # 일단 address 를 check sum address 로 바꾸지 않으면 에러가 나는 경우가 있다. # 이를 방지하기 위해서 contract address 또한 toChecksumAddress 로 변경을 해준다 # default 계정을 설정해준다. client.eth.default_account = client.eth.coinbase # contract를 가져온다. mToken = client.eth.contract(address=Web3.toChecksumAddress(tx_receipt.contractAddress), abi=abi) # totalSuupply() 함수 테스트 print (mToken.functions.totalSupply().call()) # transfer() 함수 테스트 #print (mToken.functions.transfer(receiver, 100).call()) ``` - 무사히 수행된 것을 볼 수 있었다. 결론 --- - 인터넷이 사용 가능한 환경에서 ERC20 기반의 자체 토큰을 만드는 것은 매우 간단하다. Ganache 를 사용해서 간단하게 테스트 해보는 것도 간편하다. 하지만, 오프라인 환경에서 직접 네트워크를 구성하고 개발 및 배포하는 것이 그렇게 간단하지 않기 때문에 고생을 한 측면도 있다. - 오프라인 환경에서 최소한의 기능을 가지고 최소한의 툴 사용을 개발을 하니 원래 자동으로 동작하던 기능들이 어떻게 동작하는지 알 수 있었다. 다양한 이유들로 에러가 나와 삽질을 했지만, Geth 노드가 어떻게 동작하는지 알 수 있는 기회가 되었다. - 이것저것 해보면서 궁금해진 점이 몇 가지 생겼다. - block 생성 속도가 genesis.json에 적혀 있는 difficulty 대로 유지하는 것이 아니라, 지속적으로 변경되는 것 같다. 이걸 constant 하게 유지하는 방법이 있을까? - pending transaction pool에 들어간 tx 들 중에서 가스비가 많이 부족한 것들은 처리가 안되는 경우가 있다. 이것은 왜 그런지, 혹시 그렇다면 가스비를 수정할 수 있는 방법이 있을까? - 이제 내가 원하는 contract를 개발할 수 있는 환경이 되었으니, front running을 구현하거나 해킹 사례를 직접 시뮬레이션 해볼 수 있겠다.