僅供教育參考,不構成投資建議;投資可能產生損失。
直接答案
delegatecall 會在呼叫者合約的上下文中執行目標合約的程式碼。呼叫者保留自己的儲存空間、餘額和 address(this),而 msg.sender 與 msg.value 則保留原始呼叫中的值。
這種行為讓代理合約、函式庫和智慧錢包模組成為可能,但也會把呼叫者的實際權限交給受委派的程式碼。儲存寫入、資產轉移、授權或外部呼叫均以呼叫者而非提供程式碼的目標合約身分執行。
應將每個可觸及的 delegatecall 目標都視為特權程式碼。安全性取決於目標選擇規則、相容的儲存布局、初始化狀態、升級控制,以及該筆交易實際使用的確切實作。
運作原理
普通外部呼叫會讀寫被呼叫者自己的儲存空間。使用 delegatecall 時,目標位元組碼則在呼叫者的儲存空間上執行:一條 SSTORE 指令會更改屬於呼叫者的儲存槽。目標程式碼中的變數名稱在執行階段並不重要,只有計算得出的儲存槽位置才重要。
因此需要稽核以下四個邊界:
- 目標控制: 確定目的地是固定的、由使用者選擇的、透過註冊表解析的,還是可由管理員更改的。
- 儲存相容性: 比較每個實作版本的變數順序、型別、繼承關係、儲存間隙,以及命名空間槽或標準化槽。
- 初始化與授權: 確認初始化器無法重複執行,並確認升級或模組管理函式會強制檢查預期的呼叫者與治理延遲。
- 回傳值處理: 驗證失敗會向上傳播,且回傳資料會按照預期型別解碼;低階呼叫不會提供 Solidity 通常具備的合約型別檢查。
ERC-1967 將實作、信標和管理員位址放在正常編譯器配置範圍之外的標準化儲存槽中,從而減少代理合約的儲存衝突。但它並不能證明某個實作是安全的,也不能證明經授權的升級是善意的。
範例
假設某個錢包將 owner 儲存在 slot 0。一個外掛程式在編譯時把 counter 放在 slot 0,錢包透過 delegatecall 呼叫該外掛程式時,外掛程式會遞增這個計數器。
由於儲存空間屬於錢包,這次寫入會改變錢包的 owner 值。如果寫入後的字編碼了攻擊者控制的位址,後續授權檢查就可能把攻擊者識別為所有者,儘管外掛程式本身從未持有錢包資產。
成功的交易收據無法區分預期狀態變更和有害狀態變更。因此,交易模擬應針對確切的代理位址和實作位址,檢查儲存差異、資產與授權變化、發出的事件以及下游呼叫。
風險
- 任意目標執行: 使用者控制或驗證薄弱的目標可以利用呼叫者的權限執行惡意程式碼。
- 儲存衝突: 實作合約可以覆寫所有權、餘額、暫停狀態,甚至覆寫用於選擇下一個實作的儲存槽。
- 不安全升級: 管理員或治理流程遭到入侵後,可以在使用者存入資產或授予權限之後替換先前已經審查的程式碼。
- 初始化失敗: 未初始化的代理或實作合約可能讓其他帳戶取得特權角色,或設定危險的相依項目。
- 誤導性檢查: 只驗證代理原始碼、目前實作或介面,可能會遺漏信標、待執行升級、模組註冊表或其他執行路徑。
簽署前,應在近期區塊上解析實作位址,確認誰能在何種延遲下更改它,檢查目標經驗證的位元組碼和儲存布局,模擬完整呼叫資料,並比較執行前後的敏感儲存與代幣授權。對於智慧錢包,還應審查模組如何啟用、停用,以及它們如何獲准選擇目標。
常見誤解
- 「目標無法動用呼叫者的資產。」 受委派的程式碼以呼叫者身分執行;只要呼叫者具備相應能力,它就能呼叫外部合約、轉移資產或建立授權。
- 「變數名稱相同就能避免衝突。」 EVM 使用儲存槽,而不是原始碼中的名稱。布局順序、繼承關係和型別必須保持相容。
- 「代理程式碼經過驗證,就代表整個系統已經過驗證。」 目前實作、信標、升級管理員、初始化狀態和模組權限都是信任邊界中彼此獨立的部分。
相關主題
來源
- Introduction to Smart Contracts - Solidity Documentation (查閱日期: 2026-08-20)
- Units and Globally Available Variables - Solidity Documentation (查閱日期: 2026-08-20)
- ERC-1967: Proxy Storage Slots - Ethereum Improvement Proposals (查閱日期: 2026-08-20)