GnosisSafe.sol 学习
Posted MateZero
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了GnosisSafe.sol 学习相关的知识,希望对你有一定的参考价值。
GnosisSafe是以太坊区块链上最流行的多签钱包!它的最初版本叫 MultiSigWallet,现在新的钱包叫Gnosis Safe,意味着它不仅仅是钱包了。它自己的介绍为:以太坊上的最可信的数字资产管理平台(The most trusted platform to manage digital assets on Ethereum)。
上次我们学习到了OwnerManager.sol
,这次我们按着导入的顺序接着学习。
1. SignatureDecoder.sol
首先看源码:
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.7.0 <0.9.0;
/// @title SignatureDecoder - Decodes signatures that a encoded as bytes
/// @author Richard Meissner - <richard@gnosis.pm>
contract SignatureDecoder
/// @dev divides bytes signature into `uint8 v, bytes32 r, bytes32 s`.
/// @notice Make sure to peform a bounds check for @param pos, to avoid out of bounds access on @param signatures
/// @param pos which signature to read. A prior bounds check of this parameter should be performed, to avoid out of bounds access
/// @param signatures concatenated rsv signatures
function signatureSplit(bytes memory signatures, uint256 pos)
internal
pure
returns (
uint8 v,
bytes32 r,
bytes32 s
)
// The signature format is a compact form of:
// bytes32 rbytes32 suint8 v
// Compact means, uint8 is not padded to 32 bytes.
// solhint-disable-next-line no-inline-assembly
assembly
let signaturePos := mul(0x41, pos)
r := mload(add(signatures, add(signaturePos, 0x20)))
s := mload(add(signatures, add(signaturePos, 0x40)))
// Here we are loading the last 32 bytes, including 31 bytes
// of 's'. There is no 'mload8' to do this.
//
// 'byte' is not working due to the Solidity parser, so lets
// use the second best option, 'and'
v := and(mload(add(signatures, add(signaturePos, 0x41))), 0xff)
SignatureDecoder 顾名思义,用来解码签名,它只有一个函数signatureSplit
,从它的注释和参数中我们可以猜出signatures
是一个包含了多个签名的bytes
变量(肯定和多签相关了),pos
用来指明解码哪个签名,相当于数组的索引。 返回的结果就是解码后的v,r,s
了。
注释中也提到,签名是紧密排列的,其格式为bytes32 rbytes32 suint8 v
,因此其大小为0x41字节(两个 0x20 和一个0x1)。
注意中提到,在调用之前要进行边界检查,函数里是没有进行检查的。
我们重点看一下如下四行内嵌汇编:
let signaturePos := mul(0x41, pos)
r := mload(add(signatures, add(signaturePos, 0x20)))
s := mload(add(signatures, add(signaturePos, 0x40)))
v := and(mload(add(signatures, add(signaturePos, 0x41))), 0xff)
第一行,计算某个签名的偏移量(起始位置),我们提到了一个签名的大小是0x41(65)个字节,因此相应pos的领衔量为 0x41 * pos.
第二行,分离出r
的值。这里很简单,就是读取r
开始地址的一个word就行了。注意到这里又add
了一个0x20
,是为了略过长度前缀。
第三行,同样的方法分离s
的值,这里add 0x40
是因为长度前缀一个word,r
一个word,所以我们得跳过两个word。
第四行,分离出 v
值的办法稍微有一些技巧,按道理这里应该读取add 0x60
(长度前缀 + 前面的r,s),但是这样读取后的32字节只有第一个字节是我们需要的数据,后来都是无用的冗余数据。而处理32字节头部远比处理32字节尾部麻烦,因此,从后往前推一个字节读取,把欲分离的变量读取的word的最后,就是另一种思路。所以代码里它直接从add 0x41
开始读,这样,v
就包含在读取的32字节的最后,我们只要把它和0xff
按位与,就能把前面的冗余数据清零,得到最终的结果。
这里注释'byte' is not working due to the Solidity parser
的意思是 字节(uint8)无法直接用于Solidity解析,所以借助了工具and
。
分离出v
类似的技巧还可以应用于分离地址类型的值,因为地址类型其实是uint160,不完全占用32字节,当然其它非32字节的固定大小类型也可以分离。
笔者注:其实最后可以不使用and
操作,因为将结果赋值给了uint8会直接截断得到最后的一个字节。类似的示例见UniswapV3中BytesLib.sol
中的toUint24
函数。代码如下:
function toUint24(bytes memory _bytes, uint256 _start) internal pure returns (uint24)
require(_start + 3 >= _start, 'toUint24_overflow');
require(_bytes.length >= _start + 3, 'toUint24_outOfBounds');
uint24 tempUint;
assembly
tempUint := mload(add(add(_bytes, 0x3), _start))
return tempUint;
不过,bool类型赋值时不是只看最后一位,只要整个word不为0就是true。
2. SecuredTokenTransfer.sol
这个和UniswapV2的_safeTransfer
及Openzeppelin的SafeERC20
类似,都是使用底层调用来转移ERC20代币。为什么要使用底层调用呢?因为部分早期的代币不是标准的ERC20代币(例如USDT)其transfer
函数没有返回值,使用contract.transfer
方式会由于函数返回值检查通不过而失败。使用底层调用能绕过这个返回值检查。
本合约源码如下:
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.7.0 <0.9.0;
/// @title SecuredTokenTransfer - Secure token transfer
/// @author Richard Meissner - <richard@gnosis.pm>
contract SecuredTokenTransfer
/// @dev Transfers a token and returns if it was a success
/// @param token Token that should be transferred
/// @param receiver Receiver to whom the token should be transferred
/// @param amount The amount of tokens that should be transferred
function transferToken(
address token,
address receiver,
uint256 amount
) internal returns (bool transferred)
// 0xa9059cbb - keccack("transfer(address,uint256)")
bytes memory data = abi.encodeWithSelector(0xa9059cbb, receiver, amount);
// solhint-disable-next-line no-inline-assembly
assembly
// We write the return value to scratch space.
// See https://docs.soliditylang.org/en/v0.7.6/internals/layout_in_memory.html#layout-in-memory
let success := call(sub(gas(), 10000), token, 0, add(data, 0x20), mload(data), 0, 0x20)
switch returndatasize()
case 0
transferred := success
case 0x20
transferred := iszero(or(iszero(success), iszero(mload(0))))
default
transferred := 0
这里的代码完全是内嵌汇编,和以前的稍有不同。一是call
调用时预留了1000gas,另一个是将返回值写在了第内存的第一个字节。
接下来的switch case倒是简单,如果没有返回值,则返回是否成功(这时1代表成功,0代表失败)。如果有返回值(只有一个word),那么根据是否调用成功和返回值是否为0来得到最后的transferred
。如果返回值不只一个word,则表示不能处理的返回值,直接算失败。
注意,这里的代码并没有重置交易,只是返回了一个代表是否转移成功的布尔值。在实际使用中一定要根据不同的返回值进行处理。
如果想失败时直接重置交易,直接使用_safeTransfer
倒是个不错的选择,相关代码如下:
bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));
function _safeTransfer(address token, address to, uint value) private
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value));
require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED');
3. ISignatureValidator.sol
这个合约很简单,源码如下:
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.7.0 <0.9.0;
contract ISignatureValidatorConstants
// bytes4(keccak256("isValidSignature(bytes,bytes)")
bytes4 internal constant EIP1271_MAGIC_VALUE = 0x20c13b0b;
abstract contract ISignatureValidator is ISignatureValidatorConstants
/**
* @dev Should return whether the signature provided is valid for the provided data
* @param _data Arbitrary length data signed on the behalf of address(this)
* @param _signature Signature byte array associated with _data
*
* MUST return the bytes4 magic value 0x20c13b0b when function passes.
* MUST NOT modify state (using STATICCALL for solc < 0.5, view modifier for solc > 0.5)
* MUST allow external calls
*/
function isValidSignature(bytes memory _data, bytes memory _signature) public view virtual returns (bytes4);
这个类似ERC721
中的onERC721Received
,也就是给别人一个执行完成后的回调,返回0x20c13b0b
这个常量。
4. FallbackManager.sol
这个从名字可以看出来,是回调管理,到底是什么回调呢?其实是Solidity合约的默认回调函数fallback()
。
先上源码:
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.7.0 <0.9.0;
import "../common/SelfAuthorized.sol";
/// @title Fallback Manager - A contract that manages fallback calls made to this contract
/// @author Richard Meissner - <richard@gnosis.pm>
contract FallbackManager is SelfAuthorized
event ChangedFallbackHandler(address handler);
// keccak256("fallback_manager.handler.address")
bytes32 internal constant FALLBACK_HANDLER_STORAGE_SLOT = 0x6c9a6c4a39284e37ed1cf53d337577d14212a4870fb976a4366c693b939918d5;
function internalSetFallbackHandler(address handler) internal
bytes32 slot = FALLBACK_HANDLER_STORAGE_SLOT;
// solhint-disable-next-line no-inline-assembly
assembly
sstore(slot, handler)
/// @dev Allows to add a contract to handle fallback calls.
/// Only fallback calls without value and with data will be forwarded.
/// This can only be done via a Safe transaction.
/// @param handler contract to handle fallbacks calls.
function setFallbackHandler(address handler) public authorized
internalSetFallbackHandler(handler);
emit ChangedFallbackHandler(handler);
// solhint-disable-next-line payable-fallback,no-complex-fallback
fallback() external
bytes32 slot = FALLBACK_HANDLER_STORAGE_SLOT;
// solhint-disable-next-line no-inline-assembly
assembly
let handler := sload(slot)
if iszero(handler)
return(0, 0)
calldatacopy(0, 0, calldatasize())
// The msg.sender address is shifted to the left by 12 bytes to remove the padding
// Then the address without padding is stored right after the calldata
mstore(calldatasize(), shl(96, caller()))
// Add 20 bytes for the address appended add the end
let success := call(gas(), handler, 0, 0, add(calldatasize(), 20), 0, 0)
returndatacopy(0, 0, returndatasize())
if iszero(success)
revert(0, returndatasize())
return(0, returndatasize())
我们知道,GnosisSafe采用的是一个代理/实现模式。我们实际调用的合约是GnosisSafeProxy
,它会将所有调用通过fallback
全部委托调用GnosisSafe
执行,
也就是这里已经回调过一次了,那么在实现合约中能否再实现一个fallback
再次进行调用/委托调用呢?答案是肯定的。本合约指定了一个特定插槽,将插槽的内容不为空地址时,则将消息调用转发到插槽对应的地址执行(注意是call调用),同时,会在原payload
上添加调用者的地址。
至于为什么要这样做,暂时还未清楚,等后面学习后也许就可以解惑了。
4.1 internalSetFallbackHandler 函数
内部调用,就是设置插槽的值,很简单
4.2 setFallbackHandler 函数
外部调用,注意限定了自调用。就是设置插槽的值再加触发一个事件。注意,这里是可以设置零地址的,这样就不用再次调用其它合约了。
4.3 fallback 函数
这个也比较简单,首先读取插槽值,如果为零地址,则直接退出执行。否则的话将calldata数据(payload)全部复制到内存中0地址开始的地方,注意这里并未使用变量,因此是没有长度前缀的,内存中零地址开始到calldatasize
的内容就是payload
的值。
mstore(calldatasize(), shl(96, caller()))
是在后面添加了一个msg.sender地址,为了节省空间,将caller(msg.sender)向左移动了12个字节(地址是20字节40位,一个word是32字节,所以左移12个字节相当于把地址放在一个word的开头位置)。最后使用mstore
添加在payload
后。这样0 ~ calldatasize
就是原payload
,calldatasize ~ calldatasize + 32
就是原msg.sender。有人说,你这并未节省啊,因为mstore还是操作的word,你还是占32字节,请往下看。
let success := call(gas(), handler, 0, 0, add(calldatasize(), 20), 0, 0)
。
我们再次回顾一个call
函数的简单定义:call(g, a, v, in, insize, out, outsize)
对比一下,我们可以看到以前使用时insize
的值为calldatasize
,这次的值为calldatasize + 20
,这样我们就把内存中复制的旧payload
后附加的地址一并作为新的payload
发送了出去。这是是add 20不是add 0x20,所以call左移12字节后最右边的12字节的0并没有作为新的payload发送出去。
后面的执行同原来的示例类似,注意 call
和delegatecall
返回0代表失败,1代表成功。
这里有一个风险点:当回调地址插槽FALLBACK_HANDLER_STORAGE_SLOT
的内容为任一EOA账号(Externally Owned Account)时,由于EOA账号没有代码,此时call
执行会成功(这是由于EVM的特性造成的,其它项目有过类似的安全事故)。所以在执行前必须小心设定该插槽的内容。
5. StorageAccessible.sol
本合约只有两个函数,一个函数getStorageAt
是获取连续的多个内存插槽的内容,另一个simulateAndRevert
是模拟执行并返回结果。
我们贴了源码后分别学习。
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.7.0 <0.9.0;
/// @title StorageAccessible - generic base contract that allows callers to access all internal storage.
/// @notice See https://github.com/gnosis/util-contracts/blob/bb5fe5fb5df6d8400998094fb1b32a178a47c3a1/contracts/StorageAccessible.sol
contract StorageAccessible
/**
* @dev Reads `length` bytes of storage in the currents contract
* @param offset - the offset in the current contract's storage in words to start reading from
* @param length - the number of words (32 bytes) of data to read
* @return the bytes that were read.
*/
function getStorageAt(uint256 offset, uint256 length) public view returns (bytes memory)
bytes memory result = new bytes(length * 32);
for (uint256 index = 0; index < length; index++)
// solhint-disable-next-line no-inline-assembly
assembly
let word := sload(add(offset, index))
mstore(add(add(result, 0x20), mul(index, 0x20)), word)
return result;
/**
* @dev Performs a delegetecall on a targetContract in the context of self.
* Internally reverts execution to avoid side effects (making it static).
*
* This method reverts with data equal to `abi.encode(bool(success), bytes(response))`.
* Specifically, the `returndata` after a call to this method will be:
* `success:bool || response.length:uint256 || response:bytes`.
*
* @param targetContract Address of the contract containing the code to execute.
* @param calldataPayload Calldata that should be sent to the target contract (encoded method name and arguments).
*/
function simulateAndRevert(address targetContract, bytes memory calldataPayload) external
// solhint-disable-next-line no-inline-assembly
assembly
let success := delegatecall(gas(), targetContract, add(calldataPayload, 0x20), mload(calldataPayload), 0, 0)
mstore(0x00, success)
mstore(0x20, returndatasize())
returndatacopy(0x40, 0, returndatasize())
revert(0, add(returndatasize(), 0x40))
5.1 getStorageAt 函数
我们知道,在Solidity中,内存是使用字节为单位的,比如地址0x20,则是从0x20字节开始。但是存储却是以插槽为单位的,理论上插槽是从1到2**256-1个。
所以getStorageAt的两个参数分别为:offset
,起始插槽,length
欲获取内容的插槽数量。这两个弄明白了后面就很好解决了。
因为插槽内容也是32字节(key-value都是一个word,32字节),所以bytes memory result = new bytes(length * 32);
先分配了一块内存区域来保存返回结果。
接下来一个For循环遍历读取每个欲获取的插槽内容,并且写入到result
中对应的位置去。这里为什么会有add(result, 0x20)
呢?记住result是个bytes,动态类型,它有个长度前缀,所以得跳过这个长度前缀word。
5.2 simulateAndRevert 函数
这个函数逻辑倒是不难,难点在应用上。我们直接学习汇编代码。
首先,进行了一个委托调用,这里delegatgecall
我们已经学习很多次了,所以可以直接看明白。
接下来,将是否调用成功存在了内存第一个32字节,将返回的数据大小存在了内存的第二个32字节。将返回的数据存在了内存第三个32字节开始的地方。
接下来重置交易,因为是模拟执行,所以不能改变状态,然后将内存中returndatasize + 0x40
大小的内容(其实就是包含了返回数据和内存中前两个word)作为重置原因返回。
可以看到,该函数为非view函数,因此就算是调用失败了,也仍然会记录在链上,仍然要花费gas。所以这里免费调用可能需要一定的技巧,例如在ethers.js
中使用provider.call
来调用(不绑定钱包,这样就不花费了),然后catch
返回的结果就可以了(暂未测试验证,待有空验证)。
6. GuardManager.sol
先上源码:
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.7.0 <0.9.0;
import "../common/Enum.sol";
import "../common/SelfAuthorized.sol";
interface Guard
function checkTransaction(
address to,
uint256 value,
bytes memory data,
Enum.Operation operation,
uint256 safeTxGas,
uint256 baseGas,
uint256 gasPrice,
address gasToken,
address payable refundReceiver,
bytes memory signatures,
address msgSender
) external;
function checkAfterExecution(bytes32 txHash, bool success) external;
/// @title Fallback Manager - A contract that manages fallback calls made to this contract
/// @author Richard Meissner - <richard@gnosis.pm>
contract GuardManager is SelfAuthorized
event ChangedGuard(address guard);
// keccak256("guard_manager.guard.address")
bytes32 internal constant GUARD_STORAGE_SLOT = 0x4a204f620c8c5ccdca3fd54d003badd85ba500436a431f0cbda4f558c93c34c8;
/// @dev Set a guard that checks transactions before execution
/// @param guard The address of the guard to be used or the 0 address to disable the guard
function setGuard(address guard) external authorized
bytes32 slot = GUARD_STORAGE_SLOT;
// solhint-disable-next-line no-inline-assembly
assembly
sstore(slot, guard)
emit ChangedGuard(guard);
function getGuard() internal view returns (address guard)
bytes32 slot = GUARD_STORAGE_SLOT;
// solhint-disable-next-line no-inline-assembly
assembly
guard := sload(slot)
可以看到,本合约源文件包含一个接口定义和一个简单的检查合约。
接口 Guard
定义了一个函数用来检查交易,还定义了一个函数用来要执行后检查(具体逻辑未知)。
合约GuardManager
定义了一个插槽GUARD_STORAGE_SLOT
用来保存实现Guard
接口的合约的地址,并分别提供了get
和set
函数。注意set
函数是限定自调用的。
好了,到此我们GnosisSafe
合约所有的导入合约都学习完毕,从学习中我们可以看出,它采用模块化设计的,导入的合约虽然多,但是每个都是独立实现了一个小功能,方便复用。
有了这些导入合约学习作基础,我们接下来便可学习GnosisSafe
合约的正文了。
以上是关于GnosisSafe.sol 学习的主要内容,如果未能解决你的问题,请参考以下文章