ICO
in Blockchain on Blockchain
1. ICO 이해
ICO(Initial Coin Offering)란 암호화폐를 사용하는 일종의 크라우드 펀딩이다.
- ICO는 이더리움과 같이 유명하고 가치 있는 암호 화폐와 교환하여 투자자에게 새로운 암호 화폐 또는 암호 화폐의 일부 단위를 제공하는 스타트업 회사의 자본원이 된다.
- 세계 최초의 ICO(토큰 세일라고도 함)는 2013년 7월 Mastercoin이라는 암호화폐에서 시작되었다. 이더리움 프로젝트는 2014년에 토큰 세일을 통해 자금을 모았으며, 처음 12시간 동안 3,700 BTC를 모금했다.
- 누구나 ICO를 시작할 수 있으나, 현재 싱가포르와 스위스 정도를 제외한 대부분의 국가는 현재 ICO가 금지되어 있다.
- 따라서 ICO를 시작하기 전에 법률을 확인하고 준수해야 하며 또한 대다수의 ICO가 실패했다는 점을 유의해야 한다.
- 우리나라는 정권교체에 따른 가상자산 정책완화로 단계적 ICO허용을 기대할 수 있는 상황이다.
2. ICO 구현
2.1 계획
- ICO는 ChanChanChan(CHCHCH)이라는 자체 토큰과 교환하여 ETH를 받는 스마트 컨트랙트가 될 것이다.
- CHCHCH 토큰은 완전히 호환되는 ERC20 토큰이며 ICO 시간에 생성된다.
- 투자자는 ETH를 ICO 계약 주소로 보내고 그 대가로 일정량의 CHCHCH를 받는다.
- ICO 계약으로 전송된 ETH를 자동으로 받는 입금 주소(EOA 계정)가 있다.
- wei단위 CHCHCH 토큰 가격은 다음과 같다. 1CHCHCH = 0.001Eth = 10**15 wei, 1Eth = 1000 CHCHCH);
- 최소 투자 금액은 0.01 ETH이고 최대 투자 금액은 5 ETH이다.
- ICO 하드캡은 300 ETH이다.
- ICO에는 ICO 시작 및 종료 시간을 지정하는 관리자가 있다.
- ICO는 하드캡 또는 종료 시간에 도달하면 종료된다(둘 중 먼저 도래하는 시점).
- CHCHCH 토큰은 가격 폭락을 막기위해 일정기간 잠그어 관리자가 설정한 특정 시간 후에만 거래할 수 있다.
- 긴급 상황의 경우 관리자는 ICO를 중지할 수 있으며 주소에 이상이 생길 경우 예금 주소를 변경할 수 있다.
- ICO는 다음 상태 중 하나이다. beforeStart, running, afterEnd, halted;
- 또한 ICO에서 판매되지 않은 토큰은 소각할 수 있도록 구현한다.
- ICO 투자 후 Invest 이벤트가 발생한다.
2.2 전체 코드
github repo : https://github.com/prettygood236/ico_erc20
contract address : 0x2a2474a6bfdb8b4b7ea08b5327a2c98c9c6139f6
// file: 'ChanChanChanICO_erc20.sol'
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.7;
// ----------------------------------------------------------------------------
// EIP-20: ERC-20 Token Standard
// https://eips.ethereum.org/EIPS/eip-20
// -----------------------------------------
// ChanChanChan 컨트랙트에서 사용할 함수를 선언한다.
interface ERC20Interface {
// ERC-20 토큰의 총 발행량 확인
function totalSupply() external view returns (uint);
// tokenOwner가 가지고 있는 토큰의 보유량 확인
function balanceOf(address tokenOwner) external view returns (uint balance);
// 토큰을 전송
function transfer(address to, uint tokens) external returns (bool success);
// tokenOwner가 spender에게 양도 설정한 토큰의 양을 확인한다
function allowance(address tokenOwner, address spender) external view returns (uint remaining);
// spender 에게 value 만큼의 토큰을 인출할 권리를 부여. 이 함수를 이용할 때는 반드시 Approval 이벤트 함수를 호출해야 한다.
function approve(address spender, uint tokens) external returns (bool success);
// spender가 거래 가능하도록 양도 받은 토큰을 전송한다.
function transferFrom(address from, address to, uint tokens) external returns (bool success);
// 토큰이 이동할 때마다 로그를 남긴다.
event Transfer(address indexed from, address indexed to, uint tokens);
// approve 함수가 실행 될 때 로그를 남긴다.
event Approval(address indexed tokenOwner, address indexed spender, uint tokens);
}
// ChanChanChan 컨트랙트 안에서 ERC20Interface에 선언된 함수와 이벤트를 사용한다.
contract ChanChanChan is ERC20Interface{
string public name = "ChanChanChan";
string public symbol = "CHCHCH";
uint public decimals = 0;
uint public override totalSupply;
address public founder;
mapping(address => uint) public balances;
mapping(address => mapping(address => uint)) allowed;
constructor(){
totalSupply = 1000000;
founder = msg.sender;
balances[founder] = totalSupply;
}
function balanceOf(address tokenOwner) public view override returns (uint balance){
return balances[tokenOwner];
}
// virtual을 통해 상속 ICO 컨트랙트에서 transfer를 수정할 수 있도록 한다.
function transfer(address to, uint tokens) public virtual override returns(bool success){
require(balances[msg.sender] >= tokens);
balances[to] += tokens;
balances[msg.sender] -= tokens;
emit Transfer(msg.sender, to, tokens);
return true;
}
function allowance(address tokenOwner, address spender) view public override returns(uint){
return allowed[tokenOwner][spender];
}
function approve(address spender, uint tokens) public override returns (bool success){
require(balances[msg.sender] >= tokens);
require(tokens > 0);
allowed[msg.sender][spender] = tokens;
emit Approval(msg.sender, spender, tokens);
return true;
}
// virtual을 통해 상속 ICO 컨트랙트에서 transferFrom을 수정할 수 있도록 한다.
function transferFrom(address from, address to, uint tokens) public virtual override returns (bool success){
require(allowed[from][to] >= tokens);
require(balances[from] >= tokens);
balances[from] -= tokens;
balances[to] += tokens;
allowed[from][to] -= tokens;
emit Transfer(from, to, tokens);
return true;
}
}
// ERC20 토큰 ChanChanChan 컨트랙트를 상속하여 새 컨트랙트 ChanChanChanICO를 선언한다.
contract ChanChanChanICO is ChanChanChan{
// ICO에는 컨트랙트 배포 계정인 관리자가 있어 긴급 상황이 발생하면 ICO를 중지할 수 있고, 손상될 경우 예금 주소를 변경할 수 있다.
address public admin;
// 컨트랙트에 전송된 Ether로 전송되는 주소. 투자자는 Eth를 컨트랙트 주소로 보낸다.
// Ether는 자동으로 입금 주소로 전송되고 암호화폐는 투자자의 잔액에 추가된다.
// 이 방법은 컨트랙트에 이더를 저장하는 것보다 안전하다.
address payable public deposit;
// 1ETH == 1000 CHCHCH, 1CHCHCH = 0.001ETH
uint tokenPrice = 0.001 ether;
// ICO에는 투자할 수 있는 최대 이더(하드캡)가 있고, 300이더로 설정한다.
uint public hardCap = 300 ether;
// ICO로 전송된 이더의 총량을 보유하는 raiseAmount변수를 선언힌
uint public raisedAmount;
// ICO에는 시작 날짜와 종료 날짜가 있으므로 바로 시작할 필요는 없다.
// 예) block.timestamp + 3600 : 1시간 뒤에 시작.
uint public saleStart = block.timestamp;
// 바로 시작해서 일주일 후에 종료되도록 한다.
uint public saleEnd = block.timestamp + 604800;
// 초기 투자자들이 토큰을 시장에 버려 가격이 폭락하지 않도록
// ICO가 끝난 후 일주일 후에 토큰을 전송할 수 있도록 한다.
uint public tokenTradeStart = saleEnd + 604800;
// 최소 및 최대 투자금액을 설정한다.
uint public minInvestment = 0.1 ether;
uint public maxInvestment = 5 ether;
// ICO의 4가지 상태를 가지는 enum 변수를 선언한다.
enum State {beforeStart, running, afterEnd, halted}
State public icoState;
constructor(address payable _deposit){
// deposit 주소를 초기화한다.
deposit = _deposit;
// 관리자를 컨트랙트 배포 주소로 초기화한다.
// deposit 및 admin은 같은 주소일 수도, 아닐수도 있다.
admin = msg.sender;
// ICO는 배포 후에 시작된다.
icoState = State.beforeStart;
}
// 관리자만 호출 가능한 함수에 적용할 onlyAdmin modifier를 선언한다.
modifier onlyAdmin(){
require(msg.sender == admin);
_;
}
// 관리자는 문제 발생 시 언제든지 ICO를 중지할 수 있어야 한다.
function halt() public onlyAdmin{
icoState = State.halted;
}
// 관리자는 문제가 해결된 후 ICO를 다시 시작할 수 있어야 한다.
function resume() public onlyAdmin{
icoState = State.running;
}
// 관리자는 또한 deposit 주소에 문제가 생긴 경우 변경할 수 있다.
function changeDepositAddress(address payable newDeposit) public onlyAdmin{
deposit = newDeposit;
}
// ICO의 상태를 리턴하는 함수 getCurrentState 선언 (상태를 변경하지 않는 읽기 전용 함수)
function getCurrentState() public view returns(State){
// ICO 중지 상태.
if(icoState == State.halted){
return State.halted;
// ICO 시작 전.
}else if(block.timestamp < saleStart){
return State.beforeStart;
// ICO 진행 중.
}else if(block.timestamp >= saleStart && block.timestamp <= saleEnd){
return State.running;
// ICO 종료.
}else{
return State.afterEnd;
}
}
// (프론트엔드에서 처리) 블록체인에 기록될 로그 메시지인 이벤트를 선언한다.
event Invest(address investor, uint value, uint tokens);
// Invest는 ICO의 주요 기능이며 투자자는 Front-End 앱을 사용하여 이 함수를 호출하고
// 지갑으로 보내거나 ICO 주소로 직접 보내어 암호화폐를 구입할 수 있다.
// Invest 함수는 누군가 ETH를 컨트랙트에 보내고 ETH를 수신할 때 호출된다.
function invest() payable public returns (bool){
// ICO가 실행 중이어야 한다.
icoState = getCurrentState();
require(icoState == State.running);
// 또한 컨트랙트에 전송된 값이 최소 투자비용과 최대 투자비용 사이에 있어야 한다.
require(msg.value >= minInvestment && msg.value <= maxInvestment);
// 또한 ICO가 정해놓은 hardCap에 도달하지 않았어야 한다.
raisedAmount += msg.value;
require(raisedAmount <= hardCap);
// 투자자가 방금 보낸 Ether에 대해 얻을 토큰 수를 계산한다.
// 보낸 wei의 수를 wei의 토큰 가격으로 나눈다.
uint tokens = msg.value / tokenPrice;
// 계산으로 나온 금액만큼 해당 토큰을 투자자의 잔액에 추가하고 founder 잔액에서 뺀다.
// balances는 ERC20 토큰 컨트랙트에서 선언되고 ICO 컨트랙트로 상속된 매핑 변수이다.
balances[msg.sender] += tokens;
balances[founder] -= tokens;
// 보낸 금액을 deposit 주소로 전송한다.
deposit.transfer(msg.value);
// 투자자 주소 및 보낸 금액, 방금 구매한 토큰 수를 이벤트로 내보낸다.
emit Invest(msg.sender, msg.value, tokens);
return true;
}
// 이 함수는 누군가 Ether를 컨트랙트 주소로 직접 보낼 때 호출된다.
receive() payable external{
invest();
}
// transfer()는 소유자가 자신의 토큰을 다른 계정으로 전송하기 위해 호출하며
// transferFrom()은 소유자가 자신이 소유한 토큰을 사용하기 위해 다른 주소를 승인한 후 토큰 소유자를 대신하여 호출된다.
// 이 2가지 함수를 재정의하여 토큰 잠금 기능을 추가한다.
function transfer(address to, uint tokens) public override returns(bool success){
// 현재 날짜와 시간이 컨트랙트 상태 변수인 tokenTradeStart보다 큰 경우에만 토큰을 전송할 수 있다.
require(block.timestamp > tokenTradeStart);
ChanChanChan.transfer(to, tokens);
return true;
}
function transferFrom(address from, address to, uint tokens) public override returns (bool success){
require(block.timestamp > tokenTradeStart);
ChanChanChan.transferFrom(from, to, tokens);
return true;
}
// ICO에 모금된 금액이 HardCap(300 ETH) 미만인 경우 남은 토큰을 소각한다. (이는 일반적으로 가격상승으로 이어진다.)
// 이 함수는 관리자뿐만 아니라 누구나 호출할 수 있다. (관리자가 마음을 바꾸어도 결국 토큰을 소각되어야 한다.)
function burn() public returns(bool){
icoState = getCurrentState();
// 토큰은 ICO가 종료된 후에만 소각된다.
require(icoState == State.afterEnd);
// 남은 토큰을 없앤다. 토큰을 다시 생성할 수 있는 코드는 컨트랙트에 없다.
balances[founder] = 0;
return true;
}
}
2.3 작동 영상
3. 개발회고
Keep
토큰이코노미의 시작이라고 할 수 있는 ICO를 직접 구현해보면서 단순히 컨트랙트 작성 지식을 넘어 투자자와 관리자, 사용자 등 여러 현실 변수에 대해 깊은 이해를 가질 수 있었다.
Problem
ICO가 대부분의 나라에서 금지되었다보니 관련된 최신의 정보를 찾기가 힘들었다.
Try
토큰 이코노미를 더욱 높일 수 있는 획기적인 방안에 대해 계속 생각 중이다.