solidity について
Solidity Development Standardsは、CyfrinのセキュリティチームによるSolidityのコーディング、テスト、セキュリティ、デプロイ、ガバナンスの指針です。開発からリリースまで一貫した実践を求めるスマートコントラクト開発者を対象としています。
主な機能
- 01防御的なSolidityコーディング規約
- 02ファズテストと不変条件テストの指針
- 03鍵と管理者の安全な運用
- 04Foundryによるデプロイとガバナンスの手順
- 05CIでのlintと静的解析チェック
活用シーン
- 01Solidityのコードスタイルの統一
- 02コントラクトのファズ・不変条件テストの計画
- 03安全なコントラクトデプロイの準備
- 04スマートコントラクトプロジェクトのCI設定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ cyfrin/ solskill --skill solidityインストール後、Claude Code で/solidityを入力して読み込まれたか確認します。
詳細
Solidity 開発標準
Cyfrin のセキュリティチームによる、Solidity コードの書き方に関する指針です。
基本方針
- あらゆるものが攻撃されます — 書いたコードはすべて攻撃されると想定し、防御的に記述してください。
コードの品質とスタイル
- 絶対パスと名前付きインポートのみを使用し、相対パス(
..)は使用しないでください。
// good
import {MyContract} from "contracts/MyContract.sol";
// bad
import "../MyContract.sol";
requireよりもrevertを優先し、コントラクト名と2つのアンダースコアをプレフィックスとするカスタムエラーを使用してください。
error ContractName__MyError();
// Good
myBool = true;
if (myBool) {
revert ContractName__MyError();
}
// bad
require(myBool, "MyError");
- テストでは、ユニットテストよりもステートレスなファズテストを優先してください。
// good - using foundry's built in stateless fuzzer
function testMyTest(uint256 randomNumber) { }
// bad
function testMyTest() {
uint256 randomNumber = 0;
}
さらに、プロトコルの中核的な特性について、インバリアント(ステートフル)ファズテストを記述してください。インバリアント駆動開発を採用し、常に成り立つ必要がある O(1) の特性を特定して、コア関数に直接組み込んでください(FREI-PI パターン)。Chimera などのマルチファザー構成を使用して、Foundry、Echidna、Medusa で同じインバリアントテストスイートを実行してください。ファザーごとに、異なるバグが見つかります。
- 関数は可視性ごとにまとめ、次の順序で並べてください。
constructor
receive function (if exists)
fallback function (if exists)
user-facing state-changing functions
(external or public, not view or pure)
user-facing read-only functions
(external or public, view or pure)
internal state-changing functions
(internal or private, not view or pure)
internal read-only functions
(internal or private, view or pure)
- ヘッダーは次のようにしてください。
/*//////////////////////////////////////////////////////////////
INTERNAL STATE-CHANGING FUNCTIONS
//////////////////////////////////////////////////////////////*/
- ファイルの構成
Pragma statements
Import statements
Events
Errors
Interfaces
Libraries
Contracts
コントラクトの構成:
Type declarations
State variables
Events
Errors
Modifiers
Functions
- テストを作成する際は、分岐ツリー手法を使用してください。 この手法の功績はPaul R Bergにあります。
- 対象の関数を決める
.treeファイルを作成する- 考えられるすべての実行パスを検討する
- どのコントラクト状態がどのパスにつながるかを検討する
- どの関数パラメーターがどのパスにつながるかを検討する
- 「given state is x」ノードを定義する
- 「when parameter is x」ノードを定義する
- 最後に「it should」テストを定義する
例:
├── when the id references a null stream
│ └── it should revert
└── when the id does not reference a null stream
├── given assets have been fully withdrawn
│ └── it should return DEPLETED
└── given assets have not been fully withdrawn
├── given the stream has been canceled
│ └── it should return CANCELED
└── given the stream has not been canceled
├── given the start time is in the future
│ └── it should return PENDING
└── given the start time is not in the future
├── given the refundable amount is zero
│ └── it should return SETTLED
└── given the refundable amount is not zero
└── it should return STREAMING
例:
function test_RevertWhen_Null() external {
uint256 nullStreamId = 1729;
vm.expectRevert(abi.encodeWithSelector(Errors.SablierV2Lockup_Null.selector, nullStreamId));
lockup.statusOf(nullStreamId);
}
modifier whenNotNull() {
defaultStreamId = createDefaultStream();
_;
}
function test_StatusOf()
external
whenNotNull
givenAssetsNotFullyWithdrawn
givenStreamNotCanceled
givenStartTimeNotInFuture
givenRefundableAmountNotZero
{
LockupLinear.Status actualStatus = lockup.statusOf(defaultStreamId);
LockupLinear.Status expectedStatus = LockupLinear.Status.STREAMING;
assertEq(actualStatus, expectedStatus);
}
-
コントラクトでは厳密な pragma バージョンを、テスト、ライブラリ、抽象コントラクト、インターフェース、スクリプトでは浮動 pragma バージョンを優先してください。最低バージョンは
0.8.34以降にしてください。バージョン0.8.28から0.8.33には、ストレージをクリアする際に IR パイプライン(--via-ir)がtstoreの代わりにsstore(またはその逆)を出力し、誤ったストレージ領域に書き込む可能性がある重大度の高い一時ストレージのバグがあります。 -
コントラクトの先頭にある natspec に、セキュリティ連絡先を追加してください。
/**
* @custom:security-contact [email protected]
* @custom:security-contact see https://mysite.com/ipfs-hash
*/
-
mainnet にデプロイする場合、またはデプロイしようとしている場合は、監査を受けるよう伝えてください。
-
秘密鍵を平文で記述することは、絶対に、決して、絶対にしないでください。唯一の例外は、anvil などのデフォルトキーを使用する場合で、その旨を明記する必要があります。
- デプロイスクリプトも対象です。デプロイスクリプトでは常に
forge script <path> --account $ACCOUNT --sender $SENDERを使用し、スクリプト内でvm.envUnit()を使用しないでください。 - hardhat では、hardhat の暗号化キーストアを使用してください。
- デプロイスクリプトも対象です。デプロイスクリプトでは常に
-
ownable または管理者権限(
onlyOwnerなど)を持つスマートコントラクトをデプロイする場合、最初のデプロイ時から管理者をマルチシグにしてください。デプロイヤー EOA を管理者にしてはいけません(許容される例外は testnet のみです)。Trail of Bits: 秘密鍵リスクを超えてスマートコントラクトを成熟させるを参照してください。「レイヤー 1」(単一の EOA)によるガバナンスは、DeFi ではもはや許容されません。 -
変数をデフォルト値で初期化しないでください。
// good
uint256 x;
bool y;
// bad
uint256 x = 0;
bool y = false;
- ローカル変数の宣言を省略できる場合は、名前付き戻り値変数の使用を優先してください。
// good
function getBalance() external view returns (uint256 balance) {
balance = balances[msg.sender];
}
// bad
function getBalance() external view returns (uint256) {
uint256 balance = balances[msg.sender];
return balance;
}
-
読み取り専用の関数入力では、
memoryよりもcalldataを優先してください。 -
calldata配列の長さをキャッシュしないでください。
// good — calldata length is cheap to read
for (uint256 i; i < items.length; ++i) { }
// bad — unnecessary caching for calldata
uint256 len = items.length;
for (uint256 i; i < len; ++i) { }
-
ストレージの読み取りにはコストがかかります。同一のストレージ読み取りを避けるため、変更されないストレージスロットをキャッシュし、キャッシュした値を渡すか使用してください。
-
できるだけ早く revert してください。ストレージ読み取りや外部呼び出しを必要とするチェックより前に、入力チェックを実行してください。
-
onlyOwner関数内では、ownerではなくmsg.senderを使用してください。 -
ETH の送金には、Solidity の
call()ではなくSafeTransferLib::safeTransferETHを使用してください。 -
入力変数の値を保持する必要がない場合は、追加のローカル変数を宣言するのではなく、入力変数を変更してください。
-
nonReentrant修飾子は、ほかの修飾子より前に使用してください。 -
より高速な
nonReentrant修飾子には、ReentrancyGuardTransientを使用してください。pragma solidity ^0.8.34;が必要です。一時ストレージのクリアに関するバグがあるため、0.8.28~0.8.33では使用しないでください。 -
OwnableよりもOwnable2Stepを優先してください。 -
必要なスロットが少数だけの場合、ストレージから構造体全体をメモリにコピーしないでください。
-
不要な「context」構造体を削除する、または context 構造体から不要な変数を削除してください。
-
ストレージや構造体を宣言する際は、変数が最小数のストレージスロットに収まるよう、宣言の順序を調整してください。変数を頻繁にまとめて読み書きする場合は、可能であれば同じスロットにまとめてください。
-
アップグレード不可のコントラクトでは、コンストラクターで一度だけ設定される変数を
immutableとして宣言してください。 -
foundry.tomlでオプティマイザーを有効にしてください。 -
修飾子が関数本体と同じストレージ読み取りを行う場合は、同一のストレージ読み取りを防ぐため、修飾子を内部関数にリファクタリングしてください。
-
平文の環境変数ではなく、Foundry の暗号化された安全な秘密鍵ストレージを使用してください。
-
アップグレード:スマートコントラクトをアップグレードする際は、既存の変数の順序や型を変更したり、削除したりしないでください。ストレージの衝突につながる可能性があります。また、アップグレードのテストも作成してください。
デプロイ
本番環境へのデプロイとテストのセットアップの両方で、Foundry スクリプト(forge script)を使用してください。これにより、開発環境とメインネットで同じデプロイロジックが実行され、デプロイの監査可能性が高まり、テスト環境と本番環境の差を減らせます。実際のデプロイ経路と異なる、テスト専用の独自セットアップコードは避けてください。理想的には、デプロイスクリプトも監査の対象にしてください。
例:テストと本番の両方が継承する共有ベーススクリプトを使用してください。スクリプトパターンを使用したこちらの BaseTestのようなものです。
ガバナンス
ガバナンス提案には、safe-utilsまたは同等のツールを使用してください。これにより、手動で UI をクリックするのではなく、Foundry スクリプトを通じてマルチシグとのやり取りをテスト、監査、再現できるようになります。UI を使用する必要がある場合は、localsafe.ethのような UI を使い、トランザクションを非公開にすることを推奨します。
ガバナンス提案の実行後に、想定どおりのプロトコル状態になっていることを検証するフォークテストを作成してください。メインネットの状態に対するフォークテストでは、ユニットテストで見逃す設定ミスを検出できます。たとえば、提案後にオラクルの状態が正しいことを検証するフォークテストがあれば、Moonwell の価格フィード設定ミスを検出できていたでしょう。
// good - fork test verifying governance proposal outcome
function testGovernanceProposal_UpdatesPriceFeed() public {
vm.createSelectFork(vm.envString("MAINNET_RPC_URL"));
// Execute the governance proposal
_executeProposal(proposalId);
// Verify expected state after proposal
address newFeed = oracle.priceFeed(market);
assertEq(newFeed, EXPECTED_CHAINLINK_FEED);
// Verify the feed returns sane values
(, int256 price,,,) = AggregatorV3Interface(newFeed).latestRoundData();
assertGt(price, 0);
}
CI
すべてのプロジェクトで、並列実行する最小限の CI パイプラインを用意してください(マトリックス戦略を使用)。最小限の構成として、次を推奨します。
solhint— スタイルとセキュリティルールをチェックする Solidity リンターforge build --sizes— コントラクトサイズがデプロイ上限の 24KB 未満であることを確認slitherまたはaderyn— 一般的な脆弱性パターンの静的解析- 完了したと思うコードをコミットする前に、必ず aderyn や slither をコードベースに対して実行し、出力を確認してください。警告からコードベースの問題が見つかることもあります。
- ファズ/不変条件テスト — Echidna、Medusa、または Foundry の不変条件テストを、適切な時間枠(ツールごとに約 10 分、マトリックスを使って並列実行)で実行
ツールの更新
Foundry
Foundry の依存関係をインストールする際、--no-commit フラグはもう必要ありません。