草庐IT

GnosisSafeProxyFactory合约学习

AiMateZero 2023-05-22 原文

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-only
pragma 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 - <stefan@gnosis.pm>
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-assembly
            assembly {
                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 it
        bytes32 salt = keccak256(abi.encodePacked(keccak256(initializer), saltNonce));
        bytes memory deploymentData = abi.encodePacked(type(GnosisSafeProxy).creationCode, uint256(uint160(_singleton)));
        // solhint-disable-next-line no-inline-assembly
        assembly {
            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-assembly
            assembly {
                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-assembly
      assembly {
          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 it
      bytes32 salt = keccak256(abi.encodePacked(keccak256(initializer), saltNonce));
      bytes memory deploymentData = abi.encodePacked(type(GnosisSafeProxy).creationCode, uint256(uint160(_singleton)));
      // solhint-disable-next-line no-inline-assembly
      assembly {
          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 calls
    function 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.0
    pragma 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的值,这是一致的。

有关GnosisSafeProxyFactory合约学习的更多相关文章

  1. LC滤波器设计学习笔记(一)滤波电路入门 - 2

    目录前言滤波电路科普主要分类实际情况单位的概念常用评价参数函数型滤波器简单分析滤波电路构成低通滤波器RC低通滤波器RL低通滤波器高通滤波器RC高通滤波器RL高通滤波器部分摘自《LC滤波器设计与制作》,侵权删。前言最近需要学习放大电路和滤波电路,但是由于只在之前做音乐频谱分析仪的时候简单了解过一点点运放,所以也是相当从零开始学习了。滤波电路科普主要分类滤波器:主要是从不同频率的成分中提取出特定频率的信号。有源滤波器:由RC元件与运算放大器组成的滤波器。可滤除某一次或多次谐波,最普通易于采用的无源滤波器结构是将电感与电容串联,可对主要次谐波(3、5、7)构成低阻抗旁路。无源滤波器:无源滤波器,又称

  2. CAN协议的学习与理解 - 2

    最近在学习CAN,记录一下,也供大家参考交流。推荐几个我觉得很好的CAN学习,本文也是在看了他们的好文之后做的笔记首先是瑞萨的CAN入门,真的通透;秀!靠这篇我竟然2天理解了CAN协议!实战STM32F4CAN!原文链接:https://blog.csdn.net/XiaoXiaoPengBo/article/details/116206252CAN详解(小白教程)原文链接:https://blog.csdn.net/xwwwj/article/details/105372234一篇易懂的CAN通讯协议指南1一篇易懂的CAN通讯协议指南1-知乎(zhihu.com)视频推荐CAN总线个人知识总

  3. 深度学习部署:Windows安装pycocotools报错解决方法 - 2

    深度学习部署:Windows安装pycocotools报错解决方法1.pycocotools库的简介2.pycocotools安装的坑3.解决办法更多Ai资讯:公主号AiCharm本系列是作者在跑一些深度学习实例时,遇到的各种各样的问题及解决办法,希望能够帮助到大家。ERROR:Commanderroredoutwithexitstatus1:'D:\Anaconda3\python.exe'-u-c'importsys,setuptools,tokenize;sys.argv[0]='"'"'C:\\Users\\46653\\AppData\\Local\\Temp\\pip-instal

  4. ruby - 我正在学习编程并选择了 Ruby。我应该升级到 Ruby 1.9 吗? - 2

    我完全不是程序员,正在学习使用Ruby和Rails框架进行编程。我目前正在使用Ruby1.8.7和Rails3.0.3,但我想知道我是否应该升级到Ruby1.9,因为我真的没有任何升级的“遗留”成本。缺点是什么?我是否会遇到与普通gem的兼容性问题,或者甚至其他我不太了解甚至无法预料的问题? 最佳答案 你应该升级。不要坚持从1.8.7开始。如果您发现不支持1.9.2的gem,请避免使用它们(因为它们很可能不被维护)。如果您对gem是否兼容1.9.2有任何疑问,您可以在以下位置查看:http://www.railsplugins.or

  5. ruby - 我如何学习 ruby​​ 的正则表达式? - 2

    如何学习ruby​​的正则表达式?(对于假人) 最佳答案 http://www.rubular.com/在Ruby中使用正则表达式时是一个很棒的工具,因为它可以立即将结果可视化。 关于ruby-我如何学习ruby​​的正则表达式?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/1881231/

  6. 深度学习12. CNN经典网络 VGG16 - 2

    深度学习12.CNN经典网络VGG16一、简介1.VGG来源2.VGG分类3.不同模型的参数数量4.3x3卷积核的好处5.关于学习率调度6.批归一化二、VGG16层分析1.层划分2.参数展开过程图解3.参数传递示例4.VGG16各层参数数量三、代码分析1.VGG16模型定义2.训练3.测试一、简介1.VGG来源VGG(VisualGeometryGroup)是一个视觉几何组在2014年提出的深度卷积神经网络架构。VGG在2014年ImageNet图像分类竞赛亚军,定位竞赛冠军;VGG网络采用连续的小卷积核(3x3)和池化层构建深度神经网络,网络深度可以达到16层或19层,其中VGG16和VGG

  7. 机器学习——时间序列ARIMA模型(四):自相关函数ACF和偏自相关函数PACF用于判断ARIMA模型中p、q参数取值 - 2

    文章目录1、自相关函数ACF2、偏自相关函数PACF3、ARIMA(p,d,q)的阶数判断4、代码实现1、引入所需依赖2、数据读取与处理3、一阶差分与绘图4、ACF5、PACF1、自相关函数ACF自相关函数反映了同一序列在不同时序的取值之间的相关性。公式:ACF(k)=ρk=Cov(yt,yt−k)Var(yt)ACF(k)=\rho_{k}=\frac{Cov(y_{t},y_{t-k})}{Var(y_{t})}ACF(k)=ρk​=Var(yt​)Cov(yt​,yt−k​)​其中分子用于求协方差矩阵,分母用于计算样本方差。求出的ACF值为[-1,1]。但对于一个平稳的AR模型,求出其滞

  8. 玩以太坊链上项目的必备技能(初识智能合约语言-Solidity之旅一) - 2

    前面一篇关于智能合约翻译文讲到了,是一种计算机程序,既然是程序,那就可以使用程序语言去编写智能合约了。而若想玩区块链上的项目,大部分区块链项目都是开源的,能看得懂智能合约代码,或找出其中的漏洞,那么,学习Solidity这门高级的智能合约语言是有必要的,当然,这都得在公链``````以太坊上,毕竟国内的联盟链有些是不兼容Solidity。Solidity是一种面向对象的高级语言,用于实现智能合约。智能合约是管理以太坊状态下的账户行为的程序。Solidity是运行在以太坊(Ethereum)虚拟机(EVM)上,其语法受到了c++、python、javascript影响。Solidity是静态类型

  9. Unity Shader 学习笔记(5)Shader变体、Shader属性定义技巧、自定义材质面板 - 2

    写在之前Shader变体、Shader属性定义技巧、自定义材质面板,这三个知识点任何一个单拿出来都是一套知识体系,不能一概而论,本文章目的在于将学习和实际工作中遇见的问题进行总结,类似于网络笔记之用,方便后续回顾查看,如有以偏概全、不祥不尽之处,还望海涵。1、Shader变体先看一段代码......Properties{ [KeywordEnum(on,off)]USL_USE_COL("IsUseColorMixTex?",int)=0 [Toggle(IS_RED_ON)]_IsRed("IsRed?",int)=0}......//中间省略,后续会有完整代码 #pragmamulti_c

  10. ruby-on-rails - 这个 C 和 PHP 程序员如何学习 Ruby 和 Rails? - 2

    按照目前的情况,这个问题不适合我们的问答形式。我们希望答案得到事实、引用或专业知识的支持,但这个问题可能会引发辩论、争论、投票或扩展讨论。如果您觉得这个问题可以改进并可能重新打开,visitthehelpcenter指导。关闭9年前。我来自C、php和bash背景,很容易学习,因为它们都有相同的C结构,我可以将其与我已经知道的联系起来。然后2年前我学了Python并且学得很好,Python对我来说比Ruby更容易学。然后从去年开始,我一直在尝试学习Ruby,然后是Rails,我承认,直到现在我还是学不会,讽刺的是那些打着简单易学的烙印,但是对于我这样一个老练的程序员来说,我只是无法将它

随机推荐