• GnosisSafeProxy合约学习


    GnosisSafeProxy 学习

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

    Gnosis Safe Contracts的核心合约采用了代理/实现这种模式,并且为了方便大家创建,使用了ProxyFractory合约来进行代理合约的创建(当然创建代理合约之前必须创建实现合约)。

    这里什么是代理/实现模式就不再讲了,不清楚的读者可以自行阅读相关文章。

    1.1 GnosisSafeProxy.sol 合约源码

    既然是代理/实现合约,那么我们平常交互的对象就是代理合约了,虽然逻辑在实现合约里面。相对其它而言,代理合约是非常简单的,和openzeppelin的代理合约也很相似,我们先看本合约源码。

    // SPDX-License-Identifier: LGPL-3.0-only
    pragma solidity >=0.7.0 <0.9.0;
    
    /// @title IProxy - Helper interface to access masterCopy of the Proxy on-chain
    /// @author Richard Meissner - 
    interface IProxy {
        function masterCopy() external view returns (address);
    }
    
    /// @title GnosisSafeProxy - Generic proxy contract allows to execute all transactions applying the code of a master contract.
    /// @author Stefan George - 
    /// @author Richard Meissner - 
    contract GnosisSafeProxy {
        // singleton always needs to be first declared variable, to ensure that it is at the same location in the contracts to which calls are delegated.
        // To reduce deployment costs this variable is internal and needs to be retrieved via `getStorageAt`
        address internal singleton;
    
        /// @dev Constructor function sets address of singleton contract.
        /// @param _singleton Singleton address.
        constructor(address _singleton) {
            require(_singleton != address(0), "Invalid singleton address provided");
            singleton = _singleton;
        }
    
        /// @dev Fallback function forwards all transactions and returns all received return data.
        fallback() external payable {
            // solhint-disable-next-line no-inline-assembly
            assembly {
                let _singleton := and(sload(0), 0xffffffffffffffffffffffffffffffffffffffff)
                // 0xa619486e == keccak("masterCopy()"). The value is right padded to 32-bytes with 0s
                if eq(calldataload(0), 0xa619486e00000000000000000000000000000000000000000000000000000000) {
                    mstore(0, _singleton)
                    return(0, 0x20)
                }
                calldatacopy(0, 0, calldatasize())
                let success := delegatecall(gas(), _singleton, 0, calldatasize(), 0, 0)
                returndatacopy(0, 0, returndatasize())
                if eq(success, 0) {
                    revert(0, returndatasize())
                }
                return(0, returndatasize())
            }
        }
    }
    

    1.2 源码学习

    注意:阅读注释很重要,魔鬼细节全在注释里。

    我们现在开始学习,直接跳过版权声明和pragma声明部分。

    • IProxy 定义了一个代理合约需要实现的接口,它仅有一个函数masterCopy(),功能为返回其实现合约地址。

    • contract GnosisSafeProxy 代理合约定义。注意注释中提到,它会根据master合约中的代码来执行所有交易(其实这里有一个例外,就是masterCopy函数本身。注意,合约定义并没有is IProxy,也就是不需要显式实现masterCopy函数。这是因为为了节省gas,该函数统一通过fallback函数来实现,所以不需要显式定义合约必须实现IProxy接口。

    • singleton 字面意思类似Java中单例,也就是唯一实现master。注意,它是合约中的第一个状态变量,所以存储在插槽0。实现合约中的相同的状态变量必须和代理合约中保持插槽顺序一致(否则会引起插槽冲突),也就是说实现合约的第一个状态变量必须也是singleton。这个我们以后学习到实现合约时再做验证。

    • 注释中提到它是内部可见性,是为了节省gas。它可以通过getStorageAt也就是直接读取插槽位置获取,当然了,本合约中可以通过IProxy定义的接口函数masterCopy获取,当然,它内部也是通过读取插槽0实现的。

    • 构造器参数是实现合约地址,验证了它不能为0地址,这个很简单,当然我们可以进一步验证其它必须为合约地址。

    • fallback 函数。我们知道,调用一个合约时,如果合约匹配不到相应的函数,则会调用fallback函数(如果有定义)。代理/实现模式利用了这一特点,在fallback 函数里将所有的调用转为调用实现合约中相应的逻辑,再返回相应结果。因为本合约未定义receive函数,所以接收ETH也是执行的本函数。

    • 本列中的fallback函数和openzeppelin合约中的略有不同,首先,它判断了调用是否为masterCopy函数,如果是的话,直接返回singleton地址,因此变相实现了IProxy。如果不是调用的masterCopy函数,则委托调用实现合约的相关逻辑。我们来简单学习一下它的代码。

      需要注意的是,在内嵌汇编中,所有的EVM dialect涉及的数据类型都是uint256类型,没有其它类型。接下来的文档中如果没有特殊说明,所有的word均指32字节(256位)。EVM中的操作一般是以一个word为单位的。

      1. let _singleton := and(sload(0), 0xffffffffffffffffffffffffffffffffffffffff) 这行代码先读取插槽0的数据(32字节,256位),然后和40个F按位与操作,重置前面未使用的数据位为0。这是一个良好的习惯,我们不能假定前面未使用的数据位一定为0,虽然本例中的确为0。最后的结果得到 singleton地址,注意前面提到过,其不是地址类型,而是uint256

      2. if eq(calldataload(0), 0xa619486e00000000000000000000000000000000000000000000000000000000) {
            mstore(0, _singleton)
            return(0, 0x20)
        }
        

        判断调用是否为masterCopy。注意,虽然我们平常调用合约时,类似masterCopy这样的没有参数的函数调用它的数据只有8位0xa619486e(函数选择器),但是calldataload读取的是calldata中的0地址开始的一个word内容,它是256位的,不足的话会被右边补0。所以if语句中相比较的是补0后的函数选择器,那么补了多少个0呢?由于uint256是64个16进制长度,函数选择器的长度是8,所以补了 64 - 8 = 56 个0.

        如果比较相等,则把singleton地址保存到内存中0地址开始的字节中去,然后返回该地址。注意return(0, 0x20)返回内存中0地址开始的一个word,第一个参数0代表开始地址,第二个参数0x20代表返回内容的长度(字节数)。0x20 = 32 也就是一个word(32字节),刚好是上一步压入内存的地址。

      3. 如果不是masterCopy函数,则执行逻辑和openzeppelin中相关函数一致,我们来看代码:

        calldatacopy(0, 0, calldatasize())
        let success := delegatecall(gas(), _singleton, 0, calldatasize(), 0, 0)
        returndatacopy(0, 0, returndatasize())
        if eq(success, 0) {
            revert(0, returndatasize())
        }
        return(0, returndatasize())
        

        第一行将所有的calldata数据复制到内存中(从calldata的0地址开始,复制到内存中的0地址开始位置)。

        第二行进行委托调用,对应的参数按顺序分别为剩余的gas,实现合约地址,内存中开始地址,数据大小,output开始位置 ,output大小(最后两项一般为0)。 因为上一步复制了calldata到内存0位置,所以这里我们是从0地址开始的,大小刚好就是calldatasize

        第三行将返回值复制到了内存中从0地址开始的位置(多次利用了零地址开头的内存)。

        4-6行判断如果返回值是0(代表delegatecall失败),则将返回值revert(这里一般是出错原因)。第一个参数0代表内存开始位置 ,第二个参数代表数据大小–字节数。

        第7行如果调用成功,则将返回值return。(第一个参数0代表内存开始位置 ,第二个参数代表数据大小–字节数)

      4. 我们可以对比一下openzeppelin中相关代码_delegate函数,基本是类似的:

        function _delegate(address implementation) internal virtual {
            assembly {
                // Copy msg.data. We take full control of memory in this inline assembly
                // block because it will not return to Solidity code. We overwrite the
                // Solidity scratch pad at memory position 0.
                calldatacopy(0, 0, calldatasize())
        
                // Call the implementation.
                // out and outsize are 0 because we don't know the size yet.
                let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)
        
                // Copy the returned data.
                returndatacopy(0, 0, returndatasize())
        
                switch result
                // delegatecall returns 0 on error.
                case 0 {
                    revert(0, returndatasize())
                }
                default {
                    return(0, returndatasize())
                }
            }
        }
        

        Gnosis的代码和这个相比,仅是多了一个masterCopy的调用判断及返回。

      5. 知识拓展。我们知道,在Solidity中,有自由内存指针,并且还有scratch。我们平常并不是从内存中零地址开始操作的,通常是从自由内存指针指向的地址开始操作的,一般为0x80(前四个word已经被占用)。但是这里openzeppelin的注释解释的很清楚,它并没有采用Solidity的内存控制,而是自己完全控制,因为它不涉及到Solidity代码(内嵌汇编是Yul代码),因此是不冲突的。同时它还解释了我们将delegatecall最后两个参数设置为0的原因是我们无法知道返回值大小。

      好了,GnosisSafeProxy.sol 就算学习结束了,它只是一个简单的代理合约。和标准的代理合约相比,它多了一个masterCopy函数的调用判断。

      为什么没有把它单独列为一个函数呢?根据注释猜想应该是为了节省gas

      相对而言,openzeppelin模板中的TransparentUpgradeableProxy 合约专门提供了一个函数implementation用来返回实现合约的地址。 另外, TransparentUpgradeableProxy中的实现合约一般不是插槽位置0的状态变量,例如实现了eip-1967

      ERC1967Upgrade合约,它的实现插槽是根据"eip1967.proxy.implementation" 计算的哈希值减去1 得到的,虽然这样会存在哈希碰撞的可能,但仅存于理论上。

      采用相同插槽位置(从0开始)来保存相同状态变量的代理/实现模式还有CompoundV2版本的合约,大家有兴趣的可以自己去看一下相关源码。

      拓展一点:
      openzeppelin在它自己的访问提到了为什么会有TransparentUpgradeableProxy.是因为本合约这种最简单的代理实现模式可能存在函数选择器冲突。如果实现合约恰好有一个函数的选择器和masterCopy相同(利用编程语言可以构造一个),那么在调用这个函数时其实是会调用masterCopy,从而得到的一个错误的结果。但是我们这里的实现合约是固定的,所以不会存在这个问题。大家有兴趣的可以参考:
      https://docs.openzeppelin.com/contracts/4.x/api/proxy#TransparentUpgradeableProxy

  • 相关阅读:
    【javaEE】多线程进阶(Part1 锁策略、CAS、synchronized )
    java-net-php-python-7jsp在线购物计算机毕业设计程序
    修正TiKnob的指示箭头显示问题
    云原生之旅 - 9)云原生时代网关的后起之秀Envoy Proxy 和基于Envoy 的 Emissary Ingress
    FSC认证助您进入日新月异的时尚领域
    从迷之自信到逻辑自信(简版)
    Uni-App之使用RichText组件实现富文本内容展示教程
    MQ系列3:RocketMQ 架构分析
    SqlSessionFactory与SqlSession
    【PAT甲级 - C++题解】1078 Hashing
  • 原文地址:https://blog.csdn.net/weixin_39430411/article/details/127038144