在以太坊中,大部分的業務場景對智慧合約的要求都是部署一次,但也有些場景,需要根據不同情況動態部署合約,比如在交易所中,為每個使用者部署一個充提合約。對於第二種情況,往往需要方便並且低成本去生成和部署合約。類似程式設計中常見的工廠模式,不需要關係的物件的具體建立邏輯,只需要根據暴露的介面就可以建立出想要的物件。solidity也有類似的工廠,分為普通工廠和克隆工廠。一、普通工廠普通工廠,就是在工廠合約中以new的方式建立一個新合約。我這裡以MetaCoin合約示例,合約程式碼如下所示。pragma solidity ^0.5.0;contract MetaCoin { mapping (address => uint) balances; constructor(address metaCoinOwner, uint256 initialBalance) public { balances[metaCoinOwner] = initialBalance; } function sendCoin(address receiver, uint amount) public returns(bool sufficient) { if (balances[msg.sender] < amount) return false; balances[msg.sender] -= amount; balances[receiver] += amount; return true; } function getBalance(address addr) view public returns(uint) { return balances[addr]; }}contract MetaCoinFactory { MetaCoin[] public metaCoinAddresses; event MetaCoinCreated(MetaCoin metaCoin); address private metaCoinOwner; constructor(address _metaCoinOwner ) public { metaCoinOwner = _metaCoinOwner ; } function createMetaCoin(uint256 initialBalance) external { MetaCoin metaCoin = new MetaCoin(metaCoinOwner, initialBalance); metaCoinAddresses.push(metaCoin); emit MetaCoinCreated(metaCoin); } function getMetaCoins() external view returns (MetaCoin[] memory) { return metaCoinAddresses; }}在MetaCoinFactory工廠合約中, createMetaCoin方法中使用new建立MetaCoin新合約,並將得到的合約地址儲存在metaCoinAddresses陣列中。這種方式的優點就是簡單,透過工廠部署的合約是一個獨立的合約,相關的交易資訊在瀏覽器上可查。缺點就是手續費太高。二、克隆工廠如果每次部署的合約都一樣,那就沒必要對合約的位元組碼重新部署,耗費手續費。基於這一思想,以太坊提出了EIP1167,最小代理合約,底層根據delegatecall,將克隆出來的合約呼叫都委派到一個已知的固定合約地址中。先來看一個例子,還是以MetaCoin為例,這裡方便演示,我把多個合約合併到了一個檔案中,合約程式碼如下所示。pragma solidity ^0.5.0;contract MetaCoinClonable { mapping (address => uint) balances; function initialize(address metaCoinOwner, uint256 initialBalance) public { balances[metaCoinOwner] = initialBalance; } function sendCoin(address receiver, uint amount) public returns(bool sufficient) { if (balances[msg.sender] < amount) return false; balances[msg.sender] -= amount; balances[receiver] += amount; return true; } function getBalance(address addr) view public returns(uint) { return balances[addr]; }}contract Ownable { /** * @dev Event to show ownership has been transferred * @param previousOwner representing the address of the previous owner * @param newOwner representing the address of the new owner */ event OwnershipTransferred(address previousOwner, address newOwner); // Owner of the contract address private _owner; /** * @dev Throws if called by any account other than the owner. */ modifier onlyOwner() { require(msg.sender == owner()); _; } /** * @dev The constructor sets the original owner of the contract to the sender account. */ constructor() public { setOwner(msg.sender); } /** * @dev Tells the address of the owner * @return the address of the owner */ function owner() public view returns (address) { return _owner; } /** * @dev Sets a new owner address */ function setOwner(address newOwner) internal { _owner = newOwner; } /** * @dev Allows the current owner to transfer control of the contract to a newOwner. * @param newOwner The address to transfer ownership to. */ function transferOwnership(address newOwner) public onlyOwner { require(newOwner != address(0)); emit OwnershipTransferred(owner(), newOwner); setOwner(newOwner); }}// https://github.com/optionality/clone-factory/blob/master/contracts/CloneFactory.solcontract CloneFactory { function createClone(address target) internal returns (address result) { bytes20 targetBytes = bytes20(target); assembly { let clone := mload(0x40) mstore(clone, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000) mstore(add(clone, 0x14), targetBytes) mstore(add(clone, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000) result := create(0, clone, 0x37) } } function isClone(address target, address query) internal view returns (bool result) { bytes20 targetBytes = bytes20(target); assembly { let clone := mload(0x40) mstore(clone, 0x363d3d373d3d3d363d7300000000000000000000000000000000000000000000) mstore(add(clone, 0xa), targetBytes) mstore(add(clone, 0x1e), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000) let other := add(clone, 0x40) extcodecopy(query, other, 0, 0x2d) result := and( eq(mload(clone), mload(other)), eq(mload(add(clone, 0xd)), mload(add(other, 0xd))) ) } }}contract MetaCoinCloneFactory is CloneFactory, Ownable { MetaCoinClonable[] public metaCoinAddresses; event MetaCoinCreated(MetaCoinClonable metaCoin); address public libraryAddress; address public metaCoinOwner; function setLibraryAddress(address _libraryAddress) external onlyOwner { libraryAddress = _libraryAddress; } function createMetaCoin(address _metaCoinOwner, uint256 initialBalance) external { MetaCoinClonable metaCoin = MetaCoinClonable( createClone(libraryAddress) ); metaCoin.initialize(_metaCoinOwner, initialBalance); metaCoinAddresses.push(metaCoin); emit MetaCoinCreated(metaCoin); } function getMetaCoins() external view returns (MetaCoinClonable[] memory) { return metaCoinAddresses; }}部署流程:1. 先部署MetaCoinClonable合約,得到地址如0x692a70d2e424a56d2c6c27aa97d1a86395877b3a2. 部署MetaCoinCloneFactory合約,得到地址如0xbbf289d846208c16edc8474705c748aff07732db3. 呼叫setLibraryAddress方法,引數為MetaCoinClonable的合約地址。4. 呼叫createMetaCoin方法,建立MetaCoin新合約。5. 呼叫getMetaCoins方法,可獲取已建立的MetaCoin合約地址,如得到一個地址0xe5240103E1Ff986A2C8aE6B6728FFe0d9a395C596. 使用MetaCoin合約地址0xe5240103E1Ff986A2C8aE6B6728FFe0d9a395C59呼叫MetaCoinClonable合約的getBalance方法,即可得到對應地址初始化時的數量,如下圖所示。
基本原理
克隆工廠核心是CloneFactory合約,在createClone方法中,使用solidity的內聯彙編(assembly)來克隆合約。
· let clone := mload(0x40)在 Solidity 中,記憶體插槽 0x40 位置是比較特殊的,它包含了下一個可用的空閒記憶體指標的值。每次將變數直接儲存到記憶體時,都應透過查詢 0x40 位置的值,來確定變數儲存在記憶體的位置。
· mstore(clone, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000),這句的意思是將0x3d...的儲存在了clone指標指向的位置。
· mstore(add(clone, 0x14), targetBytes),將clone的指標向後移動0x14(20)個位元組,在儲存targetBytes(20位元組)的值。我們上邊部署MetaCoinClonable合約,得到targetBytes的值是0x692a70d2e424a56d2c6c27aa97d1a86395877b3a,此時clone指向的空間儲存的內容為0x3d602d80600a3d3981f3363d3d373d3d3d363d73+692a70d2e424a56d2c6c27aa97d1a86395877b3a
· mstore(add(clone, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000),將clone的指標向後移動0x28(40)個位元組,然後存證0x5af43...的值,此時clone指向的空間儲存的內容為0x3d602d80600a3d3981f3363d3d373d3d3d363d73+692a70d2e424a56d2c6c27aa97d1a86395877b3a+5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000
· result := create(0, clone, 0x37),create操作碼的功能是根據指定的合約位元組碼建立新合約,並返回合約地址。第一個引數0代表傳送的以太幣個數;第二個引數clone指合約位元組碼的起始位置;0x37(55)指合約位元組碼的終止位置。新合約的位元組碼就是0x3d602d80600a3d3981f3363d3d373d3d3d363d73692a70d2e424a56d2c6c27aa97d1a86395877b3a5af43d82803e903d91602b57fd5bf3。可以透過eth_getCode獲取我們上邊得到的克隆出來的合約0xe5240103E1Ff986A2C8aE6B6728FFe0d9a395C59的位元組碼比對,是一樣的。
在合約位元組碼中3d602d80600a3d3981f3是EIP-1167標準克隆協議部署的一部分,固定不變。其餘對應的EVM操作碼如下圖所示。
使用這種有以下需要注意的地方:
· 被克隆的合約不能有建構函式,MetaCoinClonable合約使用initialize方法替代了建構函式。
· 克隆工廠MetaCoinCloneFactory合約中的母合約libraryAddress可以被替換,替換後之前已克隆出的合約不受影響,新克隆合約將以新的母合約克隆。
· 用於克隆的母合約如果銷燬了,則克隆出的合約將不可用。
三、參考
https://eips.ethereum.org/EIPS/eip-1167 https://github.com/optionality/clone-factory/issues/10 https://soliditydeveloper.com/clonefactory