-
Notifications
You must be signed in to change notification settings - Fork 19
/
Copy pathTokenHolder.sol
451 lines (383 loc) · 12.5 KB
/
TokenHolder.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
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
pragma solidity ^0.4.23;
// Copyright 2018 OpenST Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import "./SafeMath.sol";
import "./EIP20TokenInterface.sol";
import "./MultiSigWallet.sol";
import "./TokenRules.sol";
/**
* @title TokenHolder contract.
*
* @notice Implements executable transactions (EIP-1077) for users to interact
* with token rules. It enables users to authorise sessions for
* ephemeral keys that dapps and mainstream applications can use to
* generate token events on-chain.
*/
contract TokenHolder is MultiSigWallet {
/* Usings */
using SafeMath for uint256;
/* Events */
event SessionAuthorizationSubmitted(
uint256 indexed _transactionID,
address _ephemeralKey,
uint256 _spendingLimit,
uint256 _expirationHeight
);
event SessionRevoked(
address _ephemeralKey
);
event RuleExecuted(
address indexed _to,
bytes4 _functionSelector,
address _ephemeralKey,
uint256 _nonce,
bytes32 _messageHash,
bool _status
);
/* Enums */
enum AuthorizationStatus {
NOT_AUTHORIZED,
AUTHORIZED,
REVOKED
}
/* Structs */
/** expirationHeight is the block number at which ephemeralKey expires. */
struct EphemeralKeyData {
uint256 spendingLimit;
uint256 nonce;
uint256 expirationHeight;
AuthorizationStatus status;
}
/* Constants */
bytes4 constant public AUTHORIZE_SESSION_CALLPREFIX = bytes4(
keccak256("authorizeSession(address,uint256,uint256)")
);
bytes4 public constant EXECUTE_RULE_CALLPREFIX = bytes4(
keccak256(
"executeRule(address,bytes,uint256,uint8,bytes32,bytes32)"
)
);
/* Storage */
EIP20TokenInterface public token;
mapping(address /* key */ => EphemeralKeyData) public ephemeralKeys;
address public tokenRules;
/* Modifiers */
modifier keyIsNotNull(address _key)
{
require(_key != address(0), "Key address is null.");
_;
}
/** Requires that key is in authorized state. */
modifier keyIsAuthorized(address _key)
{
AuthorizationStatus status = ephemeralKeys[_key].status;
require(
status == AuthorizationStatus.AUTHORIZED,
"Key is not authorized."
);
_;
}
/** Requires that key was not authorized. */
modifier keyDoesNotExist(address _key)
{
AuthorizationStatus status = ephemeralKeys[_key].status;
require(
status == AuthorizationStatus.NOT_AUTHORIZED,
"Key exists."
);
_;
}
/* Special Functions */
/**
* @dev Constructor requires:
* - EIP20 token address is not null.
* - Token rules address is not null.
*
* @param _token eip20 contract address deployed for an economy.
* @param _tokenRules Token rules contract address.
* @param _wallets array of wallet addresses.
* @param _required No of requirements for multi sig wallet.
*/
constructor(
EIP20TokenInterface _token,
address _tokenRules,
address[] _wallets,
uint256 _required
)
public
MultiSigWallet(_wallets, _required)
{
require(
_token != address(0),
"Token contract address is null."
);
require(
_tokenRules != address(0),
"TokenRules contract address is null."
);
token = _token;
tokenRules = _tokenRules;
}
/* External Functions */
/**
* @notice Submits a transaction for a session authorization with
* the specified ephemeral key.
*
* @dev Function requires:
* - Only registered wallet can call.
* - The key is not null.
* - The key does not exist.
* - Expiration height is bigger than the current block height.
*
* @param _ephemeralKey Ephemeral key to authorize.
* @param _spendingLimit Spending limit of the key.
* @param _expirationHeight Expiration height of the ephemeral key.
*
* @return transactionID_ Newly created transaction id.
*/
function submitAuthorizeSession(
address _ephemeralKey,
uint256 _spendingLimit,
uint256 _expirationHeight
)
public
onlyWallet
keyIsNotNull(_ephemeralKey)
keyDoesNotExist(_ephemeralKey)
returns (uint256 transactionID_)
{
require(
_expirationHeight > block.number,
"Expiration height is lte to the current block height."
);
transactionID_ = addTransaction(
address(this),
abi.encodeWithSelector(
AUTHORIZE_SESSION_CALLPREFIX,
_ephemeralKey,
_spendingLimit,
_expirationHeight
)
);
emit SessionAuthorizationSubmitted(
transactionID_,
_ephemeralKey,
_spendingLimit,
_expirationHeight
);
confirmTransaction(transactionID_);
}
/**
* @notice Revokes session for the specified ephemeral key.
*
* @dev Function revokes the key even if it has expired.
* Function requires:
* - Only registered wallet can call.
* - The key is authorized.
*
* @param _ephemeralKey Ephemeral key to revoke.
*/
function revokeSession(address _ephemeralKey)
external
onlyWallet
keyIsAuthorized(_ephemeralKey)
{
ephemeralKeys[_ephemeralKey].status = AuthorizationStatus.REVOKED;
emit SessionRevoked(_ephemeralKey);
}
/* Public Functions */
/**
* @notice Evaluates executable transaction signed by an ephemeral key.
*
* @dev As a first step, function validates executable transaction by
* checking that the specified signature matches one of the
* authorized (non-expired) ephemeral keys.
*
* On success, function executes transaction by calling:
* _to.call(_data);
*
* Before execution, it approves the tokenRules as a spender
* for ephemeralKey.spendingLimit amount. This allowance is cleared
* after execution.
*
* Function requires:
* - The key used to sign data is authorized and have not expired.
* - nonce matches the next available one (+1 of the last
* used one).
*
* @param _to The target contract address the transaction will be executed
* upon.
* @param _data The payload of a function to be executed in the target
* contract.
* @param _nonce The nonce of an ephemeral key that was used to sign
* the transaction.
*
* @return executeStatus_ True in case of successfull execution of the
* executable transaction, otherwise, false.
*/
function executeRule(
address _to,
bytes _data,
uint256 _nonce,
uint8 _v,
bytes32 _r,
bytes32 _s
)
public
payable
returns (bool executionStatus_)
{
bytes32 messageHash = bytes32(0);
address ephemeralKey = address(0);
(messageHash, ephemeralKey) = verifyExecutableTransaction(
EXECUTE_RULE_CALLPREFIX,
_to,
_data,
_nonce,
_v,
_r,
_s
);
EphemeralKeyData storage ephemeralKeyData = ephemeralKeys[ephemeralKey];
TokenRules(tokenRules).allowTransfers();
token.approve(
tokenRules,
ephemeralKeyData.spendingLimit
);
// solium-disable-next-line security/no-call-value
executionStatus_ = _to.call.value(msg.value)(_data);
token.approve(tokenRules, 0);
TokenRules(tokenRules).disallowTransfers();
bytes4 functionSelector = bytesToBytes4(_data);
emit RuleExecuted(
_to,
functionSelector,
ephemeralKey,
_nonce,
messageHash,
executionStatus_
);
}
function authorizeSession(
address _ephemeralKey,
uint256 _spendingLimit,
uint256 _expirationHeight
)
public
onlyMultisig
keyIsNotNull(_ephemeralKey)
keyDoesNotExist(_ephemeralKey)
{
require(
_expirationHeight > block.number,
"Expiration height is lte to the current block height."
);
EphemeralKeyData storage keyData = ephemeralKeys[_ephemeralKey];
keyData.spendingLimit = _spendingLimit;
keyData.expirationHeight = _expirationHeight;
keyData.nonce = 0;
keyData.status = AuthorizationStatus.AUTHORIZED;
}
/* Private Functions */
function verifyExecutableTransaction(
bytes4 _callPrefix,
address _to,
bytes _data,
uint256 _nonce,
uint8 _v,
bytes32 _r,
bytes32 _s
)
private
returns (bytes32 messageHash_, address key_)
{
messageHash_ = getMessageHash(
_callPrefix,
_to,
keccak256(_data),
_nonce
);
key_ = ecrecover(messageHash_, _v, _r, _s);
EphemeralKeyData storage keyData = ephemeralKeys[key_];
require(
keyData.status == AuthorizationStatus.AUTHORIZED &&
keyData.expirationHeight > block.number,
"Ephemeral key is not active."
);
uint256 expectedNonce = keyData.nonce.add(1);
require(
_nonce == expectedNonce,
"The next nonce is not provided."
);
keyData.nonce = expectedNonce;
}
/**
* @notice The hashed message format is compliant with EIP-1077.
*
* @dev EIP-1077 enables user to sign messages to show intent of execution,
* but allows a third party relayer to execute them.
* https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1077.md
*/
function getMessageHash(
bytes4 _callPrefix,
address _to,
bytes32 _dataHash,
uint256 _nonce
)
private
view
returns (bytes32 messageHash_)
{
messageHash_ = keccak256(
abi.encodePacked(
byte(0x19), // Starting a transaction with byte(0x19) ensure
// the signed data from being a valid ethereum
// transaction.
byte(0), // The version control byte.
address(this), // The from field will always be the contract
// executing the code.
_to,
uint8(0), // The amount in ether to be sent.
_dataHash,
_nonce,
uint8(0), // gasPrice
uint8(0), // gasLimit
uint8(0), // gasToken
_callPrefix, // 4 byte standard call prefix of the
// function to be called in the 'from' contract.
// This guarantees that signed message can
// be only executed in a single instance.
uint8(0), // 0 for a standard call, 1 for a DelegateCall and 2
// for a create opcode
bytes32(0) // extraHash is always hashed at the end. This is
// done to increase future compatibility of the
// standard.
)
);
}
/**
* @dev Retrieves the first 4 bytes of input byte array into byte4.
* Function requires:
* - Input byte array's length is greater than or equal to 4.
*/
function bytesToBytes4(bytes _input) public pure returns (bytes4 out_) {
require(
_input.length >= 4,
"Input bytes length is less than 4."
);
for (uint8 i = 0; i < 4; i++) {
out_ |= bytes4(_input[i] & 0xFF) >> (i * 8);
}
}
}