Solidityの「浮動小数点数がない」という呪い:なぜその計算がシステムを崩壊させるのか
エンジニアの諸君、現場で「Solidityに浮動小数点数がないなんて不便だ」と愚痴をこぼしたことはないか?確かに、Web2の世界でPHPやPythonを書いている感覚で 0.1 + 0.2 を計算しようとすれば、Solidityは即座にコンパイルエラーを吐く。
だが、この「不便さ」こそが、ブロックチェーンという極めてシビアな環境において、計算の整合性を守るための最後の砦なんだ。なぜなら、浮動小数点数(IEEE 754)は計算プロセスにおいて微細な誤差を生む。金融システムやIoTの制御閾値において、その「微細な誤差」は、攻撃者にとっては「無限に資金を引き出せるバグ」に直結する。
今日は、スマートコントラクトにおける「スケーリング」の概念と、それを実務にどう落とし込むか、泥臭い話をしよう。
—
攻撃者の視点:なぜ「端数」が命取りになるのか
攻撃者は、システムが 10 / 3 を計算する際の結果を狙う。もしコントラクト内で 3.333... という値が丸められたり、予期せぬ切り捨てが発生したりすれば、そこには必ず「消えたはずの価値」が漂う。
例えば、あるDEX(分散型取引所)のトークン交換レートで、スケーリングを怠った結果、ユーザーが0.0000001トークンを繰り返し送ることで、計算の繰り上げ誤差を突いてプール内の資産をドレイン(枯渇)させる手法は古典中の古典だ。
—
実践:固定小数点演算の「定石」
Solidityで小数のような挙動を実現するには、「スケーリング係数(Scaling Factor)」を使い、常に整数として扱うのが鉄則だ。
1. Solidityにおける実装パターン
基本は 10^18(1 ether)を基準にする。Solidityでは除算の前に乗算を行うのが鉄則だ。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract FixedPointMath {
// スケーリング係数 10^18
uint256 constant SCALE = 1e18;
// A * B / C を計算する際、先に掛け算を行って精度を維持する
function calculateScaledValue(uint256 a, uint256 b, uint256 c) public pure returns (uint256) {
// 先に a * b を行うことで、除算による精度欠損を最小化する
// 注意: (a * b) が uint256 の最大値を超えないかチェックが必要
require(c != 0, "Divide by zero");
return (a * b) / c;
}
}
—
Web2との連携:バックエンド側の防御
スマートコントラクトだけをセキュアにしても、そのフロントエンドやバックエンド(Node.js/Python)で浮動小数点数を使って計算し、それをコントラクトに送っていては意味がない。
フロントやバックエンドでは、BigNumber.js や decimal.js を使用し、「コントラクトに送る直前まで絶対に浮動小数点数として処理しない」という強い意志が必要だ。
JavaScriptでの実装例(BigNumber.jsを使用)
const BigNumber = require('bignumber.js');
// 1.23 と 4.56 を掛け合わせる場合、浮動小数を使わない
const val1 = new BigNumber("1230000000000000000"); // 1.23 * 10^18
const val2 = new BigNumber("4560000000000000000"); // 4.56 * 10^18
const scale = new BigNumber("1000000000000000000");
// 計算後、スケーリング分を調整する
const result = val1.times(val2).div(scale);
console.log(result.toString()); // 正確な整数値として扱う
—
インフラレベルでの防御:WAFと入力バリデーション
IoTデバイスからスマートコントラクトを叩くゲートウェイを構築している場合、攻撃者は「不正な桁数」の数値を送り込んで計算エラーやオーバーフローを誘発させようとする。
NginxやクラウドWAF(AWS WAF等)で、極端に大きな数値や、浮動小数点数を含む文字列(1.23など)をペイロードから弾く設定をしておくのも、多層防御の観点からは極めて有効だ。
Nginxでの簡易的な不正パケットフィルタリング(正規表現)
# 数値フィールドに小数が含まれている場合は拒否する設定例
# JSONペイロード内の "amount": "x.y" 形式をブロック
location /api/submit {
if ($request_body ~* "amount":[^,}]*\.[0-9]+") {
return 403;
}
proxy_pass http://backend_cluster;
}
—
セキュリティチーフからの提言
「たかが計算」と侮ってはいけない。ブロックチェーンの世界では、「計算コードこそが法」だ。
1. 除算は常に最後に行え:精度を保つための大原則だ。
2. Solidityのバージョンは最新を使え:0.8.x 以降はオーバーフローチェックが組み込まれている。古いバージョンの SafeMath ライブラリに依存し続ける必要はないが、ロジック上の「精度欠損」はライブラリでは防げない。
3. テストコードで「極端な値」を試せ:最大値、最小値、ゼロ、そしてスケーリングした際の値が期待通りか、単体テストで徹底的に叩き込むこと。
現場でバグを見つけてから修正するのは「作業」だが、設計段階で誤差を封じ込めるのは「エンジニアリング」だ。諸君、コードを書くときは常に「この計算で、1円でも消えることはないか?」と自問自答してほしい。それがプロフェッショナルというものだ。
コメント