僅供教育參考,不構成投資建議;投資可能產生損失。
直接答案
當實作程式碼以不同於現有狀態建立時布局所賦予的含義,讀取或寫入代理的儲存槽,就會發生代理儲存衝突。升級可能因此把餘額當作位址、清空擁有者、破壞映射的根槽,或覆寫升級控制資料。
使用 delegatecall 時,實作位元組碼在代理的上下文中執行:儲存、餘額和 address(this) 都屬於代理。變數名稱不會儲存在鏈上,因此 EVM 只按新程式碼計算出的槽位和位元組偏移操作。
因此,升級必須保持已部署布局相容,而不只是成功編譯或公開相同函式。授權升級之前,應將編譯器產生的布局與確切的已部署版本比較。
運作原理
Solidity 通常在繼承關係經過 C3 線性化後,按宣告順序從槽 0 起放置狀態變數。小於 32 位元組的值可能共用一個槽;結構和陣列還有額外的放置規則,而映射和動態陣列會根據基礎槽推導資料位置。基礎槽一旦移動,其衍生資料的位置也會改變。
需要稽核以下四類不同的衝突邊界:
- 代理與實作之間: 實作位址、管理員等代理自有欄位不得占用應用狀態所用的槽。ERC-1967 為實作、信標和管理員資料規定了編譯器常規分配不會觸及的標準槽。
- 舊實作與新實作之間: 現有應用變數必須保持相容的槽、偏移和型別。在末尾追加變數可能安全,但插入、重排、刪除或改變變數型別會重新解釋已有儲存字。
- 繼承關係: 向基底合約新增狀態或改變繼承順序,可能移動衍生合約宣告的儲存,即使衍生合約原始碼沒有變化。
- 預留或命名空間儲存: 正確使用儲存間隙可以為基底合約預留空間,ERC-7201 風格的命名空間可以隔離布局。但兩者都不允許任意修改既有布局內部。
範例
假設版本 1 的布局如下:
uint256 totalAssets; // slot 0
address owner; // slot 1版本 2 錯誤地在開頭插入一個變數:
bool paused; // slot 0, offset 0
uint256 totalAssets; // slot 1
address owner; // slot 2升級後,paused 會讀取舊 totalAssets 的低位位元組,新 totalAssets 會把舊 owner 儲存字當作整數讀取,而 owner 會讀取槽 2 中原有的內容,通常為零。原始儲存字仍然存在,但新程式碼賦予了它們不同含義。交易可能成功執行,卻以錯誤解釋後的狀態進行授權或記帳。
風險與升級檢查
- 為兩個實作產生儲存布局輸出,並以實際部署的參考合約為準,比較槽、偏移、型別和繼承資訊。
- 對傳統線性布局,只在末尾追加新變數。除非已證明相容,否則不要重排現有欄位、改變型別、刪除後複用,也不要修改基底合約。
- 使用儲存間隙時,應按實際占用的預留槽數精確縮減間隙。使用命名空間儲存時,須確保命名空間識別碼唯一,並驗證每個現有命名空間內的變更。
- 不要把 ERC-1967 當作完整保護。它將代理中繼資料與編譯器分配的應用槽分開,但不能保證兩個實作布局彼此相容。
- 在分叉網路或狀態快照上測試升級及任何重新初始化函式。執行前後都要核對擁有者、角色、餘額、授權額度、映射項目、暫停狀態、實作槽以及回復或緊急控制。
如果升級已疑似造成衝突,在治理權限允許時,應暫停後續升級和狀態變更呼叫。保留升級前區塊號與實作位元組碼,比較受影響槽位的原始儲存,並由合格的合約工程師設計遷移方案、接受獨立審查。沒有經驗證的儲存映射就反覆升級,可能破壞更多原本可恢復的狀態。
常見誤解
- 「變數名稱沒變,布局就是安全的。」 名稱不決定儲存位置,型別、順序、打包、繼承和命名空間規則才決定。
- 「刪除變數就能釋放槽位供複用。」 代理儲存會持續存在。除非經過嚴格審查的遷移將舊值清除或轉換,否則複用槽位只是給舊儲存字賦予新含義。
- 「一筆測試交易成功就證明升級相容。」 測試可能只觸及少量槽位。布局驗證和狀態差異測試必須涵蓋特權欄位、打包值、映射、陣列和繼承儲存。
相關主題
來源
- 智慧合約簡介 - Solidity Documentation(存取日期:2026-08-21)
- 儲存與暫態儲存中狀態變數的布局 - Solidity Documentation(存取日期:2026-08-21)
- ERC-1967:代理儲存槽 - Ethereum Improvement Proposals(存取日期:2026-08-21)
- 編寫可升級合約 - OpenZeppelin Documentation(存取日期:2026-08-21)