委派呼叫
DELEGATECALL 是一份合約執行另一份合約程式碼、彷彿那是它自己程式碼的特殊方式。用普通的 CALL,被呼叫者在自己的家裡執行——它的儲存、它的身分。用 DELEGATECALL,借來的程式碼則在呼叫者的上下文中執行:它讀寫呼叫者的儲存,把呼叫者的地址當作「this」,並保留原本的 msg.sender 與 msg.value。打個比方,就像請一位承包商在你家、用你的工具、處理你的物品工作——他帶來技術,狀態仍歸你。
這單一個特性,正是幾乎所有可升級智能合約背後的引擎。代理合約持有全部的儲存與資金,本身卻幾乎沒有邏輯;被呼叫時,它 delegatecall 進入一份獨立的「實作」(邏輯)合約。由於邏輯是針對代理的儲存執行,你可以部署一份新實作、讓代理指向它,從而「升級」行為,同時保住每一筆餘額與設定。函式庫也用同樣的技巧:合約 delegatecall 一個函式庫的函式,會直接作用在該合約的狀態上。
這份威力帶著鋒利的刃口:儲存佈局必須對齊。借來的程式碼以槽號定址儲存,因此若代理與實作對「哪個變數住在第 0 槽」有分歧,程式碼就會讀寫到錯誤的資料——這就是儲存碰撞。隨之而來的是一類惡名昭彰的臭蟲:未經初始化的實作合約可能被奪取,而掌控被 delegatecall 之程式碼的攻擊者,就掌控了代理的整個狀態。2017 年 Parity 多重簽章凍結事件鎖死了約 513,000 ETH,根源正是 delegatecall 到一份函式庫,其初始化被觸發後又被 selfdestruct。
正因如此,DELEGATECALL 只能指向受信任、佈局相容的程式碼。透明代理、UUPS、鑽石模式等標準的存在,正是為了安全地管理 delegatecall——規範儲存佈局、守護初始化,並把管理呼叫與使用者呼叫分流,使它們不致碰撞。
// minimal proxy forwarding (bool ok, ) = implementation.delegatecall(msg.data); require(ok); // implementation runs on THIS contract's storage
可升級合約的基礎:借來的邏輯、在地的儲存。
DELEGATECALL 連價值上下文也一併保留:借來的程式碼裡的 msg.value 是原始框架所擁有的值,儘管 delegatecall 本身並未送出任何新的 ETH。