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类型参数data
为calldata
类型,也就是不存在memory
中,这样还需要额外的操作将data从calldata
复制到memory
中,所以这里使用public
和memory
反而更能节省gas
。 -
proxyRuntimeCode
函数,用来返回proxy
合约的运行时代码,注释中提到可以用来检查proxy合约是否部署,这里暂时不是很明白检查的意义。 -
proxyCreationCode
函数,用来返回proxy
合约的创建时代码,注意,创建时代码运行后会得到运行时代码。部署合约时使用的是创建时代码,该代码执行后产生的是运行时代码,也就是合约的线上代码。注释中提到可以用来计算地址,这里主要是create2
时使用。 -
deployProxyWithNonce
函数,用一个saltNonce
来控制产生的proxy
合约的地址。本函数的核心是使用了create2
函数而非new
来创建合约,create2
中有一个可自定义的salt
,从而可以控制生成的合约地址。根据注释,不同的
initializer
需要导致不行的地址,因此把它和输入参数中的saltNonce
一起编码作来salt
。这里,一般我们创建proxy
时initializer
是固定的,而通过不断改变saltNonce
的值来得到不同的地址。那么这里问题来了,如果initializer
和_singleton
及saltNonce
都相同,那我们能在相同的地址部署两次么?我们晚点针对这个问题进行测试。这里拓展一下,为什么要使用
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
是将initializer
与saltNonce
组合得到的一个值。注释中提到,哈希比直接连接便宜,这里我想是因为initializer
是bytes
类型,不知道具体长度大小,所以连接操作不方便。而哈希之后就是固定长度的bytes32,所以操作更便宜一些。注意,这里
keccak256(initializer)
中initializer
的值并不是它在内存中的起始位置 ,它就是实际的bytes
数据,只有在操作内存时才代表其内存地址(一般在内嵌汇编中使用)。
这里可以看到最后计算的地址和本合约地址(固定的),deploymentData 及 salt 相关,而 deploymentData 又和合约的创建时代码(固定的)及 构造器参数_singleton
相关。所以最终,我们任意改变_singleton
、initializer
或者saltNonce
的值,就可以得到一个不同的proxy地址,当然这个地址也可以线下计算出来。
从上面的代码中还可以看出来,我们构造器参数其实是附在创建时代码后面的,这和不使用create2
例如正常外部账号部署合约时是一致的。
最后一点要注意的是,正如注释中所说,它并没有调用新创建proxy合约的initializer
,因此它是功能不完整的,只是创建功能的抽象,所以它只是个内部函数,供其它函数调用 。
-
createProxyWithNonce
说曹操,曹操到。弄清楚了上面的函数,这个函数就很简单的。第一步,调用上面的内部函数创建proxy
合约,第二步,如果initializer
不为空,则调用proxy
中initializer
对应的函数进行初始化(注意并不是调用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
在线快速编辑测试,使用相同的salt
和arg
调用createDSalted
两次。结果为:第一次会创建一个合约,第二次会出错重置(因为你不能在同一地址创建合约两次,除非原合约自杀了)。通过Remix我们也可以看到,合约D的x状态变量的值正是我们调用
createDSalted
时arg
的值,这是一致的。