25

以太坊使用最小Gas克隆合约-合约工厂

 3 years ago
source link: https://learnblockchain.cn/article/1446
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

使用合约克隆工厂以最低的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.sol
contract 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 合约,得到地址如0xbbf289d846208c16edc8474705c748aff07732db
  3. 调用 setLibraryAddress 方法,参数为 MetaCoinClonable 的合约地址。
  4. 调用 createMetaCoin 方法,创建MetaCoin新合约。
  5. 调用 getMetaCoins 方法,可获取已创建的MetaCoin合约地址,如得到一个地址0xe5240103E1Ff986A2C8aE6B6728FFe0d9a395C59
  6. 使用MetaCoin合约地址0xe5240103E1Ff986A2C8aE6B6728FFe0d9a395C59调用 MetaCoinClonable 合约的 getBalance 方法,即可得到对应地址初始化时的数量,如下图所示。

yYN7Zfm.png!mobile

基本原理

克隆工厂核心是 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操作码如下图所示。

EFFNjim.png!mobile

使用这种有以下需要注意的地方:

  • 被克隆的合约不能有构造函数, MetaCoinClonable 合约使用 initialize 方法替代了构造函数。
  • 克隆工厂 MetaCoinCloneFactory 合约中的母合约 libraryAddress 可以被替换,替换后之前已克隆出的合约不受影响,新克隆合约将以新的母合约克隆。
  • 用于克隆的母合约如果销毁了,则克隆出的合约将不可用。

三、参考

https://eips.ethereum.org/EIPS/eip-1167 https://github.com/optionality/clone-factory/issues/10 https://soliditydeveloper.com/clonefactory

在以太坊中,大部分的业务场景对智能合约的要求都是部署一次,但也有些场景,需要根据不同情况动态部署合约,比如在交易所中,为每个用户部署一个充提合约。 对于第二种情况,往往需要方便并且低成本去生成和部署合约。类似编程中常见的工厂模式,不需要关系的对象的具体创建逻辑,只需要根据暴露的接口就可以创建出想要的对象。 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.sol
contract 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 合约,得到地址如0xbbf289d846208c16edc8474705c748aff07732db
  3. 调用 setLibraryAddress 方法,参数为 MetaCoinClonable 的合约地址。
  4. 调用 createMetaCoin 方法,创建MetaCoin新合约。
  5. 调用 getMetaCoins 方法,可获取已创建的MetaCoin合约地址,如得到一个地址0xe5240103E1Ff986A2C8aE6B6728FFe0d9a395C59
  6. 使用MetaCoin合约地址0xe5240103E1Ff986A2C8aE6B6728FFe0d9a395C59调用 MetaCoinClonable 合约的 getBalance 方法,即可得到对应地址初始化时的数量,如下图所示。

yYN7Zfm.png!mobile

基本原理

克隆工厂核心是 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操作码如下图所示。

EFFNjim.png!mobile

使用这种有以下需要注意的地方:

  • 被克隆的合约不能有构造函数, MetaCoinClonable 合约使用 initialize 方法替代了构造函数。
  • 克隆工厂 MetaCoinCloneFactory 合约中的母合约 libraryAddress 可以被替换,替换后之前已克隆出的合约不受影响,新克隆合约将以新的母合约克隆。
  • 用于克隆的母合约如果销毁了,则克隆出的合约将不可用。

三、参考

https://eips.ethereum.org/EIPS/eip-1167 https://github.com/optionality/clone-factory/issues/10 https://soliditydeveloper.com/clonefactory

本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

  • 发表于 17分钟前
  • 阅读 ( 10 )
  • 学分 ( 0 )
  • 分类:智能合约

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK