ERC-20 코드 살펴보기(feat. OpenZeppelin)
본 글은 OpenZeppelin 4.x 버전을 기반으로 작성되었습니다.
ERC-20은 이더리움 블록체인 네트워크에서 정한 토큰의 표준 규격(세부적인 내용은 이전 포스팅 참고)을 의미합니다. ERC-20이 제공하는 기능 중 일부는 다음과 같습니다.
- 한 계정에서 다른 계정으로 토큰 전송
- 계정의 현재 잔여 토큰(잔액) 확인
- 계정 내 토큰을 제3자가 사용할 수 있게 승인. (DEX 등 가능)
즉, 토큰에 필요한 다양한 기능의 규격을 통일하여 거래소 등에서 활용할 수 있게 만든 것이 특징입니다. 해당 내용을 준수하지 않는 이더리움 기반 토큰은 거래소에 상장될 수 없습니다.
이번 글에서는 코드를 통해 ERC-20를 살펴보도록 하겠습니다.
오픈제플린으로 살펴보는 ERC-20
오픈제플린(OpenZeppelin)은 솔리디티 기반의 스마트 컨트랙트를 개발할 수 있는 표준 프레임워크입니다. 스크래치로 ERC20 토큰을 작성할 수는 있으나, 테스트를 거친 템플릿을 사용하여 더 안전하고 효율적인 컨트랙트를 빠르게 구축할 수 있습니다.
오픈제플린에서는 ERC-20, ERC-721, ERC-1155등 다양한 토큰 표준을 제공합니다. 가장 최근 버전인 4.x 을 기반으로 살펴볼 예정이며, 전체 코드는 아래 링크에서 확인 가능합니다.
오픈제플린 ERC20은 다음과 같이 코드가 구성되어 있습니다.
IERC20: 모든 ERC20 구현이 준수해야하는 인터페이스IERC20Metadata:name,symbol,decimals등의 설정ERC20: ERC20의 실제 구현체
[OpenZeppelin] IERC20와 IERC20MetaData
IERC는 ERC-20에 필요한 함수 등 구현해야할 목록으로 생각하면 됩니다. ERC20을 위해서는 다음 함수가 구현되어야 합니다. 즉, 거래소에 상장된 토큰은 아래의 함수가 모두 구현되어 있습니다.
totalSupply(): 전체 공급량. 전체 발행된 토큰양 반환balanceOf(account): 잔고. 특정 주소가 소유한 토큰 양 반환approve(spender, amount): 승인. 다른 사람에게 토큰 사용 허용. 토큰의 양을 확인하여 거래량 제한allowance(owner, spender): 허용.owner가spender에게 허락한 토큰 개수 확인transfer(to, amount): 전송. 함수 호출 주소에서 개인 계정으로 송금transferFrom(sender, recipient, amount): 대리 전송.approve이후 사용자 간 송금 제공
IERC20MetaData 에는 ERC-20의 name, symbol, decimals 함수와 관련한 인터페이스입니다.
name(): 토큰의 이름.symbol(): 토큰의 심볼. (이름의 축약어로 이해할 수 있음)decimals(): 소수점 단위 설정.
일반적으로 decimal은 18로 설정합니다. 이더(ether)와 단위와 같으며 대다수 이제 표준처럼 사용하고 있습니다.
[OpenZeppelin] ERC20
ERC20 함수는 6개이지만 실제 코드에서 핵심적인 내용은 변수 3개와 함수 2개에 담겨있습니다.
우선 변수 3개는 코드와 함께 살펴보겠습니다.
mapping(address => uint256) private _balances;
mapping(address => mapping(address => uint256)) private _allowances;
uint private _totalSupply;맵핑(mapping) 변수는 C++에서 map, Java에서 HashMap, Python에서 dict, JavaScript에서 Map 의 역할을 하는 솔리디티 변수입니다.
keccak256입니다._balances는 주소와 각 주소가 보유한 토큰량을 쌍으로 들고 있는 변수이며, _allowances는 주소 A의 토큰을 주소 B가 대리 사용가능하며 이때 사용 가능한 토큰량을 저장하고 있는 변수입니다. 그리고 _totalSupply는 전체 발행된 토큰량을 저장합니다.
그리고 핵심 함수 2개는 다음과 같습니다.
_approve(owner, spender, amount): 승인 함수_transfer(from, to, amount): 전송 함수.
_approve의 함수 내부를 살펴보겠습니다.
function _approve(
address owner,
address spender,
uint256 amount
) internal virtual {
require(owner != address(0), "ERC20: approve from the zero address");
require(spender != address(0), "ERC20: approve to the zero address");
_allowances[owner][spender] = amount;
emit Approval(owner, spender, amount);
}코드를 한 줄 씩 순서대로 살펴보면 다음과 같습니다.
- 보내는 주소와 받는 주소가 zero address인지 체크합니다.
_allowances에owner-spender매핑에amount값을 갱신합니다. 즉, 토큰 소유주-토큰 가용자 매칭과 가용 토큰양을 기록해둡니다.Approval이벤트를 발생시킵니다. (로그 기록)
_transfer 함수 내부를 살펴보겠습니다.
function _transfer(
address from,
address to,
uint256 amount
) internal virtual {
require(from != address(0), "ERC20: transfer from the zero address");
require(to != address(0), "ERC20: transfer to the zero address");
_beforeTokenTransfer(from, to, amount);
uint256 fromBalance = _balances[from];
require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
unchecked {
_balances[from] = fromBalance - amount;
}
_balances[to] += amount;
emit Transfer(from, to, amount);
_afterTokenTransfer(from, to, amount);
}코드를 한 줄 씩 순서대로 살펴보면 다음과 같습니다.
- 보내는 주소와 받는 주소가 zero address인지 체크합니다.
_beforeTokenTransfer와_afterTokenTransfer는 토큰 전송 이전과 이후 추가적인 내용을 작성할 수 있는 선택적 함수입니다. 생략해도 무방합니다.from의 잔액을 불러옵니다.- 잔액이
amount보다 많은지 체크합니다. from에서는 보내는 양을 빼주고,to에 그만큼 더해줍니다.Transfer이벤트를 발생시킵니다. (로그 기록)
결국 간단하게 설명하면 잔액 체크 후, 보내는 계좌에서 빼고 받는 계좌에서 더해주는 간단한 코드입니다.
이제 위 변수와 함수를 사용하면 ERC-20에 필요한 기능을 모두 구현할 수 있습니다.
totalSupply():_totalSupply변수 사용balanceOf():_balances변수 사용approve():_approve함수 사용allowance():_allowances사용transfer():_transfer사용transferFrom(): (1)allowance함수로 체크후, (2)_approve함수 갱신, (3)_transfer함수 사용
간단하게 적긴 했으나, 내부에는 여러 require문이 존재합니다. 내부 코드 점검에는 크게 3가지 포인트만 잊지 않으면 됩니다.
- 잔액과 전송되는 토큰량이 음수가 되게 하지 말 것.
- 데이터가 변경될 때, 연관되는 데이터를 모두 고려할 것.
- 불필요한 연산은 최소화 시킬 것. (예시.
_transfer에서잔액>전송량을require로 체크했으므로 마이너스 연산에unchecked키워드를 사용하여 연산 효율성 증가)
[OpenZeppelin] 그 외 구현
그렇다면 꼭 위에 정해진 함수만 작성해야할까요? 정답부터 말하면 그렇지 않습니다. 위 기능은 기본 기능이고, 토큰의 목적에 따라 다양한 함수를 추가적으로 구현할 수 있습니다. 오픈제플린에는 4개의 함수가 추가적으로 구현되어 있습니다.
increaseAllowance()/decreaseAllowance():allowance양 증감_mint: 토큰 민팅._totalSupply와_balances[owner]에 토큰 동시 증가_burn: 토큰 소각._totalSupply와_balances[owner]에 토큰 동시 감소
[OpenZeppelin] ERC20 Extensions
오픈제플린에서도 ERC-20에 여러 추가 기능을 구현한 템플릿이 일부 존재합니다.
ERC20Burnable: 토큰 소각과 대리 소각 가능ERC20Capped: 총 공급량 한정ERC20Pausable: 토큰 전송 일시 중지 가능ERC20Snapshot: 과거 상태를 저장하는 스냅샷 기능ERC20Votes: 투표 및 투표 위임 기능ERC20VotesComp: 투표 및 투표 위임 기능(Compound와 호환)ERC20Wrapper: 서로 다른 종류의 토큰으로 변환(민팅 및 소각 사용)ERC20FlashMint: 플래시론 지원
ERC-20 토큰 종류
이더스캔에서 목록을 살펴볼 수 있습니다. 거래가 안되고 있는 토큰을 포함하여 940개의 토큰이 이더스캔에 잡히며, 총 54만개 정도의 내부 컨트랙트 함수가 있다고 합니다. 대표적으로 알려져 있는 토큰은 다음과 같습니다.
- BNB(BNB)
- Tether USD(USDT)
- USD Coin(USDC)
- Wrapped BTC(BTC)
- ChainLink Token(LINK)
- Uniswap(UNI)
그 외에도 해당 ERC20 토큰은 이더스캔에서 내부 작성된 컨트랙트를 살펴볼 수 있습니다.

마치며
ERC-20을 시작으로 ERC-721, ERC-1155 등의 표준을 하나씩 살펴보고, 핵심적인 스마트 컨트랙트 사례(DeFi 등)를 살펴볼 예정입니다.
그 외에도 솔리디티와 함께 구체적으로 살펴보기 위해서는 아래 주제로 살펴볼 부분도 있는데, 기회가 되면 하나씩 다뤄보겠습니다.
- overflow 방지를 위한 SafeMath 라이브러리
- 접근 권한 제한과 OnlyOwner
- ERC-223과 ERC-777
- 각 연산 메모리 영역(storage, memory, colldata, stack)
