ERC란 Ethereum Request for Comment의 약자로, 이더리움 개발을 위한 요구 사항을 담은 문서 규약이다. 이 중 ERC-20은 대체 가능한(Fungible)한 토큰을 규정하는 표준이며, 가상화폐나 유틸리티 토큰으로 사용된다.
이 글에서는 ERC-20이 무엇인지 알아보고, ERC-20을 따르는 스마트 컨트랙트를 작성해본다.
1. ERC-20이란?
ERC-20은 이더리움 블록체인에서 대체 가능한 자산을 위한 기술 표준으로, 이더리움 생태계 내에서 자산이 올바르게 동작하기 위해 따라야 하는 일반적인 규칙 세트를 의미한다. 이러한 규칙은 이더리움 블록체인 내에서 ERC-20 자산이 전송되는 방식과 공급 및 주소 잔액이 일관되게 기록되는 방식과 관련이 있다.
* 여기에서 '대체 가능하다'라는 것은 같은 종류인 모든 자산이 동일한 가치와 속성을 가져 다른 자산과 대체와 호환이 가능하다는 것이다. 예를 들어, 1,000원짜리 지폐는 다른 1,000원짜리 지폐와 완전히 동일한 가치를 지니고 있기 때문에 서로 교환해도 아무런 문제가 없다.
반면, '대체 불가능하다'라는 것은 모든 자산이 각각 고유한 가치를 지니고 있어 다른 자산들과 대체와 호환이 불가능한 것이다. 예를 들어, 모나리자와 진주 귀걸이를 한 소녀 작품은 각각 세상에 하나뿐인 그림으로 각각 고유한 특성과 가치를 지니고 있어 서로 교환될 수 없다.
ERC-20는 필수적인 여섯 가지 기능을 따라야 하며, 선택적으로 세 가지 기능을 가질 수 있다.
먼저 필수적으로 갖춰야 하는 여섯 가지 기능은 다음과 같다.
1) 총발행량 (totalSupply) : 발행될 총 토큰의 수 → 전제 유통량 확인
2) 잔고(balanceOf) : 토큰 소유자 계정의 토큰 잔액 → 사용자 계정의 잔액 확인
3) 전송(transfer) : 내가 가진 토큰을 다른 사람에게 직접 전송 → 직접적인 토큰 전송 수행
4) transferFrom : 다른 사람이 미리 승인한 토큰을 대신 전송 → 제3자가 사용자 대신 토큰 전송 수행
5) 승인(approve) : 특정 주소가 일정량의 토큰을 대신 사용할 수 있도록 승인 → 타인이 내 토큰을 대신 전송하거나 사용하는 것을 허용
6) allowance : 특정 주소가 사용 가능한 승인된 토큰의 잔액 → 승인받은 주소가 얼마나 많은 토큰을 사용할 수 있는지 확인
[transfer vs transferFrom]
transfer는 사용자가 자신의 토큰을 직접 다른 주소로 전송할 때 사용되며, transferFrom은 대리인이 다른 사람의 토큰을 승인된 범위 내에서 전송할 때 사용된다. transferFrom의 경우, 소유자가 대리인에게 권한을 부여하기 위해 사전에 approve 함수로 승인이 필요하다.
예를 들어, Alice가 Bob에게 50개의 토큰을 전송하려는 상황을 생각해보자.
Alice는 직접 Bob에게 50개의 토큰을 전송할 수 있다.
transfer(address(0xBob), 50);
Alice는 거래소에 자신의 토큰을 입금하고, 거래소가 Alice를 대신하여 Alice의 토큰을 Bob에게 전송할 수도 있다.
approve(address(0xExchange), 100); // Alice가 거래소(0xExchange)에게 100개 토큰 사용 권한 부여
transferFrom(address(0xAlice), address(0xBob), 50); // 거래소가 Alice의 50개 토큰을 Bob에게 전송
세 가지 옵션 기능은 다음과 같다.
1) name : 토큰의 이름
2) symbol : 토큰의 간략한 표기 방식
3) decimals : 토큰 단위의 소수점 자릿수 (토큰을 분할할 수 있는 정도)
2. ERC-20 Interface
위에서 살펴본 필수 기능들과 옵션 기능을 포함하는 ERC-20 컨트랙트의 인터페이스이다.
//Methods
function name() public view returns (string)
function symbol() public view returns (string)
function decimals() public view returns (uint8)
function totalSupply() public view returns (uint256)
function balanceOf(address _owner) public view returns (uint256 balance)
function transfer(address _to, uint256 _value) public returns (bool success)
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success)
function approve(address _spender, uint256 _value) public returns (bool success)
function allowance(address _owner, address _spender) public view returns (uint256 remaining)
//Events
event Transfer(address indexed _from, address indexed _to, uint256 _value)
event Approval(address indexed _owner, address indexed _spender, uint256 _value)
이벤트라는 것은 블록체인 네트워크의 블록에 특정 값을 기록하는 것이다.
tansfer 함수와 approve 함수가 실행되면 각각 Transfer와 Approval 이벤트를 호출하여 로그를 남긴다.
3. ERC-20 컨트랙트 구현
ERC-20 표준을 따르는 토큰 컨트랙트는 다음과 같이 작성할 수 있다.
//Tether.sol
pragma solidity ^0.5.0;
contract Tether {
string public name = 'Moke Tether Token';
string public symbol = 'mUSDT'; //종목 코드
uint256 public totalSupply = 1000000000000000000000000; //총공급: 토큰 100만 개
uint8 public decimals = 18;
//전송 이벤트
event Transfer(
address indexed _from,
address indexed _to,
uint _value
);
//전송 승인 이벤트 *승인은 언제나 소유자(메시지 발신자)가 해야 함.
event Approval(
address indexed _owner,
address indexed _spender,
uint _value
);
mapping(address => uint256) public balanceOf; //현재 계약을 하는 사람에게 남은 액수
mapping(address => mapping(address => uint256)) public allowance; //
constructor() {
balanceOf[msg.sender] = totalSupply;
}
//테더 전송 함수
function tranfer(address _to, uint256 _value) public returns (bool success) {
require(balanceOf[msg.sender] >= _value);
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
emit Transfer(msg.sender, _to, _value);
return true;
}
//제3자가 전송 함수
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success){
require(balanceOf[_from] >= _value);
require(allowance[msg.sender] >= _value);
balanceOf[_from] -= _value;
balanceOf[_to] += _value;
allowance[msg.sender][_from] -= _value;
emit Transfer(_from, _to, _value);
return true;
}
//전송 승인 함수
function approve(address _spender, uint256 _value) public returns (bool success){
allowance[msg.sender][_spender] = value;
emit Approval(msg.sender, _spender, _value);
return true;
}
}
OpenZeppelin 라이브러리를 사용하여 직접 구현할 필요 없이 토큰 컨트랙트를 간편하게 작성할 수도 있다.
pragma solidity ^0.8.0;
// OpenZeppelin의 ERC-20 표준을 임포트
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyToken is ERC20, Ownable {
uint public initialSupply = 1000000000000000000000000
// constructor는 토큰의 이름과 심볼을 설정하고, 지정된 초기 공급량만큼의 토큰을 배포자의 지갑에 발행
constructor(uint256 initialSupply) ERC20("Moke Tether Token", "mUSDT") {
_mint(msg.sender, initialSupply);
}
}