누구나 만들 수 있는 이더리움 ERC20 코인/토큰 실전 개발 (1편)

"누구나 만들 수 있는 이더리움 ERC20 코인/토큰 실전 개발" 을 시작하며

여러분, 안녕하세요? KStarCoin을 개발한 TaeKim 입니다. 작년 여름에 steemit 계정만 만들어두고 활동은 못했었는데 드디어 시작하게 되었네요.

제가 이더리움 기반의 토큰을 개발하다 보니, 아직 실전 문서라고 할 수 있는 개발 문서가 너무 없었습니다. 디테일한 부분까지 들어가면 한글 뿐 아니라 영문 자료도 너무나 부족하더군요. 이더리움 Solidity 공식 문서(ethdocs, solidity docs)는 컴파일러 버전 업데이트가 반영이 되지 않은 일부 내용이나 예제들도 있었습니다. 사실 공식 문서의 가장 큰 문제는 검색 기능이 개판입니다.

그런데 막상 다 공부하고 나니 solidity 는 참 쉬운 언어더군요. 이더리움 스마트 콘트랙트는 실행될 때마다 코드 길이에 비례하여 가스 비용이 나갑니다 (정확한 가격표는 여기 를 참조). 그러다 보니 태생적으로 최대한 구조가 간결하고 단순해야 할 수 밖에 없습니다. 따라서 점점 방대해지고 있는 타 언어에 비해 아주 아주 쉽다고 할 수 있습니다. 가이드 문서만 제대로 되어 있다면 저처럼 고생하지 않고 누구나 쉽게 입문할 수 있다고 생각하여 이 글을 시작하게 되었습니다.

앞으로 많은 응원(공유, 보트, 댓글) 부탁드립니다!

여기서 잠깐 - "ERC20 이 뭐에요?"

이더리움에는 이더리움의 표준안을 만들기 위해 유저들이 제안하는 게시판 같은 곳 Ethereum Improvement Proposals (EIPs) 이 있습니다. 20이라는 숫자는 바로 여기에 올라온 글 번호입니다. 그리고 이 게시판에는 Core / Networking / Interface / ERC 의 4가지 카테고리가 있는데, 이 중 ERC 는 Ethereum Request for Comments 의 준 말이라고 합니다.

어쨌든 ERC20 은 EIPs 게시판에 ERC 카테고리로 올라온 20번 글이고, 이더리움 창시자인 비탈릭이 직접 제안한 "이더리움 스마트 콘트랙트로 코인을 만들 땐 다음과 같이 만들어라!" 라는 코인 표준안입니다. 구현된 코드가 있는 것은 아니고 어떻게 만들라는 인터페이스 규약만 정의되어 있습니다.

참고로 이더리움 등의 플랫폼 위에서 스마트 콘트랙트로 만들어진 코인을 특히 '토큰' 이라고 구분하여 부르기도 하는데요, 다시 말해 ERC20 규약에 따라서 만든 스마트 콘트랙트가 바로 'ERC20 토큰' 인 셈이지요.

ERC20 개발의 정석 - OpenZeppelin 을 소개합니다.

제 글의 주인공은 이더리움 공식 문서나 각종 커뮤니티에서 많이 소개가 되고 있는 OpenZeppelin 오픈 소스 프로젝트입니다. 제가 KStarCoin 을 개발하면서 당시 ICO 를 진행 중이던 다른 몇몇 토큰들의 소스를 분석해보았는데 거의 모든 토큰들이 다 OpenZeppelin 의 소스를 기반으로 만들어져 있었습니다. 그렇다고 이 소스가 무조건 정답이라고 할 수는 없으니, 다른 소스도 많이 찾아보고 제 나름대로 이렇게 저렇게 바꿔서 만들어보기도 했습니다. 하지만 solidity 에 익숙해지면 익숙해질 수록, 코인이 완성되면 완성될 수록, 저의 소스는 zeppelin 의 소스와 비슷해져 갔습니다. 그리고 결국 다음과 같은 결론을 내렸습니다.

"ERC20 토큰 개발, 고민 말고 OpenZeppelin 소스에서부터 출발하세요!"

참고 : OpenZeppelin 소스는 업데이트가 굉장히 활발합니다. 이 문서의 내용은 학습용으로만 사용하고, 실제 개발 시에는 꼭 최신 소스를 반영하여 사용하시기 바랍니다.


코드의 시작 - "ERC20Basic.sol"

https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/contracts/token/ERC20/ERC20Basic.sol

pragma solidity ^0.4.18;

/**
 * @title ERC20Basic
 * @dev Simpler version of ERC20 interface
 * @dev see https://github.com/ethereum/EIPs/issues/179
 */
contract ERC20Basic {
  function totalSupply() public view returns (uint256);
  function balanceOf(address who) public view returns (uint256);
  function transfer(address to, uint256 value) public returns (bool);
  event Transfer(address indexed from, address indexed to, uint256 value);
}

이 파일은 비탈릭이 제안한 ERC20 인터페이스의 일부를 그대로 쓴 소스입니다. ERC20 을 다 표현하지 않고 일부만 먼저 쪼갠 이유는 제 3자가 대신 송금해주는 기능(transferFrom)은 굳이 필수로 포함할 필요가 없기 때문에 이 부분을 빼고 더 심플한 토큰을 만들 수 있기 때문입니다.

이 소스를 1줄 요약로 요약하자면, "ERC20Basic 컨트랙트는 totalSupply, balanceOf, transfer 함수를 가지고 있고, 내부에서 Transfer 이벤트를 발생시킬 수 있다." 라는 '선언' 을 담고 있습니다.

선언된 각 함수의 의미는 ERC20 규약에 따라서 다음과 같습니다.

함수명내용리턴값
totalSupply발행한 전체 토큰의 자산이 얼마인가?전체 토큰 수
balanceOf(who)who 주소의 계정에 자산이 얼마 있는가?보유한 토큰 수
transfer(to, value)내가 가진 토큰 value 개를 to 에게 보내라. 여기서 '나' 는 가스를 소모하여 transfer 함수를 호출한 계정입니다.성공/실패

Transfer 이벤트는 외부에서 호출하는 함수가 아닌 소스 내부에서 호출되는 이벤트 함수입니다. ERC20 에 따르면 '토큰이 이동할 때에는 반드시 Transfer 이벤트를 발생시켜라.' 라고 규정 짓고 있습니다. 이 내용은 이 글을 더 읽다 보면 쉽게 이해하실 수 있을 것입니다.

이제 위 소스에서 사용된 solidity 문법의 몇몇 키워드를 소개해드리겠습니다.

pragma solidity ^0.4.18

Solidity 컴파일러 버전을 지정

Solidity 는 이더리움 재단에서 만든 신생 언어로서 현재 굉장히 빠르게 업데이트 되고 있는 중입니다. 그렇기 때문에 버전에 따라 지원되는 기능이 꽤나 다릅니다. 계속 새로운 좋은 기능들이 추가되고 있기 때문에 보통 최신 버전을 지정해주시면 됩니다. 찾아보니 오늘 기준 최신 버전은 0.4.21 이네요. 그러면 pragma solidity ^0.4.21 이렇게 선언하면 됩니다.

address

이더리움 계정 주소를 저장하기 위한 변수 타입

이더리움 계정은 두 가지가 있는데 Externally owned accounts (EOAs) 와 Contract accounts 입니다. 쉽게 구분하자면 EOA 는 사람이 관리하는 계정(지갑 주소 등)이고, Cotract accounts 는 프로그래밍된 스마트 콘트랙트의 주소 계정입니다.

재밌는 건 Contract accounts 도 돈(이더)을 가질 수 있습니다. 이건 단순하면서도 정말 엄청한 발상인데요, 이 덕분에 여러 명의 동의를 얻어야 돈을 빼는 '조건이 부여된 지갑' 도 만들 수 있고, 이더를 입금하면 자동으로 코인이 나오는 ICO 도 쉽게 진행할 수 있게 되었습니다.

address 는 이러한 계정 주소를 저장하기 위한 변수 타입입니다. 참고로 uint256256 bits 사이즈를 가진 unsigned integer (0 과 양수만 있는 정수) 변수 타입입니다. 이러한 변수 타입들은 하나 하나 설명드리기 어렵고 공식 문서 를 읽어봐주세요.

public

'이 함수는 누구나 호출할 수 있다' 는 접근 권한을 부여하는 키워드

콘트랙트 내의 함수와 변수에는 접근 권한을 줄 수가 있습니다. external, public, internal, private 의 4종류 키워드가 있는데 자세한 건 solidity 공식 문서 를 읽어보세요.

view

'변수에 데이타를 저장하거나 수정하지 않는다는 것을 보장' 함을 명시하는 키워드

쉽게 생각하면 '읽기 전용' 함수라고 생각하면 됩니다. 이더리움에서는 이 키워드가 매우 중요한데요, view 함수는 수수료(가스)를 소모하지 않기 때문입니다. 따라서 데이타를 저장하지 않는 함수에는 무조건 view 키워드를 붙여줘야 합니다!

pure

'블록체인 데이타와 아예 무관한 함수' 임을 명시하는 키워드

참고로 view 를 넘어선 pure 함수도 있습니다. pure 함수는 블록체인에 데이타를 쓰는 것 뿐 아니라 아예 읽어오지도 않겠다는 뜻입니다. 예를 들어 아래 코드를 보면, ab 라는 파라메터는 외부에서 입력 받는 파라메터이기 때문에 블록체인에 저장된 데이타와 무관하게 독자적으로 돌아갑니다.

function plus(uint256 a, uint256 b) public pure returns(uint256) {
  return a + b;
}

event

'이벤트 함수' 임을 명시하는 키워드

이벤트 함수는 이더리움 블록체인에서 외부에 신호를 보내기 위한 함수이고 function 키워드가 아니라 event 키워드로 선언합니다.

스마트 콘트랙트 소스에서 이벤트 함수를 호출하면 이더리움 시스템이 외부(자바스크립트 등)에서 실시간으로 캐치할 수 있게 블록체인에 별도로 기록해줍니다. 예를 들어 소스에서 Transfer(0x01, 0x02, 10) 라고 이벤트 함수를 호출하면, ' 0x01 주소에서 0x02 주소로 10개의 토큰을 전송함.' 이라는 정보를 블록체인에 별도로 기록해주고, 이 데이타는 외부에서 실시간 이벤트로 받아서 사용할 수 있습니다.

indexed

'이 변수는 검색에 사용될 것임' 을 명시하는 키워드

이벤트 함수가 트랜잭션의 결과로 기록될 때 indexed 파라메터가 붙은 변수는 검색을 위한 해시 테이블에 저장이 됩니다. 해시 테이블이 뭔지 몰라도 됩니다. 그냥 '차후 검색해서 찾아야 할 값에 이 키워드를 붙인다.' 라고 생각하시면 됩니다.

예를 들어 Transfer 이벤트 함수에는 보내는 주소(from) 과 받는 주소(to)에 indexed 키워드가 붙어 있습니다. 즉, ERC20 규약에 따르면 보내는 사람과 받는 사람으로 검색하여 토큰 송금 이력을 찾아볼 수 있습니다.

한 개의 이벤트 함수에 최대 3개의 변수까지 indexed 로 지정할 수 있습니다.


가장 기본적인 첫 토큰 소스 - "BasicToken.sol"

https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/contracts/token/ERC20/BasicToken.sol

pragma solidity ^0.4.18;

import "./ERC20Basic.sol";
import "../../math/SafeMath.sol";

/**
 * @title Basic token
 * @dev Basic version of StandardToken, with no allowances.
 */
contract BasicToken is ERC20Basic {
  using SafeMath for uint256;

  mapping(address => uint256) balances;

  uint256 totalSupply_;

  /**
  * @dev total number of tokens in existence
  */
  function totalSupply() public view returns (uint256) {
    return totalSupply_;
  }

  /**
  * @dev transfer token for a specified address
  * @param _to The address to transfer to.
  * @param _value The amount to be transferred.
  */
  function transfer(address _to, uint256 _value) public returns (bool) {
    require(_to != address(0));
    require(_value <= balances[msg.sender]);

    // SafeMath.sub will throw if there is not enough balance.
    balances[msg.sender] = balances[msg.sender].sub(_value);
    balances[_to] = balances[_to].add(_value);
    Transfer(msg.sender, _to, _value);
    return true;
  }

  /**
  * @dev Gets the balance of the specified address.
  * @param _owner The address to query the the balance of.
  * @return An uint256 representing the amount owned by the passed address.
  */
  function balanceOf(address _owner) public view returns (uint256 balance) {
    return balances[_owner];
  }
}

드디어 진짜 소스가 나왔습니다. 사실 앞선 ERC20Basic 컨트랙트는 몸통에 해당하는 실제 구현 소스가 없이 껍데기만 있는 'Abstract Contract' 였습니다. 그리고 드디어 BasicToken 콘트랙트에서 ERC20Basic 을 상속 받은 후 실제 몸통을 구현하고 있네요.

참고로 선언만 하고 구현(implementation)되지 않은 함수가 하나라도 있으면 Abstract Contract 가 되는데요, Abstract Contract 만으로는 부족한 부분이 있기 때문에 컴파일이 되지 않고 무시됩니다. 더 자세한 내용은 공식 문서 를 참조해주시기 바랍니다.

일단 소스에서 몇몇 키워드 먼저 설명드리겠습니다.

import

다른 파일을 불러와서 포함시켜라.

소스를 여러 파일로 나눠서 관리하기 위해 사용합니다. 지금의 경우에는 BasicToken 소스가 ERC20Basic 소스와 SafeMath 소스를 참조하고 있습니다. SafeMath 소스에는 곱하기(mul), 나누기(div), 빼기(sub), 더하기(add) 함수가 정의되어 있는데, overflow 에 대한 예외처리가 되어 있기 때문에 기본 연산자 *, /, -, + 를 쓰는 것보다 안전합니다.

is

is 뒤에 오는 부모 콘트랙트를 상속 받아라.

상속의 개념은 코드 재사용의 백미입니다. 우리 모두가 좋아하는 '복붙' 을 업그레이드 시켜놓은 개념입니다.

상속을 주는 쪽을 '부모 Contract', 받는 쪽을 '자식 Contract' 라고 부르는데, BasicToken 콘트랙트의 경우에는 ERC20Basic 콘트랙트를 부모 콘트랙트로 지정한 상태입니다. 내리 사랑의 법칙에 따라 자식 콘트랙트는 부모 콘트랙트의 모든 것을 그대로 물려 받습니다. 쉽게 생각해서 부모 콘트랙트의 코드가 자식 콘트랙트로 복붙된다고 생각하면 됩니다.

그런데 단순히 복붙만 되는게 아닙니다. 만약 자식 콘트랙트에 똑같은 함수가 있다면 부모를 따르지 않고 새롭게 덮어 씁니다. 따라서 부모 콘트랙트 하나를 잘 만들어두면 약간씩만 변경하는 것만으로도 여러 자식 콘트랙트를 쉽게 만들 수 있습니다.

using A for B

B 자료형에 A 라이브러리 함수(Library Function) 를 붙여라.

위 코드에서의 using SafeMath for uint256;uint256자료형에 SafeMath 라이브러리 함수를 붙이라는 뜻입니다. 이 방식은 기본 자료형에 마치 객체지향 개념이 적용된 것처럼 dot(.) 으로 호출할 수 있는 함수를 붙여 줍니다. 예를 들면 다음과 같습니다.

uint256 a= 1;
a.add(1); // 오류

using SafeMath for uin256;
uint256 b= 1;
b.add(1); // 정상 작동

라이브러리 함수와 using for 에 대해 더 자세한 것은 여기를 참고하시면 됩니다.

mapping

하나의 변수에 여러 값을 저장하기 위한 map 변수 타입

mapping(자료형 A => 자료형 B) 변수명; 과 같이 사용하면 자료형 A 의 값을 key 로 자료형 B 의 값을 저장하고 찾을 수 있게 합니다. 설명은 괜히 어렵고 소스로 설명하는게 더 쉬울 것 같습니다.

mapping(string => string) grade; // 성적 저장용
grade["수학"]= "A+";
grade["영어"]= "B";
my_grade= grade["수학"]; // my_grade 에 "A+" 을 저장

mapping(uint256 => uint256) prime_nums; // 소수를 순차적으로 저장
prime_nums[1]= 2;
prime_nums[2]= 3;
prime_nums[3]= 5;
prime_nums[4]= 7;
prime_nums[5]= 11;

자료형 A 의 경우에는 'mapping, a dynamically sized array, a contract, an enum and a struct' 를 제외한 모든 타입을 지원하고, 자료형 Bmapping 을 포함한 모든 자료형 ('any type, including mappings') 에 대해 사용 가능합니다. 자료형 B 에 또 다시 mapping 을 넣어주면 다음과 같은 다중 map 을 만들 수 있습니다.

mapping(string => mapping(string => string)) grades;
grades["TaeKim"]["영어"]= "B";

require

조건이 참인지 체크하고 거짓이면 예외처리(실행 중단) 발생

require 는 가스 비용을 아껴주는 매우 고마운 예외처리 키워드입니다. 괄호 안의 조건이 참이라면 다음 소스로 계속 진행이 되고, 만약 거짓이라면 소스 실행이 중단됩니다. 여기서 중요한 것은 require 는 그냥 중단하는 것이 아니라 아예 모든 실행을 취소시킵니다. 예를 들어 데이타를 저장했던 것도 원래대로 복구시키고 가스 비용까지도 취소됩니다.

예를 들어 require(_to != address(0)) 라는 소스는 _to 변수의 값이 0 이 아닐 때(!= address(0))에만 조건을 참으로 인식하고 코드 실행을 허용합니다. 돈 받을 주소가 0 이면 안되니 항상 체크해줘야겠죠?

글이 너무 길어져서 여기서 한번 자르고 글을 두개로 나눕니다.
2편으로 고고~

글을 완성시키고 보니 새벽 6시네요. 저의 정성을 갸륵히 여겨 Vote 를... 읍읍....


TaeKim(@nida-io) 의 프로젝트를 구경하세요.

  • 니다닷컴 : 쉽게 읽히는 "한글 멘트 그대로의 링크" 를 만들어드립니다. 마케팅 콘텐츠, 홈페이지, 쇼핑몰, 블로그, 청첩장, 포트폴리오 등의 링크에 사용하여 가독성과 클릭율을 높여보세요.
  • 케이스타코인 : 830만 팔로어 전세계 1위 한류 미디어 KStarLive 와 함께 만든 한류 플랫폼에 사용되는 코인입니다. 스팀잇처럼 커뮤니티 활동을 하면서 코인을 얻을 수 있으며, 한류 콘텐츠 구매, 공연 예매, 한국 관광 관련, 기부 및 팬클럽 활동 등에 사용될 계획입니다.
H2
H3
H4
3 columns
2 columns
1 column
Join the conversation now