modifier とは

modifier は、Solidityで関数の実行前後に条件チェックや共通処理を挿入する仕組みです。主にアクセス制御(「この関数はオーナーのみ呼び出せる」など)に使われますが、再入攻撃の防止や状態チェックなど、様々な用途に活用できます。

// 基本構文
modifier modifierName() {
    // 関数実行前の処理
    require(条件, "エラーメッセージ");
    _;  // ← ここで修飾対象の関数本体が実行される
    // 関数実行後の処理(必要な場合)
}

// 使い方
function someFunction() public modifierName {
    // 関数の本体
}

_(アンダースコア)は、修飾対象の関数本体が実行される位置を示す特別な記号です。

onlyOwner: 基本のアクセス制御

最もよく使われるmodifierは、コントラクトのオーナーのみに実行を許可する onlyOwner です。

// contracts/OwnableExample.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract OwnableExample {
    address public owner;
    string public data;

    event OwnershipTransferred(
        address indexed previousOwner,
        address indexed newOwner
    );

    constructor() {
        owner = msg.sender;
    }

    /// @notice オーナーのみ実行可能
    modifier onlyOwner() {
        require(msg.sender == owner, "Caller is not the owner");
        _;
    }

    /// @notice データを設定する(オーナーのみ)
    function setData(string memory _data) public onlyOwner {
        data = _data;
    }

    /// @notice オーナーを変更する(オーナーのみ)
    function transferOwnership(address newOwner) public onlyOwner {
        require(newOwner != address(0), "New owner is zero address");
        emit OwnershipTransferred(owner, newOwner);
        owner = newOwner;
    }
}

テスト

// test/OwnableExample.test.ts
import { expect } from "chai";
import { ethers } from "hardhat";

describe("OwnableExample", function () {
  async function deployFixture() {
    const [owner, other] = await ethers.getSigners();
    const Contract = await ethers.getContractFactory("OwnableExample");
    const contract = await Contract.deploy();
    return { contract, owner, other };
  }

  it("オーナーがデータを設定できる", async function () {
    const { contract } = await deployFixture();

    await contract.setData("テストデータ");
    expect(await contract.data()).to.equal("テストデータ");
  });

  it("オーナー以外はデータを設定できない", async function () {
    const { contract, other } = await deployFixture();

    await expect(
      contract.connect(other).setData("不正なデータ")
    ).to.be.revertedWith("Caller is not the owner");
  });

  it("オーナーを変更できる", async function () {
    const { contract, owner, other } = await deployFixture();

    await contract.transferOwnership(other.address);
    expect(await contract.owner()).to.equal(other.address);

    // 元のオーナーはもう操作できない
    await expect(
      contract.connect(owner).setData("もう無理")
    ).to.be.revertedWith("Caller is not the owner");

    // 新しいオーナーは操作できる
    await contract.connect(other).setData("新オーナーのデータ");
    expect(await contract.data()).to.equal("新オーナーのデータ");
  });
});

引数付きの modifier

modifierにパラメータを渡すこともできます。

// contracts/RoleBasedAccess.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract RoleBasedAccess {
    address public owner;

    // ロール管理
    mapping(bytes32 => mapping(address => bool)) private _roles;

    bytes32 public constant ADMIN_ROLE = keccak256("ADMIN");
    bytes32 public constant CURATOR_ROLE = keccak256("CURATOR");
    bytes32 public constant RESTORER_ROLE = keccak256("RESTORER");

    event RoleGranted(bytes32 indexed role, address indexed account);

    constructor() {
        owner = msg.sender;
        _roles[ADMIN_ROLE][msg.sender] = true;
    }

    /// @notice 特定のロールを要求するmodifier
    modifier onlyRole(bytes32 role) {
        require(
            _roles[role][msg.sender],
            "AccessControl: account is missing role"
        );
        _;
    }

    /// @notice ロールを付与する(管理者のみ)
    function grantRole(
        bytes32 role,
        address account
    ) public onlyRole(ADMIN_ROLE) {
        _roles[role][account] = true;
        emit RoleGranted(role, account);
    }

    /// @notice 学芸員のみが実行できる関数
    function curate(uint256 itemId)
        public
        onlyRole(CURATOR_ROLE)
        returns (string memory)
    {
        return "Curated successfully";
    }

    /// @notice 修復士のみが実行できる関数
    function restore(uint256 itemId)
        public
        onlyRole(RESTORER_ROLE)
        returns (string memory)
    {
        return "Restored successfully";
    }

    /// @notice ロールの確認
    function hasRole(bytes32 role, address account)
        public view returns (bool)
    {
        return _roles[role][account];
    }
}

複数の modifier を組み合わせる

ひとつの関数に複数のmodifierを適用できます。左から右の順に実行されます。

// contracts/MultiModifier.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract MultiModifier {
    address public owner;
    bool public paused;
    mapping(uint256 => bool) public itemExists;

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

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

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

    modifier validItem(uint256 itemId) {
        require(itemExists[itemId], "Item does not exist");
        _;
    }

    /// @notice 一時停止する(オーナーのみ)
    function pause() public onlyOwner {
        paused = true;
    }

    /// @notice 再開する(オーナーのみ)
    function unpause() public onlyOwner {
        paused = false;
    }

    /// @notice アイテムを登録する(オーナーのみ、非停止時のみ)
    function registerItem(uint256 itemId)
        public
        onlyOwner           // 1. オーナーチェック
        whenNotPaused       // 2. 停止中でないことを確認
    {
        itemExists[itemId] = true;
    }

    /// @notice アイテムを更新する(3つのmodifierを適用)
    function updateItem(uint256 itemId, string memory newData)
        public
        onlyOwner           // 1. オーナーチェック
        whenNotPaused       // 2. 停止中でないことを確認
        validItem(itemId)   // 3. アイテムの存在確認
    {
        // 更新処理
    }
}

require と revert

modifier内でよく使われる requirerevert について整理しましょう。

// require: 条件がfalseの場合にリバートする
require(条件, "エラーメッセージ");

// revert: 無条件にリバートする
revert("エラーメッセージ");

// カスタムエラー(Solidity 0.8.4以降、ガス効率が良い)
error Unauthorized(address caller);
error InsufficientBalance(uint256 requested, uint256 available);

function withdraw(uint256 amount) public {
    if (msg.sender != owner) {
        revert Unauthorized(msg.sender);
    }
    if (balances[msg.sender] < amount) {
        revert InsufficientBalance(amount, balances[msg.sender]);
    }
    // ...
}

カスタムエラーを使ったmodifier

// contracts/CustomErrors.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract CustomErrors {
    error NotOwner(address caller, address owner);
    error ContractPaused();
    error ItemNotFound(uint256 itemId);

    address public owner;
    bool public paused;

    constructor() {
        owner = msg.sender;
    }

    modifier onlyOwner() {
        if (msg.sender != owner) {
            revert NotOwner(msg.sender, owner);
        }
        _;
    }

    modifier whenNotPaused() {
        if (paused) {
            revert ContractPaused();
        }
        _;
    }
}

カスタムエラーのテスト

it("カスタムエラーが正しく発生する", async function () {
  const { contract, other } = await deployFixture();

  await expect(
    contract.connect(other).setData("test")
  )
    .to.be.revertedWithCustomError(contract, "NotOwner")
    .withArgs(other.address, owner.address);
});

OpenZeppelinの Ownable を使う

実際のプロジェクトでは、自分で onlyOwner を実装する代わりに、OpenZeppelinのライブラリを使うことが推奨されます。

npm install @openzeppelin/contracts
// contracts/UsingOZOwnable.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

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

contract UsingOZOwnable is Ownable {
    string public data;

    // Ownable(msg.sender) でオーナーを初期化
    constructor() Ownable(msg.sender) {}

    function setData(string memory _data) public onlyOwner {
        data = _data;
    }
}

OpenZeppelinの Ownable は、セキュリティ監査済みで広く使われているため、独自実装よりも安全です。

まとめ

本章では、Solidityの modifier を使ったアクセス制御について学びました。onlyOwner パターンから、引数付きmodifier、複数modifierの組み合わせ、カスタムエラーまでを実践しました。

modifierは、セキュリティに直結する重要な機能です。適切なアクセス制御がなければ、コントラクト内の資産が不正に操作される危険があります。OpenZeppelinなどの検証済みライブラリを活用することも重要です。

次章では、開発したコントラクトをSepoliaテストネットにデプロイする方法を学びます。

関連記事