스마트 컨트랙트와 ERC-20개발

Lumi·2021년 11월 23일
0
post-thumbnail

🔥 ERC-20이란?

보통 ERC20과 같이 -를 제거 하고 사용하기도 한다.

ERC20같은 경우에는 이더리움 블록체인 네트워크에서 정한 표준 토큰 스펙이다.

  • 즉 이더리움과의 호환성을 위해 모든 요구사항을 충족시키는 표준을 말한다.

ERC20토콘은 이더리움과 교환 가능하며 이더리움 지갑으로 전송이 가능하다.

쉽게 말해 이더리움을 활용하기 위한 기본 규칙이다.

이더리움은 자신의 생태계를 활용하여 다른 탈중앙화된 애플리케이션이 작동할수 있게 만들어진 플랫폼 네트워크 이다.

가장 큰 특징으로는 dApp이 있고 스마트 계약을 이용하고 쉽고 빠르게 토큰을 발행할 수 있다.

생태계에서 사용하는 화폐는 이더(ETH)가 사용이 되며, 다른 dApp들은 각각 자신들 만의 고유한 화폐를 가지고 있다.

  • 이더는 dApp에서도 활용이 가능하다.

이러한 것이 가능한 이유는 모든 dApp들은 ERC20토큰를 따라가기 떄문이다.

이런 의미에서 ERC20토큰은 이더리움 생태계에서 유동될수 이쓴 토큰, 호환성을 보장하기 위한 토큰이라는 의미도 가지게 된다.

가장 큰 장점은 앞서 말한 dApp이다.

스마트 계약의 속성을 지원하며 탈중앙화된 서비스를 구현이 가능하다는 점이 이더리움 생태계의 가장 큰 장점이며 특징이라고 할 수가 있다.

이런 식으로 만들어진 시스템은 자신만의 비지니스가 구현이 가능하지만 사용료를 이더리움에게 지불을 해야 하는 체계를 가지고 있고 이러한 사용료를 위해서 자신들의 토큰을 발행 하는 것이다.

  • 자신들만의 토큰을 발행하는 목적이 이것은 아니지만 이러한 이유도 있다.

결과적으로 ERC20토큰을 사용해야 이더리움 생태계를 활용 가능하고 이더리움 생태계와 쉽게 소통이 가능하다.

쉽게 말해 이더리움을 사용하려면 반드시 ERC20토큰을 사용해야 한다.

반대로 이런 경우도 있다

ERC20토큰을 사용하여 이더리움과 호환을 하지만 ERC20토큰을 사용 하였지만 자체적으로 메인넷을 발표하여 독립적인 생태계를 가지는 경우도 있다.

🔥 ERC-20 토큰 기본

ERC-20토큰의 전체코드를 살펴보는 글이 될 것이다.

// 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);
}

contract SimpleToken is ERC20Interface {
    mapping (address => uint256) private _balances;
    mapping (address => mapping (address => uint256)) public _allowances;

    uint256 public _totalSupply;
    string public _name;
    string public _symbol;
    uint8 public _decimals;
    
    constructor(string memory getName, string memory getSymbol) {
        _name = getName;
        _symbol = getSymbol;
        _decimals = 18;
        _totalSupply = 100000000e18;
        _balances[msg.sender] = _totalSupply;
    }
    
    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 currentAllownace = _allowances[spender][msg.sender];
        require(currentAllownace >= amount, "ERC20: Transfer amount exceeds allowance");
        _approve(msg.sender, spender, currentAllownace, 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");
        uint256 senderBalance = _balances[sender];
        require(senderBalance >= amount, "ERC20: transfer amount exceeds balance");
        _balances[sender] = senderBalance - amount;
        _balances[recipient] += amount;
    }
    
    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);
    }
}
  • ERC-20 토큰의 전체 코드이다.

토큰을 만드는 사람에 따라 조금씩 변화하기도 하며 토큰의 구조를 하나씩 살펴보도록 하겠다.

🌪 ERC20Interface

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);
}

가장 처음으로 확인해 볼수 있는 코드이다.

Interface는 단순히 사용할 함수의 형태만을 지정해 두고 실제 함수는 Contract에서 사용이 이루어 진다.

  • 일종의 어떤 함수가 있는지 알려주는 메뉴판이라고 생각을 하자.

함수 같은 경우에는 어렵지 않지만

이벤트 같은 경우에는 어려울 것이다

  • 사실 내가;; ㅎㅎ

이벤트 또한 선언할떄 입력값, 반환값을 설정 가능하다.

기본적으로 Transfer 이벤트는 토큰이 이동할 때마다 로그를 남기고
Approval 이벤트 는 approve함수가 실행 될 떄마다 로그를 남기게 된다.

  • 이벤트는 쉽게 말해 로그를 남기는 용도로 사용한다

🌪 SimpleToken

이제 함수의 내용을 다루는 곳이다.

contract SimpleToken is ERC20Interface {
    mapping (address => uint256) private _balances;
    mapping (address => mapping (address => uint256)) public _allowances;

    uint256 public _totalSupply;
    string public _name;
    string public _symbol;
    uint8 public _decimals;
    
    constructor(string memory getName, string memory getSymbol) {
        _name = getName;
        _symbol = getSymbol;
        _decimals = 18;
        _totalSupply = 100000000e18;
        _balances[msg.sender] = _totalSupply;
    }
    
    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, uint256 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, uint256 amount) external virtual override returns (bool) {
        uint256 currentAllownace = _allowances[spender][msg.sender];
        require(currentAllownace >= amount, "ERC20: Transfer amount exceeds allowance");
        _approve(msg.sender, spender, currentAllownace, 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");
        uint256 senderBalance = _balances[sender];
        require(senderBalance >= amount, "ERC20: transfer amount exceeds balance");
        _balances[sender] = senderBalance - amount;
        _balances[recipient] += amount;
    }
    
    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);
    }
}

contract SimpleToken is ERC20Interface 를 통해서 앞선 인터페이스(메뉴판)을 사용할 수 있다고 선언을 한다.

  • 이렇게하면 이벤트를 사용가능하다.

또한 이중으로 맵핑된 값들도 확인할 수가 있다.

🔨 함수의 종류
totalSupply

  • 해당 스마트 컨트랙트 기반 ERC-20토큰의 총 발행량을 확인

balanceOf

  • owner가 가지고 있는 토큰의 보유량 확인

transfer

  • 토큰을 전송

approve

  • 토큰을 전송 가능하도록 사용자에게 양도할 토큰의 양을 설정

allowance

  • owner가 사용자에게 양도 설정한 토큰의 양을 확인

transferFrom

  • 사용자가 거래 가능하도록 양도 받은 토큰을 전송

아마 코드를 보면 totalSupply,balanceOf,transfer은 쉽게 이해가 가능하겠지만

나머지 함수들은 좀 이해가 어려울 것이다.

  • 내가 그러고 있다 ㅋㅋㅋ

ERC-20에서는 토큰의 owner가 직접 토큰을 드른 사람에게 전송할 수도 있지만, 토큰을 양도할 만큼 등록해두고, 필요할때만 제 3자를 통해 토큰을 양도 할 수도 있다.

직접 다른 사람들에게 토큰을 전송하는 것은 trnsfer함수를 활용하지만

토큰을 등록하는 방식은 approve,allowance,transferFrom를 사용한다.

대략적인 작동방식은 이렇다.

approve는 지갑의 주인이 토큰을 EXCHANGE에 자신이 가진 토큰의 수보다 적은 금액을 거래 가능하도록 맡긴다.
allowance는 OWNER와 EXCHANGE값을 입력 해서 몇개가 등록 되어있는지 확인 할 수 있다.
transferFrom은 EXCHANGE가 BUYER가 구매 신청해놓은 금액에 대해 OWNER가 맡겨둔 토큰을 판매한다.

이중으로 맵핑한 approvals 변수는 일종의 이중 객체이다.

키 값은 OWNER의 지갑 주소를 가르키며 값은 토큰을 양도받은 제 3자에 대한 객체값을 가지게 된다.

🔨 ERC-20함수 : transfer

내부함수인 _transfer를 호출하며, 호출이 정상적으로 완료될 경우 이벤트를 발생시킨다.

_transferrequire를 통해 세자기 조건을 검사하게 되는데

  1. 보내는 사람의 주소가 잘못되었는지 체크
  2. 받는사람의 주소가 잘못되었는지 체크
  3. transfer함수를 실행한 사람이 가진 토큰이 신청한 값보다 많은지 체크

위 세 조건을 충족하는 경우, 실행한 사람이 가지고 있는 토큰을 받을 사람의 토큰 지갑으로 전송을 하게 된다.

🔨 ERC-20함수 : approve

일단 양도할 토큰값이 내가 현재 가지고있는 토큰의 양보다 적은지 검사를 한다.

문제가 없다면 _approve를 호출하게 된다.

_approve에서는 내가 토큰을 양도할 상대방에게 양도할 값을 기록을 한다.

그리고 이벤트를 호출하여 기록을 한다.

  • 이 상태에서는 실제로 양도가 이루어 지는 것이 아니라 주소와 값을 정하는 것 뿐이다.

🔨 ERC-20함수 : transferFrom

이 부분에서 실제로 토큰 거래가 이루어 진다.

transferFrom은 양도를 수행하는 거래 대행자(msg.sender)가 허락해준 값 만큼 상대방에게 토큰을 이동 시킨다.

이동을 위해서 _transfer함수를 실행 시키게 된다.

_transfer에서는 잔고처리를 하며 문제가 없을시에 _approve함수를 실행한다.

테스트넷을 통해서 나만의 ERC-20토큰을 배포하여 사용해 볼수도 있다.

🔥 SafeMath

일단 오버플로와 언더 플로에 대해서 집고 넘어가야 한다.

오버플로 란 어떠한 것이 용량을 초과해 흘러 넘치는 것을 말한다.

언더플로 는 오버플로와 완전하게 반대되는 의미로 정해진 범위 아래로 마이너스가 되는 것을 의미한다.

가령 솔리디티의 int8은 -64 ~ 63까지의 값만 가질수 있지만 만야 70이 할당되면 오버플로 가 발생하게 된다.

두 에러는 프로그램이 무한루프를 걸리게 하거나 고장이 나게 할 수 있다.

하지만 이더리움의 경우에는 이러한 문제를 가스비를 이용해 자동으로 판단하도록 설계를 해두었고 만약 가스비가 소비되었지만 함수의 실행이 실패가 되는 경우에는 자동으로 예외처리하게 해주는 여러 함수들이 제공이 된다.

이번 글에서는 오버플로, 언더플로에 대비한 예외처리 함수들을 알아보는 글이 될 것이다.

// 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;
    // ~~ 이전 강의 참조
}

이전 글에 있던 ERC-20토큰 소스코드에 SafeMath의 코드를 추가해 주었다.

using SafeMath for uint256

  • 자료형 uint256에 대해서 SafeMath라이브러리를 사용 한다는 의미이다.
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.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].add(amount);
    }

이후 전에는 +, - 를 사용하던 연산에 MathSafe라이브러리를 활용한 함수를 사용하게 된다.

  • currentAllowance와 senderBalance는 sub함수를 사용하였고
  • _balancessms add함수를 사용 하였다.

사실 아직 솔리디티를 제대로 다루지 못하는 입장에서 이런것까지 설정을 할 수 있을지는 모르겟다..ㅠㅠㅠ

🔥 OwnerHelper

스마트 컨트랙트를 만들다보면, 관리자만 사용할 수 있는 함수가 필요할 수가 있다.

이번 글에서는 특정 함수를 관리자만 사용할 수 있또록 서정하는

OwnerHelper를 사용하여 publice로 공개 되어 있으에도 관리자만 접근 가능한 함수를 만들어 본다.

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);
  	}
}

contract SimpleToken is ERC20Interface, OwnerHelper{...}

일단 SafeMath와 같은 역할을 할수 있는 추상 컨트랙트를 만들어 준다.

  • 구현된 기능과 interface의 추상화 기능 모두를 포함한다.
  • 만약 실제 contract에서 사용되지 않는다면 단순히 추상으로 표시된다.
  • 즉 사용하지 않아도 된다.

address private _owner

  • _owner는 관리자를 나타낸다.

마찬가지로 이벤트가 존재하며 관리자가 변경되었을떄 이전 관리자의 정보를 로그에 남긴다.

onlyOwner 함수 변경자는 함수 실행 이전에 함수를 실행시키는 사람이 관리자인지 확인을 하게 된다.

    bool public _tokenLock;
    mapping (address => bool) public _personalTokenLock;

    constructor(string memory getName, string memory getSymbol) {
        // ~~
        _tokenLock = true;
    }

    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;
        }
    }

이후 기본 스마트계약에 이러한 코드를 추가해 준다.

기본적으로 isTokenLock함수를 통해서 전체 토큰 락과 개인 토큰 락을 검사할 수가 있다.

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].add(amount);
    }

그후 _transfer 함수를 수정하여 락이 걸려있을시에 토큰의 이동이 불가능하게 만들어 준다.

이 락을 제거하려면 오직 관리자만 해제 가능하며 이러한 부분은

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

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

이런식으로 처리가 가능하다.

코드를 중간중간 수정하는 내용이 주로 이루기 떄문에 아마 처음 글을 읽게되면 이해가 어려울거 같다;;

나중에 혹시 복습하는 시간을 가지게 된다면 그떄 좀더 전체적으로 정리를 해보고자 한다.!!

profile
[기술 블로그가 아닌 하루하루 기록용 블로그]

0개의 댓글