본문 바로가기
Blockchain

[Blockchain] 이더리움 토큰 발행하기 (ERC-20 라이브러리 사용)

by 개발자 염상진 2022. 7. 14.

이더리움 블록체인에서 자신만의 토큰을 발행하고, 다른 토큰과 교환하거나 이더(ETH)와 교환해 현금화할 수 있습니다. 토큰을 발행하기 위해서는 ERC-20 표준 스펙을 준수하는 스마트 컨트랙트를 통해 발행이 가능합니다. ERC-20은 기본적으로 대체 가능한 토큰을 생산합니다. 

 

 

 

 

 

[Blockchain] 이더리움 토큰 발행 ERC-20 이란?

이더리움 블록체인 메인넷에서 토큰을 발행하고, 관리할 수 있습니다. 내가 만든 토큰으로 디앱을 구동하고, 특정 행동을 유인하는 수단으로 사용할 수 있습니다. ERC(Ethereum Request for Comment)는 이

about-tech.tistory.com

 

 

이더리움 테스트넷 토큰 발행하기

 

토큰을 발행하기 전 토큰 발행 스마트 컨트랙트를 배포하기 위해서는 소량이 이더(ETH)가 가스비로 소모됩니다. 테스트넷에서는 이더를 공짜로 받을 수 있습니다.

테스트 넷(Ropsten) 이더 받는 곳

  1. Ropsten Ether Faucet(1) 
  2. Ropsten Ether Faucet(2)

 

① 메타마스크 지갑과 테스트넷(Ropsten)을 연결합니다.

 

② SimpleToken.sol 파일을 생성합니다. 생성된 파일에 ERC-20 표준 코드를 입력합니다.

 

 

③ SimpleToken.sol 파일을 컴파일 합니다. 저의 경우 pragma solidity 버전을 0.8.7로 진행했습니다.

 

④ 컴파일한 스마트 컨트랙트를 배포합니다. 메타마스크 지갑과 연동해 Ropsten 테스트넷에 배포할 예정이므로 ENVIRONMENT는 "Injected Web3"로 지정합니다.

배포하고자 하는 컨트랙를 확인합니다. ERC20Interface가 아닌 SimpleToken 컨트랙트를 배포합니다. 

 

배포(Depoy)를 하면 메타마스크 지갑이 열립니다. 확인 버튼을 클릭합니다.

반드시 이더리움 메인넷이 아닌 테스트넷 위에서 배포를 확인합니다.

 

이더스캔에서 배포 현황을 확인할 수 있습니다.  트랜잭션이 올라가고 약 10~20초 정도 뒤 블록이 생성되면서 계약 배포가 완료됩니다. 

 

 

 

⑤ 컨트랙트의 주소값을 복사합니다. 메타마스크 하단의 "토큰 가져오기"를 클릭합니다.

 

 

 

이더리움 블록체인 테스트넷(Ropsten)에 배포한 SimpleToken 컨트랙트를 통해 새로운 토큰을 발행했습니다. 토큰의 총 발행량은 기본값인 1억개가 발행되었습니다. 

 

ERC-20 라이브러리 SafeMath

 

솔리디티에서는 2가지 변수 타입이 존재합니다. 부호가 존재하는 int와 부호가 존재하지 않는 uint 입니다. 예를 들어 int8은 -64에서 63까지의 데이터를 담을 수 있습니다. 만약 99라는 데이터를 int8에 할당하면 원하는 결과값이 나오지 않습니다. 바로 Overflow 문제가 발생했기 때문입니다.

오버플로, 언더플로는 용량이 초과하거나 마이너스로 떨어지는 현상입니다. 해커는 의도적으로 변수의 합당한 범위를 넘어선 데이터를 의도적으로 할당하면서 시스템 공격을 하기도 합니다. 솔리디티에서 오버플로, 언더플로를 방어할 수 있는 라이브러리 SafeMath가 있습니다.

 

 

SafeMath

pure 함수는  storage 영역에 저장되는 상태변수를 읽거나 쓰지 않는 경우 사용합니다. SafeMath 라이브러리의 함수들은 단순 연산 결과만을 반환하므로 상태 변수에 접근하지 않습니다.

internal 는 컨트랙트 내부 혹은 해당 컨트랙트를 상속하는 자식 컨트랙트에서 접근가능한 제어자입니다. 

// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.10;

interface ERC20Interface {
     // ~~ 이전 강의 참조 ~~
}

library SafeMath {
  	function mul(uint256 a, uint256 b) internal pure returns (uint256) {
		uint256 c = a * b;
		assert(a == 0 || c / a == b);
		return c;
  	}

  	function div(uint256 a, uint256 b) internal pure returns (uint256) {
	    uint256 c = a / b;
		return c;
  	}

  	function sub(uint256 a, uint256 b) internal pure returns (uint256) {
		assert(b <= a);
		return a - b;
  	}

  	function add(uint256 a, uint256 b) internal pure returns (uint256) {
		uint256 c = a + b;
		assert(c >= a);
		return c;
	}
}

contract SimpleToken is ERC20Interface {
    using SafeMath for uint256;

         // ~~ 이전 강의 참조 ~~

    function transferFrom(address sender, address recipient, uint256 amount) external virtual override returns (bool) {
        _transfer(sender, recipient, amount);
        emit Transfer(msg.sender, sender, recipient, amount);
        uint256 currentAllowance = _allowances[sender][msg.sender];
        require(currentAllowance >= amount, "ERC20: transfer amount exceeds allowance");
        _approve(sender, msg.sender, currentAllowance, currentAllowance - amount);
        return true;
    }

    function _transfer(address sender, address recipient, uint256 amount) internal virtual {
        require(sender != address(0), "ERC20: transfer from the zero address");
        require(recipient != address(0), "ERC20: transfer to the zero address");
        uint256 senderBalance = _balances[sender];
        require(senderBalance >= amount, "ERC20: transfer amount exceeds balance");
        _balances[sender] = senderBalance - amount;
        _balances[recipient] += amount;
    }
}

 

SafeMath 적용하기

SafeMath는 라이브러리로 제공되므로 스마트 컨트랙트에서 외부 라이브러리는 다음과 같이 사용합니다.

using SafeMath fro uint256;
using SafeMath fro uint32;
using SafeMath fro uint8;

 

+ / - 가 되는 부분에 SafeMath 라이브러리 제공 함수를 사용해 오버플로 / 언더플로를 예방할 수 있습니다.

data++;
data.add(1);
function transferFrom(address sender, address recipient, uint256 amount) external virtual override returns (bool) {
    _transfer(sender, recipient, amount);
    emit Transfer(msg.sender, sender, recipient, amount);
    uint256 currentAllowance = _allowances[sender][msg.sender];
    require(currentAllowance >= amount, "ERC20: transfer amount exceeds allowance");
    // 다음의 코드에서 currentAllowance.sub(amount)이 SafeMath 라이브러리 함수를 사용한 예시입니다.
    _approve(sender, msg.sender, currentAllowance, currentAllowance.sub(amount));
    return true;
}

function _transfer(address sender, address recipient, uint256 amount) internal virtual {
    require(sender != address(0), "ERC20: transfer from the zero address");
    require(recipient != address(0), "ERC20: transfer to the zero address");
    uint256 senderBalance = _balances[sender];
    require(senderBalance >= amount, "ERC20: transfer amount exceeds balance");
    _balances[sender] = senderBalance.sub(amount);
    _balances[recipient] = _balances[recipient].add(amount);
}

 

ERC-20 라이브러리 OwnerHelper, TokenLock

 

스마트 컨트랙트에서 관리자만 함수를 사용해야 하는 경우 필요한 라이브러리가 OwnerHelper 입니다. OwnerHelper는 public으로 공개되어 있는 함수들을 관리자만 접근할 수 있도록 제어해주는 역할을 담당합니다.

TokenLock 라이브러리는 사용자 계정에 락이 걸려있는지 확인합니다. 만약 락이 걸린 사용자가 있다면 토큰 전송이 불가능하므로, 미리 확인하는 라이브러리입니다.

 

 

OwnerHelper

OwnerHelper는 abstract contract 형식입니다. 즉, contract가 구현된 기능과 Interface의 추상화 기능 모두를 제공하고 있습니다. 만약 실제 contract에서 사용되지 않으면 abstract로 표시되어 사용되지 않습니다. 

가장 우선적으로 _owner를 상태변수로 선언합니다. 관리자를 의미합니다.

다음으로 OwnershipTransferred 이벤트가 선언되어 있습니다. 만약 관리자가 변경된다면 이전 관리자와 새로운 관리자 주소를 로그로 기록합니다. 

다음으로 onlyOwner modifier를 선언합니다. 함수를 실행하기 전 해당 사용자가 관리자인지 확인한 후 이후 함수 로직을 수행합니다. 

abstract contract OwnerHelper {
  	address private _owner;

  	event OwnershipTransferred(address indexed preOwner, address indexed nextOwner);

  	modifier onlyOwner {
		require(msg.sender == _owner, "OwnerHelper: caller is not owner");
		_;
  	}

  	constructor() {
		_owner = msg.sender;
  	}

  	function owner() public view virtual returns (address) {
		return _owner;
  	}

  	function transferOwnership(address newOwner) onlyOwner public {
		require(newOwner != _owner);
		require(newOwner != address(0x0));
		_owner = newOwner;
		emit OwnershipTransferred(_owner, newOwner);
  	}
}

 

TokenLock

TokenLock은 tokenLock과 tokenPersonalLock으로 구분됩니다. tokenLock은 토큰의 전체 락에 대한 검사를 진행하며, tokenPersonalLock은 개인 락에 대한 검사를 진행합니다. 

TokenLock의 내부 함수인 isTokenLock()은 전체락, 송신자 계정, 수신자 계정의 락을 검사합니다.

require(isTokenLock(sender, recipient) == false, "TokenLock: invalid token transfer")

 

TokenLock 라이브러리의 내장 함수 removeTokenLock은 전체 토큰 락을 풀어버리고, removePersonalTokenLock은 개인 계정의 락을 해제합니다. 여기에 onlyOwner modifier를 적용하면 관리자만 Lock을 풀도록 로직을 구성할 수 있습니다. 

function removeTokenLock() onlyOwner public {
    require(_tokenLock == true);
    _tokenLock = false;
}

function removePersonalTokenLock(address _who) onlyOwner public {
    require(_personalTokenLock[_who] == true);
    _personalTokenLock[_who] = false;
}

 

 

 

전체 소스코드

// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.10;

interface ERC20Interface {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount) external returns (bool);
    function approve(address spender, uint256 amount) external returns (bool);
    function allowance(address owner, address spender) external view returns (uint256);
    function transferFrom(address spender, address recipient, uint256 amount) external returns (bool);

    event Transfer(address indexed from, address indexed to, uint256 amount);
    event Transfer(address indexed spender, address indexed from, address indexed to, uint256 amount);
    event Approval(address indexed owner, address indexed spender, uint256 oldAmount, uint256 amount);
}

library SafeMath {
  	function mul(uint256 a, uint256 b) internal pure returns (uint256) {
			uint256 c = a * b;
			assert(a == 0 || c / a == b);
			return c;
  	}

  	function div(uint256 a, uint256 b) internal pure returns (uint256) {
	    uint256 c = a / b;
			return c;
  	}

  	function sub(uint256 a, uint256 b) internal pure returns (uint256) {
			assert(b <= a);
			return a - b;
  	}

  	function add(uint256 a, uint256 b) internal pure returns (uint256) {
			uint256 c = a + b;
			assert(c >= a);
			return c;
	}
}

abstract contract OwnerHelper {
  	address private _owner;

  	event OwnershipTransferred(address indexed preOwner, address indexed nextOwner);

  	modifier onlyOwner {
			require(msg.sender == _owner, "OwnerHelper: caller is not owner");
			_;
  	}
    
  	constructor() {
      _owner = msg.sender;
  	}

    function owner() public view virtual returns (address) {
      return _owner;
    }

  	function transferOwnership(address newOwner) onlyOwner public {
      require(newOwner != _owner);
      require(newOwner != address(0x0));
      address preOwner = _owner;
	    _owner = newOwner;
	    emit OwnershipTransferred(preOwner, newOwner);
  	}
}

contract SimpleToken is ERC20Interface, OwnerHelper {
    using SafeMath for uint256; 

    mapping (address => uint256) private _balances;
    mapping (address => mapping (address => uint256)) public _allowances;

    uint256 public _totalSupply;
    string public _name;
    string public _symbol;
    uint8 public _decimals;
    bool public _tokenLock;
    mapping (address => bool) public _personalTokenLock;

    constructor(string memory getName, string memory getSymbol) {
      _name = getName;
      _symbol = getSymbol;
      _decimals = 18;
      _totalSupply = 100000000e18;
      _balances[msg.sender] = _totalSupply;
      _tokenLock = true;
    }

    function name() public view returns (string memory) {
      return _name;
    }

    function symbol() public view returns (string memory) {
      return _symbol;
    }

    function decimals() public view returns (uint8) {
      return _decimals;
    }

    function totalSupply() external view virtual override returns (uint256) {
      return _totalSupply;
    }

    function balanceOf(address account) external view virtual override returns (uint256) {
      return _balances[account];
    }

    function transfer(address recipient, uint amount) public virtual override returns (bool) {
      _transfer(msg.sender, recipient, amount);
      emit Transfer(msg.sender, recipient, amount);
      return true;
    }

    function allowance(address owner, address spender) external view override returns (uint256) {
      return _allowances[owner][spender];
    }

    function approve(address spender, uint amount) external virtual override returns (bool) {
      uint256 currentAllowance = _allowances[msg.sender][spender];
      require(_balances[msg.sender] >= amount,"ERC20: The amount to be transferred exceeds the amount of tokens held by the owner.");
      _approve(msg.sender, spender, currentAllowance, amount);
      return true;
    }

    function transferFrom(address sender, address recipient, uint256 amount) external virtual override returns (bool) {
      _transfer(sender, recipient, amount);
      emit Transfer(msg.sender, sender, recipient, amount);
      uint256 currentAllowance = _allowances[sender][msg.sender];
      require(currentAllowance >= amount, "ERC20: transfer amount exceeds allowance");
      _approve(sender, msg.sender, currentAllowance, currentAllowance - amount);
      return true;
    }

    function _transfer(address sender, address recipient, uint256 amount) internal virtual {
      require(sender != address(0), "ERC20: transfer from the zero address");
      require(recipient != address(0), "ERC20: transfer to the zero address");
      require(isTokenLock(sender, recipient) == false, "TokenLock: invalid token transfer");
      uint256 senderBalance = _balances[sender];
      require(senderBalance >= amount, "ERC20: transfer amount exceeds balance");
      _balances[sender] = senderBalance.sub(amount);
      _balances[recipient] = _balances[recipient].add(amount);
    }

    function isTokenLock(address from, address to) public view returns (bool lock) {
      lock = false;

      if(_tokenLock == true)
      {
           lock = true;
      }

      if(_personalTokenLock[from] == true || _personalTokenLock[to] == true) {
           lock = true;
      }
    }

    function removeTokenLock() onlyOwner public {
      require(_tokenLock == true);
      _tokenLock = false;
    }

    function removePersonalTokenLock(address _who) onlyOwner public {
      require(_personalTokenLock[_who] == true);
      _personalTokenLock[_who] = false;
    }

    function _approve(address owner, address spender, uint256 currentAmount, uint256 amount) internal virtual {
      require(owner != address(0), "ERC20: approve from the zero address");
      require(spender != address(0), "ERC20: approve to the zero address");
      require(currentAmount == _allowances[owner][spender], "ERC20: invalid currentAmount");
      _allowances[owner][spender] = amount; 
      emit Approval(owner, spender, currentAmount, amount);
    }
}

 

 

 

 

[Blockchain] 이더리움 토큰 발행 ERC-20 이란?

이더리움 블록체인 메인넷에서 토큰을 발행하고, 관리할 수 있습니다. 내가 만든 토큰으로 디앱을 구동하고, 특정 행동을 유인하는 수단으로 사용할 수 있습니다. ERC(Ethereum Request for Comment)는 이

about-tech.tistory.com

 

 

[Blockchain] 가위바위보 스마트 컨트랙트 개선하기

가위바위보 스마트 컨트랙트 게임을 배포하고 난 후 문제점이 발견됩니다. 새로운 방을 생성한 방장이 입력한 조건값이 트랜잭션에 그대로 노출된다는 점입니다. 스마트 컨트랙트에서 발생한

about-tech.tistory.com

 

 

[Blockchain] 가위 바위 보 스마트 컨트랙트 만들기

가위 바위 보 게임을 솔리디티로 작성할 수 있습니다. 플레이어들은 각각 가위, 바위, 보 3가지 옵션 중 하나를 선택해 게임에 참여할 수 있고, 승리한 플레이어는 베팅된 금액을 모두 가져갈 수

about-tech.tistory.com

 

댓글