Solidity 튜토리얼 5: truffle + zeppelin-solidity을 이용한 크라우드 세일 계약 IV

Yoonjae Yoo
DNEXT
Published in
11 min readMay 8, 2018

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

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

토큰 판매를 위한 계약 작성

토큰 판매를 위한 계약의 이름은 DNextTokenSale.sol 로 명명합니다.

pragma solidity ^4.0.21;contract DNextTokenSale {
}

정의될 상태 변수는 아래와 같습니다.

uint public constant EMMAS_PER_WEI = 10000000;
uint public constant HARD_CAP = 500000000000000;
DNextToken public token;
DNextTokenWhitelist public whitelist;
uint public emmasRaised;
bool private closed;
  • EMMAS_PER_WEI : 1 wei 당 교환해줄 Emma(10⁸ Emma = 1 DNX)의 비율
  • HARD_CAP : 최대 판매 수량
  • token: 우리가 만든 토큰을 참조
  • emmasRaised : 크라우드 세일 과정에서 판매된 전체 Emma의 수
  • closed : 크라우드 세일이 종료되었는지 여부

위의 두가지 상태 변수에 대해 public 키워드 뒤에 붙은 constant 키워드는 해당 변수가 ‘상수’임을 나타내고, EVM의 저장소(storage)에 저장되지 않고 매번 값을 계산해 사용하도록 지시합니다. 앞으로 변경될 예정이 없는 값들을 상수로 지정을 하면, 가스 비용을 조금이나마 줄일 수 있습니다.

아래는 토큰 판매 계약의 생성자입니다.

function DNextTokenSale(DNextToken _token, DNextTokenWhitelist _whitelist) public {
require(_token != address(0));
token = _token;
whitelist = _whitelist;
}

생성자의 첫번째 파라미터(parameter)인 _token는 우리의 DNextToken이 배포된 계약 계정의 주소를 의미합니다. 즉, 이 계약을 배포하기 이전에 미리 DNextToken을 배포해야 주소를 얻을 수 있고, 그 주소를 이용해 DNextTokenSale 계약을 배포하는 것입니다. 마찬가지로 두번째 파라미터인 _whitelistDNextTokenWhitelist 계약 계정의 주소입니다.

두개의 파라미터의 자료형은 각각DNextTokenDNextTokenWhitelist로 명시했는데, 이를 위해서는 DNextTokenDNextTokenWhitelist를 import해줘야 합니다. 이는 소스 코드의 최상단에서 pragma solidity ^0.4.21;이 있는 줄과 contract DNextTokenSale 사이에 다음과 같은 줄을 삽입해줘야 합니다.

import './DNextToken.sol';
import './DNextTokenWhitelist.sol';

다음은 실제 판매가 이루어지는 함수입니다.

function() external payable {
require(!closed);
require(msg.value != 0);
require(whitelist.isRegistered(msg.sender));

uint emmasToTransfer = msg.value.mul(EMMAS_PER_WEI);
uint weisToRefund = 0;
if (emmasRaised + emmasToTransfer > HARD_CAP) {
emmasToTransfer = HARD_CAP - emmasRaised;
weisToRefund = msg.value - emmasToTransfer.div(EMMAS_PER_WEI);
closed = true;
}
emmasRaised = emmasRaised.add(emmasToTransfer);
if (weisToRefund > 0) {
msg.sender.transfer(weisToRefund);
}

token.transfer(msg.sender, emmasToTransfer);
}

우선 함수의 이름이 없다는 점에 주목합시다. 이렇게 이름이 없는 함수를 솔리디티에서는 fallback 함수라고 부릅니다. 어떤 계약이든 단 하나의 이름없는 fallback 함수를 가질 수 있고, 이 함수는 어떤 파라미터도 가질 수 없으며 어떤 값도 반환(return)할 수 없습니다. 그리고 이름이 나타내는 것처럼, 어떤 계약의 함수를 호출하려고 할 때 매칭되는 함수가 존재하지 않는다면 이 fallback 함수가 호출됩니다.

그리고 이 fallback 함수는 payable로 설정되어 있습니다. 즉, 해당 함수로의 거래를 생성할 때 이더의 값을 명시할 수 있다는 의미이고 그만큼의 이더가 계약 계정으로 전송이 됩니다. 이는 토큰 판매를 위한 함수이기에 필수적으로 설정되어야하는 사항입니다.

코드를 살펴보자면 우선 최상단에서 require(!closed)require(msg.value != 0)을 통해 각각 ‘토큰 판매가 종료되지 않았는지’와 ‘이더 금액을 명시했는지’를 확인합니다. 그리고 require(whitelist.isRegistered(msg.sender))을 통해 토큰 구매를 원하는 계정의 주소가 사전 등록 목록에 포함되어 있는지를 확인합니다. whitelistisRegistered() 함수는 우리가 지난 튜토리얼에서 작성한 함수입니다. 세가지 조건 중 하나라도 충족되지 않는다면 다음의 코드는 실행되지 않고 함수가 즉시 종료됩니다.

그 다음줄에서는 msg.value.mul(EMMAS_PER_WEI)을 통해 emmasToTransfer(판매의 댓가로 전송해줘야할 Emma의 수)를 계산합니다. 여기서 mul() 함수를 사용했는데, 이는 zeppelin-solidity에서 제공하는 SafeMath라는 라이브러리에서 제공하는 기능입니다. SafeMath에서는 안전하게 수학 연산을 할 수 있도록 mul(), div(), sub(), add() 네 가지의 함수를 제공해줍니다. 물론 솔리디티가 기본적으로 수학 연산을 위한 *, /, +, -을 제공하지만, 이를 사용하면 오버플로(overflow)¹가 일어나 해커의 공격 대상이 될 가능성이 있습니다. 따라서 우리는 이와같은 취약점을 사전에 방지하기 위해 SafeMath를 사용할 것이고, 이를 위해서는 아래와 같이 import를 해줘야 합니다.

import 'zeppelin-solidity/contracts/math/SafeMath.sol';

다음으로는 weisToRefund를 0으로 초기화해주었는데, 이 변수의 의미는 만약 최대 판매 수량(Hard cap)이 초과되었을 때 환불해야할 wei의 수를 나타냅니다.

바로 다음줄에 나오는 emmasRaised + emmasToTransfer > HARD_CAP를 통해 최대 판매 수량을 초과했는지 확인해서, 만약 그렇다면 emmasToTransferHARD_CAP - emmasRaised로 낮춰주고 환불 금액을 계산한 후 closed 상태 변수를 true로 만들어 줍니다. 최대 판매 수량에 도달하는 순간 판매는 종료되고 더이상 크라우드 세일에 참여할 수 없게 하기 위함입니다. 환불 금액에 해당하는 weisToRefund를 계산하는 식은 msg.value - emmasToTransfer.div(EMMAS_PER_WEI)와 같습니다. 즉, 거래를 통해 전송한 총 wei 개수에서, 실제 구입을 위해 소모된 wei(구입할 Emma 양을 EMMAS_PER_WEI 비율로 나눈 값)을 뺀 값입니다.

그 다음으로는 emmasRaised = emmasRaised.add(emmasToTransfer)을 통해 현재까지 판매된 전체 Emma 양을 업데이트해주고, weisToRefund > 0을 확인해서 만약 환불할 금액이 있다면, msg.sender.transfer(weisToRefund)을 통해 함수를 실행한 계정에게 해당하는 wei 만큼을 전송하게 됩니다.

마지막으로 어쩌면 이 함수에서 가장 중요하다고 할 수 있는 token.transfer(msg.sender, emmasToTransfer)을 통해 토큰 판매에 참여한 계정에게 실제로 Emma 양을 전송해줍니다. 여기서 쓰인 transfer() 함수는 ERC20의 표준으로 첫번째 파라미터에 해당하는 주소로 두번째 파라미터의 양만큼 토큰을 전송하는 역할을 합니다. (전송되는 토큰은 토큰 판매 계약 계정에서 인출되는 것으로써, 토큰 판매를 시작하기 전 우리의 계약 계정으로 판매를 위한 토큰을 전송해놓는 작업이 선행되어야 합니다.)

중요한 점은, 이 전송 코드는 마지막에 위치해야 한다는 점입니다. 그렇지 않은 경우 해커가 계약 계정을 이용해 크라우드 세일에 참여하여, 취약점을 이용해 토큰을 여러번 반복해서 탈취할 수 있습니다. (DAO 해킹이 유사한 방법으로 이루어졌고, 당시 가치 기준으로 750억원의 토큰이 도난당했습니다. 구체적으로 어떻게 해킹이 이루어졌는지에 대해서는 추후 포스트에서 다룰 예정입니다.)

이렇게 토큰 판매를 위한 계약 작성을 완료했습니다. 전체 소스 코드는 아래와 같습니다.

pragma solidity ^0.4.21;

import './DNextToken.sol';
import './DNextTokenWhitelist.sol';
import 'zeppelin-solidity/contracts/math/SafeMath.sol';

contract DNextTokenSale {
using SafeMath for uint256;
uint public constant EMMAS_PER_WEI = 10000000;
uint public constant HARD_CAP = 500000000000000;
DNextToken public token;
DNextTokenWhitelist public whitelist;
uint public emmasRaised;
bool private closed;

function DNextTokenSale(DNextToken _token, DNextTokenWhitelist _whitelist) public {
require(_token != address(0));
token = _token;
whitelist = _whitelist;
}

function() external payable {
require(!closed);
require(msg.value != 0);
require(whitelist.isRegistered(msg.sender));

uint emmasToTransfer = msg.value.mul(EMMAS_PER_WEI);
uint weisToRefund = 0;
if (emmasRaised + emmasToTransfer > HARD_CAP) {
emmasToTransfer = HARD_CAP - emmasRaised;
weisToRefund = msg.value - emmasToTransfer.div(EMMAS_PER_WEI);
closed = true;
}
emmasRaised = emmasRaised.add(emmasToTransfer);
if (weisToRefund > 0) {
msg.sender.transfer(weisToRefund);
}

token.transfer(msg.sender, emmasToTransfer);
}
}

이렇게 간단한 형태의 크라우드 세일을 위한 계약 작성에 대해 알아보았습니다.

다음 포스트에서는 작성한 코드를 배포하는 과정 및 기타 준비를 통해 실제 크라우드 세일이 어떻게 일어나는지 절차에 대해서 다뤄보려고 합니다.

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

또한 저희 DNext에서는 토큰 발행 및 판매와 관련한 전문 컨설팅 서비스를 제공하고 있습니다. 도움이 필요하신 분들은 support@dnext.co로 문의주시기 바랍니다.

  1. 오버플로: 수학 연산 과정에서 시스템이 다룰수 있는 범위보다 더 큰 값이 연산되어 부정확한 연산 결과를 반환하는 문제.

--

--