Post

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 등의 기능을 추가할 생각이다.

자 이제 배포를 해보자!
난 아래와 같이 생성자 파라미터를 구성했다. (개인 마음대로 변경해도 상관없다!)

Deploy

그런 뒤에 Transet 버튼을 누르면

아래와 같이 배포 계약이 실행된다.

Meta1

그리고 배포 한 뒤에 아래의 토큰 가져오기를 클릭하고 배포한 Contract주소를 넣고 가져오기를 실행하면

JTK

아까 배포시에 설정했던 이름의 토큰이 발행된걸 확인할 수 있다!

또한, Deploy 후 Remix에서 파라미터를 넣어서 함수들을 테스트해볼 수 있다.

JTK

EtherScan에 들어가 Contract에 대해 확인이 가능하다.

JTK

This post is licensed under CC BY 4.0 by the author.