Solidity 튜토리얼 3: truffle + zeppelin-solidity를 이용한 크라우드 세일 계약 II

Yoonjae Yoo
DNEXT
Published in
12 min readApr 5, 2018

Solidity 튜토리얼 2: truffle + zeppelin-solidity을 이용한 크라우드 세일 계약 I에서 이어집니다.

여기서 쓰인 예제 코드들은 실제로 작동하는 코드이지만, 취약점이 존재할 가능성이 있습니다. 따라서 실제 크라우드 세일에서 사용하지 말아야 합니다. 사용했을 때 발생하는 문제에 대해서는 책임을 지지 않습니다. 계약의 보안성을 높이기 위해서는 스마트 계약 전문 Audit 업체들에게 서비스를 받는 것을 추천합니다.

DNX 토큰 계약 작성

우리의 DNX 토큰을 위한 스마트 계약을 작성해 볼 차례입니다. 앞으로 C:\Users\Yoonjae\Documents\dnx와 같은 우리의 프로젝트 경로를 최상위 경로(root path)로 지칭할 것입니다.

최상위 경로의 contracts 디렉토리 밑에 DNextToken.sol 파일을 생성해줍니다. 그리고 에디터로 파일을 열어 아래와 같이 작성해줍니다.

pragma solidity ^0.4.21;

import 'zeppelin-solidity/contracts/token/ERC20/StandardToken.sol';
contract DNextToken is StandardToken {
}

현재 이 계약은 기본적인 ERC20 토큰입니다. 즉, 잔액을 조회하고 다른 주소로 토큰을 전송하는 등의 기능이 모두 구현되어 있습니다. 위의 단 6줄의 코드로 이를 가능하게 했는데, 비밀은 StandardToken에 있습니다. StandardToken은 zeppelin-solidity가 제공해주는 라이브러리로써 ERC20의 기본적인 기능들을 모두 구현하고 있습니다. 우리의 DNextTokenStandardToken을 상속(inheritance)¹했기 때문에 이를 모두 물려받아 그대로 사용이 가능한 것입니다. 라이브러리의 역할은 이렇듯 최소한의 코드로 다른 스마트 계약 작성자들이 만들어놓은 잘 정제된 코드를 그대로 가져올 수 있게 하는 것입니다.

아래와 같은 상태 변수(state variable)를 추가해서 DNext 토큰의 정보를 명시해줍니다.

uint public INITIAL_SUPPLY = 21000000;
string public name = 'DNextToken';
string public symbol = 'DNX';
uint8 public decimals = 8;
address owner;

name은 토큰의 이름, symbol은 심볼, decimals는 토큰을 10의 몇자리까지 나눌 수 있는지를 나타냅니다. 우리는 비트코인과 동일하게 8로 설정했습니다. INITIAL_SUPPLY를 21000000으로 설정함으로써 기본적인 정보를 입력했습니다.

가장 마지막의 owner는 권한 부여를 위한 상태 변수로써, 토큰을 발행한 계정의 주소(발행자)가 저장되어야 합니다.

그리고 아래와 같은 생성자(constructor)를 추가해줍니다. 생성자는 계약 배포시 최초 한번만 실행되는 특수한 함수입니다. 실행시키는 주체는 배포자인 여러분입니다.

function DNextToken() public {
totalSupply_ = INITIAL_SUPPLY * 10 ** uint(decimals);
balances[msg.sender] = INITIAL_SUPPLY;
owner = msg.sender;
}

totalSupply_는 우리가 상속받은StandardToken가 상속받은 BasicToken 계약서에서 사용되는 변수로써, 해당값을 설정하면 토큰의 총 공급량이 정해집니다. 그 값을 INITIAL_SUPPLY * 10 ** uint(decimals)로 설정해준 이유는, ERC20 표준에 따라 우리 토큰의 최소 단위 기준으로 토큰의 총 공급량을 명시해줘야하기 때문입니다(즉, 21000000 * 10⁸). 그리고 balances 또한 BasicToken의 변수로써, 각 계정의 잔액을 저장하고 있습니다. msg.sender는 계약 배포자의 주소이므로 최초에는 배포자에게 모든 금액이 입금되어 있는 셈입니다. 그리고 마지막으로 ownermsg.sender로 설정해줌으로써 추후에 사용될 owner 변수값을 저장했습니다.

bool public released = false;function release() public {
require(owner == msg.sender);
require(!released);
released = true;
}

계약 배포시에는 released 변수는 false로 초기화되어 있습니다. 추후에 이 값이 true로 바뀌어야지만 토큰이 기능을 할 수 있습니다. 그리고 release() 함수는 토큰을 출시하는 역할을 하는데, require(owner == msg.sender);를 통해 토큰의 발행자만이 토큰 출시를 설정할 수 있도록 합니다. 다른 주소의 계정은 해당 함수를 실행시키려고 해도 항상 실패할 것입니다. require 은 뒤의 조건문(여기서는 owner == msg.sender)이 거짓이면 throw를 발생시켜 함수 실행을 중지시킵니다.

단, 여기서 중요한 점은 released 변수가 true 혹은 false인지 여부와는 관계없이 계약은 이미 배포가 되었고 이더리움 블록체인 위에 올라가 있는 상태입니다. ‘출시’라는 개념은 우리가 계약 내에서 만들어낸 인위적인 개념으로써, 크라우드 세일이 끝나기 전까지 토큰을 사용할 수 없도록 막아놓는 역할을 합니다. 크라우드 세일이 끝난다면 토큰 발행자는 release() 함수를 호출해 참여자들이 토큰을 사용할 수 있도록 설정하면 됩니다.

이렇게 설정한 released는 아직 사용되는 곳이 없습니다. 따라서 토큰의 기능을 수행하는 각각의 함수에서 released 여부를 확인하여 그렇지 않을 경우 함수를 실행하지 말아야 합니다. 우선, 우리 토큰이 제공하는 함수 중 ERC20에 해당하는 것은 아래의 6개와 같습니다. 이는 모두 상위(super) 계약⁶인 StandardTokenBasicToken, ERC20, ERC20Basic 계약에 포함되어 있어 우리의 계약에는 명시되어 있지 않습니다.

function totalSupply() public view returns (uint256);
function balanceOf(address who) public view returns (uint256);
function transfer(address to, uint256 value) public returns (bool);
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);

이제 우리가 할 것은 이 중 토큰 거래에 사용되는 함수(transfer, allowance, transferFrom, approve)를 우리의 계약에 명시해주고, 위에서 우리가 설정해준 released 변수를 확인해서 false이면 함수가 실행되지 않도록 하는 것입니다.

이를 위해서는 변경자(modifier)를 활용해야 합니다. 변경자란, 함수와 유사하지만 그 자체로써 실행될 수는 없고 다른 함수의 앞/뒤에서 함수의 기능을 변경(추가)하는 역할을 합니다. 우리가 작성할 변경자는 아래와 같습니다.

modifier onlyReleased() {
require(released);
_;
}

위 변경자의 의미는, released 변수가 true 일 때에만 함수를 실행시키도록 하는 것입니다. _;의 의미는 해당 변경자가 적용된 함수의 실제 내용을 나타냅니다.

그리고 변경자를 적용하기 위해서는 아래와 같이 함수를 작성해야 합니다.

function transfer(address to, uint256 value) public onlyReleased returns (bool) {
super.transfer(to, value);
}
function allowance(address owner, address spender) public onlyReleased view returns (uint256) {
super.allowance(owner,spender);
}
function transferFrom(address from, address to, uint256 value) public onlyReleased returns (bool) {
super.transferFrom(from, to, value);
}
function approve(address spender, uint256 value) public onlyReleased returns (bool) {
super.approve(spender,value);
}

여기서 중요한 포인트는 크게 세가지입니다.

  1. 함수 재정의(override) : transfer, allowance, transferFrom, approve 네 개의 함수는 상위 계약²에 정의되어 있는 것들이지만 우리는 같은 이름의 네 개의 함수를 다시 명시해주었습니다. 이렇게 함으로써, 상위 계약의 함수를 새롭게 작성할 수 있게 됩니다.
  2. 상위(super) 함수 호출 : 각각의 함수 내용은 모두 동일하게 super. 으로 시작합니다. super.<함수 이름>의 의미는 상위 계약의 특정 함수를 호출하겠다는 것입니다. 우리가 작성한 네 개의 함수 모두 상위 계약에서 동일한 이름의 함수를 호출하고 있습니다. 즉, 상위 계약과 다른 행동을 하도록 함수를 작성할 수 있지만 그렇게 하지 않은 것입니다.
  3. 변경자(modifier) 적용 : 변경자를 적용하기 위해서는 함수 이름 뒤에 오는 괄호의 쌍이 끝나고 나서 변경자의 이름을 명시해 주면 됩니다. 우리는 네개의 함수 모두에 onlyReleased 변경자를 적용했습니다. onlyReleased 변경자는 위에서 보았다시피 토큰이 출시되지 않았으면 함수가 실행되지 않도록 하는 역할을 합니다.

이렇게 우리의 토큰 발행을 위한 스마트 계약이 완성되었습니다. 전체 소스 코드는 아래와 같습니다.

pragma solidity ^0.4.21;

import 'zeppelin-solidity/contracts/token/ERC20/StandardToken.sol';


contract DNextToken is StandardToken {

uint public INITIAL_SUPPLY = 21000000;
string public name = 'DNextToken';
string public symbol = 'DNX';
uint8 public decimals = 8;
address owner;
bool public released = false;

function DNextToken() public {
totalSupply_ = INITIAL_SUPPLY * 10 ** uint(decimals);
balances[msg.sender] = INITIAL_SUPPLY;
owner = msg.sender;
}

function release() public {
require(owner == msg.sender);
require(!released);
released = true;
}

modifier onlyReleased() {
require(released);
_;
}

function transfer(address to, uint256 value) public onlyReleased returns (bool) {
super.transfer(to, value);
}

function allowance(address owner, address spender) public onlyReleased view returns (uint256) {
super.allowance(owner, spender);
}

function transferFrom(address from, address to, uint256 value) public onlyReleased returns (bool) {
super.transferFrom(from, to, value);
}

function approve(address spender, uint256 value) public onlyReleased returns (bool) {
super.approve(spender, value);
}
}

다음 포스트에서는 두번째 스마트 계약인, 사전 등록을 위한 스마트 계약 작성에 대해 다룰 예정입니다.

스마트 계약과 관련한 도움이 필요하다면 DNext에서 운영하고 있는 블록체인 전문 교육 기관인 DNext Campus를 언제든 방문해주시기 바랍니다. 블록체인, 이더리움, 스마트 계약에 관련된 주제로 정규 교육 과정을 운영중에 있습니다.

또한 저희 DNext에서는 블록체인 비즈니스를 위한 전문 컨설팅 서비스를 제공하고 있습니다. 도움이 필요하신 분들은 support@dnext.co로 문의주시기 바랍니다.

  1. 상속: 특정 계약의 코드를 그대로 재사용할 수 있는 기능. is 키워드를 통해 상속받을 계약을 명시해준다면 해당 계약의 코드들이 마치 상속받은 계약에 존재하는 것과 같은 효과.
  2. 상위 계약: 상속받은 대상이 되는 계약.

--

--