以太坊使用最小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方法,即可得到对应地址初始化时的数量,如下图所示。
    image.png

基本原理

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

image.png

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

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

三、参考

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容