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

微信扫一扫,分享到朋友圈

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

使用合约克隆工厂以最低的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 方法,即可得到对应地址初始化时的数量,如下图所示。

基本原理

克隆工厂核心是 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

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

基本原理

克隆工厂核心是 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

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

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

小爱课程表APP问世:覆盖920所高校 支持一键导入

上一篇

电动牙刷哪个牌子好,如何选择一把好用的电动牙刷?

下一篇

你也可能喜欢

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

长按储存图像,分享给朋友