Skip to content
Published on

スマートコントラクト開発完全ガイド2025: Solidity、Foundry、セキュリティパターン、DeFi実装

Authors

なぜスマートコントラクトなのか: プログラマブルマネーの革命

2016年のThe DAOハッキング事件(じけん)は、スマートコントラクトのセキュリティがいかに重要(じゅうよう)かを世界に刻みつけました。わずか数行(すうぎょう)のコード欠陥(けっかん)により360万ETH(当時約5,000万ドル、現在価値で約120億ドル)が盗まれ、Ethereumのハードフォーク(Ethereum Classicの誕生(たんじょう))を引き起こしました。この事件はスマートコントラクト開発のパラダイムを根本(こんぽん)から変えました。

2026年現在、スマートコントラクトはもはや実験的(じっけんてき)な技術ではありません。DeFi(分散型金融)のTVL(Total Value Locked)は2,000億ドルを超え、Uniswap、Aave、Compound、MakerDAOなどのプロトコルは数百万人のユーザーを抱えています。NFTはアート、ゲーム、アイデンティティ領域(りょういき)へと拡大(かくだい)し、現実(げんじつ)資産のトークン化も本格化(ほんかくか)しています。

スマートコントラクトの核心(かくしん)価値は3つあります。

第一に、プログラマブルマネー。お金がコードのルールに従って自律的(じりつてき)に動きます。「AがB USDを支払えば、Bに自動でトークンを送る」を仲介者(ちゅうかいしゃ)なしに強制できます。

第二に、不変性(ふへんせい)。デプロイされたコードは変更できません。これが信頼(しんらい)の基盤(きばん)です。

第三に、透明性(とうめいせい)。すべてのコードとトランザクションがブロックチェーンに永久的(えいきゅうてき)に記録されます。

しかしこの特性は諸刃(もろは)の剣(つるぎ)です。不変性はバグ修正(しゅうせい)の困難(こんなん)さを意味し、透明性は攻撃表面をそのまま公開し、プログラマブルマネーは一度のミスで数百万ドルが消えることを意味します。

したがってスマートコントラクト開発は通常のソフトウェア開発とはまったく異なる思考様式(しこうようしき)を要求(ようきゅう)します。「動けばよい」ではなく「すべての攻撃を想定(そうてい)し防御(ぼうぎょ)する」です。

EVMの基礎

EVMとは何か

Ethereum Virtual Machine(EVM)はスマートコントラクトが実行される状態機械(じょうたいきかい)です。世界中のEthereumノードは同じEVMを実行し、トランザクションが発生すると全ノードが同じ結果(けっか)を計算(けいさん)しなければなりません。これが分散化(ぶんさんか)の本質(ほんしつ)です。

EVMの核心的特徴(とくちょう):

  • スタックマシン: レジスタがなく、256ビットのスタックを使用します。最大1024スロット。
  • 256ビットワード: 32バイトが基本単位(きほんたんい)。これはkeccak256の出力(しゅつりょく)サイズと一致(いっち)し、uint256が最も自然(しぜん)な理由(りゆう)です。
  • 決定論的実行(けっていろんてきじっこう): 同じ入力に対して常に同じ出力。乱数(らんすう)、時刻(じこく)、外部API呼び出しは不可能(ふかのう)。

データ配置(はいち): Storage、Memory、Calldata、Stack

位置寿命(じゅみょう)ガスコスト用途
Storage永久非常(ひじょう)に高いコントラクト状態変数
Memoryトランザクション安い関数内一時データ
Calldataトランザクション最も安い外部関数引数
Stack実行中無料(1024上限)ローカル変数
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract DataLocationDemo {
    uint256[] private _storedArray;

    function processMemory(uint256[] memory data) public pure returns (uint256) {
        data[0] = 999;
        return data[0];
    }

    function processCalldata(uint256[] calldata data) external pure returns (uint256) {
        return data[0];
    }

    function storeArray(uint256[] calldata data) external {
        _storedArray = data;
    }
}

原則: 外部関数の配列(はいれつ)/構造体(こうぞうたい)引数は常にcalldataを使ってください。

ガスモデル

EVMのすべてのOPCODEはgasという単位(たんい)のコストを持ちます。これは2つの理由からです: 攻撃防止(ぼうし)と資源料金(しげんりょうきん)。

主要OPCODEコスト:

  • ADD, SUB: 3 gas
  • MUL, DIV: 5 gas
  • SLOAD: 2,100 (cold) / 100 (warm)
  • SSTORE: 20,000 / 5,000
  • CALL: 2,600 (cold) + extras
  • CREATE: 32,000

Storage操作(そうさ)は演算(えんざん)より数百倍高いという直観(ちょっかん)を必ず持ってください。

Solidity言語(げんご): 核心文法

型システム

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract TypeShowcase {
    uint256 public unsignedInt;
    int256 public signedInt;
    uint8 public smallInt;

    bool public flag;
    address public owner;
    address payable public recipient;

    bytes32 public hash;
    bytes4 public selector;

    bytes public dynamicBytes;
    string public name;

    uint256[] public dynamicArray;
    uint256[10] public fixedArray;

    mapping(address => uint256) public balances;
    mapping(address => mapping(address => uint256)) public allowances;

    struct User {
        uint256 id;
        string name;
        uint256 balance;
        bool active;
    }

    mapping(address => User) public users;

    enum Status { Pending, Active, Completed, Cancelled }
    Status public currentStatus;
}

関数、可視性(かしせい)、状態変更

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract FunctionDemo {
    uint256 private _counter;
    uint256 public publicValue;

    function incrementExternal() external {
        _counter += 1;
    }

    function _internalHelper() internal view returns (uint256) {
        return _counter * 2;
    }

    function getCounter() external view returns (uint256) {
        return _counter;
    }

    function add(uint256 a, uint256 b) external pure returns (uint256) {
        return a + b;
    }

    function deposit() external payable {}
}

イベント

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract EventDemo {
    event Transfer(address indexed from, address indexed to, uint256 value);

    mapping(address => uint256) private _balances;

    function transfer(address to, uint256 amount) external {
        require(_balances[msg.sender] >= amount, "Insufficient balance");
        _balances[msg.sender] -= amount;
        _balances[to] += amount;

        emit Transfer(msg.sender, to, amount);
    }
}

Modifier

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract ModifierDemo {
    address public owner;
    bool public paused;

    constructor() {
        owner = msg.sender;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        _;
    }

    modifier whenNotPaused() {
        require(!paused, "Contract paused");
        _;
    }

    function pause() external onlyOwner whenNotPaused {
        paused = true;
    }
}

Foundry深掘(ふかぼ)り

なぜFoundryか

Foundryは2021年にParadigmが公開したRustベースのSolidity開発ツールです。Hardhat/Truffleに比べて次の決定的(けっていてき)な長所(ちょうしょ)があります:

  1. 速度(そくど): Hardhat比10-100倍高速なテスト実行
  2. Solidityでテスト作成: JavaScript文脈切替(ぶんみゃくきりかえ)不要
  3. 強力なファズテスト: 無作為入力(むさくいにゅうりょく)でエッジケース発見(はっけん)
  4. castツール: コントラクト対話用CLI
  5. anvilローカルノード: メインネットフォーク可能

2026年基準で、新規プロジェクトの70%以上がFoundryを選択しています。

インストールとプロジェクト作成

curl -L https://foundry.paradigm.xyz | bash
foundryup

forge init my-project
cd my-project

foundry.toml設定(せってい)

[profile.default]
src = "src"
out = "out"
libs = ["lib"]
solc = "0.8.24"
optimizer = true
optimizer_runs = 200

[profile.ci]
fuzz = { runs = 10000 }
invariant = { runs = 1000 }

[rpc_endpoints]
mainnet = "${MAINNET_RPC_URL}"
sepolia = "${SEPOLIA_RPC_URL}"

forge test基本

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {Test, console} from "forge-std/Test.sol";
import {Counter} from "../src/Counter.sol";

contract CounterTest is Test {
    Counter public counter;
    address alice = address(0x1);

    function setUp() public {
        counter = new Counter();
        vm.deal(alice, 10 ether);
    }

    function test_Increment() public {
        counter.increment();
        assertEq(counter.number(), 1);
    }

    function test_OnlyOwner() public {
        vm.prank(alice);
        vm.expectRevert("Not owner");
        counter.setNumber(100);
    }
}

実行:

forge test
forge test --match-test test_Increment
forge test -vvv
forge test --gas-report

Fuzz Testing

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {Test} from "forge-std/Test.sol";
import {Counter} from "../src/Counter.sol";

contract CounterFuzzTest is Test {
    Counter public counter;

    function setUp() public {
        counter = new Counter();
    }

    function testFuzz_SetNumber(uint256 x) public {
        counter.setNumber(x);
        assertEq(counter.number(), x);
    }

    function testFuzz_IncrementBounded(uint8 times) public {
        vm.assume(times > 0 && times < 100);
        for (uint256 i = 0; i < times; i++) {
            counter.increment();
        }
        assertEq(counter.number(), times);
    }
}

Invariant Testing

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {Test} from "forge-std/Test.sol";
import {MyToken} from "../src/MyToken.sol";

contract TokenInvariantTest is Test {
    MyToken public token;

    function setUp() public {
        token = new MyToken("Test", "TST", 1_000_000 ether);
    }

    function invariant_TotalSupplyConstant() public view {
        assertEq(token.totalSupply(), 1_000_000 ether);
    }
}

castとanvil

cast call 0xContract "balanceOf(address)(uint256)" 0xUser
cast send 0xContract "transfer(address,uint256)" 0xTo 100 --private-key $PK --rpc-url $RPC
cast to-wei 1 ether

anvil
anvil --fork-url https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY

OpenZeppelin Contracts

ERC-20トークン

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

contract MyToken is ERC20, Ownable {
    constructor(
        string memory name,
        string memory symbol,
        uint256 initialSupply
    ) ERC20(name, symbol) Ownable(msg.sender) {
        _mint(msg.sender, initialSupply);
    }

    function mint(address to, uint256 amount) external onlyOwner {
        _mint(to, amount);
    }

    function burn(uint256 amount) external {
        _burn(msg.sender, amount);
    }
}

ERC-721 NFT

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {ERC721URIStorage} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

contract MyNFT is ERC721, ERC721URIStorage, Ownable {
    uint256 private _nextTokenId;

    constructor() ERC721("MyNFT", "MNFT") Ownable(msg.sender) {}

    function safeMint(address to, string memory uri) public onlyOwner {
        uint256 tokenId = _nextTokenId++;
        _safeMint(to, tokenId);
        _setTokenURI(tokenId, uri);
    }

    function tokenURI(uint256 tokenId)
        public view override(ERC721, ERC721URIStorage) returns (string memory)
    {
        return super.tokenURI(tokenId);
    }

    function supportsInterface(bytes4 interfaceId)
        public view override(ERC721, ERC721URIStorage) returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
}

AccessControl

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract RoleBasedToken is ERC20, AccessControl {
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");

    constructor() ERC20("RBT", "RBT") {
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _grantRole(MINTER_ROLE, msg.sender);
    }

    function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
        _mint(to, amount);
    }
}

ReentrancyGuard

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

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

    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }

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

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

セキュリティパターン

1. Reentrancy(再入)攻撃

// 脆弱(ぜいじゃく)
contract VulnerableVault {
    mapping(address => uint256) public balances;

    function withdraw() external {
        uint256 amount = balances[msg.sender];
        require(amount > 0);

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

        balances[msg.sender] = 0;
    }
}

// 安全
contract SafeVault {
    mapping(address => uint256) public balances;

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

        balances[msg.sender] = 0;

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

Checks-Effects-Interactionsパターンを守らないと、2016年The DAOハッキング、2022年Fei Rariハッキング(8,000万ドル)、2023年Euler Financeハッキング(1億9,700万ドル)のような事態が発生します。

2. 整数(せいすう)オーバーフロー

Solidity 0.8.0以降は基本的にoverflowがrevertを発生させます。uncheckedブロックでは依然(いぜん)として可能(かのう)なので注意が必要です。

pragma solidity ^0.8.24;

contract OverflowDemo {
    function safeAdd(uint256 a, uint256 b) external pure returns (uint256) {
        return a + b;
    }

    function uncheckedAdd(uint256 a, uint256 b) external pure returns (uint256) {
        unchecked {
            return a + b;
        }
    }
}

3. Front-Running(MEV)

ブロックチェーンのトランザクションはmempoolに公開(こうかい)されます。攻撃者はこれを見て自身のトランザクションを前に挟(はさ)み込(こ)めます(サンドイッチ攻撃)。

防御(ぼうぎょ):

  • Commit-Revealパターン
  • Private mempool (Flashbots Protect)
  • スリッページ制限
  • Uniswap V4フックなどプロトコルレベル防御

4. オラクル操作(そうさ)

DeFiプロトコルは外部価格データを参照(さんしょう)することが多いです。攻撃者がこの価格を操作できれば、借り入れを利用して資産を抜き取れます。防御はChainlinkなどの分散オラクル使用、TWAP(時間加重平均価格)使用、複数ソース結合です。

5. 未チェック外部呼び出し

// 脆弱
function unsafeTransfer(address to, uint256 amount) external {
    payable(to).call{value: amount}("");
}

// 安全
function safeTransfer(address to, uint256 amount) external {
    (bool success, ) = payable(to).call{value: amount}("");
    require(success, "Transfer failed");
}

ガス最適化技法

1. Storage Packing

contract Unpacked {
    uint256 a;
    uint256 b;
    uint256 c;
}

contract Packed {
    uint128 a;
    uint64 b;
    uint64 c;
}

2. immutableとconstant

contract ConstantsDemo {
    uint256 public constant MAX_SUPPLY = 1_000_000;
    address public immutable owner;

    constructor() {
        owner = msg.sender;
    }
}

3. Custom Errors

contract ErrorDemo {
    error ValueMustBePositive();
    error InsufficientBalance(uint256 available, uint256 required);

    function newWay(uint256 x) external pure {
        if (x == 0) revert ValueMustBePositive();
    }

    function withdraw(uint256 amount, uint256 balance) external pure {
        if (balance < amount) {
            revert InsufficientBalance(balance, amount);
        }
    }
}

Upgradeable Contracts

UUPSパターン

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

contract MyTokenV1 is Initializable, OwnableUpgradeable, UUPSUpgradeable {
    uint256 public value;

    function initialize() public initializer {
        __Ownable_init(msg.sender);
        __UUPSUpgradeable_init();
    }

    function setValue(uint256 _value) external {
        value = _value;
    }

    function _authorizeUpgrade(address) internal override onlyOwner {}
}

シンプルなAMM実装

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract SimpleAMM {
    IERC20 public immutable tokenA;
    IERC20 public immutable tokenB;

    uint256 public reserveA;
    uint256 public reserveB;

    constructor(address _tokenA, address _tokenB) {
        tokenA = IERC20(_tokenA);
        tokenB = IERC20(_tokenB);
    }

    function addLiquidity(uint256 amountA, uint256 amountB) external {
        tokenA.transferFrom(msg.sender, address(this), amountA);
        tokenB.transferFrom(msg.sender, address(this), amountB);

        reserveA += amountA;
        reserveB += amountB;
    }

    function swap(uint256 amountAIn) external returns (uint256 amountBOut) {
        require(amountAIn > 0, "Invalid input");

        uint256 amountAInWithFee = (amountAIn * 997) / 1000;
        amountBOut = (reserveB * amountAInWithFee) / (reserveA + amountAInWithFee);

        tokenA.transferFrom(msg.sender, address(this), amountAIn);
        tokenB.transfer(msg.sender, amountBOut);

        reserveA += amountAIn;
        reserveB -= amountBOut;
    }
}

監査(かんさ)ツール

  • Slither: 静的分析(せいてきぶんせき)
  • Mythril: シンボリック実行
  • Echidna: Foundryを超える強力なファズテスター

デプロイワークフロー

  1. ローカル開発(anvil + forge test)
  2. テストネットデプロイ(Sepolia、Holesky)
  3. 監査(Trail of Bits、OpenZeppelin、Consensys Diligence)
  4. バグバウンティ(Immunefi)
  5. メインネットデプロイ
  6. モニタリング(Forta、Tenderly)
forge script script/Deploy.s.sol \
  --rpc-url sepolia \
  --broadcast \
  --verify

クイズ

Q1. Checks-Effects-Interactionsパターンはなぜ重要ですか?

外部呼び出し(call/transfer)の後に状態を更新すると、受け手のコントラクトが再度元の関数を呼び出せます(再入)。このとき残高がまだ差し引かれていなければ複数回の引き出しが可能です。状態を先に更新し外部呼び出しを最後にすればこの攻撃を防止できます。

Q2. calldatamemoryの違いは?

calldataはトランザクション入力データを指し、読み取り専用で最も安価です。memoryは関数内の一時領域で修正可能ですがコピーコストがあります。外部関数引数として配列/構造体を受ける際、修正が不要なら常にcalldataを使ってください。

Q3. Solidity 0.8.0以降もuncheckedブロックを使う理由は?

ガス節約のためです。0.8.0以降は基本的にすべての算術演算にオーバーフロー検査が入りますが、数学的にオーバーフローが不可能な場合(例: ループカウンタ)はuncheckedで検査を省略しガスを削減できます。ただし安全性が証明されている場合のみ使用するべきです。

Q4. UUPSプロキシの長所は?

UUPSはアップグレードロジックがロジックコントラクト内にあり、プロキシ自体がより軽量でガスコストが低くなります。ただしロジックコントラクトがアップグレード関数を失うと永久的にアップグレード不可能になるリスクがあります。

Q5. Fuzz TestingとInvariant Testingの違いは?

Fuzz Testingは関数一つをさまざまな無作為入力でテストします。Invariant Testingは複数の関数を無作為順序で呼びながらも常に真でなければならない性質(例: totalSupplyは変わらない)を検証します。DeFiプロトコルではInvariant Testingが遥かに強力な保証を提供します。

参考資料

  1. Solidity Documentation
  2. Foundry Book
  3. OpenZeppelin Contracts
  4. Ethereum.org - Smart Contracts
  5. Consensys Smart Contract Best Practices
  6. SWC Registry
  7. Trail of Bits - Building Secure Contracts
  8. Slither
  9. Echidna
  10. Immunefi
  11. The DAO Hack Post-Mortem
  12. Uniswap V2 Core
  13. Paradigm Research
  14. Rekt.news