mcpField
S
solidity

solidity

github.com/cyfrin開発144179 回インストール19 週間前に更新
リポジトリ

solidity について

Solidity Development Standardsは、CyfrinのセキュリティチームによるSolidityのコーディング、テスト、セキュリティ、デプロイ、ガバナンスの指針です。開発からリリースまで一貫した実践を求めるスマートコントラクト開発者を対象としています。

主な機能

  1. 01防御的なSolidityコーディング規約
  2. 02ファズテストと不変条件テストの指針
  3. 03鍵と管理者の安全な運用
  4. 04Foundryによるデプロイとガバナンスの手順
  5. 05CIでのlintと静的解析チェック

活用シーン

  1. 01Solidityのコードスタイルの統一
  2. 02コントラクトのファズ・不変条件テストの計画
  3. 03安全なコントラクトデプロイの準備
  4. 04スマートコントラクトプロジェクトのCI設定

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/cyfrin/solskill --skill solidity

インストール後、Claude Code で/solidityを入力して読み込まれたか確認します。

詳細

Solidity 開発標準

Cyfrin のセキュリティチームによる、Solidity コードの書き方に関する指針です。

基本方針

  • あらゆるものが攻撃されます — 書いたコードはすべて攻撃されると想定し、防御的に記述してください。

コードの品質とスタイル

  1. 絶対パスと名前付きインポートのみを使用し、相対パス(..)は使用しないでください。
// good
import {MyContract} from "contracts/MyContract.sol";

// bad
import "../MyContract.sol";
  1. require よりも revert を優先し、コントラクト名と2つのアンダースコアをプレフィックスとするカスタムエラーを使用してください。
error ContractName__MyError();

// Good
myBool = true;
if (myBool) {
    revert ContractName__MyError();
}

// bad
require(myBool, "MyError");
  1. テストでは、ユニットテストよりもステートレスなファズテストを優先してください。
// 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 で同じインバリアントテストスイートを実行してください。ファザーごとに、異なるバグが見つかります。

  1. 関数は可視性ごとにまとめ、次の順序で並べてください。
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)
  1. ヘッダーは次のようにしてください。
    /*//////////////////////////////////////////////////////////////
                      INTERNAL STATE-CHANGING FUNCTIONS
    //////////////////////////////////////////////////////////////*/
  1. ファイルの構成
Pragma statements
Import statements
Events
Errors
Interfaces
Libraries
Contracts

コントラクトの構成:

Type declarations
State variables
Events
Errors
Modifiers
Functions
  1. テストを作成する際は、分岐ツリー手法を使用してください。 この手法の功績は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);
}
  1. コントラクトでは厳密な pragma バージョンを、テスト、ライブラリ、抽象コントラクト、インターフェース、スクリプトでは浮動 pragma バージョンを優先してください。最低バージョンは 0.8.34 以降にしてください。バージョン 0.8.28 から 0.8.33 には、ストレージをクリアする際に IR パイプライン(--via-ir)が tstore の代わりに sstore(またはその逆)を出力し、誤ったストレージ領域に書き込む可能性がある重大度の高い一時ストレージのバグがあります。

  2. コントラクトの先頭にある natspec に、セキュリティ連絡先を追加してください。

/**
  * @custom:security-contact [email protected]
  * @custom:security-contact see https://mysite.com/ipfs-hash
  */  
  1. mainnet にデプロイする場合、またはデプロイしようとしている場合は、監査を受けるよう伝えてください。

  2. 秘密鍵を平文で記述することは、絶対に、決して、絶対にしないでください。唯一の例外は、anvil などのデフォルトキーを使用する場合で、その旨を明記する必要があります。

    • デプロイスクリプトも対象です。デプロイスクリプトでは常に forge script <path> --account $ACCOUNT --sender $SENDER を使用し、スクリプト内で vm.envUnit() を使用しないでください。
    • hardhat では、hardhat の暗号化キーストアを使用してください。
  3. ownable または管理者権限(onlyOwner など)を持つスマートコントラクトをデプロイする場合、最初のデプロイ時から管理者をマルチシグにしてください。デプロイヤー EOA を管理者にしてはいけません(許容される例外は testnet のみです)。Trail of Bits: 秘密鍵リスクを超えてスマートコントラクトを成熟させるを参照してください。「レイヤー 1」(単一の EOA)によるガバナンスは、DeFi ではもはや許容されません。

  4. 変数をデフォルト値で初期化しないでください。

// good
uint256 x;
bool y;

// bad
uint256 x = 0;
bool y = false;
  1. ローカル変数の宣言を省略できる場合は、名前付き戻り値変数の使用を優先してください。
// 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;
}
  1. 読み取り専用の関数入力では、memory よりも calldata を優先してください。

  2. 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) { }
  1. ストレージの読み取りにはコストがかかります。同一のストレージ読み取りを避けるため、変更されないストレージスロットをキャッシュし、キャッシュした値を渡すか使用してください。

  2. できるだけ早く revert してください。ストレージ読み取りや外部呼び出しを必要とするチェックより前に、入力チェックを実行してください。

  3. onlyOwner 関数内では、owner ではなく msg.sender を使用してください。

  4. ETH の送金には、Solidity の call() ではなく SafeTransferLib::safeTransferETH を使用してください。

  5. 入力変数の値を保持する必要がない場合は、追加のローカル変数を宣言するのではなく、入力変数を変更してください。

  6. nonReentrant 修飾子は、ほかの修飾子より前に使用してください。

  7. より高速な nonReentrant 修飾子には、ReentrancyGuardTransient を使用してください。pragma solidity ^0.8.34; が必要です。一時ストレージのクリアに関するバグがあるため、0.8.28~0.8.33 では使用しないでください。

  8. Ownable よりも Ownable2Step を優先してください。

  9. 必要なスロットが少数だけの場合、ストレージから構造体全体をメモリにコピーしないでください。

  10. 不要な「context」構造体を削除する、または context 構造体から不要な変数を削除してください。

  11. ストレージや構造体を宣言する際は、変数が最小数のストレージスロットに収まるよう、宣言の順序を調整してください。変数を頻繁にまとめて読み書きする場合は、可能であれば同じスロットにまとめてください。

  12. アップグレード不可のコントラクトでは、コンストラクターで一度だけ設定される変数を immutable として宣言してください。

  13. foundry.toml でオプティマイザーを有効にしてください。

  14. 修飾子が関数本体と同じストレージ読み取りを行う場合は、同一のストレージ読み取りを防ぐため、修飾子を内部関数にリファクタリングしてください。

  15. 平文の環境変数ではなく、Foundry の暗号化された安全な秘密鍵ストレージを使用してください。

  16. アップグレード:スマートコントラクトをアップグレードする際は、既存の変数の順序や型を変更したり、削除したりしないでください。ストレージの衝突につながる可能性があります。また、アップグレードのテストも作成してください。

デプロイ

本番環境へのデプロイとテストのセットアップの両方で、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 フラグはもう必要ありません。

コメント