본문으로 건너뛰기

ERC-20

소개

이 튜토리얼은 Kaia 토큰 표준, 특히 대체 가능한 토큰 표준 (ERC-20)을 준수하는 ERC-20 호환 토큰 예제를 만들 수 있도록 도와줍니다.

ERC-20 토큰 표준은 아래와 같이 두 가지 이벤트와 9가지 메서드(3가지 옵션 메서드 포함)를 정의하고 있습니다. ERC-20 호환 토큰은 다음과 같은 인터페이스를 구현하는 토큰 컨트랙트입니다.


function name() public view returns (string) //optional
function symbol() public view returns (string) //optional
function decimals() public view returns (uint8) //optional
function totalSupply() public view returns (uint256)
function balanceOf(address _owner) public view returns (uint256 balance)
function transfer(address _to, uint256 _value) public returns (bool success)
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success)
function approve(address _spender, uint256 _value) public returns (bool success)
function allowance(address _owner, address _spender) public view returns (uint256 remaining)
event Transfer(address indexed _from, address indexed _to, uint256 _value)
event Approval(address indexed _owner, address indexed _spender, uint256 _value)

개발자는 위 인터페이스를 기반으로 새로운 기능과 로직을 추가하여 토큰을 커스터마이징하고 Kaia 네트워크에 배포할 수 있습니다. 자세한 내용은 공식 ERC-20 문서를 참고하시기 바랍니다.

이 튜토리얼에서는 ERC-20 호환 토큰인 MyERC20.sol을 구현하겠습니다. 이 토큰은 미리 정의된 양의 토큰을 발행하고 배포 시 모든 토큰을 컨트랙트 소유자에게 보냅니다.

MyERC20.sol은 OpenZeppelin의 ERC20 구현을 기반으로 합니다. 이 튜토리얼의 코드 대부분은 OpenZeppelin 2.3에서 포크되었으며, MyERC20.sol을 구현하기 위해 다음 Solidity 파일이 사용됩니다.

1. ERC-20 스마트 컨트랙트 작성하기

1.1 MyERC20의 전체 구조

MyERC20.sol의 전체 소스 코드는 아래와 같습니다. 이 구현에서 constructor는 컨트랙트 배포 시 미리 정의된 양의 토큰을 발행하기 위해 _mint를 호출합니다.


pragma solidity ^0.5.0;
/**
* @dev Interface of the ERC20 standard as defined in the EIP. Does not include
* the optional functions; to access them see `ERC20Detailed`.
*/
interface IERC20 {
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address recipient, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
library SafeMath {
/**
* @dev Returns the addition of two unsigned integers, reverting on
* overflow.
*
* Counterpart to Solidity's `+` operator.
*
* Requirements:
* - Addition cannot overflow.
*/
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
require(c >= a, "SafeMath: addition overflow");
return c;
}
/**
* @dev Returns the subtraction of two unsigned integers, reverting on
* overflow (when the result is negative).
*
* Counterpart to Solidity's `-` operator.
*
* Requirements:
* - Subtraction cannot overflow.
*/
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
require(b <= a, "SafeMath: subtraction overflow");
uint256 c = a - b;
return c;
}
/**
* @dev Returns the multiplication of two unsigned integers, reverting on
* overflow.
*
* Counterpart to Solidity's `*` operator.
*
* Requirements:
* - Multiplication cannot overflow.
*/
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
// Gas optimization: this is cheaper than requiring 'a' not being zero, but the
// benefit is lost if 'b' is also tested.
// See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522
if (a == 0) {
return 0;
}
uint256 c = a * b;
require(c / a == b, "SafeMath: multiplication overflow");
return c;
}
/**
* @dev Returns the integer division of two unsigned integers. Reverts on
* division by zero. The result is rounded towards zero.
*
* Counterpart to Solidity's `/` operator. Note: this function uses a
* `revert` opcode (which leaves remaining gas untouched) while Solidity
* uses an invalid opcode to revert (consuming all remaining gas).
*
* Requirements:
* - The divisor cannot be zero.
*/
function div(uint256 a, uint256 b) internal pure returns (uint256) {
// Solidity only automatically asserts when dividing by 0
require(b > 0, "SafeMath: division by zero");
uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}
/**
* @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo),
* Reverts when dividing by zero.
*
* Counterpart to Solidity's `%` operator. This function uses a `revert`
* opcode (which leaves remaining gas untouched) while Solidity uses an
* invalid opcode to revert (consuming all remaining gas).
*
* Requirements:
* - The divisor cannot be zero.
*/
function mod(uint256 a, uint256 b) internal pure returns (uint256) {
require(b != 0, "SafeMath: modulo by zero");
return a % b;
}
}
/**
* @dev Implementation of the `IERC20` interface.
*
* This implementation is agnostic to the way tokens are created. This means
* that a supply mechanism has to be added in a derived contract using `_mint`.
* For a generic mechanism see `ERC20Mintable`.
*
* *For a detailed writeup see our guide [How to implement supply
* mechanisms](https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226).*
*
* We have followed general OpenZeppelin guidelines: functions revert instead
* of returning `false` on failure. This behavior is nonetheless conventional
* and does not conflict with the expectations of ERC20 applications.
*
* Additionally, an `Approval` event is emitted on calls to `transferFrom`.
* This allows applications to reconstruct the allowance for all accounts just
* by listening to said events. Other implementations of the EIP may not emit
* these events, as it isn't required by the specification.
*
* Finally, the non-standard `decreaseAllowance` and `increaseAllowance`
* functions have been added to mitigate the well-known issues around setting
* allowances. See `IERC20.approve`.
*/
contract MyERC20 is IERC20 {
using SafeMath for uint256;
mapping (address => uint256) private _balances;
mapping (address => mapping (address => uint256)) private _allowances;
// NOTE Start of https://github.com/OpenZeppelin/openzeppelin-solidity/blob/v2.3.0/contracts/token/ERC20/ERC20Detailed.sol
string private _name;
string private _symbol;
uint8 private _decimals;
constructor (string memory name, string memory symbol, uint8 decimals) public {
_name = name;
_symbol = symbol;
_decimals = decimals;
_mint(msg.sender, 100000 * 10 ** uint256(decimals)); // CAUTION!
}
/**
* @dev Returns the name of the token.
*/
function name() public view returns (string memory) {
return _name;
}
/**
* @dev Returns the symbol of the token, usually a shorter version of the
* name.
*/
function symbol() public view returns (string memory) {
return _symbol;
}
/**
* @dev Returns the number of decimals used to get its user representation.
* For example, if `decimals` equals `2`, a balance of `505` tokens should
* be displayed to a user as `5,05` (`505 / 10 ** 2`).
*
* Tokens usually opt for a value of 18, imitating the relationship between
* Ether and Wei.
*
* > Note that this information is only used for _display_ purposes: it in
* no way affects any of the arithmetic of the contract, including
* `IERC20.balanceOf` and `IERC20.transfer`.
*/
function decimals() public view returns (uint8) {
return _decimals;
}
// NOTE End of https://github.com/OpenZeppelin/openzeppelin-solidity/blob/v2.3.0/contracts/token/ERC20/ERC20Detailed.sol
uint256 private _totalSupply;
/**
* @dev See `IERC20.totalSupply`.
*/
function totalSupply() public view returns (uint256) {
return _totalSupply;
}
/**
* @dev See `IERC20.balanceOf`.
*/
function balanceOf(address account) public view returns (uint256) {
return _balances[account];
}
/**
* @dev See `IERC20.transfer`.
*
* Requirements:
*
* - `recipient` cannot be the zero address.
* - the caller must have a balance of at least `amount`.
*/
function transfer(address recipient, uint256 amount) public returns (bool) {
_transfer(msg.sender, recipient, amount);
return true;
}
/**
* @dev See `IERC20.allowance`.
*/
function allowance(address owner, address spender) public view returns (uint256) {
return _allowances[owner][spender];
}
/**
* @dev See `IERC20.approve`.
*
* Requirements:
*
* - `spender` cannot be the zero address.
*/
function approve(address spender, uint256 value) public returns (bool) {
_approve(msg.sender, spender, value);
return true;
}
/**
* @dev See `IERC20.transferFrom`.
*
* Emits an `Approval` event indicating the updated allowance. This is not
* required by the EIP. See the note at the beginning of `ERC20`;
*
* Requirements:
* - `sender` and `recipient` cannot be the zero address.
* - `sender` must have a balance of at least `value`.
* - the caller must have allowance for `sender`'s tokens of at least
* `amount`.
*/
function transferFrom(address sender, address recipient, uint256 amount) public returns (bool) {
_transfer(sender, recipient, amount);
_approve(sender, msg.sender, _allowances[sender][msg.sender].sub(amount));
return true;
}
/**
* @dev Atomically increases the allowance granted to `spender` by the caller.
*
* This is an alternative to `approve` that can be used as a mitigation for
* problems described in `IERC20.approve`.
*
* Emits an `Approval` event indicating the updated allowance.
*
* Requirements:
*
* - `spender` cannot be the zero address.
*/
function increaseAllowance(address spender, uint256 addedValue) public returns (bool) {
_approve(msg.sender, spender, _allowances[msg.sender][spender].add(addedValue));
return true;
}
/**
* @dev Atomically decreases the allowance granted to `spender` by the caller.
*
* This is an alternative to `approve` that can be used as a mitigation for
* problems described in `IERC20.approve`.
*
* Emits an `Approval` event indicating the updated allowance.
*
* Requirements:
*
* - `spender` cannot be the zero address.
* - `spender` must have allowance for the caller of at least
* `subtractedValue`.
*/
function decreaseAllowance(address spender, uint256 subtractedValue) public returns (bool) {
_approve(msg.sender, spender, _allowances[msg.sender][spender].sub(subtractedValue));
return true;
}
/**
* @dev Moves tokens `amount` from `sender` to `recipient`.
*
* This is internal function is equivalent to `transfer`, and can be used to
* e.g. implement automatic token fees, slashing mechanisms, etc.
*
* Emits a `Transfer` event.
*
* Requirements:
*
* - `sender` cannot be the zero address.
* - `recipient` cannot be the zero address.
* - `sender` must have a balance of at least `amount`.
*/
function _transfer(address sender, address recipient, uint256 amount) internal {
require(sender != address(0), "ERC20: transfer from the zero address");
require(recipient != address(0), "ERC20: transfer to the zero address");
_balances[sender] = _balances[sender].sub(amount);
_balances[recipient] = _balances[recipient].add(amount);
emit Transfer(sender, recipient, amount);
}
/** @dev Creates `amount` tokens and assigns them to `account`, increasing
* the total supply.
*
* Emits a `Transfer` event with `from` set to the zero address.
*
* Requirements
*
* - `to` cannot be the zero address.
*/
function _mint(address account, uint256 amount) internal {
require(account != address(0), "ERC20: mint to the zero address");
_totalSupply = _totalSupply.add(amount);
_balances[account] = _balances[account].add(amount);
emit Transfer(address(0), account, amount);
}
/**
* @dev Destroys `amount` tokens from `account`, reducing the
* total supply.
*
* Emits a `Transfer` event with `to` set to the zero address.
*
* Requirements
*
* - `account` cannot be the zero address.
* - `account` must have at least `amount` tokens.
*/
function _burn(address account, uint256 value) internal {
require(account != address(0), "ERC20: burn from the zero address");
_balances[account] = _balances[account].sub(value);
_totalSupply = _totalSupply.sub(value);
emit Transfer(account, address(0), value);
}
/**
* @dev Sets `amount` as the allowance of `spender` over the `owner`s tokens.
*
* This is internal function is equivalent to `approve`, and can be used to
* e.g. set automatic allowances for certain subsystems, etc.
*
* Emits an `Approval` event.
*
* Requirements:
*
* - `owner` cannot be the zero address.
* - `spender` cannot be the zero address.
*/
function _approve(address owner, address spender, uint256 value) internal {
require(owner != address(0), "ERC20: approve from the zero address");
require(spender != address(0), "ERC20: approve to the zero address");
_allowances[owner][spender] = value;
emit Approval(owner, spender, value);
}
/**
* @dev Destoys `amount` tokens from `account`.`amount` is then deducted
* from the caller's allowance.
*
* See `_burn` and `_approve`.
*/
function _burnFrom(address account, uint256 amount) internal {
_burn(account, amount);
_approve(account, msg.sender, _allowances[account][msg.sender].sub(amount));
}
}

MyERC20.sol은 하나의 인터페이스 IERC20, 하나의 라이브러리 SafeMath, 그리고 IERC20 인터페이스를 구현하는 하나의 컨트랙트 MyERC20으로 구성되어 있습니다.

  • IERC20 인터페이스는 ERC-20 규격에서 설명하는 필수 인터페이스를 정의합니다.
  • SafeMath 라이브러리는 Solidity의 산술 연산에 대한 래퍼를 정의하며, uint256 타입의 Solidity를 안전하게 계산하기 위해 오버플로우 검사를 추가했습니다.
  • MyERC20IERC20 인터페이스를 구현하며, ERC-20 사양에 설명된 세 가지 선택적 메서드도 정의합니다.
    • ERC20 외에도 constructor가 정의되어 있으며, 이 생성자는 새로운 ERC20 토큰 이름과 심볼을 정의하고 미리 정의된 양의 토큰을 발행하는 데 사용됩니다. constructor는 처음 배포할 때 한 번 호출됩니다.

1.2 중요한 방법 살펴보기

몇 가지 중요한 방법을 자세히 살펴보겠습니다.

(1) function balanceOf(address account) external view returns (uint256);

balanceOf는 ERC-20의 필수 메서드입니다. balanceOf\`는 주어진 주소의 잔액을 반환합니다.


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

balanceOf_balances에 저장된 account 키의 값을 아래와 같이 mapping (address => uint256) 유형으로 반환합니다.


mapping (address => uint256) private _balances;

balances에 사용 가능한 키 account가 없으면 0을 반환합니다.

(2) function transfer(address recipient, uint256 amount) external returns (bool);

transfer은 ERC-20의 필수 메서드입니다. transferrecipient에게 amount의 토큰을 전송하며, 반드시 Transfer 이벤트를 발생시켜야 합니다. 메시지 발신자의 계정 잔액에 사용할 토큰이 충분하지 않은 경우 이 함수가 발생해야 합니다.

transfer는 아래와 같이 실제 전송 및 이벤트를 구현하는 내부 메서드 _transfer를 호출하기만 하면 됩니다.


function transfer(address recipient, uint256 amount) public returns (bool) {
_transfer(msg.sender, recipient, amount);
return true;
}

transfer은 ERC-20의 transfer 메서드의 실제 동작을 구현합니다.

또한 아래와 같이 require를 사용하여 0 주소로 토큰을 보내거나 보내는 것을 방지합니다.


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

(3) function approve(address spender, uint256 amount) external returns (bool);

approve은 ERC-20의 필수 방식입니다. approve을 사용하면 spenderamount까지 계정에서 여러 번 인출할 수 있습니다. 이 함수가 여러 번 호출되면 단순히 허용량을 amount으로 재설정합니다.

approveapprove의 실제 동작을 구현하는 내부 메서드 _approve를 호출할 뿐입니다. msg.sender는 계정 owner로서 전달됩니다.


function approve(address spender, uint256 value) public returns (bool) {
_approve(msg.sender, spender, value);
return true;
}
function _approve(address owner, address spender, uint256 value) internal {
require(owner != address(0), "ERC20: approve from the zero address");
require(spender != address(0), "ERC20: approve to the zero address");
_allowances[owner][spender] = value;
emit Approval(owner, spender, value);
}

_approve은 특정 address에서 spender에 대해 허용된 value를 유지하는 2차원 사전인 _allowances을 업데이트합니다.


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

(4) function _mint(address account, uint256 amount) internal

_mint는 ERC-20의 일부가 아닙니다. 하지만 새로운 ERC-20 토큰을 생성하는 방법이 필요했고, 이번 구현에서는 아래와 같이 새로운 토큰을 생성하기 위해 _mint를 도입했습니다.


function _mint(address account, uint256 amount) internal {
require(account != address(0), "ERC20: mint to the zero address");
_totalSupply = _totalSupply.add(amount);
_balances[account] = _balances[account].add(amount);
emit Transfer(address(0), account, amount);
}

_mint는 내부 메서드이며 이 컨트랙트 내부에서 호출할 수 있습니다.

MyERC20.sol에서 _mint는 스마트 컨트랙트를 배포할 때 constructor에서 한 번만 호출하여 미리 정의된 양의 토큰을 발행합니다.

스마트 컨트랙트를 배포한 후 토큰을 추가로 발행하려면 mint와 같은 새로운 공개 방식을 도입해야 합니다. 이 방법은 권한이 있는 사용자만 토큰을 발행할 수 있어야 하므로 주의해서 구현해야 합니다.

자세한 내용은 OpenZeppelin 예제 ERC20Mintable.sol을 참고하세요.

2. 스마트 컨트랙트 배포

In this section, you'll deploy your MyERC20 smart contract using Remix Online IDE. The complete source code for MYERC20.sol was given at Writing ERC-20 Smart Contract.

2.1 Prerequisites

  • Kaia Wallet: used to deploy contracts, sign transactions and interact with contracts.
  • Test KAIA from Faucet: fund your account with sufficient KAIA.

Remix 온라인 IDE를 사용하거나 Truffle을 사용하여 MyERC20 스마트 컨트랙트를 배포할 수 있습니다.

2.2 Deploying smart contract using Remix Online IDE

Remix IDE

  • Navigate to Kaia Plugin for Remix
  • Create a MyERC20.sol file in the contracts folder
  • In Remix, click compile contract.
  • Click the Kaia (prev Klaytn) tab on your left having installed the plugin
  • Select Environment > Injected Provider - Kaia Wallet.
  • In Contract field, select your contract. For example, MyERC20.
  • Assign the following arguments at deployment KAIROSTOKENKAIROS and 8
  • Click Deploy.

ERC20-1-deploy

배포 후, 컨트랙트를 배포할 때 사용한 계정으로 balanceOf를 호출할 수 있습니다. 아래와 같이 계정에서 10000000000000 토큰을 사용할 수 있습니다. 위 컨트랙트를 배포할 때 decimal8로 설정했기 때문에 생성자에서 고정된 수의 100000 토큰을 발행했으며, 한 토큰의 소수점 값은 10^8입니다. totalSupply 메서드는 발행된 토큰의 총 공급량을 반환하며, 이 역시 10000000000000이어야 합니다.

ERC20-2-owner-token

이제 MyERC20이 출시되었습니다!

3. 카이아 지갑에서 ERC-20 토큰과 상호작용하기

You can use Kaia Wallet to check your balance and transfer the ERC-20-compatible KAIROSTOKEN you just deployed. To view your token balance in Kaia Wallet, follow the steps below:

Kaia Wallet

  • Open up Kaia Wallet
  • Click on the Token List Icon, and then click Add Token button

  • Paste the address of myERC20.sol contract in the Token Contract Address field under Custom Token tab.
  • Follow the prompts afterwards to add your token. Your Token List modal should like like this: