ERC20を実装したトークンを作成する
今回は、ERC20の規格に準拠したトークンを作成してみましょう.
ERC20は、FT(Fungible Token)のためのインタフェースです。
ERC20に準拠することで、スマートコントラクトから扱いやすくなり、別の種類のトークンとの交換が容易になります。
ここで、FTとは代替可能なトークンであり、同じ種類のトークンであれば、すべてのトークンで同じ価値を持ちます.
BitcoinやEtherもFTです.
また、1 Bitcoinを0.7 Bitcoinと0.3 Bitcoinのように分割することも可能です.
FTと対になるものがNFT(Non-Fungible Token)です.
NFTは、各トークンが唯一無二であり、異なる性質を持ちます.
NFTはデータにイラストなどのコンテンツを参照するアドレスを保持します.
OpenSeaといったマーケットプレイスで、NFTを購入することができます.
また、FTのように、1つのNFTをより小さい単位に分割することはできません.
ERC20のインタフェース
ERC20に準拠するには、以下のMethodsとEventsを実装する必要があります.
以下に、MethodsとEventsのシグネチャと説明を記載しました.
EIP-20: Token Standard からMethods, Eventsのシグネチャを引用
Methods
メソッド名 | 引数 | 返り値 | 必須 | 説明 |
---|---|---|---|---|
name | - | string | - | トークンの名前を返す |
symbol | - | string | - | トークンのシンボル(ex: USDT)を返す |
decimals | - | uint8 | - | ユーザのトークンの保有量を表示するときに、この数で割った値を使用する |
totalSupply | - | uint256 | ○ | トークンの総量を返す |
balanceOf | address _owner | uint256 balance | ○ | _owner のアドレスのトークンの残高を返す |
transfer | address _to, uint256 _value |
bool success | ○ | トークンを自身のアドレスからto のアドレスに転送する転送に成功した場合、 Transfer イベントを発火する必要がある |
transferFrom | address _from, address _to, uint256 _value |
bool success | ○ | トークンをfrom のアドレスからto のアドレスに転送する転送に成功した場合、 Transfer イベントを発火する必要がある |
approve | address _spender, uint256 _value |
bool success | ○ | ユーザが_value で指定した量までのトークンを、_spender が自身のアドレスに転送することを許可する |
allowance | address _owner, address _spender |
uint256 remaining | ○ | ユーザが_owner のアドレスから自身のアドレスに転送可能なトークンの量を返す |
Events
イベント名 | 引数 | 説明 |
---|---|---|
Transfer | address indexed _from, address indexed _to, uint256 _value |
トークンが転送されたときや新たにミントされたときに発火するイベント |
Approval | address indexed _owner, address indexed _spender, uint256 _value |
approve の呼び出しに成功したときに発火するイベント |
ERC20に準拠したトークンの実装
では、実際にトークンを実装してみましょう.
今回は、OpenZeppelinのERC20
を継承して実装しました.
ERC 20 - OpenZeppelin Docs
ここで、ERC20
はIERC20
を実装したものです.
以下のリンクからソースコードを確認できます.
github.com
ソースコード
今回、作成したトークンは、LotteryToken
です.
LotteryToken
は、以下の性質を持ちます.
mint
と一緒にETHを送金する(コントラクトに預ける)ことで、送金したETHと同じの量のトークンを入手できる- 送金したETHと同じ量のトークンを
burn
することで、コントラクトから送金したETHを引き落とすことができる - トークンを転送したとき(mint, burnを除く)に、トークンがmintされ、ETHを預けているアカウントのいずれかに転送される
(トークンが転送されるアカウントは、預けているETHの割合に応じて決定される)
ソースコード全体は、以下のリンクから確認できます.
github.com
//SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.9; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/utils/structs/EnumerableMap.sol"; contract LotteryToken is ERC20 { using EnumerableMap for EnumerableMap.AddressToUintMap; // transferが発生したときに、いずれかのアカウントに転送されるトークンの量 uint REWARD = 100; // 各アカウント預けているETHの量を表すのに、EnumerableMapを使用した. // これを使うことで、Mapの要素数を取得したり、イテレートが可能. EnumerableMap.AddressToUintMap etherBalances; event Win(address indexed winner, uint amount); // コンストラクターで、name, symbol を設定 constructor() ERC20("LotteryToken", "LT") {} function mint() external payable { require(msg.value > 0, "ETH is required to mint tokens."); (, uint amount) = etherBalances.tryGet(msg.sender); etherBalances.set(msg.sender, amount + msg.value); _mint(msg.sender, msg.value); } function redeem(uint amount) public { require(balanceOf(msg.sender) >= amount, "Redemption amount must not exceed balance."); // 引き落とすETHと同じ量のトークンを保持している必要がある (, uint curAmount) = etherBalances.tryGet(msg.sender); require(curAmount >= amount, "Redemption amount must not exceed ether balance."); _burn(msg.sender, amount); etherBalances.set(msg.sender, curAmount - amount); (bool sent,) = payable(msg.sender).call{value : amount}(""); require(sent, "Failed to send Ether"); } // transferの完了後に呼ばれるHook. function _afterTokenTransfer(address from, address to, uint256 amount) internal override { // mintもしくはburnしたときは、報酬なし if (from == address(0) || to == address(0)) return; // パラメータ群から、0 <= x < コントラクト残高 の範囲の値を生成する // 完全なランダムな値ではないので、注意すること uint targetAmount = uint(keccak256(abi.encodePacked(block.timestamp, totalSupply(), from, to, amount))) % address(this).balance; address receiver = _lottery(targetAmount); _mint(receiver, REWARD); emit Win(receiver, REWARD); } function _lottery(uint targetAmount) private view returns (address receiver) { // アカウントの残高に応じて、報酬を受け取るアカウントを決定する // アカウントの残高が多いほど、レンジが広くなり、targetAmountを含む確率が高くなる uint total; for (uint i = 0; i < etherBalances.length(); i++) { (address account, uint amount) = etherBalances.at(i); if (total <= targetAmount && targetAmount < total + amount) { receiver = account; break; } total = total + amount; } } }
テストコード
const {expect} = require("chai"); const {loadFixture} = require("@nomicfoundation/hardhat-network-helpers"); describe("LotteryToken contract", function () { async function deployLotteryTokenFixture() { const LotteryToken = await ethers.getContractFactory("LotteryToken"); const [owner, addr1, addr2] = await ethers.getSigners(); const lotteryToken = await LotteryToken.deploy(); await lotteryToken.deployed(); return {lotteryToken, owner, addr1, addr2}; } describe("Mint", function () { it("Should mint token", async function () { const {lotteryToken, owner} = await loadFixture(deployLotteryTokenFixture); await expect(lotteryToken.mint({value: 100})) .to.changeTokenBalance(lotteryToken, owner, 100) .to.changeEtherBalances([owner, lotteryToken], [-100, 100]) }); }); describe("Transfer", function () { it("Should transfer token and win reward", async function () { const {lotteryToken, owner, addr1} = await loadFixture(deployLotteryTokenFixture); await lotteryToken.mint({value: 100}) await expect(lotteryToken.transfer(addr1.address, 50)) .to.changeTokenBalances(lotteryToken, [owner, addr1], [50, 50]) }); it("Should emit event", async function () { const {lotteryToken, owner, addr1} = await loadFixture(deployLotteryTokenFixture); await lotteryToken.mint({value: 100}) await expect(lotteryToken.transfer(addr1.address, 50)) .to.emit(lotteryToken, "Win").withArgs(owner.address, 100) }); it("Should increase total supply", async function() { const {lotteryToken, owner, addr1, addr2} = await loadFixture(deployLotteryTokenFixture); await lotteryToken.mint({value: 100}) await lotteryToken.connect(addr1).mint({value: 100}) await lotteryToken.connect(addr2).mint({value: 100}) await expect(lotteryToken.transfer(addr1.address, 10)) await expect(lotteryToken.connect(addr1).transfer(addr2.address, 20)) await expect(lotteryToken.connect(addr2).transfer(owner.address, 30)) expect(await lotteryToken.connect(owner).totalSupply()).to.equal(600) }); }); describe("Redeem", function () { it("Should redeem ETH", async function () { const {lotteryToken, owner, addr1} = await loadFixture(deployLotteryTokenFixture); await lotteryToken.mint({value: 100}) await expect(lotteryToken.redeem(100)) .to.changeTokenBalance(lotteryToken, owner, -100) .to.changeEtherBalances([owner, lotteryToken], [100, -100]) }); it("Should revert redemption", async function () { const {lotteryToken, addr1} = await loadFixture(deployLotteryTokenFixture); await lotteryToken.mint({value: 1000}) await lotteryToken.transfer(addr1.address, 500) await expect(lotteryToken.redeem(1000)) .to.be.revertedWith("Redemption amount must not exceed balance."); }); }); });