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)