過去1週間(2026年10月19日–2026年10月25日)に、本レポートでは、推定損失額が合計で約557万 USDに上るセキュリティインシデント2件を追跡しました。詳細分析では、Ethereum上のX100 Freezerで起きたガバナンスの乗っ取りを取り上げます。
| 日付 | インシデント | 種類 | 推定損失額* |
|---|---|---|---|
| 2026年10月21日 | NEM Token | ビジネスロジックの欠陥 | 約82.0万 USD |
| 2026年10月24日 | X100 Freezer | ガバナンス設計の欠陥 | 約475万 USD |
*損失額は概算です。X100 Freezerの金額は、以下で分析する代表的なトランザクションを対象としており、Phalconの当初の推定額と一致します。合計額には、別途報告された、その後の追加流出は含まれません。NEMの日付はオンチェーントランザクションの日付です。公開アラートは10月22日に投稿されました[1]。
- X100 Freezer: このインシデントは、ガバナンスへの参加資格が即座に有効になることと、実行までのタイムロックがないことの複合的なリスクを示しています。攻撃者はフラッシュローンで借りた資金を使ってトークンを購入・凍結し、直ちにSenateの過半数の議席を獲得したうえで、同一トランザクション内で悪意あるトレジャリー提案を可決・実行しました。
Web3に最適なセキュリティ監査サービス
公開前に設計、コード、ビジネスロジックを検証
今週の注目事例:X100 Freezer
2026年10月24日、Ethereum上のオンチェーン・ステーキングおよび流動性管理システムであるX100 Freezerが悪用され、ここで取り上げるトランザクションでは約475万 USDの損失が発生しました。ガバナンス設計の欠陥により、新たに取得して凍結した残高が直ちにSenateの議席を付与し、提案の承認から実行までを隔てるタイムロックもありませんでした。攻撃者は3議席のうち2議席を確保し、トレジャリーのスワップを攻撃者が管理するプールに向け、回収したトークンの一部を売却してフラッシュローンを返済し、利益を得ました[2]。
背景
X100のユーザーは、報酬を得るためにXトークンをティア別のステーキングポジションで凍結します。0x79f3...2a46のFreezerコントラクトは、Uniswap V3/V4およびPancakeSwap V3上でプロジェクトの流動性も管理します。このコントラクトはDexTreasuryを通じてSenateガバナンスコントラクトを継承しており、Senateは提案と投票によってトレジャリーの操作を決定します。
公開エントリーポイントであるfreeze()は、ユーザーのトークンをコントラクトに移転した後、非公開の_freeze()関数を呼び出してステーキングポジションを作成し、その会計上の記録を更新します。_tierTotal[owner][tier]には、各アカウントについて各ティアで凍結された数量が記録されます。_freeze()はこれらの残高を更新し、_onStakeChanged(owner)を通じてガバナンスシステムに通知します。
function _freeze(address owner, uint8 tier, uint256 amount) private returns (uint256 id) {
_settle(owner);
Account storage account = _accounts[owner];
if (account.firstFreezeAt == 0) {
account.firstFreezeAt = block.timestamp;
}
id = _create(owner, tier, amount, block.timestamp, block.timestamp);
_tierTotal[owner][tier] += amount;
totalFrozen += amount;
totalFrozenIn[tier] += amount;
_onStakeChanged(owner);
emit Frozen(id, owner, tier, amount);
}
Senateの候補者リストに加わるには、アカウントからapplyForSenate()を呼び出します。これにより_register()が実行され、登録の記録と立候補手数料の徴収が行われ、同じステーク更新フックが呼び出されます。
function _register(address account) internal {
require(!_registered[account], AlreadyRegistered(account));
_registered[account] = true;
_collectCandidacyFee(account);
emit Registered(account);
_onStakeChanged(account);
}
このフックは、未登録のアカウントを処理しません。登録済みのアカウントについては、凍結残高による順位付きリストを管理し、_link()を通じてアカウントを挿入または移動します。isSenator()は、そのリストの先頭からsenateSize個のアカウントをSenate議員と判定します。このシステムは3~21議席をサポートし、各議席が1票を持ちます。凍結トークンには出金までの期間が設けられており、その期間はポジションのfrozenAtタイムスタンプとステーキングティアに基づいて_unfreezeAt()が計算します。
Senate議員は、propose()を通じて許可されたトレジャリー操作を提案します。この関数は提案を記録し、提案者の賛成票を自動的に投じます。ほかのSenate議員はvote()を通じて投票し、_tally()が現時点の議席による賛成票を集計します。提案には作成時刻と、デフォルトで3日間のvotingPeriodによって定まる期限が記録されます。
v3Swap()などのトレジャリー関数は、DexTreasury._isProposable()で提案の対象として許可されています。これらの関数のonlySenate修飾子は、コントラクト自体からの呼び出しを要求します。実行経路では、承認された提案データを使ってこの自己呼び出しを行います。その後、Freezer._afterExecute()が残りのX残高でtotalFrozenを賄えることを確認し、凍結された元本の合計を維持します。
脆弱性の分析
Freezerのガバナンス設計には、新たに取得したステークがSenateの権限を付与するまでの待機期間も、承認された提案を実行するまでのタイムロックもありませんでした。
第一に、順位付けに用いるステークは、現在の凍結残高から直接取得されます。_stakeOf()はfrozenOf()を呼び出し、ステークの保有期間を確認したり過去の残高を読み取ったりすることなく、_tierTotalを合算します。
function _stakeOf(address account) internal view override returns (uint256) {
return frozenOf(account);
}
function frozenOf(address owner) public view returns (uint256 total) {
total = 0;
for (uint8 tier = 0; tier < TIERS; ++tier) {
total += _tierTotal[owner][tier];
}
}
上記の入金と登録の経路では、同じ一連の呼び出し内で参加資格が更新されます。_freeze()はポジションのタイムスタンプを記録しますが、順位付けとSenate議員の判定では、それを使って最低保有期間を適用していません。そのため、十分な資金を持つ新たな候補者は、同一トランザクション内で議席を獲得し、提案権と投票権を行使できます。トークンの出金までの期間は、これらのガバナンス権限の行使を遅らせません。
第二に、3日間の投票期間は提案がいつ期限切れになるかを定めるだけです。_state()は、提案が作成されるとすぐにOpenを返し、提案は実行、取り消し、または期限切れまでその状態を維持します。過半数の判定は経過時間とは無関係です。
function _state(Proposal storage proposal) private view returns (ProposalState) {
if (proposal.executed) {
return ProposalState.Executed;
}
if (proposal.cancelled) {
return ProposalState.Cancelled;
}
if (block.timestamp > proposal.deadline) {
return ProposalState.Expired;
}
return ProposalState.Open;
}
function _isPassed(uint256 support) private view returns (bool) {
return support * 2 > senateSize;
}
execute()は、賛成票がsenateSizeの半数を超えた時点で、オープンな提案を受け入れます。トレジャリー操作を呼び出す前に、createdAtからの最低経過時間も、承認後の待機期間も確認しません。
function execute(uint256 id) external {
Proposal storage proposal = _requireOpen(id);
uint256 size = senateSize;
(uint256 support, address[] memory seats) = _tally(id);
require(_isPassed(support), NotPassed(id, support, size));
proposal.executed = true;
(bool ok, bytes memory result) = address(this).call(proposal.data);
if (!ok) {
assembly ("memory-safe") {
revert(add(result, 32), mload(result))
}
}
_afterExecute(id);
emit ProposalExecuted(id, _msgSender(), support, size, seats);
}
自己呼び出しによってonlySenateの条件は満たされるため、トレジャリーへのアクセスチェックによる実行の遅延は生じません。最後に行われる凍結元本のチェックも、提案の実行時期を制約せず、トレジャリースワップの最低受取額を強制しません。これらの条件により、新たに形成された過半数が1つのトランザクション内でトレジャリー操作を承認・実行できます。
攻撃の分析
以下の分析は、トランザクション0x38c608...4d81d0に基づいています。
-
ステップ1: 攻撃者はフラッシュローンで借りた資金を使って
Xトークンを購入し、自身が管理する2つのコントラクトに分配しました。借り入れた資金は、2つのステーキングポジションの確立に使うトークンの購入に充てられました。 -
ステップ2: 管理下の各コントラクトは、それぞれの
XをFreezerで凍結し、applyForSenate()を呼び出しました。トレースには、両コントラクトが登録後にRankedイベントを受けたことが示されています。それぞれの凍結残高により、このトランザクション内でSenateの2議席を確保しました。攻撃時、Senateは3議席だったため、2議席を支配すればトレジャリー提案の可決に必要な過半数を得られました。

- ステップ3: 攻撃者はPancakeSwap V3に
X/USDTプールを作成し、流動性を供給しました。管理下の一方のコントラクトがトレジャリースワップを提案し、賛成票が自動的に投じられました。もう一方が2票目の賛成票を投じました。過半数の条件が満たされ、Freezerは提案7を実行しました。そのv3Swap()呼び出しでは、入力に400,000X、最低受取額にamountOutMinimum = 0が指定されていました。トレジャリーが受け取ったのは約35.68USDTだけでした。トレースにはvotesFor = 2とsenateSize = 3が記録されており、奪取した過半数によって操作が承認されたことを示しています。

- ステップ4: 攻撃者は新たに作成したPancakeSwap V3プールから流動性を引き出し、トレジャリーがスワップに出した
Xを回収しました。そのトークンの一部を既存のUniswap V3X/USDTプールで売却し、フラッシュローンを返済しました。返済の原資は、その後の売却によるものであり、凍結されたステーキングポジションから直ちに出金したものではありません。
結論
このインシデントは、ガバナンス設計の欠陥に起因します。現在の凍結残高が直ちに権限を付与し、承認された提案はタイムロックなしで実行できました。攻撃者はこれらの条件を組み合わせ、フラッシュローンの返済期限が来る前に過半数を獲得して行使しました。その結果、トレジャリーは攻撃者が管理するPancakeSwap V3プールを通じて、400,000Xを約35.68USDTと交換しました。攻撃者はそのプールから流動性を引き出してトレジャリーのトークンを回収し、その一部を別の既存のUniswap V3 X/USDTプールで売却して、売却代金でフラッシュローンを返済し、利益を手元に残しました。
ガバナンスでは、ステーキングの最低経過期間を設けるか、現在のトランザクションより前のブロックで取得した過去の残高スナップショットに基づいて、議席の資格と投票権を判定すべきです。これにより、同一トランザクション内で資金を投入したポジションが、直ちにトレジャリーからの資産移動を承認することを防げます。承認から実行までのタイムロックは、乗っ取りから資産流出までの一連の操作が1つのトランザクション内で完了することを防ぎ、悪意ある提案を確認または取り消す時間を確保するために設けるべきです。これらの対策はガバナンス経路の異なる段階に対応するため、スマートコントラクト監査では併せて評価すべきです。
参考資料
[1] BlockSec Phalcon:NEM Tokenインシデントの分析
[2] BlockSec Phalcon:X100 Freezerのガバナンスインシデントの分析
BlockSecについて
BlockSecは、ブロックチェーンセキュリティと暗号資産コンプライアンスを包括的に提供する企業です。私たちは、プロトコルとプラットフォームのライフサイクル全体にわたり、コード監査(スマートコントラクト、ブロックチェーン、ウォレットを含む)、リアルタイムでの攻撃阻止、インシデント分析、不正資金の追跡、AML/CFT義務への対応を支援する製品とサービスを開発しています。
BlockSecは、著名な学会でブロックチェーンセキュリティに関する論文を複数発表し、DeFiアプリケーションに対する複数のゼロデイ攻撃を報告してきました。また、複数のハッキングを阻止して2,000万 USDを超える資産を救出し、数十億ドル規模の暗号資産を保護してきました。
-
公式ウェブサイト: https://blocksec.com/
-
公式Twitterアカウント: https://twitter.com/BlockSecTeam



