こんにちは!ブロックチェーンの世界へようこそ。スマートコントラクトの開発に足を踏み入れたばかりの頃は、見慣れない用語やルールがたくさんあって、戸惑ってしまうことも多いですよね。
今回は、スマートコントラクトの歴史において数々の億単位の資金を消し去ってきた、ちょっぴり恐ろしいけれど非常に大切なテーマ「整数オーバーフロー・アンダーフロー」について、一緒に学んでいきましょう!
難しいセキュリティ用語が並ぶと身構えてしまうかもしれませんが、身近な「お家の鍵」や「オドメーター(車の積算距離計)」に例えながら、一歩ずつ優しくほどいていきますので、どうぞリラックスして読んでくださいね。
—
1. 整数オーバーフローってなに?身近な例えで考えてみよう
まずは、コンピュータの世界における「数字のルール」からお話ししますね。
現実世界では、数字に終わりはありませんよね。「100億」の次には「101億」が来ます。無限に大きくしていくことができます。
しかし、スマートコントラクトが動くEVM(Ethereum Virtual Machine:イーサリアム仮想マシン)の世界では、扱える数字の大きさに「お部屋の広さ(ビット数)」という限界がカチッと決められています。
車の走行距離メーター(オドメーター)の例え
古い車や自転車のダイヤル式の距離計を思い浮かべてみてください。すべての桁が「9」の状態で、さらに「1」進むとどうなるでしょうか?
そう、数字は「00000」に戻ってしまいますよね。
スマートコントラクトの数字もこれと全く同じです。
例えば、最大で 255 までしか入らない小さなお部屋(uint8型というデータ型ですね)があったとします。このお部屋にすでに入っている 255 という数字に対して、うっかり + 1 を足してしまうと、コンピュータはパニックを起こして……なんと、一番小さな 0 にクルリと戻ってしまうのです。
これが 「整数オーバーフロー(Overflow)」 です。
逆に、一番小さな 0 から - 1 を引いてしまうと、今度は一気に一番大きな 255 にワープしてしまいます。これが 「整数アンダーフロー(Underflow)」 です。
—
2. 攻撃者はこの「数字のワープ」をどう悪用するのか?
「数字が勝手にゼロに戻るなんて、なんだかオモシロいバグだね」なんて思っていませんか?
実はこれが、サイバー攻撃者にとっては「大金庫のダイヤルを勝手に開ける魔法の鍵」になってしまうんです。
例えば、あなたがこんなシンプルな「トークン購入コントラクト」を作ったとしましょう。
// 古いSolidityの書き方の例です
contract TokenSale {
mapping(address => uint256) public balances;
// トークンを買う関数
function buyTokens() public payable {
// 1ETHにつき100トークンを渡す処理
uint256 tokensToBuy = msg.value * 100;
balances[msg.sender] += tokensToBuy;
}
}
平和な世界線では何の問題もありません。ですが、ここに悪意ある攻撃者が現れたらどうなるでしょうか?
攻撃者は、オーバーフローを引き起こすような「計算結果が限界突破して極端に小さくなる(または0になる)ような巨大な数値」を計算トリックを使って送り込みます。
すると、本来はお金を支払わなければ手に入らないはずの大量のトークン残高が、プログラムの計算ミス(オーバーフロー)によって、「持っていることになってしまう」のです。泥棒が鍵をピッキングする音すらせず、システムが自ら「どうぞお持ち帰りください」と大金庫を開けてしまうようなものですね。これが、過去に多くのDeFi(分散型金融)プロジェクトを泣かせてきた原因です。
—
3. 救世主の登場:歴史を支えた「SafeMath」ライブラリ
「そんな恐ろしいバグがあるなら、安心してコードが書けないよ……」と思いましたよね。ご安心ください!先人たちの努力によって、素晴らしい対策が用意されてきました。
Solidity 0.8.0以前の時代、開発者たちは SafeMath という特別なガードマン(ライブラリ)を必ずコードにインポートして身を守っていました。
// SafeMathを使った安全な足し算のイメージ
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
contract SafeExample {
using SafeMath for uint256;
uint256 public myBalance = 100;
function addSafely(uint256 amount) public {
// 単に 「+」を使うのではなく、SafeMathの関数を使うことで
// オーバーフローしそうになった瞬間に「エラー!」と処理を止めてくれます
myBalance = myBalance.add(amount);
}
}
SafeMath は、計算を行う前に「おっと、この足し算をしたら限界を超えちゃうぞ!」と事前にチェックし、もしオーバーフローやアンダーフローが起きそうになったら、トランザクション全体を安全にキャンセル(リバート)してくれる賢い番人でした。
—
4. 最新のSolidity 0.8.0以降ではどうなっているの?
「じゃあ、今の時代も毎回めんどくさいSafeMathを書き続けなきゃいけないの?」
そんな疑問が湧いたあなた、素晴らしい着眼点です!
実は、Solidityのバージョン 0.8.0 からは、このSafeMathのチェック機能が言語の「標準装備(デフォルト)」になりました。
現在主流となっているモダンなSolidityでコードを書く場合、特別なライブラリを読み込ませなくても、コンパイラが自動的にすべての算術演算を監視してくれます。
// Solidity 0.8.0 以降の世界
contract ModernContract {
uint8 public tinyBox = 255;
function triggerError() public {
// 0.8.0以降なら、ここでわざとオーバーフローさせようとすると
// 自動的にトランザクションが失敗(Revert)します!
tinyBox = tinyBox + 1;
}
}
これで、うっかりミスによるオーバーフローで資金がハッキングされるリスクは劇的に減りました。本当にありがたい進化ですよね。
⚠️ ちょっと待って!「あえてチェックを外したい」ときの裏ワザ
「自動チェックって最高じゃん!」と思う一方で、ガス代(手数料)を極限まで削りたい高度な最適化の現場や、「絶対にオーバーフローしないことが数学的に証明されているカウンター変数(例えば for ループのインデックスなど)」においては、チェック機能がかえって無駄なガスコストになってしまうことがあります。
そんなときは、unchecked という特別なブロックを使うことで、一時的に昔の(チェックなしの)挙動に戻すことができます。
function optimizedLoop() public pure returns (uint256) {
uint256 sum = 0;
// この中ではオーバーフローチェックが無効化されます(自己責任エリア!)
unchecked {
for (uint256 i = 0; i < 10; i++) {
sum += i;
}
}
return sum;
}
ただし、新人エンジニアのうちは、この unchecked を使うときは「本当にオーバーフローが絶対に起きないと言い切れるか」を何度も自分に問いかけるようにしてくださいね。少しでも不安があるなら、デフォルトの安全な状態のままにしておくのがベストプラクティスです。
—
まとめ:一歩ずつ、安全なコントラクトを作ろう
今回は、整数オーバーフロー・アンダーフローの仕組みから、歴史的なSafeMathの役割、そして最新のSolidity 0.8.0以降の挙動までを紐解いてみました。
- オーバーフローとは: 数字の限界を超えたときに、予期せず「0」などにワープしてしまう現象。
- 攻撃への影響: 悪意あるユーザーが計算の隙をついて、トークンや資金を不正に増やしてしまう。
- 現在の対策: Solidity 0.8.0以降ならデフォルトで保護されているが、
uncheckedを使う場所には十分注意する。
セキュリティの世界は奥が深く、最初は覚えることも多いですが、基礎的なメカニズムをこうして一つずつ理解していけば、決して怖くありません。
ぜひご自身の開発環境でも、実際にコードを書いて「エラーで止まる瞬間」や「正常に動く瞬間」を確かめてみてくださいね。
あなたのスマートコントラクト開発の旅が、安全でワクワクするものでありますように!それではまた次の記事でお会いしましょう。
コメント