본 글은 이더리움 정식 튜토리얼을 기반으로 의역 및 수정한 내용입니다.
https://www.ethereum.org/token
이더리움에서 토큰은 기존의 상품권이나 포인트를 대체할 수 있습니다.
이더리움 월렛과 호환 가능한 토큰을 만들기 위해 이더리움에서 제공하는 ERC20 표준을 사용하여 토큰을 만들어 보았습니다.
복잡한 표준 토큰을 만들기 전에 토큰의 기본 골격을 살펴보면 아래와 같습니다.
토큰 기본 골격
contract MyToken {
/* This creates an array with all balances */
mapping (address => uint256) public balanceOf;
/* Initializes contract with initial supply tokens to the creator of the contract */
function MyToken(
uint256 initialSupply
) {
balanceOf[msg.sender] = initialSupply; // Give the creator all initial tokens
}
/* Send coins */
function transfer(address _to, uint256 _value) {
require(balanceOf[msg.sender] >= _value); // Check if the sender has enough
require(balanceOf[_to] + _value >= balanceOf[_to]); // Check for overflows
balanceOf[msg.sender] -= _value; // Subtract from the sender
balanceOf[_to] += _value; // Add the same to the recipient
}
}
MyToken
이라는 contract
클래스가 있고 하위에 balanceOf
라는 배열이 있습니다.
생성자에서 msg.sender
의 밸런스에 이니셜 토큰을 할당하고
transfer
라는 함수에서 보낼 주소 _to
와 보낼 금액 _value
를 입력받아
msg.sender
의 밸런스가 보낼 금액보다 큰 지 보낼 주소의 밸런스에 보낼 금액을 더했을때 오버플로우가 나지 않는지 확인한 후
메시지 발신자의 밸런스에서 보낼 금액을 차감하고 보낼 주소의 밸런스에 보낼 금액을 더합니다.
표준 토큰
표준 토큰을 위한 완전한 코드는 아래와 같습니다.
pragma solidity ^0.4.16;
interface tokenRecipient { function receiveApproval(address _from, uint256 _value, address _token, bytes _extraData) public; }
contract TokenERC20 {
// Public variables of the token
string public name;
string public symbol;
uint8 public decimals = 18;
// 18 decimals is the strongly suggested default, avoid changing it
uint256 public totalSupply;
// This creates an array with all balances
mapping (address => uint256) public balanceOf;
mapping (address => mapping (address => uint256)) public allowance;
// This generates a public event on the blockchain that will notify clients
event Transfer(address indexed from, address indexed to, uint256 value);
// This notifies clients about the amount burnt
event Burn(address indexed from, uint256 value);
/**
* Constructor function
*
* Initializes contract with initial supply tokens to the creator of the contract
*/
function TokenERC20(
uint256 initialSupply,
string tokenName,
string tokenSymbol
) public {
totalSupply = initialSupply * 10 ** uint256(decimals); // Update total supply with the decimal amount
balanceOf[msg.sender] = totalSupply; // Give the creator all initial tokens
name = tokenName; // Set the name for display purposes
symbol = tokenSymbol; // Set the symbol for display purposes
}
/**
* Internal transfer, only can be called by this contract
*/
function _transfer(address _from, address _to, uint _value) internal {
// Prevent transfer to 0x0 address. Use burn() instead
require(_to != 0x0);
// Check if the sender has enough
require(balanceOf[_from] >= _value);
// Check for overflows
require(balanceOf[_to] + _value > balanceOf[_to]);
// Save this for an assertion in the future
uint previousBalances = balanceOf[_from] + balanceOf[_to];
// Subtract from the sender
balanceOf[_from] -= _value;
// Add the same to the recipient
balanceOf[_to] += _value;
Transfer(_from, _to, _value);
// Asserts are used to use static analysis to find bugs in your code. They should never fail
assert(balanceOf[_from] + balanceOf[_to] == previousBalances);
}
/**
* Transfer tokens
*
* Send `_value` tokens to `_to` from your account
*
* @param _to The address of the recipient
* @param _value the amount to send
*/
function transfer(address _to, uint256 _value) public {
_transfer(msg.sender, _to, _value);
}
/**
* Transfer tokens from other address
*
* Send `_value` tokens to `_to` on behalf of `_from`
*
* @param _from The address of the sender
* @param _to The address of the recipient
* @param _value the amount to send
*/
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) {
require(_value <= allowance[_from][msg.sender]); // Check allowance
allowance[_from][msg.sender] -= _value;
_transfer(_from, _to, _value);
return true;
}
/**
* Set allowance for other address
*
* Allows `_spender` to spend no more than `_value` tokens on your behalf
*
* @param _spender The address authorized to spend
* @param _value the max amount they can spend
*/
function approve(address _spender, uint256 _value) public
returns (bool success) {
allowance[msg.sender][_spender] = _value;
return true;
}
/**
* Set allowance for other address and notify
*
* Allows `_spender` to spend no more than `_value` tokens on your behalf, and then ping the contract about it
*
* @param _spender The address authorized to spend
* @param _value the max amount they can spend
* @param _extraData some extra information to send to the approved contract
*/
function approveAndCall(address _spender, uint256 _value, bytes _extraData)
public
returns (bool success) {
tokenRecipient spender = tokenRecipient(_spender);
if (approve(_spender, _value)) {
spender.receiveApproval(msg.sender, _value, this, _extraData);
return true;
}
}
/**
* Destroy tokens
*
* Remove `_value` tokens from the system irreversibly
*
* @param _value the amount of money to burn
*/
function burn(uint256 _value) public returns (bool success) {
require(balanceOf[msg.sender] >= _value); // Check if the sender has enough
balanceOf[msg.sender] -= _value; // Subtract from the sender
totalSupply -= _value; // Updates totalSupply
Burn(msg.sender, _value);
return true;
}
/**
* Destroy tokens from other account
*
* Remove `_value` tokens from the system irreversibly on behalf of `_from`.
*
* @param _from the address of the sender
* @param _value the amount of money to burn
*/
function burnFrom(address _from, uint256 _value) public returns (bool success) {
require(balanceOf[_from] >= _value); // Check if the targeted balance is enough
require(_value <= allowance[_from][msg.sender]); // Check allowance
balanceOf[_from] -= _value; // Subtract from the targeted balance
allowance[_from][msg.sender] -= _value; // Subtract from the sender's allowance
totalSupply -= _value; // Update totalSupply
Burn(_from, _value);
return true;
}
}
코드 이해하기
새로운 컨트랙트 배포하기
이더리움 월렛 앱을 열고 컨트랙트
탭으로 가서 신규 컨트랙트 설치
를 누릅니다.
솔리더티 컨트랙트 소스 코드
라는 텍스트 입력창에 아래와 같이 코드를 입력합니다.
contract MyToken {
/* This creates an array with all balances */
mapping (address => uint256) public balanceOf;
}
mapping
은 연관 배열이라는 자료구조의 하나로, 위 코드에서는 주소들과 밸런스들이 연관되어 있으며
주소라는 키를 통해 밸런스라는 값을 얻을 수 있습니다.
주소들은 기본적인 16진수 이더리움 포맷으로 이루어져 있고 밸런스는 0에서 115*(10의 75승) 범위의 정수를 가질 수 있습니다.
public
이라는 키워드는 balanceOf
라는 배열이 블록체인 상에 있는 누구에게나 접근 가능하다는 것을 의미합니다.
새로운 컨트랙트 수정하기
위의 컨트랙트 코드로 특정 주소의 밸런스를 조회하는 것이 가능해졌지만
아직 코인이 발행되지 않아서 모든 밸런스가 0 으로 보입니다.
따라서 이제 2100만개의 토큰을 발행하기 위해 mapping
라인 밑에 아래 코드를 추가해 보겠습니다.
function MyToken() {
balanceOf[msg.sender] = 21000000;
}
MyToken
이라는 함수는 컨트랙트가 네트워크에 업로드 될 때 한번만 실행되는 생성자로써
반드시 컨트랙트 이름과 일치시켜야 합니다.
이 함수는 컨트랙트를 배포하는 msg.sender
의 밸런스를 2100만으로 설정하게 됩니다.
2100만이라는 숫자는 컨트랙트 코드 상에서 얼마든지 조정할 수 있지만
아래의 initialSupply
와 같이 함수의 파라미터로 넘기는 것이 더 바람직합니다.
function MyToken(uint256 initialSupply) public {
balanceOf[msg.sender] = initialSupply;
}
컨트랙트 소스 코드 입력 부분 옆에 보면 설치할 컨트랙트를 선택하세요
라는 드롭다운 리스트가 있습니다.
여기에서 "MyToken" 컨트랙트를 선택하면 밑에 컨스트럭터 입력값들
이라는 섹션을 볼 수 있습니다.
해당 입력값들을 이용하여 발행하려는 토큰에 맞게 마음대로 조정할 수 있어
나중에 새로운 토큰을 만들때 동일한 코드를 재사용할 수 있습니다.
새로운 컨트랙트 편집하기
위 코드로 토큰의 밸런스를 만드는 컨트랙트를 완성했지만 아직 송금하는 기능은 없습니다.
따라서 이번에는 아래 코드를 컨트랙트의 마지막에 넣어 송금 기능을 추가해 보겠습니다.
/* Send coins */
function transfer(address _to, uint256 _value) {
/* Add and subtract new balances */
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
}
transfer
는 매우 단순한 함수로 보낼 주소 _to
와 보낼 금액 _value
를 입력받아
메시지 발신자의 밸런스에서 보낼 금액을 차감하고 보낼 주소의 밸런스에 보낼 금액을 더합니다.
이런 방식으로는 보내는 사람이 본인이 가지고 있는 금액보다 많이 보내려고 한다거나
너무 큰 금액을 보내려는 경우 밸런스가 오버플로우 날 수 있는 문제가 있습니다.
따라서 문제가 발생하는 경우 컨트랙드의 실행을 중간에 멈추기 위해 return
이나 throw
를 사용할 수 있는데
return
의 경우 가스를 적게 사용하지만 컨트랙트가 실행되며 변경된 내용들이 유지가 되어 골치가 아플 수 있습니다.
반면에 throw
는 컨트랙트 실행에 따라 변경되었던 모든 내용들을 취소할 수 있지만 메시지 발신자가 gas
를 위해 썼던 모든 Ether 를 잃을 수도 있습니다.
하지만 이더리움 월렛에서 alert
를 통해 컨트랙트가 throw
될 것을 감지할 수 있으므로 Ether 가 소모되는 것을 방지할 수 있습니다.
function transfer(address _to, uint256 _value) {
/* Check if sender has balance and for overflows */
require(balanceOf[msg.sender] >= _value && balanceOf[_to] + _value >= balanceOf[_to]);
/* Add and subtract new balances */
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
}
이제 해야 할 일은 컨트랙트에 대한 기본적인 정보를 입력하는 것입니다.
향후 해당 정보들은 토큰 레지스트리에 의해 별도로 관리될 예정이지만 현재로써는 아래와 같이 컨트랙트에 직접 입력해야 합니다.
string public name;
string public symbol;
uint8 public decimals;
위와 같이 선언한 변수들에 값을 설정하기 위해 컨스트럭터 함수를 업데이트합니다.
/* Initializes contract with initial supply tokens to the creator of the contract */
function MyToken(uint256 initialSupply, string tokenName, string tokenSymbol, uint8 decimalUnits) {
balanceOf[msg.sender] = initialSupply; // Give the creator all initial tokens
name = tokenName; // Set the name for display purposes
symbol = tokenSymbol; // Set the symbol for display purposes
decimals = decimalUnits; // Amount of decimals for display purposes
}
이제 이벤트라는 것을 사용할 때가 됐습니다.
이벤트는 대문자로 시작하며 이더리움 월렛과 같은 클라이언트가 컨트랙트에서 벌어지는 액티비티들을 지속적으로 트래킹하기 위해 사용됩니다.
이벤트는 선언하기 위해 아래 코드를 컨트랙트의 시작 부분에 추가합니다.
event Transfer(address indexed from, address indexed to, uint256 value);
그리고 아래 두 라인을 "transfer" 함수에 추가합니다.
/* Notify anyone listening that this transfer took place */
Transfer(msg.sender, _to, _value);
이제 토큰 발행을 위한 컨트랙트 준비는 끝났습니다!
컨트랙트 설치
이더리움 지갑을 열고 컨트랙트
탭으로 가서 새로운 컨트랙트 설치
를 클릭합니다.
지금까지 작성한 컨트랙트 코드를 솔리더티 컨트랙트 소스 코드
에 붙여넣습니다.
코드를 컴파일하는데 문제가 없으면 오른쪽 드롭다운 리스트에 컨트랙트 선택
이라고 표시되는데 여기서 "MyToken" 컨트랙트를 선택합니다.
오른쪽 컬럼에 있는 파라미터들을 이용하여 각자 목적에 맞게 토큰을 커스터마이징할 수 있습니다.
여기서는 아래와 같이 설정해보았습니다.
Initial supply
: 10,000
Token name
: ecoin
Token symbol
: ECO
페이지 하단 끝까지 스크롤하면 설치하려는 컨트랙트의 계산 비용을 예측한 값을 볼 수 있습니다.
해당 비용은 사용자가 임의로 조절할 수 있지만 실제 컨트랙트에서 사용되지 않은 Ether 는 돌려받게 되므로 기본값으로 설정합니다.
설치
버튼을 누르고 계정 암호를 입력하면 30초 내에 블럭체인에 등록되고 프론트 페이지로 이동합니다.
프론트 페이지에서는 트랜잭션이 확인되기를 기다리고 있다는 메시지를 볼 수 있습니다.
메인 어카운트인 Etherbase
계정을 클릭하고 몇 분이 지나면 방금 설치한 컨트랙트에 의해 토큰의 100%가 자신의 계정에 들어온 것을 볼 수 있습니다.
해당 토큰을 친구들에게 보내려면 상단의 보내기
탭에서 새로 생성한 토큰을 선택하고 수신처
에 친구의 주소를 입력한 후 하단에 있는 보내기
버튼을 클릭합니다.
하지만 이렇게 보내더라도 친구의 지갑에서 토큰을 바로 볼 수는 없습니다.
왜냐하면 이더리움 지갑은 현재 지갑이 알고있는 토큰만 트래킹하기 때문입니다.
따라서 친구는 자신의 지갑에 수동으로 컨트랙트를 추가해야 합니다.
친구에게 컨트랙트 주소를 알려주기 위해 이더리움 지갑의 컨트랙트
탭에 가서 생성한 컨트랙트에 들어간 후 주소 복사
를 클릭합니다.
해당 주소를 메모장에 붙여넣기하여 친구에게 알려주어 친구가 직접 자신의 이더리움 지갑에 컨트랙트 주소를 입력할 수 있도록 합니다.
친구는 컨트랙트 페이지에 가서 토큰 추가
를 클릭한 후 팝업창이 뜨면 컨트랙트 주소를 입력하면 됩니다.
토큰 이름
이나 토큰 심볼
, 최소단위 십진 자리수
등은 자동으로 채워지지만 혹시 안된다면 수동으로 입력할 수도 있습니다.
해당 내용들은 현재 지갑에서 해당 토큰을 표시하는 데에만 영향을 미칩니다.
이렇게 하여 이더리움 지갑에서 밸런스를 보거나 다른 사람에게 보낼 수 있는 암호 토큰을 완성하였습니다.
토큰 개선하기
표준 코드를 전혀 수정하지 않고도 암호 토큰을 발행할 수 있지만 화폐 본연의 가치를 갖게 하기 위해 좀 더 토큰을 커스터마이징 해보겠습니다.
기본적인 기능
현재 만들어 놓은 토큰이 다른 컨트랙트와 인터랙션하기 위해서는 approve
나 sendFrom
과 같은 기능을 추가해야 합니다.
분산화된 교환 방식으로 토큰을 판매하기 위해서는 단순히 주소에 토큰을 보내는 방식으로는 충분하지 않습니다.
컨트랙트가 이벤트들이나 function calls 에 가입할 수 없기 때문에 새로운 토큰이나 토큰을 보낸 사람을 알 수 없습니다.
따라서 컨트랙트에서 우선 계정에서 나갈 토큰의 양을 승인해야 하고 토큰들이 스스로할 일을 알게하기 위해 approveAndCall
을 해야합니다.
이러한 기능들은 대부분 토큰 전송 부분을 다시 구현해야하기 때문에 컨트랙트 자신만 호출할 수 있는 내부 함수로 만드는 것이 좋습니다.
/* Internal transfer, can only be called by this contract */
function _transfer(address _from, address _to, uint _value) internal {
require (_to != 0x0); // Prevent transfer to 0x0 address. Use burn() instead
require (balanceOf[_from] >= _value); // Check if the sender has enough
require (balanceOf[_to] + _value > balanceOf[_to]); // Check for overflows
require(!frozenAccount[_from]); // Check if sender is frozen
require(!frozenAccount[_to]); // Check if recipient is frozen
balanceOf[_from] -= _value; // Subtract from the sender
balanceOf[_to] += _value; // Add the same to the recipient
Transfer(_from, _to, _value);
}
_transfer
함수는 내부 함수이기 때문에 함수를 호출한 사람이 _from
의 밸런스를 바꿀 권한을 가지고 있는지 확인하지 않습니다.
따라서 컨트랙트에서 _transfer
함수를 호출하기 전에는 반드시 권한을 검증하는 코드를 넣어줘야 합니다.
중앙 관리자
기본적으로 모든 분산형 앱들은 완전히 분산되어 있지만, 반드시 중앙 관리자가 없는 것은 아닙니다.
더 많은 코인을 발행하기 위해서 조폐 기능이 필요하거나 특정 사람들이 토큰을 사용하지 못하도록 추방시키고 싶은 경우 중앙 관리자를 둘 수 있습니다.
다만, 모든 토큰 소유자들이 이러한 정책을 미리 인지할 수 있도록 해당 기능들은 컨트랙트를 설치하기 전에만 추가할 수 있습니다.
아무튼 이런 기능을 위해 중앙 관리자가 필요한 경우, 해당 관리자는 특정 계정이나 또 다른 컨트랙트가 될 수 있습니다.
컨트랙트를 더 잘 사용하기 위해서 inheritance
와 같은 기능을 사용할 수도 있습니다.
inheritance
는 별도의 재정의 없이 부모 컨트랙트의 속성을 물려받아 코드를 더 깔끔하고 쉽게 재사용할 수 있게 해줍니다.
아래의 코드를 contract MyToken {
전에 추가합니다.
contract owned {
address public owner;
function owned() {
owner = msg.sender;
}
modifier onlyOwner {
require(msg.sender == owner);
_;
}
function transferOwnership(address newOwner) onlyOwner {
owner = newOwner;
}
}
이 코드는 컨트랙트의 소유권에 대한 일반적인 기능들을 정의합니다.
이제 아래와 같이 기존 컨트랙트가 owned
컨트랙트를 상속받고 있다는 것을 표시해 줍니다.
contract MyToken is owned {
/* the rest of the contract as usual */
이렇게 하면 MyToken
에 있는 모든 함수에서 owner
변수와 onlyOwner
모디파이어을 사용할 수 있습니다.
또한 컨트랙트는 소유권을 이전하기 위한 기능도 갖게됩니다.
컨트랙트 시작과 동시에 소유자를 설정하기 위해 아래 코드를 생성자에 추가합니다.
function MyToken(
uint256 initialSupply,
string tokenName,
uint8 decimalUnits,
string tokenSymbol,
address centralMinter
) {
if(centralMinter != 0 ) owner = centralMinter;
}
중앙 화폐 발행 기관
중앙 화폐 발행 기관을 두어 토큰을 발행하거나 소각하여 토큰의 유통량을 조절하고 가격을 조절할 수 있습니다.
이를 위해 우선 totalSupply
변수를 추가하고 컨스트럭터 함수에서 할당해줍니다.
contract MyToken {
uint256 public totalSupply;
function MyToken(...) {
totalSupply = initialSupply;
...
}
...
}
이제 owner
가 토큰을 생성할 수 있도록 새로운 함수를 추가해줍니다.
function mintToken(address target, uint256 mintedAmount) onlyOwner {
balanceOf[target] += mintedAmount;
totalSupply += mintedAmount;
Transfer(0, owner, mintedAmount);
Transfer(owner, target, mintedAmount);
}
위 코드에서 함수명 뒤에 onlyOwener
라는 모디파이어가 있는 것에 주목해야 합니다.
이건 이 함수가 이전에 정의한 onlyOwner
로부터 코드를 상속받기 위해서 컴파일 시점에서 다시 작성될 거라는 것을 의미합니다.
이 함수의 코드는 모디파이어 함수의 언더라인이 있는 곳에 추가될 것이고 이건 해당 함수가 owner
로 설정된 계정에 의해서만 호출 가능하다는 뜻입니다.
이렇게 하여 코인을 더 발행하기 위한 기능이 컨트랙트에 추가되었습니다.
자산 동결
특정 주소에 들어있는 자산을 옮기지 못하게 해야 할 경우 아래 코드를 추가하여 자산 동결을 시킬 수 있습니다.
해당 코드는 컨트랙트의 아무 곳에나 추가해도 문제는 없지만 코드 가독성을 위해 mapping
과 event
다른 매핑 및 이벤트들과 함께 놓는 것이 좋습니다.
mapping (address => bool) public frozenAccount;
event FrozenFunds(address target, bool frozen);
function freezeAccount(address target, bool freeze) onlyOwner {
frozenAccount[target] = freeze;
FrozenFunds(target, freeze);
}
모든 계정들은 기본적으로 unfrozen
상태이지만 owener
는 freezeAccount 함수를 호출하여 상태를
freeze` 로 바꿀 수 있습니다.
위 코드만으로는 아직 transfer
함수 부분이 고쳐지지 않아서 자산 동결이 실제로 효과를 발휘하지 못합니다.
따라서 transfer
함수에 아래 코드를 추가해 줍니다.
function transfer(address _to, uint256 _value) {
require(!frozenAccount[msg.sender]);
이제 자산 동결된 계정은 자산은 해당 계정에 남아있지만 옮길 수는 없게 되었습니다.
혹시 이렇게 일일이 자산을 동결시키는 대신에 화이트리스트 제도처럼 모든 계정을 기본적으로는 동결시켜 놓고 허가된 계정만 자산을 옮길 수 있게 하려면 frozenAccount
함수 이름을 approvedAccount
로 바꾸고 transfer
함수에 아래 코드를 추가합니다.
require(approvedAccount[msg.sender]);
자동 판매 및 구매
지금까지는 토큰에 가치를 부여하기 위해 은행이나 다른 믿을만한 기관에 의존해왔지만 앞으로는 토큰을 시장 가격에 자동으로 구매 및 판매함으로써 토큰의 가치를 Ether 나 다른 토큰들에 의존할 수 있습니다.
이를 위해 일단 구매 및 판매 단가를 설정합니다.
uint256 public sellPrice;
uint256 public buyPrice;
function setPrices(uint256 newSellPrice, uint256 newBuyPrice) onlyOwner {
sellPrice = newSellPrice;
buyPrice = newBuyPrice;
}
이런 방식은 가격이 변할때마다 트랜잭션을 실행하기 위한 Ether 가 소모되므로 가격이 자주 변하지 않는 경우에만 유효합니다.
만약 고정된 가격을 원한다면 아래와 같이 이더리움이 제공하는 표준 데이터 입력 방식을 권장합니다.
https://github.com/ethereum/wiki/wiki/Standardized_Contract_APIs#data-feeds
다음 단계로 구매와 판매 함수를 구현합니다.
function buy() payable returns (uint amount){
amount = msg.value / buyPrice; // calculates the amount
require(balanceOf[this] >= amount); // checks if it has enough to sell
balanceOf[msg.sender] += amount; // adds the amount to buyer's balance
balanceOf[this] -= amount; // subtracts amount from seller's balance
Transfer(this, msg.sender, amount); // execute an event reflecting the change
return amount; // ends function and returns
}
function sell(uint amount) returns (uint revenue){
require(balanceOf[msg.sender] >= amount); // checks if the sender has enough to sell
balanceOf[this] += amount; // adds the amount to owner's balance
balanceOf[msg.sender] -= amount; // subtracts the amount from seller's balance
revenue = amount * sellPrice;
msg.sender.transfer(revenue); // sends ether to the seller: it's important to do this last to prevent recursion attacks
Transfer(msg.sender, this, amount); // executes an event reflecting on the change
return revenue; // ends function and returns
}
이 함수로는 새로운 토큰을 만들지 않고 컨트랙트 소유자의 밸런스만 변경합니다.
컨트랙트 소유자는 자체 토큰과 Ether 를 둘 다 보유하면서 가격을 설정하거나 새로운 토큰을 발행할 수는 있지만 Ether 나 다른 토큰을 건드릴 수는 없습니다.
컨트랙트가 펀드를 옮길 수 있는 유일한 방법은 자체 토큰을 팔거나 Ether 또는 다른 토큰들을 사는 것입니다.
주의할 점은 구매 및 판매 가격이 Ether 가 아니라 시스템의 최소 단위인 wei 로 설정됩니다.
유로나 달러에서 센트나 비트코인에서 사토시의 개념으로 생각하면 됩니다.
1 Ether 는 1000000000000000000 wei (10의 18승) 입니다.
따라서 토큰의 가격을 1 Ether 로 설정하고 싶으면 0을 18개 넣어줘야 합니다.
컨트랙트를 생성할 때는 모든 토큰을 다시 사들일 수 있을 정도의 충분한 Ether를 보내는 것이 좋습니다.
그렇지 않으면 컨트랙트는 지급 불능 상태가 되고 토큰 사용자들은 토큰을 팔 수 없게 됩니다.
지금까지 예제에서는 중앙에 하나의 구매자 및 판매자만 있는 컨트랙트였지만,
향후 누구든지 각기 다른 가격을 입찰하거나 외부 소스로부터 직접 가격을 불러올 수 있게 만들면 훨씬 재미있는 컨트랙트가 될 수 있습니다.
자동 리필
이더리움에서는 스마트 컨트랙트를 수행하기 위해 트랜잭션을 발생시킬때마다 채굴자에게 수수료를 지불해야 합니다.
수수료 지불 수단은 향후에는 바뀔 수도 있지만 현재로써는 Ether 로만 가능하기 때문에 모든 토큰 사용자들은 Ether 가 필요합니다.
따라서 계정의 밸런스가 수수료보다 적은 경우 소유자가 필요한 수수료를 지불할 때까지 막혀있게 됩니다.
토큰 사용자들이 이더리움 수수료에 대해 고민하지 않게 하기 위해서는 사용자의 밸런스가 부족해질 때마다 코인이 자동으로 리필되게 만들 수 있습니다.
이를 위해 우선 밸런스 최소값에 대한 변수와 이를 변경하기 위한 함수를 생성해야 합니다.
최소값을 얼마로 정해야 할지 모르겠으면 그냥 5 finney (0.005 Ether) 로 설정합니다.
uint minBalanceForAccounts;
function setMinBalance(uint minimumBalanceInFinney) onlyOwner {
minBalanceForAccounts = minimumBalanceInFinney * 1 finney;
}
Then, add this line to the transfer function so that the sender is refunded:
/* Send coins */
function transfer(address _to, uint256 _value) {
...
if(msg.sender.balance < minBalanceForAccounts)
sell((minBalanceForAccounts - msg.sender.balance) / sellPrice);
}
You can also instead change it so that the fee is paid forward to the receiver by the sender:
/* Send coins */
function transfer(address _to, uint256 _value) {
...
if(_to.balance<minBalanceForAccounts)
_to.send(sell((minBalanceForAccounts - _to.balance) / sellPrice));
}
작업 증명 (PROOF OF WORK)
수학 공식에 의해 코인 공급을 조절하는 방법이 있습니다.
가장 간단한 방법 중에 하나는 이더리움에서 블록을 찾는 채굴자가 블록 내에 있는 리워드 함수를 호출하여 코인을 리워드로 받게하는 "merged mining" 이 있습니다.
해당 함수가 호출되면 블록을 찾은 채굴자를 참조하는 coinbase
라는 특수 키워드를 사용하여 리워드를 지급하게 됩니다.
function giveBlockReward() {
balanceOf[block.coinbase] += 1;
}
수학 문제를 푼 사람에게 리워드를 지급하는 방법도 가능합니다.
아래 예제는 현재 숫자인 1의 세제곱근 (cubic root) 을 맞추는 사람에게 포인트를 주고 다음 숫자를 선택할 권한을 부여합니다.
uint currentChallenge = 1; // Can you figure out the cubic root of this number?
function rewardMathGeniuses(uint answerToCurrentReward, uint nextChallenge) {
require(answerToCurrentReward**3 == currentChallenge); // If answer is wrong do not continue
balanceOf[msg.sender] += 1; // Reward the player
currentChallenge = nextChallenge; // Set the next challenge
}
물론 이 정도 수준의 문제는 사람에겐 어려울 수도 있지만 계산기만 있으면 누구든 쉽게 풀 수 있어서 컴퓨터에 의해 쉽게 풀릴 수 있습니다.
또한 문제를 푼 사람이 다음 문제를 선택할 수 있어서 자기가 답을 아는 문제만 고를 수도 있어 공정한 게임이 되기 어렵습니다.
사람에게는 쉽지만 컴퓨터에게는 어려운 문제도 있지만 보통 이런 건 코드로 만들기가 어렵습니다.
공정한 시스템이 되기 위해서는 컴퓨터가 풀기는 굉장히 어려우면서 컴퓨터로 검증하기에는 그렇게 어렵지 않은 것이어야 합니다.
가장 적합한 문제는 채굴자들이 여러 숫자들을 해싱해서 특정 숫자보다 작은 숫자를 찾는 해쉬 함수입니다.
이 방법은 1997년에 Adam Back 이라는 사람에 의해 Hashcash 라는 이름으로 처음 제안되었고
2008년에는 사토시 나카모토에 의해 비트코인에서 작업 증명을 위한 방법으로 구현되었습니다.
이더리움 역시 현재는 이러한 작업 증명을 보안 모델로 사용하고 있지만 향후 작업 증명과 지분 증명을 병행하는 캐스퍼 (Casper) 로 변경될 예정입니다.
하지만 해싱을 코인 발행 도구로만 사용한다면 여전히 작업 증명 방식의 이더리움을 기반으로 화폐를 생성할 수 있습니다.
bytes32 public currentChallenge; // The coin starts with a challenge
uint public timeOfLastProof; // Variable to keep track of when rewards were given
uint public difficulty = 10**32; // Difficulty starts reasonably low
function proofOfWork(uint nonce){
bytes8 n = bytes8(sha3(nonce, currentChallenge)); // Generate a random hash based on input
require(n >= bytes8(difficulty)); // Check if it's under the difficulty
uint timeSinceLastProof = (now - timeOfLastProof); // Calculate time since last reward was given
require(timeSinceLastProof >= 5 seconds); // Rewards cannot be given too quickly
balanceOf[msg.sender] += timeSinceLastProof / 60 seconds; // The reward to the winner grows by the minute
difficulty = difficulty * 10 minutes / timeSinceLastProof + 1; // Adjusts the difficulty
timeOfLastProof = now; // Reset the counter
currentChallenge = sha3(nonce, currentChallenge, block.blockhash(block.number - 1)); // Save a hash that will be used as the next proof
}
컨트랙트가 처음 실행될 때 timeOfLastProof
가 0 이어서 난이도가 너무 어려워지지 않도록 아래 코드를 컨트트럭터에 추가합니다.
timeOfLastProof = now;
이제 컨트랙트가 실행되면 Proof of work
를 선택하고 nonce
필드에 임의의 숫자를 입력한 후 실행해봅니다.
만약 컨펌 윈도우에서 Data can't be execute
라는 경고 문구가 뜨면 다른 숫자를 넣어서 트랜잭션이 허용될 때까지 시도해봅니다.
조건에 맞는 숫자를 찾는 경우 마지막 리워드 시각부터 매 분마다 토큰 1개씩 받게 되고 난이도는 10분마다 한번씩 리워드받도록 조정됩니다.
이렇게 조건에 맞는 숫자를 찾아서 리워드를 받는 과정을 채굴이라고 부릅니다.
난이도가 증가할 수록 숫자를 찾기는 점점 어려워지지만 찾은 숫자가 조건에 맞는지 검증하는 건 항상 쉽습니다.
업그레이드된 코인
지금까지 설명한 모든 옵션을 추가하려는 경우, 최종 코드는 아래와 같습니다.
pragma solidity ^0.4.16;
contract owned {
address public owner;
function owned() public {
owner = msg.sender;
}
modifier onlyOwner {
require(msg.sender == owner);
_;
}
function transferOwnership(address newOwner) onlyOwner public {
owner = newOwner;
}
}
interface tokenRecipient { function receiveApproval(address _from, uint256 _value, address _token, bytes _extraData) public; }
contract TokenERC20 {
// Public variables of the token
string public name;
string public symbol;
uint8 public decimals = 18;
// 18 decimals is the strongly suggested default, avoid changing it
uint256 public totalSupply;
// This creates an array with all balances
mapping (address => uint256) public balanceOf;
mapping (address => mapping (address => uint256)) public allowance;
// This generates a public event on the blockchain that will notify clients
event Transfer(address indexed from, address indexed to, uint256 value);
// This notifies clients about the amount burnt
event Burn(address indexed from, uint256 value);
/**
* Constrctor function
*
* Initializes contract with initial supply tokens to the creator of the contract
*/
function TokenERC20(
uint256 initialSupply,
string tokenName,
string tokenSymbol
) public {
totalSupply = initialSupply * 10 ** uint256(decimals); // Update total supply with the decimal amount
balanceOf[msg.sender] = totalSupply; // Give the creator all initial tokens
name = tokenName; // Set the name for display purposes
symbol = tokenSymbol; // Set the symbol for display purposes
}
/**
* Internal transfer, only can be called by this contract
*/
function _transfer(address _from, address _to, uint _value) internal {
// Prevent transfer to 0x0 address. Use burn() instead
require(_to != 0x0);
// Check if the sender has enough
require(balanceOf[_from] >= _value);
// Check for overflows
require(balanceOf[_to] + _value > balanceOf[_to]);
// Save this for an assertion in the future
uint previousBalances = balanceOf[_from] + balanceOf[_to];
// Subtract from the sender
balanceOf[_from] -= _value;
// Add the same to the recipient
balanceOf[_to] += _value;
Transfer(_from, _to, _value);
// Asserts are used to use static analysis to find bugs in your code. They should never fail
assert(balanceOf[_from] + balanceOf[_to] == previousBalances);
}
/**
* Transfer tokens
*
* Send `_value` tokens to `_to` from your account
*
* @param _to The address of the recipient
* @param _value the amount to send
*/
function transfer(address _to, uint256 _value) public {
_transfer(msg.sender, _to, _value);
}
/**
* Transfer tokens from other address
*
* Send `_value` tokens to `_to` in behalf of `_from`
*
* @param _from The address of the sender
* @param _to The address of the recipient
* @param _value the amount to send
*/
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) {
require(_value <= allowance[_from][msg.sender]); // Check allowance
allowance[_from][msg.sender] -= _value;
_transfer(_from, _to, _value);
return true;
}
/**
* Set allowance for other address
*
* Allows `_spender` to spend no more than `_value` tokens in your behalf
*
* @param _spender The address authorized to spend
* @param _value the max amount they can spend
*/
function approve(address _spender, uint256 _value) public
returns (bool success) {
allowance[msg.sender][_spender] = _value;
return true;
}
/**
* Set allowance for other address and notify
*
* Allows `_spender` to spend no more than `_value` tokens in your behalf, and then ping the contract about it
*
* @param _spender The address authorized to spend
* @param _value the max amount they can spend
* @param _extraData some extra information to send to the approved contract
*/
function approveAndCall(address _spender, uint256 _value, bytes _extraData)
public
returns (bool success) {
tokenRecipient spender = tokenRecipient(_spender);
if (approve(_spender, _value)) {
spender.receiveApproval(msg.sender, _value, this, _extraData);
return true;
}
}
/**
* Destroy tokens
*
* Remove `_value` tokens from the system irreversibly
*
* @param _value the amount of money to burn
*/
function burn(uint256 _value) public returns (bool success) {
require(balanceOf[msg.sender] >= _value); // Check if the sender has enough
balanceOf[msg.sender] -= _value; // Subtract from the sender
totalSupply -= _value; // Updates totalSupply
Burn(msg.sender, _value);
return true;
}
/**
* Destroy tokens from other account
*
* Remove `_value` tokens from the system irreversibly on behalf of `_from`.
*
* @param _from the address of the sender
* @param _value the amount of money to burn
*/
function burnFrom(address _from, uint256 _value) public returns (bool success) {
require(balanceOf[_from] >= _value); // Check if the targeted balance is enough
require(_value <= allowance[_from][msg.sender]); // Check allowance
balanceOf[_from] -= _value; // Subtract from the targeted balance
allowance[_from][msg.sender] -= _value; // Subtract from the sender's allowance
totalSupply -= _value; // Update totalSupply
Burn(_from, _value);
return true;
}
}
/******************************************/
/* ADVANCED TOKEN STARTS HERE */
/******************************************/
contract MyAdvancedToken is owned, TokenERC20 {
uint256 public sellPrice;
uint256 public buyPrice;
mapping (address => bool) public frozenAccount;
/* This generates a public event on the blockchain that will notify clients */
event FrozenFunds(address target, bool frozen);
/* Initializes contract with initial supply tokens to the creator of the contract */
function MyAdvancedToken(
uint256 initialSupply,
string tokenName,
string tokenSymbol
) TokenERC20(initialSupply, tokenName, tokenSymbol) public {}
/* Internal transfer, only can be called by this contract */
function _transfer(address _from, address _to, uint _value) internal {
require (_to != 0x0); // Prevent transfer to 0x0 address. Use burn() instead
require (balanceOf[_from] >= _value); // Check if the sender has enough
require (balanceOf[_to] + _value > balanceOf[_to]); // Check for overflows
require(!frozenAccount[_from]); // Check if sender is frozen
require(!frozenAccount[_to]); // Check if recipient is frozen
balanceOf[_from] -= _value; // Subtract from the sender
balanceOf[_to] += _value; // Add the same to the recipient
Transfer(_from, _to, _value);
}
/// @notice Create `mintedAmount` tokens and send it to `target`
/// @param target Address to receive the tokens
/// @param mintedAmount the amount of tokens it will receive
function mintToken(address target, uint256 mintedAmount) onlyOwner public {
balanceOf[target] += mintedAmount;
totalSupply += mintedAmount;
Transfer(0, this, mintedAmount);
Transfer(this, target, mintedAmount);
}
/// @notice `freeze? Prevent | Allow` `target` from sending & receiving tokens
/// @param target Address to be frozen
/// @param freeze either to freeze it or not
function freezeAccount(address target, bool freeze) onlyOwner public {
frozenAccount[target] = freeze;
FrozenFunds(target, freeze);
}
/// @notice Allow users to buy tokens for `newBuyPrice` eth and sell tokens for `newSellPrice` eth
/// @param newSellPrice Price the users can sell to the contract
/// @param newBuyPrice Price users can buy from the contract
function setPrices(uint256 newSellPrice, uint256 newBuyPrice) onlyOwner public {
sellPrice = newSellPrice;
buyPrice = newBuyPrice;
}
/// @notice Buy tokens from contract by sending ether
function buy() payable public {
uint amount = msg.value / buyPrice; // calculates the amount
_transfer(this, msg.sender, amount); // makes the transfers
}
/// @notice Sell `amount` tokens to contract
/// @param amount amount of tokens to be sold
function sell(uint256 amount) public {
require(this.balance >= amount * sellPrice); // checks if the contract has enough ether to buy
_transfer(msg.sender, this, amount); // makes the transfers
msg.sender.transfer(amount * sellPrice); // sends ether to the seller. It's important to do this last to avoid recursion attacks
}
}
배포하기
컨트랙트를 스크롤 다운해보면 배포하기 위한 예상 비용을 볼 수 있습니다.
원한다면 슬라이더를 움직여 수수료를 더 작게 설정할 수도 있지만 시장가에 비해 너무 작게 설정하면 트랜잭션을 블록에 포함시키는데 엄청 오래 걸릴 수도 있습니다.
배포하기
를 클릭하고 계정의 암호를 입력합니다.
몇 초 후에 화면이 대시보드로 바뀌고 최근 트랜잭션
에서 컨트랙트 생성 중
이라는 메시지를 볼 수 있습니다.
몇 초를 더 기다리면 누군가 해당 트랜잭션을 가져가게 되고 얼마나 많은 노드들이 트랜잭션을 컨펌하고 있는지 보여주는 파란 박스가 천천히 증가합니다.
컨펌이 많아질수록 코드가 성공적으로 배포되고 있다는 것을 의미합니다.
생성된 토큰
관리자 페이지를 클릭하면 새롭게 생성된 화폐로 원하는 건 뭐든지 할 수 있는 세상에서 가장 단순한 중앙 은행 대시보드를 갖게 됩니다.
왼쪽에 있는 Read to Contract
아래에는 컨트랙트로부터 정보를 읽어오기 위한 옵션들과 함수들이 있습니다.
컨트랙트에서 읽어오는 건 무료입니다.
만약 토큰의 소유자가 있다면 여기에 소유자의 주소가 표시됩니다.
해당 주소를 복사하여 Balance of
에 붙여 넣으면 어떤 계정의 밸런스든지 다 볼 수 있습니다.
토큰을 가지고 있는 어떤 계정 페이지에서든 밸런스는 자동으로 보여집니다.
오른쪽에 있는 Write to Contract
아래에는 블록체인을 바꾸거나 고치기 위한 함수들을 볼 수 있습니다.
쓰기 함수를 사용하기 위해서는 가스가 필요합니다.
새로운 코인 발행을 허용한 컨트랙트의 경우, Mint Token
이라는 함수를 선택합니다.
Manage central dollar
새로운 화폐들을 받을 주소와 수량을 선택합니다.
decimals
를 2 로 설정한 경우에는 원하는 수량의 뒤에 두 개의 0 을 추가해야 정확히 원하는 만큼을 생성할 수 있습니다.
소유자로 설정된 계정을 선택해서 실행하는 경우 Ether 수량은 0 으로 두고 실행해도 됩니다.
몇 번의 컨펌 이후, 수신자의 밸런스에 새로운 수량이 반영됩니다.
하지만 수신자의 지갑에서는 사용자가 만든 토큰들을 자동으로 보여주지는 못하고 수동으로 추가해야 합니다.
관리자 페이지에서 토큰의 주소를 복사하여 수신자에게 보내주면 수신자가 직접 자신의 지갑에서 컨트랙트 탭에 있는 Watch Token
에 해당 주소를 추가할 수 있습니다.
표시된 Name
과 symbols
, decimal amounts
들은 커스터마이징이 가능합니다.
특히 비슷하거나 같은 이름의 토큰이 있다면 이름을 수정할 수 있습니다.
하지만 메인 아이콘은 바꿀 수 없고 해당 아이콘은 토큰을 보내거나 받을 때 잘못된 유사 토큰이 아닌지 확인하는 용도로 사용됩니다.
토큰 추가하기
토큰을 설치하고 나면 계정에 watched tokens
으로 추가되어서 전체 밸런스를 볼 수 있습니다.
토큰을 보내기 위해서는 send
탭에 가서 토큰을 보유하고 있는 계정을 선택하면 됩니다.
Ether
아래에 리스팅 되어 있는 토큰을 선택하고 보낼 토큰의 양을 입력합니다.
혹시 다른 사람의 토큰을 추가하고 싶다면 Contracts
탭에 가서 Watch token
을 클릭하면 됩니다.
예제로 주소에 0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7
를 입력하여 Unicorn (🦄) 토큰을 watch list
에 추가해볼 수도 있습니다.
유니콘 토큰은 이더리움 재단에서 관리하는 0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359
주소로 기부한 사람들에게 기념품처럼 제공됩니다.
마치며
지금까지 이더리움을 이용해서 토큰을 만드는 방법을 배웠습니다.
이 토큰으로 회사의 지분을 나타낸다던가 인플레이션 조절을 위해 언제 새로운 토큰을 발행할 건지 투표하는 중앙위원회를 만들 수도 있고 크라우드세일을 통해 자금을 조달 받을 수도 있습니다.
도움이 되셨다면 보팅 해주시면 감사하겠습니다.
Congratulations @dongmin.son! You received a personal award!
Click here to view your Board of Honor
Downvoting a post can decrease pending rewards and make it less visible. Common reasons:
Submit
Congratulations @dongmin.son! You received a personal award!
You can view your badges on your Steem Board and compare to others on the Steem Ranking
Vote for @Steemitboard as a witness to get one more award and increased upvotes!
Downvoting a post can decrease pending rewards and make it less visible. Common reasons:
Submit