forked from tokencard/contracts
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathwallet.sol
More file actions
847 lines (742 loc) · 38.5 KB
/
Copy pathwallet.sol
File metadata and controls
847 lines (742 loc) · 38.5 KB
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
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
/**
* The Consumer Contract Wallet
* Copyright (C) 2019 The Contract Wallet Company Limited
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-License-Identifier: GPLv3
pragma solidity ^0.6.11;
import "./externals/Address.sol";
import "./externals/ECDSA.sol";
import "./externals/initializable.sol";
import "./externals/SafeMath.sol";
import "./externals/SafeERC20.sol";
import "./interfaces/IERC20.sol";
import "./interfaces/IERC165.sol";
import "./interfaces/ILicence.sol";
import "./internals/balanceable.sol";
import "./internals/controllable.sol";
import "./internals/ensResolvable.sol";
import "./internals/ownable.sol";
import "./internals/tokenWhitelistable.sol";
import "./internals/transferrable.sol";
/// @title ControllableOwnable combines Controllable and Ownable
/// @dev providing an additional modifier to check if Owner or Controller
contract ControllableOwnable is Controllable, Ownable {
/// @dev Check if the sender is the Owner or one of the Controllers
modifier onlyOwnerOrController() {
require(_isOwner(msg.sender) || _isController(msg.sender), "only owner||controller");
_;
}
}
/// @title SelfCallableOwnable allows either owner or the contract itself to call its functions
/// @dev providing an additional modifier to check if Owner or self is calling
/// @dev the "self" here is used for the meta transactions
contract SelfCallableOwnable is Ownable {
/// @dev Check if the sender is the Owner or self
modifier onlyOwnerOrSelf() {
require(_isOwner(msg.sender) || msg.sender == address(this), "only owner||self");
_;
}
}
/// @title AddressWhitelist provides payee-whitelist functionality.
/// @dev This contract will allow the user to maintain a whitelist of addresses
/// @dev These addresses will live outside of the various spend limits
contract AddressWhitelist is ControllableOwnable, SelfCallableOwnable {
using SafeMath for uint256;
event AddedToWhitelist(address _sender, address[] _addresses);
event CancelledWhitelistAddition(address _sender, bytes32 _hash);
event SubmittedWhitelistAddition(address[] _addresses, bytes32 _hash);
event CancelledWhitelistRemoval(address _sender, bytes32 _hash);
event RemovedFromWhitelist(address _sender, address[] _addresses);
event SubmittedWhitelistRemoval(address[] _addresses, bytes32 _hash);
mapping(address => bool) public whitelistMap;
address[] public whitelistArray;
address[] private _pendingWhitelistAddition;
address[] private _pendingWhitelistRemoval;
bool public submittedWhitelistAddition;
bool public submittedWhitelistRemoval;
bool public isSetWhitelist;
/// @dev Check if the provided addresses contain the owner or the zero-address address.
modifier hasNoOwnerOrZeroAddress(address[] memory _addresses) {
for (uint256 i = 0; i < _addresses.length; i++) {
require(!_isOwner(_addresses[i]), "contains owner address");
require(_addresses[i] != address(0), "contains 0 address");
}
_;
}
/// @dev Check that neither addition nor removal operations have already been submitted.
modifier noActiveSubmission() {
require(!submittedWhitelistAddition && !submittedWhitelistRemoval, "whitelist sumbission pending");
_;
}
/// @dev Cancel pending whitelist addition.
function cancelWhitelistAddition(bytes32 _hash) external onlyOwnerOrController {
// Check if operation has been submitted.
require(submittedWhitelistAddition, "no pending submission");
// Require that confirmation hash and the hash of the pending whitelist addition match
require(_hash == calculateHash(_pendingWhitelistAddition), "non-matching pending whitelist hash");
// Reset pending addresses.
delete _pendingWhitelistAddition;
// Reset the submitted operation flag.
submittedWhitelistAddition = false;
// Emit the cancellation event.
emit CancelledWhitelistAddition(msg.sender, _hash);
}
/// @dev Cancel pending removal of whitelisted addresses.
function cancelWhitelistRemoval(bytes32 _hash) external onlyOwnerOrController {
// Check if operation has been submitted.
require(submittedWhitelistRemoval, "no pending submission");
// Require that confirmation hash and the hash of the pending whitelist removal match
require(_hash == calculateHash(_pendingWhitelistRemoval), "non-matching pending whitelist hash");
// Reset pending addresses.
delete _pendingWhitelistRemoval;
// Reset pending addresses.
submittedWhitelistRemoval = false;
// Emit the cancellation event.
emit CancelledWhitelistRemoval(msg.sender, _hash);
}
/// @dev Confirm pending whitelist addition.
/// @dev This will only ever be applied post 2FA, by one of the Controllers
/// @param _hash is the hash of the pending whitelist array, a form of lamport lock
function confirmWhitelistAddition(bytes32 _hash) external onlyController {
// Require that the whitelist addition has been submitted.
require(submittedWhitelistAddition, "no pending submission");
// Require that confirmation hash and the hash of the pending whitelist addition match
require(_hash == calculateHash(_pendingWhitelistAddition), "non-matching pending whitelist hash");
// Whitelist pending addresses.
for (uint256 i = 0; i < _pendingWhitelistAddition.length; i++) {
// check if it doesn't exist already.
if (!whitelistMap[_pendingWhitelistAddition[i]]) {
// add to the Map and the Array
whitelistMap[_pendingWhitelistAddition[i]] = true;
whitelistArray.push(_pendingWhitelistAddition[i]);
}
}
// Emit the addition event.
emit AddedToWhitelist(msg.sender, _pendingWhitelistAddition);
// Reset pending addresses.
delete _pendingWhitelistAddition;
// Reset the submission flag.
submittedWhitelistAddition = false;
}
/// @dev Confirm pending removal of whitelisted addresses.
function confirmWhitelistRemoval(bytes32 _hash) external onlyController {
// Require that the pending whitelist is not empty and the operation has been submitted.
require(submittedWhitelistRemoval, "no pending submission");
// Require that confirmation hash and the hash of the pending whitelist removal match
require(_hash == calculateHash(_pendingWhitelistRemoval), "non-matching pending whitelist hash");
// Remove pending addresses.
for (uint256 i = 0; i < _pendingWhitelistRemoval.length; i++) {
// check if it exists
if (whitelistMap[_pendingWhitelistRemoval[i]]) {
whitelistMap[_pendingWhitelistRemoval[i]] = false;
for (uint256 j = 0; j < whitelistArray.length.sub(1); j++) {
if (whitelistArray[j] == _pendingWhitelistRemoval[i]) {
whitelistArray[j] = whitelistArray[whitelistArray.length - 1];
break;
}
}
whitelistArray.pop();
}
}
// Emit the removal event.
emit RemovedFromWhitelist(msg.sender, _pendingWhitelistRemoval);
// Reset pending addresses.
delete _pendingWhitelistRemoval;
// Reset the submission flag.
submittedWhitelistRemoval = false;
}
/// @dev Getter for pending addition array.
function pendingWhitelistAddition() external view returns (address[] memory) {
return _pendingWhitelistAddition;
}
/// @dev Getter for pending removal array.
function pendingWhitelistRemoval() external view returns (address[] memory) {
return _pendingWhitelistRemoval;
}
/// @dev Add initial addresses to the whitelist.
/// @param _addresses are the Ethereum addresses to be whitelisted.
function setWhitelist(address[] calldata _addresses) external onlyOwnerOrSelf hasNoOwnerOrZeroAddress(_addresses) {
// Require that the whitelist has not been initialized.
require(!isSetWhitelist, "whitelist initialized");
// Add each of the provided addresses to the whitelist.
for (uint256 i = 0; i < _addresses.length; i++) {
// Dedup addresses before writing to the whitelist
if (!whitelistMap[_addresses[i]]) {
// adds to the whitelist mapping
whitelistMap[_addresses[i]] = true;
// adds to the whitelist array
whitelistArray.push(_addresses[i]);
}
}
isSetWhitelist = true;
// Emit the addition event.
emit AddedToWhitelist(msg.sender, whitelistArray);
}
/// @dev Add addresses to the whitelist.
/// @param _addresses are the Ethereum addresses to be whitelisted.
function submitWhitelistAddition(address[] calldata _addresses) external onlyOwnerOrSelf noActiveSubmission hasNoOwnerOrZeroAddress(_addresses) {
// Require that the whitelist has been initialized.
require(isSetWhitelist, "whitelist not initialized");
// Require this array of addresses not empty
require(_addresses.length > 0, "empty whitelist");
// Set the provided addresses to the pending addition addresses.
_pendingWhitelistAddition = _addresses;
// Flag the operation as submitted.
submittedWhitelistAddition = true;
// Emit the submission event.
emit SubmittedWhitelistAddition(_addresses, calculateHash(_addresses));
}
/// @dev Remove addresses from the whitelist.
/// @param _addresses are the Ethereum addresses to be removed.
function submitWhitelistRemoval(address[] calldata _addresses) external onlyOwnerOrSelf noActiveSubmission {
// Require that the whitelist has been initialized.
require(isSetWhitelist, "whitelist not initialized");
// Require that the array of addresses is not empty
require(_addresses.length > 0, "empty whitelist");
// Add the provided addresses to the pending addition list.
_pendingWhitelistRemoval = _addresses;
// Flag the operation as submitted.
submittedWhitelistRemoval = true;
// Emit the submission event.
emit SubmittedWhitelistRemoval(_addresses, calculateHash(_addresses));
}
/// @dev Method used to hash our whitelist address arrays.
function calculateHash(address[] memory _addresses) public pure returns (bytes32) {
return keccak256(abi.encodePacked(_addresses));
}
}
/// @title DailyLimitTrait This trait allows for daily limits to be included in other contracts.
/// This contract will allow for a DailyLimit object to be instantiated and used.
library DailyLimitTrait {
using SafeMath for uint256;
event UpdatedAvailableLimit();
struct DailyLimit {
uint256 value;
uint256 available;
uint256 limitTimestamp;
uint256 pending;
bool controllerConfirmationRequired;
}
/// @dev Confirm pending set daily limit operation.
function _confirmLimitUpdate(DailyLimit storage self, uint256 _amount) internal {
// Require that pending and confirmed spend limit are the same
require(self.pending == _amount, "confirmed/submitted limit mismatch");
// Modify spend limit based on the pending value.
_modifyLimit(self, self.pending);
}
/// @dev Use up amount within the daily limit. Will fail if amount is larger than daily limit.
function _enforceLimit(DailyLimit storage self, uint256 _amount) internal {
// Account for the spend limit daily reset.
_updateAvailableLimit(self);
require(self.available >= _amount, "available<amount");
self.available = self.available.sub(_amount);
}
/// @dev Returns the available daily balance - accounts for daily limit reset.
/// @return amount of available to spend within the current day in base units.
function _getAvailableLimit(DailyLimit storage self) internal view returns (uint256) {
if (now > self.limitTimestamp.add(24 hours)) {
return self.value;
} else {
return self.available;
}
}
/// @dev Modify the spend limit and spend available based on the provided value.
/// @dev _amount is the daily limit amount in wei.
function _modifyLimit(DailyLimit storage self, uint256 _amount) private {
// Account for the spend limit daily reset.
_updateAvailableLimit(self);
// Set the daily limit to the provided amount.
self.value = _amount;
// Lower the available limit if it's higher than the new daily limit.
if (self.available > self.value) {
self.available = self.value;
}
}
/// @dev Set the daily limit.
/// @param _amount is the daily limit amount in base units.
function _setLimit(DailyLimit storage self, uint256 _amount) internal {
// Require that the spend limit has not been set yet.
require(!self.controllerConfirmationRequired, "limit already set");
// Modify spend limit based on the provided value.
_modifyLimit(self, _amount);
// Flag the operation as set.
self.controllerConfirmationRequired = true;
}
/// @dev Submit a daily limit update, needs to be confirmed.
/// @param _amount is the daily limit amount in base units.
function _submitLimitUpdate(DailyLimit storage self, uint256 _amount) internal {
// Require that the spend limit has been set.
require(self.controllerConfirmationRequired, "limit hasn't been set yet");
// Assign the provided amount to pending daily limit.
self.pending = _amount;
}
/// @dev Update available spend limit based on the daily reset.
function _updateAvailableLimit(DailyLimit storage self) private {
if (now > self.limitTimestamp.add(24 hours)) {
// Update the current timestamp.
self.limitTimestamp = now;
// Set the available limit to the current spend limit.
self.available = self.value;
emit UpdatedAvailableLimit();
}
}
}
/// @title it provides daily spend limit functionality.
contract SpendLimit is ControllableOwnable, SelfCallableOwnable {
event SetSpendLimit(address _sender, uint256 _amount);
event SubmittedSpendLimitUpdate(uint256 _amount);
using DailyLimitTrait for DailyLimitTrait.DailyLimit;
DailyLimitTrait.DailyLimit internal _spendLimit;
/// @dev Confirm pending set daily limit operation.
function confirmSpendLimitUpdate(uint256 _amount) external onlyController {
_spendLimit._confirmLimitUpdate(_amount);
emit SetSpendLimit(msg.sender, _amount);
}
/// @dev Sets the initial daily spend (aka transfer) limit for non-whitelisted addresses.
/// @param _amount is the daily limit amount in wei.
function setSpendLimit(uint256 _amount) external onlyOwnerOrSelf {
_spendLimit._setLimit(_amount);
emit SetSpendLimit(msg.sender, _amount);
}
/// @dev View your available limit
function spendLimitAvailable() external view returns (uint256) {
return _spendLimit._getAvailableLimit();
}
/// @dev Is there an active spend limit change
function spendLimitPending() external view returns (uint256) {
return _spendLimit.pending;
}
/// @dev Has the spend limit been initialised
function spendLimitControllerConfirmationRequired() external view returns (bool) {
return _spendLimit.controllerConfirmationRequired;
}
/// @dev View how much has been spent already
function spendLimitValue() external view returns (uint256) {
return _spendLimit.value;
}
/// @dev Submit a daily transfer limit update for non-whitelisted addresses.
/// @param _amount is the daily limit amount in wei.
function submitSpendLimitUpdate(uint256 _amount) external onlyOwnerOrSelf {
_spendLimit._submitLimitUpdate(_amount);
emit SubmittedSpendLimitUpdate(_amount);
}
/// @dev Initializes the daily spend limit in wei.
function _initializeSpendLimit(uint256 _limit) internal initializer {
_spendLimit = DailyLimitTrait.DailyLimit(_limit, _limit, now, 0, false);
}
}
/// @title GasTopUpLimit provides daily limit functionality.
contract GasTopUpLimit is ControllableOwnable, SelfCallableOwnable {
event SetGasTopUpLimit(address _sender, uint256 _amount);
event SubmittedGasTopUpLimitUpdate(uint256 _amount);
uint256 private constant _MAXIMUM_GAS_TOPUP_LIMIT = 500 finney;
uint256 private constant _MINIMUM_GAS_TOPUP_LIMIT = 1 finney;
using DailyLimitTrait for DailyLimitTrait.DailyLimit;
DailyLimitTrait.DailyLimit internal _gasTopUpLimit;
/// @dev Confirm pending set top up gas limit operation.
function confirmGasTopUpLimitUpdate(uint256 _amount) external onlyController {
_gasTopUpLimit._confirmLimitUpdate(_amount);
emit SetGasTopUpLimit(msg.sender, _amount);
}
/// @dev View your available gas top-up limit
function gasTopUpLimitAvailable() external view returns (uint256) {
return _gasTopUpLimit._getAvailableLimit();
}
/// @dev Is there an active gas top-up limit change
function gasTopUpLimitPending() external view returns (uint256) {
return _gasTopUpLimit.pending;
}
/// @dev Has the gas top-up limit been initialised
function gasTopUpLimitControllerConfirmationRequired() external view returns (bool) {
return _gasTopUpLimit.controllerConfirmationRequired;
}
/// @dev View how much gas top-up has been spent already
function gasTopUpLimitValue() external view returns (uint256) {
return _gasTopUpLimit.value;
}
/// @dev Sets the daily gas top up limit.
/// @param _amount is the gas top up amount in wei.
function setGasTopUpLimit(uint256 _amount) external onlyOwnerOrSelf {
require(_MINIMUM_GAS_TOPUP_LIMIT <= _amount && _amount <= _MAXIMUM_GAS_TOPUP_LIMIT, "out of range top-up");
_gasTopUpLimit._setLimit(_amount);
emit SetGasTopUpLimit(msg.sender, _amount);
}
/// @dev Submit a daily gas top up limit update.
/// @param _amount is the daily top up gas limit amount in wei.
function submitGasTopUpLimitUpdate(uint256 _amount) external onlyOwnerOrSelf {
require(_MINIMUM_GAS_TOPUP_LIMIT <= _amount && _amount <= _MAXIMUM_GAS_TOPUP_LIMIT, "out of range top-up");
_gasTopUpLimit._submitLimitUpdate(_amount);
emit SubmittedGasTopUpLimitUpdate(_amount);
}
/// @dev Initializes the daily gas topup limit in wei.
function _initializeGasTopUpLimit() internal initializer {
_gasTopUpLimit = DailyLimitTrait.DailyLimit(_MAXIMUM_GAS_TOPUP_LIMIT, _MAXIMUM_GAS_TOPUP_LIMIT, now, 0, false);
}
}
/// @title LoadLimit provides daily load limit functionality.
contract LoadLimit is ControllableOwnable, SelfCallableOwnable, TokenWhitelistable {
event SetLoadLimit(address _sender, uint256 _amount);
event SubmittedLoadLimitUpdate(uint256 _amount);
uint256 private constant _MAXIMUM_STABLECOIN_LOAD_LIMIT = 10000; // USD
uint256 private _maximumLoadLimit;
using DailyLimitTrait for DailyLimitTrait.DailyLimit;
DailyLimitTrait.DailyLimit internal _loadLimit;
/// @dev Sets a daily card load limit.
/// @param _amount is the card load amount in current stablecoin base units.
function setLoadLimit(uint256 _amount) external onlyOwnerOrSelf {
require(_amount <= _maximumLoadLimit, "out of range load amount");
_loadLimit._setLimit(_amount);
emit SetLoadLimit(msg.sender, _amount);
}
/// @dev Submit a daily load limit update.
/// @param _amount is the daily load limit amount in wei.
function submitLoadLimitUpdate(uint256 _amount) external onlyOwnerOrSelf {
require(_amount <= _maximumLoadLimit, "out of range load amount");
_loadLimit._submitLimitUpdate(_amount);
emit SubmittedLoadLimitUpdate(_amount);
}
/// @dev Confirm pending set load limit operation.
function confirmLoadLimitUpdate(uint256 _amount) external onlyController {
_loadLimit._confirmLimitUpdate(_amount);
emit SetLoadLimit(msg.sender, _amount);
}
/// @dev View your available load limit
function loadLimitAvailable() external view returns (uint256) {
return _loadLimit._getAvailableLimit();
}
/// @dev Is there an active load limit change
function loadLimitPending() external view returns (uint256) {
return _loadLimit.pending;
}
/// @dev Has the load limit been initialised
function loadLimitControllerConfirmationRequired() external view returns (bool) {
return _loadLimit.controllerConfirmationRequired;
}
/// @dev View how much laod limit has been spent already
function loadLimitValue() external view returns (uint256) {
return _loadLimit.value;
}
function _initializeLoadLimit(bytes32 _tokenWhitelistNode) internal initializer {
_initializeTokenWhitelistable(_tokenWhitelistNode);
(, uint256 stablecoinMagnitude, , , , , ) = _getStablecoinInfo();
require(stablecoinMagnitude > 0, "no stablecoin");
_maximumLoadLimit = _MAXIMUM_STABLECOIN_LOAD_LIMIT * stablecoinMagnitude;
_loadLimit = DailyLimitTrait.DailyLimit(_maximumLoadLimit, _maximumLoadLimit, now, 0, false);
}
}
/// @title Asset wallet with extra security features, gas top up management and card integration.
contract Wallet is ENSResolvable, AddressWhitelist, SpendLimit, GasTopUpLimit, LoadLimit, IERC165, Transferrable, Balanceable {
using Address for address;
using ECDSA for bytes32;
using SafeERC20 for IERC20;
using SafeMath for uint256;
event ExecutedRelayedTransaction(bytes _data, bytes _returnData);
event ExecutedTransaction(address _destination, uint256 _value, bytes _data, bytes _returnData);
event IncreasedRelayNonce(address _sender, uint256 _currentNonce);
event LoadedTokenCard(address _asset, uint256 _amount);
event ToppedUpGas(address _sender, address _owner, uint256 _amount);
event Transferred(address _to, address _asset, uint256 _amount);
event UpdatedAvailableLimit(); // This is here because our tests don't inherit events from a library
string public constant WALLET_VERSION = "3.4.1";
// keccak256("isValidSignature(bytes,bytes)") = 20c13b0bc670c284a9f19cdf7a533ca249404190f8dc132aac33e733b965269e
bytes4 private constant _EIP_1271 = 0x20c13b0b;
// keccak256("isValidSignature(bytes32,bytes)") = 1626ba7e356f5979dd355a3d2bfb43e80420a480c3b854edce286a82d7496869
bytes4 private constant _EIP_1654 = 0x1626ba7e;
/// @dev Supported ERC165 interface ID.
bytes4 private constant _ERC165_INTERFACE_ID = 0x01ffc9a7; // solium-disable-line uppercase
/// @dev this is an internal nonce to prevent replay attacks from relayer
uint256 public relayNonce;
/// @dev Is the registered ENS node identifying the licence contract.
bytes32 private _licenceNode;
/// @dev Initializes the wallet top up limit and the vault contract.
/// @param _owner_ is the owner account of the wallet contract.
/// @param _transferable_ indicates whether the contract ownership can be transferred.
/// @param _ens_ is the address of the ENS registry.
/// @param _tokenWhitelistNode_ is the ENS name node of the Token whitelist.
/// @param _controllerNode_ is the ENS name node of the Controller contract.
/// @param _licenceNode_ is the ENS name node of the Licence contract.
/// @param _spendLimit_ is the initial spend limit.
function initializeWallet(
address payable _owner_,
bool _transferable_,
address _ens_,
bytes32 _tokenWhitelistNode_,
bytes32 _controllerNode_,
bytes32 _licenceNode_,
uint256 _spendLimit_
) external initializer {
_initializeENSResolvable(_ens_);
_initializeControllable(_controllerNode_);
_initializeOwnable(_owner_, _transferable_);
_initializeSpendLimit(_spendLimit_);
_initializeGasTopUpLimit();
_initializeLoadLimit(_tokenWhitelistNode_);
_licenceNode = _licenceNode_;
}
/// @dev Checks if the value is not zero.
modifier isNotZero(uint256 _value) {
require(_value != 0, "value=0");
_;
}
/// @dev This function allows for the controller to relay transactions on the owner's behalf,
/// the relayed message has to be signed by the owner.
/// @param _nonce only used for relayed transactions, must match the wallet's relayNonce.
/// @param _data abi encoded data payload.
/// @param _signature signed prefix + data.
function executeRelayedTransaction(
uint256 _nonce,
bytes calldata _data,
bytes calldata _signature
) external onlyController {
// Expecting prefixed data ("monolith:") indicating relayed transaction...
// ...and an Ethereum Signed Message to protect user from signing an actual Tx
uint256 id;
assembly {
id := chainid() //1 for Ethereum mainnet, > 1 for public testnets.
}
bytes32 dataHash = keccak256(abi.encodePacked("monolith:", id, address(this), _nonce, _data)).toEthSignedMessageHash();
// Verify signature validity i.e. signer == owner
require(isValidSignature(dataHash, _signature) == _EIP_1654, "sig not valid");
// Verify and increase relayNonce to prevent replay attacks from the relayer
require(_nonce == relayNonce, "tx replay");
_increaseRelayNonce();
// invoke wallet function with an external call
(bool success, bytes memory returnData) = address(this).call(_data);
require(success, string(returnData));
emit ExecutedRelayedTransaction(_data, returnData);
}
/// @dev This returns the balance of the contract for any ERC20 token or ETH.
/// @param _asset is the address of an ERC20 token or 0x0 for ETH.
function getBalance(address _asset) external view returns (uint256) {
return _balance(_asset);
}
/// @dev This allows the user to cancel a transaction that was unexpectedly delayed by the relayer
function increaseRelayNonce() external onlyOwner {
_increaseRelayNonce();
}
/// @dev This bumps the relayNonce and emits an event accordingly
function _increaseRelayNonce() internal {
relayNonce++;
emit IncreasedRelayNonce(msg.sender, relayNonce);
}
/// @dev Implements EIP-1271: receives the raw data (bytes)
/// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1271.md
/// @param _data Arbitrary length data signed on the behalf of address(this)
/// @param _signature Signature byte array associated with _data
function isValidSignature(bytes calldata _data, bytes calldata _signature) external view returns (bytes4) {
bytes32 dataHash = keccak256(abi.encodePacked(_data));
// isValidSignature call reverts if not valid.
require(isValidSignature(dataHash, _signature) == _EIP_1654, "sig not valid");
return _EIP_1271;
}
/// @return licence contract node registered in ENS.
function licenceNode() external view returns (bytes32) {
return _licenceNode;
}
/// @dev Load a token card with the specified asset amount.
/// @dev the amount send should be inclusive of the percent licence.
/// @param _asset is the address of an ERC20 token or 0x0 for ether.
/// @param _amount is the amount of assets to be transferred in base units.
function loadTokenCard(address _asset, uint256 _amount) external payable onlyOwnerOrSelf {
// check if token is allowed to be used for loading the card
require(_isTokenLoadable(_asset), "token not loadable");
// Convert token amount to stablecoin value.
uint256 stablecoinValue = convertToStablecoin(_asset, _amount);
// Check against the daily spent limit and update accordingly, require that the value is under remaining limit.
_loadLimit._enforceLimit(stablecoinValue);
// Get the TKN licenceAddress from ENS
address licenceAddress = _ensResolve(_licenceNode);
if (_asset != address(0)) {
IERC20(_asset).safeApprove(licenceAddress, _amount);
ILicence(licenceAddress).load(_asset, _amount);
} else {
ILicence(licenceAddress).load{value: _amount}(_asset, _amount);
}
emit LoadedTokenCard(_asset, _amount);
}
/// @dev Checks for interface support based on ERC165.
function supportsInterface(bytes4 _interfaceID) external override view returns (bool) {
return _interfaceID == _ERC165_INTERFACE_ID;
}
/// @dev Refill owner's gas balance, revert if the transaction amount is too large
/// @param _amount is the amount of ether to transfer to the owner account in wei.
function topUpGas(uint256 _amount) external isNotZero(_amount) onlyOwnerOrController {
// Check contract balance is sufficient for the operation
require(address(this).balance > _amount, "balance not sufficient");
// Check against the daily spent limit and update accordingly, require that the value is under remaining limit.
_gasTopUpLimit._enforceLimit(_amount);
// Then perform the transfer
owner().transfer(_amount);
// Emit the gas top up event.
emit ToppedUpGas(msg.sender, owner(), _amount);
}
/// @dev This function allows for the wallet to send a batch of transactions instead of one,
/// it calls executeTransaction() so that the daily limit is enforced.
/// @param _transactionBatch data encoding the transactions to be sent,
/// following executeTransaction's format i.e. (destination, value, data)
function batchExecuteTransaction(bytes memory _transactionBatch) public onlyOwnerOrSelf {
uint256 batchLength = _transactionBatch.length + 32; // because the pos starts from 32
uint256 remainingBytesLength = _transactionBatch.length; // remaining bytes to be processed
uint256 pos = 32; //the first 32 bytes denote the byte array length, start from actual data
address destination; // destination address
uint256 value; // trasanction value
uint256 dataLength; // externall call data length
bytes memory data; // call data
while (pos < batchLength) {
// there should always be at least 84 bytes remaining: the minimun #bytes required to encode a Tx
remainingBytesLength = remainingBytesLength.sub(84);
assembly {
// shift right by 96 bits (256 - 160) to get the destination address (and zero the excessive bytes)
destination := shr(96, mload(add(_transactionBatch, pos)))
// get value: pos + 20 bytes (destinnation address)
value := mload(add(_transactionBatch, add(pos, 20)))
// get data: pos + 20 (destination address) + 32 (value) bytes
// the first 32 bytes denote the byte array length
dataLength := mload(add(_transactionBatch, add(pos, 52)))
data := add(_transactionBatch, add(pos, 52))
}
// pos += 20 + 32 + 32 + dataLength, reverts in case of overflow
pos = pos.add(dataLength).add(84);
// revert in case the encoded dataLength is gonna cause an out of bound access
require(pos <= batchLength, "out of bounds");
// if length is 0 ignore the data field
if (dataLength == 0) {
data = bytes("");
}
// call executeTransaction(), if one of them reverts then the whole batch reverts.
executeTransaction(destination, value, data);
}
}
/// @dev Convert ERC20 token amount to the corresponding ether amount.
/// @param _token ERC20 token contract address.
/// @param _amount amount of token in base units.
function convertToEther(address _token, uint256 _amount) public view returns (uint256) {
// Store the token in memory to save map entry lookup gas.
(, uint256 magnitude, uint256 rate, bool available, , , ) = _getTokenInfo(_token);
// If the token exists require that its rate is not zero.
if (available) {
require(rate != 0, "rate=0");
// Safely convert the token amount to ether based on the exchange rate.
return _amount.mul(rate).div(magnitude);
}
return 0;
}
/// @dev Convert ether or ERC20 token amount to the corresponding stablecoin amount.
/// @param _token ERC20 token contract address.
/// @param _amount amount of token in base units.
function convertToStablecoin(address _token, uint256 _amount) public view returns (uint256) {
// avoid the unnecessary calculations if the token to be loaded is the stablecoin itself
if (_token == _stablecoin()) {
return _amount;
}
uint256 amountToSend = _amount;
// 0x0 represents ether
if (_token != address(0)) {
// convert to eth first, same as convertToEther()
// Store the token in memory to save map entry lookup gas.
(, uint256 magnitude, uint256 rate, bool available, , , ) = _getTokenInfo(_token);
// require that token both exists in the whitelist and its rate is not zero.
require(available, "token not available");
require(rate != 0, "rate=0");
// Safely convert the token amount to ether based on the exchangeonly rate.
amountToSend = _amount.mul(rate).div(magnitude);
}
// _amountToSend now is in ether
// Get the stablecoin's magnitude and its current rate.
(, uint256 stablecoinMagnitude, uint256 stablecoinRate, bool stablecoinAvailable, , , ) = _getStablecoinInfo();
// Check if the stablecoin rate is set.
require(stablecoinAvailable, "token not available");
require(stablecoinRate != 0, "stablecoin rate=0");
// Safely convert the token amount to stablecoin based on its exchange rate and the stablecoin exchange rate.
return amountToSend.mul(stablecoinMagnitude).div(stablecoinRate);
}
/// @dev This function allows for the owner to send any transaction from the Wallet to arbitrary addresses
/// @param _destination address of the transaction
/// @param _value ETH amount in wei
/// @param _data transaction payload binary
function executeTransaction(
address _destination,
uint256 _value,
bytes memory _data
) public onlyOwnerOrSelf returns (bytes memory) {
// If value is send across as a part of this executeTransaction, this will be sent to any payable
// destination. As a result enforceLimit if destination is not whitelisted.
if (!whitelistMap[_destination]) {
_spendLimit._enforceLimit(_value);
}
// Check if the destination is a Contract and it is one of our supported tokens
if (address(_destination).isContract() && _isTokenAvailable(_destination)) {
// to is the recipient's address and amount is the value to be transferred
address to;
uint256 amount;
(to, amount) = _getERC20RecipientAndAmount(_destination, _data);
if (!whitelistMap[to]) {
// If the address (of the token contract, e.g) is not in the TokenWhitelist used by the convert method
// then etherValue will be zero
uint256 etherValue = convertToEther(_destination, amount);
_spendLimit._enforceLimit(etherValue);
}
// use callOptionalReturn provided in SafeERC20 in case the ERC20 method
// returns false instead of reverting!
IERC20(_destination)._callOptionalReturn(_data);
// if ERC20 call completes, return a boolean "true" as bytes emulating ERC20
bytes memory b = new bytes(32);
b[31] = 0x01;
emit ExecutedTransaction(_destination, _value, _data, b);
return b;
}
(bool success, bytes memory returnData) = _destination.call{value: _value}(_data);
require(success, string(returnData));
emit ExecutedTransaction(_destination, _value, _data, returnData);
// returns all of the bytes returned by _destination contract
return returnData;
}
/// @dev Implements EIP-1654: receives the hashed message(bytes32)
/// https://github.com/ethereum/EIPs/issues/1654.md
/// @param _hashedData Hashed data signed on the behalf of address(this)
/// @param _signature Signature byte array associated with _dataHash
function isValidSignature(bytes32 _hashedData, bytes memory _signature) public view returns (bytes4) {
address from = _hashedData.recover(_signature);
require(_isOwner(from), "invalid signature");
return _EIP_1654;
}
/// @dev Transfers the specified asset to the recipient's address.
/// @param _to is the recipient's address.
/// @param _asset is the address of an ERC20 token or 0x0 for ether.
/// @param _amount is the amount of assets to be transferred in base units.
function transfer(
address payable _to,
address _asset,
uint256 _amount
) public onlyOwnerOrSelf isNotZero(_amount) {
// Checks if the _to address is not the zero-address
require(_to != address(0), "destination=0");
// If address is not whitelisted, take daily limit into account.
if (!whitelistMap[_to]) {
// initialize ether value in case the asset is ETH
uint256 etherValue = _amount;
// Convert token amount to ether value if asset is an ERC20 token.
if (_asset != address(0)) {
etherValue = convertToEther(_asset, _amount);
}
// Check against the daily spent limit and update accordingly
// Check against the daily spent limit and update accordingly, require that the value is under remaining limit.
_spendLimit._enforceLimit(etherValue);
}
// Transfer token or ether based on the provided address.
_safeTransfer(_to, _asset, _amount);
// Emit the transfer event.
emit Transferred(_to, _asset, _amount);
}
}