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 기반 토큰을 만들고, 만든 토큰을 이동하는 실습 진행.
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 _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);
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 : 비공개함수라고 한다. 비공개함수는 계약서 내부에서도 자신만 사용하는 함수라는 것을 표시한다. 상태변수와 함수 모두 파생된 계약이 아닌 정의된 계약에서만 볼 수 있다.
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 _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 값을 입력하여 몇개가 등록 되어있는지 확인 가능.
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**를 통해 세가지 조건을 검사한다.
보내는 사람의 주소가 잘못되었는지 체크.
받는 사람의 주소가 잘못되었는지 체크.
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는 단순 변경을 위한 함수이기 때문에 내부적으로 값을 올리고, 내리는increaseApproval 과 decreaseApproval함수를 사용하기도 한다.
approve는 spender 가 당신의 계정으로부터 amount 한도 하에 여러번 출금하는 것을 허용한다. 이 함수를 여러번 호출시, 단순히 허용량을 amount 로 재설정한다.
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 함수를 실행**한다.