ERC20

ERC-20 ?

  • ERC-20은 Ethereum Request for Comment 20의 약어로, 20은 리퀘스트 숫자이다.
  • 이더리움 네크워크의 개선안을 제안하는 EIPs(Ethereum Improvement Proposals)에서 관리하는 공식 프로토콜이다.
  • 이더리움 블록체인 네트워크에서 정한 표준 토큰 스펙이다.
  • 필요한 이더리움과 호환성이 있는 모든 요구 사항을 충족시키는 표준ERC-20로 간주한다.
  • ERC-20 토큰은 이더리움과 교환 가능하고, 이더리움 지갑으로 전송 가능하다.

EIP vs ERC 차이

  • EIP(Ethereum Improvement Proposals)는 이더리움 개선 제안
    • ex. “이더리움 생태계를 이런식으로 개선해보면 어떨까요~?”
  • ERC(Ethereum Request for Comments)는 이더리움 기능 표준
    • ex. ”이 **기능(=프로그래밍 코드)**은 앞으로 이더리움 기능의 표준이 될 것 입니다.”

ERC-20

  • ERC-20 표준 사용 이유 : 토큰끼리의 호환을 위함.
    • 안드로이드 운영체제를 사용하는 네이버 지도 → 카카오톡 바로 공유할 수 있는 것처럼 ERC-20 기반으로 생성된 토큰은 상호 호환이 가능함. ERC-20 기반 토큰들은 동일한 이더리움 지갑으로 전송 가능함.

ERC-20 토큰은 스마트 컨트랙트를 통해서 생성된다. ERC-20 토큰을 생성하면 해당 토큰을 다른 주소로 보낼 수 있고 여러 역할을 해준다.

이더리움은 자체 블록체인을 기반으로 다양한 탈중앙화 된 애플리케이션들이 작동할 수 있도록 만들어진 하나의 플랫폼 네트워크다. dApp은 이더리움 플랫폼 상에서 컨트랙트를 이용해 쉽고 빠르게 토큰을 발행할 수 있다. 이더리움 블록체인에서는 **이더(ETH)**가 사용되며, 이더리움 블록체인 상의 dApp은 다른 다양한 분야에 적용될 수 있게 각각의 솔루션을 보유하고 있어서 그에 맞는 토큰을 발행한다. 이 때 발행된 토큰은 독자적인 토큰인 듯 보이지만 실제로는 이더리움 생태계에서 호환 및 사용 가능하다.

→ 안드로이드 및 iOS가 하나의 플랫폼 역할 & 그 위 수많은 앱이 존재하는 것 처럼 생각

  • 안드로이드 / iOS = 이더리움
  • 앱 = 디앱(dApp)

디앱 내의 토큰 교환 및 또 다른 이더리움 플랫폼을 기반으로 한 디앱의 토큰과 교환이 가능한데 이를 위해 ERC-20 토큰 표준이 만들어졌다. 다양한 디앱에 흩어져있는 ERC-20 표준 호환 토큰들은 나중에 통합되어 한 번에 이더로 모두 바꿔 현금화 할 수 있다.

ERC-20 토큰은 스마트 컨트랙트의 속성을 지원, 스마트 컨트랙트의 강점으로 온라인 환경에서 암호화폐 교환 시에 일정 행동이 불가역적으로 전개되는 기능을 활용하며, 중앙관리가 배제된 서비스를 구현할 수 있다.

따라서, 디앱이더리움 블록체인 플랫폼을 활용하여 자신의 비즈니스를 구현하고, 자금모집과 거래체계, 플랫폼 사용료이더리움으로 지불하는 체계를 가지고 토큰을 발행할 필요가 있으며, 실제로 이더리움 기반 토큰 발행이 많다.

결국, ERC-20 토큰이 되기 위한 기준스마트 컨트랙트 기능이 포함 되었나의 유무로 볼 수 있다. 또한 ERC-20은 디앱이 발행하는 토큰이 이더리움의 통화인 이더리움과 호환성을 충적하기 위해 규정하는 프로그래밍 기준이다. ERC-20 기준을 맞춰 디앱을 설계한 후 토큰 발행시, 이더리움과 쉽게 교환 가능하고, 표준 이더리움 지갑(My Ether Wallet, Meta Mask, Mist 등)에 자유롭게 전송 가능하다. 결론적으로 이더리움 블록체인을 활용하는 토큰의 경우 ERC-20 기준에 맞춰야 한다.



ERC-20 토큰 개발하기

개발 준비

ERC-20은 세계에서 가장 많이 사용되는 스마트 컨트랙트 토큰이다. 토큰을 ERC-20 형태로 발행시 이더리움 네트워크의 수많은 플랫폼과 서로 호환 가능한 최소한의 기준이다. 이 기준을 충족시키면, 다양한 이더리움 네트워크에서 호환 가능하여 메타마스크, 이더스캔, 마이이더월렛 등에서 더 쉽게 사용할 수 있다.

ERC-20을 기반으로 토큰을 만들면, 토큰을 이동하거나 탈중앙화 거래소를 이용하여 거래에 사용하는 등의 토큰을 통해 할 수 있는 것들이 늘어난다.


ERC-20 전체 코드

Remix와 Metamask를 활용해서 이더리움 ERC-20 기반 토큰을 만들고, 만든 토큰을 이동하는 실습 진행.


ERC-20 토큰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
// 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);
}
}

⇒ 이 코드를 ERC-20 토큰이라고 한다. ERC-20 코드는 토큰을 만드는 사람에 따라 기본 코드에 추가로 함수를 추가할 수 있다. But, ERC-20의 기본적인 내용과 흐름을 이해한다면, 라이브러리를 사용해 변형된 ERC-20 토큰 코드도 쉽게 사용 가능하다.



ERC-20 토큰 구조

ERC20 Interface

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
~ 생략 ~

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);
}

~ 생략 ~

ERC20Interface는 ERC-20 코드에서 가장 처음 확인할 수 있는 코드이다.

솔리디티에서 Interface는 사용할 함수의 형태를 선언한다. 실제 함수의 내용은 Contract에서 사용한다.

ERC20Interface는 예제에서 쓰일 SimpleToken 컨트랙트에서 사용할 함수의 형태를 선언하고, ERC-20에서 사용하는 함수의 형태를 선언하는 것을 확인할 수 있다.

함수(Function)는 이더리움에서 제공하는 함수로, event는 이더리움에서 제공하는 로그이다. function과 event를 선언할 때는 입력값과 반환값은 선택할 수 있지만, function의 자료형과 이름, 순서를 주의해야 한다.

ERC20InterfaceTransfer 이벤트토큰이 이동할 때마다 로그를 남기고, Approval 이벤트는 approve 함수가 실행될 때 로그를 남긴다.


솔리디티 함수(function) 구성

1
function (<parameter types>) {internal | external | public | private} [pure | constant | view | payable] [(modifiers)] [returns (<return types>)]
  • parameter types
    • 함수에서 받아올 매개변수를 타입과 같이 선언
  • Visibility Keyword (internal, external, public, private)
    • Java와 C++에서의 Public, Private, Protected 같은 접근제어자(access control) 역할
    • 솔리디티 언어에서 스마트 컨트랙트 내의 **상태 변수(State variable)**와 함수에 적용할 수 있는 Visibility 4가지
      • internal : Smart contract의 interface로 비공개한다. 계약서(Contract)의 해당 내용을 비공개한다는 의미로, 계약서 내에서만 사용하는 함수라는 것을 표시한다. 상태변수(state variable)는 internal이 기본값이다. 아무것도 적용하지 않을시 자동적으로 internal이 적용된다. 계약서 자신과 상속받은 계약서만 사용할 수 있다.
      • external : Smart contract의 interface로 공개한다. 계약서(Contract)의 해당 내용을 공개한다는 의미로, 계약서의 외부에서 사용하는 함수라는 것을 표시한다. 상태변수(state variable)는 external일수 없다. 계약서 내부에서 사용할 경우 this를 사용해서 접근해야 한다.
      • public : 공개함수라고 한다. 공개 기능은 계약 인터페이스의 일부이며 내부적으로 혹은 메시지를 통해 호출할 수 있다. 공개 상태 변수의 경우 자동 getter 함수가 생성된다.
      • private : 비공개함수라고 한다. 비공개함수는 계약서 내부에서도 자신만 사용하는 함수라는 것을 표시한다. 상태변수와 함수 모두 파생된 계약이 아닌 정의된 계약에서만 볼 수 있다.
  • 함수 동작 관련 키워드
    • pure : storage에서 변수를 읽어오거나 쓰지 않는 함수임을 명시.
    • constant, view : 상태를 변경하지 않는 함수임을 명시.
    • payable : 임금을 받을 수 있는 함수임을 명시.

Contract (Simple Token)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
~ 생략 ~

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 currentAllownace = _allowances[spender][msg.sender]; // 삭제
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);
}
}

ERC20Interface에서는 함수의 형태만 선언하고, 함수의 내용은 SimpleToken 컨트랙트에서 사용한다.

contract SimpleToken 뒤에 is ERC20Interface를 붙여서 SimpleToken 컨트랙트가 ERC20Interface 함수를 사용할 수 있다고 선언한다.
이렇게 사용하면 SimpleToken 안에서 ERC20Interface에 선언된 함수와 이벤트를 사용할 수 있다.

또한 이중으로 매핑된 _allowances를 확인할 수 있다. _allowances 변수는 일종의 이중 객체이다.
객체의 키는 **OWNER의 address(주소값)**이며, 토큰을 양도받은 SPENDER에 대한 객체입니다.

  • KEY : OWNER의 address
  • VALUE : ( KEY : SPENDER의 address , VALUE : 맡겨둔 TOKEN의 개수 )
1
2
3
4
5
6
7
contract SimpleToken is ERC20Interface {
mapping (address => uint256) private _balances;
mapping (address => mapping (address => uint256)) public _allowances;

~ 생략 ~

}


ERC-20 함수

  • totalSupply : 해당 스마트 컨트랙트 기반 ERC-20 토큰의 총 발행량 확인
  • **balanceOf** : owner가 가지고 있는 토큰의 보유량 확인
  • transfer : 토큰을 전송
  • approve : spender에게 value 만큼의 토큰을 인출할 권리 부여함. 이 함수 사용시 반드시 Approval 이벤트 함수를 호출해야 함
  • allowance : owner가 spender에게 양도 설정한 토큰의 양을 확인
  • transferFrom : spender가 거래 가능하도록 양도 받은 토큰을 전송

→ totalSupply / balanceOf / transfer는 쉽게 이해 가능하지만, approve / allowance / transferFrom 은 이해가 어려울 수 있음.

ERC-20에서는 토큰의 owner가 직접 토큰을 다른 사람에게 전송할 수도 있으나, 토큰을 양도할 만큼 등록해놓고, 필요시 제삼자를 통해 토큰을 양도할 수 있다.

  • transfer 함수 : 직접 토큰을 다른 사람에게 전송할 때
  • approve, allowance, transferFrom 함수 : 토큰을 등록하는 방식 사용시 사용

approve, allowance, transferFrom 의 실행 흐름.

  • approve : 지갑의 주인이 토큰을 EXCHANGE에 자신이 가진 토큰의 수보다 적은 수량을 거래 가능 하도록 맡길 수 있다.
  • allowanve : OWNER와 EXCHANGE 값을 입력하여 몇개가 등록 되어있는지 확인 가능.
  • transferFrom : EXCHANGE가 BUYER가 구매 신청해둔 금액에 대해 OWNER가 맡겨둔 토큰을 판매한다.

totalSupply - ERC20 함수

토큰의 총 발행량을 반환한다.

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

balanceOf - ERC20 함수

매핑된 값인 _balanceOf 에서 입력한 address인 account가 가진 토큰의 수를 리턴한다.

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

transfer - ERC20 함수

내부 함수인 _transfer 를 호출한다. 호출이 정상적으로 이뤄졌을 경우 Transfer event를 발생시킨다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}

**_transfer**는 **require**를 통해 세가지 조건을 검사한다.

  1. 보내는 사람의 주소가 잘못되었는지 체크.
  2. 받는 사람의 주소가 잘못되었는지 체크.
  3. t**ransfer 함수를 실행한 사람(sender)**이 가진 **토큰(senderBalance)**이 신청한 값(amount)보다 많은 토큰을 가지고 있는지 체크.

여기서 체크 함수로 require를 사용했다. 만약 해당 조건문 안에 exception이 throw 된다면 실행할 수 없다.

위의 세 조건을 충족하면, 실행한 사람(sender)이 가진 토큰의 지갑에서 토큰의 개수만큼 빼고, 받을 사람(recipient)의 토큰 지갑에 개수만큼 더해준다.


approve - ERC20 함수

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);
}

내부 함수인 _approve 를 호출한다.

_approve 에서는 내가 **토큰을 양도할 상대방(spender)**에게 양도할 값(amount)를 allowances에 기록한다. 그리고 Approval event를 호출하여 기록한다. 이 상태는 양도가 실제로 이뤄진 것이 아닌, 양도를 할 주소와 양을 정한 것이다.

approve단순 변경을 위한 함수이기 때문에 내부적으로 값을 올리고, 내리는 increaseApprovaldecreaseApproval 함수를 사용하기도 한다.

approvespender당신의 계정으로부터 amount 한도 하에 여러번 출금하는 것을 허용한다. 이 함수를 여러번 호출시, 단순히 허용량을 amount 로 재설정한다.


allowance - ERC20 함수

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

**allowance** 는 입력한 두개의 주소값에 대한 _allowances, 즉, owner가 spender에게 토큰을 등록한 양을 반환한다.


transferFrom - ERC20 함수

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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);
}

**transferFrom**양도를 수행하는 거래 대행자(msg.sender)가 sender가 허락한 값만큼 상대방(recipient)에게 토큰을 이동한다.

이동을 위해 **_transfer함수를 실행**시킨다. _transfer 에서는 양도를 하는 sender의 잔고를 amount 만큼 줄이고, recipient의 잔고를 amount 만큼 늘린다.

**_transfer함수 실행이 완료되고, **require 를 모두 통과하면 currentAllowance를 체크하여 **_approve 함수를 실행**한다.