[Blockchain] 토큰 생성과 배포

토큰 생성과 배포

OpenZeppelin에서 제공해주는 토큰 컨트렉트를 사용하여 내가 원하는 토큰을 생성하고 배포를해보자

OpenZeppelin

OpenZeppelin에서는 ERC20 표준 규격의 토큰을 제작하고 배포할 수 있는 다양한 기능이 포함된 라이브러리이다.

ERC20의 구성은 이전 포스팅에 정리되어있다.

[Blockchain] ERC20 Documentation

환경 구성

먼저 이더리움 컨트렉트 프레임워크인 truffle을 설치한다.

앞으로 자주 사용되므로 글로벌 설정으로 설치한다.

npm install truffle -g

디렉터리를 생성하고 해당 디렉터리 내부에서 truffle init명령을 수행하여 기본 구성을 세팅한다.

mkdir myToken

cd myToken

truffle init

세팅된 기본 디렉터리 구조는 다음과 같다.

1
2
3
4
5
6
├── contracts
│   └── Migrations.sol
├── migrations
│   └── 1_initial_migration.js
├── test
└── truffle-config.js

그다음 토큰을 생성할 때 사용할 OpenZeppelin을 설치해준다.

npm install @openzeppelin/contracts

그러면 package.json파일이 생성되고 dependencies에 OpenZeppelin이 추가될 것이다.

1
2
3
"dependencies": {
    "openzeppelin-solidity": "^3.1.0"
  }

truffle-config.js를 열어 아래와 같이 수정해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module.exports = {
  networks: {
    development: {
      host: "127.0.0.1",
      port: 7545,
      network_id: "*", 
    }
  },
  compilers: {
    solc: {
      version: "^0.6.0"
    }
  }
};

먼저 networks는 생성한 토큰을 배포할 이더리움 서버를 설정하는 곳이다.

해당 글에서 사용되는 서버는 truffle과 함께 많이 사용되는 ganache라는 로컬 가상 이더리움 서버이다.

그다음 comilers에서 sol파일의 컴파일러인 solc의 버전을 0.6.0버전으로 맞춰주는 이유는 OpenZeppelin에서 제공되는 ERC20 컨트렉트가 요구하는 컴파일러 버전이 0.6.0이기 때문이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
pragma solidity ^0.6.0;

import "../../GSN/Context.sol";
import "./IERC20.sol";
import "../../math/SafeMath.sol";
import "../../utils/Address.sol";

/**
 * @dev Implementation of the {IERC20} interface.
 *
 * This implementation is agnostic to the way tokens are created. This means
 * that a supply mechanism has to be added in a derived contract using {_mint}.
 * For a generic mechanism see {ERC20PresetMinterPauser}.
 *
 * TIP: For a detailed writeup see our guide
 * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How
 * to implement supply mechanisms].
 *
 * We have followed general OpenZeppelin guidelines: functions revert instead
 * of returning `false` on failure. This behavior is nonetheless conventional
 * and does not conflict with the expectations of ERC20 applications.
 *
 * Additionally, an {Approval} event is emitted on calls to {transferFrom}.
 * This allows applications to reconstruct the allowance for all accounts just
 * by listening to said events. Other implementations of the EIP may not emit
 * these events, as it isn't required by the specification.
 *
 * Finally, the non-standard {decreaseAllowance} and {increaseAllowance}
 * functions have been added to mitigate the well-known issues around setting
 * allowances. See {IERC20-approve}.
 */
contract ERC20 is Context, IERC20 {
    using SafeMath for uint256;
    using Address for address;
:
:
:

토큰 제작

truffle 설정이 끝났다면 contracts 디렉터리 내부에 MyToken.sol이라는 파일을 하나 생성하고 아래와 같이 작성한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pragma solidity ^0.6.0;

import "../node_modules/openzeppelin-solidity/contracts/token/ERC20/ERC20.sol";

contract MyToken is ERC20  {

    uint256 public INITIAL_SUPPLY = 120000;

    constructor() public ERC20("MyToken", "MT") {

        // msg.sender에게 INITIAL_SUPPLY 만큼의 토큰을 할당
        _mint(msg.sender, INITIAL_SUPPLY);
    }

}

solidity에서의 contract는 마치 다른 언어의 class랑 비슷하게 상속도 가능하고 생성자도 존재한다. 상속을 받을 때는 is를 사용하면 된다.

해당 컨트렉트에서는 OpenZeppelin의 ERC20을 상속받았고 덕분에 ERC20이 제공하는 모든 함수들을 사용할 수 있다.

MyToken의 생성자는 상속받은 ERC20의 생성자에 접근하여 진행하는데 ERC20의 생성자는 이렇게 구성되어있다.

1
2
3
4
5
constructor (string memory name, string memory symbol) public {
        _name = name;
        _symbol = symbol;
        _decimals = 18;
    }

토큰의 이름과 심볼을 매개변수로 전달받아 내부 변수를 초기화한다.

여기서 decimals는 소수점값을 의미한다. 예를들어 505의 값에 decimals이 2라면 5.05로 표현될 것이다. 이더리움에서 이더의 가장 작은 단위인 웨이가

1/10^18이더인 것과 같은 맥락이다.

MyToken의 생성자에서 ERC20의 생성자로 “MyToken”와 “MT”를 매개변수로 넘겼으니 “MyToken”이라는 이름과 “MT”라는 심볼을 가진 토큰이 되는 것이다.

또한 생성자 내부에서 새로운 토큰을 할당해주는 _mint(address account, uint256 amount) 함수를 호출하여 사용하고있다.

첫 번째 매개변수로는 이 컨트렉트를 배포한 계정의 주소값을, 두번째 매개변수로는 INITIAL_SUPPLY를 넘겨주고있다. INITIAL_SUPPLY의 값은 120000이므로 컨트렉트가 배포되는 순간 컨트렉트를 배포한 계정에게 120000MT가 전달 될 것이다.

토큰 배포

토큰 컨트렉트의 코드를 모두 작성하였으니 배포를 진행해보자

migrations 디렉터리에 2_deploy_mytoken.js파일을 생성하고 다음과 같이 작성한다.

1
2
3
4
5
var MyToken = artifacts.require("MyToken");

module.exports = function(deployer) {
    deployer.deploy(MyToken);
}

그다음 truffle compile명령으로 컴파일을 진행한다.

1
2
3
4
5
6
7
8
> truffle compile

Compiling your contracts...
===========================
 Fetching solc version list from solc-bin. Attempt #1
> Compiling .\contracts\MyToken.sol
 Fetching solc version list from solc-bin. Attempt #1
> Compilation warnings encountered:

컴파일이 끝나면 build/contracts 경로에 json형태의 컴파일된 컨트랙트가 생성 될 것이다.

이제 토큰을 배포할 이더리움 서버인 Ganache 준비해준다.

다운로드

다운로드 한 설치 파일로 Ganache를 설치하고 실행하면 다음과 같은 화면이 보인다.

ganache1

좌측의 QUICKSTART를 통해 임시로 사용할 서버를 생성해준다.

ganache2 상단의 메뉴를 보면 RPC Server 주소가 적혀있는데 앞부분에서 networks의 설정 값이 이 값과 동일하다.

또한 Ganache는 100이더를 소유한 계정 10개를 기본적으로 제공해준다.

서버가 정상적으로 동작 하였으면 컴파일한 컨트렉트를 truffle migrate명령어로 배포해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
> truffle migrate

Compiling your contracts...
===========================
> Everything is up to date, there is nothing to compile.

Starting migrations...
======================
> Network name:    'development'     
> Network id:      5777
> Block gas limit: 6721975 (0x6691b7)

1_initial_migration.js
======================

   Deploying 'Migrations'
   ----------------------
   > transaction hash:    0x6b97999f8882c04f89acb718123ad4a58a164040f8ba6cfb7e747aad2a5eda92
   > Blocks: 0            Seconds: 0
   > contract address:    0x298aB23f87e80B11710785316372E8Aa9b7A33C4
   > block number:        1
   > block timestamp:     1596900576
   > account:             0x761EaB5058Fdfd7E6d53bA3A6575dAA7b3583737
   > balance:             99.9955993
   > gas used:            220035 (0x35b83)
   > gas price:           20 gwei
   > value sent:          0 ETH
   > total cost:          0.0044007 ETH

   > Saving migration to chain.
   > Saving artifacts
   -------------------------------------
   > Total cost:           0.0044007 ETH

2_deploy_mytoken.js
=====================

   Deploying 'MyToken'
   ---------------------
   > transaction hash:    0x74fb83a864f661ec2095bea32fb61a8c4f70a038c535ae1863ac6742d6122eb9
   > Blocks: 0            Seconds: 0
   > contract address:    0xd4c3eFed53154B676563aEd19CFFaD62dc24B4d7
   > block number:        3
   > block timestamp:     1596900577
   > account:             0x761EaB5058Fdfd7E6d53bA3A6575dAA7b3583737
   > balance:             99.96952636
   > gas used:            1261287 (0x133ee7)
   > gas price:           20 gwei
   > value sent:          0 ETH
   > total cost:          0.02522574 ETH

   > Saving migration to chain.
   > Saving artifacts
   -------------------------------------
   > Total cost:          0.02522574 ETH

Summary
=======
> Total deployments:   2
> Final cost:          0.02962644 ETH

컨트렉트를 배포하는 트렌젝션이 발생할 때 수수료가 필요하고 이 수수료는 해당 컨트렉트를 배포하는 계정인 10개의 가나슈 계정 중 첫번째 계정에서 지출된다.

truffle networks명령어를 사용하면 배포가 잘 됐는지 확인할 수 있다.

1
2
3
4
5
> truffle netwokrs

Network: development (id: 5777)
  MyToken: 0xd4c3eFed53154B676563aEd19CFFaD62dc24B4d7
  Migrations: 0x298aB23f87e80B11710785316372E8Aa9b7A33C4

배포가 잘 이루어졌다면 실행된 블록체인 서버에 접속한다.

truffle console 명령어로 Ganache 네트워크에 접속할 수 있다.

1
2
> truffle console
truffle(development)>

배포한 컨트렉트의 인스턴스를 생성해서 토큰 관련 기능을 사용해보자

1
2
3
4
5
6
7
8
9
let myToken = await MyToken.at("0xd4c3eFed53154B676563aEd19CFFaD62dc24B4d7")
undefined
truffle(development)> myToken.totalSupply()
BN {
  negative: 0,
  words: [ 120000, <1 empty item> ],
  length: 1,
  red: null
}

토큰의 총 발행량을 확인하는 함수인 totalSupply() 를 사용하면 생성자에서 설정한 값인 INITIAL_SUPPLY만큼 존재하는 것을 확인할 수 있다.

이 토큰은 모두 컨트렉트를 베포한 계정인 Ganache의 첫번째 계정이 가지고있으며 balanceOf함수로 확인할 수 있다.

1
2
3
4
5
6
7
8
9
myToken.balanceOf(accounts[0])
BN {
  negative: 0,
  words: [ 120000, <1 empty item> ],
  length: 1,
  red: null
}
truffle(development)> myToken.balanceOf(accounts[1])
BN { negative: 0, words: [ 0, <1 empty item> ], length: 1, red: null }

Reference

OpenZeppelin/openzeppelin-contracts