メインコンテンツまでスキップ

lesson-2_トークンを作成しよう

🖥 このレッスンの参考動画URL

Dapp University

👀 contractディレクトリにファイルを作成する

次にpackages/contract/contractsにスマートコントラクトを記述するためのファイルを作成しましょう。

作成するファイルは以下の3つです。

  • DappToken.sol
  • MockDaiToken.sol
  • TokenFarm.sol

下のようにディレクトリ構造になっていればOKです。

contracts
├── DappToken.sol
├── MockDaiToken.sol
└── TokenFarm.sol

TokenFarm.solはYield-Farmを管理するためのコントラクトです。

MockDaiToken.solは、AstarやLinkなど、既存の仮想通貨を模したトークンです。

一方、DappToken.solは、ユーザーがステークしたコインやトークンに対して付与されるコミュニティ・トークンを表します。

🪙 ERC-20トークンのしくみ

ERC-20は、イーサリアムトークンの構築方法に関するAPI仕様です。これは、トークンを様々なユースケースでサポートできるようにするために、Ethereumコミュニティによって採用された規格です。

ERC-20規格に関する詳しい説明はこちらをご覧ください。

この規格に準拠したトークンをMockDaiToken.solDappToken.solで作成することになります。

ERC-20トークンの場合、スマートコントラクトはトークンの操作に関連するすべてのアクションを管理し、トークンの所有権と口座残高を追跡します。

ERC-20標準規格を使用することで、トークンは以下のユースケースに準拠することが保証されます。

  • ウォレット間のトランスファー(あるアカウントから別のアカウントへのトークンの送信)
  • 暗号通貨取引所での売買
  • クラウドセール(ICO)でのトークンの購入

🧪 DappToken.solを深ぼる

DappToken.solMockDaiToken.solに記述されている機能はほぼ同じです。

ERC-20規格に準拠した機能を実装するために、DappToken.solを下のように編集しましょう。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

contract DappToken {
// トークン名を格納
string public name = 'DApp Token';
// 暗号通貨交換用のトークンシンボルを格納
string public symbol = 'DAPP';
// 存在するトークンの総供給量を格納
uint256 public totalSupply = 1000000000000000000000000; // 1 million tokensを供給
uint8 public decimals = 18;

event Transfer(address indexed _from, address indexed _to, uint256 _value);

event Approval(
address indexed _owner,
address indexed _spender,
uint256 _value
);

// Solidityマッピングを使用して、トークンを所有する各アカウントの残高を保存
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;

constructor() {
balanceOf[msg.sender] = totalSupply;
}

// ユーザーがトークンを別のアカウントに送信できるようにする機能を実装
function transfer(address _to, uint256 _value) public returns (bool success) {
require(balanceOf[msg.sender] >= _value);
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
emit Transfer(msg.sender, _to, _value);
return true;
}

// 暗号通貨交換のように、別のアカウントがトークンを使用できるようにする機能を実装
// これにより、allowanceマッピングが更新され、アカウントが使用できる金額を確認できる
function approve(
address _spender,
uint256 _value
) public returns (bool success) {
allowance[msg.sender][_spender] = _value;
emit Approval(msg.sender, _spender, _value);
return true;
}

// 別のアカウントからトークンを転送できるようにする
function transferFrom(
address _from,
address _to,
uint256 _value
) public returns (bool success) {
require(_value <= balanceOf[_from]);
require(_value <= allowance[_from][msg.sender]);
balanceOf[_from] -= _value;
balanceOf[_to] += _value;
allowance[_from][msg.sender] -= _value;
emit Transfer(_from, _to, _value);
return true;
}
}

次にMockDaiToken.solを以下のように編集しましょう。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

contract DaiToken {
string public name = 'Mock DAI Token';
string public symbol = 'mDAI';
uint256 public totalSupply = 1000000000000000000000000; // 1 million tokens
uint8 public decimals = 18;

event Transfer(address indexed _from, address indexed _to, uint256 _value);

event Approval(
address indexed _owner,
address indexed _spender,
uint256 _value
);

mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;

constructor() {
balanceOf[msg.sender] = totalSupply;
}

function transfer(address _to, uint256 _value) public returns (bool success) {
require(balanceOf[msg.sender] >= _value);
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
emit Transfer(msg.sender, _to, _value);
return true;
}

function approve(
address _spender,
uint256 _value
) public returns (bool success) {
allowance[msg.sender][_spender] = _value;
emit Approval(msg.sender, _spender, _value);
return true;
}

function transferFrom(
address _from,
address _to,
uint256 _value
) public returns (bool success) {
require(_value <= balanceOf[_from]);
require(_value <= allowance[_from][msg.sender]);
balanceOf[_from] -= _value;
balanceOf[_to] += _value;
allowance[_from][msg.sender] -= _value;
emit Transfer(_from, _to, _value);
return true;
}
}

これでトークンの準備は完了しました。これら2つのファイルはほとんど同じ記述をしています。

ではトークンのコントラクトがどのような構成になっているのかみていきましょう!

まず、DappToken.solの4-10行目に注目してください。

// トークン名を格納
string public name = "DApp Token";
// 暗号通貨交換用のトークンシンボルを格納
string public symbol = "DAPP";
// 存在するトークンの総供給量を格納
// 1 million tokensを供給
uint256 public totalSupply = 1000000000000000000000000;

ここでは、DappToken.solで使用する変数を定義し、値を格納しています。

次に、25行目を見ていきましょう。

mapping(address => uint256) public balanceOf;

このbalanceOfというマッピングは、「KeyがaddressでValueがuint256」という解釈をします。

balanceOfは、実際のユーザーのaddressを引数に、そのユーザーアドレスの残高(残高)を返します。

そして、このマッピングはpublicなので、コントラクトの外から呼び出すことができます。

1️⃣ transfer

次に、transfer関数( DappToken.solの31-38行)を見ていきましょう。

// ユーザーがトークンを別のアカウントに送信できるようにする機能を実装
function transfer(address _to, uint256 _value) public returns (bool success) {
// 移動したい額のトークンがユーザーのアドレスに存在するか確認
// 存在しない場合は、エラーを返す
require(balanceOf[msg.sender] >= _value);
// 関数を呼び出したユーザーアドレスから _value の金額を引き抜く
balanceOf[msg.sender] -= _value;
// 送金先のアドレスに _value の金額を足す
balanceOf[_to] += _value;
// Transferイベントを実行する
emit Transfer(msg.sender, _to, _value);
return true;
}

transfer関数は、あるアドレス( _from)から別のアドレス( _to)へ、ある量のトークン( _value)を転送する関数です。その際に、必ず、 Transferイベントを発生させなければいけません。

Transferイベントを発生させるために、DappToken.solの12-16行目に以下のeventが定義されています。

event Transfer(
address indexed _from,
address indexed _to,
uint256 _value
);

Transferイベントには以下の変数が必要になります。

  • _from: トークンを送る人のアドレス
  • _to: トークンが送られる人のアドレス
  • _value: 送られるトークンの金額

トークンがピアツーピアで転送されるときは、必ずTransferイベントを発生させる必要があります。新しいトークンを作成する際には、 _fromアドレスを0x0に設定して、 Transferイベントを発生させるようにします。

2️⃣ approve

次に、approve関数を見ていきましょう。

まず、allowanceマッピング( DappToken.solの26行目)に着目してください。

mapping(address => mapping(address => uint256)) public allowance;

allowanceマッピングはネストされたマッピングの形をとっていますが、至って簡単なコンセプトです。以下を見ていきましょう。

mapping(address => mapping(address => uint256)) public allowance;
(1) (2) (3)

まず、(1)-(3)の変数の意味を見ていきましょう。

  • (1): コントラクトオーナー( owner)のアドレス
  • (2): spender(任意のユーザー)のアドレス
  • (3): spenderが引き出すことができる金額( _value)

allowanceマッピングは(1)ownerのアドレスをKeyとして、そのコントラクトを叩いたユーザー ( spender ) が引き出すことのできる金額 ( _value ) を返すマッピングです。

次に、approve関数(DappToken.solの41-45行目)を見ていきましょう。

// 別のアカウントがトークンを使用できるようにする機能を実装
function approve(address _spender, uint256 _value) public returns (bool success) {
allowance[msg.sender][_spender] = _value;
emit Approval(msg.sender, _spender, _value);
return true;
}

approve関数は、任意のユーザー( spender )が一定量( _value )のトークンを繰り返し引き出せるようにする関数です。この関数が再び呼び出された場合、現在引き出し可能な金額は新しい_value値で上書きされます。

次に、26行目で定義したallowanceマッピングがapprove関数の中で呼び出されていることを確認しましょう。

allowance[msg.sender][_spender] = _value;

ここでは_spender(任意のユーザー)がmsg.sender(コントラクトのオーナーユーザー)からまだ引き出すことができる金額( _value)を設定しています。

最後に、Approvalイベントを発生させていることに着目してください。

emit Approval(msg.sender, _spender, _value);

Approvalイベントはapprove関数の呼び出しに成功したときに発生します。

Approvalイベントを発生させるために、DappToken.solの18-22行目に以下のeventが定義されていることを確認してください。

event Approval(
address indexed _owner,
address indexed _spender,
uint256 _value
);

3️⃣ transferFrom

最後に、DappToken.solの47-56行目に記載されているtransferFrom関数を見ていきましょう。

// 別のアカウントからトークンを転送できるようにする
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) {
require(_value <= balanceOf[_from]);
require(_value <= allowance[_from][msg.sender]);
balanceOf[_from] -= _value;
balanceOf[_to] += _value;
allowance[_from][msg.sender] -= _value;
emit Transfer(_from, _to, _value);
return true;
}

transferFromは、_valueの量のトークンをアドレス_fromからアドレス_toに転送し、Transferイベントを発生させる関数です。

transferFromは、コントラクトがあなたに代わってトークンを転送することを許可する、トークン引き出しワークフローに使用されます。これは例えば、コントラクトがあなたに代わってトークンを転送したり、サブ通貨で手数料を請求したりすることを可能にするために使用できます。

🙋‍♂️ 質問する

ここまでの作業で何かわからないことがある場合は、Discordの#ethereumで質問をしてください。

ヘルプをするときのフローが円滑になるので、エラーレポートには下記の3点を記載してください ✨

1. 質問が関連しているセクション番号とレッスン番号
2. 何をしようとしていたか
3. エラー文をコピー&ペースト
4. エラー画面のスクリーンショット

テンプレートのクローン成功おめでとうございます!次はいよいよコーディングをしていきましょう!