본문 바로가기
Blockchain

[Blockchain] 이더리움 토큰 발행 ERC-20 이란?

by 개발자 염상진 2022. 7. 14.

이더리움 블록체인 메인넷에서 토큰을 발행하고, 관리할 수 있습니다. 내가 만든 토큰으로 디앱을 구동하고, 특정 행동을 유인하는 수단으로 사용할 수 있습니다. 

ERC(Ethereum Request for Comment)는 이더리움 블록체인 네트워크에서 정한 표준 스펙입니다. 그 중에서도 20번째로 제안된 표준이 바로 ERC-20입니다. 이더리움에서 새로운 표준이 적용되기 위해서는 EIP(Ethereum Improvemnet Proposal)을 통해 제안되고, ERC에서 구체화되어 표준이 됩니다. 즉 EIP는 개선 제안이며, ERC는 기능 표준을 의미합니다. 

 

 

예를 들어 이더리움에서 블루투스 통신이 가능하게 하면 어떨까요? 라고 제안하는 것은 EIP라고 할 수 있고, 블루투스 표준 스펙을 사용해 이더리움 내에서 기술적으로 구현해 표준으로 발표하는 것이 ERC입니다. 

이더리움 네트워크에서 호환성이 있는 모든 요구 사항을 충족하는 표준은 ERC-20로 간주되고, ERC-20 표준을 사용해 발행한 토큰은 이더리움과 교환 가능합니다. 

 

ERC-20 표준 스펙

 

ERC-20이 발표되고 난 후 이더리움 네트워크에서 서로 다른 토큰간의 교환이 가능해집니다. 또한 동일한 이더리움 지갑으로 교환 작업이 가능해집니다. 

ERC-20 토큰은 대체 가능한 토큰(Fungible Token)입니다. 즉, 화폐의 수단은 작은 단위로 쪼개서 교환이 가능하기 때문에 대체 가능한 토큰 표준을 준수합니다. NFT(Non-Fungible Token)은 그림이나 예술품 처럼 작은 단위로 쪼갤 수 없는 토큰이며 ERC-721 표준을 준수합니다.

ERC-20 토큰은 기본적으로 스마트 컨트랙트에 의해 생성됩니다. 토큰이 생성되면 다른 지갑 주소로 전송이 가능해집니다. 이더리움 블록체인 위에 배포된 댑(dApp)들은 스마트 컨트랙트를 통해 토큰을 발행하고, 다양한 분야에 적용될 각자의 토큰을 사용합니다. ERC-20에 의해 발행되는 토큰은 독자적으로 보이지만, 이더리움 네트워크 내에서 상호 교환이 가능하다는 장점을 가집니다. 각각의 댑(dApp)에서 발행된 토큰들은 한번에 이더(ETH)로 교환해 현금화도 가능합니다. 

ERC-20 토큰들은 스마트 컨트랙트를 통해 발급되기 때문에 특정 행동에 따라 결정론적으로 토큰 발행을 위임할 수 있습니다. 즉 중앙은행의 역할을 하는 TTP(Trusted Third Party)가 없어도 토큰을 발행하고, 교환을 보장할 수 있게 됩니다.

 

 

탈중앙화된 애플리케이션을 작성하고, 표준 이더리움 지갑을 통해 토큰 교환을 자유롭게 하며, 특정 행동을 강화하기 위해서는 ERC-20을 준수하는 경우 이더리움 블록체인의 ERC-20 표준을 준수해야 합니다.

 

ERC-20 전체 코드

 

아래 코드는 ERC-20 전체 소스 코드 입니다. 필요에 따라 함수를 추가하는 등 커스터마이징을 통해 표준을 준수하면서 자신만의 토큰을 발행할 수 있습니다. 

ERC-20 코드를 보면 크게 ERC20Interface와 SimpleToken Contract로 구성됩니다. Interface는 함수선언의 집합입니다. 구체적인 로직은 구현하지 않고, 선언만 한 상태로 상속해 사용할 수 있습니다. ERC20Interface에는 총 6개의 함수와 3개의 이벤트가 선언되어 있습니다.

Function 

  1. totalSupply() : ERC-20 토큰의 총 발행량을 확인합니다.
  2. balanceOf() : owner가 보유한 토큰 보유량을 확인합니다.
  3. transfer() : 토큰을 전송합니다.
  4. approve() : spender라는 사용자에게 value 만큼의 토큰을 출금할 권한을 부여합니다. approve() 함수를 호출할 때는 반드시 Approval 이벤트가 호출되어야 합니다.
  5. allowance() : owner가 spender 사용자에게 양도 설정한 토큰의 양을 확인합니다.
  6. ransferFrom() : 권한을 받은 spender가 양도받은 토큰을 전송합니다. 

 

 

추가로 이더리움에서 토큰을 전송하는 방법은 크게 두가지 입니다. 먼저 토큰을 보유한 owner가 직접 토큰을 보내는 방법(transfer() 함수 사용)과 토큰을 양도한 다음, 양도한 만큼의 토큰을 제3의 사용자를 통해 보내는 방법입니다. 2가지 방법을 사용하는 이유는 거래소(DEX)에서 토큰을 효율적으로 거래하기 위해서 입니다.

토큰 소유자는 거래소에 거래 권한을 양도하고, 토큰을 매매할 수 있습니다. 예를 들어 A라는 사용자는 거래소에게 자신이 가진 토큰의 양보다 적은 양을 거래 가능하도록 양도합니다. 이 때 approve()를 사용합니다.

토큰 소유자와 거래소 값을 통해 얼마만큼의 토큰이 양도되어 있는지 확인할 수 있습니다. 이 때 allowance()를 사용합니다.

거래소는 토큰 소유자가 양도한 금액 중에서 B라는 사용자가 신청한 금액만큼 토큰을 판매합니다. 이 때 transferFrom() 함수를 사용합니다. 

 

제3자에게 전송할 권리를 주는 approve() 함수를 실행합니다.

 

토큰을 전송할 권리를 제3의 spender에게 양도했습니다.

 

이제 제3의 spender는 권한을 양도받은 양도자의 토큰을 타인에게 전송할 수 있게 됩니다.

 

 

// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.14;

interface ERC20Interface {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount) external returns (bool);
    function approve(address spender, uint256 amount) external returns (bool);
    function allowance(address owner, address spender) external view returns (uint256);
    function transferFrom(address spender, address recipient, uint256 amount) external returns (bool);

    event Transfer(address indexed from, address indexed to, uint256 amount);
    event Transfer(address indexed spender, address indexed from, address indexed to, uint256 amount);
    event Approval(address indexed owner, address indexed spender, uint256 oldAmount, uint256 amount);
}

contract SimpleToken is ERC20Interface {
    mapping (address => uint256) private _balances;
    mapping (address => mapping (address => uint256)) public _allowances;

    uint256 public _totalSupply;
    string public _name;
    string public _symbol;
    uint8 public _decimals;
    uint private E18 = 1000000000000000000;

    constructor(string memory getName, string memory getSymbol) {
        _name = getName;
        _symbol = getSymbol;
        _decimals = 18;
        _totalSupply = 100000000 * E18;
        _balances[msg.sender] = _totalSupply; // 추가
    }

    function name() public view returns (string memory) {
        return _name;
    }

    function symbol() public view returns (string memory) {
        return _symbol;
    }

    function decimals() public view returns (uint8) {
        return _decimals;
    }

    function totalSupply() external view virtual override returns (uint256) {
        return _totalSupply;
    }

    function balanceOf(address account) external view virtual override returns (uint256) {
        return _balances[account];
    }

    function transfer(address recipient, uint amount) public virtual override returns (bool) {
        _transfer(msg.sender, recipient, amount);
        emit Transfer(msg.sender, recipient, amount);
        return true;
    }

    function allowance(address owner, address spender) external view override returns (uint256) {
        return _allowances[owner][spender];
    }

    function approve(address spender, uint amount) external virtual override returns (bool) {
        uint256 currentAllowance = _allowances[msg.sender][spender];  
        require(_balances[msg.sender] >= amount,"ERC20: The amount to be transferred exceeds the amount of tokens held by the owner.");
        _approve(msg.sender, spender, currentAllowance, amount);
        return true;
    }

    function transferFrom(address sender, address recipient, uint256 amount) external virtual override returns (bool) {
        _transfer(sender, recipient, amount);
        emit Transfer(msg.sender, sender, recipient, amount);
        uint256 currentAllowance = _allowances[sender][msg.sender];
        require(currentAllowance >= amount, "ERC20: transfer amount exceeds allowance");
        _approve(sender, msg.sender, currentAllowance, currentAllowance - amount);
        return true;
    }

    function _transfer(address sender, address recipient, uint256 amount) internal virtual {
        require(sender != address(0), "ERC20: transfer from the zero address");
        require(recipient != address(0), "ERC20: transfer to the zero address");
        uint256 senderBalance = _balances[sender];
        require(senderBalance >= amount, "ERC20: transfer amount exceeds balance");
        _balances[sender] = senderBalance - amount;
        _balances[recipient] += amount;
    }

    function _approve(address owner, address spender, uint256 currentAmount, uint256 amount) internal virtual {
        require(owner != address(0), "ERC20: approve from the zero address");
        require(spender != address(0), "ERC20: approve to the zero address");
        require(currentAmount == _allowances[owner][spender], "ERC20: invalid currentAmount");
        _allowances[owner][spender] = amount;  
        emit Approval(owner, spender, currentAmount, amount);
    }
}

 

SimpleToken Contract

 

SimpleToken 컨트랙트는 ERC20Interface를 상속해 함수를 구현합니다.

① totalSupply()

totalSupply()는 토큰의 총 발행량을 출력합니다.

function totalSupply() external view virtual override returns (uint256) {
    return _totalSupply;
}

 

② balanceOf()

특정 사용자가 보유한 토큰의 수를 반환합니다.

function balanceOf(address account) external view virtual override returns (uint256) {
     return _balances[account];
}

 

③ transfer()

Interface에 선언되어 있는 함수입니다. 내부 함수 _transfer()을 호출해 토큰을 전송하고, 정상적으로 토큰 전송이 되면 Transfer Event를 호출하게 됩니다. 

내부 함수 _transfer() 함수는 3가지 require 조건을 검사합니다. 함수 호출이 완료되면 보내는 사용자의 보유량에서 토큰량이 차감되고, 받는 사용자의 토큰량이 추가됩니다.

  1. 토큰을 보내는 사용자의 주소가 맞는지 확인합니다. 
  2. 토큰을 받는 사용자의 주소가 맞는지 확인합니다.
  3. transfer() 함수를 호출한 사용자(토큰을 보내는)가 가진 토큰이 보내고자 하는 토큰 보다 많은지 확인합니다.
function transfer(address recipient, uint amount) public virtual override returns (bool) {
    _transfer(msg.sender, recipient, amount);
    emit Transfer(msg.sender, recipient, amount);
    return true;
}

function _transfer(address sender, address recipient, uint256 amount) internal virtual {
    require(sender != address(0), "ERC20: transfer from the zero address");
    require(recipient != address(0), "ERC20: transfer to the zero address");
    uint256 senderBalance = _balances[sender];
    require(senderBalance >= amount, "ERC20: transfer amount exceeds balance");
    _balances[sender] = senderBalance - amount;
    _balances[recipient] += amount;
}

 

 

④ approve()

approve() 함수는 토큰 보유자가 특정 사용자에게 토큰 전송 권한을 양도합니다. 내부함수 _approve() 함수를 호출합니다. approve() 함수를 호출하면 spender가 설정된 Amount 내에서 몇번이고 출금하는 것을 허용하게 됩니다.

_transfer() 내부 함수와 마찬가지로 _approve() 내부 함수에서도 수신자, 권한 위임자, 토큰 양을 체크하는 require() 로직이 작동합니다. 권한 설정을 마친 후 Approval() 이벤트를 호출합니다.  

function approve(address spender, uint amount) external virtual override returns (bool) {
    uint256 currentAllowance = _allowances[msg.sender][spender];
    require(_balances[msg.sender] >= amount,"ERC20: The amount to be transferred exceeds the amount of tokens held by the owner.");
    _approve(msg.sender, spender, currentAllownace, amount);
    return true;
}

function _approve(address owner, address spender, uint256 currentAmount, uint256 amount) internal virtual {
    require(owner != address(0), "ERC20: approve from the zero address");
    require(spender != address(0), "ERC20: approve to the zero address");
    require(currentAmount == _allowances[owner][spender], "ERC20: invalid currentAmount");
    _allowances[owner][spender] = amount;
    emit Approval(owner, spender, currentAmount, amount);
}

 

⑤ allowance()

토큰 소유자가 spender에게 위임한 토큰의 양을 반환합니다.

function allowance(address owner, address spender) external view override returns (uint256) {
    return _allowances[owner][spender];
}

 

⑥ transferFrom()

토큰 소유자에게 권한을 위임받는 spender(msg.sender)가 위임받은 값만큼의 토큰을 상대방(receipent)에게 전송합니다. 전송을 위해서는 _transfer() 내부함수를 사용하고, 차감된 금액을 반영하기 위해 _approve() 함수를 호출하게 됩니다.

function transferFrom(address sender, address recipient, uint256 amount) external virtual override returns (bool) {
    _transfer(sender, recipient, amount);
    emit Transfer(msg.sender, sender, recipient, amount);
    uint256 currentAllowance = _allowances[sender][msg.sender];
    require(currentAllowance >= amount, "ERC20: transfer amount exceeds allowance");
    _approve(sender, msg.sender, currentAllowance, currentAllowance - amount);
    return true;
}

function _transfer(address sender, address recipient, uint256 amount) internal virtual {
    require(sender != address(0), "ERC20: transfer from the zero address");
    require(recipient != address(0), "ERC20: transfer to the zero address");
    uint256 senderBalance = _balances[sender];
    require(senderBalance >= amount, "ERC20: transfer amount exceeds balance");
    _balances[sender] = senderBalance - amount;
    _balances[recipient] += amount;
}

function _approve(address owner, address spender, uint256 currentAmount, uint256 amount) internal virtual {
    require(owner != address(0), "ERC20: approve from the zero address");
    require(spender != address(0), "ERC20: approve to the zero address");
    require(currentAmount == _allowances[owner][spender], "ERC20: invalid currentAmount");
    _allowances[owner][spender] = amount;
    emit Approval(owner, spender, currentAmount, amount);
}

 

 

Reference

 

 

 

[Blockchain] 가위바위보 스마트 컨트랙트 개선하기

가위바위보 스마트 컨트랙트 게임을 배포하고 난 후 문제점이 발견됩니다. 새로운 방을 생성한 방장이 입력한 조건값이 트랜잭션에 그대로 노출된다는 점입니다. 스마트 컨트랙트에서 발생한

about-tech.tistory.com

 

 

[Blockchain] 블록체인 오라클(Oracle) 문제란?

블록체인에 올라간 스마트 컨트랙트는 수동적인 존재입니다. 즉, EOA를 통해서만 작동하게 됩니다. 즉, 스마트 컨트랙트는 주어진 데이터에 따라 계약을 이행하기만 하기 때문에 외부 데이터가

about-tech.tistory.com

 

 

[Blockchain] 이더리움 Geth 사용 방법

로컬 테스트넷에서 Geth 실행하는 방법 Geth를 로컬 테스트넷에서 실행하기 위해서는 먼저 데이터 디렉토리 + genesis.json 파일이 준비되어야 합니다. 데이터 디렉토리에서는 송수신한 블록 데이터

about-tech.tistory.com

 

댓글