制御システムとWeb3の交差点:なぜスマートコントラクトの「小数」は命取りになるのか
OT(制御システム)の現場や、莫大なTVL(Total Value Locked)を抱えるDeFiプロトコルのスマートコントラクトを監査していて最も背筋が凍る瞬間は、コードの表面的な美しさではなく、「データ型の不整合」という極めてプリミティブな設計ミスに気づいたときだ。
PLC(プログラマブル・ロジック・コントローラー)のファームウェア解析でも、SolidityによるEVM(Ethereum Virtual Machine)のバイトコード解析でも、根本的な悪夢の構造は変わらない。物理世界を動かすセンサー値の処理であれ、AMM(自動マーケットメーカー)の流動性プールにおける価格曲線(Bonding Curve)の計算であれ、「浮動小数点数(Floating-Point Numbers)」の取り扱いを誤った瞬間、システムは致命的な脆弱性を孕む。
Solidityは歴史的に、そして意図的に浮動小数点数(float や double)をサポートしていない。これはEVM上での決定論的(Deterministic)な実行を担保し、異なるバリデーター間で浮動小数点の丸め誤差によるコンセンサス分裂を防ぐための必然的な仕様だ。しかし、この制約を理解せず、JavaScriptの感覚のまま泥臭いハックで小数を扱おうとした開発者が生み出した「自作の計算ロジック」が、幾度となく億単位の資金を消し飛ばしてきた。
今回は、スマートコントラクトにおける浮動小数点数演算の回避、スケーリング係数(Scaling Factor)を用いた安全な整数演算のイディオム、そして攻撃者がどのように丸め誤差や精度落ち(Precision Loss)を突いてインフィニティ・ドレインを成し遂げるのか、その深層を紐解いていく。
—
根本原因:EVMにおける「小数欠如」と丸め誤差の罠
IEEE 754標準に基づく浮動小数点数は、ハードウェアレベル(CPUのFPUなど)では効率的だが、異なるアーキテクチャ間や分散合意システムにおいては「非決定的な動作」を引き起こす温床となる。そのため、EVMは最初から浮動小数を排除し、すべての数値を整数(基本は uint256)として処理することを強制している。
ここで開発者が陥る典型的なアンチパターンが、「割り算を先に行う」ことによる精度の喪失だ。
// 【危険なアンチパターン】精度落ちを引き起こす例
// Solidityでは整数同士の割り算は切り捨て(Truncation)が発生する
function calculateFeeBad(uint256 amount, uint256 feeRate) public pure returns (uint256) {
// feeRateがパーセンテージ(例: 0.5%を表現するために 50 / 10000 としたい場合)
// 先に 10000 で割ると、amount * 50 が 10000 未満の場合に結果が 0 になる
return (amount / 10000) * feeRate;
}
このコードの何が問題か。例えば amount が 5000 で、feeRate が 50(0.5%)の場合、5000 / 10000 は 0 になり、最終的な手数料は 0 になる。攻撃者はこの仕様の隙を突き、微小なトランザクションを大量に発行することで、プロトコルから本来徴収されるべき手数料を完全にバイパスすることが可能になる。OT/IoTの文脈に置き換えれば、水処理プラントの薬品注入量や、電力網の周波数制御におけるPID制御の係数計算で同様の丸め誤差が発生した場合、物理的なバルブ制御の破綻やハードウェアの破損に直結する。
—
スケーリング係数による防御アーキテクチャ
この問題を解決するためのゴールドスタンダードが、「スケーリング係数(Scaling Factor)を用いた固定小数点数(Fixed-Point Arithmetic)エミュレーション」である。
金融や高精度な制御ロジックでは、小数をそのまま扱うのではなく、事前に一定の倍率(通常は 1e18 など、Weiの単位やトークンのDecimalsに合わせた基数)を掛け合わせた状態で計算を保持し、最終的な出力の直前まで割り算を遅延させる(Multiply-First, Divide-Last)。
以下に、実務のスマートコントラクトセキュリティ監査で合格点を出せる、堅牢なスケーリング演算の実装パターンを示す。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @title FixedPointMathLibrary
* @notice 浮動小数点数を排除し、スケーリング係数を用いて高精度な算術演算を行うライブラリ
* @dev 溢れ(Overflow)と精度落ち(Precision Loss)の双方を防ぐ設計
*/
library FixedPointMathLibrary {
// 10の18乗を基数(スケーリング係数)とする(WAD形式)
uint256 internal constant SCALE = 1e18;
/**
* @notice 高精度な掛け算
* @param x 最初の数値(スケール済み)
* @param y 2番目の数値(スケール済み)
* @return 演算結果(スケール済み)
*/
function mul(uint256 x, uint256 y) internal pure returns (uint256) {
if (x == 0 || y == 0) {
return 0;
}
// オーバーフローチェックを内包しつつ、四捨五入(Rounding)を考慮した乗算
uint256 prod = x * y;
require(prod / x == y, "FixedPoint: Multiplication overflow");
// スケールを戻すための調整(四捨五入ロジック)
return (prod + (SCALE / 2)) / SCALE;
}
/**
* @notice 高精度な割り算
* @param numerator 分子(スケール済み)
* @param denominator 分母(スケール済み)
* @return 演算結果(スケール済み)
*/
function div(uint256 numerator, uint256 denominator) internal pure returns (uint256) {
require(denominator > 0, "FixedPoint: Division by zero");
if (numerator == 0) {
return 0;
}
// 割り算の前にスケーリング係数を乗算することで、精度落ちを防止する
// 例: (A * SCALE) / B
uint256 scaledNumerator = numerator * SCALE;
require(scaledNumerator / numerator == SCALE, "FixedPoint: Multiplication overflow");
// 四捨五入を適用して割り算を実行
return (scaledNumerator + (denominator / 2)) / denominator;
}
}
この実装における最大のポイントは、「掛け算を先に行い、割り算を最後に行う(Multiply-First, Divide-Last)」という鉄則の遵守と、(SCALE / 2) を加算することによる「四捨五入(Round-to-Nearest)」の担保である。単純な切り捨て(Floor)を放置すると、微小な端数が蓄積し、プール内の残高と内部台帳の合計が一致しなくなる「Rounding Dust Attack(端数攻撃)」の標的となる。
—
チーフホワイトハッカーの視点:攻撃者はこの脆弱性をどう悪用するか?
セキュリティリサーチャーとして、我々がDeFiプロトコルやIoTオーケストレーション層のスマートコントラクトを攻撃・監査する際、浮動小数点数演算の回避ミスは「宝の山」に見える。
1. ドレイン攻撃(Rounding Exploitation):
流動性プールの報酬計算やステーキングの利息計算において、割り算の順序が不適切な箇所を見つけ出す。意図的に極小の額(例: 1 wei)の入出金を数千回繰り返させるフラッシュローンを用いたループ攻撃により、システム側の丸め誤差のバグを突いて、本来ユーザーが得られない端数をかき集め、コントラクトの残高を枯渇させる。
2. ガバナンス・クォーラムのバイパス:
投票権の重み付け計算(例: 総供給量に対する割合)において、分母と分子の除算順序の誤りにより、微小な保有量のクジラが投票権を数倍に水増ししたり、逆にゼロと判定されて提案が通らなくなったりするロジックの破綻を引き起こす。
OT/IoTのファームウェア開発においても、センサーから取得したアナログ値(電圧や温度)をデジタル変換する際の固定小数点スケーリング係数の設定ミスは、オーバーフローによる制御不能(暴走)を引き起こす。制御系エンジニアとWeb3スマートコントラクトエンジニアは、根底にある「有限の世界で無限の精度をどう近似するか」という数学的課題において、全く同じ苦悩を共有しているのだ。
—
まとめ:安全なアーキテクチャ構築に向けて
スマートコントラクトにおける浮動小数点数演算の回避は、単に「エラーが出ないようにコードを書く」という次元の話ではない。それは、EVMの制約を深く理解し、ビット精度のレベルで数値のライフサイクルを完全にコントロールするという、シニアエンジニアリングの極みである。
開発現場においては、自前で怪しい計算ライブラリを書くことは直ちに禁止し、OpenZeppelinの数学ライブラリや、定評のある固定小数点ライブラリ(例: PRBMath など)を適切にインポートして使用するべきだ。そして、プロトコルのリリース前には、ファズテスト(Foundryの invariant testing など)を用いて、あらゆる境界値(type(uint256).max や 0 付近)で丸め誤差が資金流出に繋がらないかを徹底的に検証し尽くすこと。
妥協のないコードのみが、ダークフォレスト(暗黒の闇市)と化したブロックチェーンの荒野で生き残る唯一の盾となる。
コメント