# 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을 구현하거나 해킹 사례를 직접 시뮬레이션 해볼 수 있겠다.