Solidityで無限Mint攻撃を防ぐ方法

この記事の著者

無限ミント攻撃とは何か、その仕組みと危険性

無限ミント攻撃は、トークンコントラクトが攻撃者に対して正当な認証無しにトークンを生成させる、あるいは最大供給量を強制しない、あるいは両方の条件が満たされていない場合に発生します。攻撃者は任意の残高をミント(新規発行)し、その後市場やシステムが反応する前に、新たに生成されたトークンを売却、スワップ、預入、あるいは担保化します。

この攻撃の結果は単なるインフレの問題にとどまらず、トークンの交換価値の崩壊、流動性プールの枯渇、貸付や財務システム内の無担保担保資産の増加といった甚大な被害を引き起こす可能性があります。例として、Elrondは2022年6月5日に「Infinite Mint」と分類される事件で1億1300万ドルの損失を被った記録があり、発行のバウンダリーが破綻するとエコシステム全体に甚大な損失をもたらすことが明示されています。

無限ミント攻撃の仕組みとそのメカニズム

無限ミント攻撃は、「誰がどの程度ミントできるか」を信頼性の高い証明なしに定めるトークン発行経路を悪用します。一般公開、あるいは誤ったアクセス制御の下にある mint 関数は、攻撃者にとって合法的なERC-20トークンとして扱われる残高を作り出すことが可能となります。

mint 関数が安全であるためには、その呼び出し権限、発行条件、供給管理がプロトコルの経済設計と一致している必要があります。いずれかの呼び出し元が _mint に相当するロジックにアクセスできたり、特権を持つ呼び出し元が供給上限をバイパスできる場合、そのトークンの希少性は単なる仮定に過ぎません。

典型的な無限ミント攻撃の流れは以下のとおりです:

  1. 攻撃者が外部から呼び出せるミント経路や回避可能なアクセスチェックを特定。
  2. 攻撃者が自分のコントロールする受取人と金額を指定して関数を呼択。
  3. コントラクトが攻撃者の残高やtotalSupplyを増加させる。
  4. 攻撃者がミントしたトークンをDEXプールやレンディング市場、ブリッジ、またはバルクに移動。
  5. 偽造されたトークンを実資産と交換したり、担保として利用。
  6. 流動性提供者、貸し手、トークン保有者が結果的な経済的損失を被る。

トークン残高が他のコントラクトに受け入れられる際、これらのコントラクトが残高の生成方法を独立して確認しない場合、無限ミントの脆弱性は特に危険です。たとえば、貸付プロトコルは、意図された排出を利用したトークンと、アクセスコントロールの欠落によるトークンを区別できません。

Solidityにおいて無限ミントの脆弱性を引き起こすパターンは何か

無限ミントの脆弱性は、主に以下の要因から発生します:

  • 認証の欠如
  • 供給上限の欠如
  • 表面上は正しいが、意図しない管理経路を通じてアクセス権が得られる認証

残高を増加させるすべての関数(直接または間接的に)を見直すことが重要です。

最も明白なパターンは、呼び出し制限のない公開ミント関数です:

function mint(address to, uint256 amount) external {
    balanceOf[to] += amount;
    totalSupply += amount;
}

また、誤った身元確認もリスクを高めます。例えば、tx.originを認証に用いるのは避けるべきです。攻撃者は特権付与されたウォレットを不適切なコントラクトに呼ばせることが可能だからです。同様に、受取人が特権者かどうかを確認するだけでは、呼び出し元がトークンを発行する権利を持つ保証にはなりません。

その他の一般的な原因例は次のとおりです:

  • setMinter 関数が誰でも呼び出せてしまい、自分に発行権を付与できる。
  • アップグレード可能なプロキシがミント制限を置換可能。
  • ブリッジやクロスチェーンハンドラが任意のメッセージをミント命令として信頼。
  • あるミント関数の上限設定はあるが、報酬請求やマイグレーションの他のルートでは設定されていない。
  • 数学的ミスや単位変換ミス、名目上の上限を無効化する誤り。
  • バーンとミントの帳簿管理において、実際にバーンが検証されずに置き換えが行われる。

totalSupply のみを制御するのは不十分であり、実際にはすべての発行経路で totalSupply + amount が最大供給量を超えないようにしなければなりません。

脆弱なコード例と修正版の例

脆弱な無限ミント例

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

contract VulnerableInfiniteMintToken {
    string public constant name = "Vulnerable Token";
    string public constant symbol = "VULN";

    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;

    event Transfer(address indexed from, address indexed to, uint256 amount);

    function mint(address to, uint256 amount) external {
        // 脆弱性:誰もが任意のトークンを作成可能。
        totalSupply += amount;
        balanceOf[to] += amount;
        emit Transfer(address(0), to, amount);
    }

    function transfer(address to, uint256 amount) external returns (bool) {
        require(balanceOf[msg.sender] >= amount, "残高不足");

        unchecked {
            balanceOf[msg.sender] -= amount;
        }
        balanceOf[to] += amount;

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

修正版例

管理者と発行権者を分離し、最大供給量を制御することで安全性を確保します:

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

contract CappedRoleMintToken {
    string public constant name = "Capped Token";
    string public constant symbol = "CAP";

    address public immutable admin;
    uint256 public immutable maxSupply;
    uint256 public totalSupply;

    mapping(address => bool) public isMinter;
    mapping(address => uint256) public balanceOf;

    event Transfer(address indexed from, address indexed to, uint256 amount);
    event MinterUpdated(address indexed minter, bool allowed);

    modifier onlyAdmin() {
        require(msg.sender == admin, "管理者だけアクセス可能");
        _;
    }

    modifier onlyMinter() {
        require(isMinter[msg.sender], "発行権者ではない");
        _;
    }

    constructor(uint256 maxSupply_) {
        require(maxSupply_ > 0, "ゼロ容量は不可");
        admin = msg.sender;
        maxSupply = maxSupply_;
        isMinter[msg.sender] = true;
    }

    function setMinter(address minter, bool allowed) external onlyAdmin {
        isMinter[minter] = allowed;
        emit MinterUpdated(minter, allowed);
    }

    function mint(address to, uint256 amount) external onlyMinter {
        // 修正点:供給上限を超えない範囲でのみミントを許可
        require(totalSupply + amount <= maxSupply, "容量超過");
        totalSupply += amount;
        balanceOf[to] += amount;
        emit Transfer(address(0), to, amount);
    }

    function transfer(address to, uint256 amount) external returns (bool) {
        require(balanceOf[msg.sender] >= amount, "残高不足");

        unchecked {
            balanceOf[msg.sender] -= amount;
        }
        balanceOf[to] += amount;

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

リワードやブリッジ、マイグレーションなどを通じてミントを行う場合は、スマートコントラクト監査およびセキュリティレビューを実施し、各発行経路の認証と供給不変条件を精査してください。

Solidityにおける無限ミント攻撃を防ぐための対策

無限ミント攻撃を防ぐには、多層的な発行設計が必要です。明示的な権限設定、供給制約の強化、厳格な管理権限分離、動作検証(テスト)によって、他の経路からの不正な残高生成を排除します。供給が固定、上限付き、エミッションベース、担保付き、あるいはブリッジバックであるかに応じて、適切な設計を採用してください。

方法 防止できるリスク コスト(ガス、複雑さ) 制限事項
役割制御されたmint 不正な外部呼び出しによるミント ストレージ読取り 権限が侵害されれば内部で追加ミント可能
最大供給量の設定 設計された供給量超過のミント 一回の算術チェック 権限者が全容量を早期にミントすることを防止できない
管理者と発行者の役割分離 自己に権限追加をさせる不正 追加の役割管理コスト 管理者は重要な信頼境界のまま
マルチシグやタイムロック管理 重要変更に遅延を付与 運用の手間とガバナンスコスト 既存の不適切なミントロジックを修正しない
セントラル化された内部_mint経路 二次的発行関数での上限未設定 リファクタリング作業 すべての外部ルートをレビューする必要がある
ミント活動のイベント監視 不正発行の遅延検出 オフチェーン監視インフラ 既に行われた悪意あるミントを防止できない

固定供給トークンでは、発行はデプロイ時に一括して行い、その後のミント機能を排除するのが最も堅牢です。エミッション型トークンは、スケジュールを明確に定義し、その総量制限を内部発行関数に組み込み、外部呼び出しの認可を明確に分離する必要があります。

Sokenメソドロジーでは、ミントを資産生成の境界と位置付けており、あらゆる経路の権限モデル、明示的な経済的制限、そしてテスト可能な不変条件が必要です。

実例のインシデントと「無限ミント」として分類された事例

記録されている無限ミントの事例は、多くのチェーンを横断し、資産や新興プロトコルに対して発行の抜け穴が存在することを示しています。以下はDefiLlamaのハックデータベースに記録されたインシデント例です。

プロトコル 日付 損失額 チェーン
Elrond 2022-06-05 1億1300万ドル Elrond
Paid Network 2021-03-05 2,700万ドル Ethereum
Gala 2024-05-20 2,230万ドル Ethereum
Haven Protocol 2021-07-03 820万ドル Haven
OcelotDex 2024-09-20 490万ドル Ethereum
TSR 2026-06-01 250万ドル BSC
Raydium AMM 2026-06-10 130万ドル Solana
Flooring Protocol 2026-06-08 50万ドル Ethereum

これらの事例は、裏付けのないトークンがインテグレートされた市場に流入した時点で、攻撃はトークンコントラクトの範囲を越え、流動性プールや貸付市場、ブリッジやVaultにバランスを伝播させることを共通点としています。

したがって、検討すべき重大な問いは、「攻撃者がmintを呼び出せるか」ではなく、「信頼できない経路や過剰に信頼された経路を通じて、未裏付けのトークンを有効な資産として認識させることが可能か」となるでしょう。

デプロイ前に無限ミント脆弱性を検出する方法

発見には、すべての残高増加をもたらす状態遷移を洗い出し、その各経路が権限付与されたものであり、不変条件を満たすことを証明する必要があります。手動レビューはトークンコントラクトから始め、橋、Vault、報酬配布者、アップグレードモジュール、管理補助などまで広範囲に及びます。

以下のチェックリストを使用してください:

  1. 残高やシェア、供給カウンター、内部ミント関数へのすべての書き込みを確認。
  2. 各外部発行経路において、呼び出し権限が適切に設定されているか検証。
  3. すべての経路で同一の上限やエミッション制約を適用しているか確認。
  4. 発行者や管理者の追加・削除・上書き機能を検証。
  5. アップグレード、ブリッジ、マイグレーション、コールバック経路を追跡し、ミントロジックに到達可能か調査。
  6. 権限のあるアカウントが想定外に上限や発行者、会計単位を変更できるかどうか確認。
  7. ミントイベントとtotalSupplyが実際に残高に対応しているか確認。
  8. 押収担保や流動性、Vault資産として受け入れられる統合機能をテスト。

Slitherは、外部呼び出し関数、特権状態の変化、疑わしいアクセス制御パターンを洗い出すのに有用ですが、プロトコルの供給モデルの経済的正しさまでは保証しません。間接的なミント経路やコントラクト間の権限考慮には人間による判断が不可欠です。

Foundryのファジングを用いることで、任意の呼び出しがミントを行えないこと、および供給が常に上限を超えないことを検証できます。特に不変条件は、次のようにセキュリティの性質を直接表現できるため有効です。

function invariant_supplyNeverExceedsCap() public view {
    require(token.totalSupply() <= token.maxSupply(), "供給制限違反");
}

もう一つの不変条件は、「供給増加を許可できるのは承認されたアクターだけ」とするもので、mintだけでなくすべての公開関数に対してテストされるべきです。アクセス制御や資産管理、その他コントラクトリスクについての詳細はSoken Hubを参照してください。

セキュリティインサイト: Sokenメソドロジーでは、ミントの経路は資産生成の境界と位置付け、コントラクトが誰の認証に基づいてその資産を生成したかを証明できない場合、その経路はセキュリティ境界として再設計が必要です。

役割チェックと供給制限を適用すれば、無制限の発行を制約された操作に変換できます。ただし、すべてのミント経路がこれらの制御を使用していることが前提です。重要な工程は、残高生成を中央集権化し、バイパスを構造的に不可能にすることにあります。

ローンチやアップグレード前には、「意図した供給不変条件」を平易な言葉で記述し、共有のミントパスに実装し、Foundryの不変条件テストで確実に適用しましょう。

この記事の著者

よくある質問

Solidityでの無限Mint攻撃とは何ですか?

Solidityのトークン契約での無限Mint攻撃は、攻撃者が有効な承認なしにトークンを生成できる状態です。最大供給制限がなく、正当なERC-20トークンとして扱われる不正なバランスを作り出します。

無限Mint攻撃の仕組みは何ですか?

この攻撃は、誰がどれだけの供給をMintできるかを証明しないトークン発行経路を悪用します。公開や不適切に保護されたMint機能を利用して、正当なERC-20トークンとして扱われるバランスを作り出します。

無限Mint攻撃による被害は何ですか?

この攻撃は、制御不能なインフレを招き、トークンの交換価値を破壊します。新規Mintされたトークンは売買や担保に使用され、流動性プールの枯渇やレバレッジ取引の危険性を高めます。

Elrond無限Mint事件とは何ですか?

2022年6月5日にElrondが$113.0Mの損失を出した事件です。トークン発行の境界が破られると、市場や他のシステムが新たなトークンを受け入れ、大きな損失に繋がります。

Solidity開発者は無限Mint攻撃をどう防ぐべきですか?

確実に誰が何をMintできるか証明し、最大供給制限を設けて、すべてのMint経路に正当な承認を要求します。これにより、不正なトークン作成を防止できます。

チャット