メインコンテンツへスキップ

ERC-20コントラクトの詳細

Solidityerc-20
初級
Ori Pomerantz
2021年3月9日
45 分の読書 minute read

はじめに

イーサリアムの最も一般的な用途の 1 つは、グループが取引可能なトークン、いわば独自の通貨を作ることです。 これらのトークンは通常、ERC-20という規格に準拠しています。 この規格により、流動性プールやウォレットなど、すべての ERC-20 トークンで利用できるツールの作成が可能になります。 今回は、OpenZeppelin Solidity ERC-20 の実装(opens in a new tab)と、インターフェース定義(opens in a new tab)について分析していきます。

ここではアノテーション付きのソースコードを見ていきます。 ERC-20 を実装する場合は、こちらのチュートリアル(opens in a new tab)をご覧ください。

インターフェース

ERC-20 のような規格の目的は、ウォレットや分散型取引所のようなアプリケーション間で相互運用可能な多くのトークンの実装を可能にすることです。 しかしその実現にはインターフェース(opens in a new tab)が必要です。 トークンコントラクトを使用する必要があるすべてのコードは、インターフェースで同じ定義を使用することができ、その定義を使用するすべてのトークンコントラクトと互換性があります。それらには MetaMask などのウォレット、etherscan.io などの分散型アプリケーション(Dapp)、流動性プールなどの異なるコントラクトが含まれます。

ERC-20インターフェースの図

経験豊富なプログラマーであれば、Java(opens in a new tab)C 言語のヘッダーファイル(opens in a new tab)で同様の構造を見たことがあるのではないでしょうか。

これは OpenZeppelin によるERC-20 インターフェースの定義(opens in a new tab)です。 人間が読めるように(opens in a new tab)Solidity のコードにしています。 もちろん、インターフェースそのものは、何をするかを定義していません。 これは後述のコントラクトのソースコードで説明されています。

1// SPDX-License-Identifier: MIT
コピー

Solidity のファイルには、ライセンス識別子が含まれているはずです。 ライセンス一覧はこちら(opens in a new tab)でご覧いただけます。 別のライセンスが必要な場合は、コメントしてください。

1pragma solidity >=0.6.0 <0.8.0;
コピー

Solidity 言語は現在も急速に進化しており、新しいバージョンは古いコードと互換性がない場合があります(詳しくはこちら(opens in a new tab))。 そのため、この言語の最小バージョンだけでなく、コードをテストした最新バージョンも指定することをお勧めします。

1/**
2 * EIP で定義された ERC20 規格の @dev インターフェース。
3 */
コピー

コメント中の@devNatSpec 形式(opens in a new tab)の一部で、ソースコードからドキュメントを作成するために使用されます。

1interface IERC20 {
コピー

慣例では、インターフェース名は「I」で始まります。

1 /**
2 * @dev Returns the amount of tokens in existence.
3 */
4 function totalSupply() external view returns (uint256);
コピー

この関数はexternalです。つまり、コントラクトの外部からのみ呼び出すことができます(opens in a new tab)。 コントラクト内のトークンの総供給量を返します。 この値は、イーサリアムで最も一般的な型である符号なし 256 ビット(イーサリアム仮想マシン(EVM)のネイティブワードサイズ)で返されます。 この関数もviewであり、状態を変更しないため、ブロックチェーンのすべてのノードで実行するのではなく、単一のノードで実行できます。 このような関数はトランザクションを発生させず、ガス代もかかりません。

注: 理論的には、コントラクト作成者が、実際の値よりも少ない総供給量を返し、各トークンを実際の価格よりも高価に見せることで、不正を行うことが出来るように思えるかもしれません。 しかし、そのような心配をするということは、ブロックチェーンの本質を忘れているということになります。 ブロックチェーン上で起こるすべてのことは、すべてのノードで検証できます。 検証をするためのすべてのコントラクトの機械語コードとストレージは、全ノードで入手可能です。 コントラクトの Solidity コードを公開する必要はありませんが、ソースコードとコンパイルに使用した Solidity のバージョンを公開しない限り、不正への懸念を真剣に受け止めてもらうことはできません。その場合は、提供した機械語コードで検証することができます。 例えば、このコントラクト(opens in a new tab)をご覧ください。

1 /**
2 * @dev Returns the amount of tokens owned by `account`.
3 */
4 function balanceOf(address account) external view returns (uint256);
コピー

その名の通り、balanceOfはアカウントの残高を返します。 Solidity では、イーサリアムアカウントは 160 ビットを保持するaddress型で識別されます。 また、externalviewもあります。

1 /**
2 * @dev Moves `amount` tokens from the caller's account to `recipient`.
3 *
4 * Returns a boolean value indicating whether the operation succeeded.
5 *
6 * Emits a {Transfer} event.
7 */
8 function transfer(address recipient, uint256 amount) external returns (bool);
コピー

transfer関数は、呼び出し元から別のアドレスにトークンを転送します。 これは状態の変化を伴うので、viewではありません。 ユーザーがこの関数を呼び出すと、トランザクションが作成され、ガス代がかかります。 さらに、Transferというイベントが発行され、ブロックチェーン上の全ノードにそのイベントが通知されます。

この関数には、2 つの異なる呼び出し元のタイプに応じて、2 つのタイプの出力があります。

  • ユーザーがユーザーインターフェースから関数を直接呼び出す場合。 通常、トランザクションを送信したユーザーは、その応答を待ちません。応答にどれくらいの時間がかかるか分からないからです。 ユーザーは、トランザクションレシート(トランザクションハッシュによって識別可能)を探すか、Transferイベントを探すことで状況を把握できます。
  • 他のコントラクトがトランザクション全体の一部として関数を呼び出す場合。 これらのコントラクトは、すぐに結果を得ることができます。関数が同じトランザクション内で実行されるため、関数の戻り値を使用できるからです。

同じタイプの出力は、コントラクトの状態を変更する他の関数によって作成されます。

割当量(allowance)を設定すると、あるアカウントが別の所有者に属しているトークンの一部を使えるようになります。 これは、コントラクトが売り手として機能する場合などに役立ちます。 コントラクトはイベントを監視できないため、買い手が売り手のコントラクトにトークンを直接転送した場合、売り手のコントラクトはその支払いを認識できません。 代わりに、買い手が売り手のコントラクトに一定量の使用を許可し、売り手がその量を転送するようにします。 この処理は売り手のコントラクトが呼び出した関数を通して行われるため、売り手のコントラクトは処理が成功したかどうかを確認できます。

1 /**
2 * @dev Returns the remaining number of tokens that `spender` will be
3 * allowed to spend on behalf of `owner` through {transferFrom}. これは
4 * デフォルトでゼロです。
5 *
6 * This value changes when {approve} or {transferFrom} are called.
7 */
8 function allowance(address owner, address spender) external view returns (uint256);
コピー

allowance関数を使用すると、誰でも、あるアドレス(owner)が別のアドレス(spender)に使用を許可している割当量(allowance)を照会できます。

1 /**
2 * @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
3 *
4 * Returns a boolean value indicating whether the operation succeeded.
5 *
6 * IMPORTANT: Beware that changing an allowance with this method brings the risk
7 * that someone may use both the old and the new allowance by unfortunate
8 * transaction ordering. One possible solution to mitigate this race
9 * condition is to first reduce the spender's allowance to 0 and set the
10 * desired value afterwards:
11 * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
12 *
13 * Emits an {Approval} event.
14 */
15 function approve(address spender, uint256 amount) external returns (bool);
すべて表示
コピー

approve関数は、割当量を作成します。 これがどのように悪用される可能性があるのかについて、該当のメッセージを必ず読んでください。 イーサリアムでは、自分のトランザクションの順序は制御できますが、他のユーザーのトランザクションが発生したことを確認してからトランザクションを送信しない限り、他のユーザーのトランザクションが実行される順序を制御することはできません。

1 /**
2 * @dev Moves `amount` tokens from `sender` to `recipient` using the
3 * allowance mechanism. `amount` is then deducted from the caller's
4 * allowance.
5 *
6 * Returns a boolean value indicating whether the operation succeeded.
7 *
8 * Emits a {Transfer} event.
9 */
10 function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
すべて表示
コピー

最後に、使用者(spender)がtransferFromを使用して、割当量 (allowance)を実際に使用します。

1
2 /**
3 * @dev Emitted when `value` tokens are moved from one account (`from`) to
4 * another (`to`).
5 *
6 * Note that `value` may be zero.
7 */
8 event Transfer(address indexed from, address indexed to, uint256 value);
9
10 /**
11 * @dev Emitted when the allowance of a `spender` for an `owner` is set by
12 * a call to {approve}. `value` is the new allowance.
13 */
14 event Approval(address indexed owner, address indexed spender, uint256 value);
15}
すべて表示
コピー

これらのイベントは、ERC-20 コントラクトの状態が変更されるタイミングで発行されます。

実際のコントラクト

以下は、こちらから取得した(opens in a new tab)、ERC-20 規格を採用している実際のコントラクトです。 そのまま使うためのものではありませんが、継承(opens in a new tab)することで使用可能なコントラクトに拡張することができます。

1// SPDX-License-Identifier: MIT
2pragma solidity >=0.6.0 <0.8.0;
コピー

インポートステートメント

コントラクト定義では、上記のインターフェース定義に加え、別の 2 つのファイルをインポートします。

1
2import "../../GSN/Context.sol";
3import "./IERC20.sol";
4import "../../math/SafeMath.sol";
コピー
  • GSN/Context.solは、イーサ(ETH)を持たないユーザーがブロックチェーンを使用できるようにするシステムである、OpenGSN(opens in a new tab)を使用するために必要な定義です。 これは古いバージョンであることに注意してください。OpenGSN と統合する場合は、こちらのチュートリアルをご覧ください(opens in a new tab)
  • SafeMath ライブラリ(opens in a new tab)は、オーバーフローを起こさずに加算と減算を実行できるようにするために使用されます。 このライブラリが必要な理由は、これがないと、1 つのトークンを持っているユーザーが何らかの方法で 2 つのトークンを使用した場合、2^256-1 のトークンを持ってしまう可能性があるためです。

以下のコメントは、コントラクトの目的を説明しています。

1/**
2 * @dev Implementation of the {IERC20} interface.
3 *
4 * This implementation is agnostic to the way tokens are created. This means
5 * that a supply mechanism has to be added in a derived contract using {_mint}.
6 * For a generic mechanism see {ERC20PresetMinterPauser}.
7 *
8 * TIP: For a detailed writeup see our guide
9 * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How
10 * to implement supply mechanisms].
11 *
12 * We have followed general OpenZeppelin guidelines: functions revert instead
13 * of returning `false` on failure. This behavior is nonetheless conventional
14 * and does not conflict with the expectations of ERC20 applications.
15 *
16 * Additionally, an {Approval} event is emitted on calls to {transferFrom}.
17 * This allows applications to reconstruct the allowance for all accounts just
18 * by listening to said events. Other implementations of the EIP may not emit
19 * these events, as it isn't required by the specification.
20 *
21 * Finally, the non-standard {decreaseAllowance} and {increaseAllowance}
22 * functions have been added to mitigate the well-known issues around setting
23 * allowances. See {IERC20-approve}.
24 */
25
すべて表示
コピー

コントラクトの定義

1contract ERC20 is Context, IERC20 {
コピー

この行では、継承を指定しています。ここでは、IERC20と OpenGSN のためのContextを継承しています。

1
2 using SafeMath for uint256;
3
コピー

この行では、SafeMathライブラリをuint256型にアタッチしています。 このライブラリの詳細については、こちら(opens in a new tab)をご覧ください。

変数の定義

これらの定義では、コントラクトの状態変数を指定します。 変数には、privateで宣言しているものがありますが、ブロックチェーン上の他のコントラクトから読み取れないというだけです。 ブロックチェーンに秘密はありません。すべてのノードのソフトウェアは、すべてのブロックのすべてのコントラクトの状態を保持します。 状態変数は、慣例として_<something>のように命名されます。

最初の 2 つの変数は、マッピング(opens in a new tab)であり、キーが数値であることを除いて、連想配列(opens in a new tab)とほぼ同じような振る舞いをします。 ストレージは、デフォルト(ゼロ)とは異なる値を持つエントリのみに割り当てられます。

1 mapping (address => uint256) private _balances;
コピー

最初のマッピングである_balancesは、トークンを保持しているアドレスとそれぞれの残高です。 残高にアクセスするには、_balances[<address>]という構文を使用します。

1 mapping (address => mapping (address => uint256)) private _allowances;
コピー

この変数_allowancesには、前述の割当量(allowance)が格納されます。 最初のインデックスは、トークンの所有者であり、2 番目のインデックスは割当量(allowance)を使用するコントラクトです。 アドレス A が、アドレス B のアカウントから使える量にアクセスするには、_allowances[B][A]のようにします。

1 uint256 private _totalSupply;
コピー

名前が示すように、この変数はトークンの総供給量を追跡します。

1 string private _name;
2 string private _symbol;
3 uint8 private _decimals;
コピー

この 3 つの変数は、可読性を向上させるために使用されます。 最初の 2 つの変数は自明ですが、_decimalsはそうではありません。

イーサリアムには浮動小数点変数も小数変数もない一方で、 ユーザーはトークンの分割を好みます。 人々が金を通貨にすると決めた理由の一つとして、誰かがアヒル一羽の値段の分だけ牛を買おうとしたときに、お金を崩すのが難しかったことが挙げられます。

これを解決するには整数で追跡すればよいのですが、実際のトークンの代わりに、価値が非常に小さな小数トークンで計算します。 イーサ(ETH)の場合、小数トークンはウェイ(wei)と呼ばれており、10^18 wei が 1 ETH と等価です。 執筆時点では、10,000,000,000,000 wei が、約 1 米ドルまたは約 1 ユーロセントです。

アプリケーションには、トークンの残高を表示する方法が必要です。 ユーザーが 3,14,000,000,000,000,000,000,000wei を持っている場合、それは 3.14 ETH でしょうか? それとも、31.41 ETH でしょうか? 3,141 ETH でしょうか? イーサ (ETH) の場合、1 ETH が 10^18 wei と定義されていますが、トークンでは別の値を選択可能です。 トークンを分割する必要がなければ、値がゼロの_decimalsを使用できます。 ETH と同じ基準を使用したい場合は、18の値を指定してください。

コンストラクタ

1 /**
2 * @dev Sets the values for {name} and {symbol}, initializes {decimals} with
3 * a default value of 18.
4 *
5 * To select a different value for {decimals}, use {_setupDecimals}.
6 *
7 * All three of these values are immutable: they can only be set once during
8 * construction.
9 */
10 constructor (string memory name_, string memory symbol_) public {
11 _name = name_;
12 _symbol = symbol_;
13 _decimals = 18;
14 }
すべて表示
コピー

コンストラクタは、コントラクトが最初に作成されたときに呼び出されます。 慣例として関数パラメータは、 <something>_のように命名されます。

ユーザーインターフェース関数

1 /**
2 * @dev Returns the name of the token.
3 */
4 function name() public view returns (string memory) {
5 return _name;
6 }
7
8 /**
9 * @dev Returns the symbol of the token, usually a shorter version of the
10 * name.
11 */
12 function symbol() public view returns (string memory) {
13 return _symbol;
14 }
15
16 /**
17 * @dev Returns the number of decimals used to get its user representation.
18 * For example, if `decimals` equals `2`, a balance of `505` tokens should
19 * be displayed to a user as `5,05` (`505 / 10 ** 2`).
20 *
21 * Tokens usually opt for a value of 18, imitating the relationship between
22 * ether and wei. This is the value {ERC20} uses, unless {_setupDecimals} is
23 * called.
24 *
25 * NOTE: This information is only used for _display_ purposes: it in
26 * no way affects any of the arithmetic of the contract, including
27 * {IERC20-balanceOf} and {IERC20-transfer}.
28 */
29 function decimals() public view returns (uint8) {
30 return _decimals;
31 }
すべて表示
コピー

これらの関数(namesymboldecimals)は、ユーザーインターフェースがコントラクトの情報を入手できるようするため、ユーザーインターフェースにコントラクトの情報が正しく表示されるようになります。

戻り値の型は、 string memoryです。メモリに格納されている文字列が返されることを意味します。 文字列などの変数は、以下の 3 つの場所に格納できます。

存続期間コントラクトアドレスガス代
メモリ関数呼び出しの間読み取り/書き込み数十または数百(上位のロケーションほど高い)
コールデータ関数呼び出しの間読み取り専用戻り値型としては使用できず、関数パラメータ型のみ
ストレージ変更されるまで読み取り/書き込み高額(読み取りに 800、書き込みに 20 k)

このケースでは、memoryの使用が最善の選択肢です。

トークン情報の読み取り

以下の関数は、トークンの情報(総供給量またはアカウントの残高のいずれか)を提供します。

1 /**
2 * @dev See {IERC20-totalSupply}.
3 */
4 function totalSupply() public view override returns (uint256) {
5 return _totalSupply;
6 }
コピー

totalSupply関数は、トークンの総供給量を返します。

1 /**
2 * @dev See {IERC20-balanceOf}.
3 */
4 function balanceOf(address account) public view override returns (uint256) {
5 return _balances[account];
6 }
コピー

アカウントの残高を読み取ります。 誰でも他のユーザーのアカウント残高を取得できることに注意してください。 どのノードでも取得可能な情報であるため、隠そうとしても無駄です。 ブロックチェーンに秘密はありません

トークンの転送

1 /**
2 * @dev See {IERC20-transfer}.
3 *
4 * Requirements:
5 *
6 * - `recipient` cannot be the zero address.
7 * - the caller must have a balance of at least `amount`.
8 */
9 function transfer(address recipient, uint256 amount) public virtual override returns (bool) {
すべて表示
コピー

transfer関数は、送信者のアカウントから別のアカウントにトークンを転送するために呼び出します。 戻り値がブール値になっていますが、この値は常にtrueを返すことに注意してください。 転送が失敗した場合、コントラクトは、呼び出しを取り消します。

1 _transfer(_msgSender(), recipient, amount);
2 return true;
3 }
コピー

_transfer関数は、実際の作業を行います。 これは private 関数であり、他のコントラクト関数からのみ呼び出せます。 慣例として、private 関数は状態変数と同様に、_<something>のように命名されます。

通常、Solidity では、メッセージ送信者にmsg.senderを使用します。 しかし、OpenGSN(opens in a new tab)では使えません。 イーサ(ETH)無しのトークンのトランザクションを許可したい場合は、_msgSender()を使用しなければなりません。 通常のトランザクションでは、msg.senderを返しますが、イーサ(ETH)無しのトランザクションの場合は、メッセージを中継したコントラクトではなく、元の署名者を返します。

割当量(allowance)関連の関数

割当量機能を実装した関数には、allowanceapprovetransferFrom_approveがあります。 さらに、OpenZeppelin 実装には、基本的な標準に加えてセキュリティを向上させる increaseAllowancedecreaseAllowanceなどの複数の機能が含まれます。

allowance 関数

1 /**
2 * @dev See {IERC20-allowance}.
3 */
4 function allowance(address owner, address spender) public view virtual override returns (uint256) {
5 return _allowances[owner][spender];
6 }
コピー

allowance関数を使用すると、誰でも割当量を確認することができます。

approve 関数

1 /**
2 * @dev See {IERC20-approve}.
3 *
4 * Requirements:
5 *
6 * - `spender` cannot be the zero address.
7 */
8 function approve(address spender, uint256 amount) public virtual override returns (bool) {
コピー

この関数は、割当量を作成するときに呼び出します。 上述のtransfer関数と似ています。

  • この関数は、単に実際に作業を行う internal 関数(この場合は_approve)を呼び出します。
  • この関数は、成功した場合はtrueを返し、失敗した場合は取り消します。
1 _approve(_msgSender(), spender, amount);
2 return true;
3 }
コピー

internal 関数を使用して、状態変更が起こる場所の数を最小限にしています。 状態を変更するすべての関数には、潜在的なセキュリティリスクがあり、セキュリティ監査が必要です。 このような方法で、間違いを犯す可能性を下げています。

transferFrom 関数

これは、使用者(spender)が割当量(allowance)を使用するために呼び出す関数です。 これには、使う量の転送操作と、その量を割当量(allowance)から減らす操作の、2 つの操作が必要になります。

1 /**
2 * @dev See {IERC20-transferFrom}.
3 *
4 * Emits an {Approval} event indicating the updated allowance. これは
5 * EIPでは必要ありません。 {ERC20}の最初にある注意事項を参照してください。
6 *
7 * Requirements:
8 *
9 * - `sender` and `recipient` cannot be the zero address.
10 * - `sender` must have a balance of at least `amount`.
11 * - the caller must have allowance for ``sender``'s tokens of at least
12 * `amount`.
13 */
14 function transferFrom(address sender, address recipient, uint256 amount) public virtual
15 override returns (bool) {
16 _transfer(sender, recipient, amount);
すべて表示
コピー

a.sub(b, "message")関数の呼び出しでは、次の 2 つのことを行います。 まず、a-bを計算します。これが新しい割当量になります。 次に、この結果が負の数になっていないかをチェックします。 負になっている場合、提供されているメッセージを表示して、呼び出しが取り消されます。 呼び出しが取り消されると、呼び出し中に実行されたすべての処理が無効になるため、_transferを元に戻す必要がないことに注意してください。

1 _approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount,
2 "ERC20: transfer amount exceeds allowance"));
3 return true;
4 }
コピー

OpenZeppelin による安全性の向上

ゼロ以外の割当量を別のゼロ以外の値に設定することには、リスクが伴います。自分が制御できるのは自分のトランザクションの順序のみであり、他のユーザーのトランザクションの順序を制御することはできないからです。 アリスという初心者ユーザーと、ビルという誠実さに欠けるユーザーがいるとします。 アリスは、ビルが提供しているサービスを購入することにしました。購入には 5 トークンの費用がかかるため、アリスは 5 トークンの割当量(allowance)をビルに付与しました。

その後、ビルの設定した価格が何らかの理由で 10 トークンに上がりました。 アリスは依然としてそのサービスの購入を希望しており、ビルへの割当量を 10 に設定したトランザクションを送信しました。 ビルは、トランザクションプールでこの新しいトランザクションを確認した瞬間に、より早くマイニングされるようにかなり高いガス代を設定した、アリスの 5 トークンを使うトランザクションを送信します。 この方法で、ビルは 5 トークンを使います。その後、アリスが送信した新しい割当量がマイニングされたら、さらに 10 トークンを使います。こうして、アリスが承認するつもりだった量を超える、合計 15 トークンを使えることになります。 この手法は、フロントランニング(opens in a new tab)と呼ばれます。

アリスのトランザクションアリスのノンスビルのトランザクションビルのノンスビルの割当量ビルのアリスからの総収入
approve(Bill, 5)1050
transferFrom(Alice, Bill, 5)10,12305
approve(Bill, 10)11105
transferFrom(Alice, Bill, 10)10,124015

この問題を回避するには、割当量を特定の量に変更できる 2 つの関数(increaseAllowancedecreaseAllowance)を使用します。 これらの関数を使用すれば、ビルがすでに 5 トークンを使用していた場合、その後に使用できるのは残りの 5 トークンのみとなります。 タイミングに応じて、次のような 2 つの動作が考えられますが、いずれの場合でもビルが取得するのは 10 トークンのみです。

A:

アリスのトランザクションアリスのノンスビルのトランザクションビルのノンスビルの割当量ビルのアリスからの総収入
approve(Bill, 5)1050
transferFrom(Alice, Bill, 5)10,12305
increaseAllowance(Bill, 5)110+5 = 55
transferFrom(Alice, Bill, 5)10,124010

B:

アリスのトランザクションアリスのノンスビルのトランザクションビルのノンスビルの割当量ビルのアリスからの総収入
approve(Bill, 5)1050
increaseAllowance(Bill, 5)115+5 = 100
transferFrom(Alice, Bill, 10)10,124010
1 /**
2 * @dev Atomically increases the allowance granted to `spender` by the caller.
3 *
4 * This is an alternative to {approve} that can be used as a mitigation for
5 * problems described in {IERC20-approve}.
6 *
7 * Emits an {Approval} event indicating the updated allowance.
8 *
9 * Requirements:
10 *
11 * - `spender` cannot be the zero address.
12 */
13 function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {
14 _approve(_msgSender(), spender, _allowances[_msgSender()][spender].add(addedValue));
15 return true;
16 }
すべて表示
コピー

a.add(b)関数は、安全な加算です。 万が一、a+b>=2^256のような計算が行われても、通常の加算で発生してしまうオーバー(アンダー)フローが発生しません。

1
2 /**
3 * @dev Atomically decreases the allowance granted to `spender` by the caller.
4 *
5 * This is an alternative to {approve} that can be used as a mitigation for
6 * problems described in {IERC20-approve}.
7 *
8 * Emits an {Approval} event indicating the updated allowance.
9 *
10 * Requirements:
11 *
12 * - `spender` cannot be the zero address.
13 * - `spender` must have allowance for the caller of at least
14 * `subtractedValue`.
15 */
16 function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {
17 _approve(_msgSender(), spender, _allowances[_msgSender()][spender].sub(subtractedValue,
18 "ERC20: decreased allowance below zero"));
19 return true;
20 }
すべて表示
コピー

トークン情報を変更する関数

次の 4 つの関数(_transfer_mint_burn_approve)は、実際の処理を行います。

_transfer 関数 {#_transfer}

1 /**
2 * @dev Moves tokens `amount` from `sender` to `recipient`.
3 *
4 * This is internal function is equivalent to {transfer}, and can be used to
5 * e.g. implement automatic token fees, slashing mechanisms, etc.
6 *
7 * Emits a {Transfer} event.
8 *
9 * Requirements:
10 *
11 * - `sender` cannot be the zero address.
12 * - `recipient` cannot be the zero address.
13 * - `sender` must have a balance of at least `amount`.
14 */
15 function _transfer(address sender, address recipient, uint256 amount) internal virtual {
すべて表示
コピー

_transfer関数は、トークンをあるアカウントから別のアカウントへ転送します。 この関数は、(送信者自身のアカウントから転送する)transferと、(割当量を使用するために他のユーザーのアカウントから転送する)transferFromの両方から呼び出されます。

1 require(sender != address(0), "ERC20: transfer from the zero address");
2 require(recipient != address(0), "ERC20: transfer to the zero address");
コピー

イーサリアムでは、実際にアドレス 0 を所有しているユーザーはいません(つまり、対応する公開鍵がゼロアドレスに変換される秘密鍵を知っているユーザーはいないということです)。 誰かがこのアドレスを使っている場合、通常それはソフトウェアのバグです。そのため、送信者または受取人にゼロアドレスが指定されている場合は失敗します。

1 _beforeTokenTransfer(sender, recipient, amount);
2
コピー

このコントラクトを使用するには 2 つの方法があります。

  1. 自分のコードのテンプレートとして使う
  2. このコントラクトを継承(opens in a new tab)し、変更する必要がある関数のみをオーバーライドする

OpenZeppelin ERC-20 のコードはすでに監査を受けており、安全であることが知られているため、2 つ目の方法をお勧めします。 継承を使用すると、変更した関数が明らかになります。他のユーザーは、変更された特定の関数を監査するだけで、そのコントラクトを信頼することができます。

多くの場合、トークンの所有者が変わるたびに関数を実行すると便利です。 ただし、_transferは非常に重要な関数ですが、安全でない書き込みをしてしまう可能性があるため(下記参照)、オーバーライドしないことをお勧めします。 この解決策として、_beforeTokenTransferというフック関数(opens in a new tab)があります。 この関数をオーバライドすれば、転送のたびに呼び出されるようになります。

1 _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance");
2 _balances[recipient] = _balances[recipient].add(amount);
コピー

この 2 行で、実際に転送を行っています。 この 2 行の間に何もないことと、転送する量を送信者から減算してから、その量を受取人に加算していることに注意してください。 この 2 行の間に別のコントラクトへの呼び出しがある場合、このコントラクトで不正を行うために使用される可能性があるため、このことは非常に重要になります。 こうすることで、転送がアトミックになる(他の操作に割り込まれないようになる)ため、転送の途中で何かが発生することはなくなります。

1 emit Transfer(sender, recipient, amount);
2 }
コピー

最後に、Transferイベントを発行します。 イベントは、スマートコントラクトからアクセスできません。しかし、ブロックチェーンの外で実行されているコードは、イベントをリッスンして対応することができます。 例えば、ウォレットで所有者がより多くのトークンを得た時期を追跡できます。

_mint 関数と_burn 関数 {#_mint-and-_burn}

この 2 つの関数(_mint_burn)は、トークンの総供給量を変更します。 これらは internal であり、このコントラクト内にこれらを呼び出す関数はありません。そのため、コントラクトを継承し、新しいトークンのミントや既存のトークンのバーンを実行する条件を決定する独自のロジックを追加する場合にのみ役立ちます。

注: すべての ERC-20 トークンには、トークン管理を規定している独自のビジネスロジックがあります。 例えば、固定した供給量のコントラクトでは、コンストラクタ内で _mintのみを呼び出す可能性があり、_burnを呼び出すことはありません。 トークンを販売するコントラクトは、支払いが行われたタイミングで_mintを呼び出し、天井知らずのインフレを避けるために、ある時点で_burnを呼び出すことが考えられます。

1 /** @dev Creates `amount` tokens and assigns them to `account`, increasing
2 * the total supply.
3 *
4 * Emits a {Transfer} event with `from` set to the zero address.
5 *
6 * Requirements:
7 *
8 * - `to` cannot be the zero address.
9 */
10 function _mint(address account, uint256 amount) internal virtual {
11 require(account != address(0), "ERC20: mint to the zero address");
12 _beforeTokenTransfer(address(0), account, amount);
13 _totalSupply = _totalSupply.add(amount);
14 _balances[account] = _balances[account].add(amount);
15 emit Transfer(address(0), account, amount);
16 }
すべて表示
コピー

トークンの総額が変更された場合は、_totalSupplyを必ずアップデートしてください。

1 /**
2 * @dev Destroys `amount` tokens from `account`, reducing the
3 * total supply.
4 *
5 * Emits a {Transfer} event with `to` set to the zero address.
6 *
7 * Requirements:
8 *
9 * - `account` cannot be the zero address.
10 * - `account` must have at least `amount` tokens.
11 */
12 function _burn(address account, uint256 amount) internal virtual {
13 require(account != address(0), "ERC20: burn from the zero address");
14
15 _beforeTokenTransfer(account, address(0), amount);
16
17 _balances[account] = _balances[account].sub(amount, "ERC20: burn amount exceeds balance");
18 _totalSupply = _totalSupply.sub(amount);
19 emit Transfer(account, address(0), amount);
20 }
すべて表示

_burn関数は、方向が逆であることを除き_mintとほぼ同じです。

_approve 関数 {#_approve}

これは、実際の割当量(allowance)を指定する関数です。 所有者は自身の現在の残高よりも高い割当量を指定できることに注意してください。 残高は転送時にチェックされるため、割当量の作成時の残高と異なっていても問題ありません。

1 /**
2 * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens.
3 *
4 * This internal function is equivalent to `approve`, and can be used to
5 * e.g. set automatic allowances for certain subsystems, etc.
6 *
7 * Emits an {Approval} event.
8 *
9 * Requirements:
10 *
11 * - `owner` cannot be the zero address.
12 * - `spender` cannot be the zero address.
13 */
14 function _approve(address owner, address spender, uint256 amount) internal virtual {
15 require(owner != address(0), "ERC20: approve from the zero address");
16 require(spender != address(0), "ERC20: approve to the zero address");
17
18 _allowances[owner][spender] = amount;
すべて表示
コピー

Approvalイベントを発行します。 アプリケーションがどのように書かれているかによって異なりますが、使用者(spender)のコントラクトには、所有者またはこれらのイベントをリッスンしているサーバーのいずれかによって承認(Approval)が通知されます。

1 emit Approval(owner, spender, amount);
2 }
3
コピー

小数変数の変更

1
2
3 /**
4 * @dev Sets {decimals} to a value other than the default one of 18.
5 *
6 * WARNING: This function should only be called from the constructor. Most
7 * applications that interact with token contracts will not expect
8 * {decimals} to ever change, and may work incorrectly if it does.
9 */
10 function _setupDecimals(uint8 decimals_) internal {
11 _decimals = decimals_;
12 }
すべて表示
コピー

この関数は、_decimals変数を変更します。_decimals変数は、量(金額)の解釈方法をユーザーインターフェースに伝えるのに使用されます。 コンストラクタから呼び出される必要があります。 その後のどの時点においても、この関数を呼び出すと不正になります。アプリケーションは、このような処理をするようには設計されていません。

フック

1
2 /**
3 * @dev Hook that is called before any transfer of tokens. これには
4 * ミントとバーンが含まれます。
5 *
6 * Calling conditions:
7 *
8 * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens
9 * will be to transferred to `to`.
10 * - when `from` is zero, `amount` tokens will be minted for `to`.
11 * - when `to` is zero, `amount` of ``from``'s tokens will be burned.
12 * - `from` and `to` are never both zero.
13 *
14 * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
15 */
16 function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual { }
17}
すべて表示
コピー

このフック関数は、転送中に呼び出されます。 空になっていますが、何かを実行するのにこの関数が必要な場合は、オーバーライドしてください。

まとめ

確認のため、このコントラクトの最も重要な点を以下にまとめています(個人的な意見のため、他者にとって重要な点とは異なる場合があります) 。

  • ブロックチェーンに秘密はありません。 スマートコントラクトがアクセスできる情報は、世界中で利用可能であることを意味します。
  • 自分のトランザクションの順序は自分で制御できますが、他のユーザーのトランザクションが発生するタイミングは制御できません。 これが、割当量(allowance)の変更に伴うリスクになります。変更により、使用者(spender)が変更前と変更後の両方の割当量を使用できてしまうためです。
  • uint256型の値がオーバー(アンダー)フローします。 つまり、例えば0-1=2^256-1です。 これが望ましい動作ではない場合、プログラムで確認する必要があります(または、それを行う SafeMath ライブラリを使用します)。 この仕様は、Solidity 0.8.0(opens in a new tab)で変更されていることに注意してください。
  • 監査を容易にするため、特定の場所で特定の型のすべての状態変更を行います。 例えば、_approveが、approvetransferFromincreaseAllowancedecreaseAllowanceなどによって呼び出されるのは、この理由です。
  • (_transferで見られるように)状態変更は、処理の途中で他の操作に割り込まれることがないアトミックである必要があります。 これは状態変更中に、一貫性のない状態が存在するためです。 例えば、送信者の残高からトークンを差し引いた時点から、受取人の残高にそのトークンを加えるまでの間は、存在すべき数よりも少ない数のトークンが存在することになります。 この 2 つの処理の間に別の操作(特に、異なるコントラクトの呼び出しなど)がある場合、このトークンの状態が悪用される可能性があります。

ここまで、OpenZeppelin ERC-20 コントラクトがどのように書かれているかについて見てきました。特に、より安全に記述する方法を学びました。是非自分でも安全なコントラクトとアプリケーションを作成してみてください。

最終編集者: @HiroyukiNaito(opens in a new tab), 2023年8月15日

このチュートリアルは役に立ちましたか?