/
SafeMultisigWallet.sol
412 lines (359 loc) · 15.1 KB
/
SafeMultisigWallet.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
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
pragma solidity >=0.6.0;
pragma experimental ABIEncoderV2;
pragma ignoreIntOverflow;
pragma AbiHeader expire;
pragma AbiHeader pubkey;
pragma AbiHeader time;
interface IAccept {
function acceptTransfer(bytes payload) external payable;
}
/// @title Multisignature wallet
/// @author Tonlabs (https://tonlabs.io)
contract MultisigWallet is IAccept {
/*
* Storage
*/
struct Transaction {
// Transaction Id.
uint64 id;
// Transaction confirmations from custodians.
uint32 confirmationsMask;
// Number of required confirmations.
uint8 signsRequired;
// Number of confirmations already received.
uint8 signsReceived;
// Public key of custodian queued transaction.
uint256 creator;
// Index of custodian.
uint8 index;
// Destination address of gram transfer.
address payable dest;
// Amount of nanograms to transfer.
uint128 value;
// Flags for sending internal message (see SENDRAWMSG in TVM spec).
uint16 sendFlags;
// Payload used as body of outbound internal message.
TvmCell payload;
// Bounce flag for header of outbound internal message.
bool bounce;
}
/*
* Constants
*/
uint8 constant MAX_QUEUED_REQUESTS = 5;
uint64 constant EXPIRATION_TIME = 3600; // lifetime is 1 hour
uint8 constant MAX_CUSTODIAN_COUNT = 32;
uint128 constant MIN_VALUE = 1e6;
uint constant MAX_CLEANUP_TXNS = 40;
// Send flags.
// Forward fees for message will be paid from contract balance.
uint8 constant FLAG_PAY_FWD_FEE_FROM_BALANCE = 1;
// Tells node to ignore errors in action phase while outbound messages are sent.
uint8 constant FLAG_IGNORE_ERRORS = 2;
// Tells node to send all remaining balance.
uint8 constant FLAG_SEND_ALL_REMAINING = 128;
/*
* Variables
*/
// Public key of custodian who deployed a contract.
uint256 m_ownerKey;
// Binary mask with custodian requests (max 32 custodians).
uint256 m_requestsMask;
// Dictionary of queued transactions waiting confirmations.
mapping(uint64 => Transaction) m_transactions;
// Set of custodians, initiated in constructor, but values can be changed later in code.
mapping(uint256 => uint8) m_custodians; // pub_key -> custodian_index
// Read-only custodian count, initiated in constructor.
uint8 m_custodianCount;
// Default number of confirmations needed to execute transaction.
uint8 m_defaultRequiredConfirmations;
/*
Exception codes:
100 - message sender is not a custodian;
102 - transaction does not exist;
103 - operation is already confirmed by this custodian;
107 - input value is too low;
108 - wallet should have only one custodian;
113 - Too many requests for one custodian;
117 - invalid number of custodians;
121 - payload size is too big;
*/
/*
* Events
*/
event TransferAccepted(bytes payload);
/*
* Runtime functions
*/
function tvm_ctos(TvmCell cell) private pure returns (uint /* slice */) {}
function tvm_tree_cell_size(uint slice) private pure returns (uint, uint) {}
/*
* Constructor
*/
/// @dev Internal function called from constructor to initialize custodians.
function _initialize(uint256[] owners, uint8 reqConfirms) inline private {
uint8 ownerCount = 0;
m_ownerKey = owners[0];
uint256 len = owners.length;
for (uint256 i = 0; (i < len && ownerCount < MAX_CUSTODIAN_COUNT); i++) {
uint256 key = owners[i];
if (!m_custodians.exists(key)) {
m_custodians[key] = ownerCount++;
}
}
m_defaultRequiredConfirmations = ownerCount <= reqConfirms ? ownerCount : reqConfirms;
m_custodianCount = ownerCount;
}
/// @dev Contract constructor.
/// @param owners Array of custodian keys.
/// @param reqConfirms Default number of confirmations required for executing transaction.
constructor(uint256[] owners, uint8 reqConfirms) public {
require(msg.pubkey() == tvm.pubkey(), 100);
require(owners.length > 0 && owners.length <= MAX_CUSTODIAN_COUNT, 117);
tvm.accept();
_initialize(owners, reqConfirms);
}
/*
* Inline helper macros
*/
/// @dev Returns queued transaction count by custodian with defined index.
function _getMaskValue(uint256 mask, uint8 index) inline private pure returns (uint8) {
return uint8((mask >> (8 * uint256(index))) & 0xFF);
}
/// @dev Increment queued transaction count by custodian with defined index.
function _incMaskValue(uint256 mask, uint8 index) inline private pure returns (uint256) {
return mask + (1 << (8 * uint256(index)));
}
/// @dev Decrement queued transaction count by custodian with defined index.
function _decMaskValue(uint256 mask, uint8 index) inline private pure returns (uint256) {
return mask - (1 << (8 * uint256(index)));
}
/// @dev Checks bit with defined index in the mask.
function _checkBit(uint32 mask, uint8 index) inline private pure returns (bool) {
return (mask & (uint32(1) << index)) != 0;
}
/// @dev Checks if object is confirmed by custodian.
function _isConfirmed(uint32 mask, uint8 custodianIndex) inline private pure returns (bool) {
return _checkBit(mask, custodianIndex);
}
/// @dev Sets custodian confirmation bit in the mask.
function _setConfirmed(uint32 mask, uint8 custodianIndex) inline private pure returns (uint32) {
mask |= (uint32(1) << custodianIndex);
return mask;
}
/// @dev Checks that custodian with supplied public key exists in custodian set.
function _findCustodian(uint256 senderKey) inline private view returns (uint8) {
(bool exists, uint8 index) = m_custodians.fetch(senderKey);
require(exists, 100);
return index;
}
/// @dev Generates new id for object.
function _generateId() inline private view returns (uint64) {
return (uint64(now) << 32) | (tvm.transLT() & 0xFFFFFFFF);
}
/// @dev Returns timestamp after which transactions are treated as expired.
function _getExpirationBound() inline private view returns (uint64) {
return (uint64(now) - EXPIRATION_TIME) << 32;
}
/// @dev Returns transfer flags according to input value and `allBalance` flag.
function _getSendFlags(uint128 value, bool allBalance) inline private pure returns (uint8, uint128) {
uint8 flags = FLAG_IGNORE_ERRORS | FLAG_PAY_FWD_FEE_FROM_BALANCE;
if (allBalance) {
flags = FLAG_IGNORE_ERRORS | FLAG_SEND_ALL_REMAINING;
value = uint128(address(this).balance);
}
return (flags, value);
}
/*
* Public functions
*/
/// @dev A payable method for accepting incoming funds. Generates
/// an event with incoming payload.
/// @param payload Payload from message body.
function acceptTransfer(bytes payload) external override payable {
emit TransferAccepted(payload);
}
/// @dev Allows custodian if she is the only owner of multisig to transfer funds with minimal fees.
/// @param dest Transfer target address.
/// @param value Amount of funds to transfer.
/// @param bounce Bounce flag. Set true if need to transfer funds to existing account;
/// set false to create new account.
/// @param flags `sendmsg` flags.
/// @param payload Tree of cells used as body of outbound internal message.
function sendTransaction(
address payable dest,
uint128 value,
bool bounce,
uint8 flags,
TvmCell payload) public view
{
require(m_custodianCount == 1, 108);
require(msg.pubkey() == m_ownerKey, 100);
tvm.accept();
dest.transfer(value, bounce, flags, payload);
}
/// @dev Allows custodian to submit and confirm new transaction.
/// @param dest Transfer target address.
/// @param value Nanograms value to transfer.
/// @param bounce Bounce flag. Set true if need to transfer grams to existing account; set false to create new account.
/// @param allBalance Set true if need to transfer all remaining balance.
/// @param payload Tree of cells used as body of outbound internal message.
/// @return transId Transaction ID.
function submitTransaction(
address payable dest,
uint128 value,
bool bounce,
bool allBalance,
TvmCell payload)
public returns (uint64 transId)
{
uint256 senderKey = msg.pubkey();
uint8 index = _findCustodian(senderKey);
require(value >= MIN_VALUE, 107);
(uint bits, uint cells) = tvm_tree_cell_size(tvm_ctos(payload));
require(bits < 8192 && cells < 8, 121);
_removeExpiredTransactions();
require(_getMaskValue(m_requestsMask, index) < MAX_QUEUED_REQUESTS, 113);
tvm.accept();
(uint8 flags, uint128 realValue) = _getSendFlags(value, allBalance);
uint8 requiredSigns = m_defaultRequiredConfirmations;
if (requiredSigns == 1) {
dest.transfer(realValue, bounce, flags, payload);
return 0;
} else {
m_requestsMask = _incMaskValue(m_requestsMask, index);
uint64 trId = _generateId();
Transaction txn = Transaction(trId, 0/*mask*/, requiredSigns, 0/*signsReceived*/,
senderKey, index, dest, realValue, flags, payload, bounce);
_confirmTransaction(trId, txn, index);
return trId;
}
}
/// @dev Allows custodian to confirm a transaction.
/// @param transactionId Transaction ID.
function confirmTransaction(uint64 transactionId) public {
uint8 index = _findCustodian(msg.pubkey());
_removeExpiredTransactions();
(bool trexists, Transaction txn) = m_transactions.fetch(transactionId);
require(trexists, 102);
require(!_isConfirmed(txn.confirmationsMask, index), 103);
tvm.accept();
_confirmTransaction(transactionId, txn, index);
}
/*
* Internal functions
*/
/// @dev Confirms transaction by custodian with defined index.
/// @param transactionId Transaction id to confirm.
/// @param txn Transaction object to confirm.
/// @param custodianIndex Index of custodian.
function _confirmTransaction(uint64 transactionId, Transaction txn, uint8 custodianIndex) inline private {
if ((txn.signsReceived + 1) >= txn.signsRequired) {
txn.dest.transfer(txn.value, txn.bounce, txn.sendFlags, txn.payload);
m_requestsMask = _decMaskValue(m_requestsMask, txn.index);
delete m_transactions[transactionId];
} else {
txn.confirmationsMask = _setConfirmed(txn.confirmationsMask, custodianIndex);
txn.signsReceived++;
m_transactions[transactionId] = txn;
}
}
/// @dev Removes expired transactions from storage.
function _removeExpiredTransactions() inline private {
uint64 marker = _getExpirationBound();
(uint64 trId, Transaction txn, bool success) = m_transactions.min();
bool needCleanup = success && (trId <= marker);
if (!needCleanup) { return; }
tvm.accept();
uint i = 0;
while (needCleanup && i < MAX_CLEANUP_TXNS) {
// transaction is expired, remove it
i++;
m_requestsMask = _decMaskValue(m_requestsMask, txn.index);
delete m_transactions[trId];
(trId, txn, success) = m_transactions.next(trId);
needCleanup = success && (trId <= marker);
}
tvm.commit();
}
/*
* Get methods
*/
/// @dev Helper get-method for checking if custodian confirmation bit is set.
/// @return confirmed True if confirmation bit is set.
function isConfirmed(uint32 mask, uint8 index) public pure returns (bool confirmed) {
confirmed = _isConfirmed(mask, index);
}
/// @dev Get-method that returns wallet configuration parameters.
/// @return maxQueuedTransactions The maximum number of unconfirmed transactions that a custodian can submit.
/// @return maxCustodianCount The maximum allowed number of wallet custodians.
/// @return expirationTime Transaction lifetime in seconds.
/// @return minValue The minimum value allowed to transfer in one transaction.
/// @return requiredTxnConfirms The minimum number of confirmations required to execute transaction.
function getParameters() public view
returns (uint8 maxQueuedTransactions,
uint8 maxCustodianCount,
uint64 expirationTime,
uint128 minValue,
uint8 requiredTxnConfirms) {
maxQueuedTransactions = MAX_QUEUED_REQUESTS;
maxCustodianCount = MAX_CUSTODIAN_COUNT;
expirationTime = EXPIRATION_TIME;
minValue = MIN_VALUE;
requiredTxnConfirms = m_defaultRequiredConfirmations;
}
/// @dev Get-method that returns transaction info by id.
/// @return trans Transaction structure.
/// Throws exception if transaction does not exist.
function getTransaction(uint64 transactionId) public view
returns (Transaction trans) {
(bool exists, Transaction txn) = m_transactions.fetch(transactionId);
require(exists, 102);
trans = txn;
}
/// @dev Get-method that returns array of pending transactions.
/// Returns not expired transactions only.
/// @return transactions Array of queued transactions.
function getTransactions() public view returns (Transaction[] transactions) {
uint64 bound = _getExpirationBound();
(uint64 id, Transaction txn, bool success) = m_transactions.min();
while (success) {
// returns only not expired transactions
if (id > bound) {
transactions.push(txn);
}
(id, txn, success) = m_transactions.next(id);
}
}
/// @dev Get-method that returns submitted transaction ids.
/// @return ids Array of transaction ids.
function getTransactionIds() public view returns (uint64[] ids) {
uint64 trId = 0;
bool success = false;
(trId, , success) = m_transactions.min();
while (success) {
ids.push(trId);
(trId, , success) = m_transactions.next(trId);
}
}
/// @dev Helper structure to return information about custodian.
/// Used in getCustodians().
struct CustodianInfo {
uint8 index;
uint256 pubkey;
}
/// @dev Get-method that returns info about wallet custodians.
/// @return custodians Array of custodians.
function getCustodians() public view returns (CustodianInfo[] custodians) {
(uint256 key, uint8 index, bool success) = m_custodians.min();
while (success) {
custodians.push(CustodianInfo(index, key));
(key, index, success) = m_custodians.next(key);
}
}
/*
* Fallback and receive functions to receive simple transfers.
*/
fallback () external payable {}
receive () external payable {}
}