引介

Wisp:Create2 的奇妙世界

Ajian   |     |   3516 次阅读

Default featured image huge

- 鬼火—— Herrmann Hendrich(1884) -

ETHCapeTown 黑客马拉松期间,我们使用功能强大的,新的以太坊操作码——CREATE2,制作了一个合约钱包的快速概念证明

较之 CREATE 操作码,CTEATE2 操作码可以在使用以太坊合约来生成新合约时更好地控制新合约的地址,当你想要反事实地(counter-factually)部署并使用一个自定义权限的合约时,这是非常有用的。

Wisp 合约的目标是演示一种生成稳定合约钱包地址的技术,该地址可与其他用户共享,也可以实现代币和以太币转账功能,同时还可以在不保留任何链上合约钱包代码的情况下控制链上资产(例如 ENS 域名)。

Wisp 地址的优点包括:首先,所有发给这种 Wisp 地址的交易都只需要确定的 21, 000 gas,但是更重要的是,这意味着可以访问代币、资金以及其他链上资产的合约钱包不能被攻击,因为它的代码实际上不在链上。

如果合约钱包中出现错误,可于再次使用前在客户端进行更新,此期间所有加密资产都是安全的。

剧透以及太长不看版: 使用静态引导程序 initcode 可以 获取 CREATE2 runtime 合约字节码从而生成可复用的合约地址。

initcode 是什么?

在深入了解这种技术之前,先得理解以太坊合约的部署过程。

合约的部署过程使用了一段被叫做 initcode 的代码,它只是一个普通的以太坊程序,正常执行将返回部署合约实际所用的合约字节码。虽然有点抽象,但是它可以支撑功能非常强大的部署系统。

你可以想象一下,JavaScript 中 “Hello Word” 的 initcode 可能就是这样:

function initcode() {
    return "console.log(\"Hello World\")";
}

这个程序运行时将返回一个 “Hello World” 程序。这样做可以实现额外的部署时配置,例如:

function init(language) {
    if (language == "en_pl") {
        return "console.log(\"Ellohay orldway\")";
    }
    return "console.log(\"Hello World\")";
}

需要注意的非常重要的一点是: 被部署的不是 initcode,而是 initcode 的运行结果。

CREATE vs CREATE2

尽管大多数以太坊开发者都使用了 CREATE 操作码(来部署链上合约),但是他们可能并没有意识到这件事。Solidity 所生成的字节码实际上是 initcode,它使用 CREATE 来执行构造函数中的对应操作,随后返回合约除构造函数外的其他部分。实际部署上链的代码不包含构造函数代码。

为确定部署上链的合约地址,CREATE 使用的标准参数包括:

  1. 发送账户(它本身可能也是一个合约)
  2. 发送账户的当前交易序号(也可以是一个合约当前的 nonce)

因此,任意两个不同的发送者将生成不同的合约地址,而来自同一个账户的任意两笔不同交易也将生成不同的合约地址。

为确定部署上链的合约地址,新的 CREATE2 使用:

  1. 发送账户(同样,它本身可能也是一个合约)
  2. 合约 initcode(执行产生合约字节码)
  3. 由开发人员设定的自定义数(salt)

因此,CREATE2 与 CREATE 一样,不同的发送者将生成不同的合约地址。而 initcode 相同\但是自定义数不同时将生成不同的合约地址;而 initcode 不同时(通常意味着不同合约),也将生成不同的合约地址。

CREATE2 的另一个值得注意的(有用的)是,由于其对计算合约地址的参数多了一点控制,如果一个合约自毁了,那么新合约未来可以再次部署到这个地址上。但是,如果已经有非自毁合约部署到这个地址上了,那么 CREATE2 不能在这个地址上再次部署一个合约。

综合一下

由于控制发送者以及自定义数很容易,因此实现 Wisp 合约目标唯一要做的事情就是绕过 CREATE2 的第二个参数;最终允许两个不同的合约在不同的时期被部署在同一个地址上

但是,initcode 只是一个用于确定所部署合约的程序。这一特点可以用各种有趣的方法加以利用。

每个 Wisp 合约的入口都是一个启用和管理所有 CREATE2 调用的跳板合约(Springboard Contract),因此跳板合约将一直是发送者。至于 salt,因为使用 msg.sender 的哈希值作为自定义数,因此同一个账户的任意两个调用将始终指向同一个 Wisp 合约。

剩下的是一个普通的(静态)引导程序 initcode。initcode 要在新合约中运行,但是此时新合约还未创建;这意味着 msg.sender 实际上是跳板合约。因此,跳板合约会将所需的合约字节码保存在自己的存储空间中,并提供一个名为 getPendingBytecode() 的公共方法,引导程序的(伪)代码可以简单地表示成如下形式:

function init() {
    contractBytecode = msg.sender.getPendingBytecode()
    return(contractBytecode);
}

由于引导程序 initcode 总是相同的,因此只要合约是强制执行的自毁合约(但是不在本篇文章的讨论范围内),跳板合约就可以控制 CREATE2 从而生成完全一致的合约地址。

也就是说,大体上,我们针对源码删除了一些小细节,同时又添加了一些额外的内容,总之,该技术运行良好,感兴趣的开发者已经可以使用了!

Wisp 合约生存周期

这是一个用于帮助说明 WIsp 合约生存周期的简单图表及摘要:

  1. 一笔交易的目的地址是跳板合约(请注意,合约也可以调用跳板合约,在这种情况下,该合约地址将拥有 Wisp 合约)
  2. CREATE2 被用于初始化 Wisp合约
  3. 在初始化期间,Wisp 合约回调跳板合约以获取所需的 runtime 字节码,该字节码随后由 initcode 返回。
  4. Wisp 合约的 execute() 方法被调用时,运行所有所需的操作
  5. Wisp 合约的 die() 方法被调用时,销毁该 Wisp 合约,因此未来可在该地址重新创建合约。

注意:所有 ETH 都将返还给 Wisp 合约所有者,因为 ETH 在计划会自毁的合约中是不安全的。

跳板合约代码(Solidity)

一个简单的例子:这是被部署到 Ropsten上的,在黑客马拉松期间使用的代码。就像大多黑客马拉松的代码那样,它有点简陋,因此不要将其用于实际应用。

该版本同时支持外部所有账户(EOA)和 ENS 域名。如果调用 ENS 域名版本,则 ENS 域名所有者控制 Wisp 合约,该版本允许通过修改 ENS 解析的地址来转移 Wisp 所有者(以及它控制的所有资产)。

pragma solidity ^0.5.5;
interface ENS {
  function resolver(bytes32) external view returns (address);
}
interface Resolver {
  function addr(bytes32) external view returns (address);
}
interface Wisp {
  function execute() external;
  function die(address owner) external;
}
contract Springboard {
  ENS _ens;
  bool _mutex;
  bytes _bootstrap;
  bytes _pendingRuntimeCode;
  constructor(address ens, bytes memory bootstrap) public {
    _ens = ENS(ens);
    _bootstrap = bootstrap;
  }
  function getBootstrap() public view returns (bytes memory) {
    return bootstrap;
  }
  function _execute(bytes runtimeCode, bytes32 salt) internal {
    // Prevent re-entry
    //防止重入
    require(!_mutex);
    _mutex = true;
    // Store the desired runtime bytecode
    //存储所需 runtime 字节码
    _pendingRuntimeCode = runtimeCode;
    bytes memory bootstrap = _bootstrap;
    address wisp;
    uint256 status;
    // Create the Wisp
    //创建 Wisp
    assembly {
      wisp := create2(callvalue, add(bootstrap, 0x20),
                      mload(bootstrap), salt)
    }
    // Run the Wisp runtime bytecode
    //运行 Wisp runtime 字节码
    Wisp(wisp).execute();
    // Remove the Wisp, so it can be re-created in the
    // future, with different runtime bytecode
    // 移除 Wisp,因此未来可使用不同的runtime bytecode 重新创建合约
    Wisp(wisp).die(msg.sender);
    _mutex = false;
  }
  // Calling this will create the Wisp on-chain, execute the
  // runtime code and then remove the Wisp from the blockchain.
  //调用该函数将在链上创建 Wisp,执行 runtime代码,并随后在链上移除 Wisp
  function execute(bytes memory runtimeCode) public payable {
    _execute(runtimeCode, keccak256(abi.encodePacked(msg.sender)));
  }
  // This method is the same as execute, except it uses ENS names
  // to manage a Wisp. This allows a simple form of ownership
  // management. To change the owner of a Wisp, simply update the
  // address that the ENS name resolves to, and all the Wisp's
  // assets will be able to be managed by that new address instead.
  //该方法与 execute 一样,除了它使用的是 ENS 域名来管理 Wisp。该方法允许简单
  //的所有权管理。为了改变 Wisp 的所有权,只需要简单的更新 ENS 域名解析的地址,
  //就可以实现 Wisp 合约的所有资产由新地址管理

  function executeNamed(bytes32 nodehash,
                        bytes memory runtimeCode) public payable {
    // Verify the ENS nodehash is owned by msg.sender
    //验证 ENS nodehash(节点哈希)由 msg.sender 所有
    Resolver resolver = Resolver(_ens.resolver(nodehash));
    address owner = resolver.addr(nodehash);
    require(owner == msg.sender);
    // Execute based on the nodehash
    //基于 nodehash(节点哈希)执行
    _execute(runtimeCode, nodehash);
  }
  // This function is called by the Wisp during its initcode
  // from the bootstrap, fetching the desired bytecode
  //Wisp在 initcode 期间从引导程序处调用该函数,获得所需的字节码
  function getPendingRuntimeCode() public view returns
                                   (bytes memory runtimeCode) {
    return _pendingRuntimeCode;
  }
}

在黑客马拉松期间使用的引导程序非常简单,并且是手写代码,因此可以使用部署脚本中的几行 JavaScript 代码轻松组装。但是由于该代码稳健性不够好,需要检查返回状态。

; mstore(0x00, 0x94198df1) (sighash("getPendingRuntimeCode()"))
0x63 0x94198df1
0x60 0x00
0x52
; push 0x03ff (resultLength)
0x61 0x03ff
; push 0x20 (resultOffset)
0x60 0x20
; push 0x04 (argsLength; 4 bytes for the sighash)
0x60 0x04
; push 0x1c (argsOffset; where the 4 byte sighash begins)
0x60 0x1c
; caller (address)
0x33
; gas
0x5a
; staticcall(gas, caller, args, argsLen, result, resultLen)
0xfa
; mload(0x40) (bytecode bytes length)
0x60 0x40
0x51
; push 0x60 (0x20 + 0x20 + 0x20) (bytecode bytes offset);
0x60 0x60
; return (bytecodeOffset, bytecodeLength)
0xf3
;; // Assemble in JavaScript:
;; // 在 JavaScript 中组装:
;; function assemble(ASM) {
;;   let opcodes = [];
;;   ASM.split("\n").filter((l) =>
;;     (l.substring(0, 1) !== ";" && l.trim() !== "")
;;   ).forEach((line) => {
;;       line.split(" ").forEach((opcode) => {
;;         opcodes.push(opcode);
;;       });
;;   });
;;   return ethers.utils.hexlify(ethers.utils.concat(opcodes));
;; }

WIsp 示例

GitHub repo 中有几个 Wisp 合约的例子,但是基本上所有的操作都可以放在 execute() 函数中。

出于黑客马拉松的目的,任何转发给 Wisp 的余额在 CREATE2 部分都作为捐赠提供,因此使用 this.balance 而不是 msg.value。正如上面的插图展示的那样,可以转发给至 execute() 函数 。下面是一些可在 Wisp 合约中完成的工作的例子:

interface WETH {
  function deposit() external payable;
  function transfer(address, uint) external returns (bool);
  function balanceOf(address) external returns (uint);
  function withdraw(uint) external;
}
contract Wisp {
  function execute() {
    // Call WETH to convert between ether and WETH
    // 调用 WETH 从而转换以太和 WETH
    WETH weth = WETH(0xe7a70dD69D8D2e2f0e775FfAC0C440f23D2ABb72);
    WETH(wethContract).deposit(0.1 ether);
    weth.withdraw(weth.balanceOf(address(this)) / 2);
    // Transfer ether
    //转移以太
    (0x30bc5920A76B483080523482D7FC2B64c9D5bd80).transfer(1 ether);
  }
  function die(address addr) {
    selfdestruct(addr);
  }
}

结论

CREATE2 操作码非常棒并且功能多样。目前我们仍在进行相关探索,我们尝试着在我们的多签名合约钱包中使用它创建资产商店。

我们的目标是创造一个可靠并且灵活的资产商店,其可以存储大量的 CryptoKitties,ENS 域名以及各种代币,同时可以轻松实现资产在多签名实例之间的转移。由于 Wisp 合约可以针对其控制的资产执行任意操作,因此它甚至可以使用资产被访问之时还不存在的功能。

可以说它基本上就是一个花哨的委托调用(Delegate Call)。

感谢阅读!感谢您对我们的工作提供反馈或者建议,如果你想跟着继续跟进我的漫谈和项目,关注我的 TwitterGitHub

请注意:大多数读者都不需要使用这个版本,如果对我们正在研究的更高级的版本感兴趣,请看下文!

(下文提供了下一代的跳板合约代码,此处省略)


原文链接: https://blog.ricmoo.com/wisps-the-magical-world-of-create2-5c2177027604
作者: RicMoo
翻译&校对: Aisling & 阿剑

本文使用 CC-BY-4 自由创作许可发布,任何人皆可在保留作者署名的前提下自由使用本文。


你可能还会喜欢:

观点 | Jeff Coleman:为什么要使用 CREATE2?
干货 | 以太坊上发送交易的九种办法
科普 | 以太坊解析:默克尔树、世界状态、交易及其他

 
0 人喜欢