概述
在一開始,代理合約很難去掌握,在這篇文章中,我們將分析最小代理標准或“EIP-1167”,並創建一個代碼示例。
在開始之前,不要混淆可升級代理和最小代理,這是非常重要的,它們是完全不同的。在這篇文章中,我們只討論最小代理。
最小代理標準早在2018年就正式發布了,這個標準的主要思想是盡可能便宜地部署基礎合約的副本。讓我進一步展開:
最好的例子是一個多重簽名錢包。讓我們假設用戶創建了一個非常簡單的多重簽名錢包,它可以接收資金、發送資金和設置n個所有者的數量。當然,為了發送資金,用戶需要達到一定的法定人數(n≥m)。一旦智能合約準備好了,有兩種主要的方法可以讓它準備好投入生產。第一個是部署合約,使所有用戶直接交互,這意味著所有的資金將存儲在該合約,為了跟踪誰擁有什麼,需要創建一個映射(地址=> uint) 公共餘額+ 一些修飾符。這種方法的問題在於,用戶將所有內容集中在一個地方,最重要的是,用戶打開了額外攻擊向量的可能性。換句話說,用戶使合約更加複雜了。我們不想這樣做,我們想把安全作為我們的首要任務。想到的第二種方法是讓用戶部署合約,為了做到這一點,用戶需要編譯合約,將字節碼放在前端,並讓用戶部署合約。這種方法的問題是效率非常低,而且gas價格昂貴。想像一下,如果合約變得太大太複雜,部署成本將非常高,再加上我們正在用大量存儲轟炸鏈。解決這個問題的方法是實現最小代理標準。
最小代理所做的是創建一個廉價的克隆(我們稱它為廉價,因為它的部署成本非常低),它具有與實現合約完全相同的邏輯,但具有自己的存儲狀態。這是通過低級別的委託調用實現的。
為了實施這個標準,我們需要:
執行合約:有時被稱為基礎合約、核心合約、主合約等。重要的是,執行合約是所有邏輯所在的地方。代理工廠或克隆工廠:顧名思義,克隆工廠合約將是我們的工廠。這意味著用戶將調用工廠的一個函數,而工廠將克隆一份實施合同的精確副本,但擁有自己的存儲空間。這意味著每個克隆都有相同的邏輯,但存儲狀態獨立。代理:如前所述,代理合約是實施合約的克隆,但具有獨特的存儲。
現在我們有了一個大致的了解,讓我們創建一個示例來鞏固我們的知識。我們將在Remix中做這個例子,使它更簡單。
合約將會非常簡單,這裡的目的是理解標準。
為此我們需要以下合約:
實現:這是我們的邏輯所在的地方,我們將其稱為Implementation.sol。 CloneFactory:這將是我們的工廠,我們將有一個clone() 函數,用戶將觸發該函數,工廠將輸出代理的地址。工廠的名稱將是CloneFactory.sol。代理:與代理無關,代理將是CloneFactory.sol 中的clone() 函數的輸出。可以有盡可能多的不同代理,這就是整個目的,以創建許多Implementation.sol的克隆。
這是它看起來的樣子:
需要記住的一個非常重要的方面是,克隆不知道構造函數,因此我們使用initialize()函數“替換構造函數”,而不是使用構造函數來分配重要變量。我們只需要確保initialize()函數只被調用一次,這樣人們就不能篡改合約,類似於構造函數的工作原理。為了做到這一點,我們通常使用openZeppelin的Initializabl。對於本例,我們不打算使用任何第三方合約,只是為了更清楚地說明。
讓我們從Implementation.sol開始。合約唯一要做的就是擁有一個帶有setter函數和修飾符的uint公共變量,限制只有所有者才能更改變量的訪問權限。
讓我們來分析一下:
uint public x→我們將在setter函數中更改的無符號整數(默認為0)。
bool public isBase→這個布爾值將確保實現合約永遠不會被初始化。如果在構造函數中看到,我們將:isBase設置為true,並且initialize()函數的第一個require語句是require(isBase ==false)。這保證了實現合約只用於邏輯,沒有人可以篡改。請記住,代理或克隆合約不知道構造函數,因此isBase將被設置為其默認值false。
address public owner→合約所有者(外部擁有的賬戶)。所有者默認為address(0)。在Solidity中,如果不分配地址類型,則默認值為address(0)。
modifier onlyOwner()→希望用戶不需要解釋這個,但基本上這是說只有所有者可以調用這個函數。
initialize(address _owner)→一旦創建代理克隆,需要立即調用initialize函數。這就像我們的構造函數,意味著如果之前有人調用這個函數,它將控制合約。如我們所見,它有一個參數(address _owner)。該參數將在CloneFactory 中提供。這裡有兩個重要的考慮:
用戶需要確保initialize函數只被調用一次。我們這樣做,是通過檢查所有者是否是地址(0)。一旦分配了所有者,並且我們試圖再次調用該函數,交易將恢復。強烈建議在Initializable合約中使用這個體系結構+ OpenZppelin的initializer()修飾符。這確保了函數只能被調用一次。使實現合約不可用:通過在構造函數中賦值isBase=true,並在initialize()函數中要求isBase== false,我們可以確保沒有人可以篡改合約。該合約的唯一目的是充當邏輯合約,如果有人試圖調用基礎合約的初始化函數,它將立即恢復。
一旦我們準備好了Implementation.sol,讓我們創建CloneFactory.sol。為此,我們將使用OpenZeppelin 的Clone 庫中的clone() 函數。
讓我們來分析一下:
interface Implementation→initialize()是我們在Implementation.sol中需要的唯一函數。一旦創建了克隆合約,我們將立即調用它。
address public implementation→implementation .sol的地址。
mapping(address => address[]) public allClone →這只是一個跟踪所有已部署克隆的映射,第一個地址是msg.sender 或克隆的所有者。
clon(address implementation)→這個函數來自Open Zeppelin。在較高的層次上,我們提供實現地址(implementation .sol),它返回該地址的一個實例,換句話說,它返回一個完全相同的implementation .sol的克隆。
clone()→這是用戶將要調用的函數。一旦有人調用這個函數,第一件事就是創建一個新的克隆並將它保存在地址sameChild 下。這個地址將持有與Implementation.sol相同的邏輯,但有自己的存儲狀態。正如我們在clone()的第三行中看到的,我們正在調用initialize函數:
這是至關重要的,這就是為什麼我們要立即調用它。這將使clone()函數的調用者成為克隆合約的所有者。一旦做了,就沒有回頭路了。
用戶可以克隆任意數量的合約,每個合約都有自己的存儲空間。任何調用_clone()函數的人都將是唯一能夠訪問更改“x”的帳戶。
結論
當我們需要為一個合約創建多個副本時,最小代理標準非常有效,每個副本都有自己的存儲狀態。最常見的用例是:Multi-Sig-Wallet、託管賬戶、某種類型的流動性池等。
作為提醒,無論何時執行此標準,都要特別注意以下事項:
實現合約中的initialize()函數只能被調用一次。需要盡快觸發實現合約中的initialize()函數。使實現合約不可用。
Source:https://medium.com/coinmonks/diving-into-smart-contracts-minimal-proxy-eip-1167-3c4e7f1a41b8