「戻り値がないトークン」で資産が凍結?ERC-20の落とし穴とSafeERC20の処方箋
現場のエンジニア諸君、お疲れ様。今日もスマートコントラクトと格闘しているか?
最近、オンチェーンのインシデント調査をしていてつくづく思うのは、「標準仕様(EIP-20)を過信した開発者が、最も痛い目を見る」ということだ。特に、多くのエンジニアが「まあ approve や transfer は呼べば動くだろう」とタカをくくっているERC-20の挙動。これこそが、DeFiプロトコルのTVL(預かり資産)を一瞬で吹き飛ばす盲点になっている。
今日は、なぜ「戻り値のないトークン」があなたのプロダクトを止めるのか、そしてそれをどう回避すべきか、実戦的な知見を共有しよう。
1. なぜ「戻り値なし」が致命傷になるのか
ERC-20の仕様書(EIP-20)では、transfer や approve は bool 型の戻り値を返すことになっている。しかし、世の中にはこの仕様に従っていないトークンが山ほど存在する。
特に有名なのが USDT(Tether)の一部バージョンや、特定の独自トークンだ。これらは transfer を実行しても、正常終了時に何も値を返さない(または revert せずに処理を終える)。
攻撃者視点の盲点
EVM(Ethereum Virtual Machine)の低レイヤーを見てみよう。呼び出し先(トークンコントラクト)が戻り値を返さない場合、呼び出し元が「戻り値があるはずだ」と期待してスタックから返り値を取り出し(RETURNDATACOPY等で読み取り)に行くと、返り値が空であるため、EVMは「戻り値のデコード失敗」とみなしてトランザクション全体を revert させる。
つまり、「送金自体は成功したのに、呼び出し元のコードが戻り値の欠落を検知して強制終了し、資産がコントラクト内にスタックしたままになる」という悲劇が起きるんだ。
2. 実践:SafeERC20による「防御」の徹底
この問題を解決する唯一の正解は、OpenZeppelinが提供する SafeERC20 ライブラリを使うことだ。これを使わずに素のインターフェースで呼ぶのは、防弾チョッキを着ずに戦場へ行くようなものだ。
JavaScript (ethers.js) でのセキュアな実装例
バックエンドでNode.jsを使ってトークン送金や承認を行う場合も、同じ考え方だ。ethers.js でトークン操作を行う際の、安全なラッパーパターンの例を書いておく。
/**
* 非標準トークン対応:SafeERC20相当のラッパー関数
* @param {ethers.Contract} tokenContract - トークンのコントラクトインスタンス
* @param {string} method - 'transfer' or 'approve'
* @param {Array} args - 引数
*/
async function safeTokenCall(tokenContract, method, ...args) {
try {
// トークンが戻り値を返さない可能性があるため、低レイヤーの呼び出しを考慮する
const tx = await tokenContract[method](...args);
const receipt = await tx.wait();
// ここで戻り値のチェックを厳密に行いすぎないのがコツ
// 実際には receipt.status が 1 であることを確認するだけで十分
return receipt;
} catch (error) {
console.error(`[Security Alert] トークン操作失敗: ${method}`, error);
throw new Error("Transaction execution failed or returned invalid data");
}
}
// 利用例:USDTのような非標準トークンでも安全に送金可能
// await safeTokenCall(usdtContract, 'transfer', recipientAddress, amount);
3. 現場で防ぐべき「設定」の罠
コントラクトコードだけがセキュリティではない。Webアプリケーション側でトークンを扱うAPIの設計も重要だ。特に、ユーザーからの入金を検知するWebフックやバッチ処理では、以下の点に注意せよ。
revertを無視しない: トランザクションが成功したか否かは、必ずreceipt.statusを見て判断すること。- ガスの過小評価: 非標準トークンは予期せぬ挙動でガス消費量が跳ね上がることがある。ガスリミットは余裕を持たせて設定しろ。
Nginx/WAFでの保護(APIエンドポイント)
もしあなたがWeb3サービスのバックエンドを構築しているなら、コントラクト呼び出しを行うAPIエンドポイントに対し、以下のNginx設定で不正な入力を遮断しておくことも推奨する。
# APIエンドポイントへのDoS攻撃を制限(コントラクト呼び出しを乱発させない)
location /api/v1/transfer {
limit_req zone=token_transfer_limit burst=5 nodelay;
# スマートコントラクトのバイトコードを狙ったエクスプロイトを防ぐための簡易的な正規化
# 基本的に入力をバリデーションし、期待されるアドレス形式以外は即座に弾く
if ($arg_amount !~* "^[0-9]+$") {
return 403;
}
}
最後に:プロの矜持として
「動いたから良い」ではない。動いた先の「戻り値がない場合」まで想定するのが、我々セキュリティエンジニアの矜持だ。
もし今、君が開発しているプロダクトで「USDTなどのトークンを直接 call している箇所」があれば、すぐに SafeERC20 への置き換えをタスクに積んでほしい。明日、突然トークンの仕様が微修正されて、あなたのサービスが全停止するリスクはゼロではないのだから。
泥臭い確認作業こそが、最も強固な防御になる。健闘を祈る。
コメント