金色の配線パターンが広がる基板の接写
現場の実践

ERC20トークン連携のバグ:残高差分で9割防げる理由

目次を見る

自社の基幹システムや決済基盤に、ERC20(イーサリアム上で発行されるトークンの共通規格)を使ったウォレット連携や入出金処理を組み込んでいる開発チームに向けた内容です。トークン連携のテストが全部通り、監査も無事に終えたのに、本番で残高がわずかにずれる不具合に悩んだことはないでしょうか。

実はこの手のトラブルの多くは、ERC20という「規格」そのものへの誤解から生まれます。ERC20は transfer や approve といった関数の名前と引数を定めたインターフェース(外部とやり取りするための窓口の仕様)にすぎません。内部でどう残高を計算し、どう送金を実行するかは、トークンの発行者が自由に実装できます。この前提を忘れると、思わぬ場所で資金がロックされたり、会計がずれたりします。

何が起きるか:残高のズレが連鎖して金庫が凍結する

典型的な事象は、ユーザーからの入金額と、実際にシステム側が受け取った金額が一致しないことです。

たとえばボールト(資金を預かるコントラクトや口座管理システム)が「100トークン入金された」という引数だけを信じて、内部の持ち分(シェア)を100分発行したとします。ところが実際に届いたのは95トークンだけだった場合、差分の5トークンはどこかから補填しなければ辻褄が合いません。

このズレは1回では大したことがなくても、入出金が繰り返されるうちに複利的に拡大します。最終的には、後から出金しようとしたユーザーの分だけ資金が足りなくなり、トランザクションが revert(処理の巻き戻り)し続けて出金不能になります。業務システムで言えば、残高テーブルと実際の口座残高が静かに乖離し、月次決算で帳尻が合わなくなる状態に近いイメージです。

なぜ起きるか:4つの思い込みを段階的に分解する

原因を一言でまとめると「指定した金額どおりに処理が動く」という思い込みです。これをもう少し分解すると、主に4つの誤った前提に行き着きます。

  • 金額の思い込み: 送金を指示した数量と、実際に届く数量が同じだと信じている
  • 実行の思い込み: トークンの移転が受動的なデータ更新であり、処理の主導権を握らないと思っている
  • 権限の思い込み: 残高と許可量(allowance)さえ足りていれば必ず送金が成功すると思っている
  • 会計の思い込み: 小数点以下の精度や挙動が、常に固定で変わらないと思っている

特に最初の2つは実害が大きいので、具体的に見ていきます。

手数料徴収型トークンでの残高ズレ

一部のトークンは、送金のたびに数%の手数料を天引きします。いわゆる Fee-on-Transfer(送金時手数料)や、残高が時間とともに自動調整される Rebasing(リベース)という仕組みです。

100トークンの送金を指示しても、5%の手数料が引かれて95トークンしか届かないことがあります。このとき、送金元が指定した「100」という引数だけを使って内部の持ち分を計算すると、実際の入金額より多く発行してしまいます。

コールバック機能を持つトークンでの再入可能攻撃

ERC-777規格や、独自のフック(送金時に外部処理を呼び出す仕組み)を持つトークンは、送金処理の途中で呼び出し元・受取先のコントラクトに制御を戻します。これは業務システムでいえば、決済APIを呼んだ瞬間に相手システムから自分のAPIへコールバックが飛んでくるようなものです。

出金処理で「先にトークンを送ってから内部の負債残高を更新する」という順序にしていると、コールバックのタイミングで同じ出金処理がもう一度呼び出され、二重出金が成立してしまいます。これは Reentrancy(再入可能性)攻撃と呼ばれる、スマートコントラクト特有の典型的な脆弱性パターンです。

自分のプロジェクトが該当するか確認する方法

対策を打つ前に、まず自社の実装がこのリスクを抱えているか棚卸しします。以下の観点でコードベースを確認してください。

  • トークンの送金処理コードで、送金前後の balanceOf を比較せず、引数の amount をそのまま内部会計に使っている箇所がないか検索する
  • 連携対象のトークンリストに、取引所や外部発行のトークン(手数料徴収型・リベース型の可能性があるもの)が含まれていないか確認する
  • 外部コントラクト呼び出し(transfer や transferFrom)の後に、自社の内部状態(残高・負債・権限フラグ)を更新している処理がないか洗い出す
  • 使用しているトークンのコントラクトコードが公開されている場合、Etherscan等でソースコードを確認し、_transfer 内部にフックや手数料ロジックがないか目視する
  • 依存ライブラリが OpenZeppelin のどのバージョンの ERC20.sol を参照しているか package.json やインポート文で確認する(標準実装と拡張実装の違いを把握する)

既存の業務システムに後から新しいトークン種別を追加する場面は、まさにこのチェックを通すタイミングです。新規トークン追加のたびに、このチェックリストを運用フローに組み込んでおくと再発を防げます。

対策の手順

手順1: 残高差分方式に会計ロジックを書き換える

引数の amount を信用せず、送金前後の実残高差分を使う方式に統一します。

uint256 balanceBefore = token.balanceOf(address(this));
token.safeTransferFrom(msg.sender, address(this), amount);
uint256 balanceReceived = token.balanceOf(address(this)) - balanceBefore;

// 内部会計には balanceReceived のみを使う
_mintShares(msg.sender, balanceReceived);

この修正だけで、手数料徴収型・リベース型トークンによる会計ズレはほぼ解消します。既存システムであれば、入出金処理の該当箇所を洗い出して段階的に置き換えるマイグレーション計画を立てるのが現実的です。

手順2: Checks-Effects-Interactions パターンを徹底する

外部コントラクトへの呼び出し(Interaction)は、必ず内部状態の更新(Effects)より後に実行します。出金処理なら「残高を減らしてから送金する」の順序を機械的に守ります。

手順3: 再入防止ガードを併用する

OpenZeppelinの ReentrancyGuard のような仕組みを、外部トークンとやり取りする関数すべてに適用します。CEIパターンと二重の防御層にしておくと、実装ミスが1箇所あっても連鎖被害を抑えられます。

手順4: 対応トークンのホワイトリスト運用を検討する

すべての任意トークンに対応する必要がない業務システムであれば、あらかじめ挙動を検証したトークンだけを許可するホワイトリスト方式も現実的な選択肢です。新規トークン追加時に上記チェックリストを通し、承認プロセスを経てから有効化する運用にすると、監査コストと柔軟性のバランスが取れます。

ERC20は規格であって保証ではありません。内部の挙動は実装次第なので、金額は「指定値」ではなく「実測の差分」で扱うのが安全です。

まとめ

ERC20まわりの不具合は、派手な脆弱性よりも「規格への過信」から生まれるケースが目立ちます。読み終えたら、まず自社の入出金処理で amount 引数を直接会計に使っている箇所を検索してみてください。

  • 送金処理は常に balanceOf の差分で会計すること
  • 外部呼び出しの前に内部状態を更新するCEIパターンを徹底すること
  • 新規トークン追加時はコード確認とホワイトリスト運用をセットで回すこと

この3点を既存コードベースの改修ロードマップに組み込んでおくと、新しいトークンが増えるたびに安心して対応範囲を広げられます。

参考

ERC20 Edge Cases Every Smart Contract Engineer Should Know

この記事について: 本記事は AI を活用して作成し、forva AI 編集部が内容を確認・監修しています。

AI 駆動開発のご相談は forva AI へ。まずはお気軽にどうぞ。