Smart Contract으로 ERC-20 토큰 만들기 (OpenZepplin ERC20 소스분석)
ERC-20
ERC-20(Ethereum Request for Commnet 20)은 EIP(Ethereum Improvement Propasls)에서 관리하는 공식 프로토콜 중 하나로 Ethereum네트워크에서 유통 가능한 토큰의 표준이다. 요약하자면, 우리가 흔히 부르는 암호화폐 또는 코인이 여기에 해당된다.
Contract 작성 전에
제일 먼저 이전에 포스팅 한 Remix로 Solidity 로컬 개발 환경 만들기을 읽고 해당 개발환경을 갖추고 오길 바란다. 이 글은 해당 환경에서 진행된다.
그다음으론 OpenZepplin
을 이용해 ERC20 SmartContract을 구현할 것이다.
그러니 OpenZepplin을 npm을 통해 다운받아주자
1
npm install @openzeppelin/cli
그 다음으론 rinkeby 테스트 넷에서 배포 후 테스트 해볼 것이기 때문에 rinkeby faucet 사이트에 들어가서 test ether를 받는다.
이 사이트에 들어가서 본인이 사용하는 지갑의 주소를 넣어주면 된다. (참고로 잘 안준다… 열심히 노가다 해야된다.)
- https://faucet.rinkeby.io/
- https://faucets.chain.link/rinkeby
- https://rinkebyfaucet.com/
자 이제 준비는 다 됐다! Smart Contract를 작성해보자
ERC20 토큰 소스 분석
배포하는 토큰을 잘 사용하기 위해선 ERC20의 소스를 살펴볼 필요가 있다.
ERC20.sol
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/ERC20.sol)
pragma solidity ^0.8.0;
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;
uint256 private _totalSupply;
string private _name;
string private _symbol;
constructor(string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
}
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;
}
function totalSupply() public view virtual override returns (uint256) {
return _totalSupply;
}
function balanceOf(address account) public view virtual override returns (uint256) {
return _balances[account];
}
function transfer(address to, uint256 amount) public virtual override returns (bool) {
address owner = _msgSender();
_transfer(owner, to, amount);
return true;
}
function allowance(address owner, address spender) public view virtual override returns (uint256) {
return _allowances[owner][spender];
}
function approve(address spender, uint256 amount) public virtual override returns (bool) {
address owner = _msgSender();
_approve(owner, spender, amount);
return true;
}
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;
}
function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {
address owner = _msgSender();
_approve(owner, spender, allowance(owner, spender) + addedValue);
return true;
}
function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {
address owner = _msgSender();
uint256 currentAllowance = allowance(owner, spender);
require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero");
unchecked {
_approve(owner, spender, currentAllowance - subtractedValue);
}
return true;
}
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);
}
function _mint(address account, uint256 amount) internal virtual {
require(account != address(0), "ERC20: mint to the zero address");
_beforeTokenTransfer(address(0), account, amount);
_totalSupply += amount;
_balances[account] += amount;
emit Transfer(address(0), account, amount);
_afterTokenTransfer(address(0), account, amount);
}
function _burn(address account, uint256 amount) internal virtual {
require(account != address(0), "ERC20: burn from the zero address");
_beforeTokenTransfer(account, address(0), amount);
uint256 accountBalance = _balances[account];
require(accountBalance >= amount, "ERC20: burn amount exceeds balance");
unchecked {
_balances[account] = accountBalance - amount;
}
_totalSupply -= amount;
emit Transfer(account, address(0), amount);
_afterTokenTransfer(account, address(0), amount);
}
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);
}
function _spendAllowance(
address owner,
address spender,
uint256 amount
) internal virtual {
uint256 currentAllowance = allowance(owner, spender);
if (currentAllowance != type(uint256).max) {
require(currentAllowance >= amount, "ERC20: insufficient allowance");
unchecked {
_approve(owner, spender, currentAllowance - amount);
}
}
}
function _beforeTokenTransfer(
address from,
address to,
uint256 amount
) internal virtual {}
function _afterTokenTransfer(
address from,
address to,
uint256 amount
) internal virtual {}
}
함수를 하나씩 알아보자! 먼저 IERC20, IERC20metadata, Context 을 상속받고 있으며 IERC20 인터페이스를 기반으로 만들어져 있다.
변수
이름 | 설명 |
---|---|
_balances | 키값으로 주어지는 address 기준으로 해당 토큰 보유량을 반환하기 위한 매핑 변수이다. |
_allowances | 키값으로 주어지는 다른 주소가 spender에게 approve한 토큰의 수를 확인하는 매핑 변수이다. |
_totalSupply | 총 발행량 |
_name | 토큰명 |
_symbol | 토큰 심볼 (ex) ETH) |
Return 함수
name
토큰 이름을 반환한다.
decimal
기본값은 18이다. 이더리움은 단 wei이다 1Ether가 되기 위한 wei의 개수가 10^18이라 기본값이 18로 되어있다.
Gwei로 가스 비용을 표현하는데 0.00000~~1 이라 말하는거보다 1gwei이다 10gwei이다 이런식으로 환산해서 말하고 표현한다.
보통 배포할 때 사용되며 필자는 deploy시에 (10 ** decimal)을 곱해 코드를 단순하게 사용한다. (송금 및 토큰 스왑시에도 사용할 수 있는 개념이다)
totalSupply
인자로 받은 주소의 토큰 보유량을 반환한다.
allowance
spender에게 승인해준 amount 값을 반환한다.
Active 함수
Approve
spender에게 amount만큼의 토큰을 사용할 수 있게 승인해주는 함수다.
보통 spneder는 SmartContact 주소가 들어가게된다. SmartContract에서 토큰을 사용하는 계약을 실행하기 전에 해주지 않으면 토큰을 송금할 수 없기 때문에 애꿎은 가스비만 날라가고 실행되지 않을 수 있다 꼭 실행해 줘야 한다.
transfer
to 파라미터로 값의 주소로 amount 만큼의 송금을 실행한다.
transferFrom
from에서 to로 amount의 송금을 실행하는 함수이다.
이 함수는 SmartContract에서 발생시키기 때문에 실행 전에 from 주소에 대해 송금하는 amount 이상의 Approve를 받아야한다.
일단 여기까지가 ERC-20의 표준 인터페이스를 구현한 함수들이다.(name,decimal은 제외
아래로는 Openzepplin에서 제공하거나 확장 가능한 virtual 함수들이다. 간단하게 짚고 넘어가려한다.
increaseAllowance (확장x)
spender의 allowance값을 증가시킨다.
decreaseAllowance (확장x)
spender의 allowance값을 감소시킨다.
_mint
토큰을 발행하고, 계정에 토큰을 할당한다.
_beforeTokenTransfer
토큰 송금 실행 전에 실행되는 함수 (Openzepplin) 확장 함수에서만 사용됨
_afterTokenTransfer
토큰 송금 실행 후에 실행되는 함수 (Openzepplin) 확장 함수에서만 사용됨
몇개의 함수가 조금 더 있지만 ERC-20 인터페이스에서 제공하는 함수와 기능을 조금 확장한 것이라 지금 설명한 함수들을 참고해 코드를 살펴보면 충분히 알 수 있는 정도의 확장이다.
Contract 작성
MyToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MyToken is ERC20 {
address public _adminAddress;
constructor(string memory _name, string memory _symbol, uint _initSupply) ERC20(_name, _symbol){
_adminAddress = msg.sender;
_mint(msg.sender, _initSupply * (10 ** uint256(decimals())));
}
}
소스를 살펴보면 굉장히 간단한 것을 볼 수 있다.
_adminAddress는 송금을 위한 관리자 지갑을 따로 변수로 저장하여 사용하며,_mint함수를 통해 토큰을 발행한 뒤 관리자 계정에 할당하였다.
그리고 주요기능은 ERC20토큰을 이용해 사용할 것이며 추후에 Swap, Staking 등의 기능을 추가할 생각이다.
자 이제 배포를 해보자!
난 아래와 같이 생성자 파라미터를 구성했다. (개인 마음대로 변경해도 상관없다!)
그런 뒤에 Transet 버튼을 누르면
아래와 같이 배포 계약이 실행된다.
그리고 배포 한 뒤에 아래의 토큰 가져오기
를 클릭하고 배포한 Contract주소를 넣고 가져오기를 실행하면
아까 배포시에 설정했던 이름의 토큰이 발행된걸 확인할 수 있다!
또한, Deploy 후 Remix에서 파라미터를 넣어서 함수들을 테스트해볼 수 있다.
EtherScan에 들어가 Contract에 대해 확인이 가능하다.