ERC-20コントラクトの詳細
はじめに
イーサリアムの最も一般的な用途の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)、流動性プールなどの異なるコントラクトが含まれます。
経験豊富なプログラマーであれば、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 * @dev Interface of the ERC20 standard as defined in the EIP.3 */コピー
コメント中の@dev
はNatSpec形式(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
型で識別されます。 また、external
やview
もあります。
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 be3 * 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 risk7 * that someone may use both the old and the new allowance by unfortunate8 * transaction ordering. One possible solution to mitigate this race9 * condition is to first reduce the spender's allowance to 0 and set the10 * desired value afterwards:11 * https://github.com/ethereum/EIPs/issues/20#issuecomment-26352472912 *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 the3 * allowance mechanism. `amount` is then deducted from the caller's4 * 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)を実際に使用します。
12 /**3 * @dev Emitted when `value` tokens are moved from one account (`from`) to4 * another (`to`).5 *6 * Note that `value` may be zero.7 */8 event Transfer(address indexed from, address indexed to, uint256 value);910 /**11 * @dev Emitted when the allowance of a `spender` for an `owner` is set by12 * 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: MIT2pragma solidity >=0.6.0 <0.8.0;コピー
インポートステートメント
コントラクト定義では、上記のインターフェース定義に加え、別の2つのファイルをインポートします。
12import "../../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 means5 * 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 guide9 * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How10 * to implement supply mechanisms].11 *12 * We have followed general OpenZeppelin guidelines: functions revert instead13 * of returning `false` on failure. This behavior is nonetheless conventional14 * 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 just18 * by listening to said events. Other implementations of the EIP may not emit19 * 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 setting23 * allowances. See {IERC20-approve}.24 */25すべて表示コピー
コントラクトの定義
1contract ERC20 is Context, IERC20 {コピー
この行では、継承を指定しています。ここでは、IERC20
とOpenGSNのためのContext
を継承しています。
12 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} with3 * 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 during8 * 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 }78 /**9 * @dev Returns the symbol of the token, usually a shorter version of the10 * name.11 */12 function symbol() public view returns (string memory) {13 return _symbol;14 }1516 /**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 should19 * be displayed to a user as `5,05` (`505 / 10 ** 2`).20 *21 * Tokens usually opt for a value of 18, imitating the relationship between22 * ether and wei. This is the value {ERC20} uses, unless {_setupDecimals} is23 * called.24 *25 * NOTE: This information is only used for _display_ purposes: it in26 * no way affects any of the arithmetic of the contract, including27 * {IERC20-balanceOf} and {IERC20-transfer}.28 */29 function decimals() public view returns (uint8) {30 return _decimals;31 }すべて表示コピー
これらの関数(name
、symbol
、decimals
)は、ユーザーインターフェースがコントラクトの情報を入手できるようするため、ユーザーインターフェースにコントラクトの情報が正しく表示されるようになります。
戻り値の型は、 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)関連の関数
割当量機能を実装した関数には、allowance
、approve
、 transferFrom
、_approve
があります。 さらに、OpenZeppelin実装には、基本的な標準に加えてセキュリティを向上させる increaseAllowance
やdecreaseAllowance
などの複数の機能が含まれます。
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 least12 * `amount`.13 */14 function transferFrom(address sender, address recipient, uint256 amount) public virtual15 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) | 10 | 5 | 0 | ||
transferFrom(Alice, Bill, 5) | 10,123 | 0 | 5 | ||
approve(Bill, 10) | 11 | 10 | 5 | ||
transferFrom(Alice, Bill, 10) | 10,124 | 0 | 15 |
この問題を回避するには、割当量を特定の量に変更できる2つの関数(increaseAllowance
とdecreaseAllowance
)を使用します。 これらの関数を使用すれば、ビルがすでに5トークンを使用していた場合、その後に使用できるのは残りの5トークンのみとなります。 タイミングに応じて、次のような2つの動作が考えられますが、いずれの場合でもビルが取得するのは10トークンのみです。
A:
アリスのトランザクション | アリスのノンス | ビルのトランザクション | ビルのノンス | ビルの割当量 | ビルのアリスからの総収入 |
---|---|---|---|---|---|
approve(Bill, 5) | 10 | 5 | 0 | ||
transferFrom(Alice, Bill, 5) | 10,123 | 0 | 5 | ||
increaseAllowance(Bill, 5) | 11 | 0+5 = 5 | 5 | ||
transferFrom(Alice, Bill, 5) | 10,124 | 0 | 10 |
B:
アリスのトランザクション | アリスのノンス | ビルのトランザクション | ビルのノンス | ビルの割当量 | ビルのアリスからの総収入 |
---|---|---|---|---|---|
approve(Bill, 5) | 10 | 5 | 0 | ||
increaseAllowance(Bill, 5) | 11 | 5+5 = 10 | 0 | ||
transferFrom(Alice, Bill, 10) | 10,124 | 0 | 10 |
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 for5 * 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
のような計算が行われても、通常の加算で発生してしまうオーバー(アンダー)フローが発生しません。
12 /**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 for6 * 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 least14 * `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 to5 * 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つの方法があります。
- 自分のコードのテンプレートとして使う
- このコントラクトを継承(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`, increasing2 * 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 the3 * 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");1415 _beforeTokenTransfer(account, address(0), amount);1617 _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 to5 * 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");1718 _allowances[owner][spender] = amount;すべて表示コピー
Approval
イベントを発行します。 アプリケーションがどのように書かれているかによって異なりますが、使用者(spender)のコントラクトには、所有者またはこれらのイベントをリッスンしているサーバーのいずれかによって承認(Approval)が通知されます。
1 emit Approval(owner, spender, amount);2 }3コピー
小数変数の変更
123 /**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. Most7 * applications that interact with token contracts will not expect8 * {decimals} to ever change, and may work incorrectly if it does.9 */10 function _setupDecimals(uint8 decimals_) internal {11 _decimals = decimals_;12 }すべて表示コピー
この関数は、_decimals
変数を変更します。_decimals
変数は、量(金額)の解釈方法をユーザーインターフェースに伝えるのに使用されます。 コンストラクタから呼び出される必要があります。 その後のどの時点においても、この関数を呼び出すと不正になります。アプリケーションは、このような処理をするようには設計されていません。
フック
12 /**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 tokens9 * 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
が、approve
、transferFrom
、increaseAllowance
、decreaseAllowance
などによって呼び出されるのは、この理由です。 - (
_transfer
で見られるように)状態変更は、処理の途中で他の操作に割り込まれることがないアトミックである必要があります。 これは状態変更中に、一貫性のない状態が存在するためです。 例えば、送信者の残高からトークンを差し引いた時点から、受取人の残高にそのトークンを加えるまでの間は、存在すべき数よりも少ない数のトークンが存在することになります。 この2つの処理の間に別の操作(特に、異なるコントラクトの呼び出しなど)がある場合、このトークンの状態が悪用される可能性があります。
ここまで、OpenZeppelin ERC-20コントラクトがどのように書かれているかについて見てきました。特に、より安全に記述する方法を学びました。是非自分でも安全なコントラクトとアプリケーションを作成してみてください。
最終編集者: @nhsz(opens in a new tab), 2024年2月18日