---
title: "12 秒，我的測試網私鑰被掃空了"
description: "「你可以幫我看一下這兩個的鏈上紀錄嗎，是不是私鑰外洩了？」"
author: "kidneyweakx"
published: 2026-09-27T03:00:00.000Z
updated: 2026-10-29T09:06:00.000Z
lang: zh-TW
tags: ["ai", "區塊鏈", "黑客松", "ens", "uniswap", "claude-code", "資安"]
canonical: https://kidneyweakx.com/blog/session-unmapped-leaked-key
---

# 12 秒，我的測試網私鑰被掃空了

> 「你可以幫我看一下這兩個的鏈上紀錄嗎，是不是私鑰外洩了？」

黑客松截止前三十幾個小時，我在 Sepolia 上轉了 0.2 顆測試 ETH 進部署用的錢包。

**12 秒後，錢就不見了。**

這篇是那個 session 的故事。它大約燒了 **4.1 億 token**，本來的任務是「幫我把 ENS v2 跟 Uniswap 的機制寫成合約、部署上鏈」，結果中間插播了一段資安事件，最後還碰上有人 force push main。

這是我在 9 月的 ETHTokyo 2026 做的作品。先講結果：**沒拿名，但做得很爽。**

<br />

## 原本的設計：名字樹就是族譜

先講一下我們想做什麼。UNMAPPED（原本叫 Unwritten）是一個用 AI 生成遊戲世界的 app，每個世界是一張「卡帶」，而且可以被別人分岔出去 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 個檔案一個 commit。

接下來只剩部署。

<br />

## 「錢還不夠」

我說「sepolia eth都處理好了」。它先做三件唯讀的檢查：`.env` 裡有沒有私鑰（不印出來）、那個地址的餘額、目前的 gas price。

> 先別急著部署：`.env` 裡這把私鑰的地址，餘額還不夠。
> 餘額：0.00046 Sepolia ETH

我以為是還沒到帳，過了幾分鐘問「有了嗎」：

> 還沒有，錢還不夠。
> `.env` 裡的私鑰換過了（檔案 01:41 被改過）……上次檢查的舊地址 `0x0c9F…2d34`，現在是 0 ETH。

**我明明有轉錢進去，為什麼兩個地址都沒錢？**

截止前三十幾個小時看到這種東西，第一個念頭就不是「到帳慢」了。

<br />

## 鏈上的 12 秒

我請它查兩個地址的鏈上紀錄。它先講清楚自己做過什麼：之前所有 dry run 用的都是每次隨機產生的臨時帳號，**從來沒用 `.env` 的私鑰送過任何交易**。然後回來的結論是：

> **舊私鑰確定外洩了**，而且早在一年多前就被偷，不是這個專案造成的。

經過是這樣（時間照它報的台灣時間）：

- **00:36:12**：我從新地址轉了 0.1998 ETH 進舊地址
- **00:36:24**，**12 秒後**：有人用這把私鑰簽出一筆 **EIP-7702 交易**，把這個地址委派給一個 sweeper 合約，同一筆交易就把 **0.4664 ETH** 全部轉走
- **00:40:48**：又有 0.05 ETH 從 faucet 進來，**委派的程式碼在同一筆交易裡就自動轉走了**

收錢的那個地址，在 Sepolia 上累積了 **117 ETH**，最近 50 筆轉入來自 18 個不同地址。**這是一隻專門掃外洩私鑰的機器人。**

再往回查，同一把私鑰在 Arbitrum One 主網上，**2025-07-20** 就把 1.86 USDC 和 0.0035 ETH 轉到同一個地址了。

也就是說，這把鑰匙一年多前就外洩了，我一直不知道，還拿它當黑客松的部署錢包。至於當初是從哪裡漏出去的，鏈上看不出來，**能確定的只有：它早就不是我一個人的鑰匙了。**

<br />

## EIP-7702 讓「外洩」變成「詛咒」

以前私鑰外洩，機器人要搶在你之前把錢轉走，是一場 gas 競賽。

EIP-7702 之後不一樣了。攻擊者把你的 EOA **委派**給一個合約，從此任何轉進來的錢，**在同一筆交易裡就被合約自動轉走**。不用搶、不用盯，連 faucet 給的 0.05 都逃不掉。

AI 的原話：

> **這個地址已經廢了。** 7702 委派還掛在上面，之後轉進去的任何東西都會被立刻轉走，包括 faucet 領的錢，請不要再往裡面轉。

**以前外洩的私鑰是一個漏洞，現在是一個永久的黑洞。**

<br />

## 換鑰匙，然後把錢包從 app 裡拿掉

處理很直接：我叫它「你創一個我 faucet 好了」。它產生一把新私鑰寫進 `.env`（在 `.gitignore` 裡、權限 600），**私鑰本身不印在任何地方，只給我地址**。新的那把在 Sepolia 上 nonce 0、餘額 0、沒有委派程式碼。

部署之前，它還先把部署腳本改成可以安全重跑：萬一中途失敗，已經部署好的合約會跳過，不會重複花 gas。commit 之前也再確認一次上鏈的 log 裡沒有私鑰。

然後 `unmapped.eth` 註冊、三份合約上 Sepolia，大約 4–5 分鐘。

這件事之後，我的下一個需求是：

> app端不要碰到錢包會比較好 勁量只用裡面有的那個passkey

於是玩家這邊改成 passkey 帳戶：`PasskeyAccountFactory` 用公鑰推算地址、第一次使用才部署；gas 由一個代付站出。最後它在 app 裡把第一個世界走完一整輪：

> 發行 → 拍賣 → 結算 → 開池 → 買入 → 發分潤。全部都是在 app 裡操作、真的上鏈，玩家從頭到尾沒有連錢包。

買入那一筆是 10 USDC 換到 536 AETHERLAND，這個世界因此累積了 5.408 AETHERLAND 的分潤。

<br />

## 插曲：有人 force push 了 main

正當另一個 session 準備把這批改動分兩次 commit 的時候，訊息傳過來：

> 遠端 main 被一個 Codex 任務改寫並 force push（它說是你要求的），新的 HEAD 是 7a04adf，舊的 commit 雜湊全部換掉了。

我只回了一句「你確保一下」。它沒有直接相信那個 session 的計畫，而是自己 fetch 新的 main，逐一比對被改寫的檔案有沒有跟市場這批重疊，確認工作區不會默默把那些改動蓋回去。

聽起來很恐怖，其實沒什麼：就是有衝突，總要有人改，**誰改都沒差**。那時候同一個 repo 上同時有 Claude 好幾個 session 加上 Codex 在跑，一個做市場、一個做第二階段、一個管 commit，各自跑各自的 E2E。

**多 session 各自跑 E2E，效率是真的高。** 代價就是偶爾要有人停下來確認一下，大家改的還是不是同一份東西。

那時候離截止（9/27 09:00 JST）大約還有 28 小時。合約編譯出來 20,595 bytes，離 24,576 的上限還有一點空間。

<br />

## 學到的事

1. **測試網的錢包也是錢包。** 這把私鑰在主網上被偷過，我卻拿它來部署。Sepolia ETH 不值錢，但同一把鑰匙在別的鏈上可能值錢
2. **EIP-7702 改變了外洩的後果。** 外洩的地址不是「要小心」，是「直接報廢」，連轉進去的測試幣都會被吃掉
3. **讓 AI 先做唯讀檢查。** 它在送出任何交易前，先查餘額、查私鑰對應的地址、查 gas。就是這個習慣，讓「錢不見了」在部署前就被抓出來，而不是部署到一半
4. **玩家不該碰到私鑰。** 這件事之後我更確定 app 端要全部走 passkey，連「錢包」這兩個字都不要出現

說真的，如果沒有這次被掃，我大概會一直用那把鑰匙到某一天在主網上出事。

**12 秒很短，但比一年多前外洩之後、我一直沒發現的那段時間誠實多了。**

ETHTokyo 最後沒拿名。但一個被掃空的錢包、一套從發行到分潤都真的上鏈的流程、還有一群 session 一起趕死線的體驗，對我來說已經值回票價。

<!-- 圖：Etherscan 上那筆 7702 交易截圖（記得遮地址）、族譜名字樹示意圖 -->
