[EOS.IO] eosio.token 컨트랙트를 분석해보자!

in #kr6 years ago

EOS.IO의 메인넷이 출시한 이후, 블록 프로듀서 선정 문제 및 램 가격 폭등에 대한 거버넌스 문제 등 예기치 못한 일들이 일어났습니다. 그 사건들의 여파인지 하락장의 당면한 흐름에 따른 것인지 EOS 및 이더리움 등의 플랫폼 코인 가격도 곤두박칠 치고 있는 현재인데요. 사실, 아직 가보지 않은 길을 가고 있는 EOS의 문제가 발생하지 않을 것이라고 예상하는 것은 섣부른 판단이겠지요.

EOS는 현재도 빠르게 다양한 문제를 흡수하며 새로운 API와 도구, 그에 기반한 커뮤니티가 확장되고 있습니다. 사실, 이런 흐름을 따라가기도 무척 벅찬 가운데 아직은 EOS 기반 댑을 개발하면서 보완해야할 부분이 많으며 상용화되기 전까지 어느정도 시간이 걸릴 것으로 보입니다. 사실, 이건 대부분의 퍼블릭 플랫폼 블록체인의 문제이기에 이에 대한 흐름을 전반적으로 캐치업하고 있습니다.

이번 글에서는 eosio.token 컨트랙트에 대해 파고들고자 합니다. 사실, EOS 개발학습을 하면서 eosio.token 컨트랙트를 중심으로 key,account,permission에 대한 이해를 하면, 보다 쉽게 학습을 진행할 수 있을 것 같아요.

eosio.token contract?

https://github.com/EOSIO/eos/tree/master/contracts/eosio.token

eosio.token으로 다른 종류의 토큰을 생성하고 발행할 수 있습니다. 이더리움의 ERC20 토큰을 발행하는 것과 마찬가지로 토큰을 생성하거나 이 토큰의 소유자를 연결합니다.

이에 대한 주요 액션은 다음과 같습니다.

(토큰 생성 - create)

void create(account_name issuer, asset maximum_supply)

여기서, account_name을 전달함으로써 해당 토큰의 발행자는 issuer 어카운트가 되는 것입니다. 이 account는 일반적인 owner, active 권한을 가지거나 토큰 생성 및 관리 용도로 구성할 수 있습니다.

asset은 “10000 SYS” “10.00 SYMBOL” 과 같은 포맷으로 전달해야하며, 소수점 18자리까지 표현 가능하며, SYMBOL은 1~7자리의 대문자로 정의해야합니다.

발행자(issuer)와 최대 발행량(maximum_supply) 을 인자로 넘김으로서, 새 토큰의 구성사항을 블록체인으로 올립니다. 이는 새 토큰의 구성사항에 대한 스토리지를 누군가가 스테이킹해줘야 하는 것을 의미하구요. 여기서는 eosio.token 어카운트가 해당 스토리지에 저장에 대한 비용을 지불할것입니다.

(토큰 발행 - issue)

void issue(account_name to, asset quantity, string memo)

issue는 토큰의 공급량을 증가시키는데 앞서 설정한 최대 공급량(maximum_supply)이 다다를때 까지 계속 발행됩니다. 여기서 발행자(issuer)는 해당 토큰 생성시 액션을 승인한 계정이죠.

(토큰 전달 - transfer)

void transfer(account_name from, account_name to, asset quantity, string memo)

한 어카운트에서 다른 어카운트로 원하는 양(quantity)만큼 전송하는데 필요합니다.

EOS Account

EOS Account는 owner key와 active key 두개의 EOS public key로 구성된다.

owner key는 EOS 계정의 마스터키로서 계정의 소유권과 관련한 키이며, active key는 EOS 전송 및 투표 활동에 대한 권한 키로 사용됩니다.

이 EOS Key는 EOS public key와 EOS private key 한쌍으로 구성되며 무한으로 생성이 가능합니다. 해당 account당 public key는 n:n 관계이며 EOS public key로 등록된 계정 이름은 여러가지가 존재합니다.

신규 EOS 계정을 만들기 위해서는 ‘생성자 계정’이 ‘신규 계정'이 ‘owner key’, ‘active key’를 가지도록 EOS 메인넷에 등록하는 과정이 필요합니다. ‘생성자 계정'이 ‘신규 계정'을 이용 가능한 계정으로 생성하기 위해서는 CPU, NET Bandwidth, RAM을 구입해서 할당해줘야 합니다.

cleos를 활용한 기본 스마트 컨트랙트

nodeos 의 REST API 커맨드 툴인 cleos를 활용하여 스마트 컨트랙트를 활용하는 방법에 대해 간략히 살펴보겠습니다. 이에 대한 관련 내용은 여기서 자세히 볼 수 있습니다.

  • 노드 실행
$nodeos -e -p eosio --plugin eosio::wallet_api_plugin --plugin eosio::chain_api_plugin --plugin eosio::history_api_plugin --contracts-console
  • mywallet이라는 이름의 월렛 생성
$cleos wallet create -n mywallet
  • public, private key 생성
$cleos create key  
  • 앞서 생성한 private key 임포트
$cleos wallet import --private-key={private_key} -n mywallet 
  • eosio.token 계정 생성
$cleos create account eosio eosio.token {public_key} 
  • eosio.token 계정 권한으로 eosio.token 컨트랙트 배포
$cleos set contract eosio.token build/contracts/eosio.token -p eosio.token 
  • create action으로 ‘SYS’ 토큰 10000개 생성
$cleos push action eosio.token create ‘[“eosio”,”10000.000 SYS”]’ -p eosio.token  
  • issue action으로 100개의 ‘SYS’토큰 발행
$cleos push action eosio.token issue ‘[“user”,”100.000 SYS”,”memo”]’ -p eosio  
  • user의 잔고 확인
$cleos get currency balance eosio.token user  
  • user 권한으로 user2에게 해당 토큰 전송
$cleos push action eosio.token transfer ‘{“from”:”user”,”to”:”user2”,”quantity”:”10.000 SYS”,”memo”:”memo”}’ -p user  

컨트랙트 코드 분석

이제 eosio.token 을 구성하는 코드를 분석해보겠습니다.

eosio.token.hpp

public:
token( account_name self ):contract(self){}

생성자와 주요 함수인 create(), issue(), transfer()은 퍼블릭 멤버 함수로 정의되어 있습니다.
생성자는 account_name 을 받는데 이는 ‘eosio.token’과 같이 배포하려는 계정의 이름이고, contract는 eosio::contract를 상속받기에 이와 같이 정의해야합니다.

다른 테이블과 헬퍼 함수들은 private 멤버 변수로 정의되어 있으며, 아래 sub_balance()와 add_balance()의 경우 transfer action시 호출됩니다.

typedef eosio::multi_index<N(accounts), account> accounts;
typedef eosio::multi_index<N(stat), currency_stats> stats;

typedef eosio::multi_index 는 테이블로 accounts와 stats 이 있습니다. accounts 테이블은 개별로 토큰을 가진 account 오브젝트로 구성되어 있으며, stats 테이블은 공급량(supply), 최대 공급량(maximum_supply), 발행자(issuer)로 이루어진 currency_stats 오브젝트로 구성되어 있습니다.

각 테이블에 대한 퍼미션이 다른데, accounts는 eosio 계정 스코프이며, stats는 토큰 심볼 이름에 따른 스코프입니다. 이 스코프에 따라 접근 권한이 달라집니다.

struct account {
            asset    balance;

    uint64_t primary_key()const { return balance.symbol.name(); }
};

accounts의 account 오브젝트는 토큰 심볼 이름이 primary key로 동작하고, asset 타입의 토큰 밸런스 값을 가지고 있습니다. 즉 어떤 유저(Will)의 스코프로 accounts 테이블을 조회하면, Will이 가지고 있는 각 심볼 (EOS, SYS 와 같은)에 대한 밸런스 값을 가져올 수 있는 것이죠.

struct currency_stats {
            asset          supply;
            asset          max_supply;
            account_name   issuer;

            uint64_t primary_key() const { return supply.symbol.name(); }
};

Stats 테이블의 stat의 경우, 현재 토큰에 대한 통계를 보여주는데, 개별 심볼에 따른 자체 스코프가 형성됩니다. 즉 ‘SYS’라는 심볼의 stat 테이블에 currency_stats 오브젝트를 가지게 되는데, 이는 여러개의 account를 가지는 accounts 테이블과는 달리, 하나의 currency_stats 오브젝트를 가집니다.

이제, 실제 액션의 구현을 담당하는 eosio.token.cpp을 보면 다음과 같습니다.

create

void token::create( account_name issuer,
                    asset        maximum_supply )

해당 액션이 실행되면 issuer는 최대 발행량 안에서 토큰을 발행할 수 있게 됩니다.

require_auth( _self );

해당 액션에서 자체 권한을 검사하기 때문에, 해당 액션을 실행하기 위하여 -p eosio.token을 명시해야 합니다.

auto sym = maximum_supply.symbol;
eosio_assert( sym.is_valid(), "invalid symbol name" );
eosio_assert( maximum_supply.is_valid(), "invalid supply");
eosio_assert( maximum_supply.amount > 0, "max-supply must be positive");

이 코드는 ‘1000.000 SYS’와 같은 파라미터 값에서 심볼(SYS)을 추출하고 값이 적절하게 대입되어 있는지 확인합니다. 여기서 eosio_assert가 fail되면, 모든 코드가 멈추고 롤백(roll-back) 되지요.

stats statstable( _self, sym.name() );
auto existing = statstable.find( sym.name() );
eosio_assert( existing == statstable.end(), "token with symbol already exists" );


statstable.emplace( _self, [&]( auto& s ) {
       s.supply.symbol = maximum_supply.symbol;
       s.max_supply    = maximum_supply;
       s.issuer        = issuer;
});

stats 테이블은 토큰 심볼 이름으로 초기화되고 이에 대한 스코프를 형성합니다.
해당 토큰이 이미 테이블에 존재하는지 체크한 후, 새 토큰을 생성하고 블록체인에 저장합니다. emplace 함수의 첫번째 파라미터는 해당 데이터를 저장하기 위해(스테이킹) 들어갈 비용을 누가 내야하는지 명시하는 것인데 여기서는 _self 로서 eosio.token이 저장 공간을 지불합니다. 그리고 supply의 심볼 프로퍼티로 해당 심볼을 저장합니다.

Issue

void token::issue( account_name to, asset quantity, string memo )

발행할 토큰을 받는 계정, 발행량, 메모를 파라미터로 받습니다. 이 함수는 두개의 액션이 한군데로 모아져있는데, 생성된 토큰의 발행량(supply)을 조정하고 발행 토큰을 transfer하는 함수를 호출하기 때문입니다.

auto sym = quantity.symbol;
eosio_assert( sym.is_valid(), "invalid symbol name" );
eosio_assert( memo.size() <= 256, "memo has more than 256 bytes" );

토큰 심볼을 추출하고 메모 크기가 256바이트 이상 크지 않은지 체크합니다.

auto sym_name = sym.name();
stats statstable( _self, sym_name );
auto existing = statstable.find( sym_name );
eosio_assert( existing != statstable.end(), "token with symbol does not exist, create token before issue" );
const auto& st = *existing;

stat 테이블에서 토큰 심볼 이름을 스코프로하고, 앞서 create 액션에서 수행했던 해당 심볼 이름 등록과정이 잘되어 있는지 확인합니다.
existing 변수는 statstable.find()를 통해 iterator 형태의 값을 받고, st는 existing이 가리키는 포인터 즉 실제 변수입니다. 이를 통해 C++ 포인터의 -> 대신에 st.functionName 형태로 멤버 변수에 접근이 가능합니다.

require_auth( st.issuer );
eosio_assert( quantity.is_valid(), "invalid quantity" );
eosio_assert( quantity.amount > 0, "must issue positive quantity" );

eosio_assert( quantity.symbol == st.supply.symbol, "symbol precision mismatch" );
eosio_assert( quantity.amount <= st.max_supply.amount - st.supply.amount, "quantity exceeds available supply");

생성된 토큰에 대한 발행자의 권한을 체크하고 추가적인 에러 핸들링을 진행합니다.

statstable.modify( st, 0, [&]( auto& s ) {
       s.supply += quantity;
});

add_balance( st.issuer, quantity, st.issuer );

if( to != st.issuer ) {
       SEND_INLINE_ACTION( *this, transfer, {st.issuer,N(active)}, {st.issuer, to, quantity, memo} );}

해당 currency_stats에 정해진 발행량 만큼 공급량에 추가되는 로직이 반영됩니다.
add_balance()는 앞서 보았던 private 함수이며, 최종적으로, SEND_INLINE_ACTION()으로 함수가 전송됩니다.
각 파라미터를 보면, *this 는 해당 액션이 속한 컨트랙트 코드, transfer 는 전송하고자 하는 액션. {st.issuer,N(active)} 는 액션이 요구하는 권한, {st.issuer,to,quantity, memo}는 액션 자체의 전달 요소들(여기서는 transfer의 파라미터들) 입니다.

transfer

void token::transfer( account_name from,
                      account_name to,
                      asset        quantity,
                      string       memo )

{
    eosio_assert( from != to, "cannot transfer to self" );
    require_auth( from );
    eosio_assert( is_account( to ), "to account does not exist");
    auto sym = quantity.symbol.name();
    stats statstable( _self, sym );
    const auto& st = statstable.get( sym );

require_auth로 from의 어카운트 권한을 확인하고, 심볼을 quantity로부터 추출합니다. 그리고 해당 심볼에 대한 currency_stats 정보를 get() 합니다.

require_recipient( from );
require_recipient( to );

이는 보내는 이와 받는이 모두에게 액션이 완료되면 이를 notify 하게 됩니다.

eosio_assert( quantity.is_valid(), "invalid quantity" );
eosio_assert( quantity.amount > 0, "must transfer positive quantity" );
eosio_assert( quantity.symbol == st.supply.symbol, "symbol precision mismatch" );
eosio_assert( memo.size() <= 256, "memo has more than 256 bytes" );

sub_balance( from, quantity );
add_balance( to, quantity, from );
}

추가적인 에러 핸들링 및 private 함수인 sub_balance()와 add_balance()를 사용하여 보내는 계정의 토큰 잔고를 감소시키고, 받는 계정의 토큰을 추가합니다.

eosio.token 컨트랙트에 대해 분석해보았습니다. 이를 기반으로 스마트 컨트랙트 개발을 진행하면 보다 수월할 듯 합니다.


최근에 제주에서 열린 개발자 컨퍼런스에 갔다온 적이 있었는데요. 내용만을 봤을 때 실망스러운 부분이 있었지만, 여러 블록체인 회사들이 확장성을 해결하기 위해 내세우는 여러 제안들을 보면서 흐름을 확인할 수 있었지요. 하지만, 그들이 주장하는 성능의 이슈는 제쳐두고 중요한 무언가가 빠진채 플랫폼화하겠다는 목표를 내세우는 것이 납득이 가지 않았습니다. 중요한 것은 생태계를 이룰 커뮤니티와 회사, 이에 밑바탕이 될 강력한 개발자 커뮤니티라고 생각합니다. 그런 관점에서 EOS는 생태계와 개발자 커뮤니티가 꾸준히 발전하고 있다고 보기에 보다 긍정적으로 바라보고 있기는 합니다.

Sort:  

Congratulations @willpark! You have completed the following achievement on Steemit and have been rewarded with new badge(s) :

Award for the number of posts published

Click on the badge to view your Board of Honor.
If you no longer want to receive notifications, reply to this comment with the word STOP

Do you like SteemitBoard's project? Then Vote for its witness and get one more award!