prosse是什么意思(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版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!