以太坊使用最小Gas克隆合約-合約工廠

買賣虛擬貨幣
在以太坊中,大部分的業務場景對智慧合約的要求都是部署一次,但也有些場景,需要根據不同情況動態部署合約,比如在交易所中,為每個使用者部署一個充提合約。對於第二種情況,往往需要方便並且低成本去生成和部署合約。類似程式設計中常見的工廠模式,不需要關係的物件的具體建立邏輯,只需要根據暴露的介面就可以建立出想要的物件。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合約,得到地址如0x692a70d2e424a56d2c6c27aa97d1a86395877b3a
2. 部署MetaCoinCloneFactory合約,得到地址如0xbbf289d846208c16edc8474705c748aff07732db3. 呼叫setLibraryAddress方法,引數為MetaCoinClonable的合約地址。4. 呼叫createMetaCoin方法,建立MetaCoin新合約。5. 呼叫getMetaCoins方法,可獲取已建立的MetaCoin合約地址,如得到一個地址0xe5240103E1Ff986A2C8aE6B6728FFe0d9a395C59

6. 使用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

免責聲明:

  1. 本文版權歸原作者所有,僅代表作者本人觀點,不代表鏈報觀點或立場。
  2. 如發現文章、圖片等侵權行爲,侵權責任將由作者本人承擔。
  3. 鏈報僅提供相關項目信息,不構成任何投資建議

推荐阅读

;