首页IT科技prosse是什么意思(GnosisSafeProxy合约学习)

prosse是什么意思(GnosisSafeProxy合约学习)

时间2025-05-02 09:30:55分类IT科技浏览3149
导读: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 - <richard@gnosis.io> 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 - <stefan@gnosis.io> /// @author Richard Meissner - <richard@gnosis.io> 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为单位的              。

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

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字节)                  ,刚好是上一步压入内存的地址         。

如果不是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代表内存开始位置       ,第二个参数代表数据大小–字节数)

我们可以对比一下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 dont 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的调用判断及返回              。

知识拓展                  。我们知道         ,在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

创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

展开全文READ MORE
window系统怎么录屏带声音(win11怎么录屏? Windows 11中录制屏幕的多种方法) 细粒度识别 局部识别(细粒度图像分类论文研读-2022)