アクセス制御
重要な関数には「誰が呼べるか」を必ず書きます。msg.senderは引数で受け取ったアドレスではなく、実際の呼び出し元です。
管理者を固定するSOLIDITY
modifier onlyAdmin() {
require(msg.sender == admin, "not admin");
_;
}
function setFee(uint256 nextFee) external onlyAdmin {
require(nextFee <= 100, "fee too high");
fee = nextFee;
}実行結果OUTPUT
adminでsetFee(5) → fee: 5
admin以外でsetFee(5) → revert "not admin"
setFee(101) → revert "fee too high"アドレスを引数にして権限判定すると、他人のアドレスを指定してなりすます設計になりかねません。
Checks-Effects-Interactions
外部コントラクト呼び出しを含む処理は、①条件確認、②自分の状態更新、③外部呼び出しの順に分けます。外部呼び出し先が予想外のコードを実行する可能性を忘れないための基本パターンです。
引き出しの順序SOLIDITY
function withdraw() external {
uint256 amount = credits[msg.sender];
require(amount > 0, "no credit");
credits[msg.sender] = 0;
(bool success, ) = payable(msg.sender).call{value: amount}("");
require(success, "send failed");
}実行結果OUTPUT
credits[Account 0] = 1 ether
withdraw() → credits[Account 0] = 0、送金成功再入可能性を知る
外部へ送金すると、相手のfallbackやreceiveが呼ばれ、相手から元の関数を再度呼ばれることがあります。状態更新を送金の前に行う、再入防止ロックを使う、pull paymentに分けるなどの対策を検討します。
オンライン例をそのまま資産管理へ使わない
安全性はコントラクト全体、依存ライブラリ、コンパイラー、運用方法で決まります。小さな例でもレビューとテストが必要です。
計算と入力を検証する
0.8系の通常の算術演算はオーバーフロー時にrevertしますが、入力値の上限やゼロ除算、配列の範囲外、ガスを使い切るループは別に検証します。ユーザーが入力する値は必ず境界値をテストします。
公開前の確認リスト
- 権限のないアカウントで管理関数を呼んだか
- 0、最大値、残高不足、二重実行を試したか
- 外部呼び出しの前に状態を更新しているか
- イベントとエラーで失敗を調査できるか
- コンパイラーと依存ライブラリのバージョン指定・互換性を確認したか
この講座の例ではpragma solidity ^0.8.20;のように0.8系の互換範囲を指定しています。本番では使用するコンパイラー、最適化設定、依存ライブラリのバージョンを記録し、互換性を確認してから公開します。
次はテストとデプロイで、自動確認と公開前の手順を学びます。