-
Notifications
You must be signed in to change notification settings - Fork 4
/
MultiSig.sol
194 lines (163 loc) · 8.2 KB
/
MultiSig.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
/* Abstract multisig contract to allow multi approval execution of atomic contracts scripts
e.g. migrations or settings.
* Script added by signing a script address by a signer (NEW state)
* Script goes to ALLOWED state once a quorom of signers sign it (quorom fx is defined in each derived contracts)
* Script can be signed even in APPROVED state
* APPROVED scripts can be executed only once.
- if script succeeds then state set to DONE
- If script runs out of gas or reverts then script state set to FAILEd and not allowed to run again
(To avoid leaving "behind" scripts which fail in a given state but eventually execute in the future)
* Scripts can be cancelled by an other multisig script approved and calling cancelScript()
* Adding/removing signers is only via multisig approved scripts using addSigners / removeSigners fxs
*/
pragma solidity 0.4.24;
import "./SafeMath.sol";
contract MultiSig {
using SafeMath for uint256;
mapping(address => bool) public isSigner;
address[] public allSigners; // all signers, even the disabled ones
// NB: it can contain duplicates when a signer is added, removed then readded again
// the purpose of this array is to being able to iterate on signers in isSigner
uint public activeSignersCount;
enum ScriptState {New, Approved, Done, Cancelled, Failed}
struct Script {
ScriptState state;
uint signCount;
mapping(address => bool) signedBy;
address[] allSigners;
}
mapping(address => Script) public scripts;
address[] public scriptAddresses;
event SignerAdded(address signer);
event SignerRemoved(address signer);
event ScriptSigned(address scriptAddress, address signer);
event ScriptApproved(address scriptAddress);
event ScriptCancelled(address scriptAddress);
event ScriptExecuted(address scriptAddress, bool result);
constructor() public {
// deployer address is the first signer. Deployer can configure new contracts by itself being the only "signer"
// The first script which sets the new contracts live should add signers and revoke deployer's signature right
isSigner[msg.sender] = true;
allSigners.push(msg.sender);
activeSignersCount = 1;
emit SignerAdded(msg.sender);
}
function sign(address scriptAddress) public {
require(isSigner[msg.sender], "sender must be signer");
Script storage script = scripts[scriptAddress];
require(script.state == ScriptState.Approved || script.state == ScriptState.New,
"script state must be New or Approved");
require(!script.signedBy[msg.sender], "script must not be signed by signer yet");
if (script.allSigners.length == 0) {
// first sign of a new script
scriptAddresses.push(scriptAddress);
}
script.allSigners.push(msg.sender);
script.signedBy[msg.sender] = true;
script.signCount = script.signCount.add(1);
emit ScriptSigned(scriptAddress, msg.sender);
if (checkQuorum(script.signCount)) {
script.state = ScriptState.Approved;
emit ScriptApproved(scriptAddress);
}
}
function execute(address scriptAddress) public returns (bool result) {
// only allow execute to signers to avoid someone set an approved script failed by calling it with low gaslimit
require(isSigner[msg.sender], "sender must be signer");
Script storage script = scripts[scriptAddress];
require(script.state == ScriptState.Approved, "script state must be Approved");
result = _execute(scriptAddress);
}
/* Function to test scripts without signatures - always reverts.
It's even possible to test scripts without deployment if script deployed and dry run on a ganache */
function dryExecute(address scriptAddress) public {
bool result = _execute(scriptAddress);
if(!result) { // if delegatecall failed then revert with the revert returndata
// solium-disable-next-line security/no-inline-assembly
assembly {
let ptr := mload(0x40) // starts from the "free memory pointer"
returndatacopy(ptr, 0, returndatasize) // retreive delegatecall revert reason message
revert(ptr, returndatasize) // revert with the reason message from delegeatecall
}
}
revert("dryExecute success");
}
function cancelScript(address scriptAddress) public {
require(msg.sender == address(this), "only callable via MultiSig");
Script storage script = scripts[scriptAddress];
require(script.state == ScriptState.Approved || script.state == ScriptState.New,
"script state must be New or Approved");
script.state = ScriptState.Cancelled;
emit ScriptCancelled(scriptAddress);
}
/* requires quorum so it's callable only via a script executed by this contract */
function addSigners(address[] signers) public {
require(msg.sender == address(this), "only callable via MultiSig");
for (uint i = 0; i < signers.length; i++) {
if (!isSigner[signers[i]]) {
require(signers[i] != address(0), "new signer must not be 0x0");
activeSignersCount++;
allSigners.push(signers[i]);
isSigner[signers[i]] = true;
emit SignerAdded(signers[i]);
}
}
}
/* requires quorum so it's callable only via a script executed by this contract */
function removeSigners(address[] signers) public {
require(msg.sender == address(this), "only callable via MultiSig");
for (uint i = 0; i < signers.length; i++) {
if (isSigner[signers[i]]) {
require(activeSignersCount > 1, "must not remove last signer");
activeSignersCount--;
isSigner[signers[i]] = false;
emit SignerRemoved(signers[i]);
}
}
}
/* implement it in derived contract */
function checkQuorum(uint signersCount) internal view returns(bool isQuorum);
function getAllSignersCount() external view returns (uint allSignersCount) {
return allSigners.length;
}
// UI helper fx - Returns signers from offset as [signer id (index in allSigners), address as uint, isActive 0 or 1]
function getSigners(uint offset, uint16 chunkSize)
external view returns(uint[3][]) {
uint limit = SafeMath.min(offset.add(chunkSize), allSigners.length);
uint[3][] memory response = new uint[3][](limit.sub(offset));
for (uint i = offset; i < limit; i++) {
address signerAddress = allSigners[i];
response[i - offset] = [i, uint(signerAddress), isSigner[signerAddress] ? 1 : 0];
}
return response;
}
function getScriptsCount() external view returns (uint scriptsCount) {
return scriptAddresses.length;
}
// UI helper fx - Returns scripts from offset as
// [scriptId (index in scriptAddresses[]), address as uint, state, signCount]
function getScripts(uint offset, uint16 chunkSize)
external view returns(uint[4][]) {
uint limit = SafeMath.min(offset.add(chunkSize), scriptAddresses.length);
uint[4][] memory response = new uint[4][](limit.sub(offset));
for (uint i = offset; i < limit; i++) {
address scriptAddress = scriptAddresses[i];
response[i - offset] = [i, uint(scriptAddress),
uint(scripts[scriptAddress].state), scripts[scriptAddress].signCount];
}
return response;
}
function _execute(address scriptAddress) private returns (bool result) {
Script storage script = scripts[scriptAddress];
// passing scriptAddress to allow called script access its own public fx-s if needed
if (scriptAddress.delegatecall.gas(gasleft() - 23000)
(abi.encodeWithSignature("execute(address)", scriptAddress))) {
script.state = ScriptState.Done;
result = true;
} else {
script.state = ScriptState.Failed;
result = false;
}
emit ScriptExecuted(scriptAddress, result);
}
}