-
-
Notifications
You must be signed in to change notification settings - Fork 110
/
LilGnosis.sol
201 lines (160 loc) · 6.54 KB
/
LilGnosis.sol
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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.10;
/// @title lil gnosis
/// @author Miguel Piedrafita
/// @notice An optimised ERC712-based multisig implementation
contract LilGnosis {
/// ERRORS ///
/// @notice Thrown when the provided signatures are invalid, duplicated, or out of order
error InvalidSignatures();
/// @notice Thrown when the execution of the requested transaction fails
error ExecutionFailed();
/// EVENTS ///
/// @notice Emitted when the number of required signatures is updated
/// @param newQuorum The new amount of required signatures
event QuorumUpdated(uint256 newQuorum);
/// @notice Emitted when a new transaction is executed
/// @param target The address the transaction was sent to
/// @param value The amount of ETH sent in the transaction
/// @param payload The data sent in the transaction
event Executed(address target, uint256 value, bytes payload);
/// @notice Emitted when a new signer gets added or removed from the trusted signers
/// @param signer The address of the updated signer
/// @param shouldTrust Wether the contract will trust this signer going forwards
event SignerUpdated(address indexed signer, bool shouldTrust);
/// @dev Components of an Ethereum signature
struct Signature {
uint8 v;
bytes32 r;
bytes32 s;
}
/// @notice Signature nonce, incremented with each successful execution or state change
/// @dev This is used to prevent signature reuse
/// @dev Initialised at 1 because it makes the first transaction slightly cheaper
uint256 public nonce = 1;
/// @notice The amount of required signatures to execute a transaction or change the state
uint256 public quorum;
/// @dev The EIP-712 domain separator
bytes32 public immutable domainSeparator;
/// @notice A list of signers, and wether they're trusted by this contract
/// @dev This automatically generates a getter for us!
mapping(address => bool) public isSigner;
/// @dev EIP-712 types for a signature that updates the quorum
bytes32 public constant QUORUM_HASH =
keccak256('UpdateQuorum(uint256 newQuorum,uint256 nonce)');
/// @dev EIP-712 types for a signature that updates a signer state
bytes32 public constant SIGNER_HASH =
keccak256('UpdateSigner(address signer,bool shouldTrust,uint256 nonce)');
/// @dev EIP-712 types for a signature that executes a transaction
bytes32 public constant EXECUTE_HASH =
keccak256('Execute(address target,uint256 value,bytes payload,uint256 nonce)');
/// @notice Deploy a new LilGnosis instance, with the specified name, trusted signers and number of required signatures
/// @param name The name of the multisig
/// @param signers An array of addresses to trust
/// @param _quorum The number of required signatures to execute a transaction or change the state
constructor(
string memory name,
address[] memory signers,
uint256 _quorum
) payable {
unchecked {
for (uint256 i = 0; i < signers.length; i++) isSigner[signers[i]] = true;
}
quorum = _quorum;
domainSeparator = keccak256(
abi.encode(
keccak256(
'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'
),
keccak256(bytes(name)),
keccak256(bytes('1')),
block.chainid,
address(this)
)
);
}
/// @notice Execute a transaction from the multisig, providing the required amount of signatures
/// @param target The address to send the transaction to
/// @param value The amount of ETH to send in the transaction
/// @param payload The data to send in the transaction
/// @param sigs An array of signatures from trusted signers, sorted in ascending order by the signer's addresses
/// @dev Make sure the signatures are sorted in ascending order by the signer's addresses! Otherwise the verification will fail
function execute(
address target,
uint256 value,
bytes calldata payload,
Signature[] calldata sigs
) public payable {
bytes32 digest = keccak256(
abi.encodePacked(
'\x19\x01',
domainSeparator,
keccak256(abi.encode(EXECUTE_HASH, target, value, payload, nonce++))
)
);
address previous;
unchecked {
for (uint256 i = 0; i < quorum; i++) {
address sigAddress = ecrecover(digest, sigs[i].v, sigs[i].r, sigs[i].s);
if (!isSigner[sigAddress] || previous >= sigAddress) revert InvalidSignatures();
previous = sigAddress;
}
}
emit Executed(target, value, payload);
(bool success, ) = target.call{ value: value }(payload);
if (!success) revert ExecutionFailed();
}
/// @notice Update the amount of required signatures to execute a transaction or change state, providing the required amount of signatures
/// @param _quorum The new number of required signatures
/// @param sigs An array of signatures from trusted signers, sorted in ascending order by the signer's addresses
/// @dev Make sure the signatures are sorted in ascending order by the signer's addresses! Otherwise the verification will fail
function setQuorum(uint256 _quorum, Signature[] calldata sigs) public payable {
bytes32 digest = keccak256(
abi.encodePacked(
'\x19\x01',
domainSeparator,
keccak256(abi.encode(QUORUM_HASH, _quorum, nonce++))
)
);
address previous;
unchecked {
for (uint256 i = 0; i < quorum; i++) {
address sigAddress = ecrecover(digest, sigs[i].v, sigs[i].r, sigs[i].s);
if (!isSigner[sigAddress] || previous >= sigAddress) revert InvalidSignatures();
previous = sigAddress;
}
}
emit QuorumUpdated(_quorum);
quorum = _quorum;
}
/// @notice Add or remove an address from the list of signers trusted by this contract
/// @param signer The address of the signer
/// @param shouldTrust Wether to trust this signer going forward
/// @param sigs An array of signatures from trusted signers, sorted in ascending order by the signer's addresses
/// @dev Make sure the signatures are sorted in ascending order by the signer's addresses! Otherwise the verification will fail
function setSigner(
address signer,
bool shouldTrust,
Signature[] calldata sigs
) public payable {
bytes32 digest = keccak256(
abi.encodePacked(
'\x19\x01',
domainSeparator,
keccak256(abi.encode(SIGNER_HASH, signer, shouldTrust, nonce++))
)
);
address previous;
unchecked {
for (uint256 i = 0; i < quorum; i++) {
address sigAddress = ecrecover(digest, sigs[i].v, sigs[i].r, sigs[i].s);
if (!isSigner[sigAddress] || previous >= sigAddress) revert InvalidSignatures();
previous = sigAddress;
}
}
emit SignerUpdated(signer, shouldTrust);
isSigner[signer] = shouldTrust;
}
/// @dev This function ensures this contract can receive ETH
receive() external payable {}
}