GnosisSafe是以太坊区块链上最流行的多签钱包!它的最初版本叫 MultiSigWallet,现在新的钱包叫Gnosis Safe,意味着它不仅仅是钱包了。它自己的介绍为:以太坊上的最可信的数字资产管理平台(The most trusted platform to manage digital assets on Ethereum)。

所谓Factory,顾名思义,就是能够快捷创建某类合约的合约,通过合约创建合约的方式而非直接部署一个新的合约。因此GnosisSafeProxyFactory 就是用来快速创建GnosisSafeProxy的合约。

1.1 GnosisSafeProxyFactory 源码

GnosisSafeProxyFactory 的源码并不复杂,核心为 deployProxyWithNonce函数,用来创建一个GnosisSafeProxy,其它的只是一些辅助函数和包装函数而已。

// SPDX-License-Identifier: LGPL-3.0-onlypragma solidity >=0.7.0 <0.9.0;import "./GnosisSafeProxy.sol";import "./IProxyCreationCallback.sol";/// @title Proxy Factory - Allows to create new proxy contact and execute a message call to the new proxy within one transaction./// @author Stefan George - contract GnosisSafeProxyFactory {event ProxyCreation(GnosisSafeProxy proxy, address singleton);/// @dev Allows to create new proxy contact and execute a message call to the new proxy within one transaction./// @param singleton Address of singleton contract./// @param data Payload for message call sent to new proxy contract.function createProxy(address singleton, bytes memory data) public returns (GnosisSafeProxy proxy) {proxy = new GnosisSafeProxy(singleton);if (data.length > 0)// solhint-disable-next-line no-inline-assemblyassembly {if eq(call(gas(), proxy, 0, add(data, 0x20), mload(data), 0, 0), 0) {revert(0, 0)}}emit ProxyCreation(proxy, singleton);}/// @dev Allows to retrieve the runtime code of a deployed Proxy. This can be used to check that the expected Proxy was deployed.function proxyRuntimeCode() public pure returns (bytes memory) {return type(GnosisSafeProxy).runtimeCode;}/// @dev Allows to retrieve the creation code used for the Proxy deployment. With this it is easily possible to calculate predicted address.function proxyCreationCode() public pure returns (bytes memory) {return type(GnosisSafeProxy).creationCode;}/// @dev Allows to create new proxy contact using CREATE2 but it doesn't run the initializer.///This method is only meant as an utility to be called from other methods/// @param _singleton Address of singleton contract./// @param initializer Payload for message call sent to new proxy contract./// @param saltNonce Nonce that will be used to generate the salt to calculate the address of the new proxy contract.function deployProxyWithNonce(address _singleton,bytes memory initializer,uint256 saltNonce) internal returns (GnosisSafeProxy proxy) {// If the initializer changes the proxy address should change too. Hashing the initializer data is cheaper than just concatinating itbytes32 salt = keccak256(abi.encodePacked(keccak256(initializer), saltNonce));bytes memory deploymentData = abi.encodePacked(type(GnosisSafeProxy).creationCode, uint256(uint160(_singleton)));// solhint-disable-next-line no-inline-assemblyassembly {proxy := create2(0x0, add(0x20, deploymentData), mload(deploymentData), salt)}require(address(proxy) != address(0), "Create2 call failed");}/// @dev Allows to create new proxy contact and execute a message call to the new proxy within one transaction./// @param _singleton Address of singleton contract./// @param initializer Payload for message call sent to new proxy contract./// @param saltNonce Nonce that will be used to generate the salt to calculate the address of the new proxy contract.function createProxyWithNonce(address _singleton,bytes memory initializer,uint256 saltNonce) public returns (GnosisSafeProxy proxy) {proxy = deployProxyWithNonce(_singleton, initializer, saltNonce);if (initializer.length > 0)// solhint-disable-next-line no-inline-assemblyassembly {if eq(call(gas(), proxy, 0, add(initializer, 0x20), mload(initializer), 0, 0), 0) {revert(0, 0)}}emit ProxyCreation(proxy, _singleton);}/// @dev Allows to create new proxy contact, execute a message call to the new proxy and call a specified callback within one transaction/// @param _singleton Address of singleton contract./// @param initializer Payload for message call sent to new proxy contract./// @param saltNonce Nonce that will be used to generate the salt to calculate the address of the new proxy contract./// @param callback Callback that will be invoced after the new proxy contract has been successfully deployed and initialized.function createProxyWithCallback(address _singleton,bytes memory initializer,uint256 saltNonce,IProxyCreationCallback callback) public returns (GnosisSafeProxy proxy) {uint256 saltNonceWithCallback = uint256(keccak256(abi.encodePacked(saltNonce, callback)));proxy = createProxyWithNonce(_singleton, initializer, saltNonceWithCallback);if (address(callback) != address(0)) callback.proxyCreated(proxy, _singleton, initializer, saltNonce);}/// @dev Allows to get the address for a new proxy contact created via `createProxyWithNonce`///This method is only meant for address calculation purpose when you use an initializer that would revert,///therefore the response is returned with a revert. When calling this method set `from` to the address of the proxy factory./// @param _singleton Address of singleton contract./// @param initializer Payload for message call sent to new proxy contract./// @param saltNonce Nonce that will be used to generate the salt to calculate the address of the new proxy contract.function calculateCreateProxyWithNonceAddress(address _singleton,bytes calldata initializer,uint256 saltNonce) external returns (GnosisSafeProxy proxy) {proxy = deployProxyWithNonce(_singleton, initializer, saltNonce);revert(string(abi.encodePacked(proxy)));}}

1.2 源码学习

我们跳过导入语句和pragma声明部分,由于本合约未继承任何合约,我们直接从函数createProxy开始学习。

  • createProxy 函数。 用来创建一个代理合约。 它使用了Solidity 的 new关键字进行了创建,同时指定构造器参数为singleton。第二个参数data一般用于创建或者升级后进行初始化,我们着重来看这个部分。

    if (data.length > 0)// solhint-disable-next-line no-inline-assemblyassembly {if eq(call(gas(), proxy, 0, add(data, 0x20), mload(data), 0, 0), 0) {revert(0, 0)}}

    这里if语句未使用花括号,虽然是个人风格的问题,但是还是推荐使用花括号。

    如果data的长度为0,那么data就是用来进行初始化的payload(包含了函数选择器和参数编码)。这时需要调用proxy合约进行初始化。

    这里直接调用了内嵌汇编的call函数进行proxy合约的调用,参数分别为剩余gas,proxy合约地址,发送的eth数量,内存中起始位置,调用数据的大小,output的位置 和大小。

    这里需要注意的是data的数据类型为bytes memory,为动态类型。因此它在solidity中的直接值其实代表的是包含了长度前缀的起始地址,因为长度前缀为一个word,所以add(data, 0x20) 得到了真正payload的起始地址(去除了长度前缀)。mload(data) 则是读取长度前缀的值,也就是该payload的大小。

    最后将执行的结果和0比较(0代表失败),如果失败了,revert,且无提示信息。

    函数的最后将创建的proxy地址及其实现合约地址通过事件的形式分发出去,方便客户端监听。

    这里还有一个细节,就是函数的可见性为public,通常来讲,一般为external(只要内部没有调用),这样部分数据类型更能节省gas。但是external 可见性的函数会导致其bytes类型参数datacalldata类型,也就是不存在memory中,这样还需要额外的操作将data从calldata复制到memory中,所以这里使用publicmemory反而更能节省gas

  • proxyRuntimeCode 函数,用来返回proxy合约的运行时代码,注释中提到可以用来检查proxy合约是否部署,这里暂时不是很明白检查的意义。

  • proxyCreationCode函数,用来返回proxy合约的创建时代码,注意,创建时代码运行后会得到运行时代码。部署合约时使用的是创建时代码,该代码执行后产生的是运行时代码,也就是合约的线上代码。注释中提到可以用来计算地址,这里主要是create2时使用。

  • deployProxyWithNonce 函数,用一个saltNonce来控制产生的proxy合约的地址。本函数的核心是使用了create2函数而非new来创建合约,create2中有一个可自定义的salt,从而可以控制生成的合约地址。

    根据注释,不同的initializer需要导致不行的地址,因此把它和输入参数中的saltNonce一起编码作来salt。这里,一般我们创建proxyinitializer是固定的,而通过不断改变saltNonce的值来得到不同的地址。那么这里问题来了,如果initializer_singletonsaltNonce都相同,那我们能在相同的地址部署两次么?我们晚点针对这个问题进行测试。

    这里拓展一下,为什么要使用create2呢?Solidity中有一段话:

    When creating a contract, the address of the contract is computed from the address of the creating contract and a counter that is increased with each contract creation.

    If you specify the option salt (a bytes32 value), then contract creation will use a different mechanism to come up with the address of the new contract:

    意思是,如果你直接创建合约,例如通过new或者 直接部署,生成的合约地址和创建它的地址及一个不断增加的计数器相关,例如外部账号EOA的nonce。当你定义一个salt时,它使用不同的机制来计算新合约的地址。

    create2的定义为:

    create2(v, p, n, s)create new contract with code mem[p…(p+n)) at address keccak256(0xff . this . s . keccak256(mem[p…(p+n))) and send v wei and return the new address, where 0xff is a 1 byte value, this is the current contract’s address as a 20 byte value and s is a big-endian 256-bit value; returns 0 on error

    我们对照实际代码:

    // If the initializer changes the proxy address should change too. Hashing the initializer data is cheaper than just concatinating itbytes32 salt = keccak256(abi.encodePacked(keccak256(initializer), saltNonce));bytes memory deploymentData = abi.encodePacked(type(GnosisSafeProxy).creationCode, uint256(uint160(_singleton)));// solhint-disable-next-line no-inline-assemblyassembly {proxy := create2(0x0, add(0x20, deploymentData), mload(deploymentData), salt)}require(address(proxy) != address(0), "Create2 call failed");

    这里会得到 v为0(也就是不给新合约ETH),p是内存起始位置,所以为add(0x20, deploymentData),这里为什么要加add 0x20,上面也提到过,是要去除长度前缀。 当然大小就是 mload(deploymentData)了,同deployProxyWithNonce中一样,这里的salt 是将initializersaltNonce组合得到的一个值。注释中提到,哈希比直接连接便宜,这里我想是因为initializerbytes类型,不知道具体长度大小,所以连接操作不方便。而哈希之后就是固定长度的bytes32,所以操作更便宜一些。

    注意,这里keccak256(initializer)initializer的值并不是它在内存中的起始位置 ,它就是实际的bytes数据,只有在操作内存时才代表其内存地址(一般在内嵌汇编中使用)。

​ 这里可以看到最后计算的地址和本合约地址(固定的),deploymentData 及 salt 相关,而 deploymentData 又和合约的创建时代码(固定的)及 构造器参数_singleton 相关。所以最终,我们任意改变_singletoninitializer或者saltNonce的值,就可以得到一个不同的proxy地址,当然这个地址也可以线下计算出来。

​ 从上面的代码中还可以看出来,我们构造器参数其实是附在创建时代码后面的,这和不使用create2例如正常外部账号部署合约时是一致的。

​ 最后一点要注意的是,正如注释中所说,它并没有调用新创建proxy合约的initializer,因此它是功能不完整的,只是创建功能的抽象,所以它只是个内部函数,供其它函数调用 。

  • createProxyWithNonce 说曹操,曹操到。弄清楚了上面的函数,这个函数就很简单的。第一步,调用上面的内部函数创建proxy合约,第二步,如果initializer不为空,则调用proxyinitializer对应的函数进行初始化(注意并不是调用initializer函数,initializer的含义是指初始化一次。具体调用哪个函数要看initializer前8位的函数选择器。

    这里的汇编使用就很简单了,和前面的用法 一样,在操作内存相关时,记住initializer代表了包含长度前缀的payload的起始地址就可以了。

  • createProxyWithCallback函数,在上面createProxyWithNonce函数的基础上加了一个回调函数用来通知其它合约创建并初始化成功了。实际使用的场景并不多, 主要用于调用合约和回调合约不是同一个合约的场景,类似ERC20中的ApproveAndCall

  • calculateCreateProxyWithNonceAddress 函数,看注释是用来得到一个新的proxy合约的地址,它仅用于计算目的。当然我们可以线下计算,线下计算只是让这个Factory合约更完善。不过有一点却是要注意的,该函数不是一个view函数,它的结果是使用revert返回的,并不是那么容易直接获取的。并且如果直接调用,非view函数就是发送交易,哪怕是revert并未实际改写数据,也是要花费gas费用的,我们需要一点技巧来避免此项开销,就好比UniswapV3的价格查询合约Quoter一样。大家其实可以参考Uniswap V3: Quoter合约的,地址为:https://etherscan.io/address/0xb27308f9f90d607463bb33ea1bebb41c27ce5ab6#code

    当然,最好的办法是线下计算,利用上面提到的create2的定义,参考UniswapV2中Pair地址的计算方法:

    // calculates the CREATE2 address for a pair without making any external callsfunction pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) {(address token0, address token1) = sortTokens(tokenA, tokenB);pair = address(uint(keccak256(abi.encodePacked(hex'ff',factory,keccak256(abi.encodePacked(token0, token1)),hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' // init code hash))));}

    这里的initCode 如果使用hardhat可以通过如下方式获取:

    let UniswapV2Pair = await ethers.getContractFactory("UniswapV2Pair");let InitCode = ethers.utils.keccak256(UniswapV2Pair.bytecode)

    更为直接的是直接参考Solidity官方文档示例:

    // SPDX-License-Identifier: GPL-3.0pragma solidity >=0.7.0 <0.9.0;contract D {uint public x;constructor(uint a) {x = a;}}contract C {function createDSalted(bytes32 salt, uint arg) public {// This complicated expression just tells you how the address// can be pre-computed. It is just there for illustration.// You actually only need ``new D{salt: salt}(arg)``.address predictedAddress = address(uint160(uint(keccak256(abi.encodePacked(bytes1(0xff),address(this),salt,keccak256(abi.encodePacked(type(D).creationCode,arg)))))));D d = new D{salt: salt}(arg);require(address(d) == predictedAddress);}}

    这里可以看到,使用new D{salt: salt}(arg) 可以达到和create2相同的效果,但Gnosis Safe为什么选择了create2函数呢,我想可能是使用内嵌汇编更节省gas

    在我们自己的实际应用中,使用new D{salt: salt}(arg)即可。

    这里我们可以实际测试一下,使用remix.ethereum.org在线快速编辑测试,使用相同的saltarg调用createDSalted两次。结果为:第一次会创建一个合约,第二次会出错重置(因为你不能在同一地址创建合约两次,除非原合约自杀了)。

    通过Remix我们也可以看到,合约D的x状态变量的值正是我们调用createDSaltedarg的值,这是一致的。