代理與可升級性漏洞是指當智能合約採用可升級架構,但其升級路徑、初始化機制或管理員控制權設計不良或配置錯誤時所產生的漏洞。攻擊者可以劫持代理管理員或升級角色,進而部署惡意的實作合約、重新初始化合約以奪取所有權,或是繞過初始化與遷移步驟中的關鍵檢查。
這類漏洞會影響所有使用可升級架構的合約類型,包括:
- DeFi: 借貸協議(Lending)、金庫(Vaults)、去中心化交易所(DEXes)。
- NFT: 項目方系列(Collections)、交易市場(Marketplaces)。
- DAO: 治理系統(Governance)、金庫(Treasuries)。
- 跨鏈橋(Bridges): 訊息傳遞器(Messengers)、資產合約(Asset Contracts)。
- L2/跨鏈系統。
常見的升級模式包括透明代理(Transparent Proxy)、UUPS(EIP-1822)、信標代理(Beacon Proxy)以及自定義的「路由—實作(Router-Implementation)」設計。在非 EVM 鏈上(例如 Move 模組、Solana 的程序升級),也存在類似的升級機制,且同樣面臨著高度信任與初始化失效的風險。
代理模式
區塊鏈上的智慧合約一旦部署就無法修改(Immutable)。但專案需要更新功能或修復 Bug 怎麼辦?開發者於是發明了代理模式(Proxy Pattern)。
你可以把這想像成開一家餐廳:
- 代理合約(Proxy): 這是「餐廳的店面(地址)」。它不負責做菜,只負責收錢、接單,並把客人的要求原封不動地傳給後廚。所有的資產(使用者的錢、代幣餘額)都存在店面(Proxy)裡。
- 邏輯合約(Implementation / Logic): 這是「後廚的廚師」。它負責研發菜單(執行邏輯、加減乘除)。如果廚師生病或菜色要升級,專案方只需要換一個新廚師,但餐廳的地址(Proxy)永遠不變。
兩者之間溝通的橋樑叫做 delegatecall(委託調用)。這是一種特殊的程式碼調用方式:執行「新廚師」的程式碼,但扣錢和記帳都在「店面(Proxy)」的帳本上。
漏洞容易發生的地方
1. Upgrade and admin roles
- 技術本質: 決定「誰能修改 Proxy 合約中指向 Implementation 的地址變數」。
- 關注點:
- 權限控制: 升級權限是否被嚴格限制(例如由多簽錢包或 DAO 治理合約控制)?如果升級函數暴露給公眾,任何人都能替換邏輯。
- 存儲佈局相容性: 升級到新合約時,新合約的變數宣告順序必須與舊合約完全一致,且只能在末尾追加(Append-only)新變數。如果任意改變順序,會導致新程式碼讀取到錯誤的歷史數據。
2. Initialization and re-initialization
- 技術本質: 代理架構中,Implementation 的
constructor在部署時只會影響它自己的環境,無法改變 Proxy 的狀態。因此,Proxy 必須依賴一個普通的函數(通常叫initialize())來設定初始狀態(如 Owner、代幣總量)。 - 關注點:
- Initialization Guard: 例如使用 OpenZeppelin 的
initializer修飾符,確保該函數只能被呼叫一次。 - 再初始化(Re-initialization): 當合約需要大版本升級(例如 V1 升級到 V2)時,會用到OpenZeppelin 的
reinitializer(2)。如果這些升級初始化函數沒有做好版本控制或權限檢查,就會留下破綻。
- Initialization Guard: 例如使用 OpenZeppelin 的
3. Proxy delegation
- 技術本質: 理解
delegatecall的運作脈絡(Context)。 - 關注點:
- 當 A 合約
delegatecallB 合約時,程式碼雖然是 B 的,但執行環境(Storage、Balances、msg.sender、msg.value)全部都在 A 合約中。 - 舉例: 使用者呼叫 Proxy,
msg.sender是使用者。Proxy 透過delegatecall呼叫 Implementation,此時在 Implementation 的程式碼裡,msg.sender依然是使用者,而不是 Proxy。如果開發者誤以為msg.sender是 Proxy,就會導致權限邏輯大亂。
- 當 A 合約
4. Storage layout
- 技術本質: EVM 儲存資料是基於 Slot,每個 Slot 大小為 32 位元組,從 Slot 0、Slot 1 依序往下排。
- 關注點:
- 插槽衝突(Slot Collision): Proxy 合約本身通常需要儲存一個變數叫做
_implementation(記錄目前後端地址)。如果這個變數不小心跟 Implementation 合約的某個業務變數(例如owner或balance)佔用了同一個 Slot 編號,兩者就會互相覆蓋。
- 插槽衝突(Slot Collision): Proxy 合約本身通常需要儲存一個變數叫做
5. Timelocks and governance
- 技術本質: 升級合約的「流程防禦機制」。
- 關注點:
- 時間鎖(Timelock): 限制管理員在發出升級提案後,必須等待 24~48 小時才能真正執行。這給了社群檢查新程式碼的時間,若發現專案方意圖不軌(Rug Pull)或程式碼有 Bug,使用者有時間撤資。
- 回滾能力(Rollback Capability): 如果新升級的 V2 版本上線後一分鐘內被發現有嚴重漏洞,系統是否有權限或機制在第一時間快速「退回」到穩定的 V1 版本。
常見攻擊方法
1. Unprotected upgrade functions
- 手法: 開發者在編寫
upgradeTo()或upgradeToAndCall()函數時,忘記加上onlyOwner或權限檢查修飾符。 - 後果: 攻擊者直接調用該函數,傳入自己部署的惡意合約地址。Proxy 隨即指向黑客的合約,黑客直接寫一個
withdrawAll()函數把 Proxy 裡儲存的所有資產(DeFi 資金池裡的錢)全部提走。
2. Re-initialization
- 手法: 專案方在合約升級時,可能留下了一個用於 V2 初始化的函數(如
initializeV2()),但沒有正確限制只有 Admin 能呼叫,或者沒有綁定正確的初始化版本號。 - 後果: 攻擊者強先呼叫這個函數。由於初始化函數會重設關鍵變數,攻擊者利用它直接覆蓋掉原本的
owner地址,將合約所有權變更為自己,隨後掌控整個協議。
3. Initialization through delegatecall
- 手法: 許多代理架構允許在升級的當下,順便透過
delegatecall去執行新合約的初始化邏輯(即upgradeToAndCall(impl, data))。 - 後果: 如果這個入口沒鎖好,攻擊者可以傳入自定義的
data(惡意參數)。因為是以delegatecall執行,攻擊者傳入的參數可以直接竄改 Proxy 合約核心儲存區(Storage)的任何資料,包含直接改寫管理員權限或資產歸屬。
4. Storage collision leading to overwrites
- 手法: 當 Proxy 合約與 Implementation 合約在 Slot 分配上發生重疊時(例如雙方都試圖寫入 Slot 0)。
- 後果: 攻擊者透過正常業務邏輯去修改 Implementation 的變數(例如去註冊一個名稱,觸發合約修改 Slot 0 的資料)。然而在 Proxy 的視角裡,Slot 0 存的是
admin的地址。攻擊者透過這種「隔山打牛」的方式,利用合約自身的邏輯把 Slot 0 改成了自己的地址,平白無故變成了合約的管理員。
這些問題(代理與升級漏洞)在技術本質上,往往跟『存取控制(Access Control)』是重疊的;但因為代理與升級機制出錯時,會對整個系統造成『毀滅性的連帶打擊(Systemic Impact)』,所以必須把它們拉出來,當作一個獨立的大類別來嚴肅對待。
漏洞範例
Vulnerable Upgradeable Proxy Admin
以下是一個存在漏洞的智能合約範例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract VulnerableProxyAdmin {
address public admin;
address public implementation;
constructor(address _implementation) {
// Critical: no way to set custom admin; implicitly trusts deployer logic
admin = msg.sender;
implementation = _implementation;
}
function upgrade(address newImplementation) external {
// Missing: access control (only admin) and sanity checks
implementation = newImplementation;
}
}
漏洞分析:
1.升級函數完全沒有權限控制(No access control on upgrade)
- 預期邏輯: 本來開發者的意圖應該是「只有
admin(管理員)可以更換新廚師(Implementation)」。 - 現實慘劇: 因為函數上只有一個
external(公開調用),完全沒有加上require(msg.sender == admin, "Not admin");或者是 OpenZeppelin 的onlyOwner修飾符。 - 駭客攻擊手法: 任何路人、甚至是黑客,都可以自己部署一個寫滿惡意邏輯的合約(例如:只要有人轉錢進來,就立刻把錢轉到黑客地址),然後直接呼叫這個
upgrade(malicious contract address)。從這一秒開始,這個代理系統就被黑客全面接管了。
2.完全沒有對新地址做安全檢查(No checks on newImplementation)
除了權限大開之外,這個函數對傳入的 newImplementation(新合約地址)採取「完全信任」的態度,這會引發以下幾種災難:
- 零地址檢查(Non-zero address check): 如果有人不小心呼叫
upgrade(0x0000000000000000000000000000000000000000)(傳入空地址)。合約會直接指向一個不存在的地方。一旦指向零地址,後續所有透過這個 Proxy 進行的交易都會直接失敗,這等同於直接把合約給「變磚(Brick)」、資產永久鎖死。 - 介面相容性檢查(Interface compatibility): 好的升級架構(例如 UUPS 模式中的
proxiableUUID()檢查)會去驗證「新地址到底是不是一個真正相容的智慧合約」。如果隨便傳入一個普通的錢包地址(EOA),或者是功能完全不搭嘎的合約,Proxy 轉發過去後就會因為找不到對應的函數而直接崩潰。
Initialization & Re-Initialization Risks
以下是一個存在漏洞的智能合約範例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract VulnerableLogic {
address public owner;
// Missing initializer guard
function initialize(address _owner) external {
owner = _owner;
}
}
漏洞分析:
開發者的預期邏輯
開發者知道在可升級架構中,代理合約(Proxy)無法讀取實作合約(Implementation)的 constructor。所以他想:「那我就寫一個普通的函數叫 initialize,來手動當作建構函數使用。」
開發者預期的安全部署腳本(Script)是這樣的:
- 步驟 A: 部署實作合約
VulnerableLogic。 - 步驟 B: 部署代理合約
Proxy,並將內部指針指向VulnerableLogic。 - 步驟 C(關鍵): 部署完成後的第一秒鐘,開發者自己立刻發送一筆交易去呼叫
Proxy.initialize(owner address)。
開發者天真地以為:「反正我部署完就『馬上』呼叫了,只要我手腳夠快把 owner 改成我,這家店以後就是我的了,路人就算想呼叫也只是幫我重新設定一次而已,沒差吧?」
攻擊手法
- 劇本一:攔截初始化交易(Front-running 奪權)
區塊鏈是一個「黑暗森林」,駭客絕對不會跟開發者比手速,他們使用的是自動化監控機器人(Mempool Sniffer)與Front-running手段。
- 監控: 駭客的機器人在區塊鏈的「交易暫存區(Mempool)」盯著。當開發者執行到步驟 C,發出
initialize(owner address)的交易時,駭客的機器人瞬間偵測到了。 - 強跑(Front-run): 駭客立刻發出一筆一模一樣的交易,呼叫
Proxy.initialize(hacker address),但是給出極高的手續費(Gas Fee)。 - 鳩佔鵲巢: 區塊鏈的礦工(驗證者)看到駭客給的錢比較多,便把駭客的交易排在前面。
- 慘劇: 駭客的交易先執行,代理合約(Proxy)底層的
owner被寫成了駭客。一秒後,開發者的交易才執行,此時可能因為邏輯衝突失敗,或者開發者只是傻傻地把owner重新寫了一次(但此時合約內的最高管理員已經被定性為駭客的眼線了)。
- 劇本二:直接攻擊「無人認領」的實作合約
如果開發者運氣好,步驟 C 成功了,代理合約(Proxy)裡的 owner 確實變成了開發者。但不代表這樣就安全,駭客會轉向去攻擊那個單獨躺在鏈上的「實作合約(VulnerableLogic)本身」。
- 漏洞: 雖然 Proxy 裡的
owner被設定了,但是實作合約(VulnerableLogic)自己本體裡面的owner變數依然是空白的(0x000…)! - 奪權: 駭客直接呼叫「實作合約」的地址,執行
initialize(hacker address)。這時候,駭客變成了實作合約本體的owner。 - 自殺攻擊(Self-destruct): 如果這個實作合約內部包含、或者未來升級包含了
selfdestruct(自毀)的邏輯。身為實作合約 Owner 的駭客,可以直接下達自毀指令,把實作合約本體的程式碼從區塊鏈上抹除。 - 結果: 代理合約(Proxy)頓時失去了指向的目標(指向了一片虛無)。使用者存放在 Proxy 裡面的幾千萬美元會永遠鎖死,再也無法提現或呼叫,整個協議直接報廢。
2025 Case Studies
Kinto 協議遭駭事件(2025 年 7 月,損失 155 萬美元)
攻擊者利用了未初始化的 ERC1967 代理合約。他們偵測到剛部署、但尚未被正確初始化的代理合約,隨後直接對其進行初始化,並指向包含「潛伏後門(Dormant Backdoors)」的惡意實作合約。幾個月後,攻擊者啟動了該後門,將代理合約升級為惡意程式碼,並直接鑄造(Mint)大量的 K 代幣,藉此捲走 155 萬美元。該漏洞的核心在於:未受保護的初始化機制,允許任何人都能直接成為該代理合約的管理員(Proxy Admin)。
全域未初始化代理連環攻擊(2025 年,全網協議損失超過 1,000 萬美元)
這是一場針對多條 EVM 相容鏈上、多個未初始化 ERC1967 代理合約所展開的大規模無差別攻擊活動。攻擊者利用自動化掃描器,專門在正統開發者來得及呼叫初始化之前,劫持並偵測剛部署好的代理合約,隨後用惡意的實作合約搶先完成初始化。這些埋入的暗門在合約中潛伏了數個月,成功規避了常規的程式碼審計(Audits)。當暗門被激活時,攻擊者便能直接升級這些代理合約並將裡面的資金全數掏空。