Skip to content

Latest commit

 

History

History
960 lines (723 loc) · 39.9 KB

36_ERC-20.md

File metadata and controls

960 lines (723 loc) · 39.9 KB

ERC-20

Fungible Token을 만들 수 있는 Solidity reference code


[TOC]


ERC-20

ERC-20(20은 리퀘스트 숫자)은 EIP-20에서 논의되어 생성된 표준(Standard)로, 이더리움 블록체인 네트워크에서 정한 표준 토큰(FT; Fungible Token) 스펙이다. ERC-20 기반으로 생성된 토큰들은 이더리움 플랫폼을 기반으로 한 다른 dApp의 토큰들과 상호 호환이 가능하고, 동일한 이더리움 지갑으로 전송이 가능하다.

  • EIP(Ethereum Improvement Proposals): 이더리움 개선 제안
  • ERC(Ethereum Request for Comments): 이더리움 기능 표준

ERC-20 토큰은 되기 위한 기준은 스마트 컨트랙트 기능 포함여부이다. ERC-20 기준을 맞춰 dApp을 설계한 후 토큰을 발행하면, 이더리움을 쉽게 교환할 수 있고, 표준 이더리움 지갑(My Ether Wallet, Meta Mask, Mist 등)에 자유롭게 전송할 수 있다.

<실습> ERC-20 토큰 개발하기

Remix와 Metamask를 활용한다.

전체 코드

이 코드를 ERC-20 토큰이라고 한다. 토큰을 만드는 사람에 따라 기본 코드에 추가로 함수를 더할 수 있다.

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

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

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 oladAmount, uint256 amount);
}
// ...

Interface

Interface는 사용할 함수의 형태를 선언할 때 사용하며, 실제 함수의 내용은 Contract에서 사용한다.

ERC20Interface는 예제에서 쓰일 SimpleToekn 컨트랙트에서 사용할 함수의 형태를 선언하며, ERC-20에서 사용하는 함수들의 형태를 선언한 것을 확인할 수 있다.

Function & Event

**함수(function)**는 이더리움에서 제공하는 함수이며, event는 이더리움에서 제공하는 로그이다. function과 event를 선언할 때 입력값과 반환값은 선택할 수 있으나, function의 자료형, 이름, 순서를 주의해야 한다. ERC20Interface의 Transfer 이벤트는 토큰이 이동할 때마다 로그를 남기고, Approval 이벤트approve 함수가 실행될 때 로그를 남긴다.

함수의 구성

Solidity에서 함수(function)는 다음과 같이 구성된다.

function (<parameter types) [internal | external | public | private] [pure | constant | view | payable] [(modifiers)] [returns (<return types>)]

Parameter Types

함수에서 받아올 매개변수를 타입과 함께 선언한다.

Visibility Keyword

Visibility Keyword는 Java나 C++에서 Public, Private, Protected와 같은 접근제어자(access contral) 역할을 한다. Solidity 언어에서 스마트 컨트랙트 내의 **상태 변수(State variable)**와 함수에 적용할 수 있는 visibility는 4가지가 있다.

  • internal: Smart contract의 interface로 비공개한다. 계약서(contract)의 해당 내용을 비공개로 한다는 의미이며, 계약서의 내부에서 사용하는 함수라는 것을 표시한다. 계약서 자신과 상속받은 계약서만 사용할 수 있다. (상태변수는 internal이 기본값)
  • external: Smart contract의 interface로 공개한다. 계약서(contract)의 해당 내용을 공개한다는 의미이며, 계약서의 외부에서 사용하는 함수라는 것을 표시한다. 계약서 내부에서 사용할 경우 this를 사용해서 접근해야 한다. (상태변수는 external일 수 없다.)
  • public: 공개함수이다. 공개 기능은 계약 인터페이스의 일부이며 내부적으로 또는 메시지를 통해 호출할 수 있다. 공개 상태 변수의 경우 자동 getter 함수가 생성된다.
  • private: 비공개함수이다. 계약서 내부에서도 자신만 사용하는 함수라는 것을 표시한다. 상태변수와 함수 모두 파생된 계약이 아닌 정의된 계약에서만 볼 수 있다.

함수 동작 관련 키워드

  • pure: storage에서 변수를 읽어오거나 쓰지 않는 함수임을 명시한다.
  • constant, view: 상태를 변경하지 않는 함수임을 명시한다.
  • payable: 입금을 받을 수 있는 함수임을 명시한다.

Contract(SimpleToken)

ERC20Interface에서는 함수의 형태만 선언을 하고, 함수의 내용은 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;
    uint private E18 = 1000000000000000000;

    constructor(string memory getName, string memory getSymbol) {
        _name = getName;
        _symbol = getSymbol;
        _decimals = 18;
        _totalSupply = 100000000 * E18;
        _balances[msg.sender] = _totlaSupply;    // 추가
    }

    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[spender][msg.sender];    // 삭제
        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");
        require(currentAmount == _allowances[owner][spender], "ERC20: invalid currentAmount");
        _allowances[owner][spender] = amount;  // 삭제
        emit Approval(owner, spender, currentAmount, amount);
    }
}
// ...

상속

SimpleToken 컨트랙트는 다음과 같이 선언되었다.

contract SimpleToken is ERC20Interface {...}

이렇게 하면 SimpleToken 컨트랙트가 ERC20Interface 함수를 사용할 수 있게 된다.

이중 객체

이중으로 매핑된 _allowances는 변수는 일종의 이중 객체로, 객체의 키는 OWNER의 address(주소값)이며, 값은 토큰을 양도받은 SPENDER에 대한 객체이다.

  • KEY: OWNER의 address
  • VALUE: (KEY: SPENDER의 address, VALUE: 맡겨둔 TOKEN의 개수)
contract SimpleToken is ERC20Interface {
    mapping (address => uint256) private _balances;
    mapping (address => mapping (address => uint256)) public _allowances;    // 이중 객체
}

ERC-20 함수

ERC-20에는 다음과 같은 함수가 있다.

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

  • 직접 토큰을 다른 사람에게 전송할 경우 transfer 함수
  • 토큰을 등록하는 방식을 사용하는 경우 approve, allowance, transferFrom 함수

ERC-20_approve_allowance_transform

(출처: codestates)

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

    토큰의 총 발행량을 반환한다.

        function totalSupply() external view virtual override returns (uint256) {
            return _totalSupply;
        }
  • balanceOf: owner가 가지고 있는 토큰의 보유량 확인

    매핑된 값인 _balanceOf에서 입력한 address인 account가 가지고 있는 토큰의 수를 리턴한다.

        function balanceOf(address account) external view virtual override returns (uint256) {
             return _balances[account];
        }
  • transfer: 토큰을 전송

    transfer의 내부 함수인 _transfer을 호출한다. 호출이 정상적으로 완료되었을 경우 Transfer event를 발생시킨다.

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

    _transferrequire를 통해 세 가지 조건을 검사한다.

    1. 보내는 사람의 주소가 잘못되었는지 체크한다.
    2. 받는 사람의 주소가 잘못되었는지 체크한다.
    3. transfer 함수를 실행한 사람(sender)이 가진 토큰(senderBalance)이 신청한 값(amount)보다 많읁 토큰을 가지고 있는지 체크한다.

    위 세 조건을 충족하는 경우, 실행한 사람(sender)이 가진 토큰의 지갑에서 토큰을 개수만큼 빼고, 받을 사람(recipient)의 토큰 지갑에 개수만큼 더해준다.

  • approve: spender에게 value만큼의 토큰을 인출할 권리를 부여 (이 함수 이용 시 반드시 Approval 이벤트 함수를 호출해야 함)

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

    내부 함수인 _approve를 호출한다. _approve에서는 내가 토큰을 양도할 상대방(spender)에게 양도할 값(amount)를 allowances에 기록한다. 이 상태에서는 양도가 실제로 이루어진 것이 아니라, 양도를 할 주소와 양을 정한 것이다.

    approve는 단순 변경을 위한 함수이기 때문에 내부적으로 값을 올리고 내리는 increaseApprovaldecreaseApproval 함수를 사용하기도 한다. approve는 spender가 당신의 계정으로부터 amount 한도 내에서 여러 번 출금하는 것을 허용한다. 이 함수를 여러 번 호출하면, 단순히 허용량을 amount로 재설정한다.

  • allowance: owner가 spender에게 양도 설정한 토큰의 양을 확인

    입력한 2개의 주소 값에 대한 _allowances 값, 다시 말 해 owner가 spender에게 토큰을 등록한 양을 반환한다.

        function allowance(address owner, address spender) external view override returns (uint256) {
            return _allowances[owner][spender];
        }
  • transferFrom: spender가 거래 가능하도록 양도 받은 토큰을 전송

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

    transferFrom은 양도를 수행하는 거래 대행자(msg.sender)가 sender가 허락해준 값만큼 상대방(recipient)에게 토큰을 이동한다. 이동을 위해 _transfer 함수를 실행시킨다. _transfer에서는 양도를 하는 sender의 잔고를 amount만큼 줄이고, recipient의 잔고를 amount만큼 늘린다. _transfer 함수 실행이 완료디고 require를 모두 통과한다면 currentAllowance를 체크하여 _approve 함수를 실행한다.

<실습> ERC-20 이더리움 테스트넷 배포

Ropsten 테스트넷에 ERC-20 배포하기

MetaMask 지갑, Ropsten 테스트 네트워크, Remix IDE를 사용한다. 테스트용 Ropsten rETH는 여기서 받는다.

  1. MetaMask에 접속하고 Ropsten 테스트 네트워크를 선택한다.

  2. Remix IDE에서 SimpleToken.sol 파일을 생성한다.

  3. 위에 있는 ERC-20 코드를 입력한다.

  4. SOLIDITY COMPILER 메뉴로 이동하여 SimpleToken.sol 파일을 컴파일한다.

  5. DEPLOY & RUN TRANSACTIONS 메뉴로 이동 후, 배포 옵션을 다음과 같이 설정한다.

    • Injected Web3로 변경하여 MetaMask(Ropsten (3) network)와 연동한다.
    • Account를 내가 발행할 계정으로 변경한다.
    • CONTRACT에서 SimpleToken.sol이 지정되었는지 확인한다.
    • DEPLOY의 상세 옵션을 연 후 들어갈 인자값을 입력한다.
      • GETNAME: MySimpleToken
      • GETSYMBOL: MST

    설정이 완료되면 transact 버튼을 누른다.

  6. MetaMask에서 계약 생성에 대한 내역을 확인하고, 확인 버튼을 눌러 진행한다.

  7. 계약 생성이 완료되면 Remix 내 Deployed Contracts에서 계약을 확인할 수 있다. (이더스캔에서도 확인 가능)

    MetaMask에서 토큰을 사용하기 위해 컨트랙트 주소를 복사한다.

  8. MetaMask 하단의 Import tokens를 클릭한다.

  9. 토큰 계약 주소에 Remix에서 복사한 주소를 입력한 후, Add Custom Token을 클릭한다.

    • 토큰 기호: MST
    • 토큰 십진수: 18
  10. ERC-20 코드의 totalSupply 개수만큼 잔액이 적용된 토큰을 확인할 수 있다.

토큰 보내기

  1. 토큰 화면에서 보내기를 클릭해 보낼 계정의 주소를 입력한다.
  2. 보낼 토큰의 개수를 입력하여 트랜잭션을 생성한다.

<실습> ERC-20 라이브러리 - SafeMath

  • Overflow: 어떠한 것이 용량을 초과해 흘러 넘치는 것
  • Underflow: 어떠한 것이 정해진 범위 아래로 마이너스가 되는 것

기본 연산자에 있어서 안전하게 연산이 가능하도록 SafeMath라는 함수를 만들어본다.

전체 코드

// 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 라이브러리 사용방법

메인 커트랙트인 SimpleToken에서 자료형 uint256(0부터 2^256-1 만큼의 값을 제공)에 대해서 SafeMath 라이브러리를 사용하도록 선언해준다.

    using SafeMath for uint256;

uint256으로 선언된 함수에 대해서 SafeMath Library를 이용해서 해당 함수를 사용할 수 있다.

SafeMath 사용 예시

**transferFrom**과 **_transfer**에서 사용되는 연산자(+, -)를 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");
        // 다음의 코드에서 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);
    }

uint256으로 선언했던 currentAllowancesenderBalancesub 함수를 사용할 수 있다. 또한, Mapping으로 선언했던 _balancesuint256으로 받아오는 값에서 add 함수를 사용할 수 있다.

이 SafeMath 라이브러리의 함수를 사용하면 overflow와 underflow를 예방할 수 있는 안전한 Transfer을 만들 수 있다. 만약 overflow나 underflow가 발생한다면 currentAllowance, senderBalance를 검사하는 require에서 문제가 발생할 수 있다.

SafeMath에 사용된 Solidity 문법

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;
    }
}
  • **internal**은 함수가 컨트랙트 내부에서만 사용될 수 있도록 제한할 때 사용한다.
  • **pure**는 함수가 상태 변수를 읽거나 쓰지 않을 때 사용한다. SafeMath 라이브러리의 함수에서는 단순 연산의 결과값을 반환하기 때문에 상태 변수를 읽거나 쓰지 않는다.

**assert**는 false를 리턴하지만 계약을 실행시키기 전에 확인할 수 없고, require는 계약을 실행시키기 전에 확인을 할 수 있기 때문에, require 대신 assert를 사용했다. (참고문서)

📌 Overflow와 Underflow를 예방하기 위해 의심이 되는 부분에서는 항상 SafeMath 함수 사용을 습관화하는 것이 좋다.

<실습> ERC-20 추가기능 - OwnerHelper, TokenLock

특정 함수를 관리자만 사용할 수 있도록 설정하는 OwnerHelper 함수를 만든다.

전체 코드

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

OwnerHelper

OwnerHelper 컨트랙트는 abstract contract라고 하는 추상 컨트랙트이다.

추상 컨트랙트

단순히 컨트랙트 내부에서 하나 이상의 함수가 추상 함수이면 자동으로 추상 컨트랙트가 된다. 추상 함수는 단지 함수 이름, 입력 매개 변수, 출력 매개 변수만 선언해두고, 내용은 없는 함수이다. 내용이 업기 때문에 중괄호 없이 끝에 ;만 붙이면 된다.

function name(x1, ... xN) option returns (y1, ..., yN);

abstract contract는 contract의 구현된 기능과 interface의 추상화 기능 모두를 포함하며, 만약 실제 contract에서 사용하지 않는다면 추상으로 표시되어 사용되지 않는다.

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

_owner는 관리자를 나타낸다.

    address private _owner;

ownershipTransferred 이벤트는 관리자가 변경되었을 때 이전 관리자의 주소와 새로운 관리자의 주소 로그를 남긴다.

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

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

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

TokenLock

isTokenLcok

tokenLock은 토큰의 전체 lock에 대한 처리, tokenPersonalLock은 토큰의 개인 lock에 대한 처리이다. 함수 isTokenLock은 전체 lock과 보내는 사람의 lock, 받는 사람의 lock을 검사하여 lock이 걸려 있는지 확인한다.

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

_transfer에 락 검사하기

_transfer에 검사를 추가해, 보내는 사람과 받는 사람 중 lock이 걸려있다면 토큰은 이동이 불가능하다.

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

이 lock들을 제거할 수 있는 removeTokenLockremovePersonalTokenLock도 존재한다. 해당 함수들은 onlyOwner를 적용하여 관리자만 lock을 해제할 수 있도록 해야한다. 이렇게 lock을 적용하게 되면 모든 lock을 해제할 때만 토큰의 이동이 가능하다.

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

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

배포 및 테스트

Remix의 Simple Token 예제를 활용해 OwnerHelper, TokenLock을 테스트해본다.

  1. Simple Token 코드에 OwnerHelper 코드를 추가한다. (위의 전체코드 복사)

  2. Ropsten 네트워크에 OwnerHelper가 적용된 SimpleToken을 배포한다.

    DEPLOY & RUN TRANSACTIONS

    • DEPLOY 옵션 펼치기
      • GETNAME: OwnerHelperTest
      • GETSYMBOL: OHT
  3. 트랜잭션이 완료되면 토큰을 추가한다.

  4. 다른 주소로 전송해본다.

    당연히 전송에 실패한다.

  5. 이더스캔에서 트랜잭션이 실패한 원인을 확인한다.

    Status란을 보면 Fail with error "TokenLock: invalid token transfer"라고 표시된다. (표시가 안 되는 경우도 있는 것 같음)

  6. Lock을 해제하기 위해 removeTokenLock 함수와 전송할 계정, 받을 계정에 대해 removePersonalTokenLock 함수를 실행한다.

    Remix의 DEPLOY & RUN TRANSACTIONS 가장 하단에 보면 Deployed Contracts라는 부분이 있는데 이 옵션을 펼치면 함수를 실행할 수 있는 부분이 표시된다.

    • removePersonalTokenLock: (내 주소와 상대방 주소를 넣고 transact한다.)
    • removeTokenLock: (입력 값 없이 실행한다.)

    함수를 실행하면 gas fee가 필요하다는 팝업이 표시되는데, gas fee를 지불한다.

(Advanced) 더 해보기

  1. 관리자가 한 명인 컨트랙트를 세 명인 컨트랙트로 변경하여 투표를 통해 관리자 변경으로 만들어본다.
  2. Lock을 일회성이 아닌 재사용 가능항 형태의 함수로 변경해본다.

Copyright © 2022 Song_Artish