QUEST LOG · September 27, 2026· UPDATED October 29, 2026

12 秒で、テストネットの秘密鍵が空っぽにされた

「この 2 つのオンチェーン履歴を見てもらえる? 秘密鍵、漏れてない?」

ハッカソンの締め切りまで 30 時間ちょっと。私は Sepolia でテスト用 ETH を 0.2 枚、デプロイ用のウォレットに送りました。

12 秒後、そのお金は消えていました。

これはそのセッションの話です。消費したトークンはおよそ 4.1 億。もともとのタスクは「ENS v2 と Uniswap の仕組みをコントラクトにして、オンチェーンにデプロイして」だったのに、途中でセキュリティインシデントが割り込み、最後には誰かが main を force push するというおまけ付きでした。

これは 9 月の ETHTokyo 2026 で作った作品です。先に結果を言っておくと、入賞はしなかったけど、作っていてめちゃくちゃ楽しかった。


# もともとの設計:名前ツリーがそのまま家系図

まず、何を作ろうとしていたのか。UNMAPPED(旧名 Unwritten)は AI でゲームの世界を生成するアプリで、ひとつひとつの世界が「カートリッジ」になっていて、ほかの人がそこから分岐して remix できます。

最初に、ChatGPT と話し合って出てきた設計を渡して「今の仕組み、これでいけそう?」と聞きました。NFC の物理カード(いったん見送り)、Uniswap は絶対に入れる、もっといい EIP を混ぜられないか、などを行ったり来たりして、最終的にひと言にまとまりました:

ENS はこの世界が何者で誰のものかを決め、Uniswap はそれがいくらでどう取引されるかを決め、そして hook が取引のお金を家系図に沿って名前の持ち主に分配する。

もう少し具体的に言うと:

  • 名前ツリー = 家系図:night.mushroom.zelda.unmapped.eth を右から左に読むと、「zelda が mushroom に改変され、mushroom がさらに night に改変された」になる
  • CCA でフェアローンチ:各世界のトークンは Uniswap の Continuous Clearing Auction で販売し、remix は上流の世界のトークンで入札する。つまり 新しい remix がひとつ発行されるたびに、その祖先を買っていることになる
  • v4 hook でレベニューシェア:オークションが卒業すると自動で v4 プールが開き、hook が取引のお金を家系図に沿って名前の持ち主に分配する

「ensv2 と cca で行くのがいちばん良さそうだよね、まず lineage market を一式書いて」と言ったら、本当に書いてくれました。24 ファイル、1 commit。

あとはデプロイするだけ。


# 「まだ残高が足りません」

「sepolia eth は全部用意できた」と伝えると、まず読み取り専用のチェックを 3 つやってくれました。.env に秘密鍵があるか(中身は表示しない)、そのアドレスの残高、現在の gas price。

まだデプロイしないでください:.env の秘密鍵に対応するアドレスの残高が足りません。
残高:0.00046 Sepolia ETH

まだ着金していないだけだと思って、数分後に「来た?」と聞くと:

まだです、残高が足りません。
.env の秘密鍵が差し替えられています(ファイルは 01:41 に変更されています)……前回チェックした旧アドレス 0x0c9F…2d34 は、現在 0 ETH です。

ちゃんと送金したはずなのに、なんで両方のアドレスとも空っぽなの?

締め切り 30 時間ちょっと前にこんなものを見たら、真っ先に浮かぶのは「着金が遅いだけ」ではありません。


# オンチェーンの 12 秒

両方のアドレスのオンチェーン履歴を調べてもらいました。まず、自分が何をしたかをはっきりさせてくれます。これまでの dry run はすべて毎回ランダムに生成した使い捨てアカウントで、.env の秘密鍵でトランザクションを送ったことは一度もない。そのうえで返ってきた結論は:

旧秘密鍵は確実に漏洩しています。しかも 1 年以上前に盗まれていて、このプロジェクトが原因ではありません。

経緯はこうです(時刻は報告された台湾時間):

  • 00:36:12:私が新アドレスから旧アドレスに 0.1998 ETH を送金
  • 00:36:24、12 秒後:何者かがこの秘密鍵で EIP-7702 トランザクションに署名し、このアドレスを sweeper コントラクトに委任。同じトランザクションで 0.4664 ETH をまるごと送金
  • 00:40:48:faucet からさらに 0.05 ETH が入ってきたが、委任されたコードが同じトランザクション内で自動的に送り出した

受け取り側のアドレスは Sepolia 上で 117 ETH を貯め込んでいて、直近 50 件の入金は 18 個の異なるアドレスから。漏洩した秘密鍵を専門に刈り取るボットです。

さらに遡ると、同じ秘密鍵は Arbitrum One のメインネットで、2025-07-20 の時点ですでに 1.86 USDC と 0.0035 ETH を同じアドレスに送っていました。

つまりこの鍵は 1 年以上前に漏れていて、私はずっと気づかないまま、ハッカソンのデプロイ用ウォレットに使っていたわけです。どこから漏れたのかは、チェーンを見てもわかりません。確かなのはひとつだけ。この鍵はとっくに私だけのものではなくなっていた。


# EIP-7702 は「漏洩」を「呪い」に変える

以前は秘密鍵が漏れても、ボットはこちらより先に資金を動かす必要がありました。gas の競争です。

EIP-7702 以降は違います。攻撃者があなたの EOA をコントラクトに委任すると、それ以降に入ってきたお金は同じトランザクション内でコントラクトが自動的に送り出す。競争も見張りもいらない。faucet でもらった 0.05 すら逃げられません。

AI の言葉をそのまま引用すると:

このアドレスはもう使えません。 7702 の委任が付いたままなので、今後送ったものは faucet で受け取った分も含めてすべて即座に送り出されます。これ以上このアドレスに送金しないでください。

かつて、漏洩した秘密鍵は脆弱性だった。今は永久のブラックホールだ。


# 鍵を替えて、アプリからウォレットを外す

対処はシンプルです。「新しいの作って、faucet するから」と頼むと、新しい秘密鍵を生成して .env に書き込み(.gitignore 済み、パーミッション 600)、秘密鍵そのものはどこにも表示せず、アドレスだけを渡してくれました。新しい鍵は Sepolia 上で nonce 0、残高 0、委任コードなし。

デプロイの前には、デプロイスクリプトも安全に再実行できるよう修正してくれました。途中で失敗しても、デプロイ済みのコントラクトはスキップされるので、gas を二重に払わずに済みます。commit の前には、オンチェーンのログに秘密鍵が含まれていないかも再確認。

そして unmapped.eth を登録し、3 つのコントラクトを Sepolia にデプロイ。だいたい 4〜5 分でした。

この件のあと、次に出した要望は:

アプリ側はウォレットに触らないほうがいい なるべく中にある passkey だけ使って

というわけで、プレイヤー側は passkey アカウントに切り替え。PasskeyAccountFactory が公開鍵からアドレスを算出し、初回利用時にだけデプロイ。gas は paymaster が肩代わりします。最後に、アプリ内で最初の世界をまるごと一周通してくれました:

発行 → オークション → 精算 → プール開設 → 購入 → 分配。すべてアプリ内で操作し、本当にオンチェーンで実行。プレイヤーは最初から最後までウォレットを接続していません。

購入は 10 USDC で 536 AETHERLAND。これでこの世界には 5.408 AETHERLAND の分配が貯まりました。


# 幕間:誰かが main を force push した

別のセッションがこの変更を 2 回に分けて commit しようとしていた、ちょうどそのとき、メッセージが届きました:

リモートの main が Codex のタスクによって書き換えられ、force push されました(あなたの指示だと言っています)。新しい HEAD は 7a04adf で、古い commit ハッシュはすべて変わっています。

私が返したのは「ちゃんと確認しといて」のひと言だけ。そのセッションの計画を鵜呑みにはせず、自分で新しい main を fetch し、書き換えられたファイルがマーケット側の変更と重なっていないかを一つずつ突き合わせ、作業ツリーがそれらの変更を黙って上書きし戻さないことを確認してくれました。

怖そうに聞こえますが、実はたいしたことはありません。コンフリクトがあれば誰かが直すしかないし、誰が直しても同じです。そのとき同じ repo では Claude のセッションが何本かと Codex が同時に走っていて、ひとつはマーケット、ひとつは第 2 フェーズ、ひとつは commit 担当で、それぞれが自分の E2E を回していました。

複数のセッションがそれぞれ E2E を回すのは、本当に効率がいい。 その代わり、ときどき誰かが立ち止まって、みんながまだ同じものを触っているかを確認する必要があります。

その時点で締め切り(9/27 09:00 JST)まで残り約 28 時間。コントラクトはコンパイル後 20,595 bytes で、上限の 24,576 まではまだ少し余裕がありました。


# 学んだこと

  1. テストネットのウォレットもウォレットだ。 この秘密鍵はメインネットで盗まれたことがあるのに、私はそれでデプロイしていた。Sepolia ETH に価値はなくても、同じ鍵が別のチェーンでは価値を持っているかもしれない
  2. EIP-7702 は漏洩の帰結を変えた。 漏洩したアドレスは「気をつけて使う」ものではなく「即廃棄」。送ったテストコインまで食われる
  3. AI にはまず読み取り専用のチェックをさせる。 トランザクションを送る前に、残高、秘密鍵に対応するアドレス、gas を確認する。この習慣のおかげで、「お金が消えた」がデプロイの途中ではなく、デプロイ前に見つかった
  4. プレイヤーに秘密鍵を触らせるべきではない。 この件で、アプリ側はすべて passkey にして、「ウォレット」という言葉すら出さないようにすべきだと一層確信した

正直なところ、今回刈り取られていなければ、いつかメインネットで事故るまで、あの鍵を使い続けていたと思います。

12 秒は短い。でも、1 年以上前に漏れてから私がずっと気づかなかったあの期間よりは、ずっと正直だ。

ETHTokyo は結局入賞なし。それでも、空っぽにされたウォレットがひとつ、発行から分配まで本当にオンチェーンで動くフロー、そしてたくさんのセッションと一緒に締め切りに追われた体験。私にとっては十分に元が取れました。

(この記事は 90% を AI が書いています。なにせ 1 週間後には Solana でハッカーに JitoSOL を全部持っていかれたので、自分で作らない限り、もう Solana の DeFi には手を出さないと思います)

公開中のアプリ

iOS・Android で無料。

AniSeekr

AniSeekr

MAL・AniList・Bangumi など 6 つのサービスの視聴記録をまとめて、作品の聖地も巡れるアニメ伴走アプリ。

Solid(ar)ity

Solid(ar)ity

プライバシー重視の ID ウォレット。ゼロ知識証明と署名付き名刺を対面で交換、中央サーバーなし。