スマートコントラクトが監査までは鉄壁に思えたのに、監査でこっそりした抜け穴が見つかる――そんな経験、ありませんか?まさにAccretionで監査したあるSolana DeFiプロジェクトで起きた話がこれです。ようこそ「Advent of Bugs #3」へ。ここでは、optionalなアカウント、善意の設計、そしてSolanaのステートマシンの微妙な振る舞いが、どうやって手数料収集の仕組みを無効化する“フリー乗車”に変えてしまうかを掘り下げます。
Solanaプログラムの開発や監査に深く関わっているなら、これは必読です。本記事では問題の内訳、なぜテストをすり抜けたのか、そして命令サイズを膨らませずに安全性を保つエレガントな修正方法を解説します。博士号は不要—必要なのは明快で実践的な知見だけ。あなたのブロックチェーン運用を一段上に引き上げます。
セットアップ:手数料、スワップ、そして別プログラムからの手助け
想像してください:Solana上に洗練されたDeFiスワッププロトコルがあります。運営維持のため、各取引に小さな手数料を課す。シンプルですよね?ここにひとつの工夫があります――ほとんどのユーザーはあなたのコアなスワップ命令を直接叩かず、フロントエンドやSDKを経由して別の「fee program」を通します。このfee programが手数料を回収し、その後Cross-Program Invocation(CPI)を使ってあなたのメインスワップロジックに渡します。
なぜこの余分な経路があるのか?効率性とモジュール性のためです。fee programは支払いやログ記録などを担当し、あなたのスワップはシンプルで高速に保たれます。しかし、二重徴収を避けるために、メインのスワップ命令は手数料アカウントを「optional」としてマークします。提供されていれば請求、提供されていなければ(CPIフローのように)スキップして続行する、という設計です。
賢いやり方に思えます。ところが、必ずしもそうとは限りません。
バグの本質:Optional Accountsが手数料ゼロの扉を開く
監査中に私たちが見つけた弱点はこうでした:誰でもメインのスワップ命令を直接呼び出し、fee programを完全にすっ飛ばせるのです。単に手数料アカウントを省略するだけで、ユーザーは手数料を回避できます。ポフ—賢い(あるいは悪意ある)トレーダーにとっては無料スワップの完成です。
これは初歩的なミスではありません。開発チームはハッピーパスをテストしていました:SDK経由やウェブサイト経由で、fee programがゲートキーパーとして機能するケースです。直接呼び出し?ほとんどテストされていませんでした。特にDeFiでは抽象化がプログラムの内部を覆い隠すため、生の命令を直接叩くケースは見落とされがちです。さらに、Solanaの命令サイズ制限(約1,232バイト)があるため、開発者は余分なアカウントを嫌い、optionalにすることが魅力的に映るのです。
でもブロックチェーンの世界では「そう感じる」だけでは不十分です。1つの見落としでプロトコルの経済設計が崩れます。深刻度は低〜中かもしれませんが、高頻度でスワップされる環境では実際の収入が脅かされます。
なぜ難しいのか:意図、サイズ、セキュリティのバランス
これを正しく対処するのは簡単ではありません。求められる要件は次の通りです:
- 直接ユーザーは手数料を支払わせる(アカウントを強制する)。
- fee programからのCPIユーザーは二重課金を避けられるようにする。
- 余計なアカウントを増やさず、命令サイズを膨らませない。
信頼できないプログラムからの無差別なCPI?悪夢の種です。あらゆるエッジケースをテストする?時間の無駄になりがちです。
根本原因はここにあります:Solanaプログラムは命令に反応するステートレスな機械です。明確なチェックがなければ、「optional」は「省略可能」に成り下がり、意図は勝手には強制されません。
修正方法:PDA、Signers、そして正しいCPI検証
解決策は?SolanaのProgram-Derived Addresses(PDA)を利用して発信元の暗号学的証明を付与することです。レビューしたコードの要点は次のとおりです。
fee programでは:
- "cpi_authority" のような固定文字列からPDAシードを導出する。
- このPDAを権限としてCPIをメインスワップに署名する。
- 指令にそのPDAをsignerアカウントとして渡す。
メインのスワッププログラムでは:
- CPI権限用のPDAアドレスを期待する。
- 提供されたsignerが期待値と一致するか検証する:もし signer.key() != expected なら、手数料アカウントを強制(unwrapして存在を確認し回収)する。
- 安全のため、チェック後に手数料アカウントをunwrapする――SolanaのRust SDKならこれが簡単に実装でき、実行時の保証になります。
こうすることで:
- 直接呼び出した場合はsignerチェックに失敗し、手数料が強制される。
- fee programからの正当なCPIは通り、optionalは維持される。
- 余分なアカウントは不要。PDAがそのまま証明になる。
これはSolanaのセキュリティプリミティブの模範的な利用法と言えます:決定論的でプログラム管理されたキーとしてのPDAs、意図検証のためのsigners、そして実行時保証のためのunwrap。
実際の動作を見たいですか?以下の注釈付きコードスニペットで重要行をハイライトしています。
Solanaビルダーへの教訓:ハッピーパス以外をテストせよ
このバグは黄金律を強調します:機能していることと正しく動作していることは同義ではない。特にトークンフローや経済設計においては、回避ベクタを常に監査してください。Accretionでの監査のようなツールが早期発見に役立ちます。
プロ向けのコツ:
- プログラム間の信頼にはPDAsを常に使う。
- optionalはsignerチェックで「条件付き」にする。
- 直接呼び出しをテストでシミュレートする—Anchor's test frameworkのようなツールがここで威力を発揮します。
- そして、そのunwrapを二重確認すること;それが安全ネットになります。
もしSolanaの監査や開発をしているなら、@r0bre にDMを送ってください—我々はトップ人材を募集しており、こうした課題を一緒に追究するのが大好きです。
鋭い指摘をしてくれた @brymko に感謝。あなたの経験した「optional」にまつわる最もエグいバグ話は何ですか?下に返信して教えてください。
しっかり防御を。放置すればバグはどんどん蓄積します。
この記事はAccretionでの実際の監査に触発されています。Solanaのセキュリティ深堀りや、memeトークン、DeFiハック、ブロックチェーン知見の最新情報は Meme Insider をフォローしてください。