-
Notifications
You must be signed in to change notification settings - Fork 29
/
Custodian.sol
327 lines (281 loc) · 12.4 KB
/
Custodian.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
pragma solidity ^0.4.21;
/** @title A dual control contract.
*
* @notice A general purpose contract that implements dual control over
* co-operating contracts through a callback mechanism.
*
* @dev This contract implements dual control through a 2-of-N
* threshold multi-signature scheme. The contract recognizes a set of N signers,
* and will unlock requests with signatures from any distinct pair of them.
* This contract signals the unlocking through a co-operative callback
* scheme.
* This contract also provides time lock and revocation features.
* Requests made by a 'primary' account have a default time lock applied.
* All other request must pay a 1 ETH stake and have an extended time lock
* applied.
* A request that is completed will prevent all previous pending requests
* that share the same callback from being completed: this is the
* revocation feature.
*
* @author Gemini Trust Company, LLC
*/
contract Custodian {
// TYPES
/** @dev The `Request` struct stores a pending unlocking.
* `callbackAddress` and `callbackSelector` are the data required to
* make a callback. The custodian completes the process by
* calling `callbackAddress.call(callbackSelector, lockId)`, which
* signals to the contract co-operating with the Custodian that
* the 2-of-N signatures have been provided and verified.
*/
struct Request {
bytes32 lockId;
bytes4 callbackSelector; // bytes4 and address can be packed into 1 word
address callbackAddress;
uint256 idx;
uint256 timestamp;
bool extended;
}
// EVENTS
/// @dev Emitted by successful `requestUnlock` calls.
event Requested(
bytes32 _lockId,
address _callbackAddress,
bytes4 _callbackSelector,
uint256 _nonce,
address _whitelistedAddress,
bytes32 _requestMsgHash,
uint256 _timeLockExpiry
);
/// @dev Emitted by `completeUnlock` calls on requests in the time-locked state.
event TimeLocked(
uint256 _timeLockExpiry,
bytes32 _requestMsgHash
);
/// @dev Emitted by successful `completeUnlock` calls.
event Completed(
bytes32 _lockId,
bytes32 _requestMsgHash,
address _signer1,
address _signer2
);
/// @dev Emitted by `completeUnlock` calls where the callback failed.
event Failed(
bytes32 _lockId,
bytes32 _requestMsgHash,
address _signer1,
address _signer2
);
/// @dev Emitted by successful `extendRequestTimeLock` calls.
event TimeLockExtended(
uint256 _timeLockExpiry,
bytes32 _requestMsgHash
);
// MEMBERS
/** @dev The count of all requests.
* This value is used as a nonce, incorporated into the request hash.
*/
uint256 public requestCount;
/// @dev The set of signers: signatures from two signers unlock a pending request.
mapping (address => bool) public signerSet;
/// @dev The map of request hashes to pending requests.
mapping (bytes32 => Request) public requestMap;
/// @dev The map of callback addresses to callback selectors to request indexes.
mapping (address => mapping (bytes4 => uint256)) public lastCompletedIdxs;
/** @dev The default period of time (in seconds) to time-lock requests.
* All requests will be subject to this default time lock, and the duration
* is fixed at contract creation.
*/
uint256 public defaultTimeLock;
/** @dev The extended period of time (in seconds) to time-lock requests.
* Requests not from the primary account are subject to this time lock.
* The primary account may also elect to extend the time lock on requests
* that originally received the default.
*/
uint256 public extendedTimeLock;
/// @dev The primary account is the privileged account for making requests.
address public primary;
// CONSTRUCTOR
function Custodian(
address[] _signers,
uint256 _defaultTimeLock,
uint256 _extendedTimeLock,
address _primary
)
public
{
// check for at least two `_signers`
require(_signers.length >= 2);
// validate time lock params
require(_defaultTimeLock <= _extendedTimeLock);
defaultTimeLock = _defaultTimeLock;
extendedTimeLock = _extendedTimeLock;
primary = _primary;
// explicitly initialize `requestCount` to zero
requestCount = 0;
// turn the array into a set
for (uint i = 0; i < _signers.length; i++) {
// no zero addresses or duplicates
require(_signers[i] != address(0) && !signerSet[_signers[i]]);
signerSet[_signers[i]] = true;
}
}
// MODIFIERS
modifier onlyPrimary {
require(msg.sender == primary);
_;
}
// METHODS
/** @notice Requests an unlocking with a lock identifier and a callback.
*
* @dev If called by an account other than the primary a 1 ETH stake
* must be paid. This is an anti-spam measure. As well as the callback
* and the lock identifier parameters a 'whitelisted address' is required
* for compatibility with existing signature schemes.
*
* @param _lockId The identifier of a pending request in a co-operating contract.
* @param _callbackAddress The address of a co-operating contract.
* @param _callbackSelector The function selector of a function within
* the co-operating contract at address `_callbackAddress`.
* @param _whitelistedAddress An address whitelisted in existing
* offline control protocols.
*
* @return requestMsgHash The hash of a request message to be signed.
*/
function requestUnlock(
bytes32 _lockId,
address _callbackAddress,
bytes4 _callbackSelector,
address _whitelistedAddress
)
public
payable
returns (bytes32 requestMsgHash)
{
require(msg.sender == primary || msg.value >= 1 ether);
// disallow using a zero value for the callback address
require(_callbackAddress != address(0));
uint256 requestIdx = ++requestCount;
// compute a nonce value
// - the blockhash prevents prediction of future nonces
// - the address of this contract prevents conflicts with co-operating contracts using this scheme
// - the counter prevents conflicts arising from multiple txs within the same block
uint256 nonce = uint256(keccak256(block.blockhash(block.number - 1), address(this), requestIdx));
requestMsgHash = keccak256(nonce, _whitelistedAddress, uint256(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF));
requestMap[requestMsgHash] = Request({
lockId: _lockId,
callbackSelector: _callbackSelector,
callbackAddress: _callbackAddress,
idx: requestIdx,
timestamp: block.timestamp,
extended: false
});
// compute the expiry time
uint256 timeLockExpiry = block.timestamp;
if (msg.sender == primary) {
timeLockExpiry += defaultTimeLock;
} else {
timeLockExpiry += extendedTimeLock;
// any sender that is not the creator will get the extended time lock
requestMap[requestMsgHash].extended = true;
}
emit Requested(_lockId, _callbackAddress, _callbackSelector, nonce, _whitelistedAddress, requestMsgHash, timeLockExpiry);
}
/** @notice Completes a pending unlocking with two signatures.
*
* @dev Given a request message hash as two signatures of it from
* two distinct signers in the signer set, this function completes the
* unlocking of the pending request by executing the callback.
*
* @param _requestMsgHash The request message hash of a pending request.
* @param _recoveryByte1 The public key recovery byte (27 or 28)
* @param _ecdsaR1 The R component of an ECDSA signature (R, S) pair
* @param _ecdsaS1 The S component of an ECDSA signature (R, S) pair
* @param _recoveryByte2 The public key recovery byte (27 or 28)
* @param _ecdsaR2 The R component of an ECDSA signature (R, S) pair
* @param _ecdsaS2 The S component of an ECDSA signature (R, S) pair
*
* @return success True if the callback successfully executed.
*/
function completeUnlock(
bytes32 _requestMsgHash,
uint8 _recoveryByte1, bytes32 _ecdsaR1, bytes32 _ecdsaS1,
uint8 _recoveryByte2, bytes32 _ecdsaR2, bytes32 _ecdsaS2
)
public
returns (bool success)
{
Request storage request = requestMap[_requestMsgHash];
// copy storage to locals before `delete`
bytes32 lockId = request.lockId;
address callbackAddress = request.callbackAddress;
bytes4 callbackSelector = request.callbackSelector;
// failing case of the lookup if the callback address is zero
require(callbackAddress != address(0));
// reject confirms of earlier withdrawals buried under later confirmed withdrawals
require(request.idx > lastCompletedIdxs[callbackAddress][callbackSelector]);
address signer1 = ecrecover(_requestMsgHash, _recoveryByte1, _ecdsaR1, _ecdsaS1);
require(signerSet[signer1]);
address signer2 = ecrecover(_requestMsgHash, _recoveryByte2, _ecdsaR2, _ecdsaS2);
require(signerSet[signer2]);
require(signer1 != signer2);
if (request.extended && ((block.timestamp - request.timestamp) < extendedTimeLock)) {
emit TimeLocked(request.timestamp + extendedTimeLock, _requestMsgHash);
return false;
} else if ((block.timestamp - request.timestamp) < defaultTimeLock) {
emit TimeLocked(request.timestamp + defaultTimeLock, _requestMsgHash);
return false;
} else {
if (address(this).balance > 0) {
// reward sender with anti-spam payments
// ignore send success (assign to `success` but this will be overwritten)
success = msg.sender.send(address(this).balance);
}
// raise the waterline for the last completed unlocking
lastCompletedIdxs[callbackAddress][callbackSelector] = request.idx;
// and delete the request
delete requestMap[_requestMsgHash];
// invoke callback
success = callbackAddress.call(callbackSelector, lockId);
if (success) {
emit Completed(lockId, _requestMsgHash, signer1, signer2);
} else {
emit Failed(lockId, _requestMsgHash, signer1, signer2);
}
}
}
/** @notice Reclaim the storage of a pending request that is uncompleteable.
*
* @dev If a pending request shares the callback (address and selector) of
* a later request has has been completed, then the request can no longer
* be completed. This function will reclaim the contract storage of the
* pending request.
*
* @param _requestMsgHash The request message hash of a pending request.
*/
function deleteUncompletableRequest(bytes32 _requestMsgHash) public {
Request storage request = requestMap[_requestMsgHash];
uint256 idx = request.idx;
require(0 < idx && idx < lastCompletedIdxs[request.callbackAddress][request.callbackSelector]);
delete requestMap[_requestMsgHash];
}
/** @notice Extend the time lock of a pending request.
*
* @dev Requests made by the primary account receive the default time lock.
* This function allows the primary account to apply the extended time lock
* to one its own requests.
*
* @param _requestMsgHash The request message hash of a pending request.
*/
function extendRequestTimeLock(bytes32 _requestMsgHash) public onlyPrimary {
Request storage request = requestMap[_requestMsgHash];
// reject ‘null’ results from the map lookup
// this can only be the case if an unknown `_requestMsgHash` is received
require(request.callbackAddress != address(0));
// `extendRequestTimeLock` must be idempotent
require(request.extended != true);
// set the `extended` flag; note that this is never unset
request.extended = true;
emit TimeLockExtended(request.timestamp + extendedTimeLock, _requestMsgHash);
}
}