こんにちは!スマートコントラクトの開発の世界へようこそ。
ブロックチェーンやWeb3の世界って、なんだか最先端で難しそうな言葉が飛び交っていて、初めて触れるときは「自分に扱えるかな……」と不安になりますよね。でも、一歩ずつ基本を紐解いていけば大丈夫です!一緒に楽しく学んでいきましょう。
今回は、スマートコントラクトのセキュリティにおいて最も基本的でありながら、過去に何度も億単位の資産を消失させてきた恐ろしい脆弱性、「整数オーバーフロー・アンダーフロー」について解説します。
身近な例えを交えながら、攻撃者がどうやって私たちのお金を狙っているのか、そしてそれをどうやって防ぐのかを優しく見ていきましょう!
—
1. 整数オーバーフローってなに? 身近なたとえで考えてみよう
いきなり難しいプログラミング用語が出てきましたが、実は仕組み自体はとってもシンプルです。
皆さんのご自宅にある「古いデジタル式の自転車の鍵」や「昔の車のオドメーター(走行距離計)」を思い浮かべてみてください。例えば、最高で「9999km」までしか表示できない走行距離計があったとします。この距離計が「9999km」の状態で、さらに1km走ったらどうなるでしょうか?
メーターは「10000km」と表示できず、「0000km」にぐるっと巻き戻ってしまいますよね。
これが、コンピュータの世界でいう「整数オーバーフロー(Overflow)」です。
コンピュータの中の数字も、実は無限の大きさを持つわけではありません。「ここまでしか入らないよ」という入れ物(箱)のサイズが決まっています。
例えば、Solidity(スマートコントラクトを書くプログラミング言語)の古いバージョンでは、数値をいれる箱のサイズ(ビット数)がガチガチに決まっていました。
- オーバーフロー(あふれちゃう現象): 入れられる最大の数字(例: 255)を超えて「+1」すると、いきなり最小の「0」に戻ってしまう。
- アンダーフロー(マイナスに行き過ぎちゃう現象): 最小の「0」の状態で「-1」すると、いきなり最大の「255」にワープしてしまう。
泥棒(攻撃者)は、この「数字がぐるっと一周してしまうルール」を悪用して、スマートコントラクトの計算を狂わせ、自分の持っているトークン(デジタル資産)の残高を勝手に爆発的に増やしてしまうのです。怖いですよね。
—
2. Solidityのバージョンによる違いを知ろう
「えっ、じゃあ毎回そんな計算の限界を気にしてコードを書かないといけないの?」と心配になりますよね。安心してください。時代の進化とともに、この問題はかなり解決しやすくなっています。
Solidityのバージョンによって、対策の仕方が大きく異なります。
① Solidity 0.8.x 以降(現代のスタンダード)
Solidity 0.8.0 からは、言語自体にオーバーフローの自動チェック機能が標準装備されました。
つまり、うっかりオーバーフローするような計算を書いてしまうと、コンピュータが「おっと、危ない!」と自動的にその取引(トランザクション)を強制終了(リバート)してくれるようになったのです。お守りが最初からついている状態ですね!
② Solidity 0.7.x 以前(レガシーなコード)
もし古いシステムを保守していたり、過去のコードを読んだりする場合、自動チェック機能はありません。そのため、開発者が自分で「SafeMath(セーフマ数学)」という専用のライブラリを使って、計算が安全かどうかを毎回チェックしてあげる必要がありました。
—
3. 実践!コードで見る安全な書き方
それでは、実際にコードを見てみましょう。新人エンジニアの皆さんが現場で迷わないよう、具体的なコード例と丁寧なコメントを用意しました。
パターンA:Solidity 0.8.x以降での書き方(自動チェック)
現在の開発では基本的にこの書き方になります。特別なライブラリをインポートしなくても、通常の算術演算子(+, -, * など)を使うだけで安全性が保たれます。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20; // 0.8.0以降のバージョンを指定
contract ModernTokenVault {
mapping(address => uint256) public balances;
// トークンを預ける関数
function deposit() public payable {
// 0.8.x以降は、balances[msg.sender] + msg.value がオーバーフローすると
// 自動的にエラーとなり、処理が巻き戻されます。
balances[msg.sender] += msg.value;
}
// トークンを引き出す関数
function withdraw(uint256 amount) public {
// 残高不足でアンダーフロー(マイナス)になりそうな場合も
// 自動的に検知してブロックしてくれます。
require(balances[msg.sender] >= amount, "残高が足りません!");
balances[msg.sender] -= amount;
// 実際にはここに送金処理が入ります
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "送金に失敗しました。");
}
}
パターンB:Solidity 0.7.x以前での書き方(SafeMathの利用)
もし古いバージョンのコントラクトを扱う場合は、OpenZeppelin社が提供するSafeMathライブラリを組み込む必要があります。
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12; // 0.7未満の古いバージョン
// OpenZeppelinのSafeMathライブラリをインポート
import "@openzeppelin/contracts/math/SafeMath.sol";
contract LegacyTokenVault {
// uint256型に対してSafeMathの機能を紐付ける
using SafeMath for uint256;
mapping(address => uint256) public balances;
function deposit() public payable {
// 足し算には .add() を使う
balances[msg.sender] = balances[msg.sender].add(msg.value);
}
function withdraw(uint256 amount) public {
require(balances[msg.sender] >= amount, "残高が足りません!");
// 引き算には .sub() を使う(ここで自動的にアンダーフローをチェック)
balances[msg.sender] = balances[msg.sender].sub(amount);
msg.sender.transfer(amount);
}
}
このように、古いバージョンでは + の代わりに .add()、- の代わりに .sub() を使うことで、安全な計算を担保していました。泥臭い対策ですが、当時のエンジニアにとっては命綱だったのです。
—
4. セキュリティリサーチャーからの実務アドバイス
現場でスマートコントラクトの監査や開発を行う際、私たちがチェックするポイントをこっそりお教えします。
1. コンパイラバージョンの確認:
まずは pragma solidity の記述を確認してください。もし ^0.8.0 より古いバージョンを指定している場合は、即座に最新バージョンへのアップグレードを検討するか、SafeMathが正しく全計算箇所で適用されているか目を皿のようにして確認しましょう。
2. アセンブリ(Unchecked)ブロックの罠:
Solidity 0.8.x以降であっても、ガス代(手数料)を節約するためにあえて unchecked { ... } というブロックを使い、オーバーフローのチェックを意図的に無効化するテクニックがあります。これが使われているコードに出会ったら、「本当にオーバーフロー絶対に起きないと言い切れるか?」を徹底的に疑ってください。ここが攻撃者の大好物になるポイントです。
—
まとめ
いかがでしたでしょうか?
整数オーバーフロー・アンダーフローは、一見すると地味な計算上のミスに見えますが、Web3の世界では致命的なハッキングにつながる恐ろしい脆弱性です。
- Solidity 0.8.x以降なら基本は安心(でも unchecked には注意!)
- 古いバージョンなら SafeMath(.addや.sub)を必ず使うこと
この2つの基本をしっかり押さえておけば、あなたの書くスマートコントラクトの安全性はグッと向上します。
一歩ずつ、確実になめらかなセキュリティスキルを身につけていきましょう!次回の記事もお楽しみに!
コメント