"누구나 만들 수 있는 이더리움 ERC20 코인/토큰 실전 개발" (3편)
가끔씩 코인을 개발해줄 수 있냐는 요청을 받는데요, 코인 개발에 중요한 것은 개발 그 자체보다도 "세상의 어떤 부분을 어떻게 변화시킬 것이다" 라는 그 코인만의 세계관인 것 같습니다. 제 개인적인 생각으로는, 코인의 존재를 통해 특정 산업군을 구성하는 사람들과 회사들의 참여가 독려되고 그로 인해 빠른 시간 내에 기존의 산업구조가 새롭게 재편성될 수 있도록 설계하는 것이 중요한 것 같습니다. 다음 링크를 한번 읽어보시면 도움이 되실 것 같습니다.
또한 이더리움 ERC20 토큰의 경우에는 이러한 세계관과 그에 따른 사업 플랜을 초기에 엄밀하게 설계하는 것이 아주 아주 중요합니다. 왜냐하면 토큰을 한번 발행하고 나면, 그 소스가 이더리움 블록체인에 영구적으로 올라가면서 이후 소스를 업데이트 하지 못하기 때문입니다. 기존 소스를 개선할 수 있는 유일한 방법은, 새로운 주소로 소스를 등록한 후 예전에 사용하던 토큰을 새롭게 만든 토큰으로 이전시켜주는 것입니다. 하지만 이런 작업은 토큰의 신뢰도를 떨어트릴 수 있으므로 가급적 하면 안 되겠죠?
따라서 다음을 기억하세요.
- 코인 개발을 시작하기 전에 토큰의 백서를 미리 완성시키세요.
- 개발자는 멤버들과 많은 대화를 나누고 백서를 철저히 분석하여 향후 가능한 예외상황과 사업의 확장성을 첫 소스에 모두 포함시켜야 합니다.
- 그러면서도 가스 비용을 아끼기 위해 불필요한 부분은 최대한 제거해야 합니다.
코인 개발 자체는 쉽습니다. 이 문서만 잘 숙지하셔도 누구나 코인을 개발하실 수 있습니다. 하지만 코인을 잘 만들기(2번과 3번을 동시에 만족시키기)는 어려운 것 같습니다. 상용화시키기 위해서는 경험 많은 조언자가 필요하다고 생각합니다.
참고로, 제가 개발에 참여한 코인인 KStarCoin(이하 KSC) 의 경우에는 다음과 같은 목적을 가지고 만들었습니다.
- KStarLive 커뮤니티에 참여하여 활동하는 것만으로도 KSC 를 얻을 수 있다.
- 팬들은 활동하면서 얻은 KSC 를 다양한 팬클럽 활동 및 스타를 위한 조공 등에 활용할 수 있다.
- KSC 생태계에 참여하는 기업은 KStarLive 의 830만 한류 팬 유저들에게 자동으로 홍보가 된다.
- 그 결과, KSC 는 다양한 한류 콘텐츠(음악, 영상, 상품, 관광 등)에서 실제 사용할 수 있는 재화가 된다.
이보다 더 자세한 내용이 궁금하신 분은 KSC 홈페이지 및 KSC 백서 를 참고해주세요. 또한 소스는 http://케이스타코인.소스.보기.cc 에서 확인하실 수 있습니다.
서론이 길었네요. 오늘은 ERC20 규약에 따른 토큰을 완성시킬 생각입니다. 지난 연재를 못 보신 분은 보고 다음 글을 확인하세요.
ERC20 Interface 를 모두 표현 - ERC20.sol
https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/contracts/token/ERC20/ERC20.sol
pragma solidity ^0.4.18;
import "./ERC20Basic.sol";
/**
* @title ERC20 interface
* @dev see https://github.com/ethereum/EIPs/issues/20
*/
contract ERC20 is ERC20Basic {
function allowance(address owner, address spender) public view returns (uint256);
function transferFrom(address from, address to, uint256 value) public returns (bool);
function approve(address spender, uint256 value) public returns (bool);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
지난 연재 1편에서 ERC20Basic
콘트랙트는 ERC20 규약에서 '제3자 송금 기능' 을 빼놓고 선언했다고 말씀드린 적 있습니다. 오늘 소개해드리는 ERC20
은 드디어 ERC20 규약을 온전히 모두 선언하는 가상 콘트랙트 입니다. 가상 콘트랙트가 무엇인지 잘 모르겠다면 연재 1편에서 해당 부분을 찾아서 읽어보시기 바랍니다.
ERC20
콘트랙트는 is
키워드를 통해 ERC20Basic
의 모든 것을 상속 받기 때문에 totalSupply
, balanceOf
, transfer
함수와 Transfer
이벤트 함수는 이미 가지고 있는 상태입니다. 위 소스에서는 ERC20 규약의 나머지 함수들 allowance
, transferFrom
, approve
과 이벤트 함수 Approval
을 추가로 선언하고 있네요. ERC20 규약에 따른 각 함수의 의미는 다음과 같습니다.
함수명 | 내용 | 리턴값 |
---|---|---|
approve | spender 에게 value 만큼의 토큰을 인출할 권리를 부여한다. 이 함수를 이용할 때는 반드시 Approval 이벤트 함수를 호출해야 한다. | 성공 / 실패 |
allowance | owner 가 spender 에게 인출을 허락한 토큰의 개수는 몇개인가? | 허용된 토큰의 개수 |
transferFrom | from 의 계좌에서 value 개의 토큰을 to 에게 보내라. 단, 이 함수는 approve 함수를 통해 인출할 권리를 받은 spender 만 실행할 수 있다. | 성공 / 실패 |
ERC20Basic
콘트랙트와 마찬가지로 ERC20
콘트랙트는 ERC20 인터페이스 규약을 그대로 쓴 것 밖에 되지 않습니다. 여기서는 굳이 이해하려고 하지 않고 넘어가신 후 아래 StandardToken
콘트랙트 부분을 보시면 됩니다.
참고로 solidity 에는 가상 콘트랙트와 역할이 비슷하면서 더 명시적인 표현인 interface
키워드도 존재합니다. 하지만 interface
키워드는 다른 인터페이스를 상속 받지를 못합니다. 이건 너무나 큰 단점이라 현재는 굳이 interface
키워드를 사용할 필요가 전혀 없습니다. (참고 : 공식 문서에서 'Some of these restrictions might be lifted in the future.' 라고 표현했기 때문에 차후 개선될 것 같음).
드디어 ERC20 규약을 완성! - StandardToken.sol
pragma solidity ^0.4.18;
import "./BasicToken.sol";
import "./ERC20.sol";
/**
* @title Standard ERC20 token
*
* @dev Implementation of the basic standard token.
* @dev https://github.com/ethereum/EIPs/issues/20
* @dev Based on code by FirstBlood: https://github.com/Firstbloodio/token/blob/master/smart_contract/FirstBloodToken.sol
*/
contract StandardToken is ERC20, BasicToken {
mapping (address => mapping (address => uint256)) internal allowed;
/**
* @dev Transfer tokens from one address to another
* @param _from address The address which you want to send tokens from
* @param _to address The address which you want to transfer to
* @param _value uint256 the amount of tokens to be transferred
*/
function transferFrom(address _from, address _to, uint256 _value) public returns (bool) {
require(_to != address(0));
require(_value <= balances[_from]);
require(_value <= allowed[_from][msg.sender]);
balances[_from] = balances[_from].sub(_value);
balances[_to] = balances[_to].add(_value);
allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value);
Transfer(_from, _to, _value);
return true;
}
/**
* @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender.
*
* Beware that changing an allowance with this method brings the risk that someone may use both the old
* and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this
* race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
* @param _spender The address which will spend the funds.
* @param _value The amount of tokens to be spent.
*/
function approve(address _spender, uint256 _value) public returns (bool) {
allowed[msg.sender][_spender] = _value;
Approval(msg.sender, _spender, _value);
return true;
}
/**
* @dev Function to check the amount of tokens that an owner allowed to a spender.
* @param _owner address The address which owns the funds.
* @param _spender address The address which will spend the funds.
* @return A uint256 specifying the amount of tokens still available for the spender.
*/
function allowance(address _owner, address _spender) public view returns (uint256) {
return allowed[_owner][_spender];
}
/**
* @dev Increase the amount of tokens that an owner allowed to a spender.
*
* approve should be called when allowed[_spender] == 0. To increment
* allowed value is better to use this function to avoid 2 calls (and wait until
* the first transaction is mined)
* From MonolithDAO Token.sol
* @param _spender The address which will spend the funds.
* @param _addedValue The amount of tokens to increase the allowance by.
*/
function increaseApproval(address _spender, uint _addedValue) public returns (bool) {
allowed[msg.sender][_spender] = allowed[msg.sender][_spender].add(_addedValue);
Approval(msg.sender, _spender, allowed[msg.sender][_spender]);
return true;
}
/**
* @dev Decrease the amount of tokens that an owner allowed to a spender.
*
* approve should be called when allowed[_spender] == 0. To decrement
* allowed value is better to use this function to avoid 2 calls (and wait until
* the first transaction is mined)
* From MonolithDAO Token.sol
* @param _spender The address which will spend the funds.
* @param _subtractedValue The amount of tokens to decrease the allowance by.
*/
function decreaseApproval(address _spender, uint _subtractedValue) public returns (bool) {
uint oldValue = allowed[msg.sender][_spender];
if (_subtractedValue > oldValue) {
allowed[msg.sender][_spender] = 0;
} else {
allowed[msg.sender][_spender] = oldValue.sub(_subtractedValue);
}
Approval(msg.sender, _spender, allowed[msg.sender][_spender]);
return true;
}
}
이번 소스는 조금 기네요.
일단 StandardToken
콘트랙트는 ERC20
콘트랙트와 BasicToken
콘트랙트를 상속받고 있습니다. BasicToken
에서 totalSupply
, balanceOf
, transfer
함수를 이미 구현했기 때문에 해당 내용은 구현하지 않아도 됩니다. ERC20
에서 새롭게 선언한 allowance
, transferFrom
, approve
함수는 아직 선언만 된 상태이므로 이 부분을 구현하면 되겠군요.
자, 그럼 이제 소스의 각 부분을 분석해보도록 하겠습니다.
변수 allowed
mapping (address => mapping (address => uint256)) internal allowed;
allowed
변수는 approve
함수를 통해 '누가', '누구에게', '얼마의' 인출 권한을 줄지를 저장합니다. '누가', '누구에게' 두 부분을 map 의 key 로 사용하고, '얼마' 를 value 로 저장합니다. 예를 들면 다음과 같습니다.
allowed[누가][누구에게]= 얼마;
함수 transferFrom
function transferFrom(address _from, address _to, uint256 _value) public returns (bool) {
require(_to != address(0));
require(_value <= balances[_from]);
require(_value <= allowed[_from][msg.sender]);
balances[_from] = balances[_from].sub(_value);
balances[_to] = balances[_to].add(_value);
allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value);
Transfer(_from, _to, _value);
return true;
}
- 위의
require
세 줄은- 받는 계좌가 0 이 아닌지,
- 보내려는 값이 잔고 이내인지,
- 보내려는 값이 계좌 주인
_from
이 돈을 빼려는msg.sender
에게 허용한 권한 이내인지를 체크합니다.
일반적으로 msg.sender
는 가스비를 소모하여 이 함수를 호출한 계정입니다. 즉, _from
에게 인출 권한을 받은 계정이 transferFrom
함수를 호출해야 동작합니다.
- 그 다음 세 줄도 간단합니다.
_from
의 계좌에서_value
만큼을 빼라(인출하라)._to
의 계좌에_value
만큼 더해라(입금하라).msg.sender
의 인출 권한에서_value
만큼을 제하라.
- 토큰의 이동이 일어났으므로, ERC20 규약의 가이드에 따라
Transfer
이벤트를 발생시킵니다.- 이벤트는 이더리움 블록체인에 영구적으로 기록되며,
indexed
된 값은 차후 언제든 검색 가능합니다.
- 이벤트는 이더리움 블록체인에 영구적으로 기록되며,
allowed
맵 변수를 잘 보면 _to
값은 저장이 되어 있지 않습니다. 다시 말해 _from
계좌의 주인이 approve
함수에서 spender
에게 인출 권한을 주면, 그 spender
는 이 transferFrom
함수를 호출하여 _from
계좌에서 누구에게라도 돈을 보낼 수 있습니다.
함수 approve
function approve(address _spender, uint256 _value) public returns (bool) {
allowed[msg.sender][_spender] = _value;
Approval(msg.sender, _spender, _value);
return true;
}
소스는 아주 간단합니다.
allowed[msg.sender][_spender] = _value;
함수를 호출한 본인(msg.sender
)의 계좌에서value
만큼 인출해 갈 수 있는 권리를spender
에게 부여한다.Approval(msg.sender, _spender, _value);
ERC20 규약에 따라Approval
이벤트를 발생시킨다. 이벤트 함수를 통해 세가지 값(msg.sender
,_spender
,_value
)는 영구적으로 블록체인에 기록되며indexed
된 값은 검색될 수 있다.
그런데 이 부분은 주석이 오히려 중요합니다. approve 함수 위에 다음과 같은 주석이 붙어 있었습니다.
* Beware that changing an allowance with this method brings the risk that someone may use both the old
* and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this
* race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
위 내용인 즉슨, 이더리움의 경우에는 채굴이 되기 전까지는 실행이 되지 않기 때문에, 간혹 늦게 실행한 코드가 먼저 동작하는 경우가 생깁니다. 따라서 다음과 같은 경우가 생길 수 있습니다.
- A 가 B 에게 인출할 수 있는 권리 N 을 부여함.
- A 가 생각이 바뀌어 B 에게 인출할 수 있는 권리 M 을 새롭게 부여함.
- B 가 A 가 생각이 바뀌었음(2번)을 알아차리고 재빠르게 N 을 인출 시도함.
- 채굴 구조의 특성상 3번이 2번보다 먼저 실행될 수도 있음. 그러면 B 는 N 을 인출한 후, 추가로 M 을 인출할 수 있는 권리를 얻음.
- A 가 잘못된 것을 알아차리기 전에 B 가 M 을 인출해 가면, B 는 (N+M) 을 인출해가게 됨.
이런 문제를 해결하기 위해 A 는 B 에게 인출할 권리를 N 에서 0 으로 먼저 바꾸고, 문제 없이 정상적으로 변경된 것을 확인한 후 다시 M 으로 변경하라는 뜻입니다.
채굴 순서에 의해 코드의 실행 순서가 뒤바뀔 수 있다는 부분을 간과하면, 이 외에도 다양한 보안의 허점이 생길 수 있습니다. 항상 이 부분을 염두에 두시기 바랍니다.
함수 allowance
function allowance(address _owner, address _spender) public view returns (uint256) {
return allowed[_owner][_spender];
}
_owner
가 _spender
에게 얼마만큼의 인출 권한을 부여했는지를 return 합니다. public 함수이기 때문에 누구나 계좌 주소만 알면 확인할 수 있습니다.
추가적인 함수 increaseApproval, decreaseApproval
function increaseApproval(address _spender, uint _addedValue) public returns (bool) {
allowed[msg.sender][_spender] = allowed[msg.sender][_spender].add(_addedValue);
Approval(msg.sender, _spender, allowed[msg.sender][_spender]);
return true;
}
function decreaseApproval(address _spender, uint _subtractedValue) public returns (bool) {
uint oldValue = allowed[msg.sender][_spender];
if (_subtractedValue > oldValue) {
allowed[msg.sender][_spender] = 0;
} else {
allowed[msg.sender][_spender] = oldValue.sub(_subtractedValue);
}
Approval(msg.sender, _spender, allowed[msg.sender][_spender]);
return true;
}
이 함수들은 ERC20 규약에 있는 함수는 아닙니다. 위 approve
함수에서 값을 바꿀 때 0으로 변경한 후 다시 원하는 값으로 바꾸는 것은 많이 불편한 일이기 때문에 (참고 : 0 으로 변경하는 명령이 채굴되기를 기다렸다가 다시 원하는 값으로 변경하도록 실행해야 함), 차액만큼을 더하거나 빼는 함수를 추가로 제공하고 있습니다. 이 함수를 이용하면 N+M 만큼 인출해가는 공격을 피할 수 있습니다.
소스가 어렵지 않고, 표준 규약이 아니기 때문에 소스를 굳이 설명드리진 않겠습니다.
진짜 ERC20 토큰 만들기에 써도 되는 예제 - SimpleToken
https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/contracts/examples/SimpleToken.sol
pragma solidity ^0.4.18;
import "../token/ERC20/StandardToken.sol";
/**
* @title SimpleToken
* @dev Very simple ERC20 Token example, where all tokens are pre-assigned to the creator.
* Note they can later distribute these tokens as they wish using `transfer` and other
* `StandardToken` functions.
*/
contract SimpleToken is StandardToken {
string public constant name = "SimpleToken"; // solium-disable-line uppercase
string public constant symbol = "SIM"; // solium-disable-line uppercase
uint8 public constant decimals = 18; // solium-disable-line uppercase
uint256 public constant INITIAL_SUPPLY = 10000 * (10 ** uint256(decimals));
/**
* @dev Constructor that gives msg.sender all of existing tokens.
*/
function SimpleToken() public {
totalSupply_ = INITIAL_SUPPLY;
balances[msg.sender] = INITIAL_SUPPLY;
Transfer(0x0, msg.sender, INITIAL_SUPPLY);
}
}
아주 간단한 소스이지만 ERC20 표준 규약을 따르는 토큰을 완벽하게 만들 수 있습니다. 소스 설명은 연재 2편에서의 MyBasicToken
과 거의 유사하므로 굳이 하진 않겠습니다.
대신 나만의 커스텀 토큰을 만들기 위한 방법을 가르쳐드리겠습니다. 너무나 너무나 쉽습니다. 위 소스에서 다음 한글 부분만 수정하여 사용하시면 됩니다.
string public constant name = "토큰_이름";
string public constant symbol = "토큰_심볼"; // solium-disable-line uppercase
uint8 public constant decimals = 18; // solium-disable-line uppercase
uint256 public constant INITIAL_SUPPLY = 토큰_발행량 * (10 ** uint256(decimals));
decimals
값 18 은 가급적 그냥 두시는게 좋습니다. 이 값을 변경하면 만들려는 토큰과 ether 와의 상호 계산이 복잡해질 수 있습니다.
zeppelin 의 소스 덕에 우리는 달랑 세 부분만 변경하고 아주 잘 동작하는 ERC20 토큰을 만들 수 있었습니다! 야호! 얘들아, 나 토큰 만들었어! 빨리 투자해!
하지만 아직 실제 사용할 토큰으로는 부족한 점이 많습니다. 다음 시간에는 zeppelin 의 다른 소스들을 이용하여 몇가지 기능을 추가하도록 하겠습니다.
TaeKim(@nida-io) 의 프로젝트를 구경하세요.
공유 감사합니다. 10 ** uint256(decimals)
PS. wow 니다닷컴 ! 투자하고 싶네요 ㅋ TaeKim 님좀짱인듯
감사합니다! goo.gl 의 서비스 철수 소식으로 새로운 기회가 생기려나 싶습니다. ^^
좋은글 감사합니다.
감사합니다. 꾸준히 연재 올릴 예정이니 종종 들러주세요. ^^
토큰을 이렇게 만드는군요
자세히 설명해주셔서 도움이 됩니다
좋은 글 감사합니다^^
네, 제게 힘을 주시는 댓글입니다. ^^
중간에 끊기지 않고 꼭 끝까지 완결 짓도록 하겠습니다.
보팅을 안해드릴수가 없는 글입니다. 감사합니다. ^^
감사합니다! ^^
와! 엄청 상세한 포스팅 감사드립니다 :)
보팅 드려요!!
이 글만 봐도 웬만한 궁금즘들은 다 해결될 수 있도록 노력했습니다. 감사합니다! :)
개념 & 정성 글에는 언제나.. 풀보팅 드리고 갑니다. 가좌~
감사합니다~!!! 현재의 제 보팅은 아무 영향력이 없지만... ㅜ.ㅜ 저도 곧 현질을 통해 스팀파워 빵빵하게 채우도록 하겠습니다 ㅋㅋ
openzepplin 공부중인데 많은 도움이 되었습니다 감사의미로 업보팅 합니다~
네~ 다른 소스에서 궁금한게 있으시면 물어보셔도 됩니다. 제가 아는 선에서는... ㅎㅎㅎ
토큰 스탠다드에 대해 개략적으로만 알았는데 코드 설명 들으니 좋네요..
감사합니다..
감사합니다. 계속되는 연재에 관심 부탁 드립니다. :-)
좋은 정보 고맙습니다. 정말 궁금했던 부분인데 해결되서 기쁘네요.
앗! 궁금하셨던 부분이 해결됐다니 저도 기쁩니다! 다음 연재 분도 기대해주세요. ^^
좋은 정보 주셔서 감사합니다
^^^^언제 쯤 실용 가능토큰 만들기에대한 다음글을 볼 수 있을까요
네, 원래는 오늘 올릴 예정이었는데 생각보다 처리할 일들이 많네요 흑... ㅜ.ㅜ 꼭 조만간 올리겠습니다.
일주일이 지나버려서 본문 수정을 못하네요 ㅋ 댓글로 다음 편 링크 남깁니다.
대단한 필력과 내공이십니다. 도움 많이 받았습니다~ 감사합니다.
도움이 되셨다니 다행입니다. 감사합니다~ ^^