OpenZeppelin의 표현을 빌리자면 "블록체인 상에서 표현되는 무언가"이다.
그것은 돈일수도 있고 시간, 가상 펫, 회사가 공유하는 것 등 어떤 것이든 될 수 있다. 어떤 것을 토큰으로 표현한다면, 우리는 Smart Contract
으로 하여금 그것을 생성하거나 삭제하거나, 교환하는 등 상호작용할 수 있도록 할 수 있다.
흔히 토큰의 개념을 둘러싸고 혼동이 발생하곤 하는데 그것은
Token Contract
와 실제Token
의 개념이 혼용되어있기 때문이다.Token Contract
쉽게 말해서 이더리움 스마트컨트랙을 말한다. 따라서 "Sending Token"의 의미는, 누군가 써놓거나 배포한 Contract의 method를 부른다 라는 말과 같다고 생각하면 된다.
- 요즘은 Address랑 Balance(계좌)를 연결하고 추가적으로 몇가지 method를 통해 Balance 안의 것을 빼고 더하는 정도의 역할정도만 한다. (쉽게 말해 계좌 개설, 입출금 정도 한다는 말)
Token
위에서 언급한
Balance
가 Token 그 자체를 의미한다. 따라서 "Has Tokens", 즉 토큰을 가진다는 의미는 Token Contract 안의Balance
가 0이 아니라는 말이다. 그리고 이Balance
는 돈, 게임 경험치, 소유권 유무, 투표권 등등 여러가지의 형태로 고려될 수 있다.
정리해보자면 Token
이란 돈,투표권,게임 경험치 등이 Balance
라 불리는 변수에 저장된 형태이며, 우리는 그것을 Smart Contract를 통해 다룰 수 있다정도로 이해하면 충분하지 않을까 싶다.
이제 Token에 대해 이해했으니 본격적으로 이러한 Token을 만드는 ERC-20에 대해 이야기해보자. OpenZeppelin의 ERC-20 코드를 참고하였다.
이더리움 블록체인 네트워크에서 정한 토큰의 표준 규격 중 하나를 의미한다. 대체 가능한 토큰(Fungible token) 관련 표준 규격이며, 단순함 때문에 기능들이 제한되긴 하지만 가장 널리 알려진 규격이다.
❗ERC?
Ethereum Request for Comment의 약어로, 직역하자면 이더리움 관련 아이디어 제시를 했으니 평가해달라는 것.이더리움 네트워크의 모든 것은
Smart Contract
에 의해 작동하기 때문에 각 smart contract가 어떤 표준도 없이 우후죽순으로 생성되고 실행된다면 이더리움 생태계는 너무나 복잡해질 것이다. 따라서 각 Smart Contract들이 서로 상호운용될 수 있도록 커뮤니티에서 몇가지 표준을 개발했는데, 그것을 ERC, 또는 EIP라고 부른다.그래서 토큰 관련 개발할 때 생태계가 잘 굴러갈 수 있도록 이 표준(ERC)을 지켜서 개발하라는 것이다. 이러한 표준을 지키지 않는다면 코인을 개발하더라도 거래소에 상장시킬 수 없다.
❗Fungible Token?
대체 가능한 토큰을 의미. 하나의 토큰이 다른 토큰과 정확히 같은 가치를 가지는 것. 다른 토큰과 비교해서 특별한 권리를 가지거나 하지 않음. 1달러 지폐가 다른 1달러 지폐와 정확히 같은 가치를 가지며 다른 특별한 권리를 포함하지 않는 것과 같은 의미.
<-> NonFungible Token(NFT)
대체 불가능한 토큰을 의미하며, 기본적인 Token의 특성을 띄지만, unique한 디지털 파일과 일반 토큰이 연결되어 다른 토큰과 확실히 구별점을 가지는 토큰을 의미.
IERC20
: 모든 ERC20 구현이 지켜야하는 인터페이스IERC20Metadata
: name
, symbol
, decimals
를 포함하는 확장된 ERC20 인터페이스ERC20
: ERC20 인터페이스 구현다음과 같이 import로 가져와준 뒤, ERC20 contract에 상속시켜주자.
import "./IERC20.sol";
import "./extensions/IERC20Metadata.sol";
import "../../utils/Context.sol";
contract ERC20 is Context, IERC20, IERC20Metadata {
}
mapping(address => uint256) private _balances;
mapping(address => mapping(address => uint256)) private _allowances;
int256 private _totalSupply;
string private _name;
string private _symbol;
constructor(string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
}
ERC20 토큰을 만들면서 구현해야할 가장 중요한 목록들이다.
function totalSupply() public view virtual override returns (uint256) {
return _totalSupply;
}
function balanceOf(address account) public view virtual override returns (uint256) {
return _balances[account];
}
to
에게 amount
만큼의 토큰 전송 후 성공적으로 전송됐는지 boolean 값으로 반환function transfer(address to, uint256 amount) public virtual override returns (bool) {
address owner = _msgSender();
_transfer(owner, to, amount);
return true;
}
spender
가 owner
를 대신하여 지출할 수 있는 남아있는 Token 수 반환 (owner
가 허락한 토큰 수) function allowance(address owner, address spender) public view virtual override returns (uint256) {
return _allowances[owner][spender];
}
spender
가 사용할 수 있는 토큰의 수 제한. 따라서 allowance() 값 변경 가능. 성공적으로 작동됐는지 boolean 값 반환. (마지막에 Approval event를 emit해야함)function approve(address spender, uint256 amount) public virtual override returns (bool) {
address owner = _msgSender();
_approve(owner, spender, amount);
return true;
}
approve
이후 허락받은 제 3자가 from
에서 to
로 amount
만큼의 토큰을 옮기고 잘 작동했는지 boolean 값 반환. 즉, 대리송금 기능. (마지막에 transfer event를 emit해야함) function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) {
address spender = _msgSender();
_spendAllowance(from, spender, amount);
_transfer(from, to, amount);
return true;
}
IERC20 인터페이스에 name
,symbol
, 그리고 decimals
에 대한 메타데이터 함수를 제공한다.
function name() public view virtual override returns (string memory) {
return _name;
}
function symbol() public view virtual override returns (string memory) {
return _symbol;
}
function decimals() public view virtual override returns (uint8) {
return 18;
}
IERC20 인터페이스를 구현시키기 위한 여러 함수들이 들어있다. 그 중 가장 핵심적인 함수 두가지를 살펴보자.
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;
// Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by
// decrementing then incrementing.
_balances[to] += amount;
}
emit Transfer(from, to, amount);
_afterTokenTransfer(from, to, amount);
}
to
와 from
의 주소값이 0이 아닌지 확인한다.befroeTokenTransfer
와 _afterTokenTransfer
는 토큰 전송 전과 후에 추가적으로 어떤 것(burning, minting)을 할 수 있도록 해주는 함수이다. 생략해도 무방하다.from
의 계좌 금액을 fromBalance
에 저장한 뒤, 전송 금액인 amount
와 양을 비교한다.fromBalance
에서 amount
만큼을 뺀 값을 from
의 계좌에 저장한다. (송금 후 계좌의 토큰 수 감소)amount
만큼의 토큰을 to
의 계좌에 더해준다. (입금 후 계좌의 토큰 수 증가)Transfer
event를 emit 해줌으로서 로그에 기록한다.spender
가 얼마만큼의 owner
의 토큰을 사용하도록 할건지 승인하는 함수 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);
}
owner
와 spender
의 주소값이 0이 아닌지 확인한다.owner
가 spender
에게 승인할 토큰의 수인 amount
를 _allowances의 owner-spender 쌍의 할당값으로 할당한다. Approval
event를 emit 해줌으로서 로그에 기록한다.위의 핵심적인 함수들 외에 추가적인 기능을 할 수 있는 함수들이 몇가지 있다.
spender
에 대한 owner
의 allowance를 새로 업데이트 시킨다.ERC20 또한 Extensions를 통해 더 많은 기능들을 구현할 수 있다. 정말 다양한 extensions이 있음으로 첨부링크를 들어가 한번 둘러보는 것도 좋을 듯 하다.
Fungible token의 기준인 ERC20에 대해 살펴보았으니 다음편에선 Non-Fungible Token(NFT)의 기준인 ERC721의 코드에 대해 살펴볼 예정이다. 그리고 기회가 된다면 ERC20과 ERC721을 이용해 실제 Token을 발행해보며 더 깊게 이해해보는 시간을 갖도록 하겠다.