mcpField
S
solidity-security

solidity-security

github.com/ormus-solutionsセキュリティ12380 回インストール4 週間前に更新
リポジトリ

solidity-security について

スマートコントラクトの開発者や監査担当者向けに、Solidityのセキュリティを解説するスキルです。一般的な脆弱性への対策、安全なコーディングパターン、ガス最適化、専門監査の準備を扱います。

主な機能

  1. 01再入とアクセス制御への対策
  2. 02整数の安全性と入力検証
  3. 03フロントランニングの緩和策
  4. 04ガス最適化パターン
  5. 05セキュリティテストと監査チェックリスト

活用シーン

  1. 01コントラクトの一般的な脆弱性の確認
  2. 02DeFiコントラクトへの安全なパターンの適用
  3. 03監査に向けた文書とチェック項目の準備

インストール

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

$npx skills add https://github.com/ormus-solutions/libreuiux-claude-code --skill solidity-security

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

詳細

Solidity セキュリティ

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

このスキルを使用する場面

  • 安全なスマートコントラクトを作成する
  • 既存のコントラクトを監査して脆弱性を確認する
  • 安全な DeFi プロトコルを実装する
  • 再入可能性、オーバーフロー、アクセス制御の問題を防止する
  • セキュリティを維持しながらガス使用量を最適化する
  • 専門家による監査に向けてコントラクトを準備する
  • 一般的な攻撃ベクトルを理解する

重大な脆弱性

1. 再入可能性

攻撃者が、状態が更新される前にコントラクトを再度呼び出します。

脆弱なコード:

// VULNERABLE TO REENTRANCY
contract VulnerableBank {
    mapping(address => uint256) public balances;

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

        // DANGER: External call before state update
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success);

        balances[msg.sender] = 0;  // Too late!
    }
}

安全なパターン(Checks-Effects-Interactions):

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

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

        // EFFECTS: Update state BEFORE external call
        balances[msg.sender] = 0;

        // INTERACTIONS: External call last
        (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):

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

    function transfer(address to, uint256 amount) public {
        // No overflow check - can wrap around
        balances[msg.sender] -= amount;  // Can underflow!
        balances[to] += amount;          // Can overflow!
    }
}

安全なパターン(Solidity >= 0.8.0):

// Solidity 0.8+ has built-in overflow/underflow checks
contract SecureToken {
    mapping(address => uint256) public balances;

    function transfer(address to, uint256 amount) public {
        // Automatically reverts on overflow/underflow
        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. アクセス制御

脆弱なコード:

// VULNERABLE: Anyone can call critical functions
contract VulnerableContract {
    address public owner;

    function withdraw(uint256 amount) public {
        // No access control!
        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);
    }
}

// Or implement custom role-based access
contract RoleBasedContract {
    mapping(address => bool) public admins;

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

    function criticalFunction() public onlyAdmin {
        // Protected function
    }
}
4. フロントランニング

脆弱な例:

// VULNERABLE TO FRONT-RUNNING
contract VulnerableDEX {
    function swap(uint256 amount, uint256 minOutput) public {
        // Attacker sees this in mempool and front-runs
        uint256 output = calculateOutput(amount);
        require(output >= minOutput, "Slippage too high");
        // Perform swap
    }
}

緩和策:

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

    // Step 1: Commit to trade
    function commitTrade(bytes32 commitment) public {
        usedCommitments[commitment] = true;
    }

    // Step 2: Reveal trade (next block)
    function revealTrade(
        uint256 amount,
        uint256 minOutput,
        bytes32 secret
    ) public {
        bytes32 commitment = keccak256(abi.encodePacked(
            msg.sender, amount, minOutput, secret
        ));
        require(usedCommitments[commitment], "Invalid commitment");
        // Perform swap
    }
}

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

Checks-Effects-Interactions パターン
contract SecurePattern {
    mapping(address => uint256) public balances;

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

        // 2. EFFECTS: Update state
        balances[msg.sender] -= amount;

        // 3. INTERACTIONS: External calls last
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }
}
プッシュよりプルを優先するパターン
// Prefer this (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);
    }
}

// Over this (push)
contract RiskyPayment {
    function distributePayments(address[] memory recipients, uint256[] memory amounts) public {
        for (uint i = 0; i < recipients.length; i++) {
            // If any transfer fails, entire batch fails
            payable(recipients[i]).transfer(amounts[i]);
        }
    }
}
入力値の検証
contract SecureContract {
    function transfer(address to, uint256 amount) public {
        // Validate inputs
        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");

        // Proceed with transfer
        balances[msg.sender] -= amount;
        balances[to] += amount;
    }
}
緊急停止(サーキットブレーカー)
import "@openzeppelin/contracts/security/Pausable.sol";

contract EmergencyStop is Pausable, Ownable {
    function criticalFunction() public whenNotPaused {
        // Function logic
    }

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

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

ガスの最適化

小さい型ではなく uint256 を使用する
// More gas efficient
contract GasEfficient {
    uint256 public value;  // Optimal

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

// Less efficient
contract GasInefficient {
    uint8 public value;  // Still uses 256-bit slot

    function set(uint8 _value) public {
        value = _value;  // Extra gas for type conversion
    }
}
ストレージ変数をパックする
// Gas efficient (3 variables in 1 slot)
contract PackedStorage {
    uint128 public a;  // Slot 0
    uint64 public b;   // Slot 0
    uint64 public c;   // Slot 0
    uint256 public d;  // Slot 1
}

// Gas inefficient (each variable in separate slot)
contract UnpackedStorage {
    uint256 public a;  // Slot 0
    uint256 public b;  // Slot 1
    uint256 public c;  // Slot 2
    uint256 public d;  // Slot 3
}
関数の引数には memory ではなく calldata を使用する
contract GasOptimized {
    // More gas efficient
    function processData(uint256[] calldata data) public pure returns (uint256) {
        return data[0];
    }

    // Less efficient
    function processDataMemory(uint256[] memory data) public pure returns (uint256) {
        return data[0];
    }
}
データの保存にイベントを使用する(適切な場合)
contract EventStorage {
    // Emitting events is cheaper than storage
    event DataStored(address indexed user, uint256 indexed id, bytes data);

    function storeData(uint256 id, bytes calldata data) public {
        emit DataStored(msg.sender, id, data);
        // Don't store in contract storage unless needed
    }
}

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

// Security Checklist Contract
contract SecurityChecklist {
    /**
     * [ ] Reentrancy protection (ReentrancyGuard or CEI pattern)
     * [ ] Integer overflow/underflow (Solidity 0.8+ or SafeMath)
     * [ ] Access control (Ownable, roles, modifiers)
     * [ ] Input validation (require statements)
     * [ ] Front-running mitigation (commit-reveal if applicable)
     * [ ] Gas optimization (packed storage, calldata)
     * [ ] Emergency stop mechanism (Pausable)
     * [ ] Pull over push pattern for payments
     * [ ] No delegatecall to untrusted contracts
     * [ ] No tx.origin for authentication (use msg.sender)
     * [ ] Proper event emission
     * [ ] External calls at end of function
     * [ ] Check return values of external calls
     * [ ] No hardcoded addresses
     * [ ] Upgrade mechanism (if proxy pattern)
     */
}

セキュリティテスト

// Hardhat test example
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);

        // Deposit funds
        await bank.deposit({value: ethers.utils.parseEther("10")});

        // Attempt reentrancy attack
        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();

        // Attempt overflow
        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();

        // Attempt unauthorized withdrawal
        await expect(
            contract.connect(attacker).withdraw(100)
        ).to.be.revertedWith("Ownable: caller is not the owner");
    });
});

監査の準備

contract WellDocumentedContract {
    /**
     * @title Well Documented Contract
     * @dev Example of proper documentation for audits
     * @notice This contract handles user deposits and withdrawals
     */

    /// @notice Mapping of user balances
    mapping(address => uint256) public balances;

    /**
     * @dev Deposits ETH into the contract
     * @notice Anyone can deposit funds
     */
    function deposit() public payable {
        require(msg.value > 0, "Must send ETH");
        balances[msg.sender] += msg.value;
    }

    /**
     * @dev Withdraws user's balance
     * @notice Follows CEI pattern to prevent reentrancy
     * @param amount Amount to withdraw in 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. アップグレード手段がない:アップグレードが必要な場合は、プロキシパターンを検討します

コメント