Gnosis 多签机制


Gnosis 提供一种多签名决策/执行机制。简单来说,就是多个人可以共同通过签名决定一个提案是否需要被执行。这也体现了 DAO 的思想。
本文,将从源码的角度分析通过 Gnosis 建立组织、发起提案、决策/执行的过程。

1. 创建代理

在这里,创建代理,并不是为了实现合约的可升级性,而是为了减少部署合约的代码量(所有代理合约可以共用一个执行业务逻辑的合约 GnosisSafe),从而为使用者降低部署时的 gas 费用。

使用 create2 操作码进行代理合约部署的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 {
// 第一个参数:发给创建出来的合约的 eth。
// 第二个参数:由于数组的前 32 位表示数组长度,因此,这里在计算 size 时,需要使用 deploymentData 的长度加上 0x20
// 第三个参数:读取 deploymentData 数组的长度。实际上合约的 bytecode 存储在 add(0x20, deploymentData) 到 add(0x20, deploymentData) + mload(deploymentData) 的区域。
// 第四个参数:盐值(随机数)。
// 返回部署的合约地址。
proxy := create2(0x0, add(0x20, deploymentData), mload(deploymentData), salt)
}
require(address(proxy) != address(0), "Create2 call failed");
}

在合约内部创建新合约,可以使用:createnew 或者 create2 关键字。
前两者计算新合约的地址的方式是相同的,即,通过 sender 的地址以及一个 nonce。其中,nonce 是和用户账户关联的,每次交易后都会增加。这意味着其实 create 创建的新合约的地址仍然是可预测的,前提是 sender 需要保证 nonce 的稳定性,即在此之前不发生交易。
对于 create2 来说,合约地址的生成通过如下公式:
new_address = hash(0xFF, sender, salt, bytecode),因此,地址是可预测的。

2. 多签初始化

多签设置,主要是对组织管理规则的初始化,涉及到总人数、决策参与人数、回退函数处理等细则。

记录所有 owners:

1
2
3
4
5
6
7
8
9
10
11
12
13
address currentOwner = SENTINEL_OWNERS;
for (uint256 i = 0; i < _owners.length; i++) {
// Owner address cannot be null.
address owner = _owners[i];
require(owner != address(0) && owner != SENTINEL_OWNERS && owner != address(this) && currentOwner != owner, "GS203");
// No duplicate owners allowed.
require(owners[owner] == address(0), "GS204");
owners[currentOwner] = owner;
currentOwner = owner;
}
owners[currentOwner] = SENTINEL_OWNERS;
ownerCount = _owners.length;
threshold = _threshold;

设置 fallbackHandler:

1
2
3
4
5
6
7
8
9
10
// 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)
}
}

初始化 modules:

1
2
3
4
5
6
7
8
function setupModules(address to, bytes memory data) internal {
require(modules[SENTINEL_MODULES] == address(0), "GS100");
// 初始化 modules,通过 map 实现了一个循环链表
modules[SENTINEL_MODULES] = SENTINEL_MODULES;
if (to != address(0))
// Setup has to complete successfully or transaction fails.
require(execute(to, 0, data, Enum.Operation.DelegateCall, gasleft()), "GS000");
}

调用数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
Function: createProxyWithNonce(address _singleton, bytes initializer, uint256 saltNonce)

MethodID: 0x1688f0b9
/// d9db270c1b5e3bd161e8c8503c55ceabee709552 是 GnosisSafe 合约
[0]: 000000000000000000000000d9db270c1b5e3bd161e8c8503c55ceabee709552
/// initializer 是从第 96 个字节开始(动态数据的位置偏移量)
[1]: 0000000000000000000000000000000000000000000000000000000000000060
/// saltNonce 的值是 '0x1813d227a48'(静态部分,直接表示)
[2]: 000000000000000000000000000000000000000000000000000001813d227a48
/// 第 96 个字节开始的地方。由于这是一个 bytes,动态数组,因此,0x1c4 代表数组大小 452。
[3]: 00000000000000000000000000000000000000000000000000000000000001c4
/// b63e800d 代表另外一个要调用的函数的选择器:
/// keccak256("setup(address[],uint256,address,bytes,address,address,uint256,address)")
[4]: b63e800d
/// 第一个参数 address[] 是从第 256 个字节开始的
0000000000000000000000000000000000000000000000000000000000000100
/// 至少需要 三个人 参与(阈值)
[5]: 0000000000000000000000000000000000000000000000000000000000000003
/// delegatecall 调用地址
[6]: 0000000000000000000000000000000000000000000000000000000000000000
/// 第四个参数 bytes 是从第 0x1a0,即 416 个字节开始的
[7]: 00000000000000000000000000000000000000000000000000000000000001a0
/// fallbackHandler Handler for fallback calls to this contract
[8]: 000000000000000000000000f48f2b2d2a534e402487b3ee7c18c33aec0fe5e4
/// paymentToken Token that should be used for the payment (0 is ETH)
[9]: 0000000000000000000000000000000000000000000000000000000000000000
/// payment Value that should be paid
[10]: 0000000000000000000000000000000000000000000000000000000000000000
/// paymentReceiver Address that should receive the payment (or 0 if tx.origin)
[11]: 0000000000000000000000000000000000000000000000000000000000000000
/// 第一个参数 address[] 真正开 始的地方。这里就是第 256 的位置。数组长度为 4.
[12]: 0000000000000000000000000000000000000000000000000000000000000004
/// address[0]
[13]: 000000000000000000000000792b18b5453b1e265bfe71ea33131155745200a3
/// address[1]
[14]: 0000000000000000000000005796e1051c18ed58d980ff935ce188a63d086e7a
/// address[2]
[15]: 000000000000000000000000a9227990e08aeb90e3538fbf6e629e8e545705e0
/// address[3]
[16]: 000000000000000000000000357ad87d4e546bb2406f020675db2f1e343fdda2
/// 第四个参数 bytes 真正开始的地方,即第 416 个字节开始的地方,后面一共 32+32-4 = 60 个字节。
/// Data payload for optional delegate call.
[17]: 0000000000000000000000000000000000000000000000000000000000000000
[18]: 00000000000000000000000000000000000000000000000000000000

3. 提案与执行

该过程,涉及用户发起提案、投票、执行结果的过程。
需要注意的是,用户发起提案并进行签名的过程,是在线下执行的。 当第一个能够触发执行的赞同票签名后,连带前面用户的同意签名一起将进行上链,经过合约校验通过后,会触发提案执行。

整体过程如下图所示:

可见,其实核心原理并不复杂,就是检查每个 “同意” 签名的有效性,全部通过之后,为了保险起见,如果设置了 “守卫合约”,在具体交易在执行之前后,会对交易再做检查。

只是,验证签名验证的过程,支持三种类型:

  • 对于 EOA 用户而言,涉及到消息签名验证和基于 EIP712 的签名验证;
    1
    2
    3
    4
    5
    if (v > 30) {
    currentOwner = ecrecover(keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", dataHash)), v - 4, r, s);
    } else {
    currentOwner = ecrecover(dataHash, v, r, s);
    }
  • 对于合约账户,涉及到基于 EIP1271 的签名验证;
    1
    require(ISignatureValidator(currentOwner).isValidSignature(data, contractSignature) == EIP1271_MAGIC_VALUE, "GS024");
  • 还有一种验证方式,是一种基于 approvedHashes (数据类型为 map) 的签名验证。
1
2
3
4
5
6
7
8
9
// 添加待授权的 hash
function approveHash(bytes32 hashToApprove) external {
require(owners[msg.sender] != address(0), "GS030");
approvedHashes[msg.sender][hashToApprove] = 1;
emit ApproveHash(hashToApprove, msg.sender);
}

// 判断过程
require(msg.sender == currentOwner || approvedHashes[currentOwner][dataHash] != 0, "GS025");

函数调用编码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
Function: execTransaction(address to, uint256 value, bytes data,
uint8 operation, uint256 safeTxGas, uint256 dataGas, uint256 gasPrice,
address gasToken, address refundReceiver, bytes signatures)

MethodID: 0x6a761202
/// 01be23585060835e02b77ef475b0cc51aa1e0709 合约将执行具体交易
[0]: 00000000000000000000000001be23585060835e02b77ef475b0cc51aa1e0709
/// 交易需要支付的 ethers
[1]: 0000000000000000000000000000000000000000000000000000000000000000
/// 交易的数据负载部分。由于是数组,实际上这里是数组所在位置的偏移量。即 从第 320 字节开始
[2]: 0000000000000000000000000000000000000000000000000000000000000140
/// operation. 0 代表着 call;1代表着 delegatecall.
[3]: 0000000000000000000000000000000000000000000000000000000000000000
/// safeTxGas Gas that should be used for the Safe transaction.
[4]: 0000000000000000000000000000000000000000000000000000000000000000
/// dataGas baseGas Gas costs that are independent of the transaction
/// execution(e.g. base transaction fee, signature check, payment of the refund)
[5]: 0000000000000000000000000000000000000000000000000000000000000000
/// gasPrice Gas price that should be used for the payment calculation.
[6]: 0000000000000000000000000000000000000000000000000000000000000000
/// gasToken Token address (or 0 if ETH) that is used for the payment.
[7]: 0000000000000000000000000000000000000000000000000000000000000000
/// refundReceiver Address of receiver of gas payment (or 0 if tx.origin).
[8]: 0000000000000000000000000000000000000000000000000000000000000000
/// 签名数据的偏移量是 0x1c0,即 448。
[9]: 00000000000000000000000000000000000000000000000000000000000001c0
/// 交易负载(data),在 0x01be23585060835e02b77ef475b0cc51aa1e0709 执行,数组长度 0x33,即 68
[10]: 0000000000000000000000000000000000000000000000000000000000000044
/// 函数器
[11]: a9059cbb
000000000000000000000000792b18b5453b1e265bfe71ea33131155745200a3
[12]: 0000000000000000000000000000000000000000000000000de0b6b3a7640000
[13]: 0000000000000000000000000000000000000000000000000000000000000000
/// 签名数据的长度是 0xc3,即 195。每个签名的长度是 65,这里,有 3 个人的签名
[14]: 000000000000000000000000000000000000000000000000000000c3
[15]: 023901ebcf84a9c42ad9282a24333489f87ce7d8c63ad27da98b0c0649825c2b
[16]: 40ff5061703fb9537a5284f304d341bdadabfa239d51ad0480721f4974cc499e
[17]: 1b20fa15902cbe5d4f131eeb275a57a40ac456bc63f89180dac9e06e5f9b82f7
[18]: 8a51b5ea8017b90766015f26c4a8664396d4c8a9fc2d6b8a2a49b3df0048b953
[19]: 8b1c000000000000000000000000a9227990e08aeb90e3538fbf6e629e8e5457
[20]: 05e0000000000000000000000000000000000000000000000000000000000000
[21]: 0000010000000000000000000000000000000000000000000000000000000000

参考链接

Genosis Safe
What is a module in Gnosis?
GnosisSafe
Using Ethereum’s CREATE2
Deploying Smart Contracts Using CREATE2

© 2024 YueGS