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

in #kr-dev7 years ago (edited)

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

가끔씩 코인을 개발해줄 수 있냐는 요청을 받는데요, 코인 개발에 중요한 것은 개발 그 자체보다도 "세상의 어떤 부분을 어떻게 변화시킬 것이다" 라는 그 코인만의 세계관인 것 같습니다. 제 개인적인 생각으로는, 코인의 존재를 통해 특정 산업군을 구성하는 사람들과 회사들의 참여가 독려되고 그로 인해 빠른 시간 내에 기존의 산업구조가 새롭게 재편성될 수 있도록 설계하는 것이 중요한 것 같습니다. 다음 링크를 한번 읽어보시면 도움이 되실 것 같습니다.

또한 이더리움 ERC20 토큰의 경우에는 이러한 세계관과 그에 따른 사업 플랜을 초기에 엄밀하게 설계하는 것이 아주 아주 중요합니다. 왜냐하면 토큰을 한번 발행하고 나면, 그 소스가 이더리움 블록체인에 영구적으로 올라가면서 이후 소스를 업데이트 하지 못하기 때문입니다. 기존 소스를 개선할 수 있는 유일한 방법은, 새로운 주소로 소스를 등록한 후 예전에 사용하던 토큰을 새롭게 만든 토큰으로 이전시켜주는 것입니다. 하지만 이런 작업은 토큰의 신뢰도를 떨어트릴 수 있으므로 가급적 하면 안 되겠죠?

따라서 다음을 기억하세요.

  1. 코인 개발을 시작하기 전에 토큰의 백서를 미리 완성시키세요.
  2. 개발자는 멤버들과 많은 대화를 나누고 백서를 철저히 분석하여 향후 가능한 예외상황과 사업의 확장성을 첫 소스에 모두 포함시켜야 합니다.
  3. 그러면서도 가스 비용을 아끼기 위해 불필요한 부분은 최대한 제거해야 합니다.

코인 개발 자체는 쉽습니다. 이 문서만 잘 숙지하셔도 누구나 코인을 개발하실 수 있습니다. 하지만 코인을 잘 만들기(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 규약에 따른 각 함수의 의미는 다음과 같습니다.

함수명내용리턴값
approvespender 에게 value 만큼의 토큰을 인출할 권리를 부여한다. 이 함수를 이용할 때는 반드시 Approval 이벤트 함수를 호출해야 한다.성공 / 실패
allowanceownerspender 에게 인출을 허락한 토큰의 개수는 몇개인가?허용된 토큰의 개수
transferFromfrom 의 계좌에서 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

https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/contracts/token/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

위 내용인 즉슨, 이더리움의 경우에는 채굴이 되기 전까지는 실행이 되지 않기 때문에, 간혹 늦게 실행한 코드가 먼저 동작하는 경우가 생깁니다. 따라서 다음과 같은 경우가 생길 수 있습니다.

  1. A 가 B 에게 인출할 수 있는 권리 N 을 부여함.
  2. A 가 생각이 바뀌어 B 에게 인출할 수 있는 권리 M 을 새롭게 부여함.
  3. B 가 A 가 생각이 바뀌었음(2번)을 알아차리고 재빠르게 N 을 인출 시도함.
  4. 채굴 구조의 특성상 3번이 2번보다 먼저 실행될 수도 있음. 그러면 B 는 N 을 인출한 후, 추가로 M 을 인출할 수 있는 권리를 얻음.
  5. 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) 의 프로젝트를 구경하세요.

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

공유 감사합니다. 10 ** uint256(decimals)
PS. wow 니다닷컴 ! 투자하고 싶네요 ㅋ TaeKim 님좀짱인듯

감사합니다! goo.gl 의 서비스 철수 소식으로 새로운 기회가 생기려나 싶습니다. ^^

좋은글 감사합니다.

감사합니다. 꾸준히 연재 올릴 예정이니 종종 들러주세요. ^^

토큰을 이렇게 만드는군요
자세히 설명해주셔서 도움이 됩니다
좋은 글 감사합니다^^

네, 제게 힘을 주시는 댓글입니다. ^^
중간에 끊기지 않고 꼭 끝까지 완결 짓도록 하겠습니다.

보팅을 안해드릴수가 없는 글입니다. 감사합니다. ^^

감사합니다! ^^

와! 엄청 상세한 포스팅 감사드립니다 :)
보팅 드려요!!

이 글만 봐도 웬만한 궁금즘들은 다 해결될 수 있도록 노력했습니다. 감사합니다! :)

개념 & 정성 글에는 언제나.. 풀보팅 드리고 갑니다. 가좌~

감사합니다~!!! 현재의 제 보팅은 아무 영향력이 없지만... ㅜ.ㅜ 저도 곧 현질을 통해 스팀파워 빵빵하게 채우도록 하겠습니다 ㅋㅋ

openzepplin 공부중인데 많은 도움이 되었습니다 감사의미로 업보팅 합니다~

네~ 다른 소스에서 궁금한게 있으시면 물어보셔도 됩니다. 제가 아는 선에서는... ㅎㅎㅎ

토큰 스탠다드에 대해 개략적으로만 알았는데 코드 설명 들으니 좋네요..

감사합니다..

감사합니다. 계속되는 연재에 관심 부탁 드립니다. :-)

좋은 정보 고맙습니다. 정말 궁금했던 부분인데 해결되서 기쁘네요.

앗! 궁금하셨던 부분이 해결됐다니 저도 기쁩니다! 다음 연재 분도 기대해주세요. ^^

좋은 정보 주셔서 감사합니다^^
언제 쯤 실용 가능토큰 만들기에대한 다음글을 볼 수 있을까요
^^

네, 원래는 오늘 올릴 예정이었는데 생각보다 처리할 일들이 많네요 흑... ㅜ.ㅜ 꼭 조만간 올리겠습니다.

일주일이 지나버려서 본문 수정을 못하네요 ㅋ 댓글로 다음 편 링크 남깁니다.