Zeppelin Solidity의 Contract에 대해 알아보자!(1)
주의! 이 포스팅은 최대한 비개발자의 시선에서 쉽게 설명하고자 작성되었지만 자바스크립트나 Solidity에 대해 어느정도 지식이 있다면 더욱 이해하기 수월합니다!
스마트컨트랙트에 대해 관심이 있고 Dapp을 개발해 보고 싶으셨던 분들이라면 모두 한번쯤 Zeppelin Solidity에 대해서 들어본 적이 있을 겁니다. Zeppelin Solidity는 Zeppelin Solution이라는 회사에서 제공하는 스마트컨트랙트 프레임워크입니다. Zeppelin은 스마트컨트랙트 감사를 수행하는 세계에서 몇 안되는 단체 중의 하나이고 블록체인 인프라의 구축과 보안 감사를 수행하고 있습니다. Omisego와 Aragon, Augur 같은 프로젝트와 함께 하고 있습니다.
현재 유통되고 있는 이더리움 기반의 많은 토큰들이 안전하게 검증받은 코드를 사용하여 구현이 되었는데요. 어느정도 코드에 관심이 있지만 정작 그들이 제공한 솔리디티 코드에 대해 설명을 해주는 게시물은 찾아보기가 힘들더군요, 그래서 이더리움에 관심이 있고 SmartContract 구현을 하는데 기본적인 이해를 필요로 하시는 분과 궁금증을 해소하고 싶으신 분들을 돕고자 본 게시물을 작성하게 되었습니다.
Dapp 개발자를 꿈꾸는 분들의 Voting은 케블리에게 커다란 도움과 활력을 제공합니다. 그러면 가장 핫한 ERC20에 포함되어 있는 토큰계약들을 차례차례 리뷰해보겠습니다!
1. BasicToken.sol
BasicToken은 기본적인 토큰의 기능을 담고 있는 계약입니다. 총 발행량과 전송, 잔고확인의 기능을 담고 있습니다. 이 계약을 상속하게 되면 위의 기능들을 토큰에 구현할 수 있습니다
pragma solidity ^0.4.24;
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]);
balances[msg.sender] = balances[msg.sender].sub(_value);
balances[_to] = balances[_to].add(_value);
emit 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) {
return balances[_owner];
}
}
상세한 코드리뷰를 시작하겠습니다.
pragma solidity ^0.4.24;
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_;
1행은 코드를 컴파일할 컴파일러의 버전을 명시합니다.
2행 ~ 11행은 SafeMath.sol
을 읽어오고, ERC20Basic.sol
을 상속합니다. 이렇게 함으로써 BasicToken
은 SafeMath
의 기능과 ERC20Basic
계약의 함수들을 사용할 수 있게 됩니다.
SafeMath.sol
과ERC20.sol
은 Solidity 파일을 의미합니다. 두 계약파일에 대한 코드리뷰와 자세한 기능설명은 차후 포스트에서 순차적으로 다뤄보겠습니다. 지금은 두 컨트랙트 파일을 읽어오고 상속한다는 것에 집중을 하시면 좋습니다!
여기서 inherit
상속이라는 단어가 처음 나왔는데요, 컨트랙트를 상속하는 이유는 한 계약에 모든 함수와 변수를 넣을 필요가 없기 때문입니다. 엄청 긴 하나의 컨트랙트를 짜는 것 보다 여러 컨트랙트에 코드를 나누는 것이 합리적이기 때문이죠. 상속은 한 계약안에 다른 컨트랙트 계약을 선언했을때 내부에서 다른 컨트랙트에 사용된 코드에 접근하기 위해 컨트랙트 상속을 이용합니다.
반면import
는 상속과 비슷하지만 컨트랙트를 컴파일 할 때 다른 폴더에 포함된 컨트랙트 파일을 불러오기 위해서 사용합니다. 12행에서 이 계약은uint256 SafeMath
를 사용합니다.
SafeMath
를 이용하게 되면 기본적으로 OverFlow 공격에 대한 저항을 갖게 됩니다.
13행은 address=>uint256
으로 balances
를 맵핑하여 (주소를 key
로 입력하면 256비트 정수로 value
를 반환) 배열로 저장해서 해당 주소와 잔액을 연관시킵니다. 맵핑은 다른 프로그램 언어의 map
이나 dictionary
같은 자료구조로써 키와 값을 지정하기 위해 사용합니다. 다만 Solidity
에서는 맵핑, 동적 크기배열, 컨트랙트, 구조체, enum을 key
값으로 사용할 수 없습니다. 반면 value
에는 모든 타입을 사용할 수 있습니다.
14행에서 totalSupply_
를 uint256
전역변수를 선언합니다. 이제 totalSupply_
는 이 계약 안에서 어디서든 사용이 가능한 변수가 되었습니다.
function totalSupply() public view returns (uint256) {
return totalSupply_;
}
20행의 totalSupply
함수는 public
으로 선언되었고 값을 uint256
으로 보여주기만 하는 역할을 합니다.
public
은 컨트랙트 함수에 지정할 수 있는 가시성을 나타내는데요, 앞으로 알아볼 다양한 컨트랙트에서 나오는 다른 가시성 문구들은 그때 그때 설명을 드리도록 하겠습니다.public
은 외부 컨트랙트에서도 호출할 수 있고, 계약계정에서도 호출이 가능합니다. 그리고 호출에 사용된 매개변수가 항상 메모리에 기억됩니다. 따라서 외부계정이 함수를 호출할 때 매개변수로 많은 데이터를 전송하면 가스가 많이 소모될 수 있습니다.
view
는 해당 함수가 읽기 전용이라고 컴파일러에 알려주는 기능을 합니다. 즉 읽기 전용이기 때문에 실행시에 가스를 소모하지 않습니다.
function transfer(address _to, uint256 _value) public returns (bool) {
require(_to != address(0));
require(_value <= balances[msg.sender]);
balances[msg.sender] = balances[msg.sender].sub(_value); balances[_to] = balances[_to].add(_value);
emit Transfer(msg.sender, _to, _value); return true;
}
30행의 transfer
함수는 _to
라는 이름의 주소와 _value
라는 이름의 uint256
을 인자로 받고 public
으로 선언되었으며 boolean
(true, false)을 반환합니다. (조건으로는 _to
의 주소가 address(0)
이 아니어야 하며, _value
가 함수호출을 하는 전송자(msg.sender
)의 잔고(balances
)보다 작거나 같아야 합니다.)
전송자의 잔고는 전송자(msg.sender
)의 잔고에서 SafeMath
의 sub
함수(-의 역할)를 이용하여 _value
만큼의 잔액을 감소 시킵니다.
이후 수신자의 잔고에서 SafeMath의 add
함수(+ 역할)를 이용하여 전송자(msg.sender
)가 보내는 _value
만큼의 잔액을 증가 시킵니다.
그리고 나서 emit
으로 상속받은 ERC20Basic
컨트랙트에 포함되어 있는 Transfer
이벤트를 호출하며 코드는 아래와 같습니다.
e.g. event Transfer(address indexed from, address indexed to, uint256 value);
Transfer
event는from
과to
라는 주소값과value
라는uint256
값을 가집니다.indexed
라는 키워드를 붙이게 되면 이벤트 함수에서 최대 3개의 매개변수를 색인화 하며indexed
로 수식된 각 인수가 데이터 대신 로그 주제로 처리되도록 합니다.from
은 전송자(msg.sender
),_to
는 수신자,_value
는 전송된 토큰의 액수를 의미합니다. 위의 과정을 거치면 true라는boolean
을 반환하고 함수를 종료합니다.
function balanceOf(address _owner) public view returns (uint256) {
return balances[_owner];
}
}
45행의 balanceOf
함수는 _owner
의 주소를 인수로 public
으로 선언되었으며 uint256
타입으로 값을 반환해서 보여주기만 합니다.
이 함수는 _owner
의 잔고를 반환하는 역할을 합니다.
- 이로써 기본적인 토큰기능을 가진 BasicToken 을 이용하면 발행량을 설정하고 토큰의 전송 및 잔고확인을 구현할 수 있게 됩니다.
2. BurnableToken.sol - 소각가능한 토큰
BurnableToken은 돌이킬수 없이 소각시키는 기능을 구현한 계약입니다. 마찬가지로 구현하려고 하는 토큰에 import
하거나 상속하여 이 계약의 기능을 사용할 수 있습니다.
pragma solidity ^0.4.24;
import "./BasicToken.sol";
/**
* @title Burnable Token
* @dev Token that can be irreversibly burned (destroyed).
*/
contract BurnableToken is BasicToken {
event Burn(address indexed burner, uint256 value);
/**
* @dev Burns a specific amount of tokens.
* @param _value The amount of token to be burned.
*/
function burn(uint256 _value) public {
_burn(msg.sender, _value);
}
function _burn(address _who, uint256 _value) internal {
require(_value <= balances[_who]);
/**
* no need to require value <= totalSupply, since that would imply
* the sender's balance is greater than the totalSupply,
* which *should* be an assertion failure
*/
balances[_who] = balances[_who].sub(_value);
totalSupply_ = totalSupply_.sub(_value);
emit Burn(_who, _value);
emit Transfer(_who, address(0), _value);
}
}
자세한 코드리뷰에 들어가겠습니다.
pragma solidity ^0.4.24;
import "./BasicToken.sol";
/** * @title Burnable Token
* @dev Token that can be irreversibly burned (destroyed).
*/
contract BurnableToken is BasicToken {
event Burn(address indexed burner, uint256 value);
3행 ~ 10행까지 BurnableToken
은 BasicToken
을 읽고 상속합니다.
11행에서 Burn
이라는 이벤트를 선언합니다. 이 이벤트는 Burner
라는 색인된(indexed
) 주소와 value
라는 uint256
타입 인자를 받습니다.
function burn(uint256 _value) public {
_burn(msg.sender, _value);
}
function _burn(address _who, uint256 _value) internal {
require(_value <= balances[_who]);
balances[_who] = balances[_who].sub(_value);
totalSupply_ = totalSupply_.sub(_value);
emit Burn(_who, _value);
emit Transfer(_who, address(0), _value);
}
}
18행은 burn
함수인데요, _value
라는 uint256
타입의 값을 가지고 public
으로 선언하고 burn
함수는 바로 아래 _burn
함수를 실행합니다. 그러면 22행의 _burn
함수에 대해 알아볼까요?
_burn
함수는 _who
라는 주소와 _value
라는 uint256
타입 인자를 받습니다. 이 함수는 internal
로 선언되어 함수의 내부와 BurnableToken을 상속받은 컨트랙트에서 사용될 수 있습니다. (조건으로는 _value
값이 즉, 소각하려는 토큰잔고가 _who
라는 주소의 잔고보다 작거나 같아야합니다.)
_who
(소각하려는 토큰보유자)의 잔고는 _who
에서 SafeMath
의 sub
함수(- 역할)를 이용해 _value
만큼 감소시킨 뒤, totalSupply_
에서도 _value
만큼 감소시킵니다.
그 후 Burn
이벤트를 emit
으로 호출해서 누구의 잔고를 얼마만큼 감소시켰는지 로그를 나타내고, BasicToken이 상속하는 ERC20Basic의 Transfer
이벤트를 emit
키워드를 이용해 호출하여 (_who
)누가 address(0)
계약계정의 잔고에 (_value
)얼마를 전송했는지 로그를 나타내줍니다.
- 계약계정에 전송된 토큰은 계약계정 자체에 반환함수가 존재하지 않는한 누구도 사용할 수 없게 되기때문에 우리가 생각하는 소각과 동일한 역할을 수행한다고 볼 수 있습니다.
3. CappedToken.sol -한도가 정해진 토큰계약
CappedToken은 캡이 정해진 MintableToken(발행가능한토큰) 입니다. 정해진 한도 내에서 발행이 가능한 토큰을 구현하고 싶으시다면 이 계약을 상속하거나 import
하면 됩니다.
pragma solidity ^0.4.24;
import "./MintableToken.sol";
/** * @title Capped token
* @dev Mintable token with a token cap.
* */
contract CappedToken is MintableToken { uint256 public cap;
constructor(uint256 _cap) public { require(_cap > 0);
cap = _cap;
}
/**
* @dev Function to mint tokens
* @param _to The address that will receive the minted tokens.
* @param _amount The amount of tokens to mint.
* @return A boolean that indicates if the operation was successful.
*/
function mint(address _to, uint256 _amount) public returns (bool) {
require(totalSupply_.add(_amount) <= cap);
return super.mint(_to, _amount);
}
}
CappedToken 리뷰 들어가겠습니다.
pragma solidity ^0.4.24;
import "./MintableToken.sol";
3행 ~ 10행을 보면 CappedToken은 MintableToken.sol을 읽어오고 상속합니다.
contract CappedToken is MintableToken {
uint256 public cap;
constructor(uint256 _cap) public {
require(_cap > 0);
cap = _cap;
}
11행에서는 cap
이라는 변수를 uint256
타입으로 public
하게 선언을 합니다.
12행부터 생성자 (constructor
)는 _cap
이라는 uint256
타입의 인자를 public
으로 선언합니다. (요구사항으로는 _cap
이 0보다 무조건 커야한다는 조건이 있고, cap
은 _cap
과 동일하다고 선언합니다.)
constructor
는 '생성자’라고 불리며 계약의 이름과 동일한 이름의 함수입니다. 생성자는 반환값을 가지지 않는 특수한 형태의 함수로써, 최초에 계약이 배포되는 시점에 한번만 수행되는 함수입니다. 여기서는cap
즉, 토큰의 한도를 초기화해주는 역할을 합니다.
function mint(address _to, uint256 _amount) public returns (bool) {
require(totalSupply_.add(_amount) <= cap);
return super.mint(_to, _amount);
}
}
24행의 mint
함수에서는 _to
라는 주소값과 _amount
라는 uint256
타입의 수량값을 인자로 bool
값을 반환하는 public함수입니다. 이 함수를 통해서 우리는 원하는 사람에게 토큰을 발행해줄 수 있습니다! (요구조건으로는 SafeMath의 add
(+역할)함수를 이용하여 totalSupply
에 _amount
수량을 더한 값이 cap
보다 작아야 합니다.) 그러고 나서 mint
함수를 반환합니다.
여기에서 super
라는 키워드가 처음 등장했는데요 .super
키워드의 역할은 다음과 같습니다.
- 현재 계약이 파생된 바로 그 부모 계약에 대한 액세스 권한을 부여합니다.
- 가령
contract B { function f() { u = 2} }
가 선언되고 contract A is B { function f() { u = 3; } }
일 때- A 에서
super.f();
를 호출할 시 A의 함수 f의 값인 3이 반환되지 않고 A가 상속하는 B의 f 함수를 불러오게됩니다.
따라서 CappedToken에 선언된 mint
함수의 super.mint(_to, _amount);
는 CappedToken이 상속하는 MinteableToken 계약의 mint
함수를 호출하여 _to
에게 _amount
만큼의 토큰을 발행해 줄 수 있게 됩니다.
- 이렇게 우리는 CappedToken 을 이용하여 한도치만큼 토큰발행을 가능하게 해주는 기능을 사용할 수 있게 되었습니다!
여기까지 ERC20에 포함되어있는 세가지 컨트랙트에 대해서 알아보았습니다. 다양한 기능을 가진 컨트랙트들을 조합하고 상속하면 내가 만들고 싶은 컨셉의 토큰을 입맛대로 구현해 볼 수 있습니다. 이 글을 읽어보시는 예비 Dapp 개발자분들도 자신의 토큰을 구현해보면서 재미있는 경험을 해보시길 바랍니다.
다음 포스트에서는
DetailedERC20과 ERC20, ERC20Basic에 대해서 리뷰해보겠습니다!
Reference
- https://github.com/OpenZeppelin/openzeppelin-solidity/tree/master/contracts/token/ERC20
- https://drive.google.com/file/d/1vPde8_ySsfkBEhQwfCcYSrZKdJZkT1Bi/view
- https://openzeppelin.org/
이동욱
예전부터 궁금했던 내용인데, 자세히 알려주셔서 감사합니다!