Ethereum ERC-20

Hong·2022년 12월 4일
0
post-thumbnail

why I work so hard
누군가는 불행한 현실에 반응하는 방식으로 술, 비디오게임, 담배(해외에 있다면 대마초, 마약)를 선택하지만
이것이 내가 마주하는 현실에 반응하는 방식이다

아래 작성된 코드는 remix나 truffle 등을 통해 ethereum network에 배포가능하고
metamask로 확인할 수 있다.





🪙 ERC-20이란?

ERC-20Ethereum Request for Comment 20의 약자이다.
이더리움 네트워크의 개선안을 제안하는 EIPs(Ethereum Improvement Proposals)에서 관리하는 공식 프로토콜이다. 옆줄(-) 없이 'ERC20', 'ERC 20'이라고 쓰기도 한다.
ERC-20은 이더리움 블록체인 네트워크에서 정한 표준 토큰 스펙이다.
ERC-20으로 발행된 token은 이더리움과 교환할 수 있으며 이더리움 지갑으로 전송도 가능하다.

erc-20으로 token을 만들었다는 소리를 여럿 들어봤을 것이다
때문에 누군가는 erc-20을 token생성 프로그램이라고 말하는데,
엄밀히 말하면 이것은 잘못된 설명이다.
erc-20은 token을 만들어주는 Solitity reference code이다.




🧐 What is difference between EIP and ERC?

EIP(Ethereum Improvement Proposals)는 이더리움 개선 제안이고,
ERC(Ethereum Request for Comment)는 이더리움 기능 표준이다.
ERCEIP의 한 형태다.




👨‍🔧 왜 ERC-20을 사용할까?

ERC-20은 이더리움 네트워크 위의 다른 토큰들과도 교환이 가능하고 이더리움과도 자유롭게 교환이 가능하다.
때문에 만약 내가 이더리움 생태계 위에서 dApp을 만들고 싶다면, ERC-20을 사용하여 표준을 따르는 것이 가장 효율적인 것이다.




👾 ERC-20 전체 코드

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

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;
    uint private E18 = 1000000000000000000;

    constructor(string memory getName, string memory getSymbol) {
        _name = getName;
        _symbol = getSymbol;
        _decimals = 18;
        _totalSupply = 100000000 * E18;
        _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 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");
        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");
        _allowances[owner][spender] = amount;
        emit Approval(owner, spender, currentAmount, amount);
    }
}



구조를 하나씩 살펴보자




ERC20 Interface

~ 생략 ~

interface ERC20Interface {
    function totalSupply() external view returns (uint256); // totalSupply : 해당 스마트 컨트랙트가 발행한 ERC-20 토큰의 총발행량 확인
    function balanceOf(address account) external view returns (uint256); // balanceOf : 알아보고 싶은 계정(account)을 입력받아 계정이 가지고 있는 토큰의 보유량 확인
    function transfer(address recipient, uint256 amount) external returns (bool); // transfer : 수신사(recipient)와 토큰 전송량(amount) 입력받아 토큰을 전송

    function approve(address spender, uint256 amount) external returns (bool); // approve: spender의 계정으로 value 만큼의 토큰을 인출할 권리를 부여. 이 함수를 이용할 때는 반드시 Approval 이벤트 함수를 호출해야 함
    function allowance(address owner, address spender) external view returns (uint256); // allowance : token owner가 spender에 양도 설정한 토큰의 양을 확인(exchange에 수신자(spender)로 예치되어 있는 token의 수량을 알려줌)
    function transferFrom(address spender, address recipient, uint256 amount) external returns (bool); // transferFrom : 수신자(spender)가 token을 사용할 수 있도록 exchange로부터 토큰을 전송

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

~ 생략 ~

  • totalSupply : 해당 스마트 컨트랙트가 발행한 ERC-20 토큰의 총발행량 확인
  • balanceOf : 알아보고 싶은 계정(account)을 입력받아 계정이 가지고 있는 토큰의 보유량 확인
  • transfer : 수신사(recipient)와 토큰 전송량(amount) 입력받아 토큰을 전송
  • approve : spender의 계정으로 value 만큼의 토큰을 인출할 권리를 부여. 이 함수를 이용할 때는 반드시 Approval 이벤트 함수를 호출해야 함
  • allowance : token owner가 spender에 양도 설정한 토큰의 양을 확인(exchange에 수신자(spender)로 예치되어 있는 token의 수량을 알려줌)
  • transferFrom : 수신자(spender)가 token을 사용할 수 있도록 exchange로부터 토큰을 전송


approve, allowance, transferFrom의 직관적 이해를 얻기 위해서 위 그림을 보면 도움이 된다.
ERC-20에서는 토큰의 owner가 직접 토큰을 다른 사람에게 전송할 수도 있지만, 토큰을 양도할 만큼 등록해두고, 필요할 때 제삼자를 통해 토큰을 양도할 수 있다.
직접 토큰을 다른 사람에게 전송할 때는 transfer 함수를 사용하고,
토큰을 등록하는 방식을 사용하는 경우 approve, allowance, transferFrom 함수를 사용한다.



위 내용까지는 개괄적인 ERC-20 코드 형식에 대해서 작성했고
아래에는 여러가지 기능이 추가된 ERC-20 token의 solidity code를 설명한다.




ERC-20과 함께 추가로 작성한 코드 전체

ERC-20을 바탕으로 SafeMath 라이브러리, 관리자 권한을 넘겨줄 수 있는 ownership abstract contract, 투표를 통한 관리자 선출, token계정 잠그고 해제하기 기능 등을 추가했다

아래는 전체 코드고 각각의 기능은 아래로 내려가며 설명한다.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

interface ERC20Interface {
    function totalSupply() external view returns (uint256); // totalSupply : 해당 스마트 컨트랙트가 발행한 ERC-20 토큰의 총발행량 확인
    function balanceOf(address account) external view returns (uint256); // balanceOf : 알아보고 싶은 계정(account)을 입력받아 계정이 가지고 있는 토큰의 보유량 확인
    function transfer(address recipient, uint256 amount) external returns (bool); // transfer : 수신사(recipient)와 토큰 전송량(amount) 입력받아 토큰을 전송

    function approve(address spender, uint256 amount) external returns (bool); // approve: spender의 계정으로 value 만큼의 토큰을 인출할 권리를 부여. 이 함수를 이용할 때는 반드시 Approval 이벤트 함수를 호출해야 함
    function allowance(address owner, address spender) external view returns (uint256); // allowance : token owner가 spender에 양도 설정한 토큰의 양을 확인(exchange에 수신자(spender)로 예치되어 있는 token의 수량을 알려줌)
    function transferFrom(address spender, address recipient, uint256 amount) external returns (bool); // transferFrom : 수신자(spender)가 token을 사용할 수 있도록 exchange로부터 토큰을 전송

    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 { // 오버플로(지정한 type보다 높은 값이 계산되는 경우)와 언더플로(지정된 type보다 낮은 값이 계산되는 경우)를 예방하기 위해 의심이 되는 부분에서는 항상 SafeMath 함수 사용을 습관화해야 합니다.
    // pure : SafeMath 라이브러리의 함수에서는 단순 연산의 결과값을 반환하기 때문에 상태 변수를 읽거나 쓰지 않는다.
    // assert(에러 핸들러) : gas를 다 소비한후, 특정한 조건에 부합하지 않으면 에러를 발생시킨다.

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

        return c;
    }

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

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

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




abstract contract OwnerHelper {
  	address _owner; // _owner는 관리자를 나타낸다.

    // OwnershipTransferred이벤트는 관리자가 변경되었을 때 이전 관리자의 주소와 새로운 관리자의 주소 로그를 남긴다.
  	event OwnershipTransferred(address indexed preOwner, address indexed nextOwner);

    // onlyOwner 함수 변경자는 함수 실행 이전에 함수를 실행시키는 사람이 관리자인지 확인한다.
  	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; // uint256에 대서 SafeMath 라이브러리를 사용하도록 선언해줬다.

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

    uint256 public _totalSupply;
    string public _name;
    string public _symbol;
    uint8 public _decimals;
    uint private E18 = 1000000000000000000;
    bool public _tokenLock;
    mapping (address => bool) public _personalTokenLock; // tokenPersonalLock변수는 보내는사람(from), 받는사람(to) 토큰계정의 lock에 대한 상태처리를 true, false로 한다.

    constructor(string memory getName, string memory getSymbol) {
        _name = getName;
        _symbol = getSymbol;
        _decimals = 18;
        _totalSupply = 100000000 * E18;
        _balances[msg.sender] = _totalSupply;
        _tokenLock = true; // tokenLock변수는 토큰의 전체 lock에 대한 상태처리를 true, false로 한다.
    }





    // [advanced]
    /*
    [advanced - 수도코드]
    <candidate>
    1. 구조체 안에 투표를 받았다는것을 count해주는 key값을 만든다
    2. 내가 투표했는지 안했는지 true false로 구분해주는 key값을 만든다

    <voteMakeOwner>
    modifier = <후보자> 2번을 통해 후보자가 투표를 했는지 안했는지 확인한다
    1. 투표를 보내는 기능(한명한테만 보낼 수 있도록 해야함) 인자는 from to
        1-1. 투표자의 구조체 안에 투표받았다는 count를 +1해준다

    <getVoteResult>
    1. 후보자 구조체를 전부 순회하면서 <후보자> 1.을 확인한다
    2. 제일 count를 많이 받은 후보자를 선정한다(newOwner).
    3. transferOwnership(newOwner)를 실행시킨다.
    4. 후보자 구조체의 count 투표여부 변수를 초기화한다.
    */


    uint people = 0; //총 후보자의 사람수를 나타낸다

    struct candidates { //후보자 구조체를 만든다
      address jooso; //해당 후보자의 주소
      uint count; //몇 표를 받았는지나타낸다
      uint key; //투표를 했는지나타낸다 : 투표를 안했다면 0, 투표를 했다면 1
    }

    candidates[] candidates_arr; //후보자들 배열을 만들어준다.

    //함수 add_candidate는 address를 입력받아 해당 주소를 후보자 배열에 추가시킨다
    function add_candidate (address add) public {
      candidates_arr.push(candidates(add, 0, 0)); //candidates(jooso, count, key)
      people++; //후보자를 후보자 배열에 추가했으니 총 후보자의 사람수를 +1해준다
      if(people == 1) { //나중에 transferOwnership을 해줄 때 ownership이 없다면 owner이전이 안되기 때문에 첫번째 후보자에게 ownership을 부여해준다.
        _owner = candidates_arr[0].jooso;
      }
    }

    //함수 voteMakeOwner는 from, to를 인자로 받아 from의 투표권을 행사한다.
    function voteMakeOwner (address from, address to) public {
      for(uint i = 0; i < people; i++) { //후보자 배열을 순회하며 from주소를 찾고 해당 from주소를 고정하고 to를 찾아 투표권을 행사한다.
        //from주소를 들고 순회하며 찾은 후보자의 주소가 일치하고 && 아직 투표하지않았으면
        if(candidates_arr[i].jooso == from && candidates_arr[i].key == 0) {

          //to주소를 들고 순회하며 투표를 받을 후보자를 찾는다
          for(uint j = 0; j < people; j++) {
            //to의 표를 올려줌. = count를 올려줌
            if(candidates_arr[j].jooso == to) candidates_arr[j].count++;
          }

          //from의 사람이 to에게 투표권을 행사했음으로 from의 투표권은 사용되었다 표시해주자
          candidates_arr[i].key = 1;
        } 
      }
    }

    //함수 getVoteResult는 최종 다득표로 선정된 address를 return한다 
    function getVoteReulst () public payable returns (address) {
      uint max = candidates_arr[0].count; //max는 투표를 받은 수중 가장 높은 수를 나타낸다
      uint winner = 0; //winner는 가장 높은 투표를 받은 후보자를 나타낸다

      for(uint i = 1; i < people; i++) { //people의 수만큼 candidate_arr를 순회하며 가장 높은 표를 받은 후보자를 찾는다
        //만약 i번째 후보자의 득표가 기존의 득표보다 많으면 i를 winner로 설정해준다. 
        if(candidates_arr[i].count > max) {
          winner = i;
          max = candidates_arr[i].count;
        }
      }
    
      address winner_address = candidates_arr[winner].jooso; 

      //함수 transferOwnership을 통해 관리자 권한을 winner의 계정으로 넘겨준다, 이전에 관리자 권한을 들고있던 사람은 자동으로 권한을 박탈당한다 
      transferOwnership(winner_address);

      //투표가 종료되었기 때문에 후보자 배열을 순회하며 특표수와 투표한 표시를 초기화해준다
      for(uint i = 0; i < people; i++) {
        candidates_arr[i].count = 0;
        candidates_arr[i].key = 0;
      }

      return winner_address; //최종 다득표자의 계정주소를 return한다
    }
    //




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

        // 다음의 코드에서 currentAllowance.sub(amount)이 SafeMath 라이브러리 함수를 사용한 예시다.
        _approve(sender, msg.sender, currentAllowance, currentAllowance.sub(amount)); // uint256으로 선언했던 currentAllowance에 sub함수를 사용할 수 있는 모습을 볼 수 있다.
        return true;
    }

    function _transfer(address sender, address recipient, uint256 amount) internal virtual {
        require(sender != address(0), "ERC20: transfer from the zero address"); // 보내는 사람(sender)의 계정이 이상한지 확인한다.
        require(recipient != address(0), "ERC20: transfer to the zero address"); // 받는 사람(recipient)의 계정이 이상한지 확인한다.
        require(isTokenLock(sender, recipient) == false, "TokenLock: invalid token transfer"); // 전체 token과 보내는 사람(sender), 받는 사람(recipient)의 token계정이 lock되어 있는지 확인한다.
        uint256 senderBalance = _balances[sender]; // 보내는 사람(sender)의 계정 
        require(senderBalance >= amount, "ERC20: transfer amount exceeds balance"); // 보내는 사람(sender)의 계정의 잔액이 보내고자 하는 금액(amount)보다 많이 남아있는지 확인한다.

        // 다음의 코드에서 currentAllowance.sub(amount)이 SafeMath 라이브러리 함수를 사용한 예시다. (add도 SafeMath라이브러리 함수임)
        _balances[sender] = senderBalance.sub(amount);  //uint256으로 선언했던 senderBalance에 sub함수를 사용할 수 있는 모습을 볼 수 있다.
        _balances[recipient] = _balances[recipient].add(amount);
    }








    // 함수 isTokenLock은 전체 락과, 보내는 사람의 락, 받는 사람의 락을 검사하여 락이 걸려 있는지 확인한다.
    function isTokenLock(address from, address to) public view returns (bool lock) {
      lock = false;

      // tokenLock은 토큰의 '전체' 락에 대한 true, false 상태처리를 해준다. if문을 통해 전체 토큰에 lock이 걸려있는지 확인한다.
      if(_tokenLock == true) {
           lock = true; // lock이 되어있는 상태라고 알려준다.
      }

      // tokenPersonalLock은 토큰의 '개인' 락에 대한 true, false 상태처리를 해준다. if문을 통해 보내는사람(from)의 계정과 받는사람(to)의 계정이 lock되어있는지 확인한다.
      if(_personalTokenLock[from] == true || _personalTokenLock[to] == true) {
           lock = true; // lock이 되어있는 상태라고 알려준다.
      }
    }



    // 함수 removeTokenLock은 전체 token계정의 lock을 해제한다. + 계정을 건드리는 일은 관리자만 할 수 있어야 함으로 함수에 onlyOwner를 적용시켰다.
    function removeTokenLock() onlyOwner public {
      require(_tokenLock == true); // token계정 전체가 잠긴 상태라면
      _tokenLock = false; // lock상태를 false로 바꾸어 해제한다.
    }



    // 함수 removePersonalTokenLock은 입력받은 address의 lock을 해제한다. + 계정을 건드리는 일은 관리자만 할 수 있어야 함으로 함수에 onlyOwner를 적용시켰다.
    function removePersonalTokenLock(address _who) onlyOwner public {
      require(_personalTokenLock[_who] == true); // 입력받은 개인 token계정이 잠긴 상태라면
      _personalTokenLock[_who] = false; // lock상태를 false로 바꾸어 해제한다. 
    }


    //advanced - address를 입력받아 lock을 걸어주는 함수를 만든다 onlyOwner를 적용시켜서 계정관리자만 이 기능을 사용할 수 있도록 해주자
    function assignTokenLock(address _who) onlyOwner public {
        require(_personalTokenLock[_who] == false);
        _personalTokenLock[_who] = true; 
    }



    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");
      _allowances[owner][spender] = amount;
      emit Approval(owner, spender, currentAmount, amount);
    }
}



1. SafeMath

솔리디티에서 uint256 자료형은 0부터 2^256-1 만큼의 값을 제공한다. 만약 이 범위 이하의 값을 할당하거나, 범위 이상의 값을 할당하는 경우 언더플로, 오버플로 문제가 발생한다. 따라서 어떤 연산을 통해 이 범위 이상의 결과값이 나오지 않도록 하는 것은 매우 중요하다.

때문에 기본 연산자에 있어서 안전하게 연산을 가능하도록 SafeMath라는 함수가 필요하다.

  • overflow : 지정한 type보다 높은 값이 계산되는 경우
  • underflow : 지정된 type보다 낮은 값이 계산되는 경우
library SafeMath { // 오버플로(지정한 type보다 높은 값이 계산되는 경우)와 언더플로(지정된 type보다 낮은 값이 계산되는 경우)를 예방하기 위해 의심이 되는 부분에서는 항상 SafeMath 함수 사용을 습관화해야 하다.
    // pure : SafeMath 라이브러리의 함수에서는 단순 연산의 결과값을 반환하기 때문에 상태 변수를 읽거나 쓰지 않는다.
    // assert(에러 핸들러) : gas를 다 소비한후, 특정한 조건에 부합하지 않으면 에러를 발생시킨다.

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

        return c;
    }

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

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

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

safeMath는 solidity 파일에서 contract와 구분해서 작성해준 후에
contract 안에 아래와 같이 작성해줘야 SafeMath Library를 사용할 수 있다.

using SafeMath for uint256;


contract에서 사용된 부분

    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)); // uint256으로 선언했던 currentAllowance에 sub함수를 사용할 수 있는 모습을 볼 수 있다.
        return true;
    }

    function _transfer(address sender, address recipient, uint256 amount) internal virtual {
        require(sender != address(0), "ERC20: transfer from the zero address"); // 보내는 사람(sender)의 계정이 이상한지 확인한다.
        require(recipient != address(0), "ERC20: transfer to the zero address"); // 받는 사람(recipient)의 계정이 이상한지 확인한다.
        require(isTokenLock(sender, recipient) == false, "TokenLock: invalid token transfer"); // 전체 token과 보내는 사람(sender), 받는 사람(recipient)의 token계정이 lock되어 있는지 확인한다.
        uint256 senderBalance = _balances[sender]; // 보내는 사람(sender)의 계정 
        require(senderBalance >= amount, "ERC20: transfer amount exceeds balance"); // 보내는 사람(sender)의 계정의 잔액이 보내고자 하는 금액(amount)보다 많이 남아있는지 확인한다.

        // 다음의 코드에서 currentAllowance.sub(amount)이 SafeMath 라이브러리 함수를 사용한 예시다. (add도 SafeMath라이브러리 함수임)
        _balances[sender] = senderBalance.sub(amount);  //uint256으로 선언했던 senderBalance에 sub함수를 사용할 수 있는 모습을 볼 수 있다.
        _balances[recipient] = _balances[recipient].add(amount);
    }



2. OwnerHelper

OwnerHelper 컨트랙트는 abstract contract라고 하는 추상 컨트랙트로 작성했다.

abstract contract OwnerHelper {
  	address _owner; // _owner는 관리자를 나타낸다.

    // OwnershipTransferred이벤트는 관리자가 변경되었을 때 이전 관리자의 주소와 새로운 관리자의 주소 로그를 남긴다.
  	event OwnershipTransferred(address indexed preOwner, address indexed nextOwner);

    // onlyOwner 함수 변경자는 함수 실행 이전에 함수를 실행시키는 사람이 관리자인지 확인한다.
  	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);
  	}
}

  • _owner : 변수로써 관리자를 나타낸다
  • modifier onlyOwner : 함수 실행 이전에 함수를 실행시키는 사람이 owner인지 확인한다.
  • function owner : 지금 현재 owner가 누구인지 return한다
  • function transferOwnership : 새로운 owner의 address를 입력받아 ownership을 이전시킨다.

interface와 abstract contract의 차이점




3. Contract

기본적인 ERC-20을 이용해서 token을 주고받을 수 있는 contract를 작성했고
계정을 lock하고 unlock하는 기능,
투표를 통해서 관리자(계정의 lock, unlock을 설정할 수 있음)를 선출하는 기능을 추가했다

contract SimpleToken is ERC20Interface, OwnerHelper {
    using SafeMath for uint256; // uint256에 대서 SafeMath 라이브러리를 사용하도록 선언해줬다.

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

    uint256 public _totalSupply;
    string public _name;
    string public _symbol;
    uint8 public _decimals;
    uint private E18 = 1000000000000000000;
    bool public _tokenLock;
    mapping (address => bool) public _personalTokenLock; // tokenPersonalLock변수는 보내는사람(from), 받는사람(to) 토큰계정의 lock에 대한 상태처리를 true, false로 한다.

    constructor(string memory getName, string memory getSymbol) {
        _name = getName;
        _symbol = getSymbol;
        _decimals = 18;
        _totalSupply = 100000000 * E18;
        _balances[msg.sender] = _totalSupply;
        _tokenLock = true; // tokenLock변수는 토큰의 전체 lock에 대한 상태처리를 true, false로 한다.
    }





    // [advanced]
    /*
    [advanced - 수도코드]
    <candidate>
    1. 구조체 안에 투표를 받았다는것을 count해주는 key값을 만든다
    2. 내가 투표했는지 안했는지 true false로 구분해주는 key값을 만든다

    <voteMakeOwner>
    modifier = <후보자> 2번을 통해 후보자가 투표를 했는지 안했는지 확인한다
    1. 투표를 보내는 기능(한명한테만 보낼 수 있도록 해야함) 인자는 from to
        1-1. 투표자의 구조체 안에 투표받았다는 count를 +1해준다

    <getVoteResult>
    1. 후보자 구조체를 전부 순회하면서 <후보자> 1.을 확인한다
    2. 제일 count를 많이 받은 후보자를 선정한다(newOwner).
    3. transferOwnership(newOwner)를 실행시킨다.
    4. 후보자 구조체의 count 투표여부 변수를 초기화한다.
    */


    uint people = 0; //총 후보자의 사람수를 나타낸다

    struct candidates { //후보자 구조체를 만든다
      address jooso; //해당 후보자의 주소
      uint count; //몇 표를 받았는지나타낸다
      uint key; //투표를 했는지나타낸다 : 투표를 안했다면 0, 투표를 했다면 1
    }

    candidates[] candidates_arr; //후보자들 배열을 만들어준다.

    //함수 add_candidate는 address를 입력받아 해당 주소를 후보자 배열에 추가시킨다
    function add_candidate (address add) public {
      candidates_arr.push(candidates(add, 0, 0)); //candidates(jooso, count, key)
      people++; //후보자를 후보자 배열에 추가했으니 총 후보자의 사람수를 +1해준다
      if(people == 1) { //나중에 transferOwnership을 해줄 때 ownership이 없다면 owner이전이 안되기 때문에 첫번째 후보자에게 ownership을 부여해준다.
        _owner = candidates_arr[0].jooso;
      }
    }

    //함수 voteMakeOwner는 from, to를 인자로 받아 from의 투표권을 행사한다.
    function voteMakeOwner (address from, address to) public {
      for(uint i = 0; i < people; i++) { //후보자 배열을 순회하며 from주소를 찾고 해당 from주소를 고정하고 to를 찾아 투표권을 행사한다.
        //from주소를 들고 순회하며 찾은 후보자의 주소가 일치하고 && 아직 투표하지않았으면
        if(candidates_arr[i].jooso == from && candidates_arr[i].key == 0) {

          //to주소를 들고 순회하며 투표를 받을 후보자를 찾는다
          for(uint j = 0; j < people; j++) {
            //to의 표를 올려줌. = count를 올려줌
            if(candidates_arr[j].jooso == to) candidates_arr[j].count++;
          }

          //from의 사람이 to에게 투표권을 행사했음으로 from의 투표권은 사용되었다 표시해주자
          candidates_arr[i].key = 1;
        } 
      }
    }

    //함수 getVoteResult는 최종 다득표로 선정된 address를 return한다 
    function getVoteReulst () public payable returns (address) {
      uint max = candidates_arr[0].count; //max는 투표를 받은 수중 가장 높은 수를 나타낸다
      uint winner = 0; //winner는 가장 높은 투표를 받은 후보자를 나타낸다

      for(uint i = 1; i < people; i++) { //people의 수만큼 candidate_arr를 순회하며 가장 높은 표를 받은 후보자를 찾는다
        //만약 i번째 후보자의 득표가 기존의 득표보다 많으면 i를 winner로 설정해준다. 
        if(candidates_arr[i].count > max) {
          winner = i;
          max = candidates_arr[i].count;
        }
      }
    
      address winner_address = candidates_arr[winner].jooso; 

      //함수 transferOwnership을 통해 관리자 권한을 winner의 계정으로 넘겨준다, 이전에 관리자 권한을 들고있던 사람은 자동으로 권한을 박탈당한다 
      transferOwnership(winner_address);

      //투표가 종료되었기 때문에 후보자 배열을 순회하며 특표수와 투표한 표시를 초기화해준다
      for(uint i = 0; i < people; i++) {
        candidates_arr[i].count = 0;
        candidates_arr[i].key = 0;
      }

      return winner_address; //최종 다득표자의 계정주소를 return한다
    }
    //




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

        // 다음의 코드에서 currentAllowance.sub(amount)이 SafeMath 라이브러리 함수를 사용한 예시다.
        _approve(sender, msg.sender, currentAllowance, currentAllowance.sub(amount)); // uint256으로 선언했던 currentAllowance에 sub함수를 사용할 수 있는 모습을 볼 수 있다.
        return true;
    }

    function _transfer(address sender, address recipient, uint256 amount) internal virtual {
        require(sender != address(0), "ERC20: transfer from the zero address"); // 보내는 사람(sender)의 계정이 이상한지 확인한다.
        require(recipient != address(0), "ERC20: transfer to the zero address"); // 받는 사람(recipient)의 계정이 이상한지 확인한다.
        require(isTokenLock(sender, recipient) == false, "TokenLock: invalid token transfer"); // 전체 token과 보내는 사람(sender), 받는 사람(recipient)의 token계정이 lock되어 있는지 확인한다.
        uint256 senderBalance = _balances[sender]; // 보내는 사람(sender)의 계정 
        require(senderBalance >= amount, "ERC20: transfer amount exceeds balance"); // 보내는 사람(sender)의 계정의 잔액이 보내고자 하는 금액(amount)보다 많이 남아있는지 확인한다.

        // 다음의 코드에서 currentAllowance.sub(amount)이 SafeMath 라이브러리 함수를 사용한 예시다. (add도 SafeMath라이브러리 함수임)
        _balances[sender] = senderBalance.sub(amount);  //uint256으로 선언했던 senderBalance에 sub함수를 사용할 수 있는 모습을 볼 수 있다.
        _balances[recipient] = _balances[recipient].add(amount);
    }








    // 함수 isTokenLock은 전체 락과, 보내는 사람의 락, 받는 사람의 락을 검사하여 락이 걸려 있는지 확인한다.
    function isTokenLock(address from, address to) public view returns (bool lock) {
      lock = false;

      // tokenLock은 토큰의 '전체' 락에 대한 true, false 상태처리를 해준다. if문을 통해 전체 토큰에 lock이 걸려있는지 확인한다.
      if(_tokenLock == true) {
           lock = true; // lock이 되어있는 상태라고 알려준다.
      }

      // tokenPersonalLock은 토큰의 '개인' 락에 대한 true, false 상태처리를 해준다. if문을 통해 보내는사람(from)의 계정과 받는사람(to)의 계정이 lock되어있는지 확인한다.
      if(_personalTokenLock[from] == true || _personalTokenLock[to] == true) {
           lock = true; // lock이 되어있는 상태라고 알려준다.
      }
    }



    // 함수 removeTokenLock은 전체 token계정의 lock을 해제한다. + 계정을 건드리는 일은 관리자만 할 수 있어야 함으로 함수에 onlyOwner를 적용시켰다.
    function removeTokenLock() onlyOwner public {
      require(_tokenLock == true); // token계정 전체가 잠긴 상태라면
      _tokenLock = false; // lock상태를 false로 바꾸어 해제한다.
    }



    // 함수 removePersonalTokenLock은 입력받은 address의 lock을 해제한다. + 계정을 건드리는 일은 관리자만 할 수 있어야 함으로 함수에 onlyOwner를 적용시켰다.
    function removePersonalTokenLock(address _who) onlyOwner public {
      require(_personalTokenLock[_who] == true); // 입력받은 개인 token계정이 잠긴 상태라면
      _personalTokenLock[_who] = false; // lock상태를 false로 바꾸어 해제한다. 
    }


    //advanced - address를 입력받아 lock을 걸어주는 함수를 만든다 onlyOwner를 적용시켜서 계정관리자만 이 기능을 사용할 수 있도록 해주자
    function assignTokenLock(address _who) onlyOwner public {
        require(_personalTokenLock[_who] == false);
        _personalTokenLock[_who] = true; 
    }



    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");
      _allowances[owner][spender] = amount;
      emit Approval(owner, spender, currentAmount, amount);
    }
}

  • isTokenLock : 전체 lock과, 보내는 사람의 lock, 받는 사람의 lock을 검사하여 lock이 걸려 있는지 확인하고 lock의 상태를 return한다.

  • removeTokenLock : 전체 token계정의 lock을 해제한다.

  • removePersonalTokenLock : 입력받은 address의 lock을 해제한다.

  • assignTokenLock : 입력받은 address의 lock을 걸어준다.







실제로 goerli testnet에 위의 contract를 배포했을 때
lock이 걸려있으면 아래와 같이 transaction이 실패하고 (이더스캔에서 확인함)


lock이 해제되어 있으면 transaction이 처리된다










profile
Notorious

0개의 댓글