solidity-security について
スマートコントラクトの開発者や監査担当者向けに、Solidityのセキュリティを解説するスキルです。一般的な脆弱性への対策、安全なコーディングパターン、ガス最適化、専門監査の準備を扱います。
主な機能
- 01再入とアクセス制御への対策
- 02整数の安全性と入力検証
- 03フロントランニングの緩和策
- 04ガス最適化パターン
- 05セキュリティテストと監査チェックリスト
活用シーン
- 01コントラクトの一般的な脆弱性の確認
- 02DeFiコントラクトへの安全なパターンの適用
- 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:自動セキュリティスキャナー
よくある落とし穴
- 認証に
tx.originを使用する:代わりにmsg.senderを使用します - 外部呼び出しの結果を確認しない:必ず戻り値を確認します
- 信頼できないコントラクトへの Delegatecall:コントラクトを乗っ取られる可能性があります
- 浮動 Pragma:Solidity のバージョンを特定のものに固定します
- イベントの欠落:状態の変更時にイベントを発行します
- ループ内での過剰なガス消費:ブロックのガス上限に達する可能性があります
- アップグレード手段がない:アップグレードが必要な場合は、プロキシパターンを検討します
コメント
Markdown 対応