Claude Code Plugins

Community-maintained marketplace

Feedback

solidity-security

@amurata/cc-tools
2
0

一般的な脆弱性を防ぎ、セキュアなSolidityパターンを実装するためのスマートコントラクトセキュリティのベストプラクティスをマスターします。スマートコントラクトを書く時、既存コントラクトを監査する時、またはブロックチェーンアプリケーションのセキュリティ対策を実装する時に使用してください。

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name solidity-security
description 一般的な脆弱性を防ぎ、セキュアなSolidityパターンを実装するためのスマートコントラクトセキュリティのベストプラクティスをマスターします。スマートコントラクトを書く時、既存コントラクトを監査する時、またはブロックチェーンアプリケーションのセキュリティ対策を実装する時に使用してください。

English | 日本語

Solidityセキュリティ

スマートコントラクトセキュリティのベストプラクティス、脆弱性防止、セキュアなSolidity開発パターンをマスターします。

このスキルを使用するタイミング

  • セキュアなスマートコントラクトを書く
  • 脆弱性のために既存コントラクトを監査する
  • セキュアなDeFiプロトコルを実装する
  • 再入攻撃、オーバーフロー、アクセス制御の問題を防ぐ
  • セキュリティを維持しながらガス使用量を最適化する
  • プロフェッショナル監査のためのコントラクトを準備する
  • 一般的な攻撃ベクトルを理解する

重大な脆弱性

1. 再入攻撃(Reentrancy)

攻撃者が状態が更新される前にあなたのコントラクトにコールバックします。

脆弱なコード:

// 再入攻撃に対して脆弱
contract VulnerableBank {
    mapping(address => uint256) public balances;

    function withdraw() public {
        uint256 amount = balances[msg.sender];

        // 危険: 状態更新前の外部呼び出し
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success);

        balances[msg.sender] = 0;  // 遅すぎる!
    }
}

セキュアなパターン(Checks-Effects-Interactions):

contract SecureBank {
    mapping(address => uint256) public balances;

    function withdraw() public {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "Insufficient balance");

        // EFFECTS: 外部呼び出し前に状態を更新
        balances[msg.sender] = 0;

        // INTERACTIONS: 外部呼び出しは最後
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }
}

代替案: ReentrancyGuard

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract SecureBank is ReentrancyGuard {
    mapping(address => uint256) public balances;

    function withdraw() public nonReentrant {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "Insufficient balance");

        balances[msg.sender] = 0;

        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }
}

2. 整数オーバーフロー/アンダーフロー

脆弱なコード(Solidity < 0.8.0):

// 脆弱
contract VulnerableToken {
    mapping(address => uint256) public balances;

    function transfer(address to, uint256 amount) public {
        // オーバーフローチェックなし - ラップアラウンド可能
        balances[msg.sender] -= amount;  // アンダーフロー可能!
        balances[to] += amount;          // オーバーフロー可能!
    }
}

セキュアなパターン(Solidity >= 0.8.0):

// Solidity 0.8以降はオーバーフロー/アンダーフローチェックが組み込み
contract SecureToken {
    mapping(address => uint256) public balances;

    function transfer(address to, uint256 amount) public {
        // オーバーフロー/アンダーフローで自動的にrevert
        balances[msg.sender] -= amount;
        balances[to] += amount;
    }
}

Solidity < 0.8.0の場合、SafeMathを使用:

import "@openzeppelin/contracts/utils/math/SafeMath.sol";

contract SecureToken {
    using SafeMath for uint256;
    mapping(address => uint256) public balances;

    function transfer(address to, uint256 amount) public {
        balances[msg.sender] = balances[msg.sender].sub(amount);
        balances[to] = balances[to].add(amount);
    }
}

3. アクセス制御

脆弱なコード:

// 脆弱: 誰でも重要な機能を呼び出せる
contract VulnerableContract {
    address public owner;

    function withdraw(uint256 amount) public {
        // アクセス制御なし!
        payable(msg.sender).transfer(amount);
    }
}

セキュアなパターン:

import "@openzeppelin/contracts/access/Ownable.sol";

contract SecureContract is Ownable {
    function withdraw(uint256 amount) public onlyOwner {
        payable(owner()).transfer(amount);
    }
}

// またはカスタムロールベースアクセスを実装
contract RoleBasedContract {
    mapping(address => bool) public admins;

    modifier onlyAdmin() {
        require(admins[msg.sender], "Not an admin");
        _;
    }

    function criticalFunction() public onlyAdmin {
        // 保護された機能
    }
}

4. フロントランニング

脆弱:

// フロントランニングに対して脆弱
contract VulnerableDEX {
    function swap(uint256 amount, uint256 minOutput) public {
        // 攻撃者がmempoolでこれを見てフロントランニング
        uint256 output = calculateOutput(amount);
        require(output >= minOutput, "Slippage too high");
        // スワップを実行
    }
}

緩和策:

contract SecureDEX {
    mapping(bytes32 => bool) public usedCommitments;

    // ステップ1: トレードにコミット
    function commitTrade(bytes32 commitment) public {
        usedCommitments[commitment] = true;
    }

    // ステップ2: トレードを明らかにする(次のブロック)
    function revealTrade(
        uint256 amount,
        uint256 minOutput,
        bytes32 secret
    ) public {
        bytes32 commitment = keccak256(abi.encodePacked(
            msg.sender, amount, minOutput, secret
        ));
        require(usedCommitments[commitment], "Invalid commitment");
        // スワップを実行
    }
}

セキュリティのベストプラクティス

Checks-Effects-Interactionsパターン

contract SecurePattern {
    mapping(address => uint256) public balances;

    function withdraw(uint256 amount) public {
        // 1. CHECKS: 条件を検証
        require(amount <= balances[msg.sender], "Insufficient balance");
        require(amount > 0, "Amount must be positive");

        // 2. EFFECTS: 状態を更新
        balances[msg.sender] -= amount;

        // 3. INTERACTIONS: 外部呼び出しは最後
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }
}

Pull Over Pushパターン

// これを推奨(pull)
contract SecurePayment {
    mapping(address => uint256) public pendingWithdrawals;

    function recordPayment(address recipient, uint256 amount) internal {
        pendingWithdrawals[recipient] += amount;
    }

    function withdraw() public {
        uint256 amount = pendingWithdrawals[msg.sender];
        require(amount > 0, "Nothing to withdraw");

        pendingWithdrawals[msg.sender] = 0;
        payable(msg.sender).transfer(amount);
    }
}

// これよりも(push)
contract RiskyPayment {
    function distributePayments(address[] memory recipients, uint256[] memory amounts) public {
        for (uint i = 0; i < recipients.length; i++) {
            // 転送が失敗するとバッチ全体が失敗
            payable(recipients[i]).transfer(amounts[i]);
        }
    }
}

入力検証

contract SecureContract {
    function transfer(address to, uint256 amount) public {
        // 入力を検証
        require(to != address(0), "Invalid recipient");
        require(to != address(this), "Cannot send to contract");
        require(amount > 0, "Amount must be positive");
        require(amount <= balances[msg.sender], "Insufficient balance");

        // 転送を実行
        balances[msg.sender] -= amount;
        balances[to] += amount;
    }
}

緊急停止(サーキットブレーカー)

import "@openzeppelin/contracts/security/Pausable.sol";

contract EmergencyStop is Pausable, Ownable {
    function criticalFunction() public whenNotPaused {
        // 機能のロジック
    }

    function emergencyStop() public onlyOwner {
        _pause();
    }

    function resume() public onlyOwner {
        _unpause();
    }
}

ガス最適化

より小さな型の代わりにuint256を使用

// ガス効率が良い
contract GasEfficient {
    uint256 public value;  // 最適

    function set(uint256 _value) public {
        value = _value;
    }
}

// 非効率
contract GasInefficient {
    uint8 public value;  // 依然として256ビットスロットを使用

    function set(uint8 _value) public {
        value = _value;  // 型変換のための追加ガス
    }
}

ストレージ変数をパック

// ガス効率的(1スロットに3変数)
contract PackedStorage {
    uint128 public a;  // スロット0
    uint64 public b;   // スロット0
    uint64 public c;   // スロット0
    uint256 public d;  // スロット1
}

// ガス非効率(各変数が別スロット)
contract UnpackedStorage {
    uint256 public a;  // スロット0
    uint256 public b;  // スロット1
    uint256 public c;  // スロット2
    uint256 public d;  // スロット3
}

関数引数にmemoryの代わりにcalldataを使用

contract GasOptimized {
    // よりガス効率的
    function processData(uint256[] calldata data) public pure returns (uint256) {
        return data[0];
    }

    // 非効率
    function processDataMemory(uint256[] memory data) public pure returns (uint256) {
        return data[0];
    }
}

データストレージにイベントを使用(適切な場合)

contract EventStorage {
    // イベント発行はストレージより安価
    event DataStored(address indexed user, uint256 indexed id, bytes data);

    function storeData(uint256 id, bytes calldata data) public {
        emit DataStored(msg.sender, id, data);
        // 必要でない限りコントラクトストレージに保存しない
    }
}

一般的な脆弱性チェックリスト

// セキュリティチェックリストコントラクト
contract SecurityChecklist {
    /**
     * [ ] 再入攻撃防止(ReentrancyGuardまたはCEIパターン)
     * [ ] 整数オーバーフロー/アンダーフロー(Solidity 0.8以降またはSafeMath)
     * [ ] アクセス制御(Ownable、ロール、modifier)
     * [ ] 入力検証(requireステートメント)
     * [ ] フロントランニング緩和(該当する場合はcommit-reveal)
     * [ ] ガス最適化(パックドストレージ、calldata)
     * [ ] 緊急停止メカニズム(Pausable)
     * [ ] 支払いにpull over pushパターン
     * [ ] 信頼されていないコントラクトへのdelegatecallなし
     * [ ] 認証にtx.originを使用しない(msg.senderを使用)
     * [ ] 適切なイベント発行
     * [ ] 関数の最後に外部呼び出し
     * [ ] 外部呼び出しの戻り値をチェック
     * [ ] ハードコードされたアドレスなし
     * [ ] アップグレードメカニズム(プロキシパターンの場合)
     */
}

セキュリティのためのテスト

// Hardhatテスト例
const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("Security Tests", function () {
    it("Should prevent reentrancy attack", async function () {
        const [attacker] = await ethers.getSigners();

        const VictimBank = await ethers.getContractFactory("SecureBank");
        const bank = await VictimBank.deploy();

        const Attacker = await ethers.getContractFactory("ReentrancyAttacker");
        const attackerContract = await Attacker.deploy(bank.address);

        // 資金を預金
        await bank.deposit({value: ethers.utils.parseEther("10")});

        // 再入攻撃を試行
        await expect(
            attackerContract.attack({value: ethers.utils.parseEther("1")})
        ).to.be.revertedWith("ReentrancyGuard: reentrant call");
    });

    it("Should prevent integer overflow", async function () {
        const Token = await ethers.getContractFactory("SecureToken");
        const token = await Token.deploy();

        // オーバーフローを試行
        await expect(
            token.transfer(attacker.address, ethers.constants.MaxUint256)
        ).to.be.reverted;
    });

    it("Should enforce access control", async function () {
        const [owner, attacker] = await ethers.getSigners();

        const Contract = await ethers.getContractFactory("SecureContract");
        const contract = await Contract.deploy();

        // 不正な出金を試行
        await expect(
            contract.connect(attacker).withdraw(100)
        ).to.be.revertedWith("Ownable: caller is not the owner");
    });
});

監査準備

contract WellDocumentedContract {
    /**
     * @title Well Documented Contract
     * @dev 監査のための適切なドキュメントの例
     * @notice このコントラクトはユーザーの預金と出金を処理します
     */

    /// @notice ユーザー残高のマッピング
    mapping(address => uint256) public balances;

    /**
     * @dev コントラクトにETHを預金
     * @notice 誰でも資金を預けられます
     */
    function deposit() public payable {
        require(msg.value > 0, "Must send ETH");
        balances[msg.sender] += msg.value;
    }

    /**
     * @dev ユーザーの残高を出金
     * @notice 再入攻撃を防ぐためにCEIパターンに従います
     * @param amount 出金するwei単位の金額
     */
    function withdraw(uint256 amount) public {
        // CHECKS
        require(amount <= balances[msg.sender], "Insufficient balance");

        // EFFECTS
        balances[msg.sender] -= amount;

        // INTERACTIONS
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }
}

リソース

  • references/reentrancy.md: 包括的な再入攻撃防止
  • references/access-control.md: ロールベースアクセスパターン
  • references/overflow-underflow.md: SafeMathと整数安全性
  • references/gas-optimization.md: ガス節約テクニック
  • references/vulnerability-patterns.md: 一般的な脆弱性カタログ
  • assets/solidity-contracts-templates.sol: セキュアなコントラクトテンプレート
  • assets/security-checklist.md: 監査前チェックリスト
  • scripts/analyze-contract.sh: 静的解析ツール

セキュリティ分析ツール

  • Slither: 静的解析ツール
  • Mythril: セキュリティ分析ツール
  • Echidna: ファズテストツール
  • Manticore: シンボリック実行
  • Securify: 自動セキュリティスキャナー

一般的な落とし穴

  1. 認証にtx.originを使用: 代わりにmsg.senderを使用
  2. チェックされていない外部呼び出し: 常に戻り値をチェック
  3. 信頼されていないコントラクトへのDelegatecall: コントラクトを乗っ取られる可能性
  4. 浮動Pragma: 特定のSolidityバージョンに固定
  5. イベントの欠如: 状態変更のためにイベントを発行
  6. ループ内の過剰なガス: ブロックガスリミットに達する可能性
  7. アップグレードパスなし: 必要であればプロキシパターンを検討