式と制御構造
制御構造
カーリーブレース言語で知られている制御構造のほとんどがSolidityで利用可能です。
if
、 else
、 while
、 do
、 for
、 break
、 continue
、 return
、また、CやJavaScriptで知られている通常のセマンティクスがあります。
Solidityは、 try
/catch
文の形での例外処理もサポートしていますが、 外部関数呼び出し とコントラクト作成の呼び出しにのみ対応しています。
エラーは リバート文 を使って作成できます。
条件式では括弧を省略できませんが、単一の文であれば中括弧を省略できます。
なお、CやJavaScriptのように、非ブール型からブール型への型変換はありませんので、 if (1) { ... }
はSolidityとしては有効では ありません。
関数呼び出し
内部関数呼び出し
コントラクトの関数はそのコントラクト内の関数を、直接(「内部的に」)、再帰的に呼び出すことができます。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;
// これは警告を通知します。
contract C {
function g(uint a) public pure returns (uint ret) { return a + f(); }
function f() internal pure returns (uint ret) { return g(7) + f(); }
}
これらの関数呼び出しは、EVM内部で単純なジャンプに変換されます。 これは、現在のメモリがクリアされないという効果があります。 つまり、内部で呼び出された関数にメモリ参照を渡すことは非常に効率的です。 同じコントラクトインスタンスの関数のみが内部で呼び出されます。
すべての内部関数呼び出しは少なくとも1つのスタックスロットを使用し、利用可能なスロットは1024個しかないため、過度な再帰は避けるべきです。
外部関数呼び出し
関数呼び出しには、 this.g(8);
と c.g(2);
の記法を使うこともできます。
c
はコントラクトのインスタンス、 g
は c
に属する関数です。
いずれかの方法で関数 g
を呼び出すと、ジャンプを介して直接呼び出されるのではなく、メッセージコールを使用して「外部から」呼び出されることになります。
this
の関数呼び出しはコンストラクタでは使用できないことに注意してください。
実際のコントラクトはまだ作成されていないためです。
他のコントラクトの関数は、外部から呼び出す必要があります。 外部呼び出しの際には、すべての関数の引数をメモリにコピーする必要があります。
注釈
あるコントラクトから別のコントラクトへの関数呼び出しは、独自のトランザクションを作成するものではなく、全体のトランザクションの一部としてのメッセージ呼び出しです。
他のコントラクトの関数を呼び出す場合、特別なオプション {value: 10, gas: 10000}
で呼び出しとともに送られるweiまたはガスの量を指定できます。
なお、ガスの値を明示的に指定することは推奨されません。
オペコードのガスコストは将来変更される可能性があるからです。
コントラクトに送ったWeiは、そのコントラクトの総残高に追加されます。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.2 <0.9.0;
contract InfoFeed {
function info() public payable returns (uint ret) { return 42; }
}
contract Consumer {
InfoFeed feed;
function setFeed(InfoFeed addr) public { feed = addr; }
function callFeed() public { feed.info{value: 10, gas: 800}(); }
}
info
関数にモディファイア payable
を使用する必要があります。
そうしないと、 value
オプションは使用できません。
警告
注意すべきなのは、 feed.info{value: 10, gas: 800}
は関数呼び出しで value
と送信される gas
の量をローカルに設定しているだけで、最後の括弧内は実際の呼び出しを実行しているということです。
そのため、 feed.info{value: 10, gas: 800}
は関数をコールして value
と gas
の設定が失われることはなく、 feed.info{value: 10, gas: 800}()
のみが関数呼び出しを実行します。
EVMでは、存在しないコントラクトへの呼び出しは常に成功すると考えられているため、Solidityは extcodesize
オペコードを使用して、呼び出されようとしているコントラクトが実際に存在する(コードが含まれている)かどうかをチェックし、存在しない場合は例外を発生させます。
このチェックは、呼び出し後にリターンデータがデコードされる場合にはスキップされ、存在しないコントラクトのケースをABIデコーダがキャッチします。
なお、コントラクトインスタンスではなく、アドレスを操作する 低レベル呼び出し の場合は、このチェックは行われません。
注釈
プリコンパイル済みコントラクト のハイレベルな呼び出しを使用する際には、コードを実行してデータを返すことができるにもかかわらず、コンパイラは上記の論理に従ってそれらを存在しないものとみなすため、注意が必要です。
また、関数呼び出しは、呼び出されたコントラクト自身が例外を投げたり、ガス欠になったりした場合にも例外を発生させます。
警告
他のコントラクトとの相互作用は、特にコントラクトのソースコードが事前にわからない場合、潜在的な危険をもたらします。 現在のコントラクトは呼び出されたコントラクトに制御を渡し、そのコントラクトはあらゆることを行う可能性があります。 呼び出されたコントラクトが既知の親コントラクトを継承している場合でも、継承しているコントラクトは正しいインターフェースを持っていることだけが要求されます。 しかし、コントラクトの実装は完全に恣意的なものになる可能性があり、危険を伴います。 さらに、システムの他のコントラクトを呼び出したり、最初の呼び出しが戻る前に呼び出し元のコントラクトに戻ったりする場合にも備えてください。 つまり、呼び出されたコントラクトは、その関数を介して呼び出したコントラクトの状態変数を変更できるということです。 コントラクトがreentrancyエクスプロイトに対して脆弱でないように、例えば外部関数への呼び出しがコントラクト内の状態変数の変更後に行われるように、関数を記述してください。
注釈
Solidity 0.6.2以前は、valueとgasを指定する方法として、 f.value(x).gas(g)()
を使用することが推奨されていました。
これはSolidity 0.6.2で非推奨となり、Solidity 0.7.0からはできなくなりました。
名前付きパラメータを使った関数呼び出し
関数呼び出しの引数は、次の例のように { }
で囲まれていれば、任意の順序で名前を与えることができます。
引数リストは、関数宣言のパラメータリストと名前が一致していなければなりませんが、任意の順序にできます。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract C {
mapping(uint => uint) data;
function f() public {
set({value: 2, key: 3});
}
function set(uint key, uint value) public {
data[key] = value;
}
}
関数定義での名前の省略
関数宣言のパラメータや戻り値の名前は省略できます。
名前が省略された項目はスタック上に存在しますが、名前からアクセスすることはできません。
省略された戻り値名は、 return
文を使用することで、呼び出し元に値を返すことは可能です。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;
contract C {
// 省略されたパラメータ名
function func(uint k, uint) public pure returns(uint) {
return k;
}
}
new
を使ったコントラクト作成
コントラクトは、 new
キーワードを使って他のコントラクトを作成できます。
作成されるコントラクトの完全なコードは、作成するコントラクトがコンパイルされるときに知られていなければならないので、再帰的な作成依存は不可能です。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract D {
uint public x;
constructor(uint a) payable {
x = a;
}
}
contract C {
D d = new D(4); // C のコンストラクタの一部として実行されます。
function createD(uint arg) public {
D newD = new D(arg);
newD.x();
}
function createAndEndowD(uint arg, uint amount) public payable {
// 作成と一緒にEtherを送る
D newD = new D{value: amount}(arg);
newD.x();
}
}
例に見られるように、 value
オプションを使用して D
のインスタンスを作成中にEtherを送信することは可能ですが、ガスの量を制限できません。
作成に失敗した場合(スタック不足、残高不足、その他の問題)、例外が発生します。
ソルトされたコントラクト作成 / create2
コントラクトを作成する際、コントラクトのアドレスは、作成するコントラクトのアドレスと、コントラクトを作成するたびに増加するカウンタから計算されます。
オプションの salt
(bytes32の値)を指定した場合、コントラクトの作成では、別のメカニズムで新しいコントラクトのアドレスを計算します。
作成したコントラクトのアドレス、与えられたソルト値、作成したコントラクトの(作成)バイトコード、コンストラクタの引数からアドレスを計算します。
特に、カウンター("nonce")は使用されません。 これにより、コントラクトをより柔軟に作成できます。 新しいコントラクトが作成される前に、そのアドレスを導き出すことができます。 さらに、コントラクトを作成している間に他のコントラクトを作成した場合にも、このアドレスに依存できます。
ここでの主なユースケースは、オフチェーンでのやりとりの判断材料となるコントラクトで、紛争が発生した場合にのみ作成する必要があります。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract D {
uint public x;
constructor(uint a) {
x = a;
}
}
contract C {
function createDSalted(bytes32 salt, uint arg) public {
// この複雑な式は、アドレスがどのように事前計算されるかを示しているに過ぎません。
// これは説明のために存在するだけです。
// 実際には ``new D{salt: salt}(arg)`` だけが必要です。
address predictedAddress = address(uint160(uint(keccak256(abi.encodePacked(
bytes1(0xff),
address(this),
salt,
keccak256(abi.encodePacked(
type(D).creationCode,
abi.encode(arg)
))
)))));
D d = new D{salt: salt}(arg);
require(address(d) == predictedAddress);
}
}
警告
ソルトされた作成に関しては、いくつかの特殊性があります。 コントラクトは破壊された後、同じアドレスで再作成できます。 しかし、新しく作成されたコントラクトは、作成時のバイトコードが同じであっても、デプロイ時のバイトコードが異なる可能性があります(そうしないとアドレスが変わってしまうため、これは必須条件です)。 これは、コンストラクタが2つの作成の間に変更された可能性のある外部状態を照会し、それを格納する前にデプロイされたバイトコードに組み込むことができるという事実によるものです。
式の評価順序
式の評価順序は指定されていません(より正式には、式ツリーのあるノードの子が評価される順序は指定されていませんが、もちろんそのノード自身よりも先に評価されます)。 文が順番に実行されることが保証されているだけであり、ブーリアン式の短絡は行われます。
代入
代入のデストラクティングと複数の値のリターン
Solidityは内部的にタプル型を許可しています。 つまり、潜在的に異なる型のオブジェクトのリストで、その数はコンパイル時に一定となります。 これらのタプルは、同時に複数の値を返すために使用できます。 これらの値は、新たに宣言された変数や既存の変数(または一般的なLValue)に代入できます。
タプルはSolidityでは適切な型ではなく、式の構文的なグループ化をするためにのみ使用されます。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
contract C {
uint index;
function f() public pure returns (uint, bool, uint) {
return (7, true, 2);
}
function g() public {
// 型付きで宣言され、返されたタプルから代入される変数は、すべての要素を指定する必要はありません(ただし、数は一致しなければなりません)。
(uint x, , uint y) = f();
// 値をスワップする一般的なトリック -- 値を持たないストレージ型では機能しません。
(x, y) = (y, x);
// コンポーネントは省略可能です(変数宣言の場合も同様)。
(index, , ) = f(); // indexを7に設定
}
}
変数の宣言と非宣言の代入を混在させることはできません。
つまり、次のようなものは有効ではありません。
(x, uint y) = (1, 2);
注釈
バージョン0.5.0以前では、より小さなサイズのタプルに、左側または右側(どちらかが空の場合)を埋めるように割り当てることができました。 これは現在では禁止されており、両側とも同じ数のコンポーネントを持たなければなりません。
警告
参照型が含まれる場合に複数の変数に同時に代入すると、予期しないコピー動作になることがあるので注意が必要です。
配列と構造体の複雑さ
代入のセマンティクスは、 bytes
や string
などの配列や構造体などの非値型ではより複雑になりますが。
詳細は データロケーションと代入の動作 を参照してください。
以下の例では、 g(x)
の呼び出しは、メモリ内にストレージの値の独立したコピーを作成するため、 x
に影響を与えません。
しかし、 h(x)
はコピーではなく参照のみが渡されるため、 x
の変更に成功しています。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;
contract C {
uint[20] x;
function f() public {
g(x);
h(x);
}
function g(uint[20] memory y) internal pure {
y[2] = 3;
}
function h(uint[20] storage y) internal {
y[3] = 4;
}
}
スコーピングと宣言
宣言された変数は、バイト表現がすべてゼロである初期のデフォルト値を持ちます。
変数の「デフォルト値」は、その型が何であれ、典型的な「ゼロ状態」です。
例えば、 bool
のデフォルト値は false
です。
uint
型や int
型のデフォルト値は 0
です。
静的なサイズの配列や bytes1
から bytes32
では、個々の要素はその型に対応するデフォルト値に初期化されます。
動的なサイズの配列や bytes
と string
では、デフォルト値は空の配列または文字列です。
enum
型では、初期値はその最初のメンバーです。
Solidityのスコーピングは、C99(および他の多くの言語)で広く採用されているスコーピングルールに従っています。
変数は、その宣言の直後から、その宣言を含む最小の { }
ブロックの終わりまで見ることができます。
この規則の例外として、for-loopの初期化部分で宣言された変数は、for-loopの終わりまでしか見えません。
パラメータのような変数(関数パラメータ、モディファイアパラメータ、キャッチパラメータなど)は、次のコードブロックの中に表示されます。 関数パラメータとモディファイアパラメータの場合は関数/モディファイアのボディ、キャッチパラメータの場合はキャッチブロックです。
コードブロックの外で宣言された変数やその他のアイテム(例えば、関数、コントラクト、ユーザー定義型など)は、宣言される前から見ることができます。 つまり、宣言される前の状態の変数を使用したり、関数を再帰的に呼び出したりできます。
その結果、以下の例では、2つの変数は同じ名前ですが、スコープが異なっているため、警告を出さずにコンパイルできます。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
contract C {
function minimalScoping() pure public {
{
uint same;
same = 1;
}
{
uint same;
same = 3;
}
}
}
C99のスコープルールの特別な例として、以下では、 x
への最初の代入が実際には内側の変数ではなく外側の変数を代入することに注意してください。
いずれにしても、外側の変数がシャドーイングされているという警告が表示されます。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
// これは警告を報告します
contract C {
function f() pure public returns (uint) {
uint x = 1;
{
x = 2; // これは外側の変数に代入されます
uint x;
}
return x; // xは2になります
}
}
警告
バージョン0.5.0以前のSolidityは、JavaScriptと同じスコープルールに従っていました。 つまり、関数内の任意の場所で宣言された変数は、どこで宣言されたかに関わらず、関数全体のスコープになります。 次の例は、バージョン0.5.0以降、コンパイル時にエラーが発生するコードスニペットです。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
// これはコンパイルできません
contract C {
function f() pure public returns (uint) {
x = 2;
uint x;
return x;
}
}
算術演算のチェックをするかしないか
オーバーフローまたはアンダーフローとは、制限のない整数に対して算術演算を実行したときに、結果の値が結果の型の範囲外になってしまうことです。
Solidity 0.8.0以前では、アンダーフローやオーバーフローが発生した場合、算術演算は常にラップするため、追加のチェックを導入するライブラリが広く使用されていました。
Solidity 0.8.0以降、すべての算術演算はデフォルトでオーバーフローとアンダーフローでリバートするため、これらのライブラリを使用する必要はありません。
以前のような動作を得るためには、 unchecked
ブロックを使用できます。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract C {
function f(uint a, uint b) pure public returns (uint) {
// この引き算はアンダーフローでラップされます
unchecked { return a - b; }
}
function g(uint a, uint b) pure public returns (uint) {
// この引き算はアンダーフローでリバートされます
return a - b;
}
}
f(2, 3)
を呼び出すと 2**256-1
が返され、 g(2, 3)
を呼び出すとアサーションに失敗することになります。
unchecked
ブロックは、ブロックの中であればどこでも使えますが、ブロックの代わりにはなりません。
また、入れ子にすることもできません。
この設定は、構文的にブロックの内部にある文にのみ影響します。
unchecked
ブロック内から呼び出された関数は、このプロパティを継承しません。
注釈
曖昧さを避けるため、 unchecked
ブロック内で _;
を使用できません。
以下の演算子は、オーバーフローまたはアンダーフロー時にアサーションの失敗を引き起こし、チェックされていないブロック内で使用された場合はエラーなしでラップされます。
++
, --
, +
, 二項 -
, 単項 -
, *
, /
, %
, **
+=
, -=
, *=
, /=
, %=
警告
unchecked
ブロックを使ってもゼロ除算やゼロによる剰余のチェックは無効にできません。
注釈
ビット演算子はオーバーフローやアンダーフローのチェックを行いません。
これは、整数の除算や2の累乗の代わりにビット単位のシフト( <<
、 >>
、 <<=
、 >>=
)を使用する場合に特に顕著です。
注釈
int x = type(int).min; -x;
の2つ目の文は、負の範囲が正の範囲よりも1つ多くの値を保持できるため、オーバーフローになります。
明示的な型変換は常に切り捨てられ、整数型からenum型への変換を除いて、アサーションの失敗は発生しません。
エラーハンドリング: Assert, Require, Revert, Exception
Solidityでは、エラーの処理にステートをリバートする例外を使用します。 このような例外は、現在の呼び出し(およびそのすべてのサブコール)で行われた状態への変更をすべてリバートし、呼び出し側にエラーを通知します。
サブコールで例外が発生した場合、 try/catch
文で捕捉されない限り、自動的に「バブルアップ」(例外が再スローされる)します。
このルールの例外は、 send
と低レベル関数の call
、 delegatecall
、 staticcall
です。
これらの関数は、例外が発生した場合、「バブルアップ」するのではなく、 false
を最初の戻り値として返します。
警告
低レベル関数の call
、 delegatecall
、 staticcall
は、EVMの設計の一環として、呼び出されたアカウントが存在しない場合、最初の戻り値として true
を返します。
必要に応じて、呼び出す前にアカウントの存在を確認する必要があります。
例外にはエラーデータを含めることができ、 error instances の形で呼び出し側に戻されます。
組み込みエラーの Error(string)
と Panic(uint256)
は、以下に説明するように特別な関数で使用されます。
Error
は「通常の」エラー状態に使用され、 Panic
はバグのないコードでは存在してはならないエラーに使用されます。
assert
を介したパニックと require
を介したエラー
コンビニエンス関数である assert
と require
は、条件をチェックし、条件を満たさない場合は例外を投げることができます。
assert
関数では、 Panic(uint256)
型のエラーが発生します。
以下のような特定の状況では、コンパイラによって同じエラーが発生します。
Assertは、内部エラーのテストや不変性のチェックにのみ使用します。 適切に機能しているコードは、外部からの不正な入力に対してもパニックを起こさないはずです。 もしそうなってしまったら、コントラクトにバグがあるので修正する必要があります。 言語解析ツールは コントラクトを評価し、パニックを引き起こす条件や関数呼び出しを特定します。
パニック例外は次のような場合に発生します。 エラーデータとともに提供されるエラーコードは、パニックの種類を示します。
0x00: 一般的なコンパイラの挿入されたパニックに使用されます。
0x01: falseと評価される引数で
assert
を呼び出した場合。0x11:
unchecked { ... }
ブロックの外で、演算結果がアンダーフローまたはオーバーフローになった場合。0x12; 0で除算や剰余をした場合(例:
5 / 0
や23 % 0
)。0x21: 大きすぎる値や負の値を列挙型に変換した場合。
0x22: 正しくエンコードされていないストレージのバイト配列にアクセスした場合。
0x31: 空の配列で
.pop()
を呼び出した場合。0x32: 境界外または負のインデックス(
x[i]
、i >= x.length
、i < 0
など)で配列、bytesN
、または配列スライスにアクセスした場合。0x41: メモリの割り当てが多すぎたり、大きすぎる配列を作成した場合。
0x51: 内部関数型のゼロ初期化変数を呼び出した場合。
require
関数は、データのないエラーを作成するか、 Error(string)
型のエラーを作成します。
require
関数は、実行時まで検出できない有効な条件を保証するために使用する必要があります。
これには、入力に対する条件や、外部コントラクトへの呼び出しからの戻り値が含まれます。
注釈
現在、 require
との組み合わせでカスタムエラーを使用できません。
代わりに if (!condition) revert CustomError();
を仕様してください。
Error(string)
例外(またはデータのない例外)は、以下のような場合にコンパイラによって生成されます。
x
がfalse
に評価されるときrequire(x)
を呼び出す。revert()
やrevert("description")
を使う場合。コードを含まないコントラクトを対象とした外部関数呼び出しを行った場合。
payable
モディファイアのないパブリック関数(コンストラクタ、フォールバック関数を含む)を介してコントラクトがEtherを受け取る場合。コントラクトがパブリックゲッター関数でEtherを受け取る場合。
以下のケースでは、外部の呼び出しからのエラーデータ(提供されている場合)が送金されます。
これは、 Error
または Panic
(またはその他の何かが与えられた場合)を引き起こす可能性があることを意味します。
.transfer()
が失敗した場合。メッセージコールで関数を呼び出したが、正しく終了しなかった場合(ガス欠、一致する関数がない、自分自身で例外をスローするなど)。 低レベルの操作
call
、send
、delegatecall
、callcode
、staticcall
を使用した場合を除きます。 低レベルの操作は、例外を投げることはありませんが、false
を返すことで失敗を示します。new
キーワードを使ってコントラクトを作成しても、コントラクトの作成が 正しく終了しない場合 。
require
にはオプションでメッセージ文字列を指定できますが、 assert
には指定できません。
注釈
require
に文字列の引数を与えない場合、エラーセレクタを含めずに空のエラーデータでリバートします。
次の例では、 require
で入力の状態を確認し、 assert
で内部のエラーチェックを行うことができます。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
contract Sharer {
function sendHalf(address payable addr) public payable returns (uint balance) {
require(msg.value % 2 == 0, "Even value required.");
uint balanceBeforeTransfer = address(this).balance;
addr.transfer(msg.value / 2);
// transferに失敗すると例外がスローされ、ここにコールバックすることはできないので、半分のEtherを送金せず保持する方法はないはずです。
assert(address(this).balance == balanceBeforeTransfer - msg.value / 2);
return address(this).balance;
}
}
内部的には、Solidityはリバートする操作(命令 0xfd
)を行います。
これにより、EVMは状態に加えられたすべての変更をリバートします。
リバートする理由は、期待した効果が発生しなかったために、実行を継続する安全な方法がない場合です。
トランザクションのアトミック性を維持したいので、最も安全なアクションはすべての変更をリバートし、トランザクション全体(または少なくともコール)を効果なしにすることです。
どちらの場合も、呼び出し側はそのような失敗に対して try
/ catch
を使って反応できますが、呼び出された側の変更は必ずリバートされます。
注釈
パニック例外は、Solidity 0.8.0以前は invalid
オペコードを使用していましたが、これは呼び出しに使用可能なすべてのガスを消費していました。
require
を使用する例外は、Metropolisリリースの前まではすべてのガスを消費していました。
revert
リバートは、 revert
文と revert
関数を使って直接トリガーできます。
revert
文では、カスタムエラーを括弧なしで直接引数として受け取ります。
revert CustomError(arg1, arg2);
後方互換性を考慮して、括弧を使用して文字列を受け取る revert()
関数もあります。
revert(); revert("description");
エラーデータは呼び出し側に戻されるので、そこでキャッチできます。
revert()
を使うとエラーデータなしでリバートしますが、 revert("description")
を使うと Error(string)
エラーが発生します。
カスタムエラーのインスタンスを使用すると、通常、文字列による説明よりもはるかにコストが小さくなります。 これは、わずか4バイトでエンコードされるエラーの名前を使用して説明できるからです。 より長い記述はNatSpecを介して提供できますが、これには一切のコストがかかりません。
次の例では、エラー文字列とカスタムエラーインスタンスを、 revert
と同等の require
と一緒に使用しています。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract VendingMachine {
address owner;
error Unauthorized();
function buy(uint amount) public payable {
if (amount > msg.value / 2 ether)
revert("Not enough Ether provided.");
// 別の方法:
require(
amount <= msg.value / 2 ether,
"Not enough Ether provided."
);
// 購入を実行する
}
function withdraw() public {
if (msg.sender != owner)
revert Unauthorized();
payable(msg.sender).transfer(address(this).balance);
}
}
if (!condition) revert(...);
と require(condition, ...);
の2つの方法は、 revert
と require
への引数が副作用を持たない限り、例えば単なる文字列であれば、等価です。
注釈
require
関数は、他の関数と同様に評価されます。
これは、関数自体が実行される前に、すべての引数が評価されることを意味します。
特に require(condition, f())
では、 condition
が真であっても関数 f
が実行されます。
提供された文字列は、あたかも関数 Error(string)
の呼び出しであるかのように ABIエンコード されます。
上記の例では、 revert("Not enough Ether provided.");
はエラーの返り値として次の16進数を返します。
0x08c379a0 // Error(string)の関数セレクタ
0x0000000000000000000000000000000000000000000000000000000000000020 // データオフセット
0x000000000000000000000000000000000000000000000000000000000000001a // 文字列の長さ
0x4e6f7420656e6f7567682045746865722070726f76696465642e000000000000 // 文字列データ
与えたメッセージは、以下のように try
/ catch
を使って呼び出し側が取り出すことができます。
注釈
かつて、 revert()
と同じ意味を持つ throw
というキーワードがありましたが、バージョン0.4.13で非推奨となり、バージョン0.5.0で削除されました。
try
/catch
外部呼び出しの失敗は、以下のようにtry/catch文を使ってキャッチできます。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.1;
interface DataFeed { function getData(address token) external returns (uint value); }
contract FeedConsumer {
DataFeed feed;
uint errorCount;
function rate(address token) public returns (uint value, bool success) {
// 10個以上のエラーが発生した場合、この機構を永久に無効にします。
require(errorCount < 10);
try feed.getData(token) returns (uint v) {
return (v, true);
} catch Error(string memory /*reason*/) {
// これは、getDataの内部でrevertが呼び出され、文字列reasonが提供された場合に実行されます。
errorCount++;
return (0, false);
} catch Panic(uint /*errorCode*/) {
// これはパニック、すなわちゼロによる除算やオーバーフローのような重大なエラーが発生した場合に実行されます。
// エラーコードからエラーの種類を判断できます。
errorCount++;
return (0, false);
} catch (bytes memory /*lowLevelData*/) {
// revert()が使用された場合に実行されます。
errorCount++;
return (0, false);
}
}
}
try
キーワードの後には、外部関数呼び出しやコントラクトの作成( new ContractName()
)を表す式が続く必要があります。
式の内部のエラーは捕捉されず(例えば、内部の関数呼び出しを含む複雑な式の場合)、外部呼び出し自体の内部で起こるリバートのみが捕捉されます。
続く returns
部(オプション)では、外部呼び出しが返す型に一致するリターン変数を宣言します。
エラーがなかった場合、これらの変数が代入され、コントラクトの実行は最初の成功ブロック内で継続されます。
成功ブロックの終わりに達した場合は、 catch
ブロックの後に実行が続きます。
Solidityでは、エラーの種類に応じて様々な種類のキャッチブロックをサポートしています。
catch Error(string memory reason) { ... }
: このキャッチ句は、エラーの原因がrevert("reasonString")
またはrequire(false, "reasonString")
(またはこのような例外を引き起こす内部エラー)であった場合に実行されます。catch Panic(uint errorCode) { ... }
: エラーがパニックによって引き起こされた場合、つまり、assert
の失敗、ゼロによる除算、無効な配列アクセス、算術オーバーフローなどによって引き起こされた場合、このキャッチ句が実行されます。catch (bytes memory lowLevelData) { ... }
: この句は、エラーのシグネチャが他の句と一致しない場合、エラーメッセージのデコード中にエラーが発生した場合、または例外でエラーデータが提供されなかった場合に実行されます。 宣言された変数は、その場合の低レベルのエラーデータへのアクセスを提供します。catch { ... }
: エラーデータに興味がないのであれば、前の句の代わりにcatch { ... }
を(唯一のcatch句としても)使用すればよいでしょう。
将来は、他の型のエラーデータにも対応する予定です。
文字列 Error
と Panic
は、現在、そのまま解析され、識別子としては扱われません。
すべてのエラーケースをキャッチするためには、少なくとも catch { ...}
句または catch (bytes memory lowLevelData) { ... }
句が必要です。
returns
句と catch
句で宣言された変数は、それに続くブロックでのみスコープに入ります。
注釈
try/catch文の中でリターンデータのデコード中にエラーが発生した場合、現在実行中のコントラクトで例外が発生し、そのためcatch句ではキャッチされません。
catch Error(string memory reason)
のデコード中にエラーが発生し、低レベルのcatch句がある場合は、このエラーはそこでキャッチされます。
注釈
実行がキャッチブロックに到達した場合、外部呼び出しの状態変化の影響はリバートされています。 実行が成功ブロックに到達した場合、その効果はリバートされていません。 効果がリバートした場合、実行はcatchブロック内で継続されるか、try/catch文の実行自体がリバートします(例えば、上述のようなデコードの失敗や、低レベルのcatch句を提供していないことが原因です)。
注釈
失敗したコールの原因はさまざまです。 エラーメッセージが呼び出されたコントラクトから直接来ていると思わないでください。 エラーはコールチェーンのより深いところで発生し、呼び出されたコントラクトがそれをフォワードしただけかもしれません。 また、意図的なエラー状態ではなく、ガス欠状態が原因である可能性もあります。 コール側は常に1/64以上のガスを保持しているため、コールされたコントラクトがガス欠になっても、コール側にはガスが残っています。