"누구나 만들 수 있는 이더리움 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
는 이러한 계정 주소를 저장하기 위한 변수 타입입니다. 참고로 uint256
은 256 bits 사이즈를 가진 unsigned integer (0 과 양수만 있는 정수) 변수 타입입니다. 이러한 변수 타입들은 하나 하나 설명드리기 어렵고 공식 문서 를 읽어봐주세요.
public
'이 함수는 누구나 호출할 수 있다' 는 접근 권한을 부여하는 키워드
콘트랙트 내의 함수와 변수에는 접근 권한을 줄 수가 있습니다. external
, public
, internal
, private
의 4종류 키워드가 있는데 자세한 건 solidity 공식 문서 를 읽어보세요.
view
'변수에 데이타를 저장하거나 수정하지 않는다는 것을 보장' 함을 명시하는 키워드
쉽게 생각하면 '읽기 전용' 함수라고 생각하면 됩니다. 이더리움에서는 이 키워드가 매우 중요한데요, view
함수는 수수료(가스)를 소모하지 않기 때문입니다. 따라서 데이타를 저장하지 않는 함수에는 무조건 view
키워드를 붙여줘야 합니다!
pure
'블록체인 데이타와 아예 무관한 함수' 임을 명시하는 키워드
참고로 view
를 넘어선 pure
함수도 있습니다. pure
함수는 블록체인에 데이타를 쓰는 것 뿐 아니라 아예 읽어오지도 않겠다는 뜻입니다. 예를 들어 아래 코드를 보면, a 와 b 라는 파라메터는 외부에서 입력 받는 파라메터이기 때문에 블록체인에 저장된 데이타와 무관하게 독자적으로 돌아갑니다.
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' 를 제외한 모든 타입을 지원하고, 자료형 B
는 mapping
을 포함한 모든 자료형 ('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) 의 프로젝트를 구경하세요.
죄송합니다. 글을 다 읽지 않았습니다.
"가스비용"이라는 말이 나오자마자 이것은 내가 관여할 바가 아니라는 생각이 들더군요.
시스템을 이용하는 데에는 비용이 들 수밖에 없는데, 그 비용을 소비자가 매번 감당하는 것이 과연 좋은지에 대해 고민할 수밖에 없어요.
저는 일단 비용이 지불되는 플랫폼을 별로 좋아하지 않기 때문에요.
그래도 비용문제가 잘 해결되어 이더리움이 많이 활용되었으면 좋겠습니다.
네 가스 비용에 대해 충분히 이해하고 검토한 후 개발했음에도.. 가스비가 나갈 때마다 짜증나는 건 어쩔 수 없더라고요 ㅎㅎㅎ
참고로 제 경험상으론 코인을 보낼 때 약 100-200원 정도 듭니다.
그런데 사실 개발에는 많은 리소스가 들기 마련이죠. 인건비며 개발 기간, 컴퓨팅 파워 등등.. 이더리움은 블록체인 환경의 개발을 아주 간단하고 빠르게 개발할 수 있기 때문에, 트랜잭션이 빈번하게 발생하는 경우가 아니라면 충분히 시도할 가치가 있습니다.
좋은 글 감사합니다. 팔로우합니다. 개발도 관심이 많은데 믾은 가르침 부탁드려요!!
감사합니다! ^^ 저도 팔로우 합니다~
mapping은 제가 기존에 썼던 배열을 획기적으로 편하게 만들었군요. 혹시 mapping으로 만든 변수에 쓸수있는 갯수의 제한이 있나요?
네 심플하고 아주 편하지만, 아직 solidity 가 매우 단순해서요, 타 언어에 비해서는 지원되는 기능이 약한 편이긴 합니다. 예를 들어 저장된 데이타가 몇개인지 알려주는 함수도 없습니다. ㅋ
대신 오픈 소스 iterable_mapping 을 사용하시면 좀 더 편합니다. (공식 문서에 링크되어 있는 소스에요)
변수 개수에 제한이 있다는 얘기는 들어본 적이 없습니다.
그리고 이 소스코드를 컴파일링해서 실행하는 역할은 누가 하는 건가요? 실행자의 pc가 하는건가요 아니면 채굴자가 하는건가요?
좋은 질문입니다~ 컴파일해서 블록화되면 채굴 과정을 통해 실행된다고 보시면 됩니다. 프로그램 실행을 위한 가스비를 채굴자가 가져갑니다. ^^
그럼 해당 소스코드의 컴파일은 이용자가 하고 그것을 블록체인에 올리면 채굴자가 채굴을 해서 검증을 함과 동시에 실행이 되는건가요?
네 맞습니다. 저는 그렇게 이해하고 있습니다. ^^
제가 글을 읽다가 require부분에서 잠시 멈췄는데 이게 악용될수있다고 생각했거든요. 만약 누군가가 악의적으로 많은 계산이 필요한 소스코드를 require앞에 배치시키고 무조건적으로 작동하는 require(false)를 뒤에 둔 다음 이 스마트 컨트랙트를 엄청나게 보내면 돈 한푼 안들이고 디도스 공격이 가능하지 않을까해서요...
개념글 잘 보고 갑니다. ERC20 이라고 부르는 이유에 대한 설명도 좋네요!
감사합니다! 보팅에 힘 입어 더 열심히 쓰겠습니다. ;-)
최대한 쉽게 쓰려고 했으나... '누구나 만들 수 있는' 이라는 눈높이에 맞춰 기본적인 문법과 개념들을 다 설명하려고 하다 보니 1편 내용이 많이 길어지고 복잡해졌습니다.
그런데 1편만 잘 읽어 놓으시면 2편부터는 그닥 길지 않을 거에요. 혹은 1편은 잘 이해가 안가는 부분은 넘어가면서 보신 후 궁금한게 있을 때 다시 찾아 보는 정도로 활용하셔도 괜찮을 것 같습니다.
글의 길이나 난이도에 대해 피드백 주시면 차후 글을 작성할 때 많은 도움이 될 것 같습니다. 그리고 댓글로 질문하시면 성실히 답변 달거나 본문 내용을 수정해놓도록 하겠습니다.
개발자가 아닌 제가 봐도 이해할 수 있을 정도로 쉽게 풀어쓰셨네요. 좋은글 감사합니다~!
감사합니다. ^^
잘 읽었습니다. 팔로우 합니다.
감사합니다. ^^
찜 감사합니다. ^^ 언제든 편하게 들러주세요~